mirror of
https://gitlab.com/Oreolek/salet.git
synced 2024-07-07 01:04:25 +03:00
326 lines
11 KiB
CoffeeScript
326 lines
11 KiB
CoffeeScript
markdown = require('./markdown.coffee')
|
|
###
|
|
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.)
|
|
|
|
There is only one instance of this class, and it's stored as `salet.view`.
|
|
###
|
|
|
|
assert = (msg, assertion) -> console.assert assertion, msg
|
|
|
|
way_to = (content, ref) ->
|
|
return "<a href='#{ref}' class='way'>#{content}</a>"
|
|
|
|
addClass = (element, className) ->
|
|
if (element.classList)
|
|
element.classList.add(className)
|
|
else
|
|
element.className += ' ' + className
|
|
|
|
class SaletView
|
|
init: (salet) =>
|
|
$("#content, #ways").on("click", "a", (event) ->
|
|
event.preventDefault()
|
|
a = $(this)
|
|
href = a.attr('href')
|
|
if (href.match(salet.linkRe))
|
|
if (a.hasClass("once") || href.match(/[?&]once[=&]?/))
|
|
salet.view.clearLinks(href)
|
|
salet.processClick(href)
|
|
)
|
|
$("#inventory").on("click", "a", (event) ->
|
|
event.preventDefault()
|
|
alert("Not done yet")
|
|
)
|
|
$("#load").on("click", "a", (event) ->
|
|
window.location.reload()
|
|
)
|
|
if (@hasLocalStorage())
|
|
$("#erase").click((event) ->
|
|
event.preventDefault()
|
|
return salet.eraseSave()
|
|
)
|
|
$("#save").click((event) ->
|
|
event.preventDefault()
|
|
return salet.saveGame()
|
|
)
|
|
|
|
disableSaving: () ->
|
|
$("#save").addClass('disabled')
|
|
enableSaving: () ->
|
|
$("#save").removeClass('disabled')
|
|
enableErasing: () ->
|
|
$("#erase").removeClass('disabled')
|
|
disableErasing: () ->
|
|
$("#erase").addClass('disabled')
|
|
enableLoading: () ->
|
|
$("#load").removeClass('disabled')
|
|
disableLoading: () ->
|
|
$("#load").addClass('disabled')
|
|
|
|
# Scrolls the top of the screen to the specified point
|
|
scrollTopTo: (value) ->
|
|
$('html,body').animate({scrollTop: value}, 500)
|
|
|
|
# Scrolls the bottom of the screen to the specified point
|
|
scrollBottomTo: (value) ->
|
|
scrollTopTo(value - $(window).height())
|
|
|
|
# Scrolls all the way to the bottom of the screen
|
|
scrollToBottom: () ->
|
|
scrollTopTo($('html').height() - $(window).height());
|
|
|
|
###
|
|
Removes all content from the page, clearing the main content area.
|
|
|
|
If an elementSelector is given, then only that selector will be
|
|
cleared. Note that all content from the cleared element is removed,
|
|
but the element itself remains, ready to be filled again using @write.
|
|
###
|
|
clearContent: (elementSelector = "#content") ->
|
|
if (elementSelector == "#content") # empty the intro with the content
|
|
document.getElementById("intro").innerHTML = ""
|
|
document.querySelector(elementSelector).innerHTML = ""
|
|
|
|
prepareContent: (content) ->
|
|
if typeof content == "function"
|
|
content = content()
|
|
if content instanceof jQuery
|
|
content = content[0].outerHTML
|
|
return content.toString()
|
|
|
|
# Write content to current room
|
|
write: (content, elementSelector = "#current-room") =>
|
|
if content == ""
|
|
return
|
|
content = @prepareContent(content)
|
|
block = document.querySelector(elementSelector)
|
|
if block
|
|
block.innerHTML = block.innerHTML + markdown(content)
|
|
else
|
|
# most likely this is the starting room
|
|
block = document.getElementById("content")
|
|
block.innerHTML = content
|
|
|
|
# Append content to a block. Does not replace the old content.
|
|
append: (content, elementSelector = "#content") =>
|
|
if content == ""
|
|
return
|
|
content = @prepareContent(content)
|
|
block = document.querySelector(elementSelector)
|
|
block.innerHTML = block.innerHTML + markdown(content)
|
|
|
|
# Replaces the text in the given block with the given text.
|
|
# !! Does not call markdown on the provided text. !!
|
|
replace: (content, elementSelector) =>
|
|
if content == ""
|
|
return
|
|
content = @prepareContent(content)
|
|
block = document.querySelector(elementSelector)
|
|
block.innerHTML = content
|
|
###
|
|
Turns any links that target the given href into plain
|
|
text. This can be used to remove action options when an action
|
|
is no longer available. It is used automatically when you give
|
|
a link the 'once' class.
|
|
###
|
|
clearLinks: (code) ->
|
|
for a in $("#content").find("a[href='" + code + "']")
|
|
html = a.innerHTML
|
|
a = $(a)
|
|
a.replaceWith($("<span>").addClass("ex_link").html(html))
|
|
return true
|
|
|
|
###
|
|
Given a list of situation ids, this outputs a standard option
|
|
block with the situation choices in the given order.
|
|
|
|
The contents of each choice will be a link to the situation,
|
|
the text of the link will be given by the situation's
|
|
outputText property. Note that the canChoose function is
|
|
called, and if it returns false, then the text will appear, but
|
|
the link will not be clickable.
|
|
|
|
Although canChoose is honored, canView and displayOrder are
|
|
not. If you need to honor these, you should either do so
|
|
manually, ot else use the `getSituationIdChoices` method to
|
|
return an ordered list of valid viewable situation ids.
|
|
###
|
|
writeChoices: (salet, listOfIds) =>
|
|
if (not listOfIds? or listOfIds.length == 0)
|
|
return
|
|
|
|
currentRoom = salet.getCurrentRoom()
|
|
$options = $("<ul>").addClass("options")
|
|
for roomId in listOfIds
|
|
room = salet.rooms[roomId]
|
|
assert(room, "unknown_situation".l({id:roomId}))
|
|
if (room == currentRoom)
|
|
continue
|
|
|
|
optionText = room.optionText.fcall(salet, currentRoom)
|
|
if (!optionText)
|
|
optionText = "choice".l({number:i+1})
|
|
$option = $("<li>")
|
|
$a = $("<span>")
|
|
if (room.canChoose.fcall(this, salet, currentRoom))
|
|
$a = $("<a>").attr({href: roomId})
|
|
$a.html(optionText)
|
|
$option.html($a)
|
|
$options.append($option)
|
|
@write($options)
|
|
|
|
# Marks all links as old. This gets called in a `processLink` function.
|
|
mark_all_links_old: () ->
|
|
$('.new').removeClass('new')
|
|
|
|
# Removes links and transient sections.
|
|
# Arguments:
|
|
# interactive - if we're working in interactive mode (or we're loading a save)
|
|
removeTransient: (interactive = false) ->
|
|
for a in $('#content').find('a')
|
|
a = $(a)
|
|
if (a.hasClass('sticky') || a.attr("href").match(/[?&]sticky[=&]?/))
|
|
return
|
|
a.replaceWith($("<span>").addClass("ex_link").html(a.html()))
|
|
contentToHide = $('#content .transient, #content ul.options')
|
|
contentToHide.add($("#content a").filter(() ->
|
|
return $(this).attr("href").match(/[?&]transient[=&]?/)
|
|
))
|
|
if (interactive)
|
|
contentToHide.animate({opacity: 0}, 350).
|
|
slideUp(500, () ->
|
|
$(this).remove()
|
|
)
|
|
else
|
|
contentToHide.remove()
|
|
|
|
# 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, callback) =>
|
|
maxLevel = 6
|
|
if level < maxLevel
|
|
i = level + 1
|
|
hideArray = []
|
|
while i <= maxLevel
|
|
hideArray.push("#content .lvl"+i)
|
|
i++
|
|
directive = hideArray.join(", ")
|
|
$(directive).hide()
|
|
@write system.view.wrapLevel(callback(), level)
|
|
|
|
wrapLevel: (text, level) =>
|
|
return "<div class='lvl#{level}'>"+markdown(text)+'</div>'
|
|
|
|
# At last, we scroll the view so that .new objects are in view.
|
|
endOutputTransaction: () =>
|
|
if !@interactive
|
|
return; # We're loading a save; do nothing at all.
|
|
$new = $('.new')
|
|
viewHeight = $(window).height()
|
|
newTop = newBottom = newHeight = optionHeight = 0
|
|
|
|
if $new.length == 0
|
|
return; # Somehow, there's nothing new.
|
|
|
|
newTop = $new.first().offset().top
|
|
newBottom = $new.last().offset().top + $new.last().height()
|
|
newHeight = newBottom - newTop
|
|
|
|
# We take the options list into account, because we don't want the new
|
|
# content to scroll offscreen when the list disappears. So we calculate
|
|
# scroll points as though the option list was already gone.
|
|
if ($('.options').not('.new').length)
|
|
optionHeight = $('.options').not('new').height()
|
|
|
|
if (newHeight > (viewHeight - optionHeight - 50))
|
|
# The new content is too long for our viewport, so we scroll the
|
|
# top of the new content to roughly 75% of the way up the viewport's
|
|
# height.
|
|
scrollTopTo(newTop-(viewHeight*0.25) - optionHeight);
|
|
else
|
|
if (newTop > $('body').height() - viewHeight)
|
|
# If we scroll right to the bottom, the new content will be in
|
|
# view. So we do that.
|
|
scrollToBottom();
|
|
else
|
|
# Our new content is too far up the page. So we scroll to place
|
|
# it somewhere near the bottom.
|
|
scrollBottomTo(newBottom+100 - optionHeight);
|
|
|
|
# Feature detection
|
|
hasLocalStorage: () ->
|
|
return window.localStorage?
|
|
|
|
# Any point that an option list appears, its options are its first links.
|
|
fixClicks: () ->
|
|
$("body").on('click', "ul.options li", (event) ->
|
|
# Make option clicks pass through to their first link.
|
|
link = $("a", this)
|
|
if (link.length > 0)
|
|
$(link.get(0)).click()
|
|
)
|
|
|
|
showBlock: (selector) ->
|
|
block = document.querySelector(selector)
|
|
if block
|
|
block.style.display = "block"
|
|
hideBlock: (selector) ->
|
|
block = document.querySelector(selector)
|
|
if block
|
|
block.style.display = "none"
|
|
updateWays: (salet, ways, name) ->
|
|
if document.getElementById("ways") == null
|
|
return
|
|
content = ""
|
|
distances = []
|
|
if ways then for way in ways
|
|
if salet.rooms[way]?
|
|
title = salet.rooms[way].title.fcall(this, name)
|
|
content += "<li class='nav-item'><a class='nav-link' href='#{way}'>#{title}</a></li>"
|
|
distances.push({
|
|
key: way
|
|
distance: salet.rooms[way].distance
|
|
})
|
|
@showBlock(".ways #ways_hint")
|
|
else
|
|
@hideBlock(".ways #ways_hint")
|
|
document.getElementById("ways").innerHTML = content
|
|
min = Infinity
|
|
min_key = []
|
|
for node in distances
|
|
if node.distance < min
|
|
min = node.distance
|
|
min_key = [node.key]
|
|
if node.distance == min
|
|
min_key.push(node.key)
|
|
if min < Infinity
|
|
for node in min_key
|
|
waylink = document.getElementById("waylink-#{node}")
|
|
if waylink
|
|
addClass(waylink, "destination")
|
|
|
|
pictureTag: (picture) ->
|
|
extension = picture.substr((~-picture.lastIndexOf(".") >>> 0) + 2)
|
|
if (extension == "webm")
|
|
return """
|
|
<video src="#{picture}" controls>
|
|
Your browser does not support the video tag for some reason.
|
|
You won't be able to view this video in this browser.
|
|
</video>
|
|
"""
|
|
return "<img class='img-responsive' src='#{picture}' alt='Room illustration'>"
|
|
|
|
cycleLink: (content) ->
|
|
return "<a href='./_replacer_cyclewriter' class='cycle' id='cyclewriter'>#{content}</a>"
|
|
|
|
module.exports = SaletView
|