mirror of
https://gitlab.com/Oreolek/black_phone.git
synced 2024-06-28 21:05:09 +03:00
Salet update
This commit is contained in:
parent
074f78d1ab
commit
5435ce886f
21
lib/character.coffee
Normal file
21
lib/character.coffee
Normal file
|
@ -0,0 +1,21 @@
|
|||
class Character
|
||||
constructor: (spec) ->
|
||||
@inventory = []
|
||||
|
||||
@take = (thing) =>
|
||||
@inventory.push thing
|
||||
@drop = (thing) =>
|
||||
for i in @inventory
|
||||
if i.name == thing
|
||||
index = @objects.indexOf(thing)
|
||||
@inventory.splice(index, 1)
|
||||
|
||||
for index, value of spec
|
||||
this[index] = value
|
||||
return this
|
||||
|
||||
character = (spec) ->
|
||||
spec ?= {}
|
||||
return( new Character(spec) )
|
||||
|
||||
module.exports = character
|
|
@ -20,32 +20,36 @@ class SaletObj
|
|||
unless spec.name?
|
||||
console.error("Trying to create an object with no name")
|
||||
return null
|
||||
|
||||
@level = 0 # if > 0 it's hidden
|
||||
@order = 0 # you can use this to sort the descriptions
|
||||
@look = (system, f) =>
|
||||
if @dsc and @dsc != ""
|
||||
text = markdown(@dsc.fcall(this, system, f).toString())
|
||||
text = system.view.wrapLevel(text, @level)
|
||||
# replace braces {{}} with link to _act_
|
||||
return parsedsc(text, @name)
|
||||
@takeable = false
|
||||
@take = (system) => "You take the #{@name}." # taking to inventory
|
||||
@act = (system) => "You don't find anything extraordinary about the #{@name}." # object action
|
||||
@dsc = (system) => "You see a {{#{@name}}} here." # object description
|
||||
@inv = (system) => "It's a {{#{@name}.}}" # inventory description
|
||||
@location = ""
|
||||
@put = (salet, location) =>
|
||||
@level = 0 # this is scenery
|
||||
if salet.rooms[location]?
|
||||
@location = location
|
||||
salet.rooms[location].take(this)
|
||||
else
|
||||
console.log("Could not find location #{location} for an object #{@name}")
|
||||
@delete = (salet, location = false) =>
|
||||
if location == false
|
||||
location = @location
|
||||
salet.rooms[location].drop(this)
|
||||
|
||||
for key, value of spec
|
||||
this[key] = value
|
||||
level: 0 # if > 0 it's hidden
|
||||
order: 0 # you can use this to sort the descriptions
|
||||
look: (system, f) =>
|
||||
if @dsc and @dsc != ""
|
||||
text = markdown(@dsc.fcall(this, system, f).toString())
|
||||
text = system.view.wrapLevel(text, @level)
|
||||
# replace braces {{}} with link to _act_
|
||||
return parsedsc(text, @name)
|
||||
takeable: false
|
||||
take: (system) => "You take the #{@name}." # taking to inventory
|
||||
act: (system) => "You don't find anything extraordinary about the #{@name}." # object action
|
||||
dsc: (system) => "You see a {{#{@name}}} here." # object description
|
||||
inv: (system) => "It's a {{#{@name}.}}" # inventory description
|
||||
location: ""
|
||||
put: (location) =>
|
||||
@level = 0 # this is scenery
|
||||
if salet.rooms[location]?
|
||||
salet.rooms[location].take(this)
|
||||
@location = location
|
||||
delete: (location = false) =>
|
||||
if location == false
|
||||
location = @location
|
||||
salet.rooms[location].drop(this)
|
||||
|
||||
|
||||
obj = (name, spec) ->
|
||||
spec ?= {}
|
||||
spec.name = name
|
||||
|
|
458
lib/room.coffee
458
lib/room.coffee
|
@ -19,239 +19,237 @@ Array::remove = (e) -> @[t..t] = [] if (t = @indexOf(e)) > -1
|
|||
|
||||
class SaletRoom
|
||||
constructor: (spec) ->
|
||||
@visited = 0
|
||||
@title = "Room"
|
||||
@objects = {}
|
||||
@canView = true
|
||||
@canChoose = true
|
||||
@priority = 1
|
||||
@displayOrder = 1
|
||||
@tags = []
|
||||
@choices = ""
|
||||
@optionText = "Choice"
|
||||
|
||||
# room illustration image, VN-style. Can be a GIF or WEBM. Can be a function.
|
||||
@pic = false
|
||||
@dsc = false # room description
|
||||
@extendSection = false
|
||||
@distance = Infinity # distance to the destination
|
||||
@clear = true # clear the screen on entering the room?
|
||||
@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 = (system, to) =>
|
||||
return true
|
||||
|
||||
###
|
||||
I call SaletRoom.enter every time the player enters this room but before the section is opened.
|
||||
Unlike @before this gets called before the current section is opened.
|
||||
It's a styling difference.
|
||||
|
||||
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 = (system, from) =>
|
||||
return true
|
||||
|
||||
###
|
||||
Salet's Undum version calls Situation.entering every time a situation is entered, and
|
||||
passes it three arguments; The character object, the system object,
|
||||
and a string referencing the previous situation, or null if there is
|
||||
none (ie, for the starting situation).
|
||||
|
||||
My version of `enter` splits the location description from the effects.
|
||||
Also if f == this.name (we're in the same location) the `before` and `after` callbacks are ignored.
|
||||
###
|
||||
@entering = (system, f) =>
|
||||
if @clear and f?
|
||||
system.view.clearContent()
|
||||
|
||||
if f != @name and f?
|
||||
@visited++
|
||||
if system.rooms[f].exit?
|
||||
system.rooms[f].exit system, @name
|
||||
|
||||
if @enter
|
||||
@enter system, f
|
||||
|
||||
room_content = ""
|
||||
if not @extendSection
|
||||
classes = if @classes then ' ' + @classes.join(' ') else ''
|
||||
room = document.getElementById('current-room')
|
||||
if room?
|
||||
room.removeAttribute('id')
|
||||
# Javascript DOM manipulation functions like jQuery's append() or document.createElement
|
||||
# don't work like a typical printLn - they create *DOM nodes*.
|
||||
# You can't leave an unclosed tag just like that. So we have to buffer the output.
|
||||
room_content = "<section id='current-room' data-room='#{@name}' class='room-#{@name}#{classes}'>"
|
||||
|
||||
if f != @name and @before?
|
||||
room_content += markdown(@before.fcall(this, system, f))
|
||||
|
||||
room_content += @look system, f
|
||||
|
||||
if f != @name and @after?
|
||||
room_content += markdown(@after.fcall(this, system, f))
|
||||
|
||||
if not @extendSection
|
||||
room_content += "</section>"
|
||||
|
||||
system.view.write(room_content)
|
||||
|
||||
if @choices
|
||||
system.view.writeChoices(system, system.getSituationIdChoices(@choices, @maxChoices))
|
||||
|
||||
if system.autosave
|
||||
system.saveGame()
|
||||
|
||||
###
|
||||
An internal function to get the room's description and the descriptions of
|
||||
every object in this room.
|
||||
###
|
||||
@look = (system, f) =>
|
||||
system.view.updateWays(system, @ways, @name)
|
||||
retval = ""
|
||||
|
||||
if @pic
|
||||
retval += '<div class="pic">'+system.view.pictureTag(@pic.fcall(this, system, f))+'</div>'
|
||||
|
||||
# Print the room description
|
||||
if @dsc and @dsc != ""
|
||||
dsc = @dsc.fcall(this, system, f).toString()
|
||||
retval += markdown(dsc)
|
||||
|
||||
objDescriptions = []
|
||||
for thing in @objects
|
||||
if thing.name and typeof(thing.look) == "function" and thing.level == 0 and thing.look(system, f)
|
||||
objDescriptions.push ({
|
||||
order: thing.order,
|
||||
content: thing.look(system, f)
|
||||
})
|
||||
|
||||
objDescriptions.sort((a, b) ->
|
||||
return a.order - b.order
|
||||
)
|
||||
|
||||
for description in objDescriptions
|
||||
retval += description.content
|
||||
|
||||
return retval
|
||||
|
||||
###
|
||||
Puts an object in this room.
|
||||
###
|
||||
@take = (thing) =>
|
||||
@objects.push(thing)
|
||||
|
||||
@drop = (name) =>
|
||||
for thing in @objects
|
||||
if thing.name == name
|
||||
index = @objects.indexOf(thing)
|
||||
@objects.splice(index, 1)
|
||||
|
||||
###
|
||||
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 = (system, action) =>
|
||||
if (link = action.match(/^_(act|cycle)_(.+)$/)) #object action
|
||||
for thing in @objects
|
||||
if thing.name == link[2]
|
||||
if link[1] == "act"
|
||||
# If it's takeable, the player can take this object.
|
||||
# If not, we check the "act" function.
|
||||
if thing.takeable
|
||||
console.log system
|
||||
system.character.take(thing)
|
||||
@drop name
|
||||
system.view.clearContent()
|
||||
@entering.fcall(this, system, @name)
|
||||
return system.view.write(thing.take.fcall(thing, system).toString())
|
||||
if thing.act
|
||||
system.view.changeLevel(thing.level)
|
||||
return system.view.write(
|
||||
system.view.wrapLevel(
|
||||
thing.act.fcall(thing, system).toString(),
|
||||
thing.level
|
||||
)
|
||||
)
|
||||
# the loop is done but no return came - match not found
|
||||
console.error("Could not find #{link[2]} in current room.")
|
||||
|
||||
# we're done with objects, now check the regular actions
|
||||
actionClass = action.match(/^_(\w+)_(.+)$/)
|
||||
that = this
|
||||
|
||||
responses = {
|
||||
writer: (ref) ->
|
||||
content = that.writers[ref].fcall(that, system, action)
|
||||
output = markdown(content)
|
||||
system.view.write(output)
|
||||
replacer: (ref) ->
|
||||
content = that.writers[ref].fcall(that, system, action)
|
||||
system.view.replace(content, '#'+ref)
|
||||
inserter: (ref) ->
|
||||
content = that.writers[ref].fcall(that, system, action)
|
||||
output = markdown(content)
|
||||
system.view.write(output, '#'+ref)
|
||||
}
|
||||
|
||||
if (actionClass)
|
||||
# Matched a special action class
|
||||
[responder, ref] = [actionClass[1], actionClass[2]]
|
||||
|
||||
if(!@writers.hasOwnProperty(actionClass[2]))
|
||||
throw new Error("Tried to call undefined writer: #{action}");
|
||||
responses[responder](ref);
|
||||
else if (@actions.hasOwnProperty(action))
|
||||
@actions[action].call(this, system, action);
|
||||
else
|
||||
throw new Error("Tried to call undefined action: #{action}");
|
||||
|
||||
# Marks every room in the game with distance to this room
|
||||
@destination = () =>
|
||||
@distance = 0
|
||||
|
||||
candidates = [this]
|
||||
while candidates.length > 0
|
||||
current_room = candidates.shift()
|
||||
if current_room.ways
|
||||
for node in current_room.ways
|
||||
if node.distance == Infinity
|
||||
node.distance = current_room.distance + 1
|
||||
candidates.push(node)
|
||||
|
||||
@register = (salet) =>
|
||||
if not @name?
|
||||
console.error("Situation has no name")
|
||||
return this
|
||||
salet.rooms[@name] = this
|
||||
return this
|
||||
|
||||
@writers = {
|
||||
cyclewriter: (salet) =>
|
||||
responses = @cycle
|
||||
if typeof responses == "function"
|
||||
responses = responses()
|
||||
cycleIndex = window.localStorage.getItem("cycleIndex")
|
||||
cycleIndex ?= 0
|
||||
response = responses[cycleIndex]
|
||||
cycleIndex++
|
||||
if cycleIndex == responses.length
|
||||
cycleIndex = 0
|
||||
window.localStorage.setItem("cycleIndex", cycleIndex)
|
||||
return salet.view.cycleLink(response)
|
||||
}
|
||||
|
||||
for index, value of spec
|
||||
this[index] = value
|
||||
return this
|
||||
visited: 0
|
||||
title: "Room"
|
||||
objects: {}
|
||||
|
||||
# room illustration image, VN-style. Can be a GIF or WEBM. Can be a function.
|
||||
pic: false
|
||||
|
||||
canView: true
|
||||
canChoose: true
|
||||
priority: 1
|
||||
displayOrder: 1
|
||||
tags: []
|
||||
choices: ""
|
||||
optionText: "Choice"
|
||||
|
||||
dsc: false # room description
|
||||
extendSection: false
|
||||
distance: Infinity # distance to the destination
|
||||
clear: true # clear the screen on entering the room?
|
||||
|
||||
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: (system, to) =>
|
||||
return true
|
||||
|
||||
###
|
||||
I call SaletRoom.enter every time the player enters this room but before the section is opened.
|
||||
Unlike @before this gets called before the current section is opened.
|
||||
It's a styling difference.
|
||||
|
||||
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: (system, from) =>
|
||||
return true
|
||||
|
||||
###
|
||||
Salet's Undum version calls Situation.entering every time a situation is entered, and
|
||||
passes it three arguments; The character object, the system object,
|
||||
and a string referencing the previous situation, or null if there is
|
||||
none (ie, for the starting situation).
|
||||
|
||||
My version of `enter` splits the location description from the effects.
|
||||
Also if f == this.name (we're in the same location) the `before` and `after` callbacks are ignored.
|
||||
###
|
||||
entering: (system, f) =>
|
||||
if @clear and f?
|
||||
system.view.clearContent()
|
||||
|
||||
if f != @name and f?
|
||||
@visited++
|
||||
if system.rooms[f].exit?
|
||||
system.rooms[f].exit system, @name
|
||||
|
||||
if @enter
|
||||
@enter system, f
|
||||
|
||||
room_content = ""
|
||||
if not @extendSection
|
||||
classes = if @classes then ' ' + @classes.join(' ') else ''
|
||||
room = document.getElementById('current-room')
|
||||
if room?
|
||||
room.removeAttribute('id')
|
||||
# Javascript DOM manipulation functions like jQuery's append() or document.createElement
|
||||
# don't work like a typical printLn - they create *DOM nodes*.
|
||||
# You can't leave an unclosed tag just like that. So we have to buffer the output.
|
||||
room_content = "<section id='current-room' data-room='#{@name}' class='room-#{@name}#{classes}'>"
|
||||
|
||||
if f != @name and @before?
|
||||
room_content += markdown(@before.fcall(this, system, f))
|
||||
|
||||
room_content += @look system, f
|
||||
|
||||
if f != @name and @after?
|
||||
room_content += markdown(@after.fcall(this, system, f))
|
||||
|
||||
if not @extendSection
|
||||
room_content += "</section>"
|
||||
|
||||
system.view.write(room_content)
|
||||
|
||||
if @choices
|
||||
system.view.writeChoices(system, system.getSituationIdChoices(@choices, @maxChoices))
|
||||
|
||||
if system.autosave
|
||||
system.saveGame()
|
||||
|
||||
###
|
||||
An internal function to get the room's description and the descriptions of
|
||||
every object in this room.
|
||||
###
|
||||
look: (system, f) =>
|
||||
system.view.updateWays(system, @ways, @name)
|
||||
retval = ""
|
||||
|
||||
if @pic
|
||||
retval += '<div class="pic">'+system.view.pictureTag(@pic.fcall(this, system, f))+'</div>'
|
||||
|
||||
# Print the room description
|
||||
if @dsc and @dsc != ""
|
||||
dsc = @dsc.fcall(this, system, f).toString()
|
||||
retval += markdown(dsc)
|
||||
|
||||
objDescriptions = []
|
||||
for thing in @objects
|
||||
console.log thing
|
||||
if thing.name and typeof(thing.look) == "function" and thing.level == 0 and thing.look(system, f)
|
||||
objDescriptions.push ({
|
||||
order: thing.order,
|
||||
content: thing.look(system, f)
|
||||
})
|
||||
|
||||
objDescriptions.sort((a, b) ->
|
||||
return a.order - b.order
|
||||
)
|
||||
|
||||
for description in objDescriptions
|
||||
retval += description.content
|
||||
|
||||
return retval
|
||||
|
||||
###
|
||||
Puts an object in this room.
|
||||
###
|
||||
take: (thing) =>
|
||||
@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.
|
||||
salet.rooms["start"].objects = {}
|
||||
|
||||
drop: (name) =>
|
||||
delete @objects[name]
|
||||
|
||||
###
|
||||
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: (system, action) =>
|
||||
if (link = action.match(/^_(act|cycle)_(.+)$/)) #object action
|
||||
for thing in @objects
|
||||
if thing.name == link[2]
|
||||
if link[1] == "act"
|
||||
# If it's takeable, the player can take this object.
|
||||
# If not, we check the "act" function.
|
||||
if thing.takeable
|
||||
system.character.inventory.push thing
|
||||
@drop name
|
||||
system.view.clearContent()
|
||||
@entering.fcall(this, system, @name)
|
||||
return system.view.write(thing.take.fcall(thing, system).toString())
|
||||
if thing.act
|
||||
system.view.changeLevel(thing.level)
|
||||
return system.view.write(
|
||||
system.view.wrapLevel(
|
||||
thing.act.fcall(thing, system).toString(),
|
||||
thing.level
|
||||
)
|
||||
)
|
||||
# the loop is done but no return came - match not found
|
||||
console.error("Could not find #{link[2]} in current room.")
|
||||
|
||||
# we're done with objects, now check the regular actions
|
||||
actionClass = action.match(/^_(\w+)_(.+)$/)
|
||||
that = this
|
||||
|
||||
responses = {
|
||||
writer: (ref) ->
|
||||
content = that.writers[ref].fcall(that, system, action)
|
||||
output = markdown(content)
|
||||
system.view.write(output)
|
||||
replacer: (ref) ->
|
||||
content = that.writers[ref].fcall(that, system, action)
|
||||
system.view.replace(content, '#'+ref)
|
||||
inserter: (ref) ->
|
||||
content = that.writers[ref].fcall(that, system, action)
|
||||
output = markdown(content)
|
||||
system.view.write(output, '#'+ref)
|
||||
}
|
||||
|
||||
if (actionClass)
|
||||
# Matched a special action class
|
||||
[responder, ref] = [actionClass[1], actionClass[2]]
|
||||
|
||||
if(!@writers.hasOwnProperty(actionClass[2]))
|
||||
throw new Error("Tried to call undefined writer: #{action}");
|
||||
responses[responder](ref);
|
||||
else if (@actions.hasOwnProperty(action))
|
||||
@actions[action].call(this, system, action);
|
||||
else
|
||||
throw new Error("Tried to call undefined action: #{action}");
|
||||
|
||||
# Marks every room in the game with distance to this room
|
||||
destination: () =>
|
||||
@distance = 0
|
||||
|
||||
candidates = [this]
|
||||
while candidates.length > 0
|
||||
current_room = candidates.shift()
|
||||
if current_room.ways
|
||||
for node in current_room.ways
|
||||
if node.distance == Infinity
|
||||
node.distance = current_room.distance + 1
|
||||
candidates.push(node)
|
||||
|
||||
register: (salet) =>
|
||||
if not @name?
|
||||
console.error("Situation has no name")
|
||||
return this
|
||||
salet.rooms[@name] = this
|
||||
return this
|
||||
|
||||
writers:
|
||||
cyclewriter: (salet) ->
|
||||
responses = @cycle
|
||||
if typeof responses == "function"
|
||||
responses = responses()
|
||||
cycleIndex = window.localStorage.getItem("cycleIndex")
|
||||
cycleIndex ?= 0
|
||||
response = responses[cycleIndex]
|
||||
cycleIndex++
|
||||
if cycleIndex == responses.length
|
||||
cycleIndex = 0
|
||||
window.localStorage.setItem("cycleIndex", cycleIndex)
|
||||
return salet.view.cycleLink(response)
|
||||
|
||||
|
||||
room = (name, salet, spec) ->
|
||||
spec ?= {}
|
||||
spec.name = name
|
||||
|
|
713
lib/salet.coffee
713
lib/salet.coffee
|
@ -1,6 +1,7 @@
|
|||
markdown = require('./markdown.coffee')
|
||||
SaletView = require('./view.coffee')
|
||||
Random = require('./random.js')
|
||||
character = require('./character.coffee')
|
||||
languages = require('./localize.coffee')
|
||||
|
||||
###
|
||||
|
@ -17,429 +18,427 @@ String.prototype.fcall = () ->
|
|||
|
||||
assert = (msg, assertion) -> console.assert assertion, msg
|
||||
|
||||
class Character
|
||||
inventory: []
|
||||
|
||||
###
|
||||
This is the control structure, it has minimal amount of data and
|
||||
this data is volatile anyway (as in, it won't get saved).
|
||||
|
||||
There is only one instance of this class.
|
||||
###
|
||||
class Salet
|
||||
# REDEFINE THIS IN YOUR GAME
|
||||
game_id: null
|
||||
game_version: "1.0"
|
||||
autosave: true
|
||||
constructor: (spec) ->
|
||||
@character = character()
|
||||
|
||||
# REDEFINE THIS IN YOUR GAME
|
||||
@game_id = null
|
||||
@game_version = "1.0"
|
||||
@autosave = true
|
||||
|
||||
@rnd = null
|
||||
@time = 0
|
||||
|
||||
rnd: null
|
||||
time: 0
|
||||
# Corresponding room names to room objects.
|
||||
@rooms = {}
|
||||
|
||||
# Corresponding room names to room objects.
|
||||
rooms: {}
|
||||
# The unique id of the starting room.
|
||||
@start = "start"
|
||||
|
||||
# The unique id of the starting room.
|
||||
start: "start"
|
||||
# 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_-]+))?$/
|
||||
|
||||
# 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_-]+))?$/
|
||||
###
|
||||
This function is called at the start of the game. It is
|
||||
normally overridden to provide initial character creation
|
||||
(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.
|
||||
###
|
||||
@init = () ->
|
||||
|
||||
character: new Character
|
||||
###
|
||||
This function is called before entering any new
|
||||
situation. It is called before the corresponding situation
|
||||
has its `enter` method called.
|
||||
###
|
||||
@enter = (oldSituationId, newSituationId) ->
|
||||
|
||||
###
|
||||
This function is called at the start of the game. It is
|
||||
normally overridden to provide initial character creation
|
||||
(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.
|
||||
###
|
||||
init: () ->
|
||||
###
|
||||
Hook for when the situation has already been carried out
|
||||
and printed.
|
||||
###
|
||||
@afterEnter = (oldSituationId, newSituationId) ->
|
||||
|
||||
###
|
||||
This function is called before entering any new
|
||||
situation. It is called before the corresponding situation
|
||||
has its `enter` method called.
|
||||
###
|
||||
enter: (oldSituationId, newSituationId) ->
|
||||
###
|
||||
This function is called before carrying out any action in
|
||||
any situation. It is called before the corresponding
|
||||
situation has its `act` method called.
|
||||
|
||||
###
|
||||
Hook for when the situation has already been carried out
|
||||
and printed.
|
||||
###
|
||||
afterEnter: (oldSituationId, newSituationId) ->
|
||||
If the function returns true, then it is indicating that it
|
||||
has consumed the action, and the action will not be passed
|
||||
on to the situation. Note that this is the only one of
|
||||
these global handlers that can consume the event.
|
||||
###
|
||||
@beforeAction = (situationId, actionId) ->
|
||||
|
||||
###
|
||||
This function is called before carrying out any action in
|
||||
any situation. It is called before the corresponding
|
||||
situation has its `act` method called.
|
||||
###
|
||||
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 = (situationId, actionId) ->
|
||||
|
||||
If the function returns true, then it is indicating that it
|
||||
has consumed the action, and the action will not be passed
|
||||
on to the situation. Note that this is the only one of
|
||||
these global handlers that can consume the event.
|
||||
###
|
||||
beforeAction: (situationId, actionId) ->
|
||||
###
|
||||
This function is called after leaving any situation. It is
|
||||
called after the corresponding situation has its `exit`
|
||||
method called.
|
||||
###
|
||||
@exit = (oldSituationId, newSituationId) ->
|
||||
|
||||
###
|
||||
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: (situationId, actionId) ->
|
||||
###
|
||||
Returns a list of situation ids to choose from, given a set of
|
||||
specifications.
|
||||
|
||||
###
|
||||
This function is called after leaving any situation. It is
|
||||
called after the corresponding situation has its `exit`
|
||||
method called.
|
||||
###
|
||||
exit: (oldSituationId, newSituationId) ->
|
||||
This function is a complex and powerful way of compiling
|
||||
implicit situation choices. You give it a list of situation ids
|
||||
and situation tags (if a single id or tag is needed just that
|
||||
string can be given, it doesn't need to be wrapped in a
|
||||
list). Tags should be prefixed with a hash # to differentiate
|
||||
them from situation ids. The function then considers all
|
||||
matching situations in descending priority order, calling their
|
||||
canView functions and filtering out any that should not be
|
||||
shown, given the current state. Without additional parameters
|
||||
the function returns a list of the situation ids at the highest
|
||||
level of priority that has any valid results. So, for example,
|
||||
if a tag #places matches three situations, one with priority 2,
|
||||
and two with priority 3, and all of them can be viewed in the
|
||||
current context, then only the two with priority 3 will be
|
||||
returned. This allows you to have high-priority situations that
|
||||
trump any lower situations when they are valid, such as
|
||||
situations that force the player to go to one destination if
|
||||
the player is out of money, for example.
|
||||
|
||||
###
|
||||
Returns a list of situation ids to choose from, given a set of
|
||||
specifications.
|
||||
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
|
||||
priority resuls will be guaranteed to be returned, but the
|
||||
lowest priority group will have to fight it out for the
|
||||
remaining places.
|
||||
|
||||
This function is a complex and powerful way of compiling
|
||||
implicit situation choices. You give it a list of situation ids
|
||||
and situation tags (if a single id or tag is needed just that
|
||||
string can be given, it doesn't need to be wrapped in a
|
||||
list). Tags should be prefixed with a hash # to differentiate
|
||||
them from situation ids. The function then considers all
|
||||
matching situations in descending priority order, calling their
|
||||
canView functions and filtering out any that should not be
|
||||
shown, given the current state. Without additional parameters
|
||||
the function returns a list of the situation ids at the highest
|
||||
level of priority that has any valid results. So, for example,
|
||||
if a tag #places matches three situations, one with priority 2,
|
||||
and two with priority 3, and all of them can be viewed in the
|
||||
current context, then only the two with priority 3 will be
|
||||
returned. This allows you to have high-priority situations that
|
||||
trump any lower situations when they are valid, such as
|
||||
situations that force the player to go to one destination if
|
||||
the player is out of money, for example.
|
||||
Before this function returns its result, it sorts the
|
||||
situations in increasing order of their displayOrder values.
|
||||
###
|
||||
@getSituationIdChoices = (listOfOrOneIdsOrTags, maxChoices) =>
|
||||
datum = null
|
||||
i = 0
|
||||
|
||||
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
|
||||
priority resuls will be guaranteed to be returned, but the
|
||||
lowest priority group will have to fight it out for the
|
||||
remaining places. In this case, a random sample is chosen,
|
||||
taking into account the frequency of each situation. So a
|
||||
situation with a frequency of 100 will be chosen 100 times more
|
||||
often than a situation with a frequency of 1, if there is one
|
||||
space available. Often these frequencies have to be taken as a
|
||||
guideline, and the actual probabilities will only be
|
||||
approximate. Consider three situations with frequencies of 1,
|
||||
1, 100, competing for two spaces. The 100-frequency situation
|
||||
will be chosen almost every time, but for the other space, one
|
||||
of the 1-frequency situations must be chosen. So the actual
|
||||
probabilities will be roughly 50%, 50%, 100%. When selecting
|
||||
more than one result, frequencies can only be a guide.
|
||||
# First check if we have a single string for the id or tag.
|
||||
if (typeof(listOfOrOneIdsOrTags) == 'string')
|
||||
listOfOrOneIdsOrTags = [listOfOrOneIdsOrTags]
|
||||
|
||||
Before this function returns its result, it sorts the
|
||||
situations in increasing order of their displayOrder values.
|
||||
###
|
||||
getSituationIdChoices: (listOfOrOneIdsOrTags, maxChoices) ->
|
||||
datum = null
|
||||
i = 0
|
||||
# First we build a list of all candidate ids.
|
||||
allIds = []
|
||||
for tagOrId in listOfOrOneIdsOrTags
|
||||
if (tagOrId.substr(0, 1) == '#')
|
||||
ids = @getRoomsTagged(tagOrId.substr(1))
|
||||
for id in ids
|
||||
allIds.push(id)
|
||||
else #it's an id, not a tag
|
||||
allIds.push(tagOrId)
|
||||
|
||||
# First check if we have a single string for the id or tag.
|
||||
if (typeof(listOfOrOneIdsOrTags) == 'string')
|
||||
listOfOrOneIdsOrTags = [listOfOrOneIdsOrTags]
|
||||
#Filter out anything that can't be viewed right now.
|
||||
currentRoom = @getCurrentRoom()
|
||||
viewableRoomData = []
|
||||
for roomId in allIds
|
||||
room = @rooms[roomId]
|
||||
assert(room, "unknown_situation".l({id:roomId}))
|
||||
|
||||
# First we build a list of all candidate ids.
|
||||
allIds = []
|
||||
for tagOrId in listOfOrOneIdsOrTags
|
||||
if (tagOrId.substr(0, 1) == '#')
|
||||
ids = @getRoomsTagged(tagOrId.substr(1))
|
||||
for id in ids
|
||||
allIds.push(id)
|
||||
else #it's an id, not a tag
|
||||
allIds.push(tagOrId)
|
||||
if (room.canView.fcall(this, currentRoom))
|
||||
viewableRoomData.push({
|
||||
priority: room.priority
|
||||
id: roomId
|
||||
displayOrder: room.displayOrder
|
||||
})
|
||||
|
||||
#Filter out anything that can't be viewed right now.
|
||||
currentRoom = @getCurrentRoom()
|
||||
viewableRoomData = []
|
||||
for roomId in allIds
|
||||
room = @rooms[roomId]
|
||||
assert(room, "unknown_situation".l({id:roomId}))
|
||||
# Then we sort in descending priority order.
|
||||
viewableRoomData.sort((a, b) ->
|
||||
return b.priority - a.priority
|
||||
)
|
||||
|
||||
if (room.canView.fcall(this, currentRoom))
|
||||
viewableRoomData.push({
|
||||
priority: room.priority
|
||||
id: roomId
|
||||
displayOrder: room.displayOrder
|
||||
committed = []
|
||||
|
||||
# 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
|
||||
})
|
||||
|
||||
# Then we sort in descending priority order.
|
||||
viewableRoomData.sort((a, b) ->
|
||||
return b.priority - a.priority
|
||||
)
|
||||
# Now sort in ascending display order.
|
||||
committed.sort((a, b) ->
|
||||
return a.displayOrder - b.displayOrder
|
||||
)
|
||||
|
||||
committed = []
|
||||
# And return as a list of ids only.
|
||||
result = []
|
||||
for i in committed
|
||||
result.push(i.id)
|
||||
|
||||
# if we need to filter out the results
|
||||
if (maxChoices? && viewableRoomData.length > maxChoices)
|
||||
viewableRoomData = viewableRoomData[-maxChoices..]
|
||||
return result
|
||||
|
||||
for candidateRoom in viewableRoomData
|
||||
committed.push({
|
||||
id: candidateRoom.id
|
||||
displayOrder: candidateRoom.displayOrder
|
||||
})
|
||||
# This is the data on the player's progress that gets saved.
|
||||
@progress = {
|
||||
# A random seed string, used internally to make random
|
||||
# sequences predictable.
|
||||
seed: null
|
||||
# Keeps track of the links clicked, and when.
|
||||
sequence: [],
|
||||
# The time when the progress was saved.
|
||||
saveTime: null
|
||||
}
|
||||
|
||||
# Now sort in ascending display order.
|
||||
committed.sort((a, b) ->
|
||||
return a.displayOrder - b.displayOrder
|
||||
)
|
||||
# The Id of the current room the player is in.
|
||||
@current = null
|
||||
|
||||
# And return as a list of ids only.
|
||||
result = []
|
||||
for i in committed
|
||||
result.push(i.id)
|
||||
# Tracks whether we're in interactive mode or batch mode.
|
||||
@interactive = true
|
||||
|
||||
return result
|
||||
# The system time when the game was initialized.
|
||||
@startTime = null
|
||||
|
||||
# This is the data on the player's progress that gets saved.
|
||||
progress: {
|
||||
# A random seed string, used internally to make random
|
||||
# sequences predictable.
|
||||
seed: null
|
||||
# Keeps track of the links clicked, and when.
|
||||
sequence: [],
|
||||
# The time when the progress was saved.
|
||||
saveTime: null
|
||||
}
|
||||
# The stack of links, resulting from the last action, still be to resolved.
|
||||
@linkStack = null
|
||||
|
||||
# The Id of the current situation the player is in.
|
||||
current: null;
|
||||
@getCurrentRoom = () =>
|
||||
if (@current)
|
||||
return @rooms[@current]
|
||||
return null
|
||||
|
||||
# Tracks whether we're in interactive mode or batch mode.
|
||||
interactive: true
|
||||
# Gets the unique id used to identify saved games.
|
||||
@getSaveId = (slot = "") =>
|
||||
return 'salet_'+@game_id+'_'+@game_version#+'_'+slot
|
||||
|
||||
# The system time when the game was initialized.
|
||||
startTime: null
|
||||
# This gets called when a link needs to be followed, regardless
|
||||
# of whether it was user action that initiated it.
|
||||
@processLink = (code) =>
|
||||
# Check if we should do this now, or if processing is already underway.
|
||||
if @linkStack != null
|
||||
@linkStack.push(code)
|
||||
return
|
||||
|
||||
# The stack of links, resulting from the last action, still be to resolved.
|
||||
linkStack: null
|
||||
@view.mark_all_links_old
|
||||
|
||||
# We're processing, so make the stack available.
|
||||
@linkStack = []
|
||||
|
||||
getCurrentRoom: () =>
|
||||
if (@current)
|
||||
return @rooms[@current]
|
||||
return null
|
||||
# Handle each link in turn.
|
||||
@processOneLink(code);
|
||||
while (@linkStack.length > 0)
|
||||
code = @linkStack.shift()
|
||||
@processOneLink(code)
|
||||
|
||||
# Gets the unique id used to identify saved games.
|
||||
getSaveId: (slot = "") ->
|
||||
return 'salet_'+@game_id+'_'+@game_version#+'_'+slot
|
||||
# We're done, so remove the stack to prevent future pushes.
|
||||
@linkStack = null;
|
||||
|
||||
# This gets called when a link needs to be followed, regardless
|
||||
# of whether it was user action that initiated it.
|
||||
processLink: (code) =>
|
||||
# Check if we should do this now, or if processing is already underway.
|
||||
if @linkStack != null
|
||||
@linkStack.push(code)
|
||||
return
|
||||
# Scroll to the top of the new content.
|
||||
@view.endOutputTransaction()
|
||||
|
||||
@view.mark_all_links_old
|
||||
# We're able to save, if we weren't already.
|
||||
@view.enableSaving()
|
||||
|
||||
# We're processing, so make the stack available.
|
||||
@linkStack = []
|
||||
@goTo = (roomId) =>
|
||||
return @processLink(roomId)
|
||||
|
||||
# Handle each link in turn.
|
||||
@processOneLink(code);
|
||||
while (@linkStack.length > 0)
|
||||
code = @linkStack.shift()
|
||||
@processOneLink(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}))
|
||||
|
||||
# We're done, so remove the stack to prevent future pushes.
|
||||
@linkStack = null;
|
||||
situation = match[1]
|
||||
action = match[3]
|
||||
|
||||
# Scroll to the top of the new content.
|
||||
@view.endOutputTransaction()
|
||||
# Change the 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())
|
||||
|
||||
# We're able to save, if we weren't already.
|
||||
@view.enableSaving()
|
||||
# Carry out the action
|
||||
if (action)
|
||||
room = @getCurrentRoom()
|
||||
if (room and @beforeAction)
|
||||
# Try the global act handler
|
||||
consumed = @beforeAction(room, action)
|
||||
if (consumed != true)
|
||||
room.act(this, action)
|
||||
|
||||
goTo: (roomId) ->
|
||||
return @processLink(roomId)
|
||||
if (@afterAction)
|
||||
@afterAction(this, room, action)
|
||||
|
||||
###
|
||||
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}))
|
||||
# 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
|
||||
@progress.sequence.push({link:code, when:@time})
|
||||
@processLink(code)
|
||||
|
||||
situation = match[1]
|
||||
action = match[3]
|
||||
# Transition between rooms.
|
||||
@doTransitionTo = (newRoomId) =>
|
||||
oldRoomId = @current
|
||||
oldRoom = @getCurrentRoom()
|
||||
newRoom = @rooms[newRoomId]
|
||||
|
||||
# Change the 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())
|
||||
assert(newRoom, "unknown_situation".l({id:newRoomId}))
|
||||
|
||||
# We might not have an old situation if this is the start of the game.
|
||||
if (oldRoom and @exit)
|
||||
@exit(oldRoomId, newRoomId)
|
||||
|
||||
# Carry out the action
|
||||
if (action)
|
||||
room = @getCurrentRoom()
|
||||
if (room and @beforeAction)
|
||||
# Try the global act handler
|
||||
consumed = @beforeAction(room, action)
|
||||
if (consumed != true)
|
||||
room.act(this, action)
|
||||
@current = newRoomId
|
||||
|
||||
# Remove links and transient sections.
|
||||
@view.removeTransient(@interactive)
|
||||
|
||||
if (@afterAction)
|
||||
@afterAction(this, room, action)
|
||||
# Notify the incoming situation.
|
||||
if (@enter)
|
||||
@enter(oldRoomId, newRoomId)
|
||||
newRoom.entering(this, oldRoomId)
|
||||
|
||||
# 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
|
||||
@progress.sequence.push({link:code, when:@time})
|
||||
@processLink(code)
|
||||
# additional hook for when the situation text has already been printed
|
||||
if (@afterEnter)
|
||||
@afterEnter(oldRoomId, newRoomId)
|
||||
|
||||
# Transitions between situations.
|
||||
doTransitionTo: (newRoomId) ->
|
||||
oldRoomId = @current
|
||||
oldRoom = @getCurrentRoom()
|
||||
newRoom = @rooms[newRoomId]
|
||||
###
|
||||
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.
|
||||
###
|
||||
@eraseSave = (force = false) =>
|
||||
saveId = @getSaveId() # save slot
|
||||
if (localStorage.getItem(saveId) and (force or confirm("erase_message".l())))
|
||||
localStorage.removeItem(saveId)
|
||||
window.location.reload()
|
||||
|
||||
assert(newRoom, "unknown_situation".l({id:newRoomId}))
|
||||
# Find and return a list of ids for all situations with the given tag.
|
||||
@getRoomsTagged = (tag) =>
|
||||
result = []
|
||||
for id, room of @rooms
|
||||
for i in room.tags
|
||||
if (i == tag)
|
||||
result.push(id)
|
||||
break
|
||||
return result
|
||||
|
||||
# We might not have an old situation if this is the start of the game.
|
||||
if (oldRoom and @exit)
|
||||
@exit(oldRoomId, newRoomId)
|
||||
# Saves the character and the walking history 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
|
||||
|
||||
@current = newRoomId
|
||||
# Save the game.
|
||||
window.localStorage.setItem(@getSaveId(), JSON.stringify({
|
||||
progress: @progress,
|
||||
character: @character
|
||||
}))
|
||||
|
||||
# Remove links and transient sections.
|
||||
@view.removeTransient(@interactive)
|
||||
# Switch the button highlights.
|
||||
@view.disableSaving()
|
||||
@view.enableErasing()
|
||||
@view.enableLoading()
|
||||
|
||||
# Notify the incoming situation.
|
||||
if (@enter)
|
||||
@enter(oldRoomId, newRoomId)
|
||||
newRoom.entering(this, oldRoomId)
|
||||
# Loads the game from the given data
|
||||
@loadGame = (saveFile) =>
|
||||
@progress = saveFile.progress
|
||||
@character = saveFile.character
|
||||
|
||||
# additional hook for when the situation text has already been printed
|
||||
if (@afterEnter)
|
||||
@afterEnter(oldRoomId, newRoomId)
|
||||
|
||||
###
|
||||
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.
|
||||
###
|
||||
eraseSave: (force = false) =>
|
||||
saveId = @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.
|
||||
getRoomsTagged: (tag) =>
|
||||
result = []
|
||||
for id, room of @rooms
|
||||
for i in room.tags
|
||||
if (i == tag)
|
||||
result.push(id)
|
||||
break
|
||||
return result
|
||||
|
||||
# Saves the character and the walking history 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.
|
||||
window.localStorage.setItem(@getSaveId(), JSON.stringify({
|
||||
progress: @progress,
|
||||
character: @character
|
||||
}))
|
||||
|
||||
# Switch the button highlights.
|
||||
@view.disableSaving()
|
||||
@view.enableErasing()
|
||||
@view.enableLoading()
|
||||
|
||||
# Loads the game from the given data
|
||||
loadGame: (saveFile) ->
|
||||
@progress = saveFile.progress
|
||||
@character = saveFile.character
|
||||
|
||||
@rnd = new Random(@progress.seed)
|
||||
|
||||
# Don't load the save if it's an autosave at the first room (start).
|
||||
# We don't need to clear the screen.
|
||||
if saveFile.progress? and saveFile.progress.sequence.length > 1
|
||||
@view.clearContent()
|
||||
|
||||
# Now play through the actions so far:
|
||||
if (@init)
|
||||
@init()
|
||||
|
||||
# Run through all the player's history.
|
||||
interactive = false
|
||||
for step in @progress.sequence
|
||||
# The action must be done at the recorded time.
|
||||
@time = step.when
|
||||
@processLink(step.link)
|
||||
interactive = true
|
||||
|
||||
# Reverse engineer the start time.
|
||||
now = new Date().getTime() * 0.001
|
||||
startTime = now - @progress.saveTime
|
||||
|
||||
view: new SaletView
|
||||
|
||||
beginGame: () ->
|
||||
@view.fixClicks()
|
||||
|
||||
# Handle storage.
|
||||
saveFile = false
|
||||
if (@view.hasLocalStorage())
|
||||
saveFile = localStorage.getItem(@getSaveId())
|
||||
|
||||
if (saveFile)
|
||||
try
|
||||
@loadGame(JSON.parse(saveFile))
|
||||
@view.disableSaving()
|
||||
@view.enableErasing()
|
||||
catch err
|
||||
console.log "There was an error loading your save. The save is deleted."
|
||||
console.error err
|
||||
@eraseSave(true)
|
||||
else
|
||||
@progress.seed = new Date().toString()
|
||||
|
||||
character = new Character()
|
||||
@rnd = new Random(@progress.seed)
|
||||
@progress.sequence = [{link:@start, when:0}]
|
||||
|
||||
# Start the game
|
||||
@startTime = new Date().getTime() * 0.001
|
||||
# Don't load the save if it's an autosave at the first room (start).
|
||||
# We don't need to clear the screen.
|
||||
if saveFile.progress? and saveFile.progress.sequence.length > 1
|
||||
@view.clearContent()
|
||||
|
||||
# Now play through the actions so far:
|
||||
if (@init)
|
||||
@init(character)
|
||||
@init()
|
||||
|
||||
# Do the first state.
|
||||
@doTransitionTo(@start)
|
||||
# Run through all the player's history.
|
||||
interactive = false
|
||||
for step in @progress.sequence
|
||||
# The action must be done at the recorded time.
|
||||
@time = step.when
|
||||
@processLink(step.link)
|
||||
interactive = true
|
||||
|
||||
getRoom: (name) ->
|
||||
return @rooms[name]
|
||||
# Reverse engineer the start time.
|
||||
now = new Date().getTime() * 0.001
|
||||
startTime = now - @progress.saveTime
|
||||
|
||||
# Just an alias for getCurrentRoom
|
||||
here: () -> @getCurrentRoom()
|
||||
@view = new SaletView
|
||||
|
||||
isVisited: (name) ->
|
||||
place = @getRoom(name)
|
||||
if place
|
||||
return Boolean place.visited
|
||||
return 0
|
||||
@beginGame = () =>
|
||||
@view.fixClicks()
|
||||
|
||||
module.exports = Salet
|
||||
# Handle storage.
|
||||
saveFile = false
|
||||
if (@view.hasLocalStorage())
|
||||
saveFile = localStorage.getItem(@getSaveId())
|
||||
|
||||
if (saveFile)
|
||||
try
|
||||
@loadGame(JSON.parse(saveFile))
|
||||
@view.disableSaving()
|
||||
@view.enableErasing()
|
||||
catch err
|
||||
console.log "There was an error loading your save. The save is deleted."
|
||||
console.error err
|
||||
@eraseSave(true)
|
||||
else
|
||||
@progress.seed = new Date().toString()
|
||||
|
||||
character = new Character()
|
||||
@rnd = new Random(@progress.seed)
|
||||
@progress.sequence = [{link:@start, when:0}]
|
||||
|
||||
# Start the game
|
||||
@startTime = new Date().getTime() * 0.001
|
||||
if (@init)
|
||||
@init(character)
|
||||
|
||||
# Do the first state.
|
||||
@doTransitionTo(@start)
|
||||
|
||||
@getRoom = (name) => @rooms[name]
|
||||
|
||||
# Just an alias for getCurrentRoom
|
||||
@here = () => @getCurrentRoom()
|
||||
|
||||
@isVisited = (name) =>
|
||||
place = @getRoom(name)
|
||||
if place
|
||||
return Boolean place.visited
|
||||
return 0
|
||||
|
||||
for index, value of spec
|
||||
this[index] = value
|
||||
return this
|
||||
|
||||
salet = (spec) ->
|
||||
spec ?= {}
|
||||
retval = new Salet(spec)
|
||||
retval.view.init(retval)
|
||||
return retval
|
||||
|
||||
module.exports = salet
|
||||
|
|
|
@ -9,6 +9,8 @@ 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.)
|
||||
|
||||
There is only one instance of this class, and it's stored as `salet.view`.
|
||||
###
|
||||
|
||||
assert = (msg, assertion) -> console.assert assertion, msg
|
||||
|
@ -23,7 +25,7 @@ addClass = (element, className) ->
|
|||
element.className += ' ' + className
|
||||
|
||||
class SaletView
|
||||
init: (salet) ->
|
||||
init: (salet) =>
|
||||
$("#content, #ways").on("click", "a", (event) ->
|
||||
event.preventDefault()
|
||||
a = $(this)
|
||||
|
@ -95,7 +97,7 @@ class SaletView
|
|||
return content.toString()
|
||||
|
||||
# Write content to current room
|
||||
write: (content, elementSelector = "#current-room") ->
|
||||
write: (content, elementSelector = "#current-room") =>
|
||||
if content == ""
|
||||
return
|
||||
content = @prepareContent(content)
|
||||
|
@ -109,7 +111,7 @@ class SaletView
|
|||
|
||||
# Replaces the text in the given block with the given text.
|
||||
# !! Does not call markdown on the provided text. !!
|
||||
replace: (content, elementSelector) ->
|
||||
replace: (content, elementSelector) =>
|
||||
if content == ""
|
||||
return
|
||||
content = @prepareContent(content)
|
||||
|
@ -143,7 +145,7 @@ class SaletView
|
|||
manually, ot else use the `getSituationIdChoices` method to
|
||||
return an ordered list of valid viewable situation ids.
|
||||
###
|
||||
writeChoices: (salet, listOfIds) ->
|
||||
writeChoices: (salet, listOfIds) =>
|
||||
if (not listOfIds? or listOfIds.length == 0)
|
||||
return
|
||||
|
||||
|
@ -195,7 +197,7 @@ class SaletView
|
|||
# Remove every section marked as a different level.
|
||||
# For a link level 0, we hide every link of level 1 and above.
|
||||
# It's for the player to focus.
|
||||
changeLevel: (level) ->
|
||||
changeLevel: (level) =>
|
||||
maxLevel = 6
|
||||
if level < maxLevel
|
||||
i = level + 1
|
||||
|
@ -206,7 +208,7 @@ class SaletView
|
|||
directive = hideArray.join(", ")
|
||||
$(directive).fadeOut("slow")
|
||||
|
||||
wrapLevel: (text, level) ->
|
||||
wrapLevel: (text, level) =>
|
||||
return "<span class='lvl#{level}'>"+text+'</span>'
|
||||
|
||||
# At last, we scroll the view so that .new objects are in view.
|
||||
|
@ -269,6 +271,7 @@ class SaletView
|
|||
key: way
|
||||
distance: salet.rooms[way].distance
|
||||
})
|
||||
document.querySelector(".ways h2").style.display = "block"
|
||||
else
|
||||
document.querySelector(".ways h2").style.display = "none"
|
||||
document.getElementById("ways").innerHTML = content
|
||||
|
|
Loading…
Reference in a new issue