diff --git a/.gitignore b/.gitignore index 71b27ad..6d163d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ node_modules/* !node_modules/undularity -!node_modules/undum devel/build/** game/** *.sublime-workspace \ No newline at end of file diff --git a/lib/situation.js b/lib/situation.js new file mode 100644 index 0000000..ba8ceea --- /dev/null +++ b/lib/situation.js @@ -0,0 +1,417 @@ +var undum = require('undum'), + md = require('markdown-it'), + $ = require('jquery'); + +/* --------------------------------------------------------------------------- + Undularity is a rethought API for Undum, featuring more usable interfaces + which coalesce as a DSL for defining Undum stories. +----------------------------------------------------------------------------*/ + +/* --------------------------------------------------------------------------- + Helper functions +----------------------------------------------------------------------------*/ + +/* + Normalises the whitespace on a string. So the indentation level of the + first line will become 0. FIXME: This isn't quite ideal. Need to figure out + a better way of preventing strings in source code ending up interpreted as +
 blocks.
+*/
+
+String.prototype.normaliseTabs = function () {
+  let lines = this.split('\n');
+  let indent = lines[0].match(/^\s+/) || lines[1].match(/^\s+/);
+  if (!indent) return this;
+  return lines.map( s => s.replace(new RegExp('^' + indent), '')).join('\n');
+};
+
+/* Agnostic Call */
+/*
+  Many properties in Undularity can be either a String, or a function that
+  takes some objects from the game state (character, system, and the current
+  situation) and returns a String. Or in Haskell terms:
+  String | (CharacterObject -> SystemObject -> SituationString -> String)
+
+  fcall() is added to the prototypes of both String and Function to handle
+  these situations. 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 = function () {return this;};
+
+/*
+  Markdown renderer, defined with options.
+*/
+
+var markdown = new md({
+  typographer: true, // Use smart quotes.
+  html: true // Passthrough html.
+});
+
+/*
+  Ensures a string is a HTML string, by wrapping it in span tags.
+*/
+
+String.prototype.spanWrap = function () {
+  return `${this}`;
+};
+
+/*
+  Adds the "fade" class to a htmlString.
+*/
+
+String.prototype.fade = function () {
+  return $(this).addClass('fade');
+};
+
+/* Situations ----------------------------------------------------------------
+  
+  The prototype UndularitySituation is the basic spec for situations
+  created with Undularity. It should be able to handle any use case for Undum.
+
+  Properties:
+
+    (In addition to properties inherited from undum.Situation)
+
+    actions :: {key: (character, system, from) -> null}
+
+    An object containing definitions for actions, which are called when an
+    action without a special marker (writer, inserter, replacer) is called
+    when the situation is current, usually by clicking an action link.
+
+    after :: (character, system, from) -> null
+
+    A function that is called right after printing the content of the
+    situation. Useful for housekeeping tasks (Such as changing character
+    stats) or implementing custom behaviour in general.
+
+    before :: (character, system, from) -> null
+
+    Similar to after, but called first 
+
+    choices :: [String]
+
+    A list of situation names and/or tags that can be listed as choices for
+    this situation. That list will be further filtered by CanView and
+    CanChoose.
+
+    content :: markdownString | (character, system, from) -> markdownString
+
+    The main content of the situation, printed when the situation is entered.
+
+    visited :: Number
+
+    Defaults to 0. Incremented every time the situation is entered.
+
+    writers :: {key: markdownString | (character, system, from) -> markdownString}
+
+    An object containing definitions for special actions called by inserter,
+    writer, and replacer links. Note that the content of writer links will be
+    interpreted as a regular markdownString, while the content of replacer
+    and inserter links, on the assumption that it's meant to be written into
+    an existing paragraph, will be interpreted as a inline markdown.
+
+*/
+
+var UndularitySituation = function (spec) {
+  undum.Situation.call(this, spec);
+
+  // Add all properties of the spec to the object, indiscriminately.
+  Object.keys(spec).forEach( key => {
+    if (this[key] === undefined) {
+      this[key] = spec[key];
+    }
+  });
+
+  this.visited = 0;
+
+};
+
+UndularitySituation.inherits(undum.Situation);
+
+/*
+  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).
+
+  Undularity's version of enter is set up to fulfill most use cases.
+*/
+
+UndularitySituation.prototype.enter = function (character, system, f) {
+
+  this.visited++;
+
+  if (this.before) this.before(character, system, f);
+
+  if (this.content) {
+    system.write(
+      markdown.render(
+        this.content.fcall(this, character, system, f).normaliseTabs()));
+  }
+
+  if (this.after) this.after(character, system, f);
+
+  if (this.choices) {
+    let choices = system.getSituationIdChoices(this.choices,
+      this.minChoices, this.maxChoices);
+    system.writeChoices(choices);
+  }
+};
+
+/*
+  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.
+
+  Undularity'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.
+*/
+
+UndularitySituation.prototype.act = function (character, system, action) {
+  var actionClass,
+      self = this;
+
+  var responses = {
+    writer: function (ref) {
+      if (self.writers[ref] === undefined) {
+        console.log(self);
+        throw new Error("Tried to call undefined writer:" + ref);
+      }
+      system.write(
+        markdown.render(
+          self.writers[ref].fcall(self, character, system, action)
+        ).fade());
+    },
+    replacer: function (ref) {
+      if (self.writers[ref] === undefined) {
+        throw new Error("Tried to call undefined replacer:" + ref);
+      }
+      system.replaceWith(
+        markdown.renderInline(
+          self.writers[ref].fcall(self, character, system, action)
+        ).spanWrap().fade(), `#${ref}`);
+    },
+    inserter: function (ref) {
+      if (self.writers[ref] === undefined) {
+        throw new Error("Tried to call undefined inserter:" + ref);
+      }
+      system.writeInto(
+        markdown.renderInline(
+          self.writers[ref].fcall(self, character, system, action)
+          ).spanWrap().fade(), `#${ref}`);
+    }
+  };
+
+  if (actionClass = action.match(/^_(\w+)_(.+)$/)) {
+    responses[actionClass[1]](actionClass[2]);
+  } else if (self.actions.hasOwnProperty(action)) {
+    self.actions[action].call(self, character, system, action);
+  } else {
+    throw new Err(`Action "${action}" attempted with no corresponding` +
+      'action in current situation.');
+  }
+
+};
+
+/* Element Helpers */
+/*
+  While you can write HTML elements by hand, those helpers make it easier to
+  place anchors (especially with special purpose) and spans.
+
+  They define a monadic interface:
+
+  a.id('my-link').content('link').writer('my-ref')
+    -> link
+
+  span -> 
+
+  The object supplies a toString() method that outputs the tag. Element
+  helpers are immutable, so theoretically this should be safe from side
+  effects. This interface allows the safe definition of templates which
+  can be used and modified at will, for instance:
+
+  let mySpanClass = span.class('myclass'); // -> 
+  mySpanClass.content("Hello!"); // -> Hello!
+
+  Methods on an elementHelper return a new elementHelper that inherits from
+  itself. Since those objects (should be) immutable by being frozen when they
+  are created, this is semantically equivalent to returning a copy but more
+  efficient in terms
+
+  Methods:
+    class :: String -> elementHelper
+      Returns a new elementHelper with the given class added.
+
+    classes :: [String] -> elementHelper
+      Returns a new elementHelper with the given classes.
+
+    id :: String -> elementHelper
+      Returns a new elementHelper with the given id.
+
+
+*/
+
+
+var elementHelper = function (element) {
+  this.element = element;
+  this._classes = [];
+};
+
+var elementSetterGen = function (prop) {
+  return function (value) {
+    return Object.freeze(Object.create(this, {
+      [prop]: {value}
+    }));
+  };
+};
+
+elementHelper.prototype.classes = function (newClasses) {
+  return Object.freeze(Object.create(this, {
+    _classes: {
+      value: newClasses
+    }
+  }));
+};
+
+elementHelper.prototype.class = function (newClass) {
+  return this.classes(this._classes.concat(newClass));
+};
+
+elementHelper.prototype.id = elementSetterGen("_id");
+elementHelper.prototype.type = elementSetterGen("_linkType");
+elementHelper.prototype.content = elementSetterGen("_content");
+elementHelper.prototype.ref = elementSetterGen("_ref");
+elementHelper.prototype.url = elementHelper.prototype.ref;
+elementHelper.prototype.situation = elementHelper.prototype.ref;
+elementHelper.prototype.once = () => this.class('once');
+
+var linkTypeGen = function (type) {
+  return function (ref) {
+    return this.type(type).ref(ref);
+  }
+};
+
+elementHelper.prototype.writer = linkTypeGen("writer");
+elementHelper.prototype.replacer = linkTypeGen("replacer");
+elementHelper.prototype.inserter = linkTypeGen("inserter");
+elementHelper.prototype.action = linkTypeGen("action");
+
+elementHelper.prototype.toString = function () {
+  var classes = "",
+      classString = "",
+      idString = "",
+      hrefString= "",
+      contentString = "";
+
+  if (this._classes) {
+    classes += this._classes.join(' ');
+  }
+  if (this._linkType) {
+    classes += (' ' + this._linkType);
+  }
+  if (classes) {
+    classString = ` class="${classes}"`;
+  }
+
+  if (this._id) {
+    idString = ` id="${this._id}"`;
+  }
+
+  if (this.element === "a") {
+    if (this._linkType) {
+      if (this._linkType === "action") {
+        hrefString = ` href="./${this._ref}"`;
+      } else {
+        hrefString = ` href="./_${this._linkType}_${this._ref}"`;
+      }
+    } else {
+      hrefString = ` href="${this._ref}"`;
+    }
+  }
+
+  if (this._content) {
+    contentString = markdown.renderInline(this._content);
+  }
+
+  return `<${this.element}${classString}${idString}${hrefString}>${contentString}`;
+};
+
+var a_proto = Object.freeze(new elementHelper("a"));
+var span_proto = Object.freeze(new elementHelper("span"));
+
+var a = function (content) {
+  if (content) return a_proto.content(content);
+  return a_proto;
+};
+
+var span = function (content) {
+  if (content) return span_proto.content(content);
+  return span_proto;
+};
+
+/*
+  Quality definition function
+
+  Meant to be called only once in the main story source file, this definition
+  is passed a spec to define qualities. The spec is an object containing quality
+  groups as objects, which contain qualities that themselves hold definitions.
+*/
+
+var qualities = function (spec) {
+  Object.keys(spec).forEach(function(group) {
+    /* The special "name" and "options" properties are passed on. */
+    var groupName = (spec[group].name === undefined) ? null : spec[group].name;
+    var groupOpts = (spec[group].options === undefined) ? {} : spec[group].options;
+    undum.game.qualityGroups[group] = new undum.QualityGroup(groupName, groupOpts);
+    Object.keys(spec[group]).forEach(function(quality) {
+      if (quality === "name" || quality === "options") return;
+      undum.game.qualities[quality] = spec[group][quality](group);
+    });
+  });
+};
+
+var qualityShim = {
+  integer: "IntegerQuality",
+  nonZeroInteger: "NonZeroIntegerQuality",
+  numeric: "NumericQuality",
+  fudgeAdjectives: "FudgeAdjectivesQuality",
+  onOff: "OnOffQuality",
+  yesNo: "YesNoQuality"
+};
+
+Object.keys(qualityShim).forEach(function (key) {
+  qualities[key] = function (title, spec={}) {
+    return function (group) {
+      spec.group = group;
+      return new undum[qualityShim[key]](title, spec);
+    };
+  };
+});
+
+/*
+  WordScaleQuality has a different interface (naughty!) so it has to be
+  defined by hand.
+*/
+
+qualities.wordScale = function (title, words, spec={}) {
+  return function (group) {
+    spec.group = group;
+    return new undum.WordScaleQuality(title, words, spec);
+  };
+};
+
+module.exports = function (name, spec) {
+  spec.name = name;
+  undum.game.situations[name] = new UndularitySituation(spec);
+};
+
+module.exports.a = a;
+module.exports.span = span;
+
+module.exports.qualities = qualities;
\ No newline at end of file
diff --git a/lib/tools.js b/lib/tools.js
new file mode 100644
index 0000000..b8c5b37
--- /dev/null
+++ b/lib/tools.js
@@ -0,0 +1,130 @@
+/*
+  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 */
+
+/* Array.prototype.shuffle() */
+
+/*
+  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 = function (system) {
+  var rng = (system) ? system.rnd.random : Math.random;
+  // slice() clones the array. Object members are copied by reference,
+  // beware.
+  var newArr = this.slice();
+  var m = newArr.length;
+
+  while (m) {
+    let i = Math.floor(rng() * m --);
+    let 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.
+*/
+
+var oneOf = function (ary) {
+
+  return {
+    inOrder () {
+      var i = 0;
+      return function () {
+        return ary[i++];
+      };
+    },
+
+    cycling () {
+      var i = 0;
+      return function () {
+        if (i >= ary.length) i = 0;
+        return ary[i++];
+      };
+    },
+
+    stopping () {
+      var i = 0;
+      return function () {
+        if (i >= ary.length) i = ary.length - 1;
+        return ary[i++];
+      }
+    },
+
+    randomly (system) {
+      var rng = (system) ? system.random : Math.random,
+          last;
+      return function () {
+        var i;
+
+        do {
+          i = Math.floor(rng() * ary.length);
+        } while (i === last);
+
+        last = i;
+        return ary[i];
+      };
+    },
+
+    trulyAtRandom (system) {
+      var rng = (system) ? system.random : Math.random;
+      return function () {
+        return ary[Math.floor(rng() * ary.length)];
+      };
+    },
+
+    inRandomOrder (system) {
+      var shuffled = ary.shuffle(system),
+          i = 0;
+      return function () {
+        return shuffled[i++];
+      };
+    }
+  };
+};
+
+exports.oneOf = oneOf;
\ No newline at end of file
diff --git a/node_modules/undularity b/node_modules/undularity
new file mode 120000
index 0000000..7951405
--- /dev/null
+++ b/node_modules/undularity
@@ -0,0 +1 @@
+lib
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..6578dd2
--- /dev/null
+++ b/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "undularity",
+ "version": "0.1.0",
+ "description": "A friendly API framework for building hypertext interactive fiction with Undum",
+ "license": "MIT",
+ "author": { "name": "Bruno Dias" },
+ "files": ["lib"],
+ "dependencies": {
+    "jquery": "~2.1.3",
+    "markdown-it": "~4.1.0",
+    "undum": "git://github.com/sequitur/undum.git#commonjs"
+  },
+  "devDependencies": {
+    "gulp": "~3.8.11",
+    "browserify": "~9.0.8",
+    "babelify": "~6.0.2",
+    "vinyl-source-stream": "~1.1.0",
+    "gulp-less": "~3.0.2"
+  }
+}
\ No newline at end of file