1
0
Fork 0
mirror of https://gitlab.com/Oreolek/black_phone.git synced 2024-06-30 22:05:08 +03:00

Salet update

This commit is contained in:
Alexander Yakovlev 2016-02-10 20:38:27 +07:00
parent 074f78d1ab
commit 5435ce886f
5 changed files with 642 additions and 617 deletions

21
lib/character.coffee Normal file
View 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

View file

@ -20,32 +20,36 @@ class SaletObj
unless spec.name?
console.error("Trying to create an object with no name")
return null
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) =>
@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) =>
@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]?
salet.rooms[location].take(this)
@location = location
delete: (location = false) =>
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
obj = (name, spec) ->
spec ?= {}
spec.name = name

View file

@ -19,37 +19,30 @@ Array::remove = (e) -> @[t..t] = [] if (t = @indexOf(e)) > -1
class SaletRoom
constructor: (spec) ->
for index, value of spec
this[index] = value
return this
visited: 0
title: "Room"
objects: {}
@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
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) =>
@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) =>
@exit = (system, to) =>
return true
###
@ -60,7 +53,7 @@ class SaletRoom
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) =>
@enter = (system, from) =>
return true
###
@ -72,7 +65,7 @@ class SaletRoom
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) =>
@entering = (system, f) =>
if @clear and f?
system.view.clearContent()
@ -118,7 +111,7 @@ class SaletRoom
An internal function to get the room's description and the descriptions of
every object in this room.
###
look: (system, f) =>
@look = (system, f) =>
system.view.updateWays(system, @ways, @name)
retval = ""
@ -132,7 +125,6 @@ class SaletRoom
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,
@ -151,20 +143,20 @@ class SaletRoom
###
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 = {}
@take = (thing) =>
@objects.push(thing)
drop: (name) =>
delete @objects[name]
@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) =>
@act = (system, action) =>
if (link = action.match(/^_(act|cycle)_(.+)$/)) #object action
for thing in @objects
if thing.name == link[2]
@ -172,7 +164,8 @@ class SaletRoom
# 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
console.log system
system.character.take(thing)
@drop name
system.view.clearContent()
@entering.fcall(this, system, @name)
@ -219,7 +212,7 @@ class SaletRoom
throw new Error("Tried to call undefined action: #{action}");
# Marks every room in the game with distance to this room
destination: () =>
@destination = () =>
@distance = 0
candidates = [this]
@ -231,15 +224,15 @@ class SaletRoom
node.distance = current_room.distance + 1
candidates.push(node)
register: (salet) =>
@register = (salet) =>
if not @name?
console.error("Situation has no name")
return this
salet.rooms[@name] = this
return this
writers:
cyclewriter: (salet) ->
@writers = {
cyclewriter: (salet) =>
responses = @cycle
if typeof responses == "function"
responses = responses()
@ -251,6 +244,11 @@ class SaletRoom
cycleIndex = 0
window.localStorage.setItem("cycleIndex", cycleIndex)
return salet.view.cycleLink(response)
}
for index, value of spec
this[index] = value
return this
room = (name, salet, spec) ->
spec ?= {}

View file

@ -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,33 +18,33 @@ 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()
rnd: null
time: 0
# REDEFINE THIS IN YOUR GAME
@game_id = null
@game_version = "1.0"
@autosave = true
@rnd = null
@time = 0
# Corresponding room names to room objects.
rooms: {}
@rooms = {}
# The unique id of the starting room.
start: "start"
@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_-]+))?$/
character: new Character
@linkRe = /^([0-9A-Za-z_-]+|\.)(\/([0-9A-Za-z_-]+))?$/
###
This function is called at the start of the game. It is
@ -53,20 +54,20 @@ class Salet
processing could also be done by the first situation's
enter function.
###
init: () ->
@init = () ->
###
This function is called before entering any new
situation. It is called before the corresponding situation
has its `enter` method called.
###
enter: (oldSituationId, newSituationId) ->
@enter = (oldSituationId, newSituationId) ->
###
Hook for when the situation has already been carried out
and printed.
###
afterEnter: (oldSituationId, newSituationId) ->
@afterEnter = (oldSituationId, newSituationId) ->
###
This function is called before carrying out any action in
@ -78,21 +79,21 @@ class Salet
on to the situation. Note that this is the only one of
these global handlers that can consume the event.
###
beforeAction: (situationId, actionId) ->
@beforeAction = (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: (situationId, actionId) ->
@afterAction = (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) ->
@exit = (oldSituationId, newSituationId) ->
###
Returns a list of situation ids to choose from, given a set of
@ -122,23 +123,12 @@ class Salet
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.
remaining places.
Before this function returns its result, it sorts the
situations in increasing order of their displayOrder values.
###
getSituationIdChoices: (listOfOrOneIdsOrTags, maxChoices) ->
@getSituationIdChoices = (listOfOrOneIdsOrTags, maxChoices) =>
datum = null
i = 0
@ -200,7 +190,7 @@ class Salet
return result
# This is the data on the player's progress that gets saved.
progress: {
@progress = {
# A random seed string, used internally to make random
# sequences predictable.
seed: null
@ -210,30 +200,30 @@ class Salet
saveTime: null
}
# The Id of the current situation the player is in.
current: null;
# The Id of the current room the player is in.
@current = null
# Tracks whether we're in interactive mode or batch mode.
interactive: true
@interactive = true
# The system time when the game was initialized.
startTime: null
@startTime = null
# The stack of links, resulting from the last action, still be to resolved.
linkStack: null
@linkStack = null
getCurrentRoom: () =>
@getCurrentRoom = () =>
if (@current)
return @rooms[@current]
return null
# Gets the unique id used to identify saved games.
getSaveId: (slot = "") ->
@getSaveId = (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.
processLink: (code) =>
@processLink = (code) =>
# Check if we should do this now, or if processing is already underway.
if @linkStack != null
@linkStack.push(code)
@ -259,7 +249,7 @@ class Salet
# We're able to save, if we weren't already.
@view.enableSaving()
goTo: (roomId) ->
@goTo = (roomId) =>
return @processLink(roomId)
###
@ -268,7 +258,7 @@ class Salet
code that further calls doLink, and so on. This method processes
each one, and processLink manages this.
###
processOneLink: (code) ->
@processOneLink = (code) =>
match = code.match(@linkRe)
assert(match, "link_not_valid".l({link:code}))
@ -295,14 +285,14 @@ class Salet
@afterAction(this, room, 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
@time = now - @startTime
@progress.sequence.push({link:code, when:@time})
@processLink(code)
# Transitions between situations.
doTransitionTo: (newRoomId) ->
# Transition between rooms.
@doTransitionTo = (newRoomId) =>
oldRoomId = @current
oldRoom = @getCurrentRoom()
newRoom = @rooms[newRoomId]
@ -334,14 +324,14 @@ class Salet
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) =>
@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) =>
@getRoomsTagged = (tag) =>
result = []
for id, room of @rooms
for i in room.tags
@ -351,7 +341,7 @@ class Salet
return result
# Saves the character and the walking history to local storage.
saveGame: () ->
@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
@ -369,7 +359,7 @@ class Salet
@view.enableLoading()
# Loads the game from the given data
loadGame: (saveFile) ->
@loadGame = (saveFile) =>
@progress = saveFile.progress
@character = saveFile.character
@ -396,9 +386,9 @@ class Salet
now = new Date().getTime() * 0.001
startTime = now - @progress.saveTime
view: new SaletView
@view = new SaletView
beginGame: () ->
@beginGame = () =>
@view.fixClicks()
# Handle storage.
@ -430,16 +420,25 @@ class Salet
# Do the first state.
@doTransitionTo(@start)
getRoom: (name) ->
return @rooms[name]
@getRoom = (name) => @rooms[name]
# Just an alias for getCurrentRoom
here: () -> @getCurrentRoom()
@here = () => @getCurrentRoom()
isVisited: (name) ->
@isVisited = (name) =>
place = @getRoom(name)
if place
return Boolean place.visited
return 0
module.exports = Salet
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

View file

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