Pre-release
AdventureJS Docs Downloads Bundler Devlog Score: 0 Moves: 0
Example game featuring the Cloak of Darkness. cloak of darkness CloakOfDarkness, player, cloak, foyer, cloakroom, bar, introcard, outrocard styles

Examples:Cloak of Darkness

This is an implementation of Cloak of Darkness for AdventureJS. Cloak of Darkness is a sort of "hello world" for interactive fiction systems; a simple game that demonstrates how to apply common authoring techniques in each system.

 

// CloakOfDarkness.js
/* global AdventureJS A CloakOfDarkness C */

// Here's how we create a new game in AdventureJS.
// We're naming the game object CloakOfDarkness
// and also creating shortcut C for convenience.
// https://adventurejs.com/doc/GettingStarted_CreateAGame.html
var CloakOfDarkness,
  G = new AdventureJS.Game("CloakOfDarkness", "CloakOfDarknessContainer");

// A game's setting object contains numerous options for
// controlling the game's behavior.
// https://adventurejs.com/doc/AdventureJS.Settings.html
G.settings.set({
  title: "Cloak of Darkness",
  author: "Ivan Cockrum",
  description: "You may lose your mind wearing... the Cloak of Darkness!",
  version: "0.0.1",

  // Debug options print directly to the game display.
  // https://adventurejs.com/doc/Debugging_InGame.html
  debug: {
    //   general: true,
    //   verbaction: true,
    //   verbreaction: true,
    //   verbphase: true,
    // travel: true,
  },

  // Log options log to the web browser's inspector window.
  // https://adventurejs.com/doc/Debugging_InConsole.html
  log: {
    // verbaction: true,
    // verbreaction: true,
    // verbphase: true,
    // travel: true,
    // game: true,
    // verb: true,
    // parser: true,
    // entity: true,
    all: true,
  },
});

// We're going to use several world variables.
// These are vars that get written with the game state
// so they remain accurate through undo/save/restore.
// https://adventurejs.com/doc/BasicScripting_WorldVariables.html

// went_north tracks how many times the player
// has tried to go north from the foyer
G.setVar("went_north", 0);

// disturbed_sawdust tracks how many turns the player
// has spendt in the bar while it's dark
G.setVar("disturbed_sawdust", 0);

// max_disturbance is the maximum number of turns the player
// can spend in the dark bar before we consider it a loss
G.setVar("max_disturbance", 3);
// Player.js
/* global AdventureJS A G */

var player = G.createAsset({
  class: "Player",
  place: { in: "Foyer" },
  name: "Opera Fan",

  description: function (e) {
    let msg = `You cut rather a dashing figure, if you do say so yourself, `;
    if (cloak.is.worn) {
      msg += `wrapped in your celestially black cloak. `;
    } else {
      msg += `even without your amazing monocolor dreamcoat. `;
    }
    return msg;
  },

  // if the player tries to go north from the foyer,
  // we want to present a series of snarky responses
  tryTravel: {
    foyer_north: function (action) {
      const count = G.incVar("went_north");
      let msg = "";
      switch (count) {
        case 1:
          msg = `You can't go that way, as the door appears to be nothing more than a decal. There now, aren't you dissatisfied? We warned you not to try it. But did you listen? No. `;
          break;
        case 2:
          msg = `It's really quite certain that you can't go that way. The north door is clearly just a poorly painted stage backdrop. It's really not worth continuing to try. `;
          break;
        case 3:
          msg = `Well all righty then. You wind yourself up and dash forward in a full tilt run, like Wile E. Coyote throwing yourself into pursuit of the Roadrunner. And, much like Wile E., you slam face first into the painted-on north exit, before sliding down into a flattened puddle on the floor. `;
          break;
        case 4:
          msg = `Once more for the gold, then. You steel yourself. Close your eyes. Lean in. Fly forward. And... you go! Where you should have slammed into the painted door frame at bone breaking speed, instead you sail through and onward, into a new and brilliant life of unending excitement and deep satisfaction. You open your eyes to find yourself laid out on the floor of the Opera House foyer. You leap to your feet and performatively dust yourself off before remembering that you are the only character present. `;
          break;
        default:
          msg = `Enough with the north door already. `;
          break;
      }
      this.game.print(msg);
      return false;
    },
  },

  // To handle the warning message about disturbing things,
  // we're going to set up an interval when the player
  // enters the bar while carrying the cloak.
  // https://adventurejs.com/doc/BasicScripting_Intervals.html
  doMoveThisToThat: {
    bar: function (action) {
      if (this.has("cloak")) {
        G.startInterval({
          // startInterval takes the name or id of an asset
          // "player" is an alias to the current player character's ID
          id: "player",
          // callback must be the name of a function attached
          // to the specified asset
          // We do it this way so that intervals are subject to
          // undo/save/restore
          callback: "disturbSawdust",
        });
      }
    },
  },

  // Clear the interval when the player leaves the bar.
  doMoveThisFromThat: {
    bar: function (action) {
      G.stopInterval({
        id: "player",
        callback: "disturbSawdust",
      });
    },
  },

  // disturbSawdust is our interval function
  disturbSawdust: function (e) {
    if (G.getVar("disturbed_sawdust") > 0) {
      let msg = `You'd best not delve too deeply (or hang around in the dark too long) as you may disturb things in the dark. And/or get nibbled by a gnu. `;
      // intervals can return text to be printed at the end of the turn
      // though they may alternately include their own print statements.
      return msg;
    }
    G.incVar("disturbed_sawdust");
  },
});
// Cloak.js
/* global AdventureJS A G */

var cloak = G.createAsset({
  class: "Clothing",
  place: { in: "Opera Fan" },
  is: {
    // worn puts it on the player character
    worn: true,
    // setting cloak.is.listed: false suppresses the cloak's
    // appearance in asset lists. We don't need it
    // listed because it can only exist in two places
    // and it's always explicitly referenced in its
    // container's description.
    listed: false,
  },
  name: "cloak",
  full_name: "black velvet cloak",
  synonyms: ["coat", "cloat"],
  description: `This velvet cloak is so immaculately black that it almost appears to absorb light. `,
  emits: {
    light: function () {
      return this.roomIs("Bar") ? -1 : 0;
    },
  },

  // We want the player to be able to hang the cloak
  // on the hook. If player inputs "hang cloak on hook"
  // no problem, but if they just input "hang up cloak"
  // then we need to help the parser find the indirect
  // object by explicitly setting the relationship here.
  dov: { hang: { with_assets: ["small brass hook"] } },
  // this could alternately be expressed as
  // cloak.dov.hang.with_assets = ["small brass hook"];

  // We want to ensure that the player can't drop the cloak
  // in the bar, thus losing it. There are several ways to
  // catch that event. We could tie it directly to the verb
  // drop, but then we might not catch throw, put, etc, so
  // we're going to use a verb reaction hook,
  // which will catch the event regardless of what verb the
  // player uses to discard the cloak. We're using
  // tryMoveThisFromThat as opposed to doMoveThisFromThat
  // because tryMoveThisFromThat fires before success and
  // doMoveThisFromThat fires after success.
  tryMoveThisFromThat: {
    "Opera Fan": function () {
      if (G.roomIs("Bar")) {
        let msg = `Better not. You might not find it again in the dark. Or worse, it might be nibbled by a gnu. `;
        G.print(msg);
        return false;
      }
    },

    // If the player tries to take the cloak from the hook,
    // let's deny them, just to keep things simple.
    "small brass hook": function () {
      let msg = "";
      msg += `Though it pains you to witness the painstakingly handcrafted cloak hung in such paltry surroundings, just for the sake of sticking to the game specification, let's say we leave it there for now. `;
      G.print(msg);
      return false;
    },
  },

  // We'll use another verb reaction hook to catch
  // the player putting the cloak on the small brass hook.
  doMoveThisToThat: {
    "small brass hook": function () {
      let msg = "";
      if (this.is.worn) {
        msg += `You sweep the tailored cloak from your shoulders with a dramatic flourish before reluctantly hanging it on the cheap imitation brass hook. `;
      } else {
        msg += `You reluctantly hang the immaculately tailored cloak on the cheap imitation brass hook. `;
      }
      msg += `The cloak drapes over the hook and hangs, balefully, as if it knows that it's too good for its surroundings. `;

      // In other blocks we're using
      //   G.print(msg);
      //   return false;
      // The difference between that and
      //   G.overrideOutput(msg)
      // is that the former breaks the verb action
      // and prints directly, while the latter allows
      // the verb to complete and overrides the output.
      G.overrideOutput(msg);
    },
  },

  // we want to prevent the player from removing the cloak while in the bar
  tryRemove: {
    "Opera Fan": function (action) {
      if (G.roomIs("Bar")) {
        let msg = `Better not. You might not find it again in the dark. Or worse, it might be nibbled by a gnu. `;
        G.print(msg);
        return false;
      }
    },
  },

  // Just for fun, we'll also use a verb action to offer
  // a custom response if the player just removes the cloak.
  doRemove: {
    "Opera Fan": function (action) {
      let msg = `You sweep the tailored cloak from your shoulders with a dramatic flourish, and carefully drape it over one arm, fastidiously protecting it from touching any of the unfinished surfaces in this place. `;
      G.overrideOutput(msg);
    },
  },

  // Wnd when the player wears the cloak.
  doWear: {
    "Opera Fan": function (action) {
      let msg = `You snap the cloak crisply over your shoulders and fix it in place. Though the fabric is quite heavy, wearing it makes you feel cooler. `;
      G.overrideOutput(msg);
    },
  },

  tryDrop: function (action) {
    // if player drops cloak in cloakroom,
    // we're going to hijack "drop" and forward to "hang"
    if (G.roomIs("Cloakroom")) {
      let msg = `Drop it? "Drop" it? You'll neatly HANG it like a civilized fashionista. `;
      G.print(msg);
      G.dictionary.doVerb("hang");
      return false;
    }

    // otherwise, NEVER!
    let msg = `What? On the filthy unscrubbed floor? `;
    if (G.roomIs("Bar")) {
      msg += `In the dark, where a gnu might nibble it? `;
    } else {
      msg += `Like an uncultured savage? Like some cinema patron? `;
    }
    msg += `No, you'll fastidiously fold and drape it when the time comes. `;
    G.print(msg);
    return false;
  },

  tryLick: function (action) {
    let msg = `While the idea is intriguing – the inky black cloak is almost scintillating in the way it captures the light – you'd rather not get cloak fluff on your tongue. `;
    G.overrideOutput(msg);
  },
});
// foyer.js
/* global AdventureJS A G */

G.createAsset({
  class: "Room",
  name: "Foyer",
  full_name: "Foyer of Ye Olde Elegant Opera House",
  definite_article: "the",
  descriptions: {
    look: `
    Contrary to its name, Ye Olde Elegant Opera House seems to have been built of sticks, paste, and burlap. The foyer is oddly devoid of Non Player Characters. Perhaps even NPCs know to avoid this rickety hall. But, according to the dodgy specifications document, this is where The Game Begins. Doors lead to the south and west. For unspecified reasons. there is also an unusable exit to the north. Don't even try to go that way, because you'll only be dissatisfied with the results.
  `,

    brief: `This is the foyer of Ye Olde Elegant Opera House. Doors lead to the south and west, while a door to the north appears to be merely painted on. `,

    listen: `You would expect to hear the murmur of an audience, and instruments tuning up, but instead it's oddly silent here, almost as if there was nothing beyond this empty foyer. `,

    // We're dynamically updating the foyer's north door
    // and we want to append a brief description to the
    // end of the foyer's list of exits.
    append_exits: function () {
      const count = G.getVar("went_north");
      switch (count) {
        case 0:
          return `You <em>can't</em> go north. `;
        case 1:
          return `You <em>really</em> can't go north. `;
        case 2:
          return `You really, <em>really</em> can't go north. `;
        default:
          return `You can't go north. `;
      }
    },
  },
  exits: {
    south: "Bar",
    west: "Cloakroom",
    // in player.js, we're using tryTravel with
    // a "went_north" variable to change up the
    // travel description, and we want to revise
    // the visual description to match
    north: function () {
      const count = G.getVar("went_north");
      switch (count) {
        case 0:
          return `
          The north door appears to be nothing more than a painting of a door. `;
        case 1:
          return `
          The north door is clearly just a poorly painted stage backdrop. `;
        case 2:
          return `          
          The  painted-on north exit looks like the sort of ersatz tunnel that Wile E. Coyote would paint onto a cliff face to brain the Roadrunner. `;
        default:
          return `
          You've seen as much of the faux door as you care to. `;
      }
    },
  },
});
// cloakroom.js
/* global AdventureJS A G */

G.createAsset({
  class: "Room",
  name: "Cloakroom",
  full_name: "Empty Cloakroom",
  definite_article: "the",
  descriptions: {
    look: function () {
      if (G.$("cloak").isOn(G.$("small brass hook"))) {
        return `Your black velvet cloak is hung from a small brass hook on the wall, sucking light from the room with a subdued malevolence. `;
      } else {
        return `This cloakroom is disturbingly bare, lacking both a coat check NPC and racks and hangars. Fixed to one peeling papered wall is a single, small brass hook. Apparently the cloakroom can only accommodate one cloak per show. No wonder theater is dying. `;
      }
    },
    listen: function () {
      if (G.$("cloak").isOn(G.$("small brass hook"))) {
        return `You imagine that you hear a slight rustling. Is that your cloak, shifting uncomfortably on the hook? `;
      } else {
        return `Can you hear a faint jingle of bells muffled by freshly fallen snow? No wait, that's a wardrobe, not a cloakroom.  `;
      }
    },
  },
  exits: {
    east: "Foyer",
  },
});

var coathook = G.createAsset({
  class: "Coathook",
  name: "small brass hook",
  place: { attached: "Cloakroom" },
  aspects: {
    on: {
      accepts_assets: ["cloak"],
    },
  },

  description: function () {
    if (G.$("cloak").isIn(G.$("small brass hook"))) {
      return `The small brass hook is largely obscured by your black velvet cloak hanging from it. `;
    } else {
      return `On closer examination, the "brass" of the brass hook appears to be pressed on foil, flaking in places to reveal rusting cast iron beneath. `;
    }
  },

  tryLick: function (action) {
    let msg = `The filthy foil pressed iron hook that has borne generations of common anoraks? You would rather lick a gnu. `;
    G.overrideOutput(msg);
  },
});
// bar.js
/* global AdventureJS A G */

G.createAsset({
  class: "Room",
  name: "Bar",
  definite_article: "the",
  full_name: "Ye Olde Elegant Bar & Grill",
  descriptions: {
    look: function () {
      let msg = `This alleged "bar" looks like it was hammered together using salvaged wood only minutes before you entered. There are no customers, no liquors, no tables, and in fact, no bar, unless you consider the sheet of scarred plywood propped up on two DIY sawhorses to be a bar. The floor is covered with a thick layer of sawdust. It appears that a message has been roughly scratched into the sawdust. `;
      return msg;
    },
    dark: `It is pitch black – as black as your cloak, in fact. You are likely to be nibbled by a gnu. (Apparently this clapboard opera house is too low-rent even for grues.)`,
    listen: function () {
      // We ask if room.isDark() rather than room.is.dark
      // because room.is.dark is just the room's setting,
      // while isDark() takes light sources into account.
      if (this.isDark()) {
        return `All you hear is the blood rushing through your veins. Or is that the ocean? Is your ear like a conch? If you took one off and put it against the other ear, would you hear the ocean? `;
      } else {
        return `It's startlingly quiet. No sounds of milling audience, no instruments tuning up in the pit. Is there even any opera in this opera house? `;
      }
    },
  },
  exits: {
    north: "Foyer",
  },
});

G.createAsset({
  class: "Entity",
  name: "sawdust",
  synonyms: ["message", "writing", "scratch", "scratches"],
  place: { in: "bar" },
  // The sawdust is mentioned in the room description
  // so we don't need it listed in the room.
  is: { listed: false },

  // There are several ways to achieve the endgame
  // that's caused by looking at the sawdust.
  //
  // In AdventureJS, descriptions can be strings or arrays or functions.
  // https://adventurejs.com/doc/AdvancedDescriptions_Descriptions.html
  // One method is to set the description to a method
  // that in turn calls the outrocard.
  // The benefit of this is that it works for any verb
  // that results in the description being shown.
  // The Cloak of Darkness specs say:
  // The message reads either "You have won" or "You have lost",
  // depending on how much it was disturbed by the player while the room was dark.
  descriptions: {
    look: function () {
      let max = G.getVar("max_disturbance");
      let disturbed = G.getVar("disturbed_sawdust");
      let msg = disturbed > max ? "You have lost." : "You have won.";
      G.outrocard.show(msg);
    },
    // When using the verb read, verb logic looks for
    // asset.descriptions.read, so we'll redirect that to look.
    read: function () {
      this.descriptions.look();
    },
    taste: function () {
      G.overrideOutput(
        `Against your better judgment – almost as if you were being puppeted by some unseen force – you kneel upon the floor – careful not to disturb the scratched message – and lower your tongue to the wood shavings. Definitely pine. As you would expect in such an impoverished establishment. `
      );
    },
  },

  // A second method is to use verb action hooks.
  // https://adventurejs.com/doc/BasicVerbs_ActionHooks.html
  // Verb actions get extremely granular and
  // doLook is the least granular version of this.
  //
  // doLook: function () {
  //   let msg = `The message reads either "You have won" or "You have lost", depending on how much it was disturbed by the player while the room was dark.`;
  //   G.outrocard.show(msg);
  // },
  // doRead: function () {
  //   this.doLook();
  // },

  // A third method is to use verb phase hooks.
  // https://adventurejs.com/doc/BasicVerbs_PhaseHooks.html
  // These offer a way to completely override a verb.
  //
  // dov: {
  //   look: {
  //     on_success: function () {
  //       let msg = `The message reads either "You have won" or "You have lost", depending on how much it was disturbed by the player while the room was dark.`;
  //       G.outrocard.show(msg);
  //     },
  //   },
  //   read: {
  //     on_success: function () {
  //       this.dov.look.on_success();
  //     },
  //   },
  // },

  // The sawdust is a generic Entity asset that isn't subscribed
  // to the verb read, so we need to enable that.
  // https://adventurejs.com/doc/BasicVerbs_Subscriptions.html
  dov: { read: true },
});

// It is pitch black. You are likely to be eaten by a grue.
// Introcard.js
/* global AdventureJS A G */

// Introcards can be used to set in introductory or title screen.
// https://adventurejs.com/doc/IntroOutro_Introcards.html
G.introcard.set({
  // text can be plain text or HTML.
  text: `
    <p class="fx-glitch cloak-title" data-text="{title}">{title}</p>
    <p>Version {version}</p>
    <p>By {author}</p>
    <br/>
    <p>Developed with <br/><a href="https://adventurejs.com" class="intro-link" target="_blank">AdventureJS</a></p>
    <p>A full-featured, open-source, interactive fiction engine 
    <br/>made in vanilla javascript, by humans for humans.</p>
  `,
  foo: `
    <div class="ajs-fullsize ajs-vertical-center">
      <p class="fx-glitch cloak-title" data-text="{title}">{title}</p>
      <p>Version {version}</p>
      <p>By {author}</p>
      <br/>
      <p>Developed with <br/><a href="https://adventurejs.com" class="intro-link" target="_blank">AdventureJS</a></p>
      <p>A full-featured, open-source, interactive fiction engine 
      <br/>made in vanilla javascript, by humans for humans.</p>
    </div>
  `,

  // A variety of options control the behavior of the introcard.
  hide_statusbar: true,
  show_statusbar_on_continue: true,
  // hide_titlebar: true,
  // show_titlebar_on_continue: true,
  prompt_to_continue: true,
  prompt_text: `[Press any key to continue...]`,
  clear_on_continue: true,
});
// Outrocard.js
/* global AdventureJS A G */

// Outrocards can be used to show a final message
// at the end of the game.
// https://adventurejs.com/doc/IntroOutro_Outrocards.html
G.outrocard.set({
  // hide_statusbar: true,
  // hide_titlebar: true,
  // clear_on_show: true,
  // clear_on_continue: true,
  show_statusbar_on_continue: true,
  show_titlebar_on_continue: true,
  show_undo: true,
  show_restore: true,
  show_restart: true,
  show_quit: true,

  // text can be a string or function but we're
  // not using either because we're going to pass
  // the text in when we call the outrocard from
  // the sawdust description.
  // text: `* * * You have died. * * *`,
  // text: function (e) {
  //   return `OUTRO!`;
  // },
});

Demo Source files

Right-click and choose "Save as..." to download source files.

Notes:

  • Demo games are set to a small size to fit doc pages. They can easily be enlarged via display settings.
  • Demo games all use the same theme. Colors and fonts can be changed via custom CSS.
  • Demo games don't include the core AdventureJS code. To run downloaded games locally, you'll need to also download copies of the core JS and CSS and set up this local folder structure.
AdventureJS
└── js
|   └── adventure.min.js
└── css
|   └── adventurejs.min.css
└── games
    ├── demogame01
    ├── demogame02
    └── demogame03