Last active
March 30, 2024 08:24
-
-
Save stephaned68/03af77f48568ca2bead3926ff4658cc2 to your computer and use it in GitHub Desktop.
A template for writing Roll20 MOD scripts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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