Backports from Rachel, new interface.

This commit is contained in:
Alexander Yakovlev 2016-01-15 08:06:03 +07:00
parent f1a6d47dea
commit 16b82caf8d
17 changed files with 832 additions and 251 deletions

View file

@ -1,118 +1,131 @@
watchify = require('watchify')
browserify = require('browserify')
browserSync = require('browser-sync')
gulp = require('gulp')
source = require('vinyl-source-stream')
gutil = require('gulp-util')
coffeify = require('coffeeify')
coffee = require("gulp-coffee")
sass = require('gulp-sass')
uglify = require('gulp-uglify')
buffer = require('vinyl-buffer')
zip = require('gulp-zip')
_ = require('lodash')
concat = require('gulp-concat')
reload = browserSync.reload;
reload = browserSync.reload
# Copy assets over without touching them
assets = (target) ->
html = (target) ->
return () ->
return gulp.src([
'img/*.png',
'img/*.jpeg',
'img/*.jpg'
]).pipe(gulp.dest(target))
return gulp.src(['html/index.html','html/en.html'])
.pipe(gulp.dest(target));
gulp.task('assets', assets('./build'));
img = (target) ->
return () ->
return gulp.src(['img/*.png', 'img/*.jpeg', 'img/*.jpg'])
.pipe(gulp.dest(target));
gulp.task('sass', function () {
audio = (target) ->
return () ->
return gulp.src(['audio/*.mp3'])
.pipe(gulp.dest(target));
gulp.task('html', html('./build'))
gulp.task('img', img('./build/img'))
gulp.task('audio', audio('./build/audio'))
gulp.task('sass', () ->
gulp.src('sass/main.scss')
.pipe(sass().on('error', sass.logError))
.pipe(sass({outputStyle: 'compressed'}).on('error', sass.logError))
.pipe(gulp.dest('./build/css'));
)
sources = [
'./game/begin.coffee',
'./game/story.coffee',
'./game/init.coffee',
]
opts = _.assign({}, watchify.args, {
entries: ["./build/game/main.coffee"]
debug: true
transform: [coffeify]
});
bundler = watchify(browserify(opts));
gulp.task('html', html('./build'));
bundle = () ->
return bundler.bundle()
.on('error', gutil.log.bind(gutil, 'Browserify Error'))
.pipe(source('bundle.js'))
.pipe(gulp.dest('./build/game'));
gulp.task('concatCoffee', function() {
return gulp.src([
'./game/begin.coffee',
'./game/story.coffee',
'./game/init.coffee',
])
gulp.task('concatCoffee', () ->
return gulp.src(sources)
.pipe(concat('./main.coffee'))
.pipe(gulp.dest('./build/game'));
});
);
gulp.task('coffee', ['concatCoffee'])
gulp.task('coffee', ['concatCoffee'], bundle);
bundler.on('update', coffee);
bundler.on('log', gutil.log); # Output build logs to terminal
bundler.on('update', bundle);
bundler.on('log', gutil.log);
gulp.task('build', ['html', 'img', 'sass', 'coffee'])
gulp.task('build', ['html', 'img', 'sass', 'coffee', 'audio'])
gulp.task('serve', ['build'], () ->
browserSync({
server: {
baseDir: 'build'
}
})
});
sassListener = () ->
reload('./build/css/main.css');
gulp.watch(['./html/*.html'], ['html']);
gulp.watch(['./sass/*.scss'], ['sass']);
gulp.watch(['./build/css/main.css'], sassListener);
gulp.watch(['./img/*.png', './img/*.jpeg', './img/*.jpg'], ['img']);
gulp.watch(['./game/*.coffee'], ['coffee']);
gulp.watch(['./build/css/main.css'], sassListener);
gulp.watch(
['./build/game/bundle.js', './build/img/*', './build/index.html'],
browserSync.reload)
})
/* Distribution tasks */
var undumDistBundler = browserify();
undumDistBundler.require('undum-commonjs');
gulp.task('undum-dist', function () {
return undumDistBundler.bundle().pipe(source('undum.js'))
.pipe(buffer())
.pipe(uglify())
.pipe(gulp.dest('./dist/game'));
});
browserSync.reload);
)
gulp.task('html-dist', html('./dist'));
gulp.task('img-dist', img('./dist/img'));
gulp.task('legal-dist', function () {
gulp.task('audio-dist', audio('./dist/audio'));
gulp.task('legal-dist', () ->
return gulp.src(['LICENSE.txt'])
.pipe(gulp.dest("./dist"));
});
);
gulp.task('sass-dist', function () {
gulp.task('sass-dist', () ->
return gulp.src('./sass/main.scss')
.pipe(sass({outputStyle: 'compressed'}))
.pipe(gulp.dest('./dist/css'));
});
);
var distBundler = browserify({
distBundler = browserify({
debug: false,
entries: ['./build/game/main.coffee'],
transform: ['coffeeify']
});
distBundler.external('undum-commonjs');
gulp.task('coffee-dist', ['undum-dist', 'concatCoffee'], function () {
gulp.task('coffee-dist', ['concatCoffee'], () ->
return distBundler.bundle()
.pipe(source('bundle.js'))
.pipe(buffer())
.pipe(uglify())
.on('error', gutil.log)
.pipe(gulp.dest('./dist/game'));
});
);
gulp.task('dist', ['html-dist', 'img-dist', 'sass-dist', 'coffee-dist', 'legal-dist'],
function () {
return;
});
gulp.task('dist', ['html-dist', 'img-dist', 'sass-dist', 'coffee-dist', 'audio-dist', 'legal-dist']);
gulp.task('zip', ['dist'], function () {
gulp.task('zip', ['dist'], () ->
return gulp.src('dist/**')
.pipe(zip('dist.zip'))
.pipe(gulp.dest('.'));
});
);

View file

@ -2,6 +2,7 @@
A general client-side framework for cybertext interactive fiction games.
**Salet** is based upon the ideas of [Undum,](https://github.com/idmillington/undum) but is not a direct follower.
It also uses some code from [Raconteur,](https://github.com/sequitur/raconteur) rewritten in CoffeeScript and altered to its own needs.
## License

View file

@ -1,170 +1,44 @@
situation = require('raconteur')
markdown = require('../../lib/markdown.coffee')
room = require("../../lib/room.coffee")
obj = require('../../lib/obj.coffee')
dialogue = require('../../lib/dialogue.coffee')
undum = require('undum-commonjs')
oneOf = require('raconteur/lib/oneOf.js')
qualities = require('raconteur/lib/qualities.js')
oneOf = require('../../lib/oneOf.coffee')
$ = require("jquery")
require('../../lib/interface.coffee')
Array.prototype.oneOf = () ->
oneOf.apply(null, this)
md = require('markdown-it')
markdown = new md({
typographer: true,
html: true
})
shortid = require('shortid')
# you have to alter linkRe in Undum core to use that.
# Undum doesn't allow using uppercase letters in situation names by default.
undum.game.id = "6a909a4-586a-4089-bd18-26da684d1c8d"
undum.game.id = "your-game-id-here"
undum.game.version = "1.0"
a = require('raconteur/lib/elements.js').a
way_to = (content, ref) -> a(content).class('way').ref(ref)
textlink = (content, ref) -> a(content).once().writer(ref)
actlink = (content, ref) -> a(content).once().action(ref)
textcycle = (content, ref) -> a(content).replacer(ref).class("cycle").id(ref).toString()
###
Element helpers. There is no real need to build monsters like a().id("hello")
because you won't use them as is. It does not make sense in context, the
author has Markdown and all utilities to *forget* about the markup.
###
way_to = (content, ref) ->
return "<a href='#{ref}' class='way'>#{content}</a>"
textlink = (content, ref) ->
return "<a href='./_writer_#{ref}' class='once'>#{content}</a>"
actlink = (content, ref) ->
return "<a href='./#{ref}' class='once'>#{content}</a>"
textcycle = (content, ref) ->
return "<a href='./_replacer_#{ref}' class='cycle' id='#{ref}'>#{content}</a>"
# usage: writemd( system, "Text to write")
writemd = (system, text) ->
if typeof text is Function
text = text()
text = markdown.render(text)
text = markdown(text)
system.write(text)
preparemd = (text, mark) ->
if typeof text is Function
text = text()
text = markdown.render(text)
if mark?
text = """
<div class="#{mark}">
#{text}
</div>
"""
return text
money = (character, system, amount) ->
system.setQuality("money", character.qualities.money + amount)
code_can_input = (character) ->
return character.sandbox.code.length < 8
code_print = (character) ->
mask = 8 - character.sandbox.code.length
retval = character.sandbox.code
if mask > 0
for i in [1..mask]
retval += "_"
return retval
code_input = (character, digit) ->
if code_can_input(character)
character.sandbox.code = character.sandbox.code + digit
code_reset = (character) ->
character.sandbox.code = ""
code_check = (character, system) ->
if character.sandbox.code.length >= 8
# There is an Undum.. let's call it a feature
# that prevents the player from entering "3112".
# You see, you can't select the situation 1 when you are
# already in this situation, so you can't input 1 twice.
if character.sandbox.code == "01012017"
character.sandbox.box_opened = 1
if character.sandbox.knows_the_code == 0
writemd(system, """
Is he an extraordinary puzzle cracker or was it a sheer luck, but Ronald manages to *guess* the code.
""")
else
writemd(system, """
New Year 2017.
L. Y. must be Leonard Yakovlev, a famous painter.
Some tabloids tried to connect him with Ana but it seemed like a weak link.
By that logic, his sketch is worth more than all the cash here.
Ronald thinks about it, but decides to "let the woman have her memories".
""")
writemd(system, """
Something clicks and box opens.
The phone is slick, black and light in Ronald's hand.
It springs to life, humming with purpose.
The screen plays an animation: "LOADING..."
Ronald has no other business here.
It's time to go.
""")
system.doLink("bedroom")
else
writemd(system, "Something clicks and the display resets, but the box stays locked.")
if character.sandbox.code == "000000"
writemd(system, "Of course, Ronald didn't hope it would be that easy.")
character.sandbox.code = ""
update_ways = (ways) ->
content = ""
for way in ways
if undum.game.situations[way]?
content += way_to(undum.game.situations[way].title, way)
$("#ways").html(content)
situation "start",
# The first room of the game.
# For accessibility reasons the text is provided in HTML, not here.
room "start",
content: """
Peter had so much trouble sleeping he had to drown his pills in at least an hour of thoughts.
A violent ringing of the bell awakened him.
He rose from the bed, grumbling:
Crazy neighbors and their guests. It must be three o'clock!”
The visitor entered the hallway.
It was him ringing the bell, but he was not going to meet Peter.
In fact, he wasn't looking for meeting anybody here.
Fourth floor, apartment 406.
There, he tried two keys.
The second of them fitted the lock.
Burglary is a curious line of employment.
Befittedly, Ronald Chernoff was very curious about a black phone behind the door of apartment 406 in a wooden box on a small table no farther than two meters from the bed.
A gift, a prototype, a valuable treasure left by Anastacia Kozlowa when she fled the country.
Of course, one had to be reasonably au fait with her *Instagram* to notice that.
Peter opened his door to find an empty silent corridor.
He went to the neighbor's door and met a closed door.
Ronald was working inside, quietly walking around the apartment.
He began the inspection from [the living room.](living-room)
<hr>
"""
""",
choices: "#start"
# This function needs to go after the start room.
is_visited = (situation) ->
situations = undum.game.situations[situation]
if situations
return Boolean situations.visited
place = undum.game.situations[situation]
if place
return Boolean place.visited
return 0
# N-th level examine function
level = (text, mark) ->
$("#content .#{mark}").fadeOut()
return preparemd(text, mark)
lvl1 = (text) ->
$("#content .lvl2").fadeOut()
$("#content .lvl3").fadeOut()
$("#content .lvl4").fadeOut()
level(text, "lvl1")
lvl2 = (text) ->
$("#content .lvl3").fadeOut()
$("#content .lvl4").fadeOut()
level(text, "lvl2")
lvl3 = (text) ->
$("#content .lvl4").fadeOut()
level(text, "lvl3")
lvl4 = (text) ->
level(text, "lvl4")

View file

@ -1,14 +1,6 @@
# This is where you initialize your game.
# All code in this file comes last, so the game is almost ready by this point.
player "Player"
money: 0
status: "Good"
undum.game.init = (character, system) ->
$("#ways").on("click", "a", (event) ->
event.preventDefault()
undum.processClick($(this).attr("href"))
)
window.onload = undum.begin

View file

@ -0,0 +1,28 @@
# Your game goes here
dialogue "Option 1", "start", "secretary", """
No spoilers!
"""
dialogue "Option 2", "start", "secretary", """
No spoilers!
"""
room "university-start",
tags: ["secretary"]
ways: ["supermarket"]
optionText: "Leave the University"
before: () ->
document.getElementById("intro").innerHTML = ""
document.getElementById("content").innerHTML = ""
undum.game.situations["supermarket"].destination()
"""
You leave the University.
"""
content: """
Okay, now to the supermarket.
"""
room "supermarket",
content: """
A trendy supermarket.
"""

View file

@ -2,16 +2,33 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>Salet tutorial</title>
<title>Salet showcase</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href='https://fonts.googleapis.com/css?family=PT+Sans:400,400italic|PT+Sans+Caption' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="css/main.css">
</head>
<body>
<div id="page" class="container">
<div class="row">
<div id="title" class="title">
<div class="label">
<h1>Salet</h1>
<h2>A general cybertext IF engine</h2>
<noscript>
<p class="noscript_message">This game requires Javascript.</p>
</noscript>
</div>
</div>
</div>
<div id="content_wrapper" class="row">
<div id="content">
<h1>Salet tutorial</h1>
<div id="intro" class="content">
<section>
<p>Intro here.</p>
<noscript>You need to turn on Javascript to play this game.</noscript>
</section>
</div>
<div id="content" class="content">
</div>
<a name="end_of_content"></a>
</div>
@ -20,22 +37,31 @@
<h2>Other rooms</h2>
<div id="ways"></div>
</div>
<div class='buttons'>
<button id="undo">Undo</button>
<button id="save">Save</button>
<button id="load">Load</button>
<button id="erase">Restart</button>
<div class="menu">
<ul class="nav nav-pills">
<li class="nav-item">
<span class="nav-link disabled" id="inventory">Inventory</span>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">System</a>
<div class="dropdown-menu">
<span class="dropdown-item disabled" id="save">Save</span>
<span class="dropdown-item" id="erase">Restart</span>
<div class="dropdown-divider"></div>
<span class="dropdown-item disabled" id="undo">Undo (non-fuctioning)</span>
</div>
</li>
</ul>
</div>
</div><!-- end of tools -->
</div> <!-- End of div.tools_wrapper -->
<div class="row">
<div id="legal">
<div id="footleft">
<!-- Author credit goes here -->
<p>The game was written by <b><a href="http://en.oreolek.ru/" target="_blank">Oreolek.</a></b></p>
<!-- It's a good gesture to specify how long is your game. -->
<p>Approximate play time: five minutes.</p>
<p>Written using <a href="http://git.oreolek.ru/oreolek/salet" target="_blank">Salet.</a>
<p>The game was written by <em>(you should put your name here)</em></p>
<p>Approximate play time: not measured.</p>
<p>Written using <a href="http://git.oreolek.ru/oreolek/salet" target="_blank">Salet</a>.</p>
<p>Betatesting credit: none yet</p>
</div>
<div id="footright">
<a href="./LICENSE.txt"><img src="img/mit.png" alt="This program is licensed under MIT license."></a>
@ -44,6 +70,27 @@
</div>
</div> <!-- End of div.page -->
<script type="text/javascript" src="game/main.js"></script>
<div id="ui_library">
<div id="quality" class="quality">
<span class="name" data-attr="name"></span>
<span class="value" data-attr="value"></span>
</div>
<div id="quality_group" class="quality_group">
<h2 data-attr="title"></h2>
<div class="qualities_in_group">
</div>
</div>
<hr id="turn_separator">
</div>
<div id="content_library"></div>
<!-- CDN JS Libraries -->
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.min.js"></script>
<script type="text/javascript" src="//code.jquery.com/jquery-2.2.0.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
<script type="text/javascript" src="game/bundle.js"></script>
</body>
</html>

33
lib/dialogue.coffee Normal file
View file

@ -0,0 +1,33 @@
room = require("./room.coffee")
randomid = () ->
alphabet = "abcdefghijklmnopqrstuvwxyz0123456789" # see the dreaded linkRe expression in Undum
rndstr = []
for i in [1..10]
rndstr.push alphabet.charAt(Math.floor(Math.random() * alphabet.length))
return rndstr.join('').toString()
###
A dialogue shortcut.
Usage:
dialogue "Point out a thing in her purse (mildly)", "start", "mild", """
Point out a thing in her purse (mildly)
""", "character.sandbox.mild = true"
###
dialogue = (title, startTag, endTag, text, effect) ->
retval = room(randomid(), {
optionText: title
content: text
choices: "#"+endTag
})
if typeof(startTag) == "string"
retval.tags = [startTag]
else if typeof(startTag) == "object"
retval.tags = startTag
if effect?
retval.before = (character, system) ->
eval(effect)
return retval
module.exports = dialogue

14
lib/interface.coffee Normal file
View file

@ -0,0 +1,14 @@
###
Salet interface configuration.
###
$ = require("jquery")
$(document).ready(() ->
$("#ways").on("click", "a", (event) ->
event.preventDefault()
undum.processClick($(this).attr("href"))
)
$("#inventory").on("click", "a", (event) ->
event.preventDefault()
)
)

32
lib/markdown.coffee Normal file
View file

@ -0,0 +1,32 @@
###
Indent normalization. Removes tabs AND spaces from every line beginning.
Implies that you don't mix up your tabs and spaces.
Copyright 2015 Bruno Dias
###
normaliseTabs = (text) ->
lines = text.split('\n');
indents = lines
.filter((l) => l != '')
.map((l) => l.match(/^\s+/))
.map((m) ->
if (m == null)
return ''
return m[0]
)
smallestIndent = indents.reduce((max, curr) ->
if (curr.length < max.length)
return curr
return max
)
return lines.map((l) ->
return l.replace(new RegExp('^' + smallestIndent), '')
).join('\n')
markdown = (text) ->
if typeof text is Function
text = text()
return marked(normaliseTabs(text), {
smartypants: true
})
module.exports = markdown

33
lib/obj.coffee Normal file
View file

@ -0,0 +1,33 @@
markdown = require('./markdown.coffee')
undum = require('undum-commonjs')
$ = require("jquery")
objlink = (content, ref) ->
return "<a href='./_act_#{ref}'>#{content}</a>"
class RaconteurObj
constructor: (spec) ->
for key, value of spec
this[key] ?= value
level: 0
look: (character, system, f) ->
if @dsc
text = markdown(@dsc.fcall(this, character, system, f))
text = "<span class='look lvl#{@level}'>" + text + "</span>"
window.name = @name
text = text.replace /([\s^])\{\{(\w+)\}\}([\s$])/g, (str, p1, p2, p3) ->
name = window.name
window.name = undefined
return p1+objlink(p2, name)+p3
return text
take: () -> "You take the #{@name}." # taking to inventory
act: () -> "You don't find anything extraordinary about the #{@name}." # object action
dsc: () -> "You see a {{#{@name}}} here." # object description
put: (room) ->
@level = 0 # this is scenery
undum.game.situations[room].objects[@name] = this
obj = (name, spec) ->
spec ?= {}
spec.name = name
return new RaconteurObj(spec)
module.exports = obj

170
lib/oneOf.coffee Normal file
View file

@ -0,0 +1,170 @@
###
oneOf.js
Copyright (c) 2015 Bruno Dias
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
###
###
Undularity Tools
Those functions are not a core part of Undularity, but provide some
general functionality that relates to adaptive text generation.
This is provided partly as a helper to less technical users, and as
a convenience for authors.
###
# Monkey patching
###
Shuffles an array. It can use Undum's random number generator implementation,
so it expects a System.rnd object to be passed into it. If one isn't
supplied, it will use Math.Random instead.
This is an implementation of the Fischer-Yates (Knuth) shuffle.
Returns the shuffled array.
###
Array.prototype.shuffle = (system) ->
rng = if system then system.rnd.random else Math.random
# slice() clones the array. Object members are copied by reference, beware.
newArr = this.slice()
m = newArr.length
while (m)
i = Math.floor(rng() * m--)
t = newArr[m]
newArr[m] = newArr[i]
newArr[i] = t
return newArr
###
oneOf()
Takes an array and returns an object with several methods. Each method
returns an iterator which iterates over the array in a specific way:
inOrder()
Returns the array items in order.
cycling()
Returns the array items in order, cycling back to the first item when
it runs out.
stopping()
Returns the array items in order, then repeats the last item when it
runs out.
randomly()
Returns the array items at random. Takes a system object, for consistent
randomness. Will never return the same item twice in a row.
trulyAtRandom()
Returns the array items purely at random. Takes a system object, for
consistent randomness.
inRandomOrder()
Returns the array items in a random order. Takes a system object, for
consistent randomness.
###
###
Takes a function and gives it a toString() property that calls itself and
returns its value, allowing for ambiguous use of the closure object
as a text snippet.
Returns the modified function.
###
stringish = (callback) ->
callback.toString = () ->
return '' + this.call()
return callback
oneOf = (ary...) ->
if ary.length == 0
throw new Error(
"tried to create a oneOf iterator with a 0-length array");
return {
inOrder: () ->
i = 0
return stringish(() ->
if i >= ary.length
return null
return ary[i++]
)
cycling: () ->
i = 0
return stringish(() ->
if (i >= ary.length)
i = 0
return ary[i++]
)
stopping: () ->
i = 0
return stringish(() ->
if (i >= ary.length)
i = ary.length - 1
return ary[i++]
)
randomly: (system) ->
rng = if system then system.rnd.random else Math.random
last = null
if (ary.length<2)
throw new Error("attempted to make randomly() iterator with a 1-length array")
return stringish( () ->
i = null
offset = null
if not last?
i = Math.floor(rng() * ary.length)
else
###
Let offset be a random number between 1 and the length of the
array, minus one. We jump offset items ahead on the array,
wrapping around to the beginning. This gives us a random item
other than the one we just chose.
###
offset = Math.floor(rng() * (ary.length -1) + 1);
i = (last + offset) % ary.length;
last = i
return ary[i]
)
trulyAtRandom: (system) ->
rng = if system then system.rnd.random else Math.random
return stringish(() ->
return ary[Math.floor(rng() * ary.length)];
)
inRandomOrder: (system) ->
shuffled = ary.shuffle(system)
i = 0
return stringish(() ->
if (i >= ary.length)
i = 0
return shuffled[i++]
)
}
Array.prototype.oneOf = () ->
oneOf.apply(null, this)
module.exports = oneOf;

166
lib/room.coffee Normal file
View file

@ -0,0 +1,166 @@
# I confess that this world model heavily borrows from INSTEAD engine. - A.Y.
undum = require('undum-commonjs')
RaconteurSituation = require('./situation.coffee')
obj = require('./obj.coffee')
markdown = require('./markdown.coffee')
$ = require("jquery")
way_to = (content, ref) ->
return "<a href='#{ref}' class='way' id='waylink-#{ref}'>#{content}</a>"
# jQuery was confused by this point where's the context so I did it vanilla-way
print = (content) ->
if typeof content == "function"
content = content()
block = document.getElementById("current-situation")
if block
block.innerHTML = block.innerHTML + markdown(content)
else #the game is not initialized yet. This is dangerous and will not augment any links.
block = document.getElementById("content")
block.innerHTML = markdown(content)
Array::remove = (e) -> @[t..t] = [] if (t = @indexOf(e)) > -1
addClass = (element, className) ->
if (element.classList)
element.classList.add(className)
else
element.className += ' ' + className
# Function to return the current room.
# Works because our `enter()` function sets the `data-situation` attribute.
here = () ->
return undum.game.situations[document.getElementById("current-situation").getAttribute("data-situation")]
update_ways = (ways) ->
content = ""
distances = []
if ways
for way in ways
if undum.game.situations[way]?
title = undum.game.situations[way].name
content += way_to(title, way)
distances.push({
key: way
distance: undum.game.situations[way].distance
})
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
addClass(document.getElementById("waylink-#{node}"), "destination")
class SaletRoom extends RaconteurSituation
constructor: (spec) ->
RaconteurSituation.call(this, spec)
if spec.objects?
@objects = spec.objects
if spec.exit?
@exit = spec.exit
return this
objects: []
distance: Infinity # distance to the destination
###
I call SaletRoom.exit every time the player exits to another room.
###
exit: (character, system, to) ->
###
Undum calls Situation.enter 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.
###
enter: (character, system, f) ->
#system.clearContent()
if f != @name and f?
@visited++
undum.game.situations[f].exit(character, system, @name)
if not @extendSection
classes = if @classes then ' ' + @classes.join(' ') else ''
situation = document.getElementById('current-situation')
if situation?
situation.setAttribute('id', undefined)
system.write("<section id='current-situation' data-situation='#{@name}' class='situation-#{@name}#{classes}'>")
if f != @name and @before?
print(@before.fcall(this, character, system, f))
if @look
@look character, system, f
if f != @name and @after?
print(@after.fcall(this, character, system, f))
if not @extendSection
system.write("</section>")
if @choices
system.writeChoices(system.getSituationIdChoices(@choices, @minChoices, @maxChoices))
look: (character, system, f) ->
update_ways(@ways)
# Print the room description
if @content
system.write(markdown(@content.fcall(this, character, system, f)))
if @objects? then for thing in @objects
system.write thing.look()
###
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: (character, system, action) ->
# default Raconteur action
if (action.match(/^_(writer|replacer|inserter)_.+$/))
return RaconteurSituation.prototype.act.call(this, character, system, f)
if (link = action.match(/^_act_(.+)$/)) #object action
for thing in @objects
if thing.name == link[1]
# We check the "take" function. If it exists, the player can take this object.
# If not, we check the "act" function.
if thing.take
@objects.remove(thing)
character.sandbox.inventory.push thing
@enter(character, system, @name)
return print(thing.take.fcall(thing, character, system))
if thing.act
return print(thing.act.fcall(thing, character, system))
# the loop is done but no return came - match not found
console.error("Could not find #{link[1]} in current room.")
# 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)
room = (name, spec) ->
if spec
spec.name = name
retval = new SaletRoom(spec)
return retval.register()
module.exports = room

101
lib/situation.coffee Normal file
View file

@ -0,0 +1,101 @@
###
This file is built on top of Raconteur.
Raconteur is copyright (c) 2015 Bruno Dias
This file is copyright (c) 2016 Alexander Yakovlev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
###
undum = require('undum-commonjs')
markdown = require('./markdown.coffee')
###
fcall() (by analogy with fmap) is added to the prototypes of both String and
Function. When called on a Function, it's an
alias for Function#call(); when called on a String, it only returns the
string itself, discarding any input.
###
Function.prototype.fcall = Function.prototype.call;
String.prototype.fcall = () -> return this
#Adds the "fade" class to a htmlString.
String.prototype.fade = () ->
return this.classList.add("fade")
###
The prototype RaconteurSituation is the basic spec for situations
created with Raconteur. It should be able to handle any use case for Undum.
This prototype is fairly complex; see the API documentation.
###
RaconteurSituation = (spec) ->
if RaconteurSituation.arguments.length == 0
return
undum.Situation.call(this, spec)
for key, value of spec
this[key] ?= value
@visited = 0
return this
RaconteurSituation.inherits(undum.Situation)
###
Situation.prototype.act() is called by Undum whenever an action link
(Ie, a link that doesn't point at another situation or an external URL) is
clicked.
Raconteur's version of act() is set up to implement commonly used
functionality: "writer" links, "replacer" links, "inserter" links, and
generic "action" links that call functions which access the underlying
Undum API.
###
RaconteurSituation.prototype.act = (character, system, action) ->
actionClass = action.match(/^_(\w+)_(.+)$/)
that = this
responses = {
writer: (ref) ->
content = @writers[ref].fcall(that, character, system, action)
output = markdown(content).fade()
system.writeInto(output, '#current-situation')
replacer: (ref) ->
content = @writers[ref].fcall(that, character, system, action)
output = markdown(content).fade()
system.replaceWith(output, '#'+ref)
inserter: (ref) ->
content = @writers[ref].fcall(that, character, system, action)
output = markdown(content).fade()
system.writeInto(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, character, system, action);
else
throw new Error("Tried to call undefined action: #{action}");
RaconteurSituation.prototype.register = () ->
if not @name?
console.error("Situation has no name")
return this
undum.game.situations[@name] = this
return this
module.exports = RaconteurSituation

View file

@ -1,15 +1,25 @@
{
"dependencies": {
"undum-commonjs": "git://github.com/oreolek/undum-commonjs#commonjs",
"jquery": "^2.1.3"
},
"private": true,
"devDependencies": {
"babelify": "^6.0.2",
"browser-sync": "^2.6.0",
"browserify": "^9.0.8",
"browserify-shim": "^3.8.8",
"coffeeify": "^1.0.0",
"gulp": "^3.8.11",
"gulp-uglify": "^1.2.0",
"gulp-sass": "^2.1.1",
"gulp-coffee": "^2.3.1",
"gulp-util": "^3.0.4",
"gulp-zip": "^3.0.2",
"gulp-shell": "^0.5.1",
"gulp-concat": "^2.6.0",
"browser-sync": "^2.11.0"
"gulp-sass": "^2.1.1",
"lodash": "^3.6.0",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"watchify": "^3.1.0"
}
}

View file

@ -6,7 +6,7 @@ $body-bg: #F1EED9;
$body-color: #58351A;
$link-color: #382313;
$btn-bg: #C33601;
$btn-color: $body-color;
$btn-color: lighten($btn-bg, 50%);
$secondary-bg: #F1EED9;
$waycolor: $link-color;

@ -1 +1 @@
Subproject commit 08031d6a76337e9e6d0c7aa3d034b8006e26e705
Subproject commit 643bd8eaeb7a2a692fec3add22a3b61eff0fb62c

View file

@ -9,7 +9,11 @@
@import "bootstrap/scss/type";
@import "bootstrap/scss/images";
@import "bootstrap/scss/grid";
@import "bootstrap/scss/buttons";
//@import "bootstrap/scss/buttons";
@import "bootstrap/scss/animation";
@import "bootstrap/scss/dropdown";
@import "bootstrap/scss/nav";
@import "bootstrap/scss/responsive-embed";
@import "bootstrap/scss/utilities";
@ -18,22 +22,67 @@ body {
overflow-x: hidden;
background: $body-bg;
}
// The title block
.title {
margin-top: 2em;
@include col(10,12);
@media (min-width: breakpoint-min(sm)) {
@include make-col-offset(1);
}
cursor: pointer; // Until we click to start.
.label {
overflow: hidden;
margin: auto;
max-width: 18em;
position: relative;
text-align: center;
}
.subtitle {
font-size: smaller;
color: #aaa;
}
h1,
h2,
h3 {
text-shadow: rgba(255,255,255,0.5) 2px 2px 2px,
rgba(0,0,0,0.1) -1px -1px 2px;
}
h2 {
font-size: 1.5rem;
}
.warnings {
font-size: small;
font-style: italic;
p {
margin-bottom: 1em;
}
}
.noscript_message {
left: 0;
right: 0;
bottom: 0;
position: absolute;
font-size: 0.9em;
font-style: italic;
text-align: center;
color: #943;
}
}
#tools_wrapper {
display: none; // Shown by Javascript
.ways {
padding: 0.5em;
// @include col(4, 5);
@include col(9, 10);
@include col(8, 9);
@media (min-width: breakpoint-min(sm)) {
@include make-col-offset(1);
}
}
.buttons {
@include col(1, 2);
button {
@extend .btn;
@include button-variant($btn-color, $btn-bg, $btn-color);
margin-bottom: 1em;
.destination {
font-weight: bold;
}
.menu {
@include col(3, 4);
span {
cursor: pointer;
}
}
}
@ -41,7 +90,7 @@ body {
background: $text_background;
border-radius: 5px;
}
#content {
.content {
@include col(10, 12);
@media (min-width: breakpoint-min(sm)) {
@include make-col-offset(1);
@ -97,7 +146,6 @@ body {
margin-top: 1em;
color: darken($body-color, 10%);
font-size: smaller;
display: none; // Shown by Javascript
#footleft {
@include make-col();
@media (min-width: breakpoint-min(sm)) {
@ -120,6 +168,11 @@ body {
}
}
#content_library,
#ui_library {
display: none;
}
.way {
color: $waycolor;
margin-right: 1em;
@ -128,6 +181,20 @@ body {
color: darkgreen;
border-bottom: darkgreen dashed 1px;
}
ul.options {
border: 1px solid #876;
li {
border-bottom: 1px solid #876;
}
li:hover {
background-color: rgba(153,136,119,0.2);
}
}
#legal {
.muted {
color: grey;
}
}
hr {
width: 50%;
border-color: $body-color;