Skip to content

Modding Page

Coke And Code edited this page Jul 2, 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)

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

(() => {
    return new class {
        id = "MyUniqueId";
        name = "MyName";
        version = 1;

        // ... 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 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);
};

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 on the end indicates that this tool is used to target empty locations. The pick axe for instance would have a false 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

Reacting to Events

How to react to events

Clone this wiki locally