Adding campaigns

The game_master service is responsible for managing the different campaigns for the players. We can trigger different campaigns depending on the kingdom that the player chose, and also depending on factors such as the biome, game mode, etc.

What are campaigns, arcs and encounters?

Campaigns are narrative-guided gameplay events (for example the goblin campaign, the trader campaign, etc).

They are divided into arcs, each of which fulfills a dramatic/gameplay purpose, and which are made up of encounters.

Encounters are campaign-specific instances of generic, modular, gameplay pieces. A generic encounter (such as spawn camp, or show dialog) can be re-skinned or instantiated with a specific campaign in mind, and put together with other encounters to create many possible narratively-coherent gameplay challenges.

For example, the goblin campaign, which throws increasing numbers of enemies at the player, is made of three arcs (beginning, hit-and-run, and finale) and multiple goblin-specific instances of a few basic, generic encounters (spawn camp, wait, city raid, etc).

Game master tree

All encounters, arcs, and campaigns are nodes (they inherit from stonehearth/services/server/game_master/controllers/node.lua) and exist in a tree, with the game master controller as root node. You can visualize them in the Campaign browser from debugtools:

campaign_browser_tree

The game master has campaigns (as children), campaigns have arcs (Trigger, Challenge, Climax), and arcs have encounters. Encounters themselves actually run the content using a reusable script (e.g.: create_camp_encounter.lua) and the JSON data of the specific node listed in the arc index file (e.g.: ambient_threats_arc.json) to customize the encounter parameters.

All the existing campaigns, arcs and encounters can be found inside subdirectories of stonehearth/data/gm.

In-edges and out-edges

Each encounter node needs to have what we call an "in_edge" and can have zero, one, or more "out_edge"s.

These are used to shape a graph or tree that defines the flow of the campaign. The in_edge is a custom identifier, usually the same name than the encounter file, so that different nodes can trigger the same encounters. Example from daily_report_encounter.json:

  "in_edge": "daily_report_encounter",

icon For the first encounter of each arc, the "in_edge" must be "start". You can only have one encounter with this in_edge per arc.

For out_edges, the syntax will vary. Examples:

We can also mix types, like this:

  "out_edge" : {
     "type" : "trigger_many",
     "out_edges" : [
        "trigger this edge always",
        {
           "type" : "trigger_one",
           "out_edges" : [
              "this edge has weight 1 (20% chance)",
              {
                 "type" : "weighted_edge",
                 "out_edge" : "this edge has weight 4 (80% chance)",
                 "weight" : 4
              }
           ]
        }
     ]
  }

To signal the end of the current arc, allowing the campaign to start another arc, we use "arc:finish":

  "out_edge" : "arc:finish"

Other encounters from this arc might still be around, but they should resolve themselves on their own.

icon Some of the encounters will define their out edges inside their info section so that they can control which one will spawn depending on how the encounter ends, so they won't have the "out_edge" property at the root level of the JSON.

Starting encounters conditionally

There are some encounters that are used to check for conditions themselves, in order to trigger their out edges when the condition is met.

But we can also add checks in any encounter JSON file to allow the encounter to start only under certain conditions.

These are added in a "can_start" : {} section, normally below the in/out edges. For example:

  "can_start": {
     "test_1": {
        "game_mode_check": {
           "type": "deny_if",
           "item": "game_mode",
           "value": "stonehearth:game_mode:peaceful"
        }
     }
  }

Here, "test_1" is a custom identifier for the test set. If we have more than one set, the encounter will start when ANY of them passes. We can have more than one check inside a test, in that case the encounter will start only when ALL of its checks pass.

We can use whatever name for the checks ("game_mode_check" in the example above), because what matters is the properties inside:

The custom name that we use for the check can be used in game mode tuning overrides. If you create a custom encounter type, you can have a custom can_start function in it too.

Planning your campaigns

You can make many types of encounters, from a storyline that evolves with player choices, to just random ambient encounters, like the ambient threats from the stonehearth mod.

It is important that you plan carefully what you want to add to your campaigns and work on it little by little, since depending on the encounter type, creating chains of encounters can involve many files.

Once you know how will your campaign develop, think which of the different encounter types available is most suited for your encounters, and if none is useful, create your own reusable encounter type, or use a script encounter instead.

Reusing the existing encounter types allows us to create campaigns by using only JSON files, which is easy. For script encounters or custom controllers, you will need to use Lua.

How to create campaigns

To create campaigns you can copy the stonehearth/data/gm/gm_index.json file and edit it.

Make sure to add this alias to your manifest:

  "aliases" : {
     "data:gm_index" : "file(data/gm/gm_index.json)"
  }

Then add a mixinto to the gm index of the stonehearth mod:

  "mixintos" : {
     "stonehearth:data:gm_index" : "my_mod:data:gm_index"
  }

If you want to remove campaigns from the stonehearth mod so that they don't run when your mod is enabled, you can use mixintypes, but remember that it may affect other mods that assume that those campaigns will run.

Your gm_index.json file will look something like this (example from the northern_alliance mod):

  {
     "type": "index",
     "campaigns": {
        "hunting": {
           "animal_camps": "file(campaigns/hunting/animal_campaign.json)"
        },
        "town_progression": {
           "na_town_progression": "file(campaigns/town_progression/town_progression_campaign.json)"
        }
     }
  }

Each key under "campaigns" is a custom identifier (the campaign subtype), and contains a campaign node name pointing to a campaign index file path.

How to create arcs

Once you've created your campaign file, it's time to edit it. Continuing from the example above, this is from animal_campaign.json:

  {
     "type": "campaign",
     "rarity": "common",
     "can_start": {
        "test_1": {
           "kingdom_check": {
              "type": "deny_if_not",
              "item": "kingdom",
              "value": "northern_alliance:kingdoms:northern_alliance"
           }
        }
     },
     "arcs": {
        "trigger": {
           "migratory_animals": "file(arcs/migratory_animals_arc.json)"
        },
        "challenge": {},
        "climax": {}
     }
  }

We can have can_start checks for preventing the campaign to start here. The "rarity" is not really implemented.

Then we have an "arcs" section. Arcs progress from "trigger" to "challenge" to "climax". Only the "trigger" arc is mandatory. Many campaigns such as the ambient threats and trader campaigns only use the trigger arc.

We define only one arc file per each, with an identifier name for debugging, pointing to the arc index JSON file. An example of what it should contain (from the kitties campaign):

  {
     "type": "arc",
     "rarity": "common",
     "encounters": {
        "kitty_generator": "file(encounters/kitty_generator.json)",
        "spawn_koda": "file(encounters/spawn_koda.json)",
        "spawn_jacko": "file(encounters/spawn_jacko.json)",
        "do_nothing": "file(encounters/do_nothing.json)"
     }
  }

The "type" is "arc". Inside "encounters" we list all of the encounter JSON files that we'll use in our campaign (add them as you create them). The identifiers for the encounters only have lowercase, numbers and underscores by convention. Try to use short, descriptive names.

How to create encounters

Encounters are just JSON files, like the campaign and arc indexes. We can organize our GM files in subfolders, so that every folder contains the encounters of a different arc, etc.

In general, encounter files will have the following:

  {
     "type": "encounter",
     "encounter_type": "create_camp",
     "rarity": "common",
     "in_edge": "spawn_friendly_entities",
     "out_edge": {
        "type": "trigger_many",
        "out_edges": [
           "spawn_enemy_entities",
           "wait_for_duration"
        ]
     },
     "create_camp_info": {
     }
  }

As mentioned before, the "rarity" is not really used.

How to create your own encounter type

You can create a new type of reusable encounter in your mod (pretty similar to the script encounter but more organized). To do this:

All encounters (and all scripts started from encounters) are controllers. More on controllers in this section of the guide. So you can use lifecycle functions if you need to. These controllers need to have the start(ctx,info) function, and may also have other functions, such as stop(), suspend()/continue() or can_start(ctx), besides any custom function that you create.

You can find the controllers for the existing encounter types in stonehearth/services/server/game_master/controllers/encounters. There's also other scripts used for encounters in other subfolders of stonehearth/services/server/game_master/controllers.

How to test your campaigns with debugtools

Refer to the section about the Campaign Browser for how to use it.

Editing campaigns with SHED

SHED has a great tool for creating encounters. At the time that this guide was written, you must prepare the gm_index.json, the campaign and arc JSON files before using it.

  1. Add an alias in your mod pointing to your gm_index.

    "aliases": {
       "data:gm_index" : "file(data/gm/gm_index.json)"
    }
    

    icon The alias has to be exactly "data:gm_index", otherwise SHED won't be able to find it. Also, remember to add a mixinto to the stonehearth's gm_index so that you can test your campaigns in the game.

  2. Open SHED and click on the "Encounter Designer" tab.

    You should see a tree listing the campaigns of all the mods you have ready in your folder. Click on one of the campaigns and you should see a graph at the right displaying what the campaign looks like:

    goblin_campaign

    You can zoom in/out of the graph with the mouse wheel, and you can pan around the graph with the third mouse button (press the mouse wheel, hold it down, and move the mouse). You can see subtrees in the graph being highlighted when you hover over them.

    Whenever you change a node, remember to save the changes and click on the "Refresh Encounters" button at the top right, so that the graph gets updated (for example, imagine you create a new encounter/node, it will be unattached to the graph. But if you edit another node so that it points to your new node as an out_edge, that won't be reflected in the graph until you reload/refresh). Sometimes it will refresh automatically, for example when deleting nodes.

  3. To create an encounter: right-click in an empty part of the tree/graph, go to 'Add New Node->' and choose the type of the encounter you want to add (only default encounter types are supported, custom ones won't appear).

    Then, a dialog will appear. Go to the encounters folder of your choice (it might be pointing to the wrong arc perhaps) and write a name for your new JSON file. It is recommended that the name for the file is in lowercase and with no spaces or symbols. It can contain underscores and numbers, though.

    Once you click on 'Save', the JSON file will be created and the encounter will get added to the list of encounters (if you have more than one arc it won't, but right-clicking on the node in the graph will show you an option to parent it to the arc of your choice). If you don't see it added you might need to refresh the encounters.

    After it is created, select its node and change some of the default values to get rid of the errors in the file, such as the in/out edges, etc. If it's your first encounter, make sure to give it an in edge of "start", so that it gets parented to the arc node after refreshing and starts shaping the tree.

    You can also clone an encounter or delete it via context menu action (right-clicking on the encounter node). It will add/remove the encounter to the arc JSON file automatically. When deleting, it will really delete the JSON file (unlike when removing aliases in the Manifest tab) so make sure that you truly want to delete it.

You should see several options in the bar above the JSON editor. The first one is to open the current file in your default text editor (for JSON files), the second one is for opening the containing folder of this file, the third one is to save, and the last one is to localize the file (remember that sometimes localizing might not work). If you added mixins, the preview with mixins button should also appear at the end.

When you save, the JSON will be formatted nicely if you had some bad indentations.

The JSON editor in SHED supports autocomplete and hints for encounters, which is extremely useful. If you don't get the dropdown for a property, try deleting everything until the property name, then type the ':' and see if it appears. Navigate through the options with the arrow keys and press Enter or double click to confirm an option:

auto_complete

If the color at the left of the file is green, everything's fine. When it's red, SHED won't let you save the file (syntax errors). Hovering over the arrows at the left will show a tooltip describing the problem. If it's orange, there might be some missing field or some data that SHED doesn't recognize, but will let you proceed. When it's gray it means that SHED isn't checking for errors in this file because it doesn't know what should it contain, so you're responsible for what you're writing.

These are different node types you can see in the graph and their meaning: