Skip to content

Instantly share code, notes, and snippets.

@stephaned68
Last active March 30, 2024 08:24
Show Gist options
  • Save stephaned68/03af77f48568ca2bead3926ff4658cc2 to your computer and use it in GitHub Desktop.
Save stephaned68/03af77f48568ca2bead3926ff4658cc2 to your computer and use it in GitHub Desktop.
A template for writing Roll20 MOD scripts
/**
* Last update : 2024-03-30
* How to use :
* - Rename the ModName variable in the factory function declaration
* - Change the MOD_COMMAND constant if not the same as the MOD script name
* - Rename the ModName reference in the on('ready') callback function
* - Change the STATEKEY value. This will be used to save state, as well as the name of the MOD and its API !command
*/
var ModName =
ModName ||
(function () {
const STATEKEY = "ModName";
const MOD_NAME = `Mod:${STATEKEY}`;
const MOD_VERSION = "0.1.0";
const MOD_COMMAND = `!${STATEKEY.toLowerCase()}`;
/**
* Base state schema
*/
const MOD_BASE_STATE = {
version: MOD_VERSION,
stringSetting: '',
numSetting: 0,
boolSetting: true,
logging: false,
};
const GM_ONLY = "/w gm";
/**
* Parse string and return integer value
* @param {string} value - string to parse
* @param {number} [onError=0] - default value
* @returns integer or default value
*/
function int(value, onError = 0) {
return parseInt(value) || onError;
}
/**
* Parse string and return float value
* @param {string} value - string to parse
* @param {number} [onError=0] - default value
* @returns float or default value
*/
function float(value, onError = 0) {
return parseFloat(value) || onError;
}
/**
* Return string or default/empty if falsey
* @param {string} value - string value
* @param {string} [onError=""] - default value
* @returns string or default value
*/
function stringOrDefault(value, onError = "") {
return value || onError;
}
/**
* Log a message to the debug console
* @param {string} logMessage - message to log
* @param {boolean} [force=true] - force logging (overrides state logging)
*/
function writeLog(logMessage, force = true) {
if (getParam("logging") || force) {
if (typeof(logMessage) !== 'object') {
log(`${MOD_NAME} | ${logMessage}`);
} else {
for (const [prop, value] of Object.entries(logMessage)) {
log(`${MOD_NAME} | ${prop} = ${value}`);
}
}
}
}
/**
* Send message to chat
* @param {string} message - Message to output in chat
* @param {boolean} [noArchive=true] - Archived message flag (optional, default = true)
*/
function writeChat(message, noArchive = true) {
sendChat(
MOD_NAME,
message,
null,
{ noarchive: noArchive }
);
}
/**
* Returns the current page players are on
* @returns Current players page id
*/
function currentPage() {
return Campaign().get("playerpageid");
}
/**
* Get a state parameter value
* @param {string} name - Parameter name
* @param {any} defaultValue - Default parameter value
* @returns {any} - Parameter value
*/
function getParam(name, defaultValue = null) {
if (!state[STATEKEY]) {
return defaultValue;
}
return state[STATEKEY][name] || defaultValue;
}
/**
* Set a state parameter value
* @param {string} name - Parameter name
* @param {any} value - Parameter value
*/
function setParam(name, value) {
if (!state[STATEKEY]) {
state[STATEKEY] = {};
}
state[STATEKEY][name] = value;
}
/**
* Split a chat message content into parts
* @param {object} chatMsg - Roll20 chat message object
* @returns {string[]} Chat message parts
*/
function splitMessage(chatMsg) {
return chatMsg.content.replace(/<br\/>/g, '').split(/\s+/);
}
/**
* @typedef ParsedMessage
* @type {object}
* @property {string} command !api command
* @property {string} option Command option
* @property {string[]} arguments Arguments for command
*/
/**
* Parse a chat message content
* @param {object} chatMsg - Roll20 chat message object
* @returns {ParsedMessage}
*/
function parseMesssage(chatMsg) {
const [command, option, ...arguments ] = splitMessage(chatMsg);
return { command, option, arguments };
}
/**
* Display configuration settings
*/
function configDisplay() {
let helpMsg = GM_ONLY + `&{template:default} {{name=${MOD_NAME} v${MOD_VERSION} | Configuration}}`;
const strOpt = state[STATEKEY].stringSetting;
helpMsg += `{{stringSetting=${strOpt}`;
helpMsg += ` [Other Value](${MOD_COMMAND} config --stringSetting OtherValue)`;
helpMsg += ` [Third Value](${MOD_COMMAND} config --stringSetting ThirdValue)`;
helpMsg += ' }}';
helpMsg += `{{boolSetting=${state[STATEKEY].boolSetting} [Toggle](${MOD_COMMAND} config --boolSetting) }}`;
helpMsg += `{{Logging=${state[STATEKEY].logging} [Toggle](${MOD_COMMAND} config --logging) }}`;
writeChat(helpMsg);
}
/**
* Parse arguments and configure the state object
* @param {string[]} args - argument list
*/
function configSetup(args) {
if (args.length === 0) {
configDisplay();
return;
}
const [ name, value ] = args;
switch (name.toLowerCase()) {
case '--stringsetting':
setParam("stringSetting", stringOrDefault(value));
break;
case '--numsetting':
setParam("numSetting", int(value));
break;
case '--boolsetting':
setParam("boolSetting", !getParam("boolSetting"));
break;
case '--logging':
setParam("logging", !getParam("logging"));
break;
}
configDisplay(); // Display new configuration state
}
/**
* Display script help
*/
function displayHelp() {
let helpMsg = GM_ONLY + `&{template:default} {{name=${MOD_NAME} v${MOD_VERSION} | Help }}`;
helpMsg += [
{ command: MOD_COMMAND, description: "followed by..." },
{ command: "command", description: "description" },
{ command: "command", description: "description" },
{ command: "--option", description: "description" }
].reduce((helpText, help) => {
helpText += `{{${help.command}=${help.description} }}`;
return helpText;
}, "");
writeChat(helpMsg);
}
/**
* Parse command line into options object
* @param {string[]} args - Command line arguments
* @returns {object}
* @example
* !xxx {{ --this|thisValue --that|thatValue }}
* results in
* options = {
* this: thisValue
* that: thatValue
* }
*/
function getOptions(args) {
const options = {};
for (const arg of args) {
if (arg.indexOf('--') === 0) {
const [ name, value ] = arg.slice(2).split('|');
value = (value || 'true').toLowerCase() === 'true' ? true : value;
value = value.toLowerCase() === 'false' ? false : value;
options[name] = value;
}
}
return options;
}
/**
* Process the MOD chat command
* @param {object} chatMsg - Roll20 chat message object
*/
function handleInput(chatMsg) {
const [ command, ...args ] = splitMessage(chatMsg);
if (chatMsg.type !== 'api' || command.indexOf(MOD_COMMAND) !== 0) return;
if (args.length > 0) {
if (args[0] === '{{') args.shift();
if (args[args.length - 1] === '}}') args.pop();
}
const action = args[0] || "";
// help command
if (action.toLowerCase() === 'help') {
displayHelp();
return;
}
// config command
if (action.toLowerCase() === 'config') {
args.shift();
configSetup(args);
return;
}
const options = getOptions(args);
// the rest of the script code here...
}
/**
* Upgrade state schema
*/
function migrateState() {
// code here any changes to the state schema
setParam("version", MOD_VERSION);
}
/**
* Check for 1st time run or upgrade
*/
function checkInstall() {
if (!state[STATEKEY]) {
state[STATEKEY] = MOD_BASE_STATE;
writeChat(`${GM_ONLY} Type '${MOD_COMMAND} help' for help on commands`);
writeChat(`${GM_ONLY} Type '${MOD_COMMAND} config' to configure the MOD script`);
}
if (getParam("version") !== MOD_VERSION) {
migrateState();
}
writeLog(state[STATEKEY], true);
}
/**
* Register event handler functions
*/
function registerEventHandlers() {
/**
* Wire-up event for API chat message
*/
on('chat:message', handleInput);
}
return {
name: MOD_NAME,
version: MOD_VERSION,
checkInstall,
registerEventHandlers,
};
})();
/**
* Runs when game/campaign loaded and ready
*/
on('ready', function () {
ModName.checkInstall();
ModName.registerEventHandlers();
log(`${ModName.name} version ${ModName.version} running`);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment