Functions
Use functions in your Ink story to manipulate strings, perform math, create balanced stat systems and keep track of time! This guide covers both Ink’s built-in functions and the extras this template adds.
Table of Contents
Quick Reference
Ink Built-in Functions
These work out of the box, no setup required.
| Function | Description | Example |
|---|---|---|
RANDOM(min, max) |
Random integer (inclusive) | RANDOM(1, 6) |
FLOOR(x) |
Round down | FLOOR(3.7) → 3 |
CEILING(x) |
Round up | CEILING(3.2) → 4 |
INT(x) |
Truncate to integer | INT(3.9) → 3 |
FLOAT(x) |
Convert to decimal | FLOAT(3) → 3.0 |
POW(x, y) |
x to the power of y | POW(2, 3) → 8 |
MIN(a, b) |
Smaller of two values | MIN(5, 3) → 3 |
MAX(a, b) |
Larger of two values | MAX(5, 3) → 5 |
TURNS() |
Choices made so far | TURNS() |
CHOICE_COUNT() |
Current available choices | CHOICE_COUNT() |
SEED_RANDOM(x) |
Seed the RNG | SEED_RANDOM(42) |
TURNS_SINCE(-> knot) |
Turns since visiting knot | TURNS_SINCE(-> intro) |
READ_COUNT(-> knot) |
Times knot was visited | READ_COUNT(-> shop) |
List functions: LIST_COUNT(), LIST_MIN(), LIST_MAX(), LIST_ALL(), LIST_INVERT(), LIST_RANDOM(), LIST_RANGE(), LIST_VALUE()
See the official Ink documentation, it explains some of these (especially LIST functions).
Template Functions
These require declaring EXTERNAL functions in your Ink file (see setup below).
String Functions
| Function | Description | Example |
|---|---|---|
UPPERCASE(str) |
Convert to uppercase | UPPERCASE("hello") → “HELLO” |
LOWERCASE(str) |
Convert to lowercase | LOWERCASE("HELLO") → “hello” |
CAPITALIZE(str) |
Capitalize first letter | CAPITALIZE("john") → “John” |
TRIM(str) |
Remove leading/trailing spaces | TRIM(" hi ") → “hi” |
LENGTH(str) |
Character count | LENGTH("hello") → 5 |
CONTAINS(str, search) |
Check if contains substring | CONTAINS("hello", "ell") → true |
STARTS_WITH(str, search) |
Check string start | STARTS_WITH("hello", "he") → true |
ENDS_WITH(str, search) |
Check string end | ENDS_WITH("hello", "lo") → true |
REPLACE(str, old, new) |
Replace first occurrence | REPLACE("hello", "l", "L") → “heLlo” |
REPLACE_ALL(str, old, new) |
Replace all occurrences | REPLACE_ALL("hello", "l", "L") → “heLLo” |
Math Functions
| Function | Description | Example |
|---|---|---|
ROUND(x) |
Round to nearest integer | ROUND(3.5) → 4 |
CLAMP(x, min, max) |
Constrain value to range | CLAMP(150, 0, 100) → 100 |
ABS(x) |
Absolute value | ABS(-5) → 5 |
PERCENT(value, total) |
Calculate percentage | PERCENT(25, 200) → 13 |
Fairmath Functions
| Function | Description | Example |
|---|---|---|
FAIRADD(stat, percent) |
Add with diminishing returns | FAIRADD(80, 20) → 84 |
FAIRSUB(stat, percent) |
Subtract with diminishing returns | FAIRSUB(20, 20) → 16 |
Time Functions
| Function | Description | Example |
|---|---|---|
NOW() |
Current Unix timestamp (seconds) | NOW() → 1732645200 |
SECONDS_SINCE(start) |
Seconds elapsed since timestamp | SECONDS_SINCE(start) → 45 |
MINUTES_SINCE(start) |
Minutes elapsed since timestamp | MINUTES_SINCE(start) → 5 |
TIME_SINCE(start) |
Human-readable elapsed time | TIME_SINCE(start) → “5 minutes” |
FORMAT_DATE(ts, locale) |
Format timestamp as date | FORMAT_DATE(ts, "en-US") → “November 26, 2025” |
FORMAT_TIME(ts, locale) |
Format timestamp as time | FORMAT_TIME(ts, "en-US") → “3:45 PM” |
FORMAT_DATETIME(ts, locale) |
Format timestamp as date and time | FORMAT_DATETIME(ts, "en-US") → “November 26, 2025, 3:45 PM” |
OFFSET_DATE(ts, y, mo, d, h, mi) |
Add/subtract from timestamp | OFFSET_DATE(ts, -5, 0, 0, 0, 0) → 5 years ago |
Setup
To use template functions, add EXTERNAL declarations at the top of your main .ink file. These declarations only need to be added once per story to be used anywhere in the story. Only include the functions you actually use:
// String functions
EXTERNAL UPPERCASE(str)
EXTERNAL LOWERCASE(str)
EXTERNAL CAPITALIZE(str)
EXTERNAL TRIM(str)
EXTERNAL LENGTH(str)
EXTERNAL CONTAINS(str, search)
EXTERNAL STARTS_WITH(str, search)
EXTERNAL ENDS_WITH(str, search)
EXTERNAL REPLACE(str, old, new)
EXTERNAL REPLACE_ALL(str, old, new)
// Math functions
EXTERNAL ROUND(x)
EXTERNAL CLAMP(x, min, max)
EXTERNAL ABS(x)
EXTERNAL PERCENT(value, total)
// Fairmath functions
EXTERNAL FAIRADD(stat, percent)
EXTERNAL FAIRSUB(stat, percent)
// Time functions
EXTERNAL NOW()
EXTERNAL SECONDS_SINCE(start)
EXTERNAL MINUTES_SINCE(start)
EXTERNAL TIME_SINCE(start)
EXTERNAL FORMAT_DATE(timestamp, locale)
EXTERNAL FORMAT_TIME(timestamp, locale)
EXTERNAL FORMAT_DATETIME(timestamp, locale)
EXTERNAL OFFSET_DATE(timestamp, years, months, days, hours, minutes)
Examples
Working with Player Names
EXTERNAL TRIM(str)
EXTERNAL CAPITALIZE(str)
VAR raw_input = " jane "
VAR player_name = ""
~ player_name = TRIM(raw_input)
Hello, {CAPITALIZE(player_name)}!
Output: Hello, Jane!
Clamping Stats
EXTERNAL CLAMP(x, min, max)
VAR health = 150
VAR max_health = 100
Your health is {CLAMP(health, 0, max_health)}.
Output: Your health is 100.
Progress Percentage
EXTERNAL PERCENT(value, total)
VAR quests_done = 7
VAR total_quests = 20
You've completed {PERCENT(quests_done, total_quests)}% of all quests.
Output: You've completed 35% of all quests.
Conditional Text with String Functions
EXTERNAL CONTAINS(str, search)
EXTERNAL ENDS_WITH(str, search)
VAR player_name = "john smith"
{CONTAINS(player_name, "smith"): You must be one of the Smith family!}
{ENDS_WITH(player_name, "son"): A Scandinavian name, perhaps?}
Using Built-in Random
No external declaration needed:
VAR dice_roll = 0
~ dice_roll = RANDOM(1, 6)
You rolled a {dice_roll}!
{dice_roll == 6: Critical hit!}
{dice_roll == 1: Critical miss!}
Using Fairmath
Fairmath (popularized by ChoiceScript) creates balanced stat progression. Instead of flat changes, stats become harder to move the closer they get to extremes.
FAIRADD gives you a percentage of your remaining headroom (distance to 100):
- At 50:
FAIRADD(50, 20)→ 50 + (50 × 0.20) = 60 - At 80:
FAIRADD(80, 20)→ 80 + (20 × 0.20) = 84 - At 95:
FAIRADD(95, 20)→ 95 + (5 × 0.20) = 96
FAIRSUB takes a percentage of your current value:
- At 50:
FAIRSUB(50, 20)→ 50 - (50 × 0.20) = 40 - At 20:
FAIRSUB(20, 20)→ 20 - (20 × 0.20) = 16 - At 5:
FAIRSUB(5, 20)→ 5 - (5 × 0.20) = 4
Results are automatically clamped between 0 and 100.
Fairmath Example
EXTERNAL FAIRADD(stat, percent)
EXTERNAL FAIRSUB(stat, percent)
VAR reputation = 50
=== tavern ===
The innkeeper eyes you cautiously.
+ [Help with the dishes]
~ reputation = FAIRADD(reputation, 15)
She smiles warmly. "Thank you, traveler."
-> tavern
+ [Steal from the tip jar]
~ reputation = FAIRSUB(reputation, 20)
You pocket a few coins when no one's looking.
-> tavern
+ [Leave]
-> END
Session Tracking
EXTERNAL NOW()
EXTERNAL TIME_SINCE(start)
VAR session_start = 0
~ session_start = NOW()
// Later in your story...
You've been playing for {TIME_SINCE(session_start)}.
Output: You've been playing for 12 minutes.
Real-Time Narrative
EXTERNAL NOW()
EXTERNAL FORMAT_DATE(timestamp, locale)
EXTERNAL FORMAT_TIME(timestamp, locale)
VAR LOCALE = "en-US"
{FORMAT_DATE(NOW(), LOCALE)} - Dear diary...
The clock on the wall reads {FORMAT_TIME(NOW(), LOCALE)}.
Flashbacks with Date Math
Use OFFSET_DATE to calculate dates relative to now. Parameters are: timestamp, years, months, days, hours, minutes.
EXTERNAL NOW()
EXTERNAL FORMAT_DATE(timestamp, locale)
EXTERNAL OFFSET_DATE(timestamp, years, months, days, hours, minutes)
VAR LOCALE = "en-US"
VAR flashback_date = 0
~ flashback_date = OFFSET_DATE(NOW(), -5, 0, 0, 0, 0)
The incident happened on {FORMAT_DATE(flashback_date, LOCALE)}.
Output: The incident happened on November 26, 2020.
Time-Based Gameplay
EXTERNAL NOW()
EXTERNAL MINUTES_SINCE(start)
VAR quest_started = 0
~ quest_started = NOW()
// Later, check if player took too long
{MINUTES_SINCE(quest_started) > 30:
The merchant has closed up shop for the day.
- else:
The merchant waves you over.
}
Using Time Functions
Time functions give your Ink story real-world time awareness. Ink has no native concept of real time, so these bridge that gap.
Note: Timestamps are stored in seconds (Unix timestamp format), not milliseconds.
Locale Support
The FORMAT_* functions require a locale parameter. You can find a list of locales at simplelocalize.io.
| Locale | Date Output | Time Output |
|---|---|---|
"en-US" |
November 26, 2025 | 3:45 PM |
"en-GB" |
26 November 2025 | 15:45 |
"fr-FR" |
26 novembre 2025 | 15:45 |
"de-DE" |
26. November 2025 | 15:45 |
"ja-JP" |
2025年11月26日 | 15:45 |
Tip: Define a LOCALE variable once and reuse it throughout your story:
VAR LOCALE = "en-US"
Today is {FORMAT_DATE(NOW(), LOCALE)}.
The time is {FORMAT_TIME(NOW(), LOCALE)}.
OFFSET_DATE Parameters
OFFSET_DATE(timestamp, years, months, days, hours, minutes)
Use negative numbers to go back in time:
// 5 years ago
~ past = OFFSET_DATE(NOW(), -5, 0, 0, 0, 0)
// 2 years, 3 months, 15 days ago
~ past = OFFSET_DATE(NOW(), -2, -3, -15, 0, 0)
// 1 week from now
~ future = OFFSET_DATE(NOW(), 0, 0, 7, 0, 0)
// 6 hours from now
~ later = OFFSET_DATE(NOW(), 0, 0, 0, 6, 0)
Tips
- You can’t use logic inside string literals. Use
~ variable = FUNCTION(x)instead ofVAR variable = "{FUNCTION(x)}" - You can chain functions:
{UPPERCASE(TRIM(player_input))}works as expected - Test in browser! Functions execute at runtime, so test in your actual story
Troubleshooting
“Missing function binding” error?
- Check that you declared the
EXTERNALfunction in your Ink file - Make sure
ink-functions.jsis loaded beforestory-manager.jsin yourindex.htmlfile
Function returns unexpected value?
REPLACEonly replaces the first occurrence, useREPLACE_ALLfor allPERCENTreturns an integer (rounded), not a decimal
Date showing wrong format?
- Make sure you’re passing a valid locale string (e.g., “en-US”, “fr-FR”)
- Invalid locales will fall back to “en-US” (check browser console for warnings)
Found a bug or have a feature idea?
Open an issue
on GitHub, or use the feedback forms:
Bug report ·
Feature request