Custom Events - Monster-Train-2-Modding-Group/Trainworks-Reloaded GitHub Wiki

Making custom events is a bit more involved than most other content and requires learning ink. Ink is a scripting language for writing an interactive narrative. This tutorial will not be explaining Ink and everything the scripting language supports for that you can view its documentation. Note that Trainworks won't support all of the functionality of ink.

The steps to write a new custom event:

  1. Write a script for it in ink. Specifically what's required is a ink file with a single knot that defines your event. To end your knot a divert to END should be used.

  2. Compile the ink script into an .ink.json file.

  3. Genereate a StoryEventData based off of the .ink.json file.

  4. Edit the StoryEventData providing localizations for choices, and the event text.

  5. Define RewardDatafor potential story rewards corresponding to commands in the ink script.

  6. Define a GameObject for the story foreground including an optional SkeletionAnimation

Writing a script

Here's a simple ink script based off the original Bone Dog event from MT2.

=== BoneDogTitan2 ===

<style="Body">The legendary <wave><smallcaps><color="white">Dog made of Bone</color></smallcaps></wave> bounces towards you, its bonetail <shake>rattling</shake> together in an excited wag. </style>

<style="Body"><align="center"><size=175%><b><smallcaps><funbounce>“Yip!”</funbounce></smallcaps></b></size></align></style>

<style="Body"><size=100%>The dog of bone lies on its back and suddenly roars</size> <size=200%><wave>WASH MY BELLY</wave></size>.</style>

<style="Prompt"><size=80%>Will you stop for the <funbounce>Bone Dog</funbounce> and wash its belly?</size></style>

+ [Wash]
    <style="Body">You attempt to Wash Bone Dog's belly, but it doesn't have one... Bonedog offers you a trinket in response.</style>
    >>>GIVE_REWARD: BoneDogTitanPyreLifeRelicReward
    -> END

+ [Deny]
    <style="Body">Bone dog gets very upset and forces himself into your train.</style>
    >>>GIVE_REWARD: BoneDogTitanCardReward
    -> END

+ [Leave]
    <style="Body">The Bone Dog’s tail stops <sway>wagging</sway> and he screams as a tornado flings him away. He realizes that, yet again, he is to be left at the side of the Rail.
You monster!</style>
    -> END

Each distinct line of text should be in a <style="Body"> tag or <style="Prompt"> tag.

Choices must be given using the + [Choice] syntax, Note that the actual choice text is not used here but a simple identifier. The choice text is not actually defined within the ink script.

For each choice we have text to display when the player selects the choice.

Next we can instruct the game to give rewards or do other things to the player with special commands >>>GIVE_REWARD gives a specific reward to the player. If you are giving a custom modded reward don't forget the '@' character before the reward name.

Lastly to end the event, we use a divert to END via the syntax -> END which instructs ink that the script is done and to hand control back to the game.

Requirements

For minimal issues with the game the ink script must be structured like the above.

  1. The ink script must define a singular knot without any dependencies. If you need variables then I would suggest defining an external function and connecting it in C# to a function to run.

  2. Choices must use the + [CHOICEKEY] syntax. Do not use the exact phrasing for what the choice is, just have CHOICEKEY be a singular word. The actual text is replaced at runtime and is defined by a specific localization key. More on that when we define the StoryEventData

  3. When your event is done you must divert to end via the syntax -> END. Do not divert to another knot or DONE.

Commands

Within a ink story knot you can insert commands to make the game do things such as give a reward to the player, deal damage to the pyre, or gain gold.

To utilize a command on a single line write >>> COMMAND_NAME: <parameters>

For instance

>>> GIVE_REWARD: RandomCardDraftReward

Command List

  • GIVE_REWARD

  • TRIGGER_ACHIEVEMENT

  • REMOVE_CARD

  • REMOVE_RELIC

  • SHOW_REWARDS

  • DAMAGE_TRAIN

  • HEAL_TRAIN

  • TAKE_GOLD

  • GRANT_GOLD

  • REMOVE_PREDETERMINED_RANDOM_RELIC

  • REMOVE_RANDOM_RELIC

  • SET_CHOSEN_TO_FIGHT_TFB

Text Effects

In addition specifically for events, special text effects can be applied to text within an event. These are all specified in your script in HTML tags. For instance <bigshake>Big Shake</bigshake> will result in text that shakes.

  • balatro

  • banished

  • bigshake

  • bigwave

  • blood

  • cloud

  • darkangel

  • dominion

  • dominionchatter

  • entropy

  • entropychatter

  • entropycolor

  • funbounce

  • glimmer

  • green

  • greenpulse

  • heavenwave

  • lazarusleague

  • lunacoven

  • mediumwave

  • mystical

  • onfire

  • pend

  • pulse

  • pyreborne

  • redpulse

  • savagery

  • savagerychatter

  • shake

  • smallshake

  • sway

  • tutpress

  • underlegion

  • void

  • wave

  • whisper

  • wiggle

  • wonderous

Compiling an Ink Script

You will compile your ink script with the program inklecate. A specific version of this program must be used as the developers use a very old version of the ink runtime.

Download inklecate from - https://github.com/inkle/ink/releases/tag/0.9.0

While you can also use Inky to edit your ink files and compile them, be sure to get the version of Inky that uses the same version of the ink compiler.

Once you have inklecate you can run this command to compile your ink file run this in the console or terminal.

inklecate.exe example2.ink

Ensuring that both inklecate and your .ink file are in the same directory.

The command will produce an .ink.json file you will need this for the next step.

Generating a stubbed StoryEvent definition

There is a lot of things that need to be gathered from the compiled ink script that we will need to include in our StoryEventData. Particularly the text and choices you write in the event need to have localizations provided for each line and the english translation needs to match exactly with what you wrote in the ink script.

To alleviate the heavy burden of doing all of this, I have authored a python script that will do most of these manual steps for you.

The python script is located in this Trainworks-Reloaded tools directory Download the file and run it on your generated .ink.json file.

JSON Definition

Given the example script after running python stub_event.py example2.ink.json this would be the output. It generated the file event_BoneDogTitan2.json This is the expected contents.

{
    "$schema": "https://github.com/Monster-Train-2-Modding-Group/Trainworks-Reloaded/releases/latest/download/schema.json",
    "events": [
        {
            "id": "BoneDogTitan2",
            "knot_name": "BoneDogTitan2",
            "num_runs_completed_to_see": 1,
            "priority_ticket_count": 10,
            "num_classes_needed_to_show": 1,
            "min_distance_allowed": 3,
            "max_distance_allowed": 8,
            "story_data": "example2.ink.json",
            "is_followup_event": false,
            "texts": [
                {
                    "english": "<style=\"Body\">You attempt to Wash Bone Dog's belly, but it doesn't have one... Bonedog offers you a trinket in response.</style>"
                },
                {
                    "english": "You monster!</style>"
                },
                {
                    "english": "<style=\"Body\"><align=\"center\"><size=175%><b><smallcaps><funbounce>“Yip!”</funbounce></smallcaps></b></size></align></style>"
                },
                {
                    "english": "<style=\"Body\">Bone dog gets very upset and forces himself into your train.</style>"
                },
                {
                    "english": "<style=\"Body\"><size=100%>The dog of bone lies on its back and suddenly roars</size> <size=200%><wave>WASH MY BELLY</wave></size>.</style>"
                },
                {
                    "english": "<style=\"Prompt\"><size=80%>Will you stop for the <funbounce>Bone Dog</funbounce> and wash its belly?</size></style>"
                },
                {
                    "english": "<style=\"Body\">The legendary <wave><smallcaps><color=\"white\">Dog made of Bone</color></smallcaps></wave> bounces towards you, its bonetail <shake>rattling</shake> together in an excited wag. </style>"
                },
                {
                    "english": "<style=\"Body\">The Bone Dog’s tail stops <sway>wagging</sway> and he screams as a tornado flings him away. He realizes that, yet again, he is to be left at the side of the Rail."
                }
            ],
            "choice_texts": [
                {
                    "choice": "Wash",
                    "texts": {
                        "english": "<The actual choice text goes here.>"
                    },
                    "preview_obtain_texts": {
                        "english": "Get {0}.",
                        "french": "Obtenez {0}.",
                        "german": "Belohnung: {0}.",
                        "russian": "Получите: «{0}».",
                        "portuguese": "Receba {0}.",
                        "chinese": "获得{0}。",
                        "spanish": "Obtienes {0}.",
                        "chinese_traditional": "獲得{0}。",
                        "korean": "{0}를 획득합니다.",
                        "japanese": "{0}を得る。"
                    },
                    "preview_infos": [
                        {
                            "preview_type": "<insert type here>",
                            "references": [
                                "@<reference of the above type this is usually not the reward>"
                            ]
                        }
                    ]
                },
                {
                    "choice": "Deny",
                    "texts": {
                        "english": "<The actual choice text goes here.>"
                    },
                    "preview_obtain_texts": {
                        "english": "Get {0}.",
                        "french": "Obtenez {0}.",
                        "german": "Belohnung: {0}.",
                        "russian": "Получите: «{0}».",
                        "portuguese": "Receba {0}.",
                        "chinese": "获得{0}。",
                        "spanish": "Obtienes {0}.",
                        "chinese_traditional": "獲得{0}。",
                        "korean": "{0}를 획득합니다.",
                        "japanese": "{0}を得る。"
                    },
                    "preview_infos": [
                        {
                            "preview_type": "<insert type here>",
                            "references": [
                                "@<reference of the above type this is usually not the reward>"
                            ]
                        }
                    ]
                }
            ]
        }
    ],
    "rewards": [
        {
            "id": "BoneDogTitanPyreLifeRelicReward",
            "type": "<insert type here>",
            "is_story_reward": true,
            "costs": [
                100
            ],
            "extensions": [
                {
                    "<insert type here>": {}
                }
            ]
        },
        {
            "id": "BoneDogTitanCardReward",
            "type": "<insert type here>",
            "is_story_reward": true,
            "costs": [
                100
            ],
            "extensions": [
                {
                    "<insert type here>": {}
                }
            ]
        }
    ]
}

This file event_BoneDogTitan2.json should be included in your project under the json directory, and included in the c.AddMergedJsonFile( in your Plugin's Awake function.

The fileexample2.ink.json used to generate this file also needs also be included in your project, However do not include this json file in the AddMergedJsonFile call as it is NOT a Trainworks based JSON file but rather the ink script data that needs to be injected into the game's ink script data.

Be sure to edit the story_data property with the relative path to this file in regards to the project. For instance if you place example2.ink.json in the json file then the story_data property should be changed to "json/example2.ink.json"

Breaking down the fields

  • id/knot_name DO NOT edit either of these fields. Editing them requires modifying the original ink file. id is of course the id of the event, and if you would like to use the event as a followup as always you would reference the event in other places in your JSON files with the @ syntax.

  • num_runs_completed_to_see / num_classes_needed_to_show - These fields control when the event is available to show, the default values are used, so if you don't need these then feel free to delete them.

  • priority_ticket_count - The important field that controls how often the event appears in runs. The default value 10 is used which corresponds to about a 2.8-3.2% chance for your event to be selected at the first Celestial Alcove. Think of this value as the number of tickets assigned to this event. When an event is chosen for the particular node, the game sums the amount of tickets for the available events and then picks a random number, the event with the ticket "wins" and is picked for that alcove. Events can only appear once per run so the event is removed from subsequent event nodes.

  • min_distance_allowed / max_distance_allowed - This controls what rings the event can appear in. Note that Celestial Alcoves can only appear in rings 3-7.

  • story_data - This is important parameter which points to the ink.json story data you generated in the first step. This is a relative file path to this file.

  • texts - This field will be pre-populated with all of the text in the script for easy localization to other languages. You must not edit the english localization, if you need to edit this text, then you must start over with editing the ink script, compiling with inklecate again and regenerating the StoryEventData with the stub_event.py program and copying this property over.

  • choice_texts - This field will also be pre-poluated with all of the choices from the script.

    • The choice property SHOULD NOT be modified as it is required to be named exactly the same as the + [CHOICENAME] from the original ink file.

    • You are able to modify the texts property under this object to specify the exact choice text that should be presented as the choice to the player.

    • preview_obtain_texts is a special optional additional indicator to the player as to what reward the player will receive upon picking the choice. This is a format string that will generate the required text needed with the preview_infos field. If you want to give the player multiple rewards then add additional substitutions {1}, {2} etc.

    • preview_infos - This field specifies what items are actually available for preview when the player mouses over the option.

      • preview_type - This field is required to be filled out. The available options for this are as follows:

        • none

        • card

        • relic

        • enhancer

        • reward

        • coin

        • deck_reward

        • delayed_enhance_reward

        • relic_name

        • deck_rewards

      • references - This field is a reference to the item pointed to by preview_type. For instance, if preview_type is card then specify a CardData. Like the stub says this is not generally the actual reward you are giving the player, but the actual item that is given to the player.

Editing the StoryEvent

Defining Rewards

Event GameObjects

Definining FollowupEvents

Testing your Event

⚠️ **GitHub.com Fallback** ⚠️