1
0
Fork 0
mirror of https://gitlab.com/Oreolek/salet.git synced 2024-07-07 01:04:25 +03:00
salet/lib/room.coffee
2016-02-01 20:39:16 +07:00

244 lines
7.5 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
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) ->
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
dsc = @dsc.fcall(this, system, f).toString()
retval += markdown(dsc)
for name, thing of @objects
retval += thing.look()
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 name, thing of @objects
if 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
return system.view.write(thing.act.fcall(thing, system).toString())
elseif link[1] == "cycle"
# TODO object cyclewriter
# the loop is done but no return came - match not found
console.error("Could not find #{link[1]} 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
return new SaletRoom(spec).register(salet)
module.exports = room