mirror of
https://gitlab.com/Oreolek/salet.git
synced 2024-07-07 01:04:25 +03:00
253 lines
8 KiB
CoffeeScript
253 lines
8 KiB
CoffeeScript
# I confess that this world model heavily borrows from INSTEAD engine. - A.Y.
|
|
|
|
require('./salet.coffee')
|
|
obj = require('./obj.coffee')
|
|
markdown = require('./markdown.coffee')
|
|
|
|
assert = (msg, assertion) -> console.assert assertion, msg
|
|
|
|
Function.prototype.fcall = Function.prototype.call
|
|
Boolean.prototype.fcall = () ->
|
|
return this.valueOf()
|
|
String.prototype.fcall = () ->
|
|
return this
|
|
|
|
way_to = (content, ref) ->
|
|
return "<a href='#{ref}' class='way' id='waylink-#{ref}'>#{content}</a>"
|
|
|
|
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()
|
|
else
|
|
system.view.removeTransient()
|
|
|
|
if f != @name and f?
|
|
@visited++
|
|
if system.rooms[f].exit?
|
|
system.rooms[f].exit system, @name
|
|
|
|
if @enter
|
|
@enter system, f
|
|
|
|
if not @extendSection
|
|
classes = if @classes then ' ' + @classes.join(' ') else ''
|
|
room = document.getElementById('current-room')
|
|
if room?
|
|
room.removeAttribute('id')
|
|
system.view.append "<section id='current-room' data-room='#{@name}' class='room-#{@name}#{classes}'></section>"
|
|
|
|
if f != @name and @before?
|
|
system.view.write markdown(@before.fcall(this, system, f))
|
|
|
|
system.view.write @look system, f
|
|
|
|
if f != @name and @after?
|
|
system.view.write markdown(@after.fcall(this, system, f))
|
|
|
|
if @beforeChoices?
|
|
@beforeChoices.fcall(this, system, f)
|
|
|
|
if @choices
|
|
system.view.writeChoices(system, system.getSituationIdChoices(@choices, @maxChoices))
|
|
|
|
if @afterChoices?
|
|
@afterChoices.fcall(this, system, f)
|
|
|
|
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
|
|
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
|
|
return system.view.changeLevel(thing.level, () ->
|
|
return thing.act.fcall(thing, system).toString()
|
|
)
|
|
# 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
|
|
|
|
room = (name, salet, spec) ->
|
|
spec ?= {}
|
|
spec.name = name
|
|
return new SaletRoom(spec).register(salet)
|
|
|
|
module.exports = room
|