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 = """ -
- #{text} -
- """ - 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) - -
- """ + """, + 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") diff --git a/game/init.coffee b/game/init.coffee index 4057f96..658e2ad 100644 --- a/game/init.coffee +++ b/game/init.coffee @@ -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 diff --git a/game/story.coffee b/game/story.coffee index e69de29..e02bcd4 100644 --- a/game/story.coffee +++ b/game/story.coffee @@ -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. + """ diff --git a/html/index.html b/html/index.html index 399db6d..35ee6ee 100644 --- a/html/index.html +++ b/html/index.html @@ -2,16 +2,33 @@ - Salet tutorial + Salet showcase
+
+
+
+

Salet

+

A general cybertext IF engine

+ +
+
+
-
-

Salet tutorial

+
+
+

Intro here.

+ + +
+
+
@@ -20,22 +37,31 @@

Other rooms

-
- - - - + -
+
- +
+
+ + +
+ +
+

+
+
+
+ +
+
+ +
+ + + + + + 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;