We have looked in the past at using the Ink language to write a branching story.

I thought it might be good to try adding some more gameplay elements in a story, such as fighting. It also provides a good opportunity to learn about variables.

Ink allows defining variables of different types (number, booleans, lists…) and use them within your story.

It enables basic operations:

// setting variables 
VAR HERO_HP = 10
VAR MONSTER_HP = 6

VAR IS_DEFENDING = false

// checking variable values to create branching paths 
{ MONSTER_HP <= 0:
    -> END
}

// update current variable values 
~ IS_DEFENDING = true
~ MONSTER_HP = MONSTER_HP - 1 

Now let’s imagine a simple scene: the hero encounters a monster and the fight starts.

The hero can either attack or defend. If he attacks, he does damage to the monster, but the monster can hurt him. If he defends, he receives no damage. The monster will always attack.

As such, the scene is a simple loop of alternating attackers (hero, monster, hero…) until the exit condition is met (monster hp at 0 or lower).

We can use our newfound knowledge of variables to record player/monster HP and whether the player attacks or defends and perform branching depending on the state of the game.

VAR HERO_HP = 10
VAR MONSTER_HP = 6

VAR TEMP_DMG = 0

VAR IS_DEFENDING = false

-> fight_intro

=== fight_intro ===
-> fight_main


=== fight_main ===
= turn_hero
What will you do?
+ [Attack]
    ~ IS_DEFENDING = false
    ~ TEMP_DMG = RANDOM(1, 4)
    You attack the monster for {TEMP_DMG} damage.
    ~ MONSTER_HP = MONSTER_HP - TEMP_DMG
    -> turn_monster
+ [Defend] 
    ~ IS_DEFENDING = true
    You defend.
    -> turn_monster

= turn_monster
{ MONSTER_HP <= 0:
    The Monster Died!
    -> fight_end
- else:
    The Monster attacks...
    { IS_DEFENDING:
        You receive 0 damage!
    - else:
    You receive 1 damage!
    ~ HERO_HP = HERO_HP - 1
    }
    -> turn_hero
}

=== fight_end ===
The fight is over!
-> END

Let’s run it: it works! Now, if you’ve paid attention, you will have realized that we used a function to get player damage: RANDOM. Random enables drawing values between boundaries and is obviously extremely useful. It also shows that the Ink language enables us to define functions!

Actually, functions in Ink are simply knots with a (facultative) call stack, and a return statement.

In our case, let’s move the player action logic into a function called player_acts, which takes as input whether the player attacks or not, modifies state accordingly and returns damage done to enemy:

=== function player_acts(is_attacking) ===
    ~ temp damage = 0
    { is_attacking: 
        ~ IS_DEFENDING = false
        ~ damage = RANDOM(1, 4)
        ~ MONSTER_HP = MONSTER_HP - damage
    - else:
        ~ IS_DEFENDING = true
    }
    ~ return damage


// turn hero becomes a bit tidier: 
= turn_hero
What will you do?
+ [Attack]
    You attack the monster for {player_acts(true)} damage.
    -> turn_monster
+ [Defend] 
    ~ player_acts(false)
    You defend.
    -> turn_monster

Now, we have somewhat seperated combat logic from text logic, which makes our scene tidier. We also introduced a new concept for variables: temporary (or local) variables, which are defined using “temp” before their name (see damage in player_acts function). We have been able to remove the global TEMP_DMG variable using this temp variable.

We now have a lot more potential in our stories now that we can now create variables, state and reuse logic in functions. Still, Ink is a interpreter written in javascript, which means performance can take a hit for complex logic and large state.

To quote the developers: “In general we recommend doing complex logic on the game side, and keeping your specific narrative-relevant state in ink, such that it can be modelled with the constructs we do provide.”

Considering all this, it could be more beneficial to handle state logic directly in javascript and interact with ink afterward. For an example of modifying Ink state from pure javascript, see this great article by Dan Cox.