StoryScript

4. Adventure gaming

Created on 17 January 2026. Updated on 18 January 2026.

In the early days of adventure gaming, classics such as King's Quest and Monkey Island defined the point & click adventure in which you wander the game world solving riddles by combining items, using them on characters or objects etc. Creating an adventure game is a good next step, as it requires the introduction of Items that the player can collect and use. It also requires the introduction of a new concept, Combinations, that StoryScript uses to create these kind of games. But we can cover both in one tutorial. We'll also throw in Features to flesh out our locations a bit more while we're at it 😃.

Working with combinations will require you to get your feet wet with some TypeScript programming. I’ve tried to make the StoryScript programming interface (Application Programming Interface, or API) as simple to use as possible. That said, all programming requires some getting used to. If you stick with it, though, it gives you a lot of power to create unique games.

For this tutorial, you can refer to the MyAdventureGame game in your StoryScript folder or the demo online. As in tutorial 1, create a new game, copy the GameContainer component and put in this template:

<template>
  <div class="container-fluid body-content">
    <div class="row">
      <div v-if="game.state === 'Play'" id="party-container" :class="{ 'col-4': showCharacterPane }">
        <div v-for="character of game.party.characters">
          <backpack :character="character"></backpack>
        </div>
      </div>
      <div id="location-container"
           :class="{ 'col-8': game.state === 'Play' && showCharacterPane, 'col-12': game.state !== 'Play' || !showCharacterPane }">
        <div v-if="game.state === 'Play'">
          <location-text></location-text>
        </div>
      </div>
    </div>
    <div v-if="game.state === 'Play'" class="row">
      <div class="col-12">
        <combinations :combinations="game.combinations"></combinations>
      </div>
    </div>
  </div>
</template>

While you are at it, change the gameName in the customTexts.ts file to ‘My adventure game’. We have a nice clean UI to start with:

Now we can start defining our combinations. Defining what combinations should be available in the game is done in your rules.ts file in your game's folder. Let's define ‘Walk’, ‘Use’, ‘Touch’ and ‘Look at’ for this example. First, create a new combinations.ts file in your game's folder with these contents, so we define our combinations in one place:

export const enum Combinations {
    WALK = 'Walk',
    USE = 'Use',
    TOUCH = 'Touch',
    LOOKAT = 'Look'
}

We can use these definitions to create the combinations in our rules.ts file like this. You can use the ssCombinationAction snippet to add a new combination action:

getCombinationActions: (): ICombinationAction[] => {
	return [
		{
			text: Combinations.WALK,
			preposition: 'to',
			requiresTool: false
		},
		{
			text: Combinations.USE,
			preposition: 'on'
		},
		{
			text: Combinations.TOUCH,
			requiresTool: false
		},
		{
			text: Combinations.LOOKAT,
			preposition: 'at',
			requiresTool: false,
			failText: (ame: IGame, target: ICombinable, tool: ICombinable): string => { 
			    return 'You look at the ' + target.name + '. There is nothing special about it';
			}
		}
	];
}

When you paste in the code above, TypeScript will complain that it cannot find Combinations by showing it in red. You can fix these errors by hovering over such a highlighted text and using CONTROL + ‘.’ to add the required imports automatically. Make sure to select the right imports, those that are part of your game (in this case, './combinations.ts')!

There are a few things to note here:

  1. You can see that the combinations have an action text (e.g. 'Use', 'Look') and an optional preposition ('on' or 'at'). In StoryScript, these will be used to create combinations such as ‘Use pen on paper’ or ‘Look at gate’.
  2. As some combinations require two parts (we’ll call these parts the tool and the target, e.g. Use requires both but Look does not (of course, you could also use binoculars to get a better look, but you get the idea)) you should specify when one does not require a tool.
  3. You can specify a default text displayed when a combination is tried that doesn’t work in the customTexts.ts file. There are two templates you can override, one for combinations requiring a tool and one for those who don’t. The numbers between the curly brackets are placeholders, and will be replaced at runtime:
    • noCombination: "You {2} the {0} {3} the {1}. Nothing happens.". For example: “You use the pen on the paper”.
      • 0: The ‘tool’ for the combination
      • 1: The ‘target’ for the combination
      • 2: The combination name (Use, Look)
      • 3: The preposition (on, at)
    • noCombinationNoTarget: "You {1} {2} the {0}. Nothing happens.". For example: “You look at the gate”.
      • 0: The ‘target’ for the combination
      • 1: The combination name (Use, Look)
      • 2: The preposition (on, at)
  4. If you want something more specific, you can specify a fail text per combination, as shown above. You can be even more specific when you know what combination is tried on what object, which will be discussed in a bit.

With your combinations defined, they should show up in your browser:

For our combinations to do anything, we need tools and targets for them. These can be any of the following StoryScript entities:

  • Features
  • Items
  • Enemies and Persons
  • Barriers

In this part of the tutorial, we’ll focus on features and items. Enemies, persons and barriers will be discussed later.

In StoryScript, you can work with combinations in two ways, which you can mix if you want to. The first is text-based, using text descriptions for your locations and features. You can also go picture-based, which means you use one or more pictures to make your world come to life. We’ll start with a text example in this tutorial, and show how you can build the example using pictures in the next.

To create a location players can interact with, we’ll create some features. Features are noteworthy elements of a location that a player can interact with. Let’s start with a fountain as an example, one found in a forest clearing. Go to your Start.html and change the description element as shown below. Add the feature element in between the text. You can use the ssFeature snippet to do so more quickly:

<description>
    <p>
        You stand at the edge of a forest clearing. Tall trees border it on all sides,
        with thick undergrowth making the clearing hard to get to. In the center of the 
        clearing is <feature name="fountain">a fountain with a six-sided base</feature>.
    </p>
</description>

Second, open the Start.ts file and change it like this. To add a feature in a location, you can use the ssFature-inline snippet (the idea of features is that they are specific to one location so you can declare them inline, unlike e.g. items that are not (usually) bound to one location. You can also create stand-alone features if you want to use them somewhere else as well or to organize your code. That will be shown later):

export function Start() {
    return Location({
        name: 'Start',
        description: description,
        features: [
            {
                name: 'Fountain',
                combinations: {
                    combine: [
                        {
                            combinationType: Combinations.LOOKAT,
                            match: (game, target, tool): string => {
                                return 'You look at the fountain water. It is very clear and reflects the forest.';
                            }
                        }
                    ]
                }
            }
        ]
    });
}

Your browser should now show you the fountain feature. Press the Look combination button and then the fountain feature to see your match text displayed:

When instead of Look you use Touch, you should see the default fail text as you haven’t specified any combination for the fountain feature and Touch.

Ok, let’s make the Touch action do something as well. When the player walks towards the fountain and touches the water, he’ll hear a soft muttering coming from the undergrowth at the edge of the clearing. This should give him the opportunity to go and check out that spot. For this to work, we need to add a couple of things.

Start with adding a new location called Passage using the npm run scl passage command. Give it a name. This time, we’ll use a stand-alone feature to demonstrate how that works. Use the StoryScript Create Feature command to create an empty feature:

 npm run scf Corridor

Add the code below to the new Corridor feature, using the ssCombine snippet to add the combine entry:

export function Corridor() {
	return Feature({
		name: 'Corridor',
		description: 'A passage through the undergrowth',
		combinations: {
		    combine: [
		        {
		            combinationType: Combinations.WALK,
		            match: (game, target, tool): string => {
		                game.changeLocation(Passage);
		                return 'You crawl through the passage.';
		            }
		        },
		    ]
		}
	});
}

With the new location added, we can make the feature to travel to it available when the Touch combination on the fountain is triggered with a bit more code to the Start.ts file:

{
	combinationType: Combinations.LOOKAT,
	match: (game, target, tool): string => {
		return 'You look at the fountain water. It is very clear and reflects the forest.';
	}
},
{
	combinationType: Combinations.TOUCH,
	match: (game, target, tool): string => {
		if (!game.currentLocation.features.get(Corridor)) {
		    game.currentLocation.features.add(Corridor);
		    return `You walk towards the fountain and touch the fountain water.
			It is a little cold. When you pull back your hand, you hear a soft
			muttering. It is coming from a small passage in the undergrowth.`;
		}
		else {
		    return 'The fountain water is pleasant to the touch.';
		}
	}
}

Note how I used the backtick (‘`’) to break up the long text across multiple lines.

 

IMPORTANT NOTE: I’m adding the Corridor feature in code using the add function on the current location’s feature collection. ALWAYS use add to add StoryScript entities to collections, NEVER the Javascript push function!

 

Touching the fountain, the player now unlocks a new feature! In the code, I also made sure that nothing happens the second time you touch the water using the if/else block. If you touch the water when the Corridor feature is already there, you’ll just see a message displayed.

As you see, the fountain inline feature is becoming big, so in order to organize your code you can also make a stand-alone feature out of it. Do so and clean up te Start.ts file like this:

export function Start() {
    return Location({
        name: 'Start',
        description: description,
        features: [
            Fountain()
        ]
    });
}

With these simple interactions that do not use a tool in place, let’s create a combination with the Use command to show an example of a combination that does use a tool. We'll first create a new feature for the Passage location with a Look combination that will give the player an Item.

First, add a Flask item using the StoryScript Create Item command:

npm run sci flask

You can see that as for locations, items come with an html file as well, flask.html in this case. You can put a description for the item in there. When a description is present, a View button will show in the Backpack component that shows the description to the player.

 


A NOTE ON DESCRIPTIONS

using HTML files to describe your items (and later enemies) gives you much more formatting flexibility for your description text. However, it may be more than you need. You can opt not to use descriptions for items by running the npm run sci flask command
with the additional ‘p’ argument, so npm run sci flask p. This will generate just a .ts file without the additional html file. You can now omit the description completely or set the property with some plain text:

export function Flask() {
    return Item({
        name: 'Flask',
        description: 'A simple flask',
        equipmentType: EquipmentType.Miscellaneous
    });
}

 

Now we have a flask item in the game that we can use. Modify the Passage.html file like this:

<description>
    <p>
        The small tunnel in the undergrowth is hard to get through. On the other end
        the trees are close together, blocking most of the sunlight.
        <feature name="woundedwarrior">A gravely injured warrior lies on the ground here.</feature>
    </p>
</description>

Then change the Passage.ts location file like this:

export function Passage() {
    return Location({
        name: 'A passage in the undergrowth',
        description: description,
        features: [{
            name: 'Wounded warrior',
            combinations: {
                failText: 'That won\'t help him.',
                combine: [
                    {
                        combinationType: Combinations.LOOKAT,
                        match: (game: IGame, target, tool): string => {
                            if (!game.worldProperties.takenFlask) {
                                game.worldProperties.takenFlask = true;
                                game.activeCharacter.items.add(Flask);
                                return `Looking at the warrior, you see a flask on his belt.
                                        carefully, you remove it.`;
                            }
                            else {
                                return 'You see nothing else that might help.';
                            }
                        }
                    }
                ]
            }
        }]
    });
}

Go to the Passage location and look at the warrior. You should now receive a flask:

Great, we now have an item to use. We’ll let the player fill the flask with fountain water to give to the warrior. Add a new water item for the fountain water and call it Fountain water. Then, add a new combination to the fountain.ts feature file:

{
    combinationType: Combinations.USE,
    tool: Flask,
    match: (game, target, tool): string => {
        const flask = game.activeCharacter.items.get(Flask);

        if (flask) {
            game.activeCharacter.items.delete(flask);
            game.activeCharacter.items.add(Water);
            return `You fill the flask with the clear fountain water.`;
        }
        else {
            return 'The fountain water is pleasant to the touch.';
        }
    }
}

 

IMPORTANT NOTE: I’m using the delete function on the items collection to remove the flask from the player’s backpack. ALWAYS use delete, NEVER use the JavaScript popshift or splice functions!

 

As we’re now at the Passage location with no way to return to start, we also need to add an additional feature to be able to travel back. Add it to the passage.ts file like this:

{
	name: 'Passage back',
	combinations: {
		combine: [
			{
			    combinationType: Combinations.WALK,
			    match: (game, target, tool): string => {
					game.changeLocation(Start);
					return 'You crawl back to the fountain.';
			    }
			}
		]
	}
}

And to the passage.html like this:

<description>
    <p>
        The small tunnel in the undergrowth is hard to get through. On the other end
        the trees are close together, blocking most of the sunlight.
        <feature name="woundedwarrior">A gravely injured warrior lies on the ground here.</feature>
        <feature name="passageback">The way back is hard to see.</feature>
    </p>
</description>

Use ‘Walk’ on the passage back to get back to the fountain. Try the ‘Use’ combination with the flask and the fountain. You should see the Flask item replaced by a Fountain water item.

Note that the order in which you select the tool and a target for combinations that require both matters in most cases. Only when using two items together will a combination be resolved irrespective of whether the tool or the target was selected first. Let’s demonstrate this by adding one more item to wrap up.

Add a new Herbs item and add this combination. Note that I’m specifying that the feature should be removed when a successful touch combination match is made:

{
    combinationType: Combinations.TOUCH,
    match: (game, target, tool): string | ICombinationMatchResult => {
        if (game.activeCharacter.items.find(Herbs)) {
            return 'The herb leaves are soft to the touch.';
        }
        
        game.activeCharacter.items.add(Herbs);
        return { 
            text: 'You collect the herbs.', 
            removeTarget: true 
        };
    }
}

Add the herbs as a feature to the Passage.html file:

<description>
    <p>
        The small tunnel in the undergrowth is hard to get through. On the other end
        the trees are close together, blocking most of the sunlight.
        <feature name="woundedwarrior">A gravely injured warrior lies on the ground here.</feature>
        <feature name="herbs">A little bit further away, some herbs are growing.</feature>
        <feature name="passageback">The way back is hard to see.</feature>
    </p>
</description>

Then, add the herbs item to the Passage.ts as a new feature:

features: [
    Herbs(),
    ...
]

Note the brackets after the item name, these are needed to instantiate a new herbs item for the location (there is a technical reason why you need them here and not when e.g. adding an item using add. You can also use the brackets when you’re adding a new item, but it doesn’t make sense when removing one).

When you now go to the Passage location, you’ll find the herbs there. Collect them using the Touch combination. When you do that the feature disappears, and you get the Herbs item.

Now try to complete this demo by adding a Use combination to the Water item that will remove both the Water and Herbs items and add a Healing Potion item instead. Then, create a combination to allow the player to cure the wounded warrior using the healing potion.