diff --git a/Gulpfile.coffee b/Gulpfile.coffee
index 6e266ef..6cae8a9 100644
--- a/Gulpfile.coffee
+++ b/Gulpfile.coffee
@@ -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('.'));
-});
+);
diff --git a/README.md b/README.md
index 9637290..8f4cb84 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/game/begin.coffee b/game/begin.coffee
index 0e6a24f..5454b96 100644
--- a/game/begin.coffee
+++ b/game/begin.coffee
@@ -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 "#{content}"
+textlink = (content, ref) ->
+ return "#{content}"
+actlink = (content, ref) ->
+ return "#{content}"
+textcycle = (content, ref) ->
+ return "#{content}"
+
# 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 = """
-
-
+
+
+
+
+
+
+
+
+
diff --git a/lib/dialogue.coffee b/lib/dialogue.coffee
new file mode 100644
index 0000000..ae6c697
--- /dev/null
+++ b/lib/dialogue.coffee
@@ -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
diff --git a/lib/interface.coffee b/lib/interface.coffee
new file mode 100644
index 0000000..bc58d84
--- /dev/null
+++ b/lib/interface.coffee
@@ -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()
+ )
+)
diff --git a/lib/markdown.coffee b/lib/markdown.coffee
new file mode 100644
index 0000000..9b09980
--- /dev/null
+++ b/lib/markdown.coffee
@@ -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
diff --git a/lib/obj.coffee b/lib/obj.coffee
new file mode 100644
index 0000000..febdb05
--- /dev/null
+++ b/lib/obj.coffee
@@ -0,0 +1,33 @@
+markdown = require('./markdown.coffee')
+undum = require('undum-commonjs')
+$ = require("jquery")
+objlink = (content, ref) ->
+ return "
#{content}"
+
+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 = "
" + text + ""
+ 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
diff --git a/lib/oneOf.coffee b/lib/oneOf.coffee
new file mode 100644
index 0000000..051e129
--- /dev/null
+++ b/lib/oneOf.coffee
@@ -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;
diff --git a/lib/room.coffee b/lib/room.coffee
new file mode 100644
index 0000000..ddf9bba
--- /dev/null
+++ b/lib/room.coffee
@@ -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 "
#{content}"
+
+# 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("
")
+
+ 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("")
+
+ 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
diff --git a/lib/situation.coffee b/lib/situation.coffee
new file mode 100644
index 0000000..ea18c59
--- /dev/null
+++ b/lib/situation.coffee
@@ -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
diff --git a/package.json b/package.json
index ffbd622..478f94f 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
}
diff --git a/sass/_variables.scss b/sass/_variables.scss
index a2b12ae..539c3a5 100644
--- a/sass/_variables.scss
+++ b/sass/_variables.scss
@@ -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;
diff --git a/sass/bootstrap b/sass/bootstrap
index 08031d6..643bd8e 160000
--- a/sass/bootstrap
+++ b/sass/bootstrap
@@ -1 +1 @@
-Subproject commit 08031d6a76337e9e6d0c7aa3d034b8006e26e705
+Subproject commit 643bd8eaeb7a2a692fec3add22a3b61eff0fb62c
diff --git a/sass/main.scss b/sass/main.scss
index 7ff2828..1bca8fc 100644
--- a/sass/main.scss
+++ b/sass/main.scss
@@ -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;