Skip to content

Modding Page

Coke And Code edited this page Jul 12, 2023 · 14 revisions

Unearthed Modding

Overview

The Unearthed game supports modding through "mods" that make use of the API provided by the game. A mod is registered by the server owner and (at time of writing) runs on the server side (that is the browser thats serving the game to the others).

Mods can change tools, blocks and mobs - including adding and changing the game assets. Mods can be single javascript files (where no assets are required)

Some sample mods can be installed from the Mod Install Page.

The general shape of a mod looks like this in a single file named mod.js:

new class {
    id = "MyUniqueId";
    name = "MyName";
    version = 1;
    // don't have to set this yet but its here for forward compatibility
    apiVersion = 0; 

    // ... life cycle and event methods ...
}

A mod life cycle follows this flow:

  1. The mod is invoked and created as above
  2. The onGameStart is called if it exists
  3. The onWorldStart is called if it exists
  4. The onTick is called every frame if it exists
  5. The event handlers are called as game state is updated

Install a Mod

To install a mod:

  • Go into "Settings > Server Settings"
  • Click Add (to add a Mod)
  • Upload a single JS file or a zip file of the mod

API for Modding

The full API for mods is documented here: https://kevglass.github.io/unearthed/ and is described here: https://github.com/kevglass/unearthed/blob/main/src/mods/Mods.ts. While a mod can access other internal state in the game this may not be supported in the future - fair warning.

The key classes

  • ServerMod - The interface that mods need to implement. Mandatory name, id and version. Everything else is opt-in.
  • GameContext - The interface that represents the game when you're in the mod code. Use this to interact with the game.
  • MobContext - The interface that represents any mob (players included) when you're in the mod code. Use this to interact with a specific mob.

Example Mods in Code

Theres a few example mods available:

Debugging

The Javascript console is your friend. The Game API provides the log() and error() functions to allow a mod to print out content in its context to the javascript console. All calls to a mod are wrapped to catch any errors which will also be displayed in the Javascript console in the context of the mod.

In general you want to keep the Javascript console open while you're working on a mod or if anything appears to go wrong.

Worked Examples

The following examples take code from the sample mods and explain it.

Changing Assets

A common request is to change the graphics in the game. All graphics in the game are stored in a cache, keyed on a string based ID. Mods can add graphics to that cache by using GameContext.addImage. If the symbolic name thats used matches an existing resource it'll be replaced - in this way you can override any graphic in the game.

So for example, in the Hello World mod you'll see:

onGameStart = (game) => {
  game.addImage("tiles/tnt", game.getModResource("tntalt.png"));
};

The mod has a image packaged with the javascript in a zip file, named "tntalt.png". To get a reference to that image we use game.getResource. This reference can then be used to add an image to the cache with game.addImage. In this case we use the symbolic name "tiles/tnt" which happens to be the name the game uses for the TNT sprite. This override the original TNT sprite and shows the mod's new one instead.

The list of symbolic names used by the core game can be seen displayed in the Javascript console when the game loads, e.g.:

image

Adding a Tool

Add a tool to the game puts a new item in player's inventory. They'll appear in the tool section. A tool can either place a new block (like the block tools in the default game) or take an action on a location (like the pick axe). When defining a tool you can either give it a block to place or a tool ID that you'll receive back when a player tries to use the tool. So, in the following we add a wand item to the inventory:

onGameStart = (game) => {
    game.addTool("holding/wand", 0, "magic-wand", true, false);
};

In this case the graphic to be used for the wand is stored against the cache ID "holding/wand". It's going to place block 0 - this indicates that it doesn't place a block. If this was a tool that placed a block we'd see the ID of the block here. The tool ID we've given is "magic-wand" (see the callback below). Finally the true and false on the end indicates that this tool is used to target empty locations but not full locations (locations that have a block in). The pick axe for instance would have a false and true here, because it targets filled locations.

The mod has a listener configured to complete the action of the wand. It looks like this:

onUseTool = (game, player, x, y, layer, toolId) => {
    // if the player has waves our magic wand at a location
    if (toolId === "magic-wand") {
        // if theres no block already there
        if (game.getBlock(x, y, 0) === 0) {
            // create a block of a random type
            game.setBlock(x, y, 0, Math.floor(Math.random() * 5) + 10);
            // play a whizzy sound effect
            game.playSfx("foreground", 0.5);
            // add some particles for style!
            game.addParticlesAtTile("particles/blue", x, y, 10);
            game.addParticlesAtTile("particles/white", x, y, 10);
         }
     }
};

Every time a tool is used in game this listener is going to be called on the mod. First we check if the player is using our tool (identified by our tool ID "magic-wand"). Next we check if the location they've acted upon is empty. If both conditions are met we want to apply our wand logic.

First we set the targeted location to a random block. Next we play a sound effect identified by the cache ID "foreground" at half volume. Finally splat some particles in there to make it look like magic!

Adding a Block Type

Another key thing, blocks!? To add a new block we just register it against an ID with the game like so:

onGameStart = (game) => {
    game.addImage("tiles/bounce", game.getModResource("block.png"));
    game.addBlock(110,
        {
            sprite: "tiles/bounce",
            blocks: true,
            blocksDown: true, 
            ladder: false, 
            needsGround: false, 
            blocksDiscovery: false, 
            leaveBackground: false, 
            blocksLight: false
        },
     );
     game.addTool("tiles/bounce", 110, "", false);
};

First we load an image for the new sprite and register it against the ID "tiles/bounce". This puts it in the cache ready for a new block to use.

Next we register the actual tile. We've allocated block ID 110 for this particular block. That means in the game map IDs of 110 will placed for our new block. We then have a series of properties of the block to define. You only actually have to specify the true ones but I've shown them all for ease. You can see the current list of block properties in the code here: https://github.com/kevglass/unearthed/blob/main/src/Block.ts

Here's a quick run down for the ones above.

  • sprite - the image to display for this block.
  • blocks - whether this block should prevent the player moving from all directions
  • blocksDown - whether this block should prevent the player falling though it (this is useful for thing platforms that you can jump up into)
  • ladder - indicates whether this block should act as a ladder, that is you can climb up and down on it.
  • needsGround - if set to true this block must have a block beneath it. If that block is removed so is this block. Useful for plants growing out of things.
  • leavesBackground - used to control whether when a block on the foreground is destroyed it leaves a copy of itself in an empty background. Good for mining scenarios.
  • blocksLight - indicates if this block prevents lights passing through it.

Finally in this piece of code we add a tool to place this new block. You can see it's configured with the same image ID, "tiles/bounce". It uses a block placement of 110, matching our new block and doesn't need a tool ID since we're placing a block.

Reacting to Collisions

There are a series of different listeners a mod can react to. A set of these are related to mobs colliding with the world, including onStandOn, onHitHead and onBlocked. In the example mod for bouncy blocks we use the standing on listener like so:

onStandOn = (game, mob, x, y) => {
    // if we're standing on a bounce block then change the velocity of the player
    if (game.getBlock(x, y, 0) === 110) {
        // we want to allow the player to walk across the block with out bouncing
        if (mob.vy > 1) {
            mob.vy = -25;
        }
    }
};

Every time any player is blocked from falling the mod function onStandOn is called telling us which mob was falling and which location blocked their fall. In this case we check if the block they've fallen on to is our bouncy block (block ID 110). If it is, then we want the player to bounce up off the block. However, we only want that to happen if the player is falling - not if they just walk across the block.

To achieve this we check if the mob is currently moving down the screen (checking velocity y component - vy). If its moving at a reasonable rate then it's likely falling so adjust the mob's velocity so it bounces off.

Reacting to Events

Another aspect of the game is the manual triggering of events in game. We have the trigger button that the player can use at any time and a mod can react. In the switch blocks example we add a new block with ID 122 that acts as a switch that the player can press to toggle the state of other blocks in the world. These other blocks have two states (on and off) and are assigned the IDs 121 and 120.

In the mod we add a listener for the trigger button like so:

onTrigger = (game, mob, x, y) => {
    // if the player has triggered our switch
    if (game.getBlock(x,y,0) === 122) {
        if (this.switchState === 0) {
            this.switchState = 1;
            // we're turning block on so scan through and replace
            game.replaceAllBlocks(121, 120);
        } else {
            this.switchState = 0;
            game.replaceAllBlocks(120, 121);
        }
    }
};

When a mob triggers a location the mod is invoked. We start by checking if they're triggering on the tile we configured for the switch (122). If they are we then use a piece of internal mod state (you can add as much as you like) to check whether we should be turning blocks on or off. In either case we simply toggle all the blocks of one type to the other.