Mod Creation/Essentials: Difference between revisions

From Melvor Idle
(Clarified that traditional function expressions are more suitable than arrow functions for method patching)
Tag: visualeditor-switched
 
(18 intermediate revisions by 2 users not shown)
Line 13: Line 13:
Before you begin writing code, it's a good idea to start by defining some metadata in the manifest.json file. A complete manifest.json might look like the following:
Before you begin writing code, it's a good idea to start by defining some metadata in the manifest.json file. A complete manifest.json might look like the following:


  <nowiki>{
  <syntaxhighlight lang="js" line>{
   "namespace": "hello-world",
   "namespace": "helloWorld",
   "icon": "assets/icon.png",
   "icon": "assets/icon.png",
   "setup": "src/setup.mjs",
   "setup": "src/setup.mjs",
   "load": ["assets/style.css"]
   "load": ["assets/style.css"]
}</nowiki>
}</syntaxhighlight>


==== namespace?: string ====
==== namespace?: string ====
Line 26: Line 26:
The namespace can only contain alphanumeric characters and underscores and cannot start with the word "melvor".
The namespace can only contain alphanumeric characters and underscores and cannot start with the word "melvor".


{|
{| class="wikitable"
! Namespace !! Valid !!
! Namespace !! Valid  
|-
|-
| <code>helloWorld</code> || ✔️
| <code>helloWorld</code> || ✔️
Line 43: Line 43:


An optional icon to be displayed alongside your mod in a number of places, like the My Mods list in the Mod Manager. The value should be the path to the image file relative to the root of your mod (where your manifest is located). Accepted file types for an icon are <code>.png</code> or <code>.svg</code>, and the icon is typically displayed at a maximum of 38px in-game.
An optional icon to be displayed alongside your mod in a number of places, like the My Mods list in the Mod Manager. The value should be the path to the image file relative to the root of your mod (where your manifest is located). Accepted file types for an icon are <code>.png</code> or <code>.svg</code>, and the icon is typically displayed at a maximum of 38px in-game.
Alternatively, you can supply an absolute path to a web URL and that will be used instead.


==== setup?: string ====
==== setup?: string ====
Line 54: Line 56:
''This property is '''required''' only if the <code>"setup"</code> property is not present in the manifest.''
''This property is '''required''' only if the <code>"setup"</code> property is not present in the manifest.''


This value accepts either a single path or an array of paths to resources to load. These resources are loaded in the order of the array, after the <code>"setup"</code> resource has been run. Valid resources to be loaded through this property are JavaScript script files (<code>.js</code>), JavaScript module files (<code>.mjs</code>), CSS stylesheets (<code>.css</code>), and HTML files containing templates (<code>.html</code>). However, unless your mod is very simple, the recommended approach to loading JavaScript resources (<code>.js</code> or <code>.mjs</code>) is through code in your mod's entry-point (<code>"setup"</code>).
This value accepts either a single path or an array of paths to resources to load. These resources are loaded in the order of the array, after the <code>"setup"</code> resource has been run. Valid resources to be loaded through this property are JavaScript script files (<code>.js</code>), JavaScript module files (<code>.mjs</code>), CSS stylesheets (<code>.css</code>), JSON files containing game data packages (<code>.json</code>), and HTML files containing templates (<code>.html</code>). However, unless your mod is very simple, the recommended approach to loading JavaScript resources (<code>.js</code> or <code>.mjs</code>) is through code in your mod's entry-point (<code>"setup"</code>).


It's also important to note that while <code>.js</code> is considered a valid extension for JavaScript module files for the "setup" property, modules loaded through "load" must end with <code>.mjs</code> or they will be treated as regular script files.
It's also important to note that while <code>.js</code> is considered a valid extension for JavaScript module files for the "setup" property, modules loaded through "load" must end with <code>.mjs</code> or they will be treated as regular script files.
Line 68: Line 70:
Let's start with what a module that's defined as your mod's <code>"setup"</code> entry-point should look like:
Let's start with what a module that's defined as your mod's <code>"setup"</code> entry-point should look like:


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup(ctx) {
export function setup(ctx) {
   console.log('Hello World!');
   console.log('Hello World!');
}</nowiki>
}</syntaxhighlight>


We export a function named <code>setup</code> here because that is what the Mod Manager looks for when loading a <code>"setup"</code> module. Without one, an error would be thrown when loading this mod. This <code>setup</code> function is called and receives the mod's context object as soon as the mod is loaded, which happens just before the character select screen is visible. Therefore, this mod would write 'Hello World!' to the console at that time.
We export a function named <code>setup</code> here because that is what the Mod Manager looks for when loading a <code>"setup"</code> module. Without one, an error would be thrown when loading this mod. This <code>setup</code> function is called and receives the mod's context object as soon as the mod is loaded, which happens just before the character select screen is visible. Therefore, this mod would write 'Hello World!' to the console at that time.
Line 79: Line 81:
If we define a helper module helper.mjs:
If we define a helper module helper.mjs:


  <nowiki>// helper.mjs
  <syntaxhighlight lang="js" line>// helper.mjs
export function greet(name) {
export function greet(name) {
   console.log(`Hello, ${name}!`);
   console.log(`Hello, ${name}!`);
}</nowiki>
}</syntaxhighlight>


We can then use code we export in our setup function:
We can then use code we export in our setup function:


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export async function setup({ loadModule }) {
export async function setup({ loadModule }) {
   const { greet } = await loadModule('helper.mjs');
   const { greet } = await loadModule('helper.mjs');
   greet('Melvor'); // > Hello, Melvor!
   greet('Melvor'); // > Hello, Melvor!
}</nowiki>
}</syntaxhighlight>


If you need to access the context object from your helper module, there are two approaches:
If you need to access the context object from your helper module, there are two approaches:
Line 96: Line 98:
1. Pass the context object from the setup function to the loaded module:
1. Pass the context object from the setup function to the loaded module:


  <nowiki>// configService.mjs
  <syntaxhighlight lang="js" line>// configService.mjs
export function init(ctx) {
export function init(ctx) {
   // Perform actions using the context object here...
   // Perform actions using the context object here...
Line 105: Line 107:
   const configService = await ctx.loadModule('configService.mjs');
   const configService = await ctx.loadModule('configService.mjs');
   configService.init(ctx);
   configService.init(ctx);
}</nowiki>
}</syntaxhighlight>


2. Use the <code>getContext</code> method on the global <code>mod</code> object:
2. Use the <code>getContext</code> method on the global <code>mod</code> object:


  <nowiki>// configService.mjs
  <syntaxhighlight lang="js" line>// configService.mjs
const ctx = mod.getContext(import.meta);
const ctx = mod.getContext(import.meta);


export function init() {
export function init() {
   // Perform actions using the context object here...
   // Perform actions using the context object here...
}</nowiki>
}</syntaxhighlight>


You must pass <code>import.meta</code> - a special JavaScript object available in all modules - to the <code>mod.getContext</code> method to receive your mod's context object.
You must pass <syntaxhighlight lang="js" inline>import.meta</syntaxhighlight> - a special JavaScript object available in all modules - to the <syntaxhighlight lang="js" inline>mod.getContext</syntaxhighlight> method to receive your mod's context object.


==== Using Scripts ====
==== Using Scripts ====
Line 124: Line 126:
Loading a script through the context object is very similar to loading a module but you will not receive back a value.
Loading a script through the context object is very similar to loading a module but you will not receive back a value.


  <nowiki>export async function setup({ loadScript }) {
  <syntaxhighlight lang="js" line>export async function setup({ loadScript }) {
   // Make sure you await the call to loadScript if your code beyond relies on it
   // Make sure you await the call to loadScript if your code beyond relies on it
   await loadScript('hello-melvor-script.js');
   await loadScript('hello-melvor-script.js');
Line 131: Line 133:
   // But don't bother awaiting it if it's not time-sensitive
   // But don't bother awaiting it if it's not time-sensitive
   loadScript('some-independent-script.js');
   loadScript('some-independent-script.js');
}</nowiki>
}</syntaxhighlight>


From inside your script, you can still access the context object:
From inside your script, you can still access the context object:


  <nowiki>mod.register(ctx => {
  <syntaxhighlight lang="js" line>mod.register(ctx => {
   // Use the context object here
   // Use the context object here
});</nowiki>
});</syntaxhighlight>


Note that the mod.register method will only work on scripts injected through either loadScript or the "load" property of the manifest.
Note that the mod.register method will only work on scripts injected through either loadScript or the "load" property of the manifest.
Line 153: Line 155:
=== Load (Import) a Module ===
=== Load (Import) a Module ===


Use <code>ctx.loadModule</code> to import a JavaScript module's exported features.
Use <syntaxhighlight lang="js" inline>ctx.loadModule</syntaxhighlight> to import a JavaScript module's exported features.


  <nowiki>// my-module.mjs
  <syntaxhighlight lang="js" line>// my-module.mjs
export function greet(name) {
export function greet(name) {
   console.log(`Hello, ${name}!`);
   console.log(`Hello, ${name}!`);
}
}


export const importantData = ['e', 'r', 'e', 'h', 't', ' ', 'o', 'l', 'l', 'e', 'h'];</nowiki>
export const importantData = ['e', 'r', 'e', 'h', 't', ' ', 'o', 'l', 'l', 'e', 'h'];</syntaxhighlight>


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export async function setup({ loadModule }) {
export async function setup({ loadModule }) {
   const myModule = await loadModule('my-module.mjs');
   const myModule = await loadModule('my-module.mjs');
   myModule.greet('Melvor'); // Hello, Melvor!
   myModule.greet('Melvor'); // Hello, Melvor!
   console.log(myModule.importantData.reverse().join('')); // hello there
   console.log(myModule.importantData.reverse().join('')); // hello there
}</nowiki>
}</syntaxhighlight>


=== Load (Inject) a Script ===
=== Load (Inject) a Script ===


Use <code>ctx.loadScript</code> to inject a JavaScript file into the page.
Use <syntaxhighlight lang="js" inline>ctx.loadScript</syntaxhighlight> to inject a JavaScript file into the page.


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export await function setup({ loadScript }) {
export await function setup({ loadScript }) {
   // Wait for script to run
   // Wait for script to run
Line 179: Line 181:
   // Or not
   // Or not
   loadScript('my-independent-script.js');
   loadScript('my-independent-script.js');
}</nowiki>
}</syntaxhighlight>


=== Load (Inject) HTML Templates ===
=== Load (Inject) HTML Templates ===


Use <code>ctx.loadTemplates</code> to inject all <code>&lt;template&gt;</code> elements into the document body.
Use <syntaxhighlight lang="js" inline>ctx.loadTemplates</syntaxhighlight> to inject all <syntaxhighlight lang="html" inline><template></syntaxhighlight> elements into the document body.


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ loadTemplates }) {
export function setup({ loadTemplates }) {
   loadTemplates('my-templates.html');
   loadTemplates('my-templates.html');
}</nowiki>
}</syntaxhighlight>


=== Load (Inject) a Stylesheet ===
=== Load (Inject) a Stylesheet ===


Use <code>ctx.loadStylesheet</code> to inject a CSS file into the page.
Use <syntaxhighlight lang="js" inline>ctx.loadStylesheet</syntaxhighlight> to inject a CSS file into the page.


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ loadStylesheet }) {
export function setup({ loadStylesheet }) {
   loadStylesheet('my-styles.css');
   loadStylesheet('my-styles.css');
}</nowiki>
}</syntaxhighlight>


=== Load Data from JSON ===
=== Load Data from JSON ===


Use <code>ctx.loadData</code> to read and automatically parse a JSON resource.
Use <syntaxhighlight lang="js" inline>ctx.loadData</syntaxhighlight> to read and automatically parse a JSON resource.


  <nowiki>// my-data.json
  <syntaxhighlight lang="js" line>// my-data.json
{
{
   "coolThings": [
   "coolThings": [
     "rocks"
     "rocks"
   ]
   ]
}</nowiki>
}</syntaxhighlight>
 
<small>''Comments in JSON are purely illustrative and not valid markup''</small>


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export async function setup({ loadData }) {
export async function setup({ loadData }) {
   const data = await loadData('my-data.json');
   const data = await loadData('my-data.json');
   console.log(data.coolThings[0]); // ['rocks']  
   console.log(data.coolThings[0]); // ['rocks']  
}</nowiki>
}</syntaxhighlight>


=== Images, Sounds, and Anything Else ===
=== Images, Sounds, and Anything Else ===


Nearly any resource can be accessed and used in some way with <code>ctx.getResourceUrl</code> - the helper methods above all use this behind the scenes. With the resource's URL, you can use built-in JavaScript methods to consume the resource.
Nearly any resource can be accessed and used in some way with <syntaxhighlight lang="js" inline>ctx.getResourceUrl</syntaxhighlight> - the helper methods above all use this behind the scenes. With the resource's URL, you can use built-in JavaScript methods to consume the resource.


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ getResourceUrl }) {
export function setup({ getResourceUrl }) {
   const url = getResourceUrl('sea-shanty-2.ogg');
   const url = getResourceUrl('sea-shanty-2.ogg');
Line 226: Line 230:
   song.loop = true;
   song.loop = true;
   song.play();
   song.play();
}</nowiki>
}</syntaxhighlight>


== Game Lifecycle Hooks ==
== Game Lifecycle Hooks ==
Line 241: Line 245:
All of the game's lifecycle hooks are available through your mod's context object and accept a callback function as a sole parameter. This callback function can be synchronous or asynchronous and will be executed at the specified time and receive your mod's context object as a parameter.
All of the game's lifecycle hooks are available through your mod's context object and accept a callback function as a sole parameter. This callback function can be synchronous or asynchronous and will be executed at the specified time and receive your mod's context object as a parameter.


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ onModsLoaded, onCharacterLoaded, onInterfaceReady }) {
export function setup({ onModsLoaded, onCharacterLoaded, onInterfaceReady }) {
   onModsLoaded(ctx => {
   onModsLoaded(ctx => {
Line 258: Line 262:
     // Build or modify in-game UI elements
     // Build or modify in-game UI elements
   });
   });
}</nowiki>
}</syntaxhighlight>
 
== Adding and Modifying Game Objects ==
 
''View this topic's relevant API reference here [[Mod Creation/Mod Context API Reference#Game Object Registration]].''
 
Mods can now register or modify game objects (items, skills, pages, etc.) in a streamlined way. The entry point for doing so is either the <code>"load"</code> property of your <code>manifest.json</code>, or the <code>gameData</code> endpoint within the mod context API. There is massive variety on what data is needed between different game object types but the general concept is the same. You will need to either define a data package using JSON and load that into the game, or you can dynamically build one via code (certain dynamic objects like skills requires the latter).
 
=== Defining a Data Package ===
 
The first, simpler option for building game object data is defining all (or as much as possible) data in a <code>.json</code> file that is then read into the game to register your game objects.
 
'''Pros'''
* Simpler
* More easily separate data from your mod's logic
* Your text editor can provide typing by defining the <code>$schema</code> property in your JSON file
 
'''Cons'''
* Does not support all game object types, such as skills
 
To begin with this approach, your JSON files should all be constructed with:
 
<syntaxhighlight lang="js" line>{
  "$schema": "https://melvoridle.com/assets/schema/gameData.json",
  "data": {
 
  }
}</syntaxhighlight>
 
If you're using a text editor that supports it, you should now get autocomplete and type checking on the fields you create.
 
Here is an example of defining an item:
 
<syntaxhighlight lang="js" line>{
  "$schema": "https://melvoridle.com/assets/schema/gameData.json",
  "data": {
    "items": [{
      "id": "Wooden_Dagger",
      "name": "Wooden Dagger",
      "category": "Combat",
      "type": "Weapon",
      "media": "wooden-dagger.png",
      "ignoreCompletion": false,
      "obtainFromItemLog": false,
      "golbinRaidExclusive": false,
      "sellsFor": 0,
      "tier": "wooden",
      "validSlots": ["Weapon"],
      "occupiesSlots": [],
      "equipRequirements": [
        {
          "type": "SkillLevel",
          "skillID": "melvorD:Attack",
          "level": 1
        }
      ],
      "equipmentStats": [
        { "key": "attackSpeed", "value": 2200 },
        { "key": "stabAttackBonus", "value": 4 },
        { "key": "slashAttackBonus", "value": 1 },
        { "key": "blockAttackBonus", "value": 4 },
        { "key": "meleeStrengthBonus", "value": 1 }
      ],
      "itemType": "Weapon",
      "attackType": "melee"
    }]
  }
}</syntaxhighlight>
 
You would then register your game data using one of the following methods:
 
<syntaxhighlight lang="js" line>// manifest.json
{
  "namespace": "helloWorld",
  "load": ["path-to-your-data.json"]
}</syntaxhighlight>
 
<small>''Comments in JSON are purely illustrative and not valid markup''</small>
 
''or''
 
<syntaxhighlight lang="js" line>// setup.mjs
export async function setup({ gameData }) {
  await gameData.addPackage('path-to-your-data.json');
}</syntaxhighlight>
 
=== Building a Data Package at Runtime ===
 
The other option for building game object data is doing so dynamically through the mod context API.
 
'''Pros'''
* Can be used to register any type of game object
* Enables the ability to dynamically build game objects
 
'''Cons'''
* Messier and more complex
* No type support at the moment
 
The entry-point for using this approach looks like this:
 
<syntaxhighlight lang="js" line>// setup.mjs
export function setup({ gameData }) {
  gameData.buildPackage((p) => {
    // use the `p` object to add game objects
  }).add();
}</syntaxhighlight>
 
Following the same example above of adding an item:
 
<syntaxhighlight lang="js" line>// setup.mjs
export function setup({ gameData }) {
  gameData.buildPackage((p) => {
    p.items.add({
      id: 'Wooden_Dagger',
      name: 'Wooden Dagger',
      category: 'Combat',
      type: 'Weapon',
      media: 'wooden-dagger.png',
      ignoreCompletion: false,
      obtainFromItemLog: false,
      golbinRaidExclusive: false,
      sellsFor: 0,
      tier: 'wooden',
      validSlots: ['Weapon'],
      occupiesSlots: [],
      equipRequirements: [{
        type: 'SkillLevel',
        skillID: 'melvorD:Attack',
        level: 1
      }],
      equipmentStats: [
        { key: 'attackSpeed', value: 2200 },
        { key: 'stabAttackBonus', value: 4 },
        { key: 'slashAttackBonus', value: 1 },
        { key: 'blockAttackBonus', value: 4 },
        { key: 'meleeStrengthBonus', value: 1 }
      ],
      itemType: 'Weapon',
      attackType: 'melee'
    });
  }).add();
}</syntaxhighlight>
 
Your game data should already be registered from the <code>.add()</code> method being called on your built package.


== Mod Settings ==
== Mod Settings ==


''View this topic's relevant API reference here [[Mod Creation/Mod Context API Reference#Mod Settings]].''
''View this topic's relevant API reference here [[Mod Creation/Mod Context API Reference#Mod Settings]].''
{{Disclaimer|When loading your mod as a Local Mod via the Creator Toolkit, the mod must be linked to mod.io and you must have subscribed to and installed the mod via mod.io in order for this data to persist.}}


Your mod can define settings for the player to interact with and visually configure your mod in-game. This feature is accessible through a <code>settings</code> property on the context object. If your mod has any settings defined, your mod will appear in the sidebar under Mod Settings. Clicking this will open up a window with all of your defined settings.
Your mod can define settings for the player to interact with and visually configure your mod in-game. This feature is accessible through a <code>settings</code> property on the context object. If your mod has any settings defined, your mod will appear in the sidebar under Mod Settings. Clicking this will open up a window with all of your defined settings.
Line 270: Line 419:
Settings are divided (in code and visually) into sections. Get or create a section using the <code>section(name)</code> method on the <code>settings</code> object. The value passed in for the <code>name</code> parameter is used as a header for the section, so this should be human-readable. These sections are displayed in the order that they are created.
Settings are divided (in code and visually) into sections. Get or create a section using the <code>section(name)</code> method on the <code>settings</code> object. The value passed in for the <code>name</code> parameter is used as a header for the section, so this should be human-readable. These sections are displayed in the order that they are created.


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ settings }) {
export function setup({ settings }) {
   // Creates a section labeled "General"
   // Creates a section labeled "General"
Line 277: Line 426:
   // Future calls to that section will not create a new "General" section, but instead return the already existing one
   // Future calls to that section will not create a new "General" section, but instead return the already existing one
   settings.section('General');
   settings.section('General');
}</nowiki>
}</syntaxhighlight>


The object returned from using <code>section()</code> can then be used for adding settings to that section. Refer to the next section for settings configurations.
The object returned from using <code>section()</code> can then be used for adding settings to that section. Refer to the next section for settings configurations.


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ settings }) {
export function setup({ settings }) {
   const generalSettings = settings.section('General');
   const generalSettings = settings.section('General');
Line 303: Line 452:
     hint: '1 through 10'
     hint: '1 through 10'
   }]);
   }]);
}</nowiki>
}</syntaxhighlight>


You can then <code>get</code> or <code>set</code> the value of any defined setting by its <code>name</code> property.
You can then <code>get</code> or <code>set</code> the value of any defined setting by its <code>name</code> property.


  <nowiki>// elsewhere.mjs
  <syntaxhighlight lang="js" line>// elsewhere.mjs
const { settings } = mod.getContext(import.meta);
const { settings } = mod.getContext(import.meta);


const generalSettings = settings.section('General');
const generalSettings = settings.section('General');
generalSettings.set('pick-a-number', 1);
generalSettings.set('pick-a-number', 1);
console.log(generalSettings.get('pick-a-number')); // 1</nowiki>
console.log(generalSettings.get('pick-a-number')); // 1</syntaxhighlight>


=== Setting Types ===
=== Setting Types ===
Line 344: Line 493:
Each of the customizable (categories, items, subitems) pieces are generally interacted with the same way.
Each of the customizable (categories, items, subitems) pieces are generally interacted with the same way.


  <nowiki>const combat = sidebar.catetory('Combat'); // Get the Combat category, or create one if it doesn't exist
  <syntaxhighlight lang="js" line>const combat = sidebar.catetory('Combat'); // Get the Combat category, or create one if it doesn't exist
const attack = sidebar.category('Combat').item('Attack'); // Get the Attack item within Combat or create one if it doesn't exist
const attack = sidebar.category('Combat').item('Attack'); // Get the Attack item within Combat or create one if it doesn't exist
attack.subitem('Wut'); // Get the Wut subitem within Attack or create one if it doesn't exist</nowiki>
attack.subitem('Wut'); // Get the Wut subitem within Attack or create one if it doesn't exist</syntaxhighlight>


In addition, these can be called with a configuration object as a second parameter to create or update the existing piece with the new configuration.
In addition, these can be called with a configuration object as a second parameter to create or update the existing piece with the new configuration.


  <nowiki>sidebar.category('Combat').item('Slayer', {
  <syntaxhighlight lang="js" line>sidebar.category('Combat').item('Slayer', {
   before: 'Attack', // Move the Slayer item above Attack
   before: 'Attack', // Move the Slayer item above Attack
   ignoreToggle: true // Keep Slayer visible when its category has been hidden
   ignoreToggle: true // Keep Slayer visible when its category has been hidden
});</nowiki>
});</syntaxhighlight>


The full definition of each sidebar piece's configuration object can be found in the [[Mod Creation/Sidebar API Reference]] guide.
The full definition of each sidebar piece's configuration object can be found in the [[Mod Creation/Sidebar API Reference]] guide.
Line 359: Line 508:
If you need to retrieve all existing categories, items, or subitems, use their respective methods:
If you need to retrieve all existing categories, items, or subitems, use their respective methods:


  <nowiki>sidebar.categories(); // returns an array of all categories
  <syntaxhighlight lang="js" line>sidebar.categories(); // returns an array of all categories
sidebar.category('Combat').items(); // returns an array of all Combat items
sidebar.category('Combat').items(); // returns an array of all Combat items
sidebar.category('General').item('Completion Log').subitems(); // returns an array of all Completion Log subitems</nowiki>
sidebar.category('General').item('Completion Log').subitems(); // returns an array of all Completion Log subitems</syntaxhighlight>


Removing categories, items, and subitems is also possible:
Removing categories, items, and subitems is also possible:


  <nowiki>sidebar.category('Non-Combat').remove(); // Remove the entire Non-Combat category
  <syntaxhighlight lang="js" line>sidebar.category('Non-Combat').remove(); // Remove the entire Non-Combat category
sidebar.removeCategory('Combat'); // Alternative (this avoids creating a Combat category if it didn't already exist)
sidebar.removeCategory('Combat'); // Alternative (this avoids creating a Combat category if it didn't already exist)
sidebar.removeAllCategories(); // Remove all categories, but why?
sidebar.removeAllCategories(); // Remove all categories, but why?
Line 371: Line 520:
// Same kind of structure for items and subitems:
// Same kind of structure for items and subitems:
sidebar.category('Modding').item('Mod Manager').remove();
sidebar.category('Modding').item('Mod Manager').remove();
sidebar.category('General').item('Completion Log').removeAllSubitems();</nowiki>
sidebar.category('General').item('Completion Log').removeAllSubitems();</syntaxhighlight>


== Creating Reusable HTML Components ==
== Creating Reusable HTML Components ==
Line 385: Line 534:
If you have the following HTML file:
If you have the following HTML file:


  <nowiki><!-- templates.html -->
  <syntaxhighlight lang="html" line><!-- templates.html -->
<template id="counter-component">
<template id="counter-component">
   <span class="text-light">{{ count }}</span>
   <span class="text-light">{{ count }}</span>
   <button class="btn btn-secondary" @click="inc">+</button>
   <button class="btn btn-secondary" @click="inc">+</button>
</template></nowiki>
</template></syntaxhighlight>


You would import the template in one of the following two ways:
You would import the template in one of the following two ways:


  <nowiki>// manifest.json
  <syntaxhighlight lang="js" line>// manifest.json
{
{
   "load": "templates.html"
   "load": "templates.html"
}</nowiki>
}</syntaxhighlight>
 
<small>''Comments in JSON are purely illustrative and not valid markup''</small>


''or''
''or''


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ loadTemplates }) {
export function setup({ loadTemplates }) {
   loadTemplates('templates.html');
   loadTemplates('templates.html');
}</nowiki>
}</syntaxhighlight>


=== Defining a Component ===
=== Defining a Component ===
Line 409: Line 560:
Using the [https://github.com/vuejs/petite-vue#components PetiteVue documentation on components], you should define each component as a function. This component should define its template selector using the <code>$template</code> property, and then any additional properties or methods that the rendered component will use. For example:
Using the [https://github.com/vuejs/petite-vue#components PetiteVue documentation on components], you should define each component as a function. This component should define its template selector using the <code>$template</code> property, and then any additional properties or methods that the rendered component will use. For example:


  <nowiki>function Counter(props) {
  <syntaxhighlight lang="js" line>function Counter(props) {
   return {
   return {
     $template: '#counter-component',
     $template: '#counter-component',
Line 417: Line 568:
     }
     }
   };
   };
}</nowiki>
}</syntaxhighlight>


=== Creating a Component Within the UI ===
=== Creating a Component Within the UI ===
Line 423: Line 574:
Now that your template is loaded and you have a component defined, you can use the helper function <code>ui.create</code> to create an instance of the component within the UI.
Now that your template is loaded and you have a component defined, you can use the helper function <code>ui.create</code> to create an instance of the component within the UI.


  <nowiki>// Create a counter component at the bottom of the Woodcutting page
  <syntaxhighlight lang="js" line>// Create a counter component at the bottom of the Woodcutting page
ui.create(Counter({ count: 0 }), document.getElementById('woodcutting-container'));</nowiki>
ui.create(Counter({ count: 0 }), document.getElementById('woodcutting-container'));</syntaxhighlight>


== Storing Data ==
== Storing Data ==
Line 431: Line 582:


''View the account storage's relevant API reference here [[Mod Creation/Mod Context API Reference#Account Data Storage]].''
''View the account storage's relevant API reference here [[Mod Creation/Mod Context API Reference#Account Data Storage]].''
{{Disclaimer|When loading your mod as a Local Mod via the Creator Toolkit, the mod must be linked to mod.io and you must have subscribed to and installed the mod via mod.io in order for this data to persist.}}


There are two options for storing data for your mod that isn't already saved as part of the game or settings: data saved with a character or data saved to the player's account. For most cases, however, character storage should be the preferred location and account storage used sparingly. Both of these stores are available through your mod's context object, as <code>characterStorage</code> and <code>accountStorage</code>, respectively. Aside from where the data is ultimately saved, character and account storage have identical methods and behaviors. Character storage is not available until after a character has been loaded (<code>onCharacterLoaded</code> lifecycle hook).
There are two options for storing data for your mod that isn't already saved as part of the game or settings: data saved with a character or data saved to the player's account. For most cases, however, character storage should be the preferred location and account storage used sparingly. Both of these stores are available through your mod's context object, as <code>characterStorage</code> and <code>accountStorage</code>, respectively. Aside from where the data is ultimately saved, character and account storage have identical methods and behaviors. Character storage is not available until after a character has been loaded (<code>onCharacterLoaded</code> lifecycle hook).


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ characterStorage }) {
export function setup({ characterStorage }) {
   // This would all function identically with accountStorage, but also be available across characters
   // This would all function identically with accountStorage, but also be available across characters
Line 444: Line 597:


   characterStorage.clear(); // Removes all currently stored items
   characterStorage.clear(); // Removes all currently stored items
}</nowiki>
}</syntaxhighlight>


=== Limitations ===
=== Limitations ===
Line 460: Line 613:
A common modding scenario is to want to override/modify an in-game method or perform an action before or after it has completed. Your mod's context object contains a patch property that can be used for this these cases. Patches can only be applied to methods that exist on a JavaScript class (<code>Player</code>, <code>Enemy</code>, <code>CombatManager</code>, <code>Woodcutting</code>, etc.). To start, define the class and method that you want to patch:
A common modding scenario is to want to override/modify an in-game method or perform an action before or after it has completed. Your mod's context object contains a patch property that can be used for this these cases. Patches can only be applied to methods that exist on a JavaScript class (<code>Player</code>, <code>Enemy</code>, <code>CombatManager</code>, <code>Woodcutting</code>, etc.). To start, define the class and method that you want to patch:


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ patch }) {
export function setup({ patch }) {
   const xpPatch = patch(Skill, 'addXP');
   const xpPatch = patch(Skill, 'addXP');
}</nowiki>
}</syntaxhighlight>


From there you can use that patch to perform any of the following actions.
From there you can use that patch to perform any of the following actions.
=== A Quick Note on Function Syntax ===
When patching methods, for most scenarios you'll want to use a traditional function expression, rather than the arrow expression syntax. This will ensure <code>this</code> is bound to the class instance that is calling the method, rather than the context where the patch was defined.
For example,
<syntaxhighlight lang="js" line="1">export function setup({ patch }) {
  const methodPatch = patch(Class, 'method');
  // Do this
  methodPatch.before(function () { });
 
  // Or this
  function beforePatch () { }
  methodPatch.before(beforePatch);
 
  // Not this, unless you understand the implications of doing so
  methodPatch.before(() => { });
}</syntaxhighlight>


=== Do Something Before ===
=== Do Something Before ===
Line 471: Line 642:
Use the <code>before</code> method on the patch object to execute code immediately before the patched method. In addition, the callback hook will receive the arguments that were used to call the patched method as parameters, and can optionally modify them by returning the new arguments as an array.
Use the <code>before</code> method on the patch object to execute code immediately before the patched method. In addition, the callback hook will receive the arguments that were used to call the patched method as parameters, and can optionally modify them by returning the new arguments as an array.


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line="1">// setup.mjs
export function setup({ patch }) {
export function setup({ patch }) {
   patch(Skill, 'addXP').before((amount, masteryAction) => {
   patch(Skill, 'addXP').before(function (amount, masteryAction) {
     console.log(`Doubling XP from ${amount} to ${amount * 2}!`);
     console.log(`Doubling XP from ${amount} to ${amount * 2}!`);
     return [amount * 2, masteryAction]; // Double all XP gains
     return [amount * 2, masteryAction]; // Double all XP gains
   });
   });
}</nowiki>
}</syntaxhighlight>


=== Do Something After ===
=== Do Something After ===
Line 483: Line 654:
Use the <code>after</code> method on the patch object to execute code immediately after the patched method. In addition, the callback hook will receive the value returned from the patched method along with the arguments used to call it as parameters. Optionally, an after hook can choose to override the returned value by returning a value itself. '''''Only''' a return value of <code>undefined</code> will be ignored.''
Use the <code>after</code> method on the patch object to execute code immediately after the patched method. In addition, the callback hook will receive the value returned from the patched method along with the arguments used to call it as parameters. Optionally, an after hook can choose to override the returned value by returning a value itself. '''''Only''' a return value of <code>undefined</code> will be ignored.''


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line="1">// setup.mjs
export function setup({ patch }) {
export function setup({ patch }) {
   patch(Player, 'rollToHit').after((willHit) => {
   patch(Player, 'rollToHit').after(function (willHit) {
     if (!willHit) console.log('A miss? I think not!');
     if (!willHit) console.log('A miss? I think not!');
     return true;
     return true;
   });
   });
}</nowiki>
}</syntaxhighlight>


=== Replace the Method Entirely ===
=== Replace the Method Entirely ===
Line 495: Line 666:
The <code>replace</code> method on the patch object will override the patched method's body, but before and after hooks will still be executed. The replacement method will receive the current method implementation (the one being replaced) along with the arguments used to call it as parameters. The return value of the replacement method will be the return value of the method call, subject to any changes made in an after hook.
The <code>replace</code> method on the patch object will override the patched method's body, but before and after hooks will still be executed. The replacement method will receive the current method implementation (the one being replaced) along with the arguments used to call it as parameters. The return value of the replacement method will be the return value of the method call, subject to any changes made in an after hook.


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ patch }) {
export function setup({ patch }) {
   patch(Skill, 'addXP').replace(function(o, amount, masteryAction) {
   patch(Skill, 'addXP').replace(function(o, amount, masteryAction) {
Line 502: Line 673:


     // Double any mining XP
     // Double any mining XP
     if (this.id=== 'melvorD:Mining') return o(amount * 2, masteryAction);
     if (this.id === 'melvorD:Mining') return o(amount * 2, masteryAction);


     // Grant all other XP as normal
     // Grant all other XP as normal
     return o(amount, masteryAction);
     return o(amount, masteryAction);
   });
   });
}</nowiki>
}</syntaxhighlight>


It's important to note that the using the replace method replaces the '''current''' method implementation. This means that multiple replacements on the same patched method will be executed in reverse order than they were declared:
It's important to note that the using the replace method replaces the '''current''' method implementation. This means that multiple replacements on the same patched method will be executed in reverse order than they were declared:


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ patch, onInterfaceReady }) {
export function setup({ patch, onInterfaceReady }) {
   const xpPatch = patch(Skill, 'addXP');
   const xpPatch = patch(Skill, 'addXP');
Line 534: Line 705:
   console.log('XP replace B');
   console.log('XP replace B');
   return o(amount, masteryAction);
   return o(amount, masteryAction);
}</nowiki>
}</syntaxhighlight>


== Exposing APIs ==
== Exposing APIs ==
''View this topic's relevant API reference here [[Mod Creation/Mod Context API Reference#Exposing Properties and Methods (Mod API)]].''


If your mod serves as a tool for other mods to integrate with, exposing APIs through the context object using <code>ctx.api</code> is the recommended approach. This is especially useful when paired with a mod developed using modules. The <code>api</code> method accepts an object and will expose any properties on that object to the global <code>mod</code> object within the <code>api['your-mods-namespace']</code> property. You can call the <code>api</code> method multiple times to append more APIs.
If your mod serves as a tool for other mods to integrate with, exposing APIs through the context object using <code>ctx.api</code> is the recommended approach. This is especially useful when paired with a mod developed using modules. The <code>api</code> method accepts an object and will expose any properties on that object to the global <code>mod</code> object within the <code>api['your-mods-namespace']</code> property. You can call the <code>api</code> method multiple times to append more APIs.


  <nowiki>// manifest.json
  <syntaxhighlight lang="js" line>// manifest.json
{
{
   "namespace": "helloWorld",
   "namespace": "helloWorld",
   "setup": "setup.mjs"
   "setup": "setup.mjs"
}</nowiki>
}</syntaxhighlight>
 
<small>''Comments in JSON are purely illustrative and not valid markup''</small>


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ api }) {
export function setup({ api }) {
   api({
   api({
     greet: name => console.log(`Hello, ${name!}`);
     greet: name => console.log(`Hello, ${name!}`);
   });
   });
}</nowiki>
}</syntaxhighlight>


Other mods would then be able to interact with your API:
Other mods would then be able to interact with your API:


  <nowiki>// some other mod
  <syntaxhighlight lang="js" line>// some other mod
mod.api.helloWorld.greet('Melvor'); // Hello, Melvor!</nowiki>
mod.api.helloWorld.greet('Melvor'); // Hello, Melvor!</syntaxhighlight>


== The Dev Context ==
== The Dev Context ==
Line 562: Line 737:
To make it easier to test code before committing to uploading a mod, there is a 'dev' mod context that you can access to try out any of the context object's methods that don't require additional resources, i.e. you can't use <code>loadModule</code>. To access this context, you can use the following in your browser console:
To make it easier to test code before committing to uploading a mod, there is a 'dev' mod context that you can access to try out any of the context object's methods that don't require additional resources, i.e. you can't use <code>loadModule</code>. To access this context, you can use the following in your browser console:


  <nowiki>const devCtx = mod.getDevContext();</nowiki>
  <syntaxhighlight lang="js" line>const devCtx = mod.getDevContext();</syntaxhighlight>


''This method/context '''should not''' be used from within a mod.''
''This method/context '''should not''' be used from within a mod.''
Line 571: Line 746:
* [[Mod Creation/Mod Context API Reference]]
* [[Mod Creation/Mod Context API Reference]]
* [[Mod Creation/Sidebar API Reference]]
* [[Mod Creation/Sidebar API Reference]]
{{ModGuideNav}}
{{Menu}}

Latest revision as of 03:00, 5 June 2024

First time writing a mod for Melvor Idle? Consider starting with the Mod Creation/Getting Started guide.

Creating a Mod

A mod in Melvor Idle is simply composed of two parts: metadata and resources.

The metadata is defined in your mod's mod.io profile page (name, version, previews, etc.) and in a manifest.json file that must be located at the root of your mod's directory (this holds metadata that tells the Mod Manager how to load your mod).

Resources are really everything else. JavaScript modules and scripts, CSS stylesheets, images, sounds, etc. are all considered resources that will be accessed through either the manifest or dynamically through your JavaScript code. When referencing a resource from anywhere (manifest or code), the path should always be relative to the root of your mod (where the manifest.json file is located).

The Manifest

Before you begin writing code, it's a good idea to start by defining some metadata in the manifest.json file. A complete manifest.json might look like the following:

{
  "namespace": "helloWorld",
  "icon": "assets/icon.png",
  "setup": "src/setup.mjs",
  "load": ["assets/style.css"]
}

namespace?: string

A few important modding APIs (tools) available from JavaScript require a namespace to be defined. This helps the game to keep your mod's data organized - think of this as an id for your mod that will be used by other mods and the game itself to access stored data that you've defined. As such, it's best to choose a namespace that easily identifies your mod in case another mod wants to interact with it.

The namespace can only contain alphanumeric characters and underscores and cannot start with the word "melvor".

Namespace Valid
helloWorld ✔️
hello_world_123 ✔️
HelloWorld!
melvorWorld

While this property is optional, it's good practice to include it to avoid future troubleshooting if you end up using an API that requires a namespace.

icon?: string

An optional icon to be displayed alongside your mod in a number of places, like the My Mods list in the Mod Manager. The value should be the path to the image file relative to the root of your mod (where your manifest is located). Accepted file types for an icon are .png or .svg, and the icon is typically displayed at a maximum of 38px in-game.

Alternatively, you can supply an absolute path to a web URL and that will be used instead.

setup?: string

This property is required only if the "load" property is not present in the manifest.

This value should be a file path pointing to a JavaScript module to act as the entry-point to your mod; this concept will be covered more in the following section.

load?: string | string[]

This property is required only if the "setup" property is not present in the manifest.

This value accepts either a single path or an array of paths to resources to load. These resources are loaded in the order of the array, after the "setup" resource has been run. Valid resources to be loaded through this property are JavaScript script files (.js), JavaScript module files (.mjs), CSS stylesheets (.css), JSON files containing game data packages (.json), and HTML files containing templates (.html). However, unless your mod is very simple, the recommended approach to loading JavaScript resources (.js or .mjs) is through code in your mod's entry-point ("setup").

It's also important to note that while .js is considered a valid extension for JavaScript module files for the "setup" property, modules loaded through "load" must end with .mjs or they will be treated as regular script files.

Structuring Your Code

Using Modules (Recommended)

There are a number of ways to structure your code to be loaded, whether it's scripts or modules, "setup" or "load". Each might have a good use case but the recommended approach for most mods is to write your code using JavaScript modules and to have a single entry-point (defined as "setup" in manifest.json), while leaving the "load" property exclusively for loading your CSS.

Using this approach will keep your code clean and manageable, while avoiding polluting the global JavaScript scope, and as a result, avoiding conflicts with other mods. If you're unfamiliar with JavaScript modules, you can check out resources like JavaScript Modules on W3Schools or on MDN for a more detailed look. The general pattern you'll be using is exporting module "features" (functions, variables, etc) and then import them into other modules for use. This is also the approach this guide will use in all of its examples.

Let's start with what a module that's defined as your mod's "setup" entry-point should look like:

// setup.mjs
export function setup(ctx) {
  console.log('Hello World!');
}

We export a function named setup here because that is what the Mod Manager looks for when loading a "setup" module. Without one, an error would be thrown when loading this mod. This setup function is called and receives the mod's context object as soon as the mod is loaded, which happens just before the character select screen is visible. Therefore, this mod would write 'Hello World!' to the console at that time.

To take advantage of the modular approach to JavaScript, however, we cannot use a static import at the top of the file like we would prefer to in a regular JavaScript environment. Due to the nature of how modding resources are stored, we have to dynamically import modules using a special loadModule function, contained within the context object. This acts identical to a dynamic import but allows you to use a resource path relative to the root of your mod.

If we define a helper module helper.mjs:

// helper.mjs
export function greet(name) {
  console.log(`Hello, ${name}!`);
}

We can then use code we export in our setup function:

// setup.mjs
export async function setup({ loadModule }) {
  const { greet } = await loadModule('helper.mjs');
  greet('Melvor'); // > Hello, Melvor!
}

If you need to access the context object from your helper module, there are two approaches:

1. Pass the context object from the setup function to the loaded module:

// configService.mjs
export function init(ctx) {
  // Perform actions using the context object here...
}

// setup.mjs
export async function setup(ctx) {
  const configService = await ctx.loadModule('configService.mjs');
  configService.init(ctx);
}

2. Use the getContext method on the global mod object:

// configService.mjs
const ctx = mod.getContext(import.meta);

export function init() {
  // Perform actions using the context object here...
}

You must pass import.meta - a special JavaScript object available in all modules - to the mod.getContext method to receive your mod's context object.

Using Scripts

If you choose to include plain scripts in your mod, whether it's out of familiarity or a special use case, you can load (inject) scripts into the game either through the context object (perhaps received from a "setup" module) or the "load" property of the manifest.

Loading a script through the context object is very similar to loading a module but you will not receive back a value.

export async function setup({ loadScript }) {
  // Make sure you await the call to loadScript if your code beyond relies on it
  await loadScript('hello-melvor-script.js');
  // hello-melvor-script.js has executed

  // But don't bother awaiting it if it's not time-sensitive
  loadScript('some-independent-script.js');
}

From inside your script, you can still access the context object:

mod.register(ctx => {
  // Use the context object here
});

Note that the mod.register method will only work on scripts injected through either loadScript or the "load" property of the manifest.

The Context Object

Your mod's context object is the central point used for setting up your mod and making modifications to the game. The majority of the other sections in this guide will cover the concepts enabled through the APIs available on the object. For a more in-depth look at the documentation for the context object, refer to the Mod Creation/Mod Context API Reference guide.

Accessing Your Mod's Resources

View this topic's relevant API reference here Mod Creation/Mod Context API Reference#Loading Resources.

Chances are you will package some resources in your mod that aren't covered by the loading options defined in the manifest and instead need to rely on loading these resources through your code. Your mod's context object provides methods for retrieving these resources. Keep in mind that all file path references to your resources should be relative to the root of your mod. Some common scenarios are below.

Load (Import) a Module

Use ctx.loadModule to import a JavaScript module's exported features.

// my-module.mjs
export function greet(name) {
  console.log(`Hello, ${name}!`);
}

export const importantData = ['e', 'r', 'e', 'h', 't', ' ', 'o', 'l', 'l', 'e', 'h'];
// setup.mjs
export async function setup({ loadModule }) {
  const myModule = await loadModule('my-module.mjs');
  myModule.greet('Melvor'); // Hello, Melvor!
  console.log(myModule.importantData.reverse().join('')); // hello there
}

Load (Inject) a Script

Use ctx.loadScript to inject a JavaScript file into the page.

// setup.mjs
export await function setup({ loadScript }) {
  // Wait for script to run
  await loadScript('my-script.js');
  // Or not
  loadScript('my-independent-script.js');
}

Load (Inject) HTML Templates

Use ctx.loadTemplates to inject all <template> elements into the document body.

// setup.mjs
export function setup({ loadTemplates }) {
  loadTemplates('my-templates.html');
}

Load (Inject) a Stylesheet

Use ctx.loadStylesheet to inject a CSS file into the page.

// setup.mjs
export function setup({ loadStylesheet }) {
  loadStylesheet('my-styles.css');
}

Load Data from JSON

Use ctx.loadData to read and automatically parse a JSON resource.

// my-data.json
{
  "coolThings": [
    "rocks"
  ]
}

Comments in JSON are purely illustrative and not valid markup

// setup.mjs
export async function setup({ loadData }) {
  const data = await loadData('my-data.json');
  console.log(data.coolThings[0]); // ['rocks'] 
}

Images, Sounds, and Anything Else

Nearly any resource can be accessed and used in some way with ctx.getResourceUrl - the helper methods above all use this behind the scenes. With the resource's URL, you can use built-in JavaScript methods to consume the resource.

// setup.mjs
export function setup({ getResourceUrl }) {
  const url = getResourceUrl('sea-shanty-2.ogg');
  const song = new Audio(url);
  song.loop = true;
  song.play();
}

Game Lifecycle Hooks

View this topic's relevant API reference here Mod Creation/Mod Context API Reference#Lifecycle Hooks.

Utilizing the game's lifecycle hooks will allow your mod to perform actions at specific times, which may be useful for waiting for certain game objects to be available. The game lifecycle hooks are as follows:

  • onModsLoaded: Occurs after all enabled mods have completed their initial load
  • onCharacterSelectionLoaded: Occurs after the character selection screen is completely loaded.
  • onCharacterLoaded: Occurs after a character has been selected and all game objects have been constructed, but before offline progress has been calculated.
  • onInterfaceReady: Occurs after offline progress has been calculated and the in-game interface can be reliably modified. Also useful for any long-running, or non-vital processes that might negatively impact the player experience by increased character load times.

All of the game's lifecycle hooks are available through your mod's context object and accept a callback function as a sole parameter. This callback function can be synchronous or asynchronous and will be executed at the specified time and receive your mod's context object as a parameter.

// setup.mjs
export function setup({ onModsLoaded, onCharacterLoaded, onInterfaceReady }) {
  onModsLoaded(ctx => {
    // Utilize other mod APIs at character select
  });

  onCharacterSelectionLoaded(ctx => {
    // Build or modify character selection UI elements
  });

  onCharacterLoaded(ctx => {
    // Modify or hook into game objects to influence offline calculations
  });

  onInterfaceReady(ctx => {
    // Build or modify in-game UI elements
  });
}

Adding and Modifying Game Objects

View this topic's relevant API reference here Mod Creation/Mod Context API Reference#Game Object Registration.

Mods can now register or modify game objects (items, skills, pages, etc.) in a streamlined way. The entry point for doing so is either the "load" property of your manifest.json, or the gameData endpoint within the mod context API. There is massive variety on what data is needed between different game object types but the general concept is the same. You will need to either define a data package using JSON and load that into the game, or you can dynamically build one via code (certain dynamic objects like skills requires the latter).

Defining a Data Package

The first, simpler option for building game object data is defining all (or as much as possible) data in a .json file that is then read into the game to register your game objects.

Pros

  • Simpler
  • More easily separate data from your mod's logic
  • Your text editor can provide typing by defining the $schema property in your JSON file

Cons

  • Does not support all game object types, such as skills

To begin with this approach, your JSON files should all be constructed with:

{
  "$schema": "https://melvoridle.com/assets/schema/gameData.json",
  "data": {

  }
}

If you're using a text editor that supports it, you should now get autocomplete and type checking on the fields you create.

Here is an example of defining an item:

{
  "$schema": "https://melvoridle.com/assets/schema/gameData.json",
  "data": {
    "items": [{
      "id": "Wooden_Dagger",
      "name": "Wooden Dagger",
      "category": "Combat",
      "type": "Weapon",
      "media": "wooden-dagger.png",
      "ignoreCompletion": false,
      "obtainFromItemLog": false,
      "golbinRaidExclusive": false,
      "sellsFor": 0,
      "tier": "wooden",
      "validSlots": ["Weapon"],
      "occupiesSlots": [],
      "equipRequirements": [
        {
          "type": "SkillLevel",
          "skillID": "melvorD:Attack",
          "level": 1
        }
      ],
      "equipmentStats": [
        { "key": "attackSpeed", "value": 2200 },
        { "key": "stabAttackBonus", "value": 4 },
        { "key": "slashAttackBonus", "value": 1 },
        { "key": "blockAttackBonus", "value": 4 },
        { "key": "meleeStrengthBonus", "value": 1 }
      ],
      "itemType": "Weapon",
      "attackType": "melee"
    }]
  }
}

You would then register your game data using one of the following methods:

// manifest.json
{
  "namespace": "helloWorld",
  "load": ["path-to-your-data.json"]
}

Comments in JSON are purely illustrative and not valid markup

or

// setup.mjs
export async function setup({ gameData }) {
  await gameData.addPackage('path-to-your-data.json');
}

Building a Data Package at Runtime

The other option for building game object data is doing so dynamically through the mod context API.

Pros

  • Can be used to register any type of game object
  • Enables the ability to dynamically build game objects

Cons

  • Messier and more complex
  • No type support at the moment

The entry-point for using this approach looks like this:

// setup.mjs
export function setup({ gameData }) {
  gameData.buildPackage((p) => {
    // use the `p` object to add game objects
  }).add();
}

Following the same example above of adding an item:

// setup.mjs
export function setup({ gameData }) {
  gameData.buildPackage((p) => {
    p.items.add({
      id: 'Wooden_Dagger',
      name: 'Wooden Dagger',
      category: 'Combat',
      type: 'Weapon',
      media: 'wooden-dagger.png',
      ignoreCompletion: false,
      obtainFromItemLog: false,
      golbinRaidExclusive: false,
      sellsFor: 0,
      tier: 'wooden',
      validSlots: ['Weapon'],
      occupiesSlots: [],
      equipRequirements: [{
        type: 'SkillLevel',
        skillID: 'melvorD:Attack',
        level: 1
      }],
      equipmentStats: [
        { key: 'attackSpeed', value: 2200 },
        { key: 'stabAttackBonus', value: 4 },
        { key: 'slashAttackBonus', value: 1 },
        { key: 'blockAttackBonus', value: 4 },
        { key: 'meleeStrengthBonus', value: 1 }
      ],
      itemType: 'Weapon',
      attackType: 'melee'
    });
  }).add();
}

Your game data should already be registered from the .add() method being called on your built package.

Mod Settings

View this topic's relevant API reference here Mod Creation/Mod Context API Reference#Mod Settings.

When loading your mod as a Local Mod via the Creator Toolkit, the mod must be linked to mod.io and you must have subscribed to and installed the mod via mod.io in order for this data to persist.

Your mod can define settings for the player to interact with and visually configure your mod in-game. This feature is accessible through a settings property on the context object. If your mod has any settings defined, your mod will appear in the sidebar under Mod Settings. Clicking this will open up a window with all of your defined settings.

Settings' values are persisted on a per-character basis and will be saved within the character's save file.

Settings are divided (in code and visually) into sections. Get or create a section using the section(name) method on the settings object. The value passed in for the name parameter is used as a header for the section, so this should be human-readable. These sections are displayed in the order that they are created.

// setup.mjs
export function setup({ settings }) {
  // Creates a section labeled "General"
  settings.section('General');

  // Future calls to that section will not create a new "General" section, but instead return the already existing one
  settings.section('General');
}

The object returned from using section() can then be used for adding settings to that section. Refer to the next section for settings configurations.

// setup.mjs
export function setup({ settings }) {
  const generalSettings = settings.section('General');
  // You can add settings one-by-one
  generalSettings.add({
    type: 'switch',
    name: 'awesomeness-detection',
    label: 'Awesomeness Detection',
    hint: 'Determines if you are awesome or not.',
    default: false
  });

  // Or multiple at a time by passing in an array
  generalSettings.add([{
    type: 'label',
    display: 'I am just a label though my story seldom told...'
  }, {
    type: 'number',
    name: 'pick-a-number',
    label: 'Pick a Number',
    hint: '1 through 10'
  }]);
}

You can then get or set the value of any defined setting by its name property.

// elsewhere.mjs
const { settings } = mod.getContext(import.meta);

const generalSettings = settings.section('General');
generalSettings.set('pick-a-number', 1);
console.log(generalSettings.get('pick-a-number')); // 1

Setting Types

There are currently eight predefined setting types that will automatically create a usable input:

  • Text
  • Number
  • Switch
  • Dropdown
  • Button
  • Checkbox Group
  • Radio Group
  • Label

In addition, you add a custom setting by configuring additional properties. For more information on the configuration options available for each of these, refer to the relevant section of the Mod Creation/Mod Context API Reference#Mod Settings guide.

Customizing the Sidebar

View this topic's relevant API reference here Mod Creation/Sidebar API Reference.

If you want to add or modify the in-game sidebar (the menu with the bank, skills, etc.) there is an globally-scoped in-game API, sidebar, for doing so. The sidebar is organized into four levels:

  • Sidebar
    • Categories
      • Items
        • Subitems

An example of a category is Combat and Attack would be an item within that category. Subitems are what's used for the Completion Log's sections.

Each of the customizable (categories, items, subitems) pieces are generally interacted with the same way.

const combat = sidebar.catetory('Combat'); // Get the Combat category, or create one if it doesn't exist
const attack = sidebar.category('Combat').item('Attack'); // Get the Attack item within Combat or create one if it doesn't exist
attack.subitem('Wut'); // Get the Wut subitem within Attack or create one if it doesn't exist

In addition, these can be called with a configuration object as a second parameter to create or update the existing piece with the new configuration.

sidebar.category('Combat').item('Slayer', {
  before: 'Attack', // Move the Slayer item above Attack
  ignoreToggle: true // Keep Slayer visible when its category has been hidden
});

The full definition of each sidebar piece's configuration object can be found in the Mod Creation/Sidebar API Reference guide.

If you need to retrieve all existing categories, items, or subitems, use their respective methods:

sidebar.categories(); // returns an array of all categories
sidebar.category('Combat').items(); // returns an array of all Combat items
sidebar.category('General').item('Completion Log').subitems(); // returns an array of all Completion Log subitems

Removing categories, items, and subitems is also possible:

sidebar.category('Non-Combat').remove(); // Remove the entire Non-Combat category
sidebar.removeCategory('Combat'); // Alternative (this avoids creating a Combat category if it didn't already exist)
sidebar.removeAllCategories(); // Remove all categories, but why?

// Same kind of structure for items and subitems:
sidebar.category('Modding').item('Mod Manager').remove();
sidebar.category('General').item('Completion Log').removeAllSubitems();

Creating Reusable HTML Components

This topic is covered in greater detail in Mod Creation/Reusable Components with PetiteVue.

Melvor Idle ships with PetiteVue for mods to use to create reusable HTML components. You can use documentation from the official GitHub page to assist in using the PetiteVue library. However, there are some helper functions for making it easier for mods to interact with.

Import HTML Templates

Using either the manifest.json's "load" property or the context API's loadTemplates method, you can import all <template> elements from an HTML file into the document body. These will then be available for use when creating a component.

If you have the following HTML file:

<!-- templates.html -->
<template id="counter-component">
  <span class="text-light">{{ count }}</span>
  <button class="btn btn-secondary" @click="inc">+</button>
</template>

You would import the template in one of the following two ways:

// manifest.json
{
  "load": "templates.html"
}

Comments in JSON are purely illustrative and not valid markup

or

// setup.mjs
export function setup({ loadTemplates }) {
  loadTemplates('templates.html');
}

Defining a Component

Using the PetiteVue documentation on components, you should define each component as a function. This component should define its template selector using the $template property, and then any additional properties or methods that the rendered component will use. For example:

function Counter(props) {
  return {
    $template: '#counter-component',
    count: props.count,
    inc() {
      this.count++;
    }
  };
}

Creating a Component Within the UI

Now that your template is loaded and you have a component defined, you can use the helper function ui.create to create an instance of the component within the UI.

// Create a counter component at the bottom of the Woodcutting page
ui.create(Counter({ count: 0 }), document.getElementById('woodcutting-container'));

Storing Data

View the character storage's relevant API reference here Mod Creation/Mod Context API Reference#Character Data Storage.

View the account storage's relevant API reference here Mod Creation/Mod Context API Reference#Account Data Storage.

When loading your mod as a Local Mod via the Creator Toolkit, the mod must be linked to mod.io and you must have subscribed to and installed the mod via mod.io in order for this data to persist.

There are two options for storing data for your mod that isn't already saved as part of the game or settings: data saved with a character or data saved to the player's account. For most cases, however, character storage should be the preferred location and account storage used sparingly. Both of these stores are available through your mod's context object, as characterStorage and accountStorage, respectively. Aside from where the data is ultimately saved, character and account storage have identical methods and behaviors. Character storage is not available until after a character has been loaded (onCharacterLoaded lifecycle hook).

// setup.mjs
export function setup({ characterStorage }) {
  // This would all function identically with accountStorage, but also be available across characters
  characterStorage.setItem('my-favorite-pet', 7);

  console.log(PETS[characterStorage.getItem('my-favorite-pet')].name); // Larry, the Lonely Lizard

  characterStorage.removeItem('my-favorite-pet');

  characterStorage.clear(); // Removes all currently stored items
}

Limitations

Currently, a mod's character storage and account storage are each (separately) limited to 8,192 bytes (8kb) of total data. This means each character can store up to 8kb per mod, but only 8kb total can be stored to an account.

In addition, only JSON-serializable data can be stored. This includes any JavaScript primitive value (strings, numbers, and booleans) or an object or array containing only primitive values (or an object or array containing only primitive values, etc.). You do not have to serialize/deserialize the data yourself.

Finally, due to the nature of account data being persisted to the cloud, data integrity cannot be 100% guaranteed due to possible network issues the player might experience.

Game Object Patching/Hooking

View this topic's relevant API reference here Mod Creation/Mod Context API Reference#Game Object Patching/Hooking.

A common modding scenario is to want to override/modify an in-game method or perform an action before or after it has completed. Your mod's context object contains a patch property that can be used for this these cases. Patches can only be applied to methods that exist on a JavaScript class (Player, Enemy, CombatManager, Woodcutting, etc.). To start, define the class and method that you want to patch:

// setup.mjs
export function setup({ patch }) {
  const xpPatch = patch(Skill, 'addXP');
}

From there you can use that patch to perform any of the following actions.

A Quick Note on Function Syntax

When patching methods, for most scenarios you'll want to use a traditional function expression, rather than the arrow expression syntax. This will ensure this is bound to the class instance that is calling the method, rather than the context where the patch was defined.

For example,

export function setup({ patch }) {
  const methodPatch = patch(Class, 'method');
  // Do this
  methodPatch.before(function () { });
  
  // Or this
  function beforePatch () { }
  methodPatch.before(beforePatch);
  
  // Not this, unless you understand the implications of doing so
  methodPatch.before(() => { });
}

Do Something Before

Use the before method on the patch object to execute code immediately before the patched method. In addition, the callback hook will receive the arguments that were used to call the patched method as parameters, and can optionally modify them by returning the new arguments as an array.

// setup.mjs
export function setup({ patch }) {
  patch(Skill, 'addXP').before(function (amount, masteryAction) {
    console.log(`Doubling XP from ${amount} to ${amount * 2}!`);
    return [amount * 2, masteryAction]; // Double all XP gains
  });
}

Do Something After

Use the after method on the patch object to execute code immediately after the patched method. In addition, the callback hook will receive the value returned from the patched method along with the arguments used to call it as parameters. Optionally, an after hook can choose to override the returned value by returning a value itself. Only a return value of undefined will be ignored.

// setup.mjs
export function setup({ patch }) {
  patch(Player, 'rollToHit').after(function (willHit) {
    if (!willHit) console.log('A miss? I think not!');
    return true;
  });
}

Replace the Method Entirely

The replace method on the patch object will override the patched method's body, but before and after hooks will still be executed. The replacement method will receive the current method implementation (the one being replaced) along with the arguments used to call it as parameters. The return value of the replacement method will be the return value of the method call, subject to any changes made in an after hook.

// setup.mjs
export function setup({ patch }) {
  patch(Skill, 'addXP').replace(function(o, amount, masteryAction) {
    // Prevent any woodcutting XP  
    if (this.id === 'melvorD:Woodcutting') return;

    // Double any mining XP
    if (this.id === 'melvorD:Mining') return o(amount * 2, masteryAction);

    // Grant all other XP as normal
    return o(amount, masteryAction);
  });
}

It's important to note that the using the replace method replaces the current method implementation. This means that multiple replacements on the same patched method will be executed in reverse order than they were declared:

// setup.mjs
export function setup({ patch, onInterfaceReady }) {
  const xpPatch = patch(Skill, 'addXP');

  xpPatch.replace(xpA);
  xpPatch.replace(xpB);

  onInterfaceReady(() => {
    game.woodcutting.addXP(100);
    // Logs:
    // XP replace B
    // XP replace A
  });
}

function xpA(o, amount, masteryAction) {
  console.log('XP replace A');
  return o(amount, masteryAction);
}

function xpB(o, amount, masteryAction) {
  console.log('XP replace B');
  return o(amount, masteryAction);
}

Exposing APIs

View this topic's relevant API reference here Mod Creation/Mod Context API Reference#Exposing Properties and Methods (Mod API).

If your mod serves as a tool for other mods to integrate with, exposing APIs through the context object using ctx.api is the recommended approach. This is especially useful when paired with a mod developed using modules. The api method accepts an object and will expose any properties on that object to the global mod object within the api['your-mods-namespace'] property. You can call the api method multiple times to append more APIs.

// manifest.json
{
  "namespace": "helloWorld",
  "setup": "setup.mjs"
}

Comments in JSON are purely illustrative and not valid markup

// setup.mjs
export function setup({ api }) {
  api({
    greet: name => console.log(`Hello, ${name!}`);
  });
}

Other mods would then be able to interact with your API:

// some other mod
mod.api.helloWorld.greet('Melvor'); // Hello, Melvor!

The Dev Context

To make it easier to test code before committing to uploading a mod, there is a 'dev' mod context that you can access to try out any of the context object's methods that don't require additional resources, i.e. you can't use loadModule. To access this context, you can use the following in your browser console:

const devCtx = mod.getDevContext();

This method/context should not be used from within a mod.

Next Steps

Hopefully this guide covers most common modding scenarios and will be a useful reference during your mod development time. For more in-depth looks at specific concepts, consider checking out the following guides: