text: /**
   * A script that checks the interpolation of a token's movement to detect
   * whether they have passed through a square containing a trap.
   *
   * A trap can be any token on the GM layer for which the cobweb status is
   * active. Flying tokens (ones with the fluffy-wing status or angel-outfit
   * status active) will not set off traps unless the traps are also flying.
   *
   * This script works best for square traps equal or less than 2x2 squares or
   * circular traps of any size.
   */
  var ItsATrap = (function() {
  
    /**
     * A message describing the chat message and other special effects for a trap
     * being set off.
     * @typedef {object} TrapEffect
     * @property {string} message
     *           The message template that will be sent in the chat by Admiral Ackbar.
     *           This can include inline rolls and API chat commands.
     * @property {string} trapId
     *           The ID of the trap.
     * @property {string} victimId
     *           The ID of the token that activated the trap.
     */
  
    /**
     * Returns the first trap a token collided with during its last movement.
     * If it didn't collide with any traps, return false.
     * @param {Graphic} token
     * @return {Graphic || false}
     */
    var getTrapCollision = function(token) {
      var pageId = token.get('_pageid');
      var traps = findObjs({
        _pageid: pageId,
        _type: "graphic",
        status_cobweb: true,
        layer: "gmlayer"
      });
  
      // Some traps don't affect flying tokens.
      traps = _.filter(traps, function(trap) {
        return !isTokenFlying(token) || isTokenFlying(trap);
      });
      return TokenCollisions.getFirstCollision(token, traps);
    };
  
    /**
     * Gets the effect for a trap set off by a character's token defined in the
     * trap's GM notes.
     * If the GM notes property is not set, then it will generate a default
     * message using the trap and victim's names.
     * @param  {Graphic} victim
     *         The token that set off the trap.
     * @param  {Graphic} trap
     * @return {TrapEffect}
     */
    function getTrapEffect(victim, trap) {
      var effect;
  
      // URI-escape the notes and remove the HTML elements.
      var notes = decodeURIComponent(trap.get('gmnotes')).trim();
      notes = notes.split(/<[/]?.+?>/g).join('');
  
      // If GM notes are set, interpret those.
      if(notes) {
  
        // Should the message be interpretted as a JSON object?
        if(notes.indexOf('{') === 0)
          try {
            effect = JSON.parse(notes);
          }
          catch(err) {
            effect = {
              message: 'ERROR: invalid TrapEffect JSON.'
            };
          }
        else
          effect = {
            message: notes
          };
      }
  
      // Use a default message.
      else {
        var trapName = trap.get("name");
        if(trapName)
          effect = {
            message: victim.get("name") + " set off a trap: " + trapName + "!"
          };
        else
          effect = {
            message: victim.get("name") + " set off a trap!"
          };
      }
  
      // Capture the token and victim's IDs in the effect.
      _.extend(effect, {
        trapId: trap.get('_id'),
        victimId: victim.get('_id')
      });
      return effect;
    }
  
    /**
     * Gets the message template sent to the chat by a trap.
     * @param  {Graphic} victim
     *         The token that set off the trap.
     * @param  {Graphic} trap
     * @return {string}
     */
    function getTrapMessage(victim, trap) {
      var notes = unescape(trap.get('gmnotes')).trim();
      if(notes) {
  
        // Should the message be interpretted as a JSON object?
        if(notes.indexOf('{') === 0)
          return JSON.parse(notes).message;
        else
          return notes;
      }
  
      // Use a default message.
      else {
        var trapName = trap.get("name");
        if(trapName)
          return victim.get("name") + " set off a trap: " + trapName + "!";
        else
          return victim.get("name") + " set off a trap!";
      }
    }
  
  
    /**
     * Determines whether a token is currently flying.
     * @param {Graphic} token
     * @return {Boolean}
     */
    var isTokenFlying = function(token) {
      return token.get("status_fluffy-wing") || token.get("status_angel-outfit");
    };
  
  
    /**
     * Moves the specified token to the same position as the trap.
     * @param {Graphic} token
     * @param {Graphic} trap
     */
    var moveTokenToTrap = function(token, trap) {
      var x = trap.get("left");
      var y = trap.get("top");
  
      token.set("lastmove","");
      token.set("left", x);
      token.set("top", y);
    };
  
  
    /**
     * When a graphic on the objects layer moves, run the script to see if it
     * passed through any traps.
     */
    on("change:graphic", function(victim) {
      // Objects on the GM layer don't set off traps.
      if(victim.get("layer") === "objects") {
        var trap = getTrapCollision(victim);
        if(trap) {
          var effect = getTrapEffect(victim, trap);
          var msg = effect.message;
  
          if(msg.indexOf('!') !== 0)
            msg = "IT'S A TRAP!!! " + msg;
          sendChat("Admiral Ackbar", msg);
          moveTokenToTrap(victim, trap);
  
          // Reveal the trap if it's set to become visible.
          if(trap.get("status_bleeding-eye")) {
            trap.set("layer","objects");
            toBack(trap);
          }
        }
      }
    });
  
    return {
      getTrapCollision: getTrapCollision,
      getTrapEffect: getTrapEffect,
      getTrapMessage: getTrapMessage,
      isTokenFlying: isTokenFlying,
      moveTokenToTrap: moveTokenToTrap
    }
  })();
  