npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lamppost/ink-player

v2.1.2

Published

A web player for running Ink stories created by Lamp Post Projects.

Readme

Lamp Post Ink Player

This is a web player for running Ink stories created by Lamp Post Projects.

[!WARNING] This software still under active development, please use with caution as it's likely to change.

Getting Started

Requirements: Node.js and pnpm.

  • Run pnpm create @lamppost/create-ink-player
  • Inside your terminal, go into the new project directory that you just created.
  • Run pnpm dev to see the result in your browser and confirm that things are running correctly.
  • Open up story/game.ink in the Inky editor and make modifications, go back to your game in the browser and see your changes live!
  • Update the settings in src/index.ts to use your game name and update any other settings.
  • Update the About page in src/About.tsx to include some information about your game (or remove that page entirely by updating src/index.ts).
  • Add any custom CSS styling inside the src/styles.scss file.
  • Run pnpm build to create the final HTML/JS/CSS/Image files needed to display the game. The files will be output to the dist/ directory. You can then bundle them or upload them to the location of your choice.
  • To update your game to the latest version of the ink player you can run pnpm update-ink-player.

Page Configuration

You can configure which pages are shown in the sidebar and in what order they are displayed. These are displayed in the sidebar and in the header (on mobile).

There is one built-in required page: "game". It must have the id of "game" but can have custom a title.

You can reorder the pages however you like, the first listed page will be shown to the user by default.

You can also add custom pages by providing a component. The component will be rendered in a tab, and can be any valid React component.

The component is provided with the following props:

  • setPage: A function to navigate to a different page
  • loading: A boolean indicating if the game is loading

All of this is configured in the src/index.ts file in the pages array:

import {About} from "./about";
...
pages: [
    {
        id: "about",
        title: "About",
        component: About,
    },
    {
        id: "game",
        title: "Game",
        component: "game",
    },
],

Themes

By default the display will use the built-in Bootstrap light theme. You can override this by setting defaultTheme: "dark" in the src/index.ts file.

Custom Ink Features

The Lamp Post Ink Player includes a number of custom tags and markup that be used to show special features exclusive to this player.

Disabled Choices

Individual choices can be disabled by adding a # disabled tag within the choice. It will make it so that the choice cannot be selected by the user.

Sticky Tags

The Lamp Post Ink Player has the ability to "persist" tags defined in knots to future knots. This can help to simplify some of the configuration of some tags used by features (such as Location, ImageHeader, etc.). If you wish to turn "off" a persisted tag then you can set the tag to be "None", like so: # Location:None.

To make a tag sticky, in src/index.ts:

stickyTags: ["Location"],

Plugins

Plugins extend the functionality of the Ink player by providing additional functionality. You can add them in by importing the plugin into your settings file and adding it to the plugins list.

For example, to enable locations, in src/index.ts you would do:

import locationPlugin from "@lamppost/ink-player/plugins/location"
...
plugins: [
    ...
    locationPlugin({}),
    ...
]

Dark Mode

If you want the theme to change based upon the user's preferred theme is (which is configured via their operating system) you can include the dark mode plugin. The user will also be presented with a way to configure the theme in the page header. If the user chooses an option here it will override any specified defaultTheme.

To enable dark mode, in src/index.ts you would do:

import darkModePlugin from "@lamppost/ink-player/plugins/dark-mode"
...
plugins: [
    ...
    darkModePlugin({}),
    ...
]

History

Adds a page to show a game log for the player's entire playthrough. Includes an optional way for users to download their game log as an HTML file for sharing.

Options:

  • groupBy: Type: (currentState: GameState) => { id: string; title: string }. The system will cluster the log by id and give each cluster their assigned title. It will be visible in a collapsible accordion.
  • hideDownloadButton: Set to true if you don't want the user to be able to download their game log.

To enable this, in src/index.ts you would do:

import historyPlugin from "@lamppost/ink-player/plugins/history"
...
pages: [
    {
        id: "game",
        title: "Game",
        component: "game",
    },
    {
        id: "history",
        title: "History",
        component: "history",
    },
],
...
plugins: [
    ...
    historyPlugin({}),
    ...
]

Header Images

Images to be used in the header for a knot. Can be configured by setting the "Image:Name" tag in your Ink story. The "Name" value will be used to match one of the images defined in the src/index.ts file.

You'll probably want to specify "Image" as a sticky tag, as well, in order to simplify the display of the header image. More info on Sticky Tags in the section below.

For example, in your Ink file, add the following tag:

# Image:Foyer

In src/index.ts:

import Foyer from "./assets/images/foyer.jpg";
import headerImagePlugin from "@lamppost/ink-player/plugins/header"
...
stickyTags: ["Image"],
plugins: [
    ...
    headerImagePlugin({
        images: {
            Foyer: Foyer,
        }
    }),
    ...
]

Locations

Display a textual location at the top of a knot. Can be configured by setting the "Location:Name" tag in your Ink story.

You'll probably want to specify "Location" as a sticky tag, as well, in order to simplify the display of the location across multiple knots. More info on Sticky Tags in the section below.

For example, in your Ink file, add the following tag:

# Location:Foyer

To enable locations and make them sticky, in src/index.ts:

import locationPlugin from "@lamppost/ink-player/plugins/location"
...
stickyTags: ["Location"],
plugins: [
    ...
    locationPlugin({}),
    ...
]

Portraits

Portraits to be displayed in a knot. Can be configured by setting the "# Portrait:Name" tag in your Ink story. The "Name" value will be used to match one of the portraits defined in the src/index.ts file. If a large image is specified then it'll be shown in a modal when the portrait is clicked.

You'll probably want to specify "Portrait" as a sticky tag, as well, in order to simplify the display of portraits during a long conversation. More info on Sticky Tags in the section below.

For example, in your Ink file, add the following tag:

# Portrait:Rion

In src/index.ts:

import Rion from "./assets/images/rion.jpg";
import RionLarge from "./assets/images/rion large.jpg";
import portraitPlugin from "@lamppost/ink-player/plugins/portrait"
...
stickyTags: ["Portrait"],
plugins: [
    ...
    portraitPlugin({
        images: {
            Rion: {
                small: Rion,
                large: RionLarge,
            },
        }
    }),
    ...
]

Background Music

Background music can be played between scenes. Can be configured by setting the "# Background Music:Name" tag in your Ink story. The "Name" value will be used to match one of the music files defined in the src/index.ts file.

The volume levels will be controlled via a button in the navbar.

You'll probably want to specify "BackgroundMusic" as a sticky tag, as well, in order to ensure seemless music playback during a long conversation. More info on Sticky Tags in the section below.

For example, in your Ink file, add the following tag:

# BackgroundMusic:Name

In src/index.ts:

import ThemeA from "./assets/audio/theme-a.mp3";
import ThemeB from "./assets/audio/theme-b.mp3";
import backgroundMusicPlugin from "@lamppost/ink-player/plugins/background-music";
...
stickyTags: ["BackgroundMusic"],
plugins: [
    ...
    backgroundMusicPlugin({
        audioFiles: {
            ThemeA: ThemeA,
            ThemeB: ThemeB,
        }
    }),
    ...
]

Inline Images

Images to be displayed inline in a knot. Can be configured by using a special !widget:image syntax in your Ink story. The "Name" value will be used to match one of the images defined in the src/index.ts file. If a large image is specified then it'll be shown in a modal when the portrait is clicked.

The alt text is optional, and will be used as the alt text for the image (but should be provided for accessibility reasons).

The align property is optional, and can be "left", "right", or "center". The default is "center". Left or right will float the image and allow the text to flow around it.

You can add a caption to the image using the "caption" property. If you add a URL in the "source" property then it'll be added as a link to the caption.

For example, in your Ink file, add the following text:

!widget:image name="Map" alt="A map of the world..." align="left"

In src/index.ts:

import MapImage from "./assets/images/map.jpg";
import MapImageLarge from "./assets/images/map large.jpg";
import imagePlugin from "@lamppost/ink-player/plugins/image";
...
plugins: [
    ...
    imagePlugin({
        images: {
            Map: {
                small: Map,
                large: MapLarge,
            },
        }
    }),
    ...
]

Text Input

A text input to be displayed inside a choice. Can be configured by using a special !widget:text-input syntax in a choice in your Ink story. Can be enable in your settings by doing:

import textInputPlugin from "@lamppost/ink-player/plugins/text-input";
...
plugins: [
    ...
    textInputPlugin({}),
    ...
]

The text input widget has the following props:

  • name (required): This corresponds with the Ink variable that the result will be written to whenever the user submits their answer.
  • label (required): The label to set on the input, to help inform the user about what is being asked of them.
  • input-label (optional, default: "Submit"): The label to set on the submit button.

For example, in your Ink file, add the following text as one of your choices:

!widget:text-input name="name-var" label="Your Name" submit-label="Set Name"

Card Choices

A card to be displayed inside a choice. Can be configured by using a special !widget:card syntax in a choice in your Ink story.

The card widget has the following props:

  • title (required): The primary text of the choice that will be displayed both in the game and in the history/logs.
  • description (optional): Additional contextual text to help guide the user towards making a decision.
  • icon (optional): The image to use on the side of the card. Can be a path to an image or a React component/SVG.

For example, in your Ink file, add the following text as one of your choices:

!widget:card icon="library" title="Visit the Library"

To add any card images, in src/index.ts:

import LibraryImage from "./assets/images/library.jpg";
import SunIcon from "bootstrap-icons/icons/sun.svg?react";
import cardPlugin from "@lamppost/ink-player/plugins/card";
...
plugins: [
    ...
    cardPlugin({
        images: {
            "library": LibraryImage,
            "sun": SunIcon,
        }
    }),
    ...
]
...

Achievements

Achievements are a way to reward users with successfully accessing parts of the ink file. They can be triggered by adding a # Achievement:all-endings tag in your game (replacing all-endings with the name of your achievement).

To add an achievement, in src/index.ts:

import MapImage from "./assets/images/map.jpg";
import SunIcon from "bootstrap-icons/icons/sun.svg?react";
import achievementsPlugin from "@lamppost/ink-player/plugins/achievements";
...
plugins: [
    ...
    achievementsPlugin({
        achievements: {
            "all-endings": {
                icon: MapImage,
                title: "All Endings",
                description: "Complete all the endings of the game.",
                hidden: true,
                showHiddenButtonText: "View Locked Achievement",
            },
            "new-day": {
                icon: SunIcon,
                title: "Reach a New Day",
                description: "Start a new day.",
            },
        },
    }),
    ...
],

The properties for configuring an achievement are as follows:

  • icon: Can be a path to an image or a React component which renders an image.
  • title: The name of the achievement to display to the user.
  • description (optional): The description of the achievement to display to the user.
  • hidden (optional, defaults to false): Should this achievement be hidden on the achievements page?
  • showHiddenButtonText (optional): If the achievement is hidden then a button will be overlaid on it, with this text in it, which a user can click to reveal the hidden achievement.

Additionally in src/index.ts you'll want to add an Achievements page:

pages: [
    ...
    {
        id: "achievements",
        title: "Achievements",
        component: "achievements",
    },
    ...
],

Dice Rolls

A widget for displaying the results of a die roll. Doesn't actually perform a roll itself, instead it's a mechanism for displaying a non-looping die roll gif and delaying the display of all text and choices after the die roll (for dramatic effect).

You can insert a die roll into a page using:

!widget:dice-roll die="d4" value="1" alt="A 1 on a 4-sided die"

The options are as follows:

  • die (required): The type of die being rolled (used to look up the right image).
  • value (required): The face of the die (used to look up the right image).
  • alt (required): The text used for screen readers and also for display in the game history and logs.
  • duration (optional, default 1500): How long to delay the display of all subsequent text and choices. Set to "0" to have no delay.

To configure the images to use in src/index.ts:

import Dice1 from "./assets/images/dice/d4_1.gif";
import Dice2 from "./assets/images/dice/d4_2.gif";
import Dice3 from "./assets/images/dice/d4_3.gif";
import Dice4 from "./assets/images/dice/d4_4.gif";
import diceRollPlugin from "@lamppost/ink-player/plugins/diceRoll";
...
plugins: [
    ...
    diceRollPlugin({
        images: {
            d4: {
                "1": Dice1,
                "2": Dice2,
                "3": Dice3,
                "4": Dice4,
            },
        },
    }),
    ...
]

Glossary

If you add a function into your game file with the name like:

=== function glossary_word ===
A definition of the word.

Then it'll automatically find all instances of that word in the game text and make them clickable to view the definition of the word as a popover.

To enable this in src/index.ts:

import glossaryPlugin from "@lamppost/ink-player/plugins/glossary";
...
plugins: [
    ...
    glossaryPlugin({}),
    ...
]

Comments

You can make it so that play testers can leave comments inside their transcript for future analysis.

You can turn this feature on by enabling this setting in src/index.ts:

import commentPlugin from "@lamppost/ink-player/plugins/comment";
...
plugins: [
    ...
    commentPlugin({}),
    ...
]

The results will show up in the game, history, and the downloaded HTML log.

Footnotes

You can add collapsible footnotes that will display beneath the choices. You can add a footnote inside your content by doing:

[widget:footnote]The contents of your footnote.[/widget:footnote]

If you want to add a small linkable id that the user can click to visit the footnote, then do:

[widget:footnote id="1" link="true"]The contents of your footnote.[/widget:footnote]

You can turn this feature on by enabling this setting in src/index.ts:

import footnotesPlugin from "@lamppost/ink-player/plugins/footnotes";
...
plugins: [
    ...
    footnotesPlugin({}),
    ...
]

The results will show up in the game, history, and the downloaded HTML log.

Credits and License

This software is released under an MIT license.