diff --git a/CalenderData/1.0/CalenderData.js b/CalenderData/1.0/CalenderData.js index bd0bfc969e..6d419f78d2 100644 --- a/CalenderData/1.0/CalenderData.js +++ b/CalenderData/1.0/CalenderData.js @@ -1,4 +1,4 @@ -// Github: https://github.com/boli32/QuestTracker/blob/main/QuestTracker.js +// Github: https://github.com/Roll20/roll20-api-scripts/tree/master/CalenderData // By: Boli (Steven Wrighton): Professional Software Developer, Enthusiatic D&D Player since 1993. // Contact: https://app.roll20.net/users/3714078/boli // Readme https://github.com/boli32/QuestTracker/blob/main/README.md @@ -16139,52 +16139,52 @@ on('ready', () => { }, "wind": { "0": { - "description": "Completely calm, an unnatural stillness in the air." + "description": "Completely calm, no movement in the air." }, "5": { - "description": "Barely perceptible breeze, air feels eerily still." + "description": "Still air, indistinguishable from calm conditions." }, "10": { - "description": "Gentle stirrings, slight movement in the air." + "description": "Barely perceptible movement, no noticeable effect." }, "15": { - "description": "Very light breeze, barely noticeable and pleasant." + "description": "Faint air currents, only detectable with sensitive instruments." }, "20": { - "description": "Light breeze, occasional rustling of leaves." + "description": "Minimal movement, not strong enough to rustle leaves." }, "25": { - "description": "Noticeable breeze, light objects may shift slightly." + "description": "Slight stirrings, barely enough to move small particles." }, "30": { - "description": "Fresh breeze, tree branches show slight movement." + "description": "Extremely light breeze, slight movement in smoke." }, "35": { - "description": "Moderate breeze, pleasant but noticeable resistance while walking." + "description": "Very gentle air movement, only felt in an open field." }, "40": { - "description": "Brisk wind, small branches begin to sway." + "description": "Soft breeze, barely enough to move thin grass." }, "45": { - "description": "Strong breeze, noticeable movement of larger branches." + "description": "Light breeze, small leaves begin to flutter." }, "50": { - "description": "Blustery wind, significant movement of objects outdoors." + "description": "Noticeable breeze, small objects might shift slightly." }, "60": { - "description": "Gale force, walking becomes difficult, minor damage possible." + "description": "Fresh breeze, tree leaves sway, noticeable wind resistance while walking." }, "70": { - "description": "Strong gale, structural damage to weak buildings and trees likely." + "description": "Strong wind, difficult to walk against, small branches sway." }, "80": { - "description": "Severe storm, hazardous conditions with potential widespread damage." + "description": "Severe windstorm, large branches move, walking becomes challenging." }, "90": { - "description": "Violent storm, extreme danger and widespread destruction likely." + "description": "Very strong storm winds, debris begins to fly, structures may take damage." }, "100": { - "description": "Hurricane force, catastrophic devastation expected." + "description": "Extreme hurricane-force winds, widespread devastation likely." } }, "precipitation": { diff --git a/CommandMaster/4.0.1/CommandMaster.js b/CommandMaster/4.0.1/CommandMaster.js new file mode 100644 index 0000000000..9beda8ebcd --- /dev/null +++ b/CommandMaster/4.0.1/CommandMaster.js @@ -0,0 +1,5797 @@ +// Github: https://github.com/Roll20/roll20-api-scripts/tree/master/CommandMaster +// Beta: https://github.com/DameryDad/roll20-api-scripts/tree/CommandMaster/CommandMaster +// By: Richard @ Damery +// Contact: https://app.roll20.net/users/6497708/richard-at-damery + +var API_Meta = API_Meta||{}; // eslint-disable-line no-var +API_Meta.CommandMaster={offset:Number.MAX_SAFE_INTEGER,lineCount:-1}; +{try{throw new Error('');}catch(e){API_Meta.CommandMaster.offset=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-8);}} + +/** + * CommandMaster.js + * + * * Copyright 2020: Richard @ Damery. + * Licensed under the GPL Version 3 license. + * http://www.gnu.org/licenses/gpl.html + * + * This script is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This script is the GM control capability for the RedMaster series + * of APIs for Roll20. It allows the other RedMaster series APIs to + * register with it with their current commands and ability button specs + * which are used by CommandMaster to build the GM Maintenance Menu, + * and to update Character Sheets that use the commands in abilities + * with the latest command structures. It supports other functions + * useful to the GM as needed. + * + * v0.001 02/05/2021 Initial creation as a clone of attackMaster + * v0.002-1.025 Early development. See v2.027 for details. + * v2.026 27/02/2022 Added Class-DB as a standard database. Fixed 'Specials' action + * button & updated command registration. Fixed command registration errors. + * Added token-setup buttons to add all possible character Powers & Priest + * spells from Class-DB. + * v2.027 10/03/2022 Add sychronisation of Database indexing to ensure happens after + * all databases loaded by all APIs before indexing done. Add + * "Creature" character class. Use AttackMaster version of --check-saves + * to unify character setup. Added token-setup buttons to check who + * controls what Char Sheets & toggle control DM/Player + * v2.030 28/03/2022 Moved all Table Mgt, Ability Mgt, Chat Mgt, Database Mgt to a + * shared library + * v2.031 24/04/2022 Moved all game-specific and character sheet-specific data structures + * to the RPGM game-specific library api. + * v0.2.32 20/07/2022 Converted to use revised internal database structures + * v0.2.33 17/09/2022 Moved additional common RPGMaster functions to Library. Moved common + * help handouts to Library. Optionally allow semi-colons as terminators + * for escaped characters in commands. Improve generating of spell lists + * for Priest classes, and Power lists for all classes. Change --help to + * provide a menu of links to help handouts + * v1.3.00 17/09/2022 First release of RPGMaster APIs with RPG-version-specific Library + * v1.3.01 04/10/2022 Allow Wizard and Priest spells to be stored as powers + * v1.3.02 23/10/2022 Fixed error with Specials action button + * v1.3.03 29/10/2022 Trap initialisation errors. Add All Powers will add race powers from + * the race database. Add Race selection to Class menu. + * v1.3.04 17/11/2022 Added possible creature data attributes, & fixed certain creature + * setup issues. Added weapon data update after race/class/level change + * to support things like attacks/round improvement + * v1.4.01 28/11/2022 Added support for the fighting Styles-DB. Fixed help menu. Improved + * creature attribute dice rolling. Improved creature HD & HP calcs. + * Added creature attribute dexdef dexterity bonus for ranged attacks. + * Added use of alpha indexed drop down for selection of creatures. + * Added suppression of power inheritance. Added function to check + * player, speakingas, token & character names for illegal characters. + * v1.4.02 13/12/2022 Added API button on token & character name checks to support immediate + * correction. Add ability to specify weapons and armour for creatures + * with probability of different sets. Split handling of PC Race specifications + * and Creature specifications, so that Races don't get set up with + * spurious creature specs. + * v1.4.03 16/01/2023 Added creature attkmsg & dmgmsg attributes to support messages to + * display with attack and damage rolls respectively. + * v1.4.04 24/01/2023 Added support for converting manually created character sheets to + * RPGMaster format. Added ability to configure the default token bars. + * Added functions to set token bars for all campaign tokens. Added + * separate Ammo list for Equipment conversion. Tidied up Token-Setup + * menu. + * v1.4.05 02/03/2023 Non-functional update release to maintain version sequence. + * v1.4.06 08/04/2023 Added support for when a magic item character sheet is first dragged + * to the player map, to set it to isdrawing=true and link to the CS. + * v1.4.07 14/04/2023 Fixed bug stopping Token Setup, Add to Spellbook from working. + * v1.5.01 19/05/2023 Non-functional version number synchronisation release. + * v2.1.0 06/06/2023 Made many more functions asynchronous to multi-thread. Parsed a + * creature's description in its database entry and created a Character + * Sheet Bio from it. + * v2.2.0 21/07/2023 Implemented The Aaron's API_Meta error handling. Added senderId override + * capability as id immediately after !magic & before 1st --cmd. Added + * Drag & Drop containers to the Drag & Drop system, using the + * Race-DB-Containers & Locks-Traps-DB databases. Made reSpellSpecs, + * reClassSpecs & reAttr accessible by other APIs from the library. + * Made –initialise command automatic on startup if the defined GM macros + * have changed. Enhanced parseDesc() to cater for both creatures and + * containers, locks & traps. Removed potential setTimeout() issues + * with asynchronous use of variable values – passed as parameters instead. + * Fixed error when cs attribute 'trap-name' is missing or empty. + * v2.3.0 30/09/2023 Added drag & drop query attribute that adds an extra level of query to + * drag & drop creature selection and returns variables based on the selection. + * Fixed parsing of {{prefix=...}} in drag & drop creature descriptions. + * Added new maths evaluation function to support new race 'query' variables. + * Add memorisation of random spells for spell-casting drag & drop creatures. + * Added new dice roll evaluator. Moved characterLevel() to library. + * v2.3.1 19/10/2023 Added "Token Image Quick Copy" --copyimg function. Added ^^gmid^^ field + * for container macros to support TokenMod --api-as parameter. Stop + * specification of races not in the database for now. Ensured spell names + * in spell books are hyphenated to avoid inconsistencies. + * v2.3.2 20/10/2023 Added age: attribute as a condition value for Drag & Drop creature items. + * Improved the maths parser to iterate more deeply. Changed how 'GM-Roll' + * flag builds GM rolls into container macros. Fixed hyphenation of + * reviewed weapons, spells & powers. + * v3.0.0 01/10/2023 Added support for other character sheets & game systems. + * Moved parseData() to library. + * v3.1.0 16/12/2023 Modified to support AD&D1e character sheet and rule set. Fixed weapon + * multi-hand parsing issue, clashing with % chance. Added library functions + * to support different ways of specifying and displaying the class & level + * on the character sheet. Fixed support for Drag & Drop creature hp ranges. + * v3.2.0 08/02/2024 Add proficiency list for AD&D1e version. Fix bug in some creature definitions + * that listed weapons in-hand. + * v3.2.1 23/02/2024 Improved parseStr() handling of undefined or empty strings without erroring. + * v3.3.0 18/03/2024 Non-functional sequencing release to synchronise versions + * v3.4.0 18/03/2024 Non-functional sequencing release to synchronise versions + * v3.5.0 18/03/2024 Non-functional sequencing release to synchronise versions + * v3.5.1 03/08/2024 Fix scaling of container images to match page. + * v4.0.1 20/09/2024 Fix bug in container scaling. Add NPC management, including adding automatic + * rogue table completion with random level mods, and optional random equipment + * and magic items added to items carried. Added weapon proficiencies for Creatures + * & NPCs, default to Proficient. Added Extra Strength calculation for strength of 18 + * (if specified for NPC - not class dependent). Corrected allocation of Spell Menu + * action button so all those able to cast spells have it. Allow query results in + * weapon, armour & item specifications for Creatures/NPCs. + */ + +var CommandMaster = (function() { + 'use strict'; + var version = '4.0.1', + author = 'RED', + pending = null; + const lastUpdate = 1738351019; + + /* + * Define redirections for functions moved to the RPGMaster library + */ + + const getRPGMap = (...a) => libRPGMaster.getRPGMap(...a); + const getHandoutIDs = (...a) => libRPGMaster.getHandoutIDs(...a); + const setAttr = (...a) => libRPGMaster.setAttr(...a); + const attrLookup = (...a) => libRPGMaster.attrLookup(...a); + const getTableField = (...t) => libRPGMaster.getTableField(...t); + const getTable = (...t) => libRPGMaster.getTable(...t); + const getLvlTable = (...t) => libRPGMaster.getLvlTable(...t); + const initValues = (...v) => libRPGMaster.initValues(...v); + const setAbility = (...a) => libRPGMaster.setAbility(...a); + const abilityLookup = (...a) => libRPGMaster.abilityLookup(...a); + const doDisplayAbility = (...a) => libRPGMaster.doDisplayAbility(...a); + const getAbility = (...a) => libRPGMaster.getAbility(...a); + const getDBindex = (...a) => libRPGMaster.getDBindex(...a); + const updateHandouts = (...a) => libRPGMaster.updateHandouts(...a); + const findThePlayer = (...a) => libRPGMaster.findThePlayer(...a); + const findCharacter = (...a) => libRPGMaster.findCharacter(...a); + const fixSenderId = (...a) => libRPGMaster.fixSenderId(...a); + const calcAttr = (...a) => libRPGMaster.calcAttr(...a); + const rollDice = (...a) => libRPGMaster.rollDice(...a); + const evalAttr = (...a) => libRPGMaster.evalAttr(...a); + const getCharacter = (...a) => libRPGMaster.getCharacter(...a); + const classObjects = (...a) => libRPGMaster.classObjects(...a); + const characterLevel = (...a) => libRPGMaster.characterLevel(...a); + const updateClassLevel = (...a) => libRPGMaster.updateClassLevel(...a); + const displayClassLevel = (...a) => libRPGMaster.displayClassLevel(...a); + const addMIspells = (...a) => libRPGMaster.addMIspells(...a); + const getMagicList = (...a) => libRPGMaster.getMagicList(...a); + const rogueLevelPoints = (...a) => libRPGMaster.rogueLevelPoints(...a); + const handleCheckThiefMods = (...a) => libRPGMaster.handleCheckThiefMods(...a); + const handleCheckSaves = (...a) => libRPGMaster.handleCheckSaves(...a); + const handleCheckWeapons = (...a) => libRPGMaster.handleCheckWeapons(...a); + const parseData = (...a) => libRPGMaster.parseData(...a); + const resolveData = (...a) => libRPGMaster.resolveData(...a); + const caster = (...a) => libRPGMaster.caster(...a); + const findPower = (...a) => libRPGMaster.findPower(...a); + const handleSetNPCAttributes = (...a) => libRPGMaster.handleSetNPCAttributes(...a); + const handleGetBaseThac0 = (...a) => libRPGMaster.handleGetBaseThac0(...a); + const creatureWeapDefs = (...a) => libRPGMaster.creatureWeapDefs(...a); + const sendToWho = (...m) => libRPGMaster.sendToWho(...m); + const sendPublic = (...m) => libRPGMaster.sendPublic(...m); + const sendAPI = (...m) => libRPGMaster.sendAPI(...m); + const sendFeedback = (...m) => libRPGMaster.sendFeedback(...m); + const sendResponse = (...m) => libRPGMaster.sendResponse(...m); + const sendResponsePlayer = (...p) => libRPGMaster.sendResponsePlayer(...p); + const sendResponseError = (...e) => libRPGMaster.sendResponseError(...e); + const sendError = (...e) => libRPGMaster.sendError(...e); + const sendCatchError = (...e) => libRPGMaster.sendCatchError(...e); + const sendParsedMsg = (...m) => libRPGMaster.sendParsedMsg(...m); + const sendGMquery = (...m) => libRPGMaster.sendGMquery(...m); + const sendWait = (...m) => libRPGMaster.sendWait(...m); + + /* + * Handle for reference to character sheet field mapping table. + * See RPG library for your RPG/character sheet combination for + * full details of this mapping. See also the help handout on + * RPGMaster character sheet setup. + */ + + var fields = { + defaultTemplate: 'RPGMdefault', + warningTemplate: 'RPGMwarning', + menuTemplate: 'RPGMmenu', + messageTemplate: 'RPGMmessage', + }; + + /* + * Handle for the Database Index, used for rapid access to the character + * sheet ability fields used to hold database items. + */ + + var DBindex = {}; + + /* + * Handle for the API Databases in the RPGM Game-specific Library + */ + + var dbNames = {}; + + /* + * Handle for the library object used to pass back RPG & character sheet + * specific data tables. + */ + + var RPGMap = {}; + + const design = { + turncolor: '#D8F9FF', + roundcolor: '#363574', + statuscolor: '#F0D6FF', + statusbgcolor: '#897A87', + statusbordercolor: '#430D3D', + edit_icon: 'https://s3.amazonaws.com/files.d20.io/images/11380920/W_Gy4BYGgzb7jGfclk0zVA/thumb.png?1439049597', + delete_icon: 'https://s3.amazonaws.com/files.d20.io/images/11381509/YcG-o2Q1-CrwKD_nXh5yAA/thumb.png?1439051579', + settings_icon: 'https://s3.amazonaws.com/files.d20.io/images/11920672/7a2wOvU1xjO-gK5kq5whgQ/thumb.png?1440940765', + apply_icon: 'https://s3.amazonaws.com/files.d20.io/images/11407460/cmCi3B1N0s9jU6ul079JeA/thumb.png?1439137300', + open_img: 'https://s3.amazonaws.com/files.d20.io/images/355714477/jouzZ3bALliE0SV-1NWaNg/thumb.png?1692686089', + closed_img: 'https://s3.amazonaws.com/files.d20.io/images/355657918/NcpSVNL3LIpQPNxDcCBpog/thumb.png?1692651167|70|50', + grey_button: '"display: inline-block; background-color: lightgrey; border: 1px solid black; padding: 4px; color: dimgrey; font-weight: extra-light;"', + dark_button: '"display: inline-block; background-color: lightgrey; border: 1px solid black; padding: 4px; color: black; font-weight: normal;"', + selected_button: '"display: inline-block; background-color: white; border: 1px solid red; padding: 4px; color: red; font-weight: bold;"', + boxed_number: '"display: inline-block; background-color: yellow; border: 1px solid blue; padding: 2px; color: black; font-weight: bold;"', +// grey_action: '<span style="display: inline-block; background-color: lightgrey; border: 1px solid black; padding: 4px; color: dimgrey; font-weight: extra-light;">$1$2</span>$4', + grey_action: '<span style="display: inline-block; background-color: lightgrey; border: 1px solid black; padding: 4px; color: dimgrey; font-weight: extra-light;">$1</span>$3', + }; + + /* + * CommandMaster related help handout information. + */ + + const handouts = Object.freeze({ + CommandMaster_Help: {name:'CommandMaster Help', + version:4.01, + avatar:'https://s3.amazonaws.com/files.d20.io/images/257656656/ckSHhNht7v3u60CRKonRTg/thumb.png?1638050703', + bio:'
New: Added Drag & Drop help in this help handout
' + +'New: Drag & Drop NPCs added, and enhanced Creature definitions
' + +'New: Allow query results in Drag & Drop weapon, armour & item deinitions
' + + +'The CommandMaster API is part of the RPGMaster suite of APIs for Roll20, and manages the initialisation of a Campaign to use the RPGMaster APIs, communication and command syntax updates between the APIs and, most importantly for the DM, easy menu-driven setup of Tokens and Character Sheets to work with the APIs.
' + +'The CommandMaster API is called using !cmd.
' + +'!cmd --initialise' + +'
Commands to be sent to the CommandMaster API must be preceded by two hyphens \'--\' as above for the --initialise command. Parameters to these commands are separated by vertical bars \'|\', for example:
' + +'!cmd --register action|description|api-call|api-command|parameters' + +'
Commands can be stacked in the call, for example:
' + +'!cmd --initialise --abilities' + +'
When specifying the commands in this document, parameters enclosed in square brackets [like this] are optional: the square brackets are not included when calling the command with an optional parameter, they are just for description purposes in this document. Parameters that can be one of a small number of options have those options listed, separated by forward slash \'/\', meaning at least one of those listed must be provided (unless the parameter is also specified in [...] as optional): again, the slash \'/\' is not part of the command. Parameters in UPPERCASE are literal, and must be spelt as shown (though their case is actually irrelevant).
' + +'[General API Help]' + +'The CommandMaster API coordinates other APIs in the RPGMaster API series and provides the DM with facilities to set the Campaign up to use them. It will initialise a Campaign in Roll20 to use the RPGMaster series APIs. APIs can register their commands with CommandMaster and, should they change in the future, CommandMaster will search all Character Sheets and databases for that command and offer the DM the option to automatically update any or all of those found to the new command structure of that API. Selected Tokens and their associated Character Sheets can be set up with the correct Token Action Buttons, with spell-users given spells in their spell book, fighters given weapon proficiencies, setting saving throws correctly, and linking token circles to standard Character Sheet fields.
' + +'Using the --initialise command will add a number of Player Macros for the DM that will run the most-used RPGMater DM commands, which can be flagged to appear in the Macro Bar at the bottom of the DM\'s screen for ease of access.
' + +'Selecting one or multiple tokens and running the --abilities command will allow token action buttons and RPGMaster API capabilities to be set up for all the represented Character Sheets at the same time, though all Character Sheets will be set up the same way.
' + +'Any API command can be registered with CommandMaster using the --register command. This will allow the command registered to be added as a Token Action Button to Character Sheets by the abilities command, and to be optionally updated in all Character Sheets wherever used should the details of the registration change.
' + +'Danger: this command is very powerful, and can ruin your campaign if mis-used! The --edit command can be used to change any string in Character Sheet ability macros to any other string, using \'escaped\' characters to replace even the most complex strings. However, use with care!
' + +'CommandMaster can combine all its capabilities for managing character sheets to automatically populate a blank character sheet with data specific to an NPC of a particular race, class and level, or to make that character sheet represent a creature from The Monsterous Compendium. Data held in the Race-DB-NPCs and Race-DB-Creatures databases is used to configure the blank character sheet, and the GM or game creator can add their own bespoke NPC and creature definitions to their own databases to enhance and extend those provided in the same fashion as with other RPGMaster databases and APIs. See the Class & Race Database Help handout for more information.
' + +'To create a Drag & Drop NPC or Creature, add a blank character sheet to the Journal using the standard Roll20 [Character+] button at the top of the Roll20 Journal. Give the sheet a name and an image as desired, then close it. Drag the blank sheet onto the map surface to drop a token, select the token just dropped, and down the bottom of the Chat Window a dialog will have appeared, with options to select the [Creature] or [NPC] for the sheet (as well as other options which can be ignored for Drag & Drop). Use the buttons to select the NPC or Creature you want from the Roll Queries that appear on screen, which may also ask for further information (such as level of the NPC, or age of the creature etc). Be patient and wait for the API to set the character sheet up! This is one of the most complex things you can ask RPGMaster APIs to do - just think how long it would take you to set up a character sheet manually... Once complete, a number of dialogs will appear in the Chat Window describing the characteristics of the NPC or creature created. It is then necessary to click away from the token (de-select it) and then select it again to refresh the Action Buttons for the token. The token and character sheet can then be immediately used in play as that NPC or Creature.
' + +'It is possible to re-write any character sheet as a different NPC or Creature using the GM\'s [Token Setup] macro-menu button, or using the !cmd --abilities command, with the token representing the character sheet selected, and selecting the [Choose Race/Class] function on the displayed dialog. This displays the same dialog for selecting [NPC] or [Creature] as described above. The GM will be prompted for confirmation of over-writing an already populated sheet.
' + +'Drag & Drop Containers work in a similar fashion to other Drag & Drop functions for setting up character sheets. Drop a blank character sheet onto a map to drop a token, ensure that token is selected, then go to the dialog that appears at the bottom of the Chat Window. Select the [Container] button, and a list of various types of container appears as a Roll Query from which you can select a type. Then a new dialog appears in the Chat Window asking you to specify the characteristics of the container to create. The container can have a lock of various types (e.g. a combination lock, a password lock, a simple key lock, etc.), and can be trapped with various types of trap (such as a poison dart trap, an explosive runes trap, etc).
' + +'The types of container, and the macros used to drive the container\'s actions, are all defined in the Locks-Traps-DB. As with other RPGMaster databases, the GM or game creator can add their own containers, locks and traps by adding their own database. Refer to the Locks and Traps Help handout to get more information.
' + + +'--initialise' + +'
' + +'--abilities
--conv-spells' + +'
' + +'--conv-items
' + +'--token-defaults
' + +'--check-chars
' + +'--class-menu [token_id]
' + +'--add-spells [POWERS/MUSPELLS/PRSPELLS] | [level]
' + +'--add-profs
' + +'--set-prof [NOT-PROF/PROFICIENT/SPECIALIST/MASTERY] | weapon | weapon-type
' + +'--set-all-prof
' + +'--token-img [token_id]
--register action|description|api-call|api-command|parameters' + +'
' + +'--edit old-string | new-string
--help' + +'
' + +'--debug [ON/OFF]
--initialise' + +'
This command creates a number of Player Macros which can be found under the Player Macro tab in the Chat window (the tab that looks like three bulleted lines, next to the cog). These macros hold a number of DM commands that are useful in setting up and running a campaign. It is recommended that the "Show in Bar" flags for these macros are set, and the "Show Macro Bar" flag is set (the macro bar is standard Roll20 functionality - see Roll20 Help Centre for more information).
' + +'The buttons added are:
' + +'The DM can drag Macro Bar buttons around on-screen to re-order them, or even right-click them to change their name and colour of the button. Feel free to do this to make the Macro Bar as usable for you as you desire.
' + +'--abilities' + +'
Displays a menu with which one or more selected tokens and the Character Sheets they represent can be set up with the correct Token Action Buttons and data specific to the RPGMaster APIs, to work with the APIs in the best way. The menu provides buttons to add any command registered with CommandMaster (see --register command) as a Token Action Button, change control of the sheet and set the visibility of the token name to players (which also affects RoundMaster behaviour), set the Character Class and Level of the Character, add spells to spell books, add granted powers, add or change weapon proficiencies and proficiency levels for each weapon, set the correct saving throws based on race, class & level of character / NPC / creature, and optionally clear or set the Token \'circles\' to represent AC (bar 1), base Thac0 (bar 2) and HP (bar 3). Essentially, using this menu accesses the commands in section 2 without the DM having to run them individually.
' + +'All tokens selected when menu items are used will be set up the same way: exceptions to this are using the Set Saves button (sets saves for each selected token/character sheet correctly for the specifications of that sheet), and the Set All Profs button (sets weapon proficiencies to proficient based on the weapons in each individual token/character sheet\'s item bag). Different tokens can be selected and set up differently without having to refresh the menu.
' + +'The commands in this section can be accessed using the --abilities command menu. The individual commands below are used less frequently.
' + +'--conv-spells' + +'
Works on multiple selected tokens representing several Character Sheets.
' + +'For Character Sheets that have not been created using the commands provided by the !cmd --abilities menu, pre-existing from previous Roll20 campaigns using the Advanced D&D2e Character Sheet, this command does its best to convert all spells in tables on the Character Sheet to RPGMaster format and replace them with spells that exist in the RPGMaster spell databases. Those that the system can\'t automatically match are subsequently displayed in a menu, with additional buttons that list the spells that do exist in the databases that can be used to replace them by selecting both the spell to be replaced and the replacement spell.
' + +'It is possible that not all spells will be able to be replaced, if the Character Sheet reflects campaign experience where bespoke spells or spells from other handbooks have been available. In this case, the spells can be left unconverted, and the DM might add the spells to their own databases using the information provided in the Magic Database Help handout. Until the spells are added to the databases, they will not work, and cannot be memorised for spell use.
' + +'This command can be used on multiple selected tokens, as stated above. All the Character Sheets represented by the selected tokens will be converted, and the displayed list of spells to manually match represents the unmatched spells from all those Character Sheets. As the spells are manually matched, they will be replaced on all of the selected Character Sheets.
' + +'--conv-items' + +'
Works on multiple selected tokens representing several Character Sheets.
' + +'As for the --conv-spells command, Character Sheets that have not been created using the commands provided by the !cmd --abilities menu, pre-existing from previous Roll20 campaigns using the Advanced D&D2e Character Sheet, this command does its best to convert all weapons, armour, other items of equipment and magical items such as potions, rings etc, in tables on the Character Sheet to RPGMaster format and replace them with weapons, armour and items that exist in the RPGMaster spell databases. Those that the system can\'t automatically match are subsequently displayed in a menu, with additional buttons that list the items that do exist in the databases that can be used to replace the unknown ones by selecting both the item to be replaced and the replacement item.
' + +'It is possible that not all weapons, armour, equipment and especially magic items will be able to be matched if the Character Sheet reflects campaign experience where bespoke magic items and equipment or equipment from other handbooks have been available. In this case, the items can be left unconverted, and the DM might add the items to their own databases using the information provided in the Weapon & Armour Database Help or Magic Database Help handouts. Until the items of equipment are added to the databases, if they are weapons they cannot be taken in-hand to fight with, armour will not be counted towards armour class calculations, and items that contribute to saving throws will not do so.
' + +'As with the --conv-spells command, this command can be used on multiple selected tokens. All the Character Sheets represented by the selected tokens will be converted, and the displayed list of items to manually match represents the unmatched items from all those Character Sheets. As the items are manually matched, they will be replaced on all of the selected Character Sheets.
' + +'--token-defaults' + +'
This command uses the selected token as a model to set the default token bar mappings that will be used in future by the RPGMaster APIs.
' + +'The standard defaults distributed with the APIs are for token bar1 to represent AC, bar2 to represent Thac0-base, and bar3 to represent HP. However, alternative mappings can be made. It is highly recommended that HP, AC and Thac0-base are represented in some order because these are the most common values to be affected by spells and circumstances, both in and out of combat situations.
' + +'If no token is selected, or the token selected to be the model does not have any bars linked to a character sheet, an error message will be displayed. If some but not all the bars are linked, then any bars not linked will be automatically matched to some of the recommended Character Sheet fields of AC, Thac0-base, and HP (in that order of priority).
' + +'Once this mapping is done, a menu will be displayed that can be used to map other tokens to the new defaults: either just the selected tokens, or all tokens in the campaign, or just those tokens that have bars currently linked to Character Sheets (i.e. excluding creature mobs with multiple tokens with unlinked bars representing a single character sheet). A button also exists to clear the bar links for all selected tokens to create creature mobs.
' + +'--check-chars' + +'
Displays a list of every Character Sheet with a defined Class, Level, or Monster Hit Dice categorised by DM Controlled, Player Controlled PCs & NPCs, Player Controlled Creatures, and Controlled by Everyone. Each name is shown as a button which, if selected, swaps control of that Character Sheet between DM control and the control of a selected Player (the Player, of course, must be one that has already accepted an invite to join the campaign). A button is also provided at the bottom of this menu to toggle the running of this check whenever the Campaign is loaded.
' + +'--class-menu [token_id]' + +'
Takes an optional ID for a token representing a character. If not specified, takes the currently selected token
' + +'Displays a menu from which the Race, Class and Level of a Character can be set, or a Creature species can be selected. Setting the Race, Class and Level of a Character (PC or NPC) enables all other capabilities to be used as appropriate for that character sheet in this and other APIs in the RPGMaster API suite, such as spell use, appropriate race & class powers, selection of allowed weapons, and the like. Selecting a Creature species automatically sets up the Character Sheet in an optimal way for the APIs to use it to represent the chosen creature, including saves, armour class, hit dice and rolling of hit points, as well as special attacks such as paralysation & level drain of high level undead, spell use by the likes of Orc Shamen, regeneration powers, and so on. However, it does not automatically give weapons, armour equipment, or magic items to Creatures - if appropriate this still needs to be done by the DM/Game Creator.
' + +'DMs/Game Creatores can add to or amend the Class, Race and Creature definitions. Refer to the appropriate database help handout distributed with the APIs and created as handouts in your campaign for more information.
' + +'--add-spells [POWERS/MUSPELLS/PRSPELLS] | [level]' + +'
Displays a menu allowing spells in the Spells Databases to be added to the Character Sheet(s) represented by the selected Token(s). If no spell type and/or spell level is specified, the initial menu shown is for Level 1 Wizard spells (MUSPELLS). Buttons are shown on the menu that allow navigation to other levels, spell types and powers. For Priests, a button is also provided to add every spell allowed for the Priest\'s Class to their spellbooks at all levels (of course, they will only be able to memorise those that their experience level allows them to). For all Character Classes that have Powers (or Power-like capabilities, such as Priestly Turn Undead or Paladin Lay on Hands), there is a button on the Powers menu to add Powers that the character\'s Class can have.
' + +'Note: adding spells / powers to a sheet does not mean the Character can immediately use them. They must be memorised first. Use the commands in the MagicMaster API to memorise spells and powers.
' + +'--add-profs' + +'
Displays a menu from which to select proficiencies and level of proficiency for any weapons in the Weapon Databases for the Character Sheet(s) represented by the selected tokens. Also provides a button for making the Character proficient in all weapons carried (i.e. those currently in their Item table).
' + +'All current proficiencies are displayed, with the proficiency level of each, which can be changed or removed. It is also now possible to select proficiencies in Fighting Styles as introduced by The Complete Fighter\'s Handbook: these can be found under the Choose Style button, and can also be set as Proficient or Specialised. Selecting a Fighting Style proficiency grants benefits as defined in the Handbook, or as modified by the DM - see the Styles Database Help handout for more information.
' + +'Note: this does more than just entering the weapon in the proficiency table. It adds the weapon group that the weapon belongs to as a field to the table (see weapon database help handouts for details), which is then used by the AttackMaster API to manage related weapon attacks and give the correct proficiency bonuses or penalties for the class and weapon used.
' + +'--set-prof [NOT-PROF/PROFICIENT/SPECIALIST/MASTERY] | weapon | weapon-type' + +'
Sets a specific weapon proficiency to a named level. If the proficiency level is omitted, PROFICIENT is assumed. If the weapon already exists in the proficiencies table, the existing proficiency level is updated to that specified. Otherwise, the weapon (and its weapon group) are added to the table at the specified level.
' + +'Note: this does more than just entering the weapon in the proficiency table. It adds the weapon group that the weapon belongs to as a field to the table (see weapon database help handouts for details), which is then used by the AttackMaster API to manage related weapon attacks and give the correct proficiency bonuses or penalties for the class and weapon used.
' + +'--set-all-prof' + +'
Adds all currently carried weapons (those in the Items table) to PROFICIENT, saving them and their weapon group to the weapon proficiency table. Those weapons found that are already in the table are reset to PROFICIENT (overwriting any existing different proficiency level). Any other proficiencies already in the table are not altered.
' + +'Note: this command always adds a weapon proficiency called innate. This proficiency is used for attacks with innate weapons, such as claws and bites, but also for spells that require a touch attack. Indeed, to make this even more specific, the weapons database distributed with the AttackMaster and MagicMaster APIs includes a weapon called Touch.
' + +'Tip: if using the MagicMaster API then running the !magic --gm-edit-mi command and adding weapons before running this command can speed up setting up character sheets.
' + +'--token-img [token_id]' + +'
Displays a menu for changing the images and variables used for containers. The optional token_id (defaults to the selected token) must represent a character sheet, and the dialog expects it to be a character sheet of a container, potentially one set up using the Drag & Drop container functionality. Containers set up using Drag & Drop will have information about the use of images and variables in the "Bio" tab of the Character Sheet.
' + +'--register action|description|api-call|api-command|parameters' + +'
Register an API command with the CommandMaster API to achieve two outcomes: allow the command to be set up as a Token Action Button, and/or automatically maintain & update the syntax of the commands in Character Sheet ability macros and the RPGMaster API databases.
' + +'This is a powerful and potentially hazardous command. Registry of an API command is remembered by the system in the state variable, which is preserved throughout the life of the Campaign in Roll20. If a subsequent registration of the same action has different parameters, the system detects this and searches all Character Sheet ability macros for the old version of the command and replaces all of them with the new command. It also changes the parameters, using a syntax including a range of character \'escapes\' to substitute characters that Roll20 might otherwise interpret as commands itself. In detail, the --register command takes:
' + +'action: | the unique name given to this command in the whole system. This can be any legal text name including A-Z, a-z, 1-9, -, _ only. Must start with an alpha. Case is ignored. |
---|---|
description: | a short description of the command, which is displayed in the menu that allows the command to be added as a Token Action Button. |
api-call: | the API call without the !, e.g. cmd, or magic, etc |
api-command: | the command to be passed to the specified API, with the hyphens replaced by ~~ or plusses replaced by **, e.g. ~~cast-spell or **menu. |
parameters: | the parameters (or initial parameters) to be passed as part of this command to replace the matching existing command parameters. This string is \'escaped\' using the following character replacements: |
Character | Parameter separator | ? | [ | ] | < | > | @ | - | | | : | & | { | } |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Substitute | %% | ^ | << | >> | ` | ~ | | | & | { | } | |||
Alternative | \\vbar; | \\ques; | \\lbrak; | \\rbrak; | \\lt; | \\gt; | \\at; | \\dash; | \\vbar; | \\clon; | \\amp; | \\lbrc; | \\rbrc; |
Commands cannot have a CR (carrage return/new line) in the middle of them, but CR can separate commands in multi-command sequences.
' + +'If the parameter string ends with $$, this will ensure that a complete command up to the next CR is to be replaced (including everything up to the CR even if not part of the command). If there is not a $$ on the end of the parameter string, then only the command and parameters that are matched are replaced (using a parameter count of each old and new parameter separated by \'%%\') - the rest of the line (including any remaining parameters not so matched) will be left in place.
' + +'Here are some examples of registration commands:
' + +'--register Spells_menu|Open a menu with spell management functions|magic|~~spellmenu |\`{selected|token_id}
' + +'--register Use_power|Use a Power|magic|~~cast-spell|POWER%%\`{selected|token_id}
' + +'--register Attack_hit|Do an attack where Roll20 rolls the dice|attk|~~attk-hit|\`{selected|token_id}
--edit existing-string | new-string' + +'
Danger: use this command with extreme care! It can destroy your Campaign! It is recommended that you make a backup copy of your Campaign before using this command. --register is more controlled, as it has been tested with the RPGMaster command strings, and any future releases that change the API commands will be fully tested before release for their effect on Campaigns, with accompanying release notes. Using the --edit function directly can have unintended consequences!
' + +'Replaces an existing \'escaped\' string with a new replacement string in all ability macros on all Character Sheets. These strings both use the same escape sequence replacements as for the --register command (see section 3.1) as in fact --register and --edit use the same functionality.
' + +'Examples of its use are to change API command calls, or Character Sheet field name access in macros should the field names change.
' + +'--help' + +'
This command does not take any arguments. It displays a very short version of this document, showing the mandatory and optional arguments, and a brief description of each command.
' + +'--debug (ON/OFF)' + +'
Takes one mandatory argument which should be ON or OFF.
' + +'The command turns on a verbose diagnostic mode for the API which will trace what commands are being processed, including internal commands, what attributes are being set and changed, and more detail about any errors that are occurring. The command can be used by the DM or any Player - so the DM or a technical advisor can play as a Player and see the debugging messages.
' + +'The RPGMaster APIs use a number of databases to hold Macros defining races, creatures, character classes, spells, powers, magic items and their effects, and now containers, locks & traps. The version of these databases distributed with the APIs are held internally to the APIs. However, the AttackMaster or MagicMaster API command --extract-db can be used to extract any or all standard databases to Character Sheets for examination and update. The APIs are distributed with many class, spell, power, magic item and container definitions, and DMs can add their own containers, locks, traps, character classes, spells, items, weapons, ammo and armour to additional databases in their own database character sheets, with new definitions for database items held in Ability Macros. Additional database character sheets should be named as follows:
' + +'Wizard Spells: | additional databases: MU-Spells-DB-[added name] where [added name] can be replaced with anything you want. |
---|---|
Priest Spells: | additional databases: PR-Spells-DB-[added name] where [added name] can be replaced with anything you want. |
Powers: | additional databases: Powers-DB-[added name] where [added name] can be replaced with anything you want. |
Magic Items: | additional databases: MI-DB-[added name] where [added name] can be replaced with anything you want. |
Character Classes: | additional databases: Class-DB-[added name] where [added name] can be replaced with anything you want. |
Races, Creatures & Containers: | additional databases: Race-DB-[added name] where [added name] can be replaced with anything you want. |
Locks & Traps: | additional databases: Locks-Traps-DB-[added name] where [added name] can be replaced with anything you want. |
Attack Calculations: | additional databases: Attacks-DB-[added name] where [added name] can be replaced with anything you want. |
However: the system will ignore any database with a name that includes a version number of the form "v#.#" where # can be any number or group of numbers e.g. "MI-DB v2.13" will be ignored. This is so that the DM can version control their databases, with only the current one (without a version number) being live.
' + +'There can be as many additional databases as you want. Other Master series APIs come with additional databases, some of which overlap - this does not cause a problem as version control and merging unique macros is managed by the APIs.
' + +'Important Note: all Character Sheet databases must have their \'ControlledBy\' value (found under the [Edit] button at the top right of each sheet) set to \'All Players\'. This must be for all databases, both those provided (set by the API) and any user-defined ones. Otherwise, Players will not be able to run the macros contained in them.
' + +'Each added database has a similar structure, with:
' + +'As with other RPGMaster suite databases, the GM need not worry about creating anything other than a correctly formatted macro entry, and then can run the !magic --check-db Database-Name command to check the database formatting, create the custom attributes and list entries, and re-index all database entries so that the new entries can be immediately used in live play.
' + +'Ability Macros can be whatever the DM wants and can be as simple or as complex as desired. Roll Templates are very useful when defining class, spell, power, magic item, trap and lock ability macros. When a Player or an NPC or Monster views or casts a spell, power or uses a container or magic item the APIs run the relevant Ability Macro from the databases as if it had been run by the Player from the chat window. All Roll20 functions for macros are available.
' + +'If you want to replace any Ability Macro provided in any of the databases, you can do so simply by creating an Ability Macro in one of your own databases with exactly the same name as the provided item to be replaced. The API gives preference to Ability Macros in user-defined databases, so yours will be selected in preference to the one provided with the APIs.
' + +'Important Note: In order for Drag & Drop containers to work, you must have the ChatSetAttr and TokenMod Mods loaded as well as RPGMaster suite APIs and ChatSetAttr must have "Players can modify all characters" option ticked, and TokenMod must have "Players can use --ids" option ticked.
' + +'Any token representing a character sheet can be a container. This includes characters, NPCs, and creatures as well as token/character sheet pairs specifically intended to be containers such as chests, barrels, desks, bags and the like. Characters can have equipment such as weapons and armour added to their sheets, and obtain magic items on their quest - this is the character acting as a "container" for these items and equipment. Characters (and NPCs and some creatures) can search containers to find and loot items from them using the !magic --search command (see MagicMaster Help handout) or the MI menu / Search for MIs & Treasure action selection. They can also store items in a container using !magic --pickorput or MI menu / Store MIs. The GM can also store and edit items in any container by using the GM\'s Add Items dialog, which also can configure the number of slots for items in a container, and the type of a container, as well as many other container and item management functions - see the MagicMaster Help handout for more information on the !magic --gm-edit-mi command (which is what the Add Items button calls).
' + +'When any container is searched by a character (or NPC or creature), depending on the type of container (see below) it will eventually show its contents (if any). The contents can be added by the GM using the GM\'s Add Items dialog, or by characters, NPCs or creatures storing items in the container. The contents of the container are shown to those searching in one of two possible dialogs - one which shows a button for each item or stack of items (long menu), and another which provides a drop-down list of the items (short menu). Either dialog allows the searcher to select items to take from the container and add to their own equipment.
' + +'The GM can use the controls on the Add Items menu to mark any container to either show the exact names of the contents in these dialogs, or only show the type of each item: e.g. Potion of Healing might be shown simply as a Potion, or a Broadsword +2 be displayed as a Long Blade i.e. only describing what the character sees. When the searcher takes the item into their own equipment it might then be listed in that searcher\'s equipment by its true name (unless the GM has also hidden the item as another item or hidden it automatically with the Looks-Like function: see MagicMaster Help and Magic Database Help handouts).
' + +'The GM can also set the maximum number of "slots" that a container has: each unique item or type of item requires a slot to store it in. Once all slots are full, no more items can be stored in the container. Certain types of item can be "stacked": e.g. Flight Arrows can be stacked, with any number of Flight Arrows occupying one slot. However, Flight Arrows and Sheaf Arrows cannot be stacked together, and nor can Flight Arrows and Flight Arrows +1. A wand with multiple charges will perhaps look like a stacked item (as the quantity reflects the number of charges), but another wand of the same type cannot be stacked with it, as each is a unique item. When stacks are looted, a dialog will appear asking how many of the items in the stack want to be taken: alternatively, when a unique item with multiple charges is looted, the whole item will automatically be taken (i.e. a Wand of Missiles with 20 charges cannot be split into two or more with fewer charges).
' + +'Not all containers will react the same way when searched. How the container reacts is determined by the type of container set on the character sheet by the GM using the Add Items dialog, or automatically when creatures or containers are Dragged & Dropped. The current types are:
' + +'Type | # | Description |
---|---|---|
Empty Inanimate Object | 0 | A simple container that is not a character, NPC or creature, such as a simple chest or a dead body, does not have a lock or trap, but is empty apart (perhaps) for some text-only treasure descriptions. |
Inanimate Container with stuff | 1 | A simple container that is not a character, NPC or creature, does not have a lock or trap, and may contain items and equipment and possibly some text-only treasure descriptions. |
Empty Sentient Creature | 2 | A character, NPC or creature that requires its "pocket to be picked" in order to loot stuff, but is empty (or their items are not pick-able) apart (perhaps) for some text-only treasure descriptions. |
Sentient Creature with Stuff | 3 | A character, NPC or creature that requires its "pocket to be picked" in order to loot stuff, and may possess items and equipment and possibly some text-only treasure descriptions. |
Trapped Container | 4 | A container of any type that may have a lock or trap that must be overcome before items it contains can be looted, and may or may not possess items and equipment and possibly some text-only treasure descriptions. |
The default type for a new character sheet just created is 0, an Empty Inanimate Container. However, if a Drag & Drop creature is created, the type is set to 2 (Empty Sentient Creature), or if a trapped or locked Drag & Drop container is created, the type is set to 4 (Trapped Container). If the Add Items dialog or any character action adds items to an empty container or empty sentient creature, the type is changed to 1 or 3 respectively. As with everything to do with containers, the container type can be changed in the GM\'s Add Items dialog.
' + +'When an attempt is made to search a container with type 2 or 3 (a sentient creature), the API will ask the searcher to make a "Pick Pockets" roll (whether the searcher is a Rogue or otherwise). The Pick Pockets percentage displayed is that shown on the Rogue tab of the character sheet (minimum 5%). If successful, the looting process is the same as any other container. If failed, the contents are not displayed and, as per the Dungeon Master\'s Guide, the chance of the victim noticing is a percentage equal to 3 x victim\'s experience level. The API notifies the GM with the result of this roll, i.e. whether the victim notices the Pick Pockets attempt or not. The GM can then decide what happens next.
' + +'Containers of type 4 are seen by the APIs as perhaps being trapped or locked, and instead of displaying their contents the API runs the Ability Macro on the container\'s character sheet with the name "Trap-@{container-name|trap-version}". Generally, when first called, the attribute "trap-version" on the container character sheet will either not exist (the API will default it to a value of 0) or be 0, so the Ability Macro "Trap-0" will be run. If no Ability Macro with the name "Trap-0" exists on the container\'s sheet, the API tries to run one called just "Trap". If neither of these exist, the API just displays the contents of the container as if no trap or lock existed. The value of the "trap-version" attribute can be used to change the behavior of the container as actions occur, with Ability Macros "Trap-1", "Trap-2", etc having different outcomes on future searches of the same container - e.g. a successful removal of a trap may change the trap-version to 1 and the "Trap-1" macro just display the contents without having to overcome the trap again.
' + +'The trap / lock Ability Macros are standard Roll20 macros and can do anything that a standard Roll20 macro can do. See the Roll20 Help Center for more information on macros. Typically, the "Trap-0" macro will ask the searcher to provide some key, password, combination number or perform some other activity in order to unlock its contents without negative consequences. The macro can call other macros, perhaps via API buttons or Roll Queries (see Roll20 help) which offer choices. For those Game Authors and GMs who are less comfortable coding Ability Macros, the RPGMaster suite offers configurable Drag & Drop containers which do all the hard work for you!
' + +'From v2.2 of RPGMaster suite, Drag & Drop containers are provided with the APIs, alongside a database of Lock & Trap Macros they use to provide their locks and traps. When Dragged & Dropped (in the same way as a Drag & Drop creature: create a new blank character sheet, give it a sensible name, and then just drag it onto the playing surface to drop a token) the Race, Class & Creature dialog pops up, but now also with a new button called [Containers]. Remember to ensure the newly dropped token is selected, then selecting the [Containers] button opens a drop-down list in the centre of the screen allowing you to select a basic container type. Each of these have default images associated with them that will display a token appropriate to the container selected. Select the one you want and a dialog appears supporting the selection of various pre-programmed locks & traps, the ability to change the images used for the container, and to change the effects of some locks and traps by setting different values. Once selected, the token is automatically given a representative icon and all necessary aspects of the character sheet are configured for the trapped / locked container. The GM just needs to (optionally) add items to it using their Add Items dialog.
' + +'The description of the container can be looked at (including instructions on configuring it) by opening the "Bio" tab of the container\'s character sheet. Typically, the Drag & Drop container will come with an action button displayed when the container\'s token is selected to close any lock and/or trap without changing its status, so once opened the player characters can close the container and searching it will open it again (often without setting off the trap again!). There is also a new GM Macro Bar macro button added which allows the GM to reset the locks and traps on the container (so you can test the trap/lock and then reset it for others to find) - go to the Collection tab of the Chat Window and enable Reset Container to be in your Macro Bar. The GM\'s [Token Setup] button (or the command !cmd --token-img) can be used to configure the container (give the token new images, choose new locks & traps, and set variables the macros can use to new values). Review the container\'s "Bio" to check what the configuration options are - e.g. how to set a new list of password choices, set a new combination, set the key number that must be possessed, etc.
' + +'The Drag & Drop Container system supports images to make tokens dynamic, appearing to change state such as opening, being destroyed and becoming a pile of ash, turning into a summoned creature, etc. To do this it stores image URLs, and the GM can change these images to apear as they desire.
' + +'Image URLs must comply with the Roll20 rules for any token image set by Roll20 Mods and APIs. That includes that they can\'t be Premium Asset images, such as those from the Market Place: they must be from your own image library. The images provided in the databases are from "Creative Commons" licenced sources. If you wish to replace the images provided, the best way to do so is to select the Container token, use the GM\'s [Token Setup] button to open the Container Configuration dialogue, then drag an image to the playing surface from your "My Library" image library, ensure that newly dropped image is selected and press the appropriate image button on the configuration dialogue. That will save the correctly formatted URL to the container.
' + +'You can add your own images to your "My Library" images using the [Upload] button on the "My Library" tab of the chat. Please source images responsibly.
' + +'If you read the Player\'s Handbook when it describes Thieving skills, the roll for Find Traps is normally made by the GM. Drag & Drop containers can work in two ways, depending on the configuration settings: the GM can be prompted to make the roll for the Player (the default setting); or the Player can make the roll. In either case, the result of the roll will be handled by the coding of the Drag & Drop container.
' + +'In order to change which of the two options is in operation, use the GM\'s [Config-RPGM] button or the !magic --config command and set the "Thievish Chance" configuration as desired.
' + +'There are other rolls that can be made by Players when encountering locked and/or trapped containers: only the Find Traps roll is set by default to be able to be rolled by either the player or the GM, based on the configuration. However, the Locks-Traps-DB includes example "GM-Roll-" versions of the macros involved in making Remove Traps, Pick Locks, and Pick Pockets rolls. The GM or game creator can use these in programming their own containers in their own extensions to the Locks-Traps-DB.
' + +'In order to save time, effort and data space, any Race definition can "inherrit" data values from a "parent" definition, or even a "parent tree". As containers are considered to be a form of "Race" by the APIs, they too can inherrit data values, meaning that "families" of containers can be created where common data settings can be set once in the root definition. Any inherrited data tags can be overwritten with different values, and additional ones specified just by adding them to a data specification of the same name in the inherriting (child) definition. In the case of container definitions, the data is held as RaceData specifications.
' + +'In order to inherrit data, the Specs specification field four (the Supertype) for the inherriting "child" container must name the "parent" to be inherrited from. If the "parent" also has a Specs Supertype field that refers to a "grand-parent" the tree will be followed on upward until a Supertype that does not specify a valid definition of the same database item Class (Specs field two) is encountered: typically in the case of containers the tree would terminate with a database item of Supertype "Container".
' + +'Note: this inheritance does not work for Lock and Trap definitions in the Locks-Traps-DB as in those definitions the Supertype specifically refers to the stored ability macro, and cannot refer to a parent database item.
' + +'Another way of reusing common aspects of database definitions is to use the standard Roll20 macro technique of merging one ability macro specification into another, using the syntax "%{character_name|ability_macro}". However, in the case of use with RPGMaster database entries, the "character_name" can be the name of any internal or external database, or even just a database root name ending in "-DB" (e.g. the root name of the Race-DB-Containers database is Race-DB). This is the case even if the referred to ability is in a database held in code: the merge will occur as if between externally held database macros. As with inheritance, chaining of specifications can occur, and "%{...|...}" entries will be resolved until the point that no unresolved entries remain.
' + +'The way the merge progresses means that uniquely named Roll Template fields will all appear in the final display, but those with the same names will only display the data in the last specified field of that name in the merged templates, but in the position in sequence of the first encountered field of that name. Also, Roll Template fields with no data after the "=" will not be included in the display: this is also the case for fields defined as "{{}}" (allowing for bracketing of Specs & Data fields that must be defined between template fields in very short templates).
' + +'Important Note: The APIs always find the first Specs= and ....Data= specifications in any merged database entries. It is best to ensure that the Specs and Data specifications are as early as possible in the specification, ahead of any "%{...|...}, so that it is those from the called database entry are used.
' + +'As with other RPGMaster database items, when updating the existing Lock & Trap items or creating new ones you should not add Roll20 "whispers", "emotes" etc. The APIs do some fairly special stuff with working out where posts should go, in terms of which players, characters, the GM, and also redirecting output when players are not logged on, and the APIs add their own Roll20 posting commands to ensure the right players see the right information at the right time, and don\'t see what they shouldn\'t.
' + +'That does not stop players from whispering, using emotes and other commands in chat to each other and the GM, or the GM in using "/desc" and other commands as normal.
' + +'The Drag & Drop container system draws on definitions for different types of basic containers that are contained in the Containers Database. As with all other RPGMaster suite databases, this Database is distributed with the APIs in code, but can be extracted to review and copy elements by using the !magic --extract-db Race-DB-Containers command, which will extract the database to a character sheet, where the entries can be seen as ability macros. (It is recommended that you do not keep complete extracted databases, but instead copy those elements you want to use to your own database and then delete the extracted database - this will optimise system performance).
' + +'The Containers databases have names that start with Race-DB-Containers (or, in fact, can just start with Race-DB) and can have anything put at the end, though those with version numbers of the form v#.# as part of the name will be ignored.
' + +'As previously stated, each database definition has 3 parts in the database (see Section 1): an Ability Macro with a name that is unique and describes the type of container, a custom Attribute with the name of the Ability Macro preceded by "ct-", and a listing in the database character sheet of the ability macro name separated by \'|\' along with other container macros. The quickest way to understand these entries is to examine existing entries. Do extract the root databases and take a look (but remember to delete them after exploring the items in them, so as not to slow the system down unnecessarily).
' + +'Note: The DM creating new containers does not need to worry about anything other than the Ability Macro in the database, as running the !magic -check-db < Database name > command will update all other aspects of the database appropriately, as long as the Specs fields are correctly defined.
' + +'Ability macros can be added to a database just by using the [+Add] button at the top of the Abilities column in the Attributes and Abilities tab of the Database Character Sheet, and then using the edit "pencil" icon on the new entry to open it for editing. Ability macros are standard Roll20 functionality and not dependent on the API. Refer to the Roll20 Help Center for more information.
' + +'The Containers database includes a number of example container definitions, but the GM or game creator can add to these with their own definitions by following the information in this handout (and their own imagination!). Here is an example of a container definition:
' + +'&{template:RPGMdefault}{{title=Large Chest}}{{subtitle=Container}}Specs=[Large Chest,Container,0H,Container]{{Size=3ft x 2ft x 2ft, capacity 100lbs}}{{Slots=This chest has stackable 18 slots}}RaceData=[w:Large Chest, slots:18, lock:No-Lock, trap:No-Trap, attr:ac=4|hp=20, cimg:https://s3.amazonaws.com/files.d20.io/images/163011054/ LD6xZDT2SlYSow0Q5QHb3g/thumb.png?1599509586|105|75, oimg:https://s3.amazonaws.com/files.d20.io/images/352175458/ BYTfuvbA_JvbL0IUjeM0Ug/thumb.png?1690472095|105|105]{{GM Info=Use the GM\'s [Token Setup] button, or the **!cmd --token-img** command to change the token\'s images, locks & traps, and set variables to vary behaviour. Items can be stored in it by using the GM\'s [Add Items] button, or the **!magic --gm-edit-mi** command}}{{desc=This is an ordinary chest, which can be configured to have locks and traps. When searched, after locks and traps have been overcome, it will open and list its contents. Resetting it closes it again and resets the locks and traps.}}
' + +'There are a number of important aspects to this definition:
' + +'Roll Template:The first element of note in this definition is that it is using a Roll Template for formatting the message to the player. Roll Templates are standard Roll20 functionality, generally provided by character sheet authors, but the RPGMaster suite has its own suite of Roll Template definitions, of which RPGMdefault is one. See the RPGMaster Library Help handout for details of RPGMaster Roll Templates
' + +'Specs: The next item to note is the Specs field, with a format that is standard across all RPGMaster database entries.
' + +'Specs=[Large Chest,Container,0H,Container]' + +'
Slotted between Roll Template fields, the Specs field will not appear to the players, but is only available to the APIs. The first field is always the database item type (or name), the second is the class of database item (in this case a "Container" database item), the third is the handedness of the item (not yet relevant to Container items), and the fourth the Super-type which in the case of Container items is usually "Container": however, if using "Race Definition Inheritance", the fourth element will be the name of the Container definition that this container inherits its RaceData from (see information on inheritance above).
' + +'RaceData: As any character or creature can be a container, chests are part of the Race family that use RaceData specifications. Indeed, the preloaded examples include a sleeping character (who will wake if they detect you picking their pockets...) and a "dead(?)" body that transforms into a Zombie (or any other undead you want). RaceData can be "inherited" from another container definition - set the fourth Specs field to be the name of the Container database entry to inherrit from. See information on "Inheritance" above.
' + +'The elements in the RaceData specification define data that the locks & traps use during game play, that may affect their behaviour. Some of the relevant data tags are:
' + +'Tag | Format | Default | Description |
---|---|---|---|
w: | Text | \' \' | The name of the container |
slots: | # | 18 | The initial maximum number of container slots |
lock: | Text | \' \' | The default Lock for this type of container |
trap: | Text | \' \' | The default Trap for this type of container |
attr: | Data Tags | \' \' | Attribute specifications for the container, typically AC & HP |
cimg: | < url > | undefined | The URL of the image to use for a closed container |
oimg: | < url > | undefined | The URL of the image to use for an open container |
limg#: | < url >[%%label] | undefined | The optional URL of a user-definable image related to locks stored as lock-img# with an optional variable label |
timg#: | < url >[%%label] | undefined | The optional URL of a user-definable image related to traps stored as trap-img# with an optional variable label |
lvar#: | < text or # >[%%label] | \' \' | An initial setting for lock-var# with an optional variable label |
tvar#: | < text or # >[%%label]> | \' \' | An initial setting for trap-var# with an optional variable label |
The Large Chest container is specified as having 18 slots, no lock (represented by the "No-Lock" lock type), no trap (represented by the "No-Trap" trap type), an Armour Class of 4, 20 hit points, an image URL for a closed container and an image URL for an open container. No lock or trap variables are preset by this container definition.
' + +'Any of the RaceData tags can be inherrited from another container definition as described above, under the description for the Specs. Also, any of these tags can be overwritten by the Locks and/or Traps set for the container (not just the default ones specified in the container definition, but any lock or trap subsequently set).
' + +'The "attr" tag allows certain attributes on the character sheet of the container to be set, in the case of the Large Chest setting an Armour Class (AC) of 4 and 20 Hit Points(HP). The data format is for a pipe-delimited string of "< tag >=< value >|< tag >=< value >| ... ". The full list that can be used can be found at the end of this document, and also in more detail in the Class & Race Database Help handout.
' + +'The Lock and Trap variables (lvar# and tvar#) can be preset with values and variable names. The names for the variables appear on the Container Configuration dialogue to prompt the user to enter appropriate data. Generally, lock and trap variables are set by the lock & trap definitions.
' + +'The Open & Closed Container image URLs must be of the correct format (see above), and are generally set by the container definition (though they can be overwritten by the Lock and Trap definitions if needed). The Open & Closed Container image tags do not accept a name specification, as they are always just those images. Alternate images can be specified for the container, for use by Lock and Trap macros to display when the container enters other states: however, generally speaking, these Lock & Trap images are set by the Lock & Trap definitions.
' + +'There are a number of character sheet attributes that are set by various conditions and by some macros in the Locks-Traps-DB that can be useful to GMs and game creators that are programming their own locks & traps. The table below gives you an idea of what they are and how they are used, but the best way to learn is to extract the database and view some examples. Note: many of these attributes are set by each Trap-# macro when it is run by using the ChatSetAttr !setAttr command (see database for example Trap-# macros). You can add and set others to your own Trap-# macros as you require. Remember, these attributes can be accessed in the lock & trap macros by using the form @{^^chest^^|attribute-name}.
' + +'Attribute | Values | Description |
---|---|---|
trap-version | # | The version of the "trap-" macros that will be called if a --search is conducted: initially 0 |
trap-status | Armed or Disarmed | Flag stating if the trap has already been disarmed |
trap-status|max | Locked or Unlocked | Flag stating if the container has already been unlocked |
gm-rolls | \'GM-Roll-\' or \' \' | Set to GM-Roll- if the configuration is for GMs to roll for Find Traps |
trap-name | < text > | The name of the trap on this container |
trap-name|max | < text > | The name of the lock on this container |
chest | token_id | The token ID of the container |
playerid | player_id | The player ID of the player that controls the character that is searching the container |
thief | token_id | The token ID of the character searching the container |
charName | < text > | The name of the character that is searching the container |
tstr | # | The strength score of the character searching the container |
tint | # | The intelligence score of the character searching the container |
tdex | # | The dexterity score of the character searching the container |
bruteStr | # | The "bend-bars" chance of the character searching the container |
openLock | # | The "open locks" score of the character searching the container |
remTrap | # | The "find/remove traps" score of the character searching the container |
The attributes "gm-rolls" & "trap-status" are used in the macros as qualifiers to onward macro calls. For example:
' + +'!magic --display-ability @{^^chest^^|thief}|^^chestid^^|@{^^chest^^|gm-rolls}Unlocked' + +'
will either run the macro Unlocked
or GM-Roll-Unlocked
depending on the current value of "gm-rolls" on that container, where the "Unlocked" macro allows the Player to roll the dice for finding traps, whereas "GM-Roll-Unlocked" has the GM make the roll. And as another example:
!magic --display-ability @{^^chest^^|thief}|^^chestid^^|Trap-@{^^chest^^|trap-status}' + +'
will run either the macro Trap-Armed
or Trap-Disarmed
depending on the value of "trap-status" with alternative consequences for the party!
Of course, the ChatSetAttr command !setAttr can be used to change the values of these attributes as the player progresses through the sequence of macros, successfully removing the trap and setting trap-status to "Disarmed", or getting the correct combination and setting the trap-status|max to "Unlocked". Use of these statuses is important when the !magic --find-traps command or the equivalent Items Menu / Find Traps dialog is used, which finds (and possibly removes) the trap before tackling any lock.
' + +'The Drag & Drop container draws upon ability macro definitions in the Locks & Traps Database to configure their traps and locks. As with all other RPGMaster suite databases, this Database is distributed with the APIs in code, but can be extracted to review and copy elements by using the !magic --extract-db Locks-Traps-DB command, which will extract the database to a character sheet, where the entries can be seen as ability macros. (It is recommended that you do not keep complete extracted databases, but instead copy those elements you want to use to your own database and then delete the extracted database - this will optimise system performance).
' + +'The Locks & Traps databases have names that start with Locks-Traps-DB and can have anything put at the end, though those with version numbers of the form v#.# as part of the name will be ignored.
' + +'As previously stated, each database definition has 3 parts in the database (see Section 1): an Ability Macro with a name that is unique and matches the trap/lock action, a custom Attribute with the name of the Ability Macro preceded by "ct-", and a listing in the database character sheet of the ability macro name separated by \'|\' along with other trap/lock macros. The quickest way to understand these entries is to examine existing entries. Do extract the root databases and take a look (but remember to delete them after exploring the items in them, so as not to slow the system down unnecessarily).
' + +'Note: The DM creating new trap/lock macros does not need to worry about anything other than the Ability Macro in the database, as running the !magic -check-db Locks-Traps-DB command will update all other aspects of the database appropriately, as long as the Specs fields are correctly defined.
' + +'Ability macros can be added to a database just by using the [+Add] button at the top of the Abilities column in the Attributes and Abilities tab of the Database Character Sheet, and then using the edit "pencil" icon on the new entry to open it for editing. Ability macros are standard Roll20 functionality and not dependent on the API. Refer to the Roll20 Help Center for more information.
' + +'Here is an example of a Lock & Trap Database entry for a "Trap-0" type macro. Note that Trap-# macros are somewhat different from other Ability Macros in the Locks-Traps-DB, as they set up the trap/lock on first selection of the locked / trapped container.
' + +'&{template:RPGMmessage}{{title=^^chest^^}}Specs=[Key-Lock,Lock|Ability,0H,Trap-0]{{desc=Do you have the key? [Key @{^^chest^^|lock-var1}](!magic --display-ability c|^^tid^^|^^chest^^|Unlocked). If not are you going to try and pick the lock? To do so [Roll 1d100](~^^chest^^|Open-Locks) and get less than your Open Locks percentage, which is [[{ {@{selected|olt} },{5} }kh1]]. If neither, then I suppose you can try [Brute Strength](!magic --display-ability c|^^tid^^|^^chest^^|Smash-the-Lock)?}}AbilityData=[w:Key Lock, lvar1:53%%Key Number, ns:11],[cl:AB,w:Open+List],[cl:AB,w:Opened-Lock],[cl:AB,w:Pick-a-Lock],[cl:AB,w:Return-Trap-2],[cl:AB,w:Find-Trap-Roll],[cl:AB,w:Return-Trap-3],[cl:AB,w:Find-Remove-Traps],[cl:AB,w:Smash-Lock],[cl:AB,w:Lock-Smash],[cl:AB,w:Not-Smashed],[cl:AB,w:Reset-Trap,action:1]{{GM desc=A key lock requires the searcher to have in their possession (though the system does not check) a particular key - or alternatively the searcher can try to pick the lock or smash it. If the lock pick fails or a critical fail is made on smashing the lock, any associated trap is triggered.
The GM can set the key name and the critical fail roll by selecting the container token and using the GM\'s [Token Setup] button, or the **!cmd --abilities** command.}}
!setattr --silent --charid ^^chestid^^ --dexterity|@{^^cname^^|dexterity} --thief|^^tid^^ --chest|^^targettid^^ --bruteStr|@{^^cname^^|bendbar} --strength|@{^^cname^^|strength} --openLock|@{^^cname^^|olt} --remTrap|@{^^cname^^|rtt} --intelligence|@{^^cname^^|intelligence} --charName|^^cname^^
There are a number of aspects in common with the Container database entry described in the previous section: it uses a Roll Template, in this case one provided by the RPGMaster suite, it has a Specs specification in common with all RPGMaster database items, and there is a Data specification. However, the Specs and Data specifications have some differences:
' + +'Specs: the format that is standard across all RPGMaster database entries:
' + +'Specs=[Key-Lock,Lock|Ability,0H,Trap-0]' + +'
Slotted between Roll Template fields, the Specs field will not appear to the players, but is only available to the APIs. The first field is always the database item type (or name), the second is the class of database item (in this case a "Lock|Ability" database item), the third is the handedness of the item (not currently relevant to Lock & Trap items), and the fourth the Super-type which in the case of Lock & Trap items is the name this macro will be stored as in the container character sheet that uses it.
' + +'Important Note: Lock & Trap database entry names (both the name of the Ability Macro and the first Specs data field) must be different from any Super-type Specs field (the fourth field). This ensures the database & character sheet tidying functions don\'t delete the lock & trap macros from trapped / locked container character sheets. Trapped / locked containers run from Roll20 macros stored on the character sheet - if a --tidy function is executed on a trapped / locked container, any ability macros on the character sheet that have the same name as a Locks-Traps-DB data entry will be deleted (the API assumes that they can be read from the database, but in the case of Locks & Traps they can\'t).
' + +'The database entry class (the second Specs field) can be one of Lock|Ability, Trap|Ability, or just Ability. The (somewhat obvious) meaning is that those with Lock specify types of lock, and those with Trap specify types of trap. Those with just Ability specify steps in the action of a lock or trap. Locks and traps can be mixed for any container, allowing lots of interesting combinations.
' + +'AbilityData: AbilityData specifications cannot "inherit" from parent ability definitions, as stated above. The elements in the AbilityData specification define data that the locks & traps use during game play, that may affect their behaviour. The data tags in the AbilityData are very similar to those in RaceData specifications for containers, and will overwrite those in the container definition when selected as a lock or trap for a container. Some of the relevant data tags are:
' + +'Tag | Format | Default | Description |
---|---|---|---|
w: | Text | \' \' | The name of the lock/trap |
magical: | [ 0 | 1 ] | 0 | Only for traps: trap is magical in nature (1), so "Remove Trap" rolls have half the chance they would otherwise |
cimg: | url | undefined | The URL of the image to use for a closed container |
oimg: | url | undefined | The URL of the image to use for an open container |
limg#: | < url >[%%label] | undefined | The optional URL of a user-definable image related to locks stored as lock-img# with an optional variable label |
timg#: | < url >[%%label] | undefined | The optional URL of a user-definable image related to traps stored as trap-img# with an optional variable label |
lvar#: | < text or # >[%%label] | \' \' | An initial setting for lock-var# with an optional variable label |
tvar#: | < text or # >[%%label]> | \' \' | An initial setting for trap-var# with an optional variable label |
ns: | # | 0 | The number of extra data specifications following the first dataset |
cl: | AB | AC | MI | WP | \' \' | The class of the extra dataset. AB = Abilities that form part of this lock/trap |
w: | Ability name | undefined | The name of the data item pointed to by the extra dataset, in this case a lock/trap ability |
action: | [ 0 / 1 ] | 0 | A flag indicating if the Ability should appear as a token Action Button |
The Key Lock is defined as: setting user variable 1 as 53 (in this case predefining an expectation that key 53 opens the chest) with a label of "Key Number"; states there are 11 additional extra datasets; all the extra datasets are of the Abilities class; and each of the extra datasets specifies a macro definition from the Locks-Traps-DB that will be processed and stored in the Dragged & Dropped container.
' + +'Dynamic fields: There are a number of Dynamic Fields used in this macro - the fields of the format ^^...^^. These are dynamically replaced at run time with the data they represent, which is dependent on the specific circumstances at that point in time. Most of these are only valid in "Trap-" macros (any macro with a Supertype (Specs field 4) starting "Trap-"):
' + +'Dynamic Field | Validity | Description |
---|---|---|
^^chest^^ | All macros | The character sheet name of this particular container |
^^chestid^^ | All macros | The character sheet ID of this particular container |
^^cname^^ | Trap- only | The character name of the character searching (or storing items in) the container |
^^tname^^ | Trap- only | The token name of the character searching (or storing items in) the container |
^^cid^^ | Trap- only | The character sheet ID of the character searching (or storing items in) the container |
^^tid^^ | Trap- only | The token ID of the character searching (or storing items in) the container |
^^targetchar^^ | Trap- only | Another way of specifying the character sheet name of the container (but only in Trap- macros) |
^^targettoken^^ | Trap- only | The token name of the container |
^^targetcid^^ | Trap- only | The character sheet ID of the container |
^^targettid^^ | Trap- only | The token ID of the container |
API buttons: The lock & trap macros run using standard Roll20 functionality, and can contain any legitimate functions that Roll20 supports, as well as API calls to loaded mods (such as the RPGMaster suite itself). To support the player (and GM) interacting with the locks and traps, API buttons are used. API buttons are text enclosed in brackets, followed by commands in parentheses e.g. [Button text](command). These are standard Roll20 macro functionality and help on them can be found in the Roll20 Help Center.
' + +'The most common use in locks & traps is to have the player run the next macro from a choice of possible actions: the next macro can be called using (~^^chest^^|macro-name) or (!magic --display-ability whisper-type|to-token-ID|^^chest^^|macro-name). The !magic --display-ability command has the advantage of being able to whisper to a character, player, the GM or publicly, whereas the tilde \'~\' macro call is less flexible, but \'~\' can allow 3D dice rolls, whereas !magic --display-ability will not. See MagicMaster Help handout for more information on the --display-ability command.
' + +'Use of !setattr: the call to the ChatSetAttr API (see separate documentation) is used to set several local custom attributes on the container\'s character sheet, so that the values can be accessed as the trap / lock sequence of macros progresses. Each of the values stored may not be otherwise available, as the creature doing the searching may lose the focus, and no longer be the "selected" token which otherwise would cause errors.
' + +'Ability Macros other than those that represent "Trap-" macros do not have a number of the same features. The Open-Locks macro from the Key Lock sequence is shown below: note that the Ability Macro name in the database is "Pick-a-Lock", following the rule that the database macro name and the Supertype must be different, with the Supertype being the name that will end up in the container:
' + +'&{template:RPGMdefault}{{title=^^chest^^}}Specs=[Pick-a-Lock,Ability,0H,Open-Locks]{{desc=You are trying to pick the lock of ^^chest^^. Your success is determined against your Open Locks percentage}}{{Target=[[{ {5},{@{^^chest^^|openLock} } }kh1]]%}}{{Roll=[[?{Roll to Pick the Lock|1d100}]]}}{{Result=Roll<=Target}}{{successcmd=!magic ~~display-ability c¦`{^^chest^^¦thief}¦^^chest^^¦Unlocked}}{{failcmd=!magic ~~display-ability public¦`{^^chest^^¦thief}¦^^chest^^¦Triggered}}
' + +'Here it can be seen that the Specs database specifier shows that the database name is "Pick-a-Lock" (the first field), but it is saved in the container character sheet as "Open-Locks" (the 4th field). The macro also uses the value of OpenLock, a custom attribute that was stored using the ChatSetAttr API call in the "Trap-" macro call, as it cannot be guaranteed at this point in the process that the searching character token is still selected. It is also the case that the only dynamic fields available in macros other than the "Trap-" macros are "^^chest^^" and "^^chestid^^", which will always be replaced with the character sheet name and ID (respectively) of the container when it is built by the Drag & Drop process and the ability macros are added to its sheet.
' + +'Note: This macro is using some specialised features of the RPGMaster Roll Templates: in this case the Result field tag will actually run a comparison of the numbers in the Roll and Target fields and display a green "Success" bar or a red "Failed" bar, depending on the result. The template will also run either the command string in the "successcmd" field, or the command string in the "failcmd" field, depending on the result, thus automatically affecting the macro sequence.
' + +'The other macros required to make the Key Locked Chest work are defined and used in a similar fashion. See an extracted database for more examples.
' + +'The author of the container ability macros can, of course, use the ChatSetAttr API command !setattr to store new custom attributes on any character sheet (but most typically the container\'s character sheet) to make changes in behavior. However, up to nine lock variables and nine trap variables exist that can be set using the RPGMaster container management dialog without going into the macro code. The AbilityData specification in any Lock|Ability or Trap|Ability macro can give them initial values and labels (using the syntax "lvar#:value%%label" or "tvar#:value%%label" with the %%label being optional), and those values altered for each individual dropped container to achieve changes in behavior as documented in each container specification. In the Ability Macros, these are then accessed either with @{^^chest^^|lock-var#} or @{^^chest^^|trap-var#}.
' + +'There are up to 20 image URL variables that can be set by the Drag & Drop container system, each of which are paired with values for the width and height to set the token to. Two of these are reserved for the URLs of the images of the closed and open containers, nine are available for use by locks, and another nine for use by traps.
' + +'Data Tag | Image variable | Width variable | Height variable | Used for |
---|---|---|---|---|
cimg | closed-img | closed-img-size | closed-img-size|max | The image URL for a closed container |
oimg | open-img | open-img-size | open-img-size|max | The image URL for an open container |
limg1 | lock-img1 | lock-img1-size | lock-img1-size|max | The image URL for the first user-defined lock image |
limg2 | lock-img2 | lock-img2-size | lock-img2-size|max | The image URL for the second user-defined lock image |
limg# | ... | ... | ... | The image URL for the #th user-defined lock image |
limg9 | lock-img9 | lock-img9-size | lock-img9-size|max | The image URL for the ninth user-defined lock image |
timg1 | trap-img1 | trap-img1-size | trap-img1-size|max | The image URL for the first user-defined trap image |
timg# | ... | ... | ... | The image URL for the #th user-defined trap image |
timg9 | trap-img9 | trap-img9-size | trap-img9-size|max | The image URL for the ninth user-defined trap image |
All the variables and the images can be set using RaceData tags in the container specification, can be added to or overwritten by the AbilityData tags in the lock and trap specifications, and altered individually for each dropped container using the GM\'s [Token Setup] button or the !cmd --token-img token_id command. An example of the use of the trap-img1 is below:
' + +'&{template:RPGMwarning}{{name=Poison Dart trap}}Specs=[Poison-dart-trap,Trap|Ability,0H,Triggered]{{desc=Oh no! You\'ve triggered a trap and four poison darts fly out, one from each side of the ^^chest^^, each doing [[@{^^chest^^|trap-var1}]]HP piercing damage and [[@{^^chest^^|trap-var2}]]HP poison damage (@{^^chest^^|trap-var3})}}AbilityData=[w:Poison Dart trap, magical:0, tvar1:1%%Dart Damage Roll, tvar2:2d4%%Poison Damage Roll, tvar3:Poison Type C%%Poison Type, timg1:https://s3.amazonaws.com/files.d20.io/images/352954823/u8yGNHOGKcTsoJQB_A3b6Q/thumb.png?1690920737|280%%Dart Ranges, ns:3],[cl:AB,w:Poison-dart-trap-noticed],[cl:AB,w:Remove-Trap-Roll],[cl:AB,w:Reset-trap,action:1]{{GM desc=The poison dart trap will shoot poison darts from the container if triggered. The GM can set the damage done by the darts, the damage done by the poison, the name of the poison type (DMG p73), and set trap image 1 displaying the ranges of the poison darts by selecting the container token and then using the GM\'s [Token Setup] button, or the **!cmd --abilities**.}}
!token-mod --ignore-selected --ids @{^^chest^^|chest} --set imgsrc|@{^^chest^^|trap-img1} width|@{^^chest^^|trap-img1-size} height|@{^^chest^^|trap-img1-size|max}
The key part of this macro is the !token-mod call to the TokenMod API that changes the imgsrc, width, and height values of the container token, to the image stored in trap-img1 which in this case shows the chest with the ranges of the four poisoned darts that are ejected by the trap on this chest.
' + +'The container database that comes with the APIs has examples of a number of different types of lock and trap:
' + +'There may also be more than these - this is the list at the time of writing. And each lock can be matched to any trap (or none) - so matching a combination with "Wake the Dead" and get the combination wrong a Lich is summoned! These can be extracted from the APIs using the command !magic --extract-db Locks-Traps-DB and used as examples.
' + +'As you will recognise if you have reviewed the earlier sections of this document, there are a number of different Roll20 ability macros that are inserted into a container\'s character sheet by the Drag & Drop Container system. The combination of macros inserted to the character sheet is important to make sure the container locks and traps work. It is key that there is a starting point for a character searching and looting the container, and that this starting point leads on to a sequence of macros that lead the character on a journey to either open the container and the ability to loot its items, or to suffer the consequences of a lack of caution.
' + +'The starting point is always a macro of database item Supertype (Specs field 4) Trap-# (generally Trap-0) (or just Trap). This macro will include text to describe to the player what they are faced with as a lock &/or trap, in the current state of the lock & trap. The state of the container will be determined by the Trap-version attribute of the container:
' + +'Trap-version | State represented |
---|---|
0 | Locks & Traps (if any) are in tact and in a locked & set state |
1 | Locks & Traps have all been overcome and the container is easily lootable |
2 | Lock partially overcome, or lock open and awaiting a hunt for traps |
3 | Found a trap and awaiting the removal of the trap |
# | Other numbers can represent additional states the container can be left in |
When a character conducts a search of the container, the RPGMaster APIs will start by calling a macro on the container called Trap-#, where "#" is the current value of the Trap-version. Thus, the character encounters the container in its current state.
' + +'The Trap-0 macro is a special case: it not only is an entry point lock macro, but it defines the whole structure of the lock, the other database items that make it up, and the initial state of variables and images. It is always of database item class Lock|Ability. Its name will be the name of the lock displayed when specifying the lock for the container with the !cmd --token-img command, or the [Token Setup] action. It is the only lock macro to have an AbilityData specification. The AbilityData includes a number of repeating data sets, one for each additional Locks-Traps-DB database item that makes up the lock. Each of these extra data sets is of cl: type "AB" (although they can also include type "AC", "MI" or "WP" to add items, equipment or weapons to the container), and a w: of the database item name (not the Supertype of the item).
' + +'Each of the database items named in the Trap-0 macro must have a Specs specification with:
' + +'The Supertypes of the lock must form links via API button calls (or other means of linking) in a meaningful sequence. The paths must lead to a hand-off to the macros that define any trap that is associated with the container. In general, the lock also accepts an interface from the trap macro path to a macro with a Supertype of "Opens-list" which is included in the Lock specification. The lock may also provide macros of Supertypes "Trap-1" and "Trap-2".
' + +'The trap macros will have entry points of "Triggered" and "Trap-Noticed". In this case, the special case is the macro of Supertype Triggered, with the database item class Trap|Ability. This macro is the one who\'s name will be listed when selecting a trap for the container with the !cmd --token-img command, or the [Token Setup] action. It is also the only trap macro to have an AbilityData specification specifying initial states for trap-var# attributes, any token images used by the trap which, like the lock "Trap-0" macro, includes a number of repeating data sets, one for each additional Locks-Traps-DB database item that makes up the trap. Each of these extra data sets is of cl: type "AB", and a w: of the database item name (not the Supertype of the item).
' + +'Each of the database items named in the Triggered macro must have a Specs specification of the same structure as the lock macros. As with locks, the Supertypes of the trap macros must form links via API button calls (or other means of linking) in a meaningful sequence. The paths typically lead to a conclusion that either results in inflicting some sort of penalty on the character trying to loot the container, or the container opening and revealing its contents. The opening is generally by handing off to a macro provided by the lock of Supertype "Opens-list".
' + +'A macro of Supertype "Trap-Noticed" will be called by the lock macros if a trap has been found by the character, and they want to attempt to remove it. The Trap-Noticed" macro will do what is necessary to determine if the particular trap selected can be removed successfully, or if the trap is actually triggered, calling other macros in sequence as necessary, often including calling the Triggered macro if the trap is not removed successfully.
' + +'Here is an example of how a lock and a trap, taken from the Locks-Traps-DB, create a sequence of ability macros in the chosen container character sheet. It is based on the Key Lock and Poison Dart Trap already introduced earlier in this help handout.
' + +'Yellow = A part of the Lock | Green = A part of the Trap | |||||||||||||||||
Trap-1 | => | Opens-List | ||||||||||||||||
Trap-3 | => | Trap-Noticed | => | Unlocked-Remove-Trap | [Success] | => | ||||||||||||
Trap-2 | => | Unlocked | => | Unlocked-Find-Trap | [Success] | => | [Fail] | => | Triggered | |||||||||
Trap-0 | [Have Key] | => | [Fail] | => | ||||||||||||||
[Pick the Lock] | => | Open-Locks | [Success] | => | ||||||||||||||
[Fail] | => | |||||||||||||||||
[Brute Strangth] | => | Smash-The-Lock | => | Smashed-Lock-Check | [Success] | => | Unlocked | ^^^ | ||||||||||
[Critical Fail] | => | |||||||||||||||||
[Fail] | => | Still-Locked |
Below are lists of the current possible values for the Lock and Trap database Ability macro sections.
' + +'Specs=[Lock Type, Lock|Ability, Handedness, Trap-0 (or Trap)]' + +'
Specs=[Lock Type, Ability, Handedness, Supertype]' + +'
Specs=[Trap Type, Trap|Ability, Handedness, Triggered]' + +'
Specs=[Trap Type, Ability, Handedness, Supertype]' + +'
If the item class (field 2) is "Lock|Ability", the Supertype must be "Trap-0" or "Trap", and visa-versa.
' + +'If the item class (field 2) is "Trap|Ability", the Supertype must be "Triggered", and visa-versa.
' + +'All fields must be explicitly specified.
' + +'There is an infinite list of lock types: the type of the "Lock|Ability" macro is the Lock name.
' + +'There is an infinite list of trap types: the type of the "Trap|Ability" macro is the Trap name.
' + +'Classes: One of "Lock|Ability", "Trap|Ability", or just "Ability". This field is used to add the Lock or Trap name to the right base list for selection by the GM to configure the container.
' + +'Handedness for Locks & Traps are not currently restricted or used by the system. In future, the number of hands specified for a lock or trap might indicate how many hands need to be contributed to enact the opening of a lock or the removal of a trap.
' + +'The following Supertypes must exist for each defined lock:
' + +'Trap-0 (or Trap), Opens-list' + +'
The following Supertypes must be called by each defined lock:
' + +'Triggered' + +'
The following Supertypes must exist for each defined trap:
' + +'Triggered, Trap-Noticed' + +'
Below is a table of the Supertypes currently provided and used in the Locks-Traps-DB as distributed with the APIs. Each Supertype may be used by several lock or trap sequence macros, each having a slightly different effect for that stage in the sequence. Some Supertypes require different processing for certain Locks/Traps if the Player is rolling for skills as opposed to the GM (set by the !magic --config command) - the appropriate macro will be used depending on the configuration for who rolls for skills at the point the Drag & Drop container is built. Note: changing the configuration after the container is built will not alter the behaviour of the container. The container must be rebuilt if you wish the behaviour to change.
' + +'Supertype | Macro Versions | |
---|---|---|
Player Rolls | GM Rolls | |
Trap-0 | Combination-Lock Key-Lock Password-Lock No-Lock Sleeping-Creature Undead-Body | |
Triggered | Four-Dart-Trap Destroying-Spell-Trap Single-Poison-Dart-Trap Summon-Undead Summon-Creature Wizard-Spell-Trap Combination-Wrong-No-Trap Explosive-Runes-Trap No-Trap | |
First-Digit-0 | First-Digit-Right | |
First-Digit-1 | First-Digit-Wrong | |
Second-Digit-0 | Second-Digit-Right | |
Second-Digit-1 | Second-Digit-Wrong | |
Third-Digit-0 | Third-Digit-Right | |
Third-Digit-1 | Third-Digit-Wrong | |
Attempt-Right | Password-Right | |
Attempt-Wrong | Password-Wrong | |
Open-Locks | Pick-a-Lock Pick-a-Pocket | GM-Roll-Pick-a-Lock GM-Roll-Pick-a-Pocket |
Lock-Unlocked | Unlocked-Lock | |
Unlocked | Find-Traps | GM-Roll-Find-Traps |
Successful-PP | ||
Unlocked-Find-Trap | Find-Trap-Roll | GM-Roll-Find-Trap-Roll |
Open-Or-Find-Traps | No-Traps-Found | |
Unlocked-Remove-Trap | Remove-Trap-Roll | GM-Roll-Remove-Trap |
Trap-Not-Removed | Trap-Remains | |
Detected-Runes | Detect-Runes | |
Smash-the-Lock | Smash-Lock | GM-Roll-Smash-Lock |
Smashed-Lock-Check | Lock-Smash | GM-Roll-Lock-Smash |
Still-Locked | Not-Smashed | |
Trap-Noticed | Poison-dart-trap-noticed Found-Trap | GM-Roll-Dart-Trap-Noticed GM-Roll-Found-Trap |
No-Trap-Noticed Runes-Trap-Noticed | ||
No-Lock-Trap | No-Lock-No-Trap No-Lock-Is-Trapped | |
Trake-Damage | Taken-Damage | |
Opens-List | Open+List Open-the-Spellbook | |
Trap-1 | Opened-Lock Opened-the-Spellbook | |
Trap-2 | Return-Trap-2 | |
Trap-3 | Return-Trap-3 | |
Trap-4 | Destroyed-Container Destroyed-the-Spellbook | |
Close | Close-Container | |
Reset | Reset-Trap |
Trap-version | Mandatory | State represented |
---|---|---|
0 (or none) | Yes | Locks & Traps (if any) are in tact and in a locked & set state |
1 | No | Locks & Traps have all been overcome and remain so and the container is easily lootable |
2 | No | Lock partially overcome, or lock open and awaiting a hunt for traps |
3 | No | Found a trap and awaiting the removal of the trap |
# | No | Other numbers can represent additional states the container can be left in |
Below are the definitions for each of the possible RaceData fields.
' + +'Note: Always refer to the database specification definitions in other sections above for detailed information on the use of these Field specifiers. Not all specifiers have an obvious use. Square brackets \'[...]\' indicate optional data - don\'t include the brackets when specifying the optional data.
' + +'Field | ' + +'Format | ' + +'Default Value | ' + +'Attribute | ' + +'Description | ' + +'
---|---|---|---|---|
w: | < text > | \' \' | Name of the database item | |
magical: | [ 0 | 1 ] | 0 | Only for traps: trap is magical in nature (1), so "Remove Trap" rolls have half the chance they would otherwise | |
slots: | # | 18 | container-size | Number of slots available in the container |
lvar#: | < text > | undefined | lock-var# | Variable used for lock processing. # can be 1 to 9 |
tvar#: | < text > | undefined | trap-var# | Variable used for trap processing. # can be 1 to 9 |
cimg: | URL [| width [| height ]] | Image of a closed chest | closed-img | URL of image of closed container, in a valid Roll20 format. Can be followed by width and/or height in pixels separated by pipes \'|\' |
oimg: | URL [| width [| height ]] | Image of an open chest | open-img | URL of image of open container, in a valid Roll20 format. Can be followed by width and/or height in pixels separated by pipes \'|\' |
limg#: | URL [| width [| height ]] [%% name] | undefined | lock-img# lock-img#-size lock-img#-size|max | URL of alternate lock image of container, in a valid Roll20 format. Can be followed by width and/or height in pixels separated by pipes \'|\', and a label for the image preceded by %% |
timg#: | URL [| width [| height ]] [%% name] | undefined | trap-img# trap-img#-size trap-img#-size|max | URL of alternate trap image of container, in a valid Roll20 format. Can be followed by width and/or height in pixels separated by pipes \'|\', and a label for the image preceded by %% |
ns: | < # > | 0 | Number of repeating data sets | |
cl: | < AB | MI | AC | WP > | \' \' | Type of the repeating data set, AB = Lock / Trap ability | |
w: | < text > | \' \' | In repeating data set, database item name of macro to include in a lock or trap flow |
The Character Sheet field mapping to the API script can be altered using the definition of the fields object, the definition for which can be found at the top of the relevant RPGMaster Library API. You can find the complete mapping for all APIs in the RPGMaster series, with an explanation of each, in a separate document - ask the API Author for a copy.
' + +'' + + '<%= confirm_button %>' + + ' | ' + + '' + + '<%= reject_button %>' + + ' | ' + + '
'+descObj.desc+'
'; + for (let i=1; i<=9; ++i) { + if (!_.isUndefined(descObj['desc'+i])) content += ''+(descObj['desc'+i].replace(/\*\*\*(.*?)\*\*\*/img,'$1') + .replace(/\*\*(.*?)\*\*/img,'$1') + .replace(/\*(.*?)\*/img,'$1'))+'
'; + }; + }; + _.each(descObj,(t,k) => { + t = t.replace(/\*\*\*(.*?)\*\*\*/img,'$1'); + t = t.replace(/\*\*(.*?)\*\*/img,'$1'); + t = t.replace(/\*(.*?)\*/img,'$1'); + descObj[k] = t; + if (!t || !t.length) return; + if (!onlyGM && k.toLowerCase().startsWith('section')) { + if (!t.toLowerCase().includes('description')) { + if (t.endsWith('')) { + content += '' + t + '
'; + } + } + } else if (['gminfo','gmdesc'].includes(k.toLowerCase().replace(/\s/g,''))) { + GMcontent += ''+t+'
' + k + ': ' + t + '
'; + } + }); + }; + return [content,GMcontent]; + }; + + charCS.get('bio', bio => { + bio = (bio.match(/[^]*~~~ Place your own text above this line ~~~/im) || ['~~~ Place your own text above this line ~~~'])[0]; + + [bioPart,GMpart] = createBio( charCS, raceObj, "Race" ); + bio += bioPart; + GMbio += GMpart; + + _.each( classObjs, cObj => { + if (cObj.name === 'creature' || cObj.base === 'creature') return; + [bioPart,GMpart] = createBio( charCS, cObj, "Class" ); + bio += bioPart; + GMbio += GMpart; + }); + + if (lock && lock.length) { + [bioPart,GMpart] = createBio( charCS, lockObj, "Lock", true ); + GMbio += bioPart + GMpart; + }; + + if (trap && trap.length) { + [bioPart,GMpart] = createBio( charCS, trapObj, "Trap", true ); + GMbio += bioPart + GMpart; + }; + + charCS.set( "bio", bio ); + }); + + charCS.get('gmnotes', bio => { + bio = (bio.match(/[^]*~~~ Place your own text above this line ~~~/im) || ['~~~ Place your own text above this line ~~~'])[0]; + charCS.set( "gmnotes", bio+GMbio ); + }) + } + + /* + * Set NPC thieving skill levels using relative percentages, + * either specified or randomly. + */ + + var handleNPCthiefSkillPoints = function( charCS, senderId ) { + + var raceData = resolveData( attrLookup( charCS, fields.Race ), fields.RaceDB, reRaceData, charCS).attrs, + classes = classObjects( charCS, senderId ), + rogue = _.find( classes, c => c.base === 'rogue' ), + keys = [], + capacity = 100; + +// log('handleNPCthiefSkillPoints: returned from resolveData() '+attrLookup( charCS, fields.Race )+' raceData = '+_.pairs(raceData)); + + _.each(rogueSkills,skill => keys.push(skill.factors[6])); + + raceData = _.pick(raceData, (val,key) => keys.includes(key)); + +// log('handleNPCthiefSkillPoints: then after pick, '+attrLookup( charCS, fields.Race )+' raceData = '+_.pairs(raceData)); + + if (rogue) { + let totalPoints = rogueLevelPoints( charCS, classes ); + raceData = _.mapObject( raceData, (val,key) => evalAttr(val) ); + let divider = _.reduce(raceData, (tot,val) => parseInt(tot) + parseInt(val)); +// log('handleNPCthiefSkillPoints: totalPoints = '+totalPoints+', divider = '+divider); + keys = []; + let maxPoints = totalPoints; + _.each( rogueSkills, skill => { + if (totalPoints <= 0 || keys.includes(skill.factors[6])) return; + let value = divider > 0 ? Math.floor(maxPoints*(raceData[skill.factors[6]] || 0)/divider) : 0; + setAttr( charCS, [skill.factors[6],'current'], value ); + totalPoints -= value; + keys.push(skill.factors[6]); +// log('handleNPCthiefSkillPoints: initial allocation for '+skill.factors[6]+' is '+value+', leaving a remaining '+totalPoints); + }); +// log('handleNPCthiefSkillPoints: completed initial alloation leaving a remaining '+totalPoints); + while (totalPoints > 0 && capacity > 0) { + keys = []; + maxPoints = Math.ceil(totalPoints/4); + capacity = 0; + _.each( rogueSkills, skill => { + if (totalPoints <= 0 || keys.includes(skill.factors[6])) return; + let curVal = parseInt(attrLookup(charCS,[skill.factors[6],'current']) || 0); + let value = Math.min(totalPoints,(randomInteger(Math.ceil(maxPoints))),(100-curVal)); + setAttr( charCS, [skill.factors[6],'current'], parseInt(value)+curVal ); + totalPoints -= value; + capacity += 100-curVal; + keys.push(skill.factors[6]); +// log('handleNPCthiefSkillPoints: remaining allocation for '+skill.factors[6]+' is '+value+', leaving a remaining '+totalPoints); + }); + }; + }; + }; + + /** + * Set creature/monster attributes if specified in the + * race definition + **/ + + var setCreatureAttrs = function( cmd, charCS, senderId, creature, token, qualifier=[] ) { + + var raceData, attrData, + raceDesc, i, + tokenObj = getObj('graphic',token._id), + isReset = cmd.toUpperCase() === BT.RESET_CONTAINER, + isCreature = !isReset && cmd.toUpperCase() !== BT.CONTAINER; + + async function addPowersAndItems( charCS, token, isCreature, senderId, qualifier ) { + if (await handleAddAllPowers( [BT.RACE], 'PW', [token], senderId )) { + handleSetAbility( ['',BT.AB_SILENT,'Use Power',std.use_power.api,std.use_power.action,'2.Use Power','replace'], [token] ); + handleSetAbility( ['',BT.AB_SILENT,'Powers menu',std.powers_menu.api,std.powers_menu.action,'3.Powers Menu','replace'], [token] ); + } + sendAPI('!magic --mem-all-powers '+token._id); + if (!isCreature) { + await handleAddAllPowers( [BT.RACE], 'AB', [token], senderId ); + } else { + await handleAddAllPowers( [BT.RACE], 'MU', [token], senderId ); + await handleAddAllPowers( [BT.RACE], 'PR', [token], senderId ); + sendAPI('!magic --mem-all-spells ALL_MUSPELLS|'+token._id+' --mem-all-spells ALL_PRSPELLS|'+token._id); + } + let content = ((await handleAddAllItems( token, charCS, senderId, 'wp', qualifier ) + + await handleAddAllItems( token, charCS, senderId, 'ac', qualifier ) + + await handleAddAllItems( token, charCS, senderId, 'mi', qualifier )) || '').trim(); + if (content && content.length) sendFeedback( '&{template:'+fields.menuTemplate+'}{{title=Items added to '+charCS.get('name')+'}}' + content ); + } + + if (creature && creature.trim().length) { + + let dataObj = resolveData( creature, fields.RaceDB, /}}\s*?racedata\s*?=\s*\[(.*?)\],?{{/im, null, null, null, qualifier ); + raceData = dataObj.parsed; attrData = dataObj.attrs; // rawData = dataObj.raw; + if (!raceData || !attrData) return; + setAttr( charCS, fields.Race, creature ); + } + + if (isCreature) { + + if (!creature || !creature.trim().length) return; + + classLevels.forEach( c => { + setAttr( charCS, c[0], '' ); + setAttr( charCS, c[1], '' ); + }); + + let hd = attrData.hd.match(/(\(.+?\)|\d+)(?:d\d+)?([-+]\d+(?:d\d+)?(?:[-+]\d+)?)?(?:r(\d+))?/i) || ['','1','0','']; + let hpExtra = (hd[2] || '0').match(/([-+]\d+)(?:d(\d+))?([-+]\d+)?/); + let age = (attrData.age.split(':') || ['','']); + let str = evalAttr(attrData.str); + let monInt = evalAttr(attrData.intel); + let attrRoll = state.attackMaster.attrRoll; + let attrRestrict = state.attackMaster.attrRestrict; + if (str == 18 && attrData.exstr) str = String(str) + '(' + evalAttr(attrData.exstr) + ')'; + setAttr( charCS, fields.Monster_int, monInt ); + setAttr( charCS, fields.Age, age[0] ); + setAttr( charCS, fields.AgeVal, (_.isUndefined(age[1]) ? age[0] : evalAttr(age[1]) )); + setAttr( charCS, fields.MonsterAC, evalAttr(parseStr(attrData.cac || '10')) ); + setAttr( charCS, fields.Monster_mov, attrData.mov+(attrData.fly ? ', FL'+attrData.fly : '')+(attrData.swim ? ', SW'+attrData.swim : '') ); + setAttr( charCS, fields.MonsterThac0, evalAttr(attrData.thac0) ); + setAttr( charCS, fields.Thac0_base, evalAttr(attrData.thac0) ); + setAttr( charCS, fields.Monster_size, attrData.size ); + setAttr( charCS, fields.Strength_hit, evalAttr(attrData.tohit) ); + setAttr( charCS, fields.Strength_dmg, evalAttr(attrData.dmg) ); + setAttr( charCS, fields.Dex_acBonus, evalAttr(attrData.dexdef) ); + setAttr( charCS, fields.MonsterCritHit, evalAttr(attrData.crith) ); + setAttr( charCS, fields.MonsterCritMiss, evalAttr(attrData.critm) ); + setAttr( charCS, fields.Monster_dmg1, parseStr(attrData.attk1.replace(/:/g,',')) ); + setAttr( charCS, fields.Monster_dmg2, parseStr(attrData.attk2.replace(/:/g,',')) ); + setAttr( charCS, fields.Monster_dmg3, parseStr(attrData.attk3.replace(/:/g,',')) ); + setAttr( charCS, fields.Monster_attks, (((attrData.attk1 && attrData.attk1.length) ? 1 : 0) + ((attrData.attk2 && attrData.attk2.length) ? 1 : 0) + ((attrData.attk3 && attrData.attk3.length) ? 1 : 0)) ); + setAttr( charCS, fields.Monster_mr, evalAttr(attrData.mr) ); + setAttr( charCS, fields.Attk_specials, parseStr(attrData.attkmsg) ); + setAttr( charCS, fields.Dmg_specials, parseStr(attrData.dmgmsg) ); + setAttr( charCS, fields.Monster_speed, evalAttr(attrData.speed) ); + setAttr( charCS, fields.SpellSpeedOR, evalAttr(attrData.spellspeed) ); + setAttr( charCS, fields.Regenerate, evalAttr(attrData.regen) ); + setAttr( charCS, fields.Monster_spAttk, parseStr(raceData.spattk) || 'Nil' ); + setAttr( charCS, fields.Monster_spDef, parseStr(raceData.spdef) || 'Nil' ); + setAttr( charCS, fields.Strength, str || (!attrRoll ? '' : evalAttr(attrRestrict ? '8:15' : '3d6'))); + setAttr( charCS, fields.Constitution, evalAttr(attrData.con || (!attrRoll ? '' : (attrRestrict ? '7:14' : '3d6')))); + setAttr( charCS, fields.Dexterity, evalAttr(attrData.dex || (!attrRoll ? '' : (attrRestrict ? '7:14' : '3d6')))); + setAttr( charCS, fields.Intelligence, (attrData.int ? evalAttr(attrData.int) : (monInt || evalAttr(!attrRoll ? '' : (attrRestrict ? '7:15' : '3d6'))))); + setAttr( charCS, fields.Wisdom, evalAttr(attrData.wis || (!attrRoll ? '' : (attrRestrict ? '7:15' : '3d6')))); + setAttr( charCS, fields.Charisma, evalAttr(attrData.chr || (!attrRoll ? '' : (attrRestrict ? '7:15' : '3d6')))); + setAttr( charCS, fields.ItemContainerHide, 1 ); + + if (!attrData.hp && hd && hd.length) { + hd[2] = ((hpExtra && hpExtra.length >= 2 && parseInt(hpExtra[2])) ? (rollDice( hpExtra[1], hpExtra[2], 0 ) + parseInt(hpExtra[3] || 0)) : parseInt(hd[2] || 0)); + let res = evalAttr(hd[1]); + attrData.hp = rollDice( res, 8, hd[3] ) + hd[2]; + } + setAttr( charCS, fields.Monster_hitDice, (evalAttr(hd[1]||'1')) ); + setAttr( charCS, fields.Monster_hpExtra, (hd[2]||'0') ); + setAttr( charCS, fields.Monster_hdReroll, (hd[3]||'') ); + if (attrData.hp) { + attrData.hp = evalAttr(attrData.hp); + setAttr( charCS, fields.HP, attrData.hp ); + setAttr( charCS, fields.MaxHP, attrData.hp ); + } + + setAttr( charCS, fields.Race, raceData.name ); + setAttr( charCS, fields.Gender, 'Creature' ); + if (attrData.lv) { + let classData = (attrData.cl || 'F:Warrior').split('/').map( c => c.split(':') ); + let levels = attrData.lv.split('/'); + let isCaster = false; + let classField, levelField; + _.each( classData, (c,k) => { + switch (c[0].toUpperCase()) { + case 'MU': + classField = fields.Wizard_class; + levelField = fields.Wizard_level; + break; + case 'PR': + classField = fields.Priest_class; + levelField = fields.Priest_level; + break; + case 'RO': + classField = fields.Rogue_class; + levelField = fields.Rogue_level; + break; + case 'PS': + classField = fields.Psion_class; + levelField = fields.Psion_level; + break; + default: + classField = fields.Fighter_class; + levelField = fields.Fighter_level; + break; + } + setAttr( charCS, classField, c[1] || ''); + setAttr( charCS, levelField, evalAttr(levels[k])); + isCaster = isCaster || (caster( charCS, 'MU' ).lv > 0) || (caster( charCS, 'PR' ).lv > 0); + }); + handleAddAllPRspells( ['',BT.ALL_PRSPELLS,0], [token], senderId ); + handleNPCthiefSkillPoints( charCS, senderId ); + if (isCaster) { + handleSetAbility( ['',BT.AB_SILENT,'Cast Spell',std.cast_spell.api,std.cast_spell.action,'2.Cast Spell','replace'], [token] ); + handleSetAbility( ['',BT.AB_SILENT,'Spells menu',std.spells_menu.api,std.spells_menu.action,'3.Spells Menu','replace'], [token] ); + }; + }; + if (!attrData.thac0) { + let thac0 = handleGetBaseThac0( charCS ); + setAttr( charCS, fields.Thac0_base, thac0 ); + setAttr( charCS, fields.MonsterThac0, thac0 ); + }; + handleSetNPCAttributes( charCS ); + tokenObj.set('isdrawing',false); + handleSetAbility( ['',BT.AB_SILENT,'Init menu',std.init_menu.api,std.init_menu.action,'1.Initiative','replace'], [token] ); + handleSetAbility( ['',BT.AB_SILENT,'Attack',std.attk_hit.api,std.attk_hit.action,'2.Attack','replace'], [token] ); + handleSetAbility( ['',BT.AB_SILENT,'Attk menu',std.attk_menu.api,std.attk_menu.action,'3.Attk Menu','replace'], [token] ); + handleSetAbility( ['',BT.AB_SILENT,'Items menu',std.mi_menu.api,std.mi_menu.action,'3.Items Menu','replace'], [token] ); + handleSetAbility( ['',BT.AB_SILENT,'Other Actions',std.other_actions.api,std.other_actions.action,'4.Other actions','replace'], [token] ); + handleSetAbility( ['',BT.AB_SILENT,'Specials',std.specials.api,std.specials.action,'5.Specials','replace'], [token] ); + creatureWeapDefs( charCS ); + charCS.set('controlledby',''); + + } else { + _.each( findObjs({_characterid:charCS.id, _type:'ability'}), ab => { + ab.set('istokenaction',((ab.get('action').match(reAction) || [0,0])[1] == 1)); + }); + if (creature && creature.trim().length) { + setAttr( charCS, fields.Container, creature); + setAttr( charCS, fields.ItemContainerSize, raceData.slots ); + setAttr( charCS, fields.ItemContainerType, (raceData.trap ? 4 : 1)); + if (!isReset) { + setAttr( charCS, fields.Old_trap, attrLookup( charCS, fields.Container_trap )); + setAttr( charCS, fields.Old_lock, attrLookup( charCS, fields.Container_lock )); + setAttr( charCS, fields.Container_lock, raceData.lock ); + setAttr( charCS, fields.Container_trap, raceData.trap ); + } + setAttr( charCS, fields.Lock_imgs, 0 ); + setAttr( charCS, fields.Trap_imgs, 0 ); + setAttr( charCS, fields.MonsterAC, calcAttr(parseStr(attrData.cac || '10')) ); + if (attrData.hp) { + setAttr( charCS, fields.HP, attrData.hp ); + setAttr( charCS, fields.MaxHP, attrData.hp ); + } + setImgs(tokenObj,charCS,raceData,!isReset); + } + setAttr( charCS, fields.Gender, 'Container' ); + setAttr( charCS, fields.Trap_tokenID, token._id ); + setAttr( charCS, fields.Trap_version, 0 ); + setAttr( charCS, fields.Lock_status, 'Locked' ); + setAttr( charCS, fields.Trap_status, 'Armed' ); + + charCS.set('controlledby','all'); + + tokenObj.set('isdrawing',true); + } + if (creature && creature.trim().length || isReset) { + addPowersAndItems( charCS, token, isCreature, senderId, qualifier ); + handleCheckThiefMods( [tokenObj.id], senderId, true ); + if (!isCreature) makeChangeImagesMenu( [tokenObj.id], senderId ); + } + return; + } + +// ---------------------------------------------------- Make Menus --------------------------------------------------------- + + /* + * Display a menu to add spells and powers to the spellbooks of a character + */ + + async function makeSpellsMenu( args, selected, senderId, msg ) { + + try { + const reActionButton = /((? attrLookup( charCS, [field,param] ); + + if (isPower) { + desc = 'Powers'; + word = 'power'; + cmd = 'POWERS'; + rootDB = fields.PowersDB; + listAttr = fields.PowersSpellbook; + listType = 'power'; + } else if (isMU) { + desc = 'Level '+level+' MU spell book'; + cmd = 'MUSPELLS'; + rootDB = fields.MU_SpellsDB; + listAttr = [fields.MUSpellbook[0]+spellLevels.mu[level].book,fields.MUSpellbook[1]]; + listType = 'muspelll'+level; + if (spell) { + spellObj = abilityLookup( fields.PR_SpellsDB, spell, null, true ); + pwrPrefix = !spellObj.obj ? '' : 'MU-'; + } + } else { + desc = 'Level '+level+' PR spell book'; + cmd = 'PRSPELLS'; + rootDB = fields.PR_SpellsDB; + listAttr = [fields.PRSpellbook[0]+spellLevels.pr[level].book,fields.PRSpellbook[1]]; + listType = 'prspelll'+level; + if (spell) { + spellObj = abilityLookup( fields.MU_SpellsDB, spell, null, true ); + pwrPrefix = !spellObj.obj ? '' : 'PR-'; + } + } + args[0] = cmd; + cmdStr = args.join('|'); + + if (charCS) { + setAttr( charCS, fields.Casting_name, charCS.get('name')); + setAttr( charCS, fields.CastingLevel, characterLevel( charCS )); + curSpells = (attrLookup( charCS, listAttr ) || '').replace(/\|/g,', '); + } + + let spellList = getMagicList( rootDB, spTypeLists, listType, senderId ); + + content = '&{template:'+fields.menuTemplate+'}{{name=Grant Spells}}{{ ='+(msg||'')+'}}{{'+desc+'='+curSpells+'}}' + + '{{desc=1. [Choose](!cmd --button CHOOSE_'+cmd+'|'+level+'|?{Choose which spell|'+spellList+'}) a '+word+'\n'; + + if (spell) { + spellObj = getAbility( rootDB, spell, charCS ); + if (!state.MagicMaster.viewActions && spellObj.obj) { + spellObj.obj[0].set('action',(spellObj.obj[0].get('action') || '').replace(/@\{selected\|token_id\}/img,'') + .replace(/@\{selected\|(.+?)(?:\|(current|max))?\}/img,setVal) + .replace(reActionButton,grey_action) + .replace(/^!.+$/mg,'')); + } + content += '...Optionally [Review '+spellName+'](!cmd --button REV_'+cmdStr + + ' /w gm %{' + spellObj.dB + '|'+spell.hyphened()+'})'; + } else { + content += '...Optionally Review the chosen '+word+''; + } + + if (isPR && (apiCommands.attk || apiCommands.magic)) { + content += ' or [Add all valid Priest spells](!cmd --button '+BT.ALL_PRSPELLS+'|'+level+')'; + } + + if (isPower && (apiCommands.attk || apiCommands.magic)) { + content += ' or [Add all Class/Race powers](!cmd --button '+BT.ALL_POWERS+')'; + } + + content += '}}{{desc1=2. '+(spell ? '[' : '')+'Add '+(spell ? spellName+'](!cmd --button ADD_'+cmdStr+')' : 'the '+word+'' ) + + ' to '+(isPower ? 'Powers' : ('level '+level+(isMU ? ' MU' : ' PR')+' spellbook')) + + (isPower ? '}}' : '[Race](!cmd --button '+BT.RACE+'|?{Which Race?|'+races+'}) | '+(isGM ? ('[Creature](!cmd --button '+BT.CREATURE+'|?{Which Creature?|'+creatures+'}) | [NPC](!cmd --button '+BT.NPC+'|?{Which NPC?|'+npcs+'}) | [Container](!cmd --button '+BT.CONTAINER+'|?{Which Container?|'+containers+'}) |
['+fighter_class+'](!cmd --button '+BT.CLASS_F+'|?{Which Warrior Class?|'+fighter_classes+'}) | [Level '+fighter_level+'](!cmd --button '+BT.LEVEL_F+'|?{Which Warrior Level?|'+fighter_level+'}) | '+fighter_default+' |
['+wizard_class+'](!cmd --button '+BT.CLASS_W+'|?{Which Wizard Class?|'+wizard_classes+'}) | [Level '+wizard_level+'](!cmd --button '+BT.LEVEL_W+'|?{Which Wizard Level?|'+wizard_level+'}) | '+wizard_default+' |
['+priest_class+'](!cmd --button '+BT.CLASS_P+'|?{Which Priest Class?|'+priest_classes+'}) | [Level '+priest_level+'](!cmd --button '+BT.LEVEL_P+'|?{Which Priest Level?|'+priest_level+'}) | '+priest_default+' |
['+rogue_class+'](!cmd --button '+BT.CLASS_R+'|?{Which Rogue Class?|'+rogue_classes+'}) | [Level '+rogue_level+'](!cmd --button '+BT.LEVEL_R+'|?{Which Rogue Level?|'+rogue_level+'}) | '+rogue_default+' |
['+psion_class+'](!cmd --button '+BT.CLASS_PSI+'|?{Which Psion Class?|'+psion_classes+'}) | [Level '+psion_level+'](!cmd --button '+BT.LEVEL_PSI+'|?{Which Psion Level?|'+psion_level+'}) | '+psion_default+' |
Optionally, review ['+chosen+'](!cmd --button '+(base != 'Human' ? BT.REVIEW_CLASS : BT.REVIEW_RACE)+'|'+chosen+'|'+base+'|true) |
'+(imgRows[r+1]?(imgRows[r+1][0]):' ')+' | '+(imgRows[r]?(imgRows[r][0]):' ')+' | '+(imgRows[r+2]?(imgRows[r+2][0]):' ')+' |
'+(imgRows[r+1]?(imgRows[r+1][1]):' ')+' | '+(imgRows[r]?(imgRows[r][1]):' ')+' | '+(imgRows[r+2]?(imgRows[r+2][1]):' ')+' |
[Closed container](!cmd --button '+BT.TOKEN_IMG+'|'+tokenID+'|'+fields.Token_closedImg[0]+') | [Open container](!cmd --button '+BT.TOKEN_IMG+'|'+tokenID+'|'+fields.Token_openImg[0]+') |
'+(closedImg ? (' | '
+ + ''+(openImg ? (' |
Lock is ['+lockType+'](!cmd --button '+BT.LOCKTYPE+'|'+tokenID+'|?{Choose a lock type (or n0 lock)|'+getMagicList(fields.AbilitiesDB,miTypeLists,'lock',senderId,'None')+'}) | ' + + 'Trap is ['+trapType+'](!cmd --button '+BT.TRAPTYPE+'|'+tokenID+'|?{Choose a trap type (or no trap)|'+getMagicList(fields.AbilitiesDB,miTypeLists,'trap',senderId,'None')+'}) |
Ability | Description |
'+buttonType('Init menu',BT.ABILITY,std.init_menu.api,std.init_menu.action,'Ability name?','1.Initiative')+' | Initiative Menu, for all classes |
'+buttonType('Attack',BT.ABILITY,std.attk_hit.api,std.attk_hit.action,'Ability name?','2.Attack')+' | Attack ability (Roll20 rolls dice), for all monsters & classes with weapons |
'+buttonType('Attack menu',BT.ABILITY,std.attk_menu.api,std.attk_menu.action,'Ability name?','3.Attk menu')+' | Attack menu for all monsters & classes with weapons |
'+buttonType('Cast Spell',BT.ABILITY,std.cast_spell.api,std.cast_spell.action,'Ability name?','2.Cast Spell')+' | Ability to cast either a Wizard or Priest spell |
'+buttonType('Spells menu',BT.ABILITY,std.spells_menu.api,std.spells_menu.action,'Ability name?','3.Spells menu')+' | Spells menu (both Wizard & Priest) |
'+buttonType('Use Power',BT.ABILITY,std.use_power.api,std.use_power.action,'Ability name?','2.Use Power')+' | Ability to use Powers |
'+buttonType('Powers menu',BT.ABILITY,std.powers_menu.api,std.powers_menu.action,'Ability name?','3.Powers menu')+' | Powers menu, for all classes |
'+buttonType('Use MI',BT.ABILITY,std.use_mi.api,std.use_mi.action,'Ability name?','2.Use MI')+' | Ability to use a Magic Item |
'+buttonType('Items menu',BT.ABILITY,std.mi_menu.api,std.mi_menu.action,'Ability name?','3.Item menu')+' | Item Menu, for all classes |
'+buttonType('Other Actions',BT.ABILITY,std.other_actions.api,std.other_actions.action,'Ability name?','4.Other Actions')+' | Other Actions Menu, for all classes |
'+buttonType('Rest',BT.ABILITY,std.rest.api,std.rest.action,'Ability name?','5.Rest')+' | Ability to Rest, for all classes |
'+buttonType('Specials',BT.ABILITY,std.specials.api,std.specials.action,'Ability name?','4.Specials')+' | Display special attacks & defences |
'+buttonType('Bar',BT.ABILITY,std.bar.api,std.bar.action,'Ability name?','0._________')+' | Insert a separator bar |
[Access All Abilities](!cmd-master --button '+BT.AB_FULL+') | |
'+buttonType(c.action,BT.ABILITY,c.api,c.action,'Ability name?',c.action)+' | '+c.desc+' |
[Access Simple Abilities](!cmd --button '+BT.AB_SIMPLE+') | |
'+(dm ? '[Make' : (selButton + 'Is')) + ' Player-Character' + (dm ? ('](!cmd --button '+BT.AB_PC+'|'+menuType+'|0|?{Whick player will control?|'+players.join('|')+'})') : '')+' | ' + + ''+(pc ? '[Make' : (selButton + 'Is')) + ' Controlled by DM' + (pc ? ('](!cmd --button '+BT.AB_DM+'|'+menuType+'|0|)') : '')+' |
[Check Who Controls What](!cmd --check-chars) | |
[Choose Race/Class](!cmd --button '+BT.AB_CLASSES+') | [Set Saving Throws](!attk --check-saves |'+menuType+'|0) |
[Add to Spellbook](!cmd --add-spells MUSPELLS) | [Add to Proficiencies](!cmd --add-profs) |
[Copy Token Image](!cmd --copy-img '+selected[0]._id+') | [Manage Token Bars](!cmd --button '+BT.AB_MANAGE_TOKEN+'|'+menuType+') |
**Convert Character Sheet to RPGMaster** | |
[Convert Items](!cmd --conv-items) | [Convert Spells](!cmd --conv-spells) |
[['+ppdSave+']] | vs. Paralysis, Poison & Death |
[['+rswSave+']] | vs. Rod, Staff & Wand |
[['+ppSave+']] | vs. Petrification & Polymorph |
[['+bSave+']] | vs. Breath |
[['+sSave+']] | vs. Spell |
New: Added Drag & Drop help in this help handout
' + +'New: Drag & Drop NPCs added, and enhanced Creature definitions
' + +'New: Allow query results in Drag & Drop weapon, armour & item deinitions
' + +'The CommandMaster API is part of the RPGMaster suite of APIs for Roll20, and manages the initialisation of a Campaign to use the RPGMaster APIs, communication and command syntax updates between the APIs and, most importantly for the DM, easy menu-driven setup of Tokens and Character Sheets to work with the APIs.
' +'The CommandMaster API is called using !cmd.
' @@ -244,12 +281,7 @@ var CommandMaster = (function() { +'Commands can be stacked in the call, for example:
' +'!cmd --initialise --abilities' +'
When specifying the commands in this document, parameters enclosed in square brackets [like this] are optional: the square brackets are not included when calling the command with an optional parameter, they are just for description purposes in this document. Parameters that can be one of a small number of options have those options listed, separated by forward slash \'/\', meaning at least one of those listed must be provided (unless the parameter is also specified in [...] as optional): again, the slash \'/\' is not part of the command. Parameters in UPPERCASE are literal, and must be spelt as shown (though their case is actually irrelevant).
' - +'When a command is sent to Roll20 APIs / Mods, Roll20 tries to work out which player or character sent the command and tells the API its findings. The API then uses this information to direct any output appropriately. However, when it is the API itself that is sending commands, such as from a {{successcmd=...}} or {{failcmd=...}} sequence in a RPGMdefault Roll Template, Roll20 sees the API as the originator of the command and sends output to the GM by default. This is not always the desired result.
' - +'To overcome this, or when output is being misdirected for any other reason, a Controlling Player Override Syntax (otherwise known as a SenderId Override) has been introduced (for RPGMaster Suite APIs only, I\'m afraid), with the following command format:
' - +'!cmd [sender_override_id] --cmd1 args1... --cmd2 args2...' - +'
The optional sender_override_id (don\'t include the [...], that\'s just the syntax for "optional") can be a Roll20 player_id, character_id or token_id. The API will work out which it is. If a player_id, the commands output will be sent to that player when player output is appropriate, even if that player is not on-line (i.e. no-one will get it if they are not on-line). If a character_id or token_id, the API will look for a controlling player who is on-line and send appropriate output to them - if no controlling players are on-line, or the token/character is controlled by the GM, the GM will receive all output. If the ID passed does not represent a player, character or token, or if no ID is provided, the API will send appropriate output to whichever player Roll20 tells the API to send it to.
' - +'The CommandMaster API coordinates other APIs in the RPGMaster API series and provides the DM with facilities to set the Campaign up to use them. It will initialise a Campaign in Roll20 to use the RPGMaster series APIs. APIs can register their commands with CommandMaster and, should they change in the future, CommandMaster will search all Character Sheets and databases for that command and offer the DM the option to automatically update any or all of those found to the new command structure of that API. Selected Tokens and their associated Character Sheets can be set up with the correct Token Action Buttons, with spell-users given spells in their spell book, fighters given weapon proficiencies, setting saving throws correctly, and linking token circles to standard Character Sheet fields.
' +'Any API command can be registered with CommandMaster using the --register command. This will allow the command registered to be added as a Token Action Button to Character Sheets by the abilities command, and to be optionally updated in all Character Sheets wherever used should the details of the registration change.
' +'Danger: this command is very powerful, and can ruin your campaign if mis-used! The --edit command can be used to change any string in Character Sheet ability macros to any other string, using \'escaped\' characters to replace even the most complex strings. However, use with care!
' + +'CommandMaster can combine all its capabilities for managing character sheets to automatically populate a blank character sheet with data specific to an NPC of a particular race, class and level, or to make that character sheet represent a creature from The Monsterous Compendium. Data held in the Race-DB-NPCs and Race-DB-Creatures databases is used to configure the blank character sheet, and the GM or game creator can add their own bespoke NPC and creature definitions to their own databases to enhance and extend those provided in the same fashion as with other RPGMaster databases and APIs. See the Class & Race Database Help handout for more information.
' + +'To create a Drag & Drop NPC or Creature, add a blank character sheet to the Journal using the standard Roll20 [Character+] button at the top of the Roll20 Journal. Give the sheet a name and an image as desired, then close it. Drag the blank sheet onto the map surface to drop a token, select the token just dropped, and down the bottom of the Chat Window a dialog will have appeared, with options to select the [Creature] or [NPC] for the sheet (as well as other options which can be ignored for Drag & Drop). Use the buttons to select the NPC or Creature you want from the Roll Queries that appear on screen, which may also ask for further information (such as level of the NPC, or age of the creature etc). Be patient and wait for the API to set the character sheet up! This is one of the most complex things you can ask RPGMaster APIs to do - just think how long it would take you to set up a character sheet manually... Once complete, a number of dialogs will appear in the Chat Window describing the characteristics of the NPC or creature created. It is then necessary to click away from the token (de-select it) and then select it again to refresh the Action Buttons for the token. The token and character sheet can then be immediately used in play as that NPC or Creature.
' + +'It is possible to re-write any character sheet as a different NPC or Creature using the GM\'s [Token Setup] macro-menu button, or using the !cmd --abilities command, with the token representing the character sheet selected, and selecting the [Choose Race/Class] function on the displayed dialog. This displays the same dialog for selecting [NPC] or [Creature] as described above. The GM will be prompted for confirmation of over-writing an already populated sheet.
' + +'Drag & Drop Containers work in a similar fashion to other Drag & Drop functions for setting up character sheets. Drop a blank character sheet onto a map to drop a token, ensure that token is selected, then go to the dialog that appears at the bottom of the Chat Window. Select the [Container] button, and a list of various types of container appears as a Roll Query from which you can select a type. Then a new dialog appears in the Chat Window asking you to specify the characteristics of the container to create. The container can have a lock of various types (e.g. a combination lock, a password lock, a simple key lock, etc.), and can be trapped with various types of trap (such as a poison dart trap, an explosive runes trap, etc).
' + +'The types of container, and the macros used to drive the container\'s actions, are all defined in the Locks-Traps-DB. As with other RPGMaster databases, the GM or game creator can add their own containers, locks and traps by adding their own database. Refer to the Locks and Traps Help handout to get more information.
' + +'[Race](!cmd --button '+BT.RACE+'|?{Which Race?|'+races+'}) | '+(isGM ? ('[Creature](!cmd --button '+BT.CREATURE+'|?{Which Creature?|'+creatures+'}) | [Container](!cmd --button '+BT.CONTAINER+'|?{Which Container?|'+containers+'}) |
[Race](!cmd --button '+BT.RACE+'|?{Which Race?|'+races+'}) | '+(isGM ? ('[Creature](!cmd --button '+BT.CREATURE+'|?{Which Creature?|'+creatures+'}) | [NPC](!cmd --button '+BT.NPC+'|?{Which NPC?|'+npcs+'}) | [Container](!cmd --button '+BT.CONTAINER+'|?{Which Container?|'+containers+'}) |
['+fighter_class+'](!cmd --button '+BT.CLASS_F+'|?{Which Warrior Class?|'+fighter_classes+'}) | [Level '+fighter_level+'](!cmd --button '+BT.LEVEL_F+'|?{Which Warrior Level?|'+fighter_level+'}) | '+fighter_default+' | ||||||||
['+wizard_class+'](!cmd --button '+BT.CLASS_W+'|?{Which Wizard Class?|'+wizard_classes+'}) | [Level '+wizard_level+'](!cmd --button '+BT.LEVEL_W+'|?{Which Wizard Level?|'+wizard_level+'}) | '+wizard_default+' | ||||||||
['+v+'](!cmd --button '+BT.TOKEN_IMG+'|'+tokenID+'|'+prefix+i+'|?{Enter '+v+'}) | '+splitVariable(c.trim())+' | |||||||||
['+v+'](!cmd --button '+BT.TOKEN_IMG+'|'+tokenID+'|'+prefix+i+'|?{Enter '+v+'|'+list+'}) | '+splitVariable(c.trim())+' |
Standard: | the Party and the Foes (DM) each roll 1d10, and all of whichever gets the lowest roll goes first. The system supports taking the two rolls, and putting entries in the Turn Order for all defined Party members, and one entry for the Foes. |
---|---|
Group: | the Party and the Foes (DM) each roll 1d10, and then all Party members and all Foes choose what actions they will perform during the next round. The speed/casting time of the Character\'s / Foes selected action will then be added to the relevant roll to define the Character\'s / Foes initiative(s) which are added to the Turn Order. |
Individual: | each individual Character & Foe chooses what action they will do each round, and the speed/casting time of that action is added to an individual system-rolled 1d10 for that Character / Foe resulting in each Character\'s initiative(s) which are all added to the Turn Order. |
The type of initiative selected persists between game sessions.
' + +'Who is in the party can be defined by using API Buttons on the menu to do one of: search all maps in the Campaign for tokens controlled by Players; search just the map the Players are on for tokens controlled by Players; select a number of tokens on any map and add them to the list; or replace the whole list with the selected tokens.
' + +'The level of detail for initiative selections involving an attack can be selected to be "by weapon" or "by action". E.g. if a Longsword+1,+3 vs Undead has two attack action lines (a +1 attack, and another for +3 vs Undead) in the melee weapon table, the initiative by weapon option will just show 1 option of "Longsword+1,+3 vs Undead" whereas the initiative by action option will offer two initiative options of "Longsword+1" and "Longsword+3 vs Undead". The GM can choose which level of detail is presented to players.
' + +'Another API button checks to see if the Turn Order contains entries for every token listed as being in the Party, i.e. that everybody has selected their actions for the next round.
' + +'This menu can appear automatically as each completed round finishes if RoundMaster API is managing the Turn Order and Rounds. This is useful for standard and group initiative, as the first thing that needs to happen is for the Party & Foe initiative dice rolls to be entered. It is less useful for this menu to appear for individual initiative, and it can be turned off with an API Button on the menu.
' + +'--type < STANDARD / GROUP / INDIVIDUAL >' + +'
Takes a mandatory initiative type which must be one of those shown.
' + +'This command sets the initiative type to the specified type without bringing up the complete --init menu. The type of initiative specified persists between game sessions.
' + +'--init-level < WEAPON / ACTION >' + +'
Takes a mandatory initiative level which must be one of those shown.
' + +'This command sets the level of detail for players to specify initiative actions for attacks, which can be at the level of weapons in-hand or (where individual weapons have more than one possible attack action) at the level of every possible attack action. See the "--init" command above for more details and an example.
' + +'--menu [token-id]' + +'
Takes an optional token ID.
' + +'This command displays a chat menu of buttons for types of action that the Character / NPC / creature can perform. Each of these buttons may take the Player to a more detailed list of specific action buttons. Selecting any of the buttons will add the speed/casting time and correct number of instances of the selected action to the group or individual initiative dice roll (1d10) and enter the result in the Turn Order using the RoundMaster API - \'individual\'-type initiative dice rolls are performed in the background by the API and there is currently no option for the Player to do the roll instead. The system records the action selected and the speed of that action along with any modifiers as a message to display when the Character\'s / NPCs / creature\'s turn comes around.
' + +'For multiple actions per round, those subsequent to the first action with the same item have speeds in the Turn Order incremented from each other by the speed of the action: thus multiple attacks with a Longbow (2 per round, speed 8) after an initiative roll of 5 on a 1d10, will happen at priority 13 & 21. For attacks by a Fighter with two weapons, such as a Longsword (sp 5) in their left hand and a Short sword (sp 3) in their right hand, after an initiative roll of 5, the Short sword will get a Turn Order priority of 8 and the Longsword 10 - that is they are concurrent not sequential.
' + +'See the individual menu explanations for more detail on each type of action.
' + +'--monmenu [token-id]' + +'
Takes an optional token ID.
' + +'This produces a slightly simpler form of the initiative action menu for creatures. Otherwise, all actions result in similar processing as per the normal action selection.
' + +'If the creature is very simple (only uses the simple attack lines on the Monster tab of the AD&D2e Character Sheet), then it might be sensible to use the --monster command instead: see below.
' + +'--weapon [token-id]' + +'
Takes an optional token ID.
' + +'Displays a chat menu listing all the weapons that the Character / NPC / creature has "in-hand" (i.e. that are currently in the Weapon and Ranged tables), with additional options as appropriate to the Character Sheet. Rogue class characters will get a "Backstab" option which will apply the Rogue backstab multiplier as appropriate. Fighter & Rogue classes will get an option to choose two weapons (if there are two one-handed weapons in-hand) which presents the option of selecting a Primary and a Secondary weapon to do initiative for. Weapons can be those typed into the Character Sheet weapons tables (see RPGMaster CharSheet Setup handout) or loaded using the AttackMaster API (see AttackMaster documentation).
' + +'If the Character / NPC / creature has Powers or Magic Items they can use, buttons also appear on the menu to go to the menus to select these instead of doing a weapon initiative - see the --power and --mibag commands. There are also buttons for "Other" actions, such as Moving, Changing Weapon (which takes a round), doing nothing, or Player-specified actions - see the --other command.
' + +'--monster [token-id]' + +'
Takes an optional token ID.
' + +'Displays a chat menu only listing innate monster attacks from the Monster tab of the AD&D2e Character Sheet.
' + +'Creatures using the Innate Monster Attack fields on the AD&D2e Character Sheet Monster tab benefit from an extended syntax for entries in these fields: each field can take [<Attack name>,]<damage dice roll>[,<speed>][,<attack type>] for example Claw,1d8,2,S
and Club+1,2d4+1,5,B
. These will result in possible initiative actions for that creature for Claw and Club+1. If Attack Name is omitted, the dice roll is displayed as the action name instead. If the speed is omitted, the Innate attack speed field value is used instead. The speed will then be used to calculate the Turn Order priority. The optional attack type of S(slashing), P(piercing), or B(bludgeoning), or any combination of these, will be used by the AttackMaster API when displaying the success or otherwise of a targeted attack.
--complex [token-id]' + +'
Takes an optional token ID.
' + +'Displays a more complex monster attack menu, with both "Innate" attacks from the Monster tab as well as weapon attacks from the Character tab weapons tables (the API does not use the recently introduced Weapon table for Monsters on the Monster tab so that the AttackMaster API only has to deal with one set of tables) - see 3.1 above for entering weapons and 3.2 for setting up monster attacks. If the creature has powers or magic items, it will also offer action menu buttons for those. The selected attack or weapon speed will then be used to calculate the Turn Order priority.
' + +'--muspell [token-id]' + +'
Takes an optional token ID.
' + +'Displays a menu of Wizard spells that the Character / NPC has memorised (see the MagicMaster API documentation for memorising spells, or see RPGMaster CharSheet Setup handout for entering spells manually). Any spell that is still memorised can be selected for initiative, and the relevant casting time will be used to calculate the Turn Order priority.
' + +'--prspell [token-id]' + +'
Takes an optional token ID.
' + +'Displays a menu of Priest spells that the Character / NPC has memorised (see the MagicMaster API documentation for memorising spells, or see RPGMaster CharSheet Setup handout for entering spells manually). Any spell that is still memorised can be selected for initiative, and the relevant casting time will be used to calculate the Turn Order priority.
' + +'
--power [token-id]' + +'
Takes an optional token ID.
' + +'Displays a menu of Powers that the Character / NPC has been granted (see the MagicMaster API documentation for managing powers, or see RPGMaster CharSheet Setup handout for entering powers manually). Any power that has not been consumed can be selected for initiative, and the relevant casting time will be used to calculate the Turn Order priority.
' + +'--mibag [token-id]' + +'
Takes an optional token ID.
' + +'Displays a menu of Magic Items and non-magical equipment that the Character / NPC / creature has on their person - that is in the Item table (by default, the Potions table on the AD&D2e character sheet): see the Character Sheet Setup handout, or the MagicMaster API documentation for information on Items. Selecting an item for initiative uses the speed of action of that item to calculate the Turn Order priority.
' + +'--thief [token-id]' + +'
Takes an optional token ID.
' + +'Displays a menu of Thievish actions (with current percentage proficiencies of each). Selecting one for initiative uses the speed of action of that item to calculate the Turn Order priority.
' + +'--other [token-id]' + +'
Takes an optional token ID.
' + +'Displays a menu of other (non-attacking) actions that the Character / NPC / creature can take, namely: Moving (speed 0 as it is an innate ability); Changing Weapon (also speed 0 but takes all round); Doing Nothing (obviously speed 0); and one that allows the Player to enter a description and specify a speed for that action (presumably with the agreement of the DM).
' + +'--check-init [token-id]' + +'
Takes an optional token ID.
' + +'Displays a dialog showing all currently in-play initiative modifiers for the character represented by the identified or selected token. Also provides buttons to add, change or remove modifiers - one for mods that add or subtract to the initiative roll, and one for mods that multiply or reduce the number of attack actions the character can undertake. See the --set-mods command for more information.
' + +'--set-mods [token-id] | (DEL / FIX / MOD / MULT / BOTH) | name | [[=][+/-]mod] | [[=][+/-]mult] | [SILENT]' + +'
Takes an optional token ID, a command specifying the action, the name of the mod, the optional value of an addative modifier optionally preceeded with = or - or +, the optional value of a multiplying modifier optionally preceeded with = or - or +, and an optional "silent" qualifier.
' + +'Sets, fixes, changes, or deletes a named initiative modifier that can have one or both of additive and multiplicative elements. Each of these modifiers can include maths to be evaluated using standard maths operators +, -, *, /, (, ), and ^(#,#,...) for max, and v(#,#,...) for min (commas can be replaced by semi-colons ;). Preceeding a mod or mult value by = will set that value (e.g. =-2 will set the value to -2), or preceeding by + or - without the = will amend the current value by that amount. The FIX command will cause the named modifier to override all other modifiers and the initiative dice roll and set future initiative rolls to the value of mod (until DEL or a different command is used for the same name of modifier). Including the SILENT argument will not display any outcome, while ommitting it will display the result in a --check-init dialog.
' + +'--maint' + +'
DM Only command. Does not take any parameters.
' + +'Displays a chat menu of action API Buttons to control the Turn Order Tracker window using commands sent to the RoundMaster API. The key one is Start/Pause, which initialises RoundMaster and starts it managing the Turn Order, or pauses it so that stepping through the Turn Order does not trigger any RoundMaster actions (such as counting down token status timers or initiating Effects). The full list of functions is:
' + +'Maintenance Menu Button | ' + +'RoundMaster !rounds command (unless otherwise stated) | ' + +'Description | ' + +' ' + +'
---|---|---|
Start / Pause | --start | Starts / Pauses RoundMaster functioning |
Start Melee | --clearonround on --clear | Causes the Turn Order to automatically clear at the end of each round (once all actions have completed) ready for Players to select actions for their Characters |
Stop Melee | --clearonround off | Stops the Turn Order from automatically clearing at the end of each round, so that the Turn Order is preserved. Can be useful when just wanting to cycle around a list of Characters selected in the !init --init menu command and running "Standard" initiative. |
Re-start | --sort | Re-sorts the current Turn Order, effectively re-starting the round. Useful if the DM accidentally starts the next round by moving the Turn Order on before all Players have completed their initiative actions - allow new actions to be selected and then use Re-start |
Set Round Number | --reset # | Sets the current Round number to #. If # is larger than the current round, all token status counters will advance by the number of rounds difference, ending if they reach 0 with the consequential Effects triggered |
Clear Turn Order | --clear | Clears the Turn Order of all entries (except the round number) |
Remove Tokens from Tracker | --removefromtracker | Removes all the selected tokens from the Turn Order and the Tracker window. Multiple tokens can be selected and removed all at the same time. |
Edit Selected Tokens | --edit | Displays the status markers on all the selected tokens, and offers options to edit or delete them. The "spanner" icon edits the status, and the "bin" icon deletes it. |
Move Token Status | --moveStatus | For each of the selected tokens in turn, searches for tokens in the whole campaign with the same name and representing the same character sheet, and moves all existing statuses and markers from all the found tokens to the selected token (removing any duplicates). This supports Players moving from one Roll20 map to another and, indeed, roundMaster detects page changes and automatically runs this command for all tokens on the new page controlled by the Players who have moved to the new page. |
Clean Selected Tokens | --clean | Drops all status markers from the selected token, whether they have associated effects or time left, or are just manually applied markers. Useful when there might have been corruption, or everyone is just confused! The token statuses still exist, and associated markers will be correctly rebuilt at the start of the next round or the next trigger event (but not manually added ones). |
Enable Long Rest for PCs | !init --end-of-day | Run the normal initMaster end-of-day command |
Enable Long Rest for selected tokens | !init --enable-rest | Init API command to enable a long rest only for the characters / NPCs / creatures represented by the selected tokens, at no cost. See the MagicMaster API documentation for information on Long Rests |
Set Date | Currently not implemented - future expansion | |
Set Campaign | Currently not implemented - future expansion | |
Update Selected Tokens | !cmd --abilities | Use the CommandMaster API function (if loaded) to setup and maintain Character ability action buttons, weapon proficiencies, spell books & granted powers, saving throws, token "bar & circle" assignment etc. See CommandMaster API documentation on the --abilities command. |
Emergency Stop! | --stop | After confirmation, performs a Full Stop and re-start of the RoundMaster API, dropping all internal tables of statuses & effects, token markers, timers etc. Use with care! |
--check-tracker' + +'
DM Only command. Does not take any parameters.
' + +'Uses the Player Character name list created & maintained in the --init menu or with the --list-pcs command, and checks that all of the Character\'s named have completed initiative selection to the point where their token name is in the Turn Order at least once, and appears in the Tracker window. Names those that have not in a message to the DM, or states that initiative is complete.
' + +'--list-pcs < ALL / MAP / REPLACE / ADD >' + +'
DM Only command. Takes a specifier for the tokens to have in the Player Character list which must be one of those listed.
' + +'Updates the internally held list of Characters that are controlled by Players (and others that the DM can add at will). This list is displayed on the --init menu, and is used by --check-tracker and --end-of-day commands. The list persists between sessions of game-play. The following parameters have the following effects:
' + +'all: | looks across all tokens in the campaign and creates a new list composed of those representing Character Sheets controlled by a Player (standard Roll20 Character Sheet functionality - refer to the Help Centre for information on setting Players to control Character Sheets and their tokens). |
---|---|
map: | creates a new list that only has Characters represented by tokens on the current Player map that are controlled by Players. (See Roll20 Help Centre on how to select the current Player map). |
replace: | creates a new list including all the currently selected token(s) (whomever controls them), and no others. |
add: | adds the currently selected token(s) (whomever controls them) to the existing list leaving all the others unchanged. |
--end-of-day [ASK/ASKTOREST/OVERNIGHT/REST/SET/FOES]|[=][cost]' + +'
DM Only command. Takes an optional type of rest (which, if provided, must be one of those shown - defaults to ASK) and an optional cost parameter, optionally preceded by an \'=\' character. If cost is not provided, it defaults to that previously set with SET and/or \'=\'.
' + +'This command performs the "End-of-Day" processing for the campaign. This consists of enabling Long Rests for all Characters / NPCs / creatures to regain their spells and powers, and for recharging Magic Items to regain their charges (see MagicMaster API documentation for information on Long Rests). It also removes spent ammunition from quivers that has not been recovered, as it is assumed to be lost, broken or taken by other creatures during the period of the night (see AttackMaster API documentation about recovery of ammunition and its loss over a Long Rest).
' + +'Each day can cost or earn the members of the Party money, perhaps depending on where they stay overnight, whether they eat just camp rations or lavish meals, use an Inn and drink too much, or earn money doing a job. The optional cost parameter can be set to a positive cost to the party which will be deducted from every member, or a negative quantity which will be earned (a negative cost).
' + +'ASK: | If no rest type is supplied, or ASK is used, the DM is asked to confirm if they wish the cost to be deducted from/earned by all the Characters listed. If No is selected, nothing is deducted or earned. The system then sets flags to allow Players to perform a Rest command on their characters (see MagicMaster API). |
---|---|
ASKTOREST: | Asks the DM to confirm the cost/earnings in the same way as ASK, but then automatically performs the MagicMaster API --rest command for each character in the party, and the Players do not need to do so. |
OVERNIGHT: | Applies the cost to the Party members without asking and enables them to rest (they have to do the rest themselves). If cost (or the previously set default cost) is not a number (e.g. a Roll Query), asks if a charge is to be made. |
REST: | Does the same as OVERNIGHT, but automatically runs the MagicMaster API --rest command for all characters in the party, and the Players do not need to do so. |
FOES: | Does the same as OVERNIGHT, but for all NPCs and Monsters, allowing them to rest. |
SET: | If the rest type is SET and/or there is an \'=\' before the cost, will not run the "End-of-Day", but instead will set the standard cost for each night if no cost parameter is given when other commands are used. If the \'=\' is followed by a Roll Query (see Roll20 Help Centre for information on Roll Queries), the Roll Query will be run each time the –end-of-day command is run without a cost parameter, allowing (for instance) the DM to select from a list of possible daily costs or earnings. However, remember to replace the \'?\' at the start of the Roll Query with ? so that the Roll Query does not run when it is passed in to be set. Other characters can be substituted as follows: |
Character | ? | [ | ] | @ | - | | | : | & | { | } |
---|---|---|---|---|---|---|---|---|---|---|
Substitute | ^ | << | >> | ` | ~ | | | & | { | } | |
Alternative | \\ques; | \\lbrak; | \\rbrak; | \\at; | \\dash; | \\vbar; | \\clon; | \\amp; | \\lbrc; | \\rbrc; |
--help' + +'
This command does not take any arguments. It displays a very short version of this document, showing the mandatory and optional arguments, and a brief description of each command.
' + +'--hsq from|[command]' + +'
' + +'--handshake from|[command]
Either form performs a handshake with another API, whose call (without the \'!\') is specified as the from paramater in the command parameters. The response from InitiativeMaster is always an --hsr command. The command calls the from API command responding with its own command to confirm that RoundMaster is loaded and running: e.g.
' + +'Received: !init --hsq magic
'
+ +'Response: !magic --hsr init
Optionally, a command query can be made to see if the command is supported by InitMaster if the command string parameter is added, where command is the InitMaster command (the \'--\' text without the \'--\'). This will respond with a true/false response: e.g.
' + +'Received: !init --handshake attk|monster
'
+ +'Response: !attk --hsr init|monster|true
--debug (ON/OFF)' + +'
Takes one mandatory argument which should be ON or OFF.
' + +'The command turns on a verbose diagnostic mode for the API which will trace what commands are being processed, including internal commands, what attributes are being set and changed, and more detail about any errors that are occurring. The command can be used by the DM or any Player - so the DM or a technical advisor can play as a Player and see the debugging messages.
' + +'' + + '<%= confirm_button %>' + + ' | ' + + '' + + '<%= reject_button %>' + + ' | ' + + '
Commands can be stacked in the call, for example:
' +'!init --list-pcs ALL --init' +'
When specifying the commands in this document, parameters enclosed in square brackets [like this] are optional: the square brackets are not included when calling the command with an optional parameter, they are just for description purposes in this document. Parameters that can be one of a small number of options have those options listed, separated by forward slash \'/\', meaning at least one of those listed must be provided (unless the parameter is also specified in [] as optional): again, the slash \'/\' is not part of the command. Parameters in UPPERCASE are literal, and must be spelt as shown (though their case is actually irrelevant).<\p>' - +'
When a command is sent to Roll20 APIs / Mods, Roll20 tries to work out which player or character sent the command and tells the API its findings. The API then uses this information to direct any output appropriately. However, when it is the API itself that is sending commands, such as from a {{successcmd=...}} or {{failcmd=...}} sequence in a RPGMdefault Roll Template, Roll20 sees the API as the originator of the command and sends output to the GM by default. This is not always the desired result.
' - +'To overcome this, or when output is being misdirected for any other reason, a Controlling Player Override Syntax (otherwise known as a SenderId Override) has been introduced (for RPGMaster Suite APIs only, I\'m afraid), with the following command format:
' - +'!init [sender_override_id] --cmd1 args1... --cmd2 args2...' - +'
The optional sender_override_id (don\'t include the [...], that\'s just the syntax for "optional") can be a Roll20 player_id, character_id or token_id. The API will work out which it is. If a player_id, the commands output will be sent to that player when player output is appropriate, even if that player is not on-line (i.e. no-one will get it if they are not on-line). If a character_id or token_id, the API will look for a controlling player who is on-line and send appropriate output to them - if no controlling players are on-line, or the token/character is controlled by the GM, the GM will receive all output. If the ID passed does not represent a player, character or token, or if no ID is provided, the API will send appropriate output to whichever player Roll20 tells the API to send it to.
' - +'The most common approach for the Player to run these commands is to use Ability macros on their Character Sheets which are flagged to appear as Token Action Buttons: Ability macros & Token Action Buttons are standard Roll20 functionality, refer to the Roll20 Help Centre for information on creating and using these.
' - +'In fact, the simplest configuration is to provide only Token Action Buttons for the menu commands: --menu and --monmenu. From these, most other commands can be accessed. If using the CommandMaster API, its character sheet setup functions can be used to add all the necessary and/or desired Ability Macros and Token Action Buttons to any Character Sheet.
' + +'[General API Help]' +'The Initiative Master API ("InitMaster") provides commands that allow the DM to set and manage the type of initiative to be used in the campaign, and for Players to undertake initiative rolls. The API uses data on the Character Sheet represented by a selected token to show menus of actions that can be taken: these commands are often added to the Character Sheet as Ability Macros that can be shown as Token Actions (see Roll20 Help Centre for how to achieve this, or the CommandMaster API documentation). The API displays resulting Turn Order token names with action priorities in the Turn Order Tracker window (standard Roll20 functionality - see Roll20 documentation & Help Centre).
' +'Note: Use the --maint command to display the Maintenance Menu and start the RoundMaster API using the Start / Pause button (at the top of the displayed menu) before using the Turn Order Tracker. The top entry in the Turn Order Tracker window should change from showing a "Stopped" symbol, and change to a "Play".'
@@ -675,7 +670,7 @@ var initMaster = (function() {
if (_.isUndefined(state.initMaster.changedRound))
{state.initMaster.changedRound = false;}
if (!state.initMaster.dailyCost)
- {state.initMaster.dailyCost = '?{What costs?|Camping 1sp,0.1|Inn D&B&B 2gp,2|Inn B&B 1gp,1|Set other amount,?{How many GP - fractions OK?}|No charge,0}';}
+ {state.initMaster.dailyCost = '?{What costs?|Camping 1sp,0.1|Inn D&B&B 2gp,2|Inn B&B 1gp,1|No charge,0|Set other amount,?{How many GP - fractions OK?}}';}
if (!state.initMaster.playerChars)
{state.initMaster.playerChars = getPlayerCharList();}
if (!state.initMaster.initType)
@@ -930,15 +925,19 @@ var initMaster = (function() {
*/
var sendDebug = function(msg) {
if (!!state.initMaster.debug) {
- var player = getObj('player',state.initMaster.debug),
- to;
- if (player) {
- to = '/w "' + player.get('_displayname') + '" ';
- } else
- {throw ('sendDebug could not find player');}
- if (!msg)
- {msg = 'No debug msg';}
- sendChat('Init Debug',to + ''+msg+'',null,{noarchive:!flags.archive, use3d:false});
+ if (playerIsGM(state.initMaster.debug)) {
+ log('InitMaster Debug: '+msg);
+ } else {
+ var player = getObj('player',state.initMaster.debug),
+ to;
+ if (player) {
+ to = '/w "' + player.get('_displayname') + '" ';
+ } else
+ {throw ('sendDebug could not find player');}
+ if (!msg)
+ {msg = 'No debug msg';}
+ sendChat('Init Debug',to + ''+msg+'',null,{noarchive:!flags.archive, use3d:false});
+ };
};
};
@@ -2618,7 +2617,6 @@ var initMaster = (function() {
};
};
if (!forceFind) rememberWeapRef(charCS,hand,ref);
-// log('getRef: hand '+hand+' is '+miName+', '+(ref%2 ? 'MW' : 'RW')+' table index = '+index+', button ref = '+ref);
return [index,ref];
};
@@ -2648,7 +2646,6 @@ var initMaster = (function() {
}
};
if (weapCount.monster > 0 && (!charButton || !charButton.length || charButton == -1)) {
-// log('makeWeaponMenu: dealing with monster preselected attack');
charButton = 0;
monButton = weapCount.monster > 1 ? 0 : 1;
handleInitMonster( Monster.COMPLEX, charCS, [BT.MON_INNATE,tokenID,charButton,monButton], senderId );
@@ -2676,7 +2673,6 @@ var initMaster = (function() {
+ '}}';
}
if (hands > 2 || weapCount.monster > 1) {
-// log('makeWeaponMenu: hands = '+hands+', & weapCount.monster = '+weapCount.monster+', so showing All Weapons button');
content += '{{Many Hands Option='
+ (-2 == charButton ? '' : (submitted ? '' : '['))
+ 'All Weapons'
@@ -3122,8 +3118,7 @@ var initMaster = (function() {
// find armour type
armourType = (attrLookup( charCS, fields.Armor_name ) || 'leather' ).toLowerCase();
- switch (armourType.toLowerCase()) {
- case 'no armour':
+ switch (armourType.toLowerCase().replace('armour','armor')) {
case 'no armor':
case 'none':
armourMod = fields.Armor_mod_none;
@@ -3131,13 +3126,18 @@ var initMaster = (function() {
case 'light':
case 'leather':
+ case 'leather armor':
armourMod = fields.Armor_mod_leather;
break;
case 'studded':
case 'padded':
+ case 'studded armor':
+ case 'padded armor':
case 'studded leather':
case 'padded leather':
+ case 'studded leather armor':
+ case 'padded leather armor':
armourMod = fields.Armor_mod_studded;
break;
@@ -3370,7 +3370,11 @@ var initMaster = (function() {
setAttr( charCS, fields.initMultiplier, totalMult );
// log('makeCheckInitMenu: content = '+content);
- if (!silent) sendResponse( charCS, content, senderId );
+ if (!silent) {
+ sendResponse( charCS, content, senderId );
+ } else {
+ sendWait( senderId, 0, 'init' );
+ }
return {mod:totalMod,mult:totalMult};
}
@@ -4030,7 +4034,8 @@ var initMaster = (function() {
names = _.pluck(state.initMaster.playerChars,'name');
content = '&{template:'+fields.menuTemplate+'}{{name=End of Day}}';
if (rest || night || foes) {
- content += '{{desc=The following characters have '+(night ? 'overnighted ' : 'rested ')+(cost < 0 ? 'and earned ' : ' at a cost of ')+cost+' gp}}{{desc1=';
+ cost = parseFloat(cost) || 0;
+ content += '{{desc=The following characters have '+(night ? 'overnighted ' : 'rested ')+(cost < 0 ? 'and earned ' : ' at a cost of ')+Math.abs(cost)+' gp}}{{desc1=';
if (foes) content += '\nAll NPCs & monsters';
filterObjs( function(obj) {
if (!names.length) return false;
@@ -4055,7 +4060,6 @@ var initMaster = (function() {
names = _.without(names,tokenName);
setAttr( charObj, fields.Timespent, '1' );
setAttr( charObj, fields.CharDay, state.moneyMaster.inGameDay );
- cost = parseFloat(cost) || 0;
if (cost == 0) return true;
setAttr( charObj, fields.Money_copper, ((parseInt(attrLookup( charObj, fields.Money_copper )||0)||0) - Math.floor((cost*100)%10)) );
setAttr( charObj, fields.Money_silver, ((parseInt(attrLookup( charObj, fields.Money_silver )||0)||0) - Math.floor((cost*10)%10)) );
@@ -4351,7 +4355,7 @@ var initMaster = (function() {
selected = msg.selected,
roundsExists = apiCommands.rounds && apiCommands.rounds.exists,
isGM = (playerIsGM(senderId) || state.initMaster.debug === senderId),
- t = 10;
+ t = 2;
var doInitCmd = function( e, selected, senderId ) {
var arg = e, i=arg.indexOf(' '), cmd, argString;
diff --git a/InitMaster/script.json b/InitMaster/script.json
index 7b454918d1..80e1d27478 100644
--- a/InitMaster/script.json
+++ b/InitMaster/script.json
@@ -2,8 +2,8 @@
"$schema": "https://github.com/DameryDad/roll20-api-scripts/blob/InitMaster/InitMaster/Script.json",
"name": "InitMaster",
"script": "initMaster.js",
- "version": "3.5.0",
- "previousversions": ["1.037","1.039","1.041","1.043","1.045","1.046","1.3.00","1.3.01","1.3.02","1.3.03","1.4.01","1.4.02","1.4.05","1.4.06","1.4.07","1.5.01","2.1.0","2.2.0","2.3.0","2.3.1","2.3.3","3.0.0","3.1.2","3.2.0","3.3.0","3.3.1","3.4.0"],
+ "version": "4.0.1",
+ "previousversions": ["1.037","1.039","1.041","1.043","1.045","1.046","1.3.00","1.3.01","1.3.02","1.3.03","1.4.01","1.4.02","1.4.05","1.4.06","1.4.07","1.5.01","2.1.0","2.2.0","2.3.0","2.3.1","2.3.3","3.0.0","3.1.2","3.2.0","3.3.0","3.3.1","3.4.0","3.5.0"],
"description": "The InitMaster API supports initiative for RPGs using the Turn Order and the Tracker window. It provides functions dealing with all aspects of: managing how initiative is done; rolling for initiative; for 'group' and 'individual' initiative types providing Character action selection to determine the speed and number of attacks of weapons, the casting time of spells & the usage speed of magic items; supporting initiative for multiple attacks with one or multiple weapons per round; supporting and tracking actions that take multiple rounds; managing the resulting Turn Order; as well as performing the 'End of Day' activity. It works very closely with the RoundMaster API to the extent that InitiativeMaster cannot work without RoundMaster (though the reverse is possible). InitiativeMaster also works closely with AttackMaster API and MagicMaster API and uses the data configured on the Character Sheet by these other APIs, although it can use manually completed Character Sheets once correctly configured.\n[InitMaster Documentation](https://wiki.roll20.net/Script:InitMaster) \n\n### Related APIs\nThis API works best with the RPGMaster series of APIs, and requires RoundMaster API to work (loaded automatically on One-Click install)\n[RPGMaster Documentation](https://wiki.roll20.net/RPGMaster)\n\n### Getting Started\n* If using with CommandMaster API, use the `!cmd --initialise` command to install the DMs Macro Quick Bar buttons, or\n* If not using CommandMaster API, as a Macro in the DM's macro quick bar, add the command `!init --maint` to manage RoundMaster functions\n* Add the command `!init --menu` as an Ability Macros on Character Sheets of Characters, NPCs & Monsters that will use the API, and tick 'Show as Token Action'. These menus will then be available to Players controlling those sheets and give access to all common commands used in game-play.",
"authors": "Richard E.",
"roll20userid": "6497708",
diff --git a/MagicMaster/4.0.1/MagicMaster.js b/MagicMaster/4.0.1/MagicMaster.js
new file mode 100644
index 0000000000..da74bd12e7
--- /dev/null
+++ b/MagicMaster/4.0.1/MagicMaster.js
@@ -0,0 +1,9971 @@
+// Github: https://github.com/Roll20/roll20-api-scripts/tree/master/MagicMaster
+// Beta: https://github.com/DameryDad/roll20-api-scripts/tree/MagicMaster/MagicMaster
+// By: Richard @ Damery
+// Contact: https://app.roll20.net/users/6497708/richard-at-damery
+
+var API_Meta = API_Meta||{}; // eslint-disable-line no-var
+API_Meta.MagicMaster={offset:Number.MAX_SAFE_INTEGER,lineCount:-1};
+{try{throw new Error('');}catch(e){API_Meta.MagicMaster.offset=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-8);}}
+
+/**
+ * MagicMaster.js
+ *
+ * * Copyright 2020: Richard @ Damery.
+ * Licensed under the GPL Version 3 license.
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * This script is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This script is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ *
+ * The goal of this script is to create and automate aspects of magic spell & item
+ * discovery, storage & use, initially for the ADnD 2e game in Roll20
+ *
+ * v0.1.0 to v2.3.4 For earlier change log see earlier versions
+ * v3.0.0 31/10/2023 Added support for other character sheets and game systems. Corrected how the
+ * casting level of spells used as powers are calculated. Fixed storing items in
+ * type 6 or 7 containers. Added query: attribute to magic items to add Roll Queries
+ * to adding a new MI to a sheet to drive variable data. Moved parseData() to library.
+ * Fixed Long Rest for MI spells & powers. Added config option for magical weapon plus
+ * affecting weapon speeds, and corrected --config command documentation.
+ * v3.1.0 17/12/2023 Added additional support for other character sheets. Moved configuation menu to
+ * library. Added support for an "item carried" flag as used on the AD&D1e sheet.
+ * v3.1.2 15/01/2024 Implemented inheritance for magic item database objects. Implemented magic item
+ * query: and variables. Implemented "change-last" and "cursed+change-last" magic item
+ * classes for items that change to a different item when reaching zero charges. Added
+ * "Remove Curse" option to GM-only MI menu. Use evalAttr() when specifying qty for
+ * storing an MI
+ * v3.2.0 08/02/2024 New recharge types 'enable' and 'disable' which are uncharged but allow c:
+ * charge comparisons to support enabling and disabling of weapon attack rows. Fix
+ * moving hidden db item variables to always move with the trueName of the weapon
+ * (not the name). Added swordType as a shorthand query tag. Fixes to do with handling
+ * hidden equipment items. Add optional API command as a 5th parameter to --message
+ * command.
+ * v3.2.1 11/02/2024 Improvements to management of hidden items. Config item to set default reveal type
+ * to on use or manually. Better support for data attribute hide: - force hiding with
+ * 'hide', default to auto-hide state with no definition, or force no hiding with
+ * anything else. Improve parseStr() handling of undefined or empty strings.
+ * v3.3.0 26/02/2024 Allow re-usable (-1) powers to be weaponised in the same way that other spells and
+ * powers are. For spells & powers stored on items with a casting level, set the MU-
+ * and PR- casting levels to the stored level as well as the overall casting level.
+ * Extend "changing" items to allow cursed types. Define the store: attribute for
+ * bag-type objects which can be used with "nostore" to define a bag from which can
+ * be taken from but not stored. Extend GM's add-items dialog to cater for equipment.
+ * Fixed spell-storing items displaying "ghost" spells. Fixed issue with removing
+ * memorised spells.
+ * v3.4.0 27/03/2024 Added pick: and put: data attributes for commands to execute on picking & putting
+ * (or adding/removing) a magic item. Fixed bag creation on MI use. Fixed display
+ * of spells stored in a spell-storing item using a --view-spells command. Allow
+ * --mi-power command to be passed multiple '/' separated item names to cater for
+ * changing items. Add check for impact on character's initiative when picking or
+ * putting an item. Add maths evaluation to the MIqty argument of --addmi command.
+ * Fix trapped container that does not have a "Trap-1" (open/disarmed) macro to assume
+ * trap removed. Added --query-qty command to set the SpellCharges attribute on the
+ * character sheet to the charges of a spell, power or item. Added the --button SHOWMORE
+ * command to support the new [show more...] and {{hide#=...}} syntax of RPGMspell &
+ * RPGMdefault templates. On token death, save current container type & set to 6
+ * (force inanimate), but reset to original if revived.
+ * v3.4.1 23/05/2024 Fixed errors in renaming magic items using the GM-edit-mi menu.
+ * v3.5.0 06/05/2024 Updated --level-change to support "fixed class" where does not ask for classes.
+ * Allow configuration option to "grey out" spell & item action buttons when viewing
+ * (rather than using). Add "splitable" items which can be split when picked up but
+ * do not stack (e.g. used for paper, parchment & papyrus) except for renamed items
+ * which can't do this. Fix spell storing items that have an "-ADD" postfix allowing
+ * the player/character to add spells. Fix magic item rev: attribute to correctly use
+ * 'manual', 'view', or 'use'. Support renaming of items picked up that are not
+ * stackable and the container already contains that item name. Fix handling of use
+ * of --mi-charges with "=#" for setting the quantity to an absolute number. Support
+ * RPGM maths operators for numbers passed to --mi-charges. Fixed stacking of looted
+ * items. Added container self-heal capability on version change.
+ * v3.5.1 02/08/2024 Fixed storing an item to a character sheet of security type 7 (Force Sentient)
+ * v4.0.1 22/09/2024 Gave option for pre-determined name change on picking up an item with a duplicated
+ * displayed name. Fix listing/viewing/using a spell or power that is on the sheet but
+ * not in a database. Improved error checking and type conversion. Corrected speed
+ * of items to use inheritance via resolveData(). Improved --message command resolution
+ * of selected vs. id token selection. Added new MI-DB-Treasure database.
+ */
+
+var MagicMaster = (function() {
+ 'use strict';
+ var version = '4.0.1',
+ author = 'RED',
+ pending = null;
+ const lastUpdate = 1738351019;
+
+ /*
+ * Define redirections for functions moved to the RPGMaster library
+ */
+
+ const getRPGMap = (...a) => libRPGMaster.getRPGMap(...a);
+ const getHandoutIDs = (...a) => libRPGMaster.getHandoutIDs(...a);
+ const setAttr = (...a) => libRPGMaster.setAttr(...a);
+ const attrLookup = (...a) => libRPGMaster.attrLookup(...a);
+ const setAbility = (...a) => libRPGMaster.setAbility(...a);
+ const abilityLookup = (...a) => libRPGMaster.abilityLookup(...a);
+ const doDisplayAbility = (...a) => libRPGMaster.doDisplayAbility(...a);
+ const getAbility = (...a) => libRPGMaster.getAbility(...a);
+ const getTableField = (...t) => libRPGMaster.getTableField(...t);
+ const getTable = (...t) => libRPGMaster.getTable(...t);
+ const getLvlTable = (...t) => libRPGMaster.getLvlTable(...t);
+ const initValues = (...v) => libRPGMaster.initValues(...v);
+ const checkDBver = (...a) => libRPGMaster.checkDBver(...a);
+ const saveDBtoHandout = (...a) => libRPGMaster.saveDBtoHandout(...a);
+ const buildCSdb = (...a) => libRPGMaster.buildCSdb(...a);
+ const checkCSdb = (...a) => libRPGMaster.checkCSdb(...a);
+ const getDBindex = (...a) => libRPGMaster.getDBindex(...a);
+ const updateHandouts = (...a) => libRPGMaster.updateHandouts(...a);
+ const findThePlayer = (...a) => libRPGMaster.findThePlayer(...a);
+ const findCharacter = (...a) => libRPGMaster.findCharacter(...a);
+ const fixSenderId = (...a) => libRPGMaster.fixSenderId(...a);
+ const evalAttr = (...a) => libRPGMaster.evalAttr(...a);
+ const getCharacter = (...a) => libRPGMaster.getCharacter(...a);
+ const characterLevel = (...a) => libRPGMaster.characterLevel(...a);
+ const caster = (...a) => libRPGMaster.caster(...a);
+ const getTokenValue = (...a) => libRPGMaster.getTokenValue(...a);
+ const classObjects = (...a) => libRPGMaster.classObjects(...a);
+ const redisplayOutput = (...a) => libRPGMaster.redisplayOutput(...a);
+ const getMagicList = (...a) => libRPGMaster.getMagicList(...a);
+ const getShownType = (...a) => libRPGMaster.getShownType(...a);
+ const addMIspells = (...a) => libRPGMaster.addMIspells(...a);
+ const handleCheckWeapons = (...a) => libRPGMaster.handleCheckWeapons(...a);
+ const handleCheckSaves = (...a) => libRPGMaster.handleCheckSaves(...a);
+ const handleGetBaseThac0 = (...a) => libRPGMaster.handleGetBaseThac0(...a);
+ const parseClassDB = (...a) => libRPGMaster.parseClassDB(...a);
+ const parseData = (...a) => libRPGMaster.parseData(...a);
+ const parseStr = (...a) => libRPGMaster.parseStr(...a);
+ const resolveData = (...a) => libRPGMaster.resolveData(...a);
+ const getSetPlayerConfig = (...a) => libRPGMaster.getSetPlayerConfig(...a);
+ const makeConfigMenu = (...a) => libRPGMaster.makeConfigMenu(...a);
+ const sendToWho = (...m) => libRPGMaster.sendToWho(...m);
+ const sendMsgToWho = (...m) => libRPGMaster.sendMsgToWho(...m);
+ const sendPublic = (...m) => libRPGMaster.sendPublic(...m);
+ const sendAPI = (...m) => libRPGMaster.sendAPI(...m);
+ const sendFeedback = (...m) => libRPGMaster.sendFeedback(...m);
+ const sendResponse = (...m) => libRPGMaster.sendResponse(...m);
+ const sendResponsePlayer = (...p) => libRPGMaster.sendResponsePlayer(...p);
+ const sendResponseError = (...e) => libRPGMaster.sendResponseError(...e);
+ const sendError = (...e) => libRPGMaster.sendError(...e);
+ const sendCatchError = (...e) => libRPGMaster.sendCatchError(...e);
+ const sendParsedMsg = (...m) => libRPGMaster.sendParsedMsg(...m);
+ const sendGMquery = (...m) => libRPGMaster.sendGMquery(...m);
+ const sendWait = (...m) => libRPGMaster.sendWait(...m);
+
+ /*
+ * Handle for reference to character sheet field mapping table.
+ * See RPG library for your RPG/character sheet combination for
+ * full details of this mapping. See also the help handout on
+ * RPGMaster character sheet setup.
+ */
+
+ var fields = {
+ defaultTemplate: 'RPGMdefault',
+ spellTemplate: 'RPGMspell',
+ potionTemplate: 'RPGMpotion',
+ menuTemplate: 'RPGMmenu',
+ warningTemplate: 'RPGMwarning',
+ messageTemplate: 'RPGMmessage',
+ };
+
+ /*
+ * Handle for reference to database data relevant to MagicMaster.
+ * Actual data is held in the relevant RPG-specific library. Refer
+ * to the library for full details. See also the help handout for
+ * each database.
+ */
+
+ var dbNames;
+
+ /*
+ * Handle for the Database Index, used for rapid access to the character
+ * sheet ability fields used to hold database items.
+ */
+
+ var DBindex = {};
+
+ /*
+ * Handle for the library object used to pass back RPG & character sheet
+ * specific data tables.
+ */
+
+ var RPGMap = {};
+
+ /*
+ * set design strings for common icons and button colours before they
+ * are used.
+ */
+
+ const design = {
+ turncolor: '#D8F9FF',
+ roundcolor: '#363574',
+ statuscolor: '#F0D6FF',
+ statusbgcolor: '#897A87',
+ statusbordercolor: '#430D3D',
+ edit_icon: 'https://s3.amazonaws.com/files.d20.io/images/11380920/W_Gy4BYGgzb7jGfclk0zVA/thumb.png?1439049597',
+ delete_icon: 'https://s3.amazonaws.com/files.d20.io/images/11381509/YcG-o2Q1-CrwKD_nXh5yAA/thumb.png?1439051579',
+ settings_icon: 'https://s3.amazonaws.com/files.d20.io/images/11920672/7a2wOvU1xjO-gK5kq5whgQ/thumb.png?1440940765',
+ apply_icon: 'https://s3.amazonaws.com/files.d20.io/images/11407460/cmCi3B1N0s9jU6ul079JeA/thumb.png?1439137300',
+ bag_icon: 'https://s3.amazonaws.com/files.d20.io/images/335981697/ocKqy1UIfPMSD-TYEO6oXA/thumb.png?1680722832',
+ info_msg: ' New: Non-stacking duplicate items picked up offer option for default rename to make unique The MagicMaster API provides functions to manage all types of magic, including Wizard & Priest spell use and effects; Character, NPC & Monster Powers; and discovery, looting, use and cursing of Magic Items. All magical aspects can work with the RoundMaster API to implement token markers that show and measure durations, and produce actual effects that can change token or character sheet attributes temporarily for the duration of the spell or permanently if so desired. They can also work with the InitiativeMaster API to provide menus of initiative choices and correctly adjust individual initiative rolls, including effects of Haste and Slow and similar spells. This API can also interact with the MoneyMaster API (under development) to factor in the passing of time, the cost of spell material use, the cost of accommodation for resting, and the cost of training for leveling up as a spell caster (Wizard, Priest or any other). The MagicMaster API is called using !magic (or the legacy command !mibag). Commands to be sent to the MagicMaster API must be preceded by two hyphens \'--\' as above for the --help command. Parameters to these commands are separated by vertical bars \'|\', for example: If optional parameters are not to be included, but subsequent parameters are needed, use two vertical bars together with nothing between them, e.g. Commands can be stacked in the call, for example: When specifying the commands in this document, parameters enclosed in square brackets [like this] are optional: the square brackets are not included when calling the command with an optional parameter, they are just for description purposes in this document. Parameters that can be one of a small number of options have those options listed, separated by forward slash \'/\', meaning at least one of those listed must be provided (unless the parameter is also specified in [] as optional): again, the slash \'/\' is not part of the command. Parameters in UPPERCASE are literal, and must be spelt as shown (though their case is actually irrelevant). MagicMaster uses a large range of items held in databases. The current versions of these databases are distributed with the game-version-specific RPGMaster Library, updated as new versions are released via Roll20. The provided databases are held in memory, but can be extracted to ability macros in database character sheets using the !magic --extract-db command. These macros can do anything that can be programmed in Roll20 using ability macros and calls to APIs, and are found (either in the Character Sheet database or the internal database in memory) and called by the MagicMaster API when the Player selects them using the menus provided by the MagicMaster functions. The GM can add to the provided items in the databases using standard Roll20 Character Sheet editing, following the instructions provided in the Magic Database Handout. The definitions for character Races & Classes held in the Race-DB and Class-DB databases include a description of the race and class and its capabilities, the powers/non-weapon proficiencies that it comes with, any restrictions on weapons, armour and spells that it is subject to, and other class-specific aspects such as alignments and races. As you might expect, these are not just descriptions, but restrict the player character to the characteristics defined (alterable by using the !magic --config command). The Class & Race Database Help handout provides information on the structure of the race & class specifications and how the GM / game creator can add their own races and classes and alter those provided. The Ability Macros for spells and powers include descriptions of the spell they represent (limited, I\'m afraid, to avoid copyright issues), and also can optionally have API Buttons embedded in them which, if selected by the Player, can enact the actions of the spell or power. The API Buttons call one or more of the API commands listed in this document, or commands provided by other APIs. This is most powerful when combined with the RoundMaster API to implement token statuses and status markers with durations and effect macros, enabling the spells & powers to make temporary (or permanent, if desired) changes to the targeted creature\'s token and character sheet attributes. The best way to learn about these capabilities is to look at example spell definitions in the databases and use those spells or powers to see what they do. The Item database is currently split into nine parts: Weapons, Ammunition, Armour, Lights, Potions, Scrolls & Spellbooks, Wands Staves & Rods, Rings, and Miscellaneous. More might be added in future releases, and any DM can add more databases with their own items. Many magic items have actions that they can perform in the same way as Spells & Powers, using API Buttons in their macros that call MagicMaster API commands, or commands from other APIs. As with spells & powers, this is most powerful when combined with the capabilities of the RoundMaster API. Items can have stored spells (like Rings of Spell Storing) and the spells can be cast from them, and/or can have powers that can be consumed and are refreshed each day. Again, using the RoundMaster API, the spells and powers can have temporary or permanent effects on Tokens and Character Sheets, if desired. Classes are set using the CommandMaster API or via the AttackMaster !attk --other-menu menu (or can be set manually on the Character Sheet). Classes can be those provided in the Class-DB, or any other class. Class names that are not in the database will adopt the attributes of the standard classes depending on the character sheet field the class name and level are entered into: Warrior, Wizard, Priest, Rogue, and Psion. Depending on the settings selected by the GM under the --config menu, the choise of class will restrict or grant the character\'s ability to use certain items and cast certain spells. The MagicMaster API provides commands to perform menu-driven addition of items to the Character Sheet. Using these commands will set up all the necessary fields so that the Player can use the items with the other APIs - if using MagicMaster then items should not be added directly to the Character Sheet. Items can also be acquired by finding them in chests or on tables (simply tokens with images of chests or tables that represent Character Sheets with items added to them) that can be looted, or even dead bodies of NPCs that have been killed in battle. MagicMaster provides commands that support a menu-driven way to perform looting. Characters, especially Rogues, can even try to Pick Pockets to take items from NPCs (or even other Characters...), though failure may alert the DM (or other Player) to the attempt. Containers can even be trapped, with magical consequences if the trap goes off! On the other hand, Characters can also put items away into chests or onto tables or other storage places, or give them to other Characters or NPCs. Spells need to be added in two steps: 1. adding to a Character\'s or NPC\'s spell book; and 2. Memorising the spells each day. The simplest way to add spells to a Character\'s spell books is to use the CommandMaster API functions that set up Character Sheets from scratch. However, spells can be added to the Character Sheet manually: see the RPG Master CharSheet Setup handout for details of how to do this. Either approach results in the Character having a list of spells at each spell level they can use that they have available to memorise. Spells can be memorised using the MagicMaster menus or via the !magic --mem-spell MagicMaster command. This limits the number of spells memorised at each level to the number that is valid for the Character, with their specific characteristics, class, level and other valid adjustments (though it is possible to add a "fudge factor" if needed). Once memorised, they can be rememorised or changed at any time, though the DM usually limits this in game play to once each in-game day. If a Player is happy with the spells a Character has, the Character just needs to rest at the end of the day to regain their spells (and powers, and recharging magic item charges). Powers are added in exactly the same way as Spells. The difference between the two is that Powers are granted to a Character, either as a function of the class they have adopted, or from being granted powers in game-play. Of course, NPCs and creatures also have many various powers. Some Powers can be used more than once a day, or even \'at will\' (an example is Priests turning undead). Items possessed by the Character can be used to perform their functions, using MagicMaster menus. When used with the InitiativeMaster API, the action for the next round can be the use of a specific item the Character has on them, with the speed of that item. This may use charges or consume quantities of the item, and these charges may or may not be regained overnight, or through other means. The items use Roll20 ability macros that can be as simple as putting text in the chat window explaining what the item does, through to much more complex targeting of effects on the user, a single other target, or all tokens in a defined area. When used with the RoundMaster API, targeted tokens can have a status marker applied with a pre-determined duration and associated effects at the start, each round and when it finishes. Items that are totally consumed will automatically disappear from the Character Sheet. Spells memorised by the Character can be cast using MagicMaster menus. As with items, when used with the InitiativeMaster API with Group or Individual initiative, the action for the next round can be the casting of a specific spell with the speed of the Casting Time. Casting a spell will remove it from memory for the rest of the day, but a rest will bring it back. Like items, spells use Roll20 ability macros and thus can perform any function a macro or an API call can achieve. The same capability to affect tokens and Character Sheets is available if used with the RoundMaster API. MagicMaster API provides commands to change the lighting settings of the token to reflect illumination, as if holding various light sources. This includes both radiant light sources such as hooded lanterns, torches, continual light gems, magic items and magic armour, and also directed light sources such as beacon lanterns and bullseye lanterns which only illuminate in beams. The DM is provided with tools to be able to add items to chests, NPCs, Characters etc. These tools allow the DM to also change certain aspects of the items, including the displayed name and the cursed status of the item. Items that are cursed are not obvious to Characters and Players, and such items can be \'hidden\' and appear to be other items until revealed as the cursed item by the DM. The tools also allow the DM to increase or restrict the number of items Characters can have on their person: it is then possible to give each Character a \'backpack\' token/character sheet, which the Character can store items to and get items from - of course, retrieving an item from the backpack takes a round (at the DM\'s discression - the system does not impose this). DMs can also add their own items, spells and powers to additional databases (the provided databases should not be added to, but entries can be replaced by new entries in your own databases - updates will not replace your own databases - see the Magic Database Help handout). This requires some knowledge of Roll20 macro programming and use of APIs. See the Roll20 Help Centre for information. Takes an optional token ID and an optional menu type as arguments. If token ID is not specified, uses the selected token. If the specified token is not associated with a character that has a spell book of the chosen type, or any granted powers, an error message is displayed. Takes a mandatory spell book type, an optional token ID, and an optional magic item name as arguments. If token ID is not specified, uses the selected token. The Character Sheet associated with the token must have spell books specified for the relevant types of spells or powers. These are lists of spells from the spell macro databases (see Section 7) specified by level (powers are all 1 level) and as lists separated by \'|\'. E.g. Charm-Person|Light|Sleep. If the CommandMaster API is installed, the GM can use its menus to set up character spell books and granted powers. Initially displays a menu for memorising Level 1 spells (the only level for powers), with buttons to: choose a spell from the Level 1 spell book on the character sheet; review the chosen spell; and one for each memorising slot the Character has at this level. Other buttons to memorise or remove spells become available when spells or slots are chosen. Another button goes to the next available level with slots. When a granted power is memorised to a slot, a quantity per day can be specified: -1 will grant unlimited uses of the power per day. Memorising any other type of spell is limited to 1 use per slot. Depending on the settings on the --config menu, the character will be limited to memorising spells and powers allowed to their character class and level. MI-MU and MI-PR have a special function: these are used to cast memorised spells into the named spell-storing magic item (if no item is named, the last item selected by the Character running the command will be used instead), such as a Ring-of-Spell-Storing. Magic Item spells are stored in an unused level of the Character Sheet. This command displays both all memorised spells and all spell-storing magic item spell slots, and allows a memorised spell to be selected, a slot (for the same spell name) to be selected, and the spell cast from one to the other. Spells can only be replaced by the same spell that was in the slot previously (unless this is the first time spells have been stored in a blank spell-storing item). Takes a mandatory spell type, an optional token ID, and an optional magic item name. If token ID is not specified, uses the selected token. Displays a menu of all levels of memorised spells of the selected type (there is only 1 level of powers). Spells that have already been cast appear as greyed out buttons, and can\'t be selected. Spells that are still available to cast that day can be selected and this runs the spell or power macro from the relevant database without consuming the spell, so that the Player can see the specs. Action buttons on the macro are "greyed out" and can\'t be selected, with the exception of [View...] buttons on spell-storing items which will display the stored spells/powers if selected (again without consuming them). Adding MI- before any of the types of spell views the spells or powers available for the specified magic item, or the last Magic Item used by the Character if no magic item name is provided. Generally this version of the command is only called from API Buttons from the magic item\'s ability macro. Takes a mandatory spell type, an optional token ID (if not specified, uses the selected token), an optional casting level, and an optional caster name, an optional \'CHARGED\' command, and an optional magic item name. This displays a menu of all levels of the memorised spells/powers of the relevant type. MI displays the spell book for spells stored on the specified magic item, or the last magic item used or viewed if not specified (both MU & PR), and MI-POWER all stored powers in the specified or last selected magic item, (this version of the command is generally called using an API Button in the magic item ability macro). The player can select a spell/power and then a button becomes available to cast it, using up that slot/deducting a power charge until the next long rest (or until the item is recharged). If a casting_level is specified, the spell will be cast as if by a caster of that level, and if a casting_name is specified, that name will be displayed in the spell macro information. These functions are often used for magic items that cast at specific levels of use, or magic artefacts that are named and/or sentient. If these are not specified, the caster name and relevant class level are used. In either case, specified or not, the character\'s Character Sheet Attributes called @{Casting-name} and @{Casting-level} are set to the values used, and can be used in spell, power, or magic item macros. If the optional CHARGED parameter is specified (only relevant to spells and powers stored on magic items), this specifies that the Magic Item from which the spell or power is cast is charged, and looses one charge when that cast is made. This is generally the case when the spell or power is on a Scroll. When the charge quantity reaches zero, the item will follow the behaviour determined by its charge type (charged, uncharged, rechargeable, recharging, self-charging) - see section 4.1 for more information on charges and charge types. Takes a mandatory spell type, a mandatory token ID and an optional spell name. This command is used for certain spells and powers that, once cast, allow continuing effects in the same or subsequent rounds, without using additional charges. If the optional spell name is not used, the command just casts again the same spell as the last spell cast by the character associated with the selected token, at the same casting level and casting name. If a spell name is specified, this spell is cast instead as if it were the same spell: this is used where different spell macros are required to specify subsequent spell effects. Takes a mandatory token_id. Reviews all the Powers currently in the Powers Spellbook, checking for Race, Creature, Class and user-added Powers, and checks them against their respective definitions in the various databases to assess if they can be used at the level of experience/Hit Dice of the character / creature. Memorises each valid power for the number of uses per day specified in the Race, Class or Creature database definition: user-added powers are memorised at unlimited uses per day unless a default is otherwise specified in the Powers database, on the basis that DMs/Players will either change this by rememorising them individually, or otherwise play to the agreed limits of use. Takes an optional token ID as an argument. If token ID is not specified, uses the selected token. Displays a menu with the following actions: Use a magic item, Search for magic items & treasure, Store magic items in a container, Edit the contents of a character\'s magic item bag, and View the contents of a character\'s magic item bag. Searching & Storing are explained in section 4. Takes an optional token ID, and an optional item type as arguments. If token ID is not specified, uses the selected token. If the item type is not specified, defaults to MAGICAL. Displays a menu similar to editing memorised spells. At the top are buttons to choose different types of magic items which have macros in the magic item databases. If the optional item type is MARTIAL, only weapons, ammo and armour are listed; if ALL is specified, lists of all items are shown; otherwise only non-MARTIAL items are listed. The slots available in the bag are shown (with their current contents) and, when magic items and/or slots are chosen buttons become selectable below to store, review, or remove magic items in/from the bag. Storing a magic item will ask for a number - either a quantity or a number of charges. Magic Items can be of various types: Charged (is used up when reaches 0), Uncharged (a number is a pure quantity that is not consumed), Recharging (regains charges after each long rest), Rechargable (is not used up when reaches 0, stays in bag and can be recharged when the DM allows), Self-charging (recharge at a rate per round determined by the item) and can also be Cursed - more under section 4. This menu is generally used when Magic Item & treasure containers (such as Treasure Chests and NPCs/monsters with treasure) have not been set up in a campaign as lootable, and provides a means of giving found magic items to characters. The DM just tells the Player that they have found a magic item, and the Player adds it to their Character Sheet using this command (more likely accessed via the Magic Item menu). Takes an optional token ID as an argument. If token ID is not specified, uses the selected token. Displays a menu of items in the character\'s magic item bag, with the quantity possessed or the number of charges. Pressing a button displays the named Magic Item specs without using any charges so that the Player can review the specifications of that item. Action buttons on the item macro are "greyed out" and can\'t be selected, with the exception of [View...] buttons on spell-storing items which will display the stored spells/powers if selected (again without consuming them). Items for which all charges have been consumed are greyed out, and cannot be viewed as the character can no longer use them. They will become viewable again if they gain charges. Takes an optional token ID as an argument. If token ID is not specified, uses the selected token. Displays a similar menu as for viewing the contents of the Magic Item Bag, but when an item is selected, a button is enabled that uses the Magic Item and consumes a charge. Other buttons specified in the item macro might use additional charges to perform additional effects. See section 3. Items with 0 quantity or charges left are greyed out and cannot be selected, unless they have abilities to regain charges such as "spell absorbing" items. When a Charged Item reaches 0 charges left, it is removed from the character\'s Magic Item Bag automatically. Takes an optional token ID (if not provided, uses selected token), then either the name of the item to be replaced or the row number of the item in the equipment list, the name of the item to add, the quantity to add (defaults to quantity of replaced item, or 1), optionally a hand number to use to take in-hand, optionally NOCURSE if replacement of cursed items is possible, and optionally SILENT to not trigger messages, menus or dialogs. This command can be used to add a named item from the databases to a character, NPC, creature or other container without going through other dialogs to select the item. It will add the item to a numbered row in the equipment list or, more usefully, replace a named item that already exists in the list (or \'-\' to find an empty row). If the item is one that can be taken in-hand (e.g. a weapon or a shield, or a magic item like a wand or staff), the optional \'hand number\' can be used to specify which hand to take it in. 0=prime hand,1=offhand,2=both,3 onwards for other hands, or just \'=\' (or blank) means replace in-hand if mi-to-replace is in-hand or worn as a ring - if the item is not one that can be held the item will not be taken in-hand. If the item to be replaced is cursed, it will not be replaced and an error message will be displayed unless the NOCURSE option is used. Finally, the command will pop up the edit-mi dialog or the gm-edit-mi dialog (if NOCURSE is specified) showing the resulting equipment list unless the SILENT flag is also used. The quantity can be a number to set the amount of the item to add. If preceeded by an operator (such as \'+\', \'-\', \'*\', or \'/\'), the quantity will modify the quantity of the item replaced (up to the maximum quantity of the replaced item). If the quantity is just \'=\' the quantity will set to the same as the replaced item, or 1 for an added item, or if the item to be replaced is not found and quantity is \'=\', the item will not be added. Takes a mandatory token ID, a mandatory value preceeded by an optional + or -, an optional magic item name, an optional maximum value, and an optional magic item charge type override as arguments. Does not display anything but alters the number of current or recoverable charges on an item. By default, alters the last magic item used by the character, or will affect the named magic item. Warning: a character can have two items of the same name, and there is no guarantee which will be affected if the name is used. Remember: using a Charged, Recharging, Rechargeable or Self-Charging Magic Item will automatically use 1 charge on use (unless the ItemData specification includes the field c:0, in which case no charges will automatically be deducted on use). If the c: tag is not used, or is anything other than 0, then charges will be deducted (default 1 charge) on use of the item. In addition, that one charge deduction always happens - if an effect of a Magic Item uses 2 charges, only 1 more needs to be deducted. Note: \'-\' reduces current remaining charges, \'+\' adds to the maximum recoverable charges, no + or - sets the maximum recoverable charges, and \'0\' (or starting with 0 e.g. \'01\') the item will recharge to the set or current maximum. This command cannot otherwise be used to increase the current remaining charges unless the item is of type absorbing. Using minus \'-\' before the value will deduct charges from the current quantity/charges: e.g. if using an optional power of the item that uses more than 1 charge. Using + before the value will add the value to the number of recoverable charges (overnight or rechargeable to), up to any specified maximum (often used for magic items that regain variable numbers of charges overnight). Just using the value without + or - will just set the number of recoverable charges to the given value. This command is not required to recharge self-charging items but can be used to change the maximum number of charges they will self-charge up to. Absorbing items can gain charges in use from other sources, so the --mi-charges command works differently: \'-\' reduces both current and maximum charges and \'+\' only increases current charges (but only to maximum and not beyond). Using neither \'-\' or \'+\' will set the current charges (but, again, only up to the maximum). The charge-override can be used to temporarily change the charge behaviour of the magic item. Specifying an override will cause the magic item to behave as if its charging type was that of the override only for this call. Thus charges could be deducted from an uncharged item by overriding by rechargeable or charged. Takes a mandatory token ID, mandatory power name (optionally prefixed by a power type), mandatory magic item name (New which can be several names separated by forward slash), and an optional casting level as parameters. Magic Items, especially artefacts, can have their own powers that can be used a specified number of times a day, or at will. This command can be used in API buttons in the Magic Item macro to call on that power. The power name and the magic item name (or names, especially where items that change with use have powers) must be specified to select the right power. If a casting level is specified, any relevant impacts on use of the power will be taken into account: it is often the case that magic items use powers at specific levels. If not specified, the item using Character\'s level is used (user does not need to be a spell caster). Generally, magic item powers have unique names, though they do not have to. Such magic items require specific setting up by the DM - see later sections. However, powers can have a prefix that indicates a power type that specifies the power is in fact a Wizard spell (MU-), a Priest spell (PR-), or a Magic Item (MI-) or (for completeness) confirmed as a Power (PW-). Specifying a power type prefix means the appropriate database types will be searched for the named power - thus (for instance) a Wizard or Priest spell can be specified as a Magic Item power without having to program a duplicate in the Powers Databases. If no power type prefix is specified, the system will first search for a matching power in the Powers Databases (both API-supplied and user-supplied), then all Wizard spell databases, then Priest spell databases, then all Magic Item databases, and finally the character sheet of the creature wielding the Magic Item. Takes a mandatory token ID and a mandatory magic item name. This command presents a dialog in the chat window that stores spells or powers in any magic item that has been defined as being able to cast stored spells/powers. The item definition must include somewhere in its definition the command call Once a spell is cast from a spell-storing item, the spell is spent and does not return on a long or short rest: the spell must be refreshed using the --mem-spell command (see below). If a power is used from a power-storing item, the power will have a number of uses per day (or be "at will"), and will refresh on a long rest. Takes a mandatory spell type (optionally followed by -ADD or -ANY or -CHANGE), an optional Token ID for the character, and an optional magic item name. If token ID is not provided, it uses the selected token, and if the magic item name is not specified, the last used magic item is assumed. MI-MU and MI-PR mem-spell types are used to cast memorised spells into a spell-storing magic item, such as a Ring of Spell Storing. Magic Item spells are stored in an unused spell level of the Character Sheet (by default Wizard Level 15 spells). This command displays both all the character\'s memorised spells and the spell-storing magic item spell slots in the specified magic item (or the last one used if not specified), and allows a memorised spell to be selected, a slot to be selected (for the same spell name - limiting the item to only store certain defined spells unless "-ANY" or "-CHANGE" is added to the command), and the spell cast from one to the other. If either "-ANY" or "-ADD" are added to the spell type string, the player can just select a memorised spell and then immediately cast it into the device without choosing a slot: this will add the spell to the device. If the extension is "-ADD" then existing spells need to be refreshed with an identical spell, the same way as if -ADD was not specified. If "-ANY" is specified, not only can the player extend the spells stored, they can replace expended spell slots with any spell, not just the one previously stored in the slot. "-CHANGE" will allow different spells to be stored in a slot, but not give the ability to add to the number of slots. If none of these qualifiers are specified in the command, spell slots cannot be added, and slots have to be refreshed with the same spell - just like a normal Ring of Spell Storing. Generally, the GM will state that the device used for storing the spells will have a limited capacity of some type - number of spell levels, number of spells, types or spheres of spell, etc. The number of levels can be set in the database entry for the magic item (see the Magic Database Help handout) and the caster\'s spells of higher level than can be stored will not be available. The number of spells can be restricted by using the "-CHANGE" qualifier or no qualifier. Alternatively the GM can just tell the players to do so manually. Unlike some other menus, however, magic item spell slots that are full are greyed out and not selectable - their spell is intact and does not need replacing. Spell slots that need replenishing are displayed as selectable buttons with the spell name that needs to be cast into the slot. The level of the caster at the time of casting the spell into the magic item is stored in the magic item individually for each spell - when it is subsequently cast from the spell-storing magic item it is cast as if by the same level caster who stored it. A spell-storing magic item can hold spells from one or both of Wizard and Priest spells. The database where the spell is defined is also stored in the magic item with the spell, so the correct one is used when at some point in the future it is cast. A copy of the spell macro is also stored on the Character Sheet of the character that has the spell-storing magic item. If, when cast, the system can\'t find the database or the spell in that database (perhaps the character has been moved to a different campaign with different databases), and it can\'t use the copy on its own character sheet for some reason, the system will search all databases for a spell with the same name - this does not guarantee that the same spell will be found: the definition used by a different DM could be different - or the DM may not have loaded the database in question into the campaign for some reason. In this case, an error will occur when the spell is cast. See the Magic Items Database documentation for how spell-storing magic items are defined. Takes a mandatory casting type of \'MI\', an optional Token ID (if token ID is not provided, it uses the selected token), an optional casting level (which will be ignored in favour of the level of the caster of the spell who cast it into the item), an optional casting name which, if not specified, will be the name of the wielder of the magic item, an optional \'CHARGED\' command, and an optional magic item name (if not provided, uses name of the last magic item the character selected, viewed or used). This command works in the same way as for casting other spells. However, spells cast from a spell-storing magic item are not regained by resting - either short or long rests. The only way to regain spells cast from such an item is to cast them back into the item from the character\'s own memorised spells: see the --mem-spell command above. If the character does not have these spells in their spell book or is not of a level able to memorise them, then they will not be able to replace the spells and will have to get another spell caster to cast them into the item (by giving the item to the other Character and asking nicely for it back again) or wait until they can get the spells. If the optional parameter \'CHARGED\' is used, spells on the magic item are not re-storable. The spells will be deleted after they are all used up and the magic item will not be able to store any more spells. This is mainly used for Scrolls with multiple spells. Takes an optional token_id which defaults to that of the currently selected token, followed by a mandatory spell name. This command is intended for use with magic items of the type spellbook (listed in the GM\'s Add Items dialog under Scrolls), although any spell storing magic item that has the Data tag learn: set to 1 (as in learn:1) will prompt the player with a [Learn this spell] button when stored spells are viewed. If the GM stores a spellbook item in a container or adds it to an NPC character sheet, and then stores Wizard spells in the spellbook (all of this by using the GM\'s Add Items dialog), any Wizard spell-casting character looting the spellbook will gain access to view the spells the GM stored in it. Viewing any of the spells in a spellbook will display a [Learn this spell] button at the bottom of the spell description. Selecting this button runs this command, which will: If this API is used in conjunction with the RoundMaster API, Magic Items, Spells & Powers can all place status markers on tokens, and also cause real Effects to alter token & character sheet attributes and behaviours: when cast; during each round of their duration; and when they expire. See the RoundMaster documentation for further information, especially on Effects and the Effects Database. Takes mandatory CASTER, SINGLE or AREA command, a mandatory caster token ID, for SINGLE/AREA a mandatory target token ID, mandatory spell name, duration & increment (preceeded by an optional +/-), and an optional message and optional token marker name. If using the RoundMaster API, this command targets one, or a sequential set of tokens and applies a token marker to the token for the specified duration number of rounds, with the increment applied each round. The optional message will be shown below that token\'s turn announcement each round. The marker used will either be the one specified or if not specified a menu to choose one will be shown. If the Player is not the DM/GM, the system will ask the DM/GM to approve the marker/effect for each token - this allows the DM to make saving throws for monsters/NPCs affected where appropriate. See the RoundMaster API documentation for full details. Takes mandatory token ID, effect name, duration of the effect, an increment to the duration per round (often -1), an optional message each round for the targeted token, and an optional status marker to use (if not supplied, the DM or user will be asked to select one). Note: this command requires RoundMaster API to also be loaded, but is a !magic command. Sets up the Character represented by the specified token ready for an "Attack Roll" to deliver a touch attack for a spell or power or magic item use that requires an attack. The parameters are those that will be passed to the !rounds --target command if the attack is successful (see above). To use this command, add it as part of a spell, power or MI macro in the appropriate database, before or after the body of the macro text (it does not matter which, as long as it is on a separate line in the macro - the Player will not see the command). Then include in the macro (in a place the Player will see it and be able to click it) an API Button call [Button name](~Selected|To-Hit-Spell) which will run the Ability "To-Hit-Spell" on the Character\'s sheet (which has just been newly written there or updated by the --touch command). Thus, when the Player casts the Character\'s spell, power or MI, they can then press the API Button when the macro runs and the attack roll will be made. If successful, the Player can then use the button that appears to mark the target token and apply the spell effect to the target. See the RoundMaster API documentation for further information on targeting, marking and effects. Takes an optional Token ID (if not specified, uses the selected token), an optional number of levels (plus or minus: if not specified assumes -1), an optional total number of HP gained or lost, and an optional class to apply the level change to. Mainly used for attacks and spell-like effects that drain levels from opponents, this command undertakes all the calculations and Character Sheet updates that can automatically be done when a character or creature changes experience level. Saving throw targets are reassessed, weapon attacks per round recalculated, numbers of memorised spells changed, Race & Class powers checked for level appropriateness, etc. If this is a single class character or a creature, the optional class parameter will be ignored and the single class/monster HD applied. If the HP change is not specified for a single class character, then the appropriate HP dice will be rolled and changed (Tip: it\'s better to put in a roll query to ask the player for the HP to change by). If the character is multi- or dual-class, it will either use the class specified, or asks the player which class to add/drain levels to/from and the hit points for each. Takes an optional Token ID (if not specified, uses the selected token), and a mandatory change value (plus, minus, or zero), and an optional attribute name (defaults to STRENGTH) Mainly used to support magical effects and creature attacks that drain or add to attributes such as Strength, this command specifically deals with aspects such as Exceptional Strength, remembering if a Character has exceptional strength as a characteristic and taking it into account as the value is changed. Going up or down from the original rolled value and then back the other way will include as a step the exceptional, percentage value. If the change requested would take the value past the original rolled value, the change will only go as far as the original value, whatever change was requested. However, the change can then continue with subsequent calls to beyond the original value with subsequent calls. Note:Should the rolled value need to change permanently to a new rolled value, the change value of 0 (zero) will reset the remembered original rolled value to the current value of the attribute - this is not needed the first time the command is used on a character sheet, which will trigger this value to be remembered for the first time. Takes an optional token ID (if not specified, uses the selected token), an optional rest type, short or long, an optional magic type to regain charges for, and an optional timescale for days passing. Most magic requires the character to rest periodically to study spell books, rememorise spells, and regain powers and charges of magic items. This command implements both Short and Long rests. The type of rest (short or long) can be specified or, if not specified, the system will ask the Player what type of rest is to be undertaken - though Long Rests will be disabled if the Timescale (either the optional value or the character sheet attribute) is not 1 or more days (see below). The type of magic to be affected can also be specified or, if not specified, all types of magic are affected. A Short rest is deemed to be for 1 hour (though this is not a restriction of the system and is up to the DM), and allows spell casters (such as Wizards and Priests, as well as others) to regain their 1st level spells only. This can happen as often as the DM allows. A Long rest is considered to be an overnight activity of at least 8 hours (though again this is not a restriction of the system and is up to the DM). A Long rest allows spell casters to regain all their spells, all character and magic item powers to be regained to full uses per day, and for recharging magic items to regain their charges up to their current maximum. After a long rest, ammunition that has been used but not recovered can no longer be recovered using the Ammunition Management command (see AttackMaster API documentation): it is assumed that other creatures will have found the ammo, or it has been broken or otherwise lost in the 8 hours of the long rest. A Long rest can only be undertaken if certain conditions are met: either the optional Timescale (in days) must be specified as 1 or more days, or the Character Sheet must have a Roll20 attribute called Timescale, current, set to a value of 1 or more (can be set by InitiativeMaster API --end-of-day command). An internal date system is incremented: an attribute on the Character Sheet called In-Game-Day is incremented by the Timescale, and Timescale is then set to 0. If the InitiativeMaster API is being used, the system will interact with the "End of Day" command to allow rests to be coordinated with the choice of accommodation (and its cost...!) or with earnings made for the day\'s adventuring. Takes an optional Token ID (defaults to the selected token), a mandatory magic item name (case insensitive), an optional number of charges to recharge to, and an optional power name (case insensitive). This command restores the powers for a single magic item, or even a single power of a single magic item. If the optional number of charges is specified, this is the number of charges set for the power, otherwise the power is restored to its original max uses. If a power name is specified, and the item has a power of the same name, only that power will be affected. Otherwise, all powers of the item will be restored. Takes an optional token ID (defaults to selected token), a mandatory item type, the mandatory name of the item, and an optional "silent" to surpress feedback. Some spells, powers, and magic items need to know how many charges they have left in order to impact the effect they have. The quantity is difficult to find from the table entry in macro code unless the row number is known, so this command finds the item for the macro and saves the current quantity in the character sheet attribute that can be accessed using @{selected|spellcharges}. Generally a call to this command should be outside of any roll template so that the command runs before the roll template is displayed and any API buttons become available. Takes an optional token ID. If token ID is not specified, uses the selected token. This command opens a menu showing all the items in the Items table of the character sheet associated with the specified token. Unlike the Player version of the command (--edit-mi), this command shows all attributes of every magic item, including those of hidden and cursed items, and also offers an additional list of "DM Only" magic items from the magic item databases. The following functions are available once both a magic item is selected from the lists, and a slot to store it in are selected: Takes a mandatory token ID of the character\'s token, mandatory token ID of the token to check for traps, mandatory token ID of the token doing the checking. This command will check a token for any traps. If the container represented by the token was created using the Drag & Drop container system (see CommandMaster API documentaion for details of the Drag & Drop container system) this command will start the selected container\'s "Find & Remove Traps" programmed sequence, with a (small) chance of the trap (if any) being triggered. If the trap is successfully removed, the container may still be locked but will no longer be trapped. If the token represents any other type of character, container, creature or object a standard "Find/Remove Traps" sequence will ensue, resulting in the party (and the GM) being alerted to the success or otherwise of the outcome. In either case, the default approach to the Find Traps roll is that the GM is asked to make it - being presented with a drop-down list of options that includes (a) just rolling 1d100 against the character\'s chance, (b) forcing a successful roll (e.g. if they were meant to find it), and (c) forcing a failure to find a trap (e.g. if there is no trap to be found). The GM can use the !magic --config command to change this action so that the player always rolls to Find Traps, though this might result in an indication for a (non-Drag & Drop) container indicating success for a container that is not trapped! Takes a mandatory token ID of the character\'s token, mandatory token ID of the token to search and pick up items from, mandatory token ID of the token to put picked up items into. This command can be used to pick the pockets of an NPC or even another Player Character, as well as to loot magic item and treasure containers such as Chests and dead bodies. It can also be used for putting stuff away, storing items from the character\'s Magic Item Bag into a container, for instance if the MI Bag is getting too full (it is limited to the number of items specified via the --gm-edit-mi menu, though similar items can be stacked). The effect of this command depends on the type of the container: intelligent characters, NPCs and creatures (even if only with animal intelligence of 1) are considered sentient unless they are dead (Hit Points equal to or less than zero). The trapped container status is set by any Drag & Drop container, or via the GM\'s [Add Items] button or !magic --gm-edit-mi command. All other containers (tokens with character sheets) are considered inanimate and untrapped. Any status can also be overridden if so desired by resetting the container type using the Add Items dialog to set the type to a different value - a sentient creature can be forced to be inanimate (i.e. does not need a pick pockets roll), and visa-versa (e.g. luggage Terry Pratchett style). Note: Some items are not stackable - they are single items with charges such as a wand or rod, or a spell-storing item which must retain its uniqueness so the spells remain associated. However, it is also the case that non-stackable items like these need to have unique names in the container to retain their unique identity. Thus, when a second copy of a non-stackable item is picked up or put away into a container that already contains another item with the same name, the player will be asked to provide a new unique name for the item (which cannot be the same as any other magic item, even those not in the container - sorry, you can\'t turn that ring of protection+1 into a ring of wishes!). Once the item is stored with this new name, it will work in all respects like the item it is, just with a different name. Takes a mandatory token ID for the Player\'s character, a mandatory token ID for the token to pick items from, a mandatory token ID for the token to put items in to, and an optional argument specifying whether to use a long or a short menu. This command displays a menu from which items on the character sheet associated with the Pick token can be selected to put in the character sheet associated with the Put token. The Player character\'s token can be either the Put token (if picking up items from a container) or the Pick token (if storing items from their sheet into the container). The other token can be another Player Character (useful for one character giving a magic item to another character) or any other selectable token with a character sheet. No traps or sentient being checks are made by this command - this allows the DM to allow Players to bypass the searching functionality when looting a container or storing items in it. Note: the Player\'s Magic Item menu (accessed via the --mimenu command) does not have an option to loot without searching. There are two forms of this menu - the Long form displays all items in the container as individual buttons for the Player to select from, and a single button to store the item: this is generally OK when looting containers with not much in them. The Short form of the menu shows only two buttons: one button which, when clicked, brings up a pick list of all the items in the Pick container, and another button to store the item in the Put container: this is generally best for when a character is storing something from their character sheet items into a chest or other container, or giving an MI to another character, as a character\'s sheet often has many items in it which can make a Long menu very long. Each type of menu has a button on it to switch to the other type of menu without re-issuing the command. If not specified in the command, the type of menu the Player last used in this campaign is remembered and used by the system. These functions use Roll20 Dynamic Lighting to provide a token with a light source. If your campaign does not use Dynamic Lighting, they will not function. They can also be accessed through the menu displayed by the AttackMaster API !attk --other-menu command. Takes an optional token ID as an argument. If token ID is not specified, uses the selected token. This command brings up a menu showing a selection of various light sources that a character can use. Selecting one will change the Roll20 Dynamic Lighting values on the Token identified to provide this lighting effect. These are: The menu shows [ON] and [OFF] buttons for each type. Only one type can be ON for each Token: selecting an ON button for any light source turns OFF the others for that Token. Turning the current light source off will turn off all lighting effects on the identified token. Takes a mandatory token ID, and a mandatory type of light source. This command sets the light source type that the identified token is using, and changes the Roll20 Dynamic Lighting settings of the token to the relevant value shown under section 5.1, or turn off all lighting effects for the selected token if NONE is specified. This command does not take any arguments. It displays the mandatory and optional arguments, and a brief description of each command. This command takes an optional parameter stating who to send the message to, which defaults to depending on who owns the character represented by the token, an optional token_id which defaults to a selected token, a title for the message which can be an empty string, the message to display, and an optional API command string to be sent at the same time that the message is sent (can use standard & extended escape characters). The "who" parameter can be one of: This command takes an optional parameter stating who to send the output to, which defaults to depending on who owns the character represented by the token, an optional token_id which defaults to a selected token, the mandatory name or ID of a database or character sheet, the mandatory name of a database item or character sheet ability macro, two optional dice roll results (or Roll20 in-line roll specifications), and an optional token_id of a target token. This command can be used to extract database items wherever they are currently stored and display them to a player, character, the GM, or publicly - the options are the same as for the --message command above. If the "database" parameter has a name that includes "-DB" the db_item is read from the databases. This includes extracting database items from the databases held in code by the APIs and displaying them, as if they were ability macros in a Character Sheet, or from a Character Sheet database if the db_item exists there. If the "database" parameter is not for a database, but instead represents the name of a Character Sheet that does not include "-DB", the command will look for and display an ability macro with name "db_item" from that character sheet. Whether from the databases or from a character sheet ability macro, the item retrieved can optionally have up to two dice roll values and/or a target token ID passed to it. The dice roll parameters will replace the place-holders %%diceRoll1%% and %%diceRoll2%% in the retrieved item. The parameters can be plain numbers, roll queries (which will be resolved if the command is passed via the chat window or an API button), or in-line roll calculations in Roll20 format (using [[...]]). The target token_id will replace anywhere the @{target|...|token_id} syntax is used. This command takes an optional token_id. If not specified, the command will act on the character sheets represented by all currently selected tokens. This command tidies up the character sheet, removing Spell and Magic Item attribute and ability objects that are no longer for items held, and for spells no longer in any spell book. Attack ability objects will also all be removed. All of these will be recreated as and when these items, spells or attacks are again picked up, added to spell books, or used for attacks. This simplifies and speeds up the system, removing redundant processing and memory usage. Note: this command is automatically run whenever the DM moves the "Player Ribbon" to a new map page, for every token on that map page that represents a character sheet, and also whenever a character token is dragged onto the active Player page. This continually tidies the system while not imposing a heavy overhead on processing. Takes two optional arguments, the first a switchable flag name, and the second TRUE or FALSE. Allows configuration of several API behaviors. If no arguments given, displays menu for DM to select configuration. Parameters have the following effects: Takes an optional database name or part of a database name: if a partial name, checks all character sheets with the provided text in their name that also have \'-db\' as part of their name. If omitted, checks all character sheets with \'-db\' in the name. Not case sensitive. Can only be used by the GM. This command finds all databases that match the name or partial name provided (not case sensitive), and checks them for completeness and integrity. The command does not alter any ability macros, but ensures that the casting time (\'ct-\') attributes are correctly created, that the item lists are sorted and complete, and that any item-specific power & spell specifications are correctly built and saved. This command is very useful to run after creating/adding new items as ability macros to the databases (see specific database documentation). It does not check if the ability macro definition itself is valid, but if it is then it ensures all other aspects of the database consistently reflect the new ability(s). Takes an optional database name or part of a database name: if a partial name, extracts all character sheets with the provided text in their name that also have \'-db\' as part of their name. If omitted, checks all character sheets with \'-db\' in the name. Not case sensitive. Can only be used by the GM. Extracts a named database or all provided databases from the loaded RPGMaster Library, and builds the database(s) in a Character Sheet format: see the Database specific help handouts for further details of this format. This allows editing of the standard items in the databases, adding additional items to the databases, or for items to be copied into the GM\'s own databases. Unlike with previous versions of the Master Series APIs, these extracted databases will not be overwritten automatically by the system. However: using extracted databases will slow the system down - the use of the internal API databases held in memory is much faster. The best use for these extracts is to examine how various items have been programmed so that the GM can create variations of the standard items in their own databases by copying and making small alterations to the definitions, and then the extracted databases can be deleted. Important: Once a Character Sheet database is changed or deleted, run the !magic --check-db command against any database (especially a changed one) to prompt the APIs to re-index the objects in all databases. Either form performs a handshake with another API, whose call (without the \'!\') is specified as from in the command parameters (the response is always an -hsr command). The command calls the from API command responding with its own command to confirm that this API is loaded and running: e.g. Optionally, a command query can be made to see if the command is supported by MagicMaster if the command string parameter is added, where command is the MagicMaster command (the \'--\' text without the \'--\'). This will respond with a true/false response: e.g. Takes one mandatory argument which should be ON or OFF. The command turns on a verbose diagnostic mode for the API which will trace what commands are being processed, including internal commands, what attributes are being set and changed, and more detail about any errors that are occurring. The command can be used by the DM or any Player - so the DM or a technical advisor can play as a Player and see the debugging messages.MagicMaster API v'+version+'
'
+ +'and later
'
+ +'
'
+ +'New: in this Help Handout
'
+ +'
'
+ +'Syntax of MagicMaster calls
'
+ +'!magic --help
'
+ +'!magic --mi-power token_id|power_name|mi_name|[casting-level]
'
+ +'!magic --cast-spell MI|[token_id]||[casting_name]
'
+ +'!magic --spellmenu [token_id]|[MU/PR/POWER] --mimenu [token_id]
'
+ +'
'
+ +'[General API Help]'
+ +'How MagicMaster works
'
+ +'Race, Class, Item, Spell and Power databases
'
+ +'Races & Classes
'
+ +'Spells and Powers
'
+ +'Types of Item Provided
'
+ +'Adding Items to the Character
'
+ +'Adding Spells & Powers to the Character
'
+ +'Using Items
'
+ +'Casting spells and using powers
'
+ +'Dynamic lighting for tokens
'
+ +'DM tools
'
+ +'
'
+ +'Command Index
'
+ +'1.Spell and Power management
'
+ +'--spellmenu [token_id]|[MU/PR/POWER]
'
+ +'
'
+ +'--mem-spell (MU/PR/POWER)|[token_id]
'
+ +'--view-spell (MU/PR/POWER)|[token_id]
'
+ +'--cast-spell (MU/PR/POWER/MI)|[token_id]|[casting_level]|[casting_name]
'
+ +'--cast-again (MU/PR/POWER)|token_id|[spell_name]
'
+ +'--mem-all-powers token_id2.Magic Item management
'
+ +'--mimenu [token_id]
'
+ +'
'
+ +'--edit-mi [token_id]
'
+ +'--view-mi [token_id]
'
+ +'--use-mi [token_id]
'
+ +'--add-mi [token_id]|(mi-to-replace/row#)|mi-to-add|quantity|hand#|[NOCURSE]|[SILENT]
'
+ +'--mi-charges token_id|value|[mi_name]|[maximum]|[charge_override]
'
+ +'--mi-power token_id|power_name|mi_name|[casting-level]
'
+ +'--store-spells token_id|mi-name
'
+ +'--mem-spell (MI-MU/MI-PR)[-ANY/-ADD/-CHANGE]|[token_id]|[mi-name]
'
+ +'--view-spell (MI/MI-MU/MI-PR/MI-POWER)|[token_id]|[mi-name]
'
+ +'--cast-spell (MI/MI-POWER)|[token_id]|[casting_level]|[casting_name]|[CHARGED]|[mi-name]
'
+ +'--learn-spell [token_id]|spell_name3.Spell, power & magic item effects and resting
'
+ +'!rounds --target CASTER|caster_token_id|caster_token_id|spell_name|duration|increment|[msg]|[marker]
'
+ +'
'
+ +'!rounds --target (SINGLE/AREA)|caster_token_id|target_token_id|spell_name|duration|increment|[msg]|[marker]
'
+ +'--touch token_id|effect-name|duration|per-round|message|marker
'
+ +'--level-change [token_id]|[# of levels]|[HP change]|[class]
'
+ +'--change-attr [token_id]|change|[field]|[SILENT]
'
+ +'--rest [token_id]|[SHORT/LONG]|[MU/PR/MU-PR/POWER/MI/MI-POWER]|[timescale]
'
+ +'--mi-rest [token_id]|mi_name|[charges]|[power_name]
'
+ +'--query-qty [token_id]|(MU/PR/POWER/MI/MIPOWER)|item|[SILENT]4.Treasure & Item container management
'
+ +'--gm-edit-mi [token_id]
'
+ +'
'
+ +'--find-traps token_id|pick_id|put_id
'
+ +'--search token_id|pick_id|put_id
'
+ +'--pickorput token_id|pick_id|put_id|[SHORT/LONG]5.Light source management
'
+ +'--lightsources [token_id]
'
+ +'
'
+ +'--light token_id|(NONE/WEAPON/TORCH/HOODED/CONTLIGHT/BULLSEYE/BEACON)6.Other commands
'
+ +'--help
'
+ +'
'
+ +'--message [who|][token_id]|title|message|[command]
'
+ +'--display-ability [who|][token_id]|database|db_item|[dice_roll1]|[dice_roll2]|[target_id]
'
+ +'--tidy [token_id]|[SILENT]
'
+ +'--config [FANCY-MENUS/SPECIALIST-RULES/SPELL-NUM/ALL-SPELLS/ALL-POWERS/CUSTOM-SPELLS/AUTO-HIDE/ALPHA-LISTS/GM-ROLLS] | [TRUE/FALSE]
'
+ +'--check-db [db-name]
'
+ +'--extract-db [db-name]
'
+ +'--handshake from | [cmd]
'
+ +'--hsq from | [cmd]
'
+ +'--hsr from | [cmd] | [TRUE/FALSE]
'
+ +'--debug (ON/OFF)
'
+ +'1. Spell management
'
+ +'1.1 Display a menu to do actions with spells
'
+ +'--spellmenu [token_id]|[MU/PR/POWER]
'
+ +''
+ +'
'
+ +' '
+ +' MU: displays buttons for Magic User/Wizard spells for casting, resting (short or long), memorising spells from the character\'s spell book, or viewing the memorised spells. '
+ +' PR: displays buttons for Priest spells for casting, resting (short or long), memorising spells from the character\'s spell book, or viewing the memorised spells. '
+ +' POWER: displays buttons for using powers, doing a long rest, changing/resetting powers from the character\'s granted powers, or viewing the granted powers. '
+ +'None of the above: the system will check the class(es) of the character and display the appropriate menu, or if a multi-class character including both a Wizard and a Priest, ask if the player wants to display Magic User or Priest menus. 1.2 Display a menu to memorise spells from the Character\'s spell book
'
+ +'--mem-spell (MU/PR/POWER/MI-MU/MI-PR)|[token_id]|[mi-name]
'
+ +'1.3 View the memorised spells or granted powers
'
+ +'--view-spell (MU/PR/POWER/MI-MU/MI-PR/MI-POWER)|[token_id]|[mi-name]
'
+ +'1.4 Cast a memorised spell or use a granted power
'
+ +'--cast-spell (MU/PR/POWER/MI/MI-POWER)|[token_id]|[casting_level]|[casting_name]|[CHARGED]|[mi-name]
'
+ +'1.5 Cast the last used spell or power again
'
+ +'--cast-again (MU/PR/POWER)|token_id|[spell_name]
'
+ +'1.6 Memorise All Valid Powers
'
+ +'--mem-all-powers token_id
'
+ +'
'
+ +'2. Magic Item management
'
+ +'2.1 Display a menu of possible Magic Item actions
'
+ +'--mimenu [token_id]
'
+ +'2.2 Edit a Magic Item bag
'
+ +'--edit-mi [token_id]|[MARTIAL/MAGICAL/ALL]
'
+ +'2.3 View a character\'s Magic Item Bag
'
+ +'--view-mi [token_id]
'
+ +'2.4 Use a Magic Item from the bag
'
+ +'--use-mi [token_id]
'
+ +'2.5 Add an Item to a Character / Container
'
+ +'--add-mi [token_id]|(mi-to-replace/row#)|mi-to-add|[quantity]|[hand#]|[NOCURSE]|[SILENT]
'
+ +'2.6 Add, set or deduct Magic Item charges
'
+ +'--mi-charges token_id|[+/-/0]value|[mi_name]|[maximum]|[charge_override]
'
+ +'2.7 Use a Magic Item power
'
+ +'--mi-power token_id|[type-]power_name|mi_name|[casting-level]
'
+ +'2.8 Add spells to a spell-storing Magic Item
'
+ +'--store-spells token_id|mi_name
'
+ +'!magic --cast-spell MI|
or !magic --cast-spell MI-POWER|
, (or either of their --view-spell
equivalents) generally as part of an API button, or spells/powers cannot be stored. If the command is for MI, the dialog defaults to Level 1 Wizard spells, and has buttons to switch level and to Priest spells. If the command is for MI-POWER, the dialog allows powers to be stored, but Wizard and Priest spells can also be stored as powers, and the dialog will prompt for a number of uses per day for each.2.9 Restore spells in a spell-storing Magic Item
'
+ +'--mem-spell (MI-MU/MI-PR)[-ADD/-ANY/-CHANGE]|[token_id]|[mi-name]
'
+ +'2.10 Casting a spell from a spell-storing magic item
'
+ +'--cast-spell (MI/MI-POWER)|[token_id]|[casting_level]|[casting_name]|[CHARGED]|[mi-name]
'
+ +'2.11 Learning a spell in a spellbook (or other MI)
'
+ +'--learn-spell [token_id]|spell_name
'
+ +'
'
+ +'
'
+ +'3.Spell, power & magic item effects and resting
'
+ +'3.1 Target spell effects on a token (with RoundMaster API only)
'
+ +'!rounds --target CASTER|caster_token_id|[caster_token_id|]spell_name|duration|[+/-]increment|[msg]|[marker]
'
+ +'
'
+ +'!rounds --target (SINGLE/AREA)|caster_token_id|target_token_id|spell_name|duration|increment|[msg]|[marker]'
+ +'
'
+ +' '
+ +' CASTER will just take one Token ID and apply the marker to that token. '
+ +' SINGLE will take both the Token ID of the caster, and the Token ID of a target of the spell/power/MI. The marker will be applied to that of the target. '
+ +'AREA will take the Token ID of the caster, and one Token ID of the first token in the area of effect. As each token is specified the command will ask the Player to select subsequent tokens in the area of effect. Once all relevant tokens have been selected, just ignore the next prompt. 3.2 Cast a spell that requires a "touch" attack roll
'
+ +'--touch token_id|effect-name|duration|per-round|[message]|[marker]
'
+ +'3.3 Change the Experience Level
'
+ +'--level-change [token_id]|[# of levels]|[HP change]|[class]
'
+ +'3.4 Change an Attribute Value
'
+ +'--change-attr [token_id]|change|[STRENGTH/DEXTERITY/CONSTITUTION/INTELLIGENCE/WISDOM/CHARISMA]
'
+ +'3.5 Perform Short or Long Rests
'
+ +'--rest [token_id]|[SHORT/LONG]|[MU/PR/MU-PR/POWER/MI/MI-POWER]|[timescale]
'
+ +'3.6 Perform a Single Item Rest
'
+ +'--mi-rest [token_id]|mi_name|[charges]|[power_name]
'
+ +'3.7 New Query the Number of Charges
'
+ +'--query-qty [token_id]|(MU/PR/MU-PR/POWER/MI/MI-POWER)|item|[SILENT]
'
+ +'
'
+ +'4.Treasure & Item container management
'
+ +'4.1 DM/GM version of Magic Item management
'
+ +'--gm-edit-mi [token_id]
'
+ +''
+ +'
'
+ +' '
+ +' Store item: Select a magic item from the databases and store it in a slot - this is the same as the Player version. '
+ +' Hide item as different item: The magic item already in the selected bag slot is given the displayed name of the magic item selected from the databases - the Player will only see the Magic Item selected (Displayed Name), and not the hidden actual name. The MI will behave exactly like the selected, displayed item until the DM reverts the item to the hidden version using the [Reset Single MI] button. This is generally used for items in containers, especially Cursed items, so that the real nature of the item is hidden until the character uses it or the DM wants them to. Once an item has been marked as hidden, the DM can see the name it will be displayed to the palyer as by selecting that slot - the displayed name will appear on the menu, and other options for hidden items will become selectable. '
+ +' Rename MI: Allows the DM to change the actual and displayed name of an item. This will create a unique item (existing item names cannot be used) stored on the character\'s/container\'s Character Sheet which will work in exactly the same way as the original item. This can be used to resolve duplicate magic items, such as two rings of spell storing can be given different names. This is different from hiding - the actual name of the item is changed. '
+ +' Remove MI: Blanks the selected Bag slot, removing all details, both displayed & actual. '
+ +' Change MI Type: This allows the type of the item in the selected Bag slot to be changed. It can be one of the following - Charged, Discharging, Uncharged, Recharging, Rechargeable, Self-charging, Absorbing, Cursed, Cursed-Charged, Cursed-Self-charging, Cursed-Recharging, Cursed-Absorbing (cursed rechargeable items behave in exactly the same way as Cursed-Charged items). Cursed versions of items cannot be removed from the character\'s MI Bag, given away, stored or deleted by the Player, even when all their charges are depleted. Otherwise, they act in the same way as other magic items. Charged, Discharging, and Rechargeable items disappear if they reach zero charges, unless preceeded by \'perm-\'. Charged, Uncharged and Cursed items can be divided when picked up by Searching or Storing, other types cannot. '
+ +' Change Displayed Charges: Changes the number of displayed/current charges for the item in the selected Bag slot. This can be used to set the quantity of Uncharged items, or the current charges of other types. It also allows charged items in containers to be stored as a single item, for instance single Wands (current/displayed qty = 1) with multiple charges (max qty = n): when picked up the current qty is always set to the actual value - see the --pickorput command below. '
+ +' Change Actual Charges: Setting this allows the actual quantity of Uncharged items in containers to be hidden, or the maximum number of charges to be set for other types. When the item is picked up from a container, the actual number of charges will be set as the current value. '
+ +' Store Spells/Powers in MI Only enabled for items that can store & cast spells or powers: the item definition must have a call to !magic --cast-spell MI
for spell storing, or !magic --cast-spell MI-POWER
for powers, associated with an API button. If this is the case, this option opens a menu to select Wizard or Priest spells, or powers as appropriate. A blank Ring-of-Spell-Storing and a blank Scroll-of-Spells are both included in the databases, allowing GMs to build their own unique items and then give them a unique new name using the Rename function described above. '
+ +' Change Item Cost: Items can have a cost in GP (fractions allowed which get converted to SP & CP). When an item is picked up from a container, the cost will be multiplied by the quantity picked up and the Player will be asked if they want the character to pay the cost. If confirmed, the cost will be deducted from the money values on the character sheet. 0 and negative values are allowed. This supports merchants and shops to be created in the campaign. '
+ +' Reset Qty to Max: Allows the DM to reset the quantity of the selected Bag slot to the actual (max) values. '
+ +' Reveal Now: Only available when a hidden item is selected. Reveals the item, setting the displayed name to the actual name, which will function as the revealed item from that point on. '
+ +' Reveal MI Allows selection of when a hidden item is revealed: MANUALLY by DM (the default) using the Reveal Now button; on VIEWING the item; or on USING the item. From the point the item is revealed onwards, the item will behave as the revealed item. '
+ +' Edit Treasure: Mainly for use on Magic Item containers, such as Treasure Chests, but also useful for NPCs and Monsters. Allows the DM to add text only treasure descriptions to the container. The displayed menu allows [Add], [Edit], and [Delete] functions to manage multiple lines/rows of treasure description. '
+ +' Container Type: Sets the type of the Magic Item container or Bag. Available choices are: Untrapped container, Trapped container, Force to be an Inanimate Container, Force to be a Sentient creature. If searched, Inanimate objects can be looted without penalty; Sentient beings require a Pick Pockets check; Trapped containers call a Trap ability macro on the container\'s character sheet to determine the effect. See the --search command below. '
+ +' Container Size: Sets the maximum number of items that can be stored in the selected Character\'s/containers bag. The default is 18 items, though identical items can be stacked. '
+ +'Show As: Sets what level of item description a Player sees when looting a container. Either "Show as Item Types" (e.g. potion, scroll, melee weapon, etc), or "Show as Item Names" (default) which shows the display names of the items. Once picked up from the container, will always show their display names. 4.2 Check a token for traps
'
+ +'--find-traps token_id|check_id|searcher_id
'
+ +'4.3 Searching/Storing tokens with Items and Treasure
'
+ +'--search token_id|pick_id|put_id
'
+ +''
+ +'
'
+ +' '
+ +' Inanimate container: a message is shown to the Player saying the container is empty or the items in the container are displayed, and the character doing the search (associated with the put_id token ID) can pick them up and store them in their own Magic Item Bag or, if storing, put items from their character into the container. '
+ +' Sentient Creature: if searching, a Pick Pockets check is undertaken - the Player is asked to roll a dice and enter the result (or Roll20 can do it for them), which is compared to the Pick Pockets score on their character sheet. If successful, a message is displayed in the same way as an Inanimate object. If unsuccessful, a further check is made against the level of the being targeted to see if they notice, and the DM is informed either way. The DM can then take whatever action they believe is needed. Of course, you can always freely give/store items to another creature. '
+ +'Trapped container: Traps can be as simple or as complex as the DM desires. Traps may be nothing more than a lock that requires a Player to say they have a specific key, or a combination that has to be chosen from a list, and nothing happens if it is wrong other than the items in the container not being displayed. Or setting a trap off can have damaging consequences for the character searching or the whole party. It can just be a /whisper gm message to let the DM know that the trapped container has been searched. Searching a trapped container with this command calls an ability macro called "Trap-@{container_name|version}" on the container\'s character sheet: if this does not exist, it calls an ability macro just called "Trap". The first version allows the Trap macro to change the behaviour on subsequent calls to the Trap functionality (if using the ChatSetAttr API to change the version attribute), for instance to allow the chest to open normally once the trap has been defused or expended. This functionality requires confidence in Roll20 macro programming.
Important Note: all Character Sheets representing Trapped containers must have their \'ControlledBy\' value (found under the [Edit] button at the top right of each sheet) set to \'All Players\'. Otherwise, Players will not be able to run the macros contained in them that operate the trap!4.4 Looting and storing without searching a container
'
+ +'--pickorput token_id|pick_id|put_id|[SHORT/LONG]
'
+ +'
'
+ +'5.Light source management
'
+ +'5.1 Show a menu of Light Sources to select from
'
+ +'--lightsources [token_id]
'
+ +''
+ +'
'
+ +'5.2 Set a lightsource for a token
'
+ +'--light token_id|(NONE/WEAPON/TORCH/HOODED/CONTLIGHT/BULLSEYE/BEACON)
'
+ +'
'
+ +'6.Other commands
'
+ +'6.1 Display help on these commands
'
+ +'--help
'
+ +'6.2 Display a formatted message in chat
'
+ +'--message [who|][token_id]|title|message|[command]
'
+ +''
+ +'
'
+ +' '
+ +' gm Send only to the GM '
+ +' whisper Send only to the players that control the character represented by the token '
+ +' w Short for "whisper" and does the same '
+ +' public Send to all players and the GM '
+ +' standard Check which players/GMs control the character represented by the token. If the GM controls, or no-one, or the controlling player is not on-line, or the token does not represent a character, send to the GM; otherise make public. '
+ +'Anything else Same as Standard 6.3 Display a database item or Character Sheet ability
'
+ +'--display-ability [who|][token_id]|database|db_item|[dice_roll1]|[dice_roll2]|[target_id]
'
+ +'6.4 Tidy one or more character sheets
'
+ +'--tidy [token_id]
'
+ +'6.5 Configure API behavior
'
+ +'--config [FANCY-MENUS/SPECIALIST-RULES/SPELL-NUM/ALL-SPELLS/ALL-POWERS/CUSTOM-SPELLS/AUTO-HIDE/ALPHA-LISTS/GM-ROLLS] | [TRUE/FALSE]
'
+ +''
+ +'
'
+ +' '
+ +' Flag True False '
+ +' FANCY-MENUS Use chat menus with a textured background Use chat menus with a plain background '
+ +' SPECIALIST-RULES Only Specialist Wizards specified in the PHB get an extra spell per spell level Any non-Standard Wizard gets an extra spell per spell level '
+ +' SPELL-NUM Spellcaster spells per level restricted to PHB rules Spellcaster spells per level alterable using Misc Spells button '
+ +' ALL-SPELLS Spellcaster spell schools are unrestricted Spellcaster spell schools are restricted by class rules '
+ +' ALL-POWERS Class powers not restricted by level Class powers are restricted by level as per spec '
+ +' CUSTOM-SPELLS No distributed custom spells/items allowed (but CS DB allowed) All custom spells and items allowed '
+ +' AUTO-HIDE Items defined to be hideable will be automatically hidden when added to containers Hideable items must be hidden manually if desired '
+ +' ALPHA-LISTS Long lists will automatically be split into alpha lists Whole long lists will be displayed for selection '
+ +'GM-ROLLS GM is asked to roll thievish skill-based chances when using Find Traps Player rolls skill-based chances for Find Traps 6.6 Check database completeness & integrity (GM only)
'
+ +'--check-db [ db-name ]
'
+ +'6.7 Extract database for Editing
'
+ +'--extract-db [db-name]
'
+ +'6.8 Handshake with other APIs
'
+ +'-hsq from|[command]
'
+ +'
'
+ +'-handshake from|[command]
'
+ +'
'
+ +'6.9 Switch on or off Debug mode
'
+ +'--debug (ON/OFF)
'
+ +'
'
+ +''
+ + '
'
+ + ''
+ + ' '
+ + ''
+ + '<%= confirm_button %>'
+ + ' '
+ + ''
+ + '<%= reject_button %>'
+ + ' '
+ + '
';
+
+ if (selectedSpell) {
+ let renamed = !abilityLookup( magicDB, spellToDisplay ),
+ changedSpell = renamed ? 'Display-'+spellToDisplay : spellToDisplay;
+ spell = getAbility( magicDB, spellToDisplay, charCS );
+ if (!state.MagicMaster.viewActions && spell.obj) spell.obj = greyOutButtons( tokenID, charCS, spell.obj, (renamed ? changedSpell : '') );
+ content += '...Optionally [Review '+spellToDisplay+'](!magic --button '+reviewCmd+'|'+tokenID+'|'+level+'|'+spellRow+'|'+spellCol+'|'+spellToMemorise
+ + '
'+(spell.api ? '' : sendToWho(charCS,senderId,false,true))+'%{'+spell.dB+'|'+changedSpell.hyphened()+'})}}';
+ } else {
+ content += '...Optionally Review the '+magicWord+'}}';
+ }
+ content += '{{desc1=2. Choose slot to use\n'
+ + (isPower ? '' : (makeEditNumberOfSpells(args,magicType,levelSpells[level].spells)))+'\n';
+
+ // build the Spell list
+
+ r = 0;
+ while (levelSpells[level].spells > 0) {
+ c = levelSpells[level].base;
+ for (w = 1; (w <= fields.SpellsCols) && (levelSpells[level].spells > 0); w++) {
+ if (!spellTables[w]) {
+ spellTables[w] = getTable( charCS, fieldGroups.SPELLS, c );
+ }
+ selected = (r == spellRow && c == spellCol);
+ spellName = spellTables[w].tableLookup( fields.Spells_name, r, false );
+ if (_.isUndefined(spellName)) {
+ spellTables[w].addTableRow( r );
+ spellName = '-';
+ }
+ spellValue = parseInt((spellTables[w].tableLookup( fields.Spells_castValue, r )),10);
+ content += (selected ? ('') : ('['+(spellValue == 0 ? ('') : '')));
+ if (isPower && spellName != '-') {
+ content += spellValue + ' ';
+ }
+ content += spellName;
+ content += (selected || spellValue == 0 ? '' : '');
+ content += (!selected ? ('](!magic --button ' + editCmd + '|' + tokenID + '|' + level + '|' + r + '|' + c + '|' + spellToMemorise + ')') : '');
+ c++;
+ levelSpells[level].spells--;
+ }
+ r++;
+ spellTables = [];
+ }
+
+ if (level < levelLimit) {
+ nextLevel = (levelSpells[(level+1)].spells>0) ? (level+1) : 1;
+ } else {
+ nextLevel = 1;
+ }
+
+ if (selectedSlot) {
+ slotSpell = attrLookup( charCS, fields.Spells_name, fields.Spells_table, spellRow, spellCol ) || '';
+ }
+ content += '}}{{desc2=...Then\n'
+ + '3. '+(selectedBoth ? '[' : '')
+ + 'Memorise '+(selectedSpell ? spellToDisplay : ' the '+magicWord )
+ + (!selectedBoth ? '' : ('](!magic --button '+memCmd+'|'+tokenID+'|'+level+'|'+spellRow+'|'+spellCol+'|'+spellToMemorise+'|'+noToMemorise+')'))+'\n'
+ + (isPower ? (!isMI ? 'or [Memorise all valid Powers](!magic --button '+BT.MEMALL_POWERS+'|'+tokenID+'|1|-1|-1||)\n' : '') : (singleLevel ? '' : '4. When ready [Go to Level '+nextLevel+'](!magic --button '+editCmd+'|'+tokenID+'|'+nextLevel+'|-1|-1|)\n'))
+ + 'Or just do something else anytime\n\n'
+
+ + 'Or ' + (selectedSlot ? '[' : (''))
+ + 'Remove '+slotSpell
+ + (!selectedSlot ? ' the' : ('](!magic --button '+memCmd+'|'+tokenID+'|'+level+'|'+spellRow+'|'+spellCol+'|-|0)') )+' '+magicWord+'}}';
+
+ sendResponse( charCS, content, senderId, flags.feedbackName, flags.feedbackImg, tokenID );
+ return;
+ }
+
+ /*
+ * Make a menu to store spells in a Magic Item from the caster's
+ * own memorised spells.
+ */
+
+ var makeStoreMIspell = function(args,senderId,msg = '') {
+
+ var command = (args[0] || '').toUpperCase(),
+ tokenID = args[1],
+ curToken = getObj('graphic',tokenID),
+ charCS = getCharacter(tokenID);
+
+ if (!charCS) {
+ sendDebug('makeStoreMIspell: invalid tokenID passed');
+ sendError('Internal MagicMaster error');
+ return;
+ }
+
+ var isMU = command.includes('MU'),
+ isMI = command.includes('MI'),
+ isAdd = command.includes('ADD'),
+ isChange = command.includes('CHANGE'),
+ spellButton = args[(isMI ? 5 : 2)],
+ spellRow = args[(isMI ? 6 : 3)],
+ spellCol = args[(isMI ? 7 : 4)],
+ MIbutton = args[(isMI ? 2 : 5)],
+ MIrow = args[(isMI ? 3 : 6)],
+ MIcol = args[(isMI ? 4 : 7)],
+ isAny = command.includes('ANY') || (isAdd && MIbutton < 0),
+ item = attrLookup( charCS, fields.ItemChosen ) || '-',
+ row = attrLookup( charCS, fields.ItemRowRef ) || '',
+ itemObj = abilityLookup( fields.MagicItemDB, item, charCS ),
+ wisLevel = casterLevel( charCS, (isMU ? 'MU' : 'PR') ),
+ extra = isAdd ? '_ADD' : (isAny ? '_ANY' : ''),
+ spellName = 'spell',
+ MIspellName = '',
+ oldVer = 2.1 > csVer(charCS),
+ levelLimit = false,
+ col,
+ tokenName = curToken.get('name');
+
+ if (!itemObj.obj) {
+ sendError('Item '+item+' not found. Unable to store spells in this item.');
+ return;
+ } else {
+ let itemData = parseData((itemObj.data()[0][0] || {}),reSpellSpecs,true,charCS,item,row);
+ let storeSpells = (itemData.store || 'store').toLowerCase();
+ isAdd = isAdd || storeSpells === 'add';
+ isChange = isChange || storeSpells === 'change';
+ isAny = isAny || (storeSpells === 'any' || (isAdd && MIbutton < 0));
+ levelLimit = itemData.lvlimit == 1;
+ };
+
+ var memSpells, storedSpells, storedLevels, itemQty = 99;
+
+ [storedSpells,storedLevels] = makeSpellList( senderId, tokenID, (isMU ? BT.MU_MI_SLOT : BT.PR_MI_SLOT)+extra, MIbutton, !isAny, false, ('|'+spellButton+'|'+spellRow+'|'+spellCol) );
+ if (levelLimit) {
+ let Items = getTable( charCS, fieldGroups.MI ),
+ itemRow = Items.tableFind( fields.Items_name, item );
+ if (itemRow) itemQty = parseInt(Items.tableLookup( fields.Items_trueQty, itemRow )) || 99;
+ }
+ [memSpells] = makeSpellList( senderId, tokenID, (isMU ? BT.MU_TO_STORE : BT.PR_TO_STORE)+extra, spellButton, true, false, ('|'+MIbutton+'|'+MIrow+'|'+MIcol), (itemQty - storedLevels) );
+
+ var content = '&{template:'+fields.menuTemplate+'}{{name=Store Spell in '+tokenName+'\'s Magic Items}}'
+ + '{{subtitle=Storing ' + (isMU ? 'MU' : 'PR') + ' spells}}'
+ + '{{desc=**1.Choose a spell to store**\n'+memSpells+'}}'
+ + '{{desc1=**2.'+(isAny ? 'Optionally c' : 'C')+'hoose where to store it**\n'+(storedSpells || 'No spells currently stored')+'}}';
+
+ if (spellButton >= 0) {
+ spellName = attrLookup( charCS, fields.Spells_name, fields.Spells_table, spellRow, spellCol ) || '-';
+ }
+ if (MIbutton >= 0) {
+ MIspellName = attrLookup( charCS, (oldVer ? fields.Spells_macro : fields.Spells_msg), fields.Spells_table, MIrow, MIcol ) || '-';
+ if ((isAdd || isAny) && MIspellName === '-') MIbutton = -1;
+ }
+ var canStore = isAny || isChange || (isAdd && MIspellName === '-') || (spellName.dbName() == MIspellName.dbName());
+
+ content += '{{desc2=3.Once both spell and '+(isAny ? 'optionally ' : '')+'slot selected\n'
+ + ((canStore && (spellButton >= 0) && (isAdd || isAny || MIbutton >= 0)) ? '[' : '')
+ + ((isAdd || isAny) && MIbutton < 0 ? 'Add ' : 'Store ')+spellName
+ + ((canStore && (spellButton >= 0)) ? (((isAdd || isAny) && MIbutton<0) ? ('](!magic --button ADD_TO_SPELLS|'+tokenID+'|'+item+'|'+command+'|1|STORE-MI-SPELL|'+spellName+'|'+wisLevel+'||'+MIspellName+'|'+spellRow+'|'+spellCol+')')
+ : ('](!magic --button '+(isMU ? BT.MISTORE_MUSPELL : BT.MISTORE_PRSPELL)+extra+'|'+tokenID+'|'+MIbutton+'|'+MIrow+'|'+MIcol+'|'+spellButton+'|'+spellRow+'|'+spellCol+')'))
+ : '')
+ + ((spellButton >= 0 && MIbutton >= 0 && !canStore) ? ' Spells don\'t match. Must be the same\n' : '')
+ + ' or switch to ['+(isMU ? 'Priest' : 'Wizard')+'](!magic --mem-spell MI-'+(isMU ? 'PR' : 'MU')+extra+'|'+tokenID+') spells'
+ + '}}';
+ if (msg.length) {
+ content += '{{='+msg+'}}';
+ }
+ sendResponse( charCS, content, senderId, flags.feedbackName, flags.feedbackImg, tokenID );
+ return;
+ }
+
+ /*
+ * Create a menu for a player to cast a spell
+ */
+
+ var makeCastSpellMenu = function( args, senderId, submitted = false ) {
+
+ var isMU = args[0].toUpperCase().includes('MU'),
+ isMI = args[0].toUpperCase().includes('MI'),
+ isPower = args[0].toUpperCase().includes('POWER'),
+ tokenID = args[1],
+ spellButton = args[2],
+ spellRow = args[3],
+ spellCol = args[4],
+ charged = (args[5] || '').toString().toLowerCase() == 'true',
+
+ curToken = getObj('graphic',tokenID),
+ charCS = getCharacter(tokenID),
+ magicDB,
+ magicWord = 'spell',
+ spell,
+ spellName = '',
+ content = '',
+ maxLevel = 13,
+ learn = false,
+ learnText = '',
+ tokenName,
+ selectCmd,
+ storeCmd;
+
+ if (!curToken || !charCS) {
+ sendDebug('makeCastSpellMenu: invalid tokenID passed');
+ sendError('Internal MagicMaster parameter error');
+ return content;
+ }
+
+ var miName = attrLookup( charCS, fields.ItemChosen ) || '',
+ itemRow = parseInt(attrLookup( charCS, fields.ItemRowRef ));
+ tokenName = curToken.get('name');
+ content = '&{template:'+fields.menuTemplate+'}{{name=';
+ if (!isPower) {content += 'What Spell is ' + tokenName + ' casting?}}{{subtitle=Casting '};
+
+ if (isPower) {
+ content += 'What Power is ' + tokenName + ' using?}}{{subtitle=Using Powers';
+ if (spellButton >= 0) {magicDB = attrLookup( charCS, fields.Spells_db, fields.Spells_table, spellRow, spellCol ) || fields.PowersDB;}
+ magicWord = 'power';
+ selectCmd = isMI ? BT.MI_POWER : BT.POWER;
+ storeCmd = isMI ? BT.CAST_MIPOWER : BT.USE_POWER;
+ } else if (isMI) {
+ content += 'MI stored spells';
+ if (spellButton >= 0) {magicDB = attrLookup( charCS, fields.Spells_db, fields.Spells_table, spellRow, spellCol ) || fields.MU_SpellsDB;}
+ selectCmd = charged ? BT.MI_SCROLL : BT.MI_SPELL;
+ storeCmd = charged ? BT.CAST_SCROLL : BT.CAST_MISPELL;
+ if (caster(charCS,'MU').clv > 0) {
+ let miObj = abilityLookup( fields.MagicItemDB, miName, charCS );
+ if (miObj.obj) learn = resolveData( miName, fields.MagicItemDB, reItemData, charCS, {learn:reSpellSpecs.learn}, itemRow ).parsed.learn == 1;
+ };
+ } else if (isMU) {
+ content += 'MU spells';
+ magicDB = fields.MU_SpellsDB;
+ selectCmd = BT.MU_SPELL;
+ storeCmd = BT.CAST_MUSPELL;
+ } else {
+ content += 'PR spells';
+ magicDB = fields.PR_SpellsDB;
+ selectCmd = BT.PR_SPELL;
+ storeCmd = BT.CAST_PRSPELL;
+ }
+
+ if (!isPower && !isMI && charged) {
+ if (miName) {
+ if (isNaN(itemRow)) {
+ let Items = getTableField( charCS, {}, fields.Items_table, fields.Items_name );
+ itemRow = parseInt(Items.tableFind( fields.Items_name, miName ));
+ };
+ if (!isNaN(itemRow)) {
+ maxLevel = parseInt(attrLookup( charCS, fields.Items_qty, fields.Items_table, itemRow )) || 0;
+ }
+ }
+ }
+
+ content += '}}{{desc=' + (makeSpellList( senderId, tokenID, selectCmd, spellButton, true, submitted, '|'+charged, maxLevel )[0]);
+
+ if (spellButton >= 0) {
+ spellName = attrLookup( charCS, fields.Spells_name, fields.Spells_table, spellRow, spellCol ) || '-';
+// log('makeCastSpellMenu: spellName = '+spellName);
+ if (spellName.replace(reIgnore,'').length) {
+ spell = getAbility( magicDB, spellName, charCS );
+ learnText = (learn ? '{{Learn=Try to [Learn this spell](!magic --learn-spell '+tokenID+'|'+spellName+')}}' : '');
+// log('makeCastSpellMenu: get spell '+spell.obj[1].name+', learnText = '+learnText);
+ } else {
+ spellButton = -1;
+// log('makeCastSpellMenu: rejected invalid spellName');
+ }
+ } else {
+ spellName = '';
+ }
+ content += '}}{{desc1=Select '+magicWord+' above, then '
+ + (((spellButton < 0) || submitted) ? '' : '[')
+ + 'Cast '+(spellName.length > 0 ? spellName : magicWord)
+ + (((spellButton < 0) || submitted) ? '' : '](!magic --button '+ storeCmd +'|'+ tokenID +'|'+ spellButton +'|'+ spellRow +'|'+ spellCol +'|'+ charged
+ +'
'+(spell.api ? '' : sendToWho(charCS,senderId,false,true))+'%{' + spell.dB + '|' + spellName.hyphened() + '}' + learnText + ')' )
+ + '}}';
+
+// log('makeCastSpellMenu: content = '+content);
+
+ sendResponse( charCS, content, senderId, flags.feedbackName, flags.feedbackImg, tokenID );
+ return;
+ };
+
+ /*
+ * Create a menu for a player to view a character's spells
+ */
+
+ var makeViewMemSpells = function( args, senderId ) {
+
+ var cmd = args[0].toUpperCase(),
+ isMU = cmd.includes('MU'),
+ isPR = cmd.includes('PR'),
+ isMI = cmd.includes('MI'),
+ isPower = cmd.includes('POWER'),
+ isScroll = cmd.includes('SCROLL'),
+ tokenID = args[1],
+ spellButton = args[2],
+ learn = (String(args[3]) || '').toUpperCase() === 'LEARN',
+ curToken = getObj('graphic',tokenID),
+ charCS = getCharacter(tokenID),
+ spell,
+ spellName = '',
+ spellValue,
+ content = '',
+ magicWord = 'spell',
+ magicDB, magicType, tableType,
+ col, rep,
+ viewCmd,
+ levelSpells = [],
+ levelLimit,
+ l, w, r, c,
+ buttonID = 0;
+
+ if (!charCS) {
+ sendDebug('makeViewMemSpells: invalid tokenID passed');
+ sendError('Internal MagicMaster parameter error');
+ return content;
+ }
+
+ var title = isMI ? attrLookup( charCS, fields.ItemChosen ) : curToken.get('name');
+ if (isPower) {
+ levelLimit = 1;
+ magicType = 'POWER';
+ tableType = 'Powers';
+ magicWord = 'power';
+ viewCmd = isMI ? BT.VIEW_MI_POWER : BT.VIEW_POWER;
+ magicDB = fields.PowersDB;
+ } else if (isMI && !(isMU || isPR)) {
+ levelLimit = 9;
+ tableType = 'Magic Item Spells';
+ viewCmd = isScroll ? BT.VIEW_MI_SCROLL : BT.VIEW_MI_SPELL;
+ } else if (isMU) {
+ levelLimit = 9;
+ magicType = 'MU';
+ tableType = (isMI ? 'Magic Item ' : '')+'Wizard Spells';
+ viewCmd = !isMI ? BT.VIEW_MUSPELL : (isScroll ? BT.VIEW_MI_MUSCROLL : BT.VIEW_MI_MUSPELL);
+ magicDB = fields.MU_SpellsDB;
+ } else {
+ levelLimit = 7;
+ magicType = 'PR';
+ tableType = (isMI ? 'Magic Item ' : '')+'Priest Spells';
+ viewCmd = !isMI ? BT.VIEW_PRSPELL : (isScroll ? BT.VIEW_MI_PRSCROLL : BT.VIEW_MI_PRSPELL);
+ magicDB = fields.PR_SpellsDB;
+ }
+
+ content = '&{template:'+fields.menuTemplate+'}{{name=View '+title+'\'s currently memorised '+magicWord+'s}}'
+ + '{{subtitle=' + tableType + '}}'
+ + '{{desc=' + ((makeSpellList( senderId, tokenID, viewCmd, spellButton, true ))[0] || 'No '+magicWord+'s currently memorised');
+
+ content += '}}{{desc1=Select the '+magicWord+' above that you want to view the details of. It will not be cast and will remain in your memorised '+magicWord+' list.}}';
+ sendResponse( charCS, content, senderId, flags.feedbackName, flags.feedbackImg, tokenID );
+ };
+
+ /*
+ * Make a one button menu to ask the player
+ * if they want to cast the same spell/power/MI again
+ */
+
+ var makeCastAgainMenu = function( args, senderId ) {
+
+ var isMU = args[0].toUpperCase().includes('MU'),
+ isPR = args[0].toUpperCase().includes('PR'),
+ isMI = args[0].toUpperCase().includes('MI'),
+ isPower = args[0].toUpperCase().includes('POWER'),
+ spellName = args[5] || '-',
+ charCS = getCharacter( args[1] ),
+ macroDB = isPower ? fields.PowersDB : (isMU ? fields.MU_SpellsDB : (isPR ? fields.PR_SpellsDB : fields.MagicItemDB)),
+ spell = getAbility( macroDB, spellName, charCS ),
+ content = '&{template:'+fields.menuTemplate+'}{{name='+args[5]+'}}'
+ + '{{desc=[Use another charge?](!magic --button '+ args[0] +'|'+ args[1] +'|'+ args[2] +'|'+ args[3] +'|'+ args[4]
+ + '
'+(spell.api ? '' : sendToWho(charCS,senderId,false,true))+'%{' + spell.dB + '|' + (args[5].hyphened()) + '})}}';
+
+ if (charCS) {
+ sendResponse( charCS, content, senderId, flags.feedbackName, flags.feedbackImg, args[1] );
+ }
+ return;
+ }
+
+ /*
+ * Create a short menu to ask the player to select between
+ * a short or a long rest. The long rest option can be shown
+ * as disabled.
+ */
+
+ var makeRestSelectMenu = function( args, longRestEnabled, senderId ) {
+
+ var tokenID = args[0],
+ casterType = args[2] || 'MU+PR',
+ charCS = getCharacter(tokenID),
+ curToken = getObj('graphic',tokenID),
+ content = '&{template:'+fields.menuTemplate+'}{{name=Select Type of Rest for '+curToken.get('name')+'}}'
+ + '{{desc=[Short Rest](!magic --rest '+tokenID+'|short|'+casterType+') or '
+ + (longRestEnabled ? '[' : '')
+ + 'Long Rest'
+ + (longRestEnabled ? ('](!magic --rest '+tokenID+'|long|'+casterType+')') : '')
+ + '}}';
+
+ if (!longRestEnabled) {
+ content += '{{ =It looks like the DM has not enabled Long Rests.\n[Try Again](!magic --rest '+tokenID+'|SELECT|'+args[2]+') once the DM says it is enabled}}';
+ }
+ sendResponse( charCS, content, senderId, flags.feedbackName, flags.feedbackImg, tokenID );
+ return;
+ }
+
+ /**
+ * Create a version of Pick or Put for coins, jewels and other treasure
+ * Allow the player to switch from one to the other when looting
+ **/
+
+ var makeLootMenu = function(senderId,args,menuType) {
+
+ var tokenID = args[1],
+ pickID = args[3],
+ putID = args[4];
+
+ var pickCS = getCharacter( pickID ),
+ putCS = getCharacter( putID );
+
+ if (!pickCS || !putCS) {
+ sendDebug( 'makeLootMenu: pickID or putID is invalid' );
+ sendError( 'Invalid make-menu call syntax' );
+ return;
+ }
+
+ var pickName = pickCS.get('name'),
+ putName = putCS.get('name'),
+ treasure = (attrLookup( pickCS, fields.Money_treasure ) || ''),
+ content = '&{template:'+fields.menuTemplate+'}{{name=View Treasure from ' + pickName + '}}';
+
+ if (treasure && treasure.length > 0) {
+ content += treasure;
+ } else {
+ content += '{{desc=There are no coins, gems or jewellery to be found here}}';
+ }
+
+ content += '{{desc1=Make a note of this - no automatic function yet!}}';
+ content += '{{desc2=When ready [View Magic Items](!magic --pickorput '+tokenID+'|'+pickID+'|'+putID+') or do something else.}}';
+
+ return content;
+ };
+
+ /*
+ * Create a menu to view or use a magic item
+ */
+
+ async function makeViewUseMI( args, senderId, menuType ) {
+
+ try {
+ var action = args[0].toUpperCase(),
+ tokenID = args[1],
+ MIrowref = args[2] || -1,
+ isGM = playerIsGM(senderId),
+ isView = action.includes('VIEW'),
+ charCS = getCharacter(tokenID),
+ learn = '';
+
+ if (!charCS) {
+ sendDebug( 'makeViewUseMI: tokenID is invalid' );
+ sendError( 'Invalid make-menu call syntax' );
+ return;
+ }
+
+ if (!menuType) {
+ var playerConfig = getSetPlayerConfig( senderId );
+ if (playerConfig) {
+ menuType = playerConfig.viewUseMIType || 'long';
+ } else {
+ menuType = 'long';
+ }
+ }
+ var shortMenu = menuType == 'short',
+ actionText = (isView ? 'View' : 'Use'),
+ selectAction = (isView ? (shortMenu ? BT.CHOOSE_VIEW_MI : BT.VIEW_MI) : BT.CHOOSE_USE_MI),
+ submitAction = (isView ? BT.VIEW_MI : BT.USE_MI),
+ content = '&{template:'+fields.menuTemplate+'}{{name='+actionText+' '+charCS.get('name')+'\'s Magic Items}}'
+ + '{{desc=Select a Magic Item below to '+actionText
+ + (isView ? '. It will not be used and will remain in your Magic Item Bag' : ', and then press the **Use Item** button')
+ + '. Note that some items, such as Rods, Staves or Wands, may need to be taken in-hand using *Change Weapon* and used via the *Attack* action}}'
+ + '{{desc1=';
+
+ if (shortMenu) {
+ content += '[Select a Magic Item](!magic --button '+selectAction+'|'+tokenID+'|?{Which Magic Item?';
+ content += await makeMIlist( charCS, senderId, false, isView, false, isView );
+ content +='}) }}';
+ } else {
+ // build the character's visible MI Bag
+ content += await makeMIbuttons( tokenID, senderId, (isGM ? 'max' : 'current'), fields.Items_qty[1], selectAction, (isView ? 'viewMI' : ''), MIrowref, true, false, false, isView );
+ content += '}}';
+ }
+ content += '{{desc2=';
+ if (shortMenu || !isView) {
+ if (MIrowref >= 0) {
+ let Items = getTable( charCS, fieldGroups.MI ),
+ selectedMI = Items.tableLookup( fields.Items_name, MIrowref ),
+ displayMI = selectedMI.dispName(),
+ trueMI = Items.tableLookup( fields.Items_trueName, MIrowref ),
+ trueType = Items.tableLookup( fields.Items_trueType, MIrowref ).toLowerCase(),
+ reveal = Items.tableLookup( fields.Items_reveal, MIrowref ).toLowerCase(),
+ qty = parseInt(Items.tableLookup( fields.Items_qty, MIrowref )) || 0;
+
+ if (chargedList.includes(trueType) && qty <= 1) reveal = 'use';
+
+ if ((shortMenu && isView && reveal == 'view') || (!isView && (reveal == 'use' || reveal == 'view'))) {
+ selectedMI = trueMI.hyphened();
+ }
+ let hide = resolveData( selectedMI, fields.MagicItemDB, reItemData, charCS, {hide:reSpellSpecs.hide}, MIrowref ).parsed.hide,
+ showDesc = (selectedMI !== trueMI) && hide && hide.length && hide !== 'hide',
+ renamed = !abilityLookup( fields.MagicItemDB, selectedMI ).obj,
+ magicItem = getAbility( fields.MagicItemDB, selectedMI, charCS, false, isGM, (showDesc ? selectedMI : trueMI), MIrowref ),
+ changedMI = renamed ? 'Display-'+selectedMI : selectedMI;
+
+ if (!state.MagicMaster.viewActions && isView && !!magicItem.obj) magicItem.obj = greyOutButtons( tokenID, charCS, magicItem.obj, (renamed ? changedMI : '') );
+ if (magicItem.obj && caster(charCS,'MU').clv > 0) {
+ learn = resolveData( selectedMI, fields.MagicItemDB, reItemData, charCS, {learn:reSpellSpecs.learn}, MIrowref ).parsed.learn;
+ if ((!isView && learn && learn != '0' && learn != '1' && !!abilityLookup( fields.MU_SpellsDB, learn ).obj)) {
+ setAbility( charCS, changedMI, magicItem.obj[0].get('action').replace(/\}\}$/m,'}}'+'{{Learn=Try to [Learn this spell](!magic --learn-spell '+tokenID+'|'+learn+')}}'));
+ };
+ };
+ content += '['+actionText+' '+displayMI+'](!magic --button '+ submitAction +'|'+ tokenID +'|'+ MIrowref
+ +'
'+(magicItem.api ? '' : sendToWho(charCS,senderId,false,true))+'%{'+magicItem.dB+'|'+(changedMI.hyphened())+'})';
+// log('makeViewUseMI: use button = '+content);
+ } else {
+ content += ''+actionText+' Magic Item';
+ }
+ content += '\nor\n';
+ }
+ if (isView) {
+ content += '[['+(attrLookup( charCS, fields.ItemContainerSize ) - (slotCounts[charCS.id] || 0))+']] remaining slots. ';
+ }
+ menuType = (shortMenu ? 'long' : 'short');
+ content += '[Swap to a '+menuType+' menu](!magic --button '+(isView ? BT.VIEWMI_OPTION : BT.USEMI_OPTION)+'|'+tokenID+'|'+menuType+')'
+ + '}}';
+
+ sendResponse( charCS, content, senderId, flags.feedbackName, flags.feedbackImg, tokenID );
+ return;
+ } catch (e) {
+ sendCatchError('MagicMaster',msg_orig[senderId],e);
+ }
+ }
+
+ /**
+ * Make a menu to display when a Player selects to use
+ * a power of a Magic Item
+ */
+
+ var makeUseMIpowerMenu = function( args, senderId ) {
+
+ var tokenID = args[1],
+ powerName = args[2],
+ castLevel = args[3],
+ itemName = args[4],
+ MIlibrary = args[5],
+ power = args[6],
+ powerLib = args[7],
+ charCS = getCharacter(tokenID),
+ tokenName = getObj('graphic',tokenID).get('name'),
+ spell = getAbility( powerLib, power, charCS ),
+ item = getAbility( MIlibrary, itemName, charCS ),
+ toWho = sendToWho(charCS,senderId,false,true),
+ content = '&{template:'+fields.menuTemplate+'}{{name='+itemName+'\'s '+powerName+' power}}'
+ + '{{desc='+tokenName+' is about to use '+itemName+'\'s '+powerName+' power. Is this correct?}}'
+ + '{{desc1=[Use '+powerName+'](!magic --button '+ BT.MI_POWER_USED +'|'+ tokenID +'|'+ powerName +'|'+ itemName +'|'+ castLevel
+ + '
'+(spell.api ? '' : toWho)+'%{'+spell.dB +'|'+ (power.hyphened()) +'})'
+ + ' or [Return to '+itemName+'](!
'+(item.api ? '' : toWho)+'%{'+MIlibrary+'|'+(itemName.hyphened())+'})\nOr just do something else}}';
+ sendResponse(charCS,content,senderId, flags.feedbackName, flags.feedbackImg, tokenID);
+ return;
+ }
+
+
+ /**
+ * Create the Edit Magic Item Bag menu. Allow for a short version if
+ * the Short Menus status flag is set, and highlight selected buttons
+ **/
+
+ async function makeEditBagMenu(args,senderId,msg='',menuType) {
+
+ try {
+ var cmd = (args[0] || '').toUpperCase(),
+ tokenID = args[1],
+ MIrowref = args[2],
+ itemName = args[3] || '',
+ charges = args[4],
+ selectedMI = itemName.hyphened(),
+ displayMI = selectedMI,
+ alphaLists = state.MagicMaster.alphaLists,
+ charCS = getCharacter( tokenID );
+
+ if (!charCS) {
+ sendDebug( 'makeEditMImenu: Invalid character ID passed' );
+ sendError( 'Invalid MagicMaster argument' );
+ return;
+ }
+
+ var qty, mi, playerConfig, magicItem, removeMI,
+ selected = !!selectedMI && selectedMI.length > 0,
+ remove = (selectedMI.toLowerCase() == 'remove'),
+ bagSlot = !!MIrowref && MIrowref >= 0,
+ queries = '',
+ content = '&{template:'+fields.menuTemplate+'}{{name=Edit Magic Item Bag}}';
+
+ if (!menuType) {
+ playerConfig = getSetPlayerConfig( senderId );
+ if (playerConfig) {
+ menuType = playerConfig.editBagType;
+ alphaLists = playerConfig.alphaLists;
+ } else {
+ menuType = 'long';
+ }
+ }
+ var shortMenu = menuType == 'short',
+ editMartial = cmd.includes('MARTIAL'),
+ editAll = cmd.includes('ALLITEMS'),
+ optionCmd = (editMartial ? BT.EDITMARTIAL_OPTION : (editAll ? BT.EDITALLITEMS_OPTION : BT.EDITMI_OPTION)),
+ chooseCmd = (editMartial ? BT.CHOOSE_MARTIAL_MI : (editAll ? BT.CHOOSE_ALLITEMS_MI : BT.CHOOSE_MI)),
+ redoCmd = (editMartial ? BT.REDO_MARTIAL_MI : (editAll ? BT.REDO_ALLITEMS_MI : BT.REDO_CHOOSE_MI)),
+ slotCmd = (editMartial ? BT.SLOT_MARTIAL_MI : (editAll ? BT.SLOT_ALLITEMS_MI : BT.SLOT_MI)),
+ storeCmd = (editMartial ? BT.STORE_MARTIAL_MI : (editAll ? BT.STORE_ALLITEMS_MI : BT.STORE_MI)),
+ reviewCmd = (editMartial ? BT.REVIEW_MARTIAL_MI : (editAll ? BT.REVIEW_ALLITEMS_MI : BT.REVIEW_MI)),
+ removeCmd = (editMartial ? BT.REMOVE_MARTIAL_MI : (editAll ? BT.REMOVE_ALLITEMS_MI : BT.REMOVE_MI));
+
+ if (selected && !remove) {
+ magicItem = getAbility( fields.MagicItemDB, selectedMI, charCS, null, null, null, MIrowref );
+ if (!magicItem.obj) {
+ sendResponse( charCS, 'Can\'t find '+selectedMI+' in the Magic Item database', senderId, flags.feedbackName, flags.feedbackImg, tokenID );
+ return;
+ } else {
+ if (!state.MagicMaster.viewActions && isView && !!magicItem.obj) {
+ let renamed = !abilityLookup( fields.MagicItemDB, selectedMI ).obj;
+ if (renamed) displayMI = 'Display-'+selectedMI;
+ magicItem.obj = greyOutButtons( tokenID, charCS, magicItem.obj, (renamed ? displayMI : '') );
+ };
+ };
+ };
+
+ if (msg && msg.length>0) {
+ content += '{{Section='+msg+'}}';
+ };
+
+ if (!shortMenu || !selected) {
+ let potions = getMagicList(fields.MagicItemDB,miTypeLists,'potion',senderId,'',false,'',alphaLists),
+ scrolls = getMagicList(fields.MagicItemDB,miTypeLists,'scroll',senderId,'',false,'',alphaLists),
+ rods = getMagicList(fields.MagicItemDB,miTypeLists,'rod',senderId,'',false,'',alphaLists),
+ weapons = getMagicList(fields.MagicItemDB,miTypeLists,'weapon',senderId,'',false,'',alphaLists),
+ ammo = getMagicList(fields.MagicItemDB,miTypeLists,'ammo',senderId,'',false,'',alphaLists),
+ armour = getMagicList(fields.MagicItemDB,miTypeLists,'armour',senderId,'',false,'',alphaLists),
+ rings = getMagicList(fields.MagicItemDB,miTypeLists,'ring',senderId,'',false,'',alphaLists),
+ equip = getMagicList(fields.MagicItemDB,miTypeLists,'equipment',senderId,'',false,'',alphaLists),
+ misc = getMagicList(fields.MagicItemDB,miTypeLists,'miscellaneous',senderId,'',false,'',alphaLists);
+
+ content += '{{Section1=[Use '+(alphaLists ? 'full' : 'alphabeticised')+' lists](!magic --button '+BT.ALPHALIST_OPTION+'|'+tokenID+'|'+(alphaLists ? 'full' : 'alpha')+'|'+cmd+') to select items from}}'
+ + '{{desc=**1.Choose what item to store**\n'
+ + (editMartial ? '' : '[Potion](!magic --button '+chooseCmd+'|'+tokenID+'|'+MIrowref+'|?{Potion to store|'+potions+'}|'+charges+')')
+ + (editMartial ? '' : '[Scroll](!magic --button '+chooseCmd+'|'+tokenID+'|'+MIrowref+'|?{Scroll to store|'+scrolls+'}|'+charges+')')
+ + (editMartial ? '' : '[Rods, Staves, Wands](!magic --button '+chooseCmd+'|'+tokenID+'|'+MIrowref+'|?{Rod Staff Wand to store|'+rods+'}|'+charges+')')
+ + (!editMartial && !editAll ? '' : '[Weapon](!magic --button '+chooseCmd+'|'+tokenID+'|'+MIrowref+'|?{Weapon to store|'+weapons+'}|'+charges+')')
+ + (!editMartial && !editAll ? '' : '[Ammo](!magic --button '+chooseCmd+'|'+tokenID+'|'+MIrowref+'|?{Ammunition to store|'+ammo+'}|'+charges+')')
+ + (!editMartial && !editAll ? '' : '[Armour](!magic --button '+chooseCmd+'|'+tokenID+'|'+MIrowref+'|?{Armour to store|'+armour+'}|'+charges+')')
+ + (editMartial ? '' : '[Ring](!magic --button '+chooseCmd+'|'+tokenID+'|'+MIrowref+'|?{Ring to store|'+rings+'}|'+charges+')')
+ + (editMartial ? '' : '[Equipment](!magic --button '+chooseCmd+'|'+tokenID+'|'+MIrowref+'|?{Equipment to store|'+equip+'}|'+charges+')')
+ + (editMartial ? '' : '[Miscellaneous](!magic --button '+chooseCmd+'|'+tokenID+'|'+MIrowref+'|?{Misc Item to store|'+misc+'}|'+charges+')');
+ if (shortMenu) {
+ content += '\n**OR**\n'
+ + '[Choose item to Remove](!magic --button '+chooseCmd+'|'+tokenID+'|'+MIrowref+'|'+'Remove) from your MI bag}}'
+ + '{{desc2=[Swap to a long menu](!magic --button '+optionCmd+'|'+tokenID+'|'+(shortMenu ? 'long' : 'short')+')}}';
+ }
+ }
+ if (!shortMenu || selected) {
+ if (!remove) {
+ if (shortMenu) {
+ content += '{{desc=**1.Item chosen** ['+itemName+'](!magic --button '+redoCmd+'|'+tokenID+'|'+MIrowref+'), click to reselect\n';
+ }
+ content += '\nOptionally, you can '+(selected ? '[' : '')+'Review '+itemName+(selected ? ('](!magic --button '+reviewCmd+'|'+tokenID+'|'+MIrowref+'|'+selectedMI+'|
'+(magicItem.api ? '' : sendToWho(charCS,senderId,false,true))+'%{'+magicItem.dB+'|'+(displayMI.hyphened())+'})') : '')+'';
+ } else {
+ content += '{{Section1=}}{{Section2=}}{{desc=**1.Action chosen** ***Remove***, [click](!magic --button '+redoCmd+'|'+tokenID+'|'+MIrowref+') to change';
+ }
+ content += '}}';
+ }
+
+ if (bagSlot) {
+ qty = attrLookup( charCS, [fields.Items_qty[0], 'current'], fields.Items_table, MIrowref ) || 0;
+ removeMI = attrLookup( charCS, [fields.Items_name[0], 'current'], fields.Items_table, MIrowref );
+ }
+ if (!shortMenu || (selected && !bagSlot)) {
+ content += '{{desc1=';
+ if (remove) {
+ content += '2.Select the item to **remove**\n';
+ } else if (selected) {
+ content += '**2.Select the slot to add this item to**\n';
+ } else {
+ content += 'Select an Item above then\n'
+ + '**2.Select a slot to add it to**\n';
+ }
+
+ if (shortMenu) {
+ content += '[Select slot](!magic --button '+slotCmd+'|'+tokenID+'|?{Which slot?';
+ content += await makeMIlist( charCS, senderId, true );
+ content +='}|'+selectedMI+')';
+ } else {
+ content += await makeMIbuttons( tokenID, senderId, 'current', fields.Items_qty[1], slotCmd, '|'+selectedMI, MIrowref, false, true );
+ }
+
+ content += '}}';
+ } else if (shortMenu && bagSlot) {
+ removeMI = mi = attrLookup( charCS, [fields.Items_name[0], 'current'], fields.Items_table, MIrowref );
+
+ content += '{{desc1=**2.Selected** ['+qty+' '+mi+'](!magic --button '+slotCmd+'|'+tokenID+'|?{Which other slot?';
+ content += await makeMIlist( charCS, senderId, true );
+ content += '}|'+selectedMI+'|)'
+ + ' as slot to '+(remove ? 'remove' : 'store it in')+', click to change}}';
+ }
+
+ if (!shortMenu || (selected && bagSlot)) {
+
+ menuType = (shortMenu ? 'long' : 'short');
+ content += '{{desc2=**3.';
+ if (!remove) {
+ qty = String(qty)+'+1';
+ if (selected) {
+ let chosenData = resolveData( selectedMI, fields.MagicItemDB, reItemData, charCS, {qty:reSpellSpecs.qty,query:reSpellSpecs.query}, MIrowref ).parsed;
+ qty = chosenData.qty || (selectedMI.trueCompare(removeMI) ? qty : 1);
+ queries = parseQuery( chosenData.query );
+ }
+
+ content += ((selected && bagSlot) ? '[' : (''))
+ + 'Store '+itemName
+ + ((selected && bagSlot && !remove) ? ('](!magic --button '+storeCmd+'|'+tokenID+'|'+MIrowref+'|'+selectedMI+'|?{Quantity?|'+qty+'}||'+queries+')') : '')
+ + ' in your MI Bag**'+(!!removeMI ? (', overwriting **'+removeMI) : '')+'**\n\n'
+ + 'or ';
+ }
+ content += (bagSlot ? '[' : (''))
+ + 'Remove '+(!!removeMI ? removeMI : 'item')
+ + (bagSlot ? ('](!magic --button '+removeCmd+'|'+tokenID+'|'+MIrowref+'|'+removeMI+')') : '')
+ + ' from your MI Bag\n\n'
+ + 'or [Swap to a '+menuType+' menu](!magic --button '+optionCmd+'|'+tokenID+'|'+menuType+')}}';
+ }
+ sendResponse( charCS, content, senderId, flags.feedbackName, flags.feedbackImg, tokenID );
+ return;
+ } catch (e) {
+ sendCatchError('MagicMaster',msg_orig[senderId],e);
+ }
+ }
+
+ /*
+ * Create a menu for DMs to see displayed and real Magic Item information
+ * on Character Sheets. Hidden information can be what the MI really is,
+ * which the DM can set using this menu.
+ */
+
+ async function makeGMonlyMImenu(args, senderId, msg, alphaLists) {
+
+ try {
+ var cmd = args[0],
+ tokenID = args[1],
+ MIrowref = args[2],
+ MItoStore = args[3],
+ charCS = getCharacter(tokenID),
+
+ ensureUnique = function( Items, name ) {
+ var count = 1,
+ newName = name;
+ while (Items.tableFind( fields.Items_name, newName )) {
+ newName = name + String(count++);
+ }
+ return newName;
+ };
+
+ if (!charCS) {
+ sendDebug('makeGMonlyMImenu: invalid tokenID passed');
+ sendError('Internal MagicMaster error');
+ return;
+ }
+
+ if (!_.isUndefined(alphaLists) && !_.isNull(alphaLists)) {
+ state.MagicMaster.alphaLists = alphaLists;
+ } else {
+ alphaLists = state.MagicMaster.alphaLists;
+ }
+
+ var qty, mi,
+ potions = getMagicList(fields.MagicItemDB,miTypeLists,'potion',senderId,'',false,'',!!alphaLists),
+ scrolls = getMagicList(fields.MagicItemDB,miTypeLists,'scroll',senderId,'',false,'',!!alphaLists),
+ rods = getMagicList(fields.MagicItemDB,miTypeLists,'rod',senderId,'',false,'',!!alphaLists),
+ weapons = getMagicList(fields.MagicItemDB,miTypeLists,'weapon',senderId,'',false,'',!!alphaLists),
+ ammo = getMagicList(fields.MagicItemDB,miTypeLists,'ammo',senderId,'',false,'',!!alphaLists),
+ armour = getMagicList(fields.MagicItemDB,miTypeLists,'armour',senderId,'',false,'',!!alphaLists),
+ rings = getMagicList(fields.MagicItemDB,miTypeLists,'ring',senderId,'',false,'',!!alphaLists),
+ misc = getMagicList(fields.MagicItemDB,miTypeLists,'miscellaneous',senderId,'',false,'',!!alphaLists),
+ equip = getMagicList(fields.MagicItemDB,miTypeLists,['equipment','light'],senderId,'',false,'',!!alphaLists),
+ treasure = getMagicList(fields.MagicItemDB,miTypeLists,'treasure',senderId,'',false,'',!!alphaLists),
+ dmitems = getMagicList(fields.MagicItemDB,miTypeLists,'dmitem',senderId,'',false,'',false),
+ content = '&{template:'+fields.menuTemplate+'}{{name=Edit '+charCS.get('name')+'\'s Magic Item Bag}}'
+ + (msg && msg.length ? '{{section='+msg+'}}' : '')
+ + '{{desc=**1. Choose something to store** [Alpha '+!!alphaLists+'](!magic --button '+(alphaLists ? 'GM-MIalphaOff':'GM-MIalphaOn')+'|'+args[1]+'|'+args[2]+'|'+args[3]+')\n';
+
+ content += '[Potion](!magic --button GM-MItoStore|'+tokenID+'|'+MIrowref+'|?{Which Potion?|'+potions+'})'
+ + '[Scroll](!magic --button GM-MItoStore|'+tokenID+'|'+MIrowref+'|?{Which Scroll?|'+scrolls+'})'
+ + '[Rods, Staves, Wands](!magic --button GM-MItoStore|'+tokenID+'|'+MIrowref+'|?{Which Rod, Staff or Wand?|'+rods+'})'
+ + '[Weapon](!magic --button GM-MItoStore|'+tokenID+'|'+MIrowref+'|?{Which Weapon?|'+weapons+'})'
+ + '[Ammo](!magic --button GM-MItoStore|'+tokenID+'|'+MIrowref+'|?{Which Ammo?|'+ammo+'})'
+ + '[Armour](!magic --button GM-MItoStore|'+tokenID+'|'+MIrowref+'|?{Which piece of Armour?|'+armour+'})'
+ + '[Ring](!magic --button GM-MItoStore|'+tokenID+'|'+MIrowref+'|?{Which Ring?|'+rings+'})'
+ + '[Miscellaneous MI](!magic --button GM-MItoStore|'+tokenID+'|'+MIrowref+'|?{Which Misc MI?|'+misc+'})'
+ + '[Equipment](!magic --button GM-MItoStore|'+tokenID+'|'+MIrowref+'|?{What Equipment?|'+equip+'})'
+ + '[Treasure](!magic --button GM-MItoStore|'+tokenID+'|'+MIrowref+'|?{What Treasure?|'+treasure+'})'
+ + '[DM only list](!magic --button GM-MItoStore|'+tokenID+'|'+MIrowref+'|?{Which DM only item?|'+dmitems+'})}}';
+ content += '{{desc1=**2. Choose slot to edit or store in**\n';
+
+ var Items = getTable( charCS, fieldGroups.MI ),
+ slotName = (MIrowref >= 0) ? Items.tableLookup( fields.Items_name, MIrowref ) : '',
+ slotActualName = (MIrowref >= 0) ? Items.tableLookup( fields.Items_trueName, MIrowref ) : '',
+ slotType = (MIrowref >= 0) ? Items.tableLookup( fields.Items_type, MIrowref ) : '',
+ slotTrueType = (MIrowref >= 0) ? Items.tableLookup( fields.Items_trueType, MIrowref ) : '',
+ slotQty = parseInt(Items.tableLookup( fields.Items_qty, MIrowref )) || 0,
+ slotActualQty = parseInt(Items.tableLookup( fields.Items_trueQty, MIrowref )) || 0,
+ slotCost = parseFloat(Items.tableLookup( fields.Items_cost, MIrowref )) || 0,
+ slotReveal = (MIrowref >= 0) ? Items.tableLookup( fields.Items_reveal, MIrowref ) : '',
+ slotCursed = slotType.toLowerCase().includes('cursed'),
+
+ chosenMI = (MItoStore.length > 0),
+ chosenSlot = (MIrowref >= 0),
+ chosenBoth = (chosenMI && chosenSlot),
+ hideAvail = chosenBoth,
+ chosenEither = (chosenMI || chosenSlot),
+ hiddenMI = slotName !== slotActualName,
+ greyButton = '',
+ selectableSlot = chosenSlot ? '[' : greyButton,
+ selectableBoth = chosenBoth ? '[' : greyButton,
+ hideableBoth = selectableBoth,
+ selectableEither = chosenEither ? '[' : greyButton,
+ hiddenSlot = hiddenMI ? '[' : greyButton,
+ revealType = (!slotReveal || slotReveal.toLowerCase() === 'manual' ? 'Manually' : ('on '+slotReveal)),
+ intelligence = Math.max( (parseInt(attrLookup( charCS, fields.Intelligence )) || 0), (parseInt(attrLookup( charCS, fields.Monster_int )) || 0)),
+ hp = parseInt(attrLookup( charCS, fields.HP )) || 0,
+ sentient = (intelligence > 0 && hp > 0),
+ containerNo = parseInt(attrLookup( charCS, fields.ItemContainerType )) || 0,
+ containerSize = attrLookup( charCS, fields.ItemContainerSize ),
+ showTypes = parseInt(attrLookup( charCS, fields.ItemContainerHide )),
+ spellStoring = false,
+ looksLike = false,
+ queries = '',
+ chosenData, initQty, containerType, slotObj, itemObj;
+
+ // build the character's visible MI Bag
+ content += await makeMIbuttons( tokenID, senderId, 'max', 'current', 'GM-MIslot', '|'+MItoStore, MIrowref, false, true );
+ content += '}}';
+
+ if (hiddenMI) {
+ content += '{{desc2=**Which is hidden as**\n'
+ + ''
+ + (slotName != '-' ? (slotQty + ((slotQty != slotActualQty) ? '/'+slotQty : '') + ' ') : '') + slotName
+ + '}}';
+ }
+
+ if (chosenSlot) {
+ slotObj = getAbility( fields.MagicItemDB, slotActualName, charCS, false, true, slotActualName, MIrowref );
+ if (!!slotObj.obj) {
+ spellStoring = reCastMIspellCmd.test(slotObj.obj[1].body) || reCastMIpowerCmd.test(slotObj.obj[1].body);
+ looksLike = reLooksLike.test(slotObj.obj[1].body);
+ if (looksLike && !hiddenMI && !chosenMI) {
+ MItoStore = ensureUnique( Items, getShownType( slotObj, MIrowref, resolveData( slotActualName, fields.MagicItemDB, reItemData, charCS, {itemType:reSpellSpecs.itemType}, MIrowref ).parsed.itemType ));
+ hideAvail = MItoStore !== slotName;
+ hideableBoth = hideAvail ? '[' : greyButton;
+ }
+ }
+ }
+ var storableSlot = (spellStoring && chosenSlot) ? '[' : '';
+
+ if (_.isUndefined(containerSize)) {
+ containerSize = fields.MIRowsStandard;
+ setAttr( charCS, fields.ItemContainerSize, containerSize );
+ }
+
+ if (containerNo < 4) {
+ if (hp <= 0 || !sentient) {
+ containerNo = 1;
+ } else {
+ containerNo = 3;
+ }
+ }
+ switch (containerNo) {
+ case 0:
+ case 1:
+ case 6: containerType = 'Inanimate Container';
+ break;
+ case 2:
+ case 3:
+ case 7:containerType = 'Sentient Creature';
+ break;
+ case 4:
+ case 5: containerType = 'Trapped container';
+ break;
+ default:containerType = 'Empty Container';
+ containerNo = 0;
+ break;
+ }
+ setAttr( charCS, fields.ItemContainerType, containerNo );
+ setAttr(charCS, fields.ItemOldContainerType, containerNo);
+
+ var itemName = MItoStore;
+ MItoStore = (MItoStore || '').hyphened();
+ initQty = String(slotQty)+'+1';
+ if (chosenMI) {
+ itemObj = getAbility( fields.MagicItemDB, MItoStore, charCS, false, true, MItoStore, chosenSlot );
+ if (itemObj.obj) {
+ chosenData = resolveData( MItoStore, fields.MagicItemDB, reItemData, charCS, {qty:reSpellSpecs.qty,query:reSpellSpecs.query}, chosenSlot );
+ initQty = chosenData.parsed.qty || (itemName.trueCompare(slotName) ? initQty : 1);
+ queries = parseQuery( chosenData.parsed.query );
+ }
+ };
+
+ var reviewItem = ((cmd !== 'GM-MItoStore' && chosenSlot) ? slotActualName : itemName),
+ reviewObj = ((cmd !== 'GM-MItoStore' && chosenSlot) ? slotObj : itemObj),
+ renamed = !abilityLookup( fields.MagicItemDB, reviewItem ).obj,
+ changedItem = renamed ? 'Display-'+reviewItem : reviewItem;
+ if (!state.MagicMaster.viewActions && reviewObj && reviewObj.obj) reviewObj.obj = greyOutButtons( tokenID, charCS, reviewObj.obj, (renamed ? changedItem : '') );
+
+ content += '{{desc3=**3. '+selectableBoth+(chosenBoth ? ('Store '+itemName+'](!magic --button GM-StoreMI|'+tokenID+'|'+MIrowref+'|'+MItoStore+'|[[?{Quantity?|'+initQty+'}]]||'+queries+')') : ('Store'+(chosenSlot ? ('d '+slotActualName) : itemName)+''))+' **'
+ + ' or '+hideableBoth+(hideAvail ? ('Hide '+slotName+' as '+itemName+'](!magic --button GM-HideMI|'+tokenID+'|'+MIrowref+'|'+MItoStore+')') : ((hiddenMI ? ('Hidden as '+slotName) : ('Hide Item'+(chosenMI?(' as '+itemName):'')))+''))+'
'
+ + ' or '+selectableEither+'Review'+(chosenEither ? (' '+reviewItem+'](!magic --button GM-ReviewMI|'+tokenID+'|'+MIrowref+'|'+MItoStore+'
'+(reviewObj.api ? '' : '/w gm ')+'%{'+reviewObj.dB+'|'+(changedItem.hyphened())+'})') : ' the item')+'
}}'
+ + '{{desc4=1. Or select MI from above ^\n'
+ + '
}}'
+ + '{{desc5=or [Edit Treasure](!magic --button GM-TreasureMenu|'+tokenID+'|'+MIrowref+'|'+MItoStore+')\n'
+ + '['+containerSize+' slot](!magic --button GM-SetTokenSize|'+tokenID+'|'+MIrowref+'|'+MItoStore+'|?{How many slots does this container have?|'+containerSize+'})'
+ + '['+containerType+'](!magic --button GM-SetTokenType|'+tokenID+'|'+MIrowref+'|'+MItoStore+'|?{What type of token is this?|Untrapped Container,1|Trapped container,4|Force Inanimate Container,6|Force Sentient Creature,7})\n'
+ + '['+(showTypes ? 'Show as Item types' : 'Show as Item names')+'](!magic --button GM-HideAsTypes|'+tokenID+'|'+MIrowref+'|'+MItoStore+'|'+showTypes+') in container. '
+ + '[BLANK BAG](!magic --button GM-BlankBag|'+tokenID+')'
+ + '}}';
+
+ sendFeedback( content, flags.feedbackName, null, tokenID, charCS );
+ return;
+ } catch (e) {
+ sendCatchError('MagicMaster',msg_orig[senderId],e);
+ }
+ }
+
+ /*
+ * Create the DM's Edit Treasure menu
+ */
+
+ var makeEditTreasureMenu = function(args,senderId,msg) {
+
+ var tokenID = args[1],
+ charCS = getCharacter(tokenID);
+
+ if (!charCS) {
+ sendDebug('makeEditTreasureMenu: invalid tokenID passed');
+ sendError('Internal MagicMaster error');
+ return;
+ }
+
+ var charName = charCS.get('name'),
+ treasure = attrLookup( charCS, fields.Money_treasure ) || '{{Treasure=None found}}',
+ content = '&{template:'+fields.menuTemplate+'}{{name=Current treasure for '+charName+'}}'
+ + treasure +'{{=----- End of current Treasure ----}}'
+ + '{{desc1=[Add](!magic --button GM-AddTreasure|'+tokenID+'|?{Title for Treasure?}|?{Treasure text}) or '
+ + '[Edit](!magic --button GM-EditTreasure|'+tokenID+') or '
+ + '[Delete](!magic --button GM-DeleteTreasure|'+tokenID+') the treasure}}'
+ + '{{desc2=Return to [DM\'s Change MI menu](!magic --button GM-MImenu|'+tokenID+'|-1|)}}';
+ if (msg && msg.length > 0) {
+ content += '{{desc='+msg+'}}';
+ }
+
+ sendFeedback(content,flags.feedbackName,tokenID,charCS);
+ return;
+ }
+
+ /*
+ * Create a shorter, easier Pick or Put menu, that only does either
+ * Pick or Put (Player can switch between two), that uses a drop-down
+ * list of the MIs in the container to pick from (rather than buttons), and
+ * automatically selects an empty slot to put it into
+ */
+
+ async function makeShortPOPmenu( args, senderId, menuType ) { // silent
+
+ try {
+ var tokenID = args[1],
+ pickID = args[3],
+ putID = args[4],
+ pickRow = args[2] || -1,
+ putRow = args[5] || -1;
+
+ if (!pickID || !putID) {
+ sendDebug( 'makeShortPOPmenu: pickID or putID is invalid' );
+ sendError( 'Invalid make-menu call syntax' );
+ return;
+ };
+
+ var putCS = getCharacter( putID ),
+ pickCS = getCharacter( pickID ),
+ pickingUp = (tokenID == putID),
+ shortMenu = pickingUp,
+ pickOrPut = (pickingUp ? 'Pick up' : 'Put away'),
+ charCS = getCharacter(tokenID),
+ isGM = playerIsGM(senderId);
+
+ if (!putCS || !pickCS) {
+ sendDebug( 'makeShortPOPmenu: pickID or putID is invalid' );
+ sendError( 'Invalid make-menu call syntax' );
+ return;
+ }
+ if (!menuType) {
+ var playerConfig = getSetPlayerConfig( senderId );
+ if (playerConfig) {
+ shortMenu = !!!((pickingUp ? playerConfig.pickUpMIType : playerConfig.putAwayMIType) == 'long');
+ }
+ } else {
+ shortMenu = !!!(menuType.toLowerCase() == 'long');
+ }
+ menuType = shortMenu ? 'long' : 'short';
+
+ var putName = putCS.get('name'),
+ pickName = pickCS.get('name'),
+ qty, mi, miTrueName, i,
+ putItems,
+ miObj,
+ pickedMI, pickedTrueMI, pickableQty, pickedType, miType,
+ bagSize = (attrLookup( putCS, fields.ItemContainerSize ) || fields.MIRows),
+ showTypes = parseInt(attrLookup( pickCS, fields.ItemContainerHide )),
+ miList = await makeMIlist( pickCS, senderId, false, true, showTypes ),
+ treasure = (attrLookup( pickCS, fields.Money_treasure ) || '{{desc1=and there is no treasure here, either}}'),
+ content = '&{template:'+fields.menuTemplate+'}{{name=Take from ' + pickName + ' to add to ' + putName + '\'s Items of Equipment}}',
+ magicItems, slotsUsed;
+
+ putRow = -1;
+ putItems = getTableField( putCS, {}, fields.Items_table, fields.Items_name );
+ if (pickRow >= 0) {
+ pickedMI = attrLookup( pickCS, fields.Items_name, fields.Items_table, pickRow ) || '';
+ pickedTrueMI = (attrLookup( pickCS, fields.Items_trueName, fields.Items_table, pickRow ) || '').dbName() || '-';
+ pickableQty = attrLookup( pickCS, fields.Items_qty, fields.Items_table, pickRow ) || '';
+ pickedType = (attrLookup( pickCS, fields.Items_type, fields.Items_table, pickRow ) || '').dbName() || '-';
+ putItems = getTableField( putCS, putItems, fields.Items_table, fields.Items_trueName );
+ putItems = getTableField( putCS, putItems, fields.Items_table, fields.Items_type );
+ let lowerMI = pickedMI.dbName().replace(/v\d+$/,'') || '-';
+ for (i = 0; i < putItems.sortKeys.length; i++) {
+ mi = (putItems.tableLookup(fields.Items_name,i) || '').dbName().replace(/v\d+$/,'') || '-';
+ if (_.isUndefined(mi)) break;
+ if (mi != lowerMI) continue;
+ miTrueName = (putItems.tableLookup(fields.Items_trueName,i) || '').dbName() ||'-';
+ if (miTrueName != pickedTrueMI) continue;
+ miType = (putItems.tableLookup(fields.Items_type,i) || pickedType);
+ if (miType.dbName() !== '' && (miType.dbName() !== pickedType || !stackable.includes(miType.toLowerCase()))) continue;
+ putRow = i;
+ break;
+ }
+ if (showTypes) {
+ miObj = abilityLookup( fields.MagicItemDB, pickedMI, pickCS );
+ pickedMI = !miObj.obj ? pickedMI : getShownType( miObj, pickRow );
+ }
+ }
+ i = slotsUsed = 0;
+ while (i < putItems.sortKeys.length) {
+ mi = putItems.tableLookup( fields.Items_name, i, false );
+ if (_.isUndefined(mi)) {break;}
+ if (mi == '-' && putRow < 0) {
+ putRow = i;
+ } else if (mi !== '-') slotsUsed++;
+ i++;
+ }
+
+ slotCounts[putID] = slotsUsed;
+
+ if (putRow < 0) {
+ if (i >= bagSize) {
+ sendParsedMsg( tokenID, messages.miBagFull, senderId, '', putID );
+ return;
+ } else {
+ putRow = i;
+ }
+ }
+
+ shortMenu = shortMenu && (miList.split('|').length > 2);
+
+ if (pickingUp) content += treasure;
+
+ magicItems = await makeMIbuttons( tokenID, senderId, 'current', 'current', BT.POP_PICK, '|'+pickID+'|'+putID+'|'+putRow, pickRow, false, false, showTypes, true, pickID );
+
+ content += '{{desc='+putName+' has [['+(attrLookup( putCS, fields.ItemContainerSize ) - slotCounts[putID])+']] remaining slots. ';
+
+ if (magicItems && magicItems.length) {
+ if (shortMenu) {
+ content += 'Press the **[Select]** button to select the item you want to '+pickOrPut+' from a list of items in a container, '
+ + 'then press the **[Store]** button to automatically put it away in an empty slot}}'
+ + '{{Select=[Select Item to '+pickOrPut+'](!magic --button '+BT.POP_PICK+'|'+tokenID+'|?{'+pickOrPut+' which Item?'+miList+'}|'+pickID+'|'+putID+'|'+putRow+')}}'
+ + '{{Store=';
+ } else {
+ content += 'Select an item you want to '+pickOrPut+'\n'
+ + magicItems
+ + '}}{{desc1='
+ }
+ content +=((pickRow >= 0 && putRow >= 0) ? '[' : '')
+ + 'Store '+((pickRow >= 0) ? pickedMI : 'item')
+ + ((pickRow >= 0 && putRow >= 0) ? ('](!magic --button '+BT.POP_STORE+'|'+tokenID+'|'+pickRow+'|'+pickID+'|'+putID+'|'+putRow+'|-1)') : '' )
+ + ' in free slot}}{{desc2=';
+ content += '[Use '+menuType+' menu](!magic --button '+(pickingUp ? BT.PICKMI_OPTION : BT.PUTMI_OPTION)+'|'+tokenID+'|'+menuType+'|'+pickID+'|'+putID+')}}';
+
+ sendResponse( charCS, content, senderId, flags.feedbackName, flags.feedbackImg );
+ } else {
+ content = messages.header + '{{desc=' + pickCS.get('name') + ' ' + messages.fruitlessSearch + treasure;
+ sendParsedMsg( tokenID, content, senderId );
+ }
+ return;
+ } catch (e) {
+ sendCatchError('MagicMaster',msg_orig[senderId],e);
+ }
+ }
+
+ /*
+ * Create the Spells menus
+ */
+
+ var makeMUSpellsMenu = function( args, senderId ) {
+
+ var tokenID = args[0],
+ curToken = getObj('graphic',tokenID),
+ charCS = getCharacter(tokenID);
+
+ if (!charCS) {
+ sendDebug('makeMUSpellsMenu: invalid tokenID parameter');
+ sendError('Invalid MagicMaster parameter');
+ return;
+ }
+ var content = '&{template:'+fields.menuTemplate+'} {{name='+curToken.get('name')+'\'s Magic User Spells menu}}'
+ + '{{desc=[Cast MU spell](!magic --cast-spell MU|'+tokenID+')\n'
+ + ((apiCommands.rounds && apiCommands.rounds.exists) ? ('[Show an Area of Effect](!rounds --aoe '+tokenID+')\n') : ('Show an Area of Effect'))
+ + '[Short Rest for L1 MU Spells](!magic --rest '+tokenID+'|short|MU)\n'
+ + '[Long Rest and recover MU spells](!magic --rest '+tokenID+'|long|MU)\n'
+ + '[Memorise MU spells](!magic --mem-spell MU|'+tokenID+')\n'
+ + '[View MU Spellbook](!magic --view-spell MU|'+tokenID+')}}';
+
+ sendResponse( charCS, content, senderId, flags.feedbackName, flags.feedbackImg, tokenID );
+ return;
+ };
+
+ var makePRSpellsMenu = function( args, senderId ) {
+
+ var tokenID = args[0],
+ curToken = getObj('graphic',tokenID),
+ charCS = getCharacter(tokenID);
+
+ if (!charCS) {
+ sendDebug('makePRSpellsMenu: invalid tokenID parameter');
+ sendError('Invalid MagicMaster parameter');
+ return;
+ }
+ var content = '&{template:'+fields.menuTemplate+'} {{name='+curToken.get('name')+'\'s Clerical Spells menu}}'
+ + '{{desc=[Cast Priest spell](!magic --cast-spell PR|'+tokenID+')\n'
+ + ((apiCommands.rounds && apiCommands.rounds.exists) ? ('[Show an Area of Effect](!rounds --aoe '+tokenID+')\n') : ('Show an Area of Effect'))
+ + '[Short Rest for L1 Priest Spells](!magic --rest '+tokenID+'|short|PR)\n'
+ + '[Long Rest and recover Priest spells](!magic --rest '+tokenID+'|long|PR)\n'
+ + '[Memorise Priest spells](!magic --mem-spell PR|'+tokenID+')\n'
+ + '[View Priest Spellbook](!magic --view-spell PR|'+tokenID+')}}';
+
+ sendResponse( charCS, content, senderId, flags.feedbackName, flags.feedbackImg, tokenID );
+ return;
+ };
+
+ var makePowersMenu = function( args, senderId ) {
+
+ var tokenID = args[0],
+ curToken = getObj('graphic',tokenID),
+ charCS = getCharacter(tokenID);
+
+ if (!charCS) {
+ sendDebug('makePowersMenu: invalid tokenID parameter');
+ sendError('Invalid MagicMaster parameter');
+ return;
+ }
+ var content = '&{template:'+fields.menuTemplate+'} {{name='+curToken.get('name')+'\'s Powers menu}}'
+ + '{{desc=[2. Use Power](!magic --cast-spell POWER|'+tokenID+')\n'
+ + '[3. Long Rest](!magic --rest '+tokenID+'|LONG)\n'
+ + '[4. Memorise Powers](!magic --mem-spell POWER|'+tokenID+')\n'
+ + '[4. View Powers](!magic --view-spell POWER|'+tokenID+')}}';
+
+ sendResponse( charCS, content, senderId, flags.feedbackName, flags.feedbackImg, tokenID );
+ return;
+ };
+
+ /**
+ * Make a menu to ask the Player which class they want a
+ * requested level drain (or boost) to be applied to.
+ **/
+
+ var makeLevelDrainMenu = function( args, classes, senderId, msg, totalHP ) {
+
+ var tokenID = args[0],
+ drainLevels = parseInt(args[1]) || -1,
+ absLevels = Math.abs(drainLevels),
+ multiLevels = absLevels > 1,
+ content = '&{template:'+fields.menuTemplate+'}{{title=Level '+(drainLevels > 0 ? 'Boost' : 'Drain')+'}}'+(msg ? '{{Section='+msg+'}}' : '')
+ + '{{desc='+getObj('graphic',tokenID).get('name')+' is being '+(drainLevels > 0 ? 'boosted by' : 'drained of')+' '+absLevels+' level'+(multiLevels ? 's' : '')
+ + '. Which class do you want/have to '+(drainLevels > 0 ? 'gain' : 'lose')+' the '
+ + (multiLevels > 1 ? 'next one level? You will then be asked which levels to drain the rest of the levels from, one at a time.' : 'level from?')
+ + '}}{{desc1=';
+
+ _.each( classes, c => {
+ content += 'Level '+c.level+' ['+(c.classData.name || c.name)+'](!magic --button '+BT.LEVEL_CHANGE+'|'+tokenID+'|'+drainLevels+'|'+c.base+'|'+args[3]+'|?{How many HP to '+(drainLevels > 0 ? 'add' : 'deduct')+'|'+c.classData.hd+'}|'+totalHP+')\n';
+ });
+ content += '}}';
+ sendResponse( getCharacter(tokenID), content );
+ return;
+ }
+
+ /*
+ * Menu to ask the user to confirm that they want
+ * to blank the specified repeating table
+ */
+
+ var menuConfirmBlank = function( args, question, senderId ) {
+
+ var cmd = args[0],
+ tokenID = args[1],
+ charCS = getCharacter(tokenID),
+ content = '&{template:'+fields.menuTemplate+'}{{name=Confirm Action}}'
+ + '{{desc='+question+'}}'
+ + '{{desc1=[Yes](!magic --button '+args[0]+'|'+tokenID+') or [No](!magic --button '+BT.ANSWER_NO+'|'+tokenID+')}}';
+
+ sendResponse(charCS,content,senderId, flags.feedbackName, flags.feedbackImg, tokenID);
+ }
+
+ /*
+ * Display a menu to add spells and powers to spell-storing magic items
+ */
+
+ async function makeSpellsMenu( args, senderId, msg='' ) {
+
+ try {
+ var lists = args[0].toUpperCase(),
+ tokenID = args[1],
+ item = args[2].dispName(),
+ miName = args[2].hyphened(),
+ cmd = args[3].toUpperCase(),
+ level = parseInt(args[4]) || 1,
+ retMenu = (args[5] || 'VIEW-ITEM').toUpperCase(),
+ spellName = args[6].dispName(),
+ spell = args[6].hyphened(),
+ charCS = getCharacter(tokenID),
+ isMU = cmd.includes('MU'),
+ isPR = cmd.includes('PR'),
+ isPower = cmd.includes('POWER'),
+ storeBoth = lists.includes('BOTH'),
+ storeSpells = lists.includes('SPELLS'),
+ storePowers = lists.includes('POWERS'),
+ curSpells = '',
+ storedSelected = false,
+ pwList = [fields.ItemPowersList[0]+miName,fields.ItemPowersList[1]],
+ pwVals = [fields.ItemPowerValues[0]+miName,fields.ItemPowerValues[1]],
+ muList = [fields.ItemMUspellsList[0]+miName,fields.ItemMUspellsList[1]],
+ muVals = [fields.ItemMUspellValues[0]+miName,fields.ItemMUspellValues[1]],
+ prList = [fields.ItemPRspellsList[0]+miName,fields.ItemPRspellsList[1]],
+ prVals = [fields.ItemPRspellValues[0]+miName,fields.ItemPRspellValues[1]],
+ nextLevel, minLevel, rootDB, listAttr, listType,
+ storedSpellsAttr, storedLevelAttr, choice,
+ spellObj, cmdStr, shortCmdStr, desc, question, content;
+
+ lists = storeBoth ? 'BOTH' : (storePowers ? 'POWERS' : 'SPELLS');
+
+ if (isPower) {
+ desc = 'Powers';
+ choice = ' a power';
+ rootDB = fields.PowersDB;
+ storedSpellsAttr = pwList;
+ listType = ['power','itempower'];
+ minLevel = 1;
+ question = 'Cast how many per day (-1 means unlimited)?';
+ } else if (isMU) {
+ desc = storePowers ? 'Powers' : 'Stored Wizard spells';
+ choice = ' a level '+level+' Wizard spell',
+ rootDB = fields.MU_SpellsDB;
+ storedSpellsAttr = storePowers ? pwList : muList;
+ listType = ['muspelll'+level,'itemspell'];
+ minLevel = spellsPerLevel.wizard.MU[level].findIndex(num => num > 0);
+ question = 'Cast at what level (normal min caster level '+minLevel+')?';
+ } else if (isPR) {
+ desc = storePowers ? 'Powers' : 'Stored Priest spells';
+ choice = ' a level '+level+' Priest spell',
+ rootDB = fields.PR_SpellsDB;
+ storedSpellsAttr = storePowers ? pwList : prList;
+ listType = ['prspelll'+level,'itemspell'];
+ minLevel = spellsPerLevel.priest.PR[level].findIndex(num => num > 0);
+ question = 'Cast at what level (normal min caster level '+minLevel+')?';
+ } else {
+ return;
+ }
+
+ args.shift();
+ shortCmdStr = [tokenID,item,cmd,level,retMenu].join('|');
+ cmdStr = shortCmdStr+'|'+spell;
+
+ if (charCS) {
+ setAttr( charCS, fields.Casting_name, charCS.get('name'));
+ setAttr( charCS, fields.CastingLevel, minLevel );
+ curSpells = attrLookup( charCS, storedSpellsAttr ) || '';
+ if (_.isUndefined(attrLookup( charCS, pwList ))) setAttr( charCS, pwList, '' );
+ if (_.isUndefined(attrLookup( charCS, pwVals ))) setAttr( charCS, pwVals, '' );
+ if (_.isUndefined(attrLookup( charCS, muList ))) setAttr( charCS, muList, '' );
+ if (_.isUndefined(attrLookup( charCS, muVals ))) setAttr( charCS, muVals, '' );
+ if (_.isUndefined(attrLookup( charCS, prList ))) setAttr( charCS, prList, '' );
+ if (_.isUndefined(attrLookup( charCS, prVals ))) setAttr( charCS, prVals, '' );
+ }
+
+ content = '&{template:'+fields.menuTemplate+'}{{name=Store Spells & Powers}}{{Section='+(msg||'')+'}}'
+ + '{{Section1=**How to use this menu**\nThe [Choose] button selects a spell of the type indicated. It can then be reviewed or stored. *Powerful* items can store Wizard & Priest spells as Powers.'
+ + ' *Spell Storing* items only store spells. To *Remove* a stored spell, select its name and the [Remove] button will appear}}'
+ + '{{'+desc+'=';
+
+ curSpells = curSpells.split(',').filter(e=>!!e);
+ for (let storedSpell of curSpells) {
+ let selected = storedSpell.dbName() === spell.dbName();
+ storedSelected = storedSelected || selected;
+ content += (selected ? (''+storedSpell.dispName()+'') : ('['+storedSpell.dispName()+'](!magic --button CHOOSE_'+lists+'|'+shortCmdStr+'|'+storedSpell+')'));
+ }
+ let spellList = getMagicList( rootDB, spTypeLists, listType, senderId );
+ content += '}}{{desc=1. [Choose](!magic --button CHOOSE_'+lists+'|'+shortCmdStr+'|?{Choose which spell|'+spellList+'})'+choice+'\n';
+
+ if (spell) {
+ let trueName = spell;
+ if (storePowers) {
+ spellObj = findPower( charCS, spell );
+ rootDB = spellObj.dB;
+ trueName = spellObj.obj ? spellObj.obj[1].name : spell;
+ }
+ spellObj = getAbility( rootDB, trueName, charCS );
+ content += '...Optionally [Review '+spellName+'](!magic --button REVIEW_'+lists+'|'+ cmdStr
+ + '
'+(spellObj.api ? '' : sendToWho(charCS,senderId,false,true))+'%{'+ spellObj.dB +'|'+ (trueName.hyphened()) +'})}}';
+ } else {
+ content += '...Optionally Review choice}}';
+ }
+
+ content += '{{desc1=2. ';
+ if (!isPower && (storeSpells || storeBoth)) {
+ content += 'Store '+(spell ? ('**'+spellName+'**') : 'the spell' ) + ' as a ' + (spell ? '[' : (''))
+ + 'stored ' + (!isPR ? 'Wizard' : 'Priest') + ' spell' + (spell ? '](!magic --button ADD_TO_'+lists+'|'+cmdStr+'|?{'+question+'})' : '' );
+ }
+ if (storePowers || storeBoth) {
+ content += ((!isPower && storeBoth ? ' or ' : '') + 'Store '+(storeBoth ? 'it' : (spell ? ('**'+spellName+'**') : 'the spell'))+' '
+ + (spell ? '[' : '')+'as a Power'+(spell ? '](!magic --button ADD_PWR_TO_'+lists+'|'+cmdStr+'|?{Cast how many per day (-1 means unlimited)?}|?{'+question+'})' : '' ));
+ }
+ if (storedSelected) {
+ content += ' or '+(spell ? '[' : '')+'Remove '+(spell ? spellName+'](!magic --button DEL_'+(storePowers ? 'PWR_FROM_' : '')+lists+'|'+cmdStr+')' : 'the spell' );
+ }
+
+ content += '}}{{desc2=3. Choose and Store more spells or\n';
+ if (isPower) {
+ content += 'go to [Wizard](!magic --store-spells '+tokenID+'|'+item+'|MU-ALL|1|'+retMenu+') or [Priest](!magic --store-spells '+tokenID+'|'+item+'|PR-ALL|1|'+retMenu+') spells';
+ } else if (isMU) {
+ content += 'go to [Level '+(level < 9 ? level+1 : 1)+'](!magic --store-spells '+tokenID+'|'+item+'|MUSPELLS'+(storeBoth?'-ALL':'')+'|'+(level < 9 ? level+1 : 1)+'|'+retMenu+') or go to [Priest](!magic --store-spells '+tokenID+'|'+item+'|PRSPELLS'+(storeBoth?'-ALL':'')+'|1|'+retMenu+') spells'+(!storeBoth && !storePowers ? '' : (' or go to [Powers](!magic --store-spells '+tokenID+'|'+item+'|POWERS-ALL|1|'+retMenu+')'));
+ } else if (isPR) {
+ content += 'go to [Level '+(level < 7 ? level+1 : 1)+'](!magic --store-spells '+tokenID+'|'+item+'|PRSPELLS'+(storeBoth?'-ALL':'')+'|'+(level < 7 ? level+1 : 1)+'|'+retMenu+') or go to [Wizard](!magic --store-spells '+tokenID+'|'+item+'|MUSPELLS'+(storeBoth?'-ALL':'')+'|1|'+retMenu+') spells'+(!storeBoth && !storePowers ? '' : (' or go to [Powers](!magic --store-spells '+tokenID+'|'+item+'|POWERS-ALL|1|'+retMenu+')'));
+ }
+ if (retMenu !== 'VIEW-ITEM') {
+ content += ' or\n[Return to Add Items Menu](!magic --gm-edit-mi '+tokenID+')';
+ } else {
+ let miObj = getAbility( fields.MagicItemDB, miName, charCS );
+ content += ' or\n[Return to '+item+' Description](!
/w gm %{'+miObj.dB+'|'+(miName.hyphened())+'})';
+ }
+ content += 'or just do something else}}';
+ sendFeedback(content,flags.feedbackName,tokenID,charCS);
+ return;
+ } catch (e) {
+ sendCatchError('MagicMaster',msg_orig[senderId],e);
+ }
+ }
+
+// ------------------------------------------------------------ Menu Button Press Handlers --------------------------------------------
+
+ /**
+ * Handle the selection of an option button on a menu,
+ * usually used to set short or long menus.
+ */
+
+ var handleOptionButton = function( args, senderId ) {
+
+ var cmd = args[0].toUpperCase(),
+ isView = cmd.includes('VIEW'),
+ tokenID = args[1],
+ optionValue = args[2].toLowerCase(),
+ config = getSetPlayerConfig( senderId ) || {};
+
+ if (!['short','long','alpha','full'].includes(optionValue)) {
+ sendError( 'Invalid MagicMaster menuType option.' );
+ return;
+ }
+
+ switch (args[0].toUpperCase()) {
+
+ case BT.VIEWMI_OPTION:
+ case BT.USEMI_OPTION:
+ config.viewUseMIType = optionValue;
+ getSetPlayerConfig( senderId, config );
+ makeViewUseMI( [(isView ? BT.VIEW_MI : BT.USE_MI), tokenID, -1], senderId );
+ break;
+ case BT.EDITMI_OPTION:
+ case BT.EDITMARTIAL_OPTION:
+ case BT.EDITALLITEMS_OPTION:
+ config.editBagType = optionValue;
+ getSetPlayerConfig( senderId, config );
+ makeEditBagMenu( [(cmd == BT.EDITMI_OPTION ? BT.EDIT_MI :(cmd == BT.EDITMARTIAL_OPTION ? BT.EDIT_MARTIAL : BT.EDIT_ALLITEMS)), tokenID, -1, ''], senderId, 'Using '+optionValue+' Edit MI Bag menu' );
+ break;
+ case BT.PICKMI_OPTION:
+ config.pickUpMIType = optionValue;
+ getSetPlayerConfig( senderId, config );
+ makeShortPOPmenu( ['POPmenu',tokenID,-1,args[3],args[4],-1], senderId );
+ break;
+ case BT.PUTMI_OPTION:
+ config.putAwayMIType = optionValue;
+ getSetPlayerConfig( senderId, config );
+ makeShortPOPmenu( ['POPmenu',tokenID,-1,args[3],args[4],-1], senderId );
+ break;
+ case BT.ALPHALIST_OPTION:
+ config.alphaLists = optionValue === 'alpha';
+ getSetPlayerConfig( senderId, config );
+ let menu = (args[3] || '').toUpperCase();
+ let msg = 'Using '+(optionValue ? 'alphabeticised' : 'long')+' item lists';
+ switch (menu.toUpperCase()) {
+ case BT.EDIT_MI:
+ case BT.EDIT_MARTIAL:
+ case BT.EDIT_ALLITEMS:
+ makeEditBagMenu( [menu, tokenID, -1, ''], senderId, msg);
+ break;
+ case 'GMONLY':
+ makeGMonlyMImenu( ['',tokenID,-1,''], senderId, msg, config.alphaLists );
+ break;
+ default:
+ sendError( 'Invalid MagicMaster option. [Show Help](!magic --help)');
+ break;
+ }
+ break;
+ default:
+ sendError( 'Invalid MagicMaster option. [Show Help](!magic --help)');
+ break;
+ };
+ return;
+ }
+
+ /**
+ * Handle specification of a different number of Misc spells
+ */
+
+ var handleSetMiscSpell = function( args, senderId ) {
+
+ var tokenID = args[1],
+ spellClass = args[2],
+ level = args[3],
+ noSpells = args[4] || 0,
+ charCS = getCharacter(tokenID);
+
+ if (spellClass == 'MU') {
+ setAttr( charCS, [fields.MUSpellNo_table[0] + level + fields.MUSpellNo_misc[0],fields.MUSpellNo_misc[1]], noSpells );
+ } else {
+ setAttr( charCS, [fields.PRSpellNo_table[0] + level + fields.PRSpellNo_misc[0],fields.PRSpellNo_misc[1]], noSpells );
+ }
+ args = [args[5],args[1],args[3],-1,-1,'',1];
+ makeManageSpellsMenu( args, senderId, 'Modified misc = '+noSpells );
+ return;
+ }
+
+ /**
+ * Handle the results of pressing a spell-selection button
+ * or a power-selection button
+ **/
+
+ var handleChooseSpell = function( args, senderId ) {
+
+ if (args[3].length == 0 || isNaN(args[3]) || args[4].length == 0 || isNaN(args[4])) {
+ sendDebug('handleChooseSpell: invalid row or column');
+ sendError('Internal MagicMaster error');
+ }
+
+ if (args[0] == BT.MI_SPELL || args[0] == BT.MI_SCROLL || args[0].toUpperCase().includes('POWER')) {
+ var charCS = getCharacter(args[1]),
+ storedLevel = attrLookup( charCS, fields.Spells_storedLevel, fields.Spells_table, args[3], args[4] );
+ if (storedLevel && storedLevel > 0) {
+ setAttr( charCS, fields.CastingLevel, storedLevel );
+ setAttr( charCS, fields.MU_CastingLevel, storedLevel );
+ setAttr( charCS, fields.PR_CastingLevel, storedLevel );
+ }
+ }
+
+ makeCastSpellMenu( args, senderId );
+ return;
+
+ }
+
+ /**
+ * Handle a selected spell being cast
+ */
+
+ var handleCastSpell = function( args, senderId ) {
+
+ const setValue = (...a) => libRPGMaster.setAttr(...a);
+
+ var tokenID = args[1],
+ rowIndex = args[3],
+ colIndex = args[4],
+ charCS = getCharacter(tokenID),
+ db, action,
+ delScrollSpell = function ( charCS, spellName, scrollName, nameField, valueField ) {
+ spellName = spellName.dbName();
+ scrollName = scrollName.replace(/\s/g,'-');
+ var muSpellList = (attrLookup( charCS, [nameField[0]+scrollName, nameField[1]] ) || '').split(','),
+ nameIndex = _.findIndex( muSpellList, e => e.dbName() == spellName );
+ if (nameIndex >= 0) {
+ muSpellList.splice( nameIndex, 1 );
+ setValue( charCS, [nameField[0]+scrollName, nameField[1]], muSpellList.join(',') );
+ muSpellList = (attrLookup( charCS, [valueField[0]+scrollName, valueField[1]] ) || '').split(',');
+ muSpellList.splice( nameIndex, 1 );
+ setValue( charCS, [valueField[0]+scrollName, valueField[1]], muSpellList.join(',') );
+ }
+ return !muSpellList.filter(t => t.length).length;
+ };
+
+ if (!charCS) {
+ sendDebug('handleCastSpell: invalid tokenID parameter');
+ sendError('Internal MagicMaster error');
+ return;
+ }
+ if (args[3].length == 0 || isNaN(args[3]) || args[4].length == 0 || isNaN(args[4])) {
+ sendDebug('handleCastSpell: invalid row or column');
+ sendError('Internal MagicMaster error');
+ }
+
+ var oldVer = 2.1 > csVer(charCS),
+ spellTables = getTable( charCS, fieldGroups.SPELLS, colIndex ),
+ spellName = spellTables.tableLookup( fields.Spells_name, rowIndex ).hyphened(),
+ spellMsg = spellTables.tableLookup( (oldVer ? fields.Spells_macro : fields.Spells_msg), rowIndex ),
+ charName = charCS.get('name'),
+ absorb = false,
+ miName = '',
+ miRowRef;
+
+ switch (args[0].toUpperCase()) {
+ case BT.CAST_MIPOWER:
+ miName = attrLookup( charCS, fields.ItemChosen );
+ case BT.USE_POWER:
+ db = spellTables.tableLookup( fields.Spells_db, rowIndex );
+ if (!db || db == spellName) {
+ db = findPower( charCS, spellName ).dB;
+ spellTables = spellTables.tableSet( fields.Spells_db, rowIndex, db );
+ }
+ action = 'using';
+ break;
+ case BT.CAST_MUSPELL:
+ db = fields.MU_SpellsDB;
+ action = 'casting';
+ absorb = args[5] === 'true';
+ break;
+ case BT.CAST_PRSPELL:
+ db = fields.PR_SpellsDB;
+ action = 'casting';
+ absorb = args[5] === 'true';
+ break;
+ case BT.CAST_SCROLL:
+ case BT.CAST_MISPELL:
+ db = spellTables.tableLookup( fields.Spells_db, rowIndex );
+ miName = attrLookup( charCS, fields.ItemChosen );
+ action = 'using their magic item to cast';
+ spellMsg = '';
+ break;
+ }
+
+ var spell = getAbility( db, spellName, charCS ),
+ spellCost = ((!!spell.obj && !!spell.ct && ((args[0] === BT.CAST_MUSPELL) || (args[0] === BT.CAST_PRSPELL))) ? spell.obj[1].cost : 0),
+ totalLeft,
+ content,
+ spellValue = parseInt((spellTables.tableLookup( fields.Spells_castValue, rowIndex )),10);
+
+ setValue( charCS, fields.SpellToMem, spellName );
+ setValue( charCS, fields.Expenditure, spellCost );
+ setValue( charCS, fields.SpellRowRef, rowIndex );
+ setValue( charCS, fields.SpellColIndex, colIndex );
+
+ if (absorb) {
+ let level = (!spell.obj || !spell.obj[1]) ? 1 : (parseInt(spell.obj[1].type.match(/\d+/)) || 0),
+ itemRow = parseInt(attrLookup( charCS, fields.ItemRowRef ));
+ if (isNaN(itemRow)) {
+ let Items = getTable( charCS, fieldGroups.MI );
+ itemRow = parseInt(Items.tableFind( fields.Items_name, miName ));
+ };
+ if (!isNaN(itemRow)) {
+ Items = Items.tableSet( fields.Items_qty, itemRow, Math.max(parseInt(Items.tableLookup( fields.Items_qty, itemRow ) || 0)-level,0) );
+ Items = Items.tableSet( fields.Items_trueQty, itemRow, Math.max(parseInt(Items.tableLookup( fields.Items_trueQty, itemRow ) || 0)-level,0) );
+ }
+ } else if (spellValue != 0) {
+
+ if (apiCommands.attk && apiCommands.attk.exists && !!spell.obj && !!spell.obj[1] && spell.obj[1].body.match(/}}\s*tohitdata\s*=\s*\[.*?\]/im)) {
+ sendAPI(fields.attackMaster+' '+senderId+' --weapon '+tokenID+'|Take '+spellName+' in-hand as a weapon and then Attack with it||'+miName);
+ } else {
+ if (spellValue > 0) spellValue--;
+ spellTables.tableSet( fields.Spells_castValue, rowIndex, spellValue );
+ }
+ }
+ setValue( charCS, fields.SpellCharges, spellValue );
+ if (args[0] == BT.CAST_SCROLL && spellValue == 0) {
+ spellTables.addTableRow( rowIndex ); // Blanks this table row
+ if (delScrollSpell( charCS, spellName, miName, fields.ItemMUspellsList, fields.ItemMUspellValues ) &&
+ delScrollSpell( charCS, spellName, miName, fields.ItemPRspellsList, fields.ItemPRspellValues )) {
+ if (!_.isUndefined(miRowRef = attrLookup( charCS, fields.ItemRowRef ))) {
+ getTable( charCS, fieldGroups.MI ).addTableRow( miRowRef ); // Blanks this table row
+ }
+ }
+ }
+
+ if (spellMsg.length > 0) {
+ sendResponse( charCS, spellMsg, senderId, flags.feedbackName, flags.feedbackImg, tokenID );
+ }
+
+ totalLeft = spendMoney( charCS, spellCost );
+ content = charName + ' is '+action+' [' + spellName.dispName() + '](!
/w gm %{'+spell.dB+'|'+(spellName.hyphened())+'})'
+ + (parseInt(spellCost || 0) ? (' at a cost of [[' + spellCost + ']]GP (leaving [[' + totalLeft + ']]GP).') : '')
+ + ' Select ' + charName + '\'s token before pressing to see effects.';
+ sendFeedback( content, flags.feedbackName, flags.feedbackImg, tokenID, charCS );
+
+ return;
+ }
+
+ /*
+ * Handle targeting the effects of a spell
+ * Moved to RoundMaster to allow passing of the PlayerID
+ */
+
+ var handleSpellTargeting = function( args, isGM ) {
+
+ var tokenID = args[0],
+ curToken = getObj('graphic',tokenID),
+ tokenName,
+ thac0,
+ strHitBonus,
+ content,
+ charCS = getCharacter(tokenID);
+
+ if (!charCS) {
+ sendDebug('handleSpellTargeting: invalid tokenID parameter');
+ sendError('Internal MagicMaster error');
+ return;
+ }
+
+ if (!apiCommands.rounds || !apiCommands.rounds.exists) {
+ sendError('RoundMaster API must be loaded for spell targeting to work');
+ return;
+ }
+
+ args.shift();
+ tokenName = curToken.get('name');
+ thac0 = getTokenValue( curToken, fields.Token_thac0, fields.Thac0_base, fields.MonsterThac0, fields.Thac0_base ).val || 20;
+ strHitBonus = attrLookup( charCS, fields.Strength_hit ) || 0;
+ content = (isGM ? '/w gm ' : '')+'&{template:'+fields.menuTemplate+'}{{name=Try to Touch Target}}'
+ + '{{desc=**'+tokenName+' hits AC [[( ([['+thac0+']][Thac0]) - ([['+strHitBonus+']][Strength bonus]) - [[1d20]][Dice Roll] )]] with their spell.**}}'
+ + '{{desc1=If hit, [Cast on them](!rounds --target SINGLE|'+tokenID+'|@{target|Who to Attack with this spell?|token_id}|'+args.join('|')+')}}';
+ setAbility( charCS, 'To-Hit-Spell', content );
+ return;
+
+ }
+
+ /*
+ * Handle redisplaying the manage spells menu
+ * Used when selecting a spell or slot to memorise,
+ * or when changing level of spell to memorise.
+ */
+
+ var handleRedisplayManageSpells = function( args, senderId ) {
+
+ var isPower = args[0].toUpperCase().includes('POWER'),
+ msg = '',
+ name = getObj('graphic',args[1]).get('name');
+
+ if (args[3] > 0 && args[4] > 0 && (!args[5] || !args[5].length)) {
+ args[5] = attrLookup( getCharacter(args[1]), fields.Spells_name, fields.Spells_table, args[3], args[4] );
+ }
+
+ // Check this is a spell that is of a school that can be memorised
+ if (isPower ? !checkValidPower( args, senderId ) : !checkValidSpell( args )) {
+ msg=isPower ? ('**Warning:** '+name+' has not gained experience enough to use '+args[5]+' as a granted power')
+ : ('**Warning:** '+args[5]+' is not of a school or sphere '+name+' can use');
+ args[5] = '';
+ } else {
+ if ((args[3] >= 0 && args[4] >= 0) || (args[5] && args[5].length > 0)) {
+ msg += 'Selected ';
+ }
+ if (args[5] && args[5].length > 0) {
+ msg += args[5] + ' to store';
+ }
+ if (args[3] >= 0 && args[4] >= 0 && args[5] && args[5].length > 0) {
+ msg += ' and ';
+ }
+ if (args[3] >= 0 && args[4] >= 0) {
+ msg += 'a slot to store it in.';
+ }
+ }
+ makeManageSpellsMenu( args, senderId, msg );
+ return;
+ }
+
+ /*
+ * Review a chosen spell description
+ */
+
+ var handleReviewSpell = function( args, senderId ) {
+
+ var cmd = args[0].toUpperCase(),
+ isMU = cmd.includes('MU'),
+ isPR = cmd.includes('PR'),
+ isMI = cmd.includes('MI'),
+ isPower = cmd.includes('POWER'),
+ isSpell = cmd.includes('SPELL'),
+ isScroll = cmd.includes('SCROLL'),
+ isView = !cmd.includes('REVIEW'),
+ isGM = args[0].includes('GM'),
+ tokenID = args[1],
+ followOn,
+ msg,
+ charCS = getCharacter(tokenID);
+
+ if (!charCS) {
+ sendDebug('handleReviewSpell: invalid tokenID parameter');
+ sendError('Internal MagicMaster error');
+ return;
+ }
+
+ if (isMI) {
+ if (isGM) {
+ followOn = 'GM-MImenu';
+ } else if (isPower) {
+ followOn = (isView ? BT.VIEWMEM_MI_POWERS : BT.EDIT_MIPOWERS);
+ } else if (isMU) {
+ followOn = (isView ? BT.VIEWMEM_MI_MUSPELLS : BT.EDIT_MIMUSPELLS);
+ } else if (isPR) {
+ followOn = (isView ? BT.VIEWMEM_MI_PRSPELLS : BT.EDIT_MIPRSPELLS);
+ } else if (isSpell) {
+ followOn = (isView ? BT.VIEWMEM_MI_SPELLS : BT.EDIT_MISPELLS);
+ } else if (isScroll) {
+ followOn = (isView ? BT.VIEWMEM_MI_SCROLL : BT.EDIT_MISPELLS);
+ } else {
+ followOn = (isView ? BT.VIEW_MI : (args[0].includes('MARTIAL') ? BT.CHOOSE_MARTIAL_MI : (args[0].includes('ALLITEMS') ? BT.CHOOSE_ALLITEMS_MI : BT.CHOOSE_MI)));
+ }
+ } else if (isPower) {
+ followOn = (isView ? BT.VIEWMEM_POWERS : BT.EDIT_POWERS);
+ } else if (isMU) {
+ followOn = (isView ? BT.VIEWMEM_MUSPELLS : BT.EDIT_MUSPELLS);
+ } else {
+ followOn = (isView ? BT.VIEWMEM_PRSPELLS : BT.EDIT_PRSPELLS);
+ }
+
+ args[0] = followOn;
+ msg = '[Return to menu](!magic --button '+args.join('|')+')';
+ setTimeout(() => sendResponse( charCS, msg, senderId, flags.feedbackName, flags.feedbackImg, tokenID ),500);
+ return;
+ }
+
+ /*
+ * Handle learning a spell from a spellbook or scroll
+ */
+
+ var handleLearnSpell = function( args, senderId ) {
+
+ var cmd = (args[0] || ''),
+ tokenID = args[1],
+ spell = (args[2] || ''),
+ learnt = cmd.toUpperCase().includes('LEARNT'),
+ charCS = getCharacter(tokenID),
+ spellObj,spellData,level;
+
+ spellObj = abilityLookup( fields.MU_SpellsDB, spell, charCS );
+ if (!spellObj.obj) {
+ sendError('The spell '+spell+' has not been found in any database.',msg_orig[senderId]);
+ return;
+ }
+ spellData = parseData((spellObj.data()[0][0] || {}),reSpellSpecs);
+ level = spellData.level;
+ if (!level || level < 1 || level > 9) {
+ sendError('The spell '+spell+' is of an unrecognised level '+level,msg_orig[senderId]);
+ return;
+ }
+
+ var content = '&{template:RPGMdefault}{{name=Add spell to '+charCS.get('name')+'\'s spellbook}}{{desc=',
+ name = getObj('graphic',tokenID).get('name'),
+ spellbook = [fields.Spellbook[0]+spellLevels.mu[level].book,fields.Spellbook[1]],
+ curList = (attrLookup(charCS,spellbook) || ''),
+ saveObj = saveFormat.Checks.Learn_Spell,
+ save = parseInt(attrLookup( charCS, saveObj.save ) || 0),
+ saveMod = parseInt(attrLookup( charCS, saveObj.mod ) || 0),
+ saveAdj = parseInt(attrLookup( charCS, fields.Magic_saveAdj ) || 0),
+ saveSpec = checkValidSpell( ['MU',tokenID,'','','',spell] ),
+ specMod = saveSpec > 2 ? 15 : (saveSpec > 1 ? -15 : 0),
+ learnChance = Math.max(5,Math.min((save-saveMod-saveAdj+specMod),99));
+
+ if (!saveSpec) {
+ content += 'The spell '+spell+' is of a school and/or level that '+name+' cannot learn!';
+ } else if (curList.toLowerCase().includes(spell.toLowerCase())) {
+ content += 'The spell '+spell+' is already in '+charCS.get('name')+'\'s spellbook';
+ } else if (!learnt) {
+ args.shift();
+ let checkMacro = '&{template:RPGMdefault}{{name='+name+' Check vs Learn Spell}}{{Check Throw=Rolling [[?{Learn Spell roll|'+saveObj.roll+'}cf<'+(learnChance-1)+'cs>'+learnChance+']] vs. [[0+'+learnChance+']] target}}{{Result=Check Throw<='+learnChance+'}}{{desc=**'+name+'\'s target**[[0+'+save+']] base save vs. Learn_Spell with [[0+'+specMod+']] change from specialism, [[0+'+saveMod+']] improvement from race, class & Magic Items, and [[0+'+saveAdj+']] improvement from current magic effects}}{{successcmd=!magic --button '+BT.LEARNT_MUSPELL+'|'+args.join('|')+'}}';
+ setAbility(charCS,'Do-not-use-Learn_Spell-save',checkMacro);
+ content += 'Can you learn the spell "'+spell+'"? [Assess your chance](~'+charCS.get('name')+'|Do-not-use-Learn_Spell-save)';
+ } else {
+ setAttr(charCS,spellbook,((curList+'|'+spell).split('|').sort().join('|')));
+ content += 'The spell '+spell+' has been added to '+charCS.get('name')+'\'s spellbook.';
+ }
+ content += '}}';
+ sendResponse(charCS,content,senderId);
+ return;
+ };
+
+
+
+ /*
+ * Return to the spell storing menu after a review
+ */
+
+ var handleRevStore = function( args, senderId ) {
+ let cmd = args.shift().toUpperCase();
+ setTimeout( () => sendFeedback( ('[Return to menu](!magic --button '+cmd.replace('REVIEW','CHOOSE')+'|'+args.join('|')+')'), flags.feedbackName ), 500);
+ }
+
+ /*
+ * Handle memorising a selected spell in a selected slot
+ */
+
+ var handleMemoriseSpell = function( args, senderId ) {
+
+ var isMU = args[0].toUpperCase().includes('MU'),
+ isMI = args[0].toUpperCase().includes('MI'),
+ isPower = args[0].toUpperCase().includes('POWER'),
+ isAll = args[0].toUpperCase().includes('ALL'),
+ tokenID = args[1],
+ level = args[2],
+ row = args[3],
+ col = args[4],
+ spellName = args[5],
+ noToMemorise = parseInt((args[6]),10),
+ castAsLvl = parseInt((args[7]),10),
+ dbCS,
+ charCS = getCharacter(tokenID);
+
+ if (!charCS) {
+ sendDebug('handleMemoriseSpell: invalid tokenID parameter');
+ sendError('Internal MagicMaster error');
+ return;
+ }
+
+ if (args[3].length == 0 || isNaN(args[3]) || args[4].length == 0 || isNaN(args[4])) {
+ sendDebug('handleMemoriseSpell: invalid row or column');
+ sendError('Internal MagicMaster error');
+ }
+
+ if (isNaN(noToMemorise)) {
+ sendResponse(charCS, 'You must specify the number of uses as a number', senderId, flags.feedbackName, flags.feedbackImg, tokenID);
+ return;
+ }
+
+ var rootDB = isPower ? fields.PowersDB : (isMU ? fields.MU_SpellsDB : fields.PR_SpellsDB),
+ spellTables = getTable( charCS, (isPower ? fieldGroups.POWERS : fieldGroups.SPELLS), col ),
+ base = spellLevels[(isPower ? (isMI ? 'pm' : 'pw') : (isMI ? 'mi' : (isMU ? 'mu' : 'pr')))][level].base,
+ altSpellTable;
+
+ if (!isPower) {
+ altSpellTable = getLvlTable( charCS, (isMU ? fieldGroups.ALTWIZ : fieldGroups.ALTPRI), level );
+ } else if (fields.GameVersion === 'AD&D1e') {
+ altSpellTable = getLvlTable( charCS, fieldGroups.ALTPWR );
+ };
+
+ spellTables = setSpell( charCS, spellTables, altSpellTable, rootDB, spellName, row, col-base, level, undefined, '', [noToMemorise,(isPower ? 0 : noToMemorise)], castAsLvl );
+
+ if (isMI && isPower) {
+ setAttr( charCS, ['power-'+spellName, 'current'], row );
+ setAttr( charCS, ['power-'+spellName, 'max'], col );
+ }
+
+ var hand = spellTables.tableLookup( fields.Spells_equip, row );
+
+ if (spellTables.tableLookup( fields.Spells_weapon, row ) === '1' && (hand)) {
+ sendAPI(fields.attackMaster+' --button '+(hand==='2'?BT.BOTH:(hand==='1'?BT.LEFT:(hand==='0'?BT.RIGHT:BT.HAND)))+'|'+tokenID+'|'+row+':'+col+'|'+hand);
+ }
+
+ if (!isAll) {
+ args[3] = -1;
+ args[4] = -1;
+ args[5] = '';
+ args[6] = 1;
+ makeManageSpellsMenu( args, senderId, 'Memorised '+spellName );
+ }
+ return;
+ }
+
+ /*
+ * Handle memorising all currently valid powers at once
+ */
+
+ async function handleMemAllPowers( args, senderId, silent=false ) {
+
+ var cmd = args[0],
+ isMU = cmd.toUpperCase().includes('MU'),
+ isPower = cmd.toUpperCase().includes('POWER'),
+ tokenID = args[1],
+ charCS = getCharacter( tokenID ),
+ spellTables = [],
+ db, type, txt, name, levelSpells;
+
+ var memSpell = function(args,charCS,db,isPower,list,i,r,c,senderId) {
+
+ return new Promise(resolve => {
+ var spellDef, clv = false;
+ try {
+ for (let j=list.length; j > 0 && !clv; j--) {
+ let k = (randomInteger(list.length)-1);
+ spellDef = isPower ? findPower( charCS, list.shift() ) : abilityLookup( db, list[k] );
+ if (spellDef.obj) {
+ args[5] = spellDef.obj[1].name;
+ clv = isPower ? checkValidPower( args, senderId ) : (checkValidSpell( args, senderId ) ? i : 0);
+ if (!clv && !isPower) list.splice(k,1);
+ };
+ };
+ if (clv) {
+ let newArgs = [args[0],args[1],i,r,c,spellDef.obj[1].name,(isPower ? getUsesPerDay(charCS,spellDef.obj[1].name,senderId) : 1),clv];
+ handleMemoriseSpell( newArgs, senderId );
+ };
+ } catch (e) {
+ log('MagicMaster memSpell: JavaScript '+e.name+': '+e.message+' while processing sheet '+charCS.get('name'));
+ sendDebug('MagicMaster memSpell: JavaScript '+e.name+': '+e.message+' while processing sheet '+charCS.get('name'));
+ sendCatchError('MagicMaster',msg_orig[senderId],e);
+ } finally {
+ setTimeout(() => {
+ resolve([list,(clv ? spellDef.obj[1].name : ''),clv]);
+ }, 20);
+ }
+ });
+ }
+
+ if (!charCS) return;
+
+ if (isPower) {
+ type = 'POWER';
+ db = fields.PowersDB;
+ txt = 'powers';
+ } else if (isMU) {
+ type = 'MU';
+ db = fields.MU_SpellsDB;
+ txt = 'wizard spells';
+ } else {
+ type = 'PR';
+ db = fields.PR_SpellsDB;
+ txt = 'priest spells';
+ }
+ levelSpells = shapeSpellbook( charCS, type );
+ for (let i = 1; i < levelSpells.length; i++) {
+ let r = 0;
+ let storeList = false;
+ let newList = [];
+ let list = (attrLookup(charCS, [fields.Spellbook[0]+levelSpells[i].book, fields.Spellbook[1] ]) || '').split('|').filter(t=>!!t);
+ let s = (isPower) ? list.length : levelSpells[i].spells;
+ if (s > 0 && (!list || !list.join('').length || list.join('') == '-')) {
+ list = _.uniq(getMagicList( db, spTypeLists, (isPower ? 'power' : (isMU ? 'muspelll'+i : 'prspelll'+i)), senderId ).toLowerCase().split(/\,|\|/));
+ storeList = true;
+ };
+ let c = levelSpells[i].base;
+ let cellExists = true;
+ while (s > 0 || cellExists) {
+ c = levelSpells[i].base;
+ for (let w = 1; (w <= fields.SpellsCols); w++) {
+ let castAsLevel = false;
+ if (!spellTables[w]) {
+ spellTables[w] = getTable( charCS, fieldGroups.SPELLS, c );
+ }
+ cellExists = !!spellTables[w].tableLookup( fields.Spells_name, r, false );
+ if (s <= 0 && !cellExists) break;
+ spellTables[w].addTableRow( r );
+ if (s > 0){
+ [list,name,castAsLevel] = await memSpell(args,charCS,db,isPower,list,i,r,c,senderId);
+ if (castAsLevel && storeList) newList.push(name);
+ };
+ c++;
+ s--;
+ }
+ r++;
+ };
+ if (storeList) setAttr( charCS, [fields.Spellbook[0]+levelSpells[i].book, fields.Spellbook[1] ], _.uniq(newList.sort()).join('|'));
+ spellTables = [];
+ };
+ if (silent) {
+ sendWait(senderId,0);
+ return;
+ }
+
+ args[3] = -1;
+ args[4] = -1;
+ args[5] = '';
+ args[6] = 1;
+
+ makeManageSpellsMenu( args, senderId, 'Memorised all valid '+txt );
+ return;
+ }
+
+ /*
+ * Handle a level change request
+ */
+
+ var handleLevelDrain = function( args, senderId, msg = '' ) {
+
+ var tokenID = args[0], // tokenID
+ drainLevels = parseInt(args[1]) || -1, // 4
+ fixedClass = args[6] || '', // fighter
+ classChosen = args[2] || fixedClass, // fighter
+ totalLevels = parseInt(args[3]) || drainLevels, // 4
+ hitPoints = Math.abs(parseInt(evalAttr(args[4])) || 0), // [[5d10]]
+ totalHP = parseInt(args[5]) || 0, // always called = 0
+ loopCount = Math.abs(drainLevels),
+ charCS = getCharacter(tokenID),
+ increment = drainLevels > 0 ? 1 : -1,
+ levelHP = hitPoints * increment,
+ classes = classObjects( charCS, senderId ),
+ levelField, hd;
+
+ if ((classes && classes.length === 1) || fixedClass) {
+ classChosen = classChosen || classes[0].base;
+ hitPoints = parseInt(hitPoints || evalAttr(classes[0].classData.hd.replace(/(\d+)(d.+)/i,'(('+String(drainLevels)+'*$1)$2)'))) || 0;
+ levelHP = hitPoints * increment;
+ loopCount = 1;
+ increment = increment * Math.abs(drainLevels);
+ } else if (!classChosen) {
+ makeLevelDrainMenu( args, classes, senderId, msg, totalHP );
+ return;
+ };
+ switch (classChosen.toLowerCase()) {
+ case 'wizard':
+ levelField = fields.Wizard_level;
+ break;
+ case 'priest':
+ levelField = fields.Priest_level;
+ break;
+ case 'rogue':
+ levelField = fields.Rogue_level;
+ break;
+ case 'psion':
+ levelField = fields.Psion_level;
+ break;
+ default:
+ levelField = fields.Fighter_level;
+ if (!attrLookup( charCS, levelField )) {
+ levelField = fields.Monster_hitDice;
+ }
+ }
+ setAttr( charCS, levelField, Math.max(0,((parseInt(attrLookup( charCS, levelField ) || 1) || 1) + increment)) );
+ setAttr( charCS, fields.HP,((parseInt(attrLookup( charCS, fields.HP ) || 0) || 0) + levelHP) );
+ setAttr( charCS, fields.MaxHP, Math.max(0,((parseInt(attrLookup( charCS, fields.MaxHP ) || 0) || 0) + levelHP)) );
+ totalHP += hitPoints;
+ if (--loopCount > 0) {
+ let content = '&{template:'+fields.warningTemplate+'}{{title=Change in Level}}{{desc=Successfully '+(increment > 0 ? 'boosted' : 'drained')+' '+classChosen
+ + ' class by one level, which in total makes '+(Math.abs(totalLevels) - loopCount)+' across all classes.'
+ + ' A total of '+totalHP+'HP have been '+(increment > 0 ? 'gained' : 'lost')+'}}';
+ sendResponse( charCS, content );
+ handleLevelDrain( [tokenID,(drainLevels-increment),'',totalLevels,0,totalHP,fixedClass], senderId, 'Successfully '+(increment > 0 ? 'boosted' : 'drained')+' '+classChosen+' class by 1 level' );
+ } else {
+ setAttr( charCS, fields.Thac0_base, handleGetBaseThac0( charCS ) );
+ let content = '&{template:'+fields.warningTemplate+'}{{title=Change in Level}}{{desc=Successfully '+(increment > 0 ? 'boosted' : 'drained')+' '+classChosen
+ + ' class by '+((fixedClass || Math.abs(increment) > 1) ? (totalLevels+' levels') : ('one level, which in total makes '+totalLevels+' across all classes'))
+ + ', and recalculated all saves, reassessed all weapon use and reset usable powers.'
+ + ' A total of '+totalHP+'HP have been '+(increment > 0 ? 'gained' : 'lost')+'}}';
+ sendResponse( charCS, content );
+ setTimeout( () => handleMemAllPowers( [BT.MEMALL_POWERS,tokenID,1,-1,-1,'',''], senderId, true ), 100);
+ setTimeout( () => handleCheckWeapons( tokenID, charCS ), 200);
+ setTimeout( () => handleCheckSaves( [tokenID], null, null, true ), 300);
+ }
+ }
+
+ /*
+ * Handle undertaking a short rest to recover 1st level spells
+ */
+
+ var handleRest = function( args, senderId ) {
+
+ var tokenID = args[0],
+ isShort = args[1].toLowerCase().includes('short'),
+ casterType = (args[2] || 'MU+PR').toUpperCase(),
+ r, c, w,
+ col, rep;
+
+ if (casterType.includes('MI') && casterType.includes('POWER')) {
+ return;
+ }
+
+ var isMU = casterType.includes('MU'),
+ isPR = casterType.includes('PR'),
+ isMI = !isShort,
+ isPower = !isShort,
+ isMIPower = !isShort,
+ charCS = getCharacter(tokenID);
+
+ updateCharSheets( args, senderId );
+
+ if (!charCS) {
+ sendDebug('handleRest: invalid tokenID parameter');
+ sendError('Internal MagicMaster error');
+ return;
+ }
+
+ var levelSpells,
+ level,
+ levelLimit,
+ restType,
+ powerQty,
+ valueObj,
+ spellTables = [];
+
+ while (isMU || isPR || isPower || isMIPower) {
+ restType = (isMIPower ? 'MIPOWER' : (isPower ? 'POWER' : (isMU ? 'MU' : 'PR' )));
+ levelSpells = shapeSpellbook( charCS, restType );
+ level = 1;
+ levelLimit = isShort ? 2 : levelSpells.length;
+
+ while (level < levelLimit && levelSpells[level].spells > 0) {
+ r = 0;
+ while (levelSpells[level].spells > 0) {
+ c = levelSpells[level].base;
+ for (w = 1; (w <= fields.SpellsCols) && (levelSpells[level].spells > 0); w++) {
+ if (_.isUndefined(spellTables[w])) {
+ spellTables[w] = {};
+ }
+ if (_.isUndefined(spellTables[w][fields.Spells_castValue[0]])) {
+ spellTables[w] = getTableField( charCS, spellTables[w], fields.Spells_table, fields.Spells_castValue, c );
+ }
+ valueObj = spellTables[w].tableLookup( fields.Spells_castValue, r, true, true );
+ if (!valueObj) {
+ levelSpells[level].spells = 0;
+ break;
+ }
+ if (restType.includes('POWER')) {
+ if (_.isUndefined(spellTables[w][fields.Spells_castMax[0]])) {
+ spellTables[w] = getTableField( charCS, spellTables[w], fields.Spells_table, fields.Spells_castMax, c, 0 );
+ }
+ valueObj.set( fields.Spells_castValue[1], spellTables[w].tableLookup( fields.Spells_castMax, r ));
+ } else {
+ valueObj.set( fields.Spells_castValue[1], 1 );
+ }
+ c++;
+ levelSpells[level].spells--;
+ }
+ r++;
+ }
+ spellTables = [];
+ level++;
+ }
+
+ switch (restType.toUpperCase()) {
+ case 'MIPOWER':
+ isMIPower = false;
+ break;
+ case 'POWER':
+ isPower = false;
+ break;
+ case 'MU':
+ isMU = false;
+ break;
+ case 'PR':
+ isPR = false;
+ break;
+ }
+ }
+
+ if (isMI) {
+ let miBase = fields.Items_table[1],
+ MagicItems = getTable( charCS, fieldGroups.MI );
+
+ for (r = miBase; r < (MagicItems.sortKeys.length+miBase); r++) {
+ let miSpeedObj = MagicItems.tableLookup( fields.Items_speed, r, true, true ),
+ miQtyObj = MagicItems.tableLookup( fields.Items_qty, r, true, true ),
+ miTrueName = MagicItems.tableLookup( fields.Items_trueName, r ),
+ miType = MagicItems.tableLookup( fields.Items_type, r ),
+ miReveal = MagicItems.tableLookup( fields.Items_reveal, r ).toLowerCase(),
+ ItemSpecs = abilityLookup( fields.MagicItemDB, miTrueName, charCS );
+ if (_.isUndefined(miSpeedObj) || _.isUndefined(miQtyObj)) {break;}
+ if (miTrueName && miTrueName != '-') {
+ if (miReveal == 'rest') {
+ MagicItems = MagicItems.tableSet( fields.Items_name, r, miTrueName );
+ MagicItems = MagicItems.tableSet( fields.Items_type, MIrowref, MagicItems.tableLookup( fields.Items_trueType, MIrowref ));
+ MagicItems = MagicItems.tableSet( fields.Items_reveal, r, '' );
+ }
+ if (ItemSpecs.obj && ItemSpecs.obj[1] && !miType.toLowerCase().includes('recharging') && (/{{ammo=/i.test(ItemSpecs.obj[1].body))) {
+ miQtyObj.set('max',(miQtyObj.get('current')||0));
+ } else if (!miType.toLowerCase().includes('absorbing')) {
+ miQtyObj.set('current',(miQtyObj.get('max')||0));
+ }
+ miSpeedObj.set('current',(miSpeedObj.get('max')||5));
+ }
+ }
+ }
+ return;
+ }
+
+ /*
+ * Handle time passing. Update both the character sheet for
+ * this character, and the global date if it is behind the
+ * character date
+ */
+
+ var handleTimePassing = function( charCS, timeSpent ) {
+
+ timeSpent = Math.ceil(timeSpent);
+ var charDay = parseInt((attrLookup( charCS, fields.CharDay ) || 0),10) + timeSpent,
+ today = parseInt((state.moneyMaster.inGameDay || 0),10),
+ globalDay = Math.max( today, charDay );
+
+ setAttr( charCS, fields.CharDay, globalDay );
+
+ return globalDay;
+ }
+
+ /*
+ * Handle the selection of a magic item
+ * to use or view
+ */
+
+ var handleChooseMI = function( args, senderId ) {
+
+ makeViewUseMI( args, senderId );
+ return;
+ }
+
+ /*
+ * Handle viewing or using a magic item.
+ * The calling of the MI macro from the MI-DB is performed
+ * in the [Submit] button of the menu.
+ */
+
+ var handleViewUseMI = function( args, isSilent, senderId, charges, chargeOverride='' ) {
+
+ var action = args[0].toUpperCase(),
+ tokenID = args[1],
+ MIrowref = parseInt(args[2],10),
+ charCS = getCharacter(tokenID),
+ inHand, inHandRow, content, miData;
+
+ if (!charCS) {
+ sendDebug('handleViewUseMI: invalid tokenID parameter');
+ sendError('Internal MagicMaster error');
+ return;
+ }
+
+ var MItables = getTable( charCS, fieldGroups.MI ),
+ MIname = MItables.tableLookup( fields.Items_name, MIrowref ),
+ MItrueName = MItables.tableLookup( fields.Items_trueName, MIrowref ),
+ MIreveal = MItables.tableLookup( fields.Items_reveal, MIrowref ).toLowerCase();
+
+ setAttr( charCS, fields.ItemChosen, MIname );
+ setAttr( charCS, fields.ItemRowRef, MIrowref );
+
+ if (action.includes('VIEW')) {
+ if (MIreveal == 'view') {
+ MIname = MItables.tableLookup( fields.Items_trueName, MIrowref );
+ MItables = MItables.tableSet( fields.Items_name, MIrowref, MIname );
+ MItables = MItables.tableSet( fields.Items_type, MIrowref, MItables.tableLookup( fields.Items_trueType, MIrowref ));
+ MItables = MItables.tableSet( fields.Items_reveal, MIrowref, '' );
+ }
+ content = '[Return to menu](!magic --button '+BT.CHOOSE_VIEW_MI+'|'+args[1]+'|'+args[2]+')';
+ setTimeout(() => sendResponse( charCS, content, senderId, flags.feedbackName, flags.feedbackImg, tokenID ),500);
+ checkForBag( charCS, MItrueName, MIrowref );
+ return;
+ }
+ if (isNaN(MIrowref) || (fields.Items_table[1] == 0 && MIrowref < 0)) {
+ sendDebug('handleViewUseMI: invalid MIrowref parameter is '+MIrowref);
+ sendError('Internal MagicMaster error');
+ return;
+ }
+
+ var charName = charCS.get('name'),
+ MIqtyObj = MItables.tableLookup( fields.Items_qty, MIrowref, false, true ),
+ MIqty = MIqtyObj.get(fields.Items_qty[1]),
+ MImaxQty = MIqtyObj.get(fields.Items_trueQty[1]),
+ MItype = chargeOverride || MItables.tableLookup( fields.Items_trueType, MIrowref, 'uncharged' ).toLowerCase(),
+ MIdb = getAbility( fields.MagicItemDB, MIname, charCS, null, null, null, MIrowref ),
+ MIchangeTo = '',
+ MIcVal = 1;
+
+ if (MIdb.obj) {
+ miData = resolveData( MIname, fields.MagicItemDB, reItemData, charCS, {charges:reSpellSpecs.charges,changeTo:reSpellSpecs.changeTo,zero:reSpellSpecs.zero}, MIrowref ).parsed;
+ MIcVal = miData.charges;
+ MIchangeTo = miData.changeTo;
+ }
+ MIcVal = parseInt(MIcVal);
+ if (!(_.isUndefined(MIcVal) || isNaN(MIcVal)) && (_.isUndefined(charges) || _.isNull(charges))) {
+ charges = MIcVal;
+ }
+ if (_.isUndefined(charges) || _.isNull(charges)) {
+ charges = 1;
+ }
+ if (MIqty < charges) {
+ content = '&{template:'+fields.menuTemplate+'}{{name=Using '+MIname+'}}{{desc='+MIname+' does not have enough charges left to do this}}'
+ +'{{desc1=[Show '+MIname+' again](\~'+MIdb.dB+'|'+MIname+') or do something else}}';
+ sendResponse( charCS, content, senderId, flags.feedbackName, flags.feedbackImg, tokenID );
+ return false;
+ }
+
+ let item = MIname.replace(/\s/g,'-');
+
+ switch (MItype.toLowerCase()) {
+
+ case 'change-each':
+ case 'cursed+change-each':
+ if (MIchangeTo && MIchangeTo.length && charges > 0) {
+ let changeRow = MItables.tableFind( fields.Items_trueName, MIchangeTo );
+ if (isNaN(changeRow)) {
+ handleStoreMI( ['', tokenID, changeRow, MIchangeTo, charges, 'silent' ], false, senderId );
+ } else {
+ MItables.tableSet( fields.Items_qty, changeRow, (parseInt(MItables.tableLookup( fields.Items_qty, changeRow ) || 0)+charges) );
+ MItables.tableSet( fields.Items_trueQty, changeRow, (parseInt(MItables.tableLookup( fields.Items_trueQty, changeRow ) || 0)+charges) );
+ };
+ };
+ case 'charged':
+ case 'perm-charged':
+ case 'cursed+charged':
+ case 'changing':
+ case 'change-last':
+ case 'cursed+change-last':
+ case 'discharging':
+ case 'perm-discharging':
+ case 'cursed+discharging':
+ case 'rechargeable':
+ case 'perm-rechargeable':
+ case 'cursed+rechargeable':
+ if (getStoredSpells( charCS, MIname ).count > 0) break;
+ if (MIqty == charges && !MItype.includes('cursed') && !MItype.includes('perm')) {
+ if (((MItype.toLowerCase() === 'changing') || (MItype.toLowerCase() === 'change-last')) && MIchangeTo) {
+ handleStoreMI( ['',tokenID, MIrowref, MIchangeTo, 0, 'silent' ], false, senderId );
+ } else {
+ handleRemoveMI( ['',tokenID, MIrowref, MIname], false, senderId, true, false );
+ }
+ } else {
+ MIqtyObj.set('current',(MIqty-charges));
+ MIqtyObj.set('max',(MImaxQty-charges));
+ addMIspells( charCS, MIdb.obj[1] );
+ }
+ break;
+
+ case 'selfchargeable':
+ case 'cursed+selfchargeable':
+ if (MIqty >= charges) {
+ MIqtyObj.set('current',(MIqty-charges));
+ MIqtyObj.set('max',(MImaxQty-charges));
+ }
+ if ((MIqty-charges) == 0) {
+ sendAPI(fields.attackMaster + ' --blankweapon '+tokenID+'|'+MItrueName+'|silent',senderId);
+ }
+ break;
+
+ case 'recharging':
+ case 'cursed+recharging':
+ case 'absorbing':
+ case 'cursed+absorbing':
+ if (MIqty >= charges) {
+ MIqtyObj.set('current',(MIqty-charges));
+ }
+ break;
+
+ default:
+ charges = 0;
+ break;
+ }
+
+ setAttr( charCS, fields.ItemQty, MIqtyObj.get('current') );
+
+ if (MIqty > charges) checkForBag( charCS, MItrueName, MIrowref );
+ if ((MIqty - charges == 0) && miData.zero && miData.zero.length) {
+ sendAPI( parseStr(miData.zero).replace(/@{\s*selected\s*\|\s*token_id\s*}/ig,tokenID)
+ .replace(/{\s*selected\s*\|/ig,'{'+charCS.get('name')+'|'), null, 'magic use-mi');
+ }
+ if (action.includes('USE') && (MIreveal == 'view' || MIreveal == 'use')) {
+ MIname = MItables.tableLookup( fields.Items_trueName, MIrowref );
+ MItables = MItables.tableSet( fields.Items_name, MIrowref, MIname );
+ MItables = MItables.tableSet( fields.Items_type, MIrowref, MItables.tableLookup( fields.Items_trueType, MIrowref ));
+ MItables = MItables.tableSet( fields.Items_reveal, MIrowref, '' );
+ }
+
+ if (isSilent) {
+ sendWait(senderId,0);
+ return true;
+ }
+
+ content = '&{template:'+fields.menuTemplate+'}{{name='+charName+' is using '+MIname+'}}'
+ + '{{desc=To see the effects, select '+charName+'\'s token and press ['+MIname+'](!
/w gm %{'+MIdb.dB+'|'+(MIname.hyphened())+'})}}';
+ sendFeedback( content, flags.feedbackName, flags.feedbackImg, tokenID, charCS );
+ return true;
+ }
+
+ /*
+ * Handle the selection of a spell to store in
+ * a Magic Item, and the slot in the MI spellbook
+ * to store it in.
+ */
+
+ var handleSelectMIspell = function( args, senderId ) {
+
+ var tokenID = args[1],
+ charCS = getCharacter(tokenID);
+
+ if (!charCS) {
+ sendDebug('handleSelectMIspell: invalid tokenID parameter');
+ sendError('Internal MagicMaster error');
+ return;
+ }
+ var isMU = args[0].toUpperCase().includes('MU'),
+ isMI = args[0].toUpperCase().includes('MI'),
+ spellButton = args[(isMI ? 5 : 2)],
+ spellRow = args[(isMI ? 6 : 3)],
+ spellCol = args[(isMI ? 7 : 4)],
+ MIbutton = args[(isMI ? 2 : 5)],
+ MIrow = args[(isMI ? 3 : 6)],
+ MIcol = args[(isMI ? 4 : 7)],
+ spellName = '',
+ col,
+ content = '';
+
+ if (spellButton >= 0) {
+ spellName = attrLookup( charCS, fields.Spells_name, fields.Spells_table, spellRow, spellCol ) || '-';
+ content += 'Selected '+spellName+' to store';
+ }
+ if (MIbutton >= 0) {
+ col = (fields.SpellsFirstColNum || MIcol != 1) ? MIcol : '';
+ spellName = attrLookup( charCS, fields.Spells_name, fields.Spells_table, MIrow, MIcol ) || '-';
+ content += (spellButton >= 0 ? '' : 'Selected to store') + ' in the slot for '+spellName;
+ }
+ makeStoreMIspell( args, senderId, content );
+ return;
+ }
+
+ /*
+ * Handle selecting a magic item power
+ */
+
+ var handleSelectMIpower = function( args, isUse, senderId ) {
+
+ var tokenID = args[1],
+ charCS = getCharacter(tokenID);
+ if (!charCS) {
+ sendDebug('handleSelectMIpower: invalid token_id');
+ sendError('Incorrect MagicMaster syntax');
+ return;
+ }
+
+ const dbList = [['PW-',fields.PowersDB],['MU-',fields.MU_SpellsDB],['PR-',fields.PR_SpellsDB],['MI-',fields.MagicItemDB]];
+
+ var powerName = args[2] || '',
+ itemName = (args[3] || '').split('/'),
+ castLevel = args[4],
+ charges = parseInt(args[5] || '1'),
+ maxChange = parseInt(args[6] || '0'),
+ tokenName = getObj('graphic',tokenID).get('name'),
+ MIlibrary = charCS,
+ powerType = powerName.substring(0,3),
+ powerHyphen = powerName.hyphened(),
+ itemHyphen, powerObj;
+
+ if (_.some(dbList,dB=>dB[0]===powerType.toUpperCase())) {
+ powerName = powerName.slice(powerType.length);
+ if (!castLevel) castLevel = casterLevel( charCS, powerType.substring(0,2) );
+ } else {
+ powerType = ''
+ if (!castLevel) castLevel = characterLevel( charCS );
+ }
+
+ for (let i=0; !powerObj && i'
+ + selectableSlot+'Rename '+slotName+(chosenSlot ? ('](!magic --button GM-RenameMI|'+tokenID+'|'+MIrowref+'|'+MItoStore+'|?{What name should '+slotName+' now have?}) ') : ' ')+' '
+ + '
'
+ + selectableSlot+(!slotCursed ? 'Change Type' : 'Remove Curse')+(chosenSlot ? ('](!magic --button GM-ChangeMItype|'+tokenID+'|'+MIrowref+'|'+MItoStore+'|'+(slotCursed ? 'removeCurse' : ('?{Currently '+slotType+'. What type should '+slotName+' now be?|charged|uncharged|splitable|recharging|rechargeable|selfchargeable|absorbing|discharging|cursed|cursed+charged|cursed+recharging|cursed+rechargeable|cursed+selfchargeable|cursed+absorbing|cursed+discharging}'))+') ') : ' ')+'
'
+ + selectableSlot+'Change displayed charges'+(chosenSlot ? ('](!magic --button GM-ChangeDispCharges|'+tokenID+'|'+MIrowref+'|'+MItoStore+'|?{How many displayed charges should '+slotName+' now have (currently '+slotQty+')?|'+slotQty+'}) ') : ' ')+'
'
+ + selectableSlot+'Change actual charges'+(chosenSlot ? ('](!magic --button GM-ChangeActCharges|'+tokenID+'|'+MIrowref+'|'+MItoStore+'|?{How many actual charges should '+slotActualName+' now have (currently '+slotActualQty+')?|'+slotActualQty+'}) ') : ' ')+'
'
+ + storableSlot+'Store Spells/Powers in MI'+((spellStoring && chosenSlot) ? ('](!magic --store-spells '+tokenID+'|'+slotActualName+'|||GM-EDIT-MI) ') : ' ')+''+hiddenSlot+'Reveal '+revealType+((hiddenMI && chosenSlot) ? ('](!magic --set-reveal '+tokenID+'|'+slotActualName+'|?{Currently '+revealType+'. How should '+slotActualName+' be revealed?|Manually by DM,|When viewed,View|When used,Use|On Long Rest,Rest}|'+MIrowref+'|MENU) ') : ' ')+'
'
+ + selectableSlot+(hiddenMI ? 'Reveal Now' : 'Reset Qty to Max')+(chosenSlot ? ('](!magic --button GM-ResetSingleMI|'+tokenID+'|'+MIrowref+') ') : ' ')+'
'
+ + selectableSlot+'Change Cost'+(chosenSlot ? ('](!magic --button GM-SetMIcost|'+tokenID+'|'+MIrowref+'|'+MItoStore+'|?{How much should '+slotName+' now cost (currently '+slotCost+'GP)?|'+slotCost+'})') : '')+'
'
+ + selectableSlot+'REMOVE MI'+(chosenSlot ? '](!magic --button GM-DelMI|'+tokenID+'|'+MIrowref+'|'+slotActualName+') ' : ' ')+''
+ + '
'
+ + 'Menu images '+(config.menuImages ? '\u2705' : '\u2B1C')+' '
+ + 'Menu plain '+(config.menuPlain ? '\u2705' : '\u2B1C')+' '
+ + 'Menu dark '+(config.menuDark ? '\u2705' : '\u2B1C')+'
Magic Weapon | 5ft | '+weaponSwitch+'
Torch | 15ft | '+torchSwitch+'
Hooded Lantern | 30ft | '+hoodedSwitch+'
Bullseye Lantern | 60ft beam | '+bullseyeSwitch+'
Cont-Light gem | 60ft | '+contLightSwitch+'
Beacon Lantern | 240ft beam | '+beaconSwitch+'
Update: Added RPGM maths processor to many numerical command parameters
' - +'Update: On viewing a spell or an item description, action buttons are disabled
' - +'Update: Fixed --add-mi when replacing an item to also replace in-hand weapons and worn rings
' - +'Update: Extended optional parameters for --level-change
' + +'New: Non-stacking duplicate items picked up offer option for default rename to make unique
' + +'The MagicMaster API provides functions to manage all types of magic, including Wizard & Priest spell use and effects; Character, NPC & Monster Powers; and discovery, looting, use and cursing of Magic Items. All magical aspects can work with the RoundMaster API to implement token markers that show and measure durations, and produce actual effects that can change token or character sheet attributes temporarily for the duration of the spell or permanently if so desired. They can also work with the InitiativeMaster API to provide menus of initiative choices and correctly adjust individual initiative rolls, including effects of Haste and Slow and similar spells. This API can also interact with the MoneyMaster API (under development) to factor in the passing of time, the cost of spell material use, the cost of accommodation for resting, and the cost of training for leveling up as a spell caster (Wizard, Priest or any other).
' +'The MagicMaster API is called using !magic (or the legacy command !mibag).
' @@ -250,36 +255,7 @@ var MagicMaster = (function() { +'!magic --spellmenu [token_id]|[MU/PR/POWER] --mimenu [token_id]' +'
When specifying the commands in this document, parameters enclosed in square brackets [like this] are optional: the square brackets are not included when calling the command with an optional parameter, they are just for description purposes in this document. Parameters that can be one of a small number of options have those options listed, separated by forward slash \'/\', meaning at least one of those listed must be provided (unless the parameter is also specified in [] as optional): again, the slash \'/\' is not part of the command. Parameters in UPPERCASE are literal, and must be spelt as shown (though their case is actually irrelevant).
' +'The syntax of the Roll20 Roll Query has been extended within the RPGMaster MagicMaster API to support !magic API commands with Roll Queries that the GM is invited to answer, rather than the player, regardless of who issued the command. The standard syntax and the extended syntax is shown below:
' - +'Standard Syntax: ?{Query text|option1|option2|...}' - +'
' - +'Extended syntax: gm{Query text/option1/option2/...}
When used in a !magic API command, the extended Roll Query will prompt the GM with a button in the Chat Window for the GM to answer the question posed by the query text. The result will be fed into the action taken by the API command. This allows the GM to be involved when, for instance, a Staff of the Magi absorbs levels of spells cast at a character that the character & player can\'t know.
' - +'When a command is sent to Roll20 APIs / Mods, Roll20 tries to work out which player or character sent the command and tells the API its findings. The API then uses this information to direct any output appropriately. However, when it is the API itself that is sending commands, such as from a {{successcmd=...}} or {{failcmd=...}} sequence in a RPGMdefault Roll Template, Roll20 sees the API as the originator of the command and sends output to the GM by default. This is not always the desired result.
' - +'To overcome this, or when output is being misdirected for any other reason, a Controlling Player Override Syntax (otherwise known as a SenderId Override) has been introduced (for RPGMaster Suite APIs only, I\'m afraid), with the following command format:
' - +'!magic [sender_override_id] --cmd1 args1... --cmd2 args2...' - +'
The optional sender_override_id (don\'t include the [...], that\'s just the syntax for "optional") can be a Roll20 player_id, character_id or token_id. The API will work out which it is. If a player_id, the commands output will be sent to that player when player output is appropriate, even if that player is not on-line (i.e. no-one will get it if they are not on-line). If a character_id or token_id, the API will look for a controlling player who is on-line and send appropriate output to them - if no controlling players are on-line, or the token/character is controlled by the GM, the GM will receive all output. If the ID passed does not represent a player, character or token, or if no ID is provided, the API will send appropriate output to whichever player Roll20 tells the API to send it to.
' - +'Roll20 provides many excellent maths functions for commands made to the chat window and contained in API button strings. However, it is not always possible to use the Roll20 maths using the [[...]] syntax to achieve what you want. RPGMaster provides an alternative set of maths functions to help resolve these issues. Formulas can be entered for many numeric values required by RPGMaster commands using the supported syntax. However: this syntax does not work for anything other than RPGMaster commands as of writing (this might be a future develpment).
' - +'The square brackets [[...]] are not required. The syntax follows normal maths presedent with a few additional operators to support range calculations and dice rolls:
' - +'+-*/ | The standard maths operators work as expected |
---|---|
(...) | Parentheses can be used to define the order of calculation as normal |
^(#,#,#,...) | This will resolve to the maximum value in the list, and each # can also be a calculation (semi-colons can be used instead of commas) |
v(#,#,#,...) | This will resolve to the minimum value in the list, and each # can also be a calculation (semi-colons can be used instead of commas) |
c(...) | This will resolve to the ceiling (the number rounded up) of the result of the contained calculation |
f(...) | This will resolve to the floor (the number rounded down) of the result of the contained calculation |
#d#r# | Dice roll specifications can be included in the maths with optional reroll values anywhere in the calculation, and the numbers can be calculations |
#:# | A different feature is the range calculation - this will derive a number in the range between the two numbers (inclusive), but will try to do so using the equivalent to 3 dice if possible - e.g. 3:18 would make the equivalent of rolling 3d6, 7:34 will resolve to 4+(3d10), 7:35 will resolve to 4+1d11+2d10. A range can be used anywhere in the calculation, and the numbers can themselves be calculations |
The most common approach for the Player to run these commands is to use Ability macros on their Character Sheets which are flagged to appear as Token Action Buttons: Ability macros & Token Action Buttons are standard Roll20 functionality, refer to the Roll20 Help Centre for information on creating and using these.
' - +'In fact, the simplest configuration is to provide only Token Action Buttons for the menu commands: --spellmenu and --mimenu. From these, most other commands can be accessed. If using the CommandMaster API, its character sheet setup functions can be used to add the necessary Ability Macros and Token Action Buttons to any Character Sheet.
' - +'MagicMaster uses a large range of items held in databases. The current versions of these databases are distributed with the game-version-specific RPGMaster Library, updated as new versions are released via Roll20. The provided databases are held in memory, but can be extracted to ability macros in database character sheets using the !magic --extract-db command. These macros can do anything that can be programmed in Roll20 using ability macros and calls to APIs, and are found (either in the Character Sheet database or the internal database in memory) and called by the MagicMaster API when the Player selects them using the menus provided by the MagicMaster functions. The GM can add to the provided items in the databases using standard Roll20 Character Sheet editing, following the instructions provided in the Magic Database Handout.
' @@ -316,16 +292,16 @@ var MagicMaster = (function() { +'--spellmenu [token_id]|[MU/PR/POWER]' +'
' +'--mem-spell (MU/PR/POWER)|[token_id]
' - +'Update:--view-spell (MU/PR/POWER)|[token_id]
' + +'--view-spell (MU/PR/POWER)|[token_id]
' +'--cast-spell (MU/PR/POWER/MI)|[token_id]|[casting_level]|[casting_name]
' +'--cast-again (MU/PR/POWER)|token_id|[spell_name]
' +'--mem-all-powers token_id
--mimenu [token_id]
' +'--edit-mi [token_id]
' - +'Update:--view-mi [token_id]
' + +'--view-mi [token_id]
' +'--use-mi [token_id]
' - +'Update:--add-mi [token_id]|(mi-to-replace/row#)|mi-to-add|quantity|hand#|[NOCURSE]|[SILENT]
' + +'--add-mi [token_id]|(mi-to-replace/row#)|mi-to-add|quantity|hand#|[NOCURSE]|[SILENT]
' +'--mi-charges token_id|value|[mi_name]|[maximum]|[charge_override]
' +'--mi-power token_id|power_name|mi_name|[casting-level]
' +'--store-spells token_id|mi-name
' @@ -337,7 +313,7 @@ var MagicMaster = (function() { +'!rounds --target CASTER|caster_token_id|caster_token_id|spell_name|duration|increment|[msg]|[marker]
' +'!rounds --target (SINGLE/AREA)|caster_token_id|target_token_id|spell_name|duration|increment|[msg]|[marker]
' +'--touch token_id|effect-name|duration|per-round|message|marker
' - +'Update:--level-change [token_id]|[# of levels]|[HP change]|[class]
' + +'--level-change [token_id]|[# of levels]|[HP change]|[class]
' +'--change-attr [token_id]|change|[field]|[SILENT]
' +'--rest [token_id]|[SHORT/LONG]|[MU/PR/MU-PR/POWER/MI/MI-POWER]|[timescale]
' +'--mi-rest [token_id]|mi_name|[charges]|[power_name]
' @@ -381,7 +357,7 @@ var MagicMaster = (function() { +'Initially displays a menu for memorising Level 1 spells (the only level for powers), with buttons to: choose a spell from the Level 1 spell book on the character sheet; review the chosen spell; and one for each memorising slot the Character has at this level. Other buttons to memorise or remove spells become available when spells or slots are chosen. Another button goes to the next available level with slots. When a granted power is memorised to a slot, a quantity per day can be specified: -1 will grant unlimited uses of the power per day. Memorising any other type of spell is limited to 1 use per slot.
' +'Depending on the settings on the --config menu, the character will be limited to memorising spells and powers allowed to their character class and level.
' +'MI-MU and MI-PR have a special function: these are used to cast memorised spells into the named spell-storing magic item (if no item is named, the last item selected by the Character running the command will be used instead), such as a Ring-of-Spell-Storing. Magic Item spells are stored in an unused level of the Character Sheet. This command displays both all memorised spells and all spell-storing magic item spell slots, and allows a memorised spell to be selected, a slot (for the same spell name) to be selected, and the spell cast from one to the other. Spells can only be replaced by the same spell that was in the slot previously (unless this is the first time spells have been stored in a blank spell-storing item).
' - +'1.3 Updated: View the memorised spells or granted powers
' + +'1.3 View the memorised spells or granted powers
' +'--view-spell (MU/PR/POWER/MI-MU/MI-PR/MI-POWER)|[token_id]|[mi-name]' +'Takes a mandatory spell type, an optional token ID, and an optional magic item name. If token ID is not specified, uses the selected token.
' +'Displays a menu of all levels of memorised spells of the selected type (there is only 1 level of powers). Spells that have already been cast appear as greyed out buttons, and can\'t be selected. Spells that are still available to cast that day can be selected and this runs the spell or power macro from the relevant database without consuming the spell, so that the Player can see the specs. Action buttons on the macro are "greyed out" and can\'t be selected, with the exception of [View...] buttons on spell-storing items which will display the stored spells/powers if selected (again without consuming them).
' @@ -413,7 +389,7 @@ var MagicMaster = (function() { +'Displays a menu similar to editing memorised spells. At the top are buttons to choose different types of magic items which have macros in the magic item databases. If the optional item type is MARTIAL, only weapons, ammo and armour are listed; if ALL is specified, lists of all items are shown; otherwise only non-MARTIAL items are listed. The slots available in the bag are shown (with their current contents) and, when magic items and/or slots are chosen buttons become selectable below to store, review, or remove magic items in/from the bag.
' +'Storing a magic item will ask for a number - either a quantity or a number of charges. Magic Items can be of various types: Charged (is used up when reaches 0), Uncharged (a number is a pure quantity that is not consumed), Recharging (regains charges after each long rest), Rechargable (is not used up when reaches 0, stays in bag and can be recharged when the DM allows), Self-charging (recharge at a rate per round determined by the item) and can also be Cursed - more under section 4.
' +'This menu is generally used when Magic Item & treasure containers (such as Treasure Chests and NPCs/monsters with treasure) have not been set up in a campaign as lootable, and provides a means of giving found magic items to characters. The DM just tells the Player that they have found a magic item, and the Player adds it to their Character Sheet using this command (more likely accessed via the Magic Item menu).
' - +'2.3 Updated: View a character\'s Magic Item Bag
' + +'2.3 View a character\'s Magic Item Bag
' +'--view-mi [token_id]' +'Takes an optional token ID as an argument. If token ID is not specified, uses the selected token.
' +'Displays a menu of items in the character\'s magic item bag, with the quantity possessed or the number of charges. Pressing a button displays the named Magic Item specs without using any charges so that the Player can review the specifications of that item. Action buttons on the item macro are "greyed out" and can\'t be selected, with the exception of [View...] buttons on spell-storing items which will display the stored spells/powers if selected (again without consuming them). Items for which all charges have been consumed are greyed out, and cannot be viewed as the character can no longer use them. They will become viewable again if they gain charges.
' @@ -422,7 +398,7 @@ var MagicMaster = (function() { +'Takes an optional token ID as an argument. If token ID is not specified, uses the selected token.
' +'Displays a similar menu as for viewing the contents of the Magic Item Bag, but when an item is selected, a button is enabled that uses the Magic Item and consumes a charge. Other buttons specified in the item macro might use additional charges to perform additional effects. See section 3.
' +'Items with 0 quantity or charges left are greyed out and cannot be selected, unless they have abilities to regain charges such as "spell absorbing" items. When a Charged Item reaches 0 charges left, it is removed from the character\'s Magic Item Bag automatically.
' - +'2.5 Updated: Add an Item to a Character / Container
' + +'2.5 Add an Item to a Character / Container
' +'--add-mi [token_id]|(mi-to-replace/row#)|mi-to-add|[quantity]|[hand#]|[NOCURSE]|[SILENT]' +'Takes an optional token ID (if not provided, uses selected token), then either the name of the item to be replaced or the row number of the item in the equipment list, the name of the item to add, the quantity to add (defaults to quantity of replaced item, or 1), optionally a hand number to use to take in-hand, optionally NOCURSE if replacement of cursed items is possible, and optionally SILENT to not trigger messages, menus or dialogs.
' +'This command can be used to add a named item from the databases to a character, NPC, creature or other container without going through other dialogs to select the item. It will add the item to a numbered row in the equipment list or, more usefully, replace a named item that already exists in the list (or \'-\' to find an empty row). If the item is one that can be taken in-hand (e.g. a weapon or a shield, or a magic item like a wand or staff), the optional \'hand number\' can be used to specify which hand to take it in. 0=prime hand,1=offhand,2=both,3 onwards for other hands, or just \'=\' (or blank) means replace in-hand if mi-to-replace is in-hand or worn as a ring - if the item is not one that can be held the item will not be taken in-hand. If the item to be replaced is cursed, it will not be replaced and an error message will be displayed unless the NOCURSE option is used. Finally, the command will pop up the edit-mi dialog or the gm-edit-mi dialog (if NOCURSE is specified) showing the resulting equipment list unless the SILENT flag is also used.
' @@ -492,7 +468,7 @@ var MagicMaster = (function() { +'To use this command, add it as part of a spell, power or MI macro in the appropriate database, before or after the body of the macro text (it does not matter which, as long as it is on a separate line in the macro - the Player will not see the command). Then include in the macro (in a place the Player will see it and be able to click it) an API Button call [Button name](~Selected|To-Hit-Spell) which will run the Ability "To-Hit-Spell" on the Character\'s sheet (which has just been newly written there or updated by the --touch command).
' +'Thus, when the Player casts the Character\'s spell, power or MI, they can then press the API Button when the macro runs and the attack roll will be made. If successful, the Player can then use the button that appears to mark the target token and apply the spell effect to the target.
' +'See the RoundMaster API documentation for further information on targeting, marking and effects.
' - +'3.3 Update: Change the Experience Level
' + +'3.3 Change the Experience Level
' +'--level-change [token_id]|[# of levels]|[HP change]|[class]' +'Takes an optional Token ID (if not specified, uses the selected token), an optional number of levels (plus or minus: if not specified assumes -1), an optional total number of HP gained or lost, and an optional class to apply the level change to.
' +'Mainly used for attacks and spell-like effects that drain levels from opponents, this command undertakes all the calculations and Character Sheet updates that can automatically be done when a character or creature changes experience level. Saving throw targets are reassessed, weapon attacks per round recalculated, numbers of memorised spells changed, Race & Class powers checked for level appropriateness, etc. If this is a single class character or a creature, the optional class parameter will be ignored and the single class/monster HD applied. If the HP change is not specified for a single class character, then the appropriate HP dice will be rolled and changed (Tip: it\'s better to put in a roll query to ask the player for the HP to change by). If the character is multi- or dual-class, it will either use the class specified, or asks the player which class to add/drain levels to/from and the hit points for each.
' @@ -548,7 +524,7 @@ var MagicMaster = (function() { +'Takes a mandatory token ID of the character\'s token, mandatory token ID of the token to check for traps, mandatory token ID of the token doing the checking.
' +'This command will check a token for any traps. If the container represented by the token was created using the Drag & Drop container system (see CommandMaster API documentaion for details of the Drag & Drop container system) this command will start the selected container\'s "Find & Remove Traps" programmed sequence, with a (small) chance of the trap (if any) being triggered. If the trap is successfully removed, the container may still be locked but will no longer be trapped. If the token represents any other type of character, container, creature or object a standard "Find/Remove Traps" sequence will ensue, resulting in the party (and the GM) being alerted to the success or otherwise of the outcome.
' +'In either case, the default approach to the Find Traps roll is that the GM is asked to make it - being presented with a drop-down list of options that includes (a) just rolling 1d100 against the character\'s chance, (b) forcing a successful roll (e.g. if they were meant to find it), and (c) forcing a failure to find a trap (e.g. if there is no trap to be found). The GM can use the !magic --config command to change this action so that the player always rolls to Find Traps, though this might result in an indication for a (non-Drag & Drop) container indicating success for a container that is not trapped!
' - +'4.3 Update: Searching/Storing tokens with Items and Treasure
' + +'4.3 Searching/Storing tokens with Items and Treasure
' +'--search token_id|pick_id|put_id' +'Takes a mandatory token ID of the character\'s token, mandatory token ID of the token to search and pick up items from, mandatory token ID of the token to put picked up items into.
' +'This command can be used to pick the pockets of an NPC or even another Player Character, as well as to loot magic item and treasure containers such as Chests and dead bodies. It can also be used for putting stuff away, storing items from the character\'s Magic Item Bag into a container, for instance if the MI Bag is getting too full (it is limited to the number of items specified via the --gm-edit-mi menu, though similar items can be stacked). The effect of this command depends on the type of the container: intelligent characters, NPCs and creatures (even if only with animal intelligence of 1) are considered sentient unless they are dead (Hit Points equal to or less than zero). The trapped container status is set by any Drag & Drop container, or via the GM\'s [Add Items] button or !magic --gm-edit-mi command. All other containers (tokens with character sheets) are considered inanimate and untrapped. Any status can also be overridden if so desired by resetting the container type using the Add Items dialog to set the type to a different value - a sentient creature can be forced to be inanimate (i.e. does not need a pick pockets roll), and visa-versa (e.g. luggage Terry Pratchett style).
' @@ -557,7 +533,7 @@ var MagicMaster = (function() { +'' +' Sentient Creature: if searching, a Pick Pockets check is undertaken - the Player is asked to roll a dice and enter the result (or Roll20 can do it for them), which is compared to the Pick Pockets score on their character sheet. If successful, a message is displayed in the same way as an Inanimate object. If unsuccessful, a further check is made against the level of the being targeted to see if they notice, and the DM is informed either way. The DM can then take whatever action they believe is needed. Of course, you can always freely give/store items to another creature. ' +' Trapped container: Traps can be as simple or as complex as the DM desires. Traps may be nothing more than a lock that requires a Player to say they have a specific key, or a combination that has to be chosen from a list, and nothing happens if it is wrong other than the items in the container not being displayed. Or setting a trap off can have damaging consequences for the character searching or the whole party. It can just be a /whisper gm message to let the DM know that the trapped container has been searched. Searching a trapped container with this command calls an ability macro called "Trap-@{container_name|version}" on the container\'s character sheet: if this does not exist, it calls an ability macro just called "Trap". The first version allows the Trap macro to change the behaviour on subsequent calls to the Trap functionality (if using the ChatSetAttr API to change the version attribute), for instance to allow the chest to open normally once the trap has been defused or expended. This functionality requires confidence in Roll20 macro programming.
Important Note: all Character Sheets representing Trapped containers must have their \'ControlledBy\' value (found under the [Edit] button at the top right of each sheet) set to \'All Players\'. Otherwise, Players will not be able to run the macros contained in them that operate the trap!
New: Note: Some items are not stackable - they are single items with charges such as a wand or rod, or a spell-storing item which must retain its uniqueness so the spells remain associated. However, it is also the case that non-stackable items like these need to have unique names in the container to retain their unique identity. Thus, when a second copy of a non-stackable item is picked up or put away into a container that already contains another item with the same name, the player will be asked to provide a new unique name for the item (which cannot be the same as any other magic item, even those not in the container - sorry, you can\'t turn that ring of protection+1 into a ring of wishes!). Once the item is stored with this new name, it will work in all respects like the item it is, just with a different name.
' + +'Note: Some items are not stackable - they are single items with charges such as a wand or rod, or a spell-storing item which must retain its uniqueness so the spells remain associated. However, it is also the case that non-stackable items like these need to have unique names in the container to retain their unique identity. Thus, when a second copy of a non-stackable item is picked up or put away into a container that already contains another item with the same name, the player will be asked to provide a new unique name for the item (which cannot be the same as any other magic item, even those not in the container - sorry, you can\'t turn that ring of protection+1 into a ring of wishes!). Once the item is stored with this new name, it will work in all respects like the item it is, just with a different name.
' +'--pickorput token_id|pick_id|put_id|[SHORT/LONG]' +'
Takes a mandatory token ID for the Player\'s character, a mandatory token ID for the token to pick items from, a mandatory token ID for the token to put items in to, and an optional argument specifying whether to use a long or a short menu.
' @@ -735,6 +711,7 @@ var MagicMaster = (function() { RW_DMGL: 'RW_DMGL', MI_SPELL: 'MI_SPELL', MI_POWER: 'MI_POWER', + MI_SCROLL: 'MI_SCROLL', MI_POWER_USED: 'MI_POWER_USED', MI_POWER_CHARGE_USED:'MI_POWER_CHARGE_USED', LEVEL_CHANGE: 'LEVEL_CHANGE', @@ -798,6 +775,7 @@ var MagicMaster = (function() { EDIT_POWERS: 'EDIT_POWERS', EDIT_MIMUSPELLS: 'EDIT_MIMUSPELLS', EDIT_MIPRSPELLS: 'EDIT_MIPRSPELLS', + EDIT_MISPELLS: 'EDIT_MISPELLS', EDIT_MIPOWERS: 'EDIT_MIPOWERS', EDIT_MI: 'EDIT_MI', EDIT_MARTIAL: 'EDIT_MARTIAL', @@ -828,6 +806,9 @@ var MagicMaster = (function() { VIEW_MI_SPELL: 'VIEW_MI_SPELL', VIEW_MI_MUSPELL: 'VIEW_MI_MUSPELL', VIEW_MI_PRSPELL: 'VIEW_MI_PRSPELL', + VIEW_MI_SCROLL: 'VIEW_MI_SCROLL', + VIEW_MI_MUSPELL: 'VIEW_MI_MUSCROLL', + VIEW_MI_PRSPELL: 'VIEW_MI_PRSCROLL', VIEW_MI: 'VIEW_MI', VIEWMI_OPTION: 'VIEWMI_OPTION', VIEWMEM_MUSPELLS: 'VIEWMEM_MUSPELLS', @@ -837,6 +818,9 @@ var MagicMaster = (function() { VIEWMEM_MI_SPELLS: 'VIEWMEM_MI_SPELLS', VIEWMEM_MI_MUSPELLS:'VIEWMEM_MI_MUSPELLS', VIEWMEM_MI_PRSPELLS:'VIEWMEM_MI_PRSPELLS', + VIEWMEM_MI_SCROLL: 'VIEWMEM_MI_SCROLL', + VIEWMEM_MI_MUSCROLL:'VIEWMEM_MI_MUSCROLL', + VIEWMEM_MI_PRSCROLL:'VIEWMEM_MI_PRSCROLL', POP_PICK: 'POP_PICK', POP_STORE: 'POPsubmit', PICKMI_OPTION: 'PICKMI_OPTION', @@ -881,7 +865,7 @@ var MagicMaster = (function() { const reRepeatingTable = /^(repeating_.*)_\$(\d+)_.*$/; const reItemData = /}}[\s\w\-]*?(?'+msg+'',null,{noarchive:!flags.archive, use3d:false}); + if (playerIsGM(state.MagicMaster.debug)) { + log('MagicMaster Debug: '+msg); + } else { + var player = getObj('player',state.MagicMaster.debug), + to; + if (player) { + to = '/w "' + player.get('_displayname') + '" '; + } else + {throw ('sendDebug could not find player');} + if (!msg) + {msg = 'No debug msg';} + sendChat('MagicMaster Debug',to + ''+msg+'',null,{noarchive:!flags.archive, use3d:false}); + }; }; }; @@ -1661,7 +1649,7 @@ var MagicMaster = (function() { * i.e. which versions of MagicMaster it is matched to */ - var csVer = charCS => parseFloat(((attrLookup( charCS, fields.msVersion ) || '1.5').match(/^\d+\.\d+/) || ['1.5'])[0]) || 1.5; + var csVer = (charCS) => parseFloat(((attrLookup( charCS, fields.msVersion ) || '1.5').match(/^\d+\.\d+/) || ['1.5'])[0]) || 1.5; /** * Express a cost in coins for display @@ -1794,7 +1782,16 @@ var MagicMaster = (function() { setAttr( charCS, fields.Casting_name, castingName ); if (itemName.length) { setAttr( charCS, fields.ItemChosen, itemName ); - } + let item = abilityLookup( fields.MagicItemDB, itemName ); +// log('setCaster: cmd = '+args[0].toUpperCase()+', cmd test = '+(args[0].toLowerCase().includes('mi'))+', !!item.obj = '+!!item.obj+', charge = '+((!!item.obj && item.obj[1].charge) ? item.obj[1].charge.toLowerCase() : 'undefined')+' so '+((!!item.obj && item.obj[1].charge) ? chargedList.includes(item.obj[1].charge.toLowerCase()) : false)); + if (args[0].toLowerCase().includes('mi') && !!item.obj && !!item.obj[1].charge) { +// log('setCaster: testing charge = '+chargedList.includes(item.obj[1].charge.toLowerCase())); + if (chargedList.includes(item.obj[1].charge.toLowerCase())) { + args[0] = BT.MI_SCROLL; +// log('setCaster: cmd changed to '+args[0]); + } + }; + }; return args; }; @@ -2485,7 +2482,7 @@ var MagicMaster = (function() { bagCS = createObj( "character", {name:miName, avatar: design.bag_icon, - inplayerjournals:charCS.get("inplayerjournals"), + inplayerjournals:(charCS.get("inplayerjournals") || ''), controlledby:charCS.get("controlledby")}); setAttr( bagCS, fields.Race, 'Magic Item' ); @@ -2507,10 +2504,10 @@ var MagicMaster = (function() { let values = Items.copyValues(); values[fields.Items_name[0]][fields.Items_name[1]] = itemData.name; values[fields.Items_trueName[0]][fields.Items_trueName[1]] = (itemData.trueName || itemData.name); - values[fields.Items_speed[0]][fields.Items_speed[1]] = itemData.speed || 5; - values[fields.Items_trueSpeed[0]][fields.Items_trueSpeed[1]] = itemData.speed || 5; - values[fields.Items_qty[0]][fields.Items_qty[1]] = itemData.qty || 1; - values[fields.Items_trueQty[0]][fields.Items_trueQty[1]] = itemData.qty || 1; + values[fields.Items_speed[0]][fields.Items_speed[1]] = evalAttr(itemData.speed) || 5; + values[fields.Items_trueSpeed[0]][fields.Items_trueSpeed[1]] = evalAttr(itemData.speed) || 5; + values[fields.Items_qty[0]][fields.Items_qty[1]] = evalAttr(itemData.qty) || 1; + values[fields.Items_trueQty[0]][fields.Items_trueQty[1]] = evalAttr(itemData.qty) || 1; values[fields.Items_cost[0]][fields.Items_cost[1]] = 0; values[fields.Items_type[0]][fields.Items_type[1]] = itemData.type || 'uncharged'; values[fields.Items_trueType[0]][fields.Items_trueType[1]] = itemData.trueType || itemData.type || 'uncharged'; @@ -2525,7 +2522,7 @@ var MagicMaster = (function() { } } else { bagCS = bagCS[0]; - bagCS.set({inplayerjournals:charCS.get("inplayerjournals"), controlledby:charCS.get("controlledby")}); + bagCS.set({inplayerjournals:(charCS.get("inplayerjournals") || ''), controlledby:charCS.get("controlledby")}); } return; } @@ -2580,7 +2577,27 @@ var MagicMaster = (function() { .replace(/{\s*selected\s*\|/ig,'{'+charCS.get('name')+'|'), null, who), 2000); }; - + + /* + * Grey out all active buttons (except [View...] buttons when viewing + * a spell or item description and not using it. + */ + + var greyOutButtons = function( tokenID, charCS, obj, renamed='' ) { + + var setVal = ( str, field, param='current' ) => attrLookup( charCS, [field,param] ); + + var action = (obj[0].get('action') || '').replace(/@\{selected\|token_id\}/img,'') + .replace(/@\{selected\|(.+?)(?:\|(current|max))?\}/img,setVal) + .replace(reActionButton,design.grey_action) + .replace(/^!.+$/mg,''); + if (renamed) { + obj = setAbility( charCS, renamed, action ); + } else { + obj[0].set('action', action); + }; + return obj; + }; // ---------------------------------------------------- Make Menus --------------------------------------------------------- @@ -2723,6 +2740,7 @@ var MagicMaster = (function() { if (mi.length > 0 && (includeEmpty || mi != '-')) { miObj = abilityLookup( fields.MagicItemDB, mi, charCS, true ); renamed = !miObj.dB.toLowerCase().includes('-db'); + let changedMI = renamed ? 'Display-'+mi : mi; makeGrey = makeGrey || (!showMagic && (!miObj.obj || miObj.obj[1].type.toLowerCase().includes('magic'))); if (showTypes && miObj.obj) { miText = getShownType( miObj, i, resolveData( trueMI, fields.MagicItemDB, reItemData, charCS, {itemType:reSpellSpecs.itemType}, i ).parsed.itemType ); @@ -2738,8 +2756,8 @@ var MagicMaster = (function() { let hide = !miObj.obj ? '' : resolveData( mi, fields.MagicItemDB, reItemData, charCS, {hide:reSpellSpecs.hide}, i ).parsed.hide, reveal = (mi !== trueMI) && !!miObj.obj && hide && hide.length && hide !== 'hide'; miObj = getAbility( fields.MagicItemDB, mi, charCS, false, isGM, (reveal ? mi : trueMI), i ); - if (!state.MagicMaster.viewActions && !renamed && miObj.obj) miObj.obj[0].set('action',miObj.obj[0].get('action').replace(reActionButton,design.grey_action) ); - extension = ' '+sendToWho(charCS,senderId,false,true)+(miObj.api ? ' ' : '')+'%{'+miObj.dB+'|'+mi.hyphened()+'}'; + if (!state.MagicMaster.viewActions && miObj.obj) miObj.obj = greyOutButtons( tokenID, charCS, miObj.obj, (renamed ? changedMI : '') ); + extension = ' '+sendToWho(charCS,senderId,false,true)+(miObj.api ? ' ' : '')+'%{'+miObj.dB+'|'+changedMI.hyphened()+'}'; } content += (i == MIrowref || makeGrey) ? '' : '](!magic --button '+ cmd +'|'+ tokenID +'|'+ i + extension +')'; }; @@ -2860,6 +2878,29 @@ var MagicMaster = (function() { return; } + /* + * Check if an item stores spells and, if it does, return the + * spell row and column arrays, and the number of live spells + */ + + var getStoredSpells = function( charCS, miName ) { + let spellTables = {}; + let spellCount = 0; + let rows = []; + let cols = []; + rows.push((attrLookup( charCS, [fields.MIspellRows[0]+miName+'-mu',fields.MIspellRows[1]] ) || ''),(attrLookup( charCS, [fields.MIspellRows[0]+miName+'-pr',fields.MIspellRows[1]] ) || '')); + rows = rows.join().split(',').filter(r=>!!r); + cols.push((attrLookup( charCS, [fields.MIspellCols[0]+miName+'-mu',fields.MIspellCols[1]] ) || ''),(attrLookup( charCS, [fields.MIspellCols[0]+miName+'-pr',fields.MIspellCols[1]] ) || '')); + cols = cols.join().split(',').filter(c=>!!c); + if (rows.length && cols.length) { + cols.forEach( (c,i) => { + if (_.isUndefined(spellTables[c])) spellTables[c] = getTableField( charCS, {}, fields.Spells_table, fields.Spells_castValue, c ); + spellCount += parseInt((spellTables[c].tableLookup( fields.Spells_castValue, rows[i] )),10); + }); + }; + return {count:spellCount,rows:rows,cols:cols}; + }; + /* * Make a list of spells in the specified memorised/stored list */ @@ -2870,6 +2911,7 @@ var MagicMaster = (function() { isMI = command.toUpperCase().includes('MI'), isPower = command.toUpperCase().includes('POWER'), isView = command.toUpperCase().includes('VIEW'), + isScroll = command.toUpperCase().includes('SCROLL'), isGM = playerIsGM(senderId), content = '', viewCmd = '', @@ -2886,6 +2928,7 @@ var MagicMaster = (function() { toWho = sendToWho(charCS,senderId,false,true), spellTables = [], spellLevels = 0, + learnData = '', learn = false, rows = [], cols = []; @@ -2902,16 +2945,16 @@ var MagicMaster = (function() { buttonList = 'EmptyList,' + attrLookup( charCS, [fields.ItemMUspellsList[0]+miName, fields.ItemMUspellsList[1]] ) || ''; buttonList += ',' + attrLookup( charCS, [fields.ItemPRspellsList[0]+miName, fields.ItemPRspellsList[1]]) || ''; buttonList = buttonList.dbName().split(','); - let miObj = abilityLookup( fields.MagicItemDB, miName, charCS ); - if (miObj.obj) { - learn = resolveData( miName, fields.MagicItemDB, reItemData, charCS, {learn:reSpellSpecs.learn}, miRow ).parsed.learn == 1; + if (caster(charCS,'MU').clv > 0) { + if (abilityLookup( fields.MagicItemDB, miName, charCS ).obj) learnData = resolveData( miName, fields.MagicItemDB, reItemData, charCS, {learn:reSpellSpecs.learn}, miRow ).parsed.learn; + learn = (learnData && learnData != '0' && (!isScroll || !isView)); }; + // see if can build an item-specific spell list... - rows.push((attrLookup( charCS, [fields.MIspellRows[0]+miName+'-mu',fields.MIspellRows[1]] ) || ''),(attrLookup( charCS, [fields.MIspellRows[0]+miName+'-pr',fields.MIspellRows[1]] ) || '')); - rows = rows.join().split(',').filter(r=>!!r); - cols.push((attrLookup( charCS, [fields.MIspellCols[0]+miName+'-mu',fields.MIspellCols[1]] ) || ''),(attrLookup( charCS, [fields.MIspellCols[0]+miName+'-pr',fields.MIspellCols[1]] ) || '')); - cols = cols.join().split(',').filter(c=>!!c); + let storedSpells = getStoredSpells( charCS, miName ); + rows = storedSpells.rows; + cols = storedSpells.cols; if (rows.length && cols.length) { _.each( cols, (c,k) => { let r = rows[k]; @@ -2923,9 +2966,22 @@ var MagicMaster = (function() { disabled = (miStore ? (spellValue != 0) : (spellValue == 0)); if (!disabled) spellLevels = spellLevels + (parseInt(spellTables[c].tableLookup( fields.Spells_spellLevel, r )) || 1); if (!noDash || spellName != '-') { + let renamed = !abilityLookup( spellDB, spellName ), + changedSpell = renamed ? 'Display-'+spellName : spellName; + spell = getAbility( spellDB, spellName, charCS ); + if (!!spell.obj) { + if (!state.MagicMaster.viewActions && isView) { + spell.obj = greyOutButtons( tokenID, charCS, spell.obj, (renamed ? changedSpell : '') ); + } else if (renamed) { + spell.obj = setAbility( charCS, changedSpell, spell.obj[0].body ); + }; + let learnText = !learn ? '' : '{{Learn=Try to [Learn this spell](!magic --learn-spell '+tokenID+'|'+(learnData != 1 ? learnData : spellName)+')}}'; + if (!!learn) spell.obj[0].set('action',spell.obj[0].get('action').replace(/\}\}\s*$/m,'}}'+learnText) ); + }; content += (buttonID == selectedButton ? '' : ((submitted || disabled) ? '' : '[')); content += ((spellType.includes('POWER') && spellValue) ? (spellValue + ' ') : '') + (spellName || '-'); - content += (((buttonID == selectedButton) || submitted || disabled) ? '' : '](!magic --button '+ command +'|'+ tokenID +'|'+ buttonID +'|'+ r +'|'+ c + extension + ' --display-ability '+tokenID+'|'+spellDB+'|'+spellName+')'); +// content += (((buttonID == selectedButton) || submitted || disabled) ? '' : '](!magic --button '+ command +'|'+ tokenID +'|'+ buttonID +'|'+ r +'|'+ c + (!isView ? '' : (' --display-ability '+tokenID+'|'+spellDB+'|'+spellName + extension)) + ')'); + content += ((buttonID == selectedButton) || submitted || disabled) ? '' : ('](!magic --button '+ command +'|'+ tokenID +'|'+ buttonID +'|'+ r +'|'+ c + extension + (!isView ? '' : ' '+(spell.api ? '' : sendToWho(charCS,senderId,false,true))+'%{' + spell.dB + '|' + changedSpell.hyphened() + '}')+')'); } buttonID++; }); @@ -2976,9 +3032,12 @@ var MagicMaster = (function() { magicDB = findPower(charCS,spellName).dB; spellTables[w] = spellTables[w].tableSet( fields.Spells_db,r,magicDB ); } + let renamed = !abilityLookup( magicDB, spellName ), + changedSpell = renamed ? 'Display-'+spellName : spellName; spell = getAbility( magicDB, spellName, charCS ); - if (!state.MagicMaster.viewActions) spell.obj[0].set('action',spell.obj[0].get('action').replace(reActionButton,design.grey_action) ); - extension = `${!learn ? '' : ` --message ${tokenID}|Learn Spell|Try to [Learn this spell](!magic ~~learn-spell ${tokenID}¦${spellName})`} ${(spell.api ? '' : toWho)}%{${spell.dB}|${spellName}}`; + if (!!spell.obj && !state.MagicMaster.viewActions) spell.obj = greyOutButtons( tokenID, charCS, spell.obj, (renamed ? changedSpell : '') ); +// extension = `${!learn ? '' : ` --message ${tokenID}|Learn Spell|Try to [Learn this spell](!magic ~~learn-spell ${tokenID}¦${spellName})`} ${(spell.api ? '' : toWho)}%{${spell.dB}|${spellName}}`; + extension = `$ ${(spell.api ? '' : toWho)}%{${spell.dB}|${changedSpell}}`; } content += (buttonID == selectedButton ? '' : ((submitted || disabled || (lv > maxLevel)) ? '' : '[')); content += ((spellType.includes('POWER') && spellValue) ? (spellValue + ' ') : '') + spellName.dispName(); @@ -3086,10 +3145,12 @@ var MagicMaster = (function() { content += '{{desc=1. [Choose](!magic --button '+editCmd+'|'+tokenID+'|'+level+'|'+spellRow+'|'+spellCol+'|?{'+magicWord+' to memorise|'+spellbook+'}) '+magicWord+' to memorise'
+ selectableSlot+'Rename '+slotName+(chosenSlot ? ('](!magic --button GM-RenameMI|'+tokenID+'|'+MIrowref+'|'+MItoStore+'|?{What name should '+slotName+' now have?}) ') : ' ')+' ' @@ -4037,7 +4132,8 @@ var MagicMaster = (function() { pickingUp = (tokenID == putID), shortMenu = pickingUp, pickOrPut = (pickingUp ? 'Pick up' : 'Put away'), - charCS = getCharacter(tokenID); + charCS = getCharacter(tokenID), + isGM = playerIsGM(senderId); if (!putCS || !pickCS) { sendDebug( 'makeShortPOPmenu: pickID or putID is invalid' ); @@ -4076,9 +4172,9 @@ var MagicMaster = (function() { pickedType = (attrLookup( pickCS, fields.Items_type, fields.Items_table, pickRow ) || '').dbName() || '-'; putItems = getTableField( putCS, putItems, fields.Items_table, fields.Items_trueName ); putItems = getTableField( putCS, putItems, fields.Items_table, fields.Items_type ); - let lowerMI = pickedMI.dbName() || '-'; + let lowerMI = pickedMI.dbName().replace(/v\d+$/,'') || '-'; for (i = 0; i < putItems.sortKeys.length; i++) { - mi = (putItems.tableLookup(fields.Items_name,i) || '').dbName() || '-'; + mi = (putItems.tableLookup(fields.Items_name,i) || '').dbName().replace(/v\d+$/,'') || '-'; if (_.isUndefined(mi)) break; if (mi != lowerMI) continue; miTrueName = (putItems.tableLookup(fields.Items_trueName,i) || '').dbName() ||'-'; @@ -4139,7 +4235,7 @@ var MagicMaster = (function() { + ' in free slot}}{{desc2='; content += '[Use '+menuType+' menu](!magic --button '+(pickingUp ? BT.PICKMI_OPTION : BT.PUTMI_OPTION)+'|'+tokenID+'|'+menuType+'|'+pickID+'|'+putID+')}}'; - sendResponse( charCS, content, senderId, flags.feedbackName, flags.feedbackImg, tokenID ); + sendResponse( charCS, content, senderId, flags.feedbackName, flags.feedbackImg ); } else { content = messages.header + '{{desc=' + pickCS.get('name') + ' ' + messages.fruitlessSearch + treasure; sendParsedMsg( tokenID, content, senderId ); @@ -4237,9 +4333,9 @@ var MagicMaster = (function() { + '. Which class do you want/have to '+(drainLevels > 0 ? 'gain' : 'lose')+' the ' + (multiLevels > 1 ? 'next one level? You will then be asked which levels to drain the rest of the levels from, one at a time.' : 'level from?') + '}}{{desc1='; - + _.each( classes, c => { - content += 'Level '+c.level+' ['+(c.classData.name || c.name)+'](!magic --button '+BT.LEVEL_CHANGE+'|'+tokenID+'|'+drainLevels+'|'+c.base+'|'+args[3]+'|?{How many HP to '+(drainLevels > 0 ? 'add' : 'deduct')+'|'+totalHP+'})\n'; + content += 'Level '+c.level+' ['+(c.classData.name || c.name)+'](!magic --button '+BT.LEVEL_CHANGE+'|'+tokenID+'|'+drainLevels+'|'+c.base+'|'+args[3]+'|?{How many HP to '+(drainLevels > 0 ? 'add' : 'deduct')+'|'+c.classData.hd+'}|'+totalHP+')\n'; }); content += '}}'; sendResponse( getCharacter(tokenID), content ); @@ -4512,7 +4608,7 @@ var MagicMaster = (function() { sendError('Internal MagicMaster error'); } - if (args[0] == BT.MI_SPELL || args[0].toUpperCase().includes('POWER')) { + if (args[0] == BT.MI_SPELL || args[0] == BT.MI_SCROLL || args[0].toUpperCase().includes('POWER')) { var charCS = getCharacter(args[1]), storedLevel = attrLookup( charCS, fields.Spells_storedLevel, fields.Spells_table, args[3], args[4] ); if (storedLevel && storedLevel > 0) { @@ -4605,7 +4701,7 @@ var MagicMaster = (function() { } var spell = getAbility( db, spellName, charCS ), - spellCost = ((!!spell.ct && ((args[0] == BT.CAST_MUSPELL) || (args[0] == BT.CAST_PRSPELL))) ? spell.obj[1].cost : 0), + spellCost = ((!!spell.obj && !!spell.ct && ((args[0] === BT.CAST_MUSPELL) || (args[0] === BT.CAST_PRSPELL))) ? spell.obj[1].cost : 0), totalLeft, content, spellValue = parseInt((spellTables.tableLookup( fields.Spells_castValue, rowIndex )),10); @@ -4616,7 +4712,7 @@ var MagicMaster = (function() { setValue( charCS, fields.SpellColIndex, colIndex ); if (absorb) { - let level = (parseInt(spell.obj[1].type.match(/\d+/)) || 0), + let level = (!spell.obj || !spell.obj[1]) ? 1 : (parseInt(spell.obj[1].type.match(/\d+/)) || 0), itemRow = parseInt(attrLookup( charCS, fields.ItemRowRef )); if (isNaN(itemRow)) { let Items = getTable( charCS, fieldGroups.MI ); @@ -4628,7 +4724,7 @@ var MagicMaster = (function() { } } else if (spellValue != 0) { - if (apiCommands.attk && apiCommands.attk.exists && spell.obj[1].body.match(/}}\s*tohitdata\s*=\s*\[.*?\]/im)) { + if (apiCommands.attk && apiCommands.attk.exists && !!spell.obj && !!spell.obj[1] && spell.obj[1].body.match(/}}\s*tohitdata\s*=\s*\[.*?\]/im)) { sendAPI(fields.attackMaster+' '+senderId+' --weapon '+tokenID+'|Take '+spellName+' in-hand as a weapon and then Attack with it||'+miName); } else { if (spellValue > 0) spellValue--; @@ -4748,6 +4844,7 @@ var MagicMaster = (function() { isMI = cmd.includes('MI'), isPower = cmd.includes('POWER'), isSpell = cmd.includes('SPELL'), + isScroll = cmd.includes('SCROLL'), isView = !cmd.includes('REVIEW'), isGM = args[0].includes('GM'), tokenID = args[1], @@ -4771,7 +4868,9 @@ var MagicMaster = (function() { } else if (isPR) { followOn = (isView ? BT.VIEWMEM_MI_PRSPELLS : BT.EDIT_MIPRSPELLS); } else if (isSpell) { - followOn = (isView ? BT.VIEWMEM_MI_SPELLS : BT.EDIT_MIMUSPELLS); + followOn = (isView ? BT.VIEWMEM_MI_SPELLS : BT.EDIT_MISPELLS); + } else if (isScroll) { + followOn = (isView ? BT.VIEWMEM_MI_SCROLL : BT.EDIT_MISPELLS); } else { followOn = (isView ? BT.VIEW_MI : (args[0].includes('MARTIAL') ? BT.CHOOSE_MARTIAL_MI : (args[0].includes('ALLITEMS') ? BT.CHOOSE_ALLITEMS_MI : BT.CHOOSE_MI))); } @@ -5036,33 +5135,33 @@ var MagicMaster = (function() { /* * Handle a level change request */ - + var handleLevelDrain = function( args, senderId, msg = '' ) { - var tokenID = args[0], - drainLevels = parseInt(args[1]) || -1, - fixedClass = args[6] || '', - classChosen = args[2] || fixedClass, - totalLevels = parseInt(args[3]) || drainLevels, - hitPoints = Math.abs(parseInt(args[4]) || 0), - totalHP = parseInt(args[5]) || 0, + var tokenID = args[0], // tokenID + drainLevels = parseInt(args[1]) || -1, // 4 + fixedClass = args[6] || '', // fighter + classChosen = args[2] || fixedClass, // fighter + totalLevels = parseInt(args[3]) || drainLevels, // 4 + hitPoints = Math.abs(parseInt(evalAttr(args[4])) || 0), // [[5d10]] + totalHP = parseInt(args[5]) || 0, // always called = 0 loopCount = Math.abs(drainLevels), charCS = getCharacter(tokenID), increment = drainLevels > 0 ? 1 : -1, + levelHP = hitPoints * increment, classes = classObjects( charCS, senderId ), levelField, hd; - if (classes && classes.length === 1) { - classChosen = classes[0].base; - if (!hitPoints) { - hitPoints = evalAttr(classes[0].classData.hd.replace(/(\d+)(d.+)/i,'(('+String(drainLevels)+'*$1)$2)')); - loopCount = 1; - } - } - if (!classChosen) { + if ((classes && classes.length === 1) || fixedClass) { + classChosen = classChosen || classes[0].base; + hitPoints = parseInt(hitPoints || evalAttr(classes[0].classData.hd.replace(/(\d+)(d.+)/i,'(('+String(drainLevels)+'*$1)$2)'))) || 0; + levelHP = hitPoints * increment; + loopCount = 1; + increment = increment * Math.abs(drainLevels); + } else if (!classChosen) { makeLevelDrainMenu( args, classes, senderId, msg, totalHP ); return; - } + }; switch (classChosen.toLowerCase()) { case 'wizard': levelField = fields.Wizard_level; @@ -5083,20 +5182,25 @@ var MagicMaster = (function() { } } setAttr( charCS, levelField, Math.max(0,((parseInt(attrLookup( charCS, levelField ) || 1) || 1) + increment)) ); - setAttr( charCS, fields.HP,((parseInt(attrLookup( charCS, fields.HP ) || 0) || 0) + (hitPoints * increment)) ); - setAttr( charCS, fields.MaxHP, Math.max(0,((parseInt(attrLookup( charCS, fields.MaxHP ) || 0) || 0) + (hitPoints * increment))) ); + setAttr( charCS, fields.HP,((parseInt(attrLookup( charCS, fields.HP ) || 0) || 0) + levelHP) ); + setAttr( charCS, fields.MaxHP, Math.max(0,((parseInt(attrLookup( charCS, fields.MaxHP ) || 0) || 0) + levelHP)) ); totalHP += hitPoints; if (--loopCount > 0) { + let content = '&{template:'+fields.warningTemplate+'}{{title=Change in Level}}{{desc=Successfully '+(increment > 0 ? 'boosted' : 'drained')+' '+classChosen + + ' class by one level, which in total makes '+(Math.abs(totalLevels) - loopCount)+' across all classes.' + + ' A total of '+totalHP+'HP have been '+(increment > 0 ? 'gained' : 'lost')+'}}'; + sendResponse( charCS, content ); handleLevelDrain( [tokenID,(drainLevels-increment),'',totalLevels,0,totalHP,fixedClass], senderId, 'Successfully '+(increment > 0 ? 'boosted' : 'drained')+' '+classChosen+' class by 1 level' ); } else { - handleMemAllPowers( [BT.MEMALL_POWERS,tokenID,1,-1,-1,'',''], senderId, true ); - handleCheckWeapons( tokenID, charCS ); - handleCheckSaves( null, null, [getObj('graphic',tokenID)], true ); + setAttr( charCS, fields.Thac0_base, handleGetBaseThac0( charCS ) ); let content = '&{template:'+fields.warningTemplate+'}{{title=Change in Level}}{{desc=Successfully '+(increment > 0 ? 'boosted' : 'drained')+' '+classChosen - + ' class by '+(fixedClass ? (totalLevels+' levels') : ('one level, which in total makes '+totalLevels+' across all classes')) + + ' class by '+((fixedClass || Math.abs(increment) > 1) ? (totalLevels+' levels') : ('one level, which in total makes '+totalLevels+' across all classes')) + ', and recalculated all saves, reassessed all weapon use and reset usable powers.' + ' A total of '+totalHP+'HP have been '+(increment > 0 ? 'gained' : 'lost')+'}}'; sendResponse( charCS, content ); + setTimeout( () => handleMemAllPowers( [BT.MEMALL_POWERS,tokenID,1,-1,-1,'',''], senderId, true ), 100); + setTimeout( () => handleCheckWeapons( tokenID, charCS ), 200); + setTimeout( () => handleCheckSaves( [tokenID], null, null, true ), 300); } } @@ -5354,6 +5458,7 @@ var MagicMaster = (function() { case 'rechargeable': case 'perm-rechargeable': case 'cursed+rechargeable': + if (getStoredSpells( charCS, MIname ).count > 0) break; if (MIqty == charges && !MItype.includes('cursed') && !MItype.includes('perm')) { if (((MItype.toLowerCase() === 'changing') || (MItype.toLowerCase() === 'change-last')) && MIchangeTo) { handleStoreMI( ['',tokenID, MIrowref, MIchangeTo, 0, 'silent' ], false, senderId ); @@ -5971,7 +6076,7 @@ var MagicMaster = (function() { * qty -1 means not yet chosen, cost -1 means not yet agreed or no cost **/ - async function handlePickOrPut( args, senderId ) { // set + async function handlePickOrPut( args, senderId ) { var tokenID = args[1], fromRowRef = args[2], @@ -6058,7 +6163,7 @@ var MagicMaster = (function() { MItrueType = fromMIbag.tableLookup( fields.Items_trueType, fromRowRef ), MItext = MIname, slotInc = 1, - isStackable = (stackable.includes(fromSlotType) && stdEqual( toSlotName, MIname ) && stdEqual( toSlotType, MItype ) && stdEqual( toSlotTrueName, MItrueName )), + isStackable = (stackable.includes(fromSlotType) && stdEqual( toSlotName.replace(/\-v\d+$/, ''), MIname ) && stdEqual( toSlotType, MItype ) && stdEqual( toSlotTrueName, MItrueName )), finalQty, finalCharges, pickQty, charges, content, MIobj; if (showType) { @@ -6146,10 +6251,13 @@ var MagicMaster = (function() { let dupRow = toMIbag.tableFind( fields.Items_name, newName || MIname ) || toMIbag.tableFind( fields.Items_trueName, newName || MItrueName ); if (!isStackable && !_.isUndefined(dupRow) && dupRow !== toRowRef) { + let j = 2; + while (!_.isUndefined(toMIbag.tableFind( fields.Items_name, (MIname+' v'+j), false ))) {j++}; content = '&{template:'+fields.menuTemplate+'}{{name=Duplicate Item}}' + '{{desc=The item you have selected to '+pickPutText+' is not stackable with an item of the same name already stored}}' - + '{{desc1=[Choose a new name](!magic --button POPrename|'+tokenID+'|'+fromRowRef+'|'+fromID+'|'+toID+'|'+toRowRef+'|'+qty+'|'+expenditure+'|?{What new name do you want to give '+MIname+'?}) for '+MIname - + ' or [Choose something else](!magic --pickorput '+tokenID+'|'+fromID+'|'+toID+')}}'; + + '{{desc1=[Save '+MIname+' as '+(MIname+' v'+j)+'](!magic --button POPrename|'+tokenID+'|'+fromRowRef+'|'+fromID+'|'+toID+'|'+toRowRef+'|'+qty+'|'+expenditure+'|'+(MIname+' v'+j)+')' + + '\n or [Choose a new name](!magic --button POPrename|'+tokenID+'|'+fromRowRef+'|'+fromID+'|'+toID+'|'+toRowRef+'|'+qty+'|'+expenditure+'|?{What new name do you want to give '+MIname+'?}) for '+MIname + + '\n or [Choose something else](!magic --pickorput '+tokenID+'|'+fromID+'|'+toID+')}}'; sendResponse( charCS, content, senderId, flags.feedbackName, flags.feedbackImg, tokenID ); return; } else if (!_.isUndefined(newName) && newName.length) { @@ -6157,7 +6265,7 @@ var MagicMaster = (function() { MIobj = abilityLookup( fields.MagicItemDB, MIname, fromCS ); } if (MIname === MItrueName) MItrueName = newName; - if (MIobj.obj && MIobj.obj[0]) { + if (MIobj.obj && MIobj.obj[0] && MIobj.obj[1]) { MIobj.obj[0].set('name',newName); let key = 'ababzzqqrst', oldDispName = MIname.replace(/-/g,' '), @@ -6249,8 +6357,9 @@ var MagicMaster = (function() { } if (MItoStore.toLowerCase() != 'remove') { - let itemObj = abilityLookup( fields.MagicItemDB, MItoStore, charCS ); - setAttr( charCS, fields.ItemCastingTime, itemObj.obj[1].ct ); +// let itemObj = abilityLookup( fields.MagicItemDB, MItoStore, charCS ); + let itemSpeed = resolveData( MItoStore, fields.MagicItemDB, reItemData, charCS, {speed:reSpellSpecs.speed}, MIrowref ).parsed.speed; + setAttr( charCS, fields.ItemCastingTime, itemSpeed ); setAttr( charCS, fields.ItemSelected, 1 ); }; @@ -6382,11 +6491,11 @@ var MagicMaster = (function() { containerNo = parseInt(attrLookup( charCS, fields.ItemContainerType )) || 0, values = MItables.copyValues(); - if (!magicItem.ct) { - sendDebug('handleStoreMI: selected magic item speed/type not defined'); - sendError('Selected Magic Item not fully defined'); - return; - } +// if (!magicItem.ct) { +// sendDebug('handleStoreMI: selected magic item speed/type not defined'); +// sendError('Selected Magic Item not fully defined'); +// return; +// } var midbCS, MIdisplayName; @@ -6401,7 +6510,7 @@ var MagicMaster = (function() { if (miData.hide && !['hide','nohide','reveal'].includes(miData.hide)) { MIdisplayName = miData.hide; getAbility( fields.MagicItemDB, MIdisplayName, charCS, true, true, MIchosen ); - } else if (GMonly && (state.MagicMaster.autoHide || (miData.hide && miData.hide === 'hide')) && reLooksLike.test(magicItem.obj[1].body)) { + } else if (GMonly && (state.MagicMaster.autoHide || (miData.hide && miData.hide === 'hide')) && !!magicItem.obj && reLooksLike.test(magicItem.obj[1].body)) { MIdisplayName = getShownType( magicItem, MIrowref, miData.itemType ); getAbility( fields.MagicItemDB, MIdisplayName, charCS, true, true, MIchosen ); } else { @@ -6560,7 +6669,7 @@ var MagicMaster = (function() { MIrowref = args[2], MIchosen = args[3], charCS = getCharacter(tokenID), - Items, newItem; + Items, newItem, reveal; if (!charCS) { sendDebug('handleHideMI: invalid tokenID passed'); @@ -6577,6 +6686,8 @@ var MagicMaster = (function() { Items = Items.tableSet( fields.Items_name, MIrowref, MIchosen ); Items = Items.tableSet( fields.Items_trueType, MIrowref, Items.tableLookup( fields.Items_type, MIrowref ) ); + reveal = resolveData( Items.tableLookup( fields.Item_trueName, MIrowref ), fields.MagicItemDB, reItemData, charCS, {reveal:reSpellSpecs.reveal} ).parsed.reveal; + Items = Items.tableSet( fields.Items_reveal, MIrowref, (reveal.toLowerCase() !== 'manual' ? reveal : '')); newItem = abilityLookup( fields.MagicItemDB, MIchosen, charCS ); if (newItem.obj) Items = Items.tableSet( fields.Items_type, MIrowref, newItem.obj[1].charge ); @@ -7447,6 +7558,8 @@ var MagicMaster = (function() { return; } setAttr( charCS, fields.CastingLevel, casterLevel( charCS, 'MI' )); + setAttr( charCS, fields.MU_CastingLevel, casterLevel( charCS, 'MU' )); + setAttr( charCS, fields.PR_CastingLevel, casterLevel( charCS, 'PR' )); makeViewUseMI( [action, tokenID, -1], senderId ); return; @@ -7999,7 +8112,6 @@ var MagicMaster = (function() { MIBagSecurity = 1; } } - if (!search && MIBagSecurity < 0) { sendParsedMsg( putID, messages.noStoring, senderId ); return; @@ -8224,12 +8336,12 @@ var MagicMaster = (function() { msg = ''; switch (flag.toLowerCase()) { - case 'fancy-menus': +/* case 'fancy-menus': state.MagicMaster.fancy = value; if (!_.isUndefined(state.attackMaster.fancy)) state.attackMaster.fancy = value; msg = value ? 'Fancy menus will be used' : 'Plain menus will be used'; break; - +*/ case 'specialist-rules': state.MagicMaster.spellRules.specMU = value; msg = value ? 'Only rules-based specialists get extra spell' : 'Any specialist gets extra spell'; @@ -8252,7 +8364,7 @@ var MagicMaster = (function() { case 'custom-spells': state.MagicMaster.spellRules.denyCustom = value; - msg = value ? 'Custom Spells only from user databases' : 'Distributed custom spells allowed'; + msg = value ? 'Custom items only from user databases' : 'Distributed custom items allowed'; updateDBindex(true); break; @@ -8432,8 +8544,7 @@ var MagicMaster = (function() { sendResponseError(senderId,'Level change requested ('+args[1]+') is invalid'); return; }; - - handleLevelDrain( [args[0],args[1],'','',args[2],'',args[3]], senderId ); + handleLevelDrain( [args[0],args[1],'','',args[2],0,args[3]], senderId ); } /** @@ -8726,12 +8837,9 @@ var MagicMaster = (function() { if (!args) args = []; - if (!args[1] && selected && selected.length) { - args[1] = selected[0]._id; - } else if (!args[1]) { - sendDebug( 'doMessage: tokenID is invalid' ); - sendError( 'No token selected' ); - return; + var sel = ''; + if (selected && selected.length) { + sel = selected[0]._id; } if (args.length <=2) { @@ -8740,22 +8848,29 @@ var MagicMaster = (function() { return; }; - var cmd = args[0], - tokenID = args[1], - charCS = getCharacter(tokenID); + var cmd = (args[0] || '-').toLowerCase(), + tokenID = args[1] || sel, + charCS = getCharacter(tokenID), + isCmd = ['gm','whisper','w','character','c','standard','s','public','p'].includes(cmd); - if (!getObj('graphic',tokenID) && !charCS) { + if (!getObj('graphic',tokenID) && !charCS && !isCmd) { args.unshift('standard'); - cmd = args[0]; - tokenID = args[1] + cmd = (args[0] || '').toLowerCase(); + tokenID = args[1] || sel; charCS = getCharacter(tokenID); } + cmd = cmd.toLowerCase(); + if (!charCS && cmd !== 'gm') { + sendDebug( 'doMessage: tokenID is invalid' ); + sendError( 'No token selected' ); + return; + } var msg = '&{template:'+fields.messageTemplate+'}{{name=' + (args[2] || '') + '}}{{desc=' + parseStr(args[3] || '',msgReplacers) + '}}'; const reAttrs = /\^\^([^\|\^]+)\|?(max|current)?\|?([^\|\^]+)?\^\^/i; const attrRes = ( a, v, m = 'current', d = '0' ) => attrLookup( charCS, [v,m,d] ) || ''; - while (reAttrs.test(msg)) msg = msg.replace(reAttrs,attrRes); + if (!!charCS) while (reAttrs.test(msg)) msg = msg.replace(reAttrs,attrRes); switch (cmd.toLowerCase()) { case 'gm': @@ -9019,6 +9134,7 @@ var MagicMaster = (function() { case BT.PR_SPELL : case BT.MI_SPELL : case BT.MI_POWER : + case BT.MI_SCROLL : case BT.POWER : handleChooseSpell( args, senderId ); @@ -9038,6 +9154,9 @@ var MagicMaster = (function() { case BT.EDIT_PRSPELLS : case BT.EDIT_POWERS : case BT.EDIT_MIPOWERS : + case BT.EDIT_MIMUSPELLS : + case BT.EDIT_MIPRSPELLS : + case BT.EDIT_MISPELLS : handleRedisplayManageSpells( args, senderId ); break; @@ -9047,8 +9166,11 @@ var MagicMaster = (function() { case BT.VIEW_POWER : case BT.VIEW_MI_MUSPELL : case BT.VIEW_MI_PRSPELL : + case BT.VIEW_MI_MUSCROLL : + case BT.VIEW_MI_PRSCROLL : case BT.VIEW_MI_POWER : case BT.VIEW_MI_SPELL : + case BT.VIEW_MI_SCROLL : case BT.REVIEW_MUSPELL : case BT.REVIEW_PRSPELL : case BT.REVIEW_POWER : @@ -9080,6 +9202,7 @@ var MagicMaster = (function() { case BT.VIEWMEM_MI_MUSPELLS : case BT.VIEWMEM_MI_PRSPELLS : case BT.VIEWMEM_MI_SPELLS : + case BT.VIEWMEM_MI_SCROLL : case BT.VIEWMEM_MI_POWERS : makeViewMemSpells( args, senderId ); @@ -9805,6 +9928,7 @@ var MagicMaster = (function() { try { if (!obj) return; let charCS = getCharacter( obj.id ); + if (!charCS) return; if (obj.get("status_dead")) { // If the token dies and is marked as "dead" by the GM // set its container type to 6 (dead). @@ -9814,7 +9938,7 @@ var MagicMaster = (function() { setAttr(charCS, fields.ItemContainerType, (attrLookup(charCS, fields.ItemOldContainerType) || 1)); } } catch (e) { - sendCatchError('RoundMaster',null,e,'RoundMaster handleTokenDeath()'); + sendCatchError('MagicMaster',null,e,'MagicMaster handleTokenDeath()'); } return; }; diff --git a/MagicMaster/magicMaster.js b/MagicMaster/magicMaster.js index 49d083c3ff..da74bd12e7 100644 --- a/MagicMaster/magicMaster.js +++ b/MagicMaster/magicMaster.js @@ -87,14 +87,19 @@ API_Meta.MagicMaster={offset:Number.MAX_SAFE_INTEGER,lineCount:-1}; * RPGM maths operators for numbers passed to --mi-charges. Fixed stacking of looted * items. Added container self-heal capability on version change. * v3.5.1 02/08/2024 Fixed storing an item to a character sheet of security type 7 (Force Sentient) + * v4.0.1 22/09/2024 Gave option for pre-determined name change on picking up an item with a duplicated + * displayed name. Fix listing/viewing/using a spell or power that is on the sheet but + * not in a database. Improved error checking and type conversion. Corrected speed + * of items to use inheritance via resolveData(). Improved --message command resolution + * of selected vs. id token selection. Added new MI-DB-Treasure database. */ var MagicMaster = (function() { 'use strict'; - var version = '3.5.1', + var version = '4.0.1', author = 'RED', pending = null; - const lastUpdate = 1722615657; + const lastUpdate = 1738351019; /* * Define redirections for functions moved to the RPGMaster library @@ -133,6 +138,7 @@ var MagicMaster = (function() { const addMIspells = (...a) => libRPGMaster.addMIspells(...a); const handleCheckWeapons = (...a) => libRPGMaster.handleCheckWeapons(...a); const handleCheckSaves = (...a) => libRPGMaster.handleCheckSaves(...a); + const handleGetBaseThac0 = (...a) => libRPGMaster.handleGetBaseThac0(...a); const parseClassDB = (...a) => libRPGMaster.parseClassDB(...a); const parseData = (...a) => libRPGMaster.parseData(...a); const parseStr = (...a) => libRPGMaster.parseStr(...a); @@ -216,7 +222,7 @@ var MagicMaster = (function() { boxed_number: '"display: inline-block; background-color: yellow; border: 1px solid blue; padding: 2px; color: black; font-weight: bold;"', success_box: '"display: inline-block; background-color: yellow; border: 1px solid lime; padding: 2px; color: green; font-weight: bold;"', failure_box: '"display: inline-block; background-color: yellow; border: 1px solid red; padding: 2px; color: maroon; font-weight: bold;"', - grey_action: '<span style="display: inline-block; background-color: lightgrey; border: 1px solid black; padding: 4px; color: dimgrey; font-weight: extra-light;">$1</span>' + grey_action: '<span style="display: inline-block; background-color: lightgrey; border: 1px solid black; padding: 4px; color: dimgrey; font-weight: extra-light;">$2</span>$4', }; /* @@ -233,11 +239,10 @@ var MagicMaster = (function() { +' '
+' MagicMaster API v'+version+'' +'and later' + +'' +' New: in this Help Handout' - +'Update: Added RPGM maths processor to many numerical command parameters ' - +'Update: On viewing a spell or an item description, action buttons are disabled ' - +'Update: Fixed --add-mi when replacing an item to also replace in-hand weapons and worn rings ' - +'Update: Extended optional parameters for --level-change ' + +'New: Non-stacking duplicate items picked up offer option for default rename to make unique ' + +'' +' The MagicMaster API provides functions to manage all types of magic, including Wizard & Priest spell use and effects; Character, NPC & Monster Powers; and discovery, looting, use and cursing of Magic Items. All magical aspects can work with the RoundMaster API to implement token markers that show and measure durations, and produce actual effects that can change token or character sheet attributes temporarily for the duration of the spell or permanently if so desired. They can also work with the InitiativeMaster API to provide menus of initiative choices and correctly adjust individual initiative rolls, including effects of Haste and Slow and similar spells. This API can also interact with the MoneyMaster API (under development) to factor in the passing of time, the cost of spell material use, the cost of accommodation for resting, and the cost of training for leveling up as a spell caster (Wizard, Priest or any other). ' +'Syntax of MagicMaster calls' +'The MagicMaster API is called using !magic (or the legacy command !mibag). ' @@ -250,36 +255,7 @@ var MagicMaster = (function() { +'!magic --spellmenu [token_id]|[MU/PR/POWER] --mimenu [token_id]' +' When specifying the commands in this document, parameters enclosed in square brackets [like this] are optional: the square brackets are not included when calling the command with an optional parameter, they are just for description purposes in this document. Parameters that can be one of a small number of options have those options listed, separated by forward slash \'/\', meaning at least one of those listed must be provided (unless the parameter is also specified in [] as optional): again, the slash \'/\' is not part of the command. Parameters in UPPERCASE are literal, and must be spelt as shown (though their case is actually irrelevant). ' +'' - +' Roll Query Extension' - +'The syntax of the Roll20 Roll Query has been extended within the RPGMaster MagicMaster API to support !magic API commands with Roll Queries that the GM is invited to answer, rather than the player, regardless of who issued the command. The standard syntax and the extended syntax is shown below: ' - +'Standard Syntax: ?{Query text|option1|option2|...}' - +' When used in a !magic API command, the extended Roll Query will prompt the GM with a button in the Chat Window for the GM to answer the question posed by the query text. The result will be fed into the action taken by the API command. This allows the GM to be involved when, for instance, a Staff of the Magi absorbs levels of spells cast at a character that the character & player can\'t know. ' - +'' - +' Overriding the Controlling Player' - +'When a command is sent to Roll20 APIs / Mods, Roll20 tries to work out which player or character sent the command and tells the API its findings. The API then uses this information to direct any output appropriately. However, when it is the API itself that is sending commands, such as from a {{successcmd=...}} or {{failcmd=...}} sequence in a RPGMdefault Roll Template, Roll20 sees the API as the originator of the command and sends output to the GM by default. This is not always the desired result. ' - +'To overcome this, or when output is being misdirected for any other reason, a Controlling Player Override Syntax (otherwise known as a SenderId Override) has been introduced (for RPGMaster Suite APIs only, I\'m afraid), with the following command format: ' - +'!magic [sender_override_id] --cmd1 args1... --cmd2 args2...' - +' The optional sender_override_id (don\'t include the [...], that\'s just the syntax for "optional") can be a Roll20 player_id, character_id or token_id. The API will work out which it is. If a player_id, the commands output will be sent to that player when player output is appropriate, even if that player is not on-line (i.e. no-one will get it if they are not on-line). If a character_id or token_id, the API will look for a controlling player who is on-line and send appropriate output to them - if no controlling players are on-line, or the token/character is controlled by the GM, the GM will receive all output. If the ID passed does not represent a player, character or token, or if no ID is provided, the API will send appropriate output to whichever player Roll20 tells the API to send it to. ' - +'' - +' New: Doing Maths for Numeric Values' - +'Roll20 provides many excellent maths functions for commands made to the chat window and contained in API button strings. However, it is not always possible to use the Roll20 maths using the [[...]] syntax to achieve what you want. RPGMaster provides an alternative set of maths functions to help resolve these issues. Formulas can be entered for many numeric values required by RPGMaster commands using the supported syntax. However: this syntax does not work for anything other than RPGMaster commands as of writing (this might be a future develpment). ' - +'The square brackets [[...]] are not required. The syntax follows normal maths presedent with a few additional operators to support range calculations and dice rolls: ' - +'
' - +' Using Character Sheet Ability/Action buttons' - +'The most common approach for the Player to run these commands is to use Ability macros on their Character Sheets which are flagged to appear as Token Action Buttons: Ability macros & Token Action Buttons are standard Roll20 functionality, refer to the Roll20 Help Centre for information on creating and using these. ' - +'In fact, the simplest configuration is to provide only Token Action Buttons for the menu commands: --spellmenu and --mimenu. From these, most other commands can be accessed. If using the CommandMaster API, its character sheet setup functions can be used to add the necessary Ability Macros and Token Action Buttons to any Character Sheet. ' - +'' + +'[General API Help]' +' How MagicMaster works' +'Race, Class, Item, Spell and Power databases' +'MagicMaster uses a large range of items held in databases. The current versions of these databases are distributed with the game-version-specific RPGMaster Library, updated as new versions are released via Roll20. The provided databases are held in memory, but can be extracted to ability macros in database character sheets using the !magic --extract-db command. These macros can do anything that can be programmed in Roll20 using ability macros and calls to APIs, and are found (either in the Character Sheet database or the internal database in memory) and called by the MagicMaster API when the Player selects them using the menus provided by the MagicMaster functions. The GM can add to the provided items in the databases using standard Roll20 Character Sheet editing, following the instructions provided in the Magic Database Handout. ' @@ -316,16 +292,16 @@ var MagicMaster = (function() { +'1.Spell and Power management' +'--spellmenu [token_id]|[MU/PR/POWER]' +' 2.Magic Item management' +'--mimenu [token_id] |
New: Note: Some items are not stackable - they are single items with charges such as a wand or rod, or a spell-storing item which must retain its uniqueness so the spells remain associated. However, it is also the case that non-stackable items like these need to have unique names in the container to retain their unique identity. Thus, when a second copy of a non-stackable item is picked up or put away into a container that already contains another item with the same name, the player will be asked to provide a new unique name for the item (which cannot be the same as any other magic item, even those not in the container - sorry, you can\'t turn that ring of protection+1 into a ring of wishes!). Once the item is stored with this new name, it will work in all respects like the item it is, just with a different name.
' + +'Note: Some items are not stackable - they are single items with charges such as a wand or rod, or a spell-storing item which must retain its uniqueness so the spells remain associated. However, it is also the case that non-stackable items like these need to have unique names in the container to retain their unique identity. Thus, when a second copy of a non-stackable item is picked up or put away into a container that already contains another item with the same name, the player will be asked to provide a new unique name for the item (which cannot be the same as any other magic item, even those not in the container - sorry, you can\'t turn that ring of protection+1 into a ring of wishes!). Once the item is stored with this new name, it will work in all respects like the item it is, just with a different name.
' +'--pickorput token_id|pick_id|put_id|[SHORT/LONG]' +'
Takes a mandatory token ID for the Player\'s character, a mandatory token ID for the token to pick items from, a mandatory token ID for the token to put items in to, and an optional argument specifying whether to use a long or a short menu.
' @@ -735,6 +711,7 @@ var MagicMaster = (function() { RW_DMGL: 'RW_DMGL', MI_SPELL: 'MI_SPELL', MI_POWER: 'MI_POWER', + MI_SCROLL: 'MI_SCROLL', MI_POWER_USED: 'MI_POWER_USED', MI_POWER_CHARGE_USED:'MI_POWER_CHARGE_USED', LEVEL_CHANGE: 'LEVEL_CHANGE', @@ -798,6 +775,7 @@ var MagicMaster = (function() { EDIT_POWERS: 'EDIT_POWERS', EDIT_MIMUSPELLS: 'EDIT_MIMUSPELLS', EDIT_MIPRSPELLS: 'EDIT_MIPRSPELLS', + EDIT_MISPELLS: 'EDIT_MISPELLS', EDIT_MIPOWERS: 'EDIT_MIPOWERS', EDIT_MI: 'EDIT_MI', EDIT_MARTIAL: 'EDIT_MARTIAL', @@ -828,6 +806,9 @@ var MagicMaster = (function() { VIEW_MI_SPELL: 'VIEW_MI_SPELL', VIEW_MI_MUSPELL: 'VIEW_MI_MUSPELL', VIEW_MI_PRSPELL: 'VIEW_MI_PRSPELL', + VIEW_MI_SCROLL: 'VIEW_MI_SCROLL', + VIEW_MI_MUSPELL: 'VIEW_MI_MUSCROLL', + VIEW_MI_PRSPELL: 'VIEW_MI_PRSCROLL', VIEW_MI: 'VIEW_MI', VIEWMI_OPTION: 'VIEWMI_OPTION', VIEWMEM_MUSPELLS: 'VIEWMEM_MUSPELLS', @@ -837,6 +818,9 @@ var MagicMaster = (function() { VIEWMEM_MI_SPELLS: 'VIEWMEM_MI_SPELLS', VIEWMEM_MI_MUSPELLS:'VIEWMEM_MI_MUSPELLS', VIEWMEM_MI_PRSPELLS:'VIEWMEM_MI_PRSPELLS', + VIEWMEM_MI_SCROLL: 'VIEWMEM_MI_SCROLL', + VIEWMEM_MI_MUSCROLL:'VIEWMEM_MI_MUSCROLL', + VIEWMEM_MI_PRSCROLL:'VIEWMEM_MI_PRSCROLL', POP_PICK: 'POP_PICK', POP_STORE: 'POPsubmit', PICKMI_OPTION: 'PICKMI_OPTION', @@ -881,7 +865,7 @@ var MagicMaster = (function() { const reRepeatingTable = /^(repeating_.*)_\$(\d+)_.*$/; const reItemData = /}}[\s\w\-]*?(?'+msg+'',null,{noarchive:!flags.archive, use3d:false}); + if (playerIsGM(state.MagicMaster.debug)) { + log('MagicMaster Debug: '+msg); + } else { + var player = getObj('player',state.MagicMaster.debug), + to; + if (player) { + to = '/w "' + player.get('_displayname') + '" '; + } else + {throw ('sendDebug could not find player');} + if (!msg) + {msg = 'No debug msg';} + sendChat('MagicMaster Debug',to + ''+msg+'',null,{noarchive:!flags.archive, use3d:false}); + }; }; }; @@ -1661,7 +1649,7 @@ var MagicMaster = (function() { * i.e. which versions of MagicMaster it is matched to */ - var csVer = charCS => parseFloat(((attrLookup( charCS, fields.msVersion ) || '1.5').match(/^\d+\.\d+/) || ['1.5'])[0]) || 1.5; + var csVer = (charCS) => parseFloat(((attrLookup( charCS, fields.msVersion ) || '1.5').match(/^\d+\.\d+/) || ['1.5'])[0]) || 1.5; /** * Express a cost in coins for display @@ -1794,7 +1782,16 @@ var MagicMaster = (function() { setAttr( charCS, fields.Casting_name, castingName ); if (itemName.length) { setAttr( charCS, fields.ItemChosen, itemName ); - } + let item = abilityLookup( fields.MagicItemDB, itemName ); +// log('setCaster: cmd = '+args[0].toUpperCase()+', cmd test = '+(args[0].toLowerCase().includes('mi'))+', !!item.obj = '+!!item.obj+', charge = '+((!!item.obj && item.obj[1].charge) ? item.obj[1].charge.toLowerCase() : 'undefined')+' so '+((!!item.obj && item.obj[1].charge) ? chargedList.includes(item.obj[1].charge.toLowerCase()) : false)); + if (args[0].toLowerCase().includes('mi') && !!item.obj && !!item.obj[1].charge) { +// log('setCaster: testing charge = '+chargedList.includes(item.obj[1].charge.toLowerCase())); + if (chargedList.includes(item.obj[1].charge.toLowerCase())) { + args[0] = BT.MI_SCROLL; +// log('setCaster: cmd changed to '+args[0]); + } + }; + }; return args; }; @@ -2485,7 +2482,7 @@ var MagicMaster = (function() { bagCS = createObj( "character", {name:miName, avatar: design.bag_icon, - inplayerjournals:charCS.get("inplayerjournals"), + inplayerjournals:(charCS.get("inplayerjournals") || ''), controlledby:charCS.get("controlledby")}); setAttr( bagCS, fields.Race, 'Magic Item' ); @@ -2507,10 +2504,10 @@ var MagicMaster = (function() { let values = Items.copyValues(); values[fields.Items_name[0]][fields.Items_name[1]] = itemData.name; values[fields.Items_trueName[0]][fields.Items_trueName[1]] = (itemData.trueName || itemData.name); - values[fields.Items_speed[0]][fields.Items_speed[1]] = itemData.speed || 5; - values[fields.Items_trueSpeed[0]][fields.Items_trueSpeed[1]] = itemData.speed || 5; - values[fields.Items_qty[0]][fields.Items_qty[1]] = itemData.qty || 1; - values[fields.Items_trueQty[0]][fields.Items_trueQty[1]] = itemData.qty || 1; + values[fields.Items_speed[0]][fields.Items_speed[1]] = evalAttr(itemData.speed) || 5; + values[fields.Items_trueSpeed[0]][fields.Items_trueSpeed[1]] = evalAttr(itemData.speed) || 5; + values[fields.Items_qty[0]][fields.Items_qty[1]] = evalAttr(itemData.qty) || 1; + values[fields.Items_trueQty[0]][fields.Items_trueQty[1]] = evalAttr(itemData.qty) || 1; values[fields.Items_cost[0]][fields.Items_cost[1]] = 0; values[fields.Items_type[0]][fields.Items_type[1]] = itemData.type || 'uncharged'; values[fields.Items_trueType[0]][fields.Items_trueType[1]] = itemData.trueType || itemData.type || 'uncharged'; @@ -2525,7 +2522,7 @@ var MagicMaster = (function() { } } else { bagCS = bagCS[0]; - bagCS.set({inplayerjournals:charCS.get("inplayerjournals"), controlledby:charCS.get("controlledby")}); + bagCS.set({inplayerjournals:(charCS.get("inplayerjournals") || ''), controlledby:charCS.get("controlledby")}); } return; } @@ -2580,7 +2577,27 @@ var MagicMaster = (function() { .replace(/{\s*selected\s*\|/ig,'{'+charCS.get('name')+'|'), null, who), 2000); }; - + + /* + * Grey out all active buttons (except [View...] buttons when viewing + * a spell or item description and not using it. + */ + + var greyOutButtons = function( tokenID, charCS, obj, renamed='' ) { + + var setVal = ( str, field, param='current' ) => attrLookup( charCS, [field,param] ); + + var action = (obj[0].get('action') || '').replace(/@\{selected\|token_id\}/img,'') + .replace(/@\{selected\|(.+?)(?:\|(current|max))?\}/img,setVal) + .replace(reActionButton,design.grey_action) + .replace(/^!.+$/mg,''); + if (renamed) { + obj = setAbility( charCS, renamed, action ); + } else { + obj[0].set('action', action); + }; + return obj; + }; // ---------------------------------------------------- Make Menus --------------------------------------------------------- @@ -2723,6 +2740,7 @@ var MagicMaster = (function() { if (mi.length > 0 && (includeEmpty || mi != '-')) { miObj = abilityLookup( fields.MagicItemDB, mi, charCS, true ); renamed = !miObj.dB.toLowerCase().includes('-db'); + let changedMI = renamed ? 'Display-'+mi : mi; makeGrey = makeGrey || (!showMagic && (!miObj.obj || miObj.obj[1].type.toLowerCase().includes('magic'))); if (showTypes && miObj.obj) { miText = getShownType( miObj, i, resolveData( trueMI, fields.MagicItemDB, reItemData, charCS, {itemType:reSpellSpecs.itemType}, i ).parsed.itemType ); @@ -2738,8 +2756,8 @@ var MagicMaster = (function() { let hide = !miObj.obj ? '' : resolveData( mi, fields.MagicItemDB, reItemData, charCS, {hide:reSpellSpecs.hide}, i ).parsed.hide, reveal = (mi !== trueMI) && !!miObj.obj && hide && hide.length && hide !== 'hide'; miObj = getAbility( fields.MagicItemDB, mi, charCS, false, isGM, (reveal ? mi : trueMI), i ); - if (!state.MagicMaster.viewActions && !renamed && miObj.obj) miObj.obj[0].set('action',miObj.obj[0].get('action').replace(reActionButton,design.grey_action) ); - extension = ' '+sendToWho(charCS,senderId,false,true)+(miObj.api ? ' ' : '')+'%{'+miObj.dB+'|'+mi.hyphened()+'}'; + if (!state.MagicMaster.viewActions && miObj.obj) miObj.obj = greyOutButtons( tokenID, charCS, miObj.obj, (renamed ? changedMI : '') ); + extension = ' '+sendToWho(charCS,senderId,false,true)+(miObj.api ? ' ' : '')+'%{'+miObj.dB+'|'+changedMI.hyphened()+'}'; } content += (i == MIrowref || makeGrey) ? '' : '](!magic --button '+ cmd +'|'+ tokenID +'|'+ i + extension +')'; }; @@ -2860,6 +2878,29 @@ var MagicMaster = (function() { return; } + /* + * Check if an item stores spells and, if it does, return the + * spell row and column arrays, and the number of live spells + */ + + var getStoredSpells = function( charCS, miName ) { + let spellTables = {}; + let spellCount = 0; + let rows = []; + let cols = []; + rows.push((attrLookup( charCS, [fields.MIspellRows[0]+miName+'-mu',fields.MIspellRows[1]] ) || ''),(attrLookup( charCS, [fields.MIspellRows[0]+miName+'-pr',fields.MIspellRows[1]] ) || '')); + rows = rows.join().split(',').filter(r=>!!r); + cols.push((attrLookup( charCS, [fields.MIspellCols[0]+miName+'-mu',fields.MIspellCols[1]] ) || ''),(attrLookup( charCS, [fields.MIspellCols[0]+miName+'-pr',fields.MIspellCols[1]] ) || '')); + cols = cols.join().split(',').filter(c=>!!c); + if (rows.length && cols.length) { + cols.forEach( (c,i) => { + if (_.isUndefined(spellTables[c])) spellTables[c] = getTableField( charCS, {}, fields.Spells_table, fields.Spells_castValue, c ); + spellCount += parseInt((spellTables[c].tableLookup( fields.Spells_castValue, rows[i] )),10); + }); + }; + return {count:spellCount,rows:rows,cols:cols}; + }; + /* * Make a list of spells in the specified memorised/stored list */ @@ -2870,6 +2911,7 @@ var MagicMaster = (function() { isMI = command.toUpperCase().includes('MI'), isPower = command.toUpperCase().includes('POWER'), isView = command.toUpperCase().includes('VIEW'), + isScroll = command.toUpperCase().includes('SCROLL'), isGM = playerIsGM(senderId), content = '', viewCmd = '', @@ -2886,6 +2928,7 @@ var MagicMaster = (function() { toWho = sendToWho(charCS,senderId,false,true), spellTables = [], spellLevels = 0, + learnData = '', learn = false, rows = [], cols = []; @@ -2902,16 +2945,16 @@ var MagicMaster = (function() { buttonList = 'EmptyList,' + attrLookup( charCS, [fields.ItemMUspellsList[0]+miName, fields.ItemMUspellsList[1]] ) || ''; buttonList += ',' + attrLookup( charCS, [fields.ItemPRspellsList[0]+miName, fields.ItemPRspellsList[1]]) || ''; buttonList = buttonList.dbName().split(','); - let miObj = abilityLookup( fields.MagicItemDB, miName, charCS ); - if (miObj.obj) { - learn = resolveData( miName, fields.MagicItemDB, reItemData, charCS, {learn:reSpellSpecs.learn}, miRow ).parsed.learn == 1; + if (caster(charCS,'MU').clv > 0) { + if (abilityLookup( fields.MagicItemDB, miName, charCS ).obj) learnData = resolveData( miName, fields.MagicItemDB, reItemData, charCS, {learn:reSpellSpecs.learn}, miRow ).parsed.learn; + learn = (learnData && learnData != '0' && (!isScroll || !isView)); }; + // see if can build an item-specific spell list... - rows.push((attrLookup( charCS, [fields.MIspellRows[0]+miName+'-mu',fields.MIspellRows[1]] ) || ''),(attrLookup( charCS, [fields.MIspellRows[0]+miName+'-pr',fields.MIspellRows[1]] ) || '')); - rows = rows.join().split(',').filter(r=>!!r); - cols.push((attrLookup( charCS, [fields.MIspellCols[0]+miName+'-mu',fields.MIspellCols[1]] ) || ''),(attrLookup( charCS, [fields.MIspellCols[0]+miName+'-pr',fields.MIspellCols[1]] ) || '')); - cols = cols.join().split(',').filter(c=>!!c); + let storedSpells = getStoredSpells( charCS, miName ); + rows = storedSpells.rows; + cols = storedSpells.cols; if (rows.length && cols.length) { _.each( cols, (c,k) => { let r = rows[k]; @@ -2923,9 +2966,22 @@ var MagicMaster = (function() { disabled = (miStore ? (spellValue != 0) : (spellValue == 0)); if (!disabled) spellLevels = spellLevels + (parseInt(spellTables[c].tableLookup( fields.Spells_spellLevel, r )) || 1); if (!noDash || spellName != '-') { + let renamed = !abilityLookup( spellDB, spellName ), + changedSpell = renamed ? 'Display-'+spellName : spellName; + spell = getAbility( spellDB, spellName, charCS ); + if (!!spell.obj) { + if (!state.MagicMaster.viewActions && isView) { + spell.obj = greyOutButtons( tokenID, charCS, spell.obj, (renamed ? changedSpell : '') ); + } else if (renamed) { + spell.obj = setAbility( charCS, changedSpell, spell.obj[0].body ); + }; + let learnText = !learn ? '' : '{{Learn=Try to [Learn this spell](!magic --learn-spell '+tokenID+'|'+(learnData != 1 ? learnData : spellName)+')}}'; + if (!!learn) spell.obj[0].set('action',spell.obj[0].get('action').replace(/\}\}\s*$/m,'}}'+learnText) ); + }; content += (buttonID == selectedButton ? '' : ((submitted || disabled) ? '' : '[')); content += ((spellType.includes('POWER') && spellValue) ? (spellValue + ' ') : '') + (spellName || '-'); - content += (((buttonID == selectedButton) || submitted || disabled) ? '' : '](!magic --button '+ command +'|'+ tokenID +'|'+ buttonID +'|'+ r +'|'+ c + extension + ' --display-ability '+tokenID+'|'+spellDB+'|'+spellName+')'); +// content += (((buttonID == selectedButton) || submitted || disabled) ? '' : '](!magic --button '+ command +'|'+ tokenID +'|'+ buttonID +'|'+ r +'|'+ c + (!isView ? '' : (' --display-ability '+tokenID+'|'+spellDB+'|'+spellName + extension)) + ')'); + content += ((buttonID == selectedButton) || submitted || disabled) ? '' : ('](!magic --button '+ command +'|'+ tokenID +'|'+ buttonID +'|'+ r +'|'+ c + extension + (!isView ? '' : ' '+(spell.api ? '' : sendToWho(charCS,senderId,false,true))+'%{' + spell.dB + '|' + changedSpell.hyphened() + '}')+')'); } buttonID++; }); @@ -2976,9 +3032,12 @@ var MagicMaster = (function() { magicDB = findPower(charCS,spellName).dB; spellTables[w] = spellTables[w].tableSet( fields.Spells_db,r,magicDB ); } + let renamed = !abilityLookup( magicDB, spellName ), + changedSpell = renamed ? 'Display-'+spellName : spellName; spell = getAbility( magicDB, spellName, charCS ); - if (!state.MagicMaster.viewActions) spell.obj[0].set('action',spell.obj[0].get('action').replace(reActionButton,design.grey_action) ); - extension = `${!learn ? '' : ` --message ${tokenID}|Learn Spell|Try to [Learn this spell](!magic ~~learn-spell ${tokenID}¦${spellName})`} ${(spell.api ? '' : toWho)}%{${spell.dB}|${spellName}}`; + if (!!spell.obj && !state.MagicMaster.viewActions) spell.obj = greyOutButtons( tokenID, charCS, spell.obj, (renamed ? changedSpell : '') ); +// extension = `${!learn ? '' : ` --message ${tokenID}|Learn Spell|Try to [Learn this spell](!magic ~~learn-spell ${tokenID}¦${spellName})`} ${(spell.api ? '' : toWho)}%{${spell.dB}|${spellName}}`; + extension = `$ ${(spell.api ? '' : toWho)}%{${spell.dB}|${changedSpell}}`; } content += (buttonID == selectedButton ? '' : ((submitted || disabled || (lv > maxLevel)) ? '' : '[')); content += ((spellType.includes('POWER') && spellValue) ? (spellValue + ' ') : '') + spellName.dispName(); @@ -3086,10 +3145,12 @@ var MagicMaster = (function() { content += '{{desc=1. [Choose](!magic --button '+editCmd+'|'+tokenID+'|'+level+'|'+spellRow+'|'+spellCol+'|?{'+magicWord+' to memorise|'+spellbook+'}) '+magicWord+' to memorise'
+ selectableSlot+'Rename '+slotName+(chosenSlot ? ('](!magic --button GM-RenameMI|'+tokenID+'|'+MIrowref+'|'+MItoStore+'|?{What name should '+slotName+' now have?}) ') : ' ')+' ' @@ -4037,7 +4132,8 @@ var MagicMaster = (function() { pickingUp = (tokenID == putID), shortMenu = pickingUp, pickOrPut = (pickingUp ? 'Pick up' : 'Put away'), - charCS = getCharacter(tokenID); + charCS = getCharacter(tokenID), + isGM = playerIsGM(senderId); if (!putCS || !pickCS) { sendDebug( 'makeShortPOPmenu: pickID or putID is invalid' ); @@ -4076,9 +4172,9 @@ var MagicMaster = (function() { pickedType = (attrLookup( pickCS, fields.Items_type, fields.Items_table, pickRow ) || '').dbName() || '-'; putItems = getTableField( putCS, putItems, fields.Items_table, fields.Items_trueName ); putItems = getTableField( putCS, putItems, fields.Items_table, fields.Items_type ); - let lowerMI = pickedMI.dbName() || '-'; + let lowerMI = pickedMI.dbName().replace(/v\d+$/,'') || '-'; for (i = 0; i < putItems.sortKeys.length; i++) { - mi = (putItems.tableLookup(fields.Items_name,i) || '').dbName() || '-'; + mi = (putItems.tableLookup(fields.Items_name,i) || '').dbName().replace(/v\d+$/,'') || '-'; if (_.isUndefined(mi)) break; if (mi != lowerMI) continue; miTrueName = (putItems.tableLookup(fields.Items_trueName,i) || '').dbName() ||'-'; @@ -4139,7 +4235,7 @@ var MagicMaster = (function() { + ' in free slot}}{{desc2='; content += '[Use '+menuType+' menu](!magic --button '+(pickingUp ? BT.PICKMI_OPTION : BT.PUTMI_OPTION)+'|'+tokenID+'|'+menuType+'|'+pickID+'|'+putID+')}}'; - sendResponse( charCS, content, senderId, flags.feedbackName, flags.feedbackImg, tokenID ); + sendResponse( charCS, content, senderId, flags.feedbackName, flags.feedbackImg ); } else { content = messages.header + '{{desc=' + pickCS.get('name') + ' ' + messages.fruitlessSearch + treasure; sendParsedMsg( tokenID, content, senderId ); @@ -4237,9 +4333,9 @@ var MagicMaster = (function() { + '. Which class do you want/have to '+(drainLevels > 0 ? 'gain' : 'lose')+' the ' + (multiLevels > 1 ? 'next one level? You will then be asked which levels to drain the rest of the levels from, one at a time.' : 'level from?') + '}}{{desc1='; - + _.each( classes, c => { - content += 'Level '+c.level+' ['+(c.classData.name || c.name)+'](!magic --button '+BT.LEVEL_CHANGE+'|'+tokenID+'|'+drainLevels+'|'+c.base+'|'+args[3]+'|?{How many HP to '+(drainLevels > 0 ? 'add' : 'deduct')+'|'+totalHP+'})\n'; + content += 'Level '+c.level+' ['+(c.classData.name || c.name)+'](!magic --button '+BT.LEVEL_CHANGE+'|'+tokenID+'|'+drainLevels+'|'+c.base+'|'+args[3]+'|?{How many HP to '+(drainLevels > 0 ? 'add' : 'deduct')+'|'+c.classData.hd+'}|'+totalHP+')\n'; }); content += '}}'; sendResponse( getCharacter(tokenID), content ); @@ -4512,7 +4608,7 @@ var MagicMaster = (function() { sendError('Internal MagicMaster error'); } - if (args[0] == BT.MI_SPELL || args[0].toUpperCase().includes('POWER')) { + if (args[0] == BT.MI_SPELL || args[0] == BT.MI_SCROLL || args[0].toUpperCase().includes('POWER')) { var charCS = getCharacter(args[1]), storedLevel = attrLookup( charCS, fields.Spells_storedLevel, fields.Spells_table, args[3], args[4] ); if (storedLevel && storedLevel > 0) { @@ -4605,7 +4701,7 @@ var MagicMaster = (function() { } var spell = getAbility( db, spellName, charCS ), - spellCost = ((!!spell.ct && ((args[0] == BT.CAST_MUSPELL) || (args[0] == BT.CAST_PRSPELL))) ? spell.obj[1].cost : 0), + spellCost = ((!!spell.obj && !!spell.ct && ((args[0] === BT.CAST_MUSPELL) || (args[0] === BT.CAST_PRSPELL))) ? spell.obj[1].cost : 0), totalLeft, content, spellValue = parseInt((spellTables.tableLookup( fields.Spells_castValue, rowIndex )),10); @@ -4616,7 +4712,7 @@ var MagicMaster = (function() { setValue( charCS, fields.SpellColIndex, colIndex ); if (absorb) { - let level = (parseInt(spell.obj[1].type.match(/\d+/)) || 0), + let level = (!spell.obj || !spell.obj[1]) ? 1 : (parseInt(spell.obj[1].type.match(/\d+/)) || 0), itemRow = parseInt(attrLookup( charCS, fields.ItemRowRef )); if (isNaN(itemRow)) { let Items = getTable( charCS, fieldGroups.MI ); @@ -4628,7 +4724,7 @@ var MagicMaster = (function() { } } else if (spellValue != 0) { - if (apiCommands.attk && apiCommands.attk.exists && spell.obj[1].body.match(/}}\s*tohitdata\s*=\s*\[.*?\]/im)) { + if (apiCommands.attk && apiCommands.attk.exists && !!spell.obj && !!spell.obj[1] && spell.obj[1].body.match(/}}\s*tohitdata\s*=\s*\[.*?\]/im)) { sendAPI(fields.attackMaster+' '+senderId+' --weapon '+tokenID+'|Take '+spellName+' in-hand as a weapon and then Attack with it||'+miName); } else { if (spellValue > 0) spellValue--; @@ -4748,6 +4844,7 @@ var MagicMaster = (function() { isMI = cmd.includes('MI'), isPower = cmd.includes('POWER'), isSpell = cmd.includes('SPELL'), + isScroll = cmd.includes('SCROLL'), isView = !cmd.includes('REVIEW'), isGM = args[0].includes('GM'), tokenID = args[1], @@ -4771,7 +4868,9 @@ var MagicMaster = (function() { } else if (isPR) { followOn = (isView ? BT.VIEWMEM_MI_PRSPELLS : BT.EDIT_MIPRSPELLS); } else if (isSpell) { - followOn = (isView ? BT.VIEWMEM_MI_SPELLS : BT.EDIT_MIMUSPELLS); + followOn = (isView ? BT.VIEWMEM_MI_SPELLS : BT.EDIT_MISPELLS); + } else if (isScroll) { + followOn = (isView ? BT.VIEWMEM_MI_SCROLL : BT.EDIT_MISPELLS); } else { followOn = (isView ? BT.VIEW_MI : (args[0].includes('MARTIAL') ? BT.CHOOSE_MARTIAL_MI : (args[0].includes('ALLITEMS') ? BT.CHOOSE_ALLITEMS_MI : BT.CHOOSE_MI))); } @@ -5036,33 +5135,33 @@ var MagicMaster = (function() { /* * Handle a level change request */ - + var handleLevelDrain = function( args, senderId, msg = '' ) { - var tokenID = args[0], - drainLevels = parseInt(args[1]) || -1, - fixedClass = args[6] || '', - classChosen = args[2] || fixedClass, - totalLevels = parseInt(args[3]) || drainLevels, - hitPoints = Math.abs(parseInt(args[4]) || 0), - totalHP = parseInt(args[5]) || 0, + var tokenID = args[0], // tokenID + drainLevels = parseInt(args[1]) || -1, // 4 + fixedClass = args[6] || '', // fighter + classChosen = args[2] || fixedClass, // fighter + totalLevels = parseInt(args[3]) || drainLevels, // 4 + hitPoints = Math.abs(parseInt(evalAttr(args[4])) || 0), // [[5d10]] + totalHP = parseInt(args[5]) || 0, // always called = 0 loopCount = Math.abs(drainLevels), charCS = getCharacter(tokenID), increment = drainLevels > 0 ? 1 : -1, + levelHP = hitPoints * increment, classes = classObjects( charCS, senderId ), levelField, hd; - if (classes && classes.length === 1) { - classChosen = classes[0].base; - if (!hitPoints) { - hitPoints = evalAttr(classes[0].classData.hd.replace(/(\d+)(d.+)/i,'(('+String(drainLevels)+'*$1)$2)')); - loopCount = 1; - } - } - if (!classChosen) { + if ((classes && classes.length === 1) || fixedClass) { + classChosen = classChosen || classes[0].base; + hitPoints = parseInt(hitPoints || evalAttr(classes[0].classData.hd.replace(/(\d+)(d.+)/i,'(('+String(drainLevels)+'*$1)$2)'))) || 0; + levelHP = hitPoints * increment; + loopCount = 1; + increment = increment * Math.abs(drainLevels); + } else if (!classChosen) { makeLevelDrainMenu( args, classes, senderId, msg, totalHP ); return; - } + }; switch (classChosen.toLowerCase()) { case 'wizard': levelField = fields.Wizard_level; @@ -5083,20 +5182,25 @@ var MagicMaster = (function() { } } setAttr( charCS, levelField, Math.max(0,((parseInt(attrLookup( charCS, levelField ) || 1) || 1) + increment)) ); - setAttr( charCS, fields.HP,((parseInt(attrLookup( charCS, fields.HP ) || 0) || 0) + (hitPoints * increment)) ); - setAttr( charCS, fields.MaxHP, Math.max(0,((parseInt(attrLookup( charCS, fields.MaxHP ) || 0) || 0) + (hitPoints * increment))) ); + setAttr( charCS, fields.HP,((parseInt(attrLookup( charCS, fields.HP ) || 0) || 0) + levelHP) ); + setAttr( charCS, fields.MaxHP, Math.max(0,((parseInt(attrLookup( charCS, fields.MaxHP ) || 0) || 0) + levelHP)) ); totalHP += hitPoints; if (--loopCount > 0) { + let content = '&{template:'+fields.warningTemplate+'}{{title=Change in Level}}{{desc=Successfully '+(increment > 0 ? 'boosted' : 'drained')+' '+classChosen + + ' class by one level, which in total makes '+(Math.abs(totalLevels) - loopCount)+' across all classes.' + + ' A total of '+totalHP+'HP have been '+(increment > 0 ? 'gained' : 'lost')+'}}'; + sendResponse( charCS, content ); handleLevelDrain( [tokenID,(drainLevels-increment),'',totalLevels,0,totalHP,fixedClass], senderId, 'Successfully '+(increment > 0 ? 'boosted' : 'drained')+' '+classChosen+' class by 1 level' ); } else { - handleMemAllPowers( [BT.MEMALL_POWERS,tokenID,1,-1,-1,'',''], senderId, true ); - handleCheckWeapons( tokenID, charCS ); - handleCheckSaves( null, null, [getObj('graphic',tokenID)], true ); + setAttr( charCS, fields.Thac0_base, handleGetBaseThac0( charCS ) ); let content = '&{template:'+fields.warningTemplate+'}{{title=Change in Level}}{{desc=Successfully '+(increment > 0 ? 'boosted' : 'drained')+' '+classChosen - + ' class by '+(fixedClass ? (totalLevels+' levels') : ('one level, which in total makes '+totalLevels+' across all classes')) + + ' class by '+((fixedClass || Math.abs(increment) > 1) ? (totalLevels+' levels') : ('one level, which in total makes '+totalLevels+' across all classes')) + ', and recalculated all saves, reassessed all weapon use and reset usable powers.' + ' A total of '+totalHP+'HP have been '+(increment > 0 ? 'gained' : 'lost')+'}}'; sendResponse( charCS, content ); + setTimeout( () => handleMemAllPowers( [BT.MEMALL_POWERS,tokenID,1,-1,-1,'',''], senderId, true ), 100); + setTimeout( () => handleCheckWeapons( tokenID, charCS ), 200); + setTimeout( () => handleCheckSaves( [tokenID], null, null, true ), 300); } } @@ -5354,6 +5458,7 @@ var MagicMaster = (function() { case 'rechargeable': case 'perm-rechargeable': case 'cursed+rechargeable': + if (getStoredSpells( charCS, MIname ).count > 0) break; if (MIqty == charges && !MItype.includes('cursed') && !MItype.includes('perm')) { if (((MItype.toLowerCase() === 'changing') || (MItype.toLowerCase() === 'change-last')) && MIchangeTo) { handleStoreMI( ['',tokenID, MIrowref, MIchangeTo, 0, 'silent' ], false, senderId ); @@ -5971,7 +6076,7 @@ var MagicMaster = (function() { * qty -1 means not yet chosen, cost -1 means not yet agreed or no cost **/ - async function handlePickOrPut( args, senderId ) { // set + async function handlePickOrPut( args, senderId ) { var tokenID = args[1], fromRowRef = args[2], @@ -6058,7 +6163,7 @@ var MagicMaster = (function() { MItrueType = fromMIbag.tableLookup( fields.Items_trueType, fromRowRef ), MItext = MIname, slotInc = 1, - isStackable = (stackable.includes(fromSlotType) && stdEqual( toSlotName, MIname ) && stdEqual( toSlotType, MItype ) && stdEqual( toSlotTrueName, MItrueName )), + isStackable = (stackable.includes(fromSlotType) && stdEqual( toSlotName.replace(/\-v\d+$/, ''), MIname ) && stdEqual( toSlotType, MItype ) && stdEqual( toSlotTrueName, MItrueName )), finalQty, finalCharges, pickQty, charges, content, MIobj; if (showType) { @@ -6146,10 +6251,13 @@ var MagicMaster = (function() { let dupRow = toMIbag.tableFind( fields.Items_name, newName || MIname ) || toMIbag.tableFind( fields.Items_trueName, newName || MItrueName ); if (!isStackable && !_.isUndefined(dupRow) && dupRow !== toRowRef) { + let j = 2; + while (!_.isUndefined(toMIbag.tableFind( fields.Items_name, (MIname+' v'+j), false ))) {j++}; content = '&{template:'+fields.menuTemplate+'}{{name=Duplicate Item}}' + '{{desc=The item you have selected to '+pickPutText+' is not stackable with an item of the same name already stored}}' - + '{{desc1=[Choose a new name](!magic --button POPrename|'+tokenID+'|'+fromRowRef+'|'+fromID+'|'+toID+'|'+toRowRef+'|'+qty+'|'+expenditure+'|?{What new name do you want to give '+MIname+'?}) for '+MIname - + ' or [Choose something else](!magic --pickorput '+tokenID+'|'+fromID+'|'+toID+')}}'; + + '{{desc1=[Save '+MIname+' as '+(MIname+' v'+j)+'](!magic --button POPrename|'+tokenID+'|'+fromRowRef+'|'+fromID+'|'+toID+'|'+toRowRef+'|'+qty+'|'+expenditure+'|'+(MIname+' v'+j)+')' + + '\n or [Choose a new name](!magic --button POPrename|'+tokenID+'|'+fromRowRef+'|'+fromID+'|'+toID+'|'+toRowRef+'|'+qty+'|'+expenditure+'|?{What new name do you want to give '+MIname+'?}) for '+MIname + + '\n or [Choose something else](!magic --pickorput '+tokenID+'|'+fromID+'|'+toID+')}}'; sendResponse( charCS, content, senderId, flags.feedbackName, flags.feedbackImg, tokenID ); return; } else if (!_.isUndefined(newName) && newName.length) { @@ -6157,7 +6265,7 @@ var MagicMaster = (function() { MIobj = abilityLookup( fields.MagicItemDB, MIname, fromCS ); } if (MIname === MItrueName) MItrueName = newName; - if (MIobj.obj && MIobj.obj[0]) { + if (MIobj.obj && MIobj.obj[0] && MIobj.obj[1]) { MIobj.obj[0].set('name',newName); let key = 'ababzzqqrst', oldDispName = MIname.replace(/-/g,' '), @@ -6249,8 +6357,9 @@ var MagicMaster = (function() { } if (MItoStore.toLowerCase() != 'remove') { - let itemObj = abilityLookup( fields.MagicItemDB, MItoStore, charCS ); - setAttr( charCS, fields.ItemCastingTime, itemObj.obj[1].ct ); +// let itemObj = abilityLookup( fields.MagicItemDB, MItoStore, charCS ); + let itemSpeed = resolveData( MItoStore, fields.MagicItemDB, reItemData, charCS, {speed:reSpellSpecs.speed}, MIrowref ).parsed.speed; + setAttr( charCS, fields.ItemCastingTime, itemSpeed ); setAttr( charCS, fields.ItemSelected, 1 ); }; @@ -6382,11 +6491,11 @@ var MagicMaster = (function() { containerNo = parseInt(attrLookup( charCS, fields.ItemContainerType )) || 0, values = MItables.copyValues(); - if (!magicItem.ct) { - sendDebug('handleStoreMI: selected magic item speed/type not defined'); - sendError('Selected Magic Item not fully defined'); - return; - } +// if (!magicItem.ct) { +// sendDebug('handleStoreMI: selected magic item speed/type not defined'); +// sendError('Selected Magic Item not fully defined'); +// return; +// } var midbCS, MIdisplayName; @@ -6401,7 +6510,7 @@ var MagicMaster = (function() { if (miData.hide && !['hide','nohide','reveal'].includes(miData.hide)) { MIdisplayName = miData.hide; getAbility( fields.MagicItemDB, MIdisplayName, charCS, true, true, MIchosen ); - } else if (GMonly && (state.MagicMaster.autoHide || (miData.hide && miData.hide === 'hide')) && reLooksLike.test(magicItem.obj[1].body)) { + } else if (GMonly && (state.MagicMaster.autoHide || (miData.hide && miData.hide === 'hide')) && !!magicItem.obj && reLooksLike.test(magicItem.obj[1].body)) { MIdisplayName = getShownType( magicItem, MIrowref, miData.itemType ); getAbility( fields.MagicItemDB, MIdisplayName, charCS, true, true, MIchosen ); } else { @@ -6560,7 +6669,7 @@ var MagicMaster = (function() { MIrowref = args[2], MIchosen = args[3], charCS = getCharacter(tokenID), - Items, newItem; + Items, newItem, reveal; if (!charCS) { sendDebug('handleHideMI: invalid tokenID passed'); @@ -6577,6 +6686,8 @@ var MagicMaster = (function() { Items = Items.tableSet( fields.Items_name, MIrowref, MIchosen ); Items = Items.tableSet( fields.Items_trueType, MIrowref, Items.tableLookup( fields.Items_type, MIrowref ) ); + reveal = resolveData( Items.tableLookup( fields.Item_trueName, MIrowref ), fields.MagicItemDB, reItemData, charCS, {reveal:reSpellSpecs.reveal} ).parsed.reveal; + Items = Items.tableSet( fields.Items_reveal, MIrowref, (reveal.toLowerCase() !== 'manual' ? reveal : '')); newItem = abilityLookup( fields.MagicItemDB, MIchosen, charCS ); if (newItem.obj) Items = Items.tableSet( fields.Items_type, MIrowref, newItem.obj[1].charge ); @@ -7447,6 +7558,8 @@ var MagicMaster = (function() { return; } setAttr( charCS, fields.CastingLevel, casterLevel( charCS, 'MI' )); + setAttr( charCS, fields.MU_CastingLevel, casterLevel( charCS, 'MU' )); + setAttr( charCS, fields.PR_CastingLevel, casterLevel( charCS, 'PR' )); makeViewUseMI( [action, tokenID, -1], senderId ); return; @@ -7999,7 +8112,6 @@ var MagicMaster = (function() { MIBagSecurity = 1; } } - if (!search && MIBagSecurity < 0) { sendParsedMsg( putID, messages.noStoring, senderId ); return; @@ -8224,12 +8336,12 @@ var MagicMaster = (function() { msg = ''; switch (flag.toLowerCase()) { - case 'fancy-menus': +/* case 'fancy-menus': state.MagicMaster.fancy = value; if (!_.isUndefined(state.attackMaster.fancy)) state.attackMaster.fancy = value; msg = value ? 'Fancy menus will be used' : 'Plain menus will be used'; break; - +*/ case 'specialist-rules': state.MagicMaster.spellRules.specMU = value; msg = value ? 'Only rules-based specialists get extra spell' : 'Any specialist gets extra spell'; @@ -8252,7 +8364,7 @@ var MagicMaster = (function() { case 'custom-spells': state.MagicMaster.spellRules.denyCustom = value; - msg = value ? 'Custom Spells only from user databases' : 'Distributed custom spells allowed'; + msg = value ? 'Custom items only from user databases' : 'Distributed custom items allowed'; updateDBindex(true); break; @@ -8432,8 +8544,7 @@ var MagicMaster = (function() { sendResponseError(senderId,'Level change requested ('+args[1]+') is invalid'); return; }; - - handleLevelDrain( [args[0],args[1],'','',args[2],'',args[3]], senderId ); + handleLevelDrain( [args[0],args[1],'','',args[2],0,args[3]], senderId ); } /** @@ -8726,12 +8837,9 @@ var MagicMaster = (function() { if (!args) args = []; - if (!args[1] && selected && selected.length) { - args[1] = selected[0]._id; - } else if (!args[1]) { - sendDebug( 'doMessage: tokenID is invalid' ); - sendError( 'No token selected' ); - return; + var sel = ''; + if (selected && selected.length) { + sel = selected[0]._id; } if (args.length <=2) { @@ -8740,22 +8848,29 @@ var MagicMaster = (function() { return; }; - var cmd = args[0], - tokenID = args[1], - charCS = getCharacter(tokenID); + var cmd = (args[0] || '-').toLowerCase(), + tokenID = args[1] || sel, + charCS = getCharacter(tokenID), + isCmd = ['gm','whisper','w','character','c','standard','s','public','p'].includes(cmd); - if (!getObj('graphic',tokenID) && !charCS) { + if (!getObj('graphic',tokenID) && !charCS && !isCmd) { args.unshift('standard'); - cmd = args[0]; - tokenID = args[1] + cmd = (args[0] || '').toLowerCase(); + tokenID = args[1] || sel; charCS = getCharacter(tokenID); } + cmd = cmd.toLowerCase(); + if (!charCS && cmd !== 'gm') { + sendDebug( 'doMessage: tokenID is invalid' ); + sendError( 'No token selected' ); + return; + } var msg = '&{template:'+fields.messageTemplate+'}{{name=' + (args[2] || '') + '}}{{desc=' + parseStr(args[3] || '',msgReplacers) + '}}'; const reAttrs = /\^\^([^\|\^]+)\|?(max|current)?\|?([^\|\^]+)?\^\^/i; const attrRes = ( a, v, m = 'current', d = '0' ) => attrLookup( charCS, [v,m,d] ) || ''; - while (reAttrs.test(msg)) msg = msg.replace(reAttrs,attrRes); + if (!!charCS) while (reAttrs.test(msg)) msg = msg.replace(reAttrs,attrRes); switch (cmd.toLowerCase()) { case 'gm': @@ -9019,6 +9134,7 @@ var MagicMaster = (function() { case BT.PR_SPELL : case BT.MI_SPELL : case BT.MI_POWER : + case BT.MI_SCROLL : case BT.POWER : handleChooseSpell( args, senderId ); @@ -9038,6 +9154,9 @@ var MagicMaster = (function() { case BT.EDIT_PRSPELLS : case BT.EDIT_POWERS : case BT.EDIT_MIPOWERS : + case BT.EDIT_MIMUSPELLS : + case BT.EDIT_MIPRSPELLS : + case BT.EDIT_MISPELLS : handleRedisplayManageSpells( args, senderId ); break; @@ -9047,8 +9166,11 @@ var MagicMaster = (function() { case BT.VIEW_POWER : case BT.VIEW_MI_MUSPELL : case BT.VIEW_MI_PRSPELL : + case BT.VIEW_MI_MUSCROLL : + case BT.VIEW_MI_PRSCROLL : case BT.VIEW_MI_POWER : case BT.VIEW_MI_SPELL : + case BT.VIEW_MI_SCROLL : case BT.REVIEW_MUSPELL : case BT.REVIEW_PRSPELL : case BT.REVIEW_POWER : @@ -9080,6 +9202,7 @@ var MagicMaster = (function() { case BT.VIEWMEM_MI_MUSPELLS : case BT.VIEWMEM_MI_PRSPELLS : case BT.VIEWMEM_MI_SPELLS : + case BT.VIEWMEM_MI_SCROLL : case BT.VIEWMEM_MI_POWERS : makeViewMemSpells( args, senderId ); @@ -9805,6 +9928,7 @@ var MagicMaster = (function() { try { if (!obj) return; let charCS = getCharacter( obj.id ); + if (!charCS) return; if (obj.get("status_dead")) { // If the token dies and is marked as "dead" by the GM // set its container type to 6 (dead). @@ -9814,7 +9938,7 @@ var MagicMaster = (function() { setAttr(charCS, fields.ItemContainerType, (attrLookup(charCS, fields.ItemOldContainerType) || 1)); } } catch (e) { - sendCatchError('RoundMaster',null,e,'RoundMaster handleTokenDeath()'); + sendCatchError('MagicMaster',null,e,'MagicMaster handleTokenDeath()'); } return; }; diff --git a/MagicMaster/script.json b/MagicMaster/script.json index a9898203ba..60e84d778d 100644 --- a/MagicMaster/script.json +++ b/MagicMaster/script.json @@ -2,8 +2,8 @@ "$schema": "https://github.com/DameryDad/roll20-api-scripts/blob/MagicMaster/MagicMaster/Script.json", "name": "MagicMaster", "script": "MagicMaster.js", - "version": "3.5.1", - "previousversions": ["2.044","2.045","2.046","2.048","3.051","1.3.00","1.3.01","1.3.02","1.3.03","1.4.01","1.4.02","1.4.04","1.4.05","1.4.06","1.4.07","1.5.01","1.5.02","1.5.03","2.1.0","2.1.1","2.2.0","2.2.1","2.3.0","2.3.1","2.3.2","2.3.3","2.3.4","3.0.0","3.1.2","3.2.0","3.2.1","3.3.0","3.4.0","3.5.0"], + "version": "4.0.1", + "previousversions": ["2.044","2.045","2.046","2.048","3.051","1.3.00","1.3.01","1.3.02","1.3.03","1.4.01","1.4.02","1.4.04","1.4.05","1.4.06","1.4.07","1.5.01","1.5.02","1.5.03","2.1.0","2.1.1","2.2.0","2.2.1","2.3.0","2.3.1","2.3.2","2.3.3","2.3.4","3.0.0","3.1.2","3.2.0","3.2.1","3.3.0","3.4.0","3.5.0","3.5.1"], "description": "The MagicMaster API provides functions to manage all types of magic, including:\n* Wizard & Priest spell use and effects;\n* Character, NPC & Monster Powers; \n* discovery, looting, use and cursing of Magic Items;\n\n[MagicMaster Documentation](https://wiki.roll20.net/Script:MagicMaster) \n### Installation\nLoading MagicMaster via One-Click also loads the rest of the RPGMaster series of APIs \n[RPGMaster Documentation](https://wiki.roll20.net/RPGMaster) \n### Getting Started\n1. Ensure the CommandMaster API is also installed\n2. Run the CommandMaster `!cmd --initialise` command and add the player macros created to the Macro Bar, then\n3. Select tokens and use the `Token Setup` macro bar button just created to add all relevant Action Buttons to the token(s) (plus set the tokens/Characters up in any other way provided in the menu displayed)\n\n### Use In Play\nOnce the Getting Started steps have been done, the players and DM can then use the buttons displayed at the top of the screen when their character's token is selected to perform all actions needed in normal play.", "authors": "Richard E.", "roll20userid": "6497708", diff --git a/QuestTracker/1.1/QuestTracker.js b/QuestTracker/1.1/QuestTracker.js new file mode 100644 index 0000000000..d36c9c4900 --- /dev/null +++ b/QuestTracker/1.1/QuestTracker.js @@ -0,0 +1,6245 @@ +// Github: https://github.com/boli32/QuestTracker/blob/main/QuestTracker.js +// By: Boli (Steven Wrighton): Professional Software Developer, Enthusiatic D&D Player since 1993. +// Contact: https://app.roll20.net/users/3714078/boli +// Readme https://github.com/boli32/QuestTracker/blob/main/README.md + + +var QuestTracker = QuestTracker || (function () { + 'use strict'; + const getCalendarAndWeatherData = () => { + let CALENDARS = {}; + let WEATHER = {}; + if (state.CalenderData) { + if (state.CalenderData.CALENDARS) CALENDARS = state.CalenderData.CALENDARS; + if (state.CalenderData.WEATHER) WEATHER = state.CalenderData.WEATHER; + } + if (state.QUEST_TRACKER?.calendar) Object.assign(CALENDARS, state.QUEST_TRACKER.calendar); + return { CALENDARS, WEATHER }; + }; + const { CALENDARS, WEATHER } = getCalendarAndWeatherData(); + const statusMapping = { + 1: 'Unknown', + 2: 'Discovered', + 3: 'Started', + 4: 'Ongoing', + 5: 'Completed', + 6: 'Completed By Someone Else', + 7: 'Failed', + 8: 'Time ran out', + 9: 'Ignored' + }; + const frequencyMapping = { + 1: "Daily", + 2: "Weekly", + 3: "Monthly", + 4: "Yearly" + } + let QUEST_TRACKER_verboseErrorLogging = true; + let QUEST_TRACKER_globalQuestData = {}; + let QUEST_TRACKER_globalQuestArray = []; + let QUEST_TRACKER_globalRumours = {}; + let QUEST_TRACKER_Events = {}; + let QUEST_TRACKER_Calendar = {}; + let QUEST_TRACKER_Triggers = {}; + let QUEST_TRIGGER_DeleteList = []; + let QUEST_TRACKER_TriggerConversion = false; + let QUEST_TRACKER_QuestHandoutName = "QuestTracker Quests"; + let QUEST_TRACKER_RumourHandoutName = "QuestTracker Rumours"; + let QUEST_TRACKER_EventHandoutName = "QuestTracker Events"; + let QUEST_TRACKER_WeatherHandoutName = "QuestTracker Weather"; + let QUEST_TRACKER_CalendarHandoutName = "QuestTracker Calendar"; + let QUEST_TRACKER_TriggersHandoutName = "QuestTracker Triggers"; + let QUEST_TRACKER_rumoursByLocation = {}; + let QUEST_TRACKER_readableJSON = true; + let QUEST_TRACKER_pageName = "Quest Tree Page"; + let QUEST_TRACKER_TreeObjRef = {}; + let QUEST_TRACKER_questGrid = []; + let QUEST_TRACKER_jumpGate = true; + let QUEST_TRACKER_BASE_QUEST_ICON_URL = ''; // add your own image here. + let QUEST_TRACKER_ROLLABLETABLE_QUESTS = "qt-quests"; + let QUEST_TRACKER_ROLLABLETABLE_QUESTGROUPS = "qt-quest-groups"; + let QUEST_TRACKER_ROLLABLETABLE_LOCATIONS = "qt-locations"; + let QUEST_TRACKER_calenderType = 'gregorian'; + let QUEST_TRACKER_currentDate = CALENDARS[QUEST_TRACKER_calenderType]?.defaultDate; + let QUEST_TRACKER_defaultDate = CALENDARS[QUEST_TRACKER_calenderType]?.defaultDate; + let QUEST_TRACKER_currentWeekdayName = "Thursday"; + let QUEST_TRACKER_Location = 'northern temperate'; + let QUEST_TRACKER_WeatherLocation = 'plains'; + let QUEST_TRACKER_CURRENT_WEATHER = ""; + let QUEST_TRACKER_FILTER = {}; + let QUEST_TRACKER_RUMOUR_FILTER = {}; + let QUEST_TRACKER_FILTER_Visbility = false; + let QUEST_TRACKER_imperialMeasurements = { + temperature: false, + precipitation: false, + wind: true, + visibility: true + }; + let QUEST_TRACKER_WEATHER_TRENDS = { + dry: 0, + wet: 0, + heat: 0, + cold: 0, + wind: 0, + humid: 0, + visibility: 0, + cloudy: 0 + }; + let QUEST_TRACKER_FORCED_WEATHER_TRENDS = { + dry: false, + wet: false, + heat: false, + cold: false, + wind: false, + humid: false, + visibility: false, + cloudy: false + }; + let QUEST_TRACKER_HISTORICAL_WEATHER = {}; + let QUEST_TRACKER_WEATHER_DESCRIPTION = {}; + let QUEST_TRACKER_WEATHER = true; + const loadQuestTrackerData = () => { + initializeQuestTrackerState(); + QUEST_TRACKER_verboseErrorLogging = state.QUEST_TRACKER.verboseErrorLogging || true; + QUEST_TRACKER_globalQuestData = state.QUEST_TRACKER.globalQuestData; + QUEST_TRACKER_globalQuestArray = state.QUEST_TRACKER.globalQuestArray; + QUEST_TRACKER_globalRumours = state.QUEST_TRACKER.globalRumours; + QUEST_TRACKER_rumoursByLocation = state.QUEST_TRACKER.rumoursByLocation; + QUEST_TRACKER_readableJSON = state.QUEST_TRACKER.readableJSON || true; + QUEST_TRACKER_TreeObjRef = state.QUEST_TRACKER.TreeObjRef || {}; + QUEST_TRACKER_questGrid = state.QUEST_TRACKER.questGrid || []; + QUEST_TRACKER_jumpGate = state.QUEST_TRACKER.jumpGate || true; + QUEST_TRACKER_Events = state.QUEST_TRACKER.events || {}; + QUEST_TRACKER_Calendar = state.QUEST_TRACKER.calendar || {}; + QUEST_TRACKER_Triggers = state.QUEST_TRACKER.triggers || {}; + QUEST_TRACKER_TriggerConversion = state.QUEST_TRACKER.triggerConversion || false; + QUEST_TRACKER_calenderType = state.QUEST_TRACKER.calenderType || 'gregorian'; + QUEST_TRACKER_currentDate = state.QUEST_TRACKER.currentDate || CALENDARS[QUEST_TRACKER_calenderType]?.defaultDate + QUEST_TRACKER_defaultDate = state.QUEST_TRACKER.defaultDate || CALENDARS[QUEST_TRACKER_calenderType]?.defaultDate + QUEST_TRACKER_Location = state.QUEST_TRACKER.location || 'northern temperate'; + QUEST_TRACKER_WeatherLocation = state.QUEST_TRACKER.weatherLocation || 'plains'; + QUEST_TRACKER_currentWeekdayName = state.QUEST_TRACKER.currentWeekdayName || 'Thursday'; + QUEST_TRACKER_FILTER = state.QUEST_TRACKER.filter || {}; + QUEST_TRACKER_RUMOUR_FILTER = state.QUEST_TRACKER.rumourFilter || {}; + QUEST_TRACKER_FILTER_Visbility = state.QUEST_TRACKER.filterVisibility || false; + QUEST_TRACKER_WEATHER_TRENDS = state.QUEST_TRACKER.weatherTrends || { + dry: 0, + wet: 0, + heat: 0, + cold: 0, + wind: 0, + humid: 0, + visibility: 0, + cloudy: 0 + }; + QUEST_TRACKER_FORCED_WEATHER_TRENDS = state.QUEST_TRACKER.forcedWeatherTrends || { + dry: false, + wet: false, + heat: false, + cold: false, + wind: false, + humid: false, + visibility: false, + cloudy: false + }; + QUEST_TRACKER_CURRENT_WEATHER = state.QUEST_TRACKER.currentWeather; + QUEST_TRACKER_HISTORICAL_WEATHER = state.QUEST_TRACKER.historicalWeather || {}; + QUEST_TRACKER_WEATHER_DESCRIPTION = state.QUEST_TRACKER.weatherDescription || {}; + QUEST_TRACKER_WEATHER = state.QUEST_TRACKER.weather || true; + QUEST_TRACKER_imperialMeasurements = state.QUEST_TRACKER.imperialMeasurements || { + temperature: false, + precipitation: false, + wind: true, + visibility: true + } + }; + const checkVersion = () => { + if (!QUEST_TRACKER_TriggerConversion) Triggers.convertAutoAdvanceToTriggers(); + } + const saveQuestTrackerData = () => { + state.QUEST_TRACKER.verboseErrorLogging = QUEST_TRACKER_verboseErrorLogging; + state.QUEST_TRACKER.globalQuestData = QUEST_TRACKER_globalQuestData; + state.QUEST_TRACKER.globalQuestArray = QUEST_TRACKER_globalQuestArray; + state.QUEST_TRACKER.globalRumours = QUEST_TRACKER_globalRumours; + state.QUEST_TRACKER.rumoursByLocation = QUEST_TRACKER_rumoursByLocation; + state.QUEST_TRACKER.readableJSON = QUEST_TRACKER_readableJSON; + state.QUEST_TRACKER.questGrid = QUEST_TRACKER_questGrid; + state.QUEST_TRACKER.jumpGate = QUEST_TRACKER_jumpGate; + state.QUEST_TRACKER.events = QUEST_TRACKER_Events; + state.QUEST_TRACKER.calendar = QUEST_TRACKER_Calendar; + state.QUEST_TRACKER.triggers = QUEST_TRACKER_Triggers; + state.QUEST_TRACKER.triggerConversion = QUEST_TRACKER_TriggerConversion; + state.QUEST_TRACKER.currentDate = QUEST_TRACKER_currentDate; + state.QUEST_TRACKER.defaultDate = QUEST_TRACKER_defaultDate; + state.QUEST_TRACKER.calenderType = QUEST_TRACKER_calenderType; + state.QUEST_TRACKER.location = QUEST_TRACKER_Location; + state.QUEST_TRACKER.weatherLocation = QUEST_TRACKER_WeatherLocation; + state.QUEST_TRACKER.currentWeekdayName = QUEST_TRACKER_currentWeekdayName; + state.QUEST_TRACKER.currentWeather = QUEST_TRACKER_CURRENT_WEATHER; + state.QUEST_TRACKER.weatherTrends = QUEST_TRACKER_WEATHER_TRENDS; + state.QUEST_TRACKER.forcedWeatherTrends = QUEST_TRACKER_FORCED_WEATHER_TRENDS; + state.QUEST_TRACKER.historicalWeather = QUEST_TRACKER_HISTORICAL_WEATHER; + state.QUEST_TRACKER.weatherDescription = QUEST_TRACKER_WEATHER_DESCRIPTION; + state.QUEST_TRACKER.weather = QUEST_TRACKER_WEATHER; + state.QUEST_TRACKER.imperialMeasurements = QUEST_TRACKER_imperialMeasurements; + state.QUEST_TRACKER.TreeObjRef = QUEST_TRACKER_TreeObjRef; + state.QUEST_TRACKER.filter = QUEST_TRACKER_FILTER; + state.QUEST_TRACKER.rumourFilter = QUEST_TRACKER_RUMOUR_FILTER; + state.QUEST_TRACKER.filterVisibility = QUEST_TRACKER_FILTER_Visbility; + }; + const initializeQuestTrackerState = (forced = false) => { + if (!state.QUEST_TRACKER || Object.keys(state.QUEST_TRACKER).length === 0 || forced) { + state.QUEST_TRACKER = { + verboseErrorLogging: true, + globalQuestData: {}, + globalQuestArray: [], + globalRumours: {}, + rumoursByLocation: {}, + generations: {}, + readableJSON: true, + TreeObjRef: {}, + jumpGate: true, + events: {}, + calendar: {}, + triggers: {}, + triggerConversion: false, + calenderType: 'gregorian', + currentDate: CALENDARS[QUEST_TRACKER_calenderType]?.defaultDate, + defaultDate: CALENDARS[QUEST_TRACKER_calenderType]?.defaultDate, + location: 'northern temperate', + weatherLocation: 'plains', + currentWeather: null, + weatherTrends: { + dry: 0, + wet: 0, + heat: 0, + cold: 0 + }, + forcedWeatherTrends: { + dry: false, + wet: false, + heat: false, + cold: false + }, + historicalWeather: {}, + weather: true, + imperialMeasurements: { + temperature: false, + precipitation: false, + wind: true, + visibility: true + }, + filter: {}, + rumourFilter: {}, + filterVisibility: false + }; + if (!findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTS })[0]) { + const tableQuests = createObj('rollabletable', { name: QUEST_TRACKER_ROLLABLETABLE_QUESTS }); + tableQuests.set('showplayers', false); + } + if (!findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTGROUPS })[0]) { + const tableQuestGroups = createObj('rollabletable', { name: QUEST_TRACKER_ROLLABLETABLE_QUESTGROUPS }); + tableQuestGroups.set('showplayers', false); + } + let locationTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_LOCATIONS })[0]; + if (!locationTable) { + locationTable = createObj('rollabletable', { name: QUEST_TRACKER_ROLLABLETABLE_LOCATIONS }); + locationTable.set('showplayers', false); + createObj('tableitem', { + _rollabletableid: locationTable.id, + name: 'Everywhere', + weight: 1 + }); + } + if (!findObjs({ type: 'handout', name: QUEST_TRACKER_QuestHandoutName })[0]) { + createObj('handout', { name: QUEST_TRACKER_QuestHandoutName }); + } + if (!findObjs({ type: 'handout', name: QUEST_TRACKER_RumourHandoutName })[0]) { + createObj('handout', { name: QUEST_TRACKER_RumourHandoutName }); + } + if (!findObjs({ type: 'handout', name: QUEST_TRACKER_EventHandoutName })[0]) { + createObj('handout', { name: QUEST_TRACKER_EventHandoutName }); + } + if (!findObjs({ type: 'handout', name: QUEST_TRACKER_WeatherHandoutName })[0]) { + createObj('handout', { name: QUEST_TRACKER_WeatherHandoutName }); + } + if (!findObjs({ type: 'handout', name: QUEST_TRACKER_CalendarHandoutName })[0]) { + createObj('handout', { name: QUEST_TRACKER_CalendarHandoutName }); + } + if (!findObjs({ type: 'handout', name: QUEST_TRACKER_TriggersHandoutName })[0]) { + createObj('handout', { name: QUEST_TRACKER_TriggersHandoutName }); + } + Utils.sendGMMessage("QuestTracker has been initialized."); + } + }; + const Utils = (() => { + const H = { + checkType: (input) => { + if (typeof input === 'string') { + if (/^\d{4}-\d{2}-\d{2}$/.test(input)) { + return 'DATE'; + } + return 'STRING'; + } else if (typeof input === 'boolean') { + return 'BOOLEAN'; + } else if (typeof input === 'number') { + return Number.isInteger(input) ? 'INT' : 'STRING'; + } else if (Array.isArray(input)) { + return 'ARRAY'; + } else if (typeof input === 'object' && input !== null) { + return 'OBJECT'; + } else { + return 'STRING'; + } + } + }; + const sendGMMessage = (message) => { + sendChat('Quest Tracker', `/w gm ${message}`, null, { noarchive: true }); + }; + const sendMessage = (message) => { + sendChat('Quest Tracker', `${message}`); + }; + const sendDescMessage = (message) => { + sendChat('', `/desc ${message}`); + }; + const normalizeKeys = (obj) => { + if (typeof obj !== 'object' || obj === null) return obj; + if (Array.isArray(obj)) return obj.map(item => normalizeKeys(item)); + return Object.keys(obj).reduce((acc, key) => { + const normalizedKey = key.toLowerCase(); + acc[normalizedKey] = normalizeKeys(obj[key]); + return acc; + }, {}); + }; + const stripJSONContent = (content) => { + content = content + .replace(/ /gi, '') + .replace(/<\/?[^>]+(>|$)/g, '') + .replace(/ /gi, ' ') + .replace(/&[a-z]+;/gi, ' ') + .replace(/\+/g, '') + .replace(/[\r\n]+/g, ' ') + .replace(/\s{2,}/g, ' ') + .trim(); + const start = content.indexOf('{'); + const end = content.lastIndexOf('}'); + if (start === -1 || end === -1) { + log('Error: Valid JSON structure not found after stripping.'); + return '{}'; + } + const jsonContent = content.substring(start, end + 1).trim(); + return jsonContent; + }; + const sanitizeInput = (input, type) => { + if (input === undefined || input === null) { + Utils.sendGMMessage(`Error: Input is undefined or null.`); + return null; + } + switch (type) { + case 'STRING': + if (typeof input !== 'string') { + errorCheck(1, 'msg', null,`Expected a string, but received "${typeof input}`); + return null; + } + return input.replace(/<[^>]*>/g, '').replace(/["<>]/g, '').replace(/(\r\n|\n|\r)/g, '%NEWLINE%'); + case 'ARRAY': + if (!Array.isArray(input)) { + errorCheck(2, 'msg', null,`Expected an array, but received "${typeof input}`); + return [sanitizeInput(input, 'STRING')]; + } + return input.map(item => sanitizeInput(item, H.checkType(item))).filter(item => item !== null); + case 'DATE': + return /^\d{4}-\d{2}-\d{2}$/.test(input) ? input : null; + case 'BOOLEAN': + return typeof input === 'boolean' ? input : input === 'true' || input === 'false' ? input === 'true' : null; + case 'INT': + return Number.isInteger(Number(input)) ? Number(input) : null; + case 'OBJECT': + if (typeof input !== 'object' || Array.isArray(input)) { + errorCheck(3, 'msg', null,`Expected an object, but received "${typeof input}`); + return null; + } + const sanitizedObject = {}; + for (const key in input) { + if (input.hasOwnProperty(key)) { + const sanitizedKey = sanitizeInput(key, 'STRING'); + const fieldType = H.checkType(input[key]); + const sanitizedValue = sanitizeInput(input[key], fieldType); + if (sanitizedKey !== null && sanitizedValue !== null) { + sanitizedObject[sanitizedKey] = sanitizedValue; + } + } + } + return sanitizedObject; + default: + errorCheck(4, 'msg', null,`Unsupported type "${type}`); + return null; + } + }; + const updateHandoutField = (dataType = 'quest') => { + let handoutName; + switch (dataType.toLowerCase()) { + case 'rumour': + handoutName = QUEST_TRACKER_RumourHandoutName; + break; + case 'event': + handoutName = QUEST_TRACKER_EventHandoutName; + break; + case 'weather': + handoutName = QUEST_TRACKER_WeatherHandoutName; + break; + case 'quest': + handoutName = QUEST_TRACKER_QuestHandoutName; + break; + case 'calendar': + handoutName = QUEST_TRACKER_CalendarHandoutName; + break; + case 'triggers': + handoutName = QUEST_TRACKER_TriggersHandoutName; + break; + default: + return; + } + const handout = findObjs({ type: 'handout', name: handoutName })[0]; + if (errorCheck(146, 'exists', handout,'handout')) return; + handout.get('gmnotes', (notes) => { + const cleanedContent = Utils.stripJSONContent(notes); + let data; + try { + data = JSON.parse(cleanedContent); + data = normalizeKeys(data); + } catch (error) { + errorCheck(5, 'msg', null,`Failed to parse JSON data from GM notes: ${error.message}`); + return; + } + let updatedData; + switch (dataType.toLowerCase()) { + case 'rumour': + updatedData = QUEST_TRACKER_globalRumours; + break; + case 'event': + updatedData = QUEST_TRACKER_Events; + break; + case 'weather': + updatedData = QUEST_TRACKER_HISTORICAL_WEATHER; + break; + case 'calendar': + updatedData = QUEST_TRACKER_Calendar; + break; + case 'quest': + updatedData = QUEST_TRACKER_globalQuestData; + break; + case 'triggers': + updatedData = QUEST_TRACKER_Triggers; + break; + default: + return; + } + const updatedContent = QUEST_TRACKER_readableJSON + ? JSON.stringify(updatedData, null, 2) + .replace(/\n/g, ' ') + .replace(/ {2}/g, ' ') + : JSON.stringify(updatedData); + handout.set('gmnotes', updatedContent, (err) => { + if (err) { + errorCheck(6, 'msg', null,`Failed to update GM notes for "${handoutName}": ${err.message}`); + switch (dataType.toLowerCase()) { + case 'rumour': + QUEST_TRACKER_globalRumours = JSON.parse(cleanedContent); + break; + case 'event': + QUEST_TRACKER_Events = JSON.parse(cleanedContent); + break; + case 'weather': + QUEST_TRACKER_HISTORICAL_WEATHER = JSON.parse(cleanedContent); + break; + case 'calendar': + QUEST_TRACKER_Calendar = JSON.parse(cleanedContent); + break; + case 'quest': + QUEST_TRACKER_globalQuestData = JSON.parse(cleanedContent); + break; + case 'triggers': + QUEST_TRACKER_TriggersHandoutName = JSON.parse(cleanedContent); + break; + default: + return; + } + } + }); + }); + saveQuestTrackerData(); + if (dataType === 'rumours') { + Rumours.calculateRumoursByLocation(); + } + }; + const togglereadableJSON = (value) => { + QUEST_TRACKER_readableJSON = (value === 'true'); + saveQuestTrackerData(); + updateHandoutField('quest'); + updateHandoutField('rumour'); + updateHandoutField('event'); + updateHandoutField('weather'); + updateHandoutField('calendar'); + updateHandoutField('triggers'); + }; + const toggleWeather = (value) => { + QUEST_TRACKER_WEATHER = (value === 'true'); + saveQuestTrackerData(); + }; + const toggleJumpGate = (value) => { + QUEST_TRACKER_jumpGate = (value === 'true'); + saveQuestTrackerData(); + }; + const toggleVerboseError = (value) => { + QUEST_TRACKER_verboseErrorLogging = (value === 'true'); + saveQuestTrackerData(); + }; + const toggleImperial = (type, value) => { + QUEST_TRACKER_imperialMeasurements[type] = (value === 'true'); + saveQuestTrackerData(); + }; + const toggleFilterVisibility = (value) => { + QUEST_TRACKER_FILTER_Visbility = (value === 'true'); + saveQuestTrackerData(); + }; + const sanitizeString = (input) => { + if (typeof input !== 'string') { + Utils.sendGMMessage('Error: Expected a string input.'); + return null; + } + const sanitizedString = input.replace(/[^a-zA-Z0-9_ ]/g, '_'); + return sanitizedString; + }; + const roll20MacroSanitize = (text) => { + return text + .replace(/\|/g, '|') + .replace(/,/g, ',') + .replace(/{/g, '{') + .replace(/}/g, '}') + .replace(/&/g, '&') + .replace(/ /g, ' ') + .replace(/=/g, '=') + .replace(/_/g, '_') + .replace(/\(/g, '(') + .replace(/\)/g, ')') + .replace(/\[/g, '[') + .replace(/\]/g, ']') + .replace(//g, '>') + .replace(/`/g, '`') + .replace(/\*/g, '*') + .replace(/!/g, '!') + .replace(/"/g, '"') + .replace(/#/g, '#') + .replace(/-/g, '-') + .replace(/@/g, '@') + .replace(/%/g, '%'); + }; + const inputAlias = (command) => { + const aliases = { + '!qt': '!qt-menu action=main', + '!qt-date advance': '!qt-date action=modify|unit=day|new=1', + '!qt-date retreat': '!qt-date action=modify|unit=day|new=-1' + }; + return aliases[command] || command; + }; + const getNestedProperty = (obj, path) => { + const keys = path.split('.'); + return keys.reduce((current, key) => (current && current[key] !== undefined ? current[key] : null), obj); + } + return { + sendGMMessage, + sendDescMessage, + sendMessage, + normalizeKeys, + stripJSONContent, + sanitizeInput, + roll20MacroSanitize, + updateHandoutField, + togglereadableJSON, + toggleWeather, + toggleJumpGate, + toggleVerboseError, + toggleImperial, + toggleFilterVisibility, + sanitizeString, + inputAlias, + getNestedProperty + }; + })(); + const Import = (() => { + const H = { + importData: (handoutName, dataType) => { + let handout = findObjs({ type: 'handout', name: handoutName })[0]; + if (!handout) { + createObj('handout', { name: handoutName }); + } + handout.get('gmnotes', (notes) => { + const cleanedContent = Utils.stripJSONContent(notes); + try { + let parsedData = JSON.parse(cleanedContent); + const convertKeysToLowerCase = (obj) => { + if (typeof obj !== 'object' || obj === null) { + return obj; + } + if (Array.isArray(obj)) { + return obj.map(item => convertKeysToLowerCase(item)); + } + return Object.keys(obj).reduce((acc, key) => { + const lowercaseKey = key.toLowerCase(); + acc[lowercaseKey] = convertKeysToLowerCase(obj[key]); + return acc; + }, {}); + }; + parsedData = convertKeysToLowerCase(parsedData); + if (dataType === 'Quest') { + parsedData = Utils.normalizeKeys(parsedData); + QUEST_TRACKER_globalQuestArray = []; + Object.keys(parsedData).forEach((questId) => { + const quest = parsedData[questId]; + quest.relationships = quest.relationships || { logic: 'AND', conditions: [] }; + QUEST_TRACKER_globalQuestArray.push({ id: questId, weight: quest.weight || 1 }); + }); + QUEST_TRACKER_globalQuestData = parsedData; + } else if (dataType === 'Rumour') { + parsedData = Utils.normalizeKeys(parsedData); + Object.keys(parsedData).forEach((questId) => { + Object.keys(parsedData[questId]).forEach((status) => { + Object.keys(parsedData[questId][status]).forEach((location) => { + let rumours = parsedData[questId][status][location]; + if (typeof rumours === 'object' && !Array.isArray(rumours)) { + parsedData[questId][status][location] = rumours; + } else { + parsedData[questId][status][location] = {}; + } + }); + }); + }); + QUEST_TRACKER_globalRumours = parsedData; + Rumours.calculateRumoursByLocation(); + } else if (dataType === 'Events') { + parsedData = Utils.normalizeKeys(parsedData); + QUEST_TRACKER_Events = parsedData; + } else if (dataType === 'Weather') { + parsedData = Utils.normalizeKeys(parsedData); + QUEST_TRACKER_HISTORICAL_WEATHER = parsedData; + } else if (dataType === 'Calendar') { + parsedData = Utils.normalizeKeys(parsedData); + QUEST_TRACKER_Calendar = parsedData; + } else if (dataType === 'Triggers') { + parsedData = Utils.normalizeKeys(parsedData); + QUEST_TRACKER_Triggers = parsedData; + } + saveQuestTrackerData(); + Utils.sendGMMessage(`${dataType} handout "${handoutName}" Imported.`); + } catch (error) { + errorCheck(8, 'msg', null,`Error parsing ${dataType} data: ${error.message}`); + } + }); + }, + syncQuestRollableTable: () => { + let questTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTS })[0]; + const questTableItems = findObjs({ type: 'tableitem', rollabletableid: questTable.id }); + const tableItemMap = {}; + questTableItems.forEach(item => { + tableItemMap[item.get('name')] = item; + }); + const questIdsInGlobalData = Object.keys(QUEST_TRACKER_globalQuestData); + questIdsInGlobalData.forEach(questId => { + if (!tableItemMap[questId]) { + createObj('tableitem', { + rollabletableid: questTable.id, + name: questId, + weight: 1 + }); + } + }); + questTableItems.forEach(item => { + const questId = item.get('name'); + if (!QUEST_TRACKER_globalQuestData[questId]) { + item.remove(); + } + }); + }, + validateRelationships: (relationships, questId) => { + const questName = questId.toLowerCase(); + const validateNestedConditions = (conditions) => { + if (!Array.isArray(conditions)) return true; + return conditions.every(condition => { + if (typeof condition === 'string') { + const lowerCondition = condition.toLowerCase(); + if (errorCheck(9, 'exists', QUEST_TRACKER_globalQuestData.hasOwnProperty(lowerCondition),`QUEST_TRACKER_globalQuestData.hasOwnProperty(${lowerCondition})`)) return false; + return true; + } else if (typeof condition === 'object' && condition.logic && Array.isArray(condition.conditions)) { + return validateNestedConditions(condition.conditions); + } + return false; + }); + }; + const conditionsValid = validateNestedConditions(relationships.conditions || []); + const mutuallyExclusive = Array.isArray(relationships.mutually_exclusive) + ? relationships.mutually_exclusive.map(exclusive => exclusive.toLowerCase()) + : []; + mutuallyExclusive.forEach(exclusive => { + if (errorCheck(10, 'exists', QUEST_TRACKER_globalQuestData.hasOwnProperty(exclusive),`QUEST_TRACKER_globalQuestData.hasOwnProperty(${exclusive})`)) return true; + else return false; + }); + }, + cleanUpDataFields: () => { + Object.keys(QUEST_TRACKER_globalQuestData).forEach(questId => { + const quest = QUEST_TRACKER_globalQuestData[questId]; + H.validateRelationships(quest.relationships || {}, questId); + }); + saveQuestTrackerData(); + Utils.updateHandoutField('quest'); + }, + refreshCalendarData: () => { + Object.keys(CALENDARS).forEach(key => delete CALENDARS[key]); + Object.assign(CALENDARS, state.CalenderData.CALENDARS); + Object.assign(CALENDARS, state.QUEST_TRACKER.calendar); + } + }; + const fullImportProcess = () => { + H.importData(QUEST_TRACKER_QuestHandoutName, 'Quest'); + H.importData(QUEST_TRACKER_RumourHandoutName, 'Rumour'); + H.importData(QUEST_TRACKER_EventHandoutName, 'Events'); + H.importData(QUEST_TRACKER_WeatherHandoutName, 'Weather'); + H.importData(QUEST_TRACKER_CalendarHandoutName, 'Calendar'); + H.importData(QUEST_TRACKER_TriggersHandoutName, 'Triggers'); + H.syncQuestRollableTable(); + Quest.cleanUpLooseEnds(); + H.cleanUpDataFields(); + H.refreshCalendarData(); + }; + return { + fullImportProcess + }; + })(); + const Triggers = (() => { + const H = { + generateNewTriggerId: () => { + const triggers = QUEST_TRACKER_Triggers; + const allTriggerIds = Object.values(triggers).flatMap(category => + Object.values(category).flatMap(triggerGroup => Object.keys(triggerGroup)) + ); + const highestTriggerNumber = allTriggerIds.reduce((max, id) => { + const match = id.match(/^trigger_(\d+)$/); + if (match) { + const number = parseInt(match[1], 10); + return number > max ? number : max; + } + return max; + }, 0); + const newTriggerNumber = highestTriggerNumber + 1; + return `trigger_${newTriggerNumber}`; + }, + generateNewEffectId: () => { + const triggers = QUEST_TRACKER_Triggers; + const allIds = Object.values(triggers).flatMap(category => + Object.values(category).flatMap(triggerGroup => + Object.values(triggerGroup) + .filter(trigger => trigger.effects) + .flatMap(trigger => Object.keys(trigger.effects)) + ) + ); + const highestIdNumber = allIds.reduce((max, id) => { + const match = id.match(/^effect_(\d+)$/); + if (match) { + const number = parseInt(match[1], 10); + return number > max ? number : max; + } + return max; + }, 0); + const newIdNumber = highestIdNumber + 1; + return `effect_${newIdNumber}`; + }, + saveData: () => { + saveQuestTrackerData(); + Utils.updateHandoutField('triggers'); + }, + getTargetStructure: (type) => { + const structures = { + quest: QUEST_TRACKER_Triggers.quests, + date: QUEST_TRACKER_Triggers.dates, + reaction: QUEST_TRACKER_Triggers.reactions, + }; + return structures[type] || null; + }, + cleanUpEmptyKeys: () => { + const targets = [ + 'quests.null', + 'dates.null', + 'reactions.null', + ]; + targets.forEach((path) => { + const pathParts = path.split('.'); + let current = QUEST_TRACKER_Triggers; + for (let i = 0; i < pathParts.length - 1; i++) { + current = current[pathParts[i]]; + } + const lastKey = pathParts[pathParts.length - 1]; + if (current[lastKey] && Object.keys(current[lastKey]).length === 0) delete current[lastKey]; + }); + }, + fireTrigger: (triggerId) => { + const triggerPath = Triggers.locateItem(triggerId, "trigger"); + const trigger = Utils.getNestedProperty(QUEST_TRACKER_Triggers, triggerPath.replace("QUEST_TRACKER_Triggers.", "")); + if (!trigger || !trigger.effects || Object.keys(trigger.effects).length === 0) return; + Object.entries(trigger.effects).forEach(([effectId, effect]) => { + const { questid, type, value } = effect; + Quest.manageQuestObject({ + action: "update", + field: type, + current: questid, + old: null, + newItem: value, + }); + }); + Triggers.checkTriggers('reaction',triggerId); + QUEST_TRIGGER_DeleteList.push(triggerId); + } + }; + const initializeTriggersStructure = () => { + if (!QUEST_TRACKER_Triggers.quests) QUEST_TRACKER_Triggers.quests = {}; + if (!QUEST_TRACKER_Triggers.dates) QUEST_TRACKER_Triggers.dates = {}; + if (!QUEST_TRACKER_Triggers.reactions) QUEST_TRACKER_Triggers.reactions = {}; + }; + const convertAutoAdvanceToTriggers = () => { + if (QUEST_TRACKER_TriggerConversion) return; + let triggersConverted = false; + initializeTriggersStructure(); + for (const [questId, questData] of Object.entries(QUEST_TRACKER_globalQuestData)) { + if (questData.autoadvance) { + for (const [status, date] of Object.entries(questData.autoadvance)) { + const newTriggerId = H.generateNewTriggerId(); + const newEffectId = H.generateNewEffectId(); + if (!QUEST_TRACKER_Triggers.dates[date]) QUEST_TRACKER_Triggers.dates[date] = {}; + QUEST_TRACKER_Triggers.dates[date][newTriggerId] = { + name: "Converted Trigger", + enabled: true, + quest_id: questId, + change: { type: 'status', value: status }, + effects: { + [newEffectId]: { + quest_id: questId, + change: { type: 'status', value: status } + } + } + }; + triggersConverted = true; + } + delete questData.autoadvance; + } + } + QUEST_TRACKER_TriggerConversion = true; + if (triggersConverted) { + errorCheck(176, 'msg', null, `Autoadvance converted to Triggers (v1.1 update).`); + } + H.saveData(); + }; + const addTrigger = () => { + const newTriggerId = H.generateNewTriggerId(); + initializeTriggersStructure(); + if (!QUEST_TRACKER_Triggers.quests['null']) QUEST_TRACKER_Triggers.quests['null'] = {}; + QUEST_TRACKER_Triggers.quests['null'][newTriggerId] = { + name: "New Trigger", + enabled: false, + action: { type: null, effect: null }, + effects: {} + }; + H.saveData(); + }; + const initializeTrigger = (type, input = null) => { + initializeTriggersStructure(); + const sourceType = type === 'quest' ? 'date' : type === 'date' ? 'quest' : 'reaction'; + const sourcePath = locateItem(input, 'trigger'); + const pathParts = sourcePath.split('.'); + const parentPath = pathParts.slice(0, -1).join('.'); + const triggerId = pathParts[pathParts.length - 1]; + const sourceParent = Utils.getNestedProperty(QUEST_TRACKER_Triggers, parentPath.replace('QUEST_TRACKER_Triggers.', '')); + const sourceTrigger = sourceParent ? sourceParent[triggerId] : null; + const targetStructure = + type === 'quest' + ? QUEST_TRACKER_Triggers.quests + : type === 'date' + ? QUEST_TRACKER_Triggers.dates + : QUEST_TRACKER_Triggers.reactions; + let targetParentKey = 'null'; + const targetParent = targetStructure[targetParentKey] || (targetStructure[targetParentKey] = {}); + const updatedTrigger = { + ...sourceTrigger, + name: sourceTrigger.name || 'New Trigger', + enabled: sourceTrigger.enabled ?? false, + effects: sourceTrigger.effects || {}, + action: type === 'quest' ? sourceTrigger.action || { type: null, effect: null } : null, + }; + if (type !== 'quest') delete updatedTrigger.action; + if (type !== 'date') delete updatedTrigger.dateKey; + if (type !== 'reaction') delete updatedTrigger.questId; + targetParent[triggerId] = updatedTrigger; + if (sourceParent && sourceParent[triggerId]) { + delete sourceParent[triggerId]; + } + H.cleanUpEmptyKeys(); + H.saveData(); + }; + const toggleTrigger = (field, triggerId, value) => { + initializeTriggersStructure(); + const triggerPath = locateItem(triggerId, 'trigger'); + if (errorCheck(203, 'exists', triggerPath, 'triggerPath')) return; + const trigger = Utils.getNestedProperty(QUEST_TRACKER_Triggers, triggerPath.replace('QUEST_TRACKER_Triggers.', '')); + if (!trigger) { + errorCheck(204, 'msg', null, `Trigger not found at path: ${triggerPath}`); + return; + } + switch (field) { + case 'enabled': + trigger.enabled = value === "false" ? false : true; + break; + case 'name': + if (typeof value !== 'string' || value.trim() === '') { + errorCheck(204, 'msg', null, `Invalid name value: ${value}. Must be a non-empty string.`); + return; + } + trigger.name = value.trim(); + break; + default: + errorCheck(205, 'msg', null, `Invalid field: ${field}. Use 'enabled' or 'name'.`); + return; + } + H.saveData(); + }; + const manageTriggerAction = (triggerId, { part, value }) => { + initializeTriggersStructure(); + const triggerPath = locateItem(triggerId, 'trigger'); + if (errorCheck(192, 'exists', triggerPath, 'triggerPath')) return; + const trigger = eval(triggerPath); + switch (part) { + case 'quest_id': { + trigger.quest_id = value; + break; + } + case 'triggering_field': { + trigger.change.type = value; + break; + } + case 'triggering_value': { + trigger.change.value = value; + break; + } + case 'date': { + if (!triggerPath.startsWith('QUEST_TRACKER_Triggers.dates')) { + errorCheck(193, 'msg', null, `Cannot set a date on a non-date trigger.`); + return; + } + trigger.date = value; + break; + } + case 'action': { + if (!triggerPath.startsWith('QUEST_TRACKER_Triggers.reactions')) { + errorCheck(195, 'msg', null, `Cannot set an action on a non-reaction trigger.`); + return; + } + trigger.action = value; + break; + } + default: { + errorCheck(194, 'msg', null, `Invalid part: ${part}.`); + return; + } + } + H.saveData(); + }; + const manageTriggerEffects = ({ action, value = {}, id = null }) => { + initializeTriggersStructure(); + const effectPath = locateItem(id, 'effect'); + if (!effectPath && action !== 'add') { + errorCheck(195, 'msg', null, `Effect with ID ${id} not found.`); + return; + } + let effects, effect; + if (effectPath) { + const effectKeyPath = effectPath.split('.effects.')[0]; + effects = eval(effectKeyPath); + effect = eval(effectPath); + } + switch (action) { + case 'add': { + if (errorCheck(196, 'exists', effects, 'effects')) return; + const newEffectId = H.generateNewEffectId(); + effects[newEffectId] = { + quest_id: null, + change: { type: null, value: null }, + ...value + }; + break; + } + case 'remove': { + if (errorCheck(197, 'exists', effect, 'effect')) return; + delete effects[id]; + break; + } + case 'edit': { + if (errorCheck(198, 'exists', effect, 'effect')) return; + effects[id] = { ...effect, ...value }; + break; + } + default: + errorCheck(199, 'msg', null, `Invalid action: ${action}. Use 'add', 'remove', or 'edit'.`); + return; + } + H.saveData(); + }; + const deleteTrigger = (triggerId) => { + initializeTriggersStructure(); + const triggerPath = locateItem(triggerId, 'trigger'); + if (errorCheck(177, 'exists', triggerPath, 'triggerPath')) return; + if (Array.isArray(QUEST_TRIGGER_DeleteList)) { + const index = QUEST_TRIGGER_DeleteList.indexOf(triggerId); + if (index !== -1) QUEST_TRIGGER_DeleteList.splice(index, 1); + } else if (typeof QUEST_TRIGGER_DeleteList === 'object') { + delete QUEST_TRIGGER_DeleteList[triggerId]; + } + const parentPath = triggerPath.substring(0, triggerPath.lastIndexOf('.')); + const triggerKey = triggerId; + const pathParts = parentPath.split('.'); + if (pathParts.includes("dates")) { + const dateKey = pathParts[2]; + if (QUEST_TRACKER_Triggers.dates[dateKey]) { + delete QUEST_TRACKER_Triggers.dates[dateKey][triggerKey]; + if (Object.keys(QUEST_TRACKER_Triggers.dates[dateKey]).length === 0) { + delete QUEST_TRACKER_Triggers.dates[dateKey]; + } + } + } else { + const triggers = eval(parentPath); + delete triggers[triggerKey]; + if (Object.keys(triggers).length === 0) { + const grandparentPath = parentPath.substring(0, parentPath.lastIndexOf('.')); + const parentKey = parentPath.split('.').pop(); + const parentObject = eval(grandparentPath); + delete parentObject[parentKey]; + } + } + Object.entries(QUEST_TRACKER_Triggers.reactions).forEach(([reactionParent, reactionTriggers]) => { + Object.entries(reactionTriggers).forEach(([reactionTriggerId, reactionTrigger]) => { + if (reactionTrigger.action === triggerId) { + deleteTrigger(reactionTriggerId); + } + }); + }); + H.saveData(); + }; + const locateItem = (itemId, field) => { + for (const [type, category] of Object.entries(QUEST_TRACKER_Triggers)) { + for (const [parentId, items] of Object.entries(category)) { + if (field === 'trigger' && items[itemId]) { + return `QUEST_TRACKER_Triggers.${type}.${parentId}.${itemId}`; + } + if (field === 'effect') { + for (const [triggerId, trigger] of Object.entries(items)) { + if (trigger.effects && trigger.effects[itemId]) { + return `QUEST_TRACKER_Triggers.${type}.${parentId}.${triggerId}.effects.${itemId}`; + } + } + } + } + } + return null; + }; + const managePrompt = (field, triggerId, value) => { + initializeTriggersStructure(); + const sourcePath = locateItem(triggerId, 'trigger'); + const pathParts = sourcePath.split('.'); + const parentPath = pathParts.slice(0, -1).join('.'); + const sourceParent = Utils.getNestedProperty(QUEST_TRACKER_Triggers, parentPath.replace('QUEST_TRACKER_Triggers.', '')); + const sourceTrigger = sourceParent ? sourceParent[triggerId] : null; + let targetStructure; + switch(field) { + case 'quest': + targetStructure = QUEST_TRACKER_Triggers.quests; + break; + case 'date': + targetStructure = QUEST_TRACKER_Triggers.dates; + break; + case 'reaction': + targetStructure = QUEST_TRACKER_Triggers.reactions; + break; + } + let targetParentKey = value || 'null'; + const targetParent = targetStructure[targetParentKey] || (targetStructure[targetParentKey] = {}); + targetParent[triggerId] = { + ...sourceTrigger, + ...(field === 'quest' ? { action: sourceTrigger.action || { type: null, effect: null } } : {}), + ...(field === 'date' ? { dateKey: value || 'null' } : {}), + ...(field === 'reaction' ? { action: value || 'null' } : {}), + }; + delete sourceParent[triggerId]; + if (Object.keys(sourceParent).length === 0) { + const sourceStructure = pathParts[1] === 'quests' + ? QUEST_TRACKER_Triggers.quests + : pathParts[1] === 'dates' + ? QUEST_TRACKER_Triggers.dates + : QUEST_TRACKER_Triggers.reactions; + delete sourceStructure[pathParts[2]]; + } + H.saveData(); + }; + const manageActionEffect = (field, triggerId, type) => { + Triggers.initializeTriggersStructure(); + const triggerPath = Triggers.locateItem(triggerId, "trigger"); + if (!triggerPath || !triggerPath.startsWith("QUEST_TRACKER_Triggers.quests")) return; + const trigger = Utils.getNestedProperty(QUEST_TRACKER_Triggers, triggerPath.replace("QUEST_TRACKER_Triggers.", "")); + if (!trigger || !trigger.action) return; + switch(field) { + case 'action': + trigger.action.type = type; + trigger.action.effect = null; + break; + case 'effect': + trigger.action.effect = type; + break; + } + H.saveData(); + }; + const manageEffect = (triggerId, effectId, action, key = null, value = null) => { + Triggers.initializeTriggersStructure(); + const triggerPath = Triggers.locateItem(triggerId, "trigger"); + if (!triggerPath || !triggerPath.startsWith("QUEST_TRACKER_Triggers")) { + errorCheck(230, "msg", null, `Trigger ID ${triggerId} not found.`); + return; + } + const trigger = Utils.getNestedProperty(QUEST_TRACKER_Triggers, triggerPath.replace("QUEST_TRACKER_Triggers.", "")); + if (!trigger || !trigger.effects) trigger.effects = {}; + const newEffectId = action === "add" ? H.generateNewEffectId() : null; + switch (action) { + case "add": + trigger.effects[newEffectId] = { + questid: null, + type: null, + value: null + }; + break; + case "delete": + delete trigger.effects[effectId]; + break; + case "modify": + trigger.effects[effectId][key] = value; + break; + } + H.saveData(); + }; + const checkTriggers = (type, id = null) => { + switch (type) { + case "date": { + const currentDate = new Date(QUEST_TRACKER_currentDate); + Object.entries(QUEST_TRACKER_Triggers.dates).forEach(([dateKey, triggers]) => { + const triggerDate = new Date(dateKey); + if (triggerDate <= currentDate) { + Object.entries(triggers).forEach(([triggerId, trigger]) => { + if (trigger.enabled) { + H.fireTrigger(triggerId); + } + }); + } + }); + break; + } + case "quest": { + const questTriggers = QUEST_TRACKER_Triggers.quests[id]; + if (!questTriggers || Object.keys(questTriggers).length === 0) return; + Object.entries(questTriggers).forEach(([triggerId, trigger]) => { + if (!trigger.enabled || !trigger.action) return; + const { type, effect } = trigger.action; + switch (type) { + case "hidden": + const isHidden = QUEST_TRACKER_globalQuestData[id]?.hidden; + if (String(isHidden) === effect) { + H.fireTrigger(triggerId); + } + break; + case "disabled": + const isDisabled = QUEST_TRACKER_globalQuestData[id]?.disabled; + if (String(isDisabled) === effect) { + H.fireTrigger(triggerId); + } + break; + case "status": + const currentStatus = Quest.getQuestStatus(id); + const statusId = Object.keys(statusMapping).find(key => statusMapping[key] === currentStatus); + if (effect === currentStatus) { + H.fireTrigger(triggerId); + } + break; + } + }); + break; + } + case "reaction": { + Object.entries(QUEST_TRACKER_Triggers.reactions).forEach(([reactionTriggerId, reactions]) => { + Object.entries(reactions).forEach(([triggerId, trigger]) => { + if (trigger.enabled && trigger.action === id) { + H.fireTrigger(triggerId); + } + }); + }); + break; + } + } + if (QUEST_TRIGGER_DeleteList.length > 0) QUEST_TRIGGER_DeleteList.forEach(id => deleteTrigger(id)); + }; + const removeQuestsFromTriggers = (questId) => { + Triggers.initializeTriggersStructure(); + if (QUEST_TRACKER_Triggers.quests[questId]) { + Object.keys(QUEST_TRACKER_Triggers.quests[questId]).forEach((triggerId) => { + Triggers.deleteTrigger(triggerId); + }); + delete QUEST_TRACKER_Triggers.quests[questId]; + } + H.saveData(); + }; + return { + initializeTriggersStructure, + convertAutoAdvanceToTriggers, + addTrigger, + initializeTrigger, + toggleTrigger, + manageTriggerAction, + manageTriggerEffects, + deleteTrigger, + locateItem, + managePrompt, + manageActionEffect, + manageEffect, + checkTriggers, + removeQuestsFromTriggers + }; + })(); + const Quest = (() => { + const H = { + traverseConditions: (conditions, callback) => { + conditions.forEach(condition => { + if (typeof condition === 'string') { + callback(condition); + } else if (typeof condition === 'object' && condition.logic && Array.isArray(condition.conditions)) { + H.traverseConditions(condition.conditions, callback); + if (Array.isArray(condition.mutually_exclusive)) { + condition.mutually_exclusive.forEach(exclusiveQuest => { + callback(exclusiveQuest); + }); + } + } + }); + }, + updateQuestStatus: (questId, status) => { + const questTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTS })[0]; + if (!questTable) { + return; + } + const items = findObjs({ type: 'tableitem', rollabletableid: questTable.id }); + const item = items.find(i => i.get('name') === questId); + if (item) { + item.set('weight', status); + QUEST_TRACKER_globalQuestArray = QUEST_TRACKER_globalQuestArray.map(q => { + if (q.id === questId) { + q.weight = status; + } + return q; + }); + saveQuestTrackerData(); + } + }, + removeQuestFromRollableTable: (questId) => { + const questTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTS })[0]; + if (questTable) { + const item = findObjs({ type: 'tableitem', rollabletableid: questTable.id }) + .find(i => i.get('name') === questId); + if (item) { + item.remove(); + } + } + }, + getExclusions: (questId) => { + const questData = QUEST_TRACKER_globalQuestData[questId]; + if (!questData || !questData.relationships) { + return []; + } + let exclusions = new Set(); + if (Array.isArray(questData.relationships.mutually_exclusive)) { + questData.relationships.mutually_exclusive.forEach(exclusions.add, exclusions); + } + H.traverseConditions(questData.relationships.conditions || [], condition => { + if (typeof condition === 'string') { + exclusions.add(condition); + } + }); + if (questData.group) { + Object.keys(QUEST_TRACKER_globalQuestData).forEach(key => { + const otherQuest = QUEST_TRACKER_globalQuestData[key]; + if (otherQuest.group && otherQuest.group !== questData.group) { + exclusions.add(key); + } + }); + } + return Array.from(exclusions); + }, + modifyRelationshipObject: (currentRelationships, action, relationshipType, newItem, groupnum) => { + switch (relationshipType) { + case 'mutuallyExclusive': + switch (action) { + case 'add': + currentRelationships.mutually_exclusive = typeof currentRelationships.mutually_exclusive === 'string' ? [currentRelationships.mutually_exclusive] : (currentRelationships.mutually_exclusive || []); + if (!currentRelationships.mutually_exclusive.includes(newItem)) { + currentRelationships.mutually_exclusive.push(newItem); + } + break; + case 'remove': + currentRelationships.mutually_exclusive = currentRelationships.mutually_exclusive.filter( + exclusive => exclusive && exclusive !== newItem + ); + break; + default: + break; + } + break; + case 'single': + if (!Array.isArray(currentRelationships.conditions)) { + currentRelationships.conditions = []; + } + if (!currentRelationships.logic) { + currentRelationships.logic = 'AND'; + } + switch (action) { + case 'add': + const baseIndex = currentRelationships.conditions.findIndex(cond => typeof cond === 'object'); + if (baseIndex === -1) { + currentRelationships.conditions.push(newItem); + } else { + currentRelationships.conditions.splice(baseIndex, 0, newItem); + } + break; + case 'remove': + currentRelationships.conditions = currentRelationships.conditions.filter(cond => cond !== newItem); + break; + default: + break; + } + break; + case 'group': + if (groupnum === null || groupnum < 1) { + return currentRelationships; + } + if (groupnum >= currentRelationships.conditions.length || typeof currentRelationships.conditions[groupnum] !== 'object') { + currentRelationships.conditions[groupnum] = { logic: 'AND', conditions: [] }; + } + const group = currentRelationships.conditions[groupnum]; + if (typeof group === 'object' && group.logic && Array.isArray(group.conditions)) { + switch (action) { + case 'add': + if (!group.conditions.includes(newItem)) { + group.conditions.push(newItem); + } + break; + case 'remove': + group.conditions = group.conditions.filter(cond => cond !== newItem); + break; + default: + break; + } + } + break; + case 'logic': + currentRelationships.logic = currentRelationships.logic === 'AND' ? 'OR' : 'AND'; + break; + case 'grouplogic': + if (groupnum !== null && groupnum >= 1 && groupnum < currentRelationships.conditions.length) { + const group = currentRelationships.conditions[groupnum]; + if (typeof group === 'object' && group.logic) { + group.logic = group.logic === 'AND' ? 'OR' : 'AND'; + } + } + break; + case 'removegroup': + if (groupnum !== null && groupnum >= 1 && groupnum < currentRelationships.conditions.length) { + currentRelationships.conditions.splice(groupnum, 1); + } + break; + case 'addgroup': + currentRelationships.conditions.push({ + logic: 'AND', + conditions: [newItem] + }); + break; + default: + break; + } + return currentRelationships; + }, + generateNewQuestId: () => { + const existingQuestIds = Object.keys(QUEST_TRACKER_globalQuestData); + const highestQuestNumber = existingQuestIds.reduce((max, id) => { + const match = id.match(/^quest_(\d+)$/); + if (match) { + const number = parseInt(match[1], 10); + return number > max ? number : max; + } + return max; + }, 0); + const newQuestNumber = highestQuestNumber + 1; + return `quest_${newQuestNumber}`; + }, + removeQuestReferences: (questId) => { + Object.keys(QUEST_TRACKER_globalQuestData).forEach(otherQuestId => { + if (otherQuestId !== questId) { + const otherQuestData = QUEST_TRACKER_globalQuestData[otherQuestId]; + if (!otherQuestData || !otherQuestData.relationships) return; + const { conditions, mutually_exclusive } = otherQuestData.relationships; + if (Array.isArray(conditions) && conditions.includes(questId)) { + manageRelationship(otherQuestId, 'remove', 'single', questId); + } + if (Array.isArray(mutually_exclusive) && mutually_exclusive.includes(questId)) { + manageRelationship(otherQuestId, 'remove', 'mutuallyExclusive', questId); + } + if (Array.isArray(conditions)) { + conditions.forEach((condition, index) => { + if (typeof condition === 'object' && Array.isArray(condition.conditions)) { + if (condition.conditions.includes(questId)) { + manageRelationship(otherQuestId, 'remove', 'group', questId, index); + } + } + }); + } + } + }); + }, + getAllQuestGroups: () => { + let groupTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTGROUPS })[0]; + if (!groupTable) return []; + let groupItems = findObjs({ type: 'tableitem', rollabletableid: groupTable.id }); + return groupItems.map(item => item.get('name')); + }, + removeQuestsFromGroup: (groupTable, groupId) => { + const groupObject = findObjs({ type: 'tableitem', rollabletableid: groupTable.id }).find(item => item.get('weight') == groupId); + if (!groupObject) return; + + Object.keys(QUEST_TRACKER_globalQuestData).forEach(questId => { + const quest = QUEST_TRACKER_globalQuestData[questId] || {}; + if (quest.group === groupId) { + delete quest.group; + } + }); + Utils.updateHandoutField('quest'); + }, + getNewGroupId: (groupTable) => { + let groupItems = findObjs({ type: 'tableitem', rollabletableid: groupTable.id }); + if (!groupItems || groupItems.length === 0) return 1; + let maxWeight = groupItems.reduce((max, item) => Math.max(max, item.get('weight')), 0); + return maxWeight + 1; + }, + levenshteinDistance: (a, b) => { + if (!a.length) return b.length; + if (!b.length) return a.length; + const matrix = Array.from({ length: a.length + 1 }, (_, i) => Array(b.length + 1).fill(0)); + for (let i = 0; i <= a.length; i++) matrix[i][0] = i; + for (let j = 0; j <= b.length; j++) matrix[0][j] = j; + for (let i = 1; i <= a.length; i++) { + for (let j = 1; j <= b.length; j++) { + matrix[i][j] = + a[i - 1] === b[j - 1] + ? matrix[i - 1][j - 1] + : Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + 1); + } + } + return matrix[a.length][b.length]; + }, + getBestMatchingHandout: (questName) => { + const handouts = findObjs({ type: 'handout' }); + if (!handouts || handouts.length === 0) return null; + let bestMatch = null; + let bestDistance = Infinity; + handouts.forEach(handout => { + const handoutName = handout.get('name'); + const distance = H.levenshteinDistance(questName.toLowerCase(), handoutName.toLowerCase()); + if (distance < bestDistance) { + bestDistance = distance; + bestMatch = handout; + } + }); + if (bestMatch) return bestMatch.id; + else return null; + } + }; + const manageRelationship = (questId, action, relationshipType, newItem = null, groupnum = null) => { + let questData = QUEST_TRACKER_globalQuestData[questId]; + let currentRelationships = questData.relationships || { logic: 'AND', conditions: [], mutually_exclusive: [] }; + currentRelationships.conditions = currentRelationships.conditions || []; + currentRelationships.mutually_exclusive = currentRelationships.mutually_exclusive || []; + if (action === 'add' && newItem) { + let targetQuest = QUEST_TRACKER_globalQuestData[newItem]; + if (targetQuest && questData.group && !targetQuest.group) { + targetQuest.group = questData.group; + } else if (targetQuest && !questData.group && targetQuest.group) { + questData.group = targetQuest.group; + } + } + let updatedRelationships = H.modifyRelationshipObject(currentRelationships, action, relationshipType, newItem, groupnum); + Utils.updateHandoutField('quest') + }; + const getValidQuestsForDropdown = (questId) => { + const exclusions = H.getExclusions(questId); + const excludedQuests = new Set([questId, ...exclusions]); + const validQuests = Object.keys(QUEST_TRACKER_globalQuestData).filter(qId => { + return !excludedQuests.has(qId); + }); + if (validQuests.length === 0) { + return false; + } + return validQuests; + }; + const addQuest = () => { + const newQuestId = H.generateNewQuestId(); + const defaultQuestData = { + name: 'New Quest', + description: 'Description', + relationships: {}, + hidden: true, + disabled: false + }; + const questTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTS })[0]; + QUEST_TRACKER_globalQuestData[newQuestId] = defaultQuestData; + QUEST_TRACKER_globalQuestArray.push({ id: newQuestId, weight: 1 }); + if (questTable) { + createObj('tableitem', { + rollabletableid: questTable.id, + name: newQuestId, + weight: 1, + }); + } + Utils.updateHandoutField('quest') + }; + const removeQuest = (questId) => { + H.removeQuestReferences(questId); + H.removeQuestFromRollableTable(questId); + Rumours.removeAllRumoursForQuest(questId); + Triggers.removeQuestsFromTriggers(questId); + delete QUEST_TRACKER_globalQuestData[questId]; + QUEST_TRACKER_globalQuestArray = QUEST_TRACKER_globalQuestArray.filter(quest => quest.id !== questId); + Utils.updateHandoutField('quest'); + }; + const cleanUpLooseEnds = () => { + const processedPairs = new Set(); + Object.keys(QUEST_TRACKER_globalQuestData).forEach(questId => { + const quest = QUEST_TRACKER_globalQuestData[questId]; + const mutuallyExclusiveQuests = quest.relationships?.mutually_exclusive || []; + mutuallyExclusiveQuests.forEach(targetId => { + const pairKey = [questId, targetId].sort().join('-'); + if (!processedPairs.has(pairKey)) { + processedPairs.add(pairKey); + const targetQuest = QUEST_TRACKER_globalQuestData[targetId]; + if (targetQuest) { + const targetMutuallyExclusive = new Set(targetQuest.relationships?.mutually_exclusive || []); + if (!targetMutuallyExclusive.has(questId)) { + manageRelationship(targetId, 'add', 'mutuallyExclusive', questId); + Utils.sendGMMessage(`Added missing mutually exclusive relationship from ${targetId} to ${questId}.`); + } + } + } + }); + }); + }; + const getStatusNameByQuestId = (questId, questArray) => { + let quest = questArray.find(q => q.id === questId); + if (quest) { + return statusMapping[quest.weight] || 'Unknown'; + } + return 'Unknown'; + }; + const getQuestStatus = (questId) => { + const questTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTS })[0]; + if (!questTable) { + return 1; + } + const questItem = findObjs({ type: 'tableitem', rollabletableid: questTable.id }).find(item => item.get('name') === questId); + if (!questItem) { + return 1; + } + return questItem.get('weight'); + }; + const manageQuestObject = ({ action, field, current, old = '', newItem }) => { + const quest = QUEST_TRACKER_globalQuestData[current]; + switch (field) { + case 'status': + H.updateQuestStatus(current, newItem); + QuestPageBuilder.updateQuestStatusColor(current, newItem); + Rumours.calculateRumoursByLocation(); + break; + case 'hidden': + if (action === 'update') { + quest.hidden = !quest.hidden; + QuestPageBuilder.updateQuestVisibility(current, quest.hidden); + } + break; + case 'disabled': + if (action === 'update') { + quest.disabled = !quest.disabled; + } + break; + case 'name': + if (action === 'add') { + quest.name = newItem; + QuestPageBuilder.updateQuestText(current, newItem); + } else if (action === 'remove') { + quest.name = ''; + } + break; + case 'description': + if (action === 'add') { + quest.description = newItem; + QuestPageBuilder.updateQuestTooltip(current, newItem); + } else if (action === 'remove') { + quest.description = ''; + } + break; + case 'group': + if (action === 'add') { + quest.group = newItem; + } else if (action === 'remove') { + delete quest.group; + } + break; + default: + errorCheck(11, 'msg', null,`Unsupported action for type ( ${field} )`); + break; + } + Triggers.checkTriggers('quest',current); + Utils.updateHandoutField('quest'); + }; + const manageGroups = (action, newItem = null, groupId = null) => { + let groupTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTGROUPS })[0]; + if (!groupTable) { + errorCheck(12, 'msg', null,`Quest groups table not found.`) + return; + } + switch (action) { + case 'add': + const allGroups = findObjs({ type: 'tableitem', rollabletableid: groupTable.id }).map(item => item.get('name').toLowerCase()); + if (allGroups.includes(Utils.sanitizeString(newItem.toLowerCase()))) return; + const newWeight = H.getNewGroupId(groupTable); + if (newWeight === undefined || newWeight === null) return; + let newGroup = createObj('tableitem', { + rollabletableid: groupTable.id, + name: newItem, + weight: newWeight + }); + break; + case 'remove': + if (groupId === 1) return; + let groupToRemove = findObjs({ type: 'tableitem', rollabletableid: groupTable.id }).find(item => item.get('weight') == groupId); + H.removeQuestsFromGroup(groupTable, groupId); + groupToRemove.remove(); + break; + case 'update': + const groupList = findObjs({ type: 'tableitem', rollabletableid: groupTable.id }).map(item => item.get('name').toLowerCase()); + if (groupList.includes(Utils.sanitizeString(newItem.toLowerCase()))) return; + let groupToUpdate = findObjs({ type: 'tableitem', rollabletableid: groupTable.id }).find(item => item.get('weight') == groupId); + if (groupToUpdate) { + groupToUpdate.set('name', newItem); + } + break; + } + }; + const findDirectlyLinkedQuests = (startingQuestId) => { + const linkedQuests = []; + const visited = new Set(); + function isDependentOnQuest(conditions, targetQuestId) { + if (!conditions) return false; + if (Array.isArray(conditions)) { + return conditions.every(cond => { + if (typeof cond === "string") { + return cond === targetQuestId; + } else if (typeof cond === "object" && cond.logic === "AND") { + return isDependentOnQuest(cond.conditions, targetQuestId); + } else if (typeof cond === "object" && cond.logic === "OR") { + return false; + } + return false; + }); + } else if (typeof conditions === "string") { + return conditions === targetQuestId; + } else if (typeof conditions === "object" && conditions.logic === "AND") { + return isDependentOnQuest(conditions.conditions, targetQuestId); + } + return false; + } + function traverse(questId) { + if (visited.has(questId)) return; + visited.add(questId); + Object.entries(QUEST_TRACKER_globalQuestData).forEach(([currentQuestId, quest]) => { + if (currentQuestId === questId || visited.has(currentQuestId)) return; + + const relationships = quest.relationships; + if (relationships?.logic === "AND") { + const conditions = relationships.conditions; + if (isDependentOnQuest(conditions, questId)) { + linkedQuests.push(currentQuestId); + traverse(currentQuestId); + } + } + }); + } + traverse(startingQuestId); + return linkedQuests; + }; + const linkHandout = (questId, key) => { + const quest = QUEST_TRACKER_globalQuestData[questId]; + if (key === "AUTO") { + const handoutId = H.getBestMatchingHandout(quest.name); + if (handoutId) linkHandout(questId, handoutId); + else { + const newHandout = createObj('handout', { name: quest.name }); + if (newHandout) linkHandout(questId, newHandout.id); + } + } + else quest.handout = key; + Utils.updateHandoutField('quest'); + }; + const removeHandout = (questId) => { + const quest = QUEST_TRACKER_globalQuestData[questId]; + if (quest && quest.handout) { + delete quest.handout; + } + Utils.updateHandoutField('quest'); + }; + return { + getStatusNameByQuestId, + getQuestStatus, + getValidQuestsForDropdown, + manageRelationship, + addQuest, + removeQuest, + cleanUpLooseEnds, + manageQuestObject, + manageGroups, + findDirectlyLinkedQuests, + linkHandout, + removeHandout + }; + })(); + const Calendar = (() => { + const H = { + generateNewEventId: () => { + const existingEventIds = Object.keys(QUEST_TRACKER_Events); + const highestEventNumber = existingEventIds.reduce((max, id) => { + const match = id.match(/^event_(\d+)$/); + return match ? Math.max(max, parseInt(match[1], 10)) : max; + }, 0); + return `event_${highestEventNumber + 1}`; + }, + checkEvent: () => { + if (!QUEST_TRACKER_Events || typeof QUEST_TRACKER_Events !== "object") { + return; + } + const todayEvents = H.findNextEvents(0, true); + todayEvents.forEach(([eventDate, eventName, eventID]) => { + if (eventID) { + const event = QUEST_TRACKER_Events[eventID]; + if (errorCheck(13, 'exists', event, 'event')) return occurrences; + if (event.hidden === false) { + Utils.sendMessage(`${event.name} - ${event.description}`); + } else { + Utils.sendGMMessage(`Event triggered: ${event.name} - ${event.description}`); + } + if (!event.repeatable) { + delete QUEST_TRACKER_Events[eventID]; + Utils.updateHandoutField("event"); + } else { + const frequencyDays = event.frequency || 1; + const [year, month, day] = event.date.split("-").map(Number); + const nextDate = new Date(year, month - 1, day + frequencyDays) + .toISOString() + .split("T")[0]; + event.date = nextDate; + Utils.updateHandoutField("event"); + } + } else { + Utils.sendMessage(`Today is ${eventName}`); + } + }); + }, + evaluateLogic: (logic, year) => { + if (errorCheck(15, 'exists', logic,'logic')) return false; + if (errorCheck(16, 'exists', logic.operation,'logic.operation')) return false; + if (logic.conditions) { + if (logic.operation === "or") { + return logic.conditions.some((condition) => H.evaluateLogic(condition, year)); + } else if (logic.operation === "and") { + return logic.conditions.every((condition) => H.evaluateLogic(condition, year)); + } + errorCheck(17, 'msg', null,`Unsupported logic operation: ${logic.operation}`); + return false; + } + if (logic.operation === "mod") { + const result = (year % logic.operand) === logic.equals; + return logic.negate ? !result : result; + } + errorCheck(18, 'msg', null,`Unsupported condition operation: ${logic.operation}`); + return false; + }, + getDaysInMonth: (monthIndex, year) => { + const month = CALENDARS[QUEST_TRACKER_calenderType].months[monthIndex - 1]; + if (month.leap) { + const isLeapYear = H.evaluateLogic(month.leap.logic, year); + if (isLeapYear) { + return month.leap.days; + } + } + return month.days; + }, + getTotalDaysInYear: (year) => { + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + if (errorCheck(19, 'exists', calendar,'calendar')) return; + if (errorCheck(20, 'exists', calendar.months,'calendar.monthsn')) return; + return calendar.months.reduce((totalDays, monthObj, index) => { + const daysInMonth = H.getDaysInMonth(index + 1, year); + return totalDays + daysInMonth; + }, 0); + }, + calculateDateDifference: (target, baseYear, baseMonth, baseDay) => { + if (!target) return Infinity; + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + if (errorCheck(21, 'exists', calendar,'calendar')) return Infinity; + const { year: targetYear, month: targetMonth, day: targetDay } = target; + let totalDays = 0; + if (targetYear === baseYear) { + if (targetMonth === baseMonth) { + return targetDay - baseDay; + } + totalDays += H.getDaysInMonth(baseMonth, baseYear) - baseDay; + for (let m = baseMonth + 1; m < targetMonth; m++) { + totalDays += H.getDaysInMonth(m, baseYear); + } + totalDays += targetDay; + return totalDays; + } + totalDays += H.getDaysInMonth(baseMonth, baseYear) - baseDay; + for (let m = baseMonth + 1; m <= calendar.months.length; m++) { + totalDays += H.getDaysInMonth(m, baseYear); + } + for (let y = baseYear + 1; y < targetYear; y++) { + totalDays += H.getTotalDaysInYear(y); + } + for (let m = 1; m < targetMonth; m++) { + totalDays += H.getDaysInMonth(m, targetYear); + } + totalDays += targetDay; + return totalDays; + }, + isEventToday: (event, eventID) => { + let { date, repeatable, frequency, name, weekdayname } = event; + let [eventYear, eventMonth, eventDay] = date.split("-").map(Number); + const [currentYear, currentMonth, currentDay] = QUEST_TRACKER_currentDate.split("-").map(Number); + if (!repeatable) { + return date === QUEST_TRACKER_currentDate ? [[QUEST_TRACKER_currentDate, name, eventID]] : []; + } + const freqType = frequencyMapping[frequency]; + switch (freqType) { + case "Daily": + return [[QUEST_TRACKER_currentDate, name, eventID]]; + case "Weekly": + if (weekdayname && weekdayname === QUEST_TRACKER_currentWeekdayName) { + return [[QUEST_TRACKER_currentDate, name, eventID]]; + } + break; + case "Monthly": + const daysInMonth = H.getDaysInMonth(currentMonth, currentYear); + if (eventDay <= daysInMonth && eventMonth === currentMonth && eventDay === currentDay) { + return [[QUEST_TRACKER_currentDate, name, eventID]]; + } + break; + case "Yearly": + if (eventMonth === currentMonth && eventDay === currentDay) { + return [[QUEST_TRACKER_currentDate, name, eventID]]; + } + break; + default: + break; + } + return []; + }, + findNextEvents: (limit = 1, isToday = false) => { + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + const daysOfWeek = calendar.daysOfWeek || []; + const specialDays = calendar.significantDays || {}; + const events = QUEST_TRACKER_Events || {}; + const [currentYear, currentMonth, currentDay] = QUEST_TRACKER_currentDate.split("-").map(Number); + let upcomingEvents = []; + const todayEvents = []; + if (isToday) { + Object.entries(events).forEach(([eventID, event]) => { + const todaysOccurrences = H.isEventToday(event, eventID); + todayEvents.push(...todaysOccurrences); + }); + Object.entries(specialDays).forEach(([key, name]) => { + const [eventMonth, eventDay] = key.split("-").map(Number); + if (eventMonth === currentMonth && eventDay === currentDay) { + todayEvents.push([QUEST_TRACKER_currentDate, name, null]); + } + }); + return todayEvents; + } + const calculateNextOccurrences = (event, eventID, maxOccurrences) => { + let { date, repeatable, frequency, name, weekdayname } = event; + let [startYear, startMonth, startDay] = date.split("-").map(Number); + let [currentYear, currentMonth, currentDay] = QUEST_TRACKER_currentDate.split("-").map(Number); + let [eventYear, eventMonth, eventDay] = [startYear, startMonth, startDay]; + const occurrences = []; + const freqType = repeatable ? frequencyMapping[frequency] : null; + if (repeatable) { + if (`${startYear}-${String(startMonth).padStart(2, "0")}-${String(startDay).padStart(2, "0")}` < QUEST_TRACKER_currentDate) { + [eventYear, eventMonth, eventDay] = [currentYear, currentMonth, currentDay]; + } + switch (freqType) { + case "Daily": + break; + case "Weekly": + if (weekdayname) { + const targetWeekdayIndex = daysOfWeek.indexOf(weekdayname); + const currentWeekdayIndex = daysOfWeek.indexOf(QUEST_TRACKER_currentWeekdayName); + let daysToAdd = (targetWeekdayIndex - currentWeekdayIndex + daysOfWeek.length) % daysOfWeek.length; + if (daysToAdd === 0 && (eventYear === currentYear && eventMonth === currentMonth && eventDay === currentDay)) { + daysToAdd = daysOfWeek.length; + } + eventDay += daysToAdd; + if (eventDay > H.getDaysInMonth(eventMonth, eventYear)) { + eventDay -= H.getDaysInMonth(eventMonth, eventYear); + eventMonth++; + if (eventMonth > calendar.months.length) { + eventMonth = 1; + eventYear++; + } + } + } + break; + case "Monthly": + while ( + eventYear < currentYear || + (eventYear === currentYear && eventMonth < currentMonth) + ) { + eventMonth++; + if (eventMonth > calendar.months.length) { + eventMonth = 1; + eventYear++; + } + } + eventDay = Math.min(eventDay, H.getDaysInMonth(eventMonth, eventYear)); + break; + case "Yearly": + if (eventYear < currentYear) { + eventYear = currentYear; + } + break; + default: + break; + } + } + let occurrencesCount = 0; + while (occurrencesCount < maxOccurrences) { + const eventDate = `${eventYear}-${String(eventMonth).padStart(2, "0")}-${String(eventDay).padStart(2, "0")}`; + if (eventDate >= date) { + occurrences.push([eventDate, name, eventID]); + occurrencesCount++; + } + switch (freqType) { + case "Daily": + eventDay++; + if (eventDay > H.getDaysInMonth(eventMonth, eventYear)) { + eventDay -= H.getDaysInMonth(eventMonth, eventYear); + eventMonth++; + if (eventMonth > calendar.months.length) { + eventMonth = 1; + eventYear++; + } + } + break; + case "Weekly": + eventDay += daysOfWeek.length; + if (eventDay > H.getDaysInMonth(eventMonth, eventYear)) { + eventDay -= H.getDaysInMonth(eventMonth, eventYear); + eventMonth++; + if (eventMonth > calendar.months.length) { + eventMonth = 1; + eventYear++; + } + } + break; + case "Monthly": + eventMonth++; + if (eventMonth > calendar.months.length) { + eventMonth = 1; + eventYear++; + } + eventDay = Math.min(eventDay, H.getDaysInMonth(eventMonth, eventYear)); + break; + case "Yearly": + eventYear++; + break; + default: + break; + } + if (!repeatable) break; + } + return occurrences; + }; + Object.entries(events).forEach(([eventID, event]) => { + const eventOccurrences = calculateNextOccurrences(event, eventID, 5); + upcomingEvents.push(...eventOccurrences); + }); + Object.entries(specialDays).forEach(([key, name]) => { + const [eventMonth, eventDay] = key.split("-").map(Number); + let eventYear = currentYear; + if (eventMonth < currentMonth || (eventMonth === currentMonth && eventDay < currentDay)) { + eventYear++; + } + if (H.getDaysInMonth(eventMonth, eventYear) >= eventDay) { + const eventDate = `${eventYear}-${String(eventMonth).padStart(2, "0")}-${String(eventDay).padStart(2, "0")}`; + if (isToday) { + if (eventDate === QUEST_TRACKER_currentDate) { + todayEvents.push([eventDate, name, null]); + } + } else { + if (eventDate > QUEST_TRACKER_currentDate) { + upcomingEvents.push([eventDate, name, null]); + } + } + } + }); + upcomingEvents.sort((a, b) => { + const [aYear, aMonth, aDay] = a[0].split("-").map(Number); + const [bYear, bMonth, bDay] = b[0].split("-").map(Number); + return H.calculateDateDifference({ year: aYear, month: aMonth, day: aDay }, currentYear, currentMonth, currentDay) + - H.calculateDateDifference({ year: bYear, month: bMonth, day: bDay }, currentYear, currentMonth, currentDay); + }); + + return upcomingEvents.slice(0, limit); + }, + calculateWeekday: (year, month, day) => { + if (errorCheck(23, 'calendar', CALENDARS[QUEST_TRACKER_calenderType])) return; + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + if (errorCheck(24, 'calendar.daysOfWeek', calendar.daysOfWeek)) return; + if (errorCheck(25, 'calendar.startingWeekday', calendar.startingWeekday)) return; + if (errorCheck(26, 'calendar.startingYear', calendar.startingYear)) return; + const daysOfWeek = calendar.daysOfWeek; + const startingWeekday = calendar.startingWeekday; + const startingYear = calendar.startingYear; + let totalDays = 0; + for (let y = startingYear; y < year; y++) { + totalDays += H.getTotalDaysInYear(y); + } + for (let m = 1; m < month; m++) { + totalDays += typeof calendar.months[m - 1].days === "function" + ? calendar.months[m - 1].days(year) + : calendar.months[m - 1].days; + } + totalDays += day - 1; + return daysOfWeek[(daysOfWeek.indexOf(startingWeekday) + totalDays) % daysOfWeek.length]; + } + }; + const determineWeather = (date) => { + const W = { + getSeasonBoundaries: (year) => { + if (errorCheck(27, 'exists', CALENDARS[QUEST_TRACKER_calenderType]?.climates[QUEST_TRACKER_Location], `CALENDARS[${QUEST_TRACKER_calenderType}]?.climates[${QUEST_TRACKER_Location}]`)) return; + const climate = CALENDARS[QUEST_TRACKER_calenderType]?.climates[QUEST_TRACKER_Location]; + const boundaries = []; + const seasonStart = climate.seasonStart || {}; + for (const [seasonName, startMonth] of Object.entries(seasonStart)) { + let startDayOfYear = 0; + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + for (let i = 0; i < startMonth - 1; i++) { + const monthObj = calendar.months[i]; + startDayOfYear += typeof monthObj.days === "function" ? monthObj.days(year) : monthObj.days; + } + boundaries.push({ season: seasonName, startDayOfYear }); + } + boundaries.sort((a, b) => a.startDayOfYear - b.startDayOfYear); + const totalDaysInYear = H.getTotalDaysInYear(year); + boundaries.forEach((boundary, i) => { + const nextIndex = (i + 1) % boundaries.length; + boundary.endDayOfYear = + boundaries[nextIndex].startDayOfYear - 1 >= 0 + ? boundaries[nextIndex].startDayOfYear - 1 + : totalDaysInYear - 1; + }); + return boundaries; + }, + getCurrentSeason: (date) => { + const [year, month, day] = date.split("-").map(Number); + const boundaries = W.getSeasonBoundaries(year); + if (!boundaries || boundaries.length === 0) return null; + let dayOfYear = 0; + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + for (let i = 0; i < month - 1; i++) { + const monthObj = calendar.months[i]; + dayOfYear += typeof monthObj.days === "function" ? monthObj.days(year) : monthObj.days; + } + dayOfYear += day; + for (const { season, startDayOfYear, endDayOfYear } of boundaries) { + if (startDayOfYear <= endDayOfYear) { + if (dayOfYear >= startDayOfYear && dayOfYear <= endDayOfYear) { + return { season, dayOfYear }; + } + } else { + if (dayOfYear >= startDayOfYear || dayOfYear <= endDayOfYear) { + return { season, dayOfYear }; + } + } + } + return null; + }, + getSuddenSeasonalChangeProbability: (dayOfYear, boundaries) => { + const buffer = 5; + for (const { startDayOfYear, endDayOfYear } of boundaries) { + if (Math.abs(dayOfYear - startDayOfYear) <= buffer || Math.abs(dayOfYear - endDayOfYear) <= buffer) { + return 0.25; + } + } + return 0.05; + }, + applyForcedTrends: (rolls) => { + const { temperatureRoll, precipitationRoll, windRoll, humidityRoll, visibilityRoll, cloudCoverRoll } = rolls; + return { + temperatureRoll: QUEST_TRACKER_FORCED_WEATHER_TRENDS.heat + ? Math.min(100, temperatureRoll + 20) + : QUEST_TRACKER_FORCED_WEATHER_TRENDS.cold + ? Math.max(1, temperatureRoll - 20) + : temperatureRoll, + precipitationRoll: QUEST_TRACKER_FORCED_WEATHER_TRENDS.wet + ? Math.min(100, precipitationRoll + 20) + : QUEST_TRACKER_FORCED_WEATHER_TRENDS.dry + ? Math.max(1, precipitationRoll - 20) + : precipitationRoll, + windRoll: QUEST_TRACKER_FORCED_WEATHER_TRENDS.wind + ? Math.min(100, windRoll + 20) + : windRoll, + humidityRoll: QUEST_TRACKER_FORCED_WEATHER_TRENDS.humid + ? Math.min(100, humidityRoll + 20) + : humidityRoll, + visibilityRoll: QUEST_TRACKER_FORCED_WEATHER_TRENDS.visibility + ? Math.min(100, visibilityRoll + 20) + : visibilityRoll, + cloudCoverRoll: QUEST_TRACKER_FORCED_WEATHER_TRENDS.cloudy + ? Math.min(100, cloudCoverRoll + 20) + : cloudCoverRoll, + }; + }, + applyTrends: (rolls) => { + const { temperatureRoll, precipitationRoll, windRoll, humidityRoll, visibilityRoll, cloudCoverRoll } = rolls; + return { + temperatureRoll: + temperatureRoll + + (QUEST_TRACKER_WEATHER_TRENDS.heat || 0) * 2 - + (QUEST_TRACKER_WEATHER_TRENDS.cold || 0) * 2, + precipitationRoll: + precipitationRoll + + (QUEST_TRACKER_WEATHER_TRENDS.wet || 0) * 2 - + (QUEST_TRACKER_WEATHER_TRENDS.dry || 0) * 2, + windRoll: windRoll + (QUEST_TRACKER_WEATHER_TRENDS.wind || 0) * 2, + humidityRoll: humidityRoll + (QUEST_TRACKER_WEATHER_TRENDS.humid || 0) * 2, + visibilityRoll: visibilityRoll + (QUEST_TRACKER_WEATHER_TRENDS.visibility || 0) * 2, + cloudCoverRoll: cloudCoverRoll + (QUEST_TRACKER_WEATHER_TRENDS.cloudy || 0) * 2, + }; + }, + updateTrends: (rolls) => { + ["heat", "cold", "wet", "dry", "wind", "visibility", "cloudy"].forEach((trendType) => { + const roll = rolls[`${trendType}Roll`]; + if (["wind", "visibility", "cloudy"].includes(trendType) && roll < 75) { + QUEST_TRACKER_WEATHER_TRENDS[trendType] = 0; + } else if (roll > 75) { + QUEST_TRACKER_WEATHER_TRENDS[trendType] = + (QUEST_TRACKER_WEATHER_TRENDS[trendType] || 0) + 1; + } else if (QUEST_TRACKER_WEATHER_TRENDS[trendType]) { + QUEST_TRACKER_WEATHER_TRENDS[trendType] = 0; + } + }); + if (rolls.precipitationRoll > 75) QUEST_TRACKER_WEATHER_TRENDS.dry = 0; + if (rolls.temperatureRoll > 75) QUEST_TRACKER_WEATHER_TRENDS.cold = 0; + if (rolls.temperatureRoll < 25) QUEST_TRACKER_WEATHER_TRENDS.heat = 0; + }, + generateBellCurveRoll: (adj = 0) => { + const randomGaussian = () => { + let u = 0, v = 0; + while (u === 0) u = Math.random(); + while (v === 0) v = Math.random(); + return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v); + }; + const center = 50 + adj; + const lowerBound = Math.max(0, center - 25); + const upperBound = Math.min(100, center + 25); + let roll = Math.random() * (upperBound - lowerBound) + lowerBound; + let bias = roll <= center + ? Math.pow((roll - lowerBound) / (center - lowerBound), 2) + : Math.pow((upperBound - roll) / (upperBound - center), 2); + if (Math.random() < bias) { + return Math.round(roll * 100) / 100; + } else { + return W.generateBellCurveRoll(adj); + } + }, + adjustDailyFluctuation: (date, trendAdjustedRolls, suddenChangeProbability, seasonBoundary) => { + const previousWeather = QUEST_TRACKER_HISTORICAL_WEATHER[Object.keys(QUEST_TRACKER_HISTORICAL_WEATHER).reverse().find(d => d < date)]; + if (!previousWeather) return trendAdjustedRolls; + const maxChange = suddenChangeProbability > 0.05 ? 10 : 5; + const maxBoundaryChange = suddenChangeProbability > 0.05 ? 20 : 10; + const adjustedRolls = { ...trendAdjustedRolls }; + Object.keys(adjustedRolls).forEach((key) => { + const prevValue = previousWeather[key]; + if (prevValue !== undefined) { + const boundaryLimit = seasonBoundary ? maxBoundaryChange : maxChange; + const change = adjustedRolls[key] - prevValue; + if (Math.abs(change) > boundaryLimit) { + adjustedRolls[key] = prevValue + Math.sign(change) * boundaryLimit; + } + } + }); + return adjustedRolls; + } + }; + const [year, month, day] = date.split("-").map(Number); + const currentSeasonData = W.getCurrentSeason(date); + if (!currentSeasonData) return; + const { season, dayOfYear } = currentSeasonData; + const boundaries = W.getSeasonBoundaries(year); + const suddenChangeProbability = W.getSuddenSeasonalChangeProbability(dayOfYear, boundaries); + const rolls = { + temperatureRoll: W.generateBellCurveRoll(), + precipitationRoll: W.generateBellCurveRoll(), + windRoll: W.generateBellCurveRoll(-15), + humidityRoll: W.generateBellCurveRoll(), + visibilityRoll: W.generateBellCurveRoll(15), + cloudCoverRoll: W.generateBellCurveRoll(), + }; + const forcedAdjustedRolls = W.applyForcedTrends(rolls); + const trendAdjustedRolls = W.applyTrends(forcedAdjustedRolls); + W.updateTrends(trendAdjustedRolls); + const climateModifiers = CALENDARS[QUEST_TRACKER_calenderType]?.climates[QUEST_TRACKER_Location]?.modifiers; + trendAdjustedRolls.temperatureRoll += climateModifiers?.temperature?.[season] || 0; + trendAdjustedRolls.precipitationRoll += climateModifiers?.precipitation?.[season] || 0; + trendAdjustedRolls.windRoll += climateModifiers?.wind?.[season] || 0; + trendAdjustedRolls.humidityRoll += climateModifiers?.humid?.[season] || 0; + trendAdjustedRolls.visibilityRoll += climateModifiers?.visibility?.[season] || 0; + const nearBoundary = suddenChangeProbability > 0.05; + const isBoundaryDay = boundaries.some(({ startDayOfYear, endDayOfYear }) => + Math.abs(dayOfYear - startDayOfYear) <= 1 || Math.abs(dayOfYear - endDayOfYear) <= 1 + ); + const finalAdjustedRolls = W.adjustDailyFluctuation(date, trendAdjustedRolls, suddenChangeProbability, isBoundaryDay); + Object.keys(finalAdjustedRolls).forEach((key) => { + finalAdjustedRolls[key] = Math.max(1, Math.min(100, finalAdjustedRolls[key])); + }); + const weather = { + date, + season, + ...finalAdjustedRolls, + trends: { ...QUEST_TRACKER_WEATHER_TRENDS }, + forcedTrends: { ...QUEST_TRACKER_FORCED_WEATHER_TRENDS }, + nearBoundary, + }; + QUEST_TRACKER_HISTORICAL_WEATHER[date] = weather; + saveQuestTrackerData(); + Utils.updateHandoutField("weather"); + }; + const modifyDate = ({ type = "day", amount = 1, newDate = null }) => { + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + if (errorCheck(28, 'exists', calendar,'calendar')) return; + const L = { + formatDate: (year, month, day) => { + return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`; + }, + wrapAround: () => { + while (day > H.getDaysInMonth(month, year)) { + day -= H.getDaysInMonth(month, year); + month++; + if (month > calendar.months.length) { + month = 1; + year++; + } + } + while (day < 1) { + month--; + if (month < 1) { + month = calendar.months.length; + year--; + } + day += H.getDaysInMonth(month, year); + } + }, + generateDateArray: () => { + const dates = []; + let targetDate = null; + if (type === "event") { + const closestEvent = H.findNextEvents(1); + if (!closestEvent || closestEvent.length === 0) { + Utils.sendGMMessage("No upcoming festivals, events, or significant dates found."); + return []; + } + targetDate = closestEvent[0][0]; + } + while (steps >= 0 || targetDate) { + dates.push(L.formatDate(year, month, day)); + if (type === "event" && targetDate) { + const [targetYear, targetMonth, targetDay] = targetDate.split("-").map(Number); + while ( + year !== targetYear || + month !== targetMonth || + day !== targetDay + ) { + day += direction; + L.wrapAround(); + dates.push(L.formatDate(year, month, day)); + } + break; + } + switch (type) { + case "day": + day += direction; + L.wrapAround(); + break; + case "week": + day += direction * calendar.daysOfWeek.length; + L.wrapAround(); + break; + case "month": + month += direction; + if (month > calendar.months.length) { + month -= calendar.months.length; + year++; + } else if (month < 1) { + month += calendar.months.length; + year--; + } + day = Math.min(day, H.getDaysInMonth(month, year)); + break; + case "year": + year += direction; + day = Math.min(day, H.getDaysInMonth(month, year)); + break; + default: + break; + } + steps--; + } + return dates; + }, + generateCompleteDateList: (startDate, endDate) => { + const [startYear, startMonth, startDay] = startDate.split("-").map(Number); + const [endYear, endMonth, endDay] = endDate.split("-").map(Number); + let currentYear = startYear, currentMonth = startMonth, currentDay = startDay; + const dateList = []; + while ( + currentYear < endYear || + (currentYear === endYear && currentMonth < endMonth) || + (currentYear === endYear && currentMonth === endMonth && currentDay <= endDay) + ) { + dateList.push(L.formatDate(currentYear, currentMonth, currentDay)); + currentDay++; + if (currentDay > H.getDaysInMonth(currentMonth, currentYear)) { + currentDay = 1; + currentMonth++; + if (currentMonth > calendar.months.length) { + currentMonth = 1; + currentYear++; + } + } + } + dateList.push(L.formatDate(endYear, endMonth, endDay)); + return dateList; + }, + validateISODate: (date) => { + const [y, m, d] = date.split("-").map(Number); + if (!y || !m || !d || m < 1 || m > calendar.months.length) { + errorCheck(29, 'msg', null,`Invalid ISO date format or date out of range for calendar: ${date}`); + return null; + } + const daysInMonth = H.getDaysInMonth(m, y); + if (d < 1 || d > daysInMonth) { + errorCheck(30, 'msg', null,`Day out of range for the specified month: ${date}`); + return null; + } + return { year: y, month: m, day: d }; + }, + isAfterCurrentDate: (eventYear, eventMonth, eventDay) => { + if (eventYear > year) return true; + if (eventYear === year && eventMonth > month) return true; + if (eventYear === year && eventMonth === month && eventDay > day) return true; + return false; + } + }; + let [year, month, day] = QUEST_TRACKER_currentDate.split("-").map(Number); + if (type === "set") { + const { year: newYear, month: newMonth, day: newDay } = L.validateISODate(newDate); + QUEST_TRACKER_currentDate = L.formatDate(newYear, newMonth, newDay); + saveQuestTrackerData(); + return; + } + let steps = Math.abs(amount); + let direction = Math.sign(amount); + const dateArray = L.generateDateArray(); + if (QUEST_TRACKER_WEATHER && dateArray.length > 0) { + dateArray.forEach((date) => { + if (!QUEST_TRACKER_HISTORICAL_WEATHER[date]) { + determineWeather(date); + } + }); + } + const [finalYear, finalMonth, finalDay] = dateArray[dateArray.length - 1].split("-").map(Number); + year = finalYear; + month = finalMonth; + day = finalDay; + QUEST_TRACKER_currentDate = L.formatDate(year, month, day); + QUEST_TRACKER_currentWeekdayName = H.calculateWeekday(year, month, day); + H.checkEvent(); + Triggers.checkTriggers('date'); + describeWeather(); + saveQuestTrackerData(); + Utils.sendMessage(`Date is now: ${Calendar.formatDateFull()}`) + Utils.sendDescMessage(QUEST_TRACKER_CURRENT_WEATHER['description']); + Menu.buildWeather({ isMenu: false }); + }; + const addEvent = () => { + const newEventId = H.generateNewEventId(); + const defaultEventData = { + name: 'New Event', + description: 'Description', + date: `${QUEST_TRACKER_defaultDate}`, + hidden: true, + repeatable: false, + frequency: null + }; + QUEST_TRACKER_Events[newEventId] = defaultEventData; + Utils.updateHandoutField('event'); + }; + const getNextEvents = (number) => { + return H.findNextEvents(number); + }; + const removeEvent = (eventId) => { + delete QUEST_TRACKER_Events[eventId]; + Utils.updateHandoutField('event'); + }; + const manageEventObject = ({ action, field, current, old = '', newItem, date }) => { + const event = QUEST_TRACKER_Events[current]; + switch (field) { + case 'hidden': + event.hidden = !event.hidden; + break; + case 'repeatable': + event.repeatable = !event.repeatable; + event.frequency = 1; + break; + case 'frequency': + event.frequency = newItem; + if (newItem === "2") { + const [year, month, day] = date.split("-").map(Number); + event.weekdayname = H.calculateWeekday(year, month, day); + } + break; + case 'name': + event.name = newItem; + break; + case 'date': + event.date = newItem; + if (event.frequency === "2" && event.repeatable) { + const [year, month, day] = newItem.split("-").map(Number); + event.weekdayname = H.calculateWeekday(year, month, day); + } + break; + case 'description': + event.description = newItem; + break; + default: + errorCheck(31, 'msg', null,`Unknown field command: ${field}`); + break; + } + Utils.updateHandoutField('event'); + }; + const setCalender = (calender) => { + QUEST_TRACKER_calenderType = calender; + const calendar = CALENDARS[calender]; + QUEST_TRACKER_currentDate = calendar.defaultDate; + QUEST_TRACKER_defaultDate = calendar.defaultDate; + const [year, month, day] = QUEST_TRACKER_currentDate.split("-").map(Number); + QUEST_TRACKER_currentWeekdayName = H.calculateWeekday(year, month, day); + const firstClimate = Object.keys(calendar.climates)[0]; + if (firstClimate) { + setClimate(firstClimate); + } + saveQuestTrackerData(); + }; + const setClimate = (climate) => { + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + QUEST_TRACKER_Location = climate; + saveQuestTrackerData(); + }; + const setWeatherTrend = (type, amount) => { + QUEST_TRACKER_WEATHER_TRENDS[type] = parseInt(QUEST_TRACKER_WEATHER_TRENDS[type], 10) || 0; + amount = parseInt(amount, 10); + QUEST_TRACKER_WEATHER_TRENDS[type] += amount; + saveQuestTrackerData(); + }; + const formatDateFull = () => { + const [year, month, day] = QUEST_TRACKER_currentDate.split("-").map(Number); + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + const monthName = calendar.months[month - 1].name; + const format = calendar.dateFormat || "{day}{ordinal} of {month}, {year}"; + const ordinal = (n) => { + const s = ["th", "st", "nd", "rd"]; + const v = n % 100; + return s[(v - 20) % 10] || s[v] || s[0]; + }; + return format + .replace("{day}", day) + .replace("{ordinal}", ordinal(day)) + .replace("{month}", monthName) + .replace("{year}", year); + }; + const forceWeatherTrend = (field) => { + const fieldList = ["dry", "wet", "heat", "cold"]; + const isCurrentlyTrue = QUEST_TRACKER_FORCED_WEATHER_TRENDS[field]; + QUEST_TRACKER_FORCED_WEATHER_TRENDS[field] = !isCurrentlyTrue; + if (QUEST_TRACKER_FORCED_WEATHER_TRENDS[field] === true) { + fieldList + .filter((f) => f !== field) + .forEach((f) => { + QUEST_TRACKER_FORCED_WEATHER_TRENDS[f] = false; + }); + } + saveQuestTrackerData(); + }; + const getLunarPhase = (date, moonId) => { + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + if (errorCheck(153, 'exists', calendar.lunarCycle, `calendar.lunarCycle`)) return; + if (errorCheck(154, 'exists', calendar.lunarCycle[moonId], `calendar.lunarCycle[${moonId}]`)) return; + const { baselineNewMoon, cycleLength, phases, name } = calendar.lunarCycle[moonId]; + const baselineDate = new Date(baselineNewMoon); + const currentDate = new Date(date); + const daysSinceBaseline = (currentDate - baselineDate) / (1000 * 60 * 60 * 24); + const phase = (daysSinceBaseline % cycleLength + cycleLength) % cycleLength; + for (const { name: phaseName, start, end } of phases) { + if (phase >= start && phase < end) { + return `${name}: ${phaseName}`; + } + } + return `${name}: Unknown Phase`; + }; + const describeWeather = () => { + const L = { + meetsCondition: (value, cond) => { + if (cond.gte !== undefined && value < cond.gte) return false; + if (cond.lte !== undefined && value > cond.lte) return false; + return true; + }, + matchesConditions: (rolls, conditions, ignoreKeys = []) => { + for (const [metric, cond] of Object.entries(conditions)) { + if (ignoreKeys.includes(metric)) continue; + const val = rolls[metric]; + if (val === undefined) return false; + if (!L.meetsCondition(val, cond)) return false; + } + return true; + }, + countMatches: (rolls, conditions, ignoreKeys = []) => { + let matchCount = 0; + for (const [metric, cond] of Object.entries(conditions)) { + if (ignoreKeys.includes(metric)) continue; + const val = rolls[metric]; + if (val !== undefined && L.meetsCondition(val, cond)) { + matchCount++; + } + } + return matchCount; + }, + determineWeatherType: (rolls) => { + const WEATHER_TYPES = WEATHER.weather; + let matches = []; + for (const [typeName, typeData] of Object.entries(WEATHER_TYPES)) { + if (L.matchesConditions(rolls, typeData.conditions)) { + matches.push(typeName); + } + } + if (matches.length > 0) { + const chosenMatch = matches[Math.floor(Math.random() * matches.length)]; + return { type: chosenMatch }; + } + matches = []; + for (const [typeName, typeData] of Object.entries(WEATHER_TYPES)) { + if (L.matchesConditions(rolls, typeData.conditions, ['visibility'])) { + matches.push(typeName); + } + } + if (matches.length > 0) { + const chosenMatch = matches[Math.floor(Math.random() * matches.length)]; + return { type: chosenMatch }; + } + matches = []; + for (const [typeName, typeData] of Object.entries(WEATHER_TYPES)) { + if (L.matchesConditions(rolls, typeData.conditions, ['visibility', 'cloudCover'])) { + matches.push(typeName); + } + } + if (matches.length > 0) { + const chosenMatch = matches[Math.floor(Math.random() * matches.length)]; + return { type: chosenMatch }; + } + let bestType = null; + let bestCount = -1; + for (const [typeName, typeData] of Object.entries(WEATHER_TYPES)) { + const count = L.countMatches(rolls, typeData.conditions); + if (count > bestCount) { + bestCount = count; + bestType = typeName; + } + } + if (bestType) { + return { type: bestType }; + } + return { type: "unclassified normal weather" }; + }, + getScaleDescription: (metric, value) => { + const scaleEntries = Object.entries(WEATHER.scales[metric]); + const numericKeys = scaleEntries.map(([k]) => parseInt(k,10)).sort((a,b) => a - b); + let chosenKey = numericKeys[0]; + for (let k of numericKeys) { + if (k <= value) { + chosenKey = k; + } else { + break; + } + } + return WEATHER.scales[metric][chosenKey.toString()].description; + } + }; + const todayWeather = QUEST_TRACKER_HISTORICAL_WEATHER[QUEST_TRACKER_currentDate]; + if (!todayWeather) return; + const rolls = { + temperature: todayWeather.temperatureRoll, + precipitation: todayWeather.precipitationRoll, + wind: todayWeather.windRoll, + humidity: todayWeather.humidityRoll, + cloudCover: todayWeather.cloudCoverRoll, + visibility: todayWeather.visibilityRoll + }; + const result = L.determineWeatherType(rolls); + const chosenType = result.type; + let chosenWeatherData; + if (WEATHER.weather[chosenType]) { + chosenWeatherData = WEATHER.weather[chosenType]; + } else { + chosenWeatherData = { + descriptions: { + [QUEST_TRACKER_WeatherLocation]: { + "1": "Unclassified normal weather conditions." + } + } + }; + } + const envDescriptions = chosenWeatherData.descriptions[QUEST_TRACKER_WeatherLocation] || { "1": "No description available." }; + const envDescriptionKeys = Object.keys(envDescriptions); + const randomDescKey = envDescriptionKeys[Math.floor(Math.random() * envDescriptionKeys.length)]; + const chosenDescription = envDescriptions[randomDescKey]; + QUEST_TRACKER_CURRENT_WEATHER = { + weatherType: chosenType, + description: chosenDescription, + environment: WEATHER.enviroments[QUEST_TRACKER_WeatherLocation] ? WEATHER.enviroments[QUEST_TRACKER_WeatherLocation].name : QUEST_TRACKER_WeatherLocation, + rolls: { ...rolls }, + scaleDescriptions: { + temperature: L.getScaleDescription("temperature", rolls.temperature), + humidity: L.getScaleDescription("humidity", rolls.humidity), + wind: L.getScaleDescription("wind", rolls.wind), + precipitation: L.getScaleDescription("precipitation", rolls.precipitation), + cloudCover: L.getScaleDescription("cloudCover", rolls.cloudCover), + visibility: L.getScaleDescription("visibility", rolls.visibility) + } + }; + }; + const adjustLocation = (location) => { + if (WEATHER.enviroments.hasOwnProperty(location)) { + QUEST_TRACKER_WeatherLocation = location; + saveQuestTrackerData(); + } else return; + }; + return { + modifyDate, + addEvent, + removeEvent, + manageEventObject, + setCalender, + formatDateFull, + setClimate, + setWeatherTrend, + forceWeatherTrend, + getLunarPhase, + getNextEvents, + adjustLocation + }; + })(); + const QuestPageBuilder = (() => { + const vars = { + DEFAULT_PAGE_UNIT: 70, + AVATAR_SIZE: 70, + TEXT_FONT_SIZE: 14, + PAGE_HEADER_WIDTH: 700, + PAGE_HEADER_HEIGHT: 150, + ROUNDED_RECT_WIDTH: 280, + ROUNDED_RECT_HEIGHT: 60, + ROUNDED_RECT_CORNER_RADIUS: 10, + VERTICAL_SPACING: 100, + HORIZONTAL_SPACING: 160, + DEFAULT_FILL_COLOR: '#CCCCCC', + DEFAULT_STATUS_COLOR: '#000000', + QUESTICON_WIDTH: 305, + GROUP_SPACING: 800, + QUESTICON_HEIGHT: 92 + }; + const H = { + adjustPageSettings: (page) => { + page.set({ + showgrid: false, + snapping_increment: 0, + diagonaltype: 'facing', + scale_number: 1, + }); + }, + adjustPageSizeToFitPositions: (page, questPositions) => { + const positions = Object.values(questPositions); + if (positions.length === 0) return; + const minX = Math.min(...positions.map(pos => pos.x)); + const maxX = Math.max(...positions.map(pos => pos.x)); + const minY = Math.min(...positions.map(pos => pos.y)); + const maxY = Math.max(...positions.map(pos => pos.y)); + const requiredWidthInPixels = (maxX - minX) + vars.ROUNDED_RECT_WIDTH + vars.HORIZONTAL_SPACING * 2; + const requiredHeightInPixels = (maxY - minY) + vars.ROUNDED_RECT_HEIGHT + vars.VERTICAL_SPACING * 2 + vars.PAGE_HEADER_HEIGHT; + const requiredWidthInUnits = Math.ceil(requiredWidthInPixels / vars.DEFAULT_PAGE_UNIT); + const requiredHeightInUnits = Math.ceil(requiredHeightInPixels / vars.DEFAULT_PAGE_UNIT); + page.set({ width: requiredWidthInUnits, height: requiredHeightInUnits }); + }, + clearPageObjects: (pageId, callback) => { + const pageElements = [ + ...findObjs({ _type: 'graphic', _pageid: pageId }), + ...findObjs({ _type: 'path', _pageid: pageId }), + ...findObjs({ _type: 'text', _pageid: pageId }) + ]; + pageElements.forEach(obj => obj.remove()); + if (typeof callback === 'function') callback(); + }, + buildPageHeader: (page) => { + const titleText = 'Quest Tracker Quest Tree'; + const descriptionText = 'A visual representation of all quests.'; + const pageWidth = page.get('width') * vars.DEFAULT_PAGE_UNIT; + const titleX = pageWidth / 2; + const titleY = 70; + D.drawText(page.id, titleX, titleY, titleText, '#000000', 'map', 32, 'Contrail One', null, 'center', 'middle'); + const descriptionY = titleY + 40; + D.drawText(page.id, titleX, descriptionY, descriptionText, '#666666', 'map', 18, 'Contrail One', null, 'center', 'middle'); + }, + storeQuestRef: (questId, type, objRef, target = null) => { + if (!QUEST_TRACKER_TreeObjRef[questId]) { + QUEST_TRACKER_TreeObjRef[questId] = { paths: {} }; + } + if (type === 'paths' && target) { + if (!QUEST_TRACKER_TreeObjRef[questId][type][target]) { + QUEST_TRACKER_TreeObjRef[questId][type][target] = []; + } + QUEST_TRACKER_TreeObjRef[questId][type][target].push(objRef); + } else { + QUEST_TRACKER_TreeObjRef[questId][type] = objRef; + } + saveQuestTrackerData(); + }, + replaceImageSize: (imgsrc) => { + return imgsrc.replace(/\/(med|original|max|min)\.(gif|jpg|jpeg|bmp|webp|png)(\?.*)?$/i, '/thumb.$2$3'); + }, + trimText: (text, maxLength = 150) => { + if (text.length > maxLength) { + return text.slice(0, maxLength - 3) + '...'; + } + return text; + }, + getStatusColor: (status) => { + switch (status) { + case 'Unknown': + return '#A9A9A9'; + case 'Discovered': + return '#ADD8E6'; + case 'Started': + return '#87CEFA'; + case 'Ongoing': + return '#FFD700'; + case 'Completed': + return '#32CD32'; + case 'Completed By Someone Else': + return '#4682B4'; + case 'Failed': + return '#FF6347'; + case 'Time ran out': + return '#FF8C00'; + case 'Ignored': + return '#D3D3D3'; + default: + return '#CCCCCC'; + } + }, + buildDAG: (questData, vars) => { + const questPositions = {}; + const groupMap = {}; + const mutualExclusivityClusters = []; + const visitedForClusters = new Set(); + const enabledQuests = Object.keys(questData).filter((questId) => !questData[questId]?.disabled); + function findMutualExclusivityCluster(startQuestId) { + const cluster = new Set(); + const stack = [startQuestId]; + while (stack.length > 0) { + const questId = stack.pop(); + if (!cluster.has(questId)) { + cluster.add(questId); + visitedForClusters.add(questId); + const mutuallyExclusiveQuests = + questData[questId]?.relationships?.mutually_exclusive || []; + mutuallyExclusiveQuests.forEach((meQuestId) => { + if (!cluster.has(meQuestId) && enabledQuests.includes(meQuestId)) { + stack.push(meQuestId); + } + }); + } + } + return cluster; + } + enabledQuests.forEach((questId) => { + if (!visitedForClusters.has(questId)) { + const cluster = findMutualExclusivityCluster(questId); + mutualExclusivityClusters.push(cluster); + } + }); + const questIdToClusterIndex = {}; + mutualExclusivityClusters.forEach((cluster, index) => { + cluster.forEach((questId) => { + questIdToClusterIndex[questId] = index; + }); + }); + const calculateInitialLevels = (questId, visited = new Set()) => { + if (visited.has(questId)) return questData[questId].level || 0; + visited.add(questId); + const prereqs = questData[questId]?.relationships?.conditions || []; + if (prereqs.length === 0) { + questData[questId].level = 0; + return 0; + } + const prereqLevels = prereqs.map((prereq) => { + let prereqId; + if (typeof prereq === "string") { + prereqId = prereq; + } else if (typeof prereq === "object" && prereq.conditions) { + prereqId = prereq.conditions[0]; // Simplification + } + return calculateInitialLevels(prereqId, new Set(visited)) + 1; + }); + const level = Math.max(...prereqLevels); + questData[questId].level = level; + return level; + }; + enabledQuests.forEach((questId) => calculateInitialLevels(questId)); + enabledQuests.forEach((questId) => { + const group = questData[questId]?.group || "Default Group"; + if (!groupMap[group]) groupMap[group] = []; + groupMap[group].push(questId); + }); + const groupWidths = {}; + const groupOrder = Object.keys(groupMap); + Object.entries(groupMap).forEach(([groupName, groupQuests]) => { + const levels = {}; + groupQuests.forEach((questId) => { + const level = questData[questId].level; + if (!levels[level]) levels[level] = []; + levels[level].push(questId); + }); + const sortedLevels = Object.keys(levels).map(Number).sort((a, b) => a - b); + let maxLevelWidth = 0; + sortedLevels.forEach((level) => { + let questsAtLevel = levels[level]; + const totalQuests = questsAtLevel.length; + const clustersAtLevel = {}; + questsAtLevel.forEach((questId) => { + const clusterIndex = questIdToClusterIndex[questId] || null; + if (clusterIndex !== null) { + if (!clustersAtLevel[clusterIndex]) clustersAtLevel[clusterIndex] = new Set(); + clustersAtLevel[clusterIndex].add(questId); + } else { + if (!clustersAtLevel["no_cluster"]) clustersAtLevel["no_cluster"] = new Set(); + clustersAtLevel["no_cluster"].add(questId); + } + }); + const arrangedQuests = []; + Object.values(clustersAtLevel).forEach((cluster) => { + arrangedQuests.push(...Array.from(cluster)); + }); + levels[level] = arrangedQuests; + const levelWidth = + arrangedQuests.length * vars.ROUNDED_RECT_WIDTH + + (arrangedQuests.length - 1) * vars.HORIZONTAL_SPACING; + maxLevelWidth = Math.max(maxLevelWidth, levelWidth); + }); + groupWidths[groupName] = maxLevelWidth; + }); + const totalTreeWidth = groupOrder.reduce((sum, groupName, index) => { + return sum + groupWidths[groupName] + (index > 0 ? vars.GROUP_SPACING : 0); + }, 0); + let cumulativeGroupWidth = -totalTreeWidth / 2; + groupOrder.forEach((groupName) => { + const groupQuests = groupMap[groupName]; + const levels = {}; + groupQuests.forEach((questId) => { + const level = questData[questId].level; + if (!levels[level]) levels[level] = []; + levels[level].push(questId); + }); + const sortedLevels = Object.keys(levels).map(Number).sort((a, b) => a - b); + sortedLevels.forEach((level) => { + let questsAtLevel = levels[level]; + const totalQuests = questsAtLevel.length; + const arrangedQuests = levels[level]; + const levelWidth = + arrangedQuests.length * vars.ROUNDED_RECT_WIDTH + + (arrangedQuests.length - 1) * vars.HORIZONTAL_SPACING; + const levelStartX = cumulativeGroupWidth + (groupWidths[groupName] - levelWidth) / 2; + arrangedQuests.forEach((questId, index) => { + const x = + levelStartX + index * (vars.ROUNDED_RECT_WIDTH + vars.HORIZONTAL_SPACING); + const y = level * (vars.ROUNDED_RECT_HEIGHT + vars.VERTICAL_SPACING); + questPositions[questId] = { + x: x, + y: y, + group: groupName, + }; + }); + }); + cumulativeGroupWidth += groupWidths[groupName] + vars.GROUP_SPACING; + }); + return questPositions; + } + }; + const D = { + drawQuestTreeFromPositions: (page, questPositions, callback) => { + const totalWidth = page.get('width') * vars.DEFAULT_PAGE_UNIT; + Object.entries(questPositions).forEach(([questId, position]) => { + const questData = QUEST_TRACKER_globalQuestData[questId]; + if (!questData) { + errorCheck(32, 'msg', null,`Quest data for "${questId}" is missing.`); + return; + } + const x = position.x + totalWidth / 2; + const y = position.y + vars.PAGE_HEADER_HEIGHT + vars.VERTICAL_SPACING; + const isHidden = questData.hidden || false; + D.drawQuestGraphics(questId, questData, page.id, x, y, isHidden); + }); + if (typeof callback === 'function') callback(); + }, + drawQuestGraphics: (questId, questData, pageId, x, y, isHidden) => { + const questTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTS })[0]; + if (!questTable) { + errorCheck(33, 'msg', null,`Quests rollable table not found.`); + return; + } + const questTableItems = findObjs({ type: 'tableitem', rollabletableid: questTable.id }); + const questTableItem = questTableItems.find(item => item.get('name').toLowerCase() === questId.toLowerCase()); + if (!questTableItem) { + errorCheck(34, 'msg', null,`Rollable table item for quest "${questId}" not found.`); + return; + } + const statusWeight = questTableItem.get('weight'); + const statusName = statusMapping[statusWeight] || 'Unknown'; + const statusColor = H.getStatusColor(statusName); + let imgsrc = questTableItem.get('avatar'); + if (!imgsrc || !imgsrc.includes('https://')) { + imgsrc = QUEST_TRACKER_BASE_QUEST_ICON_URL; + } else { + imgsrc = H.replaceImageSize(imgsrc); + } + D.drawRoundedRectangle(pageId, x, y, vars.ROUNDED_RECT_WIDTH, vars.ROUNDED_RECT_HEIGHT, vars.ROUNDED_RECT_CORNER_RADIUS, statusColor, isHidden ? 'gmlayer' : 'map', questId); + const avatarSpacing = 10; + const avatarX = x; + const avatarY = y - (vars.ROUNDED_RECT_HEIGHT / 2) - (vars.AVATAR_SIZE / 2) - avatarSpacing; + if (imgsrc !== '') D.placeAvatar(pageId, avatarX, avatarY, vars.AVATAR_SIZE, imgsrc, isHidden ? 'gmlayer' : 'objects', questId); + }, + drawQuestTextAfterGraphics: (page, questPositions) => { + const totalWidth = page.get('width') * vars.DEFAULT_PAGE_UNIT; + Object.entries(questPositions).forEach(([questId, position]) => { + const questData = QUEST_TRACKER_globalQuestData[questId]; + if (!questData) { + errorCheck(35, 'msg', null,`Quest data for "${questId}" is missing.`); + return; + } + const x = position.x + totalWidth / 2; + const y = position.y + vars.PAGE_HEADER_HEIGHT + vars.VERTICAL_SPACING; + const isHidden = questData.hidden || false; + const textLayer = isHidden ? 'gmlayer' : 'objects'; + D.drawText( + page.id, + x, + y, + questData.name, + '#000000', + textLayer, + vars.TEXT_FONT_SIZE, + 'Contrail One', + questId, + 'center', + 'middle' + ); + }); + }, + drawQuestConnections: (pageId, questPositions) => { + const page = getObj('page', pageId); + const pageWidth = page.get('width') * vars.DEFAULT_PAGE_UNIT; + const offsetX = pageWidth / 2; + const incomingPaths = {}; + Object.entries(questPositions).forEach(([questId, position]) => { + const questData = QUEST_TRACKER_globalQuestData[questId]; + if (!questData) { + errorCheck(36, 'msg', null,`Quest data for "${questId}" is missing.`); + return; + } + (questData.relationships?.conditions || []).forEach(prereq => { + let prereqId = prereq; + if (typeof prereq === 'object' && prereq.conditions) { + prereqId = prereq.conditions[0]; + } + if (!incomingPaths[prereqId]) { + incomingPaths[prereqId] = []; + } + incomingPaths[prereqId].push(questId); + }); + }); + Object.entries(questPositions).forEach(([questId, position]) => { + const questData = QUEST_TRACKER_globalQuestData[questId]; + if (!questData) { + errorCheck(37, 'msg', null,`Quest data for "${questId}" is missing.`); + return; + } + const startX = position.x + offsetX; + const startY = position.y + vars.PAGE_HEADER_HEIGHT + vars.VERTICAL_SPACING; + const startPos = { + x: startX, + y: startY + }; + (questData.relationships?.conditions || []).forEach(prereq => { + let prereqId = prereq; + if (typeof prereq === 'object' && prereq.conditions) { + prereqId = prereq.conditions[0]; + } + const prereqPosition = questPositions[prereqId]; + if (!prereqPosition) { + errorCheck(38, 'msg', null,`Position data for prerequisite "${prereqId}" is missing.`); + return; + } + const endX = prereqPosition.x + offsetX; + const endY = prereqPosition.y + vars.PAGE_HEADER_HEIGHT + vars.VERTICAL_SPACING; + const endPos = { + x: endX, + y: endY + }; + let midY; + if (incomingPaths[prereqId].length > 1) { + midY = endPos.y + vars.VERTICAL_SPACING / 2; + } else { + midY = (startPos.y + endPos.y) / 2; + } + const isHidden = questData.hidden || QUEST_TRACKER_globalQuestData[prereqId]?.hidden; + const connectionColor = isHidden ? '#CCCCCC' : '#000000'; + const connectionLayer = isHidden ? 'gmlayer' : 'map'; + D.drawPath(pageId, startPos, endPos, connectionColor, connectionLayer, questId, prereqId, midY); + }); + }); + }, + drawPath: (pageId, startPos, endPos, color = '#FF0000', layer = 'objects', questId, pathToQuestId, controlY = null, isMutualExclusion = false) => { + let pathData; + let left, top, width, height; + controlY = (controlY === null) ? (startPos.y + endPos.y) / 2 : controlY; + if (isMutualExclusion) { + pathData = [ + ['M', startPos.x, startPos.y], + ['L', endPos.x, endPos.y] + ]; + } else { + pathData = [ + ['M', startPos.x, startPos.y], + ['L', startPos.x, controlY], + ['L', endPos.x, controlY], + ['L', endPos.x, endPos.y] + ]; + } + const minX = Math.min(startPos.x, endPos.x); + const maxX = Math.max(startPos.x, endPos.x); + const minY = Math.min(startPos.y, endPos.y, controlY); + const maxY = Math.max(startPos.y, endPos.y, controlY); + left = (minX + maxX) / 2; + top = (minY + maxY) / 2; + width = maxX - minX; + height = maxY - minY; + const adjustedPathData = pathData.map(command => { + const [cmd, ...coords] = command; + const adjustedCoords = coords.map((coord, index) => { + return coord - (index % 2 === 0 ? left : top); + }); + return [cmd, ...adjustedCoords]; + }); + const pathObj = createObj('path', { + _pageid: pageId, + layer: layer, + stroke: color, + fill: 'transparent', + path: JSON.stringify(adjustedPathData), + stroke_width: 2, + controlledby: '', + left: left, + top: top, + width: width, + height: height + }); + if (pathObj) { + if (isMutualExclusion) { + H.storeQuestRef(questId, 'mutualExclusion', pathObj.id, pathToQuestId); + H.storeQuestRef(pathToQuestId, 'mutualExclusion', pathObj.id, questId); + } else { + H.storeQuestRef(questId, 'paths', pathObj.id, pathToQuestId); + H.storeQuestRef(pathToQuestId, 'paths', pathObj.id, questId); + } + } + }, + drawMutuallyExclusiveConnections: (pageId, questPositions) => { + const page = getObj('page', pageId); + const pageWidth = page.get('width') * vars.DEFAULT_PAGE_UNIT; + const offsetX = pageWidth / 2; + const mutualExclusions = []; + Object.entries(QUEST_TRACKER_globalQuestData).forEach(([questId, questData]) => { + const mutuallyExclusiveWith = questData.relationships?.mutually_exclusive || []; + mutuallyExclusiveWith.forEach(otherQuestId => { + if (questId < otherQuestId) { + mutualExclusions.push([questId, otherQuestId]); + } + }); + }); + mutualExclusions.forEach(([questId1, questId2]) => { + const position1 = questPositions[questId1]; + const position2 = questPositions[questId2]; + if (!position1 || !position2) { + errorCheck(39, 'msg', null,`Position data for quests "${questId1}" or "${questId2}" is missing.`); + return; + } + const x1 = position1.x + offsetX; + const y1 = position1.y + vars.PAGE_HEADER_HEIGHT + vars.VERTICAL_SPACING; + const x2 = position2.x + offsetX; + const y2 = position2.y + vars.PAGE_HEADER_HEIGHT + vars.VERTICAL_SPACING; + const startPos = { x: x1, y: y1 }; + const endPos = { x: x2, y: y2 }; + const questData1 = QUEST_TRACKER_globalQuestData[questId1]; + const questData2 = QUEST_TRACKER_globalQuestData[questId2]; + const isHidden = questData1.hidden || questData2.hidden; + const connectionLayer = isHidden ? 'gmlayer' : 'map'; + D.drawPath(pageId, startPos, endPos, '#FF0000', connectionLayer, questId1, questId2, null, true); + }); + }, + drawText: (pageId, x, y, textContent, color = '#000000', layer = 'objects', font_size = vars.TEXT_FONT_SIZE, font_family = 'Arial', questId, text_align = 'center', vertical_align = 'middle') => { + const textObj = createObj('text', { + _pageid: pageId, + left: x, + top: y, + text: textContent, + font_size: font_size, + color: color, + layer: layer, + font_family: font_family, + text_align: text_align + }); + if (textObj) { + if (vertical_align !== 'middle') { + const textHeight = font_size; + let adjustedTop = y; + if (vertical_align === 'top') { + adjustedTop = y - (textHeight / 2); + } else if (vertical_align === 'bottom') { + adjustedTop = y + (textHeight / 2); + } + textObj.set('top', adjustedTop); + } + if (questId) { + H.storeQuestRef(questId, 'text', textObj.id); + } + } + }, + placeAvatar: (pageId, x, y, avatarSize, imgsrc, layer = 'objects', questId) => { + const questData = QUEST_TRACKER_globalQuestData[questId]; + let tooltipText = `${questData.description || 'No description available.'}`; + let trimmedText = H.trimText(tooltipText, 150); + let handoutLink = questData.handout ? `[Open Handout](http://journal.roll20.net/handout/${questData.handout})` : ''; + const avatarObj = createObj('graphic', { + _pageid: pageId, + left: x, + top: y, + width: avatarSize, + height: avatarSize, + layer: layer, + imgsrc: imgsrc, + tooltip: trimmedText, + controlledby: '', + gmnotes: ` + [Open Quest](!qt-menu action=quest|id=${questId}) + [Toggle Visibilty](!qt-quest action=update|field=hidden|current=${questId}|old=${questData.hidden}|new=${questData.hidden ? 'false ' : 'true'}) + [Change Status](!qt-quest action=update|field=status|current=${questId}|new=?{Change Status|Unknown,1|Discovered,2|Started,3|Ongoing,4|Completed,5|Completed By Someone Else,6|Failed,7|Time ran out,8|Ignored,9}) + ${handoutLink} + `, + name: `${questData.name || 'No description available.'}` + }); + if (avatarObj) { + H.storeQuestRef(questId, 'avatar', avatarObj.id); + } + }, + drawRoundedRectangle: (pageId, x, y, width, height, radius, statusColor, layer = 'objects', questId) => { + let pathData = []; + const w = width; + const h = height; + pathData = [ + ['M', -w / 2, -h / 2], + ['L', w / 2, -h / 2], + ['L', w / 2, h / 2], + ['L', -w / 2, h / 2], + ['L', -w / 2, -h / 2], + ['Z'] + ]; + const rectObj = createObj('path', { + _pageid: pageId, + layer: layer, + stroke: statusColor, + fill: "#FAFAD2", + path: JSON.stringify(pathData), + stroke_width: 4, + controlledby: '', + left: x, + top: y, + width: width, + height: height + }); + if (rectObj) { + H.storeQuestRef(questId, 'rectangle', rectObj.id); + } + }, + redrawQuestText: (questId) => { + let pageObj = findObjs({ _type: 'page', name: QUEST_TRACKER_pageName })[0]; + if (!pageObj) return; + const pageId = pageObj.id; + if (!QUEST_TRACKER_TreeObjRef[questId] || !QUEST_TRACKER_TreeObjRef[questId].text) return; + const textObjId = QUEST_TRACKER_TreeObjRef[questId].text; + const textObj = getObj('text', textObjId); + if (textObj) { + const questData = QUEST_TRACKER_globalQuestData[questId]; + if (!questData) { + errorCheck(152, 'msg', null,`Quest data for "${questId}" is missing.`); + return; + } + const isHidden = questData.hidden || false; + const textLayer = isHidden ? 'gmlayer' : 'objects'; + const x = textObj.get('left'); + const y = textObj.get('top'); + textObj.remove(); + D.drawText(pageId, x, y, questData.name, '#000000', textLayer, vars.TEXT_FONT_SIZE, 'Contrail One', questId); + } + } + }; + const buildQuestTreeOnPage = () => { + let questTreePage = findObjs({ _type: 'page', name: QUEST_TRACKER_pageName })[0]; + if (!questTreePage) { + errorCheck(40, 'msg', null,`Page "${QUEST_TRACKER_pageName}" not found. Please create the page manually.`); + return; + } + H.adjustPageSettings(questTreePage); + H.clearPageObjects(questTreePage.id, () => { + const questPositions = H.buildDAG(QUEST_TRACKER_globalQuestData, vars); + H.adjustPageSizeToFitPositions(questTreePage, questPositions); + H.buildPageHeader(questTreePage); + QUEST_TRACKER_TreeObjRef = {}; + D.drawQuestConnections(questTreePage.id, questPositions); + D.drawMutuallyExclusiveConnections(questTreePage.id, questPositions); + D.drawQuestTreeFromPositions(questTreePage, questPositions, () => { + D.drawQuestTextAfterGraphics(questTreePage, questPositions); + saveQuestTrackerData(); + }); + }); + }; + const updateQuestText = (questId, newText) => { + if (!QUEST_TRACKER_TreeObjRef[questId] || !QUEST_TRACKER_TreeObjRef[questId].text) return; + const textObjId = QUEST_TRACKER_TreeObjRef[questId].text; + const textObj = getObj('text', textObjId); + if (!textObj) return; + textObj.set('text', newText); + saveQuestTrackerData(); + }; + const updateQuestTooltip = (questId, newTooltip) => { + if (!QUEST_TRACKER_TreeObjRef[questId] || !QUEST_TRACKER_TreeObjRef[questId].avatar) return; + const avatarObjId = QUEST_TRACKER_TreeObjRef[questId].avatar; + const avatarObj = getObj('graphic', avatarObjId); + if (!avatarObj) return; + const trimmedTooltip = H.trimText(newTooltip, 150); + avatarObj.set('tooltip', trimmedTooltip); + saveQuestTrackerData(); + }; + const updateQuestStatusColor = (questId, statusNumber) => { + if (!QUEST_TRACKER_TreeObjRef[questId] || !QUEST_TRACKER_TreeObjRef[questId].rectangle) return; + const rectangleObjId = QUEST_TRACKER_TreeObjRef[questId].rectangle; + const rectangleObj = getObj('path', rectangleObjId); + if (!rectangleObj) return; + const statusName = statusMapping[statusNumber] || 'Unknown'; + const statusColor = H.getStatusColor(statusName); + rectangleObj.set('stroke', statusColor); + D.redrawQuestText(questId); + saveQuestTrackerData(); + }; + const updateQuestVisibility = (questId, makeHidden) => { + if (!QUEST_TRACKER_TreeObjRef[questId]) return; + const pageId = findObjs({ type: 'page', name: QUEST_TRACKER_pageName })[0].id; + if (typeof makeHidden === 'string') makeHidden = makeHidden.toLowerCase() === 'true'; + const targetLayer = makeHidden ? 'gmlayer' : 'map'; + const avatarLayer = makeHidden ? 'gmlayer' : 'objects'; + const textLayer = makeHidden ? 'gmlayer' : 'objects'; + for (const sourceQuestId in QUEST_TRACKER_TreeObjRef) { + const pathsToQuest = QUEST_TRACKER_TreeObjRef[sourceQuestId]?.paths?.[questId]; + if (pathsToQuest) { + pathsToQuest.forEach(segmentId => { + const pathObj = getObj('path', segmentId); + if (pathObj) { + pathObj.set({ + layer: targetLayer, + stroke: makeHidden ? '#CCCCCC' : '#000000' + }); + } + }); + } + } + ['rectangle', 'avatar', 'text'].forEach(element => { + const objId = QUEST_TRACKER_TreeObjRef[questId][element]; + const obj = getObj(element === 'rectangle' ? 'path' : 'graphic', objId); + if (obj) { + const layer = element === 'avatar' ? avatarLayer : textLayer; + obj.set('layer', layer); + } + }); + D.redrawQuestText(questId); + if (!makeHidden) { + saveQuestTrackerData(); + } + }; + return { + buildQuestTreeOnPage, + updateQuestText, + updateQuestTooltip, + updateQuestStatusColor, + updateQuestVisibility + }; + })(); + const Rumours = (() => { + const H = { + getNewRumourId: () => { + const existingRumourIds = []; + Object.values(QUEST_TRACKER_globalRumours).forEach(quest => { + Object.values(quest).forEach(category => { + Object.values(category).forEach(location => { + Object.keys(location).forEach(rumourId => { + const match = rumourId.match(/^rumour_(\d+)$/); + if (match) { + existingRumourIds.push(parseInt(match[1], 10)); + } + }); + }); + }); + }); + const highestRumourNumber = existingRumourIds.length > 0 ? Math.max(...existingRumourIds) : 0; + const newRumourId = `rumour_${highestRumourNumber + 1}`; + return newRumourId; + }, + getNewLocationId: (locationTable) => { + let locationItems = findObjs({ type: 'tableitem', rollabletableid: locationTable.id }); + let maxWeight = locationItems.reduce((max, item) => { + return Math.max(max, item.get('weight')); + }, 0); + let newWeight = maxWeight + 1; + return newWeight; + }, + removeRumours: (locationTable, locationid) => { + const locationObject = findObjs({ type: 'tableitem', rollabletableid: locationTable.id }).find(item => item.get('weight') == locationid); + if (!locationObject) return; + const cleanData = Utils.sanitizeString(locationObject.get('name')).toLowerCase(); + Object.keys(QUEST_TRACKER_globalRumours).forEach(questId => { + const questRumours = QUEST_TRACKER_globalRumours[questId] || {}; + Object.keys(questRumours).forEach(status => { + const statusRumours = questRumours[status] || {}; + if (statusRumours[cleanData]) { + Object.keys(statusRumours[cleanData]).forEach(rumourKey => { + }); + delete statusRumours[cleanData]; + } + }); + }); + Utils.updateHandoutField('rumour'); + calculateRumoursByLocation(); + } + }; + const calculateRumoursByLocation = () => { + let rumoursByLocation = {}; + let questTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTS })[0]; + if (errorCheck(146, 'exists', questTable, `questTable`)) return; + let questItems = findObjs({ type: 'tableitem', rollabletableid: questTable.id }); + if (errorCheck(147, 'exists', questItems, `questItems`)) return; + Object.keys(QUEST_TRACKER_globalRumours).forEach(questId => { + let relevantItem = questItems.find(item => item.get('name').toLowerCase() === questId.toLowerCase()); + if (errorCheck(148, 'exists', relevantItem, `relevantItem for questId: ${questId}`)) return; + let relevantStatus = statusMapping[relevantItem.get('weight').toString()].toLowerCase(); + let questRumours = QUEST_TRACKER_globalRumours[questId] || {}; + if (questRumours[relevantStatus]) { + Object.keys(questRumours[relevantStatus] || {}).forEach(location => { + let locationRumours = questRumours[relevantStatus][location] || {}; + if (!rumoursByLocation[location]) rumoursByLocation[location] = []; + Object.keys(locationRumours).forEach(rumourKey => { + const rumourText = locationRumours[rumourKey]; + rumoursByLocation[location].push(rumourText); + }); + }); + } + }); + QUEST_TRACKER_rumoursByLocation = rumoursByLocation; + saveQuestTrackerData(); + }; + const sendRumours = (locationId, numberOfRumours) => { + let allRumours = []; + let locationTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_LOCATIONS })[0]; + if (errorCheck(41, 'exists', locationTable,`locationTable`)) return; + let locationItems = findObjs({ type: 'tableitem', rollabletableid: locationTable.id }); + let location = locationItems.find(loc => loc.get('weight').toString() === locationId.toString()); + if (errorCheck(42, 'exists', location,`location`)) return; + const normalizedLocationId = Utils.sanitizeString(location.get('name')).toLowerCase(); + if (normalizedLocationId === 'everywhere') { + allRumours = Object.values(QUEST_TRACKER_rumoursByLocation['everywhere'] || {}).map((rumour, index) => `${index}|${Utils.sanitizeInput(rumour, 'STRING')}`); + } else { + const locationRumoursObj = QUEST_TRACKER_rumoursByLocation[normalizedLocationId] || {}; + const everywhereRumoursObj = QUEST_TRACKER_rumoursByLocation['everywhere'] || {}; + const locationRumours = Object.values(locationRumoursObj).map(rumour => Utils.sanitizeInput(rumour, 'STRING')); + const everywhereRumours = Object.values(everywhereRumoursObj).map(rumour => Utils.sanitizeInput(rumour, 'STRING')); + locationRumours.forEach((rumour, index) => { + for (let i = 0; i < 3; i++) { + allRumours.push(`${index}|${rumour}`); + } + }); + everywhereRumours.forEach((rumour, index) => { + allRumours.push(`${locationRumours.length + index}|${rumour}`); + }); + } + if (allRumours.length === 0) { + Utils.sendGMMessage(`No rumours available for this location.`); + return; + } + let selectedRumours = []; + while (selectedRumours.length < numberOfRumours && allRumours.length > 0) { + let randomIndex = Math.floor(Math.random() * allRumours.length); + let selectedRumour = allRumours[randomIndex]; + let [rumourKey, rumourText] = selectedRumour.split('|', 2); + selectedRumours.push(rumourText); + allRumours = allRumours.filter(rumour => !rumour.startsWith(`${rumourKey}|`)); + } + selectedRumours.forEach(rumour => { + Utils.sendDescMessage(rumour); + }); + }; + const getLocationNameById = (locationId) => { + const locationTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_LOCATIONS })[0]; + if (errorCheck(43, 'exists', locationTable,`locationTable`)) return null; + const locationItems = findObjs({ type: 'tableitem', rollabletableid: locationTable.id }); + const locationItem = locationItems.find(item => item.get('weight').toString() === locationId.toString()); + if (errorCheck(44, 'exists', locationItem,`locationItem`)) return null; + return locationItem.get('name'); + }; + const removeAllRumoursForQuest = (questId) => { + if (!QUEST_TRACKER_globalRumours[questId]) return; + Object.keys(QUEST_TRACKER_globalRumours[questId]).forEach(status => { + const statusRumours = QUEST_TRACKER_globalRumours[questId][status] || {}; + Object.keys(statusRumours).forEach(location => { + delete statusRumours[location]; + }); + delete QUEST_TRACKER_globalRumours[questId][status]; + }); + delete QUEST_TRACKER_globalRumours[questId]; + Utils.updateHandoutField('rumour'); + calculateRumoursByLocation(); + }; + const getAllLocations = () => { + let rollableTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_LOCATIONS })[0]; + if (errorCheck(45, 'exists', rollableTable,`rollableTable`)) return []; + const tableItems = findObjs({ _type: 'tableitem', _rollabletableid: rollableTable.id }); + const locations = tableItems.map(item => item.get('name')); + return locations; + }; + const manageRumourLocation = (action, newItem = null, locationid = null) => { + const allLocations = Rumours.getAllLocations(); + let locationTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_LOCATIONS })[0]; + if (errorCheck(46, 'exists', locationTable,`locationTable`)) return; + switch (action) { + case 'add': + if (!newItem) return; + if (allLocations.some(loc => Utils.sanitizeString(loc.toLowerCase()) === Utils.sanitizeString(newItem.toLowerCase()))) return; + const newWeight = H.getNewLocationId(locationTable); + if (newWeight === undefined || newWeight === null) return; + let newLocation = createObj('tableitem', { + rollabletableid: locationTable.id, + name: newItem, + weight: newWeight + }); + break; + case 'remove': + if (!locationid || locationid === 1) return; + let locationR = findObjs({ type: 'tableitem', rollabletableid: locationTable.id }).find(item => item.get('weight') == locationid); + H.removeRumours(locationTable,locationid) + locationR.remove(); + break; + case 'update': + if (allLocations.some(loc => Utils.sanitizeString(loc.toLowerCase()) === Utils.sanitizeString(newItem.toLowerCase())) || Utils.sanitizeString(newItem.toLowerCase()) === 'everywhere') return; + let locationU = findObjs({ type: 'tableitem', rollabletableid: locationTable.id }).find(item => item.get('weight') == locationid); + locationU.set('name', newItem); + break; + } + }; + const manageRumourObject = ({ action, questId, newItem = '', status, location, rumourId = ''}) => { + let locationString = getLocationNameById(location) + const sanitizedLocation = locationString ? Utils.sanitizeString(locationString.toLowerCase()) : ''; + if (!QUEST_TRACKER_globalRumours[questId]) QUEST_TRACKER_globalRumours[questId] = {}; + if (!QUEST_TRACKER_globalRumours[questId][status]) QUEST_TRACKER_globalRumours[questId][status] = {}; + const questRumours = QUEST_TRACKER_globalRumours[questId]; + const statusRumours = questRumours[status]; + switch (action) { + case 'add': + if (!statusRumours[sanitizedLocation]) { + statusRumours[sanitizedLocation] = {}; + } + const newRumourKey = rumourId === '' ? H.getNewRumourId() : rumourId; + statusRumours[sanitizedLocation][newRumourKey] = newItem; + break; + case 'remove': + if (statusRumours[sanitizedLocation] && statusRumours[sanitizedLocation][rumourId]) { + delete statusRumours[sanitizedLocation][rumourId]; + if (Object.keys(statusRumours[sanitizedLocation]).length === 0) { + delete statusRumours[sanitizedLocation]; + } + } + break; + default: + break; + } + Utils.updateHandoutField('rumour'); + calculateRumoursByLocation(); + }; + return { + calculateRumoursByLocation, + sendRumours, + manageRumourLocation, + getLocationNameById, + removeAllRumoursForQuest, + getAllLocations, + manageRumourObject + }; + })(); + const Menu = (() => { + const styles = { + menu: 'background-color: #fff; border: 1px solid #000; padding: 5px; border-radius: 5px; overflow: hidden;', + button: 'margin-top: 1px; display: inline-block; background-color: #000; border: 1px solid #292929 ; border-radius: 3px; padding: 2px; color: #fff; text-align: center;', + buttonDisabled: 'pointer-events: none; background-color: #666; border: 1px solid #292929; border-radius: 3px; padding: 2px; text-align: center; color: #000000;', + spanInline: 'display: inline-block; margin-top: 1px;', + smallButton: 'display: inline-block; width: 12px; height:16px;', + smallButtonMagnifier: 'display: inline-block; width: 16px; height:16px; background-color:#fff;', + smallButtonContainer: 'text-align:center; width: 20px; padding:1px', + smallButtonAdd: 'text-align:right; width: 20px; padding:1px margin-right:1px', + smallerText: 'font-size: smaller', + list: 'list-style none; padding: 0; margin: 0; overflow: hidden;', + label: 'float: left; font-weight: bold;', + topBorder: 'border-top: 1px solid #ddd;', + bottomBorder: 'border-bottom: 1px solid #ddd;', + topMargin: 'margin-top: 20px;', + column: 'overflow: hidden; padding: 5px 0;', + marginRight: 'margin-right: 2px', + strikethrough: 'text-decoration: line-through;', + floatLeft: 'float: left;', + floatRight: 'float: right;', + floatClearRight: 'float: right; clear: right;', + overflow: 'overflow: hidden; margin:1px', + rumour: 'text-overflow: ellipsis;overflow: hidden;width: 165px;display: block;word-break: break-all;white-space: nowrap;', + link: 'color: #007bff; text-decoration: underline; cursor: pointer;', + questlink: 'color: #007bff; text-decoration: none; cursor: pointer; background-color: #FFFFFF;', + filterlink: 'color: #007bff; text-decoration: none; cursor: pointer; background-color: #FFFFFF; padding:0px;', + paddedfilterlink: 'color: #007bff; text-decoration: none; cursor: pointer; background-color: #FFFFFF; padding:5px;', + treeStyle: 'display: inline-block; position: relative; text-align: center; margin-top: 0px;', + questBox50: 'display: inline-block; width: 15px; height: 6px; padding: 5px; border: 1px solid #000; border-radius: 5px; background-color: #f5f5f5; text-align: center; position: relative; margin-right: 20px;', + verticalLineStyle: 'position: absolute; width: 2px; background-color: black;', + lineHorizontalRed: 'position: absolute; width: 24px; height: 2px; background-color: red; left: 57%;', + lineHorizontal: 'position: absolute; height: 2px; background-color: black;', + treeContainerStyle: 'position: relative; width: 100%; height: 100%; text-align: center; margin-top: 20px;', + ulStyle: 'list-style: none; position: relative; padding: 0; margin: 0; display: block; text-align: center;', + liStyle: 'display: inline-block; text-align: center; position: relative;', + spanText: 'bottom: -1px; position: absolute; left: -1px; right: 0px;', + centreImage: 'display: block; margin: auto; text-align: center;' + }; + const H = { + showActiveQuests: () => { + let AQMenu = ""; + const activeStatuses = [2, 3, 4]; + const activeQuests = QUEST_TRACKER_globalQuestArray + .filter(quest => { + const status = parseInt(Quest.getQuestStatus(quest.id), 10); + return activeStatuses.includes(status); + }) + .map(quest => quest.id); + if (activeQuests.length === 0) { + AQMenu += `
| ||||||||||||||||||||||||||||
` : ` | `} + ${H.getQuestName(condition)} + | ++ c + | ++ - + | +|||||||||||||||||||||||||
+ | + Add Relationship + | ++ <${spanOrAnchor} href="!qt-questrelationship currentquest=${questId}|action=add|type=group|groupnum=${currentGroupNum}|quest=${H.buildDropdownString(questId)}" style="${renderButtonStyle} ${styles.smallButton}">+ + | +||||||||||||||||||||||||||
+ Add Relationship + | ++ <${spanOrAnchor} href="!qt-questrelationship currentquest=${questId}|action=add|type=single|quest=${H.buildDropdownString(questId)}" style="${renderButtonStyle} ${styles.smallButton}">+ + | +|||||||||||||||||||||||||||
+ Add Relationship + | ++ <${spanOrAnchor} href="!qt-questrelationship currentquest=${questId}|action=add|type=single|quest=${H.buildDropdownString(questId)}" style="${renderButtonStyle} ${styles.smallButton}">+ + | +|||||||||||||||||||||||||||
+ ${condition.logic} + | ++ c + | ++ - + | +
+ Add Relationship + | ++ <${spanOrAnchor} href="!qt-questrelationship currentquest=${questId}|action=add|type=single|quest=${H.buildDropdownString(questId)}" style="${renderButtonStyle} ${styles.smallButton}">+ + | +||
Add Relationship Group | ++ <${spanOrAnchor} href="!qt-questrelationship currentquest=${questId}|action=add|type=addgroup|quest=${H.buildDropdownString(questId)}" style="${renderButtonStyle} ${styles.smallButton}">+ + | +
+ ${quest.relationships.logic || 'AND'} + | ++ c + | +||
+ Add Relationship Group + | ++ <${spanOrAnchor} href="!qt-questrelationship currentquest=${questId}|action=add|type=addgroup|quest=${H.buildDropdownString(questId)}" style="${renderButtonStyle} ${styles.smallButton}">+ + | +
+ | + <${spanOrAnchor} href="!qt-questrelationship currentquest=${questId}|action=add|type=mutuallyexclusive|quest=${H.buildDropdownString(questId)}" style="${renderButtonStyle} ${styles.smallButton}">+ + | +
Weather | |
${QUEST_TRACKER_CURRENT_WEATHER['weatherType']} | |
Location | |
${H.returnCurrentLocation(QUEST_TRACKER_WeatherLocation)} | Change |
Temperature | ${temperatureDisplay} |
${QUEST_TRACKER_CURRENT_WEATHER['scaleDescriptions']['temperature']} | |
Precipitation | ${precipitationDisplay} |
${QUEST_TRACKER_CURRENT_WEATHER['scaleDescriptions']['precipitation']} | |
Wind | ${windSpeedDisplay} |
${QUEST_TRACKER_CURRENT_WEATHER['scaleDescriptions']['wind']} | |
Humidity | ${humidityDisplay}% |
${QUEST_TRACKER_CURRENT_WEATHER['scaleDescriptions']['humidity']} | |
Cloud Cover | ${cloudCoverDisplay}% |
${QUEST_TRACKER_CURRENT_WEATHER['scaleDescriptions']['cloudCover']} | |
Visibility | ${visibilityDisplay} |
${QUEST_TRACKER_CURRENT_WEATHER['scaleDescriptions']['visibility']} |
`;
+ html += l.buildQuestListHTML(flattenedLogic, columnInstructionsMap, 0);
+ html += `
+
|
There doesn't seem to be any Quests. You need to create a quest or Import from the Handouts.
+ `; + } else { + const filteredQuests = QUEST_TRACKER_globalQuestArray + .map(quest => { + const questData = QUEST_TRACKER_globalQuestData[quest.id]; + if (questData) { + const normalizedData = Object.keys(questData).reduce((acc, key) => { + acc[key.toLowerCase()] = questData[key]; + return acc; + }, {}); + return H.applyFilter(QUEST_TRACKER_FILTER.filter, normalizedData) + ? { ...quest, ...normalizedData } + : null; + } + return null; + }) + .filter(Boolean); + menu += H.renderQuestList(filteredQuests, QUEST_TRACKER_FILTER.groupBy); + } + menu += ` +This menu displays all the rumours currently associated with quests. Use the options below to filter, navigate through locations, and modify rumours.
`; + if (Object.keys(QUEST_TRACKER_globalQuestData).length === 0) { + menu += `There are no quests available. You need to create quests or import from handouts.
`; + } else { + const filteredQuests = Object.keys(QUEST_TRACKER_globalQuestData) + .map(questId => { + const questData = QUEST_TRACKER_globalQuestData[questId] || {}; + const normalizedData = Object.keys(questData).reduce((acc, key) => { + acc[key.toLowerCase()] = questData[key]; + return acc; + }, {}); + if (!H.applyFilter(QUEST_TRACKER_RUMOUR_FILTER.filter, normalizedData)) return null; + const questRumours = QUEST_TRACKER_globalRumours[questId] || {}; + let rumourCount = Object.values(questRumours) + .reduce((sum, statusRumours) => sum + Object.values(statusRumours) + .reduce((locSum, locationRumours) => locSum + Object.keys(locationRumours).length, 0), 0); + return { + id: questId, + name: questData.name || `Quest: ${questId}`, + rumourCount + }; + }) + .filter(Boolean) + .sort((a, b) => a.name.localeCompare(b.name)); + menu += H.renderQuestList(filteredQuests, QUEST_TRACKER_RUMOUR_FILTER.groupBy, 'rumour'); + } + menu += ` +${questData.description}
`; + const questRumours = QUEST_TRACKER_globalRumours[questId] || {}; + const allStatuses = Object.values(statusMapping); + if (allStatuses.length > 0) { + menu += `${status} ${rumourCount} rumour${rumourCount === 1 ? '' : 's'} |
+ + Show + | +
There are no rumours available; either refresh the data, or start adding manually.
+This menu displays all the rumours currently associated with ${questDisplayName} under the status "${statusName}". Use the options below to update, add, or remove rumours.
To add new lines into the rumours use %NEWLINE%. To add in quotation marks you need to use ".
Error: Locations table not found. Please check if the table exists in the game.
+${trimmedRumourText} | +
+ ![]() |
+ + c + | ++ - + | +
No rumours | |||
+ | + + + | +
${quest.description || 'No description available.'}
+ + Edit Title + + Edit Description + +Error: Locations table not found. Please check if the table exists in the game.
Error: Quest Groups table not found. Please check if the table exists in the game.
There doesn't seem to be any Events, you need to create a quest or Import from the Handouts.
+ `; + } else { + menu += `${event.description || 'No description available.'}
+ + Edit Event Name + + Edit Description + +This menu displays all the triggers currently associated with quests, dates, and reactions.
`; + if (allTriggers.length === 0) { + menu += `No triggers found. Click 'Add Trigger' to create one.
`; + } else { + menu += `+ | Quest | +${effectQuestName} | +
+ | Type | +${effectType} | +
+ | Value | +${effect.type !== null ? `${effectText}` : `${effectText}`} | +
Delete | ++ | |
Add Effect |
Active | +${enabled ? 'Enabled' : 'Disabled'} | +
Trigger Type | +${capitalizedType} | +
Triggering Event | +${activationSection} | +
v2.12 13/05/2024Attack Definitions Database
Change Log:
Warrior Fighting Stylesv1.02 23/11/2022
Change Log:v1.02 30/11/2022 Initial release database
v1.11 20/12/2024Race Database
Change Log:v1.11 20/12/2024 Changed {{name=...}} to {{title=...}}
v1.02 12/01/2025NPC Database
Change Log:v1.02 12/01/2025 A few corrections & typos fixed
v2.05 26/01/2025Creatures Database
Change Log:v2.05 26/01/2025 Added chance of random items to be added to humanoid Drag & Drop creatures
v2.05 26/01/2025Creatures Database
Change Log:v2.05 26/01/2025 Added chance of random items to be added to humanoid Drag & Drop creatures
v2.04 26/01/2025Creatures Database
Change Log:v2.04 26/01/2025 Added chance of random items to be added to humanoid Drag & Drop creatures
v2.05 27/01/2025Creatures Database
Change Log:v2.05 26/01/2025 Added chance of random items to be added to humanoid Drag & Drop creatures
v2.03 27/01/2025Creatures Database
Change Log:v2.04 26/01/2025 Added chance of random items to be added to humanoid Drag & Drop creatures
Lock & Trap Macrosv1.02 26/01/2025
Change Log:v1.02 27/01/2025 Moved chest to this DB & added random items to containers
Lock & Trap Macrosv1.10 07/06/2024
Change Log:
v2.09 20/12/2024Character Class Database
Change Log:
v1.01 30/12/2024Character Class Database
Change Log:
Armour and Shieldsv7.01 26/01/2025
Change Log:v7.01 26/01/2025 Updated with multiple changes for v4 RoundMaster APIs
Weapons Databasev7.01 26/01/2025
Change Log:v7.01 26/01/2025 Updated with multiple changes for v4 RoundMaster APIs
Weapons Databasev7.01 26/01/2025
Change Log:v7.01 26/01/2025 Updated with multiple changes for v4 RoundMaster APIs
Weapons Databasev7.01 26/01/2025
Change Log:
Weapons Databasev7.01 26/01/2025
Change Log:v7.01 26/01/2025 Updated with multiple changes for v4 RoundMaster APIs
v7.01 26/01/2025Weapons Database
Change Log:v7.01 26/01/2025 Updated with multiple changes for v4 RoundMaster APIs
v7.01 26/01/2025Equipment Database
Change Log:v7.01 26/01/2025 Updated with multiple changes for v4 RoundMaster APIs
Potions, Pills and Oilsv7.01 26/01/2025
Change Logv7.01 26/01/2025 Updated with multiple changes for v4 RoundMaster APIs
Ringsv7.01 26/01/2025
Change Log:
Scrolls & Spellbooksv7.01 26/01/2025
Change Log:
Wands, Staves & Rodsv7.01 26/01/2025
Change Log:
Miscellaneous Itemsv7.01 26/01/2025
Change Log
Custom Magic Itemsv7.01 26/01/2025
Change Logv7.01 26/01/2025 Updated with multiple changes for v4 RoundMaster APIs
Custom Magic Itemsv1.01 30/12/2024
Change Logv1.01 30/12/2024 Initial creation for testing', + root:'MI-DB', + api:'magic', + type:'mi', + avatar:'https://files.d20.io/images/338373876/hlaDwE1SLh2XNXB8EoVexA/max.jpg?1682104357', + version:1.01, + db:[{name:'Blue-Tomes',type:'treasure',ct:'0',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.defaultTemplate+'}{{name=Blue Leather-Bound Tomes}}Specs=[Blue Tomes,Treasure,1H,Tomes]{{subtitle=Tomes}}MiscData=[w:Blue Tomes,sp:0,rc:uncharged]{{desc=Set of five books, bound in sky-blue leather and trimmed in copper, with contents of a sinister nature. These five tomes have old, fragile pages; the ancient books describe procedures and details for several evil rites and ceremonies. The books make grim and harrowing reading for any character.}}'}, + {name:'Bottled-Anemone',type:'miscellaneous',ct:'0',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Anemone in a Glass Bottle}}{{subtitle=Treasure}}Specs=[Bottled Anemone,Miscellaneous,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Bottled Anemone,sp:0,st:Glass Bottle,gp:0,rc:uncharged]{{Size=Medium}}{{Immunity=None}}{{Saves=None}}{{desc=A glass bottle with an anemone trapped inside}}'}, + {name:'Broach',type:'treasure',ct:'0',charge:'single-uncharged',cost:'0',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Broach}}{{subtitle=Item}}Specs=[Broach,Treasure,1H,Item]{{Speed=[[0]]}}MiscData=[w:Broach,sp:0,st:Broach,rc:single-uncharged]{{Size=Medium}}{{Immunity=None}}{{Saves=None}}{{desc=What appears to be a perfectly normal broach, made out of some shiny metal with a pretty design stamped on the front - quite well made...}}'}, + {name:'Conch-Shell',type:'treasure',ct:'0',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Conch}}{{subtitle=Item}}Specs=[Shell,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Conch,sp:0,rc:uncharged]{{Size=Medium}}{{Immunity=None}}{{Saves=None}}{{desc=What appears to be a perfectly normal shell, made out of shell}}'}, + {name:'Copper-buckled-leather-harness',type:'treasure',ct:'0',charge:'uncharged',cost:'1',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Leather Harness with Copper Buckles}}{{subtitle=Treasure}}Specs=[Leather Harness,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Leather Harness,sp:0,st:Leather Harness,gp:1,rc:uncharged]{{Size=Medium}}{{Immunity=None}}{{Saves=None}}{{desc=A leather harness with copper buckles that will soon wear out, perhaps worth as much as 1gp if you can get someone to pay that much.}}'}, + {name:'Coral-Game-Pieces',type:'treasure',ct:'0',charge:'uncharged',cost:'10',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Coral Game Pieces}}{{subtitle=Treasure}}Specs=[Coral Game Pieces,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Coral Game Pieces,sp:0,gp:10,rc:uncharged]{{Size=Tiny}}{{Immunity=None}}{{Saves=None}}{{desc=A set of 20 coral game tokens for some game played by the Sahuagin officers. Perhaps you can interrogate one to demand the rules of the game?}}'}, + {name:'Coral-Shark-Statuette',type:'treasure',ct:'0',charge:'uncharged',cost:'20',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Coral Shark Statuette}}{{subtitle=Treasure}}Specs=[Coral Shark Statuette,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Coral Shark Statuette,sp:0,gp:20,rc:uncharged]{{Size=Small}}{{Immunity=None}}{{Saves=None}}{{desc=A small statuette of a shark made out of coral, and of reasonably fine quality. Perhaps someone would pay up to 20gp for such an item?}}'}, + {name:'Gold+Coral-Necklace',type:'treasure',ct:'0',charge:'uncharged',cost:'175',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Gold and Coral Necklace}}Specs=[Gold Necklace,Treasure,0H,Treasure]{{desc=A gold necklace set with coral beads, worth about 175gp (or whatever someone will give you for it)}}MiscData=[w:Gold+Coral Necklace,gp:175,wt:0.1,rc:uncharged]{{}}'}, + {name:'Gold+Coral-Ring',type:'treasure',ct:'0',charge:'uncharged',cost:'50',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Gold Ring set with Coral}}{{subtitle=Treasure}}Specs=[Ring,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Ring,sp:0,st:Ring,gp:50,rc:uncharged]{{Size=Small}}{{Immunity=None}}{{Saves=None}}{{desc=What appears to be a gold ring set with coral, perhaps worth in the region of 50gp if you can get someone to pay that much.}}'}, + {name:'Gold-Buckle',type:'treasure',ct:'0',charge:'uncharged',cost:'10',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Gold Buckle}}Specs=[Gold,Treasure,0H,Treasure]{{desc=A buckle for a harness or belt, made of gold, and worth about 10gp (or whatever someone will give you for it)}}MiscData=[w:Gold Buckle,gp:10,wt:0.1,rc:uncharged]{{}}'}, + {name:'Gold-Drop-Earring',type:'treasure',ct:'0',charge:'uncharged',cost:'30',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Gold Drop Earring}}{{subtitle=Treasure}}Specs=[Earring,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Earring,sp:0,st:Earring,gp:30,rc:uncharged]{{Size=Small}}{{Immunity=None}}{{Saves=None}}{{desc=What appears to be a gold drop earring, not particularly fancy but reasonably well made. Might be worth something, but would be best i you have a pair.}}'}, + {name:'Gold-and-Diamond-Wristband',type:'treasure',ct:'0',charge:'uncharged',cost:'250',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Gold \\amp Diamond Wristband}}{{subtitle=Treasure}}Specs=[Wristband,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Wristband,sp:0,st:Wristband,gp:250,rc:uncharged]{{Size=Small}}{{Immunity=None}}{{Saves=None}}{{desc=A rather flashy gold \\amp diamond wristband, to be worn on the most formal of occasions. Might be worth as much as 250gp}}'}, + {name:'Gold-and-Pearl-Band',type:'treasure',ct:'0',charge:'uncharged',cost:'200',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Gold \\amp Pearl Band}}Specs=[Gold and Pearl Band,Treasure,0H,Bracelet]{{desc=A gold bracelet set with pearls, perhaps worth about 200gp (or whatever someone will give you for it)}}MiscData=[w:Gold Bracelet,gp:200,wt:0.3,rc:uncharged]{{}}'}, + {name:'Gold-locket',type:'treasure',ct:'0',charge:'uncharged',cost:'50',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Small Gold Locket}}Specs=[Gold,Treasure,0H,Treasure]{{Looks Like=A small locket, made of gold and on a fine gold chain}}MiscData=[w:Gold Locket,gp:50,wt:0.1,rc:uncharged]{{desc=Perhaps worth as much as 50gp. When opened, it is seen to contain a miniature portrait of a human girl and a lock of blonde hair, which floats away into the surrounding water}}'}, + {name:'Golden-Gong+Striker',type:'treasure',ct:'0',charge:'uncharged',cost:'75',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Golden Gong}}Specs=[Gong,Treasure,0H,Treasure]{{desc=A mall golden gong with a gold striker, worth about 75gp (or whatever someone will give you for it)}}MiscData=[w:Gong,gp:75,wt:0.2,rc:uncharged]{{}}'}, + {name:'Good-Grog-Guide',type:'treasure',ct:'0',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.itemTemplate+'}{{name=The Good Grog Guide}}Specs=[Tome,Treasure,1H,Treasure]{{desc=A very thick book, well thumbed and with some stains and water(?) marks, this guide is an invaluable reference for the discerning quoffer of any form of grog, beer, spirit, meade, cider, wine, etc in the kingdoms of Keoland, Silverdon, and surrounding kingdoms. It has expert reviews of the standard of tipple served in establishments, views on the establishment and its proprietors, the types of clientelle that might be found there (e.g. which to avoid or frequent, depending on your needs), prices for grog and other beverages (and also for accommodation and food - should these be of any interest), and whether the local law enforcement are strict or relaxed about "trade" in these places.\nWhat is interesting is that *it never seems to be out of date!* It seems to continually update itself with new reviews and information... How it does this is a mystery.}}'}, + {name:'Hazy-Mirror',type:'treasure',ct:'0',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Hazy Mirror}}{{subtitle=Treasure}}Specs=[Mirror,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Mirror,sp:0,st:Mirror,gp:0,rc:uncharged]{{Size=Small}}{{Immunity=None}}{{Saves=None}}{{Looks Like=A mirror that only gives a poor, hazy reflection. Looks very ordinary.}}{{desc=What appears to be a perfectly normal but poor quality mirror. You might get a few coppers for it}}'}, + {name:'Holy-Symbol-of-Erythnul',type:'miscellaneous',ct:'0',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Holy Symbol of Erythnul}}Specs=[Holy Symbol,Miscellaneous,1H,Treasure]{{desc=A Holy Symbol, fashioned in the shape of a human skull pierced through by a sword, and made of some black stone. It might be worth about 5gp, though selling it might bring suspicion on the party. Might it be more useful when used to gain favour with priests of that god of slaughter if you come across them?}}'}, + {name:'Holy-Symbol-of-Procan',type:'miscellaneous',ct:'0',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Holy Symbol of Procan}}Specs=[Holy Symbol,Miscellaneous,1H,Treasure]{{desc=A Holy Symbol, fashioned in the shape of a minature trident, and made of guilded silver. It is worth about 15gp, but might it be more useful when used to gain favour with priests of that god of the sea?}}'}, + {name:'Ornate-Leather-Harness',type:'treasure',ct:'0',charge:'uncharged',cost:'75',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Ornate Leather Harness}}{{subtitle=Treasure}}Specs=[Leather Harness,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Leather Harness,sp:0,st:Leather Harness,gp:75,rc:uncharged]{{Size=Medium}}{{Immunity=None}}{{Saves=None}}{{desc=A leather harness adorned with small rubies and platinum buckles, perhaps worth in the region of 75gp if you can get someone to pay that much.}}'}, + {name:'Ozymandius-Medallion',type:'miscellaneous',ct:'0',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.defaultTemplate+'}{{name=Ozymandius\' Medallion}}Specs=[Medallion,Miscellaneous,1H,Medallion]{{subtitle=Jewellery}}MiscData=[w:Medallion,sp:0,rc:uncharged]{{desc=Just seems to be a normal medallion, made of gold with a curious pattern enscribed on both sides. The design is not familliar.}}'}, + {name:'Pearl',type:'treasure',ct:'0',charge:'uncharged',cost:'10',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Pearl}}Specs=[Pearl,Treasure,0H,Pearl]{{desc=A seemingly normal pearl, perhaps worth about 10gp (or whatever someone will give you for it)}}MiscData=[w:Pearl,gp:10,wt:0.1,rc:uncharged]{{}}'}, + {name:'Pearl-Necklace',type:'treasure',ct:'0',charge:'uncharged',cost:'500',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Pearl Necklace}}{{subtitle=Treasure}}Specs=[Necklace,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Coronet,sp:0,st:Necklace,gp:500,rc:uncharged]{{Size=Small}}{{Immunity=None}}{{Saves=None}}{{desc=A high-quality string-of-pearls necklace. Worth what someone will pay, but could perhaps fethc 500gp}}'}, + {name:'Platinum-Armband',type:'treasure',ct:'0',charge:'uncharged',cost:'200',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Platinum Armband}}{{subtitle=Treasure}}Specs=[Armband,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Armband,sp:0,st:Armband,gp:200,rc:uncharged]{{Size=Small}}{{Immunity=None}}{{Saves=None}}{{desc=What appears to be a perfectly normal armband (apart from being made of platinum) worth perhaps 200gp or what anyone will pay for it - quite well made...}}'}, + {name:'Platinum-Buckle',type:'treasure',ct:'0',charge:'uncharged',cost:'50',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Platinum Buckle}}Specs=[Platinum,Treasure,0H,Treasure]{{desc=A buckle for a harness or belt, made of platinum, and worth about 50gp (or whatever someone will give you for it)}}MiscData=[w:Platinum Buckle,gp:50,wt:0.1,rc:uncharged]{{}}'}, + {name:'Platinum-and-Pearl-Coronet',type:'treasure',ct:'0',charge:'uncharged',cost:'700',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Platinum and Pearl Coronet}}{{subtitle=Treasure}}Specs=[Coronet,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Coronet,sp:0,st:Coronet,gp:700,rc:uncharged]{{Size=Small}}{{Immunity=None}}{{Saves=None}}{{desc=An elegant coronet made of platinum and studded with pearls. It might be of sea elf design, and could fetch as much as 700gp in the right market}}'}, + {name:'Ruby',type:'treasure',ct:'0',charge:'uncharged',cost:'100',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Ruby}}Specs=[Gem,Treasure,0H,Treasure]{{desc=A well-cut ruby, worth about 100gp (or whatever someone will give you for it)}}MiscData=[w:Ruby,gp:100,wt:0.1,rc:uncharged]{{}}'}, + {name:'Sailor-Doll',type:'treasure',ct:'0',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Sailor Doll}}Specs=[Sailor Doll,Treasure,1H,Abjuration]{{subtitle=Magical Item}}MiscData=[w:Sailor Doll,sp:0,rc:uncharged]{{desc=A porcelain doll costumed to look like a sailor, and looking somewhat creepy. The doll\'s eyes are made of two pieces of jade, which might each be worth 10gp. But perhaps the doll as a whole might be more useful...}}'}, + {name:'Scary-Doll',type:'treasure',ct:'0',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Scary Doll with Jade Eyes}}Specs=[Doll,Treasure,1H,Treasure]{{desc=A doll found in the attic of The Haunted House, east of Saltmarsh. Dolls always seem a bit scary to many people, but this one\'s totally black eyes and something about it\'s demeanour give it a very creepy feeling.\nAs yet, it seems to be inert and "just a doll". But will it remain so?}}'}, + {name:'Scented-Oil',type:'potion',ct:'2+1d6',charge:'charged',cost:'0',body:'\\amp{template:'+fields.potionTemplate+'}{{title=Scented Oil}}{{splevel=Potion}}{{school=Alteration}}Specs=[Scented Oil,Potion,1H,Alteration]{{components=M}}{{time=1+1d6+1}}PotionData=[sp:2+1d6,rc:charged]{{range=User}}{{duration=4+1d4 Days}}{{aoe=[30 yds](!rounds --aoe @{selected|token_id}|circle|yards|0|30|30|light|true)}}{{save=None}}{{effects=Don\'t you smell wonderful! Everyone near you starts to smell a strange odour...}}{{materials=Potion}}'}, + {name:'Shark-Statue',type:'treasure',ct:'0',charge:'uncharged',cost:'0.01',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Shark Statue}}Specs=[Shark Statue,Treasure,0H,Treasure]{{desc=A 1-foot high wooden statue of a shark, worth whatever someone will give you for it}}MiscData=[w:Shark Statue,gp:0.01,wt:0.1,rc:uncharged]{{}}'}, + {name:'Shark-Statuette',type:'treasure',ct:'0',charge:'uncharged',cost:'200',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Shark Statuette}}{{subtitle=Treasure}}Specs=[Statuette,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Statuette,sp:0,gp:200,rc:uncharged]{{Size=Small}}{{Immunity=None}}{{Saves=None}}{{desc=A statuette of a shark made from gold, perhaps worth in the region of 200gp if you can get someone to pay that much.}}'}, + {name:'Shark-Tooth',type:'treasure',ct:'0',charge:'uncharged',cost:'1',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Shark Tooth}}Specs=[Shark Tooth,Treasure,0H,Treasure]{{desc=A shark tooth about 4 inches long, worth whatever someone will give you for it}}MiscData=[w:Shark Tooth,gp:1,wt:0.1,rc:uncharged]{{}}'}, + {name:'Silver-Bowl',type:'treasure',ct:'0',charge:'uncharged',cost:'5',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Silver Bowl}}Specs=[Silver Bowl,Treasure,0H,Treasure]{{desc=A silver bowl, highly polished but with signs of regular use, perhaps worth about 5gp (or whatever someone will give you for it)}}MiscData=[w:Silver Bowl,gp:5,wt:0.2,rc:uncharged]{{}}'}, + {name:'Silver-Bracelet-with-Turquoise-Beads',type:'treasure',ct:'0',charge:'uncharged',cost:'100',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Silver Bracelet}}Specs=[Silver Bracelet,Treasure,0H,Bracelet]{{desc=A silver bracelet set with turquoise beads, perhaps worth about 100gp (or whatever someone will give you for it)}}MiscData=[w:Silver Bracelet,gp:100,wt:0.2,rc:uncharged]{{}}'}, + {name:'Silver-Buckle',type:'treasure',ct:'0',charge:'uncharged',cost:'5',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Silver Buckle}}Specs=[Silver,Treasure,0H,Treasure]{{desc=A buckle for a harness or belt, made of silver, and worth about 5gp (or whatever someone will give you for it)}}MiscData=[w:Silver Buckle,gp:5,wt:0.1,rc:uncharged]{{}}'}, + {name:'Silver-Cup',type:'treasure',ct:'0',charge:'uncharged',cost:'5',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Silver Cup}}Specs=[Silver Cup,Treasure,0H,Treasure]{{desc=A silver cup, highly polished but with signs of regular use, perhaps worth about 5gp (or whatever someone will give you for it)}}MiscData=[w:Silver Cup,gp:5,wt:0.2,rc:uncharged]{{}}'}, + {name:'Silver-Framed-Picture',type:'treasure',ct:'0',charge:'uncharged',cost:'25',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Silver Framed Picture}}{{subtitle=Treasure}}Specs=[Picture Frame,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Picture Frame,sp:0,gp:25,rc:uncharged]{{Size=Medium}}{{Immunity=None}}{{Saves=None}}{{desc=A silver frame holding a portrait of a loved one painted in oil. The silver frame is perhaps worth in the region of 25gp if you can get someone to pay that much.}}'}, + {name:'Silver-Goblet',type:'treasure',ct:'0',charge:'uncharged',cost:'50',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Silver Goblet}}Specs=[Silver Goblet,Treasure,0H,Goblet]{{desc=A silver goblet which bears the insignia of Prince Monmurg—a spire rising against a blue ocean sky pressed into the bottom; stylized lightning bolts are engraved on the sides, and the words “Jupiter,” “Maximus,” and “Optimus” are written underneath the bolts. Perhaps worth about 50gp (or whatever someone will give you for it)}}MiscData=[w:Silver Goblet,gp:50,wt:0.3,rc:uncharged]{{}}'}, + {name:'Silver-Shark-Mask',type:'treasure',ct:'0',charge:'uncharged',cost:'50',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Silver Shark Masks}}Specs=[Silver Mask,Treasure,0H,Treasure]{{desc=A silver mask in the shape of a shark\'s head, and worth about 50gp (or whatever someone will give you for it)}}MiscData=[w:Silver MaskBuckle,gp:50,wt:0.3,rc:uncharged]{{}}'}, + {name:'Silver-and-Pearl-Band',type:'treasure',ct:'0',charge:'uncharged',cost:'25',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Silver \\amp Pearl Band}}Specs=[Silver and Pearl Band,Treasure,0H,Bracelet]{{desc=A silver bracelet set with pearls, perhaps worth about 25gp (or whatever someone will give you for it)}}MiscData=[w:Silver Bracelet,gp:25,wt:0.2,rc:uncharged]{{}}'}, + {name:'Silver-signet-ring',type:'ring',ct:'0',charge:'uncharged',cost:'10',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Silver Signet Ring}}{{subtitle=Treasure}}Specs=[Signet Ring,Ring,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Signet Ring,sp:0,st:Ring,gp:10,rc:uncharged]{{Size=Tiny}}{{Immunity=None}}{{Saves=None}}{{desc=A silver ring bearing the signet of the Prince of Monmurg—a spire rising against a blue ocean sky.}}'}, + {name:'Skull',type:'treasure',ct:'0',charge:'uncharged',cost:'0.01',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Humanoid Skull}}Specs=[Humanoid Skull,Treasure,0H,Skull]{{desc=A normal, man-sized humanoid skull, worth whatever someone will give you for it. There may be gems forced into its eye sockets (if there are, they are listed separately}}MiscData=[w:Skull,gp:0.01,wt:0.3,rc:uncharged]{{}}'}, + {name:'Small-Silver-Mirror',type:'treasure',ct:'0',charge:'uncharged',cost:'25',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Small Silver Mirror}}Specs=[Mirror,Treasure,0H,Mirror]{{desc=A small silver mirror, worth about 25gp (or whatever someone will give you for it)}}MiscData=[w:Small Silver Mirror,gp:25,wt:0.1,rc:uncharged]{{}}'}, + {name:'Uncut-Turquoise',type:'treasure',ct:'0',charge:'uncharged',cost:'10',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Uncut Turquoise Chunk}}Specs=[Turquoise,Treasure,0H,Treasure]{{desc=A chunk of uncut turquoise, perhaps worth about 10gp (or whatever someone will give you for it)}}MiscData=[w:Turquoise,gp:10,wt:0.2,rc:uncharged]{{}}'}, + ]}, + + MU_Spells_DB_L1:{bio:'
Magic User Spell Database: Level 1v8.04 26/01/2025
Change Log:v8.04 26/01/2025 Updated for greyed-out buttons to work properly
Magic User Spell Database: Level 2v8.03 26/01/2025
Change Log:v8.03 26/01/2025 Updated for greyed-out buttons to work properly
Magic User Spell Database: Level 3v8.04 26/01/2025
Change Log:
Magic User Spell Database: Level 4v8.04 26/01/2025
Change Log:v8.04 26/01/2025 Updated for greyed-out buttons to work properly
Magic User Spell Database: Level 5v8.04 26/01/2025
Change Log:v8.04 26/01/2025 Updated for greyed-out buttons to work properly
Magic User Spell Database: Level 6v8.03 26/01/2025
Change Log:v8.03 26/01/2025 Updated for greyed-out buttons to work properly
Magic User Spell Database: Level 7v8.03 26/01/2025
Change Log:v8.03 26/01/2025 Updated for greyed-out buttons to work properly
Magic User Spell Database: Level 8v8.04 26/01/2025
Change Log:v8.04 26/01/2025 Updated for greyed-out buttons to work properly
Magic User Spell Database: Level 9v8.03 26/01/2025
Change Log:v8.03 26/01/2025 Updated for greyed-out buttons to work properly
Magic User Spell Databasev8.03 26/01/2025
Change Log:v8.03 26/01/2025 Updated for greyed-out buttons to work properly
Priest Spell Databasev8.03 26/01/2025
Priest Spell Databasev8.03 26/01/2025
Priest Spell Databasev8.04 26/01/2025
Priest Spell Databasev8.03 26/01/2025
Priest Spell Databasev8.03 26/01/2025
Priest Spell Databasev8.04 26/01/2025
Priest Spell Databasev8.03 26/01/2025
Powers Databasev7.07 26/01/2025
Change Log:
The RPGMaster APIs use a number of databases to hold Macros defining character classes, spells, powers and magic items and their effects. Previous versions of the RPGMaster series of APIs held their databases all externally as character sheets: from this version onwards this is not the case for databases supplied with the APIs, which are now held internally to the APIs. However, the AttackMaster or MagicMaster API command --extract-db can be used to extract any or all standard databases to Character Sheets for examination and update. The APIs are distributed with many class, spell, power & magic item definitions, and DMs can add their own character classes, spells, items, weapons, ammo and armour to additional databases in their own database character sheets, with new definitions for database items held in Ability Macros. Additional database character sheets should be named as follows:
' + +'Wizard Spells: | additional databases: MU-Spells-DB-[added name] where [added name] can be replaced with anything you want. |
---|---|
Priest Spells: | additional databases: PR-Spells-DB-[added name] where [added name] can be replaced with anything you want. |
Powers: | additional databases: Powers-DB-[added name] where [added name] can be replaced with anything you want. |
Weapons: | additional databases: MI-DB-Weapons-[added name] where [added name] can be replaced with anything you want. |
Ammo: | additional databases: MI-DB-Ammo-[added name] where [added name] can be replaced with anything you want. |
Armour: | additional databases: MI-DB-Armour-[added name] where [added name] can be replaced with anything you want. |
Magic Items: | additional databases: MI-DB-[added name] where [added name] can be replaced with anything you want. |
Character Classes: | additional databases: Class-DB-[added name] where [added name] can be replaced with anything you want. |
Character Races: | additional databases: Race-DB-[added name] where [added name] can be replaced with anything you want. |
Attack Calculations: | additional databases: Attacks-DB-[added name] where [added name] can be replaced with anything you want. |
Fighting Styles: | additional databases: Styles-DB-[added name] where [added name] can be replaced with anything you want. |
Locks & Traps: | additional databases: Locks-Traps-DB-[added name] where [added name] can be replaced with anything you want. |
However: the system will ignore any database with a name that includes a version number of the form "v#.#" where # can be any number or group of numbers e.g. MI-DB v2.13 will be ignored. This is so that the DM can version control their databases, with only the current one (without a version number) being live.
' + +'There can be as many additional databases as you want. Other Master series APIs come with additional databases, some of which overlap - this does not cause a problem as version control and merging unique macros is managed by the APIs.
' + +'Important Note: all Character Sheet databases must have their \'ControlledBy\' value (found under the [Edit] button at the top right of each sheet) set to \'All Players\'. This must be for all databases, both those provided (set by the API) and any user-defined ones. Otherwise, Players will not be able to run the macros contained in them.
' + +'Important Note: databases extracted using the !magic --extract-db command will be able to be edited, but will also slow the system down - the versions held internally in the APIs are much faster for the system to access. Once any extracted database has been examined, it is best to delete them and use the !magic --check-db to re-index the databases so the system operates as fast as possible.
' + +'Each added database has a similar structure, with:
' + +'However, as with all other Databases in the RPGMaster Suite of APIs, if the Ability Macros are correctly set up using the formats detailed in the Help Documentation, the MagicMaster API command !magic --check-db database-name will check the database and set up all other aspects for you, including the correct Custom Attributes and List entries.
' + +'Ability Macros can be whatever the DM wants and can be as simple or as complex as desired. Roll Templates are very useful when defining class, spell, power and magic item ability macros, and are an essential part of Attack Templates. When a Player or an NPC or Monster makes an attack, the AttackMaster API runs the relevant Ability Macro from the databases as if it had been run by the Player from the chat window. All Roll20 functions for macros are available.
' + +'If you want to replace any Ability Macro provided in any of the databases, you can do so simply by creating an Ability Macro in one of your own databases (a database with the same root name) with the Ability Macro you create having exactly the same name as the provided item to be replaced. The API gives preference to Ability Macros in user-defined databases, so yours will be selected in preference to the one provided with the APIs.
' + +'Roll20 provides many excellent maths functions for commands made to the chat window and contained in API button strings. However, it is not always possible to use the Roll20 maths using the [[...]] syntax to achieve what you want. RPGMaster provides an alternative set of maths functions to help resolve these issues. Formulas can be entered for many numeric values required by RPGMaster commands using the supported syntax. However: this syntax does not work for anything other than RPGMaster commands as of writing (this might be a future develpment).
' + +'The square brackets [[...]] are not required. The syntax follows normal maths presedent with a few additional operators to support range calculations and dice rolls:
' + +'+-*/ | The standard maths operators work as expected |
---|---|
(...) | Parentheses can be used to define the order of calculation as normal |
^(#,#,#,...) | This will resolve to the maximum value in the list, and each # can also be a calculation (semi-colons can be used instead of commas) |
v(#,#,#,...) | This will resolve to the minimum value in the list, and each # can also be a calculation (semi-colons can be used instead of commas) |
c(...) | This will resolve to the ceiling (the number rounded up) of the result of the contained calculation |
f(...) | This will resolve to the floor (the number rounded down) of the result of the contained calculation |
#d#r# | Dice roll specifications can be included in the maths with optional reroll values anywhere in the calculation, and the numbers can be calculations |
#:# | A different feature is the range calculation - this will derive a number in the range between the two numbers (inclusive), but will try to do so using the equivalent to 3 dice if possible - e.g. 3:18 would make the equivalent of rolling 3d6, 7:34 will resolve to 4+(3d10), 7:35 will resolve to 4+1d11+2d10. A range can be used anywhere in the calculation, and the numbers can themselves be calculations |
All race, class, and magic item definitions (including those for weapons, ammunition & armour) can inherit data specifications and text from other similar magic item definitions. For example, the "Ring of Protection+2" is very similar to the "Ring of Protection+1" described in 2.2 above, and can inherit most of its specification from there:
' + +'&{template:RPGMring}{{}}Specs=[Ring of Protection,Protection Ring,1H,Abjuration-Protection,Ring-of-Protection+1]{{}}ACData=[a:Ring of Protection+2,+:2,svsav:2,w:Ring of Protection+2]{{}}%{MI-DB|Ring-of-Protection+1}{{name=+2}}{{Protection=+[[2]] on AC}}{{Saves=+[[2]] on saves}}
' + +'Inheritance comes in two forms: data inheritance and text inheritance.
' + +'Data Inheritance: an optional 5th parameter can be added to the Specs section, and RPGMaster will look for an item of that name (but only in the same root database tree): if not provided the 4th parameter will be used in the same way. If an item of that name is found (e.g. in this case "Ring-of-Protection+1") the data in data sections of the same name (e.g. "ACdata=") will be merged - data provided in the inheriting item (in this case "Ring-of-Protection+2") will take priority over inherited data (e.g. svsav:2 will override the inherited svsav:1). This inheritance can be nested as desired - the item being inherited from (the "parent item") can iteself inherit from another (the "grand-parent item").
' + +'Text Inheritance: If you are familiar with Roll20 ability macro programing, you will recognise the syntax %{char-name|ability-name} to insert the text of an ability macro into the chat window or another ability macro. RPGMaster item definitions do something similar but important to note not exactly the same! Instead of using the "char-name" a database name or database root name is given (e.g. "MI-DB-Weapons" or just "MI-DB") followed by the pipe \'|\' and the inherited item name. This will search both character sheet databases and databases held in memory - even if a specific database name is given, if not found in a database of that name all databases of the same root will be searched (e.g. if MI-DB-Rings is specified and the item not found there, all MI-DB databases will be searched). Note: under Text Inheritance data sections will not be merged (unless Data Inheritance is also used). Roll Template sections with the same name later in the merged definition take precidence (e.g. in this example the Roll Template section {{Protection=+[[2]] on AC}} will override that from the Ring-of-Protection+1 because it comes after the %{...|...} in the Ring-of-Protection+2 "child" item definition).
' + +'Defining Parent / Child item inheritance in this way can make the databases much smaller, allow simpler maintenance of common inherited data attributes, and cause less typing!
'; + + const General_API_Help ='The syntax of the Roll20 Roll Query has been extended within the RPGMaster APIs to support RPGMaster API commands with Roll Queries that the GM is invited to answer, rather than the player, regardless of who issued the command. The standard syntax and the extended syntax is shown below:
' + +'Standard Syntax: ?{Query text|option1|option2|...}' + +'
' + +'Extended syntax: gm{Query text/option1/option2/...}
When used in a RPGMaster API command, the extended Roll Query will prompt the GM with a button in the Chat Window for the GM to answer the question posed by the query text. The result will be fed into the action taken by the API command. This allows the GM to be involved when, for instance, a Staff of the Magi absorbs levels of spells cast at a character that the character & player can\'t know.
' + +'When a command is sent to Roll20 APIs / Mods, Roll20 tries to work out which player or character sent the command and tells the API its findings. The API then uses this information to direct any output appropriately. However, when it is the API itself that is sending commands, such as from a {{successcmd=...}} or {{failcmd=...}} sequence in a RPGMdefault Roll Template, Roll20 sees the API as the originator of the command and sends output to the GM by default. This is not always the desired result.
' + +'To overcome this, or when output is being misdirected for any other reason, a Controlling Player Override Syntax (otherwise known as a SenderId Override) has been introduced (for RPGMaster Suite APIs only, I\'m afraid), with the following command format:
' + +'!attk [sender_override_id] --cmd1 args1... --cmd2 args2...' + +'
The optional sender_override_id (don\'t include the [...], that\'s just the syntax for "optional") can be a Roll20 player_id, character_id or token_id. The API will work out which it is. If a player_id, the commands output will be sent to that player when player output is appropriate, even if that player is not on-line (i.e. no-one will get it if they are not on-line). If a character_id or token_id, the API will look for a controlling player who is on-line and send appropriate output to them - if no controlling players are on-line, or the token/character is controlled by the GM, the GM will receive all output. If the ID passed does not represent a player, character or token, or if no ID is provided, the API will send appropriate output to whichever player Roll20 tells the API to send it to.
' + +'Roll20 provides many excellent maths functions for commands made to the chat window and contained in API button strings. However, it is not always possible to use the Roll20 maths using the [[...]] syntax to achieve what you want. RPGMaster provides an alternative set of maths functions to help resolve these issues. Formulas can be entered for many numeric values required by RPGMaster commands using the supported syntax. However: this syntax does not work for anything other than RPGMaster commands as of writing (this might be a future develpment).
' + +'The square brackets [[...]] are not required. The syntax follows normal maths presedent with a few additional operators to support range calculations and dice rolls:
' + +'+-*/ | The standard maths operators work as expected |
---|---|
(...) | Parentheses can be used to define the order of calculation as normal |
^(#,#,#,...) | This will resolve to the maximum value in the list, and each # can also be a calculation (semi-colons can be used instead of commas) |
v(#,#,#,...) | This will resolve to the minimum value in the list, and each # can also be a calculation (semi-colons can be used instead of commas) |
c(...) | This will resolve to the ceiling (the number rounded up) of the result of the contained calculation |
f(...) | This will resolve to the floor (the number rounded down) of the result of the contained calculation |
#d#r# | Dice roll specifications can be included in the maths with optional reroll values anywhere in the calculation, and the numbers can be calculations |
#:# | A different feature is the range calculation - this will derive a number in the range between the two numbers (inclusive), but will try to do so using the equivalent to 3 dice if possible - e.g. 3:18 would make the equivalent of rolling 3d6, 7:34 will resolve to 4+(3d10), 7:35 will resolve to 4+1d11+2d10. A range can be used anywhere in the calculation, and the numbers can themselves be calculations |
The most common approach for the Player to run these commands is to use Ability macros on their Character Sheets which are flagged to appear as Token Action Buttons: Ability macros & Token Action Buttons are standard Roll20 functionality, refer to the Roll20 Help Centre for information on creating and using these.
' + +'In fact, the simplest configuration is to provide only Token Action Buttons for the menu commands: --menu and --other-menu. From these, most other commands can be accessed. If using the CommandMaster API, its character sheet setup functions can be used to add the necessary Ability Macros and Token Action Buttons to any Character Sheet.
' + +'This is a major release, with considerable changes in the background to make the RPGMaster suite of APIs smaller and faster, while increasing utility and helpfulness.
' + +'In 2023, RPGMaster introduced Drag & Drop creatures. This functionality has now been extended to add NPCs, with fully populated character sheets, rolled attributes, populated skills such as rogue skills, populated spell books and powers, all driven by user-definable NPC definitions. A new [NPC] button exists on the Drag & Drop Class & Race dialog, which presents a drop-down Roll Query listing the currently defined NPCs to select from. Other queries will request more detail, such as the level of the NPC. Creating and playing a Drag & Drop NPC is identical in process to a Drag & Drop creature. Help is provided in the CommandMaster Help handout, and the Class & Race Database Help handout.
' + +'To support the introduction of Drag & Drop fully-populated NPCs, the RPGMaster suite will now correctly roll and populate the Attributes for any NPC chosen, and also populate on the character sheet the associated data elements, such as to-hit plus, max weight etc granted by strength, extra spells for priests with high wisdom, and so on. These values have always been taken into account by the APIs (even if not set). However, in addition when an Other Actions > Attribute Check dialog is openned and if the [Auto-check Attributes] button is selected the APIs will automatically check the Attributes: if they have not been set, they will automatically be rolled for the selected character / NPC / creature so that a valid check can be done unless configured not to do so.
' + +'A new RPGM configuration option is now available using the GM\'s [RPGM config] macro bar button, or the !attk --config command, called "NPC Attributes" with the options [No Attributes] and [Roll Attributes]. If [Roll Attributes] is chosen, another configuration option called "NPC Attr Range" becomes available, with the options [Full Range] and [Restrict Range]. If [No Attributes] is chosen, any existing NPC / creature which does not have attribute rolls defined in its creature definition record in the database will not have attributes rolled for it. If [Roll Attributes] & [Full Range] is set, any NPC / creature for which attributes have not been set will have attributes rolled when an Attribute Check is done with a full range (3d6). If [Roll Attributes] & [Restrict Range] is set, any NPC / creature for which attributes have not been set will have attributes rolled in a restricted range that does not give them undue bonuses or penalties.
' + +'The previous release added a means of storing and displaying all saving throw & attribute check modifiers currently in effect for each character / creature / NPC, to show in detail how the current saving throw and attribute check targets were calculated. This release has extended that capability to do the same for all modifiers to AC, Thac0 (to-hit), Damage, & HP, and to manage the application and durations of these modifiers. Generally, the modifiers are applied due to magic items in the possession of the character / creature / NPC, or from spells cast on them that are currently in effect - in the majority of cases, if using the RPGMaster item and spell capabilities of the MagicMaster API and effect management of the RoundMaster API, all these mods will be applied and expired automatically. However, API commands exist for GMs and players to apply mods manually if so desired. See the AttackMaster Help handout for more information.
' + +'The Rogue Skill table can be accessed through Other Actions > Rogue Skill Check. This dialog has now been extended to work in a similar fashion to the Saving Throw dialog, with a button to [Auto-check Skill Scores] and another to [Manually check Skill Scores] - the choice will be preserved between uses and game sessions. Auto-checking will automatically review the class, race, dexterity, armour, and magic items possessed, and use the data in the definitions for each to set the values on the Rogue Skills table, leaving the user to allocate the points granted for the level and class of rogue. The auto-check will be performed continuously by the APIs so that as conditions change, such as items possessed and armour worn, the values immediately change accordingly. The manual option, on the other hand, leaves the player to enter all the values in the table, for instance for a non-standard character.
' + +'Creature innate attacks and attacks with weapons have always been considered to be "proficient". However, there is now a way to set weapon use for Drag & Drop creatures and NPCs to be "specialist" or even have "mastery". Each weapon definition in a creature / NPC definition can be annotated with the proficiency level (if none is stated, "proficient" is assumed). See the Class & Race Database Help handout for how to specify proficiency levels.
' + +'Drag & Drop creatures and NPCs have always been able to have named items allocated to them to carry, such as named weapons, armour or ammunition. They can now also have random items added to their character sheets as being "in their possession" (even if they can\'t use them), as if they had picked them up or won them in battle at some point in the past. The number of random items can be specified (or even be a random quantity, such as a dice roll or number range), and reviewed by the GM using the GM\'s [Add Items] macro bar button or the !magic --gm-edit-mi command (to ensure game balance...). Thus, PCs can loot these as treasure after a successful battle, or a rogue can pick-pocket them, etc. Details can be found in the Class & Race Database Help handout as to how to specify random items for Drag & Drop creatures & NPCs.
' + +'Drag & Drop creature and NPC database definitions can use the query: data tag to specify a query to the player that returns a selected list of parameters to feed into and alter the definition\'s results. The parameters for these queries can now be used in weapon, armour and item definition sections of NPC and creature database entries. See the Class & Race Database Help handout section on Complex Creatures with Multiple Forms for an explanation of creature and NPC query definitions.
' + +'A new database of treasure items - items that are descriptive and may have a worth, but are not magical or functional. These are intended to add colour to the campaign. A new button has been added to the GM\'s [Add Items] dialog (or !magic --gm-only-mi command) to list these items to add to NPCs and containers for player characters to find
' + +'While I am sure everything worked when first coded, subsequent changes have had unexpected consequences and players have also done things I didn\'t expect (is that not the story for all GMs?). Hence fix lists continue...
' + +'The RPGMaster Library API provides the data and rule-set processing for a specific RPG version and Roll20 Character Sheet for the RPGMaster series of APIs. This particular Library supports the Advanced Dungeons and Dragons v2e RPG, and the Advanced D&D 2e Character Sheet by Peter B.
' + +'The functions and data in the Library cannot be accessed directly by the GM or Players (with the exception of the RPGMaster Roll Templates). It does not support any API commands directly, but supports the functioning of the API commands provided by other RPGMaster APIs: InitiativeMaster, AttackMaster, MagicMaster and CommandMaster, to the extent that these APIs will not function without the Library being loaded. Note: RoundMaster does not have any RPG-version specific aspects, is independent of the Library, and does not require a RPGMaster Library to be loaded to function.
' + +'The Library does Roll Template processing for RPGMaster-defined Roll Templates. These can be used by GMs to create their own Chat menus and displays. See the descriptions in Section 2 for how to access and use these templates.
' + +'The Library also provides a number of objects and functions that can be called from other APIs. These include an extensive Character Sheet table management suite described in Section 3 below, for table structures defined by The Aaron in various standard Roll20 Character Sheets, such as the Advanced D&D 2e character sheet by Peter B. There are a number of other useful functions, which are described in Section 3.
' + +'1. General RPGMaster Library Information' + +'
' + +'2. RPGMaster Roll Templates
' + +' 2.1 User-Selectable Template Display
' + +' 2.2 Colour, Padding and Image Override Field Tags
' + +' 2.3 Template Definitions
' + +' RPGMattack
' + +' RPGMspell
' + +' RPGMmessage
' + +' RPGMdefault
' + +'3. API Library Accessible Functions
' + +' 3.1 Get Character Sheet Field Map and Global Data
' + +' 3.2 Manage Character Sheet Tables
' + +' Table Management Functions
' + +' Table Object Methods
' + +' 3.3 Attribute Management
' + +' 3.4 Database Management
Roll Templates are standard Roll20 functionality, often provided by various Character Sheet versions. The RPGMaster Library provides RPGMaster-specific versions of Roll Templates to support gameplay using the RPGMaster series of APIs. Of course, they can also be used for other purposes by the GM to support their design for gameplay, and by other APIs that might be loaded alongside the RPGMaster Library API. The list of those provided is:
' + +'Template | Based on | Description |
---|---|---|
RPGMattack | RPGMattack | Format the results of attacks, Melee or Ranged |
RPGMpotion | RPGMspell | Display the details of a potion, oil or other consumable liquid |
RPGMspell | RPGMspell | Display the specification of a spell or power |
RPGMscoll | RPGMspell | Display the details of a scroll or book |
RPGMmessage | RPGMmessage | Display a formatted message |
RPGMwarning | RPGMdefault | Display a warning message in a bold format |
RPGMweapon | RPGMdefault | Display the specification of a weapon of any type |
RPGMammo | RPGMdefault | Display the specification of ammunition for a Ranged weapon |
RPGMarmour | RPGMdefault | Display the specifications of any form of armour |
RPGMring | RPGMdefault | Display the specifications of a ring |
RPGMwand | RPGMdefault | Dosplay the specifications of a wand, stave or rod |
RPGMitem | RPGMdefault | Display the specifications of any magic item |
RPGMclass | RPGMdefault | Display the details of a character class |
RPGMmenu | RPGMdefault | Display a menu of actions that a Player or GM can perform |
RPGMdefault | RPGMdefault | A default template that can display any form of information |
The templates are built upon four base templates: RPGMattack, RPGMspell, RPGMmessage, and RPGMdefault. However, each individual template can have different colour swatches and background images. It is also possible for the GM to use special template field tags to change the colours and background images of various parts of the templates, as described below.
' + +'In the API configuration menu, opened using !attk --config or !magic --config, the GM can choose whether the default display of RPGMaster Roll Templates for everyone is Fancy, meaning the system uses background textures and images to make the chat templates look interesting, or Plain, which means the default display is of coloured templates but without textures or images.
' + +'In addition, each Player can choose their own options: at the top right of every menu or message displayed in the chat using a RPGMaster Template is a cog wheel. Clicking this cog wheel will display an options dialog for the Player with three options - Menus with Images, Plain Menus, and Dark Mode Menus. The Player can select one of the options, and the current and all future menus and messages using RPGMaster Roll Templates will be displayed for that particular Player using this new option. The option persists for that Player between gameplay sessions.
' + +'Note: if use of a template includes any of the colour or image field tags (outer image, title image or body image) these images will always appear regardless of which option the Player chooses. The field tags always override the original template definition.
' + +'All RPGMaster templates can use the following field tags (with some exceptions as noted). The field tags are used in this way:
' + +'{{field tag=... whatever you want to be in this field ...}}' + +'
However, the fields in this table do not display data, but take the content of the field after the "=" as data for a CSS command to change the behavior of the template.
' + +'shadow | drop shadow spec | The specification of the Template drop shadow for the outer box in any form of CSS units |
---|---|---|
outer | color | The colour of the outer box of the template |
title box | color | The fill colour of the title box |
title text | color | The colour of the title text |
body box | color | The fill colour of the body box |
row box | color | The colour of the outline of each row box |
row light | color | The fill colour of alternate row boxs (light / dark / light / dark ...) |
row light text | color | The colour of alternate row text (light / dark / light / dark ...) |
row dark | color | The fill colour of alternate row boxs (light / dark / light / dark ...) |
row dark text | color | The colour of alternate row text (light / dark / light / dark ...) |
outer pad | padding spec | The padding of the outer box specified using any form of CSS measurements |
title pad | padding spec | The padding of the title box specified using any form of CSS measurements |
body pad | padding spec | The padding of the body box specified using any form of CSS measurements |
row pad | padding spec | The padding of each row box specified using any form of CSS measurements |
outer image | Image URL | The URL to an image in Roll20 which will be drawn behind the whole Template |
title image | Image URL | The URL to an image in Roll20 which will be drawn behind the title box |
body image | Image URL | The URL to an image in Roll20 which will be drawn behind the body of the Template |
The data passed with these field tags is one of four types: color, padding spec, shadow spec or image URL. Note, the CSS keyword is not needed in each case, just the data specification that would follow the keyword.
' + +'Tip: In fact, you can sneak additional CSS keywords into the data - e.g. the title text colour could be \'{{title text=white; text-shadow: 1px 1px 1px gray}}\' which would sneak a drop-shadow onto the title text of the Template. This will not always work, but is worth trying.
' + +'color | Can be any valid < color > specification that can be placed after a CSS < color > command: a named color, a RGB specification, a HSL specification, or any other valid syntax. |
---|---|
padding spec | Can be any valid padding < length > specification that can be placed after a CSS < padding > command: relative units such as \'em\' or \'small\' or percentages, or can be absolute units such as \'px\' or \'cm\'. |
shadow spec | Can be any valid shadow < length > and < color > specification that can be placed after a CSS < box-shadow > command: relative units such as \'em\' or \'small\' or percentages, absolute units such as \'px\' or \'cm\'; and a named color, a RGB specification, an HSL specification, or any other valid |
Image URL | Must be a valid URL of a thumbnail image already loaded to the Roll20 image library - either your own or one you are provided with. |
As with any message sent to the Roll20 chat window, it is possible to use standard functions such as in-line rolls and maths. These all work in exactly the standard way that they do in any other template or message. In some fields of an RPGMaster template, such as those being used to assess success criteria for a {{Result= ... }}, such rolls and calculations are necessary and important.
' + +'Just as useful is the ability to reuse the value of in-line rolls at other points in the RPGMaster roll templates. In order to reuse a roll result, use the syntax $[[#]], where # is the 1-based index of which roll in the template is being reused. For example, here is a template used for a power that only deducts a charge of the magic item that cast the power if the dice roll of 1d10 is equal to the value 2, but a roll of 1 is also a success, and anything other than 1 or 2 is a failure:
' + +'&{template:RPGMdefault}{{title=@{selected|token_name} attempts to
**Improve a Gem**
using a *Jewel of Flawlessness*}}Specs=[Improve Gem,Power,1H,Alteration]{{Speed=[[10]]}}{{Range=0}}{{Duration=Permanent}}{{Area of Effect=Jewels in same container with *Jewel of Flawlessness*}}{{save=None}}{{reference=DMG p173}}SpellData=[w:Improve Gem,sp:10,cs:M]{{desc=When a jewel of flawlessness is placed with other gems, it doubles the likelihood of their being more valuable (i.e., the chance for each stone going up in value increases from 10% to 20%). The jewel has from 10-100 facets, and whenever a gem increases in value because of the magic of the jewel of flawlessness (a roll of 2 on d10), one of these facets disappears. When all are gone, the jewel is a spherical stone that has no value.}}{{Need to Roll=2 or less}}{{Improvement Roll=[[1d10]]}}{{Result=Need to Roll>=Improvement Roll}}{{successcmd=!magic ~~mi-charges @{selected|token_id}¦-[[$[[1]]-1]]¦Jewel-of-Flawlessness¦¦charged}}
The {{Need to Roll=2 or less}} sets the target that represents success. The in-line roll is included in {{Improvement Roll=[[1d10]]}}. The two values are compared in the {{Result=Need to Roll>=Improvement Roll}} which results in Success or Failure being displayed on the Roll Template in the chat window and, if successful, the {{successcmd=...}} command being run. In this case, the command is a call to the !magic --mi-charges function that will deduct charges from the magic item named in the call in the character\'s equipment bag. But a charge is only deducted if the roll of the dice is 2, as per the specification of the magic item in the DMG. To use the value of the in-line roll in both calculating the Result and use in the successcmd command, the roll needs to be referenced in the successcmd using the $[[1]] reference syntax as part of the in-line calculation [[$[[1]]-1]]. As successcmd will only execute on success of the Result, which happens if the in-line dice roll is 1 or 2, "successful dice roll minus 1" is 0 or 1, and negating this is -0 on a roll of 1 and -1 on a roll of 2.
' + +'If there were more rolls, these would be referenced using $[[2]], $[[3]], ... etc.
' + +'As with any template, the text in any of the fields can be anything allowed by Roll20 in a macro or template, except where noted otherwise. If to be used with a RPGMaster API database, the Specs and Data fields must be configured in line with the relevant database documentation and placed between the template fields so as not to be seen by the Players when the Roll Template is displayed. This is a highly graphical template (even in Plain Mode) and the Field Tags are generally not displayed.
' + +'Title / Name | Mandatory | The title text for the attack template. Either field tag can be used, and the tag is not displayed. |
---|---|---|
Subtitle | Optional | The subtitle text for the attack template. The tag is not displayed. |
AC hit | Mandatory | The value of the Armour Class that has been successfully hit by the attack. Can be a Roll20 calculation and/or dice roll specification. If the "Crit Roll" and "Fumble Roll" field tags are to function correctly, the AC hit tag must include one Roll20 calculation field tagged as the Dice Roll e.g. matches ##[Dice roll]. |
Attk type | Optional | The type of damage done by the attack: S, P, B, or any combination of these. |
Dmg S Label | Optional | The label / title to display for the Dmg S field on the template. If not provided, it defaults to "S / M". Useful if using the template for attacks that result in some outcome other than damage to the opponent. |
Dmg S | Mandatory | The specification to display for damage to Small & Medium opponents. Can be numeric, a Roll20 calculation, dice roll specification, or even an API button specification. |
Dmg L Label | Optional | The label / title to display for the Dmg L field on the template. If not provided, it defaults to "L". Useful if using the template for attacks that result in some outcome other than damage to the opponent. |
Dmg L | Mandatory | The specification to display for damage to Large and larger opponents. Can be numeric, a Roll20 calculation, dice roll specification, or even an API button specification. |
Target AC | Optional | The Armour Class value of the targeted opponent. Only used for targeted attacks. Can be a Roll20 "@{target..." command, numeric value, calculation etc. |
Target SAC | Optional | The Armour Class value of the targeted opponent vs. Slashing attacks. Only used for targeted attacks. Can be a Roll20 "@{target..." command, numeric value, calculation etc. |
Target PAC | Optional | The Armour Class value of the targeted opponent vs. Piercing attacks. Only used for targeted attacks. Can be a Roll20 "@{target..." command, numeric value, calculation etc. |
Target BAC | Optional | The Armour Class value of the targeted opponent vs. Bludgeoning attacks. Only used for targeted attacks. Can be a Roll20 "@{target..." command, numeric value, calculation etc. |
Target HP | Optional | The current Hit Points of the targeted opponent. Must result in a numeric value. |
Target MaxHP | Optional | The maximum Hit Points of the targeted opponent. Must result in a numeric value. |
Result | Optional | A comparison function between any two other field tags, an example being AC_Hit and Target_AC (note the underscores added). Can use any test from: = < > <= >= <> != e.g. AC_Hit<=Target_AC if true will give a green Success bar, if false will give a red Failure. |
SuccessCmd | Optional | The text provided will not be displayed but will be sent to the chat window if the Result comparison indicates success. Normally used to send an API command on a successful attack result |
FailCmd | Optional | The text provided will not be displayed but will be sent to the chat window if the Result comparison indicates failure. Normally used to send an API command on a failed attack result |
Crit Roll | Optional | RPGMAttack: Data must result in a numeric value which is compared to the Dice Roll value tagged in the AC Hit field. If Crit_roll<=Dice Roll, displays a green "Critical Hit!" bar. RPGMdefault: Same as Result, but displays a green Critical Success! bar if true. |
Fumble Roll | Optional | RPGMattack: Data must result in a numeric value which is compared to the Dice Roll value tagged in the AC Hit field. If Fumble_roll>=Dice Roll, displays a red "Fumbled!" bar. RPGMdefault: Same as Result, but displays a red Critical Failure! bar if true. |
CritCmd | Optional | The text provided will not be displayed but will be sent to the chat window if the Crit_Roll comparison is true. Normally used to execute an API command on a critical success |
FumbleCmd | Optional | The text provided will not be displayed but will be sent to the chat window if the Fumble Roll comparison is true. Normally used to execute an API command on a fumble |
Desc / Desc(1-9) | Optional | Up to 10 description fields can be added to the bottom of any RPGMaster attack template. The field tag is not displayed. These can, for example, hold reminders of special outcomes of the attack depending on the weapon or creature wielding it, or the roll achieved. |
As with any template, the text in any of the fields can be anything allowed by Roll20 in a macro or template, except where noted otherwise. For spell, potion and scroll descriptions especially it is quite often useful to include API Buttons in some of the fields to run certain RPGMaster API commands: this is especially the case if using the RoundMaster API and the commands !rounds --target to set status markers and timers and target spell effects, and/or !rounds --aoe to display ranges and areas of effect. If to be used with a RPGMaster API database, the Specs and Data fields must be configured in line with the relevant database documentation and placed between the template fields so as not to be seen by the Players when the Roll Template is displayed.
' + +'Title / Name | Mandatory | The title text for the template. Either field tag can be used, and is not displayed. |
---|---|---|
SPlevel | Optional | The level of the spell being cast. No need to provide for potions or scrolls. |
School | Optional | The school of the spell or spell-like effect. |
Range | Mandatory | The range of the spell or spell-like effect. |
Components | Mandatory | The components of the spell or spell-like effect. Often represented by a combination of the letters VSM. |
Duration | Mandatory | The duration of the spell or spell-like effect. |
Range | Mandatory | The range of the spell or spell-like effect. |
Time | Mandatory | The casting time or time until effect occurs of the spell or spell-like effect. |
AoE | Mandatory | The Area of Effect of the spell or spell-like effect. |
Save | Mandatory | The validity and outcome of any saving throw or other mitigating factors to the spell or spell-like effect. |
Healing | Optional | The healing or other beneficial effect of the spell or spell-like effect. |
Damage | Optional | The damage or other maleficent effect of the spell or spell-like effect. |
Reference | Optional | A reference to where more information can be found about the spell or spell-like effect. |
Materials | Optional | The material components required to cast the spell or spell-like effect. |
Looks Like | Optional | A description of what the item looks like (for potions & scrolls), especially useful for hidden items. |
Effects | Optional | A description of the effects of the spell or spell-like effect. |
Use | Optional | Instructions for how to use the spell or spell-like effect. Especially useful for complex spells that require actions to occur in a particular sequence, have different means of achieving different effects in different circumstances, or have long-lasting effects. |
GM Info | Optional | Only appears if the message is for the GM (though other players included in the same message will also see it). Instructions for the GM on how to use the spell or spell-like effect, such as how to tailor for your own campaigns. |
Desc / Desc(1-9) | Optional | Up to 10 description fields can be added to the bottom of any RPGMaster spell, potion or scroll template. The field tag is not displayed. These can, for example, hold reminders of special outcomes of the spell depending on the creature targeted, or anything else the GM thinks useful but does not fit in any other field. If desc(1-9) are used, a button to show less... will appear in the {{desc=...}} section which, if selected, will change the desc(1-9) to hide(1-9) and not display it. |
New Hide(1-9) | Optional | Up to 9 rows of text tagged with hide(1-9) will not be shown, but will trigger a show more... button to be added to the desc section which, if selected, will redisplay the template with "hide(1-9)" changed to "desc(1-9)" and displayed. |
This template is mostly used internally by the RPGMaster APIs to send messages to the GM and Players. It provides a very simple template with a minimum of overhead. As with any template, the text in any of the fields can be anything allowed by Roll20 in a macro or template, except where noted otherwise. If to be used with a RPGMaster API database, the Specs and Data fields must be configured in line with the relevant database documentation and placed between the template fields so as not to be seen by the Players when the Roll Template is displayed.
' + +'Title / Name | Optional | The title text for the template. In a Message Template, the title/name is optional and a message can appear without a title if so desired. Either field tag can be used, and is not displayed. |
---|---|---|
Desc / Desc(1-9) | Optional | Up to 10 description fields can be defined, though usually it is just the one, which will contain the message. The field tag is not displayed. |
All template definitions other than those described above use the base RPGMdefault template structure, with different colour swatches, textures and images. This template type is the most flexible, and will display pretty much any field tag that is added. Only a few are predefined, which provide the ability to structure the template. As with any template, the text in any of the fields can be anything allowed by Roll20 in a macro or template, except where noted otherwise. If to be used with a RPGMaster API database, the Specs and Data fields must be configured in line with the relevant database documentation and placed between the template fields so as not to be seen by the Players when the Roll Template is displayed.
' + +'Title / Name | Mandatory | The title text for the template. Either field tag can be used, and the tag is not displayed. |
---|---|---|
Subtitle | Optional | The subtitle text for the template. The tag is not displayed. |
Section / Section(1-9) | Optional | A section header, separating other user-defined field tags (not the Desc description field tags, which always appear at the bottom of the template). The field tag is not displayed, and the contents are centred in the row. |
Any User-defined tag | Optional | As many different user-defined field tags as are desired, using A-Z a-z - _ 0-9 and Space but always starting with an Alpha. |
Result | Optional | A comparison function between any two other field tags which have numeric data or calculations and attribute look-ups that result in numeric data. The field tag names to be compared should have underscores \'_\' instead of spaces. Can use any test from: = < > <= >= <> != e.g. AC_Hit<=Target_AC if true will give a green Success bar, if false will give a red Failure. |
SuccessCmd | Optional | The text provided will not be displayed but will be sent to the chat window if the Result comparison indicates success. Normally used to send an API command on a successful attack result |
FailCmd | Optional | The text provided will not be displayed but will be sent to the chat window if the Result comparison indicates failure. Normally used to send an API command on a failed attack result |
GM Info | Optional | Only appears if the message is for the GM (though other players included in the same message will also see it). Instructions for the GM on whatever the roll template is about, such as how to tailor a magic item for your own campaigns. |
Looks Like | Optional | A description of what the item looks like, especially useful for hidden items. |
Desc / Desc(1-9) | Optional | Up to 10 description fields can be defined, which will all be displayed at the bottom of the template in numeric order. The field tags are not displayed. If desc(1-9) are used, a button to show less... will appear in the {{desc=...}} section which, if selected, will change the desc(1-9) to hide(1-9) and not display it. |
New Hide(1-9) | Optional |
In addition to the RPGM Templates described in Section 2, the RPGMaster Library provides a number of functions, objects and methods that might be of use to API authors, independantly of the RPGMaster series of APIs. These are described in this section. To access these, the functions can be accessed using the following syntax from other APIs:
' + +'LibRPGMaster.function( parameters )' + +'
Alternatively, so as not to need to reference the Library, and/or to facilitate an easily alterable and maintainable reference that can be changed in one place, the Library reference can be made within a function mapping, like this:
' + +'const localFunctionName = (...a) => libRPGMaster.functionName(...a);' + +'
The Library function can then be accessed using the localFunctionName in the API as required and, should the Library function call change, the change can be made in just one place.
' + +'The rest of this Section describes the accessible functions, objects and methods.
' + +'The RPGMaster Library holds RPG-version-specific data which other APIs can access, and a map of the important fields on the Roll20 Character Sheet version supported. References to each of these data items can be acquired using the getRPGMap() function:
' + +'[fields,RPGMap] = getRPGMap()' + +'
This function does not take any parameters, but returns an array of two object references:
' + +'api_field_name: [char_sheet_field_name,property,defaultValue,sheetWorkerFlag]' + + 'where property is current or max and sheetWorkerFlag (optional) is true if the field is to be set with the Roll20 setWithWorker() method or false if the Roll20 set() method is to be used (default false).
dbNames | An array of the database objects of the standard, RPGMaster provided databases, indexed by name. |
---|---|
fieldGroups | An object defining the groups of fields held in repeating tables on the Character Sheet and named in the fields object |
spellsPerLevel | An object specifying the number of spells that can be memorised at each level by each class of spellcaster |
specMU | An array of strings naming the standard specialist wizard classes for this RPG version |
ordMU | An array of strings naming the standard ordinary wizard classes for this RPG version |
wisdomSpells | An array holding the additional spell slots or points per level for high wisdom scores |
raceToHitMods | An object of race toHit modifiers with various weapon types and super-types |
rangedWeapMods | The standard range mods to the toHit roll for firing ranged weapons at Near, Point Blank, Short, Medium, Long and Far ranges for this RPG version |
saveLevels | An object mapping class levels to the baseSaves table saving throw entries |
baseSaves | An object holding for each base class an array of base saving throws with improvement by level progression mapped by saveLevels |
classSaveMods | An object of class modifiers to the base saving throws |
raceSaveMods | An object of race modifiers to the base saving throws |
Some data on Character Sheets is best represented in tables, made up of multiple rows of multiple fields. Some tables can also have multiple columns of rows, each column containing rows which in turn have multiple fields (in fact, each column is considered a table in itself). Having a structured set of functions to add to, find, read, update, and remove data in these tables is a useful function to have. the ChatSetAttr API from The Aaron provides an amount of such functionality, but the RPGMaster Library provides a full suite of management functions, based on the definition of a standard Table Object, with associated Methods. The mechanics of the Character Sheet table are then hidden from the author of the API, who can concentrate on other functionality.
' + +'The Library consists of the Table Object, and the Methods. The mechanics of the Table Object are hidden from the API (but can be exposed & used if you know what you are doing and understand the structure of Character Sheet tables fully). The important aspects to know are the functions to get the table object from the character sheet, and the methods to manipulate the table object.
' + +'tableObject = getTable(character,fieldGroupDef,column,tableObject,caseSensitive)' + + '
Gets a complete table object from a character sheet. Also discovers all the valid default values for the table fields from the fields object. Takes the following parameters:
' + + 'tableObject = getTableField(character,tableObject,tableDef,attributeDef,column,defaultVal,caseSensitive)' + + '
Gets all rows for one field of a table. Adds that to the table object passed as a parameter. Faster than obtaining the full table with getTable(), but should only be used for finding and reading data and not for writing or adding new data and especially not adding rows with the .addTableRow() method. Takes the following parameters:
' + + 'defaultValues = initValues(fieldGroupDef)' + + '
Gets a new default values object for the table defined by the provided field group definition from the fieldGroup object from the RPGMap obtained with getRPGMap() (see Section 3.1). The default values are those set for the table fields in the fields object. The structure of the values object is values[field_name][field_property]. Takes the following parameter:
' + + 'value = tableObject.tableLookup(attributeDef,row,defaultValue,returnObj)' + + '
Looks up the value stored in a specified field in the specified row of the table object. Can take an optional default value (which will override the default value for the field stored in the table object itself), and an optional returnObj parameter to determine what is returned (value or object). Takes the following parameters:
' + + 'tableObject = tableObject.tableSet(attributeDef,row,value,caseSensitive)' + + '
Sets the value of the specified table field in the specified row to be the value provided. tableObject is changed in-place. Takes the following parameters:
' + + 'tableObject = tableObject.addTableRow(row,values)' + + '
Either add a new row to a table object with the provided or default values, or replace a current row with the provided or default values. tableObject is changed in-place. Takes the following parameters:
' + + 'row = tableObject.tableFind(attributeDef,valToFind)' + + '
Find the first row in the Table Object with an entry in the field defined by the attributeDef with the value of valToFind. If found, row will be a valid table row number; otherwise the method returns < undefined >. Takes the following parameters:
' + + 'Two functions that get and set attribute values on the character sheet:
' + +'value = attrLookup( character, attributeDef, tableDef, row, column, caseSensitive, defaultValue );' + +'
A function that can get the value of an attribute from a character sheet, either from a standard attribute, or from a repeating table. However: for table field values using the Table Management functions, objects and methods is prefered and may suffer from fewer issues in future. Takes the following parameters:
' + +'attributeObj = setAttr( character, attributeDef, value, tableDef, row, column, caseSensitive );' + +'
A function that sets the value held by an attribute on a character sheet. Can also set values in repeating tables on the character sheet. However: for table attributes it is highly recommended that the Table Management functions, objects and methods (see Section 3.2) are used to get and set repeating table field values, as this might suffer from fewer issues in the future. Takes for following parameters:
' + +'The RPGMaster series of APIs use a number of databases holding data about spells, powers, magic items, character classes, attack macros, and other aspects. These are held as objects within the game-version and character sheet-version specific RPGMaster Library, and can be supplemented by GM provided additional databases held in Roll20 character sheets. See the database help handouts distributed with the RPGMaster APIs for more information on each type of database.
' + +'The RPGMaster Library provides a number of database management functions which can be used by APIs to manage the provided databases. These functions are described here.
' + +'DBindex = getDBindex( forceUpdate )' + +'
A function to build or get and return an index to all entries in the RPGMaster databases. This function must be called before any items in the databases are queried or acted upon with the other database management functions. Having this index (which is globally held in the system) speeds up database access considerably over relying on the Roll20 object management functions, and allows the APIs to seamlessly access items that might be in either internal object databases or held in external character sheet databases, and supports the prioritisation of user-defined database items over Library defined items. Takes one optional parameter:
' + +'dbItemObject = abilityLookup( rootDB, dbItemName, character, silent, default );' + +'
A function to get an entry from a database. Uses the global DBindex internally to access the correct database entry directly, eliminating the speed of a Roll20 object search. The building of the index using getDBindex() will determine the priority order of the potential sources for database items: see that function description for details. However, if the item does not have an entry in the DBindex (i.e. is not in any currently loaded database), and if a character sheet object is passed as a parameter, this abilityLookup() function will also search the character sheet for a copy of the database item which might have previously been placed there by a getAbility() function call. This allows characters to have items that come from user-defined databases in one campaign that are carried with them to other campaigns which perhaps don\'t have the same user-defined databases loaded.
' + +'Takes the following parameters:
' + +'{name:\'-\',type:\'\',ct:\'0\',charge:\'uncharged\',cost:\'0\',body:\'This is a blank slot. Go search out something new to fill it up!\'}
class AbilityObj {
' + + ' constructor( dBname, abilityObj, ctObj, source ) {
' + + ' this.dB = dBname;
' + + ' this.obj = abilityObj;
' + + ' this.ct = ctObj;
' + + ' this.source = source;
' + + ' this.api = (abilityObj && abilityObj[1]) ? (abilityObj[1].body.trim()[0] == \'!\') : false;
' + + ' }
dbItemObject = getAbility( rootDB, dbItemName, character, silent )' + +'
A special version of abilityLookup() that not only gets the requested database item from the databases, but also saves that item to the stated character sheet for later reference. All parameters are defined the same as those for AbilityLookup() above. The returned dbItemObject.dB object attribute always holds the name of the character sheet, so that after a call to getAbility(), the standard Roll20 syntax of `%{${dbItemObject.dB}|${dbItemName}}` will work to display the database item in the chat window.
' + +'itemObject = setAbility( character, itemName, itemBody, actionBar )' + +'
A function to set or update a database item or an ability macro in a character sheet (database items are stored as ability macros on a character sheet). Note: the API in-memory databases cannot be updated in this way. Only items and ability macros on character sheets can be created and updated. Takes the following parameters:
' + +'errFlag = buildCSdb( dbFullName, dbObj, typeList, silent )' + +'
A function to extract an API databse from memory into a Character Sheet Database representation, so that the GM can examine the standard items stored there. Note: the function does not re-index the databases, so the Character Sheet database representation so created will not actually be used to recover data from by the system. If use of the Character Sheet database is required, it will be necessary to run a getDBindex( true ) function to force an index update. Takes the following parameters:
' + +'checkCSdb( dbName );' + +'
A function to check any character sheet database for completeness and accuracy. If the body texts of the item Ability Macros have been set up in accordance with the relevant database documentation handout, this function will ensure that all other attributes, lists and fields are set up to match the database item Ability Macros. Takes the following parameter:
' + +'The simplest way to configure a Character Sheet for use with the RPGMaster APIs is to use the functions of the APIs to set up the Character Sheet. If the CommandMaster API is installed, a number of features and commands are provided to help the DM/Game Creator in this process.
' + +'The most basic of these is to configure a blank Character Sheet.
' + +'Alternatively, the Character Sheet can be set up manually from scratch, or existing Characters with Character Sheets can be adapted for use with the APIs. This option is explored in the rest of this document. However, adding information (other than Character Attributes) to the Character Sheet manually will never be as functionally rich as using the APIs for setting up the sheet, and in some cases will restrict the operation of the APIs to not work in the optimal way. Always use the API menus where possible.
' + +'The API can work with any Token configuration but requires tokens that are going to participate in API actions to represent a Character Sheet, so that actions relevant to the token and the character it represents can be selected.
' + +'A single Character Sheet can have multiple Tokens representing it, and each of these are able to do individual actions made possible by the data on the Character Sheet jointly represented. However, if such multi-token Characters / NPCs / creatures are likely to encounter spells that will affect the Character Sheet (such as Haste and Slow) they must be split with each Token representing a separate Character Sheet, or else the one spell will affect all tokens associated with the Character Sheet, whether they were targeted or not! In fact, it is recommended that tokens and character sheets are 1-to-1 to keep things simple.
' + +'The recommended Token Bar assignments for all APIs in the Master Series are:
' + +'Bar1 (Green Circle): Armour Class (AC field) - only current value' + +'
' + +'Bar2 (Blue Circle): Base Thac0 (thac0-base field) before adjustments - only current value
' + +'Bar3 (Red Circle): Hit Points (HP field) - current & max
It is recommended to use these assignments, and they are the default bar assignments set by the CommandMaster API if its facilities are used to set up the tokens: the CommandMaster functions can be used to change the defaults to whatever you require. However the bars are set (using CommandMaster or manually), the APIs will always search for the most appropriate fields to use from the token and character sheet - only if the APIs can\'t find valid values will they revert to using the default fields on the token. It is recommended that all tokens are set the same way, whatever way you eventually choose.
' + +'The latest version of the RPGMaster series APIs use various game and character sheet-specific versions the RPGMaster Library to implement the relevant rules, parameters and databases specific to a game-version and/or Roll20 character sheet version. Loading the correct RPGMaster Library will configure all the RPGMaster APIs to work with that game rule set and character sheet. However: at the time of writing, only the AD&D 2E game version, and Advanced D&D 2E Character Sheet by Peter B. are supported.
' + +'As with other game and character sheet specific configuration, selecting the correct RPGMaster Library will configure the RPGMaster APIs for the character sheet in use. If a character sheet does not have the fields specified in the Library, the RPGMaster APIs will create them - it can, in fact, work with a totally blank character sheet definition and it will create all of the fields it requires from scratch: it just won\'t look very pretty or be very usable as a character sheet! However, you can alter the Library to work with any character sheet as follows (though the game rules and databases will remain those coded in that version of the Library).
' + +'The Library API has an object definition called \'fields\', which contains items of the form
' + +'Internal_api_name: [sheet_field_name, field_attribute, optional_default_value, optional_set_with_worker_flag]' + +'
A typical example might be:
' + +'Fighter_level:[\'level-class1\',\'current\'],' + +'
' + +'Or
' + +'MUSpellNo_memable:[\'spell-level-castable\',\'current\',\'\',true],
The internal_api_name must not be altered! Doing so will cause the system not to work. However, the sheet_field_name and field_attribute can be altered to match any character sheet.
' + +'Table names are slightly different: always have an internal_api_name ending in \'_table\' and their definition specifies the repeating table name and the index of the starting row of the table or -1 for a static field as the 1st row.
' + +'Internal_api_table: [sheet_repeating_table_name,starting_index]' + +'
An example is:
' + +'MW_table:[\'repeating_weapons\',0],' + +'
The internal_api_table must not be altered! Doing so will cause the system not to work. However, the sheet_repeating_table_name and starting_index can be altered to match any character sheet.
' + +'Each character sheet must have repeating tables to hold weapons, ammo and magic items, as well as other data (if it does not, or they are not the ones specified in the fields object, the APIs will create them). By default, melee weapons \'in hand\' are held in sections of the repeating_weapons table, melee weapon damage in the repeating_weapons-damage table, ranged weapons in the repeating_weapons2 table, ammo in the repeating_ammo table, and magic items are held in the repeating_potions table. The table management system provided by the API expands and writes to repeating attributes automatically, and the DM & Players do not need to worry about altering or updating any of these tables on the Character Sheet. If the Character Sheet does not have tables to display any specific table, the APIs will create the table and attach it to the sheet and will be able to use it, but the Players and DM will not see the content except through the API menus and dialogues.
' + +'Character Attributes of Strength, Dexterity, Constitution, Intelligence, Wisdom and Charisma are generally not directly important to the RPGMaster Series APIs, but the resulting bonuses and penalties are. All Attributes and resulting modifiers should be entered into the Character Sheet in the appropriate places (that is in the Character Sheet fields identified in the \'fields\' API object as noted in section 2 above).
' + +'The Character\'s race is also important for calculating saves and ability to use certain items. The race should be set in the appropriate Character Sheet field. Currently, the races \'dwarf\', \'elf\', \'gnome\', \'halfelf\', \'halfling\', \'half-orc\' and \'human\', as well as many of their sub-races, are implemented (not case sensitive, and spaces, hyphens and underscores are ignored). If not specified, human is assumed. The race impacts saves, some magic items and armour, and bonuses on some attacks, as well as granting some racial powers.
' + +'The system supports single-class and multi-class characters. Classes must be entered in the appropriate fields on the Character Sheet - if using the CommandMaster API, the --abilities or --class-menu commands can be used to set the classes and levels of characters and NPCs. Classes and levels affect spell casting ability, ability to do two-weapon attacks with or without penalty, and the ability to backstab and the related modifiers, among other things. Class and level also determine valid weapons, armour, shields, some magic items and saves.
' + +'Important Note: the appropriate fields must be used for relevant classes - this varies by character sheet. E.g. on the Advanced D&D 2e Character Sheet, Fighter classes must be in the first class column, Wizard classes in the second column, Priest classes in the third, Rogues in the fourth, and Psions (or any others) in the fifth. If manually adding classes, it is important that these locations are adhered to.
' + +'Note: classes of Fighter and Rogue (such as Rangers and Bards) that can use clerical &/or wizard spells will automatically be allowed to cast spells once they reach the appropriate level by the specific game version rules, but not before. They do not need to have levels set in the corresponding spell-caster columns - the casting ability & level is worked out by the system
' + +'The Class-DB database holds definitions of the classes and class rules distributed with the system. If the MagicMaster API is loaded, use the !magic --extract-db Class-DB command to extract the database to a character sheet to examine and update the class rules. Examples of Classes currently supported are:
' + +'Fighter classes | Wizard Classes | Priest Classes | Rogue Classes |
Warrior | Wizard | Priest | Rogue |
Fighter | Mage | Cleric | Thief |
Ranger | Abjurer | Druid | Bard |
Paladin | Conjurer | Healer | Assassin |
Beastmaster | Diviner | Priest of Life | |
Barbarian | Enchanter | Priest of War | |
Defender (Dwarven) | Illusionist | Priest of Light | |
Invoker | Priest of Knowledge | ||
Necromancer | Shaman | ||
Transmuter |
The level for each class must be entered in the corresponding field. Multiple classes and levels can be entered, and will be dealt with accordingly. Generally, the most beneficial outcome for any combination will be used.
' + +'All magic items and standard equipment, including weapons, armour, lanterns etc, are held in the Items table, which by default is set to the potions table, repeating_potions, on the Character Sheet. As with other fields, this can be changed in the \'fields\' object. The best way to put items into this table is by using the MagicMaster API commands --edit-mi or the GM-only command --gm-edit-mi. Alternatively, the AttackMaster --edit-weapons command can be used to load weapons, ammunition and armour into the Items table. It is generally possible to enter item names and quantities directly into the table and use them within the system, but only items that also exist in the supplied databases will actually work fully with the API (i.e. be recognised by the API as weapons, armour, ammo, etc). Other items can be in the table but will not otherwise be effective.
' + +'Items can be added to the databases. See the Database Handouts for more information on the databases.
' + +'For the APIs to work fully the melee weapons, damage, ranged weapons and ammo must be selected using the AttackMaster --weapon command to take the weapon \'in hand\'. This will display a menu to take weapons and shields from the Items table and take them in hand, ready to use. This automatically fills all the correct fields for the weapons and ammo to make attacks, including many fields that are not displayed. Entering weapon data directly into the melee weapon, damage, ranged weapon and ammo tables will generally work, but will be overwritten if the --weapon command is used. Also, some API functions may not work as well or at all.
' + +'For the InitiativeMaster API to support weapon attack actions weapon name, speed and number of attacks are the most important fields. For the AttackMaster API to support attack rolls, proficiency calculations, ranged attacks, strength and dexterity bonuses, and other aspects of functionality, fill in as many fields as are visible on the character sheet. When entering data manually, ensure that the row a melee or ranged weapon is in matches the row damage or ammo is entered in the respective tables (there is no need to do this if using AttackMaster functions to take weapons in-hand, as the relevant lines are otherwise linked).
' + +'Weapon Proficiencies must be set on the Character Sheet. This is best done by using the CommandMaster API character sheet management functions, but can be done manually. Both specific weapons and related weapon groups can be entered in the table, and when a Player changes the character\'s weapons in-hand the table of proficiencies will be consulted to set the correct bonuses and penalties. Weapon specialisation and mastery (otherwise known as double specialisation) are supported by the CommandMaster functions, but can also be set by ticking/selecting the relevant fields on the Character Sheet weapon proficiencies table. If entered manually, if a weapon or its related weapon group does not appear in the list, it will be assumed to be not proficient. However, if CommandMaster is used to set proficiencies, just having proficiency in a weapon will mean automatic related weapon proficiency in any related weapon.
' + +'The best (and easiest) way to give a Character or NPC spells and powers is to use CommandMaster API to add spells and powers to the Character\'s spellbooks, and MagicMaster API to memorise and cast spells and use powers. However, for the purposes of just doing initiative and selecting which spell to cast in the next round, the spells and powers can be entered manually onto the character sheet. Spells are held in the relevant section of the Spells table, which by default is set to the character sheet spells table, repeating_spells. As with other fields, this can be changed in the \'fields\' object. Note that on the Advanced D&D 2e character sheet Wizard spells, Priest spells & Powers are all stored in various parts of this one very large table.
' + +'If you are just using the character sheet fields to type into, add spells (or powers) to the relevant "Spells Memorised" section (using the [+Add] buttons to add more as required) a complete row at a time (that is add columns before starting the next row). Enter the spell names into the "Spell Name" field, and "1" into each of the "current" & "maximum" "Cast Today" fields - the API suite counts down to zero on using a spell, so in order for a spell to appear as available (not greyed out) on the initiative menus, the "current" number left must be > 0. This makes spells consistent with other tables in the system (e.g. potion dose quantities also count down as they are consumed, etc).
' + +'Then, you need to set the "Spell Slots" values on each level of spell to be correct for the level of caster. Just enter numbers into each of the "Level", "Misc." and "Wisdom" (for Priests) fields, and/or tick "Specialist" for the Wizard levels as relevant. This will determine the maximum number of spells memorised each day, that will appear in the spells Initiative Menu. Do the same for Powers using the "Powers Available" field. As with other fields on the character sheet, each of these fields can be re-mapped by altering the \'fields\' object in the RPGMaster Library API.
' + +'Spells can only be cast if they have macros defined in the spell databases (see Spell Database Handout). If the CommandMaster API is loaded, the DM can use the tools provided there to manage Character, NPC & creature spell books and granted powers from the provided spell & power databases.
' + +'The spells a spell caster can memorise (what they have in their spell books, or what their god has granted to them) is held as a list of spell names separated by vertical bars \'|\' in the character sheet attribute defined in fields.Spellbook (on the AD&D2E character sheet \'spellmem\') of each level of spell. On the AD&D2E sheet, the spell books are the large Spell Book text fields at the bottom of each spell level tab. The spell names used must be identical (though not case sensitive) to the spell ability macro names in the spell databases (hence the hyphens in the names). So, for example, a 1st level Wizard might have the following in their large Wizard Level 1 spell book field:
' + +'Armour|Burning-Hands|Charm-Person|Comprehend-Languages|Detect-Magic|Feather-fall|Grease|Identify|Light|Magic-Missile|Read-Magic|Sleep' + +'
Only these spells will be listed as ones they can memorise at level 1. When they learn new spells and put them in their spell book, this string can be added to just by typing into it. When they reach 3rd level and can have 2nd level spells, the following string might be put in the spell book on the Level 2 Wizard spells tab:
' + +'Alter-Self|Invisibility|Melfs-Acid-Arrow|Mirror-Image|Ray-of-Enfeeblement' + +'
Again, as they learn more spells and put them in their spell book, just edit the text to add the spells.
' + +'Once these spell books are defined, the DM or Player can use the MagicMaster --mem-spell command (or an action button and associated ability macro on the Character Sheet) to memorise the correct number of these spells in any combination and store those on the Character Sheet.
' + +'Powers are defined in the Powers database - see Database handouts - though it is possible to use Wizard or Priest spells as powers. If the CommandMaster API is also loaded, the DM can use the tools provided there to manage Character, NPC & creature spellbooks and granted powers.
' + +'Powers work in an almost identical way to Wizard & Priest spells, except that there is only 1 level of powers. Powers that the character has are added to the spell book on the Powers tab in the same way as spells, and then memorised using the --mem-spell command (which also works for powers with the right parameters). If you want to add Wizard or Priest spells as powers that can be used one or more times a day, or at will, the CommandMaster token setup spellbook function allows spells to be added to the powers spellbook. Otherwise, just add the name of the spell with a prefix of "MU-" or "PR-" manually to the powers spellbook e.g. to add a Wizard Light spell as a power, add it to the powers spellbook as "MU-Light". Spells added in this way can be memorised as powers in exactly the same way as other powers.
' + +'Weapon databases are all character sheets that have names that start with MI-DB-Weapon (though in fact, weapons can be in any database starting with MI-DB- if desired), and can have anything put at the end, though those with version numbers of the form v#.# as part of the name will be ignored. Ammunition databases are similar, with the root database MI-DB-Ammo.
' + +'As previously stated, each weapon definition has 3 (or 4) parts in the database (see Section 1): an Ability Macro with a name that is unique and matches the weapon, an Attribute with the name of the Ability Macro preceded by "ct-", a listing in the database character sheet of the ability macro name separated by \'|\' along with other weapons, and sometimes Attributes defining powers given by, or spells stored on the item. The quickest way to understand these entries is to examine existing entries. Do extract the root databases and take a look (but remember to delete them after exploring the items in them, so as not to slow the system down unnecessarily).
' + +'Note: The DM creating new weapons does not need to worry about anything other than the Ability Macro in the database, as running the AttackMaster or MagicMaster -check-db MI-DB-Weapons command will update all other aspects of the database appropriately for all databases that have a name starting with or including \'MI-DB-Weapons\', as long as the Specs and Data fields are correctly defined. Use the parameter \'MI-DB-Ammo\' to check and update the ammunition databases. Running the command -check-db with no parameters will check and update all databases.
' + +'Ability macros can be added to a database just by using the [+Add] button at the top of the Abilities column in the Attributes and Abilities tab of the Database Character Sheet, and then using the edit "pencil" icon on the new entry to open it for editing. Ability macros are standard Roll20 functionality and not dependent on the API. Refer to the Roll20 Help Centre for more information.
' + +'Here are some examples:
' + +'&{template:RPGMweapon}{{name=Longsword}} {{subtitle=Sword}}{{Speed=[[5]]}} {{Size=Medium}}{{Weapon=1-handed melee long-blade}}Specs=[Longsword,Melee,1H,Long-blade]{{To-hit=+0 + Str bonus}}ToHitData=[w:Longsword, sb:1, +:0, n:1, ch:20, cm:1, sz:M, ty:S, r:5, sp:5]{{Attacks=1 per round + level & specialisation, Slashing}}{{Damage=+0, vs SM:1d8, L:1d12, + Str bonus}}DmgData=[w:Longsword, sb:1, +:0, SM:1d8, L:1d12]{{desc=This is a normal sword. The blade is sharp and keen, but nothing special.}}
' + +'The ability specification for this Longsword uses a Roll20 Roll Template, in this case defined in the RPGMaster Library (see the help handout for the Library to review the specifications of this template), but any Roll Template you desire can be used. The entries in the Roll Template itself can be anything you desire, giving as much or as little information as you want. However, the important elements for the APIs are those highlighted. Each of these elements are inserted between the elements of the Roll Template, meaning they will not be seen by the player when the macro is run. Generally spaces, hyphens and underscores in the data elements are ignored, and case is not significant. Each element is described below:
' + +'Specs = [Type, Class, Handedness, Weapon Group]' + +'
The Specs section describes what weapon type and proficiency groups this weapon belongs to. These fields must be in this order. This format is identical for all database items, whether in these databases or others used by the Master series of APIs.
' + +'Type | is the type of the weapon, often the same as the ability macro name without magical plusses. |
Class | is one of Melee, Ranged, or Ammo (preceded by \'Innate-\' if the weapon does not require proficiency). |
Handedness | is #H, where # is the number of hands needed to wield the weapon. |
Weapon Group | is the group of related weapons that the weapon belongs to. |
ToHitData = [w:Longsword, sb:1, +:0, n:1, ch:20, cm:1, sz:M, ty:S, r:5, sp:5]' + +'
The ToHitData section specifies the data relating to an attack with the weapon. These fields can be in any order.
' + +'w: | <text> the name to display for attacks with this weapon |
sb: | <0/1> strength bonus flag - specifies if the strength bonus is applicable to the To-Hit roll |
+: | <[+/-]#> the magical attack bonus/penalty - an integer of any size |
n: | <#[/#]> the basic number of attacks per round: the API will modify to account for specialisation and level |
ch: | <1-20> the roll for a Critical Hit, shown in the API with a green border to the attack AC achieved |
cm: | <1-20> the roll for a Critical Miss, shown in the API with a red border to the attack AC achieved |
sz: | |
ty: | |
sp: | <#> the speed of the weapon in segments |
r: | <[+/-/=]# [/#/#/#] > the range or range modifier of the weapon. Ranged weapons use PB / S / M / L |
The number of attacks per round, n:, can be an integer or a fraction such as 3/2 meaning 3 attacks every 2 rounds. If using the InitMaster API the Tracker will have the correct number of entries for the Character relating to the number of attacks in the current round.
' + +'The range for the weapon, r:, can be a single integer (representing the range of a melee weapon or simple ranged weapon) or a range modifier, starting with +, -, or =. The range modifier will amend the range of the ammo for a ranged weapon - ranged weapons vary their range with the ammo used. The weapon can use that range or modify it. Ranged weapon range modifiers can be of the form [[+/-]#/][+/-]#/[+/-]#/[+/-]# which will add or subtract a different modifier for each range ([Point Blank] / Short / Medium / Long - Point Blank range is optional)
' + +'DmgData = [w:Longsword, sb:1, +:0, SM:1d8, L:1d12]' + +'
The DmgData section specifies the data relating to the damage done by the weapon, and relates to melee weapons only (not ranged weapons). These fields can be in any order.
' + +'w: | <text> the name to display for damage calculations for this weapon |
sb: | <0/1> strength bonus flag - specifies if the strength bonus is applicable to the Damage roll |
+: | <[+/-]#> the magical damage bonus/penalty - an integer of any size |
sm: | |
l: |
There are other optional fields that can be specified for To-Hit and/or Dmg data: see Section 4 below for complete details of all possible values.
' + +'&{template:RPGMweapon}{{name=Bastard Sword+1}}{{subtitle=Magic Sword}}{{Speed=[[5]]}}{{Size=Medium}}{{Weapon=1 or 2-handed melee long blade}}Specs=[Bastard-Sword,Melee,1H,Long-blade],[Bastard-Sword,Melee,2H,Long-blade]{{To-hit=+1 + Str Bonus}}ToHitData=[w:Bastard Sword+1, sb:1, +:1, n:1, ch:20, cm:1, sz:M, ty:S, r:5, sp:6,rc:uncharged],[w:Bastard Sword 2H+1, sb:1, +:1, n:1, ch:20, cm:1, sz:M, ty:S, r:5, sp:8,,rc:uncharged]{{Attacks=1 per round + specialisation & level, Slashing}}{{Damage=+1, 1-handed SM:1d8 L:1d12, 2-handed SM:2d4 L:2d8}}DmgData=[w:Bastard Sword+1,sb:1,+:1,SM:1d8,L:1d12],[w:Bastard Sword 2H+1,sb:1,+:1,SM:2d4,L:2d8]{{desc=This is a normal magical sword. The blade is sharp and keen, and is a +[[1]] magical weapon at all times.}}
' + +'The Bastardsword can be used either single handed or two handed with different to-hit and damage outcomes. This can be represented in the macro as shown here, with multiple specification sections. When using the AttackMaster API !attk --weapon command to take the Bastardsword in hand, choosing 1 hand (either left or right) will use the 1-handed specifications, and choosing to take it in Both Hands will use the 2-handed specifications.
' + +'All the field definitions are the same as for the Longsword example above, but there are (in this case) two sets of data for each section, the first set for 1-handed, the second set for 2-handed (as defined by the handedness entry in the Specs section data sets.
' + +'&{template:RPGMweapon}{{name=Longbow}}{{subtitle=Bow}}{{Speed=[[8]]}}{{Size=Medium}}{{Weapon=Ranged 2-handed Bow}}Specs=[Longbow,Ranged,2H,Bow]{{To-hit=+0 + Dex Bonus}}ToHitData=[w:Longbow,sb:0,db:1,+:0,n:2,ch:20,cm:1,sz:L,ty:P,sp:8]{{Attacks=Piercing, 2 per round}}{{desc=This is a normal longbow. The wood is polished, the string taut, but nothing special.}}
' + +'A ranged weapon like a Longbow uses the same data section definitions as melee weapons except for the following additions and differences.
' + +'ToHitData=[w:Longbow,sb:0,db:1,+:0,n:2,ch:20,cm:1,sz:L,ty:P,sp:8]' + +'
The To-Hit section has an extra option:
' + +'db: | <0/1> dexterity bonus flag - specifies if the dexterity bonus is applicable to the To-Hit roll. |
r: | the range data is not provided because this weapon does not modify the range of its ammo, but could be provided if required. |
There is no DmgData section, as damage is defined by the ammo.
' + +'&{template:RPGMweapon}{{name=Shortbow of Targeting}}{{subtitle=Bow}}{{Speed=7/6/5/3}}{{Size=Medium}}{{Weapon=2-handed ranged bow }}Specs=[Shortbow,Ranged,2H,Bow],[Shortbow,Ranged,2H,Bow],[Shortbow,Ranged,2H,Bow],[Shortbow,Ranged,2H,Bow]{{To-hit=Dexterity bonus + 0 charges = normal bow
'
+ +'1 charge = +1 to hit
'
+ +'2 charges = +2 to hit
'
+ +'3 charges = +4 to hit}}ToHitData=[w:Shortbow +0,sb:0,db:1,+:0,n:2,ch:20,cm:1,sz:M,ty:P,sp:7,c:0,rc:recharging],[w:Shortbow +1,sb:0,db:1,+:1,n:2,ch:20,cm:1,sz:M,ty:P,sp:6,c:1,rc:recharging],[w:Shortbow +2,sb:0,db:1,+:2,n:2,ch:20,cm:1,sz:M,ty:P,sp:5,c:2,rc:recharging],[w:Shortbow +4,sb:0,db:1,+:4,n:2,ch:20,cm:1,sz:M,ty:P,sp:3,c:3,rc:recharging],{{Attacks=2 per round, no increases, Piercing}}{{desc=This shortbow has a charged magical targeting sight which incorporates a zoom dial. Increasing the zoom uses more magical charges, but improves the attack roll: 1 charge = +1, 2 charges = +2 and 3 charges = +4. Extra charges also speed up the bow as it is easier to draw the shot. The zoom can be returned to zero, and the bow shot as a notrmal shortbow.
'
+ +'Once all charges are expended, the bow continues to operate as a normal bow, and the charges will be regained after a long rest.}}
This ranged weapon has magical capabilities which require charges to be expended, though it can be used as a normal weapon without expending any charges. The database definition uses the multiple attack specifications as per the Bastardsword above, but this time to create additional rows in the Attack menu with different attack speeds and damage values that use different charges.
' + +'c: | # | The number of charges expended by an attack. Defaults to 1, and does not apply to uncharged items |
When shown in the Attack menu, any version of the weapon which requires more charges than it currently has will be gray, and will not be selectable for an attack.
' + +'&{template:RPGMammo}{{name=Flight Arrow+2}}{{subtitle=Magic Weapon}}{{Speed=As per bow}}{{Size=Small}}Specs=[Flight-Arrow,Ammo,1H,Arrow],[Flight-Arrow,Ammo,1H,Arrow]{{Ammo=+2,
'
+ +'**Warbow** vs. SM:1d8, L:1d8,
'
+ +'**Other Bows** vs. SM:1d6, L:1d6, Piercing}}AmmoData=[w:Flight Arrow+2, st:Bow, sb:1, +:2, SM:1d6, L:1d6],[w:Warbow Flight Arrow+2, t:warbow, sb:1, +:2, SM:1d8,L:1d8]{{Range=PB:30, others vary by bow
'
+ +'**Shortbow:**
'
+ +'S:50, M:100, L150,
'
+ +'**Longbow:**
'
+ +'S:60, M:120, L:210,
'
+ +'**Warbow:**
'
+ +'S90, M:160, L:250,
'
+ +'**Composite Sbow:**
'
+ +'S:50, M:100, L:180,
'
+ +'**Composite Lbow:**
'
+ +'S:70, M:140, L:210}}RangeData=[t:longbow, +:2, r:3/6/12/21],[t:shortbow, +:2, r:3/5/10/15],[t:warbow, +:2, r:3/9/16/25],[t:compositelongbow, +:2, r:3/7/14/21],[t:compositeshortbow, +:2, r:3/5/10/18]{{desc=A magical Flight Arrow of very fine quality}}
Ammo has a different specification, as the To-Hit data sections are obviously part of the ranged weapon data. Instead it provides data on which weapons this can be ammo for, and what ranges it has for each. To determine this, it uses the weapon type and group-type set in the weapon Specs section.
' + +'AmmoData=[w:Flight Arrow+2, st:Bow, sb:1, +:2, SM:1d6, L:1d6],[w:Warbow Flight Arrow+2, t:warbow, sb:1, +:2, SM:1d8,L:1d8]' + +'
The AmmoData section is mostly the same as the DmgData sections (order of fields is immaterial and spaces, hyphens and underscores ignored in type and supertype names), but repeated data sets relate to the data for different types of weapon, and in addition:
' + +'t: | |
st: |
RangeData=[t:longbow, +:2, r:3/6/12/21],[t:shortbow, +:2, r:3/5/10/15],[t:warbow, +:2, r:3/9/16/25], [t:compositelongbow, +:2, r:3/7/14/21],[t:compositeshortbow, +:2, r:3/5/10/18],[st:bow, +:2, r:3/5/10/15]' + +'
The RangeData section has one or more data sets relating to weapons that result in different ranges. The range specifications can have 3 or 4 parts: if 4, the first is for Point Blank range which is only relevant for specialists; the remaining 3 are always short, medium & long ranges. The ranges are normally specified as they are in the Player\'s Handbook, with 1 representing 10 etc. However, if the Short range is specified as 10 or greater, or the ranges are preceeded by \'=\' (e.g. r:=5/10/20), the ranges will be taken as literal and not multiplied by 10.
' + +'&{template:RPGMweapon}{{name=Warhammer}}{{subtitle=Hammer/Club}} {{Speed=[[4]]}}{{Size=Medium}}{{Weapon=1-handed melee or thrown club}}Specs=[Warhammer,Melee,1H,Clubs],[Warhammer,Ranged,1H,Clubs]{{To-hit=+0 + Str & Dex bonus}}ToHitData=[w:Warhammer, sb:1, +:0, n:1, ch:20, cm:1, sz:M, ty:B, r:5, sp:4],[ w:Warhammer, sb:1, db:1, +:0, n:1, ch:20, cm:1, sz:M, ty:B, sp:4]{{Attacks=1 per round + level & specialisation, Bludgeoning}}{{Damage=+0, vs SM:1d4+1, L:1d4, + Str bonus}}DmgData=[ w:Warhammer, sb:1, +:0, SM:1+1d4, L:1d4][]{{Ammo=+0, vs SM:1d4+1, L:1d4, + Str bonus}}AmmoData=[w:Warhammer,t:Warhammer,st:Throwing-club,sb:1,+:0,SM:1+1d4,L:1d4]{{Range=S:10, M:20, L:30}}RangeData=[t:Warhammer,+:0,r:1/2/3]{{desc=This is a normal warhammer. The blade is sharp and keen, but nothing special.}}
' + +'A melee weapon that can also be thrown, and is its own ammunition, is termed a "self-ammoed" weapon. Its definition combines the data elements of both melee weapons, ranged weapons and ammunition.
' + +'Specs=[Warhammer,Melee,1H,Clubs],[Warhammer,Ranged,1H,Clubs]' + +'
Has two Specs data sets, one as a melee weapon and one as a ranged weapon.
' + +'ToHitData=[w:Warhammer, sb:1, +:0, n:1, ch:20, cm:1, sz:M, ty:B, r:5, sp:4],[ w:Warhammer, sb:1, db:1, +:0, n:1, ch:20, cm:1, sz:M, ty:B, sp:4]' + +'
ToHitData also has two sets of data, each of which relates to the corresponding Specs set.
' + +'DmgData=[ w:Warhammer, sb:1, +:0, SM:1+1d4, L:1d4],[]' + +'
DmgData does have two data sets, but the one corresponding to the ranged data is empty, as this data is in the Ammo data set.
' + +'AmmoData=[w:Warhammer,t:Warhammer,st:Throwing-club,sb:1,+:0,SM:1+1d4,L:1d4]' + +'
There is only 1 Ammo data set, as it only relates to the one weapon, itself.
' + +'RangeData=[t:Warhammer,+:0,r:1/2/3]' + +'
And only 1 Range data set, as it only relates to itself.
' + +'Important Note: Magical self-Ammoed / Thrown weapons must only have the magical bonus (or penalty for cursed items) defined in the AmmoData +: attribute. This will be added both to the To-Hit calculation and to the Damage calculation. The ToHitData +: attribute should be set to +:0 or not specified at all (defaults to 0) as otherwise the To-Hit calculation will gain both a plus from the ToHit data and also from the AmmoData. This is equivalent to having a separate ranged weapon (such as a bow) with +:0 (the normal state) firing magical ammunition (such as Flight Arrow+1).
' + +'Some ammunition (and also Self-ammoed weapons) behave differently to normal ammo when used: some breaks and cannot be retrieved (e.g. glass arrows), others magically return to the thrower\'s hand, and yet others change state when used (such as an unfolding net). These types of ammunition use the reuse attribute in their AmmoData section:
' + +'ru: | [-]# | Defines the reusability or altering state of the ammunition |
---|
The reuse attribute can take the following values:
' + +'Value | Example | Description |
---|---|---|
-2 | Staff-Spear | The ammunition and weapon will be deleted from all weapon tables when thrown as a ranged weapon, representing a non-returning magically charged thrown weapon, only recovered by using again as a magic item |
-1 | Glass Arrow | The ammunition will break on use, and is not recoverable |
0 | Flight Arrow | The default value. The ammunition will behave normally, reducing by 1 on use, and recoverable if the DM agrees |
1 | Whelm (magic Warhammer) | The ammunition quantity does not reduce with use. The ammo magically returns by itself |
2 | Spitting Snake Venoms | The ammunition is one of several possible for the weapon, but when one is used it becomes the only type available, not reducing, while the others all become 0 quantity |
3 | Net (Folded to Unfolded) | The ammunition has two or more states. The selected ammo will reduce by 1 and the other states will increase by 1 |
Generally, it is not worth wielding the average 1-Handed weapon with both hands - you gain no advantage and lose use of the other hand. Clearly, there are some exceptions like a Basterd Sword which is designed to be used as either 1-handed or 2-handed and gains extra damage from doing so.
' + +'However: certain 1-handed weapons gain benefits when a character has proficiency in the Two-Hander Fighting Style, and perhaps with other (custom) fighting styles the DM chooses to set up. However, those without these proficiencies should not gain benefit from 2-handed use of these weapons. To achieve this outcome, these weapons need to be specified in a particular fashion in the Weapons Database. Here is an example:' + +'&{template:RPGMweapon}{{name=Battle Axe}} {{subtitle=Axe}} {{Speed=[[7]]}} {{Size=Medium}} {{Weapon=1-handed melee axe}} Specs=[Battle-Axe,Melee,1H,Axe],[Battle-Axe,Melee,2H,Axe] {{To-hit=+0 + Str Bonus}} ToHitData=[w:Battle Axe,sb:1,+:0,n:1,ch:20,cm:1,sz:M,ty:S,r:5,sp:7,rc:uncharged] {{Attacks=1 per round + specialisation & level, Slashing}} {{Damage=+0, SM:1d8, L:1d12 + Str Bonus}} DmgData=[w:Battle Axe,sb:1,+:0,SM:1d8,L:1d12] {{desc=A standard Battle Axe of good quality, but nothing special}}
' + +'Here it can be seen that there are two data sets specified for the Specs field and only one data set specified for the ToHitData. Doing this tells the APIs that this weapon can be taken in both hands, but generally will not gain any different advantages. If a Character is proficient or specialised in Two-Hander Fighting Style, however, the APIs will see that this is a 1-handed weapon held in both hands, and allocate it the correct benefits. But only certain weapons gain these benefits, so only certain weapons in the database should be set up this way.
' + +'Only one dancing weapon is defined in the Dungeon Master\'s Guide, the "Sword of Dancing". However, with RPGMaster APIs, any weapon can be made to dance including ranged weapons:
' + +'&{template:RPGMweapon}{{title=Longsword of Dancing}} {{subtitle=Magical Sword}}{{Speed=[[5]]}} {{Size=Medium}}WeapData=[d:+1/4]{{Weapon=Dancing 1-handed melee long-blade}}Specs=[Longsword,Melee,1H,Long-blade]{{To-hit=+1/2/3/4 on sequential rounds + Str bonus}}ToHitData=[w:Longsword of Dancing, sb:1, +:0, n:1, ch:20, cm:1, sz:M, ty:S, r:5, sp:5]{{Attacks=1 per round + level & specialisation, Slashing}}{{Damage=+1/2/3/4 on sequential rounds, vs SM:1d8, L:1d12, + Str bonus}}DmgData=[w:Longsword, sb:1, +:0, SM:1d8, L:1d12]{{desc=This is a very special sword. It is etched with dramatic battle scenes, almost balletic in grace and poise.}}
' + +'This weapon will automatically be identified as a "dancing weapon" when taken In-hand using the Attk Menu > Change Weapon dialog. The necessary data attribute to be placed in the WeapData section is shown below:
' + +'d: | [+/-]# [|#] | Defines the change in magical plus per round, optionally followed by a pipe and number of dancing rounds (defaults to 4) |
---|
Such a weapon must be held in-hand (and presumably used to attack) for a number of rounds before it will dance, with the magical plus of the weapon changing by a defined amount each round (which can be positive, zero or negative). Once that number of rounds has passed, the weapon will automatically start dancing, and the player will be presented with the Change Weapon dialog to select a new weapon to take in hand. The dancing weapon will automatically be given an initiative roll each round while dancing, the magical plus will be reset to that defined for the first round in the specification and will again increment round on round. Once the number of rounds has again passed, the weapon will stop dancing: the player will need to take the weapon in-hand again to restart the sequence.
' + +'Note: the magical plus of the weapon on the first round in the sequence will always be that specified by the +:# attribute incremented by the dancing increment from the d:[+/-]# attribute. Thus a ToHitData and DmgData entru of +:0 and a WeapData specification of d:+1/4 will result in the first round plus being +1.
' + +'Dancing weapons work by using Effects managed by the RoundMaster API, which needs to be loaded for them to work. This weapon definition will automatically create the required effects for RoundMaster to use (using the !rounds --dancer command), which will work in a standard way, following the approach for a "Sword of Dancing" defined in the DMG. If you want a specialised dancing weapon using a different approach, you will need to define your own dancing weapon effects using the information in the Effects Database Help handout.
' + +'&{template:RPGMweapon}{{name=Jim the Sun Blade
'
+ +'Intelligent, Neutral}}{{subtitle=Magic Sword}}{{Speed=[[3]]}}WeapData=[w:Jim the Sun Blade,ns:5][cl:PW,w:Jims-Locate-Object,sp:100,lv:6,pd:1],[cl:PW,w:Jims-Find-Traps,sp:5,lv:6,pd:2],[cl:PW,w:Jims-Levitation,sp:2,lv:1,pd:3],[cl:PW,w:Jims-Sunlight,sp:3,lv:6,pd:1],[cl:PW,w:Jims-Fear,sp:4,lv:6,pd:2]{{Size=Special (feels like a Shortsword)}}{{Weapon=1 or 2 handed melee Long or Short blade}}Specs=[Bastard-sword|Short-sword,Melee,1H,Long-blade|Short-blade],[Bastard-sword|Short-sword,Melee,1H,Long-blade|Short-blade],[Bastard-sword,Melee,2H,Long-blade],[Bastard-sword,Melee,2H,Long-blade]{{To-hit=+2, +4 vs Evil + Str Bonus}}ToHitData=[w:Jim +2,sb:1,+:2,n:1,ch:20,cm:1,sz:M,ty:S,r:5,sp:3],[w:Jim vs Evil+4,sb:1,+:4,n:1,ch:20,cm:1,sz:M,ty:S,r:5,sp:3],[w:Jim 2H +2,sb:1,+:2,n:1,ch:20,cm:1,sz:M,ty:S,r:5,sp:3],[w:Jim 2H vs Evil+4,sb:1,+:4,n:1,ch:20,cm:1,sz:M,ty:S,r:5,sp:3]{{Attacks=1 per round}}{{Damage=+2, +4 vs Evil, + 1-handed SM:1d8 L:1d12, 2-handed SM:2d4 L:2d8}}DmgData=[w:Jim+2,sb:1,+:2,SM:1d8,L:1d12],[w:Jim vs Evil+4,sb:1,+:4,SM:2d4,L:2d8],[w:Jim 2H +2,sb:1,+:2,SM:1d8,L:1d12],[w:Jim 2H vs Evil+4,sb:1,+:4,SM:2d4,L:2d8]{{desc=An intelligent weapon: A Sun Blade called Jim (DMs Guide Page 185). It is Neutral. It needs its owner to be proficient with either a Short or Bastard Sword or promise to get such proficiency as soon as possible. It cannot be used by someone who is not proficient. It requires its owner to be Neutral on at least one of its axis, and may not be Evil. NG LN CN and of cause true N are all ok. Abilities:
'
+ +'**1:** It is +2 normally, or +4 against evil creatures, and does Bastard sword damage.
'
+ +'**2:** It feels and react as if it is a short sword and uses short sword striking time.
'
+ +'**3:** [Locate Object](!magic --mi-power @{selected|token_id}|Jims-Locate-Object|Jim-the-Sun-Blade|6) at [[6]]th Level in 120\' radius (1x day).
'
+ +'**4:** [Detect traps](!magic --mi-power @{selected|token_id}|Jims-Find-Traps|Jim-the-Sun-Blade|6) of large size in 10\' radius (2xday).
'
+ +'**5:** [Levitation](!magic --mi-power @{selected|token_id}|Jims-Levitation|Jim-the-Sun-Blade|1) 3x a day for 1 turn (cast at 1st Level).
'
+ +'**6:** [Sunlight](!magic --mi-power @{selected|token_id}|Jims-Sunlight|Jim-the-Sun-Blade|6) Once a day, upon command, the blade can be swung vigorously above the head, and it will shed a bright yellow radiance that is like full daylight. The radiance begins shining in a 10-foot radius around the sword-wielder, spreading outward at 5 feet per round for 10 rounds thereafter, creating a globe of light with a 60-foot radius. When the swinging stops, the radiance fades to a dim glow that persists for another turn before disappearing entirely.
'
+ +'**7:** It has a special purpose namely Defeat Evil.
'
+ +'**8:** On hitting an Evil being it causes [Fear](!magic --mi-power @{selected|token_id}|Jims-Fear|Jim-the-Sun-Blade|6) for 1d4 rounds (unless saving throw is made). It can do this **twice a day** when the wielder desires.
'
+ +'**9:** It speaks Common and its name is Jim. It will talk to the party.
'
+ +'**10:** It has an ego of 16 and is from Yorkshire.
'
+ +'**11:** It will insist on having a Neutral wielder. (See Intelligent weapons on page 187 in DMG).
'
+ +'**12:** If picked by a player, it will be keen to become the players main weapon.
'
+ +'**13:** If picked up by a player who is not Neutral it will do them 16 points of damage}}
An artefact such as an intelligent sword with powers introduces data sets that specify the powers that the artefact has and how often they can be used. These match the API Buttons with calls to the MagicMaster API to enact the powers.
' + +'WeapData=[w:Jim the Sun Blade,ns:5][cl:PW,w:Jims-Locate-Object,sp:100,lv:6,pd:1],[cl:PW,w:Jims-Find-Traps,sp:5,lv:6,pd:2],[cl:PW,w:Jims-Levitation,sp:2,lv:1,pd:3],[cl:PW,w:Jims-Sunlight,sp:3,lv:6,pd:1],[cl:PW,w:Jims-Fear,sp:4,lv:6,pd:2]
' + +'The WeapData data sets can be used to define the powers that an artefact has (or stored spells - see MagicMaster API for more information on spell storing)
' + +'1st data set:
' + +'w: | <text> The name of the weapon (not currently used) |
ns: | <#> The number of spells or powers for which the specifications follow |
Subsequent data sets:
' + +'cl: | < MU / PR / PW > The type of data: MU=Wizard, PR=Priest, PW=Power |
w: | <text> Name of the spell or power: must be the same as the corresponding database definition |
sp: | <#> Speed of the spell/power casting in segments (1/10ths of a round) |
lv: | <#> The level at which the artefact will cast the spell/power (if omitted will use character\'s level) |
pd: | <-1 / #> Number per day, or -1 for "use at will" |
&{template:RPGMwandSpell}{{title=Rod of Alertness}}Specs=[Rod of Alertness,Melee,1H,Clubs],[Rod of Alertness,Wand,1H,Conjuration-Summoning]{{splevel=Footman\'s Mace/Rod}}WeapData=[w:Rod of Alertness,wt:10,on:\\api;modattr --charid @{selected|character_id} --fb-public --fb-header @{selected|character_name} acts faster! --fb-content _CHARNAME_ improves their initiative rolls by 1 --init-mod|-1,off:\\api;modattr --charid @{selected|character_id} --fb-public --fb-header @{selected|character_name} acts more slowly --fb-content _CHARNAME_\'s initiative rolls return to normal --init-mod|+1 ]{{school=Conjuration/Summoning}}ToHitData=[w:Rod of Alertness,sb:1,+:1,n:1,ch:20,cm:1,sz:M,ty:B,r:5,sp:10,rc:uncharged]{{components=M}}DmgData=[w:Rod of Alertness,sb:1,+:1,SM:1+1d6,L:1d6]{{Time=[[10]]}}WandData=[w:Rod of Alertness,wt:10,sp:10,rc:uncharged,loc:left hand|right hand]{{range=Special}}{{duration=Special}}{{aoe=Special}}{{save=Special}}{{effects=This magical rod is indistinguishable from a footman\'s mace +1. It has eight flanges on its macelike head. The rod bestows +1 to the possessor\'s die roll for being *surprised*, and in combat the possessor gains -1 on initiative die rolls. If it is grasped firmly, the rod enables the character to ...
'
+ +'the rest of the specification of this item is not important here
Another pair of attributes that can be included in the 1st WeapData data set specify simple commands to execute when the weapon is taken "in-hand" or "sheathed" using the Change Weapon menu. Only simple commands can be specified this way which do not include any multi-line commands or Roll20 macro calls (for more complex actions when taking in-hand or sheathing weapons, see the [Effects Database Help] for the "-inhand" and "-sheathed" event macros). The example above changes the wielder\'s initiative modifier by an improvement of 1 when the Rod of Alertness is taken in-hand, using the Chat Set Attr API !modattr command, and then reduces it back by 1 when the Rod is sheathed.
' + +'on: | <command> | A simple command that can be expressed on a single line, executed when the weapon is taken "in-hand" using the Change Weapon menu |
off: | <command> | A simple command that can be expressed on a single line, executed when the weapon is "sheathed" (replaced with a different item) using the Change Weapon menu |
As with executing a command when drawing or sheathing a weapon, simple commands which do not include multiple lines or Roll20 macro calls can be included in the ToHitData specification for attack commands, in the DmgData specification for melee weapon damage commands, and in the AmmoData specification for ranged weapon damage commands.
' + +'cmd: | <command> | A simple command that can be expressed on a single line, execution occuring at a point determined by which data set the tag is in |
A typical use of such a command is in the ToHitData or DmgData of a weaponised spell:
' + +'&{template:RPGMspell}{{title=@{selected|Casting-name} casts Wither as a level @{selected|Casting-Level} caster}}{{splevel=Level 7 Priest}}{{school=Necromancy (Reversable)}}{{sphere=Necromantic}}Specs=[Wither,Innate-Melee|PRspellL7,1H,Necromancy]{{components=V,S,M}}{{range=Touch attack}}{{time=[[1]] round}}{{duration=Permanent}}{{aoe=Creature Touched}}ToHitData=[w:Wither,sp:1,sb:0,ty:SPB,r:5,msg:A successful hit withers the member or organ touched ceasing to function in 1 round and dropping off into dust in \\lbrak;2d4 turns\\rbrak;\\lpar;!rounds ~~target single¦`{selected¦token_id}¦@{target¦Who\'s member will wither?¦token_id}¦Wither¦[\\lbrak;10*2d4\\rbrak;]¦-1¦Touched limb / member / organ is withering and turning to dust¦back-pain\\rpar;,cmd:!attk ~~blank-weapon `{selected¦token_id}¦Wither¦silent]{{save=None}}DmgData=[w:Wither,sb:0,SM:0,L:0,msg:A successful hit withers the member or organ touched ceasing to function in 1 round and dropping off into dust in \\lbrak;2d4 turns\\rbrak;\\lpar;!rounds ~~target single¦`{selected¦token_id}¦@{target¦Who\'s member will wither?¦token_id}¦Wither¦[\\lbrak;10*2d4\\rbrak;]¦-1¦Touched limb / member / organ is withering and turning to dust¦back-pain\\rpar;]{{reference=PHB p234}}SpellData=[w:Wither,lv:7,sp:1,gp:0.1,cs:VSM,sph:Necromantic]{{Use=Take the spell in-hand when casting, and attack with it. Called shots attract penalties to hit.}}{{effects=Causes the member or organ touched to cease functioning in one round, dropping off into dust in 2d4 turns. Creatures must be touched for the harmful effect to occur.}}{{materials=A prayer device and unholy water}}
' + +'In this case, the command !attk --blank-weapon @{selected|token_id}|Wither|silent is included in the ToHitData specification and removes the spell as a weapon as soon as the caster attacks, whether successful or not. If this command had, instead, been included in the DmgData specification, the spell would have remained a usable weapon until the touch was successful and the damage done.
' + +'It is possible to create weapon (and armour) definitions at have configurable elements, set when the item is first added to a container or a character. This is achieved using the query: attribute in the "WeapData" or "ACdata" section of the item. An example of its use is the Magical Javelin which uses a query to ask what magical plus that armour might grant, including negative / cursed values.
' + +'&{template:RPGMweapon}{{prefix=^^weaponMagic#2^^}}{{title=Javelin}}{{name=^^weaponMagic#0^^}}Specs=[Javelin,Melee,1H,Spears], [Javelin,Melee,2H,Spears], [Javelin,Ranged,1H,Throwing-Spears]{{}}WeapData=[w:Javelin, query:weaponMagic=How magical is this weapon?|+0%%0/ |-4%%-4/Cursed |-3%%-3/Cursed |-2%%-2/Cursed |-1%%-1/Cursed |0%%0/ |+1%%1/ |+2%%2/ |+3%%3/ |+4%%4/, st:Javelin, +:^^weaponMagic#1^^, rc:^^weaponMagic#2^^]{{}}ToHitData=[w:Javelin^^weaponMagic#0^^, +:^^weaponMagic#1^^, sb:1, n:1, ch:20, cm:1, sz:M, ty:P, r:5, sp:4],[w:Javelin 2H^^weaponMagic#0^^, +:^^weaponMagic#1^^, sb:1, n:1, ch:20, cm:1, sz:M, ty:P, r:5, sp:4],[w:Javelin^^weaponMagic#0^^, +:0, sb:1, db:1, n:1, ch:20, cm:1, sz:M, ty:P, sp:4]{{}}DmgData=[w:Javelin^^weaponMagic#0^^, +:^^weaponMagic#1^^, sb:1, SM:1d4, L:1d4],[w:Javelin 2H^^weaponMagic#0^^, +:^^weaponMagic#1^^, sb:1, SM:1d6, L:1d6, msg:Does double damage if set against charge],[]{{}}AmmoData=[w:Javelin^^weaponMagic#0^^, +:^^weaponMagic#1^^, t:Javelin, st:Spear, sb:1, SM:1d4, L:1d4]{{}}RangeData=[t:Javelin, +:^^weaponMagic#1^^, r:2/2/4/6]{{}}%{MI-DB|Weapon-Info}{{}} %{MI-DB|Magical-Weapon-Info}{{subtitle=^^weaponMagic#2^^ Spear}} {{subtitle=Spear}} {{Speed=[[4]]}} {{Size=Medium}} {{Weapon=1-or 2-handed ^^weaponMagic#2^^ melee or thrown spear}} {{To-hit=^^weaponMagic#0^^ + Str Bonus}}{{Attacks=1 per round + level & specialisation, Piercing}}{{Damage=^^weaponMagic#0^^, 1H vs SM:1d4, L:1d4, 2H vs SM:1d6, L:1d6 + Str bonus}} {{Ammo=^^weaponMagic#0^^, vs SM:1d4, L:1d4 + Str bonus}} {{Range=PB:20 S:20 M:40 L:60}} {{Looks Like=Javelins are classified as light spears, suitable for melee or missile combat, usable either on horseback or on foot. Javelins may be used either one- or two-handed, and like the harpoon, there is no difference in speed factor between the two styles.}} {{desc=This is an exceptional Javelin. It is light and has a sharp point, and might be special in some way or other.}}
' + +'The query: attribute in the WeapData section defines the questions that will be asked in standard Roll20 Roll Queries. In this case, a question about the magical bonus (or penalty) is asked using the following format:
' + +'result-tag=query question|option 1 text%%value 1.1/value 1.2/.../value 1.n|option 2 text%%value 2.1/value 2.2/.../value 2.n|...%%.../.../...|option j text%%value j.1/value j.2/.../value j.n' + +'
Multiple queries can be concatinated, separated by \'$$\'. Each query posts the option texts in a list. The selected option will then provide the values that substitute dynamic attributes in the data section, which are specified with the syntax ^^result-tag#n^^
where \'n\' is the value index - index 0 is the option text itself. Using the Magical Javelin example, selecting a cursed weapon penalty of -2 replace the following dynamic attributes:
w:Javelin^^weaponMagic#0^^ | w:Javelin-2 |
---|---|
+:^^weaponMagic#1^^ | +:-2 |
rc:^^weaponMagic#2^^ | rc:Cursed |
In fact, there are three pre-defined weapon queries that can be used after the query: attribute that will save some typing:
' + +'Attribute | Resolves to |
---|---|
query:weaponPlus | query:weaponPlus=How magical is this weapon?|+0%%0/|+1%%1/|+2%%2/|+3%%3/|+4%%4/ |
query:weaponMagic | query:weaponMagic=How magical is this weapon?|+0%%0/|-4%%-4/Cursed|-3%%-3/Cursed|-2%%-2/Cursed|-1%%-1/Cursed|0%%0/|+1%%1/|+2%%2/|+3%%3/|+4%%4/ |
query:weaponCurse | query:weaponCurse=How cursed is this weapon?|-0%%0/Cursed/|-1%%-1/Cursed|-2%%-2/Cursed|-3%%-3/Cursed|-4%%-4/Cursed |
query:swordType | swordType=What type of sword?|Longsword%%M/S/5/1d8/1d12/M/S/5/1d8/1d12|Broadsword%%M/S/5/2d4/1+1d6/M/S/5/2d4/1+1d6|Bastard-Sword%%M/S/6/1d8/1d12/M/S/8/2d4/2d8|Khopesh%%M/S/9/2d4/1d6/M/S/9/2d4/1d6|Shortsword%%S/P/3/1d6/1d8/S/P/3/1d6/1d8|Scimitar%%M/S/5/1d8/1d8/M/S/5/1d8/1d8|Two-Handed-Sword%%L/S/10/0/0/L/S/10/1d10/3d6] |
Thus the query: attribute in the example could have been shortened to "query:weaponMagic,".
' + +'The Specs specification cannot use query variables. Thus, as the weapon type can vary if using the swordType pre-defined query shortcut (or a similar construct) there is an alternative approach to specifying the weapon type (and supertype if needed) by using the \'t:\' and \'st\' attributes in the ToHitData section. Each of these can be followed by a literal string defining the type or, more usefully, a query variable (e.g. t:^^swordType#0^^):
' + +'t: | Define the type of weapon for determination of proficiency |
---|---|
st: | Define the tight weapon-group of weapon for determination of related weapon proficiency |
These attributes override the values given for the 1st and 4th fields in the Specs section.
' + +'Armour databases are all character sheets that have names that start with MI-DB-Armour (as with weapons, this can be in any database starting with MI-DB- if desired), and can have anything put at the end, though those with version numbers of the form v#.# as part of the name will be ignored.
' + +'As previously stated and as per the weapon and ammunition databases, each armour definition has 3 parts in the database (see Section 1): the Ability Macro, the ct- attribute, and the listing (and occasionally attributes for powers and spells). The quickest way to understand these entries is to examine existing entries. Do extract to the root databases and take a look (but remember to delete them after examination and use the --check-db command to re-index the databases).
' + +'Note:The DM creating new armour entries does not need to worry about anything other than the Ability Macro in the database, as running the !attk --check-db MI-DB-Armour or !magic --check-db MI-DB-Armour command will update all other aspects of the database appropriately for all databases that have a name starting with or including \'MI-DB-Armour\', as long as the Specs and Data fields are correctly defined. Running the command -check-db with no parameters will check and update all databases.
' + +'Here are some examples:
' + +'&{template:RPGMarmour}{{title=Chain Mail }}{{subtitle=Armour}}Specs=[Chain Mail,Armour,0H,Mail]{{}}ACData=[a:Chain Mail, st:Mail, t:Chain-Mail, +S:2, +P:0, +B:-2, +:0, ac:5, sz:L, rc:single-uncharged, qty:1, wt:40, loc:body, rac:Chain Mail, ppa:-25, ola:-10, rta:-10, msa:-15, hsa:-15, dna:-5, cwa:-25, rla:0, lla:0]{{}}%{MI-DB|Armour-Info}{{Armour=Chain Mail}}{{AC=[[5]] vs all attacks}}{{Looks Like=This armor is made of interlocking metal rings.}}{{hide1=Chain mail is always worn with a layer of quilted fabric padding underneath to prevent painful chafing and to cushion the impact of blows. Several layers of mail are normally hung over vital areas. The links yield easily to blows, absorbing some of the shock. Most of the weight of this armor is carried on the shoulders and it is uncomfortable to wear for long periods of time.}}{{desc=Good, sturdy, well made chain but nothing special}}
' + +'The ability specification for this suit of Chain Mail uses a Roll20 Roll Template, in this case defined by the loaded RPGMaster Library. The entries in the Roll Template itself can be anything you desire, giving as much or as little information as you want. However, the important elements for the AttackMaster API are those highlighted. Each of these elements are inserted between the elements of the Roll Template, meaning they will not be seen by the player when the macro is run. Generally spaces, hyphens and underscores in the data elements are ignored, and case is not significant. Each element is described below:
' + +'Specs=[Chain Mail,Armour,0H,Mail]' + +'
The Specs section of the specification has exactly the same format as for weapons and ammunition (and indeed all database items). See section 9 for the definition of the fields.
' + +'Note:The armour Type (the 1st parameter) and Group-Type (the 4th parameter) are used to determine if the character is of a class that can use the armour. Currently implemented types are listed in Section 4.
' + +'Note: Armour that fits on the body generally does not take any hands to hold, and so the third field, Handedness, is set to \'0H\'.
' + +'ACData=[a:Chain Mail, t:Chain-Mail, st:Mail, ac:5, +:0, +S:2, +P:0, +B:-2, sz:L, wt:40, qty:1, rc:single-uncharged, loc:body, rac:Chain Mail, ppa:-25, ola:-10, rta:-10, msa:-15, hsa:-15, dna:-5, cwa:-25, rla:0, lla:0]]' + +'
The Armour Class Data (ACData) section holds data specific to the armour. As with other data sections, fields can be in any order, and spaces, hyphens, underscores and case are ignored.
' + +'a: | < text > the name of the armour to be displayed. Often the same as the Ability. | |||||||||||||||||||||||||||
t: | < armour-type > The specific armour type, often the same as the first parameter of the Specs section. | |||||||||||||||||||||||||||
st: | < group-type > the supertype of the armour, often the same as the fourth parameter of the Specs section. | |||||||||||||||||||||||||||
ac: | <[-]#> the base armour class (excluding magical bonuses) for this type of armour. | |||||||||||||||||||||||||||
+: | <[+/-]#> the magical bonus or penalty of the armour (defaults to 0 if not supplied). | |||||||||||||||||||||||||||
+s: | <[-/+]#> The magical adjustment specifically against slashing damage. | |||||||||||||||||||||||||||
+p: | <[-/+]#> The magical adjustment specifically against piercing damage. | |||||||||||||||||||||||||||
+b: | <[-/+]#> The magical adjustment specifically against bludgeoning damage. | |||||||||||||||||||||||||||
+m: | <[-/+]#> The adjustment that the armour gives vs. missiles and ammunition of ranged weapons. | |||||||||||||||||||||||||||
db: | <[0/1]> A 1 means dexterity AC bonus combines with armour, 0 means armour prevents dexterity bonus from applying. | |||||||||||||||||||||||||||
sz: | <[T/S/M/L/H]> The size of the item (not necessarily indicating its fit). | |||||||||||||||||||||||||||
wt: | <#> The weight of the item in lbs (could be considered kg - or any measure - if everything is the same). | |||||||||||||||||||||||||||
qty: | < # > Default quantity that is normally added to a character sheet. | |||||||||||||||||||||||||||
rc: | ||||||||||||||||||||||||||||
loc: | < Location > The location on a charater where the item is stored. | |||||||||||||||||||||||||||
rac: | < Thievish armour type > New The name to use for armour type on the Rogue Skills Table. | |||||||||||||||||||||||||||
ppa: | <[-/+]#> New The penalty/benefit wearing this armour gives to picking pockets. | |||||||||||||||||||||||||||
ola: | <[-/+]#> New The penalty/benefit wearing this armour gives to opening locks. | |||||||||||||||||||||||||||
rta: | <[-/+]#> New The penalty/benefit wearing this armour gives to find/remove traps. | |||||||||||||||||||||||||||
msa: | <[-/+]#> New The penalty/benefit wearing this armour gives to moving silently. | |||||||||||||||||||||||||||
hsa: | <[-/+]#> New The penalty/benefit wearing this armour gives to hiding in shadows. | |||||||||||||||||||||||||||
dna: | <[-/+]#> New The penalty/benefit wearing this armour gives to detecting noise. | |||||||||||||||||||||||||||
cwa: | <[-/+]#> New The penalty/benefit wearing this armour gives to climbing walls. | |||||||||||||||||||||||||||
rla: | <[-/+]#> New The penalty/benefit wearing this armour gives to reading languages. | |||||||||||||||||||||||||||
lla: | <[-/+]#> New The penalty/benefit wearing this armour gives to ledgend lore. |
Melee | Melee weapon which strikes while in hand |
Ranged | Weapon that causes damage when thrown or with ammunition |
Innate-Melee or Innate-Ranged | Weapons that do not get a proficiency penalty. |
Ammo | Ammunition for a ranged weapon of a specific Type or Group-Type |
Magic | A magical attack from a magic item power or function |
0H | A weapon that does not take a hand (e.g. spike on helm) |
1H | A weapon that is 1-handed, such as a short sword |
2H | A weapon that takes 2 hands to wield, such as a longbow |
3H | A weapon that takes 3 hands... |
4H | Etc (e.g. a siege weapon that needs 2 people to operate it) |
... | ... |
Weapon Group-Types determine related weapons for weapon proficiency, and whether it can be used by a Character of a specific class. The APIs use the definitions in the AD&D2e Fighter\'s Handbook section on \'Tight Groups\', extended to cover certain additional weapons and weapon types. Those implemented so far for the Weapon databases are:
' + +'Arrow | Club | Great-Blade | Long-Blade | Short-Blade | Whip |
Axe | Crossbow | Hook | Medium-Blade | Sling | |
Blowgun | Dart | Horeshoes | Pick | Spear | |
Bow | Fencing-Blade | Innate | Polearm | Staff | |
Bullet | Flail | Lance | Quarrel | Throwing-Blade |
Types and Group-Types that can be used by various Character Classes are defined in the Class-DB class database for each class type:
' + +'Warrior | Any |
Fighter | Any |
Ranger | Any |
Paladin | Any |
Beastmaster | Any |
Barbarian | Any |
Defender | "axe", "club", "flail", "long-blade", "fencing-blade", "medium-blade", "short-blade", "polearm" |
Wizard | (all types) "dagger", "staff", "dart", "knife", "sling" |
Priest / Cleric | "club", "mace", "hammer", "staff" |
Druid | "club", "sickle", "dart", "spear", "dagger", "scimitar", "sling", "staff" |
Healer | "club", "quarterstaff", "mancatcher", "sling" |
Priest of Life | "club", "quarterstaff", "mancatcher", "sling" |
Priest of War | Any |
Priest of Light | "dart", "javelin", "spear" |
Priest of Knowledge | "sling", "quarterstaff" |
Shaman | "long-blade", "medium-blade", "short--blade", "blowgun", "club", "staff", "shortbow", "horsebow", "hand-xbow" |
Rogue / Thief | "club", "short-blade", "dart", "hand-xbow", "lasso", "shortbow", "sling", "broadsword", "longsword", "staff" |
Bard | Any |
Assassin | Any |
There is an infinite list of armour types: generally the type is the armour name without any reference to magical plusses, so the Type of Plate-Mail+2 is Plate-Mail. This Type is used to check for types of armour that can be worn by various classes.
' + +'Armour | Any type of armour that does not need to be held to work |
---|---|
Shield | A barrier that is held in hand(s) and defends against one or more attacks from the front |
0H Armour and Shields that are not held in the hand (e.g. a Buckler or a Helm)
'
+ +' 1H Generally a type of Shield that must be held in a hand
'
+ +' 2H Armour and Shields that use two hands, and/or prevent use of those hands for other things
'
+ +' 3H Generally siege engines that shield against attacks... (not yet implemented)
'
+ +' ... etc.
Armour Types and Group Types determine whether the armour can be used by various Character Classes. Restrictions are defined in the Class-DB classes database (see the relevant database handout):
' + +'Warrior | Any |
Fighter | Any |
Ranger | Any |
Paladin | Any |
Beastmaster | Any |
Barbarian | "padded", "leather", "hide", "brigandine", "ring-mail", "scale-mail", "chain-mail", "shield", "ring", "magic-item","cloak" |
Defender | Any |
Wizard (all types) | "magic-item", "ring", "cloak" |
Priest / Cleric | Any |
Druid | "leather", "padded", "hide", "wooden-shield", "magic-item", "ring", "cloak" |
Healer | Any |
Priest of Life | Any |
Priest of War | Any |
Priest of Light | "studded-leather", "ring-mail", "chain-mail", "shield", "ring", "magic-item", "cloak" |
Priest of Knowledge | "magic-item", "ring", "cloak" |
Shaman | "padded", "leather", "hide", "brigandine", "ring-mail", "scale-mail", "chain-mail", "splint-mail", "banded-mail", "shield", "ring", "magic-item", "cloak" |
Rogue / Thief | Any |
Bard | "padded", "leather", "hide", "brigandine", "ring-mail", "scale-mail", "chain-mail", "ring", "magic-item", "cloak" |
Assassin | Any |
Field | ' + +'Format | ' + +'Default Value | ' + +'Description | ' + +'Can be used in | ' + +'|||||
---|---|---|---|---|---|---|---|---|---|
ToHit Data | '
+ +' Dmg Data | '
+ +' Ammo Data | '
+ +' Range Data | '
+ +' Weapon Data | '
+ +' AC Data | '
+ +' ||||
w: | < text > | \'-\' | Name to be displayed | X | X | X | |||
w: | < text > | \'-\' | Name of spell or power | X | |||||
a: | < text > | \'-\' | Name to be displayed | X | |||||
t: | < text > | \'\' | Type | X | X | X | X | ||
st: | < text > | \'\' | Group Type (aka Tight-Group) | X | X | X | X | ||
sb: | 0 / 1 | 0 | Strength Bonus | X | X | X | |||
db: | 0 / 1 | 1 | Dexterity Bonus | X | X | ||||
+: | [ + / - ] # | 0 | Magical adjustment | X | X | X | X | X | |
+m: | [ + / - ] # | 0 | Missile attack adjustment | X | |||||
+s: | [ + / - ] # | 0 | Slashing damage adjustment | X | |||||
+p: | [ + / - ] # | 0 | Piercing damage adjustment | X | |||||
+b: | [ + / - ] # | 0 | Bludgeoning damage adjustment | X | |||||
n: | # [ / # ] | 1 | Attacks per round | X | |||||
d: | [ + / - ] # [ | # ] | \'\' | Dancing increment / duration | X | |||||
dp: | # | 0 | Dancing proficiency adjustment | X | |||||
ch: | 1 - 20 | 20 | Critical Hit roll value | X | |||||
cm: | 1 - 20 | 1 | Critical Miss roll value | X | |||||
sz: | [ t / s / m / l / h ] | \'\' | Size of item | X | X | X | |||
r: | [# /] # / # / # | \'\' | Range | X | X | ||||
r: | [+/-]# [ / [+/-]# / [+/-]# / [+/-]# ] | 0 | Range Modifier | X | |||||
ty: | SPB any combination | \'\' | Type of damage | X | |||||
sp: | [-]# | 0 | Speed in segments (1/10 round) | X | X | ||||
c: | # | 1 | Charges used for attack (charged weapons only) | X | |||||
c: | # | 0 | Charges used if hit (charged weapons only) | X | |||||
pre: | 0 / 1 | 0 | Gets auto pre-Initiative attack | X | |||||
on: | < cmd > | \'\' | Cmd to execute when taken in-hand | X | |||||
off: | < cmd > | \'\' | Cmd to execute when sheathed | X | |||||
qty: | # | 0 | Maximum possible qty of Items | X | X | X | |||
ru: | [-]# | 0 | Reusability of ammunition | X | |||||
sm: | dice roll format | 0 | Damage roll for Small & Medium opponents | X | X | ||||
l: | dice roll format | 0 | Damage roll for Large & Huge opponents | X | X | ||||
msg: | < text > | \'\' | Message to display with attk/dmg | X | X | X | |||
ac: | [-]# | \'\' | Armour class | X | |||||
wt: | # | 1 | Weight of item in lbs | X | X | ||||
ns: | # | 0 | Number of spells & powers defined for item | X | X | ||||
cl: | MU / PR / PW | \'\' | Type of spell or power | X | |||||
pd: | -1 / # | 1 | Number per day (power only) | X | |||||
rc: | Charged / Uncharged / Rechargeable / Recharging / Self-charging / Cursed / Charged-Cursed / Recharging-Cursed / Self-charging-Cursed | Uncharged | Initial charged and Cursed status of item when found | X | X | ||||
rac: | < text > | Armor name | Thievish armour name | X | |||||
ppa: | <[-/+]#> | 0 | Penalty / benefit to rogue\'s pick pocket skill | X | |||||
ola: | <[-/+]#> | 0 | Penalty / benefit to rogue\'s open locks skill | X | |||||
rta: | <[-/+]#> | 0 | Penalty / benefit to rogue\'s find/remove traps skill | X | |||||
msa: | <[-/+]#> | 0 | Penalty / benefit to rogue\'s moving silently skill | X | |||||
hsa: | <[-/+]#> | 0 | Penalty / benefit to rogue\'s hide in shadows skill | X | |||||
dna: | <[-/+]#> | 0 | Penalty / benefit to rogue\'s detect noise skill | X | |||||
cwa: | <[-/+]#> | 0 | Penalty / benefit to rogue\'s detect noise skill | X | |||||
rla: | <[-/+]#> | 0 | Penalty / benefit to rogue\'s read languages skill | X | |||||
lla: | <[-/+]#> | 0 | Penalty / benefit to rogue\'s ledgend lore skill | X | |||||
lv: | # | 1 | Level at which spell/power is cast | X | |||||
lv: | #:# | 1 | Min:Max level at which weapon/ammo can be used | X | X | ||||
clv: | #:# | 1 | Min:Max caster level at which weapon/ammo can be used | X | X | ||||
mulv: | #:# | 1 | Min:Max wizard level at which weapon/ammo can be used | X | X | ||||
prlv: | #:# | 1 | Min:Max priest level at which weapon/ammo can be used | X | X | ||||
pw: | Power | \' \' | Power cast by this attack (Magic class only) | X | |||||
desc: | < text > | \' \' | Power/spell/MI macro to display (Magic class only) | X | |||||
cmd: | < text > | \' \' | Command to send to chat | X | X | X |
As stated in section 7, the Character Sheet field mapping to the API script can be altered using the definition of the fields object. You can find the complete mapping for all APIs in the RPGMaster series, with an explanation of each, in a separate document.
' + +'', + }, + SpellsDatabase_Help:{name:'Spells Database Help', + version:1.33, + avatar:'https://s3.amazonaws.com/files.d20.io/images/257656656/ckSHhNht7v3u60CRKonRTg/thumb.png?1638050703', + bio:'Spells/Powers databases have names that start with
' + +' Wizard Spells: MU-Spells-DB-[added name]
'
+ +' Priest Spells: PR-Spells-DB-[added name]
'
+ +' Powers: Powers-DB-[added name]
Those with version numbers of the form v#.# as part of the name will be ignored.
' + +'As previously stated, each spell or power definition has 3 parts in the database (see Section 1): an Ability Macro with a name that is unique and matches the spell or power, an Attribute with the name of the Ability Macro preceded by "ct-", and a listing in the database character sheet of the ability macro name separated by \'|\' along with others of the same level in the spell book of the level of the spell or power. The quickest way to understand these entries is to examine existing entries. Do extract the root databases using the !magic --extract-db command, and take a look (but remember to delete it after viewing to speed things up, and then reindex the databases using !magic --check-db)
' + +'Note: The DM creating new spells and powers does not need to worry about anything other than the Ability Macro in the database, as running the command --check-db will update all other aspects of the database appropriately for all databases, as long as the Specs and Data fields are correctly defined. Use the name of the particular database as a parameter to check and update just that database. Running the command --check-db with no parameters will check and update all databases.
' + +'Ability macros can be added to a database just by using the [+Add] button at the top of the Abilities column in the Attributes and Abilities tab of the Database Character Sheet, and then using the edit "pencil" icon on the new entry to open it for editing. Ability macros are standard Roll20 functionality and not dependent on the API. Refer to the Roll20 Help Centre for more information.
' + +'The Ability Macro for a spell may look something like this:
' + +'&{template:RPGMspell}{{title=@{selected|casting-name} casts Sleep as a level @{selected|casting-level} caster}}{{splevel=Level 1 Wizard}}{{school=Enchantment/Charm}}Specs=[Sleep,MUspellL1,1H,Enchantment-Charm]{{range=90 ft}}{{components=V, S, M}}{{duration=[[5*({10,@{selected|casting-level}}kl1)]] Rounds}}{{time=1}}{{aoe=[30ft Cube](!rounds --aoe @{selected|token_id}|square|feet|90|30||dark)}}{{save=None}}{{damage=[Sleep them](!rounds --target area|@{selected|token_id}|@{target|Select who to sleep|token_id}|Sleep|[[5*({10,@{selected|casting-level}}kl1)]]|-1|Snoring away, shake to awaken|sleepy)}}SpellData=[w:Sleep,lv:1,sp:1,gp:0.01,cs:VSM]{{effects=Up to [2d4](!\ \/r 2d4) Hit Dice of creatures with 4 HD or less are put to sleep beginning with the lowest HD creatures in the Area of Effect.}}{{materials=a pinch of fine sand, rose petals, or a live cricket.}}
' + +'The ability specification for this Sleep spell uses a Roll20 Roll Template, in this case provided by the RPGMaster Library (see the documentation for the RPGMaster Library for specifications of this Roll Template), but any Roll Template you desire can be used. The entries in the Roll Template itself can be anything you desire, giving as much or as little information as you want. However, the important elements for the MagicMaster API are those highlighted. In red, two API buttons grant the player access to run RoundMaster API commands to show the Area of Effect of the spell, and then to mark affected tokens with a "Sleepy" status.
' + +'Each of the elements important to the database are inserted between the elements of the Roll Template, meaning they will not be seen by the player when the macro is run. Generally spaces, hyphens and underscores in the data elements are ignored, and case is not significant. Each element is described below:' + +'Specs = [Type, Class, Handedness, Spell School]' + +'
The Specs section describes what spell type and school this spell belongs to. These fields must be in this order. This format is identical for all database items, whether in these databases or others used by the Master series of APIs. Where there are multiple answers for a field, separate each by \'|\'. Note: Only A-Z, a-z, 0-9, hyphen/minus(-), plus(+), equals(=) point(.) and vertical bar(|) are allowed. Replace any forward slash with hyphen.
' + +'Type | the type of the spell, often the same as the ability macro name. |
---|---|
Class | one of MUSpellL#, PRSpellL#, or Power, where # is replaced by the spell level number. |
Handedness | #H, where # is the number of hands needed to cast the spell - i.e. does it have a somatic component. |
Spell School | the group of related spells that the spell belongs to. |
SpellData=[w:Sleep,lv:1,sp:1,gp:1,cs:VSM]' + +'
The SpellData section specifies the data relating to the use of the spell. These fields can be in any order.
' + +'w: | <text> | the name of the spell |
---|---|---|
sph: | <text> | the sphere of a priest spell (not used for wizard spells) |
lv: | <#> | the level of the spell |
sp: | <[-]# or dice roll spec> | the casting time in segments for the spell. Can be >10 e.g. 20 for 2 rounds, or negative, or even a dice roll |
gp: | <#[.#]> | the cost of the material components of the spell in GP: fractions converted to SP & CP |
cs: | <VSM> | the component of the spell (Verbal, Somatic, Material) - can be any combination |
The casting time (or speed) sp: can be negative, meaning it gives a negative modifier to individual initiative (if InitMaster API is being used). It can also be greater than 10 segments, meaning it takes longer than 1 Round to cast. Multiply the number of Rounds it will take to cast by 10, or the number of Turns it will take to cast by 100 (if using the InitMaster API the rounds will be automatically counted down and the spell actually cast in the appropriate round, unless the casting is interrupted). It can also be a dice roll specification, which will be rolled at the point that a character selects the spell, power or item to use in a particular round, which means the speed can vary from round to round. E.g. under AD&D2e rules, potions are always of this nature (see the AD&D2e DMG p141).
' + +'The cost of material components, gp:, is deducted from the Caster\'s money on their Character Sheet each time the spell is cast. The GM is informed of the spell being cast, by whom, and how much money it cost and how much money the Caster has left for each casting.
' + +'The components of the spell, cs:, is currently not used and is for future expansion capabilities.
' + +'A more complex spell that needs the caster to hit the target with an attack roll, also known as a weaponised spell, might look something like this:
' + +'&{template:RPGMspell}{{title=@{selected|Casting-name} casts
'
+ +'Spiritual Hammer
as a level @{selected|Casting-Level} caster}}Specs=[Spiritual-Hammer,Innate-Melee|PRspellL2,1H,Evocation],[Spiritual-Hammer,Innate-Melee,1H,Clubs],[Spiritual-Hammer,Innate-Melee,1H,Clubs]{{splevel=Level 2 Priest}}ToHitData=[w:Spiritual Hammer+1,prlv:1:6,+:1,sb:0,n:1,ch:20,cm:1,sz:T,ty:B,sp:3],[w:Spiritual Hammer+2,prlv:7:12,+:2,sb:0,n:1,ch:20,cm:1,sz:T,ty:B,sp:2],[w:Spiritual Hammer+3,prlv:13,+:3,sb:0,n:1,ch:20,cm:1,sz:T,ty:B,sp:1]{{school=Invocation}}{{sphere=Combat}}DmgData=[w:Spiritual Hammer+1,sb:0,+:1,sm:1+1d4,l:1d4],[w:Spiritual Hammer+2,sb:0,+:2,sm:1+1d4,l:1d4],[w:Spiritual Hammer+3,sb:0,+:3,sm:1+1d4,l:1d4]{{components=V,S,M}}weapData=[on:\\api;rounds --target caster|@{selected|token_id}|Spiritual-Hammer|\\lbrak;\\lbrak;3+@{selected|Casting-Level}\\rbrak;\\rbrak;|-1|Magical weapon in direction facing requires concentration|archery-target,off:\\api;!rounds --removetargetstatus @{selected|token_id}|Spiritual-Hmmer]{{time=[[5]]}}{{range=[[10*@{selected|Casting-Level}]] yards}}{{duration=[[3+@{selected|Casting-Level}]] rounds}}{{aoe=Special}}{{save=None}}{{reference=PHB p207}}{{damage=SM [1d4+1](!
/r 1d4+1) or L [1d4](!
/r 1d4) +[[{{(ceil(@{selected|Casting-Level}/6)),3}kl1}]]}}{{damagetype=Bludgeoning}}SpellData=[w:Spiritual-Hammer,lv:2,sp:5,gp:2,cs:VSM,sph:Combat]{{effects=Base Thac0 same as caster [[@{selected|thac0-base}]] without strength bonus plus magical plus of +[[{{(ceil(@{selected|Casting-Level}/6)),3}kl1}]]. Damage is plus magical bonus but no others.}}{{materials=A normal war hammer (cost 2gp) hurled towards opponent, which disappears as spell is cast.}}{{use=Take the Spiritual Hammer in-hand using the *Change Weapon* menu for the duration of the spell, and use it to attack opponents. It will disappear when the duration expires or you *Change Weapon* to another weapon.}}
This spell definition combines the elements of spell database specification and those of a weapon specification. The Specs data now includes the Innate-Melee weapon classification as well as the spell type and level, and as well as the SpellData there are weapon ToHitData and DmgData entries. In addition to the explanation here, please refer to the Weapon and Armour Database Help for more information on these sections.
' + +'In this case, there are multiple repeating datasets in each data section: there are three possible variants of the Spiritual Hammer depending on the caster\'s level. The version to be selected is determined using the level specification attribute in the ToHitData section, in this case using "prlv", but there are four alternatives:
' + +'lv: | Min : Max | The minimum and maximum level of character (both optional) |
---|---|---|
clv: | Min : Max | The minimum and maximum level of spell caster, class based on last spell cast (both optional) |
mulv: | Min : Max | The minimum and maximum level of wizard spell caster for this weapon (both optional) |
prlv: | Min : Max | The minimum and maximum level of priest spell caster for this weapon (both optional) |
In each case, the minimum and maximum are separated by a colon, and either can be left out meaning there is no minimum or no maximum. Wizard and Priest spell casters include those other classes that can cast those spells, at their particular level of spell casting compitence: e.g. a 10th level Ranger is a 2nd level Priest spell caster. If the appropriate value for the caster of the spell falls within the range specified, then that weapon dataset will result in creation of a line in the appropriate weapon tables.
' + +'If a spell has these weapon datasets included, and is currently memorised, it will appear in the weapon lists on the Change Weapon menu. Also, if this spell is cast, the Change Weapon menu will appear automatically after the spell description in the Chat window, ready to take the spell "in-hand" as a weapon and attack with it. Whichever way the Change Weapon menu appears, choosing the spell as a weapon will always mark the spell as having been cast. The spell-weapon will remain in-hand until the weapon is changed or the !attk --blank-weapon command is used which might, for instance, be included in the custom attack macro template (e.g. for Chromatic Orb) or in a spell end-effect (e.g. as in the Spiritual-Hammer-end Effect macro).
' + +'Other spells may not persist beyond the first attack with the spell, or perhaps after the first successful attack: this is often the case with spells that require the caster to "touch" an unwilling target.
' + +'&{template:RPGMspell}{{title=@{selected|Casting-name} casts
Cause Blindness or Deafness
as a level @{selected|Casting-Level} caster}}{{splevel=Level 3 Priest (reversable)}}{{school=Abjuration}}{{sphere=Necromantic}}Specs=[Cause-Blindness-or-Deafness,Innate-Melee|PRspellL3,1H,Abjuration],[Cause-Blindness-or-Deafness,Innate-Melee|PRspellL3,1H,Abjuration]{{components=V,S}}ToHitData=[w:Cause Blindness,sp:10,r:5,touch:1],[w:Cause Deafness,sp:10,r:5,touch:1]{{time=[[10]]}}DmgData=[w:Cause Blindness,sm:0,l:0,cmd:!rounds ~~target single¦`{selected¦token_id}¦`{target¦Which creature is the victim?¦token_id}¦Blindness¦99¦0¦Blinded and suffer -4 penalty to attacks and AC and +2 penalty to initiative¦bleeding-eye],[w:Cause Deafness,sm:0,l:0,cmd:!rounds ~~target single¦`{selected¦token_id}¦`{target¦Which creature is the victim?¦token_id}¦Deafness¦99¦0¦Deafened and suffer +1 penalty to initiative as well as other effects¦bleeding-eye]{{range=Touch}}{{duration=Permanent}}{{aoe=1 creature}}{{save=Negates}}{{reference=PHB p209 (reverse Cure Blindness or Deafness)}}SpellData=[w:Cause-Blindness-or-Deafness,lv:3,sp:10,gp:0,cs:VS,sph:Necromantic]{{use=Take the spell in-hand using the *change weapon* dialog, then use the *attack* action to select which effect and attack a target}}{{effects=Requires a successful touch (successful attack roll) on the victim. If the victim rolls a successful saving throw, the effect is negated. If the saving throw is failed, a non-damaging magical blindness or deafness results.
A *deafened* creature can react only to what it can see or feel, and suffers a -1 penalty to surprise rolls, a +1 penalty to its initiative rolls, and a 20% chance of spell failure for spells with verbal components. A *blinded* creature suffers a -4 penalty to its attack rolls, a +4 penalty to its Armor Class, and a +2 penalty to its initiative rolls.}}
The ToHitData, DmgData, or AmmoData specifications can include the data tag touch:
' + +'touch: | [ 0 / 1 ] | The spell expires if touch:1 is used (defaults to 0 if not specified) |
---|
If the touch:1 data tag and value is included in the ToHitData (whether a melee touch spell or a ranged attack spell) the spell will expire and be removed as an available attack on the first attack with the spell. If the touch:1 is included in the DmgData (for melee touch spells) or AmmoData (for ranged attack spells) the spell will remain available as an attack until a successful attack is made and damage or an effect occurs: this works whether or not using targeted attacks or otherwise, with non-targeted attacks expiring if either the S/m or L damage buttons are selected on the AC Hit dialog.
' + +'Sometimes a spell will need some command to be executed when an attack is made, either only when the attack is successful or when any attack is attempted. The above example of the Cause Blindness or Deafness spell is also an example of a spell using a command on a successful attack. It uses the cmd: data tag:
' + +'cmd: | cmd-string | A command to be executed when the magical attack is made, encoded with the standard and extended RPGMaster escape sequences |
---|
If the cmd:cmd-string data tag and value is included in the ToHitData specification then the command specified will execute automatically whenever the spell is used to do an attack, whether successful or not. If the cmd:cmd-string is included in the DmgData or AmmoData specifications the command will execute only when an attack is successful.
' + +'The Ability Macro for a Power may look something like this:
' + +'&{template:RPGMspell}{{title=@{selected|token_name} attempts to Turn Undead as a level @{selected|pr-casting-level} @{selected|class3}}} {{splevel=Power}} {{school=Necromancy}}Specs=[Turn-Undead,Power,1H,Necromancy]{{components=V,S}}{{time=[[10]]}}{{range=0}}{{duration=Until broken}}{{aoe=Undead within line of sight}}{{save=See turning table}}{{reference=PHB p103}}{{damage=[Turn It](!rounds --target area|@{selected|token_id}|@{target|Select undead|token_id}|Turned|99|0|Turned undead, flee if free-willed, stand aside if controlled|screaming)}}SpellData=[w:Turn Undead, sp:10, cs:VS]{{effects=**Remember that Paladins turn as a Priest of 2 levels lower.**
'
+ +'Attempting to turn counts as an action, requiring one round and occurring during the character\'s turn in the initiative order (thus, the undead may get to act before the character can turn them). The mere presence of the character is not enough--a touch of drama from the character is important. Speech and gestures are important, so the character must have his hands free and be in a position to speak. However, turning is not like spellcasting and is not interrupted if the character is attacked during the attempt.
'
+ +'To resolve a turning attempt, look on Table 61. Cross-index the Hit Dice or type of the undead with the level of the character (two levels lower for a paladin). If there is a number listed, roll 1d20. If the number rolled is equal to or greater than that listed, the attempt is successful. If the letter "T" (for "turned") appears, the attempt is automatically successful without a die roll. If the letter "D" (for "dispel") is given, the turning utterly destroys the undead. A dash (--) means that a priest or paladin of that level cannot turn that type of undead. A successful turn or dispel affects 2d6 undead. If the undead are a mixed group, the lowest Hit Dice creatures are turned first.
'
+ +'Only one die is rolled regardless of the number of undead the character is attempting to turn in a given round. The result is read individually for each type of undead.}}{{material=The Priest\'s holy symbol}}
Essentially, Powers are just Spells by another name, that can be cast multiple times per day, and are innate to the Character\'s class, or to a creature. The specification is, therefore, almost identical to a spell. In the author\'s campaigns, Powers do not consume material components and therefore do not cost money to use (except in rare circumstances) hence there being no gp: specification (it defaults to 0gp), but other DMs can add material costs for Powers if desired. Powers are all 1 level, hence no lv: specification.
' + +'Below are lists of the current possible values for the item database Ability macro sections.
' + +'Specs=[Type, Item-Class, Handedness, Group-Type]' + +'
There are no default settings for any of the Specs data fields. All must be explicitly specified.
' + +'There is an infinite list of spell types: generally the type is the spell name.
' + +'MUSpellL# | A Wizard spell with the Level specified as a number |
---|---|
PRSpellL# | A Priest spell with the Level specified as a number |
Power | A Power |
0H A spell/power that does not take a hand (there is no Somatic component)
'
+ +'1H A spell/power that requires only 1 hand to cast (most spells are like this)
'
+ +'2H A spell/power that requires 2 hands to cast (perhaps a scroll must be held)
'
+ +'3H A spell/power that takes 3 hands... perhaps more than 1 caster together?
'
+ +'4H Etc No currently programmed spells use more than 2 hands
'
+ +'... ...
From MagicMaster v2.048 onwards, Spell Schools are specified by Class in the Class-DB definitions and, depending on the API configuration set with the --config command, will be checked by the system or otherwise. Those implemented so far for the Spells databases are:
' + +'Abjuration, Alteration, Conjuration-Summoning, Enchantment-Charm, Divination, Illusion-Phantasm, Invocation-Evocation, Necromancy.
' + +'Note that the \'/\' in School names have been replaced by hyphens. It is also allowed to use just one half of any hyphenated school name where appropriate. If a spell or power is of more than one school, separate each with a vertical bar character \'|\'
' + +'Definitions for Data Section field types for Weapons & Armour can be found in the AttackMaster API documentation. Below are the definitions for Spell, Power & other Magical Item types.
' + +'Note: Always refer to the database specification definitions in other sections above for detailed information on the use of these Field specifiers. Not all specifiers have an obvious use.
' + +'Field | ' + +'Format | ' + +'Default Value | ' + +'Description | ' + +'Can be used in | ' + +'|||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Spell Data | '
+ +' Potion Data | '
+ +' Scroll Data | '
+ +' Wand Data | '
+ +' Staff Data | '
+ +' Rod Data | '
+ +' Ring Data | '
+ +' Misc Data | '
+ +' ToHit Data | '
+ +' AC Data | '
+ +' ||||
w: | < text > | \'-\' | Name to be displayed | X | X | X | X | X | X | X | X | X | |
w: | < text > | \'\' | Name of spell or power (Not case sensitive) | X | |||||||||
+: | [ + / - ] # | 0 | Magical adjustment | X | X | X | X | X | X | ||||
n: | # [ / # ] | 1 | Attacks per round | X | X | X | X | X | |||||
st: | \' \' | \'\' | Item type to display | X | X | X | X | X | X | X | X | ||
sz: | [ t / s / m / l / h ] | \'\' | Size of item | X | X | X | X | X | X | X | |||
sp: | [-]# or Dice Roll spec | 0 | Speed in segments (1/10 round) | X | X | X | X | X | X | X | X | X | |
wt: | # | 1 | Weight of item in lbs | X | X | X | X | X | X | ||||
rules: | [+/-][rule] | ... | 0 | Save / Check rules | X | X | X | X | X | X | ||||
svXXX: | [=][+/-]# | 0 | Save / Check mod | X | X | X | X | X | X | ||||
on: | command | \'\' | Cmd to execute when worn | X | X | X | X | ||||||
off: | command | \'\' | Cmd to execute when removed | X | X | X | X | ||||||
ns: | # | 0 | Number of stored spells & powers defined for item | X | X | X | X | X | X | X | |||
w: | < text > | \'-\' | Name of stored spell or power (Not case sensitive) | X | X | X | X | X | X | X | |||
cl: | MU / PR / PW | \'\' | Type of stored spell or power | X | X | X | X | X | X | X | |||
lv: | # | 1 | Level at which spell/power is cast | X | X | X | X | X | X | ||||
pd: | -1 / # | 1 | Number per day (power only) | X | X | X | X | X | X | ||||
rc: | Charged / Uncharged / Rechargeable / Recharging / Self-chargeable / Cursed / Charged-Cursed / Recharging-Cursed / Self-chargeable-Cursed | Uncharged | Initial charged and Cursed status of item when found (Can be changed by DM using -gm-only-mi command once added to Character Sheet) Not case sensitive | X | X | X | X | X | X | X | X | X | |
c: | # | 1 | The number of charges expended by using a charged magic item. Uncharged items always use 0 charges | X | X | X | X | X | X | X | X | X | |
desc: | [MU-/PR-/PW-/MI-]name | \' \' | Power or Spell to display | X | X | ||||||||
msg: | < text > | \' \' | Attack message | X | X | ||||||||
cmd: | Command | \' \' | Attack API command | X | X | ||||||||
learn: | [ 0 | 1 ] | 0 | Learnable stored spells | X | X |
The Character Sheet field mapping to the API script can be altered using the definition of the fields object, the definition for which can be found at the top of the game-version-specific RPGMaster Library API for the game-version you are using. You can find the complete mapping for all APIs in the RPGMaster series, with an explanation of each, in a separate document - ask the Author for a copy.
' + +'Note: Help for Spells & Powers has been split out to its own help handout.
' + +'Magic Item databases have names such as
' + +'Magic Items: MI-DB-[added name]
' + +'And can have anything put at the end, though those with version numbers of the form v#.# as part of the name will be ignored.
' + +'As previously stated and as for other magic, each magic item definition has 3 parts in the database (see Section 1): an Ability Macro with a name that is unique and identifies the magic item, an Attribute with the name of the Ability Macro preceded by "ct-", and a listing in the database character sheet of the ability macro name separated by \'|\' along with others of the same magic item type, which is one of: Potion, Scroll, Rod/Stave/Wand, Weapon, Armour, Ring, Miscellaneous, and also DM Only magic items. The quickest way to understand these entries is to examine existing entries. Do extract a root database using the !magic --extract-db command and take a look (but remember to delete it after viewing to speed things up, and then reindex the databases using !magic --check-db)
' + +'Note: The DM creating new magic items does not need to worry about anything other than the Ability Macro in the database, as running the command --check-db will update all other aspects of the database appropriately for all databases, as long as the Specs and Data fields are correctly defined. Use the name of the particular database as a parameter to check and update just that database. Running the command --check-db with no parameters will check and update all databases.
' + +'Ability macros can be added to a database just by using the [+Add] button at the top of the Abilities column in the Attributes and Abilities tab of the Database Character Sheet, and then using the edit "pencil" icon on the new entry to open it for editing. Ability macros are standard Roll20 functionality and not dependent on the API. Refer to the Roll20 Help Centre for more information.
' + +'The Ability Macro may look something like this:
' + +'&{template:RPGMpotion}{{title=Oil of Etherealness}} {{splevel=Oil}} {{school=Alteration}}Specs=[Oil of Etherealness,Potion,1H,Alteration]{{components=M}}{{time=[[3]] rounds after application}} PotionData=[sp:30,rc:charged]{{range=User}}{{duration=4+1d4 turns}} {{aoe=User}} {{save=None}} {{healing=[Become Ethereal](!rounds --target single|@{selected|token_id}|@{target|Select a target|token_id}|Oil-of-Etherealness|[[10*(4+1d4)]]|-1|Ethereal|Ninja-mask)}}{{effects=This potion is actually a light oil that is applied externally to clothes and exposed flesh, conferring etherealness. In the ethereal state, the individual can pass through solid objects in any direction - sideways, upward, downward - or to different planes. The individual cannot touch non-ethereal objects.
'
+ +'The oil takes effect three rounds after application, and it lasts for 4+1d4 turns unless removed with a weak acidic solution prior to the expiration of its normal effective duration. It can be applied to objects as well as creatures. One potion is sufficient to anoint a normal human and such gear as he typically carries (two or three weapons, garments, armor, shield, and miscellaneous gear). Ethereal individuals are invisible.}}{{materials=Oil}}
There is one new field in the data section (in this case called the PotionData section):
' + +'rc: | <MI-type> | the recharging/curse type of the magic item. |
---|
All magic items have a recharging/curse type: for details, see the --gm-edit-mi command in the MagicMaster API help documentation, section 4.1. If not supplied for a magic item definition, it defaults to uncharged. Generally, items in the database are not cursed-, but can have their type changed to cursed or some recharging cursed type when the DM stores them in a container or gives them to a Character using the --gm-edit-mi command.
' + +'Items like a Ring of Protection or a Luck Blade protect the possessor by improving their saving throws and/or armour class.
' + +'&{template:RPGMring}&{template:RPGMring}{{name=Ring of Protection}}{{subtitle=Ring}}{{Speed=[[0]]}}{{Size=Tiny}}{{Immunity=None}}{{Protection=+[[2]] on AC}}Specs=[Ring of Protection,Protection Ring,1H,Abjuration-Protection]{{Saves=+[[2]] on saves}}ACData=[a:Ring of Protection+2,st:Ring,+:2,rules:-magic,sz:T,wt:0,svsav:2,w:Ring of Protection+2,sp:0,rc:uncharged,loc:left finger|right finger]{{Looks Like=A relatively plain ring made of some exotic metal. You are unable to distinguish it from any other ring by just looking at it...}}{{desc=A ring of protection improves the wearer\'s Armour Class value and saving throws versus all forms of attack. A ring +1 betters AC by 1 (say, from 10 to 9) and gives a bonus of +1 on saving throw die rolls. The magical properties of a ring of protection are cumulative with all other magical items of protection except as follows:
1. The ring does not improve Armour Class if magical armour is worn, although it does add to saving throw die rolls.
2. Multiple rings of protection operating on the same person, or in the same area, do not combine protection. Only one such ring—the strongest—functions, so a pair of protection rings +2 provides only +2 protection.}}
All items that protect that are not armour or shields have an item class of some type of protection-[item] specified as the second field of the Specs for the item. The [item] text can be anything you desire, e.g. in this case protection-ring, but only the most advantageous protection-[item] that the possessor has on them will operate. E.g. a protection-ring will work with a protection-cloak but not with another protection-ring.
' + +'Items that protect that have an effect on armour class must use the ACData section to specify their properties, otherwise the properties can be held in any other \'...data=\' specification. The Weapons & Armour Database Help handout has full specifications for ACData fields. The data field tags relevant to AC and saves are listed in the table below:
' + +'+: | [+/-]# | The effect on armour class, + being beneficial, - being a penalty (ACData only). |
---|---|---|
rules: | rule [ | rule | rule | ...] | Rules specifying what item types / supertypes / classes this item will or will not work with (see below). |
svXXX: | [=][+/-]# | The effect on various saving throws, specified by XXX, + being beneficial, - being a penalty. |
The rules: state conditions and which other items this item will or will not work with to improve armour class and/or saving throws. Each rule is separated by a \'|\' and preceeded by either \'+\' or \'-\'. The rules have the following meanings:
' + +'+inHand | This item must be held in hand for them to work, using the Change Weapon dialog |
---|---|
+worn | (Saves only) This item must be of a usable type that can be worn by the class / race of the character - default for AC |
-magic | (AC only) This item will not combine with magical armour |
-shield | (AC only) This item will not combine with a shield of any type |
-acall | (AC only) This item will not combine with any other armour (except that specified with a \'+\' - see below) |
-[supertype] | (AC only) This item will not work with any other item of the supertype specified |
+[supertype] | (AC only) This item will always work with any other item of the supertype specified, even if -acall rule has been specified |
-[item class] | (Saves only) This item will not combine with any other item of the item class specified |
The svXXX: entries state the effect on saving throws and/or ability checks that this item has if the rules are met. For Saving Throw mods the \'XXX\' can be one of \'par\', \'poi\', \'dea\', \'pet\', \'pol\', \'bre\', \'spe\', or \'sav\' each referring to the first 3 letters of the saving throw affected (or \'sav\' for all saving throw mods); and for Attribute Check mods \'XXX\' can be \'str\', \'con\', \'dex\', \'int\', \'wis\', \'chr\', and \'atr\' each refering to each Character attribute (or \'atr\' for all attribute mods); or to change all mods of both types use \'all\'. Each svXXX: field tag is followed by a number which can be optionally preceeded by \'+\' (a beneficial improvement to the mod), \'-\' (a penalty to the mod), and/or \'=\' (the mod is set to the value - overrides other changes).
' + +'Note: Changing the Attribute mods will not affect the ability checks for open doors, bend bars, learn spells etc. These mods can only be adjusted manually using the appropriate button on the Attribute Check menu.
' + +'In a similar way to protection, items can affect initiative roles for a character. This can only be achieved automatically if using group or individual initiative:
' + +'&{template:RPGMring}&{template:RPGMwandspell}{{title=Rod}}{{name= of Alertness}}specs=[Rod of Alertness,melee,1h,Clubs],[Rod of Alertness,magic,1h,clubs],[Rod of Alertness,magic,1h,clubs],[Rod of Alertness,magic,1h,clubs],[Rod of Alertness,magic,1h,clubs],[Rod of Alertness,magic,1h,clubs],[Rod of Alertness,magic,1h,clubs],[Rod of Alertness,magic,1h,clubs],[Rod of Alertness,Rod,1H,conjuration-summoning]{{splevel=Footman\'s mace/Rod}}weapdata=[w:Rod of Alertness,wt:10,st:Rod,init+:-1]{{school=Conjuration/Summoning}}tohitdata= ... ignore the rest for now...
All of the rod\'s protective functions require one charge. The animate object power require one additional charge, so, if all of the rod\'s protective devices are utilized at once, two charges are expended.
The rod can be recharged by a priest of 16th level or higher, as long as at least one charge remains in the rod when the recharging is attempted.}}{{use=Taking in hand as a weapon will improve initiative scores by 1 automatically. surprise bonus is manual, and allow use of the detection capabilities.
- selecting any of the detect buttons does not use a charge but will display the specifications of the spell and allow its effects to occur.
- when invoking alertness, the player should use the 120ft radius button to set the area of effect.
- selecting the light button should be used by the dm when alertness is triggered to point the cone of light in the right direction.
- the player should then select the 20ft radius button to show the aoe of the prayer
- then select the prayer spell button, which will expend a charge and allow the prayer effect markers to be set.
- selecting the [animate object] button will expend a charge and display the spell specs to allow it to have effect.}}
The weapdata specification includes the attribute init+:-1 which indicates that this item improves the initiative priority roll by 1 (i.e. subtracting 1 from the roll). This will be automatically applied while the item obeys the rules specified for it - see the description of the rules: data attribute in 2.3 above. If the rules are met or no rules are specified for the item, the effect on initiative rolls will apply as long as the item is on the character\'s item list (not in their backpack).
' + +'Another data attribute that modifies the initiative roll is the init*: attribute, which multiplies the attack actions each round for the possessing character (does not affect magic item use, spell casting or use of powers). a value above 1 increases attacks per round, and below 1 reduces those attacks.
' + +'Items can also affect the scores required for a Rogue (or other character class) to perform various skills.
' + +'&{template:RPGMitem}{{name=Fine Thieves Tools}}Specs=[Thieves Tools,Miscellaneous,0H,Tools]{{desc=This is a fine set of *Thieve\'s Tools*, with ivory handles and contained in a leather purse. You wonder what creature donated the ivory}}MiscData=[w:Fine Thieves Tools,gp:200,wt:1,ola:5,rta:5,rc:uncharged]{{}}
' + +'Each Rogue skill can be increased or decreased using the following data tags:
' + +'ppa:[+/-]# | Adjustment to the pick pocket skill (# can be a calculation) |
---|---|
ola:[+/-]# | Adjustment to the open locks skill (# can be a calculation) |
rta:[+/-]# | Adjustment to the find/remove traps skill (# can be a calculation) |
msa:[+/-]# | Adjustment to the move silently skill (# can be a calculation) |
hsa:[+/-]# | Adjustment to the hide in shadows skill (# can be a calculation) |
dna:[+/-]# | Adjustment to the detect noise skill (# can be a calculation) |
cwa:[+/-]# | Adjustment to the climb walls skill (# can be a calculation) |
rla:[+/-]# | Adjustment to the read languages skill (# can be a calculation) |
lla:[+/-]# | Adjustment to the legend lore skill (# can be a calculation) |
Other magic items might use different structures, and be more complex:
' + +'&{template:RPGMring}{{name=Ring of Human Influence}}{{subtitle=Ring}}Specs=[Ring of Human Influence,Ring,1H,Enchantment-Charm]{{Speed=[[0]]}}RingData=[w:Ring of Human Influence,sp:3,rc:uncharged,loc:left finger|right finger,on:\\apisetattr --fb-from Magic Items --fb-header Ring of Human Influence - Put on --fb-content _CHARNAME_ chooses to put on the Ring of Human Influence and now has a Charisma of 18 vs Humans and Humanoids --name @{selected|character_name} --RoHI-chr|@{selected|charisma} --charisma|18,off:\\apiresetattr --fb-from Magic Items --fb-header Ring of Human Influence - Take off --fb-content _CHARNAME_ chooses to take off the ring and their Charisma returns to normal --name @{selected|character_name} --RoHI-chr --charisma|@{selected|RoHI-chr},ns:2],[cl:PW,w:Suggestion,sp:3,lv:12,pd:1],[cl:PW,w:MU-Charm-Person,sp:3,lv:12,pd:1]{{Size=Tiny}}{{Immunity=None}}{{desc=Has the effect of raising the wearer\'s Charisma to 18 on encounter reactions with humans and humanoids. The wearer can make a [*suggestion*](!magic --mi-power @{selected|token_id}|Suggestion|Ring-of-Human-Influence|12) to any human or humanoid (saving throw applies). The wearer can also [charm](!magic --mi-power @{selected|token_id}|Charm-Person|Ring-of-Human-Influence|12) up to 21 levels/Hit Dice of human/humanoids (saving throws apply) just as if he were using the wizard spell, *charm person*. The two latter uses of the ring are applicable but once per day. Suggestion or charm has an initiative penalty of +3.}}{{use=Putting on the ring using the Change Weapon function changes Charisma to 18, and taking it off returns Charisma to its previous value. If using InitiativeMaster Group or Individual Initiative, select Initiative for a Magic Item, then the Ring of Human Influence to get the right item speed. Cast the spells by Using the Ring as a Magic Item, then selecting the appropriate spell in the Effect description.}}
' + +'Here, as well as having API buttons to implement powers, the RingData entry specifies commands to execute when the ring is put on using the Change Weapon menu, and another when it is taken off, as well as other aspects of the ring\'s power - but ignore everything after the "ns:" for now.
' + +'on: | Command string | A simple, single line command to execute on wearing the ring |
---|---|---|
off: | Command string | A simple, single line command to execute on taking off the ring |
Other attributes that can work in a similar way execute commands when an item is "picked up" and added to a character or creature\'s items, and also when "put away" or passed on to another character, creature or container:
' + +'pick: | Command string | A simple, single line command to execute on picking up the item |
---|---|---|
put: | Command string | A simple, single line command to execute on putting away the item |
Some more complex items can be used as weapons that have different effects or damage depending on how many charges are expended:
' + +'&{template:RPGMwand}{{name=Staff of Striking}}Specs=[Staff of Striking|Quarterstaff,Rod|Melee,1H,Staff],[Staff of Striking|Quaretstaff,Melee,1H,Staff],[Staff of Striking|Quarterstaff,Melee,1H,Staff],[Staff of Striking,Rod,1H,Conjuration-Summoning|Animal]{{subtitle=Staff}}ToHitData=[w:Staff of Striking 1 charge,sb:1,+:3,n:1,ch:20,cm:1,sz:M,ty:SPB,r:5,sp:4,c:1,rc:rechargeable],[w:Staff of Striking 2 charges,sb:1,+:3,n:1,ch:20,cm:1,sz:M,ty:SPB,r:5,sp:4,c:2,rc:rechargeable],[w:Staff of Striking 3 charges,sb:1,+:3,n:1,ch:20,cm:1,sz:M,ty:SPB,r:5,sp:4,c:3,rc:rechargeable]{{Speed=[[4]]}}WandData=[qty:19+1d6]{{Size=Medium}}{{Weapon=1-handed melee oaken staff}}{{To-hit=+3, +Str Bonus}}{{Attacks=1 per round, magically the most favourable weapon type}}{{Damage= SM: 1d6, L:1d6, 1 charge: +3, 2 charges: +6, 3 charges: +9}}DmgData=[w:Staff of Striking 1 charge,sb:1,+:3,SM:1d6,L:1d6],[w:Staff of Striking 2 charges,sb:1,+:6,SM:1d6,L:1d6],[w:Staff of Striking 3 charges,sb:1,+:9,SM:1d6,L:1d6]{{Use=Melee weapon attack as normal, selecting the appropriate plus, which will deduct the number of charges automatically.}}{{desc=This oaken staff is the equivalent of a +3 magical weapon. (If the weapon vs. armor type adjustment is used, the staff of striking is treated as the most favorable weapon type vs. any armor.) It causes 1d6+3 points of damage when a hit is scored. This expends a charge. If two charges are expended, bonus damage is doubled (1d6+6); if three charges are expended, bonus damage is tripled (1d6+9). No more than three charges can be expended per strike. The staff can be recharged.}}
' + +'The Staff of Striking is a weapon that can do additional damage if more charges are expended. The database definition uses the multiple attack / damage fields (as specified and explained in the Weapon and Armour Database Help handout) which results in multiple entries for the weapon to appear in the weapons tables on the character sheet, and in the Attack menu. Each ToHitData definition has the \'c\' attribute to define how many charges are expended when that version of the weapon is used:
' + +'c: | # | The number of charges expended when the respective attack is made. Applies only to weapons / magic items that have charges. If in ToHitData is deducted when an attack is made, or if in DmgData only applies if a hit is achieved & damage done. Defaults to 1 charge (ToHitData) or 0 (DmgData) if not specified. If this item is not a weapon and the c: is in the item Data specification, it determines how many charges are deducted when the item is used, defaulting to 1. If combined with the special recharging type of \'enable\' then charges will not be deducted, but the c:value will still be compared against the quantity/number of charges of the item and the use disabled if there are fewer charges available than required. |
---|---|---|
qty: | # or <dice spec> | The default quantity of charges the item will start with when added by the GM to a container. Can be a dice roll specification, in which case the number will be determined randomly with a dice roll. The GM can optionally alter this number when storing the item. |
When shown in the Attack menu, any version of the weapon which requires more charges than it currently has will be gray, and will not be selectable for an attack.
' + +'Some magic items, especially Rods, Staves and Wands, must be taken in-hand like a weapon in order for their abilities to become fully available to the character by making an Attack action. The Rod of Smiting described above is a weapon of this nature, but others might have magical attacks as well as, or instead of melee or ranged attacks. Here is an example of one such device:
' + +'&{template:RPGMwand}{{title=Wand of Frost}}WandData=[w:Wand of Frost,wt:1,sp:2,c:0,rc:rechargeable,loc:left hand|right hand]{{splevel=Wand}}{{school=Evocation}}Specs=[Wand of Frost,Magic|Wand,1H,Evocation],[Wand of Frost,Magic|Wand,1H,Evocation],[Wand of Frost,Magic|Wand,1H,Evocation]{{components=V,M}}{{time=[[2]]}}{{range=Special}}ToHitData=[w:Ice Storm,desc:MU-Ice-Storm,lv:6,sp:2,c:1],[w:Wall of Ice,desc:MU-Wall-of-Ice,lv:6,sp:2,c:1],[w:Cone of Cold,desc:PW-WoF-Cone-of-Cold,lv:6,sp:2,c:2]{{duration=Special}}{{aoe=Special}}{{save=Special}}{{effects=A *frost* wand can perform three functions that duplicate wizard spells:
'
+ +'• *Ice storm:* A silvery ray springs forth from the wand and an ice (or sleet) storm occurs up to 60 feet away from the wand holder. This function requires one charge.
'
+ +'• *Wall of ice:* The silvery ray forms a wall of ice, six inches thick, covering a 600-squarefoot area (10\' x 60\', 20\' x 30\', etc.). Its initiative modifier is +2, and it uses one charge.
'
+ +'• *Cone of cold:* White crystalline motes spray forth from the wand in a cone with a 60-foot length and a terminal diameter of 20 feet. The initiative modifier is +2, and the effect lasts just one second. The temperature is -100 degrees F., and damage is 6d6, treating all 1s rolled as 2s (6d6, 12-36). The cost is two charges per use. Saving throw vs. wands is applicable.
'
+ +'The wand can function once per round, and may be recharged.}}{{materials=Wand}}{{Use=Take the wand in-hand using the *Change Weapon* dialogue in order to use its powers with the *Attack* action}}
This specification introduces a new item Specs class, "Magic", and one of a new range of ToHitData fields, "desc":
' + +'Magic | The associated entries in the ToHitData will specify a magical attack, rather than a melee or ranged attack. There will not be a matching DmgData specification | |
---|---|---|
desc: | \' \' | The name of an ability macro describing the magical attack - this is a power, wizard or priest spell, or a magic item (even possibly this magic item) which will be displayed to the player when this magical attack is used. |
lv: | <#> | The level at which the magic item casts any power or spell. The spell will have effects as if cast at this level when cast from the magic item. |
The power, spell or magic item name used with the desc: field tag will be searched for in all the appropriate databases. However, some exist in more than one context (e.g. Light is both a Wizard and a Priest spell). It is possible to specify where the specific description can be found by preceding the name with one of "PW-", "MU-", "PR-", or "MI-" for Power, Wizard spell, Priest spell, and Magic Item respectively. Specifying the type also speeds up the search.
' + +'There are other field tags that can be used with a Magic class ToHitData specification:
' + +'pw: | \' \' | The name of a magic item power (with limited uses per day) to use as a magical attack, specified as per Section 4.1 below |
---|---|---|
msg: | \' \' | A message to display to the player when the magical attack is made, encoded with the standard and extended RPGMaster escape sequences |
cmd: | \' \' | An API command to be executed when the magical attack is made, encoded with the standard and extended RPGMaster escape sequences |
Generally speaking, the cmd: and msg: tags can be used together instead of a desc: if there is no equivalent spell or power to display and only a simple status, timer or effect results from the magical attack. The pw: tag operates in an almost identical way to desc: but decrements the "per day" uses for the named power/spell (specified in the item data specification - see Section 4.1 below) each time it is used, which refresh after a Long Rest.
' + +'Sometimes, GMs want Players to have to discover the properties of magic items through quests, spell use, trial and error, or paying a high-level wizard to identify them. This is not always the case, and some groups may prefer for some or all items to reveal their nature on first examination. The database specification of an item allows for both approaches. An example of how to define an item to make it easy to hide its details is
' + +'&{template:RPGMitem}{{title=Flask}}{{name= of Curses}}{{subtitle=Magic Item}}Specs=[Flask of Curses,Miscellaneous,1H,Alteration]{{Speed=[[3]]}}MiscData=[w:Flask of Curses,st:Flask,wt:1,sp:3,qty:1,rc:charged]{{Size=S}}{{Looks Like=An ordinary flask of some type, containing a little liquid of some unidentifyable sort}}{{Use=The GM will tell you what happens when you use this item}}{{desc=This item looks like an ordinary beaker, bottle, container, decanter, flask, or jug. It has magical properties, but detection will not reveal the nature of the flask of curses. It may contain a liquid or it may emit smoke. When the flask is first unstoppered, a curse of some sort will be visited upon the person or persons nearby. After that, it is harmless. The type of curse is up to the DM}}{{GM Info=Hide this as some other jug, flask or bottle, using the GM\'s *Add Items* menu, and set *Reveal* to *on use*. Invent an imaginative curse to enact! Suggestions include the reverse of the priest\'s bless spell. Typical curses found on scrolls are recommended for use here as well. Or perhaps a monster could appear and attack all creatures in sight.}}
' + +'Four elements contribute to the "simple" approach to being able to hide the item details from the Player / Character:
' + +'The key element is the inclusion of the Looks Like text tag in the definition of the item. If an item has this tag, the GM\'s Add Items dialog will have the Hide Item as Item button enabled to hide the item as either what the st: data attribute specifies or (if not specified) the item class in the Specs specification of the item. However, if auto-hiding is set in the !magic --config options, an item with a Looks Like text tag will automatically be hidden in this way when added to any container. If a player character views or uses such a hidden item, they will see only the title and the Looks Like text and nothing else. The GM can either set the item to automatically reveal its "secrets" when the player character views the item, uses the item, or only when revealed manually by the GM. It is also possible for the GM to select to hide the item as a completely different item using the Add Items dialog. For full details see the --gm-edit-mi entry in the MagicMaster Help handout.
' + +'It is possible to define the hidden status and the revealing trigger set when the item is added to a character or container as part of the data of the item definition. The hide: data tag can take the following parameters - if it is not specified, the auto-hide configuration flag defines the hiding status:
' + +'hide | Automatically hide the item regardless of the state of the auto-hide configuration flag |
---|---|
nohide | Do not hide the item regardless of the state of the auto-hide configuration flag |
The rev: data tag, which sets the trigger that will reveal a hidden item, can take the following values - if not defined, the reveal state is defined by the Reveal configuration flag:
' + +'manual | Only reveal when the GM selects to do so using the [Add Item] dialog or the button shown on the item definition (to the GM only) |
---|---|
view | Reveal the hidden item\'s true nature when the player first views the item\'s description once they have it in their possession |
use | Reveal the item\'s true nature when it is first used, but not if it is viewed before that |
It is possible to create item definitions at have configurable elements, set when the item is first added to a container or a character. This is achieved using the query: attribute in the data section of the item. An example of its use is the Armour of Blending which uses a query to ask which type of armour it really is and what magical plus that armour might grant.
' + +'&{template:RPGMarmour} {{title=Armour}} {{name=of Blending}}{{subtitle=Armour}} {{Armour=Can be any armour type}} Specs=[Armor-of-Blending,Armour,0H,MagicItem] {{AC=Varies by armour type}} ACData=[a:Armor of Blending, query:armourType=What type of armour? |Banded Mail%%Mail/4/2/0/1/Banded Armor (Disguise) |Brigandine%%Brigandine/6/1/1/0/Brigandine Armor |Bronze Plate Mail%%Mail/4/2/0/-2/Bronze Plate Mail (Disguise) |Chain Mail%%Mail/5/2/0/-2/Chain Mail |Field Plate%%Plate/2/3/1/0/Field Plate (Disguise)|Full Plate%%Plate/1/4/3/0/Full Plate (Disguise)|Leather%%Leather/8/0/-2/0/Leather Armor|Plate Mail%%Mail/3/3/0/0/Plate Mail (Disguise)|Ring Mail%%Mail/7/1/1/0/Ring Mail |Scale Mail%%Mail/6/0/1/0/Scale Mail |Splint Mail%%Mail/4/0/1/2/Splint Mail (Disguise) |Studded Leather%%Leather/7/2/1/0/Studded Leather $$ armourPlus=What magical plus? |0%%0 |+1%%1 |+2%%2 |+3%%3 |+4%%4 |+5%%5 , qty:1, st:^^armourType#1^^, t:^^armourType#0^^, +S:^^armourType#3^^, +P:^^armourType#4^^, +B:^^armourType#5^^, +:^^armourPlus#1^^, ac:^^armourType#2^^, sz:L, wt:40, loc:body, rac:^^armourType#6^^] {{Speed=[[0]]}} {{Size=Large}} {{Immunity=None}} {{Saves=No effect}} {{Looks Like=A normal suit of some type of armour (DM to determine).}} {{Use=When storing this armour, the DM/player will be asked to state what type of armour it is and its magical plus (if any). It will then operate as chosen and can be passed from container to container without further input.}} {{desc=This appears to be a normal suit of magical armor (determine type and AC modifier normally, ignoring negative results). However, upon command (a command word can be assigned if the DM desires), the armor changes shape and form, assuming the appearance of a normal set of clothing. The armor retains all its properties (including weight) when disguised. Only a *true seeing* spell will reveal the true nature of the armor when disguised.}}
' + +'The query: attribute in the ACdata section defines the questions that will be asked in standard Roll20 Roll Queries. In this case, two separate questions are asked, each using the following format:
' + +'result-tag=query question|option 1 text%%value 1.1/value 1.2/.../value 1.n|option 2 text%%value 2.1/value 2.2/.../value 2.n|...%%.../.../...|option j text%%value j.1/value j.2/.../value j.n' + +'
Multiple queries can be concatinated, separated by \'$$\'. Each query posts the option texts in a list. The selected option will then provide the values that substitute dynamic attributes in the data section, which are specified with the syntax ^^result-tag#n^^
where \'n\' is the value index - index 0 is the option text itself. Using the Armour of Blending example, selecting an armour type of Plate Mail will replace the following dynamic attributes:
t:^^armourType#0^^ | t:Plate Mail |
---|---|
st:^^armourType#1^^ | st:Mail |
ac:^^armourType#2^^ | ac:3 |
+S:^^armourType#3^^ | +S:3 |
+P:^^armourType#4^^ | +P:0 |
+B:^^armourType#5^^ | +B:0 |
rac:^^armourType#6^^ | rac:Plate Mail (disguise) |
Some magic items, especially artefacts and sentient items, can store spells and/or have powers similar to characters. MagicMaster supports magic items of this type to a degree, although there are inevitably exceptions that the DM will have to get creative in their development! These items use API buttons that call various MagicMaster commands to deliver their capabilities.
' + +'First to note is that items that have powers and spells use spell slots in the owning character\'s character sheet. These spell slots should not be used by characters in your campaign. If they are, errors might occur. By default, on the AD&D2E character sheet the system uses Wizard Level 14 spell slots for magic item powers, and Wizard Level 15 spell slots for spell-storing magic items. As standard AD&D2E only has spells up to level 9 this generally works without causing problems.
' + +'Next, in addition to the three standard elements of the Ability Macro, the \'ct-\' attribute and the listing, these items require a 4th element which specifies their powers and spells. These are:
' + +'mi-muspells-[item-name]: | Wizard spells able to be stored in the magic item |
---|---|
mi-prspells-[item-name]: | Priest spells able to be stored in the magic item |
mi-powers-[item-name]: | Powers able to be used by the magic item |
In each case the [item-name] is replaced by the Ability macro name (which is not case sensitive).
' + +'Note: The DM creating new spell storing or power wielding magic items does not need to worry about anything other than the Ability Macro in the database, as running the command --check-db will update all other aspects of the database appropriately for all databases, as long as the Specs and Data fields are correctly defined. Use the name of the particular database as a parameter to check and update just that database. Running the command --check-db with no parameters will check and update all databases.
' + +'When a spell-storing or power wielding magic item is added to a magic item bag or container using --edit-mi or --gm-edit-mi, these attributes are automatically added to the character sheet by the APIs and also they are parsed by the system and the spells and/or powers are created in the relevant spell books automatically. When such an item is found in a container by a character, or passed from character to character, all of the stored spells & powers are deleted from the old character and created in the new character. A character gaining such an item can use its spells and powers immediately.
' + +'Here is an example of a power wielding magic item:
' + +'!setattr --silent --sel --casting-level|1 --casting-name|@{selected|token_name}\'s Ring of Shooting Stars
'
+ +'&{template:RPGMring}{{name=Ring of Shooting Stars}}{{subtitle=Ring}}Specs=[Ring of Shooting Stars,Ring,1H,Evocation]{{Speed=[[5]]}}RingData=[w:Ring of Shooting Stars,sp:5,rc:charged,ns:6], [cl:PW,w:MU-Dancing-Lights,sp:5,pd:12], [cl:PW,w:MU-Light,sp:5,pd:2], [cl:PW,w:RoSS-Ball-Lightning,sp:5,pd:1], [cl:PW,w:RoSS-Shooting-Stars,sp:5,pd:3], [cl:PW,w:Faerie-Fire,sp:5,pd:2], [cl:PW,w:RoSS-Spark-Shower,sp:5,pd:1] {{Size=Tiny}} {{Immunity=None}} {{Resistance=None}} {{Saves=None}} {{desc=This ring has two modes of operation - at night and underground - both of which work only in relative darkness.
'
+ +'***During night hours, under the open sky***, the shooting stars ring will perform the following functions:
'
+ +'- [*Dancing lights*](!magic --mi-power @{selected|token_id}|Dancing-Lights|Ring-of-Shooting-Stars|1) as spell (once per hour).
'
+ +'- [*Light*](!magic --mi-power @{selected|token_id}|Light|Ring-of-Shooting-Stars|1), as spell (twice per night), 120-foot range.
'
+ +'- [*Ball lightning*](!magic --mi-power @{selected|token_id}|RoSS-Ball-Lightning|Ring-of-Shooting-Stars|1), as power (once per night).
'
+ +'- [*Shooting stars*](!magic --mi-power @{selected|token_id}|RoSS-Shooting-Stars|Ring-of-Shooting-Stars|1), as power (special).
'
+ +'***Indoors at night, or underground***, the ring of shooting stars has the following properties:
'
+ +'[*Faerie fire*](!magic --mi-power @{selected|token_id}|PR-Faerie-Fire|Ring-of-Shooting-Stars|1) (twice per day) as spell
'
+ +'[*Spark shower*](!magic --mi-power @{selected|token_id}|RoSS-Spark-Shower|Ring-of-Shooting-Stars|1) (once per day) as power
'
+ +'Range, duration, and area of effect of functions are the minimum for the comparable spell unless otherwise stated. Casting time is 5}}
Note that the ability macro starts with a call to the ChatSetAttr API to set the casting-level to 1 and the name of the caster to be \< Character-name \>\'s Ring of Shooting Stars. Not strictly necessary, but a nice cosmetic.
' + +'The data section now includes repeating data sets, one for each of the powers that the item has:
' + +'RingData=[w:Ring of Shooting Stars,sp:5,rc:charged,ns:6], [cl:PW,w:MU-Dancing-Lights,sp:5,pd:12], …' + +'
The first data set is very similar to the standard magic item data, with the addition of the ns: field, and is then followed by a number of repeated data sets specifying each of the powers:
' + +'ns: | <#> | The number of powers (or spells) that the item can wield or store |
---|---|---|
cl: | <MU/PR/PW> | The type of the power/spell specification: PW=power, MU=wizard spell, PR=priest spell |
w: | <text> | The name of the power/spell - must be exactly the same as the database name (case ignored) optionally prefixed by a power type, one of \'PW-\', \'MU-\', \'PR-\', or \'MI-\' for Power, Wizard spell, Priest spell, or Magic Item |
sp: | <[-/+]# / dice roll spec> | The speed or casting time of the power/spell in segments |
pd: | <-1/#> | The available casts per day, or -1 for \'at will\' |
By running the --check-db command (see section 6 and the note above) these data sets are used to correctly set up the database with the powers wielded, so that when a Character receives this item, the Character also gains the powers to use through the item. If a power type prefix is included for one or more power name, the respective database is searched for a matching entry: thus a Wizard or Priest spell can be specified as a power without having to explicitly add a duplicate of it to a Powers Database. If no prefix is specified, the system will first search the Powers Databases (API-supplied and user-supplied) for a match and, if not found there, will then search the MU Spells Databases, the Priest Spells Databases, all Magic Items databases, and then the character sheet of the creature wielding the item power for a match, in that order. An error occurs if no matches are found anywhere.
' + +'Note: if a Character picks up two Power-wielding items with exactly the same item name (i.e. two copies of the same item) the results are unpredictable. This is best avoided. The GM can use the --gm-edit-mi menu to rename one or both items with a unique name to differentiate them: see the MagicMaster API documentation for details.
' + +'Feel free to just copy the specification for a Ring-of-Shooting-Stars in an extracted copy of the Rings database and save it to a new Ability Macro with a different name, and then alter the power names, speeds, and uses per day, as well as the API Button --mi-power commands and the other text, to form new power-wielding magic items. Also, the Ring does not have to have 6 powers - just remove or add one or more repeating data sets to reduce or increase the number of powers.
' + +'Here is an example of a spell-storing magic item:
' + +'&{template:RPGMring}{{name=Ring of Spell Storing with Haste x2, Slow, Light & Sleep}}{{subtitle=Ring}}Specs=[Ring of Spell Storing,Ring,1H,Conjuration-Summoning]{{Speed=[[5]] regardless of spell}}RingData=[w:Ring of Spell Storing HHSLS,sp:5,rc:uncharged,ns:5], [cl:MU,w:Haste,sp:5,lv:6], [cl:MU,w:Haste,sp:5,lv:6], [cl:MU,w:Slow,sp:5,lv:7], [cl:MU,w:Light,sp:5,lv:3], [cl:MU,w:Sleep,sp:5,lv:3] {{Size=Tiny}}{{Store spell=[Store Priest Spell](!magic --mem-spell MI-PR|@{selected|token_id})
'
+ +'[Store Wizard Spell](!magic --mem-spell MI-MU|@{selected|token_id})}}{{Cast spell=[View](!magic --view-spell mi-muspells|@{selected|token_id}) or [Cast](!magic --cast-spell MI|@{selected|token_id}) spells}}{{desc=A ring of spell storing contains 1d4+1 spells which the wearer can employ as if he were a spellcaster of the level required to use the stored spells. The class of spells contained within the ring is determined in the same fashion as the spells on scrolls (see "Scrolls"). The level of each spell is determined by rolling 1d6 (for priests) or 1d8 (for wizards). The number rolled is the level of the spell, as follows:
'
+ +'Priest: 1d6, if 6 is rolled, roll 1d4 instead.
'
+ +'Wizard: 1d8, if 8 is rolled, roll 1d6 instead.
'
+ +'Which spell type of any given level is contained by the ring is also randomly determined.
'
+ +'The ring empathically imparts to the wearer the names of its spells. Once spell class, level, and type are determined, the properties of the ring are fixed and unchangeable. Once a spell is cast from the ring, it can be restored only by a character of appropriate class and level of experience (i.e., a 12th-level wizard is needed to restore a 6th-level magical spell to the ring). Stored spells have a casting time of [[5]].}}
This is a specific version of a Ring of Spell Storing as the spells stored are specified in the macro. Alternatively, a blank Ring of Spell Storing is provided in the API Rings database. It is possible to use the --gm-edit-mi command menu to select this blank ring and use the facilities provided by the menu to add spells to this blank ring, and then rename it to reflect what the GM wants the ring to be. Again, see the MagicMaster API documentation for details.
' + +'The only new field in these data sets is:
' + +'lv: | <#> | The level of the caster who cast the spell into the ring. The spell will have effects as if cast at this level when cast from the ring. |
---|
The lv: field only specifies the level of the initial spell caster when the item is first found. Once owned and used, the level of the spell caster is recorded each time a spell is refreshed by casting into the item. As the item is then passed from one Character to another, or stored in a container and recovered later, the levels at which the spells were cast is retained. However, if the item is reloaded from the databases, or a duplicate of the item is placed by the DM and found by another character, that version of the item will have the spell caster levels from the database definitions. Note that if a single Character picks up two versions of exactly the same spell storing item (i.e. with the same item name) the results are unpredicable... The GM should use the --gm-edit-mi menu to rename one or both of the rings to give them unique names.
' + +'Some spell-storing magic items are more flexible in what they can do than the standard Ring of Spell Storing. Here is an example of one that the player character can add spells to, and alter the spells stored (within certain limits):
' + +'&{template:RPGMitem}{{name=(Vibrant Purple)}}Specs=[Ioun Stone,Miscellaneous,0H,Stone]{{}}MiscData=[w:Vibrant Purple Ioun Stone,st:Floating Vibrant Purple Stone,wt:2,sp:3,qty:2d6,lvl:1,store:any,rc:single-uncharged]{{}}%{MI-DB|Ioun-Stone}
{{Size=T}}{{Use=Can store "quantity" levels of spell that the possessor casts into it. [Store Spells](!magic --mem-spell MI-MU|@{selected|token_id}|Ioun-Stone-Vibrant-Purple) or [View Spells](!magic --view-spell MI|@{selected|token_id}|Ioun-Stone-Vibrant-Purple) or [Cast Spell](!magic --cast-spell MI|@{selected|token_id}||Vibrant Purple Ioun Stone||Ioun-Stone-Vibrant-Purple). The quantity/number of charges represents the number of levels of spell that can be stored.}}{{Looks Like=A vibrant purple prismatic stone that is floating in the air}}{{GM Info=This version of the Vibrant Purple Ioun Stone can store *any* spell and the player character can change the spells stored.}}
This data definition has a couple of features not previously seen.
' + +'The store: and lvl: data attributes: These attributes are unique to spell-storing magic items, and define limits on how spells can be stored in the item, using the !magic --mem-spell
command.
lvl: | [ 1 / 0 ] | Defaults to \'0\'. Flag restricting the total levels of spell that can be cast into the item to be the item\'s qty: / number of charges |
---|---|---|
store: | [ add / any / change / none ] | Defaults to none (or not specified). Specifies flexibility of spell storing |
add | Allows the character to add additional stored spells to the item (up to any level limit) but stored spells can only be replaced by the same | |
change | Allows the character to change what spell is stored in each slot (within any level limit), but not to add spells to additional slots | |
any | Allows the character to both add additional spells and to change the currently stored spells (up to any level limit). |
Merging another item definition: Using the Roll20 standard syntax of %{...|...} to merge in another item data definition saves duplicating text that has already been written. The name before the \'pipe\' character (\'|\') is the name of the database to get the merged specification from - this can be a database held in memory by the APIs or a character sheet database: the APIs will work this out. The APIs only recognise the first Specs=[...] and Data=[...] they find, so these are presented in this definition before the %{...|...}. On the contraty, when displaying the template to a player, later {{ xxx=... }} with the same xxx will overwrite earlier ones, so the differences in the description for this particular item are placed after the %{...|...}.
' + +'Some items can store other items, including magic items. When such an item is viewed, used or exchanged between containers and characters, a character sheet specifically for the MI-storing item is created, or found if already previously created. The very act of viewing or using the item will trigger the creation or selection - there is no need for the GM or Player to do so. An example of this is a Bag of Holding.
' + +'&{template:RPGMitem}{{name=Bag of Holding}}{{subtitle=Magic Item}}Specs=[Bag of Holding,Miscellaneous,1H,Alteration]{{Size=[[15]]/[[250]]lbs, 30cu.ft}}MiscData=[w:Bag of Holding,st:Bag,sp:0,rc:uncharged,bag:2],[cl:MI,w:Potion-of-Healing,qty:1],[cl:MI,w:Scroll of Protection vs Magic,qty:2]{{Access=Drag the *Bag of Holding* token onto the map and use your MI menu *Search* function (to retrieve stuff from it) or *Store* function (to put stuff in it)}}{{desc=As with other magical bags, this one appears to be a common cloth sack of about 2 feet by 4 feet size. The Bag of Holding opens into a nondimensional space, and its inside is larger than its outside dimensions. Regardless of what is put into this item, the bag always weighs a fixed amount. This weight, the bag\'s weight limit in contents, and its volume limit are 15 lbs. 250 lbs. 30 cu. ft.
'
+ +'If overloaded, or if sharp objects pierce it (from inside or outside), the bag will rupture and be ruined. The contents will be lost forever in the vortices of nilspace.}}
The important attributes are:
' + +'bag: | <#> | Identifies the item as generating an item character sheet. Up to # items can be defined as initially being held in the item sheet (default 0) |
---|---|---|
cl: | \'MI\' | Subsequent data sets with class \'MI\' define items initially held in the item, and will be inserted in a newly created item sheet when first viewed or used |
w: | <text> | The name of the item to initially be stored in the item character sheet MI bag. Should be an item named in a database |
qty: | <#> | The initial quantity of this item to be stored in the item-holding item character sheet MI bag (default 1) |
And one additional new attribute:
' + +'st: | \'\' | Defines the "Item Type" (or SuperType) to be displayed when a container is searched that has its properties set to only show the types of items contained. If not provided, defaults to the item class from the Specs definition |
---|
Thus, the definition of the particular Bag of Holding defined above, when a character has it in their Items & Equipment and either views or uses the item, will result in a new separate character sheet being created, named "Bag of Holding" (same as the item), placed in the controlling Player\'s journal and marked as controlled by that player, and for one Potion of Healing and two Scrolls of Protection vs. Magic to be inserted automatically into the Bag of Holding, ready for the Character to find and use by dragging the bag onto the map from their Journal and Searching the Bag.
' + +'Once the Bag is created, the existing items can be taken out into the Character\'s own items & equipment, or new ones placed in the bag, the bag passed from one Character to another or itself placed in a different container.
' + +'It is recommended that, where a GM places multiple item-holding items in a campaign, such as multiple Bags of Holding, that the GM uses the functions of the [Add Items] menu to rename each with a different name in the original container to which they are placed and before each is viewed or used (i.e. before the item character sheet is created by the system) - perhaps naming each after some previous owner or its creator. This prevents confusion with multiple Character Sheets all with the same name (which Roll20 will allow, but can definately be confusing for the GM who will see them all even if players only see the ones they control).
' + +'Weapons, magical or not, are special types of items in the Magic Items databases. If coded properly (in the same way as those in the MI-DB-Weapons database), they can be used with the AttackMaster API to implement fully automatic weapon management, the ability to hold weapons "in-hand" or sheathed, to have automatic ammo and range management for ranged weapons, automatic entry of weapons into the melee and/or ranged weapons tables, ready to make attacks with magical plusses and other specifications all set up, and support for dancing weapons (ones that can attack without being held by the Character), creatures with more than 2 hands, and 1-handed weapons, 2-handed weapons, and even weapons that need more than 2 hands!
' + +'See the Weapon & Armour Database Help handout and AttackMaster API documentation for how Weapon definitions should be structured for use with the AttackMaster API, which are just a few additions to the standard definition of an item.
' + +'Like weapons, armour and shields of all types (including magical armour like magical Bracers and Rings of Protection) can be coded to be used with the AttackMaster API to automatically calculate the appropriate AC for various scenarios (such as with & without Shield, from the back, if surprised, etc). This will take into account if the armour is valid for the character class, determine which is the best armour combination that the character has, if various armour elements can or can\'t work together, and add in Dexterity bonuses or impairments. It will also allow magical effects cast on the character to take effect or be adjusted via the token "circles" and highlight when such an effect is in place by showing the relevant token bar (only when there is a difference between the token AC and calculated AC).
' + +'See the Weapon & Armour Database Help handout and AttackMaster API documentation for how Armour & Shield definitions should be structured for use with the AttackMaster API, which are just a few additions to the standard definition of an item.
' + +'Also, see the RoundMaster API documentation for how magical effects can be placed on and affect tokens and characters.
' + +'Below are lists of the current possible values for the item database Ability macro sections.
' + +'Specs=[Type, Item-Class, Handedness, Group-Type]' + +'
There are no default settings for any of the Specs data fields. All must be explicitly specified.
' + +'There is an infinite list of magic item types: generally the type is the magic item name. A magic item can have more than one type, with each separated by a vertical bar character \'|\'
' + +'Any magic item can have more than one class, each separated by a vertical bar \'|\'. It will then behave and be listed as each of the specified classes.
' + +'Weapon | Weapons that are not Melee or Ranged weapons or any other class |
---|---|
Magic | Magic attacks that are not melee or ranged attacks, often a spell or power of a magic item |
Melee | Melee weapons that are used in hand-to-hand combat |
Innate-Melee | Melee weapons that do not attract any proficiency penalties |
Ranged | Ranged weapons that are either thrown or fire ammunition |
Innate-Ranged | Ranged weapons that do not attract any proficiency penalties |
Ammo | All types of ammunition that is used by Ranged weapons |
Armour | Any type of armour that does not need to be held to work |
Armor | The same as Armour |
Helm | Any type of armour or clothing worn on the head |
Shield | A barrier that is held in hand(s) and defends against one or more attacks from the front |
Protection-cloak | Any type of clothing that has protective qualities |
Potion | Any type of potion, oil, pill or similar that is consumed or rubbed on |
Scroll | Scrolls and spell books, that contain one or multiple spells |
Scrollcase | An object that can hold a scroll |
Wand | Wands that cast spells or spell-like effects when wielded in the hand |
Staff | Quarterstaffs and similar large bludgeoning items that can also have spell-like abilities |
Rod | Walking-stick sized rods that can do spell-like effects, especially when used to attack |
Ring | Rings that are worn on a finger, one to each hand, that protect, have powers or spells |
Protection-Ring | Any special type of ring that imparts protective qualities |
Protection-[item] | Any item (other than clothing or a ring) that imparts protective qualities |
Light | All types of lantern, torch, and other illumination |
DM-item | An item that only appears in a list button on the menu displayed by --gm-edit-mi |
Attack-macro | An attack macro template for a magic item held in a MI database |
Miscellaneous | Anything that does not fit in one of the other categories |
Unspecified | Items without any Specs section or an empty Class definition are listed under DM-Only |
0H Items that do not require to be held to work (e.g. a Ring, Buckler or a Helm)
'
+ +'1H An item that must be held in one hand to work, such as a Wand
'
+ +'2H Items that need two hands to wield, like a Staff
'
+ +'3H Items that need three hands to use, perhaps by two characters...
'
+ +'... etc.
Currently, all Magic Items other than Weapons and Armour use the same set of magical schools as for Spells & Powers, as they mostly perform spell-like effects. See section 7.1(d) for the list.
' + +'Definitions for Data Section field types for Weapons & Armour can be found in the AttackMaster API documentation. Below are the definitions for Spell, Power & other Magical Item types.
' + +'Note: Always refer to the database specification definitions in other sections above for detailed information on the use of these Field specifiers. Not all specifiers have an obvious use.
' + +'Field | ' + +'Format | ' + +'Default Value | ' + +'Description | ' + +'Can be used in | ' + +'|||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Spell Data | '
+ +' Potion Data | '
+ +' Scroll Data | '
+ +' Wand Data | '
+ +' Staff Data | '
+ +' Rod Data | '
+ +' Ring Data | '
+ +' Misc Data | '
+ +' ToHit Data | '
+ +' AC Data | '
+ +' ||||
w: | < text > | \'-\' | Name to be displayed | X | X | X | X | X | X | X | X | X | |
w: | < text > | \'\' | Name of spell or power (Not case sensitive) | X | |||||||||
+: | [ + / - ] # | 0 | Magical adjustment | X | X | X | X | X | X | ||||
n: | # [ / # ] | 1 | Attacks per round | X | X | X | X | X | |||||
st: | < text > | \'\' | Item type to display | X | X | X | X | X | X | X | X | ||
hide: | [hide / nohide] | \'\' | Hidden item status | X | X | X | X | X | X | X | X | ||
rev: | [manual / view / use] | \'\' | Hidden item reveal trigger | X | X | X | X | X | X | X | X | ||
sz: | [ t / s / m / l / h ] | \'\' | Size of item | X | X | X | X | X | X | X | |||
sp: | [-]# or Dice Roll spec | 0 | Speed in segments (1/10 round) | X | X | X | X | X | X | X | X | X | |
wt: | # | 1 | Weight of item in lbs | X | X | X | X | X | X | ||||
rules: | [+/-][rule] | ... | 0 | Save / Check rules | X | X | X | X | X | X | ||||
svXXX: | [=][+/-]# | 0 | Save / Check mod | X | X | X | X | X | X | ||||
ppa: | [+/-]# | 0 | Pick Pocket mod | X | X | X | X | X | |||||
ola: | [+/-]# | 0 | Open Locks mod | X | X | X | X | X | |||||
rta: | [+/-]# | 0 | Find/Remove Traps mod | X | X | X | X | X | |||||
msa: | [+/-]# | 0 | Move Silently mod | X | X | X | X | X | |||||
hsa: | [+/-]# | 0 | Hide in Shadows mod | X | X | X | X | X | |||||
dna: | [+/-]# | 0 | Detect Noise mod | X | X | X | X | X | |||||
cwa: | [+/-]# | 0 | Climb Walls mod | X | X | X | X | X | |||||
rla: | [+/-]# | 0 | Read Languages mod | X | X | X | X | X | |||||
lla: | [+/-]# | 0 | Ledgend Lore mod | X | X | X | X | X | |||||
on: | command | \'\' | Cmd to execute when worn | X | X | X | X | ||||||
off: | command | \'\' | Cmd to execute when removed | X | X | X | X | ||||||
ns: | # | 0 | Number of stored spells & powers defined for item | X | X | X | X | X | X | X | |||
w: | < text > | \'-\' | Name of stored spell or power (Not case sensitive) | X | X | X | X | X | X | X | |||
cl: | MU / PR / PW | \'\' | Type of stored spell or power | X | X | X | X | X | X | X | |||
lv: | # | 1 | Level at which spell/power is cast | X | X | X | X | X | X | ||||
pd: | -1 / # | 1 | Number per day (power only) | X | X | X | X | X | X | ||||
rc: | Charged / Uncharged / Rechargeable / Recharging / Self-chargeable / Cursed / Charged-Cursed / Recharging-Cursed / Self-chargeable-Cursed | Uncharged | Initial charged and Cursed status of item when found (Can be changed by DM using -gm-only-mi command once added to Character Sheet) Not case sensitive | X | X | X | X | X | X | X | X | X | |
c: | # | 1 | The number of charges expended by using a charged magic item. Uncharged items always use 0 charges | X | X | X | X | X | X | X | X | X | |
desc: | [MU-/PR-/PW-/MI-]name | \' \' | Power or Spell to display | X | X | ||||||||
msg: | < text > | \' \' | Attack message | X | X | ||||||||
cmd: | Command | \' \' | Attack API command | X | X | ||||||||
learn: | [ 0 | 1 ] | 0 | Learnable stored spells | X | X |
The Character Sheet field mapping to the API script can be altered using the definition of the fields object, the definition for which can be found at the top of the game-version-specific RPGMaster Library API for the game-version you are using. You can find the complete mapping for all APIs in the RPGMaster series, with an explanation of each, in a separate document - ask the Author for a copy.
' + +'The DM can add Character Class databases as character sheets that have names that start with Class-DB. The Class definitions that come with the installed game-version-specific RPGMaster Library can be extracted to a character sheet and viewed by using the !magic --extract-db Class-DB or !attk --extract-db Class-DB commands. Note: it is best to delete the extracted Class-DB database character sheet after viewing/using, so that the system uses the much faster internal database version. After deleting or changing any character sheet database, always run the !magic --check-db or !attk --check-db command to re-index the databases.
' + +'Classes: Class-DB-[added name]
' + +'Those with version numbers of the form v#.# as part of the name will be ignored.
' + +'As previously stated, each class definition has 3 parts in the database (see Section 1): an Ability Macro with a name that is unique and matches the Class being defined, an Attribute with the name of the Ability Macro preceded by "ct-", and a listing in the database character sheet of the ability macro name separated by \'|\' along with others of the same base class: the base classes being "Warrior", "Wizard", "Priest", "Rogue", and "Psion". The quickest way to understand these entries is to examine existing entries. Do extract the root database using the --extract-db command and take a look (remember to delete it after viewing - see above)
' + +'Note: The DM creating new classes does not need to worry about anything other than the Ability Macro in the database, as running the command --check-db will update all other aspects of the database appropriately for all databases, as long as the Specs and Data fields in the Ability Macros are correctly defined. Use the name of the particular database as a parameter to check and update just that database. Running the command --check-db with no parameters will check and update all databases.
' + +'Ability macros can be added to a database just by using the [+Add] button at the top of the Abilities column in the Attributes and Abilities tab of the Database Character Sheet, and then using the edit "pencil" icon on the new entry to open it for editing. Ability macros are standard Roll20 functionality and not dependent on the API. Refer to the Roll20 Help Centre for more information.
' + +'The Ability Macro for a Class may look something like this:
' + +'&{template:RPGMclass}{{name=Thief}}{{subtitle=Rogue Class}}{{Min Abilities=Dex:[[9]]}}{{Race=Any}}{{Hit Dice=1d6}}{{Alignment=Any not Lawful}}Specs=[Thief,RogueClass,0H,Rogue]{{=**Powers**}}{{1st Level=Thieving Abilities *Pick Pockets, Open Locks, Find/Remove Traps, Move Silently, Hide in Shadows, Detect Noise, Climb Walls,* and *Read Languages* Also, Thieves can *Backstab*}}{{10th Level=Limited ability to use magical & priest scrolls, with 25% chance of backfire}}ClassData=[w:Thief, hd:1d6, align:ng|nn|n|ne|cg|cn|ce, weaps:club|shortblade|fencingblade|dart|handxbow|lasso|shortbow|sling|broadsword|longsword|staff, ac:padded|leather|studdedleather|elvenchainmail|magicitem|ring|cloak, rp:60.30, ppa:15, ola:10, rta:5, msa:10, hsa:5, dna:15, cwa:60, rla:0, lla:0]{{desc=Thieves come in all sizes and shapes, ready to live off the fat of the land by the easiest means possible. In some ways they are the epitome of roguishness.
The profession of thief is not honorable, yet it is not entirely dishonorable, either. Many famous folk heroes have been more than a little larcenous -- Reynard the Fox, Robin Goodfellow, and Ali Baba are but a few. At his best, the thief is a romantic hero fired by noble purpose but a little wanting in strength of character. Such a person may truly strive for good but continually run afoul of temptation.}}
The ability specification for this Rogue class uses a Roll20 Roll Template, in this case defined by the RPGMaster Library (see the documentation for the Library for specifications of this Roll Template), but any Roll Template you desire can be used. The entries in the Roll Template itself can be anything you desire, giving as much or as little information as you want. However, the important elements for the RPGMaster APIs are those highlighted. Each of the elements important to the database are inserted between the elements of the Roll Template, meaning they will not be seen by the player when the macro is run. Generally spaces, hyphens and underscores in the data elements are ignored, and case is not significant. Each element is described below:
' + +'Specs = [Character Class, Macro Type, Handedness, Base Class]' + +'
The Specs section describes what Character Class and Base Class this is (and tells the APIs that this is a macro of type "Class"). These fields must be in this order. This format is identical for all database items, whether in these databases or others used by the RPGMaster series of APIs. Where there are multiple answers for a field, separate each by \'|\'. Note:Only A-Z, a-z, 0-9, hyphen/minus(-), plus(+), equals(=) point(.) and vertical bar(|) are allowed. Replace any forward slash with hyphen.
' + +'Character Class | the Character Class name, often the same as the ability macro name. |
---|---|
Macro Type | the type of the data in this Ability Macro, one of WarriorClass, WizardClass, PriestClass, RogueClass, or PsionClass. |
Handedness | #H, where # is the number of hands needed to be a character of this class (not currently used). |
Base Class | the base class that this class belongs to, one of Warrior, Wizard, Priest, Rogue, or Psion. |
ClassData=[w:Thief, hd:1d6, align:ng|nn|n|ne|cg|cn|ce, npp:-3, weaps:club|shortblade|dart|handxbow|lasso|shortbow|sling|broadsword|longsword|staff, ac:padded|leather|studdedleather|elvenchain|magicitem|ring|cloak]' + +'
The ClassData section specifies the data relating to the class. These fields can be in any order.
' + +'w: | <text> | the name of the class |
---|---|---|
align: | <lg|ln|le|ng|nn|n|ne|cg|cn|ce> or <any> | the valid alignments for characters of this class, separated by \'|\' (not currently restricted) |
attr: | #[:#] | minimum and maximum starting attribute values (default is 3:18) |
race: | <list of races> or <any> | the races that can take this class, separated by \'|\' (not currently restricted) |
hd: | <dice roll spec> | the dice roll specification for hit points at each level (not currently used, for future expansion) |
npp: | <[-/+]#> | optional field to set a bespoke non-proficient weapon penalty for the character class. If not provided defaults to that for the Base Class. |
weaps: | <list of weapons & weapon types> or <any> | a vertical bar \'|\' separated list of weapons and weapon types that are valid for the class (see the Weapons database/documentation for types). Preceeding by \'!\' means \'not this weapon\' and with \'+\' means \'include this whatever\' |
ac: | <list of armour types> or <any> | a vertical bar \'|\' separated list of armour and armour types that are valid for the class (see the Weapons database/documentation for types). \'!\' and \'+\' work the same as for weapons |
The list of weapons and weapon types listed after the "weaps:" tag are checked by the system when a character tries to take a weapon in-hand using the "Change Weapons" dialogue or AttackMaster --weapon command, as determined by the API configuration setting, accessed via the MagicMaster or AttackMaster --config command. This configuration can be to restrict weapons to those listed ("Strict" mode), to give unlisted weapons a penalty of double the non-proficient weapon penalty for the base class ("Lax" mode), or to ignore this list and allow any weapon to be proficient or to just get the standard non-proficient weapon penalty ("Allowed" mode).
' + +'In exactly the same way as for weapons, armour and armour types listed after the "ac:" tag are checked when calculating the Armour Class of the character using the "Check AC" dialogue or AttackMaster --checkac command, or automatically by the APIs at various points when AC might change, again according to the API configuration settings accessed via the --config command. This configuration can restrict a class to the armours and armour types listed for the class ("Rules" mode), or not restrict usage at all ("Allowed" mode).
' + +'Three additional field tags are optionally available to allow the default weapon attacks per round progression to be overridden with a bespoke progression. Any one, two or all three can be specified: if just the progression level sequence is given, these levels will override the default levels, and similarly for the melee and ranged weapon mods, and defaults will be used or those not overridden. This only works for classes that are types of Warrior. The defaults are those specified for the Warrior class in the Player\'s Handbook.
' + +'attkl: | <0|#|#|...> | a vertical bar \'|\' separated list of levels (the first must be 0) at which the next higher number of attacks per round is achieved. |
---|---|---|
attkm: | <#|#|#|...> | a vertical bar \'|\' separated list of modifications to the standard number of attacks per round for any melee weapon used. Each can be an integer, a decimal float (# . #) or a fraction (# / #) |
attkr: | <#|#|#|...> | a vertical bar \'|\' separated list of modifications to the standard number of attacks per round for any ranged weapon used. Each can be an integer, a decimal float (# . #) or a fraction (# / #) |
The default Saving Throw table from the Player\'s Handbook can be overridden for any class definition. A new set of base saving throws by experience level can be defined.
' + +'&{template:RPGMclass}{{name=Dwarven Defender}}{{subtitle=Warrior Class}}{{Min Abilities=Str:[[12]], Con:[[15]]}}{{Race=Dwarf only}}{{Alignment=Any}}Specs=[Dwarven Defender,WarriorHRClass,0H,Warrior]{{Hit Dice=1d12}}{{=**Powers**}}{{1st Level=*Defensive Stance* (1/4 levels per day}}ClassData=[w:Fighter, align:any, hd:1d12, race:dwarf, weaps:axe|club|flail|longblade|fencingblade|mediumblade|shortblade|polearm, ac:any, svl0:16|18|17|20|19, svl1:12|17|15|16|15, svl3:11|16|14|15|14, svl5:10|14|12|12|12, svl7:9|13|11|11|11, svl10:7|11|9|8|9, svl13:4|9|6|5|7, sv16:2|7|4|3|4, ns:1][cl:PW, w:Defensive-Stance, lv:1, pd:1l4]{{desc=The Dwarven defender is a formidable warrior. They are trained in the art of defence from a young age and make a defensive line nearly unbreakable.
'
+ +'The class is limited to Dwarves.
'
+ +'They can wear any armour but tend to go with the heaviest and toughest they can afford. They always use a shield, whenever possible a special Dwarven Tower shields (+1 in melee but +3 vs missiles when braced and in position). To use a Tower Shield requires a weapon proficiency slot. The dwarven Tower Shield has to be acquired in the campaign, it isn’t just granted to the character on creation (it’s a bit like a Paladins Warhorse). It may take many levels before they get a quest to acquire one.
'
+ +'They can only become proficient, specialise and double specialise in axes (not great axes) or hammers. They can never use missile weapons like a bow or crossbow but can throw hammers or axes.
'
+ +'They get bonus non weapon proficiency slots in Armourer, Blacksmithing and Mining.}}
In addition to the elements described previously, the ClassData section specifies new elements regarding saving throws (ignore the ns: and everything beyond for now):
' + +'ClassData=[w:Fighter, align:any, hd:1d12, race:dwarf, weaps:axe|club|flail|longblade|fencingblade|mediumblade|shortblade|polearm, ac:any, svl0:16|18|17|20|19, svl1:12|17|15|16|15, svl3:11|16|14|15|14, svl5:10|14|12|12|12, svl7:9|13|11|11|11, svl10:7|11|9|8|9, svl13:4|9|6|5|7, svl16:2|7|4|3|4, ns:1]' + +'
Each svl# element specifies the base saves at and above experience level "#", for the five standard base save types, Paralysation, Poison & Death | Rod, Staff & Wand | Petrification & Polymorph | Breath Weapon | Spells. The highest specification element applies to all higher experience levels.
' + +'Magic Items, Race definitions, and other database elements that affect a character can specify modifications to the base Saving Throws (whether using the defaults or custom Class specifications) by using the data element svXXX:[+-=]#,, where "XXX" is one of par, poi, dea, rod, sta, wan, pet, pol, bre, spe or sav for all saving throws, str, dex, con, int, wis, chr or atr for all character attributes, or all for everything, followed by a colon, then a plus (+), a minus (-), an equals (=), and a number, or just a number with nothing before it. Each of the three-letter qualifiers refers to the relevant save, except "all" which applies the modifier to all saves. Preceeding the modifier amount by plus (+) or nothing improves the save by the modifier, preceeding by minus (-) worsens the save by the modifier, and by equals (=) overrides any other modifier being applied and applies only the best modifier of that type from all items preceeded by an equals. Obviously, racial mods apply at all times (unless overridden by a magic item using the "=" modifier), and magic item mods only apply if the character has the magic item in their held items.
' + +'It is also possible to specify rules for when the specified saving throw/ability check modifiers are valid. These are specified by including the data tag rules:. See the Magic Database Help handout for more information on rules:.
' + +'While standard Wizards and Priests are very similar to the standard specification above, the definitions of specialist spellcaster classes is slightly more complex.
' + +'&{template:RPGMclass}{{name=Conjurer}}{{subtitle=Wizard Class}}{{Min Abilities=Int:[[9]], Con:[[15]]}}{{Alignment=Any}}{{Race=Human & Half Elf}}{{Hit Dice=1d4}}Specs=[Conjurer,WizardClass,0H,Wizard]{{=**Spells**}}{{Specialist=Conjuration / Summoning}}{{Banned=Greater Divination & Invocation}}ClassData=[w:Conjurer, hd:1d4, race:human|halfelf, sps:conjuration|summoning|conjurationsummoning, spb:greaterdivination|invocation, weaps:dagger|staff|dart|knife|sling, ac:magicitem|ring|cloak]{{desc=This school includes two different types of magic, though both involve bringing in matter from another place. Conjuration spells produce various forms of nonliving matter. Summoning spells entice or compel creatures to come to the caster, as well as allowing the caster to channel forces from other planes. Since the casting techniques and ability requirements are the same for both types of magic, conjuration and summoning are considered two parts of the same school.}}
' + +'ClassData=[w:Conjurer, hd:1d4, race:human|halfelf, sps:conjuration|summoning|conjurationsummoning, spb:greaterdivination|invocation, weaps:dagger|staff|dart|knife|sling, ac:magicitem|ring|cloak]' + +'
The ClassData for specialist casters includes additional tags to specify the schools/spheres of magic that the caster can and cannot use (and for priests major and minor access to spheres).
' + +'sps: | <text|text|...> or <any> | a list of specialist schools or major spheres separated by vertical bars (\'|\') |
---|---|---|
spb: | <text|text|...> | a list of banned schools/spheres that this class is not allowed to use separated by vertical bars (\'|\') |
spm: | <text|text|...> | a list of minor spheres (only relevant to Priest classes) separated by vertical bars (\'|\') |
The spellcaster will be restricted to memorising only spells from the schools/spheres listed depending on the API configuration using the --config command. The configuration can be to restrict to the specified schools/spheres ("Strict" mode) or allow all at any level ("Allowed" mode). The DM will also have a single button to add all valid spells at all levels to a Priest character sheet using the [Token-setup] macro or the !cmd --abilities command, and then using the [Add to Spellbook] / [Priest] dialogue.
' + +'Using the classes called "Wizard" or "Priest" will always grant the standard Wizard and Priest spells per level respectively as per the Player\'s Handbook, thus the class specifications are no different from that above. Also, any class name placed in the Wizard class fields (e.g. the second class definition column of the Advanced 2e sheet) will get standard Wizard spell casting capabilities (unless otherwise specified as below), and those in the Priest class fields (e.g. the third class definition column of the Advanced 2e sheet) will get standard Priest spell casting capabilities (unless otherwise specified as below).
' + +'A non-standard spellcaster (such as a Ranger, Paladin or Bard, or any class you wish to specify of a similar nature) can have their spellcasting capabilities specified in the class definition:
' + +'&{template:RPGMclass}{{name=Priest of Magic}}{{subtitle=Priest Class}}{{Min Abilities=Wis:[[12]], Int:[[13]]}}{{Race=Human or Half Elf}}{{Hit Dice=1d8}}{{Reference=*House Rules v16*}}{{=**Alignment**}}{{Deity=True Neutral}}{{Priests=Any Neutral}}{{Flock=Any Alignment}}{{ =**Spells**}}{{Major Spheres=All, Divination, Protection, Healing, Elemental}}{{Minor Spheres=Sun}}Specs=[Priest of Magic,PriestClass,0H,Priest]{{Powers=None}}ClassData=[w:Priest of Magic, hd:1d8, race:human|halfelf, align:ng|nn|n|ne, weaps:dagger|staff|dart|knife|sling, ac:any, sps:any, slv:4|3|12|MU, spl1:1|2|2|3|3|3|4|4|4|4|5|5, spl2:0|0|1|1|2|2|3|3|3|4|4|4, spl3:0|0|0|0|1|1|2|2|3|3|3|3, spl4:0|0|0|0|0|0|1|1|1|2|2|3],[w:Priest of Magic, sps:all|divination|protection|healing|elemental, spm:sun, slv:7|1|100|PR, spl1:1|2|2|3|3|3|3|3|3|3|3|3|3|3|4|4|4|4|4, spl2:0|0|1|1|2|2|3|3|3|3|3|3|3|3|3|4|4|4|4, spl3:0|0|0|0|0|1|1|2|2|3|3|3|3|3|3|3|4|4|4, spl4:0|0|0|0|0|0|0|0|1|1|2|2|3|3|3|3|3|4|4, spl5:0|0|0|0|0|0|0|0|0|0|0|1|1|2|2|3|3|3|4, spl6:0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|1|2|2|3, spl7:0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|1|2|2|2]{{desc=The Priest of Magic is an optional character class that can be used if your DM allows. It is a curious class in that it is a priest of the god of Magic, who then grants the priest the use of some limited Wizard spells as well as a slightly more restricted range of clerical spells.}}
' + +'The Priest of Magic (a "House Rules" class for my group) can cast some Wizard spells at the expense of loosing some Priest spellcasting capability. Its class definition has ClassData for both "MU" and "PR" spells, in two separate sections (enclosed in each comma-separated \'[...]\').
' + +'slv: | <#|#|#|(MU/PR)> | three numbers followed by either MU or PR (no brackets), separated by vertical bars (\'|\'). The first number is the highest level of spell that can be cast, the second the first class level at which spells can be cast, and the third the maximum casting level, followed by the class of spells being specified (MU=Wizard, PR=Priest) |
---|---|---|
spl#: | <#|#|#|...> | for spells of level spl#, starting at the class level at which spells can be cast, the numbers of spells that can be cast at that and subsequent levels |
specmu: | [ 0 | 1 ] | (not shown above) a flag signifying whether a non-standard wizard-type spellcaster is a specialist or not, granting the extra spell per level if \'1\' |
Rogues, the sub-classes of rogue such as Thieves and Bards, can have the base value defined for their rogue skills, such as picking pockets and opening locks.
' + +'&{template:RPGMclass}{{name=Thief}}{{subtitle=Rogue Class}}{{Min Abilities=Dex:[[9]]}}{{Race=Any}}{{Hit Dice=1d6}}{{Alignment=Any not Lawful}}Specs=[Thief,RogueClass,0H,Rogue]{{=**Powers**}}{{1st Level=Thieving Abilities *Pick Pockets, Open Locks, Find/Remove Traps, Move Silently, Hide in Shadows, Detect Noise, Climb Walls,* and *Read Languages* Also, Thieves can *Backstab*}}{{10th Level=Limited ability to use magical & priest scrolls, with 25% chance of backfire}}ClassData=[w:Thief, hd:1d6, align:ng|nn|n|ne|cg|cn|ce, weaps:club|shortblade|fencingblade|dart|handxbow|lasso|shortbow |sling|broadsword|longsword|staff, ac:padded|leather|studdedleather|elvenchainmail|magicitem|ring|cloak, rp:60.30, ppa:15, ola:10, rta:5, msa:10, hsa:5, dna:15, cwa:60, rla:0, lla:0]{{desc=Thieves come in all sizes and shapes, ready to live off the fat of the land by the easiest means possible. In some ways they are the epitome of roguishness.
The profession of thief is not honorable, yet it is not entirely dishonorable, either. Many famous folk heroes have been more than a little larcenous -- Reynard the Fox, Robin Goodfellow, and Ali Baba are but a few. At his best, the thief is a romantic hero fired by noble purpose but a little wanting in strength of character. Such a person may truly strive for good but continually run afoul of temptation.}}
The base value for rogue skills used by this class of character are specified using the following tags:
' + +'ppa:[+/-]# | Specifies the base skill percentage for pick pockets (+ is beneficial) |
---|---|
ola:[+/-]# | Specifies the base skill percentage for open locks (+ is beneficial) |
rta:[+/-]# | Specifies the base skill percentage for find/remove traps (+ is beneficial) |
msa:[+/-]# | Specifies the base skill percentage for move silently (+ is beneficial) |
hsa:[+/-]# | Specifies the base skill percentage for hide in shadows (+ is beneficial) |
dna:[+/-]# | Specifies the base skill percentage for detect noise (+ is beneficial) |
cwa:[+/-]# | Specifies the base skill percentage for climb walls (+ is beneficial) |
rla:[+/-]# | Specifies the base skill percentage for read languages (+ is beneficial) |
lla:[+/-]# | Specifies the base skill percentage for legend lore (+ is beneficial) |
rp:#.# | Specifies the base.increase for player allocatable skill points |
Not every tag needs to be specified. The default value for any missing specification of a rogue skill is zero.
' + + +'A character class can also be granted powers, and these can be specified in the class definition both as text for the Player to read, and also coded so the APIs can read them.
' + +'&{template:RPGMclass}{{name=Priest of Light}}{{subtitle=Priest Class}}{{ =**Alignment**}}{{Deity=Neutral Good}}{{Priests=Any Good}}{{Flock=Any Neutral or Good}}{{Hit Dice=1d8}}Specs=[Priest of Light,PriesthoodClass,0H,Priest]{{ =**Powers**}}{{1st Level=*Infravision, Turn Undead*}}{{3rd Level=*Laying on Hands*}}{{5th Level=*Charm/Fascination*}}{{9th Level=*Prophecy*}}{{ =**Spells**}}{{Major Spheres=All, Charm, Divination, Healing and Sun}}{{Minor Spheres=Animal, Creation, Necromantic and Plant}}ClassData=[w:Priest of Light, align:LG|NG|CG, hd:1d8, weaps:bow|crossbow|dagger|dirk|dart|javelin|knife|slings|spear, ac:leather|padded|hide|magicitem|ring|cloak, sps:all|charm|divination|healing|sun, spm:animal|creation|necromantic|plant, ns:5][cl:PW, w:Infravision, lv:1, pd:-1][cl:PW, w:Turn Undead, lv:1, pd:-1][cl:PW, w:Laying on Hands, lv:3, pd:1][cl:PW, w:Charm-Fascination, lv:5, pd:1][cl:PW, w:Prophecy, lv:9, pd:1]{{desc=The god of all forms of light: Sunlight, moonlight, firelight, etc. The god is a friend of life, a patron of magic, a proponent of logical thought, and an enemy of the undead.
'
+ +'The priesthood of the god is devoted to celebrating these aspects of the god and to promoting positive forces such as healing.
'
+ +'Lesser gods of this attribute would be gods of one aspect of light. One god might be the god of Reason, another the god of Inspiration, etc.
'
+ +'This deity is as likely to be male as female.
'
+ +'The priests of this god are on good terms with the priests of Arts, Crafts, Darkness/Night, Dawn, Elemental Forces, Fire, Healing, Hunting, Literature/Poetry, Magic, Metalwork, Moon, Music/Dance, Oracles/Prophecy, and Sun.}}{{Reference=*The Complete Priest\'s Handbook* Sample Priesthoods}}
The ClassData specification now has a tag of ns: which specifies a following number of sections enclosed in square brackets (\'[...]\'), each of which defines a single power granted to characters of this class. These sections include the following fields:
' + +'ns: | <1> | specifies a following number of sections each of which defines a single power granted to characters of this class |
---|---|---|
cl: | <PW> | specifies the type of granted capability - for Class definitions, this is always PW (standing for Power) |
w: | <text> | the name of the power granted (which should match a definition in the Powers database Powers-DB) |
lv: | <#> | the character level at which they will gain this power |
age: | <#> | (not shown above) the character age value at which they will gain this power (used for e.g. dragons). The age value is stored in @{selected|age|max} |
pd: | < -1 / # / #L# > | the number of times per day the power can be used. A number, or -1 (meaning "at will"), or #L# which is first number per second number levels per day (e.g. 1L4 means once per day for L1 to L4, twice L5 to L8, etc) |
This allows the DM to use a single button to add all the specified powers to the Powers list of a specific character sheet using the [Token-Setup] macro or the !cmd --abilities command, and then using the [Add to Spellbook] / [Powers] dialogue. The Player will then only be able to memorise the appropriate powers for the character\'s level.
' + +'Exactly as with Character Classes, the DM can also add Race databases as character sheets that have names that start with Race-DB. The Race definitions that come with the installed game-version-specific RPGMaster Library can be extracted to a character sheet and viewed by using the !magic --extract-db Race-DB or !attk --extract-db Race-DB commands, and Creature definitions can be extracted by replacing "Race-DB" with "Race-DB-Creatures". Note: it is best to delete the extracted database character sheet after viewing/using, so that the system uses the much faster internal database version. After deleting or changing any character sheet database, always run the !magic --check-db or !attk --check-db command to re-index the databases.
' + +'Races: Race-DB-[added name]
Creatures: Race-DB-Creatures-[added name]
Those with version numbers of the form v#.# as part of the name will be ignored.
' + +'As previously stated, each database definition has 3 parts in the database (see Section 1), the same as for the Class Database explanation above. Note: The DM creating new classes does not need to worry about anything other than the Ability Macro in the database, as running the command --check-db will update all other aspects of the database appropriately.
' + +'Ability macros can be added to a database just by using the [+Add] button at the top of the Abilities column in the Attributes and Abilities tab of the Database Character Sheet, and then using the edit "pencil" icon on the new entry to open it for editing. Ability macros are standard Roll20 functionality and not dependent on the API. Refer to the Roll20 Help Centre for more information.
' + +'The database definition for a simple, standard Race might look something like this:
' + +'&{template:RPGMdefault}{{name=Human}}{{subtitle=Race}}Specs=[Human,HumanoidRace,0H,Humanoid]{{Alignment=Any}}{{Languages=Often *common*}}{{Height=Males [60+2d10](!
/r 60+2d10 ins height)ins, Females [58+2d10](!
/r 58+2d10 ins height)ins}}{{Weight=Males [140+6d10](!
/r 140+6d10 lbs weight)lbs, Females [100+6d10](!
/r 100+6d10 lbs weight)lbs}}{{Life Expectancy=95 years}}{{Section=**Attributes**}}{{Minimum=None}}{{Maximum=None}}{{Adjustment=None}}{{Section1=**Powers**}}{{Section2=None}}{{Section3=**Special Advantages**}}{{Section4=None}}{{Section5=**Special Disadvantages**}}{{Section6=None}}RaceData=[w:Human, align:any, weaps:any, ac:any]{{desc=Although humans are treated as a single race in the AD&D game, they come in all the varieties we know on Earth. A human PC can have whatever racial characteristics the DM allows.
'
+ +'Humans have only one special ability: They can be of any character class and rise to any level in any class. Other PC races have limited choices in these areas.
'
+ +'Humans are also more social and tolerant than most other races, accepting the company of elves, dwarves, and the like with noticeably less complaint.
'
+ +'Because of these abilities and tendencies, humans have become significant powers within the world and often rule empires that other races (because of their racial tendencies) would find difficult to manage.}}
As with Class definitions, and any other RPGMaster database entry, a Roll Template is used for formatting and the text in the template can be whatever you desire to display. The important elements for the RPGMaster APIs are highlighted and, as elsewhere, they are between the elements of the Roll Template to hide them when displayed, case is ignored and spaces, hyphens and underscores can be used but are ignored. Each element is described below:
' + +'Specs=[Human,HumanoidRace,0H,Humanoid]' + +'
The Specs section for a race describes the Sub-Race and base Race for the entry, and that this is a race definition. The fields must be in this order, identical for all database items.
' + +'Sub-Race | the Sub-Race or Race name, often the same as the ability macro name. |
---|---|
Macro Type | the type of the data in this Ability Macro, one of HumanoidRace, HumanoidCreature, or CreatureRace. |
Handedness | #H, where # is the number of hands needed to be a character of this race (not currently used). |
Base Race | the base race that this sub-race belongs to, and can be any other race in the database. For PCs generally one of Dwarf, Elf, Half-Elf, Halfling, Half-Orc, Human, or Gnome. |
The Sub-Race will "inherit" any data definitions and powers defined for the Base Race unless they are specifically overwritten or excluded by the Sub-Race definition. This reduces inconsistencies and the work of entering multiple duplications.
' + +'RaceData=[w:Human, align:any, weaps:any, ac:any]' + +'
All the same data fields (that are relevant) are available as for a Class definition. Where both the Class and Race of a character have specified restrictions or allowances, both the Class and the Race must allow an item / spell to be used. Whereas two Classes combine permissions as Class A OR Class B, Class and Race are Class AND Race. So a Warrior class can use a Greatsword, but a Warrior Gnome may not be able to use it as it would hit the ground as they wielded it!
' + +'There are a number of additional data fields relevant to Races:
' + +'&{template:RPGMdefault}{{name=Halfling}}{{subtitle=Race}}{{Alignment=Any (Usually NG)}}Specs=[Halfling,HumanoidRace,0H,Humanoid]{{Languages=Often *common, halfling, dwarf, elf, gnome, goblin,* and *orc*}}{{Height=Males [32+2d8](!
/r 32+2d8 ins height)ins, Females [30+2d8](!
/r 30+2d8 ins height)ins}}{{Weight=Males [52+5d4](!
/r 52+5d4 lbs weight)lbs, Females [48+5d4](!
/r 48+5d4 lbs weight)lbs}}{{Life Expectancy=100 to 150 years}}{{Section=Attributes}}{{Minimum=Str:7, Con:7, Dex:10, Int:6}}{{Maximum=Wis:17}}{{Adjustment=Dex:+1, Str:-1}}{{Section1=Powers}}{{Expert Miners=Stouts can detect slopes, and approximate direction underground}}{{Section2=Special Advantages}}{{Infravision=Any halfling character has a 15% chance to have normal infravision (this means he is pure Stout), out to 60ft; failing that chance, there is a 25% chance that he has limited infravision (mixed Stout/Tallfellow or Stout/Hairfeets lineage), effective out to 30 feet.}}{{Magic Resistance=Magic-resistant, giving a bonus to saving throws against magical wands, staves, rods, and spells of +1 for every 3.5 points of Constitution score.}}{{Poison Resistance=Save vs. poison at +1 for every 3.5 points of Constitution score.}}{{Attack bonus=+1 To Hit with slings and thrown weapons}}{{Surprise=Enemies get a –4 penalty to surprise if the halfling is: 1) moving alone, 2) is 90 feet away from the rest of their party, or 3) is with other elves or halflings and all are in nonmetal armor. If the halfling must open a door or screen to get to the enemy, the penalty is reduced to –2.}}RaceData=[w:Halfling, align:any, weaps:any, ac:any, attr:str=7|con=7|dex=10|int=6|wis=1:17, thmod:throwing=1|dart=1|hand-axe=1|magical-stone=1|slings=1, svatt:con, svpoi:3.5 svrod:3.5, svsta:3.5, svwan:3.5, svspe:3.5, ns:2],[cl:PW,w:Detect Slope,lv:0,sp:0,pd:-1],[cl:PW,w:Determine Direction Underground,lv:0,sp:0,pd:-1]{{desc=Halflings are short, generally plump people, very much like small humans. Their faces are round and broad and often quite florid. Their hair is typically curly and the tops of their feet are covered with coarse hair. They prefer not to wear shoes whenever possible. Halflings see wealth only as a means of gaining creature comforts, which they love. Though they are not overly brave or ambitious, they are generally honest and hard working when there is need.
'
+ +'Elves generally like them in a patronizing sort of way. Dwarves cheerfully tolerate them, thinking halflings somewhat soft and harmless. Gnomes, although they drink more and eat less, like halflings best, feeling them kindred spirits. Because halflings are more open and outgoing than any of these other three, they get along with other races far better.
'
+ +'There are three types of halflings: Hairfeets, Tallfellows, and Stouts. Hairfeets are the most common type, but for player characters, any of the three is acceptable.}}
attr: | #[:#] | minimum and maximum starting attribute values (default is 3:18) |
---|---|---|
svXXX[+]: | [+ - =] # | saving throw modifiers |
thmod: | weapon=[+-]# | weapon=[+-]# | ... | a list of weapon type (or super-type) to-hit modifiers separated by vertical bars (\'|\') |
The saving throw modifiers are cumulative with those relevant to a Class and/or to worn/held magic items. The XXX can be one of att, par, poi, dea, rod, sta, wan, pet, pol, bre, spe or all, optionally followed by a "+" (see next paragraph), followed by a colon, then a plus (+), a minus (-), an equals (=), and a number, or just a number with nothing before it. Each of the three-letter qualifiers refers to the relevant save, except "all" which applies the modifier to all saves, and "att" which specifies an attribute by whose value any save may vary. Preceding the modifier amount by plus (+) or nothing improves the save by the modifier, preceding by minus (-) worsens the save by the modifier, and by equals (=) overrides any other modifier being applied and applies only the modifier preceded by the equals.
' + +'If the XXX is followed by a "+" (e.g. like svpoi+:3), the save modifier will be a straight addition to (or subtraction from) the saving throw. Otherwise, the value of the attribute defined by the "svatt:" (which can be str, con, dex, int, wis, or chr, defaulting to con) is divided by the modifier provided and rounded down: e.g. if svatt:con and svpoi:3.5, a Constitution of 12 will result in a poison save bonus of 3.
' + +'To-Hit modifiers are supplied as a list separated by vertical bars. Each entry is a weapon type or weapon super-type, followed by "=", followed by a number preceded by an optional "+" or "-". A plus (+) improves the chance of hitting, and a minus (-) is a penalty. Any weapon of the specified type or super-type will get the modifier.
' + + +'Each race might have different modifiers to the success percentage for rogue skills. Such differences can be specified in the Race data.
' + +'&{template:RPGMdefault}{{title=Gnome}}{{subtitle=Race}}Specs=[Gnome,HumanoidRace,0H,Humanoid]{{Alignment=Any (Usually NG)}}{{Languages=Often *common, dwarf, gnome, halfling, goblin, kobold,* and the simple common speech of burrowing mammals (*moles, badgers, weasels, shrews, ground squirrels,* etc.)}}{{Height=Males [38+d6](!
/r 38+1d6 ins height)ins, Females [36+d6](!
/r 36+1d6 ins height)ins}}{{Weight=Males [72+5d4](!
/r 72+5d4 lbs weight)lbs, Females [68+5d4](!
/r 68+5d4 lbs weight)lbs}}{{Life Expectancy=350 years}}{{Section=**Attributes**}}{{Min Attributes=Str:6, Con:8, Int:6}}{{Attribute Adj.=Int:+1, Wis:-1}}{{Section1=**Powers**}}{{Expert Miners=Detect slopes, unsafe walls, cielings & floors, determine approximate depth and direction underground}}{{Section3=**Special Advantages**}}{{Infravision=*Infravision* to 60ft.}}{{Magic Resistance=Gnomes are magic-resistant, giving a bonus to saving throws against magical wands, staves, rods, and spells of +1 for every 3.5 points of Constitution score.}}{{Attack bonus=+1 To Hit kobolds and goblins}}{{Small size=Gnolls, bugbears, ogres, trolls, ogre magi, giants, and titans suffer a -4 penalty to attack}}{{Sense Curses=Can sense a cursed item, but only if the device fails to function}}{{Section5=**Special Disadvantages**}}{{Item failure=20% chance for failure of any magical item except weapons, armor, shields, illusionist items, and (if the character is a thief) items that duplicate thieving abilities.}}RaceData=[w:Gnome, align:any, weaps:any, ac:any, attr:str=6|con=8|int=6, +:1|kobold|goblin, -:4|gnoll|bugbear|ogre|troll|ogre-magi|oni|giant|titan, svatt:con,svrod:3.5,svsta:3.5,svwan:3.5,svspe:3.5, ola:+5,rta:+10,msa:+5,hsa:+5,dna:+10,cwa:-15, ns:4],[cl:PW,w:Detect Slope,lv:0,sp:0,pd:-1],[cl:PW,w:Detect Flawed Stonework,lv:0,sp:0,pd:-1],[cl:PW,w:Determine-Depth-Underground,lv:0,sp:0,pd:-1],[cl:PW,w:Determine Direction Underground,lv:0,sp:0,pd:-1]{{desc=Kin to dwarves, gnomes are noticeably smaller than their distant cousins. Gnomes, as they proudly maintain, are also less rotund than dwarves. Their noses, however, are significantly larger. Most gnomes have dark tan or brown skin and white hair.
Gnomes have lively and sly senses of humor, especially for practical jokes. They have a great love of living things and finely wrought items, particularly gems and jewelry. Gnomes love all sorts of precious stones and are masters of gem polishing and cutting.
Their diminutive stature has made them suspicious of the larger races - humans and elves - although they are not hostile. They are sly and furtive with those they do not know or trust, and somewhat reserved even under the best of circumstances. Dwelling in mines and burrows, they are sympathetic to dwarves, but find their cousins\' aversion to surface dwellers foolish.}}
Race modifiers to the rogue skills are specified using the same tags as used for Class definitions (see previous section). Modifiers can improve (+) or penalise (-) the skill percentage score. These modifiers will apply for any class, though of course other classes do not get class or level modifiers. GMs can allow non-rogue classes to have a chance (however minimal) to succeed at rogue skills, or not, through configuration options using the !magic --config or !attk --config commands. Creatures and NPCs can also have race modifiers to rogue skills using the same data tags.
' + + +'
As with classes, races can have specific powers. If a Sub-Race has a Base Race defined, the powers for both will be used - if you don\'t want any powers from a Base Race, do not specify one.
' + +'&{template:RPGMdefault}{{name=Dwarf}}{{subtitle=Race}}Specs=[Dwarf,HumanoidRace,0H,Humanoid]{{Alignment=Any (Usually LG)}}{{Height=4 to 4.5 ft}}{{Weight=150lbs}}{{Life Expectancy=350 years}}{{Languages=Often *Dwarf, Common, Orc, Kobold, Goblin, Gnome*}}{{Section=**Attributes**}}{{Min Attributes=Str:8, Con:11}}{{Max Attributes=Dex:17, Chr:17}}{{Attribute Adj.=Con:+1, Chr:-1}}{{Section1=**Powers**}}{{Infravision=*Infravision* to 60ft}}{{Expert Miners=Detect slopes, new tunnel construction, shifting walls, and stonework traps, and determine approximate depth underground}}{{Section2=**Special Advantages**}}{{Small size=Ogres, trolls, ogre magi, giants, and titans suffer a -4 penalty to attack Dwarves.}}{{Magic Resistance=Dwarves are nonmagical, which gives a bonus to dwarves\' saving throws against magical wands, staves, rods, and spells, of +1 for every 3.5 points of Constitution score.}}{{Sense Curses=Can sense a cursed item, but only if the device fails to function}}{{Section3=**Special Disadvantages**}}{{Item Failure=Magical items not specifically suited to the character\'s class have a 20% chance to malfunction when used.}}RaceData=[w:Dwarf, align:any, weaps:any, ac:any, attr:str=8|con=11|dex=1:17|chr=1:17, +:1|orc|half-orc|goblin|hobgoblin, -:4|ogre|troll|ogre-magi|oni|giant|titan, svatt:con,svpoi:3.5,svrod:3.5,svsta:3.5,svwan:3.5,svspe:3.5, ns:5],[cl:PW,w:Detect Slope,lv:0,sp:0,pd:-1],[cl:PW,w:Detect New Construction,lv:0,sp:0,pd:-1],[cl:PW,w:Detect Shifting Walls,lv:0,sp:0,pd:-1],[cl:PW,w:Detect Stonework Traps,lv:0,sp:0,pd:-1],[cl:PW,w:Determine Depth Underground,lv:0,sp:0,pd:-1]{{desc=Dwarves are short, stocky fellows, easily identified by their size and shape. They have ruddy cheeks, dark eyes, and dark hair. Dwarves tend to be dour and taciturn. They are given to hard work and care little for most humor. They are strong and brave. They enjoy beer, ale, mead, and even stronger drink. Their chief love, however, is precious metal, particularly gold. They prize gems, of course, especially diamonds and opaque gems (except pearls, which they do not like). Dwarves like the earth and dislike the sea. Not overly fond of elves, they have a fierce hatred of orcs and goblins. Their short, stocky builds make them ill-suited for riding horses or other large mounts (although ponies present no difficulty), so they tend to be a trifle dubious and wary of these creatures. They are ill-disposed toward magic and have little talent for it, but revel in fighting, warcraft, and scientific arts such as engineering.
'
+ +'Though dwarves are suspicious and avaricious, their courage and tenacity more than compensate for these shortcomings.
'
+ +'Dwarves typically dwell in hilly or mountainous regions. They prefer life in the comforting gloom and solidness that is found underground.}}
As master miners, Dwarves have a number of powers that they can call on when underground, to assess their situation. These use the same syntax for specification as for Classes with powers.
' + +'Dwarves also have to-hit and AC bonuses when facing various types of creature. These are specified in the race data as follows:
' + +'+: | [+-] # | race/creature | race/creature | ... | a modifier to hit of # when attacking any listed creature (not currently implemented) |
---|---|---|
-: | [+-] # | race/creature | race/creature | ... | a modifier to AC of # when being attacked by any listed creature (not currently implemented) |
A positive modifier is always a benefit, and a negative one is a penalty. These are not currently implemented as the APIs cannot know what race the opponent is, unless a targeted attack is used (see attackMaster API help handout for information on attacks) and targeted attacks are a DM and Player option which is not mandatory.
' + +'The same specification approach can be used to define creatures other than humanoids, and humanoids other than PCs and NPCs. A number of such definitions are provided in the Race-DB-Creatures database, which can be extracted to a Character Sheet DB using the !magic --extract-db Race-DB-Creatures command. All the same data fields are available as for Race definitions, with some extras.
' + +'&{template:RPGMdefault}{{name=Vampire}}{{subtitle=Creature}}{{Alignment=Usually Chaotic Evil}}Specs=[Vampire,HumanoidCreature,0H,Creature]{{Languages=Whatever they knew before they were a vampire, or what their vampire parents taught them.}}{{Height=As per pre-vampire race (usually [65+2d6](!
/r 65+2d6 ins height)ins,}}{{Weight=Their weight before becoming a vampire or [140+6d10](!
/r 140+6d10 lbs weight)lbs}}{{Life(?) Expectancy=Immortal}}{{Section=**Attributes**}}{{Minimum=Str:18(76), Int:15}}{{Maximum=Int:16}}{{Adjustment=None}}{{Section1=**Powers**}}{{Energy Drain=Drains 2 levels from anyone they successfully touch}}{{Charm=Any person who allows the vampire to look into their eyes will be affected as if by a *charm person* spell. Due to the power of this enchantment, a -2 is applied to the victim\'s saving throw vs. spell}}{{Summon Creatures=Can summon swarms of creatures to their aid}}{{Spell-like powers=*Gaseous Form* and *Spider Climb* at will}}{{Section2=**Special Advantages**}}{{Infravision=60 feet}}{{Shape Change=Can *Shape Change* into a large bat at will}}{{Plus Weapons To Hit=Attackers must use weapons of at least +1 to be able to hit a vampire}}{{Immunities=Immune to *Sleep, Charm,* and *Hold* spells, Paralysis and Poison. Spells based on cold or electricity cause only half damage}}{{Section3=**Special Disadvantages**}}{{Repellants=Odor of Strong Garlic; Mirror or Holy Symbol presented with conviction}}{{Holy Water or Symbol=Burns a vampire for 2-7 (1d6+1) damage with a successful hit}}{{Others=See Monsterous Compendium for other disadvantages}}RaceData=[w:Vampire, cattr:int=15:16|ac=1|mov=12|fly=18C|hd=8d8+3|thac0=11|attk1=hand:4+1d6:0:B,spattk:Energy drain,spdef:+1 weapon to hit; immune to *sleep, charm & hold*, ns:5],[cl:PW,w:Charm Person,sp:1,lv:0,pd:-1],[cl:PW,w:Summon Swarm,sp:2,lv:0,pd:-1],[cl:PW,w:Gaseous Form,sp:0,lv:0,pd:-1],[cl:PW,w:MU-Shape-Change,sp:9,lv:0,pd:-1],[cl:PW,w:Spider Climb,sp:1,lv:0,pd:-1]{{desc=Of all the chaotic evil undead creatures that stalk the world, none is more dreadful than the vampire. Moving silently through the night, vampires prey upon the living without mercy or compassion. Unless deep underground, they must return to the coffins in which they pass the daylight hours, and even in the former case they must occasionally return to such to rest, for their power is renewed by contact with soil from their graves.
'
+ +'One aspect that makes the vampire far more fearful than many of its undead kindred is its appearance. Unlike other undead creatures, the vampire can easily pass among normal men without drawing attention to itself for, although its facial features are sharp and feral, they do not seem inhuman. In many cases, a vampire\'s true nature is revealed only when it attacks. There are ways in which a vampire may be detected by the careful observer, however. Vampires cast no reflection in a glass, cast no shadows, and move in complete silence.}}
Creatures have additional data fields that set up all the important fields for the APIs on the AD&D2e Character Sheet (only when the creature is specified using the Class/Race menu on the "token-setup" menu - just typing the creature type into the Race field on the Character Sheet will not do this).
' + +'spattk: | text | special attacks text to be displayed when the Specials Action Button is used |
---|---|---|
spdef: | text | special defenses text to be displayed when the Specials Action Button is used |
cattr: | attr=value | attr=value | ... | a list of attribute/value pairs, where attr is one of: |
ac | #[:#] | creature armour class |
mov | # | creature movement on the ground |
fly | text | creature movement in the air |
sw | text | creature movement when swimming |
hd | #[d#][+/-#][r#] | creature hit dice roll spec |
hp | #[:#] | creature hit points |
regen | # | creature regeneration HP/round |
thac0 | # | creature Thac0 |
tohit | [+/-]# | creature to-hit modifier |
dmg | [+/-]# | creature damage modifier |
crith | # | creature critical hit roll |
critm | # | creature critical miss roll |
attk1 | roll,text,[speed],[type],[+tohit] | specification for creature innate attack 1 |
attk2 | roll,text,[speed],[type],[+tohit] | specification for creature innate attack 2 |
attk3 | roll,text,[speed],[type],[+tohit] | specification for creature innate attack 3 |
attkmsg | text[$$text][$$text] | message(s) for attacks |
speed | # | creature overall attack speed |
When a creature (or race) with these data fields is selected in the Race/Class menu, the CommandMaster API automatically sets all of the respective Character Sheet attributes to the specified values. In fact, all of these data fields can be used with a standard race, but are less useful. Note: values in square brackets are optional and the brackets should not be included if used.
' + + +'NPCs are just creatures with more detail on the "non-monster" part of the character sheet, and thus can use many of the same data tags in their definitions as stated above. Equally, Creatures can have many NPC characteristics, as exemplified by the Vampire definition example given above. However, in order for NPCs to be listed as NPCs in the Drag & Drop lists, the database record Class (the second attribute of the Specs=[...] section of the definition) must be NPCCreature, and Creatures must have CreatureRace.
' + +'&{template:RPGMdefault}{{}}Specs=[Human-Abjurer,NPCCreature,2H,Human-Wizard]{{}}RaceData=[w:Human-Abjurer, query:NPClevel, cattr:cl=MU:Abjurer| lv=??0| hp=??0d4r2| wis=15:18]{{}}%{Race-DB|Human-Wizard}{{name=Abjurer}}
' + +'The more NPC-related data tags, which Creatures can also use, are:
' + +'cattr: | attr=value | attr=value | ... | a list of attribute/value pairs, where attr is one of: |
---|---|---|
cl | [F/MU/PR/RO/PS]:Class name | Give creature a NPC class |
lv | # | Give creature a NPC level |
New | The following tags are new in this version | |
str | # | Give NPC strength (# can be a calculation) |
exstr | # | Give NPC extra strength if str evaluates to 18 (# can be a calculation & defaults to 1d100). Does not have to be a Warrior class. |
con | # | Give NPC constitution (# can be a calculation) |
dex | # | Give NPC dexterity (# can be a calculation) |
int | # | Give NPC intelligence (# can be a calculation) |
wis | # | Give NPC wisdom (# can be a calculation) |
chr | # | Give NPC charisma (# can be a calculation) |
npcpp | # | Set relative ratio for pick pokets level mod(# can be a calculation) |
npcol | # | Set relative ratio for open locks level mod(# can be a calculation) |
npcrt | # | Set relative ratio for find/remove traps level mod(# can be a calculation) |
npcms | # | Set relative ratio for move silently level mod(# can be a calculation) |
npchs | # | Set relative ratio for hide in shadows level mod(# can be a calculation) |
npcdn | # | Set relative ratio for detect noise level mod(# can be a calculation) |
npccw | # | Set relative ratio for climb walls level mod(# can be a calculation) |
npcrl | # | Set relative ratio for read languages level mod(# can be a calculation) |
npcll | # | Set relative ratio for legend lore level mod(# can be a calculation) |
For the attribute values (str, con, dex, int, wis, chr) the value is often a range speification of 3:18 which will evaluate to rolling 3d6. However, the lower value of the range may be modified to match the minimum scores for the class and race of the NPC.
' + +'For the rogue skill values (npcpp, npcol, npcrt, npcms, npchs, npcdn, npccw, npcrl, npcll) the value given to each (which can be a calulation, dice roll or range) represents the relative ratio vs. the other specified rogue skill values. The total allowable level points for the NPC is calculated by the APIs, and then allocated across the rogue skills using the specified ratios, so that the total points are fully allocated. Making the ratio values dice roll or range specifications allows for a degree of randomness in the outcome, so no two NPCs using the same definition are the same in rogue skill values.
' + + +'Many creatures have attacks using their claws, bites and other "innate" attacks, and have tough natural armoured skin. However, many humanoid creatures can use normal weapons & armour to attack adventurers and defiend themselves. It is possible to specify the possible weapon combinations and available armour for each creature type, and add a randomness to the selection criteria, as this example shows:
' + +'&{template:RPGMdefault}{{}}Specs=[Drow-Fighter,NPCCreature,0H,Drow]{{}}RaceData=[w:Drow-Fighter, query:NPClevel, align:CE, cattr:cl=F| lv=??0| hp=??0d8r4| str=8:18| con=7:14| dex=8:20| int=9:19| wis=3:18| chr=6:16| mov=12, ns:1],[cl:MI,items:random:1d??0],[cl:AC,items:chain-mail+??1|buckler+??2], [cl:WP,%:50 ,prime:shortsword+??2,offhand:dagger+??2:3], [cl:WP,%:10 ,prime:shortsword+1 < Mastery,offhand:dagger+1], [cl:WP,%:7 ,prime:shortsword+2,offhand:dagger+1:3], [cl:WP,%:5 ,prime:shortsword+1,offhand:dagger+2] , [cl:WP,%:5 ,prime:shortsword+2,offhand:dagger+2], [cl:WP,%:3 ,prime:shortsword+3,offhand:dagger+1:5], [cl:WP,%:1 ,prime:shortsword+3,offhand:dagger+2:3], [cl:WP,%:50 ,prime:shortsword+??2,offhand:dagger+??2,items:hand-crossbow|hand-quarrel+poison:10], [cl:WP,%:10 ,prime:hand-crossbow=%%3,offhand:shortsword+1,items:dagger+1|hand-quarrel+poison:10], [cl:WP,%:4 ,prime:shortsword+2,offhand:dagger+1,items:hand-crossbow|hand-quarrel+poison:10], [cl:WP,%:4 ,prime:hand-crossbow,offhand:shortsword+1,items:dagger+2|hand-quarrel+poison:10],[cl:WP,%:20,prime:Javelin+poison:3 < Specialist,items:dagger+??1:5]{{}}%{Race-DB|Drow}{{name= Fighter}}{{subtitle=Drow}}{{Alignment=Chaotic Evil}}
' + +'The additional RaceData datasets highlighted have the following fields:
' + +'ns: | [ = / - ]1 | specifies a following number of sections. An \'=\' ignores all inherited sections. A \'-\' ignores Class definitios for weapons, powers and items |
---|---|---|
cl | WP / AC / MI | Type of data in the dataset. WP = weapon, AC = armour, MI = items |
% | # | Chance of dataset being used relative to others of same type (does not have to add up to 100) |
prime | weapon [: qty][ < prof] | Name, optional quantity, and optional proficiency of weapon to take in Primary hand. Only 1 is taken in-hand, rest of qty held in items |
offhand | weapon [: qty][ < prof] | Name, optional quantity, and optional proficiency of weapon to take in Off-hand hand. Only 1 is taken in-hand, rest of qty held in items |
both | weapon [: qty][ < prof] | Name, optional quantity, and optional proficiency of two-handed weapon to take in both hands. Only 1 is taken in-hand, rest of qty held in items |
items | weap/armour/item[:qty][ < prof] | weap/armour/item[:qty][ < prof] | ... | Name, quantity & proficiency of weapons, armours, or equipment/magic items to store as items of equipment. Armour will automatically contribute to creature armour class. |
items | random:qty | weap/armour/item:qty | ... | Add qty random items from the databases to the NPCs carried items, each of which will affect the NPC appropriately and can immediately be used. |
It is possible to give spell-casting creatures spells as powers - just specify the number per day as 1, and you can even use the MU-[Spell-Name] or PR-[spell-name] syntax as the power name to specify the spells to use (as shown in the Vampire above). However, for larger numbers of spells, and/or to grant random spells, an additional syntax is available:
' + +'&{template:RPGMdefault}{{}}RaceData=[w:Frost Giant Witch Doctor,sps:any,cattr:cl=pr:frost-giant-shaman/mu:frost-giant-witch-doctor|lv=7/3,ns:2],[cl:MU,lv:1,w:random|random|random|Detect-Magic],[cl:MU,lv:2,w:random|ESP|Mirror-Image|random|random]{{}}Specs=[Frost-Giant-Witch-Doctor,CreatureRace,2H,Frost-Giant-AC0]{{}}%{Race-DB-Creatures|Frost-Giant-AC0}{{name= Witch Doctor}}{{Section3=**Witch Doctor:** This Frost Giant is a Witch Doctor that can cast spells of a number of wizard spells, and priest spheres of magic:*healing, charm, protection, divination*, or *weather*}}{{desc6=**Frost Giant Witch Doctor:** There is a 20% chance that any band of frost giants will have a shaman (80%) or witch doctor (20%). If the group is led by a jarl, there is an 80% chance for a spell caster. Frost giant shamans are priests of up to 7th level. A shaman can cast normal or reversed spells from the *healing, charm, protection, divination*, or *weather* spheres. Frost giant witch doctors are priest/wizards of up to 7th/3rd level; they prefer spells that can bewilder and confound other giants. Favorite spells include: *unseen servant, shocking grasp, detect magic, ventriloquism, deeppockets, ESP, mirror image,* and *invisibility*.}}
' + +'This Witch Doctor has a number of spells specified to be in their spellbook by using the RaceData extension data sets:
' + +'[cl:MU,lv:1,w:random|random|random|Detect-Magic],[cl:MU,lv:2,w:random|ESP|Mirror-Image|random|random]' + +'
The cl:MU
specifies that this data set specifies spells for the spellbook, and the lv:#
specifies the level of spells. The w: string then defines the spell names, separated by pipes \'|\'. These names must be the same as those given in the spells databases (which can be listed using the GMs [Token Setup] / [Add Spells & Powers] dialog), or can be \'random\'. Specifying \'random\' will do what it says on the tin - a random spell will be chosen from all available spells at that level. If more than one spell is stated as \'random\' there is a chance that the same one will come up twice - the APIs will allow this.
If a spellbook is specified for a creature in this way, it must be of a class that can cast spells or it will not be able to use them. The creature\'s level as a spellcaster will determine how many spells can be memorised at each level, regardless of how many are given in the spellbooks.
' + +'Also, if spellbooks are specified like this the API will automatically memorise spells, selecting spells from the list at random up to the correct number for the creature\'s level. If a creature can cast Wizard spells and no spellbooks are specified, random spells will be memorised from all possible wizard spells that are valid for the schools that caster can cast. The GM (or other controlling player) can always use the Spells Menu to memorise different spells, and the GM can use the [Token Setup] / [Add Spells & Powers] dialog to change the spellbooks.
' + +'If the creature can cast Priest spells, the creature will be granted a spellbook of all priest spells that are valid for the spheres it can cast, and random spells from this list automatically memorised from this list. The specification can override this behaviour by specifying particuler priest spells to have in the spellbook at a specific level using [cl:PR,lv:#,w:....]
. Alternatiely, if the w: is followed by an empty string e.g. [cl:PR,lv:#,w:]
, then the correct number of random priest spells will be memorised at each level, but only those memorised spells will be written to the spellbook. An example of this is for dragons, as according to the Monsterous Manual they only know an exclectic mix of spells they have picked up as they age and are not granted spells by a god.
Some creatures, such as Dragons, have multiple forms and also vary within a single form. For example, a Dragon can be Red, Blue, White, Gold, Silver, Crystal, ... etc. Not only that, but each of these dragons\' powers grow with age from Hatchling through Juvenile, Adulthood, to Venerable, Wyrm & Great Wyrm. Rather than create a definition for each age and colour combination (which would result in 180 definitions just for the standard dragons!) it is possible to add a query presented to the GM when selecting the type of creature to drag & drop. This query can specify data to be factored into calculations in the RaceData specification dependent on the selection made.
' + +'&{template:RPGMdefault}{{title=Red}}{{name=Dragon}}Specs=[Dragon,DragonRace,2H,Creature]{{subtitle=Dragon}}RaceData=[w:Red Dragon, query:What Age?|Hatchling%%1%%-6|Very Young%%2%%-4|Young%%3%%-2|Juvenile%%4%%0|Young Adult%%5%%1|Adult%%6%%2|Mature Adult%%7%%3|Old%%8%%4|Very Old%%9%%5|Venerable%%10%%6|Wyrm%%11%%7|Great Wyrm%%12%%8, align:CE, ac:none, cattr:int=15:16|mov=9|fly=30C|jump=3|ac=1-??1|age=??0:??1|hd=(11+??1)d8r1|mr=(v(^((??1-4);0);1)*??1*5)|cl=mu:red-dragon/pr:red-dragon|lv=8+??1/8+??1|spellsp=1|thac0=11-??1|tohit=??2|dmg=??1|size=G|attk1=1d10:Claw x 2 or Claw+Kick:0:S|attk2=3d10:Bite:0:P|attk3=2d10:Tail Swipe:0:B|attkmsg=Remember powers such as *Dragon Fear; Wing Buffet; Snatch; Plummet;* and *Spell Casting*$$Remember powers such as *Dragon Fear; Wing Buffet; Snatch; Plummet;* and *Spell Casting*$$\lbrak;Show the radius\rbrak;\lpar;!rounds ~~aoe `{selected¦token_id}¦arc180¦0¦80¦160¦red\rpar; then up to \lbrak;\lbrak;`{selected¦age|max}\rbrak;\rbrak; opponents in the area take damage and Save vs. Petrification with the penalty shown below or be \lbrak;Stunned\rbrak;\lpar;!rounds ~~target area¦`{selected¦token_id}¦`{target¦Select the stunned creature¦token_id}¦Stunned¦\lbrak;[1+1d4]\rbrak;¦-1¦Stunned by a dragon tail slap¦back-pain\rpar; for 1d4+1 rounds., spattk:*Dragon Fear; Wing Buffet; Snatch; Plummet;* and *Spell Casting*, spdef:Magic resistance \lbrak;\lbrak;({ { { {(@{selected|age|max}-4)},{0} }kh1}, {1} }kl1) * (@{selected|age|max}+1) * 5\rbrak;\rbrak;%, ns:1],[cl:PW,w:MU-Affect-Normal-Fires,pd:3,sp:1],[cl:PW,w:MU-Pyrotechnics,pd:3,sp:1],[cl:PW,w:PR-Heat-Metal,pd:1,sp:1],[cl:PW,w:MU-Suggestion,pd:1,sp:1],[cl:PW,w:MU-Hypnotism,pd:1,sp:1],[cl:PW,w:Detect-Gems-Kind+Number,pd:3,sp:1],[cl:PW,w:Red-Dragon-Breath,pd:-1,sp:1],[cl:PW,w:PW-Snatch,lv:14,pd:-1,sp:0],[cl:PW,w:PW-Plummet,pd:-1,sp:0],[cl:PW,w:PW-Wing-Buffet,lv:14,pd:-1,sp:0],[cl:PW,w:PW-Stall,pd:-1,sp:0],[cl:PW,w:PW-Dragon-Fear,lv:14,pd:-1,sp:0],[cl:PR,lv:1,w:],[cl:PR,lv:2,w:]{{Section=**Attributes**}}{{Intelligence=Exceptional (15-16)}}{{AC=Varies with age, adult red dragon is AC -5}}{{Alignment=Chaotic Evil}}{{Move=9, FL 30(C), Jump 3}}{{Hit Dice=Varies with age, adult red dragon is 15 HD}}{{THAC0=Varies with age, adult red dragon is 5}}{{Section1=**Attacks:** ToHit and damage bonus varies with age, adult red dragon is +2 and +6. 2 x Claws for 1d10 HP each, possibly with 1 or 2 kicks for 1d10 each, bite for 3d10, and tail slap for 2d10 and possible *stun* within an area varying with age. Several other attacks possible - see *Powers*}}{{Languages=*Red Dragon* and *Dragon Common*, and 16% if hatchlings (+5% per age level) can perform universal communication with any intelligent creature}}{{Size=G, varies with age}}{{Life Expectancy=Possibly in excess of 1,000 years. Adult dragons are considered between 100 and 200 years old}}{{Section2=**Powers**}}{{Breath Weapon=A cone of flame, 90ft long, 5ft wide at dragon and spreading to 30ft wide. Damage varies by age from 2d10+1 to 24d10+12. Save vs. Breath Weapon to take half damage}}{{Fear=Can inspire fear in creatures that see the dragon: affect varies with the level / HD of the viewing creature.}}{{Spell Casting=Knows a number of random wizard and priest spells cast at a level from 10 to 21 varying with age. All spells are cast at a speed of 1 segment regardless of the spell}}{{Spell-like Powers=*Young* dragons can *Affect Normal Fires* x 3 per day, *Juveniles* gain *Pyrotechnics* x3 per day, *Adult* gains *Heat Metal* x 1 per day, *Old* gain *Suggestion* x 1 per day, *Very Old* gain *Hypnotism* x 1 per day, and *Venerable* gain *Detect Gems, Kind & Number* x 3 per day}}{{Special Attacks=*Snatch, Plummet, Stall*, and *Wing Buffet*}}{{Section4=**Special Advantages**}}{{Section5=Its a Dragon!}}{{Section6=**Special Disadvantages**}}{{Section7=None}}{{Section9=**Description**}}{{desc7=Dragons are an ancient, winged reptilian race. They are known and feared for their size, physical prowess, and magical abilities. The oldest dragons are among the most powerful creatures in the world.
Most dragons are identified by the color of their scales. All subspecies of dragons have 12 age categories, and gain more abilities and greater power as they age. Dragons range in size from several feet upon hatching to more than 100 feet, after they have attained the status of great wyrm. The exact size varies according to age and subspecies... }}
In the case of dragons, this query asks the GM for the age to make the dragon, and sets two values based on the age to use in calculations (seen above in red). The query will be applied to all creatures of the Creature Database-Class (Specs definition field 2), in this case any DragonRace creature, even if the roll query is specified in the definition of only 1 such creature. Different roll queries can be specified for different Creature Database-Classes: you can make up your own db-classes to differentiate them.
' + +'The query specification in the Data section has the following syntax:
' + +'query:What Age?|Hatchling%%1%%-6|Very Young%%2%%-4|Young%%3%%-2|Juvenile%%4%%0|Young Adult%%5%%1|Adult%%6%%2|Mature Adult%%7%%3|Old%%8%%4|Very Old%%9%%5|Venerable%%10%%6|Wyrm%%11%%7|Great Wyrm%%12%%8' + +'
If you are familiar with Roll20 Roll Queries, you will note that the ?{...} has been left out: only the text between the braces is defined. This text starts with the query text to be presented to the GM - "What Age?". Possible answers are then specified, separated by pipes \'|\'. Each answer has text followed by values separated by double-percents \'%%\'. The text will be displayed in the roll query to the GM to select the option. The percent-separated values will be returned by the query, including the text as the first value.
' + +'It is then possible to refer to the returned values in the RaceData specification using the syntax ??#
, where # is the zero-based number of the value - ??0 is the text of the option selected, ??1 the next value, ??2 the value after that, etc. An example of this is given in the new age=??0:??1
data specification in red above, which will set the character sheet "age" field to the text of the dragon\'s age selected by the GM, and the sheet "age|max" field to the ??1 parameter, which is the "age value" of that age of dragon.
It is possible to do simple maths using these values and others in the data specification. The API supports plus (+), minus (-), times (*), divide (/), left & right parenthesis (()), max (^(...;...)), min (v(...;...)), dice rolls (#d#+#r#), and ranges (#:#). These operators can be combined in simple ways - to be honest, I\'ve not tested all possible combinations - keep it simple and it should work! An example is the calculation for magic resistance:
' + +'mr=(v(^((??1-4);0);1)*??1*5)' + +'
The order of calculation is:
' + +'Ranges: These are specified with two numbers separated by a colon, thus ##:##
. E.g. 3:18 - The API will attempt to roll any range by using the Roll20 dice roller, with 3 dice if possible (so that standard attribute rolls of NPCs and creatures are as close to real as possible). Thus, 3:18 is rolled using 3d6, 6:15 will be 3d4+3, 5:17 is 3d5+2. If a range is too small or cannot be done with 3 dice, 2 or 1 will be used instead, so 2:4 will be 1d3+1, 15:16 will be 1d2+14 etc. If you want more control, specify a particular dice roll.
Dice Rolls: can be specified using the syntax #d#+#r#
or #d#-#r#
: all parts except the #d# are optional. The r# specifies a re-roll number - if any dice roll is equal to or less than the r# number, that dice alone will be rerolled until the value is higher. Note: The Hit Dice specification (hd=) can use a format like #d#
or #d#+#
or #d#+#r#
(the plus can be a minus), or can be extended to #d#+#d#r#
(i.e. the roll modifier can be a dice roll itself) - but the reroll value always applies to the main dice roll. Avoid other maths for the Hit Dice value, but ??# substitutions and maths on the number of dice are possible e.g. (??1+9)d8+??2r2
is valid.
Min & Max: The operators ^ (caret - max) and v (lower-case V - min) can be used with the syntax ^(#;#;#)
or v(#;#;#;#)
(any number of values is valid) and will evaluate to the maximum or minimum value respectively. Note that a semi-colon separator \';\' is used rather than a comma.
So now it can be seen that the Magic Resistance calculation above will resolve to 0 if the ??1 value is 4 or less, or (5 times ??1) if ??1 is 5 or more. The combination of the Roll Query, values set by the answer to it, and the simple maths engine can provide powerful results.
' + +'Note: semi-colons are used for min & max seperators, and square brackets avoided for calculations, so that Roll20 calculations are not inappropriately triggered when the Roll Templates are displayed and so that these calculations can be passed in API calls. Use these forms of maths in creature definition data specifications, and not Roll20 calculations, to avoid issues. Roll20 calculations can be used inside {{...}} parts of Roll Templates as these will never be encountered by the API management functions, including in API button calls.
' + +'Below are lists of the current possible values for the Class and Race database Ability macro sections.
' + +'Specs=[Class Type, Macro Type, Handedness, Class Group-Type]' + +'
Specs=[Sub-Race, Macro Type, Handedness, Base-Race]' + +'
There are no default settings for any of the Specs data fields. All must be explicitly specified.
' + +'There is an infinite list of class types: generally the type is the class name.
' + +'There is an infinite list of race and creature types: generally the type is the sub-race or creature name.
' + +'Classes: One of "WarriorClass", "WizardClass", "PriestClass", "RogueClass", "PsionClass", relating to the base class of the character. This field is used to add the Class name to the right base class list for selection by the Players.
' + +'Races: One of "HumanoidRace", "HumanoidCreature", "CreatureRace", relating to the base race of the character. This field is used to add the Race name to the right race or creature list for selection.
' + +'0H A Race, Creature or Class that can only be taken by characters and creatures that do not have hands (e.g. a fish-type creature)
'
+ +'1H A Race, Creature or Class that can only be taken by characters or creatures with only one hand (e.g. a snake NPC that can use its prehensile tail to hold weapons)
'
+ +'2H A Race, Creature or Class that has two hands - the normal for humanoid PCs and NPCs
'
+ +'3H A Race, Creature or Class that can only be taken by characters or creatures with three or more hands
'
+ +'4H Etc
'
+ +'... ...
(Handedness for Race and Classes are not currently restricted or used by the system. In future, the number of hands specified on the "Change Weapon" dialogue may be related to the Character Race & Class)
' + +'The Base Class can currently be one of "Warrior", "Wizard", "Priest", "Rogue" or "Psion". If a character class is allowed to be of more than one base class, separate each with a vertical bar character \'|\'. This determines the valid Character Sheet fields that this Class Type can appear in.
' + +'The Base Race or Creature Type can be for any other Race or Creature definition. Multiples are not allowed (no vertical bars \'|\'), and the Sub-Race / Creature will inherit the specifications and powers of the Base Race / Creature.
' + +'Below are the definitions for each of the possible ClassData and RaceData fields.
' + +'Note: Always refer to the database specification definitions in other sections above for detailed information on the use of these Field specifiers. Not all specifiers have an obvious use.
' + +'Field | ' + +'Format | ' + +'Default Value | ' + +'Description | ' + +'
---|---|---|---|
w: | < text > | \'Fighter\' | Name of the Class |
hd: | Dice Roll spec | 0 | Hit dice roll per level |
align: | [ lg / ng / cg / ln / nn / n / cn / le / ne / ce / any ] | any | Allowed alignments |
race: | < text | text | ... > or any | any | Allowed races |
weaps: | < text | text | ... > or any | any | Allowed weapons and weapon types |
npp: | [-]# | \'\' | The weapon non-proficiency penalty for the class |
twp: | [-]#.# | 2.4 | The two weapon penalty (primary.secondary) for the class |
ac: | < text | text | ... > or any | any | Allowed armour types |
attkl: | < 0 | # | # | ... > | \'\' | Class level progression for "attacks per round" modifiers |
attkm: | < # | # | # | ... > | \'\' | Melee weapon "attacks per round" modifiers by class level progression |
attkr: | < # | # | # | ... > | \'\' | Ranged weapon "attacks per round" modifiers by class level progression |
sps: | < text | text | ... > or any | any | Allowed spell schools or major spheres |
spm: | < text | text | ... > | \'\' | Allowed minor spheres |
spb: | < text | text | ... > | \'\' | Banned spell schools |
slv: | < # | # | # | <MU / PR> > | \'\' | Non-standard spellcaster level/type specification |
spl# | < # | # | # | ... > | \'\' | No. of spells of level spl# at each character level |
attr: | #[:#] | 3:18 | minimum and maximum starting attribute values |
svXXX[+]: | [+ - =] # | 0 | saving throw modifiers |
thmod: | weapon=[+-]# | weapon=[+-]# | ... | \'\' | a list of weapon type (or super-type) to-hit modifiers separated by vertical bars (\'|\') |
+: | [+-] # | race/creature | race/creature | ... | \'\' | a modifier to hit of # when attacking any listed creature (not currently implemented) |
-: | [+-] # | race/creature | race/creature | ... | \'\' | a modifier to AC of # when being attacked by any listed creature (not currently implemented) |
spattk: | text | \'\' | special attacks text to be displayed when the Specials Action Button is used |
spdef: | text | \'\' | special defenses text to be displayed when the Specials Action Button is used |
cattr: | attr=value | attr=value | ... | \'\' | a list of attribute/value pairs, where attr defines a field on the Character Sheet Monster tab |
ns: | # | 0 | Number of granted spells/powers defined for item |
cl: | < MU / PR / PW > | \'\' | Type of granted spell/power (always PW=Power) |
w: | < text > | \'-\' | Name of granted spell/power |
lv: | # | 1 | The character level at which the Power is granted |
pd: | [ -1 / # / #L# ] | 1 | No. of times per day power can be used. -1 is "at will", and #L# is first number per second number levels, per day |
The Character Sheet field mapping to the API script can be altered using the definition of the fields object, the definition for which can be found at the top of the relevant RPGMaster Library API. You can find the complete mapping for all APIs in the RPGMaster series, with an explanation of each, in a separate document - ask the API Author for a copy.
' + +'In order to understand the Attacks Database, it is first important to understand how attacks are executed by the AttackMaster API. Under some (if not all) versions of D&D, and especially AD&D 2nd Edition, attacks are quite complex involving many factors that can vary from moment to moment. Some say that this is why they prefer RPG systems that require less maths and are faster to execute, that the complexity of the AD&D2e combat system interrupts the flow of play. The AttackMaster API handles attacks in such a way as to hide as much of that complexity from the players as possible, and thus allow game-play to flow and players to concentrate on the unfolding story.
' + +'In order for the API to achieve this, it must evaluate many factors "on the fly" such as current magical effects in place (generally or on individuals), the current attributes of a character (which can vary as they are affected by game play), the type, range and properties of the weapon combinations used at that point in time for that particular attack, and the effects of the race, class, level and proficiency of the character, among several others. Given that these factors can vary even during a single round, each attack must be fully evaluated from scratch each time it is made.
' + +'Another issue is introduced by players feeling much more satisfied if they can see dice rolling for the attack, or they may want to use the Roll20 dice rolling mouse action, or even their own physical dice. Unfortunately for API authors, at the time of writing the API it is only possible to display rolling 3D dice from Chat Window dice rolls, either typed in the entry box by the player or run from Macros displayed in the Chat Window - 3D dice will not work when called by or included in API calls and commands.
' + +'So how does the AttackMaster API achieve 3D dice rolls and attack calculations that can accelerate game-play? The answer is that it uses Attack Template definitions which it parses and turns into Ability Macros on the Character Sheet of the character that selects to do an attack. The Melee Weapon templates are parsed and the attack Ability Macros for each Melee weapon in-hand created on the Character Sheet as (in fact just before) the Attack chat window menu is displayed, and Ranged Weapon templates are parsed and their attack Ability Macros are created after the type of Ammo has been selected and just before the relevant range buttons on the Attack menu are enabled for the Player to select. Monster attack templates are parsed just before the Monster Attack menu is shown, with a template "set" created for each defined Monster innate attack on the Monster character sheet tab. When the Player selects a Melee weapon to attack with, or the relevant range button for a Ranged weapon or Monster attack, the API is then not actually involved at all - the Roll20 Chat Window button just selected is just doing a standard macro call to the relevant attack Ability Macro just created on the Character Sheet. This also means the actual attacks happen at the fastest speed Roll20 can achieve as no API code is being run at that point.
' + +'The Attack Templates are stored in the internal API Attacks Database, which can be exposed in Character Sheet Database form as Attacks-DB using the AttackMaster !attk --extract-DB Attacks-DB command. Attack Templates can also exist in additional bespoke Attacks Databases the DM/Game Creator adds using the Character Sheet name Attacks-DB-[added name]. There are 12 basic Attack Templates:
' + +'MW-ToHit | Melee Weapon calculation to assess and display the Armour Class hit by an attack |
---|---|
MW-DmgSM | Melee Weapon calculation to assess the damage done to a Medium or smaller opponent if the hit was sucessful |
MW-DmgL | Melee Weapon calculation to assess the damage done to Large or larger opponents as a result of a successful hit |
MW-Targeted-Attk | Melee Weapon calculation for using a targeted attack which rolls all attack and damage dice at once, and then displays the AC hit, the damage vs. all types of opponents, and the current AC & HP of the targeted opponent |
RW-ToHit | Ranged Weapon calculation to assess and display the Armour Class hit by an attack |
RW-DmgSM | Ranged Weapon calculation to assess the damage done to a Medium or smaller opponent if the hit was sucessful |
RW-DmgL | Ranged Weapon calculation to assess the damage done to Large or larger opponents as a result of a successful hit |
RW-Targeted-Attk | Ranged Weapon calculation for using a targeted attack which rolls all attack and damage dice at once, and then displays the AC hit, the damage vs. all types of opponents, and the current AC & HP of the targeted opponent |
Mon-Attk | Monster/Creature attack calculation to assess and display the Armour Class hit by an attack |
Mon-DmgSM | Monster/Creature damage calculation to assess and display the damage done to Medium or smaller opponets by an attack |
Mon-DmgL | Monster/Creature damage calculation to assess and display the damage done to a Large or larger opponent by an attack |
Mon-Targeted-Attk | Monster/Creature for a targeted attack calculation to assess and display the Armour Class hit and damage done by an attack, along with the target\'s current AC and HP |
The Melee Weapon Attack Templates will be parsed for each Melee Weapon in-hand at the time of the attack, and the Ranged Weapon Attack Templates will be parsed for each possible range of the Ranged Weapon/Ammo combination selected for the attack. Two additional Melee Weapon Attack Templates are parsed if the character making the attack is a Rogue class:
' + +'MW-Backstab-DmgSM | Melee Weapon calculation to assess the damage done to a Medium or smaller opponent if the hit was a Rogue doing a backstab and the attack was successful |
---|---|
MW-Backstab-DmgL | Melee Weapon calculation to assess the damage done to a Large or larger opponent if the hit was a Rogue doing a backstab and the attack was successful |
All of the above templates are provided in the Attacks-DB database supplied with the game version-specific RPGMaster Library API. They are created to follow the rules of the game version supported by that specific library: DMs and Game Creators can create their own attack and damage calculations following whatever rules they want in their own bespoke Attacks Database, using the information provided in the next section.
' + +'It is possible to add additional Attack Templates that are specific to particular Races, Classes, or even individual weapons! Indeed, the database supplied includes an example of a bespoke Attack Template set for a thrown prepared Oil Flask. When an attack is the action selected by the Player, the API will search the Attacks-DB and bespoke user Attacks Databases for Melee and Ranged Attack Templates in the following name order (replace the ?W with either MW or RW as appropriate):
' + +'Attack Templates can take the form of any message or macro that can be held in a Roll20 Character Sheet Ability Macro and be displayed in the Chat Window when called. Typically, this will use a Roll Template (standard Roll20 functionality - see Roll20 Help for information), but it can be any format you desire as long as it results in the correct display of information to the Player.
' + +'The Attack Template has a large number of template fields that it can call upon to use in its calculations - these are pre-calculated values supplied by the API that the DM / Game Creator writing a new Attack Template can use. The standard Roll20 attribute value notation of @{selected|field-name} is not recommended for use in Attack Templates, as when the template is parsed, and then later the resulting Ability Macro run as part of the attack, there are circumstances where the token for the attacking Character may not be currently selected, resulting in the wrong value being used or, worse, an error occurring and the game halting. Instead, all the following template fields are available:
' + +'All Attack Templates | |
---|---|
^^toWho^^ | Resolves to a Roll20 whisper command to the Character making the attack |
^^toWhoPublic^^ | Resolves to a Roll20 chat command to the GM if a GM controlled creature is making the attack, otherwise a public message to all Players |
^^defaultTemplate^^ | Resolves to the name of the Default Roll Template name set in the AttackMaster API |
^^cname^^ | Resolves to the Character Name of the attacking character |
^^tname^^ | Resolves to the Token Name of the attacking character |
^^cid^^ | Resolves to the Roll20 Character ID of the attacking character |
^^tid^^ | Resolves to the Roll20 Token ID of the attacking character |
^^toHitRoll^^ | Depending on if the Player chose for Roll20 to roll the attack dice or to roll their own dice, resolves to one of (a) the attack dice specification provided in the Attack Template\'s Specs field, or (b) a Roll Query requesting the Player to enter a dice roll result |
^^thac0^^ | Resolves to the base thac0 (value "to hit armour class 0") of the attacking character without any adjustments |
^^ACfield^^ | Resolves to the Character Sheet field name that holds the target creatures current Armour Class (only used in targeted attacks) |
^^targetACfield^^ | Resolves to the targeted token value Armour Class macro call @{target|Select Target|^^ACfield^^} vs. a targeted opponent (only used in targeted attacks) |
^^HPfield^^ | Resolves to the Character Sheet field name that holds the target creatures current Hit Points (only used in targeted attacks) |
^^targetHPfield^^ | Resolves to the targeted token value Hit Points macro call @{target|Select Target|^^HPfield^^} vs. a targeted opponent (only used in targeted attacks) |
^^magicAttkAdj^^ | Resolves to any magical effect attack bonus or penalty resulting from magic currently in effect |
^^strAttkBonus^^ | Resolves to the strength to-hit bonus/penalty of the attacking character |
^^strDmgBonus^^ | Resolves to the strength damage bonus/penalty of the attacking character |
^^slashWeap^^ | Resolves to 1 if the damage type of the weapon includes Slashing (or S), otherwise 0 |
^^pierceWeap^^ | Resolves to 1 if the damage type of the weapon includes Piercing (or P), otherwise 0 |
^^bludgeonWeap^^ | Resolves to 1 if the damage type of the weapon includes Bludgeoning (or B), otherwise 0 |
^^weapType^^ | Resolves to the 3 letter damage type of the weapon (S, P, B or any combination) |
^^ACvsNoMods^^ | Resolves to the targeted standard Armour Class macro call @{target|Select Target|AC-field} vs. a targeted opponent (only used in targeted attacks) |
^^ACvsSlash^^ | If a slashing weapon, resolves to the targeted Slashing damage Armour Class macro call @{target|Select Target|SlashAC-field} vs. a targeted opponent, otherwise is blank (only used in targeted attacks) |
^^ACvsPierce^^ | If a piercing weapon, resolves to the targeted Piercing Armour Class macro call @{target|Select Target|PierceAC-field} vs. a targeted opponent, otherwise is blank (only used in targeted attacks) |
^^ACvsBludgeon^^ | If a bludgeoning weapon, resolves to the targeted Bludgeoning Armour Class macro call @{target|Select Target|BludgeonAC-field} vs. a targeted opponent, otherwise is blank (only used in targeted attacks) |
^^ACvsNoModsTxt^^ | Resolves to the text "No Mods" |
^^ACvsSlashTxt^^ | If this is a slashing weapon, resolves to the text "Slash", otherwise resolves to an empty string |
^^ACvsPierceTxt^^ | If this is a piercing weapon, resolves to the text "Pierce", otherwise resolves to an empty string |
^^ACvsBludgeonTxt^^ | If this is a bludgeoning weapon, resolves to the text "Bludgeon", otherwise resolves to an empty string |
^^ACvsSTxt^^ | If this is a slashing weapon, resolves to the text "S", otherwise resolves to an empty string |
^^ACvsPTxt^^ | If this is a piercing weapon, resolves to the text "P", otherwise resolves to an empty string |
^^ACvsBTxt^^ | If this is a slashing weapon, resolves to the text "B", otherwise resolves to an empty string |
Monster Attack Templates | |
---|---|
^^attk1^^ | Resolves to the name of the creature\'s attack 1, if provided (applies to monster attacks only) |
^^attk2^^ | Resolves to the name of the creature\'s attack 2, if provided (applies to monster attacks only) |
^^attk3^^ | Resolves to the name of the creature\'s attack 3, if provided (applies to monster attacks only) |
^^monsterCritHit^^ | Resolves to the critical hit dice roll value of the creature (applies to monster/creature attacks only) |
^^monsterCritMiss^^ | Resolves to the critical miss dice roll value of the creature (applies to monster/creature attacks only) |
^^monsterDmgMacroSM^^ | Resolves to the correct Ability Macro name to use in an API button or macro call to run the matching Monster Ability damage Macro |
^^monsterDmgMacroL^^ | Resolves to the correct Ability Macro name to use in an API button or macro call to run the matching Monster Ability damage Macro |
^^monsterDmgMacro1^^ or ^^monsterDmgMacro2^^ or ^^monsterDmgMacro3^^ | Legacy values. Resolve to the same damage Macro call as ^^monsterDmgMacroSM^^ |
^^monsterDmgSM^^ or ^^monsterDmgL^^ | Depending on if the Player chose for Roll20 to roll the damage dice or to roll their own dice, resolves to one of (a) the damage dice specification for the selected Monster attack, or (b) a Roll Query requesting the Player to enter a dice roll result. Currently both SM and L resolve the same |
^^monsterDmg1^^ or ^^monsterDmg2^^ or ^^monsterDmg3^^ | Legacy values. Resolve to the same as ^^monsterDmgSM |
Melee Weapon Attack Templates | |
---|---|
^^weapon^^ | Resolves to the name of the weapon |
^^weapAttkAdj^^ | Resolves to the magical attack adjustment of the weapon |
^^weapStyleAdj^^ | Resolves to the fighting style attack adjustment of the weapon |
^^weapStrHit^^ | Resolves to a 1 if the character\'s strength to-hit bonus applies to this weapon, or 0 otherwise |
^^profPenalty^^ | Resolves to any proficiency penalty incurred by the attacking character for using a non-proficient or related weapon, or 0 if is proficient |
^^specProf^^ | Resolves to 1 if the attacking character is a specialist in the weapon, otherwise 0 |
^^masterProf^^ | Resolves to 1 if the attacking character is a master (double specialised) in the weapon, otherwise 0 |
^^raceBonus^^ | Resolves to the race bonus of the attacking character with this weapon |
^^twoWeapPenalty^^ | Resolves to any penalty relevant if the attacking character is using two weapons to attack, adjusted by any relevant Fighting Style benefit (is 0 for character classes that can use two weapons without penalty, such as rangers) |
^^weapDmgAdj^^ | Resolves to the magical damage adjustment of the weapon |
^^weapStyleDmgAdj^^ | Resolves to any fighting style damage bonus |
^^magicDmgAdj^^ | Resolves to any magical effect damage bonus or penalty resulting from magic currently in effect |
^^backstab^^ | Resolves to a 1 if a backstab is being attempted, otherwise 0 |
^^rogueLevel^^ | Resolves to the Rogue class level of the attacking character (0 if not a Rogue) |
^^weapCritHit^^ | Resolves to the critical hit dice roll value of the weapon, adjusted by any relevant Fighting Style benefit |
^^weapCritMiss^^ | Resolves to the critical miss dice roll value of the weapon, adjusted by any relevant Fighting Style benefit |
^^weapDmgSM^^ | Depending on if the Player chose for Roll20 to roll the damage dice or to roll their own dice, resolves to one of (a) the damage dice specification vs. Medium and smaller opponents for the weapon, or (b) a Roll Query requesting the Player to enter a dice roll result |
^^weapStyleDmgSM^^ | Resolves to any Fighting Style damage bonus when doing damage to a Medium or smaller opponent |
^^weapDmgL^^ | Depending on if the Player chose for Roll20 to roll the damage dice or to roll their own dice, resolves to one of (a) the damage dice specification vs. Large and larger opponents for the weapon, or (b) a Roll Query requesting the Player to enter a dice roll result |
^^weapStyleDmgL^^ | Resolves to any Fighting Style damage bonus when doing damage to a Large or larger opponent |
^^weapStrDmg^^ | Resolves to a 1 if the character\'s strength damage bonus applies to this weapon, or 0 otherwise |
^^mwSMdmgMacro^^ | Resolves to the correct Ability Macro name to use in an API button or macro call to run the matching Melee weapon Ability damage Macro against Medium and smaller opponents |
^^mwLHdmgMacro^^ | Resolves to the correct Ability Macro name to use in an API button or macro call to run the matching Melee weapon Ability damage Macro against Large and larger opponents |
Ranged Weapon Attack Templates | |
---|---|
^^weapon^^ | Resolves to the name of the weapon |
^^weapAttkAdj^^ | Resolves to the magical attack adjustment of the ranged weapon. Normally 0 as plus is often on the ammo |
^^weapStyleAdj^^ | Resolves to the Fighting Style attack adjustment of the weapon |
^^dexMissile^^ | Resolves to the dexterity missile adjustment of the attacking character |
^^weapDexBonus^^ | Resolves to a 1 if the dexterity missile bonus applies to the weapon, otherwise 0 |
^^strAttkBonus^^ | Resolves to the strength to-hit bonus/penalty of the attacking character |
^^weapStrHit^^ | Resolves to a 1 if the character\'s strength to-hit bonus applies to this weapon, or 0 otherwise |
^^profPenalty^^ | Resolves to any proficiency penalty incurred by the attacking character for using a non-proficient or related weapon, or 0 if proficient |
^^specProf^^ | Resolves to 1 if the attacking character is a specialist in the weapon, otherwise 0 |
^^masterProf^^ | Resolves to 1 if the attacking character is a master (double specialised) in the weapon, otherwise 0 |
^^raceBonus^^ | Resolves to the race bonus of the attacking character with this weapon |
^^twoWeapPenalty^^ | Resolves to any penalty relevant if the attacking character is using two weapons to attack, adjusted by any relevant Fighting Style benefit (is 0 for character classes that can use two weapons without penalty, such as rangers) |
^^weapDmgAdj^^ | Resolves to the magical damage adjustment of the weapon |
^^rangeMod^^ | Resolves to the range attack modifier, as modified by any valid fighting style |
^^rangeN^^ | Resolves to 1 if the range is "Near", otherwise 0 |
^^rangePB^^ | Resolves to 1 if the range is "Point Blank", otherwise 0 |
^^rangeS^^ | Resolves to 1 if the range is "Short", otherwise 0 |
^^rangeM^^ | Resolves to 1 if the range is "Medium", otherwise 0 |
^^rangeL^^ | Resolves to 1 if the range is "Long", otherwise 0 |
^^rangeF^^ | Resolves to 1 if the range is "Far", otherwise 0 |
^^rangeSMLF^^ | Resolves to 1 if the range is not "Near" or "Point Blank", otherwise 0 |
^^ammoDmgAdj^^ | Resolves to the magical damage adjustment of the selected ammunition of the ranged weapon |
^^ammoStyleDmgAdj^^ | Resolves to the fighting style damage adjustment of the ranged weapon |
^^magicDmgAdj^^ | Resolves to any magical effect damage bonus or penalty resulting from magic currently in effect |
^^strDmgBonus^^ | Resolves to the strength damage bonus/penalty of the attacking character |
^^weapCritHit^^ | Resolves to the critical hit dice roll value of the weapon, as modified by any valid fighting style |
^^weapCritMiss^^ | Resolves to the critical miss dice roll value of the weapon, as modified by any valid fighting style |
^^ACvsNoModsMissile^^ | Resolves to the targeted Armour Class vs missiles macro call @{target|Select Target|ACmissile-field} vs. a targeted opponent (only used in targeted attacks) |
^^ACvsSlashMissile^^ | Resolves to the targeted Slashing damage Armour Class vs missiles macro call @{target|Select Target|SlashACmissile-field} vs. a targeted opponent (only used in targeted attacks) |
^^ACvsPierceMissile^^ | Resolves to the targeted Piercing Armour Class vs missiles macro call @{target|Select Target|PierceACmissile-field} vs. a targeted opponent (only used in targeted attacks) |
^^ACvsBludgeonMissile^^ | Resolves to the targeted Bludgeoning Armour Class vs missiles macro call @{target|Select Target|BludgeonACmissile-field} vs. a targeted opponent (only used in targeted attacks) |
^^ACvsNoModsMissileTxt^^ | Resolves to the text "No Mods" |
^^ACvsSlashMissileTxt^^ | If this is a slashing weapon, resolves to the text "Slash", otherwise resolves to an empty string |
^^ACvsPierceMissileTxt^^ | If this is a piercing weapon, resolves to the text "Pierce", otherwise resolves to an empty string |
^^ACvsBludgeonMissileTxt^^ | If this is a bludgeoning weapon, resolves to the text "Bludgeon", otherwise resolves to an empty string |
^^ACvsSmissileTxt^^ | If this is a slashing weapon, resolves to the text "S", otherwise resolves to an empty string |
^^ACvsPmissileTxt^^ | If this is a piercing weapon, resolves to the text "P", otherwise resolves to an empty string |
^^ACvsBmissileTxt^^ | If this is a slashing weapon, resolves to the text "B", otherwise resolves to an empty string |
^^ammoDmgSM^^ | Depending on if the Player chose for Roll20 to roll the damage dice or to roll their own dice, resolves to one of (a) the damage dice specification vs. Medium and smaller opponents for the ammunition used, or (b) a Roll Query requesting the Player to enter a dice roll result |
^^ammoStyleDmgSM^^ | Resolves to any Fighting Style damage bonus when doing damage to a Medium or smaller opponent |
^^ammoDmgL^^ | Depending on if the Player chose for Roll20 to roll the damage dice or to roll their own dice, resolves to one of (a) the damage dice specification vs. Large and larger opponents for the ammunition used, or (b) a Roll Query requesting the Player to enter a dice roll result |
^^ammoStyleDmgL^^ | Resolves to any Fighting Style damage bonus when doing damage to a Large or larger opponent |
^^ammoStrDmg^^ | Resolves to a 1 if the character\'s strength damage bonus applies to the selected ammunition, or 0 otherwise |
^^ammoLeft^^ | Resolves to the quantity of the selected ammunition left after this attack |
^^rwSMdmgMacro^^ | Resolves to the correct Ability Macro name to use in an API button or macro call to run the matching Ranged weapon Ability damage Macro against Medium and smaller opponents |
^^rwLHdmgMacro^^ | Resolves to the correct Ability Macro name to use in an API button or macro call to run the matching Ranged weapon Ability damage Macro against Large and larger opponents |
As previously described, the Attacks Database contains Attack Templates as Ability Macro entries. For Characters and NPCs that attack using weapons "in-hand" (see AttackMaster API documentation for information on taking weapons "in-hand"), four standard Attack Templates are required: -ToHit; -DmgSM; -DmgL; and -Targeted-Attk.
' + +'A standard To Hit Attack Template looks like this (the example is for the AD&D2e version):
' + +'^^toWhoPublic^^ &{template:^^defaultTemplate^^}{{title=^^tname^^ attacks with their ^^weapon^^}}{{subtitle=Melee Attack}}{{Weapon Used=^^weapon^^}}Specs=[MWtoHit,AttackMacro,1d20,Attack]!setattr --silent --charid ^^cid^^ --ac-hit|{{AC Hit=[[([[^^thac0^^]][Thac0]) - ((([[^^weapAttkAdj^^]][Weapon+]) + ([[^^weapStyleAdj^^]][Style+]) + ([[(^^strAttkBonus^^ * ^^weapStrHit^^)]][Strength+]) + ([[^^profPenalty^^]][Prof Penalty] + [[^^specProf^^]][Specialist] + [[^^masterProf^^*3]][Mastery]) + ([[^^raceBonus^^]][Race mod]) + ([[^^magicAttkAdj^^]][Magic hit adj]) + ([[^^twoWeapPenalty^^]][2-weap penalty]))) - ([[^^toHitRoll^^cs>^^weapCritHit^^cf\<^^weapCritMiss^^]][Dice roll]) ]] }}!!!{{Attk Type=^^weapType^^}}{{Dmg S=[Roll](~^^mwSMdmgMacro^^)}}{{Dmg L=[Roll](~^^mwLHdmgMacro^^)}}{{Crit Roll=^^weapCritHit^^}}{{Fumble Roll=^^weapCritMiss^^}}' + +'
The four fields in the Specs data are the standard four used in all Specs fields: Entry Type (MWtoHit), Entry Class (AttackMacro), Handedness (in this case replaced by toHit dice roll spec 1d20), and Entry Supertype (Attack). The key field to note here is the ToHit Dice Roll Specification (which replaces the Handedness field). This can be any valid dice roll, and will be used for the ^^toHitRoll^^ template field if the Player selects Roll20 to roll the attack dice.
' + +'This Attack Template is formatted using a Roll20 Roll Template with a type specified using the ^^defaultTemplate^^ template field, but need not be - formatting is up to the creator of the Attack Template. It is important this information is displayed to the right people - Players that control a character, all Players as a public post, or just the DM for attacks by creatures & NPCs. The ^^toWhoPublic^^ template field will check if the attacking token represents a Character/NPC/Creature controlled by a Player and, if so, make a public post that all can see, but otherwise just whisper the results of the attack to the DM only. Similarly, ^^toWho^^ will whisper the attack information either only to the Player(s) that control the attacking character/creature or, if no one does, then to the DM.
' + +'The rest of the Attack Template defines the calculations using the API supplied data to display the Armour Class value that would be successfully hit by the attacking character, with the selected weapon under the current conditions. It also defines a display of the adjustments that are made to the dice roll which the Players and DM can hover a mouse over to get an explanation of the calculations. All the calculations and tag display are standard Roll20 functionality, so once the Attack Template Data Fields are replaced by actual values by the API and the resulting Ability Macro saved to the attacking character\'s Character Sheet, running it like any other Ability Macro will use only Roll20 functionality, and not involve use of the APIs (unless the Attack template specifically includes an API call).
' + +'The Attack Template shown above is a To Hit template for a Melee weapon attack. That for a Ranged weapon attack is very similar, just using some different and ranged attack related Template Fields.
' + +'There are always two damage Attack Templates to go with each To Hit Template, typically one for damage to Medium and smaller opponents and one for Large and larger. However, as will be seen when discussing the Oil Flask bespoke Attack Templates below, those are not always the outcomes of the two damage Templates. However, they do always start with MW-DmgSM and MW-DmgL for Melee weapon attacks, and RW-DmgSM and RW-DmgL for Ranged weapon attacks (optionally followed by a race, class or weapon name). The standard Damage Attack Template looks like this:
' + +'^^toWhoPublic^^ &{template:^^defaultTemplate^^}{{title=^^tname^^ does damage with their ^^weapon^^}}{{Subtitle=Melee Attack}}Specs=[MWtoHit,AttackMacro,1d20,Attack]{{AC Hit=@{^^cname^^|ac-hit}}}{{Attk Type=^^weapType^^}}{{Dmg S=[[ ([[^^weapDmgSM^^]][Dice Roll]) + ([[^^strDmgBonus^^*^^weapStrDmg^^]][Strength+]) + ([[^^weapDmgAdj^^]][Weapon+]) + ([[^^weapStyleDmgAdj^^+^^weapStyleDmgSM^^]][Style+]) + ([[^^magicDmgAdj^^]][Magic dmg adj]) + ([[^^specProf^^*2]][Specialist+] + [[^^masterProf^^*3]][Mastery+])]]}}{{Dmg L=[Roll](~^^mwLHdmgMacro^^)}}{{Crit Roll=^^weapCritHit^^}}{{Fumble Roll=^^weapCritMiss^^}}
' + +'The damage Attack Template works in the same way as the other Attack Templates as explained above for the ToHit template. While the Specs data includes the "To Hit" dice roll specification, this is not used in the damage Template, and is irrelevant. That field just needs to hold something, and a dice roll specification is what is expected. Hopefully, the rest of this damage Attack Template is self explanatory.
' + +'If a creature specified as a Monster on the Character Sheet uses in-hand weapons, as supported under the RPGMaster APIs, attacks with those weapons (Melee or Ranged) will use the standard To Hit and Damage Attack Templates described above. However, attacks by creatures that are specified as Monsters (e.g. on the Monster tab of the Advanced 2nd Edition character sheet) are generally much simpler than character attacks with in-hand weapons. The differences mean that such creatures require different Attack Templates. Attacks specified on the Monster tab use the Monster Attack Templates: Mon-Attk, Mon-DmgSM, and Mon-DmgL (or the Targeted Attack Templates, see later). Remember that the AttackMaster API supports an extension of the Monster Attk fields on the Advanced 2nd Edition Character Sheet: each can contain a comma-separated list consisting of "attack name","dice roll","speed in segments","damage type" (see AttackMaster Help for details).
' + +'The Monster Attack and Damage Templates look like this:
' + +'^^toWhoPublic^^ &{template:^^defaultTemplate^^}{{title=^^tname^^ attacks with their ^^attk^^}}{{subtitle=Monster Attack}}{{Weapon Used=^^attk^^}}Specs=[MonAttk,AttackMacro,1d20,Attack]!setattr --silent --charid ^^cid^^ --ac-hit|{{AC Hit=[[([[^^thac0^^]][Thac0]) - ([[^^magicAttkAdj^^]][Magic hit adj]) - ([[^^toHitRoll^^cs\>^^monsterCritHit^^cf\<^^monsterCritMiss^^]][Dice roll]) ]]}}!!!{{Attk Type=^^weapType^^}}{{Dmg S=[Roll](~^^monsterDmgMacroSM^^)}}{{Dmg L=[Roll](~^^monsterDmgMacroL^^)}}{{Crit Roll=^^monsterCritHit^^}}{{Fumble Roll=^^monsterCritMiss^^}}
' + +'^^toWhoPublic^^ &{template:^^defaultTemplate^^}{{title=^^tname^^ does damage with their ^^attk^^}}{{subtitle=Monster Attack}}Specs=[MonRoll,AttackMacro,1d20,Attack]{{AC Hit=[[@{^^cname^^|ac-hit}[AC Hit] ]]}}{{Attk Type=^^weapType^^}}{{Dmg L=[[(([[^^monsterDmg^^]][^^attk^^ Dmg])+([[^^magicDmgAdj^^]][Added Magic Dmg]))]]}}{{Dmg S=[Roll](~^^monsterDmgMacroSM^^)}}{{Crit Roll=^^monsterCritHit^^}}{{Fumble Roll=^^monsterCritMiss^^}}
' + +'Unsurprisingly, these work in exactly the same way as other Attack Templates.
' + +'The AttackMaster API supports the DM (and optionally, Players) using "targeted attacks". This is an attack that prompts the DM / Player to select a target token, and then performs all attack and damage dice rolls at the same time, displaying the attack results alongside the Armour Class and relative health of the targeted opponent. This speeds the attack process even further than having the API do all the attack calculations. Note: the results of the attack are not applied to the targeted opponent - the results are still open to interpretation by the Players and DM, and circumstantial adjustment before manually applying them to the Token / Character Sheet of the opponent.
' + +'The Ranged weapon Targeted Attack Template for AD&D2e (for example) looks like this:
' + +'^^toWhoPublic^^ &{template:^^defaultTemplate^^}{{title=^^tname^^ attacks @{Target|Select Target|Token_name} with their ^^weapon^^}}{{subtitle=Ranged Attack * **Ammo Left: ^^ammoLeft^^**}}Specs=[RWtargetedAttk,AttackMacro,1d20,Attack]{{AC Hit=[[([[^^thac0^^]][Thac0])-(([[^^weapAttkAdj^^]][Weapon+]) + ([[^^ammoDmgAdj^^]][Ammo+]) + ([[^^weapStyleAdj^^]][Style+]) + ([[ ^^weapDexBonus^^*[[^^dexMissile^^]]]][Dexterity+] )+([[[[^^strAttkBonus^^]]*[[^^weapStrHit^^]]]][Strength+])+([[^^raceBonus^^]][Race mod])+([[^^profPenalty^^]][Prof penalty])+([[^^magicAttkAdj^^]][Magic Hit+])+([[^^twoWeapPenalty^^]][2-weap penalty])+([[^^rangeMod^^]][Range mod]))-([[^^toHitRoll^^cs\>^^weapCritHit^^cf\<^^weapCritMiss^^]][Dice roll]) ]] }}{{Attk Type=^^weapType^^}}{{Target AC=^^targetACmissile^^}}{{Target SAC=^^ACvsSlashMissile^^}}{{Target PAC=^^ACvsPierceMissile^^}}{{Target BAC=^^ACvsBludgeonMissile^^}}{{Dmg S=[[ floor( ([[^^ammoDmgSM^^]][Dice roll]) * ([[(^^rangeN^^*0.5)+(^^rangePB^^*(1+^^masterProfPB^^))+(^^rangeSMLF^^*1)]][Range mult])) + ([[^^rangePB^^*^^masterProfPB^^*2]][Range mod]) + (([[^^ammoDmgAdj^^]][Ammo+])+([[^^ammoStyleDmgAdj^^+^^ammoStyleDmgSM^^]][Style+])+([[^^magicDmgAdj^^]][Magic dmg+]) +([[^^strDmgBonus^^*^^ammoStrDmg^^]][Strength+])) ]]}}{{Dmg L=[[ floor( ([[^^ammoDmgSM^^]][Dice roll]) * ([[(^^rangeN^^*0.5)+(^^rangePB^^*(1+^^masterProfPB^^))+(^^rangeSMLF^^*1)]][Range mult])) + ([[^^rangePB^^*^^masterProfPB^^*2]][Range mod]) + (([[^^ammoDmgAdj^^]][Ammo+])+([[^^ammoStyleDmgAdj^^+^^ammoStyleDmgL^^]][Style+])+([[^^magicDmgAdj^^]][Magic dmg+]) +([[^^strDmgBonus^^*^^ammoStrDmg^^]][Strength+])) ]]}}{{Target HP=^^targetHP^^}}{{Target MaxHP=^^targetMaxHP^^}}{{Target Heart=^^targetHP^^/^^targetMaxHP^^}}{{Crit Roll=^^weapCritHit^^}}{{Fumble Roll=^^weapCritMiss^^}}{{Result=AC Hit\<=Target AC}}
' + +'The key difference, other than doing all of the calculations for the Armour Class hit and both the damage for Medium and smaller and Large and larger, is that several of the Template Fields used resolve to appropriately formatted Roll20 @{target|...} entries, that will display values from the targeted opponent\'s token and/or character sheet. The API searches for the most appropriate token and character sheet fields to resolve the targeted Template Fields to (it searches the attacking creature\'s data, and assumes all tokens are set up the same way): if using the standard RPGMaster token settings (as set by the CommandMaster API --abilities command or [Token Setup] DM\'s Macro button) it will find the data it needs on the token; otherwise it will first search other token fields, then standard Character Sheet character tab fields, then Character Sheet monster tab fields. If the API can\'t find AC or HP data, it will display appropriate text saying the data was not found, but will not cause an error. Thus it is sensible to use these Template Fields rather than statically defined @{target|...} commands.
' + +'All of the above examples and discussion have explored the standard Attack Templates distributed with the AttackMaster API. It will be the case that this will cater for around 95% of attacks and attack-like situations (e.g. there is a weapon called "Touch" which Spell Casters can use for touch-attack spells, which uses the standard Attack Templates without change to achieve the needed outcome). However, for that other 5% (or perhaps closer to 1%) of special cases you can define your own bespoke or custom Attack Templates. Being able to change the way attacks are calculated is also essential if you wish to adapt the API to work for game systems other than those for which RPGMaster Library rulesets currently exist.
' + +'As stated in Section 1, you should not add Attack Templates to the Attacks-DB database directly. Instead, create your own Character Sheet named Attack-DB-[any-name-you-want] and add Attack Templates to it. If you want to replace the Attack Templates provided for Melee weapons, Ranged weapons and/or Monster attacks, just create ones in your added database with the same Ability Macro name and they will automatically be used in preference to the standard versions.
' + +'As also mentioned previously, while Attack Templates are always called with names of the format given in Section 3, they do not have to result in standard attacks or damage - the calculations and resulting information provided to players can be whatever is needed as a result of that attack action. An example of this is included in the distributed Attacks-DB, the Oil Flask Attack Template set:
' + +'^^toWhoPublic^^ &{template:^^defaultTemplate^^}{{title=^^tname^^ throws a prepared oil flask}}{{titlebox=transparent}}{{titletext=red; text-shadow: 1px 1px 1px gray}}{{titleimg=https://s3.amazonaws.com/files.d20.io/images/250365814/HB7bJNTar3xasqz7X9W5bg/thumb.png?1634239406}}{{subtitle=Ranged Attack * **Flasks Left: ^^ammoLeft^^**}}{{Weapon Used=Burning Oil Flask}}Specs=[RWtoHitOilFlask,AttackMacro,1d20,Attack]!setattr --silent --charid ^^cid^^ --ac-hit|{{AC Hit=[[([[^^thac0^^]][Thac0])-([[([[^^weapAttkAdj^^]][Weapon+]) + ([[^^weapStyleAdj^^]][Style+]) + ([[^^ammoDmgAdj^^]][Ammo+]) + ([[ ^^weapDexBonus^^*[[^^dexMissile^^]]]][Dexterity+] )+([[[[^^strAttkBonus^^]]*[[^^weapStrHit^^]]]][Strength+])+([[^^raceBonus^^]][Race mod])+([[^^profPenalty^^]][Prof penalty])+([[^^magicAttkAdj^^]][Magic Hit+])+([[^^twoWeapPenalty^^]][2-weap penalty])+([[^^rangeMod^^]][Range mod])]][Adjustments])-([[^^toHitRoll^^cs\>^^weapCritHit^^cf\<^^weapCritMiss^^]][Dice roll]) ]]}}!!!{{Attk Type=^^weapType^^}}{{dmgslabel=Direct Hit}}{{Dmg S=[Hit](~^^rwSMdmgMacro^^)}}{{dmgllabel=Grenade /Splash}}{{Dmg L=[Splash](~^^rwLHdmgMacro^^)}}{{Crit Roll=^^weapCritHit^^}}{{Fumble Roll=^^weapCritMiss^^}}
' + +'The To Hit Attack Template is very similar to a normal Ranged weapon Attack Template, except that instead of API buttons indicating damage rolls vs. different sized opponents, it provides one API button to select a [Direct Hit], and another selecting a [Grenade/Splash] outcome. Each of these still calls the same damage Attack Templates using the Template Fields provided but in this case, because the attack is with a weapon called Oil Flask, the Template Fields will resolve to calls to RW-DmgSM-Oil-Flask and RW-DmgL-Oil-Flask respectively, and these custom Attack Templates do damage in a very different way to a normal Ranged Weapon attack.
' + +'!rounds --aoe @{target|Who\'s the target?|token_id}|circle|feet|0|7|0|fire|true --target single|^^tid^^|@{target|Who\'s the target?|token_id}|Oil-fire|1|-1|Taking fire damage from burning oil|three-leaves
'
+ +'^^toWhoPublic^^ &{template:^^defaultTemplate^^}{{title=^^tname^^ throws a prepared oil flask}}{{TitleBox=transparent}}{{titletext=red; text-shadow: 1px 1px 1px gray}}{{TitleImg=https://s3.amazonaws.com/files.d20.io/images/250365814/HB7bJNTar3xasqz7X9W5bg/thumb.png?1634239406}} {{subtitle=Burning oil * **Flasks Left: ^^ammoLeft^^**}}{{Weapon Used=Burning Oil Flask}}Specs=[RWDmgSMOilFlask,AttackMacro,1d20,Attack]{{AC Hit=[[@{^^cname^^|ac-hit}[AC Hit] ]]}}{{Attk Type=^^weapType^^}}{{DmgSlabel=Fire round 1}}{{Dmg S=[[([[^^ammoDmgSM^^]][Dice Roll])]]}}{{DmgLlabel=Grenade /Splash}}{{Dmg L=[Splash](~^^rwLHdmgMacro^^)}}
The Oil Flask version of the RW-DmgSM Attack Template caters for damage done by an Oil Flask successfully scoring a direct hit on an opponent. This results in a damage dice roll for this round of 2d6 with no modifiers, and then also makes an API call to the RoundMaster API with two stacked commands: an Area of Effect call to place fire on the opponent\'s token, and a Target call to add a status to the opponent\'s token which will last 2 rounds, causing a Status Effect Macro to run in the second round to prompt for another damage roll of 1d6.
' + +'!rounds --aoe ^^tid^^|circle|feet|[[(^^rangeN^^*5)+(^^rangePB^^*10)+(^^rangeS^^*10)+(^^rangeM^^*20)+(^^rangeL^^*30)+(^^rangeF^^*30)-5]]|4|0|fire
'
+ +'^^toWhoPublic^^ &{template:2Egrenademiss} {{name=Prepared Oil Flask}}Specs=[RWDmgLOilFlask,AttackMacro,1d20,Attack]{{aoe=[[3]]}} {{aoesplash=[[3]]}} {{hitdmg=[Hit](~^^rwSMdmgMacro^^)}} {{splashdmg=[Damage](!
/r ^^ammoDmgL^^)}}{{direction=[[1d10]]}} {{distancename=^^range^^}} {{distance=[[(^^rangePB^^+^^rangeS^^)d6+^^rangeM^^d10+(^^rangeL^^*2)d10+(^^rangeF^^*4)d10]]}} {{hit=[[0]]}} {{splash=[[1]]}}
The Oil Flask version of the RW-DmgL Attack Template caters for an Oil Flask that either missed its intended target, or is deliberately used as a grenade-like missile. This results in a dice roll of 1d3 splash damage to anyone in the area of effect, which is shown using a call to the RoundMaster API Area of Effect command which this time can be positioned where the oil flask landed.
' + +'^^toWhoPublic^^ &{template:^^defaultTemplate^^}{{title=^^tname^^ throws an oil flask at @{Target|Select Target|Token_name} }}{{titlebox=transparent}}{{titletext=red; text-shadow: 1px 1px 1px gray}}{{titleimg=https://s3.amazonaws.com/files.d20.io/images/250365814/HB7bJNTar3xasqz7X9W5bg/thumb.png?1634239406}}{{subtitle=Ranged Attack * **Flasks Left: ^^ammoLeft^^**}}Specs=[RWtargetedOilFlask,AttackMacro,1d20,Attack]{{AC Hit=[[([[^^thac0^^]][Thac0])-(([[^^weapAttkAdj^^]][Weapon+]) + ([[^^weapStyleAdj^^]][Style+]) + ([[^^ammoDmgAdj^^]][Ammo+]) + ([[ ^^weapDexBonus^^*[[^^dexMissile^^]]]][Dexterity+] )+([[[[^^strAttkBonus^^]]*[[^^weapStrHit^^]]]][Strength+])+([[^^raceBonus^^]][Race mod])+([[^^profPenalty^^]][Prof penalty])+([[^^magicAttkAdj^^]][Magic Hit+])+([[^^twoWeapPenalty^^]][2-weap penalty])+([[^^rangeMod^^]][Range mod]))-([[^^toHitRoll^^cs\>^^weapCritHit^^cf\<^^weapCritMiss^^]][Dice roll]) ]] }}{{Attk Type=^^weapType^^}}{{Target AC=^^targetACmissile^^}}{{Target SAC=^^ACvsSlashMissile^^}}{{Target PAC=^^ACvsPierceMissile^^}}{{Target BAC=^^ACvsBludgeonMissile^^}}{{DmgSlabel=Direct Hit}}{{Dmg S=[Hit](~^^rwSMdmgMacro^^)}}{{DmgLlabel=Grenade /Splash}}{{Dmg L=[Splash](~^^rwLHdmgMacro^^)}}{{Target HP=^^targetHP^^}}{{Target MaxHP=^^targetMaxHP^^}}{{Target Heart=^^targetHP^^/^^targetMaxHP^^}}{{Crit Roll=^^weapCritHit^^}}{{Fumble Roll=^^weapCritMiss^^}}{{Result=AC Hit<=Target AC}}
' + +'The Oil Flask version of the RW-Targeted-Attk Attack Template combines the functions and calculations of the other Oil Flask custom Attack Templates in a single result display, with appropriate API buttons to implement the various outcomes of the attack.
' + +'Similar or entirely different custom Attack Templates can be created for other individual weapons with non-standard attack outcomes, or for individual classes of character, creatures, or races. Just use the Attack Template naming conventions described in Section 3, and add them to your own Attack Databases as described in Section 1, and you can give Players more interesting situations and means of dealing with them. When combined with the features and capabilities of the RoundMaster API and other APIs of the RPGMaster suite, the possibilities are endless!
' + +'Fighting style databases have names that start with Styles-DB, and can have anything put at the end, though those with version numbers of the form v#.# as part of the name will be ignored.
' + +'As previously stated, each style definition has 3 (or 4) parts in the database (see Section 1): an Ability Macro with a name that is unique and matches the style, an Attribute with the name of the Ability Macro preceded by "ct-", a listing in the database character sheet of the ability macro name separated by \'|\' along with other fighting styles. The quickest way to understand these entries is to examine existing entries. Do extract the root databases and take a look (but remember to delete them after exploring the items in them, so as not to slow the system down unnecessarily).
' + +'Note: The DM creating new weapons does not need to worry about anything other than the Ability Macro in the database, as running the AttackMaster or MagicMaster -check-db Styles-DB command will update all other aspects of the database appropriately for all databases that have a name starting with or including \'Styles-DB\', as long as the Specs and Data fields are correctly defined. Running the command -check-db with no parameters will check and update all databases.
' + +'The Styles-DB database provided with the APIs contains standard definitions for the four fighting styles defined in The Complete Fighter\'s Handbook, plus a couple of other examples for Ranged weapons to demonstrate how other styles can be defined. After extracting the provided Styles database or creating your own as discussed above, aAbility macros can be added to a database just by using the [+Add] button at the top of the Abilities column in the Attributes and Abilities tab of the Database Character Sheet, and then using the edit "pencil" icon on the new entry to open it for editing. Ability macros are standard Roll20 functionality and not dependent on the API. Refer to the Roll20 Help Centre for more information.
' + +'Here is an example:
' + +'&{template:RPGMdefault}{{name=Two Weapon Fighting Style}}Specs=[Two Weapon,Style,2H,Melee-Style]{{desc=With this popular style, the fighter has a weapon in each hand—usually a longer weapon in his good hand and a shorter one in his off-hand. Unless the character has Style Specialization in this style, the second (off-hand) weapon must be shorter than the primary weapon.}}StyleData=[prime:melee, offhand:melee, t:any, st:any],[twp:0.2],[twp:0.2]{{desc1=**Advantages**
'
+ +'One great advantage to this style is that you always have another weapon in hand if you drop or lose one. A single Disarm maneuver cannot rid you of your weapons.}}{{desc2=**Disadvantages**
'
+ +'The principal disadvantage to this style, as with some other styles, is that you don\'t gain the AC benefit of a shield.}}{{desc3=**Style Specialization**
'
+ +'Please read the "Attacking with Two Weapons" section from the Player\'s Handbook, page 96, before continuing.
'
+ +'If you devote a weapon proficiency slot to style specialization with Two-Weapon Style, you get two important benefits. First, your attack penalty drops; before, it was a –2 with your primary weapon and –4 with your secondary, but with Specialization in Two-Weapon Style it becomes 0 with your primary weapon and a –2 with your secondary weapon. (If you\'re already ambidextrous, that penalty is 0 with primary weapon and 0 with secondary weapon). Second, you\'re allowed to use weapons of the same length in each hand, so you can, for example, wield two long swords.
'
+ +'When fighting with two-weapon technique, you can choose for both weapons to try the same maneuver (for example, two strikes, or two disarms), or can have each try a different maneuver (one strike and one parry, one pin and one strike). If the two maneuvers are to be different, each receives a –1 attack penalty.
'
+ +'Though rangers don\'t suffer the off-hand penalties for two-weapons use, they do not get a bonus to attack rolls if they devote a weapon proficiency slot to Two-Weapon Style. They do get the other benefit, of being able to use weapons of equal length.}}
The ability specification for the Weapon & Shield Fighting Style uses a Roll20 Roll Template, in this case defined in the RPGMaster Library (see the help handout for the Library to review the specifications of this template), but any Roll Template you desire can be used. The entries in the Roll Template itself can be anything you desire, giving as much or as little information as you want. However, the important elements for the APIs are those highlighted. Each of these elements are inserted between the elements of the Roll Template, meaning they will not be seen by the player when the macro is run. Generally spaces, hyphens and underscores in the data elements are ignored, and case is not significant. Each element is described below:
' + +'Specs = [Type, Spec Class, Handedness, Style Group]' + +'
The Specs section describes what style type and proficiency groups this weapon belongs to. These fields must be in this order. This format is identical for all database items, whether in these databases or others used by the Master series of APIs.
' + +'Type | is the type of the style, often similar to the ability macro name. |
Spec Class | is always Style for entries in the Styles database. |
Handedness | is #H, where # is the number of hands needed to engage in this fighting style. |
Style Group | is the group of related fighting styles that the style belongs to, which currently can be Melee Style or Ranged Style. Further Style Groups related to other forms of combat may be introduced in the future. |
StyleData=[prime:melee, offhand:shield|melee, t:any, st:any],[shattk:+1],[shattk:+1,twp:0.2]' + +'
The StyleData section specifies the data relating to the style rules for which types of weapon need to be wielded in which hands, and the benefits then achieved if either Proficient in the Style, or a Specialist in the Style. The first set of brackets enclose the preconditions for the style to be valid given what the character has in-hand (as equipped using the attk menu / change weapon menu). The second set specify the benefits to be implemented if the style is valid and the character is proficient in the style, and the third set specify the benefits from being a specialist (as set by the token-setup / Add to Proficiences menu). These fields can be in any order.
' + +'prime: | Nothing in hand | The class of weapon or item that must be being held in the Primary hand for this Style to be effective: one or more of spell, melee, ranged, shield, and throwing. Can be a multi-class item, such as a weapon defined as melee|ranged for a weapon that can be used both as a melee and a ranged weapon (e.g. a warhammer) |
offhand: | Nothing in hand | The class of weapon or item that must be being held in the Offhand hand for this Style to be effective (classes as for Primary hand). Can be a multi-class item, as described for the Primary hand. In this case shield|melee is specified, meaning a shield that can be used to punch or parry as a weapon. |
weaps: | any | The list of weapon/item type(s) or supertype(s) (weapon groups) that are valid to have in-hand for this Style to be effective, separated by verticle bars (\'|\'). If a supertype is specified, all weapons/items of that supertype will be valid. |
shattk: | 0 | The number of additional attacks in a round to grant for a shield punch or parry if the style is valid and proficient at the specified level |
twp: | 2.4 | The Two Weapon Penalty to grant if the style is valid and proficient at the specified level, specified as primary penalty, dot, offhand penalty |
Whenever proficiencies for the character are changed using the token-setup / Add to Proficiences menu, or items in-hand are equipped using the Attk menu / Change Weapon menu, the APIs will scan all currently proficient and specialist styles defined for the character, and see if any are valid. If they are, the APIs will automatically apply the benefits to initiative (if using the InitiativeMaster API) and to attacks.
' + +'Additional data fields are available for supporting the rules and benefits of other styles:
' + +'twohand: | Nothing in hand | The class of weapon or item that must be being held in Both Hands or otherwise as a two (or more) handed weapon for this Style to be effective, with the same specification syntax as for the primary weapon above. Note that some one-handed weapons can now be taken in-hand as two-handed weapons using the list under the [Both hands] button when equipping, such as Battle Axe, so that the Two Hander Style can be supported as defined in The Complete Fighter\'s Handbook. This requires the latest Weapon Database to be loaded and not overridden by an old definition in a bespoke database in the campaign. |
ac: | 0 | The Armour Class bonus or penalty granted when the style is active (e.g. as required by the Single Weapon Style) |
mwsp: | 0 | The melee weapon speed bonus or penalty granted when the style is active (e.g. as required by the Two Hander Style). Negative numbers improve speed, positive numbers worsen it. |
rwsp: | 0 | The ranged weapon speed bonus or penalty granted when the style is active. Negative numbers improve speed, positive worsen speed. |
mwn: | 0 | The number of attacks per round to increase (or decrease) a melee weapon by when the style is active. Can be fractions expressed as e.g. 1/2 for one additional attack every 2 rounds. Negative numbers reduce attacks per round. |
rwn: | 0 | Same as for mwn, but for ranged weapons. |
mwadj: | 0 | Melee weapon To-hit bonus or penalty granted when the style is active. Positive numbers are beneficial. |
rwadj: | 0 | Ranged weapon To-hit bonus or penalty granted when the style is active. Positive numbers are beneficial. |
mwch: | 20 | Melee weapon Critical Hit value to set when the style is active. |
rwch: | 20 | Ranged weapon Critical Hit value to set when the style is active. |
mwcm: | 1 | Melee weapon Critical Miss value to set when the style is active. |
rwcm: | 1 | Ranged weapon Critical Miss value to set when the style is active. |
rwr: | \'\' | Adjusts the ranges of Ranged Weapons when the style is active. Format is [=][+/-]#/[+/-]#/[+/-]#/[+/-]# where each number adjusts ranges in the order Point Blank/Short/Medium/Long. If Point Blank range is irrelevant for a particular ranged weapon, the first number is ignored. Positive numbers increase range. If \'=\' is specified as the first character, the range is set to be that specified, rather than adjusted by it. |
rwrm: | \'\' | The bonuses or penalties applied at different ranges for ranged weapons when the style is active. Uses the format N=[+/-]#|PB=[+/-]#|S=[+/-]#|M=[+/-]#|L=[+/-]#|F=[+/-]# where each value will be added to the standard range bonuses/penalties. |
dmg: | 0 | The melee weapon damage benefit or penalty applied to any opponent when the style is active. |
dmgsm: | 0 | The melee weapon damage benefit or penalty applied to Medium sized and smaller opponents when the style is active. |
dmgl: | 0 | The melee weapon damage benefit or penalty applied to Large and larger opponents when the style is active. |
ammoadj: | 0 | The ranged weapon ammo damage benefit or penalty applied to all opponents when the style is active. |
ammosm: | 0 | The ranged weapon ammo damage benefit or penalty applied to Medium sized and smaller opponents when the style is active. |
ammol: | 0 | The ranged weapon ammo damage benefit or penalty applied to Large and larger opponents when the style is active. |
oneh: | \'\' | A specification using any of the above fields for benefits to be applied to one-handed weapons only. The format of the specification is oneh:key=value|key=value|... e.g. oneh:dmg=+1|mwn=1/2, |
twoh: | \'\' | A specification using any of the above fields for benefits to be applied to two-handed weapons only. The format of the specification is the same as for oneh |
Here is another example showing how the keys oneh and twoh are used in combination with other keys to implement the Two-Hander Style:
' + +'&{template:RPGMdefault}{{name=Two Hander Fighting Style}}Specs=[Two Hander,Style,2H,Melee-style]{{desc=Two-Hander Style involves carrying and wielding a weapon with both hands. Naturally, many weapons (including polearms, the great axe, the two-handed sword, and others) require two-handed technique. Other weapons (such as bastard sword, javelin, and spear) have it as a listed option.}}StyleData=[twohand:melee, weaps:any],[1H:dmg=+1, 2H:mwsp=-3],[1H:dmg=+1, 2H:mwsp=-3]{{desc1=**Advantages**
'
+ +'The main advantage of two-handed weapon technique is that it allows the character to wield large two-handed weapons which can do substantial amounts of damage.
'
+ +'A second advantage is that, if you are using a two-handed weapon, the Disarm maneuver is only of partial use against you. A single successful Disarm against a two-handed weapon user won\'t knock the weapon out of the wielder\'s hands; it will merely knock his weapon askew and make him take some time to recover, so he automatically loses initiative on his next round. However, two Disarm maneuvers successfully made against the character in the same round will knock the weapon loose.}}{{desc2=**Disadvantages**
'
+ +'As with single-weapon use, two-handed weapon technique has the drawback that the user cannot wear or use a shield, or gain the shield\'s AC bonus.}}{{desc3=**Style Specialization**
'
+ +'You can, by devoting a weapon proficiency to it, take a Style Specialization with Two-Hander Style.
'
+ +'Style Specialization with Two-Hander Style gives you a very specific benefit: When you\'re using a weapon two-handed, that weapon\'s Speed Factor is reduced by 3.
'
+ +'This is because when a fighter wields such a weapon with both hands on the hilt, he has more leverage on the blade and can move it faster. That\'s what Style Specialization in Two-Hander Style will do for the character: It teaches him how to use the weapon much faster and more aggressively than someone with less specialized training in the weapon.}}{{desc4=**One-Handed Weapons Used Two-Handed**
'
+ +'Some players don\'t realize that many other one-handed weapons can also be used two-handed. If you specialize in Two-Hander Style and then use a one-handed weapon in two hands, you also get a bonus of +1 to damage. The one-handed weapons which can be used two-handed in this fashion include: Battle axe, Club, Footman\'s flail, Footman\'s pick, Horseman\'s flail, Horseman\'s mace, Horseman\'s pick, Morning star, Long sword, Warhammer.}}
Here the data specification is:
' + +'StyleData=[twohand:melee, weaps:any],[1H:dmg=+1, 2H:mwsp=-3],[1H:dmg=+1, 2H:mwsp=-3]' + +'
This specifies that there must be a melee weapon equipped in the [Both Hands] slot, and if this is a two-handed weapon then the melee weapon speed is improved by 3 segments. However, if the weapon in the [Both Hands] slot is one of the few one-handed weapons allowed to be taken in both hands (as defined in The Complete Fighter\'s Handbook for this style), then instead it will gain +1 to the damage it inflicts. In this case, the same benefits apply whether the character is just proficient or specialist in the Two-Hander Style.
' + +'Note: only cetain one-handed weapons can be taken in both hands. Indeed, only certain one-handd weapons will appear in the weapon list shown when the [Both Hands] button is selected on the Change Weapon menu. This is achieved in the weapon specifications in the Weapons Database. For full details, see the explanation given in the Weapons & Armour Database Help handout. In summary, a one-handed weapon which can be wielded two-handed and gain Two-Hander Fighting Style benefits, such as a Battle Axe, requires a second Specs dataset with the \'2H\' attribute, but no additional ToHitData datasets. This informs the APIs that this weapon can be taken in both hands, but will not gain any benefits from doing so (unlike, e.g. a Bastard Sword) unless the Character has proficiency in the Two-Hander Fighting Style.
' + +'Other styles can be defined that are not specified in The Complete Fighter\'s Handbook, and some examples are provided in the distributed database:
' + +'&{template:RPGMdefault}{{name=Bowyer Fighting Style}}Specs=[Bowyer,Style,2H,Ranged-style]{{desc=Bowyer Fighting Style reflects fighters who practice day in, day out at the range perfecting their use of bows of all types.}}StyleData=[twohand:ranged, weaps:bow],[rwr:+1/+1/+2/+3,rwsp:-2],[rwr:+1/+1/+3/+5,rwsp:-3,rwn:+1/2]{{desc1=**Advantages**
'
+ +'The main advantage of bowyer technique is that it allows the character to wield two-handed bows which can do damage at long ranges, staying out of melee and making you a difficult enemy to attack.}}{{desc2=**Disadvantages**
'
+ +'As with any two-handed weapon use use, bowyer weapon technique has the drawback that the user cannot wear or use a shield, or gain the shield\'s AC bonus.}}{{desc3=**Style Specialization**
'
+ +'You can, by devoting a weapon proficiency to it, take a Style Specialization with Bowyer Style.
'
+ +'Style Specialization with Bowyer Style enables you to extend your accuracy at range by 10 yards at short range, 20 at medium and 30 at long range, and improve the speed of the bow by 2 segments. As their skill improves further (by dedicating two proficiency slots), range increases further (by 30 yards at medium and 50 at long), and nocking arrows and drawing the bow faster to enable them to get additional attack every other round}}
This style has a slightly more complex data specification:
' + +'StyleData=[twohand:ranged, weaps:bow],[rwr:+1/+1/+2/+3,rwsp:-2],[rwr:+1/+1/+3/+5,rwsp:-3,rwn:+1/2]' + +'
You can see here the application of the restriction of this fighting style to two-handed ranged weapons that belong to the weapon group bow, then applying benefits using the range extension key rwr, the ranged weapon speed modifier rwsp, and improving the number of attacks for a ranged weapon by one attack per two rounds using rwn.
' + +'A wide range of fighting styles can be created using the different combination of rules and benefits, to enrich the game you create, and your players experience. If you need help or guidance, or experience any issues, do access the RPGMaster forum on Roll20 - search the wiki for the link, or navigate via the Community Forums to the API / Mods forum and search for RPGMaster.
' + } + }); + + const fieldGroups = Object.freeze({ + MELEE: {prefix:'MW_', tableDef:fields.MW_table}, + DMG: {prefix:'Dmg_', tableDef:fields.Dmg_table}, + RANGED: {prefix:'RW_', tableDef:fields.RW_table}, + AMMO: {prefix:'Ammo_', tableDef:fields.Ammo_table}, + WPROF: {prefix:'WP_', tableDef:fields.WP_table}, + MI: {prefix:'Items_', tableDef:fields.Items_table}, + MAGIC: {prefix:'Magic_', tableDef:fields.Magic_table}, + SPELLS: {prefix:'Spells_', tableDef:fields.Spells_table}, + POWERS: {prefix:'Powers_', tableDef:fields.Powers_table}, + INHAND: {prefix:'InHand_', tableDef:fields.InHand_table}, + QUIVER: {prefix:'Quiver_', tableDef:fields.Quiver_table}, + STYLES: {prefix:'Style_', tableDef:fields.Style_table}, + GEAR: {prefix:'Gear_', tableDef:fields.Gear_table}, + STORED: {prefix:'StoredGear_', tableDef:fields.StoredGear_table}, + POTIONS:{prefix:'Items_', tableDef:fields.Items_table}, + DUSTS: {prefix:'Dusts_', tableDef:fields.Dusts_table}, + MISC: {prefix:'Misc_', tableDef:fields.Misc_table}, + WANDS: {prefix:'Wands_', tableDef:fields.Wands_table}, + SCROLLS:{prefix:'Scrolls_', tableDef:fields.Scrolls_table}, + GEAR: {prefix:'Gear_', tableDef:fields.Gear_table}, + STORED: {prefix:'Stored_', tableDef:fields.Stored_table}, + INIT: {prefix:'InitMagic_', tableDef:fields.InitMagic_table}, + SAVES: {prefix:'Mods_', tableDef:fields.Mods_table}, + MODS: {prefix:'Mods_', tableDef:fields.Mods_table}, +// WEAP: {prefix:'Weap_', tableDef:fields.Weap_table}, + MONWEAP:{prefix:'MonWeap_', tableDef:fields.MonWeap_table}, + ALTWIZ: {prefix:'AltSpells_', tableDef:fields.AltWizSpells_table}, + ALTPRI: {prefix:'AltSpells_', tableDef:fields.AltPriSpells_table}, + ALTPWR: {prefix:'AltPowers_', tableDef:fields.AltPowers_table}, + }); + const miTypeLists = Object.freeze({ + miscellaneous: {type:'miscellaneous',field:fields.ItemMiscList}, + protectioncloak:{type:'miscellaneous',field:fields.ItemMiscList}, + protectionboots:{type:'miscellaneous',field:fields.ItemMiscList}, + weapon: {type:'weapon',field:fields.ItemWeaponList}, + melee: {type:'weapon',field:fields.ItemWeaponList}, + innatemelee: {type:'weapon',field:fields.ItemWeaponList}, + ranged: {type:'weapon',field:fields.ItemWeaponList}, + innateranged: {type:'weapon',field:fields.ItemWeaponList}, + ammo: {type:'ammo',field:fields.ItemWeaponList}, + armor: {type:'armour',field:fields.ItemArmourList}, + armour: {type:'armour',field:fields.ItemArmourList}, + totalac: {type:'armour',field:fields.ItemArmourList}, + shield: {type:'armour',field:fields.ItemArmourList}, + helm: {type:'armour',field:fields.ItemArmourList}, + barding: {type:'armour',field:fields.ItemArmourList}, + ring: {type:'ring',field:fields.ItemRingList}, + protectionring: {type:'ring',field:fields.ItemRingList}, + potion: {type:'potion',field:fields.ItemPotionList}, + scroll: {type:'scroll',field:fields.ItemScrollList}, + scrollcase: {type:'scroll',field:fields.ItemScrollList}, + rod: {type:'rod',field:fields.ItemWandsList}, + staff: {type:'rod',field:fields.ItemWandsList}, + wand: {type:'rod',field:fields.ItemWandsList}, + magic: {type:'rod',field:fields.ItemWandsList}, + dmitem: {type:'dmitem',field:fields.ItemDMList}, + equipment: {type:'equipment',field:fields.ItemEquipList}, + light: {type:'equipment',field:fields.ItemEquipList}, + treasure: {type:'treasure',field:fields.ItemTreasureList}, + attackmacro: {type:'attack',field:fields.ItemAttacksList}, + style: {type:'style',field:fields.ItemWeaponList}, + ability: {type:'ability',field:fields.ItemAbilitiesList}, + trap: {type:'trap',field:fields.ItemTrapsList}, + lock: {type:'lock',field:fields.ItemLocksList}, + }); + var clTypeLists = { + warriorclass: {type:'warrior',field:fields.ClassWarriorList,query:''}, + warriorhrclass: {type:'warrior',field:fields.ClassWarriorList,query:''}, + warriorkitclass:{type:'warrior',field:fields.ClassWarriorList,query:''}, + wizardclass: {type:'wizard',field:fields.ClassWizardList,query:''}, + wizardhrclass: {type:'wizard',field:fields.ClassWizardList,query:''}, + wizardkitclass: {type:'wizard',field:fields.ClassWizardList,query:''}, + priestclass: {type:'priest',field:fields.ClassPriestList,query:''}, + priesthrclass: {type:'priest',field:fields.ClassPriestList,query:''}, + priesthoodclass:{type:'priest',field:fields.ClassPriestList,query:''}, + priestkitclass: {type:'priest',field:fields.ClassPriestList,query:''}, + rogueclass: {type:'rogue',field:fields.ClassRogueList,query:''}, + roguehrclass: {type:'rogue',field:fields.ClassRogueList,query:''}, + roguekitclass: {type:'rogue',field:fields.ClassRogueList,query:''}, + psionclass: {type:'psion',field:fields.ClassPsionList,query:''}, + psionhrclass: {type:'psion',field:fields.ClassPsionList,query:''}, + psionkitclass: {type:'psion',field:fields.ClassPsionList,query:''}, + creatureclass: {type:'creature',field:fields.ClassCreatureList,query:''}, + humanoidrace: {type:'humanoid',field:fields.RaceHumanoidList,query:''}, + humanoidhrrace: {type:'humanoid',field:fields.RaceHumanoidList,query:''}, + humanoidkitrace:{type:'humanoid',field:fields.RaceHumanoidList,query:''}, + humanoidcreature:{type:'creature',field:fields.RaceCreatureList,query:''}, + creaturerace: {type:'creature',field:fields.RaceCreatureList,query:''}, + creaturehrrace: {type:'creature',field:fields.RaceCreatureList,query:''}, + creaturekitrace:{type:'creature',field:fields.RaceCreatureList,query:''}, + npccreature: {type:'npc',field:fields.RaceNPCList,query:''}, + container: {type:'container',field:fields.ContainerList,query:''}, + }; + const spTypeLists = Object.freeze({ + muspelll1: {type:'muspelll1',field:['spellmem','current']}, + muspelll2: {type:'muspelll2',field:['spellmem2','current']}, + muspelll3: {type:'muspelll3',field:['spellmem3','current']}, + muspelll4: {type:'muspelll4',field:['spellmem4','current']}, + muspelll5: {type:'muspelll5',field:['spellmem30','current']}, + muspelll6: {type:'muspelll6',field:['spellmem5','current']}, + muspelll7: {type:'muspelll7',field:['spellmem6','current']}, + muspelll8: {type:'muspelll8',field:['spellmem7','current']}, + muspelll9: {type:'muspelll9',field:['spellmem8','current']}, + muspelll0: {type:'muspelll0',field:['spellmem20','current']}, + prspelll1: {type:'prspelll1',field:['spellmem10','current']}, + prspelll2: {type:'prspelll2',field:['spellmem11','current']}, + prspelll3: {type:'prspelll3',field:['spellmem12','current']}, + prspelll4: {type:'prspelll4',field:['spellmem13','current']}, + prspelll5: {type:'prspelll5',field:['spellmem14','current']}, + prspelll6: {type:'prspelll6',field:['spellmem15','current']}, + prspelll7: {type:'prspelll7',field:['spellmem16','current']}, + prspelll0: {type:'prspelll0',field:['spellmem17','current']}, + power: {type:'power', field:['spellmem23','current']}, + itempower: {type:'itempower',field:['spellmem21','current']}, + itemspell: {type:'itemspell',field:['spellmem22','current']}, + melee: {type:'',field:['']}, + innatemelee: {type:'',field:['']}, + ranged: {type:'',field:['']}, + innateranged: {type:'',field:['']}, + magic: {type:'',field:['']}, + innatemagic: {type:'',field:['']}, + }); + const primeClasses=['Warrior','Wizard','Priest','Rogue','Psion','Creature']; + const classLevels = [ + [fields.Fighter_class,fields.Fighter_level], + [fields.Wizard_class,fields.Wizard_level], + [fields.Priest_class,fields.Priest_level], + [fields.Rogue_class,fields.Rogue_level], + [fields.Psion_class,fields.Psion_level], + [fields.Fighter_class,fields.Monster_hitDice] + ]; + const casterLevels = [ + [fields.Wizard_class,fields.Wizard_level,'MU'], + [fields.Priest_class,fields.Priest_level,'PR'], + [fields.Fighter_class,fields.Fighter_level,'F'], + [fields.Rogue_class,fields.Rogue_level,'RO'], + [fields.Psion_class,fields.Psion_level,'PS'], + [fields.Fighter_class,fields.Monster_hitDice,'M'] + ]; + var classMap = [[fields.ClassMap1,fields.LevelMap1],[fields.ClassMap2,fields.LevelMap2],[fields.ClassMap3,fields.LevelMap3]]; + + const baseThac0table = [ + [20,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1], + [20,20,20,20,19,19,19,18,18,18,17,17,17,16,16,16,15,15,15,14,14], + [20,20,20,20,18,18,18,16,16,16,14,14,14,12,12,12,10,10,10,8,8], + [20,20,20,19,19,18,18,17,17,16,16,15,15,14,14,13,13,12,12,11,11], + [20,20,20,19,19,18,18,17,17,16,16,15,15,14,14,13,13,12,12,11,11], + ]; + + const exstrIndex = [0,50,75,90,99,100]; + const numNames = ['','1st','2nd','3rd','4th','5th','6th','7th','8th','9th']; + + const attrMods = { + str: { + hit: {field:fields.Strength_hit,data:[0,-5,-3,-3,-2,-2,-1,-1,0,0,0,0,0,0,0,0,0,1,1,1,2,2,2,3,3,3,4,4,5,6,7]}, + dmg: {field:fields.Strength_dmg,data:[0,-4,-2,-1,-1,-1,0,0,0,0,0,0,0,0,0,0,1,1,2,3,3,4,5,6,7,8,9,10,11,12,14]}, + weight: {field:fields.MaxWeight,data:[0,1,1,5,10,10,20,20,35,35,40,40,45,45,55,55,70,85,110,135,160,185,235,335,485,535,635,785,935,1235,1535]}, + press: {field:fields.MaxPress,data:[0,3,5,10,25,25,55,55,90,90,115,115,140,140,170,170,195,220,255,280,305,330,380,480,640,700,810,970,1130,1440,1750]}, + opendoor: {field:fields.OpenDoors,data:[[0,1,1,2,3,3,4,4,5,5,6,6,7,7,8,8,9,10,11,12,13,14,15,16,16,17,17,18,18,19,19], + [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,6,8,10,12,14,16,17,18]]}, + bendbars: {field:fields.BendBars,data:[0,0,0,0,0,0,0,0,1,1,2,2,4,4,7,7,10,13,16,20,25,30,35,40,50,60,70,80,90,95,99]}, + }, + dex: { + react: {field:fields.Dex_react,data:[0,-6,-4,-3,-2,-1,0,0,0,0,0,0,0,0,0,0,1,2,2,3,3,4,4,4,5,5]}, + missile: {field:fields.Dex_missile,data: [0,-6,-4,-3,-2,-1,0,0,0,0,0,0,0,0,0,0,1,2,2,3,3,4,4,4,5,5]}, + defadj: {field:fields.Dex_acBonus,data:[0,5,5,4,3,2,1,0,0,0,0,0,0,0,0,-1,-2,-3,-4,-4,-4,-5,-5,-5,-6,-6]}, + }, + con: { + hpadj: {field:fields.HPconAdj,data:[0,-3,-2,-2,-1,-1,-1,0,0,0,0,0,0,0,0,1,2,2,2,2,2,2,2,2,2,2]}, + fighthp: {field:fields.HPconAdj,data:[0,-3,-2,-2,-1,-1,-1,0,0,0,0,0,0,0,0,1,2,3,4,5,5,6,6,6,7,7]}, + syshock: {field:fields.SystemShock,data:[0,25,30,35,40,45,50,55,60,65,70,75,80,85,88,90,95,97,99,99,99,99,99,99,99,100]}, + resurrect: {field:fields.ResSurvive,data:[0,30,35,40,45,50,55,60,65,70,75,80,85,90,92,94,96,98,100,100,100,100,100,100,100,100]}, + poison: {field:fields.ConPoison,data:[0,-2,-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,2,3,3,4]}, + regen: {field:fields.Regenerate,data:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,5,4,3,2,1]}, + }, + int: { + lang: {field:fields.Languages,data:[0,0,1,1,1,1,1,1,1,1,2,2,2,3,3,4,4,5,6,7,8,9,10,11,12,15,20]}, + splev: {field:fields.SpellLevel,data:[0,0,0,0,0,0,0,0,0,4,5,5,6,6,7,7,8,8,9,9,9,9,9,9,9,9]}, + learn: {field:fields.LearnSpell,data:[0,0,0,0,0,0,0,0,0,35,40,45,50,55,60,65,70,75,85,95,96,97,98,99,100,100]}, + perlev: {field:fields.SpellMax,data:[0,0,0,0,0,0,0,0,0,6,7,7,7,9,9,11,11,14,18,99,99,99,99,99,99,99]}, + illusion: {field:fields.IllusionImmune,data:['','','','','','','','','','','','','','','','','','','','1st','2nd','3rd','4th','5th','6th','7th']}, + }, + wis: { + wisdef: {field:fields.Wisdom_defAdj,data:[0,-6,-4,-3,-2,-1,-1,-1,0,0,0,0,0,0,0,1,2,3,4,4,4,4,4,4,4,4]}, + wisbonus: {field:fields.BonusSpells,data:[[0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,2,3,4,3,4,5,5,6,6,7], + [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,3,4,1,5,6]]}, + fail: {field:fields.SpellFail,data:[0,80,60,50,45,40,35,30,25,20,15,10,5,0,0,0,0,0,0,0,0,0,0,0,0,0]}, + immune: {field:fields.SpellImmune,data:['','','','','','','','','','','','','','','','','','','','Cause Fear,Charm Person,Command,Friends,Hypnotism','Forget,Hold Person,Ray of Enfeeblement,Scare,Fear','Charm Monster,Confusion,Emotion,Fumble,Suggestion','Chaos,Feeblemind,Hold Monster,Magic Jar,Quest,Geas,Mass Suggestion,Rod of Rulership','Antipathy/Sympathy,Death Spell,Mass Charm']}, + }, + chr: { + hench: {field:fields.ChrHench,data:[0,0,1,1,1,2,2,3,3,4,4,4,5,5,6,7,8,10,15,20,25,30,35,40,45,50]}, + loyalty: {field:fields.ChrLoyalty,data:[0,-8,-7,-6,-5,-4,-3,-2,-1,0,0,0,0,0,1,3,4,6,8,10,12,14,16,18,20,20]}, + react: {field:fields.ChrReact,data:[0,-7,-6,-5,-4,-3,-2,-1,0,0,0,0,0,1,2,3,5,6,7,8,10,15,20,25,30,35,40,45,50]}, + }, + }; + + var saveFormat = { + Saves: { + Paralysis: {save:fields.Saves_paralysis,mod:fields.Saves_modParalysis,mon:fields.Saves_monParalysis,index:0,roll:'1d20',tag:'par'}, + Poison: {save:fields.Saves_poison,mod:fields.Saves_modPoison,mon:fields.Saves_monPoison,index:0,roll:'1d20',tag:'poi'}, + Death: {save:fields.Saves_death,mod:fields.Saves_modDeath,mon:fields.Saves_monDeath,index:0,roll:'1d20',tag:'dea'}, + Rod: {save:fields.Saves_rod,mod:fields.Saves_modRod,mon:fields.Saves_monRod,index:1,roll:'1d20',tag:'rod'}, + Staff: {save:fields.Saves_staff,mod:fields.Saves_modStaff,mon:fields.Saves_monStaff,index:1,roll:'1d20',tag:'sta'}, + Wand: {save:fields.Saves_wand,mod:fields.Saves_modWand,mon:fields.Saves_monWand,index:1,roll:'1d20',tag:'wan'}, + Petrification: {save:fields.Saves_petrification,mod:fields.Saves_modPetrification,mon:fields.Saves_monPetri,index:2,roll:'1d20',tag:'pet'}, + Polymorph: {save:fields.Saves_polymorph,mod:fields.Saves_modPolymorph,mon:fields.Saves_monPolymorph,index:2,roll:'1d20',tag:'pol'}, + Breath: {save:fields.Saves_breath,mod:fields.Saves_modBreath,mon:fields.Saves_monBreath,index:3,roll:'1d20',tag:'bre'}, + Spell: {save:fields.Saves_spell,mod:fields.Saves_modSpell,mon:fields.Saves_monSpell,index:4,roll:'1d20',tag:'spe'}, + }, + Attributes: { + Strength: {save:fields.Strength,mod:fields.Saves_modStrength,roll:'1d20',tag:'str'}, + Constitution: {save:fields.Constitution,mod:fields.Saves_modConstitution,roll:'1d20',tag:'con'}, + Dexterity: {save:fields.Dexterity,mod:fields.Saves_modDexterity,roll:'1d20',tag:'dex'}, + Intelligence: {save:fields.Intelligence,mod:fields.Saves_modIntelligence,roll:'1d20',tag:'int'}, + Wisdom: {save:fields.Wisdom,mod:fields.Saves_modWisdom,roll:'1d20',tag:'wis'}, + Charisma: {save:fields.Charisma,mod:fields.Saves_modCharisma,roll:'1d20',tag:'chr'}, + }, + Checks: { + Open_Doors: {save:fields.OpenDoors,mod:fields.Saves_modOpenDoors,roll:'1d20',tag:'opd'}, + Bend_Bars: {save:fields.BendBars,mod:fields.Saves_modBendBars,roll:'1d100',tag:'bbr'}, + System_Shock: {save:fields.SystemShock,mod:fields.Saves_modSystemShock,roll:'1d100',tag:'sys'}, + Resurrection: {save:fields.ResSurvive,mod:fields.Saves_modResSurvive,roll:'1d100',tag:'res'}, + Learn_Spell: {save:fields.LearnSpell,mod:fields.Saves_modLearnSpell,roll:'1d100',tag:'lsp'}, + Spell_Failure: {save:fields.SpellFail,mod:fields.Saves_modSpellFail,roll:'1d100',tag:'spf'}, + }, + }; + + const rogueSkills = { + pickpockets: {name:'Pick_Pockets',save:['ppt','current'],roll:'{ {1d101},{100} }kl1',tag:'pp',factors:['ppb','ppr','ppd','ppk','ppa','ppm','ppl'],gmrolls:true,success:'You got the item!',failure:'You can try again, unless you are caught!'}, + openlocks: {name:'Open_Locks',save:['olt','current'],roll:'1d100',tag:'ol',factors:['olb','olr','old','olk','ola','olm','oll'],gmrolls:false,success:'Click! After [[1d10]] rounds the lock opens',failure:'Hard luck. [[1d10]] rounds wasted. You can try once per experience level'}, + findtraps: {name:'Find_Traps',save:['rtt','current'],roll:'{ {1d101},{100} }kl1',tag:'rt',factors:['rtb','rtr','rtd','rtk','rta','rtm','rtl'],gmrolls:true,success:'After [[1d10]] rounds you know the general nature of any trap but not exact detail',failure:'[[1d10]] rounds go by. Try again at next level'}, + removetraps: {name:'Remove_Traps',save:['rtt','current'],roll:'{ {1d101},{100} }kl1',tag:'rt',factors:['rtb','rtr','rtd','rtk','rta','rtm','rtl'],gmrolls:true,success:'After [[1d10]] rounds you have successfully removed the trap',failure:'[[1d10]] rounds go by unsuccessfully. 96-00 triggers trap! Try again at next level'}, + movesilently: {name:'Move_Silently',save:['mst','current'],roll:'{ {1d101},{100} }kl1',tag:'ms',factors:['msb','msr','msd','msk','msa','msm','msl'],gmrolls:true,success:'Move at 1/3 rate. Gain -2 to surprise only if also unseen',failure:'Movement still reduced to 1/3'}, + hideinshadows: {name:'Hide_in_Shadows',save:['hst','current'],roll:'{ {1d101},{100} }kl1',tag:'hs',factors:['hsb','hsr','hsd','hsk','hsa','hsm','hsl'],gmrolls:true,success:'Does not work in darkness. Hidden only while motionless except small movements (draw weapon, drin potion etc). Cannot be seen with infravision except in darkness. "See Invisible" will see character',failure:'The character is not hidden'}, + detectnoise: {name:'Detect_Noise',save:['dnt','current'],roll:'{ {1d101},{100} }kl1',tag:'dn',factors:['dnb','dnr','dnd','dnk','dna','dnm','dnl'],gmrolls:true,success:'In silent surrounds & not wearing head-gear, sounds are heard',failure:'Even in silent surrounds & not wearing head-gear, nothing is heard'}, + climbwalls: {name:'Climb_Walls',save:['cwt','current'],roll:'1d100',tag:'cw',factors:['cwb','cwr','cwd','cwk','cwa','cwm','cwl'],gmrolls:false,success:'Can climb up to 100ft in 10 rounds, then roll again',failure:'Can\'t start or is stuck where currently is. Try again somewhere significantly different'}, + readlanguages: {name:'Read_Languages',save:['rlt','current'],roll:'1d100',tag:'rl',factors:['rlb','rlr','rld','rlk','rla','rlm','rll'],gmrolls:false,success:'Can understand about value2% of the meaning',failure:'Not understandable at all. Try again at next level'}, + legendlore: {name:'Legend_Lore',save:['ibt','current'],roll:'1d100',tag:'ib',factors:['ibb','ibr','ibd','ibk','iba','ibm','ibl'],gmrolls:false,success:'In [[1d10]] rounds of examination you learn some general information',failure:'[[1d10]] rounds of examination reveal nothing'}, + }; + const thiefSkillFactors = ['Base','Race','Dexterity','Kit','Armour','Magic','Level']; + + const rogueDexMods = [{lv:9,pp:-15,ol:-10,rt:-10,ms:-20,hs:-10,dn:0,cw:0,rl:0,ll:0}, + {lv:10,pp:-10,ol:-5,rt:-10,ms:-15,hs:-5,dn:0,cw:0,rl:0,ll:0}, + {lv:11,pp:-5,ol:0,rt:-5,ms:-10,hs:0,dn:0,cw:0,rl:0,ll:0}, + {lv:12,pp:0,ol:0,rt:0,ms:-5,hs:0,dn:0,cw:0,rl:0,ll:0}, + {lv:13,pp:0,ol:0,rt:0,ms:0,hs:0,dn:0,cw:0,rl:0,ll:0}, + {lv:14,pp:0,ol:0,rt:0,ms:0,hs:0,dn:0,cw:0,rl:0,ll:0}, + {lv:15,pp:0,ol:0,rt:0,ms:0,hs:0,dn:0,cw:0,rl:0,ll:0}, + {lv:16,pp:0,ol:5,rt:0,ms:0,hs:0,dn:0,cw:0,rl:0,ll:0}, + {lv:17,pp:5,ol:10,rt:0,ms:5,hs:5,dn:0,cw:0,rl:0,ll:0}, + {lv:18,pp:10,ol:15,rt:5,ms:10,hs:10,dn:0,cw:0,rl:0,ll:0}, + {lv:19,pp:15,ol:20,rt:10,ms:15,hs:15,dn:0,cw:0,rl:0,ll:0}, + ]; + + var ordMU =['wizard', + 'magicuser', + 'mage', + 'mu']; + + var specMU = ['abjurer', + 'conjurer', + 'diviner', + 'enchanter', + 'illusionist', + 'invoker', + 'necromancer', + 'transmuter']; + + const wisdomSpells = [ + [0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,2,2,2,2,3,3,3,3,4,4], + [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,2,2,2,3,3,3,3,3], + [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,2,2,3,3,3,3], + [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,2,3,3,3], + [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,2,3], + [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2] + ]; + const spellLevels = Object.freeze({ + mu: [{ spells: 0, base: 0, book: 0 }, + { spells: 0, base: 1, book: '' }, + { spells: 0, base: 4, book: 2 }, + { spells: 0, base: 7, book: 3 }, + { spells: 0, base: 10, book: 4 }, + { spells: 0, base: 70, book: 30}, + { spells: 0, base: 13, book: 5 }, + { spells: 0, base: 16, book: 6 }, + { spells: 0, base: 19, book: 7 }, + { spells: 0, base: 22, book: 8 }], + pr: [{ spells: 0, base: 0, book: 0 }, + { spells: 0, base: 28, book: 10}, + { spells: 0, base: 31, book: 11}, + { spells: 0, base: 34, book: 12}, + { spells: 0, base: 37, book: 13}, + { spells: 0, base: 40, book: 14}, + { spells: 0, base: 43, book: 15}, + { spells: 0, base: 46, book: 16}], + pw: [{ spells: 0, base: 0, book: 0 }, + { spells: 1, base: 67, book: 23}], + mi: [{ spells: 0, base: 0, book: 0 }, + { spells: 0, base: 64, book: 22}], + pm: [{ spells: 0, base: 0, book: 0 }, + { spells: 0, base: 61, book: 21}], + }); + + var spellsPerLevel = { + wizard: {MU:[[9,1,100,'MU'], + [0,1,2,2,3,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5], + [0,0,0,1,2,2,2,3,3,3,4,4,4,5,5,5,5,5,5,5,5], + [0,0,0,0,0,1,2,2,3,3,3,4,4,5,5,5,5,5,5,5,5], + [0,0,0,0,0,0,0,1,2,2,2,3,4,4,4,5,5,5,5,5,5], + [0,0,0,0,0,0,0,0,0,1,2,3,4,4,4,5,5,5,5,5,5], + [0,0,0,0,0,0,0,0,0,0,0,0,1,2,2,2,3,3,3,3,4], + [0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,3,3,3,3], + [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,2,3,3], + [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2], + ]}, + priest: {PR:[[7,1,100,'PR'], + [0,1,2,2,3,3,3,3,3,4,4,5,6,6,6,6,7,7,8,9,9], + [0,0,0,1,2,3,3,3,3,4,4,4,5,6,6,6,7,7,8,9,9], + [0,0,0,0,0,1,2,2,3,3,3,4,5,6,6,6,7,7,8,8,9], + [0,0,0,0,0,0,0,1,2,2,3,3,3,4,5,6,6,7,8,8,8], + [0,0,0,0,0,0,0,0,0,1,2,2,2,2,3,4,4,5,6,6,7], + [0,0,0,0,0,0,0,0,0,0,0,1,2,2,2,2,3,3,4,4,5], + [0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,2,2,2,2], + ]}, + ranger: {PR:[[3,8,9,'PR'], + [0,0,0,0,0,0,0,0,1,2,2,2,2,3,3,3,3], + [0,0,0,0,0,0,0,0,0,0,1,2,2,2,2,3,3], + [0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,2,3], + ]}, + paladin:{PR:[[4,9,9,'PR'], + [0,0,0,0,0,0,0,0,0,1,2,2,2,2,3,3,3,3,3,3,3], + [0,0,0,0,0,0,0,0,0,0,0,1,2,2,2,2,3,3,3,3,3], + [0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,2,3,3,3,3], + [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,2,3], + ]}, + bard: {MU:[[6,1,100,'MU'], + [0,0,1,2,2,3,3,3,3,3,3,3,3,3,3,3,4,4,4,4,4], + [0,0,0,0,1,1,2,2,3,3,3,3,3,3,3,3,3,4,4,4,4], + [0,0,0,0,0,0,0,1,1,2,2,3,3,3,3,3,3,3,4,4,4], + [0,0,0,0,0,0,0,0,0,0,1,1,2,2,3,3,3,3,3,4,4], + [0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,2,3,3,3,4], + [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,2,3], + ]}, + other: {MU:[[0,0,0,'']], + PR:[[0,0,0,'']]}, + }; + var defaultNonProfPenalty = [ + [fields.Fighter_class,fields.Fighter_level,-2], + [fields.Wizard_class,fields.Wizard_level,-5], + [fields.Priest_class,fields.Priest_level,-3], + [fields.Rogue_class,fields.Rogue_level,-3], + [fields.Psion_class,fields.Psion_level,-4], + [fields.Monster_class,fields.Monster_level,-2], + ]; + var rangedWeapMods = { + N : -5, + PB : 2, + S : 0, + M : -2, + L : -5, + F : -20, + }; + var saveLevels = { + warrior: [0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9], + wizard: [0,1,1,1,1,1,2,2,2,2,2,3,3,3,3,3,4,4,4,4,4,5], + priest: [0,1,1,1,2,2,2,3,3,3,4,4,4,5,5,5,6,6,6,7], + rogue: [0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,6], + psion: [0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,6], + creature: [0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9], + }; + var baseSaves = { + warrior: [[16,18,17,20,19],[14,16,15,17,17],[13,15,14,16,16],[11,13,12,13,14],[10,12,11,12,13],[8,10,9,9,11],[7,9,8,8,10],[5,7,6,5,8],[4,6,5,4,7],[3,5,4,4,6]], + wizard: [[16,18,17,20,19],[14,11,13,15,12],[13,9,11,13,10],[11,7,9,11,8],[10,5,7,9,6],[8,3,5,7,4]], + priest: [[16,18,17,20,19],[10,14,13,16,15],[9,13,12,15,14],[7,11,10,13,12],[6,10,9,12,11],[5,9,8,11,10],[4,8,7,10,9],[2,6,5,8,7]], + rogue: [[16,18,17,20,19],[13,14,12,16,15],[12,12,11,15,13],[11,10,10,14,11],[10,8,9,13,9],[9,6,8,12,7],[8,4,7,11,5]], + psion: [[16,18,17,20,19],[13,15,10,16,15],[12,13,9,15,14],[11,11,8,13,12],[10,9,7,12,7],[9,7,6,11,9],[8,5,5,9,7]], + creature: [[16,18,17,20,19],[14,16,15,17,17],[13,15,14,16,16],[11,13,12,13,14],[10,12,11,12,13],[8,10,9,9,11],[7,9,8,8,10],[5,7,6,5,8],[4,6,5,4,7],[3,5,4,4,6]], + }; + var classSaveMods = { + undefined: {att:'con',par:0,poi:0,dea:0,rod:0,sta:0,wan:0,pet:0,pol:0,bre:0,spe:0,str:0,con:0,dex:0,int:0,wis:0,chr:0}, + paladin: {att:'con',par:2,poi:2,dea:2,rod:2,sta:2,wan:2,pet:2,pol:2,bre:2,spe:2,str:0,con:0,dex:0,int:0,wis:0,chr:0}, + }; + var raceSaveMods = { + undefined: {att:'con',par:0,poi:0,dea:0,rod:0,sta:0,wan:0,pet:0,pol:0,bre:0,spe:0,str:0,con:0,dex:0,int:0,wis:0,chr:0}, + dwarf: {att:'con',par:0,poi:3.5,dea:0,rod:3.5,sta:3.5,wan:3.5,pet:0,pol:0,bre:0,spe:3.5,str:0,con:0,dex:0,int:0,wis:0,chr:0}, + elf: {att:'con',par:0,poi:0,dea:0,rod:0,sta:0,wan:0,pet:0,pol:0,bre:0,spe:0,str:0,con:0,dex:0,int:0,wis:0,chr:0}, + gnome: {att:'con',par:0,poi:0,dea:0,rod:3.5,sta:3.5,wan:3.5,pet:0,pol:0,bre:0,spe:3.5,str:0,con:0,dex:0,int:0,wis:0,chr:0}, + halfelf: {att:'con',par:0,poi:0,dea:0,rod:0,sta:0,wan:0,pet:0,pol:0,bre:0,spe:0,str:0,con:0,dex:0,int:0,wis:0,chr:0}, + halfling: {att:'con',par:0,poi:3.5,dea:0,rod:3.5,sta:3.5,wan:3.5,pet:0,pol:0,bre:0,spe:3.5,str:0,con:0,dex:0,int:0,wis:0,chr:0}, + halforc: {att:'con',par:0,poi:0,dea:0,rod:0,sta:0,wan:0,pet:0,pol:0,bre:0,spe:0,str:0,con:0,dex:0,int:0,wis:0,chr:0}, + human: {att:'con',par:0,poi:0,dea:0,rod:0,sta:0,wan:0,pet:0,pol:0,bre:0,spe:0,str:0,con:0,dex:0,int:0,wis:0,chr:0}, + creature: {att:'con',par:0,poi:0,dea:0,rod:0,sta:0,wan:0,pet:0,pol:0,bre:0,spe:0,str:0,con:0,dex:0,int:0,wis:0,chr:0}, + }; + const xlateSave = {att:'Attribute',par:'Paralysis',poi:'Poison',dea:'Death',rod:'Rod',sta:'Staff',wan:'Wand',pet:'Petrify',pol:'Polymorph',bre:'Breath',spe:'Spell',str:'Strength',con:'Constitution',dex:'Dexterity',int:'Intelligence',wis:'Wisdom',chr:'Charisma',pp:'Pick Pockets',ol:'Open Locks',rt:'Find/Remove Traps',ms:'Move Silently',hs:'Hide in Shadows',dn:'Detect Noise',cw:'Climb Walls',rl:'Read Languages',ib:'Legend Lore'}; + var classNonProfPenalty = {}; + var raceToHitMods = { + elf: [['bow',1],['longsword',1],['shortsword',1]], + halfling: [['sling',1],['thrownblade',1]], + }; + var classAllowedWeaps = { + warrior: ['any'], + fighter: ['any'], + ranger: ['any'], + paladin: ['any'], + beastmaster: ['any'], + barbarian: ['any'], + defender: ['axe','clubs','flails','longblade','fencingblade','mediumblade','shortblade','polearm'], + wizard: ['dagger','staff','dart','knife','sling'], + mage: ['dagger','staff','dart','knife','sling'], + mu: ['dagger','staff','dart','knife','sling'], + abjurer: ['dagger','staff','dart','knife','sling'], + conjurer: ['dagger','staff','dart','knife','sling'], + diviner: ['dagger','staff','dart','knife','sling'], + enchanter: ['dagger','staff','dart','knife','sling'], + illusionist: ['dagger','staff','dart','knife','sling'], + invoker: ['dagger','staff','dart','knife','sling'], + necromancer: ['dagger','staff','dart','knife','sling'], + transmuter: ['dagger','staff','dart','knife','sling'], + priest: ['clubs','hammer','staff'], + cleric: ['clubs','hammer','staff'], + druid: ['club','sickle','dart','spear','dagger','scimitar','sling','staff'], + healer: ['club','quarterstaff','mancatcher','sling'], + priestofagriculture: ['hooks','flails','handaxe','throwingaxe','scythe','sickle'], + priestofancestors: ['club','dagger','dirk','dart','knife','staff'], + priestofanimals: ['hooks','cestus','clubs','maingauche','greatblade','longblade','mediumblade','shortblade','fencingblade','warhammer'], + priestofarts: ['bow'], + priestoflife: ['club','quarterstaff','mancatcher','sling'], + priestofwar: ['any'], + priestoflight: ['dart','javelin','spears'], + priestofknowledge: ['sling','quarterstaff'], + shaman: ['longblade','mediumblade','shortblade','blowgun','club','staff','shortbow','horsebow','handcrossbow'], + rogue: ['club','shortblade','dart','handcrossbow','lasso','shortbow','sling','broadsword','longsword','staff'], + thief: ['club','shortblade','dart','handcrossbow','lasso','shortbow','sling','broadsword','longsword','staff'], + bard: ['any'], + assassin: ['any'], + psion: ['shortbow','handcrossbow','lightcrossbow','shortblade','clubs','axe','horsemanspick','scimitar','spears','warhammer'], + }; + var classAllowedArmour = { + warrior: ['any'], + fighter: ['any'], + ranger: ['any'], + paladin: ['any'], + beastmaster: ['any'], + barbarian: ['padded','leather','hide','brigandine','ringmail','scalemail','chainmail','shield','ring','magicitem','cloak'], + defender: ['any'], + wizard: ['magicitem','ring','cloak'], + mage: ['magicitem','ring','cloak'], + mu: ['magicitem','ring','cloak'], + abjurer: ['magicitem','ring','cloak'], + conjurer: ['magicitem','ring','cloak'], + diviner: ['magicitem','ring','cloak'], + enchanter: ['magicitem','ring','cloak'], + illusionist: ['magicitem','ring','cloak'], + invoker: ['magicitem','ring','cloak'], + necromancer: ['magicitem','ring','cloak'], + transmuter: ['magicitem','ring','cloak'], + priest: ['any'], + cleric: ['any'], + druid: ['leather','padded','hide','woodenshield','magicitem','ring','cloak'], + healer: ['any'], + priestofagriculture: ['leather','padded','hide','woodenshield','magicitem','ring','cloak'], + priestofancestors: ['magicitem','ring','cloak'], + priestofanimals: ['leather','padded','hide','magicitem','ring','cloak'], + priestofarts: ['magicitem','ring','cloak'], + priestofbirth: ['magicitem','ring','cloak'], + priestofchildren: ['magicitem','ring','cloak'], + priestofcommunity: ['any'], + priestofcompetition: ['any'], + priestofcrafts: ['leather','padded','hide','shields','magicitem','ring','cloak'], + priestofculture: ['any','-shields'], + priestofdarkness: ['leather','padded','hide','magicitem','ring','cloak'], + priestofnight: ['leather','padded','hide','magicitem','ring','cloak'], + priestoflife: ['any'], + priestofwar: ['any'], + priestoflight: ['studdedleather','ringmail','chainmail','shield','ring','magicitem','cloak'], + priestofknowledge: ['magicitem','ring','cloak'], + shaman: ['padded','leather','hide','brigandine','ringmail','scalemail','chainmail','splintmail','bandedmail','shield','ring','magicitem','cloak'], + rogue: ['padded','leather','studdedleather','elvenchain','shield','ring','magicitem','cloak'], + thief: ['padded','leather','studdedleather','elvenchain','shield','ring','magicitem','cloak'], + bard: ['padded','leather','hide','brigandine','ringmail','scalemail','chainmail','ring','magicitem','cloak'], + assassin: ['any'], + psion: ['leather','studdedleather','hide','smallshield','ring','magicitem','cloak'], + }; + var weapMultiAttks = { + fighter: { + Levels: ['0','7','13'], + Proficient: { melee: ['0','1/2','1'], + ranged: ['0','0','0'], + }, + }, + All: { + Specialist: { melee: ['1/2','1','3/2'], + lightxbow: ['0','1/2','1'], + heavyxbow: ['0','1/2','1'], + throwndagger: ['1','2','3'], + throwndart: ['1','2','3'], + bow: ['0','0','0'], + arquebus: ['1/3','2/3','7/6'], + blowgun: ['1','2','3'], + knife: ['1','2','3'], + sling: ['1','2','3'], + ranged: ['0','1/2','1'], + }, + }, + }; + + const punchWrestle = [ {punch:'Haymaker',dmg:2,ko:25,wrestle:'Bearhug',hold:true}, + {punch:'Wild swing',dmg:0,ko:2,wrestle:'Leg twist',hold:false}, + {punch:'Uppercut',dmg:2,ko:15,wrestle:'Headlock',hold:true}, + {punch:'Hook',dmg:2,ko:12,wrestle:'Gouge',hold:false}, + {punch:'Rabbit punch',dmg:2,ko:5,wrestle:'Arm lock',hold:true}, + {punch:'Glancing blow',dmg:1,ko:3,wrestle:'Kick',hold:false}, + {punch:'Jab',dmg:2,ko:8,wrestle:'Gouge',hold:false}, + {punch:'Combination',dmg:2,ko:10,wrestle:'Throw',hold:false}, + {punch:'Uppercut',dmg:1,ko:9,wrestle:'Headlock',hold:true}, + {punch:'Combination',dmg:1,ko:10,wrestle:'Leg lock',hold:true}, + {punch:'Glancing blow',dmg:1,ko:3,wrestle:'Elbow smash',hold:false}, + {punch:'Hook',dmg:2,ko:10,wrestle:'Gouge',hold:false}, + {punch:'Kidney punch',dmg:1,ko:5,wrestle:'Throw',hold:false}, + {punch:'Hook',dmg:2,ko:9,wrestle:'Leg lock',hold:false}, + {punch:'Uppercut',dmg:1,ko:8,wrestle:'Leg twist',hold:false}, + {punch:'Jab',dmg:2,ko:6,wrestle:'Arm lock',hold:true}, + {punch:'Glancing blow',dmg:1,ko:2,wrestle:'Elbow smash',hold:false}, + {punch:'Kidney punch',dmg:1,ko:5,wrestle:'Trip',hold:false}, + {punch:'Rabbit punch',dmg:1,ko:3,wrestle:'Kick',hold:false}, + {punch:'Wild swing',dmg:0,ko:1,wrestle:'Arm twist',hold:false}, + {punch:'Haymaker',dmg:2,ko:10,wrestle:'Bearhug',hold:true} + ]; + + const reIgnore = /[-_\s\(\)]/g; + const settings_icon = 'https://s3.amazonaws.com/files.d20.io/images/11920672/7a2wOvU1xjO-gK5kq5whgQ/thumb.png?1440940765'; + const defaultImg = 'https://s3.amazonaws.com/files.d20.io/images/2796029/tJUjL-ilXyG-Ohu6T2Ykvg/thumb.png?1390103367'; + const defaultAs = 'RPGMaster'; + const archive = false; + const use3Ddice = false; + const stdDB = ['mu_spells_db','mu_spells_db_l1','mu_spells_db_l2','mu_spells_db_l3','mu_spells_db_l4','mu_spells_db_l5','mu_spells_db_l6','mu_spells_db_l7','mu_spells_db_l8','mu_spells_db_l9','mu_spells_db_custom','pr_spells_db_l1','pr_spells_db_l2','pr_spells_db_l3','pr_spells_db_l4','pr_spells_db_l5','pr_spells_db_l6','pr_spells_db_l7','pr_spells_db_custom','powers_db','mi_db','mi_db_custom','mi_db_ammo','mi_db_armour','mi_db_equipment','mi_db_treasure','mi_db_potions','mi_db_rings','mi_db_scrolls_books','mi_db_wands_staves_rods','mi_db_weapons','attacks_db','class_db','race_db','race_db_creatures_a_e','race_db_creatures_f_j','race_db_creatures_k_o','race_db_creatures_p_t','race_db_creatures_u_z','styles_db','abilities_db']; + const waitMsgDiv = ''; + const endHeaderFrame = ' |
' + cmd.content + '
') : '')
+ + '';
+
+ sendChat(((cmd && cmd.who) ? cmd.who : defaultAs),content,null,{noarchive:false, use3d:false});
+ log('RPGMaster error: '+msg+ (cmd ? (' while processing command '+cmd.content) : ''));
+ };
+ setTimeout(postErrorMsg,500,msg,cmd);
+ };
+
+ /**
+ * Send an error caught by try/catch
+ */
+
+ LibFunctions.sendCatchError = function(apiName,msg,e,cmdStr='') {
+ var postCatchMsg = function(apiName,msg,e,cmdStr) {
+ if (!msg || !msg.content) {msg= {};msg.content = '${cmdStr}
**'+saves[(i=s.index)]+'** | '; + } + a.push(k+'('+(mods[s.tag]>=0?'+':'')+mods[s.tag]+')'); + }); + content += a.join(', ')+' |
**'+saves[index]+'** | '+modName+'('+(mods[tag]>=0?'+':'')+mods[tag]+') |
**'+LibFunctions.attrLookup(charCS,a.save)+'** | '+k+'('+(mods[a.tag]>=0?'+':'')+mods[a.tag]+') |
Menus | '+configButtons(state.MagicMaster.fancy, 'Plain menus', '!magic --config fancy-menus|false', 'Fancy menus', '!magic --config fancy-menus|true')+'|
Player Targeted Attks | '+configButtons(!state.attackMaster.weapRules.dmTarget, 'Not Allowed', '!attk --config dm-target|true', 'Allowed by All', '!attk --config dm-target|false')+'|
Allowed weapons | '+configButtons(state.attackMaster.weapRules.allowAll, 'Restrict Usage', '!attk --config all-weaps|false', 'All Can Use Any', '!attk --config all-weaps|true')+'|
Restrict weapons | '+configButtons(!state.attackMaster.weapRules.classBan, 'Strict Denial', '!attk --config weap-class|true', 'Apply Penalty', '!attk --config weap-class|false')+'|
Weapon Speed | '+configButtons(!state.attackMaster.weapRules.initPlus, 'Plus affects speed', '!attk --config weap-plus|true', 'Magic Plus Ignored', '!attk --config weap-plus|false')+'|
Critical Rolls | '+configButtons(!state.attackMaster.weapRules.criticals, 'Always hit/miss', '!attk --config criticals|true', 'Calculate hit/miss', '!attk --config criticals|false')+'|
Natural Max Min Rolls | '+configButtons(!state.attackMaster.weapRules.naturals, 'Always hit/miss', '!attk --config naturals|true', 'Calculate hit/miss', '!attk --config naturals|false')+'|
Allowed Armour | '+configButtons(state.attackMaster.weapRules.allowArmour, 'Strict Denial', '!attk --config all-armour|false', 'All Can Use Any', '!attk --config all-armour|true')+'|
Non-Prof Penalty | '+configButtons(!state.attackMaster.weapRules.prof, 'Class Penalty', '!attk --config prof|true', 'Character Sheet', '!attk --config prof|false')+'|
Ranged Mastery | '+configButtons(state.attackMaster.weapRules.masterRange, 'Not Allowed', '!attk --config master-range|false', 'Mastery Allowed', '!attk --config master-range|true')+'|
Rogue Skills | '+configButtons(state.attackMaster.thieveCrit, 'No Critical', '!attk --config rogue-crit|false', 'Critical Success', '!attk --config rogue-crit|true')+'|
Rogue Crit Value | '+configButtons(state.attackMaster.thieveCrit>1, 'Critical = 1%', '!attk --config rogue-crit-val|false', 'Critical = 5%', '!attk --config rogue-crit-val|true')+'|
NPC Attributes | '+configButtons(state.attackMaster.attrRoll, 'No Attributes', '!attk --config attr-roll|false', 'Roll Attributes', '!attk --config attr-roll|true')+'|
NPC Attr Range | '+configButtons(state.attackMaster.attrRestrict, 'Full Range', '!attk --config attr-restrict|false', 'Restrict', '!attk --config attr-restrict|true')+'|
Specialist Wizards | '+configButtons(!state.MagicMaster.spellRules.specMU, 'Specified in Rules', '!magic --config specialist-rules|true', 'Allow Any Specialist', '!magic --config specialist-rules|false')+'|
Spells per Level | '+configButtons(!state.MagicMaster.spellRules.strictNum, 'Strict by Rules', '!magic --config spell-num|true', 'Allow to Set Misc', '!magic --config spell-num|false')+'|
Spell Schools | '+configButtons(state.MagicMaster.spellRules.allowAll, 'Strict by Rules', '!magic --config all-spells|false', 'All Can Use Any', '!magic --config all-spells|true')+'|
Powers by Level | '+configButtons(state.MagicMaster.spellRules.allowAnyPower, 'Strict by Rules', '!magic --config all-powers|false', 'All Can Use Any', '!magic --config all-powers|true')+'|
Custom Objects | '+configButtons(!state.MagicMaster.spellRules.denyCustom, 'External / GM Defined', '!magic --config custom-spells|true', 'All Objects Allowed', '!magic --config custom-spells|false')+'|
Auto-Hide Items | '+configButtons(state.MagicMaster.autoHide, 'GM Hide Manually', '!magic --config auto-hide|false', 'Auto-Hide if Possible', '!magic --config auto-hide|true')+'|
Reveal Hidden Items | '+configButtons(state.MagicMaster.reveal, 'Reveal Manually', '!magic --config reveal|false', 'Reveal on Use', '!magic --config reveal|true')+'|
Action Buttons | '+configButtons(state.MagicMaster.viewActions, 'Grey on View', '!magic --config view-action|false', 'Active on View', '!magic --config view-action|true')+'|
Alphabetic Lists | '+configButtons(!state.MagicMaster.alphaLists, 'Alphabetic', '!magic --config alpha-lists|true', 'Not Alphabetic', '!magic --config alpha-lists|false')+'|
Skill-Based Chance | '+configButtons(!state.MagicMaster.gmRolls, 'GM rolls', '!magic --config gm-rolls|true', 'Player rolls', '!magic --config gm-rolls|false')+'|
[Set Default Token Bars](!cmd --button AB_ASK_TOKENBARS|) |
v1.10 14/11/2022Race Database
Change Log:v1.10 14/11/2022 First live release of the Rase Database
v1.11 20/12/2024Race Database
Change Log:v1.11 20/12/2024 Changed {{name=...}} to {{title=...}}
v2.04 14/09/2024Creatures Database
Change Log:v2.04 14/09/2024 Version change to force deletion of any extracted databases used for temporary fixes
v1.02 12/01/2025NPC Database
Change Log:v1.02 12/01/2025 A few corrections & typos fixed
v2.05 26/01/2025Creatures Database
Change Log:v2.05 26/01/2025 Added chance of random items to be added to humanoid Drag & Drop creatures
v2.03 24/10/2023Creatures Database
Change Log:v2.03 24/10/2023 Creatures in support of the Horn of the Tritons
v2.05 26/01/2025Creatures Database
Change Log:v2.05 26/01/2025 Added chance of random items to be added to humanoid Drag & Drop creatures
v2.03 20/12/2023Creatures Database
Change Log:v2.03 20/12/2023 Gave leopards the ability to use barding
v2.04 26/01/2025Creatures Database
Change Log:v2.04 26/01/2025 Added chance of random items to be added to humanoid Drag & Drop creatures
v2.03 20/10/2023Creatures Database
Change Log:v2.03 20/10/2023 Added more creatures for current campaigns
v2.05 27/01/2025Creatures Database
Change Log:v2.05 26/01/2025 Added chance of random items to be added to humanoid Drag & Drop creatures
v2.02 14/10/2023Creatures Database
Change Log:v2.02 14/10/2023 Fixed issue with War Dog & added Leopard & Snow Leopard
v2.03 27/01/2025Creatures Database
Change Log:v2.04 26/01/2025 Added chance of random items to be added to humanoid Drag & Drop creatures
Lock & Trap Macrosv1.03 27/08/2023
Change Log:v1.01 31/08/2023 Initial debugged and tested version
Lock & Trap Macrosv1.02 26/01/2025
Change Log:v1.02 27/01/2025 Moved chest to this DB & added random items to containers
Lock & Trap Macrosv1.10 07/06/2024
Change Log:
v2.08 17/05/2024Character Class Database
Change Log:
v2.09 20/12/2024Character Class Database
Change Log:
v1.01 30/12/2024Character Class Database
Change Log:
Armour and Shieldsv6.10 22/03/2024
Change Log:v6.10 22/03/2024 Updated qty: and rc: fields to make armour non-stackable
Armour and Shieldsv7.01 26/01/2025
Change Log:v7.01 26/01/2025 Updated with multiple changes for v4 RoundMaster APIs
Weapons Databasev6.26 25/01/2024
Change Log:v6.26 25/01/2024 Split growing Weapons Database into standard, special (DMG) and custom databases', + MI_DB_Weapons:{bio:'
Weapons Databasev7.01 26/01/2025
Change Log:v7.01 26/01/2025 Updated with multiple changes for v4 RoundMaster APIs
Weapons Databasev6.27 22/03/2024
Change Log:v6.27 22/03/2024 De-duped type: fields
Weapons Databasev7.01 26/01/2025
Change Log:v7.01 26/01/2025 Updated with multiple changes for v4 RoundMaster APIs
Weapons Databasev6.32 07/05/2024
Change Log:v6.32 07/05/2024 Updated sword effects to use latest features, e.g. the save mods table
Weapons Databasev7.01 26/01/2025
Change Log:
Weapons Databasev6.27 08/02/2024
Change Log:v6.27 08/02/2024 A few tweeks to conform to new standards
Weapons Databasev7.01 26/01/2025
Change Log:v7.01 26/01/2025 Updated with multiple changes for v4 RoundMaster APIs
v6.05 30/01/2024Weapons Database
Change Log:v6.05 30/01/2024 Added more magical ammo options
v7.01 26/01/2025Weapons Database
Change Log:v7.01 26/01/2025 Updated with multiple changes for v4 RoundMaster APIs
Light Sources
Change Log
v7.01 26/01/2025Equipment Database
Change Log:v7.01 26/01/2025 Updated with multiple changes for v4 RoundMaster APIs
Potions, Pills and Oilsv6.05 07/05/2024
Change Logv6.05 07/05/2024 Updated potion effects to use latest features, especially save mod table
Potions, Pills and Oilsv7.01 26/01/2025
Change Logv7.01 26/01/2025 Updated with multiple changes for v4 RoundMaster APIs
Ringsv6.10 07/06/2024
Change Log:
Ringsv7.01 26/01/2025
Change Log:
Scrolls & Spellbooksv6.09 22/05/2024
Change Log:
Scrolls & Spellbooksv7.01 26/01/2025
Change Log:
Wands, Staves & Rodsv6.12 20/05/2024
Change Log:
Wands, Staves & Rodsv7.01 26/01/2025
Change Log:
Miscellaneous Itemsv6.24 04/04/2024
Change Log
Miscellaneous Itemsv7.01 26/01/2025
Change Log
Custom Magic Itemsv6.20 04/04/2024
Change Logv6.20 04/04/2024 Started adding hide#1 sections to long desc= sections to trigger "show more..." buttons
Custom Magic Itemsv7.01 26/01/2025
Change Logv7.01 26/01/2025 Updated with multiple changes for v4 RoundMaster APIs
Magic User Spell Database: Level 1v8.02 07/05/2024
Change Log:v8.02 07/05/2024 Updated spell effects to use latest features, e.g. save mod table
Custom Magic Itemsv1.01 30/12/2024
Change Logv1.01 30/12/2024 Initial creation for testing', + root:'MI-DB', + api:'magic', + type:'mi', + avatar:'https://files.d20.io/images/338373876/hlaDwE1SLh2XNXB8EoVexA/max.jpg?1682104357', + version:1.01, + db:[{name:'Blue-Tomes',type:'treasure',ct:'0',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.defaultTemplate+'}{{name=Blue Leather-Bound Tomes}}Specs=[Blue Tomes,Treasure,1H,Tomes]{{subtitle=Tomes}}MiscData=[w:Blue Tomes,sp:0,rc:uncharged]{{desc=Set of five books, bound in sky-blue leather and trimmed in copper, with contents of a sinister nature. These five tomes have old, fragile pages; the ancient books describe procedures and details for several evil rites and ceremonies. The books make grim and harrowing reading for any character.}}'}, + {name:'Bottled-Anemone',type:'miscellaneous',ct:'0',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Anemone in a Glass Bottle}}{{subtitle=Treasure}}Specs=[Bottled Anemone,Miscellaneous,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Bottled Anemone,sp:0,st:Glass Bottle,gp:0,rc:uncharged]{{Size=Medium}}{{Immunity=None}}{{Saves=None}}{{desc=A glass bottle with an anemone trapped inside}}'}, + {name:'Broach',type:'treasure',ct:'0',charge:'single-uncharged',cost:'0',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Broach}}{{subtitle=Item}}Specs=[Broach,Treasure,1H,Item]{{Speed=[[0]]}}MiscData=[w:Broach,sp:0,st:Broach,rc:single-uncharged]{{Size=Medium}}{{Immunity=None}}{{Saves=None}}{{desc=What appears to be a perfectly normal broach, made out of some shiny metal with a pretty design stamped on the front - quite well made...}}'}, + {name:'Conch-Shell',type:'treasure',ct:'0',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Conch}}{{subtitle=Item}}Specs=[Shell,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Conch,sp:0,rc:uncharged]{{Size=Medium}}{{Immunity=None}}{{Saves=None}}{{desc=What appears to be a perfectly normal shell, made out of shell}}'}, + {name:'Copper-buckled-leather-harness',type:'treasure',ct:'0',charge:'uncharged',cost:'1',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Leather Harness with Copper Buckles}}{{subtitle=Treasure}}Specs=[Leather Harness,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Leather Harness,sp:0,st:Leather Harness,gp:1,rc:uncharged]{{Size=Medium}}{{Immunity=None}}{{Saves=None}}{{desc=A leather harness with copper buckles that will soon wear out, perhaps worth as much as 1gp if you can get someone to pay that much.}}'}, + {name:'Coral-Game-Pieces',type:'treasure',ct:'0',charge:'uncharged',cost:'10',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Coral Game Pieces}}{{subtitle=Treasure}}Specs=[Coral Game Pieces,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Coral Game Pieces,sp:0,gp:10,rc:uncharged]{{Size=Tiny}}{{Immunity=None}}{{Saves=None}}{{desc=A set of 20 coral game tokens for some game played by the Sahuagin officers. Perhaps you can interrogate one to demand the rules of the game?}}'}, + {name:'Coral-Shark-Statuette',type:'treasure',ct:'0',charge:'uncharged',cost:'20',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Coral Shark Statuette}}{{subtitle=Treasure}}Specs=[Coral Shark Statuette,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Coral Shark Statuette,sp:0,gp:20,rc:uncharged]{{Size=Small}}{{Immunity=None}}{{Saves=None}}{{desc=A small statuette of a shark made out of coral, and of reasonably fine quality. Perhaps someone would pay up to 20gp for such an item?}}'}, + {name:'Gold+Coral-Necklace',type:'treasure',ct:'0',charge:'uncharged',cost:'175',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Gold and Coral Necklace}}Specs=[Gold Necklace,Treasure,0H,Treasure]{{desc=A gold necklace set with coral beads, worth about 175gp (or whatever someone will give you for it)}}MiscData=[w:Gold+Coral Necklace,gp:175,wt:0.1,rc:uncharged]{{}}'}, + {name:'Gold+Coral-Ring',type:'treasure',ct:'0',charge:'uncharged',cost:'50',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Gold Ring set with Coral}}{{subtitle=Treasure}}Specs=[Ring,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Ring,sp:0,st:Ring,gp:50,rc:uncharged]{{Size=Small}}{{Immunity=None}}{{Saves=None}}{{desc=What appears to be a gold ring set with coral, perhaps worth in the region of 50gp if you can get someone to pay that much.}}'}, + {name:'Gold-Buckle',type:'treasure',ct:'0',charge:'uncharged',cost:'10',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Gold Buckle}}Specs=[Gold,Treasure,0H,Treasure]{{desc=A buckle for a harness or belt, made of gold, and worth about 10gp (or whatever someone will give you for it)}}MiscData=[w:Gold Buckle,gp:10,wt:0.1,rc:uncharged]{{}}'}, + {name:'Gold-Drop-Earring',type:'treasure',ct:'0',charge:'uncharged',cost:'30',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Gold Drop Earring}}{{subtitle=Treasure}}Specs=[Earring,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Earring,sp:0,st:Earring,gp:30,rc:uncharged]{{Size=Small}}{{Immunity=None}}{{Saves=None}}{{desc=What appears to be a gold drop earring, not particularly fancy but reasonably well made. Might be worth something, but would be best i you have a pair.}}'}, + {name:'Gold-and-Diamond-Wristband',type:'treasure',ct:'0',charge:'uncharged',cost:'250',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Gold \\amp Diamond Wristband}}{{subtitle=Treasure}}Specs=[Wristband,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Wristband,sp:0,st:Wristband,gp:250,rc:uncharged]{{Size=Small}}{{Immunity=None}}{{Saves=None}}{{desc=A rather flashy gold \\amp diamond wristband, to be worn on the most formal of occasions. Might be worth as much as 250gp}}'}, + {name:'Gold-and-Pearl-Band',type:'treasure',ct:'0',charge:'uncharged',cost:'200',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Gold \\amp Pearl Band}}Specs=[Gold and Pearl Band,Treasure,0H,Bracelet]{{desc=A gold bracelet set with pearls, perhaps worth about 200gp (or whatever someone will give you for it)}}MiscData=[w:Gold Bracelet,gp:200,wt:0.3,rc:uncharged]{{}}'}, + {name:'Gold-locket',type:'treasure',ct:'0',charge:'uncharged',cost:'50',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Small Gold Locket}}Specs=[Gold,Treasure,0H,Treasure]{{Looks Like=A small locket, made of gold and on a fine gold chain}}MiscData=[w:Gold Locket,gp:50,wt:0.1,rc:uncharged]{{desc=Perhaps worth as much as 50gp. When opened, it is seen to contain a miniature portrait of a human girl and a lock of blonde hair, which floats away into the surrounding water}}'}, + {name:'Golden-Gong+Striker',type:'treasure',ct:'0',charge:'uncharged',cost:'75',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Golden Gong}}Specs=[Gong,Treasure,0H,Treasure]{{desc=A mall golden gong with a gold striker, worth about 75gp (or whatever someone will give you for it)}}MiscData=[w:Gong,gp:75,wt:0.2,rc:uncharged]{{}}'}, + {name:'Good-Grog-Guide',type:'treasure',ct:'0',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.itemTemplate+'}{{name=The Good Grog Guide}}Specs=[Tome,Treasure,1H,Treasure]{{desc=A very thick book, well thumbed and with some stains and water(?) marks, this guide is an invaluable reference for the discerning quoffer of any form of grog, beer, spirit, meade, cider, wine, etc in the kingdoms of Keoland, Silverdon, and surrounding kingdoms. It has expert reviews of the standard of tipple served in establishments, views on the establishment and its proprietors, the types of clientelle that might be found there (e.g. which to avoid or frequent, depending on your needs), prices for grog and other beverages (and also for accommodation and food - should these be of any interest), and whether the local law enforcement are strict or relaxed about "trade" in these places.\nWhat is interesting is that *it never seems to be out of date!* It seems to continually update itself with new reviews and information... How it does this is a mystery.}}'}, + {name:'Hazy-Mirror',type:'treasure',ct:'0',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Hazy Mirror}}{{subtitle=Treasure}}Specs=[Mirror,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Mirror,sp:0,st:Mirror,gp:0,rc:uncharged]{{Size=Small}}{{Immunity=None}}{{Saves=None}}{{Looks Like=A mirror that only gives a poor, hazy reflection. Looks very ordinary.}}{{desc=What appears to be a perfectly normal but poor quality mirror. You might get a few coppers for it}}'}, + {name:'Holy-Symbol-of-Erythnul',type:'miscellaneous',ct:'0',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Holy Symbol of Erythnul}}Specs=[Holy Symbol,Miscellaneous,1H,Treasure]{{desc=A Holy Symbol, fashioned in the shape of a human skull pierced through by a sword, and made of some black stone. It might be worth about 5gp, though selling it might bring suspicion on the party. Might it be more useful when used to gain favour with priests of that god of slaughter if you come across them?}}'}, + {name:'Holy-Symbol-of-Procan',type:'miscellaneous',ct:'0',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Holy Symbol of Procan}}Specs=[Holy Symbol,Miscellaneous,1H,Treasure]{{desc=A Holy Symbol, fashioned in the shape of a minature trident, and made of guilded silver. It is worth about 15gp, but might it be more useful when used to gain favour with priests of that god of the sea?}}'}, + {name:'Ornate-Leather-Harness',type:'treasure',ct:'0',charge:'uncharged',cost:'75',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Ornate Leather Harness}}{{subtitle=Treasure}}Specs=[Leather Harness,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Leather Harness,sp:0,st:Leather Harness,gp:75,rc:uncharged]{{Size=Medium}}{{Immunity=None}}{{Saves=None}}{{desc=A leather harness adorned with small rubies and platinum buckles, perhaps worth in the region of 75gp if you can get someone to pay that much.}}'}, + {name:'Ozymandius-Medallion',type:'miscellaneous',ct:'0',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.defaultTemplate+'}{{name=Ozymandius\' Medallion}}Specs=[Medallion,Miscellaneous,1H,Medallion]{{subtitle=Jewellery}}MiscData=[w:Medallion,sp:0,rc:uncharged]{{desc=Just seems to be a normal medallion, made of gold with a curious pattern enscribed on both sides. The design is not familliar.}}'}, + {name:'Pearl',type:'treasure',ct:'0',charge:'uncharged',cost:'10',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Pearl}}Specs=[Pearl,Treasure,0H,Pearl]{{desc=A seemingly normal pearl, perhaps worth about 10gp (or whatever someone will give you for it)}}MiscData=[w:Pearl,gp:10,wt:0.1,rc:uncharged]{{}}'}, + {name:'Pearl-Necklace',type:'treasure',ct:'0',charge:'uncharged',cost:'500',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Pearl Necklace}}{{subtitle=Treasure}}Specs=[Necklace,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Coronet,sp:0,st:Necklace,gp:500,rc:uncharged]{{Size=Small}}{{Immunity=None}}{{Saves=None}}{{desc=A high-quality string-of-pearls necklace. Worth what someone will pay, but could perhaps fethc 500gp}}'}, + {name:'Platinum-Armband',type:'treasure',ct:'0',charge:'uncharged',cost:'200',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Platinum Armband}}{{subtitle=Treasure}}Specs=[Armband,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Armband,sp:0,st:Armband,gp:200,rc:uncharged]{{Size=Small}}{{Immunity=None}}{{Saves=None}}{{desc=What appears to be a perfectly normal armband (apart from being made of platinum) worth perhaps 200gp or what anyone will pay for it - quite well made...}}'}, + {name:'Platinum-Buckle',type:'treasure',ct:'0',charge:'uncharged',cost:'50',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Platinum Buckle}}Specs=[Platinum,Treasure,0H,Treasure]{{desc=A buckle for a harness or belt, made of platinum, and worth about 50gp (or whatever someone will give you for it)}}MiscData=[w:Platinum Buckle,gp:50,wt:0.1,rc:uncharged]{{}}'}, + {name:'Platinum-and-Pearl-Coronet',type:'treasure',ct:'0',charge:'uncharged',cost:'700',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Platinum and Pearl Coronet}}{{subtitle=Treasure}}Specs=[Coronet,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Coronet,sp:0,st:Coronet,gp:700,rc:uncharged]{{Size=Small}}{{Immunity=None}}{{Saves=None}}{{desc=An elegant coronet made of platinum and studded with pearls. It might be of sea elf design, and could fetch as much as 700gp in the right market}}'}, + {name:'Ruby',type:'treasure',ct:'0',charge:'uncharged',cost:'100',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Ruby}}Specs=[Gem,Treasure,0H,Treasure]{{desc=A well-cut ruby, worth about 100gp (or whatever someone will give you for it)}}MiscData=[w:Ruby,gp:100,wt:0.1,rc:uncharged]{{}}'}, + {name:'Sailor-Doll',type:'treasure',ct:'0',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Sailor Doll}}Specs=[Sailor Doll,Treasure,1H,Abjuration]{{subtitle=Magical Item}}MiscData=[w:Sailor Doll,sp:0,rc:uncharged]{{desc=A porcelain doll costumed to look like a sailor, and looking somewhat creepy. The doll\'s eyes are made of two pieces of jade, which might each be worth 10gp. But perhaps the doll as a whole might be more useful...}}'}, + {name:'Scary-Doll',type:'treasure',ct:'0',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Scary Doll with Jade Eyes}}Specs=[Doll,Treasure,1H,Treasure]{{desc=A doll found in the attic of The Haunted House, east of Saltmarsh. Dolls always seem a bit scary to many people, but this one\'s totally black eyes and something about it\'s demeanour give it a very creepy feeling.\nAs yet, it seems to be inert and "just a doll". But will it remain so?}}'}, + {name:'Scented-Oil',type:'potion',ct:'2+1d6',charge:'charged',cost:'0',body:'\\amp{template:'+fields.potionTemplate+'}{{title=Scented Oil}}{{splevel=Potion}}{{school=Alteration}}Specs=[Scented Oil,Potion,1H,Alteration]{{components=M}}{{time=1+1d6+1}}PotionData=[sp:2+1d6,rc:charged]{{range=User}}{{duration=4+1d4 Days}}{{aoe=[30 yds](!rounds --aoe @{selected|token_id}|circle|yards|0|30|30|light|true)}}{{save=None}}{{effects=Don\'t you smell wonderful! Everyone near you starts to smell a strange odour...}}{{materials=Potion}}'}, + {name:'Shark-Statue',type:'treasure',ct:'0',charge:'uncharged',cost:'0.01',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Shark Statue}}Specs=[Shark Statue,Treasure,0H,Treasure]{{desc=A 1-foot high wooden statue of a shark, worth whatever someone will give you for it}}MiscData=[w:Shark Statue,gp:0.01,wt:0.1,rc:uncharged]{{}}'}, + {name:'Shark-Statuette',type:'treasure',ct:'0',charge:'uncharged',cost:'200',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Shark Statuette}}{{subtitle=Treasure}}Specs=[Statuette,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Statuette,sp:0,gp:200,rc:uncharged]{{Size=Small}}{{Immunity=None}}{{Saves=None}}{{desc=A statuette of a shark made from gold, perhaps worth in the region of 200gp if you can get someone to pay that much.}}'}, + {name:'Shark-Tooth',type:'treasure',ct:'0',charge:'uncharged',cost:'1',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Shark Tooth}}Specs=[Shark Tooth,Treasure,0H,Treasure]{{desc=A shark tooth about 4 inches long, worth whatever someone will give you for it}}MiscData=[w:Shark Tooth,gp:1,wt:0.1,rc:uncharged]{{}}'}, + {name:'Silver-Bowl',type:'treasure',ct:'0',charge:'uncharged',cost:'5',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Silver Bowl}}Specs=[Silver Bowl,Treasure,0H,Treasure]{{desc=A silver bowl, highly polished but with signs of regular use, perhaps worth about 5gp (or whatever someone will give you for it)}}MiscData=[w:Silver Bowl,gp:5,wt:0.2,rc:uncharged]{{}}'}, + {name:'Silver-Bracelet-with-Turquoise-Beads',type:'treasure',ct:'0',charge:'uncharged',cost:'100',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Silver Bracelet}}Specs=[Silver Bracelet,Treasure,0H,Bracelet]{{desc=A silver bracelet set with turquoise beads, perhaps worth about 100gp (or whatever someone will give you for it)}}MiscData=[w:Silver Bracelet,gp:100,wt:0.2,rc:uncharged]{{}}'}, + {name:'Silver-Buckle',type:'treasure',ct:'0',charge:'uncharged',cost:'5',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Silver Buckle}}Specs=[Silver,Treasure,0H,Treasure]{{desc=A buckle for a harness or belt, made of silver, and worth about 5gp (or whatever someone will give you for it)}}MiscData=[w:Silver Buckle,gp:5,wt:0.1,rc:uncharged]{{}}'}, + {name:'Silver-Cup',type:'treasure',ct:'0',charge:'uncharged',cost:'5',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Silver Cup}}Specs=[Silver Cup,Treasure,0H,Treasure]{{desc=A silver cup, highly polished but with signs of regular use, perhaps worth about 5gp (or whatever someone will give you for it)}}MiscData=[w:Silver Cup,gp:5,wt:0.2,rc:uncharged]{{}}'}, + {name:'Silver-Framed-Picture',type:'treasure',ct:'0',charge:'uncharged',cost:'25',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Silver Framed Picture}}{{subtitle=Treasure}}Specs=[Picture Frame,Treasure,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Picture Frame,sp:0,gp:25,rc:uncharged]{{Size=Medium}}{{Immunity=None}}{{Saves=None}}{{desc=A silver frame holding a portrait of a loved one painted in oil. The silver frame is perhaps worth in the region of 25gp if you can get someone to pay that much.}}'}, + {name:'Silver-Goblet',type:'treasure',ct:'0',charge:'uncharged',cost:'50',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Silver Goblet}}Specs=[Silver Goblet,Treasure,0H,Goblet]{{desc=A silver goblet which bears the insignia of Prince Monmurg—a spire rising against a blue ocean sky pressed into the bottom; stylized lightning bolts are engraved on the sides, and the words “Jupiter,” “Maximus,” and “Optimus” are written underneath the bolts. Perhaps worth about 50gp (or whatever someone will give you for it)}}MiscData=[w:Silver Goblet,gp:50,wt:0.3,rc:uncharged]{{}}'}, + {name:'Silver-Shark-Mask',type:'treasure',ct:'0',charge:'uncharged',cost:'50',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Silver Shark Masks}}Specs=[Silver Mask,Treasure,0H,Treasure]{{desc=A silver mask in the shape of a shark\'s head, and worth about 50gp (or whatever someone will give you for it)}}MiscData=[w:Silver MaskBuckle,gp:50,wt:0.3,rc:uncharged]{{}}'}, + {name:'Silver-and-Pearl-Band',type:'treasure',ct:'0',charge:'uncharged',cost:'25',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Silver \\amp Pearl Band}}Specs=[Silver and Pearl Band,Treasure,0H,Bracelet]{{desc=A silver bracelet set with pearls, perhaps worth about 25gp (or whatever someone will give you for it)}}MiscData=[w:Silver Bracelet,gp:25,wt:0.2,rc:uncharged]{{}}'}, + {name:'Silver-signet-ring',type:'ring',ct:'0',charge:'uncharged',cost:'10',body:'\\amp{template:'+fields.itemTemplate+'}{{title=Silver Signet Ring}}{{subtitle=Treasure}}Specs=[Signet Ring,Ring,1H,Treasure]{{Speed=[[0]]}}MiscData=[w:Signet Ring,sp:0,st:Ring,gp:10,rc:uncharged]{{Size=Tiny}}{{Immunity=None}}{{Saves=None}}{{desc=A silver ring bearing the signet of the Prince of Monmurg—a spire rising against a blue ocean sky.}}'}, + {name:'Skull',type:'treasure',ct:'0',charge:'uncharged',cost:'0.01',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Humanoid Skull}}Specs=[Humanoid Skull,Treasure,0H,Skull]{{desc=A normal, man-sized humanoid skull, worth whatever someone will give you for it. There may be gems forced into its eye sockets (if there are, they are listed separately}}MiscData=[w:Skull,gp:0.01,wt:0.3,rc:uncharged]{{}}'}, + {name:'Small-Silver-Mirror',type:'treasure',ct:'0',charge:'uncharged',cost:'25',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Small Silver Mirror}}Specs=[Mirror,Treasure,0H,Mirror]{{desc=A small silver mirror, worth about 25gp (or whatever someone will give you for it)}}MiscData=[w:Small Silver Mirror,gp:25,wt:0.1,rc:uncharged]{{}}'}, + {name:'Uncut-Turquoise',type:'treasure',ct:'0',charge:'uncharged',cost:'10',body:'\\amp{template:'+fields.itemTemplate+'}{{name=Uncut Turquoise Chunk}}Specs=[Turquoise,Treasure,0H,Treasure]{{desc=A chunk of uncut turquoise, perhaps worth about 10gp (or whatever someone will give you for it)}}MiscData=[w:Turquoise,gp:10,wt:0.2,rc:uncharged]{{}}'}, + ]}, + + MU_Spells_DB_L1:{bio:'
Magic User Spell Database: Level 1v8.04 26/01/2025
Change Log:v8.04 26/01/2025 Updated for greyed-out buttons to work properly
Magic User Spell Database: Level 1v8.02 07/05/2024
Change Log:v8.02 07/05/2024 Updated spell effects to use latest features, e.g. save mod table
Magic User Spell Database: Level 2v8.03 26/01/2025
Change Log:v8.03 26/01/2025 Updated for greyed-out buttons to work properly
Magic User Spell Database: Level 1v8.02 07/05/2024
Change Log:v8.02 07/05/2024 Updated spell effects to use latest features, e.g. save mod table
Magic User Spell Database: Level 3v8.04 26/01/2025
Change Log:
Magic User Spell Database: Level 1v8.02 07/05/2024
Change Log:v8.02 07/05/2024 Updated spell effects to use latest features, e.g. save mod table
Magic User Spell Database: Level 4v8.04 26/01/2025
Change Log:v8.04 26/01/2025 Updated for greyed-out buttons to work properly
Magic User Spell Database: Level 1v8.02 07/05/2024
Change Log:v8.02 07/05/2024 Updated spell effects to use latest features, e.g. save mod table
Magic User Spell Database: Level 5v8.04 26/01/2025
Change Log:v8.04 26/01/2025 Updated for greyed-out buttons to work properly
Magic User Spell Database: Level 1v8.02 07/05/2024
Change Log:v8.02 07/05/2024 Updated spell effects to use latest features, e.g. save mod table
Magic User Spell Database: Level 6v8.03 26/01/2025
Change Log:v8.03 26/01/2025 Updated for greyed-out buttons to work properly
Magic User Spell Database: Level 1v8.02 07/05/2024
Change Log:v8.02 07/05/2024 Updated spell effects to use latest features, e.g. save mod table
Magic User Spell Database: Level 7v8.03 26/01/2025
Change Log:v8.03 26/01/2025 Updated for greyed-out buttons to work properly
Magic User Spell Database: Level 1v8.02 07/05/2024
Change Log:v8.02 07/05/2024 Updated spell effects to use latest features, e.g. save mod table
Magic User Spell Database: Level 8v8.04 26/01/2025
Change Log:v8.04 26/01/2025 Updated for greyed-out buttons to work properly
Magic User Spell Database: Level 1v8.02 07/05/2024
Change Log:v8.02 07/05/2024 Updated spell effects to use latest features, e.g. save mod table
Magic User Spell Database: Level 9v8.03 26/01/2025
Change Log:v8.03 26/01/2025 Updated for greyed-out buttons to work properly
Magic User Spell Databasev8.01 09/04/2024
Change Log:v8.01 09/04/2024 Split the database by level. See MU-Spells-DB-L# where # is 1 to 9
Magic User Spell Databasev8.03 26/01/2025
Change Log:v8.03 26/01/2025 Updated for greyed-out buttons to work properly
Priest Spell Databasev8.02 07/05/2024
Priest Spell Databasev8.03 26/01/2025
Priest Spell Databasev8.02 07/05/2024
Priest Spell Databasev8.03 26/01/2025
Priest Spell Databasev8.02 07/05/2024
Priest Spell Databasev8.04 26/01/2025
Priest Spell Databasev8.02 07/05/2024
Priest Spell Databasev8.03 26/01/2025
Priest Spell Databasev8.02 07/05/2024
Priest Spell Databasev8.03 26/01/2025
Priest Spell Databasev8.02 07/05/2024
Priest Spell Databasev8.04 26/01/2025
Priest Spell Databasev8.02 07/05/2024
Priest Spell Databasev8.03 26/01/2025
Powers Databasev7.04 31/03/2024
Change Log:v7.04 31/03/2024 Added final few powers for last magic items from DMG
Powers Databasev7.07 26/01/2025
Change Log:
The RPGMaster APIs use a number of databases to hold Macros defining character classes, spells, powers and magic items and their effects. Previous versions of the RPGMaster series of APIs held their databases all externally as character sheets: from this version onwards this is not the case for databases supplied with the APIs, which are now held internally to the APIs. However, the AttackMaster or MagicMaster API command --extract-db can be used to extract any or all standard databases to Character Sheets for examination and update. The APIs are distributed with many class, spell, power & magic item definitions, and DMs can add their own character classes, spells, items, weapons, ammo and armour to additional databases in their own database character sheets, with new definitions for database items held in Ability Macros. Additional database character sheets should be named as follows:
' + +'Wizard Spells: | additional databases: MU-Spells-DB-[added name] where [added name] can be replaced with anything you want. |
---|---|
Priest Spells: | additional databases: PR-Spells-DB-[added name] where [added name] can be replaced with anything you want. |
Powers: | additional databases: Powers-DB-[added name] where [added name] can be replaced with anything you want. |
Weapons: | additional databases: MI-DB-Weapons-[added name] where [added name] can be replaced with anything you want. |
Ammo: | additional databases: MI-DB-Ammo-[added name] where [added name] can be replaced with anything you want. |
Armour: | additional databases: MI-DB-Armour-[added name] where [added name] can be replaced with anything you want. |
Magic Items: | additional databases: MI-DB-[added name] where [added name] can be replaced with anything you want. |
Character Classes: | additional databases: Class-DB-[added name] where [added name] can be replaced with anything you want. |
Character Races: | additional databases: Race-DB-[added name] where [added name] can be replaced with anything you want. |
Attack Calculations: | additional databases: Attacks-DB-[added name] where [added name] can be replaced with anything you want. |
Fighting Styles: | additional databases: Styles-DB-[added name] where [added name] can be replaced with anything you want. |
Locks & Traps: | additional databases: Locks-Traps-DB-[added name] where [added name] can be replaced with anything you want. |
However: the system will ignore any database with a name that includes a version number of the form "v#.#" where # can be any number or group of numbers e.g. MI-DB v2.13 will be ignored. This is so that the DM can version control their databases, with only the current one (without a version number) being live.
' + +'There can be as many additional databases as you want. Other Master series APIs come with additional databases, some of which overlap - this does not cause a problem as version control and merging unique macros is managed by the APIs.
' + +'Important Note: all Character Sheet databases must have their \'ControlledBy\' value (found under the [Edit] button at the top right of each sheet) set to \'All Players\'. This must be for all databases, both those provided (set by the API) and any user-defined ones. Otherwise, Players will not be able to run the macros contained in them.
' + +'Important Note: databases extracted using the !magic --extract-db command will be able to be edited, but will also slow the system down - the versions held internally in the APIs are much faster for the system to access. Once any extracted database has been examined, it is best to delete them and use the !magic --check-db to re-index the databases so the system operates as fast as possible.
' + +'Each added database has a similar structure, with:
' + +'However, as with all other Databases in the RPGMaster Suite of APIs, if the Ability Macros are correctly set up using the formats detailed in the Help Documentation, the MagicMaster API command !magic --check-db database-name will check the database and set up all other aspects for you, including the correct Custom Attributes and List entries.
' + +'Ability Macros can be whatever the DM wants and can be as simple or as complex as desired. Roll Templates are very useful when defining class, spell, power and magic item ability macros, and are an essential part of Attack Templates. When a Player or an NPC or Monster makes an attack, the AttackMaster API runs the relevant Ability Macro from the databases as if it had been run by the Player from the chat window. All Roll20 functions for macros are available.
' + +'If you want to replace any Ability Macro provided in any of the databases, you can do so simply by creating an Ability Macro in one of your own databases (a database with the same root name) with the Ability Macro you create having exactly the same name as the provided item to be replaced. The API gives preference to Ability Macros in user-defined databases, so yours will be selected in preference to the one provided with the APIs.
' + +'Roll20 provides many excellent maths functions for commands made to the chat window and contained in API button strings. However, it is not always possible to use the Roll20 maths using the [[...]] syntax to achieve what you want. RPGMaster provides an alternative set of maths functions to help resolve these issues. Formulas can be entered for many numeric values required by RPGMaster commands using the supported syntax. However: this syntax does not work for anything other than RPGMaster commands as of writing (this might be a future develpment).
' + +'The square brackets [[...]] are not required. The syntax follows normal maths presedent with a few additional operators to support range calculations and dice rolls:
' + +'+-*/ | The standard maths operators work as expected |
---|---|
(...) | Parentheses can be used to define the order of calculation as normal |
^(#,#,#,...) | This will resolve to the maximum value in the list, and each # can also be a calculation (semi-colons can be used instead of commas) |
v(#,#,#,...) | This will resolve to the minimum value in the list, and each # can also be a calculation (semi-colons can be used instead of commas) |
c(...) | This will resolve to the ceiling (the number rounded up) of the result of the contained calculation |
f(...) | This will resolve to the floor (the number rounded down) of the result of the contained calculation |
#d#r# | Dice roll specifications can be included in the maths with optional reroll values anywhere in the calculation, and the numbers can be calculations |
#:# | A different feature is the range calculation - this will derive a number in the range between the two numbers (inclusive), but will try to do so using the equivalent to 3 dice if possible - e.g. 3:18 would make the equivalent of rolling 3d6, 7:34 will resolve to 4+(3d10), 7:35 will resolve to 4+1d11+2d10. A range can be used anywhere in the calculation, and the numbers can themselves be calculations |
All race, class, and magic item definitions (including those for weapons, ammunition & armour) can inherit data specifications and text from other similar magic item definitions. For example, the "Ring of Protection+2" is very similar to the "Ring of Protection+1" described in 2.2 above, and can inherit most of its specification from there:
' + +'&{template:RPGMring}{{}}Specs=[Ring of Protection,Protection Ring,1H,Abjuration-Protection,Ring-of-Protection+1]{{}}ACData=[a:Ring of Protection+2,+:2,svsav:2,w:Ring of Protection+2]{{}}%{MI-DB|Ring-of-Protection+1}{{name=+2}}{{Protection=+[[2]] on AC}}{{Saves=+[[2]] on saves}}
' + +'Inheritance comes in two forms: data inheritance and text inheritance.
' + +'Data Inheritance: an optional 5th parameter can be added to the Specs section, and RPGMaster will look for an item of that name (but only in the same root database tree): if not provided the 4th parameter will be used in the same way. If an item of that name is found (e.g. in this case "Ring-of-Protection+1") the data in data sections of the same name (e.g. "ACdata=") will be merged - data provided in the inheriting item (in this case "Ring-of-Protection+2") will take priority over inherited data (e.g. svsav:2 will override the inherited svsav:1). This inheritance can be nested as desired - the item being inherited from (the "parent item") can iteself inherit from another (the "grand-parent item").
' + +'Text Inheritance: If you are familiar with Roll20 ability macro programing, you will recognise the syntax %{char-name|ability-name} to insert the text of an ability macro into the chat window or another ability macro. RPGMaster item definitions do something similar but important to note not exactly the same! Instead of using the "char-name" a database name or database root name is given (e.g. "MI-DB-Weapons" or just "MI-DB") followed by the pipe \'|\' and the inherited item name. This will search both character sheet databases and databases held in memory - even if a specific database name is given, if not found in a database of that name all databases of the same root will be searched (e.g. if MI-DB-Rings is specified and the item not found there, all MI-DB databases will be searched). Note: under Text Inheritance data sections will not be merged (unless Data Inheritance is also used). Roll Template sections with the same name later in the merged definition take precidence (e.g. in this example the Roll Template section {{Protection=+[[2]] on AC}} will override that from the Ring-of-Protection+1 because it comes after the %{...|...} in the Ring-of-Protection+2 "child" item definition).
' + +'Defining Parent / Child item inheritance in this way can make the databases much smaller, allow simpler maintenance of common inherited data attributes, and cause less typing!
'; + + const General_API_Help ='The syntax of the Roll20 Roll Query has been extended within the RPGMaster APIs to support RPGMaster API commands with Roll Queries that the GM is invited to answer, rather than the player, regardless of who issued the command. The standard syntax and the extended syntax is shown below:
' + +'Standard Syntax: ?{Query text|option1|option2|...}' + +'
' + +'Extended syntax: gm{Query text/option1/option2/...}
When used in a RPGMaster API command, the extended Roll Query will prompt the GM with a button in the Chat Window for the GM to answer the question posed by the query text. The result will be fed into the action taken by the API command. This allows the GM to be involved when, for instance, a Staff of the Magi absorbs levels of spells cast at a character that the character & player can\'t know.
' + +'When a command is sent to Roll20 APIs / Mods, Roll20 tries to work out which player or character sent the command and tells the API its findings. The API then uses this information to direct any output appropriately. However, when it is the API itself that is sending commands, such as from a {{successcmd=...}} or {{failcmd=...}} sequence in a RPGMdefault Roll Template, Roll20 sees the API as the originator of the command and sends output to the GM by default. This is not always the desired result.
' + +'To overcome this, or when output is being misdirected for any other reason, a Controlling Player Override Syntax (otherwise known as a SenderId Override) has been introduced (for RPGMaster Suite APIs only, I\'m afraid), with the following command format:
' + +'!attk [sender_override_id] --cmd1 args1... --cmd2 args2...' + +'
The optional sender_override_id (don\'t include the [...], that\'s just the syntax for "optional") can be a Roll20 player_id, character_id or token_id. The API will work out which it is. If a player_id, the commands output will be sent to that player when player output is appropriate, even if that player is not on-line (i.e. no-one will get it if they are not on-line). If a character_id or token_id, the API will look for a controlling player who is on-line and send appropriate output to them - if no controlling players are on-line, or the token/character is controlled by the GM, the GM will receive all output. If the ID passed does not represent a player, character or token, or if no ID is provided, the API will send appropriate output to whichever player Roll20 tells the API to send it to.
' + +'Roll20 provides many excellent maths functions for commands made to the chat window and contained in API button strings. However, it is not always possible to use the Roll20 maths using the [[...]] syntax to achieve what you want. RPGMaster provides an alternative set of maths functions to help resolve these issues. Formulas can be entered for many numeric values required by RPGMaster commands using the supported syntax. However: this syntax does not work for anything other than RPGMaster commands as of writing (this might be a future develpment).
' + +'The square brackets [[...]] are not required. The syntax follows normal maths presedent with a few additional operators to support range calculations and dice rolls:
' + +'+-*/ | The standard maths operators work as expected |
---|---|
(...) | Parentheses can be used to define the order of calculation as normal |
^(#,#,#,...) | This will resolve to the maximum value in the list, and each # can also be a calculation (semi-colons can be used instead of commas) |
v(#,#,#,...) | This will resolve to the minimum value in the list, and each # can also be a calculation (semi-colons can be used instead of commas) |
c(...) | This will resolve to the ceiling (the number rounded up) of the result of the contained calculation |
f(...) | This will resolve to the floor (the number rounded down) of the result of the contained calculation |
#d#r# | Dice roll specifications can be included in the maths with optional reroll values anywhere in the calculation, and the numbers can be calculations |
#:# | A different feature is the range calculation - this will derive a number in the range between the two numbers (inclusive), but will try to do so using the equivalent to 3 dice if possible - e.g. 3:18 would make the equivalent of rolling 3d6, 7:34 will resolve to 4+(3d10), 7:35 will resolve to 4+1d11+2d10. A range can be used anywhere in the calculation, and the numbers can themselves be calculations |
The most common approach for the Player to run these commands is to use Ability macros on their Character Sheets which are flagged to appear as Token Action Buttons: Ability macros & Token Action Buttons are standard Roll20 functionality, refer to the Roll20 Help Centre for information on creating and using these.
' + +'In fact, the simplest configuration is to provide only Token Action Buttons for the menu commands: --menu and --other-menu. From these, most other commands can be accessed. If using the CommandMaster API, its character sheet setup functions can be used to add the necessary Ability Macros and Token Action Buttons to any Character Sheet.
' + +'Many spells and powers as well as magic items have an effect on saving throws and attribute checks. To date, all such effects have been lumped into a single "effect of magic" number with no further explanation when reviewing how saves and checks are calculated. Now, a (hidden) table is held on each character sheet which holds saving throw and attribute check modifiers currently in force, with a name and a source, and optional durations in number of saves to which the mod applies and/or the number of rounds duration of the mod. The mods can apply to one type of save/check, more than one, or all. The mods are specific to the character represented by the sheet. Commands exist (documented in the AttackMaster Help Handout) to add, modify, and delete the mods.
' - +'This new system can also be used to create totally new, bespoke saving throws based on any existing saving throw, which can be modified. For existance, the spell Bless only affects saves against Fear effects. Using the new mod commands a new "save vs. Fear" button can be added to the Saving Throw table for the duration of the Bless only for those characters who are blessed, which is based on a save vs. spell but with a +1 bonus.
' - +'At any time, the player can review the way their character\'s saving throws and ability checks are calculated by selecting the [Auto-check Saving Throws] button on the Saving Throw dialog, or equivalent button on the Attribute Check dialog.
' - +'The RoundMaster --target command (and associated other token status management commands) can now take a saving throw specification as the last argument, defining the type of save required and any bonus or penalty to the save. This only works if RoundMaster is being used with AttackMaster. The effect of adding this optional parameter is that the player issuing the command will be prompted to ask the player controlling the target token to make the appropriate saving throw and, if that saving throw is made using the AttackMaster saving throw dialog (which will apply the bonus or penalty as specified in the new parameter), a failure will automatically apply the status and any associated effects to the token. If the save is made the status is not applied.
' - +'Multi has been added as a command mode for the RoundMaster --target command, alongside the existing caster, single, and area modes. Multi mode is similar to area, but allows the player to shift-select multiple tokens at the same time, and then click one button to apply the status to all those tokens. If the new saving throw parameter (see above) is included, the player will be prompted to ask for saving throws to be made for all the selected tokens - which can all happen simultaneously with the outcomes described above.
' - +'For the multi command, RPGM maths for the duration, direction and/or saving throw mods can include the symbol \'#\' which will be replaced with the number of tokens selected and allow these numbers to vary by the number of tokens targeted. This is useful where saving throw bonuses or penalties vary by numbers of targets, or the duration is divided across the targets, for instance.
' - +'Important Note: When this new multi mode is used for a --target command, at the point the command is entered all tokens on the same Roll20 page as the caster token temporarily become controllable (and thus selectable) by the player controlling the caster token. Also, all tokens without controllers (or otherwise controlled by the GM) that have sight under Dynamic Lighting will be temporarily "blinded" - this is so that the player controlling the casting token is not suddenly able to see the whole map, and the dynamic lighting Explorer Mode is not compromised. The temporary blinding and control are automatically removed as soon as the casting player clicks the button to apply the statuses or issues any other API command (RPGM or otherwise).
' - +'Options have been provided for better control over the requirement for the GM to confirm all targeted token status changes made by a player using RoundMaster --target command. Two new qualifying command modifiers now exist: --target-nosave and --target-save. The first will not prompt the GM for a confirmation when applying a status to a token (though this behaviour can be altered using the --nosave command), whereas the second will always prompt for confirmation even if the command is issued by the GM. --target-nosave is useful when casting a spell or an item power that has no saving throw e.g. where applied to the caster themselves. --target-save can be useful to remind the GM to prompt for or make saving throws. The --nosave command will turn off or on the --target-nosave behaviour.
' - +'When spell and item descriptions are viewed (rather than cast or used) all action buttons in the spell or item macro are now "greyed out" and not selectable, so that effects are not inadvertently applied and charges aren\'t accidentally used due to the player selecting an action button. The one exception is for any action buttons that start with "View" in the button text - this allows, for instance, players to still view the spells contained in a spell-storing magic item.
' - +'Some items are not stackable in containers or character\'s backpacks, such as wands with charges or spell-storing items. However, it is also the case that items in containers should have unique names to identify them, so if two of the same non-stackable items are found, one of them needs to be renamed. This is now automatically detected and the player prompted to enter a new name for the second (or subsequent) items picked up. The renamed item retains all its properties, powers, stored-spells, etc.
' - +'To date, automatic hits on a natural 20, or automatic miss on a natural 1, have had to be applied manually on targeted attacks. By default, automatic hits and misses will now be applied for targeted attacks but with an RPGM Configuration option to turn this off. Similarly, Critical Hits and Misses (which need not be equivalent to natural rolls of 20 & 1) now also automatically hit or miss on targeted attacks and also have a separate RPGM configuration option to turn this off.
' - +'Now all magic items defined in the AD&D2e Dungeon Master\'s Guide have been defined in the RPGM databases, the on-going task of updating and refining them has started. Potions, scrolls, and rings have been reviewed and updated - though the impact will seem very limited to those using them. All spells, both Clerical and Wizard, and all Powers, have also been updated.
' - +'In order to make spell databases more manageable, they have been split by level, with Wizard spell databases now being MU-Spells-DB-L# and Clerical spell databases being PR-Spells-DB-L#. This will not affect any currently extracted spell databases or DM\'s custom spell databases, which will continue to function as before. The only visible impact is on extracting databases where a choice of databases will now be offered when extracting spell databases.
' - +'The RPGM alternative to Roll20 maths for numeric values now includes ceiling and floor operators. c(...) will return the value in the brackets rounded up and f(...) will return it rounded down.
' + +'This is a major release, with considerable changes in the background to make the RPGMaster suite of APIs smaller and faster, while increasing utility and helpfulness.
' + +'In 2023, RPGMaster introduced Drag & Drop creatures. This functionality has now been extended to add NPCs, with fully populated character sheets, rolled attributes, populated skills such as rogue skills, populated spell books and powers, all driven by user-definable NPC definitions. A new [NPC] button exists on the Drag & Drop Class & Race dialog, which presents a drop-down Roll Query listing the currently defined NPCs to select from. Other queries will request more detail, such as the level of the NPC. Creating and playing a Drag & Drop NPC is identical in process to a Drag & Drop creature. Help is provided in the CommandMaster Help handout, and the Class & Race Database Help handout.
' + +'To support the introduction of Drag & Drop fully-populated NPCs, the RPGMaster suite will now correctly roll and populate the Attributes for any NPC chosen, and also populate on the character sheet the associated data elements, such as to-hit plus, max weight etc granted by strength, extra spells for priests with high wisdom, and so on. These values have always been taken into account by the APIs (even if not set). However, in addition when an Other Actions > Attribute Check dialog is openned and if the [Auto-check Attributes] button is selected the APIs will automatically check the Attributes: if they have not been set, they will automatically be rolled for the selected character / NPC / creature so that a valid check can be done unless configured not to do so.
' + +'A new RPGM configuration option is now available using the GM\'s [RPGM config] macro bar button, or the !attk --config command, called "NPC Attributes" with the options [No Attributes] and [Roll Attributes]. If [Roll Attributes] is chosen, another configuration option called "NPC Attr Range" becomes available, with the options [Full Range] and [Restrict Range]. If [No Attributes] is chosen, any existing NPC / creature which does not have attribute rolls defined in its creature definition record in the database will not have attributes rolled for it. If [Roll Attributes] & [Full Range] is set, any NPC / creature for which attributes have not been set will have attributes rolled when an Attribute Check is done with a full range (3d6). If [Roll Attributes] & [Restrict Range] is set, any NPC / creature for which attributes have not been set will have attributes rolled in a restricted range that does not give them undue bonuses or penalties.
' + +'The previous release added a means of storing and displaying all saving throw & attribute check modifiers currently in effect for each character / creature / NPC, to show in detail how the current saving throw and attribute check targets were calculated. This release has extended that capability to do the same for all modifiers to AC, Thac0 (to-hit), Damage, & HP, and to manage the application and durations of these modifiers. Generally, the modifiers are applied due to magic items in the possession of the character / creature / NPC, or from spells cast on them that are currently in effect - in the majority of cases, if using the RPGMaster item and spell capabilities of the MagicMaster API and effect management of the RoundMaster API, all these mods will be applied and expired automatically. However, API commands exist for GMs and players to apply mods manually if so desired. See the AttackMaster Help handout for more information.
' + +'The Rogue Skill table can be accessed through Other Actions > Rogue Skill Check. This dialog has now been extended to work in a similar fashion to the Saving Throw dialog, with a button to [Auto-check Skill Scores] and another to [Manually check Skill Scores] - the choice will be preserved between uses and game sessions. Auto-checking will automatically review the class, race, dexterity, armour, and magic items possessed, and use the data in the definitions for each to set the values on the Rogue Skills table, leaving the user to allocate the points granted for the level and class of rogue. The auto-check will be performed continuously by the APIs so that as conditions change, such as items possessed and armour worn, the values immediately change accordingly. The manual option, on the other hand, leaves the player to enter all the values in the table, for instance for a non-standard character.
' + +'Creature innate attacks and attacks with weapons have always been considered to be "proficient". However, there is now a way to set weapon use for Drag & Drop creatures and NPCs to be "specialist" or even have "mastery". Each weapon definition in a creature / NPC definition can be annotated with the proficiency level (if none is stated, "proficient" is assumed). See the Class & Race Database Help handout for how to specify proficiency levels.
' + +'Drag & Drop creatures and NPCs have always been able to have named items allocated to them to carry, such as named weapons, armour or ammunition. They can now also have random items added to their character sheets as being "in their possession" (even if they can\'t use them), as if they had picked them up or won them in battle at some point in the past. The number of random items can be specified (or even be a random quantity, such as a dice roll or number range), and reviewed by the GM using the GM\'s [Add Items] macro bar button or the !magic --gm-edit-mi command (to ensure game balance...). Thus, PCs can loot these as treasure after a successful battle, or a rogue can pick-pocket them, etc. Details can be found in the Class & Race Database Help handout as to how to specify random items for Drag & Drop creatures & NPCs.
' + +'Drag & Drop creature and NPC database definitions can use the query: data tag to specify a query to the player that returns a selected list of parameters to feed into and alter the definition\'s results. The parameters for these queries can now be used in weapon, armour and item definition sections of NPC and creature database entries. See the Class & Race Database Help handout section on Complex Creatures with Multiple Forms for an explanation of creature and NPC query definitions.
' + +'A new database of treasure items - items that are descriptive and may have a worth, but are not magical or functional. These are intended to add colour to the campaign. A new button has been added to the GM\'s [Add Items] dialog (or !magic --gm-only-mi command) to list these items to add to NPCs and containers for player characters to find
' +'While I am sure everything worked when first coded, subsequent changes have had unexpected consequences and players have also done things I didn\'t expect (is that not the story for all GMs?). Hence fix lists continue...
' +'api_field_name: [char_sheet_field_name,property,defaultValue,sheetWorkerFlag]' - + 'where property is current or max and sheetWorkerFlag (optional) is true if the field is to be set with the Roll20 setWithSheetWorker() method or false if the Roll20 set() method is to be used (default false).
Weapons: | additional databases: MI-DB-Weapons-[added name] where [added name] can be replaced with anything you want. |
Ammo: | additional databases: MI-DB-Ammo-[added name] where [added name] can be replaced with anything you want. |
Armour: | additional databases: MI-DB-Armour-[added name] where [added name] can be replaced with anything you want. |
However: the system will ignore any database with a name that includes a version number of the form "v#.#" where # can be any number or group of numbers e.g. MI-DB v2.13 will be ignored. This is so that the DM can version control their databases, with only the current one (without a version number) being live.
' - +'There can be as many additional databases as you want. Other Master series APIs come with additional databases, some of which overlap - this does not cause a problem as version control and merging unique macros is managed by the APIs.
' - +'Important Note: all Character Sheet databases must have their \'ControlledBy\' value (found under the [Edit] button at the top right of each sheet) set to \'All Players\'. This must be for all databases, both those provided (set by the API) and any user-defined ones. Otherwise, Players will not be able to run the macros contained in them.
' - +'Important Note: databases extracted using the --extract-db command will be able to be edited, but will also slow the system down - the versions held internally in the APIs are much faster for the system to access. Once any extracted database has been examined, it is best to delete them and use the --check-db to re-index the databases so the system operates as fast as possible.
' - +'Each database has a similar structure, with:
' - +'Note: a DM only needs to program the Ability Macro using the formats shown in the next section, and then run the !attk --check-db or !magic --check-db command, which will correctly parse the ability macro and set the rest of the database entries as needed.
' - +'Ability Macros can be whatever the DM wants and can be as simple or as complex as desired. Roll Templates are very useful when defining ability macros - the RPGMaster Library provides several new Roll Templates that do not rely on any particular Character Sheet: RPGMweapon, RPGMammo, and RPGMarmour are the most relevant. See the RPGMaster Library help handout for further information. When a Player or an NPC or Monster views the specifications of a weapon, ammunition or piece of armour, the APIs run the relevant Ability Macro from the databases as if it had been run by the Player from the chat window. All Roll20 functions for macros are available.
' - +'If you want to replace any item provided in any of the databases, you can do so simply by creating an Ability Macro in one of your own databases with exactly the same name as the provided item to be replaced. The API gives preference to Ability Macros in user-defined databases, so yours will be selected in preference to the one provided with the APIs.
' - +'Item definitions can be simplified using item inheritance, where an item definition can inherit data attributes and Roll Template text from a "parent item". This is fully explained in the Magic Database Help handout and not repeated here, so please refer to that help handout for further information on this very helpful feature.
' + +'Weapon databases are all character sheets that have names that start with MI-DB-Weapon (though in fact, weapons can be in any database starting with MI-DB- if desired), and can have anything put at the end, though those with version numbers of the form v#.# as part of the name will be ignored. Ammunition databases are similar, with the root database MI-DB-Ammo.
' +'As previously stated, each weapon definition has 3 (or 4) parts in the database (see Section 1): an Ability Macro with a name that is unique and matches the weapon, an Attribute with the name of the Ability Macro preceded by "ct-", a listing in the database character sheet of the ability macro name separated by \'|\' along with other weapons, and sometimes Attributes defining powers given by, or spells stored on the item. The quickest way to understand these entries is to examine existing entries. Do extract the root databases and take a look (but remember to delete them after exploring the items in them, so as not to slow the system down unnecessarily).
' @@ -4838,7 +5308,7 @@ const libRPGMaster = (() => { // eslint-disable-line no-unused-vars +'&{template:RPGMweapon}{{name=Battle Axe}} {{subtitle=Axe}} {{Speed=[[7]]}} {{Size=Medium}} {{Weapon=1-handed melee axe}} Specs=[Battle-Axe,Melee,1H,Axe],[Battle-Axe,Melee,2H,Axe] {{To-hit=+0 + Str Bonus}} ToHitData=[w:Battle Axe,sb:1,+:0,n:1,ch:20,cm:1,sz:M,ty:S,r:5,sp:7,rc:uncharged] {{Attacks=1 per round + specialisation & level, Slashing}} {{Damage=+0, SM:1d8, L:1d12 + Str Bonus}} DmgData=[w:Battle Axe,sb:1,+:0,SM:1d8,L:1d12] {{desc=A standard Battle Axe of good quality, but nothing special}}
' +'Here it can be seen that there are two data sets specified for the Specs field and only one data set specified for the ToHitData. Doing this tells the APIs that this weapon can be taken in both hands, but generally will not gain any different advantages. If a Character is proficient or specialised in Two-Hander Fighting Style, however, the APIs will see that this is a 1-handed weapon held in both hands, and allocate it the correct benefits. But only certain weapons gain these benefits, so only certain weapons in the database should be set up this way.
' - +'Only one dancing weapon is defined in the Dungeon Master\'s Guide, the "Sword of Dancing". However, with RPGMaster APIs, any weapon can be made to dance including ranged weapons:
' +'&{template:RPGMweapon}{{title=Longsword of Dancing}} {{subtitle=Magical Sword}}{{Speed=[[5]]}} {{Size=Medium}}WeapData=[d:+1/4]{{Weapon=Dancing 1-handed melee long-blade}}Specs=[Longsword,Melee,1H,Long-blade]{{To-hit=+1/2/3/4 on sequential rounds + Str bonus}}ToHitData=[w:Longsword of Dancing, sb:1, +:0, n:1, ch:20, cm:1, sz:M, ty:S, r:5, sp:5]{{Attacks=1 per round + level & specialisation, Slashing}}{{Damage=+1/2/3/4 on sequential rounds, vs SM:1d8, L:1d12, + Str bonus}}DmgData=[w:Longsword, sb:1, +:0, SM:1d8, L:1d12]{{desc=This is a very special sword. It is etched with dramatic battle scenes, almost balletic in grace and poise.}}
' @@ -4917,34 +5387,45 @@ const libRPGMaster = (() => { // eslint-disable-line no-unused-vars +'These attributes override the values given for the 1st and 4th fields in the Specs section.
' +'Armour databases are all character sheets that have names that start with MI-DB-Armour (as with weapons, this can be in any database starting with MI-DB- if desired), and can have anything put at the end, though those with version numbers of the form v#.# as part of the name will be ignored.
' +'As previously stated and as per the weapon and ammunition databases, each armour definition has 3 parts in the database (see Section 1): the Ability Macro, the ct- attribute, and the listing (and occasionally attributes for powers and spells). The quickest way to understand these entries is to examine existing entries. Do extract to the root databases and take a look (but remember to delete them after examination and use the --check-db command to re-index the databases).
' +'Note:The DM creating new armour entries does not need to worry about anything other than the Ability Macro in the database, as running the !attk --check-db MI-DB-Armour or !magic --check-db MI-DB-Armour command will update all other aspects of the database appropriately for all databases that have a name starting with or including \'MI-DB-Armour\', as long as the Specs and Data fields are correctly defined. Running the command -check-db with no parameters will check and update all databases.
' +'Here are some examples:
' +'&{template:RPGMarmour}{{name=Chain Mail}}{{subtitle=Armour}}{{Armour=Chain Mail}}Specs=[Chain Mail,Armour,0H,Mail]{{AC=[[5]] vs all attacks}}ACData=[a:Chain Mail,st:Mail,+:0,ac:5,sz:L,wt:40]{{Speed=[[0]]}}{{Size=Large}}{{Immunity=None}}{{Saves=No effect}}{{desc=This armor is made of interlocking metal rings. It is always worn with a layer of quilted fabric padding underneath to prevent painful chafing and to cushion the impact of blows. Several layers of mail are normally hung over vital areas. The links yield easily to blows, absorbing some of the shock. Most of the weight of this armor is carried on the shoulders and it is uncomfortable to wear for long periods of time.}}
' + +'&{template:RPGMarmour}{{title=Chain Mail }}{{subtitle=Armour}}Specs=[Chain Mail,Armour,0H,Mail]{{}}ACData=[a:Chain Mail, st:Mail, t:Chain-Mail, +S:2, +P:0, +B:-2, +:0, ac:5, sz:L, rc:single-uncharged, qty:1, wt:40, loc:body, rac:Chain Mail, ppa:-25, ola:-10, rta:-10, msa:-15, hsa:-15, dna:-5, cwa:-25, rla:0, lla:0]{{}}%{MI-DB|Armour-Info}{{Armour=Chain Mail}}{{AC=[[5]] vs all attacks}}{{Looks Like=This armor is made of interlocking metal rings.}}{{hide1=Chain mail is always worn with a layer of quilted fabric padding underneath to prevent painful chafing and to cushion the impact of blows. Several layers of mail are normally hung over vital areas. The links yield easily to blows, absorbing some of the shock. Most of the weight of this armor is carried on the shoulders and it is uncomfortable to wear for long periods of time.}}{{desc=Good, sturdy, well made chain but nothing special}}
' +'The ability specification for this suit of Chain Mail uses a Roll20 Roll Template, in this case defined by the loaded RPGMaster Library. The entries in the Roll Template itself can be anything you desire, giving as much or as little information as you want. However, the important elements for the AttackMaster API are those highlighted. Each of these elements are inserted between the elements of the Roll Template, meaning they will not be seen by the player when the macro is run. Generally spaces, hyphens and underscores in the data elements are ignored, and case is not significant. Each element is described below:
' +'Specs=[Chain Mail,Armour,0H,Mail]' +'
The Specs section of the specification has exactly the same format as for weapons and ammunition (and indeed all database items). See section 9 for the definition of the fields.
' +'Note:The armour Type (the 1st parameter) and Group-Type (the 4th parameter) are used to determine if the character is of a class that can use the armour. Currently implemented types are listed in Section 4.
' +'Note: Armour that fits on the body generally does not take any hands to hold, and so the third field, Handedness, is set to \'0H\'.
' - +'ACData=[a:Chain Mail,st:Mail,+:0,ac:5,sz:L,wt:40]' + +'
ACData=[a:Chain Mail, t:Chain-Mail, st:Mail, ac:5, +:0, +S:2, +P:0, +B:-2, sz:L, wt:40, qty:1, rc:single-uncharged, loc:body, rac:Chain Mail, ppa:-25, ola:-10, rta:-10, msa:-15, hsa:-15, dna:-5, cwa:-25, rla:0, lla:0]]' +'
The Armour Class Data (ACData) section holds data specific to the armour. As with other data sections, fields can be in any order, and spaces, hyphens, underscores and case are ignored.
' +'a: | < text > the name of the armour to be displayed. Often the same as the Ability. |
t: | < armour-type > The specific armour type, often the same as the first parameter of the Specs section. |
st: | < group-type > the supertype of the armour, often the same as the fourth parameter of the Specs section. |
+: | <[+/-]#> the magical bonus or penalty of the armour (defaults to 0 if not supplied). |
ac: | <[-]#> the base armour class (excluding magical bonuses) for this type of armour. |
sz: | <[T/S/M/L/H]> The size of the item (not necessarily indicating its fit). |
wt: | <#> The weight of the item in lbs (could be considered kg - or any measure - if everything is the same). |
Other possible fields are:
' - +'t: | < armour-type > The specific armour type, often the same as the first parameter of the Specs section. |
db: | <[0/1]> A 1 means dexterity AC bonus combines with armour, 0 means armour prevents dexterity bonus from applying. |
+m: | <[-/+]#> The adjustment that the armour gives vs. missiles and ammunition of ranged weapons. |
+: | <[+/-]#> the magical bonus or penalty of the armour (defaults to 0 if not supplied). |
+s: | <[-/+]#> The magical adjustment specifically against slashing damage. |
+p: | <[-/+]#> The magical adjustment specifically against piercing damage. |
+b: | <[-/+]#> The magical adjustment specifically against bludgeoning damage. |
rc: |
pre: | 0 / 1 | 0 | Gets auto pre-Initiative attack | X | |||||
on: | < cmd > | \'\' | Cmd to execute when taken in-hand | X | |||||
off: | < cmd > | \'\' | Cmd to execute when sheathed | X | |||||
qty: | # | 0 | Maximum possible qty of Ammo | X | |||||
qty: | # | 0 | Maximum possible qty of Items | X | X | X | |||
ru: | [-]# | 0 | Reusability of ammunition | X | |||||
sm: | dice roll format | 0 | Damage roll for Small & Medium opponents | X | X | ||||
l: | dice roll format | 0 | Damage roll for Large & Huge opponents | X | X | ||||
cl: | MU / PR / PW | \'\' | Type of spell or power | X | |||||
pd: | -1 / # | 1 | Number per day (power only) | X | |||||
rc: | Charged / Uncharged / Rechargeable / Recharging / Self-charging / Cursed / Charged-Cursed / Recharging-Cursed / Self-charging-Cursed | Uncharged | Initial charged and Cursed status of item when found | X | X | ||||
rac: | < text > | Armor name | Thievish armour name | X | |||||
ppa: | <[-/+]#> | 0 | Penalty / benefit to rogue\'s pick pocket skill | X | |||||
ola: | <[-/+]#> | 0 | Penalty / benefit to rogue\'s open locks skill | X | |||||
rta: | <[-/+]#> | 0 | Penalty / benefit to rogue\'s find/remove traps skill | X | |||||
msa: | <[-/+]#> | 0 | Penalty / benefit to rogue\'s moving silently skill | X | |||||
hsa: | <[-/+]#> | 0 | Penalty / benefit to rogue\'s hide in shadows skill | X | |||||
dna: | <[-/+]#> | 0 | Penalty / benefit to rogue\'s detect noise skill | X | |||||
cwa: | <[-/+]#> | 0 | Penalty / benefit to rogue\'s detect noise skill | X | |||||
rla: | <[-/+]#> | 0 | Penalty / benefit to rogue\'s read languages skill | X | |||||
lla: | <[-/+]#> | 0 | Penalty / benefit to rogue\'s ledgend lore skill | X | |||||
lv: | # | 1 | Level at which spell/power is cast | X | |||||
lv: | #:# | 1 | Min:Max level at which weapon/ammo can be used | X | X | ||||
clv: | #:# | 1 | Min:Max caster level at which weapon/ammo can be used | X | X |
Wizard Spells: | additional databases: MU-Spells-DB-[added name] where [added name] can be replaced with anything you want. |
---|---|
Priest Spells: | additional databases: PR-Spells-DB-[added name] where [added name] can be replaced with anything you want. |
Powers: | additional databases: Powers-DB-[added name] where [added name] can be replaced with anything you want. |
Magic Items: | additional databases: MI-DB-[added name] where [added name] can be replaced with anything you want. |
Character Classes: | additional databases: Class-DB-[added name] where [added name] can be replaced with anything you want. |
Character Races: | additional databases: Race-DB-[added name] where [added name] can be replaced with anything you want. |
Attack Calculations: | additional databases: Attacks-DB-[added name] where [added name] can be replaced with anything you want. |
Fighting Styles: | additional databases: Styles-DB-[added name] where [added name] can be replaced with anything you want. |
However: the system will ignore any database with a name that includes a version number of the form "v#.#" where # can be any number or group of numbers e.g. MI-DB v2.13 will be ignored. This is so that the DM can version control their databases, with only the current one (without a version number) being live.
' - +'There can be as many additional databases as you want. Other Master series APIs come with additional databases, some of which overlap - this does not cause a problem as version control and merging unique macros is managed by the APIs.
' - +'Important Note: all Character Sheet databases must have their \'ControlledBy\' value (found under the [Edit] button at the top right of each sheet) set to \'All Players\'. This must be for all databases, both those provided (set by the API) and any user-defined ones. Otherwise, Players will not be able to run the macros contained in them.
' - +'Each added database has a similar structure, with:
' - +'However, as with all other Databases in the RPGMaster Suite of APIs, if the Ability Macros are correctly set up using the formats detailed in the Help Documentation, the MagicMaster API command !magic --check-db database-name will check the database and set up all other aspects for you, including the correct Custom Attributes and List entries.
' - +'Ability Macros can be whatever the DM wants and can be as simple or as complex as desired. Roll Templates are very useful when defining class, spell, power and magic item ability macros, and are an essential part of Attack Templates. When a Player or an NPC or Monster makes an attack, the AttackMaster API runs the relevant Ability Macro from the databases as if it had been run by the Player from the chat window. All Roll20 functions for macros are available.
' - +'If you want to replace any Ability Macro provided in any of the databases, you can do so simply by creating an Ability Macro in one of your own databases (a database with the same root name) with the Ability Macro you create having exactly the same name as the provided item to be replaced. The API gives preference to Ability Macros in user-defined databases, so yours will be selected in preference to the one provided with the APIs.
' +'Spells/Powers databases have names that start with
' +' Wizard Spells: MU-Spells-DB-[added name]
'
@@ -5307,45 +5770,22 @@ const libRPGMaster = (() => { // eslint-disable-line no-unused-vars
+'
The RPGMaster APIs use a number of databases to hold Macros defining character classes, spells, powers and magic items and their effects. Previous versions of the RPGMaster series of APIs held their databases all externally as character sheets: from this version onwards this is not the case for databases supplied with the APIs, which are now held internally to the APIs. However, the AttackMaster or MagicMaster API command --extract-db can be used to extract any or all standard databases to Character Sheets for examination and update. The APIs are distributed with many class, spell, power & magic item definitions, and DMs can add their own character classes, spells, items, weapons, ammo and armour to additional databases in their own database character sheets, with new definitions for database items held in Ability Macros. Additional database character sheets should be named as follows:
' - +'Wizard Spells: | additional databases: MU-Spells-DB-[added name] where [added name] can be replaced with anything you want. |
---|---|
Priest Spells: | additional databases: PR-Spells-DB-[added name] where [added name] can be replaced with anything you want. |
Powers: | additional databases: Powers-DB-[added name] where [added name] can be replaced with anything you want. |
Magic Items: | additional databases: MI-DB-[added name] where [added name] can be replaced with anything you want. |
Character Classes: | additional databases: Class-DB-[added name] where [added name] can be replaced with anything you want. |
Character Races: | additional databases: Race-DB-[added name] where [added name] can be replaced with anything you want. |
Attack Calculations: | additional databases: Attacks-DB-[added name] where [added name] can be replaced with anything you want. |
Fighting Styles: | additional databases: Styles-DB-[added name] where [added name] can be replaced with anything you want. |
However: the system will ignore any database with a name that includes a version number of the form "v#.#" where # can be any number or group of numbers e.g. MI-DB v2.13 will be ignored. This is so that the DM can version control their databases, with only the current one (without a version number) being live.
' - +'There can be as many additional databases as you want. Other Master series APIs come with additional databases, some of which overlap - this does not cause a problem as version control and merging unique macros is managed by the APIs.
' - +'Important Note: all Character Sheet databases must have their \'ControlledBy\' value (found under the [Edit] button at the top right of each sheet) set to \'All Players\'. This must be for all databases, both those provided (set by the API) and any user-defined ones. Otherwise, Players will not be able to run the macros contained in them.
' - +'Each added database has a similar structure, with:
' - +'However, as with all other Databases in the RPGMaster Suite of APIs, if the Ability Macros are correctly set up using the formats detailed in the Help Documentation, the MagicMaster API command !magic --check-db database-name will check the database and set up all other aspects for you, including the correct Custom Attributes and List entries.
' - +'Ability Macros can be whatever the DM wants and can be as simple or as complex as desired. Roll Templates are very useful when defining class, spell, power and magic item ability macros, and are an essential part of Attack Templates. When a Player or an NPC or Monster makes an attack, the AttackMaster API runs the relevant Ability Macro from the databases as if it had been run by the Player from the chat window. All Roll20 functions for macros are available.
' - +'If you want to replace any Ability Macro provided in any of the databases, you can do so simply by creating an Ability Macro in one of your own databases (a database with the same root name) with the Ability Macro you create having exactly the same name as the provided item to be replaced. The API gives preference to Ability Macros in user-defined databases, so yours will be selected in preference to the one provided with the APIs.
' - +'Note: Help for Spells & Powers has been split out to its own help handout.
' +'Magic Item databases have names such as
' @@ -5364,14 +5804,7 @@ const libRPGMaster = (() => { // eslint-disable-line no-unused-vars +'All magic items have a recharging/curse type: for details, see the --gm-edit-mi command in the MagicMaster API help documentation, section 4.1. If not supplied for a magic item definition, it defaults to uncharged. Generally, items in the database are not cursed-, but can have their type changed to cursed or some recharging cursed type when the DM stores them in a container or gives them to a Character using the --gm-edit-mi command.
' - +'All magic item definitions (including those for weapons, ammunition & armour) can inherit data, specifications and text from other similar magic item definitions. For example, the "Ring of Protection+2" is very similar to the "Ring of Protection+1" described in 2.2 above, and can inherit most of its specification from there:
' - +'&{template:RPGMring}{{}}Specs=[Ring of Protection,Protection Ring,1H,Abjuration-Protection,Ring-of-Protection+1]{{}}ACData=[a:Ring of Protection+2,+:2,svsav:2,w:Ring of Protection+2]{{}}%{MI-DB|Ring-of-Protection+1}{{name=+2}}{{Protection=+[[2]] on AC}}{{Saves=+[[2]] on saves}}
' - +'Inheritance comes in two forms: data inheritance and text inheritance.
' - +'Data Inheritance: an optional 5th parameter can be added to the Specs section, and RPGMaster will look for a magic item of that name: if not provided the 4th parameter will be used in the same way. If an item of that name is found (e.g. in this case "Ring-of-Protection+1") the data in data sections of the same name (e.g. "ACdata=") will be merged - data provided in the inheriting item (in this case "Ring-of-Protection+2") will take priority over inherited data (e.g. svsav:2 will override the inherited svsav:1). This inheritance can be nested as desired - the item being inherited from (the "parent item") can iteself inherit from another (the "grand-parent item").
' - +'Text Inheritance: If you are familiar with Roll20 ability macro programing, you will recognise the syntax %{char-name|ability-name} to insert the text of an ability macro into the chat window or another ability macro. RPGMaster item definitions do something similar but important to note not exactly the same! Instead of using the "char-name" a database name or database root name is given (e.g. "MI-DB-Weapons" or just "MI-DB") followed by the pipe \'|\' and the inherited item name. This will search both character sheet databases and databases held in memory - even if a specific database name is given, if not found in a database of that name all databases of the same root will be searched (e.g. if MI-DB-Rings is specified and the item not found there, all MI-DB databases will be searched). Note: under Text Inheritance data sections will not be merged (unless Data Inheritance is also used). Roll Template sections with the same name later in the merged definition take precidence (e.g. in this example the Roll Template section {{Protection=+[[2]] on AC}} will override that from the Ring-of-Protection+1 because it comes after the %{...|...} in the Ring-of-Protection+2 "child" item definition).
' - +'Defining Parent / Child item inheritance in this way can make the databases much smaller, allow simpler maintenance of common inherited data attributes, and cause less typing!
' - +'Items like a Ring of Protection or a Luck Blade protect the possessor by improving their saving throws and/or armour class.
' +'&{template:RPGMring}&{template:RPGMring}{{name=Ring of Protection}}{{subtitle=Ring}}{{Speed=[[0]]}}{{Size=Tiny}}{{Immunity=None}}{{Protection=+[[2]] on AC}}Specs=[Ring of Protection,Protection Ring,1H,Abjuration-Protection]{{Saves=+[[2]] on saves}}ACData=[a:Ring of Protection+2,st:Ring,+:2,rules:-magic,sz:T,wt:0,svsav:2,w:Ring of Protection+2,sp:0,rc:uncharged,loc:left finger|right finger]{{Looks Like=A relatively plain ring made of some exotic metal. You are unable to distinguish it from any other ring by just looking at it...}}{{desc=A ring of protection improves the wearer\'s Armour Class value and saving throws versus all forms of attack. A ring +1 betters AC by 1 (say, from 10 to 9) and gives a bonus of +1 on saving throw die rolls. The magical properties of a ring of protection are cumulative with all other magical items of protection except as follows:
1. The ring does not improve Armour Class if magical armour is worn, although it does add to saving throw die rolls.
2. Multiple rings of protection operating on the same person, or in the same area, do not combine protection. Only one such ring—the strongest—functions, so a pair of protection rings +2 provides only +2 protection.}}
The svXXX: entries state the effect on saving throws and/or ability checks that this item has if the rules are met. For Saving Throw mods the \'XXX\' can be one of \'par\', \'poi\', \'dea\', \'pet\', \'pol\', \'bre\', \'spe\', or \'sav\' each referring to the first 3 letters of the saving throw affected (or \'sav\' for all saving throw mods); and for Attribute Check mods \'XXX\' can be \'str\', \'con\', \'dex\', \'int\', \'wis\', \'chr\', and \'atr\' each refering to each Character attribute (or \'atr\' for all attribute mods); or to change all mods of both types use \'all\'. Each svXXX: field tag is followed by a number which can be optionally preceeded by \'+\' (a beneficial improvement to the mod), \'-\' (a penalty to the mod), and/or \'=\' (the mod is set to the value - overrides other changes).
' +'Note: Changing the Attribute mods will not affect the ability checks for open doors, bend bars, learn spells etc. These mods can only be adjusted manually using the appropriate button on the Attribute Check menu.
' - +'In a similar way to protection, items can affect initiative roles for a character. This can only be achieved automatically if using group or individual initiative:
' +'&{template:RPGMring}&{template:RPGMwandspell}{{title=Rod}}{{name= of Alertness}}specs=[Rod of Alertness,melee,1h,Clubs],[Rod of Alertness,magic,1h,clubs],[Rod of Alertness,magic,1h,clubs],[Rod of Alertness,magic,1h,clubs],[Rod of Alertness,magic,1h,clubs],[Rod of Alertness,magic,1h,clubs],[Rod of Alertness,magic,1h,clubs],[Rod of Alertness,magic,1h,clubs],[Rod of Alertness,Rod,1H,conjuration-summoning]{{splevel=Footman\'s mace/Rod}}weapdata=[w:Rod of Alertness,wt:10,st:Rod,init+:-1]{{school=Conjuration/Summoning}}tohitdata= ... ignore the rest for now...
All of the rod\'s protective functions require one charge. The animate object power require one additional charge, so, if all of the rod\'s protective devices are utilized at once, two charges are expended.
The rod can be recharged by a priest of 16th level or higher, as long as at least one charge remains in the rod when the recharging is attempted.}}{{use=Taking in hand as a weapon will improve initiative scores by 1 automatically. surprise bonus is manual, and allow use of the detection capabilities.
- selecting any of the detect buttons does not use a charge but will display the specifications of the spell and allow its effects to occur.
- when invoking alertness, the player should use the 120ft radius button to set the area of effect.
- selecting the light button should be used by the dm when alertness is triggered to point the cone of light in the right direction.
- the player should then select the 20ft radius button to show the aoe of the prayer
- then select the prayer spell button, which will expend a charge and allow the prayer effect markers to be set.
- selecting the [animate object] button will expend a charge and display the spell specs to allow it to have effect.}}
The weapdata specification includes the attribute init+:-1 which indicates that this item improves the initiative priority roll by 1 (i.e. subtracting 1 from the roll). This will be automatically applied while the item obeys the rules specified for it - see the description of the rules: data attribute in 2.3 above. If the rules are met or no rules are specified for the item, the effect on initiative rolls will apply as long as the item is on the character\'s item list (not in their backpack).
' +'Another data attribute that modifies the initiative roll is the init*: attribute, which multiplies the attack actions each round for the possessing character (does not affect magic item use, spell casting or use of powers). a value above 1 increases attacks per round, and below 1 reduces those attacks.
' + +'Items can also affect the scores required for a Rogue (or other character class) to perform various skills.
' + +'&{template:RPGMitem}{{name=Fine Thieves Tools}}Specs=[Thieves Tools,Miscellaneous,0H,Tools]{{desc=This is a fine set of *Thieve\'s Tools*, with ivory handles and contained in a leather purse. You wonder what creature donated the ivory}}MiscData=[w:Fine Thieves Tools,gp:200,wt:1,ola:5,rta:5,rc:uncharged]{{}}
' + +'Each Rogue skill can be increased or decreased using the following data tags:
' + +'ppa:[+/-]# | Adjustment to the pick pocket skill (# can be a calculation) |
---|---|
ola:[+/-]# | Adjustment to the open locks skill (# can be a calculation) |
rta:[+/-]# | Adjustment to the find/remove traps skill (# can be a calculation) |
msa:[+/-]# | Adjustment to the move silently skill (# can be a calculation) |
hsa:[+/-]# | Adjustment to the hide in shadows skill (# can be a calculation) |
dna:[+/-]# | Adjustment to the detect noise skill (# can be a calculation) |
cwa:[+/-]# | Adjustment to the climb walls skill (# can be a calculation) |
rla:[+/-]# | Adjustment to the read languages skill (# can be a calculation) |
lla:[+/-]# | Adjustment to the legend lore skill (# can be a calculation) |
Other magic items might use different structures, and be more complex:
' +'manual | Only reveal when the GM selects to do so using the [Add Item] dialog or the button shown on the item definition (to the GM only) |
---|---|
view | Reveal the hidden item\'s true nature when the player first views the item\'s description once they have it in their possession |
nohide | Reveal the item\'s true nature when it is first used, but not if it is viewed before that |
use | Reveal the item\'s true nature when it is first used, but not if it is viewed before that |
It is possible to create item definitions at have configurable elements, set when the item is first added to a container or a character. This is achieved using the query: attribute in the data section of the item. An example of its use is the Armour of Blending which uses a query to ask which type of armour it really is and what magical plus that armour might grant.
' @@ -5668,6 +6117,15 @@ const libRPGMaster = (() => { // eslint-disable-line no-unused-vars +'The RPGMaster APIs use a number of databases to hold Macros defining races, creatures, character classes, spells, powers and magic items and their effects. Previous versions of the RPGMaster series of APIs held their databases all externally as character sheets: from this version onwards this is not the case for databases supplied with the APIs, which are now held internally to the APIs. However, the AttackMaster or MagicMaster API command --extract-db can be used to extract any or all standard databases to Character Sheets for examination and update. The APIs are distributed with many class, spell, power & magic item definitions, and DMs can add their own character classes, spells, items, weapons, ammo and armour to additional databases in their own database character sheets, with new definitions for database items held in Ability Macros. Additional database character sheets should be named as follows:
' - +'Wizard Spells: | additional databases: MU-Spells-DB-[added name] where [added name] can be replaced with anything you want. |
---|---|
Priest Spells: | additional databases: PR-Spells-DB-[added name] where [added name] can be replaced with anything you want. |
Powers: | additional databases: Powers-DB-[added name] where [added name] can be replaced with anything you want. |
Magic Items: | additional databases: MI-DB-[added name] where [added name] can be replaced with anything you want. |
Character Classes: | additional databases: Class-DB-[added name] where [added name] can be replaced with anything you want. |
Races & Creatures: | additional databases: Race-DB-[added name] where [added name] can be replaced with anything you want. |
Attack Calculations: | additional databases: Attacks-DB-[added name] where [added name] can be replaced with anything you want. |
However: the system will ignore any database with a name that includes a version number of the form "v#.#" where # can be any number or group of numbers e.g. MI-DB v2.13 will be ignored. This is so that the DM can version control their databases, with only the current one (without a version number) being live.
' - +'There can be as many additional databases as you want. Other Master series APIs come with additional databases, some of which overlap - this does not cause a problem as version control and merging unique macros is managed by the APIs.
' - +'Important Note: all Character Sheet databases must have their \'ControlledBy\' value (found under the [Edit] button at the top right of each sheet) set to \'All Players\'. This must be for all databases, both those provided (set by the API) and any user-defined ones. Otherwise, Players will not be able to run the macros contained in them.
' - +'Each added database has a similar structure, with:
' + +'Ability Macros can be whatever the DM wants and can be as simple or as complex as desired. Roll Templates are very useful when defining class, spell, power and magic item ability macros. When a Player or an NPC or Monster views or casts a spell, power or uses a magic item the Magic Master API runs the relevant Ability Macro from the databases as if it had been run by the Player from the chat window. All Roll20 functions for macros are available.
' - +'If you want to replace any Ability Macro provided in any of the databases, you can do so simply by creating an Ability Macro in one of your own databases with exactly the same name as the provided item to be replaced. The API gives preference to Ability Macros in user-defined databases, so yours will be selected in preference to the one provided with the APIs.
' - +'The DM can add Character Class databases as character sheets that have names that start with Class-DB. The Class definitions that come with the installed game-version-specific RPGMaster Library can be extracted to a character sheet and viewed by using the !magic --extract-db Class-DB or !attk --extract-db Class-DB commands. Note: it is best to delete the extracted Class-DB database character sheet after viewing/using, so that the system uses the much faster internal database version. After deleting or changing any character sheet database, always run the !magic --check-db or !attk --check-db command to re-index the databases.
' +'Classes: Class-DB-[added name]
' @@ -5730,8 +6176,7 @@ const libRPGMaster = (() => { // eslint-disable-line no-unused-vars +'The Ability Macro for a Class may look something like this:
' +'&{template:RPGMclass}{{name=Thief}}{{subtitle=Rogue Class}}{{Min Abilities=Dex:[[9]]}}{{Race=Any}}{{Hit Dice=1d6}}{{Alignment=Any not Lawful}}Specs=[Thief,RogueClass,0H,Rogue]{{=**Powers**}}{{1st Level=Thieving Abilities *Pick Pockets, Open Locks, Find/Remove Traps, Move Silently, Hide in Shadows, Detect Noise, Climb Walls,* and *Read Languages* Also, Thieves can *Backstab*}}{{10th Level=Limited ability to use magical & priest scrolls, with 25% chance of backfire}}ClassData=[w:Thief, hd:1d6, align:ng|nn|n|ne|cg|cn|ce, npp:-3, weaps:club|shortblade|dart|handxbow|lasso|shortbow|sling|broadsword|longsword|staff, ac:padded|leather|studdedleather|elvenchain|magicitem|ring|cloak]{{desc=Thieves come in all sizes and shapes, ready to live off the fat of the land by the easiest means possible. In some ways they are the epitome of roguishness.
'
- +'The profession of thief is not honorable, yet it is not entirely dishonorable, either. Many famous folk heroes have been more than a little larcenous -- Reynard the Fox, Robin Goodfellow, and Ali Baba are but a few. At his best, the thief is a romantic hero fired by noble purpose but a little wanting in strength of character. Such a person may truly strive for good but continually run afoul of temptation.}}
&{template:RPGMclass}{{name=Thief}}{{subtitle=Rogue Class}}{{Min Abilities=Dex:[[9]]}}{{Race=Any}}{{Hit Dice=1d6}}{{Alignment=Any not Lawful}}Specs=[Thief,RogueClass,0H,Rogue]{{=**Powers**}}{{1st Level=Thieving Abilities *Pick Pockets, Open Locks, Find/Remove Traps, Move Silently, Hide in Shadows, Detect Noise, Climb Walls,* and *Read Languages* Also, Thieves can *Backstab*}}{{10th Level=Limited ability to use magical & priest scrolls, with 25% chance of backfire}}ClassData=[w:Thief, hd:1d6, align:ng|nn|n|ne|cg|cn|ce, weaps:club|shortblade|fencingblade|dart|handxbow|lasso|shortbow|sling|broadsword|longsword|staff, ac:padded|leather|studdedleather|elvenchainmail|magicitem|ring|cloak, rp:60.30, ppa:15, ola:10, rta:5, msa:10, hsa:5, dna:15, cwa:60, rla:0, lla:0]{{desc=Thieves come in all sizes and shapes, ready to live off the fat of the land by the easiest means possible. In some ways they are the epitome of roguishness.
The profession of thief is not honorable, yet it is not entirely dishonorable, either. Many famous folk heroes have been more than a little larcenous -- Reynard the Fox, Robin Goodfellow, and Ali Baba are but a few. At his best, the thief is a romantic hero fired by noble purpose but a little wanting in strength of character. Such a person may truly strive for good but continually run afoul of temptation.}}
The ability specification for this Rogue class uses a Roll20 Roll Template, in this case defined by the RPGMaster Library (see the documentation for the Library for specifications of this Roll Template), but any Roll Template you desire can be used. The entries in the Roll Template itself can be anything you desire, giving as much or as little information as you want. However, the important elements for the RPGMaster APIs are those highlighted. Each of the elements important to the database are inserted between the elements of the Roll Template, meaning they will not be seen by the player when the macro is run. Generally spaces, hyphens and underscores in the data elements are ignored, and case is not significant. Each element is described below:
' +'Specs = [Character Class, Macro Type, Handedness, Base Class]' +'
The Specs section describes what Character Class and Base Class this is (and tells the APIs that this is a macro of type "Class"). These fields must be in this order. This format is identical for all database items, whether in these databases or others used by the RPGMaster series of APIs. Where there are multiple answers for a field, separate each by \'|\'. Note:Only A-Z, a-z, 0-9, hyphen/minus(-), plus(+), equals(=) point(.) and vertical bar(|) are allowed. Replace any forward slash with hyphen.
' @@ -5798,6 +6243,25 @@ const libRPGMaster = (() => { // eslint-disable-line no-unused-vars +'Rogues, the sub-classes of rogue such as Thieves and Bards, can have the base value defined for their rogue skills, such as picking pockets and opening locks.
' + +'&{template:RPGMclass}{{name=Thief}}{{subtitle=Rogue Class}}{{Min Abilities=Dex:[[9]]}}{{Race=Any}}{{Hit Dice=1d6}}{{Alignment=Any not Lawful}}Specs=[Thief,RogueClass,0H,Rogue]{{=**Powers**}}{{1st Level=Thieving Abilities *Pick Pockets, Open Locks, Find/Remove Traps, Move Silently, Hide in Shadows, Detect Noise, Climb Walls,* and *Read Languages* Also, Thieves can *Backstab*}}{{10th Level=Limited ability to use magical & priest scrolls, with 25% chance of backfire}}ClassData=[w:Thief, hd:1d6, align:ng|nn|n|ne|cg|cn|ce, weaps:club|shortblade|fencingblade|dart|handxbow|lasso|shortbow |sling|broadsword|longsword|staff, ac:padded|leather|studdedleather|elvenchainmail|magicitem|ring|cloak, rp:60.30, ppa:15, ola:10, rta:5, msa:10, hsa:5, dna:15, cwa:60, rla:0, lla:0]{{desc=Thieves come in all sizes and shapes, ready to live off the fat of the land by the easiest means possible. In some ways they are the epitome of roguishness.
The profession of thief is not honorable, yet it is not entirely dishonorable, either. Many famous folk heroes have been more than a little larcenous -- Reynard the Fox, Robin Goodfellow, and Ali Baba are but a few. At his best, the thief is a romantic hero fired by noble purpose but a little wanting in strength of character. Such a person may truly strive for good but continually run afoul of temptation.}}
The base value for rogue skills used by this class of character are specified using the following tags:
' + +'ppa:[+/-]# | Specifies the base skill percentage for pick pockets (+ is beneficial) |
---|---|
ola:[+/-]# | Specifies the base skill percentage for open locks (+ is beneficial) |
rta:[+/-]# | Specifies the base skill percentage for find/remove traps (+ is beneficial) |
msa:[+/-]# | Specifies the base skill percentage for move silently (+ is beneficial) |
hsa:[+/-]# | Specifies the base skill percentage for hide in shadows (+ is beneficial) |
dna:[+/-]# | Specifies the base skill percentage for detect noise (+ is beneficial) |
cwa:[+/-]# | Specifies the base skill percentage for climb walls (+ is beneficial) |
rla:[+/-]# | Specifies the base skill percentage for read languages (+ is beneficial) |
lla:[+/-]# | Specifies the base skill percentage for legend lore (+ is beneficial) |
rp:#.# | Specifies the base.increase for player allocatable skill points |
Not every tag needs to be specified. The default value for any missing specification of a rogue skill is zero.
' + +'A character class can also be granted powers, and these can be specified in the class definition both as text for the Player to read, and also coded so the APIs can read them.
' +'The ClassData specification now has a tag of ns: which specifies a following number of sections enclosed in square brackets (\'[...]\'), each of which defines a single power granted to characters of this class. These sections include the following fields:
' +'ns: | <1> | specifies a following number of sections each of which defines a single power granted to characters of this class |
---|---|---|
cl: | <PW> | specifies the type of granted capability - for Class definitions, this is always PW (standing for Power) |
w: | <text> | the name of the power granted (which should match a definition in the Powers database Powers-DB) |
lv: | <#> | the character level at which they will gain this power |
attk3 | roll,text,[speed],[type],[+tohit] | specification for creature innate attack 3 |
attkmsg | text[$$text][$$text] | message(s) for attacks |
speed | # | creature overall attack speed |
cl | [F/MU/PR/RO/PS]:Class name | Give creature a NPC class |
lv | # | Give crature a NPC level |
When a creature (or race) with these data fields is selected in the Race/Class menu, the CommandMaster API automatically sets all of the respective Character Sheet attributes to the specified values. In fact, all of these data fields can be used with a standard race, but are less useful. Note: values in square brackets are optional and the brackets should not be included if used.
' - +'NPCs are just creatures with more detail on the "non-monster" part of the character sheet, and thus can use many of the same data tags in their definitions as stated above. Equally, Creatures can have many NPC characteristics, as exemplified by the Vampire definition example given above. However, in order for NPCs to be listed as NPCs in the Drag & Drop lists, the database record Class (the second attribute of the Specs=[...] section of the definition) must be NPCCreature, and Creatures must have CreatureRace.
' + +'&{template:RPGMdefault}{{}}Specs=[Human-Abjurer,NPCCreature,2H,Human-Wizard]{{}}RaceData=[w:Human-Abjurer, query:NPClevel, cattr:cl=MU:Abjurer| lv=??0| hp=??0d4r2| wis=15:18]{{}}%{Race-DB|Human-Wizard}{{name=Abjurer}}
' + +'The more NPC-related data tags, which Creatures can also use, are:
' + +'cattr: | attr=value | attr=value | ... | a list of attribute/value pairs, where attr is one of: |
---|---|---|
cl | [F/MU/PR/RO/PS]:Class name | Give creature a NPC class |
lv | # | Give creature a NPC level |
New | The following tags are new in this version | |
str | # | Give NPC strength (# can be a calculation) |
exstr | # | Give NPC extra strength if str evaluates to 18 (# can be a calculation & defaults to 1d100). Does not have to be a Warrior class. |
con | # | Give NPC constitution (# can be a calculation) |
dex | # | Give NPC dexterity (# can be a calculation) |
int | # | Give NPC intelligence (# can be a calculation) |
wis | # | Give NPC wisdom (# can be a calculation) |
chr | # | Give NPC charisma (# can be a calculation) |
npcpp | # | Set relative ratio for pick pokets level mod(# can be a calculation) |
npcol | # | Set relative ratio for open locks level mod(# can be a calculation) |
npcrt | # | Set relative ratio for find/remove traps level mod(# can be a calculation) |
npcms | # | Set relative ratio for move silently level mod(# can be a calculation) |
npchs | # | Set relative ratio for hide in shadows level mod(# can be a calculation) |
npcdn | # | Set relative ratio for detect noise level mod(# can be a calculation) |
npccw | # | Set relative ratio for climb walls level mod(# can be a calculation) |
npcrl | # | Set relative ratio for read languages level mod(# can be a calculation) |
npcll | # | Set relative ratio for legend lore level mod(# can be a calculation) |
For the attribute values (str, con, dex, int, wis, chr) the value is often a range speification of 3:18 which will evaluate to rolling 3d6. However, the lower value of the range may be modified to match the minimum scores for the class and race of the NPC.
' + +'For the rogue skill values (npcpp, npcol, npcrt, npcms, npchs, npcdn, npccw, npcrl, npcll) the value given to each (which can be a calulation, dice roll or range) represents the relative ratio vs. the other specified rogue skill values. The total allowable level points for the NPC is calculated by the APIs, and then allocated across the rogue skills using the specified ratios, so that the total points are fully allocated. Making the ratio values dice roll or range specifications allows for a degree of randomness in the outcome, so no two NPCs using the same definition are the same in rogue skill values.
' + + +'Many creatures have attacks using their claws, bites and other "innate" attacks, and have tough natural armoured skin. However, many humanoid creatures can use normal weapons & armour to attack adventurers and defiend themselves. It is possible to specify the possible weapon combinations and available armour for each creature type, and add a randomness to the selection criteria, as this example shows:
' + +'&{template:RPGMdefault}{{}}Specs=[Drow-Fighter,NPCCreature,0H,Drow]{{}}RaceData=[w:Drow-Fighter, query:NPClevel, align:CE, cattr:cl=F| lv=??0| hp=??0d8r4| str=8:18| con=7:14| dex=8:20| int=9:19| wis=3:18| chr=6:16| mov=12, ns:1],[cl:MI,items:random:1d??0],[cl:AC,items:chain-mail+??1|buckler+??2], [cl:WP,%:50 ,prime:shortsword+??2,offhand:dagger+??2:3], [cl:WP,%:10 ,prime:shortsword+1 < Mastery,offhand:dagger+1], [cl:WP,%:7 ,prime:shortsword+2,offhand:dagger+1:3], [cl:WP,%:5 ,prime:shortsword+1,offhand:dagger+2] , [cl:WP,%:5 ,prime:shortsword+2,offhand:dagger+2], [cl:WP,%:3 ,prime:shortsword+3,offhand:dagger+1:5], [cl:WP,%:1 ,prime:shortsword+3,offhand:dagger+2:3], [cl:WP,%:50 ,prime:shortsword+??2,offhand:dagger+??2,items:hand-crossbow|hand-quarrel+poison:10], [cl:WP,%:10 ,prime:hand-crossbow=%%3,offhand:shortsword+1,items:dagger+1|hand-quarrel+poison:10], [cl:WP,%:4 ,prime:shortsword+2,offhand:dagger+1,items:hand-crossbow|hand-quarrel+poison:10], [cl:WP,%:4 ,prime:hand-crossbow,offhand:shortsword+1,items:dagger+2|hand-quarrel+poison:10],[cl:WP,%:20,prime:Javelin+poison:3 < Specialist,items:dagger+??1:5]{{}}%{Race-DB|Drow}{{name= Fighter}}{{subtitle=Drow}}{{Alignment=Chaotic Evil}}
' +'The additional RaceData datasets highlighted have the following fields:
' +'cl | WP / AC | Type of data in the dataset. WP = weapon, AC = armour |
---|---|---|
ns: | [ = / - ]1 | specifies a following number of sections. An \'=\' ignores all inherited sections. A \'-\' ignores Class definitios for weapons, powers and items |
cl | WP / AC / MI | Type of data in the dataset. WP = weapon, AC = armour, MI = items |
% | # | Chance of dataset being used relative to others of same type (does not have to add up to 100) |
prime | weapon : qty | Name & quantity of weapon to take in Primary hand. Only 1 is taken in-hand, rest of qty held in items |
offhand | weapon : qty | Name & quantity of weapon to take in Off-hand hand. Only 1 is taken in-hand, rest of qty held in items |
both | weapon : qty | Name & quantity of two-handed weapon to take in both hands. Only 1 is taken in-hand, rest of qty held in items |
items | weap/armour:qty | weap/armour:qty | ... | Name & quantity of weapons or armours to store as items of equipment. Armour will automatically contribute to creature armour class |
prime | weapon [: qty][ < prof] | Name, optional quantity, and optional proficiency of weapon to take in Primary hand. Only 1 is taken in-hand, rest of qty held in items |
offhand | weapon [: qty][ < prof] | Name, optional quantity, and optional proficiency of weapon to take in Off-hand hand. Only 1 is taken in-hand, rest of qty held in items |
both | weapon [: qty][ < prof] | Name, optional quantity, and optional proficiency of two-handed weapon to take in both hands. Only 1 is taken in-hand, rest of qty held in items |
items | weap/armour/item[:qty][ < prof] | weap/armour/item[:qty][ < prof] | ... | Name, quantity & proficiency of weapons, armours, or equipment/magic items to store as items of equipment. Armour will automatically contribute to creature armour class. |
items | random:qty | weap/armour/item:qty | ... | Add qty random items from the databases to the NPCs carried items, each of which will affect the NPC appropriately and can immediately be used. |
It is possible to give spell-casting creatures spells as powers - just specify the number per day as 1, and you can even use the MU-[Spell-Name] or PR-[spell-name] syntax as the power name to specify the spells to use (as shown in the Vampire above). However, for larger numbers of spells, and/or to grant random spells, an additional syntax is available:
' +'&{template:RPGMdefault}{{}}RaceData=[w:Frost Giant Witch Doctor,sps:any,cattr:cl=pr:frost-giant-shaman/mu:frost-giant-witch-doctor|lv=7/3,ns:2],[cl:MU,lv:1,w:random|random|random|Detect-Magic],[cl:MU,lv:2,w:random|ESP|Mirror-Image|random|random]{{}}Specs=[Frost-Giant-Witch-Doctor,CreatureRace,2H,Frost-Giant-AC0]{{}}%{Race-DB-Creatures|Frost-Giant-AC0}{{name= Witch Doctor}}{{Section3=**Witch Doctor:** This Frost Giant is a Witch Doctor that can cast spells of a number of wizard spells, and priest spheres of magic:*healing, charm, protection, divination*, or *weather*}}{{desc6=**Frost Giant Witch Doctor:** There is a 20% chance that any band of frost giants will have a shaman (80%) or witch doctor (20%). If the group is led by a jarl, there is an 80% chance for a spell caster. Frost giant shamans are priests of up to 7th level. A shaman can cast normal or reversed spells from the *healing, charm, protection, divination*, or *weather* spheres. Frost giant witch doctors are priest/wizards of up to 7th/3rd level; they prefer spells that can bewilder and confound other giants. Favorite spells include: *unseen servant, shocking grasp, detect magic, ventriloquism, deeppockets, ESP, mirror image,* and *invisibility*.}}
' +'This Witch Doctor has a number of spells specified to be in their spellbook by using the RaceData extension data sets:
' @@ -5918,7 +6423,7 @@ const libRPGMaster = (() => { // eslint-disable-line no-unused-vars +'If a spellbook is specified for a creature in this way, it must be of a class that can cast spells or it will not be able to use them. The creature\'s level as a spellcaster will determine how many spells can be memorised at each level, regardless of how many are given in the spellbooks.
' +'Also, if spellbooks are specified like this the API will automatically memorise spells, selecting spells from the list at random up to the correct number for the creature\'s level. If a creature can cast Wizard spells and no spellbooks are specified, random spells will be memorised from all possible wizard spells that are valid for the schools that caster can cast. The GM (or other controlling player) can always use the Spells Menu to memorise different spells, and the GM can use the [Token Setup] / [Add Spells & Powers] dialog to change the spellbooks.
' +'If the creature can cast Priest spells, the creature will be granted a spellbook of all priest spells that are valid for the spheres it can cast, and random spells from this list automatically memorised from this list. The specification can override this behaviour by specifying particuler priest spells to have in the spellbook at a specific level using [cl:PR,lv:#,w:....]
. Alternatiely, if the w: is followed by an empty string e.g. [cl:PR,lv:#,w:]
, then the correct number of random priest spells will be memorised at each level, but only those memorised spells will be written to the spellbook. An example of this is for dragons, as according to the Monsterous Manual they only know an exclectic mix of spells they have picked up as they age and are not granted spells by a god.
Some creatures, such as Dragons, have multiple forms and also vary within a single form. For example, a Dragon can be Red, Blue, White, Gold, Silver, Crystal, ... etc. Not only that, but each of these dragons\' powers grow with age from Hatchling through Juvenile, Adulthood, to Venerable, Wyrm & Great Wyrm. Rather than create a definition for each age and colour combination (which would result in 180 definitions just for the standard dragons!) it is possible to add a query presented to the GM when selecting the type of creature to drag & drop. This query can specify data to be factored into calculations in the RaceData specification dependent on the selection made.
' +'&{template:RPGMdefault}{{title=Red}}{{name=Dragon}}Specs=[Dragon,DragonRace,2H,Creature]{{subtitle=Dragon}}RaceData=[w:Red Dragon, query:What Age?|Hatchling%%1%%-6|Very Young%%2%%-4|Young%%3%%-2|Juvenile%%4%%0|Young Adult%%5%%1|Adult%%6%%2|Mature Adult%%7%%3|Old%%8%%4|Very Old%%9%%5|Venerable%%10%%6|Wyrm%%11%%7|Great Wyrm%%12%%8, align:CE, ac:none, cattr:int=15:16|mov=9|fly=30C|jump=3|ac=1-??1|age=??0:??1|hd=(11+??1)d8r1|mr=(v(^((??1-4);0);1)*??1*5)|cl=mu:red-dragon/pr:red-dragon|lv=8+??1/8+??1|spellsp=1|thac0=11-??1|tohit=??2|dmg=??1|size=G|attk1=1d10:Claw x 2 or Claw+Kick:0:S|attk2=3d10:Bite:0:P|attk3=2d10:Tail Swipe:0:B|attkmsg=Remember powers such as *Dragon Fear; Wing Buffet; Snatch; Plummet;* and *Spell Casting*$$Remember powers such as *Dragon Fear; Wing Buffet; Snatch; Plummet;* and *Spell Casting*$$\lbrak;Show the radius\rbrak;\lpar;!rounds ~~aoe `{selected¦token_id}¦arc180¦0¦80¦160¦red\rpar; then up to \lbrak;\lbrak;`{selected¦age|max}\rbrak;\rbrak; opponents in the area take damage and Save vs. Petrification with the penalty shown below or be \lbrak;Stunned\rbrak;\lpar;!rounds ~~target area¦`{selected¦token_id}¦`{target¦Select the stunned creature¦token_id}¦Stunned¦\lbrak;[1+1d4]\rbrak;¦-1¦Stunned by a dragon tail slap¦back-pain\rpar; for 1d4+1 rounds., spattk:*Dragon Fear; Wing Buffet; Snatch; Plummet;* and *Spell Casting*, spdef:Magic resistance \lbrak;\lbrak;({ { { {(@{selected|age|max}-4)},{0} }kh1}, {1} }kl1) * (@{selected|age|max}+1) * 5\rbrak;\rbrak;%, ns:1],[cl:PW,w:MU-Affect-Normal-Fires,pd:3,sp:1],[cl:PW,w:MU-Pyrotechnics,pd:3,sp:1],[cl:PW,w:PR-Heat-Metal,pd:1,sp:1],[cl:PW,w:MU-Suggestion,pd:1,sp:1],[cl:PW,w:MU-Hypnotism,pd:1,sp:1],[cl:PW,w:Detect-Gems-Kind+Number,pd:3,sp:1],[cl:PW,w:Red-Dragon-Breath,pd:-1,sp:1],[cl:PW,w:PW-Snatch,lv:14,pd:-1,sp:0],[cl:PW,w:PW-Plummet,pd:-1,sp:0],[cl:PW,w:PW-Wing-Buffet,lv:14,pd:-1,sp:0],[cl:PW,w:PW-Stall,pd:-1,sp:0],[cl:PW,w:PW-Dragon-Fear,lv:14,pd:-1,sp:0],[cl:PR,lv:1,w:],[cl:PR,lv:2,w:]{{Section=**Attributes**}}{{Intelligence=Exceptional (15-16)}}{{AC=Varies with age, adult red dragon is AC -5}}{{Alignment=Chaotic Evil}}{{Move=9, FL 30(C), Jump 3}}{{Hit Dice=Varies with age, adult red dragon is 15 HD}}{{THAC0=Varies with age, adult red dragon is 5}}{{Section1=**Attacks:** ToHit and damage bonus varies with age, adult red dragon is +2 and +6. 2 x Claws for 1d10 HP each, possibly with 1 or 2 kicks for 1d10 each, bite for 3d10, and tail slap for 2d10 and possible *stun* within an area varying with age. Several other attacks possible - see *Powers*}}{{Languages=*Red Dragon* and *Dragon Common*, and 16% if hatchlings (+5% per age level) can perform universal communication with any intelligent creature}}{{Size=G, varies with age}}{{Life Expectancy=Possibly in excess of 1,000 years. Adult dragons are considered between 100 and 200 years old}}{{Section2=**Powers**}}{{Breath Weapon=A cone of flame, 90ft long, 5ft wide at dragon and spreading to 30ft wide. Damage varies by age from 2d10+1 to 24d10+12. Save vs. Breath Weapon to take half damage}}{{Fear=Can inspire fear in creatures that see the dragon: affect varies with the level / HD of the viewing creature.}}{{Spell Casting=Knows a number of random wizard and priest spells cast at a level from 10 to 21 varying with age. All spells are cast at a speed of 1 segment regardless of the spell}}{{Spell-like Powers=*Young* dragons can *Affect Normal Fires* x 3 per day, *Juveniles* gain *Pyrotechnics* x3 per day, *Adult* gains *Heat Metal* x 1 per day, *Old* gain *Suggestion* x 1 per day, *Very Old* gain *Hypnotism* x 1 per day, and *Venerable* gain *Detect Gems, Kind & Number* x 3 per day}}{{Special Attacks=*Snatch, Plummet, Stall*, and *Wing Buffet*}}{{Section4=**Special Advantages**}}{{Section5=Its a Dragon!}}{{Section6=**Special Disadvantages**}}{{Section7=None}}{{Section9=**Description**}}{{desc7=Dragons are an ancient, winged reptilian race. They are known and feared for their size, physical prowess, and magical abilities. The oldest dragons are among the most powerful creatures in the world.
Most dragons are identified by the color of their scales. All subspecies of dragons have 12 age categories, and gain more abilities and greater power as they age. Dragons range in size from several feet upon hatching to more than 100 feet, after they have attained the status of great wyrm. The exact size varies according to age and subspecies... }}
In the case of dragons, this query asks the GM for the age to make the dragon, and sets two values based on the age to use in calculations (seen above in red). The query will be applied to all creatures of the Creature Database-Class (Specs definition field 2), in this case any DragonRace creature, even if the roll query is specified in the definition of only 1 such creature. Different roll queries can be specified for different Creature Database-Classes: you can make up your own db-classes to differentiate them.
' @@ -6012,40 +6517,16 @@ const libRPGMaster = (() => { // eslint-disable-line no-unused-vars +'', }, AttacksDatabase_Help:{name:'Attacks Database Help', - version:1.05, + version:1.06, avatar:'https://s3.amazonaws.com/files.d20.io/images/257656656/ckSHhNht7v3u60CRKonRTg/thumb.png?1638050703', bio:'The RPGMaster APIs use a number of databases to hold Macros defining character classes, spells, powers and magic items and their effects. Previous versions of the RPGMaster series of APIs held their databases all externally as character sheets: from this version onwards this is not the case for databases supplied with the APIs, which are now held internally to the APIs. However, the AttackMaster or MagicMaster API command --extract-db can be used to extract any or all standard databases to Character Sheets for examination and update. The APIs are distributed with many class, spell, power & magic item definitions, and DMs can add their own character classes, spells, items, weapons, ammo and armour to additional databases in their own database character sheets, with new definitions for database items held in Ability Macros. Additional database character sheets should be named as follows:
' - +'Wizard Spells: | additional databases: MU-Spells-DB-[added name] where [added name] can be replaced with anything you want. |
---|---|
Priest Spells: | additional databases: PR-Spells-DB-[added name] where [added name] can be replaced with anything you want. |
Powers: | additional databases: Powers-DB-[added name] where [added name] can be replaced with anything you want. |
Magic Items: | additional databases: MI-DB-[added name] where [added name] can be replaced with anything you want. |
Character Classes: | additional databases: Class-DB-[added name] where [added name] can be replaced with anything you want. |
Character Races: | additional databases: Race-DB-[added name] where [added name] can be replaced with anything you want. |
Attack Calculations: | additional databases: Attacks-DB-[added name] where [added name] can be replaced with anything you want. |
Fighting Styles: | additional databases: Styles-DB-[added name] where [added name] can be replaced with anything you want. |
However: the system will ignore any database with a name that includes a version number of the form "v#.#" where # can be any number or group of numbers e.g. MI-DB v2.13 will be ignored. This is so that the DM can version control their databases, with only the current one (without a version number) being live.
' - +'There can be as many additional databases as you want. Other Master series APIs come with additional databases, some of which overlap - this does not cause a problem as version control and merging unique macros is managed by the APIs.
' - +'Important Note: all Character Sheet databases must have their \'ControlledBy\' value (found under the [Edit] button at the top right of each sheet) set to \'All Players\'. This must be for all databases, both those provided (set by the API) and any user-defined ones. Otherwise, Players will not be able to run the macros contained in them.
' - +'Each added database has a similar structure, with:
' - +'However, as with all other Databases in the RPGMaster Suite of APIs, if the Ability Macros are correctly set up using the formats detailed in the Help Documentation, the AttackMaster API command !attk --check-db database-name will check the database and set up all other aspects for you, including the correct Custom Attributes and List entries.
' - +'Ability Macros can be whatever the DM wants and can be as simple or as complex as desired. Roll Templates are very useful when defining class, spell, power and magic item ability macros, and are an essential part of Attack Templates. When a Player or an NPC or Monster makes an attack, the AttackMaster API runs the relevant Ability Macro from the databases as if it had been run by the Player from the chat window. All Roll20 functions for macros are available.
' - +'If you want to replace any Ability Macro provided in any of the databases, you can do so simply by creating an Ability Macro in one of your own databases (a database with the same root name) with the Ability Macro you create having exactly the same name as the provided item to be replaced. The API gives preference to Ability Macros in user-defined databases, so yours will be selected in preference to the one provided with the APIs.
' + +'In order to understand the Attacks Database, it is first important to understand how attacks are executed by the AttackMaster API. Under some (if not all) versions of D&D, and especially AD&D 2nd Edition, attacks are quite complex involving many factors that can vary from moment to moment. Some say that this is why they prefer RPG systems that require less maths and are faster to execute, that the complexity of the AD&D2e combat system interrupts the flow of play. The AttackMaster API handles attacks in such a way as to hide as much of that complexity from the players as possible, and thus allow game-play to flow and players to concentrate on the unfolding story.
' +'In order for the API to achieve this, it must evaluate many factors "on the fly" such as current magical effects in place (generally or on individuals), the current attributes of a character (which can vary as they are affected by game play), the type, range and properties of the weapon combinations used at that point in time for that particular attack, and the effects of the race, class, level and proficiency of the character, among several others. Given that these factors can vary even during a single round, each attack must be fully evaluated from scratch each time it is made.
' @@ -6272,30 +6753,16 @@ const libRPGMaster = (() => { // eslint-disable-line no-unused-vars +'The RPGMaster APIs use a number of databases to hold Ability Macros defining rules, conditions and benefits of various styles of fighting with weapons and shields, defined in the RPGMaster Library for various game versions. Databases supplied with the APIs are held internally to the APIs. However, the AttackMaster or MagicMaster API command --extract-db can be used to extract any or all standard databases to Character Sheets for examination and update. DMs can add their own fighting style definitions to additional databases held as Character Sheets. Additional databases should be named as follows:
' - +'Fighting Styles: | additional databases: Styles-DB-[added name] where [added name] can be replaced with anything you want. |
However: the system will ignore any database with a name that includes a version number of the form "v#.#" where # can be any number or group of numbers e.g. Styles-DB v2.13 will be ignored. This is so that the DM can version control their databases, with only the current one (without a version number) being live.
' - +'There can be as many additional databases as you want. Other Master series APIs come with additional databases, some of which overlap - this does not cause a problem as version control and merging unique macros is managed by the APIs.
' - +'Important Note: all Character Sheet databases must have their \'ControlledBy\' value (found under the [Edit] button at the top right of each sheet) set to \'All Players\'. This must be for all databases, both those provided (set by the API) and any user-defined ones. Otherwise, Players will not be able to run the macros contained in them.
' - +'Important Note: databases extracted using the --extract-db command will be able to be edited, but will also slow the system down - the versions held internally in the APIs are much faster for the system to access. Once any extracted database has been examined, it is best to delete them and use the --check-db to re-index the databases so the system operates as fast as possible.
' - +'Each database has a similar structure, with:
' - +'Note: a DM only needs to program the Ability Macro using the formats shown in the next section, and then run the !attk --check-db or !magic --check-db command, which will correctly parse the ability macro and set the rest of the database entries as needed.
' - +'Ability Macros can be whatever the DM wants and can be as simple or as complex as desired, as long as they include the required information specified below. Roll Templates are very useful when defining ability macros - the RPGMaster Library provides several new Roll Templates that do not rely on any particular Character Sheet: RPGMdefault is the most relevant. See the RPGMaster Library help handout for further information. When a Player or an NPC or Monster views the specifications of a fighting style the APIs run the relevant Ability Macro from the database as if it had been run by the Player from the chat window. All Roll20 functions for macros are available.
' - +'If you want to replace any style provided in the databases, you can do so simply by creating an Ability Macro in one of your own databases with exactly the same name as the provided item to be replaced. The API gives preference to Ability Macros in user-defined databases, so yours will be selected in preference to the one provided with the APIs.
' + +'Fighting style databases have names that start with Styles-DB, and can have anything put at the end, though those with version numbers of the form v#.# as part of the name will be ignored.
' +'As previously stated, each style definition has 3 (or 4) parts in the database (see Section 1): an Ability Macro with a name that is unique and matches the style, an Attribute with the name of the Ability Macro preceded by "ct-", a listing in the database character sheet of the ability macro name separated by \'|\' along with other fighting styles. The quickest way to understand these entries is to examine existing entries. Do extract the root databases and take a look (but remember to delete them after exploring the items in them, so as not to slow the system down unnecessarily).
' @@ -6394,10 +6861,16 @@ const libRPGMaster = (() => { // eslint-disable-line no-unused-vars STYLES: {prefix:'Style_', tableDef:fields.Style_table}, GEAR: {prefix:'Gear_', tableDef:fields.Gear_table}, STORED: {prefix:'StoredGear_', tableDef:fields.StoredGear_table}, + POTIONS:{prefix:'Items_', tableDef:fields.Items_table}, DUSTS: {prefix:'Dusts_', tableDef:fields.Dusts_table}, + MISC: {prefix:'Misc_', tableDef:fields.Misc_table}, + WANDS: {prefix:'Wands_', tableDef:fields.Wands_table}, SCROLLS:{prefix:'Scrolls_', tableDef:fields.Scrolls_table}, + GEAR: {prefix:'Gear_', tableDef:fields.Gear_table}, + STORED: {prefix:'Stored_', tableDef:fields.Stored_table}, INIT: {prefix:'InitMagic_', tableDef:fields.InitMagic_table}, - SAVES: {prefix:'SaveMod_', tableDef:fields.SaveMod_table}, + SAVES: {prefix:'Mods_', tableDef:fields.Mods_table}, + MODS: {prefix:'Mods_', tableDef:fields.Mods_table}, // WEAP: {prefix:'Weap_', tableDef:fields.Weap_table}, MONWEAP:{prefix:'MonWeap_', tableDef:fields.MonWeap_table}, ALTWIZ: {prefix:'AltSpells_', tableDef:fields.AltWizSpells_table}, @@ -6432,6 +6905,7 @@ const libRPGMaster = (() => { // eslint-disable-line no-unused-vars dmitem: {type:'dmitem',field:fields.ItemDMList}, equipment: {type:'equipment',field:fields.ItemEquipList}, light: {type:'equipment',field:fields.ItemEquipList}, + treasure: {type:'treasure',field:fields.ItemTreasureList}, attackmacro: {type:'attack',field:fields.ItemAttacksList}, style: {type:'style',field:fields.ItemWeaponList}, ability: {type:'ability',field:fields.ItemAbilitiesList}, @@ -6463,6 +6937,7 @@ const libRPGMaster = (() => { // eslint-disable-line no-unused-vars creaturerace: {type:'creature',field:fields.RaceCreatureList,query:''}, creaturehrrace: {type:'creature',field:fields.RaceCreatureList,query:''}, creaturekitrace:{type:'creature',field:fields.RaceCreatureList,query:''}, + npccreature: {type:'npc',field:fields.RaceNPCList,query:''}, container: {type:'container',field:fields.ContainerList,query:''}, }; const spTypeLists = Object.freeze({ @@ -6521,6 +6996,53 @@ const libRPGMaster = (() => { // eslint-disable-line no-unused-vars [20,20,20,19,19,18,18,17,17,16,16,15,15,14,14,13,13,12,12,11,11], ]; + const exstrIndex = [0,50,75,90,99,100]; + const numNames = ['','1st','2nd','3rd','4th','5th','6th','7th','8th','9th']; + + const attrMods = { + str: { + hit: {field:fields.Strength_hit,data:[0,-5,-3,-3,-2,-2,-1,-1,0,0,0,0,0,0,0,0,0,1,1,1,2,2,2,3,3,3,4,4,5,6,7]}, + dmg: {field:fields.Strength_dmg,data:[0,-4,-2,-1,-1,-1,0,0,0,0,0,0,0,0,0,0,1,1,2,3,3,4,5,6,7,8,9,10,11,12,14]}, + weight: {field:fields.MaxWeight,data:[0,1,1,5,10,10,20,20,35,35,40,40,45,45,55,55,70,85,110,135,160,185,235,335,485,535,635,785,935,1235,1535]}, + press: {field:fields.MaxPress,data:[0,3,5,10,25,25,55,55,90,90,115,115,140,140,170,170,195,220,255,280,305,330,380,480,640,700,810,970,1130,1440,1750]}, + opendoor: {field:fields.OpenDoors,data:[[0,1,1,2,3,3,4,4,5,5,6,6,7,7,8,8,9,10,11,12,13,14,15,16,16,17,17,18,18,19,19], + [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,6,8,10,12,14,16,17,18]]}, + bendbars: {field:fields.BendBars,data:[0,0,0,0,0,0,0,0,1,1,2,2,4,4,7,7,10,13,16,20,25,30,35,40,50,60,70,80,90,95,99]}, + }, + dex: { + react: {field:fields.Dex_react,data:[0,-6,-4,-3,-2,-1,0,0,0,0,0,0,0,0,0,0,1,2,2,3,3,4,4,4,5,5]}, + missile: {field:fields.Dex_missile,data: [0,-6,-4,-3,-2,-1,0,0,0,0,0,0,0,0,0,0,1,2,2,3,3,4,4,4,5,5]}, + defadj: {field:fields.Dex_acBonus,data:[0,5,5,4,3,2,1,0,0,0,0,0,0,0,0,-1,-2,-3,-4,-4,-4,-5,-5,-5,-6,-6]}, + }, + con: { + hpadj: {field:fields.HPconAdj,data:[0,-3,-2,-2,-1,-1,-1,0,0,0,0,0,0,0,0,1,2,2,2,2,2,2,2,2,2,2]}, + fighthp: {field:fields.HPconAdj,data:[0,-3,-2,-2,-1,-1,-1,0,0,0,0,0,0,0,0,1,2,3,4,5,5,6,6,6,7,7]}, + syshock: {field:fields.SystemShock,data:[0,25,30,35,40,45,50,55,60,65,70,75,80,85,88,90,95,97,99,99,99,99,99,99,99,100]}, + resurrect: {field:fields.ResSurvive,data:[0,30,35,40,45,50,55,60,65,70,75,80,85,90,92,94,96,98,100,100,100,100,100,100,100,100]}, + poison: {field:fields.ConPoison,data:[0,-2,-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,2,3,3,4]}, + regen: {field:fields.Regenerate,data:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,5,4,3,2,1]}, + }, + int: { + lang: {field:fields.Languages,data:[0,0,1,1,1,1,1,1,1,1,2,2,2,3,3,4,4,5,6,7,8,9,10,11,12,15,20]}, + splev: {field:fields.SpellLevel,data:[0,0,0,0,0,0,0,0,0,4,5,5,6,6,7,7,8,8,9,9,9,9,9,9,9,9]}, + learn: {field:fields.LearnSpell,data:[0,0,0,0,0,0,0,0,0,35,40,45,50,55,60,65,70,75,85,95,96,97,98,99,100,100]}, + perlev: {field:fields.SpellMax,data:[0,0,0,0,0,0,0,0,0,6,7,7,7,9,9,11,11,14,18,99,99,99,99,99,99,99]}, + illusion: {field:fields.IllusionImmune,data:['','','','','','','','','','','','','','','','','','','','1st','2nd','3rd','4th','5th','6th','7th']}, + }, + wis: { + wisdef: {field:fields.Wisdom_defAdj,data:[0,-6,-4,-3,-2,-1,-1,-1,0,0,0,0,0,0,0,1,2,3,4,4,4,4,4,4,4,4]}, + wisbonus: {field:fields.BonusSpells,data:[[0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,2,3,4,3,4,5,5,6,6,7], + [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,3,4,1,5,6]]}, + fail: {field:fields.SpellFail,data:[0,80,60,50,45,40,35,30,25,20,15,10,5,0,0,0,0,0,0,0,0,0,0,0,0,0]}, + immune: {field:fields.SpellImmune,data:['','','','','','','','','','','','','','','','','','','','Cause Fear,Charm Person,Command,Friends,Hypnotism','Forget,Hold Person,Ray of Enfeeblement,Scare,Fear','Charm Monster,Confusion,Emotion,Fumble,Suggestion','Chaos,Feeblemind,Hold Monster,Magic Jar,Quest,Geas,Mass Suggestion,Rod of Rulership','Antipathy/Sympathy,Death Spell,Mass Charm']}, + }, + chr: { + hench: {field:fields.ChrHench,data:[0,0,1,1,1,2,2,3,3,4,4,4,5,5,6,7,8,10,15,20,25,30,35,40,45,50]}, + loyalty: {field:fields.ChrLoyalty,data:[0,-8,-7,-6,-5,-4,-3,-2,-1,0,0,0,0,0,1,3,4,6,8,10,12,14,16,18,20,20]}, + react: {field:fields.ChrReact,data:[0,-7,-6,-5,-4,-3,-2,-1,0,0,0,0,0,1,2,3,5,6,7,8,10,15,20,25,30,35,40,45,50]}, + }, + }; + var saveFormat = { Saves: { Paralysis: {save:fields.Saves_paralysis,mod:fields.Saves_modParalysis,mon:fields.Saves_monParalysis,index:0,roll:'1d20',tag:'par'}, @@ -6553,25 +7075,38 @@ const libRPGMaster = (() => { // eslint-disable-line no-unused-vars }; const rogueSkills = { - pickpockets: {name:'Pick_Pockets',save:['ppt','current'],roll:'1d100',tag:'pp',factors:['ppb','ppr','ppd','ppk','ppa','ppm','ppl'],gmrolls:true,success:'You got the item!',failure:'You can try again, unless you are caught!'}, + pickpockets: {name:'Pick_Pockets',save:['ppt','current'],roll:'{ {1d101},{100} }kl1',tag:'pp',factors:['ppb','ppr','ppd','ppk','ppa','ppm','ppl'],gmrolls:true,success:'You got the item!',failure:'You can try again, unless you are caught!'}, openlocks: {name:'Open_Locks',save:['olt','current'],roll:'1d100',tag:'ol',factors:['olb','olr','old','olk','ola','olm','oll'],gmrolls:false,success:'Click! After [[1d10]] rounds the lock opens',failure:'Hard luck. [[1d10]] rounds wasted. You can try once per experience level'}, - findtraps: {name:'Find_Traps',save:['rtt','current'],roll:'1d100',tag:'rt',factors:['rtb','rtr','rtd','rtk','rta','rtm','rtl'],gmrolls:true,success:'After [[1d10]] rounds you know the general nature of any trap but not exact detail',failure:'[[1d10]] rounds go by. Try again at next level'}, - removetraps: {name:'Remove_Traps',save:['rtt','current'],roll:'1d100',tag:'rt',factors:['rtb','rtr','rtd','rtk','rta','rtm','rtl'],gmrolls:true,success:'After [[1d10]] rounds you have successfully removed the trap',failure:'[[1d10]] rounds go by unsuccessfully. 96-00 triggers trap! Try again at next level'}, - movesilently: {name:'Move_Silently',save:['mst','current'],roll:'1d100',tag:'ms',factors:['msb','msr','msd','msk','msa','msm','msl'],gmrolls:true,success:'Move at 1/3 rate. Gain -2 to surprise only if also unseen',failure:'Movement still reduced to 1/3'}, - hideinshadows: {name:'Hide_in_Shadows',save:['hst','current'],roll:'1d100',tag:'hs',factors:['hsb','hsr','hsd','hsk','hsa','hsm','hsl'],gmrolls:true,success:'Does not work in darkness. Hidden only while motionless except small movements (draw weapon, drin potion etc). Cannot be seen with infravision except in darkness. "See Invisible" will see character',failure:'The character is not hidden'}, - detectnoise: {name:'Detect_Noise',save:['dnt','current'],roll:'1d100',tag:'dn',factors:['dnb','dnr','dnd','dnk','dna','dnm','dnl'],gmrolls:true,success:'In silent surrounds & not wearing head-gear, sounds are heard',failure:'Even in silent surrounds & not wearing head-gear, nothing is heard'}, + findtraps: {name:'Find_Traps',save:['rtt','current'],roll:'{ {1d101},{100} }kl1',tag:'rt',factors:['rtb','rtr','rtd','rtk','rta','rtm','rtl'],gmrolls:true,success:'After [[1d10]] rounds you know the general nature of any trap but not exact detail',failure:'[[1d10]] rounds go by. Try again at next level'}, + removetraps: {name:'Remove_Traps',save:['rtt','current'],roll:'{ {1d101},{100} }kl1',tag:'rt',factors:['rtb','rtr','rtd','rtk','rta','rtm','rtl'],gmrolls:true,success:'After [[1d10]] rounds you have successfully removed the trap',failure:'[[1d10]] rounds go by unsuccessfully. 96-00 triggers trap! Try again at next level'}, + movesilently: {name:'Move_Silently',save:['mst','current'],roll:'{ {1d101},{100} }kl1',tag:'ms',factors:['msb','msr','msd','msk','msa','msm','msl'],gmrolls:true,success:'Move at 1/3 rate. Gain -2 to surprise only if also unseen',failure:'Movement still reduced to 1/3'}, + hideinshadows: {name:'Hide_in_Shadows',save:['hst','current'],roll:'{ {1d101},{100} }kl1',tag:'hs',factors:['hsb','hsr','hsd','hsk','hsa','hsm','hsl'],gmrolls:true,success:'Does not work in darkness. Hidden only while motionless except small movements (draw weapon, drin potion etc). Cannot be seen with infravision except in darkness. "See Invisible" will see character',failure:'The character is not hidden'}, + detectnoise: {name:'Detect_Noise',save:['dnt','current'],roll:'{ {1d101},{100} }kl1',tag:'dn',factors:['dnb','dnr','dnd','dnk','dna','dnm','dnl'],gmrolls:true,success:'In silent surrounds & not wearing head-gear, sounds are heard',failure:'Even in silent surrounds & not wearing head-gear, nothing is heard'}, climbwalls: {name:'Climb_Walls',save:['cwt','current'],roll:'1d100',tag:'cw',factors:['cwb','cwr','cwd','cwk','cwa','cwm','cwl'],gmrolls:false,success:'Can climb up to 100ft in 10 rounds, then roll again',failure:'Can\'t start or is stuck where currently is. Try again somewhere significantly different'}, readlanguages: {name:'Read_Languages',save:['rlt','current'],roll:'1d100',tag:'rl',factors:['rlb','rlr','rld','rlk','rla','rlm','rll'],gmrolls:false,success:'Can understand about value2% of the meaning',failure:'Not understandable at all. Try again at next level'}, - legendlore: {name:'Legend_Lore',save:['ibt','current'],roll:'1d100',tag:'ll',factors:['ibb','ibr','ibd','ibk','iba','ibm','ibl'],gmrolls:false,success:'In [[1d10]] rounds of examination you learn some general information',failure:'[[1d10]] rounds of examination reveal nothing'}, + legendlore: {name:'Legend_Lore',save:['ibt','current'],roll:'1d100',tag:'ib',factors:['ibb','ibr','ibd','ibk','iba','ibm','ibl'],gmrolls:false,success:'In [[1d10]] rounds of examination you learn some general information',failure:'[[1d10]] rounds of examination reveal nothing'}, }; const thiefSkillFactors = ['Base','Race','Dexterity','Kit','Armour','Magic','Level']; + const rogueDexMods = [{lv:9,pp:-15,ol:-10,rt:-10,ms:-20,hs:-10,dn:0,cw:0,rl:0,ll:0}, + {lv:10,pp:-10,ol:-5,rt:-10,ms:-15,hs:-5,dn:0,cw:0,rl:0,ll:0}, + {lv:11,pp:-5,ol:0,rt:-5,ms:-10,hs:0,dn:0,cw:0,rl:0,ll:0}, + {lv:12,pp:0,ol:0,rt:0,ms:-5,hs:0,dn:0,cw:0,rl:0,ll:0}, + {lv:13,pp:0,ol:0,rt:0,ms:0,hs:0,dn:0,cw:0,rl:0,ll:0}, + {lv:14,pp:0,ol:0,rt:0,ms:0,hs:0,dn:0,cw:0,rl:0,ll:0}, + {lv:15,pp:0,ol:0,rt:0,ms:0,hs:0,dn:0,cw:0,rl:0,ll:0}, + {lv:16,pp:0,ol:5,rt:0,ms:0,hs:0,dn:0,cw:0,rl:0,ll:0}, + {lv:17,pp:5,ol:10,rt:0,ms:5,hs:5,dn:0,cw:0,rl:0,ll:0}, + {lv:18,pp:10,ol:15,rt:5,ms:10,hs:10,dn:0,cw:0,rl:0,ll:0}, + {lv:19,pp:15,ol:20,rt:10,ms:15,hs:15,dn:0,cw:0,rl:0,ll:0}, + ]; + var ordMU =['wizard', 'magicuser', 'mage', 'mu']; - var specMU=['abjurer', + var specMU = ['abjurer', 'conjurer', 'diviner', 'enchanter', @@ -6580,7 +7115,7 @@ const libRPGMaster = (() => { // eslint-disable-line no-unused-vars 'necromancer', 'transmuter']; - const wisdomSpells=[ + const wisdomSpells = [ [0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,2,2,2,2,3,3,3,3,4,4], [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,2,2,2,3,3,3,3,3], [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,2,2,3,3,3,3], @@ -6705,7 +7240,7 @@ const libRPGMaster = (() => { // eslint-disable-line no-unused-vars human: {att:'con',par:0,poi:0,dea:0,rod:0,sta:0,wan:0,pet:0,pol:0,bre:0,spe:0,str:0,con:0,dex:0,int:0,wis:0,chr:0}, creature: {att:'con',par:0,poi:0,dea:0,rod:0,sta:0,wan:0,pet:0,pol:0,bre:0,spe:0,str:0,con:0,dex:0,int:0,wis:0,chr:0}, }; - const xlateSave = {att:'Attribute',par:'Paralysis',poi:'Poison',dea:'Death',rod:'Rod',sta:'Staff',wan:'Wand',pet:'Petrify',pol:'Polymorph',bre:'Breath',spe:'Spell',str:'Strength',con:'Constitution',dex:'Dexterity',int:'Intelligence',wis:'Wisdom',chr:'Charisma'}; + const xlateSave = {att:'Attribute',par:'Paralysis',poi:'Poison',dea:'Death',rod:'Rod',sta:'Staff',wan:'Wand',pet:'Petrify',pol:'Polymorph',bre:'Breath',spe:'Spell',str:'Strength',con:'Constitution',dex:'Dexterity',int:'Intelligence',wis:'Wisdom',chr:'Charisma',pp:'Pick Pockets',ol:'Open Locks',rt:'Find/Remove Traps',ms:'Move Silently',hs:'Hide in Shadows',dn:'Detect Noise',cw:'Climb Walls',rl:'Read Languages',ib:'Legend Lore'}; var classNonProfPenalty = {}; var raceToHitMods = { elf: [['bow',1],['longsword',1],['shortsword',1]], @@ -6847,7 +7382,7 @@ const libRPGMaster = (() => { // eslint-disable-line no-unused-vars const defaultAs = 'RPGMaster'; const archive = false; const use3Ddice = false; - const stdDB = ['mu_spells_db','mu_spells_db_l1','mu_spells_db_l2','mu_spells_db_l3','mu_spells_db_l4','mu_spells_db_l5','mu_spells_db_l6','mu_spells_db_l7','mu_spells_db_l8','mu_spells_db_l9','mu_spells_db_custom','pr_spells_db_l1','pr_spells_db_l2','pr_spells_db_l3','pr_spells_db_l4','pr_spells_db_l5','pr_spells_db_l6','pr_spells_db_l7','pr_spells_db_custom','powers_db','mi_db','mi_db_custom','mi_db_ammo','mi_db_armour','mi_db_equipment','mi_db_potions','mi_db_rings','mi_db_scrolls_books','mi_db_wands_staves_rods','mi_db_weapons','attacks_db','class_db','race_db','race_db_creatures_a_e','race_db_creatures_f_j','race_db_creatures_k_o','race_db_creatures_p_t','race_db_creatures_u_z','styles_db','abilities_db']; + const stdDB = ['mu_spells_db','mu_spells_db_l1','mu_spells_db_l2','mu_spells_db_l3','mu_spells_db_l4','mu_spells_db_l5','mu_spells_db_l6','mu_spells_db_l7','mu_spells_db_l8','mu_spells_db_l9','mu_spells_db_custom','pr_spells_db_l1','pr_spells_db_l2','pr_spells_db_l3','pr_spells_db_l4','pr_spells_db_l5','pr_spells_db_l6','pr_spells_db_l7','pr_spells_db_custom','powers_db','mi_db','mi_db_custom','mi_db_ammo','mi_db_armour','mi_db_equipment','mi_db_treasure','mi_db_potions','mi_db_rings','mi_db_scrolls_books','mi_db_wands_staves_rods','mi_db_weapons','attacks_db','class_db','race_db','race_db_creatures_a_e','race_db_creatures_f_j','race_db_creatures_k_o','race_db_creatures_p_t','race_db_creatures_u_z','styles_db','abilities_db']; const waitMsgDiv = 'Menus | '+configButtons(state.MagicMaster.fancy, 'Plain menus', '!magic --config fancy-menus|false', 'Fancy menus', '!magic --config fancy-menus|true')+'|||||
Menus | '+configButtons(state.MagicMaster.fancy, 'Plain menus', '!magic --config fancy-menus|false', 'Fancy menus', '!magic --config fancy-menus|true')+'|||||
Player Targeted Attks | '+configButtons(!state.attackMaster.weapRules.dmTarget, 'Not Allowed', '!attk --config dm-target|true', 'Allowed by All', '!attk --config dm-target|false')+'|||||
Allowed weapons | '+configButtons(state.attackMaster.weapRules.allowAll, 'Restrict Usage', '!attk --config all-weaps|false', 'All Can Use Any', '!attk --config all-weaps|true')+'|||||
Non-Prof Penalty | '+configButtons(!state.attackMaster.weapRules.prof, 'Class Penalty', '!attk --config prof|true', 'Character Sheet', '!attk --config prof|false')+'|||||
Ranged Mastery | '+configButtons(state.attackMaster.weapRules.masterRange, 'Not Allowed', '!attk --config master-range|false', 'Mastery Allowed', '!attk --config master-range|true')+'|||||
Rogue Skills | '+configButtons(state.attackMaster.thieveCrit, 'No Critical', '!attk --config rogue-crit|false', 'Critical Success', '!attk --config rogue-crit|true')+'|||||
Rogue Crit Value | '+configButtons(state.attackMaster.thieveCrit>1, 'Critical = 1%', '!attk --config rogue-crit-val|false', 'Critical = 5%', '!attk --config rogue-crit-val|true')+'|||||
Rogue Crit Value | '+configButtons(state.attackMaster.thieveCrit>1, 'Critical = 1%', '!attk --config rogue-crit-val|false', 'Critical = 5%', '!attk --config rogue-crit-val|true')+'|||||
NPC Attributes | '+configButtons(state.attackMaster.attrRoll, 'No Attributes', '!attk --config attr-roll|false', 'Roll Attributes', '!attk --config attr-roll|true')+'|||||
NPC Attr Range | '+configButtons(state.attackMaster.attrRestrict, 'Full Range', '!attk --config attr-restrict|false', 'Restrict', '!attk --config attr-restrict|true')+'|||||
Specialist Wizards | '+configButtons(!state.MagicMaster.spellRules.specMU, 'Specified in Rules', '!magic --config specialist-rules|true', 'Allow Any Specialist', '!magic --config specialist-rules|false')+'|||||
Spells per Level | '+configButtons(!state.MagicMaster.spellRules.strictNum, 'Strict by Rules', '!magic --config spell-num|true', 'Allow to Set Misc', '!magic --config spell-num|false')+'|||||
Spell Schools | '+configButtons(state.MagicMaster.spellRules.allowAll, 'Strict by Rules', '!magic --config all-spells|false', 'All Can Use Any', '!magic --config all-spells|true')+'|||||
Powers by Level | '+configButtons(state.MagicMaster.spellRules.allowAnyPower, 'Strict by Rules', '!magic --config all-powers|false', 'All Can Use Any', '!magic --config all-powers|true')+'|||||
Custom Objects | '+configButtons(!state.MagicMaster.spellRules.denyCustom, 'External / GM Defined', '!magic --config custom-spells|true', 'All Items Allowed', '!magic --config custom-spells|false')+'|||||
Custom Objects | '+configButtons(!state.MagicMaster.spellRules.denyCustom, 'External / GM Defined', '!magic --config custom-spells|true', 'All Objects Allowed', '!magic --config custom-spells|false')+'|||||
Auto-Hide Items | '+configButtons(state.MagicMaster.autoHide, 'GM Hide Manually', '!magic --config auto-hide|false', 'Auto-Hide if Possible', '!magic --config auto-hide|true')+'|||||
Reveal Hidden Items | '+configButtons(state.MagicMaster.reveal, 'Reveal Manually', '!magic --config reveal|false', 'Reveal on Use', '!magic --config reveal|true')+'|||||
Action Buttons | '+configButtons(state.MagicMaster.viewActions, 'Grey on View', '!magic --config view-action|false', 'Active on View', '!magic --config view-action|true')+'
+-*/ | Standard operators can be used to do maths |
---|---|
(...) | Parentheses can be used to set the evaluation order |
^(... ; ... ; ...) | A \'^\' preceeding parentheses with values (which can be formulas) separated by \';\' (or commas) will return the maximum of the values |
v(... ; ... ; ...) | A \'v\' preceeding parentheses and \';\' (or comma) separated values will return the minimum of the values |
c(...) | A \'c\' preceeding parentheses returns the ceiling of the value (which can itself be a formula) |
f(...) | A \'f\' preceeding parentheses returns the floor of the value (which can itself be a formula) |
# | A \'#\' in any place other than the 1st character of the duration (see below) will be replaced with the number of selected tokens (only works well with --target multi command) |
Next, durations for statuses are normally just an integer number of rounds. However if preceeded by \'+\', \'-\', \'<\', \'>\', \'#\', \'$\' or \'=\' and a status of the same name is already set on the identified token the command will modify the current duration (or add a new effect) like so:
' + +'If a status of the same name does not exist on the identified token, the duration will be applied as normal to a new status for that token.
' + +'!rounds --addstatus status|duration|direction|[message]|[marker]|[savemod]' + +'
Fix: Adds a status and a marker for that status to the currently selected token(s) (unless an optional savemod is specified, see below). The status has the name given in the status parameter, with the format described above, and will be given the duration specified (or a modified duration as stated above) which will be changed by direction each round. Thus setting a duration of 8 and direction of -1 will decrement the duration by 1 each round for 8 rounds. If the duration gets to 0 the status and token marker will be removed automatically. direction can be any number - including a positive one meaning duration will increase. Each Turn Announcement for the turn of a token with one or more statuses will display the effect-name/status (or the Player Text if specified), the duration and direction, and the message, if specified. The specified marker will be applied to the token - if it is not specified, or is not a valid token marker name, the option will be given to pick one from a menu in the chat window (which can be declined).
' + +'For player-characters, when the duration reaches 9 or less the duration will be counted-down by a number appearing on the marker. For NPCs this number does not appear (so that Players don\'t see the remaining duration for statuses on NPCs), but the remaining duration does appear for DM only on the status message below the Turn Announcement on the NPCs turn. Turn announcement durations and status count-downs can also be surpressed for player characters by specifying a direction value of less than -1 and a duration suitably multiplied to achieve the same outcome. For example, the spell fly has an uncertain duration and perhaps the player should not be aware of what it is: multiplying the duration by 10 and setting the direction as -10 per round means that the turn announcement will not show the players the remaining duration of the status, and the final count of the duration will be from "10" down to "0" so the status count-down on the token will never display! Alternatively, if you want to give the player just a small hint of it coming to an end, multiplying the duration by 5 and setting the direction to -5 will display one count-down of "5" on the token before dropping to "0" and removing the status (perhaps a bit misleading), or x 2 and -2 will show "8", "6", "4", "2", then remove the status.
' + +'If also using the RPGMaster AttackMaster API, a savemod can optionally be specified, with the form svXXX:[+/-]#, where XXX is the type of saving throw to be made - one of \'par\', \'poi\', \'dea\', \'rod\', \'sta\', \'wan\', \'pet\', \'pol\', \'bre\', \'spe\' for paralysis, poison, death, rod, staff, wand, petrification, polymorph, breath, or spell respectively. The subsequent value (optinally preceeded by plus or minus) is the modifier to the saving throw (usually +0). If a savemod is added to the command call, a status marker will not immediately be applied, but a prompt will appear for the user of the command to ask the player(s) who control the selected token(s) to make the appropriate saving throw - the saving throw modifier will automatically be applied to this saving throw, and the status set automatically if the saving throw is failed: if the save is successfully made, the status is not applied. If AttackMaster is not loaded then the savemod parameter will be ignored.
' + +'If a Player other than the DM uses this command, the DM will be asked to confirm the setting of the status and marker. This allows the DM to make any decisions on effectiveness.
' + +'The API-held Effects database and any GM-supplied additional Effects databases will be searched in three ways: when a status marker is set, any Ability Macro with the name Effect-name-start (where Effect-name is from the command using the syntax described above) is run. Each round when it is the turn of a token with the status marker set, the Ability Macro with the name Effect-name-turn is run. And when the status ends (duration reaches 0) or the status is removed using --removestatus, or the token has the Dead marker set or is deleted, an Ability Macro with the name Effect-name-end is run. See the Effects database documentation for full information on effect macros and the options and parameters that can be used in them.
' + +'!rounds --addtargetstatus tokenID|status|duration|direction|[message]|[marker]|[savemod]' + +'
Fix: This command is identical to addstatus, except for the addition of a tokenID. Instead of using a selected token or tokens to apply the status to, this applies the status to the specified token. The optional savemod parameter also works in the same way.
' + +'!rounds --edit' + +'
This command brings up a menu in the chat window showing the current status(es) set on the selected token(s), with the ability to remove or edit them. Against each named status, a spanner icon opens another menu to edit the selected status name, duration, direction, message and marker on all the selected token(s), and also allows this status to be set as a favourite. A bin icon will remove the status from all the selected token(s), and run any status-end macros, if any.
' + +'!rounds --target[-save/-nosave] CASTER/MULTI|casterID|casterID|status|duration|direction|[message]|[marker]|[savemod]' + +'
' + +'!rounds --target[-save/-nosave] SINGLE/AREA|casterID|targetID|status|duration|direction|[message]|[marker]|[savemod]
This command targets a status on a token or a series of tokens. If a version using CASTER is called, it acts identically to the addtargetstatus command, using the casterID as the target token. If the SINGLE version is called, the targetID is used. If the AREA version is used, after applying the status to the targetID token, the system asks in the chat window if the status is to be applied to another target and, if confirmed, asks for the next target to be selected, repeating this process after each targeting and application. If the MULTI version is used, the player is prompted to select multiple tokens and then confirm the selection with a button in the chat window. In each case, it applies the status (with the format defined above), effect macro and marker to the specified token(s) in the same way as --addtargetstatus, including prompting a saving throw if using AttackMaster API and a savemod is specified.
' + +'Note: the MULTI version of the command temporarily modifies every token on the same Roll20 page as the casterID token, and the character sheets they represent, to set the player who controls the casterID token as a controller for every token, so that the player can select any or all tokens. The command also temporarily changes any GM-controlled or uncontrolled tokens so they do not "have sight" in a dynamic lighting situation - this is so the player does not see anything they shouldn\'t and so that "Explorer Mode" continues to work correctly. Once the player has finished selection and clicks the button in the chat window to confirm the selection (or in fact does any other command) all control and sight returns to the same as it was before the command.
' + +'The behaviour of the --target command can be affected by using the qualifiers -save and -nosave. --target-save will force the GM to always be prompted for a confirmation of the setting of the status even if it is the GM who has made the --target-save call. This allows the GM to ensure a saving throw or other check is made before the status is confirmed as applying to the token. --target-nosave does the opposite: even if it is a player that has made the --target-nosave command call, the GM will not be prompted to confirm the status, which will always be applied immediately in a similar fashion to --gm-target. However, the --target-nosave behaviour can be overridden using the --nosave configuration command - see below - whereas --gm-target cannot be overriden.
' + +'!rounds --nosave (ON/OFF)' + +'
As described under the --target-nosave command, the default situation for --target-nosave is to not present the GM with the option to confirm the application of a status to a token. The --nosave command can alter this behaviour: --nosave OFF will make --target-nosave behave in the same way as --target, asking the GM for confirmation if issued by a player, but not if issued by the GM. The --target-nosave behaviour can be restored by using --nosave ON.
' + +'!rounds --gm-target CASTER|casterID|casterID|status|duration|direction|[message]|[marker]' + +'
' + +'!rounds --gm-target SINGLE/AREA|casterID|targetID|status|duration|direction|[message]|[marker]
These commands work identically to the --target commands, with the exception that if used by a player, the player will temporarily be given GM privilidges and the GM will not be asked to confirm the status targeting. Use carefully as this may not give the GM the opportunity to do saving throws or other retaliatory actions.
' + +'!rounds --aoe tokenID|[shape]|[units]|[range]|[length]|[width]|[image]|[confirmed]' + +'
' + +'!rounds --aoe tokenID|[shape]|[units]|[range]|[length]|[width]|[image]|[confirmed]|casterID|SINGLE/AREA|status|duration|direction|[message]|[marker]
' + +'!rounds --movable-aoe tokenID|[shape]|[units]|[range]|[length]|[width]|[image]|[confirmed]
shape | [BOLT/ CIRCLE/ CONE/ ELLIPSE/ RECTANGLE/ SQUARE/ WALL] |
---|---|
units | [SQUARES/ FEET/ YARDS/ UNITS] |
image | [ACID/ COLD/ DARK/ FIRE/ LIGHT/ LIGHTNING/ MAGIC/ RED/ YELLOW/ BLUE/ GREEN/ MAGENTA/ CYAN/ WHITE/ BLACK] |
confirmed | [TRUE / FALSE] |
range, length & width | numbers specified in whatever unit was specified as [units] |
This command displays an Area of Effect for an action that has or is to occur, such as a spell. This quite often can be used before the --target area command to identify targets. The system will present lists of options for each parameter that is not specified for the Player to select. On executing this command, if the range is not zero the Player will be given a crosshair to position the effect, and if the range is zero the effect will be centred on the Token (or at its "finger-tips" for directional effects like cones). The range of the effect will be centred on the TokenID specified and will be displayed as a coloured circle - the crosshair should be positioned within this area (the system does not check). The Crosshair (or if range is zero, the Token) can be turned to affect the direction of the effect. The effect "direction" will be the direction the token/crosshair is facing. If Confirmed is false or omitted, the Player will be asked to confirm the positioning of the token/crosshair with a button in the chat window. Setting Confirmed to true will apply the effect immediately - good for range zero circular effects (i.e. don\'t need placing or direction setting).
' + +'The second form of the --aoe command, with more parameters, combines the display of an area of effect with a subsequent call to a --target command, using the parameters as described for the --target command above. Once the area of effect is shown, a button will be presented in the chat window to select a target (which can be the first in a sequence if the "AREA" parameter is used).
' + +'Using the aoe command means the area of effect presented is movable or deletable by the DM but not the Player(s). If using the movable-aoe command instead, then any Player who controls the specified token can move or delete the area of effect image. This is useful for representing spells such as Flaming Sphere
' + +'The effect can have one of the shapes listed:
' + +'For the units, Feet & Yards are obvious and are scaled to the map. Squares are map squares (whatever scale they are set to), and Units are the map scale units and are not scaled.
' + +'Images are set with transparency and sent to the back of the Object layer. Red/ Yellow/ Blue/ Green/ Magenta/ Cyan/ White/ Black colour the effect area the specified colour, and Acid/ Cold/ Dark/ Fire/ Light/ Lightning/ Magic use textured fills.
' + +'!rounds --clean' + +'
Drops all the status markers on the selected token(s), without removing the status(es) from the campaign status object, meaning live statuses will be rebuilt at the end of the round or the next trigger event. This deals with situations where token markers have become corrupted for some reason, and should not be needed very often.
' + +'!rounds --removestatus status(es) / ALL' + +'
' + +'!rounds --removeglobalstatus status(es) / ALL
Removes the status, a comma-delimited list of statuses, or all statuses, and their status marker(s) from the selected token(s) (or all tokens if the global version is used), and runs any associated status-end Ability Macros in any existing Effects database in the campaign. See addstatus command and the Effect database documentation for details on effect macros. Statuses can be "all" which will remove all statuses from the selected token(s). Take care when using the global version as it can have unintended consequences.
' + +'!rounds --removetargetstatus targetID | status(es) / ALL' + +'
Exactly the same as the removestatus command, but for a specified token rather than any that is selected. Removes the status, a comma-delimited list of statuses, or all statuses, and their status marker(s) from the specified token, and runs any associated status-end Ability Macros in any existing Effects database in the campaign. See addstatus command and the Effect database documentation for details on effect macros. Statuses can be "all" which will remove all statuses from the token.
' + +'!rounds --deletestatus status(es) / ALL' + +'
' + +'!rounds --delglobalstatus status(es) / ALL
Works the same as removestatus command, except that it does not run any effect macros.
' + +'!rounds --deltargetstatus tokenID|status(es) / ALL' + +'
Works the same as removetargetstatus command, except that it does not run any effect macros.
' + +'!rounds --movestatus' + +'
For each of the selected tokens in turn, searches for tokens in the whole campaign with the same name and representing the same character sheet, and moves all existing statuses and markers from all the found tokens to the selected token (removing any duplicates). This supports Players moving from one Roll20 map to another and, indeed, roundMaster detects page changes and automatically runs this command for all tokens on the new page controlled by the Players who have moved to the new page.
' + +'!rounds --state-extract' + +'
' + +'!rounds --state-load [RPGM/ALL/API-Name][|API-Name2|API-Name3|...]
These commands extract the current Roll20 state variable for the current campaign to a character sheet called "StatusMule", ready to be copied or transmogrified to a new (identical) campaign, and then provide the ability to load all or part of the state variable into the copy campaign. This provides support for Roll20 upgrades, such as JumpGate.
' + +'The --state-extract command does not take any arguments, and extracts the whole Roll20 state variable for the current campaign in JSON text form to the "State" ability on the character sheet "StatusMule".
' + +'The --state-load command takes one text argument, which can be:
' + +'RPGM | Loads only the parts of the state variable relevant to the currently installed RPGMaster Mods - including RoundMaster |
---|---|
ALL | Loads the whole state variable for all installed Mods (See Note below) |
API-names | Loads the state variables for the named API/Mod(s). Additional names can be separated with pipes "|" (See note below). |
Note: Individual API / Mod names are case sensitive (except for RPGM API names, which are checked & corrected automatically). No guarantee is given for the validity or effect of loading non-RPGM API / Mod state variables. You should only load non-RPGM state variables to a copy of a campaign you are prepared to loose if the load does not work properly.
' + +'!rounds --disptokenstatus [tokenID]' + +'
Shows the statuses on the specified token to the DM using the same display format as used in the Turn Announcement.
' + +'!rounds --dancer [INHAND/REBUILD]|tokenID|weapon|[plusChange]|[duration]' + +'
If cmd is INHAND, gives the identified weapon a dancing-inhand status, with the change in the magical plus of the weapon each round (can be zero or negative, defaults to +1), and the number of rounds to be used in-hand before dancing for that number of rounds (defaults to 4). Also automatically creates the necessary effects to make the weapon dance based on the templates in the in-memory database.
' + +'If cmd is REBUILD, rebuilds the dancing effects for the specified weapon in the in-memory database, but does not apply any statuses: this version of the command is generally used after the Roll20 campaign is restarted when a character already has a dancing weapon in-hand.
' + +'!rounds --listmarkers' + +'
Shows a display of all markers available in the API to the DM, and also lists which are currently in use.
' + +'!rounds --listfav' + +'
Shows statuses to the DM that have been defined as favourites (see the edit command), and provides buttons to allow the DM to apply one or more favourite statuses to the selected token(s), and to edit the favourite statuses or remove them as favourites.
' + +'!rounds --help' + +'
Displays a listing of RoundMaster commands and their syntax.
' + +'!rounds --hsq from|[command]' + +'
' + +'!rounds --handshake from|[command]
Either form performs a handshake with another API, whose call (without the \'!\') is specified as from in the command parameters. The command calls the from API command responding with its own command to confirm that RoundMaster is loaded and running: e.g.
' + +'Received: !rounds --hsq magic
'
+ +'Response: !magic --hsr rounds
Optionally, a command query can be made to see if the command is supported by RoundMaster if the command string parameter is added, where command is the RoundMaster command (the \'--\' text without the \'--\'). This will respond with a true/false response: e.g.
' + +'Received: !rounds --hsq init|addtotraker
'
+ +'Response: !init --hsr rounds|addtotracker|true
!rounds --debug (ON/OFF)' + +'
Takes one mandatory argument which should be ON or OFF.
' + +'The command turns on a verbose diagnostic mode for the API which will trace what commands are being processed, including internal commands, what attributes are being set and changed, and more detail about any errors that are occurring. The command can be used by the DM or any Player - so the DM or a technical advisor can play as a Player and see the debugging messages.
' + +'Effect-DB is a database character sheet created, used and updated by the RoundMaster API (see separate handout). The database holds macros as Ability Macros that are run when certain matching statuses are placed on or removed from tokens (see Roll20 Help Centre for information on Ability Macros and Character Sheet maintenance). The macros are run when various events occur, such as end-of-round or Character\'s turn, at which point no token or an incorrect token may be selected - this makes @{selected|attribute-name} useless as a macro command. Therefore, the macros have certain defined parameters dynamically replaced when run by RoundMaster, which makes the token & character IDs and names, and values such as AC, HP and Thac0, available for manipulation.
' + +'The Effects database as distributed with the API holds many effects that work with the spell & magic item macros distributed with other RPGMaster APIs. The API also checks for, creates and updates the Effects database to the latest version on start-up. DMs can add their own effects to additional databases, but the database provided is totally rewritten when new updates are released and so the DM must add their own database sheets. If the provided databases are accidentally deleted or overwritten, they will be automatically recreated the next time the Campaign is opened. Additional databases should be named as Effects-DB-[added-name] where "[added-name]" can be any name you want.
' + +'However: the system will ignore any database with a name that includes a version number of the form "v#.#" where # can be any number or group of numbers e.g. Effects-DB v2.13 will be ignored. This is so that the DM can version control their databases, with only the current one (without a version number) being live.
' + +'There can be as many additional databases as you want. Other Master series APIs come with additional databases, some of which overlap - this does not cause a problem as version control and merging unique macros is managed by the APIs.
' + +'Important Note: all Character Sheet databases must have their \'ControlledBy\' value (found under the [Edit] button at the top right of each sheet) set to \'All Players\'. This must be for all databases, both those provided (set by the API) and any user-defined ones. Otherwise, Players will not be able to run the macros contained in them.
' + +'Effect macros are primarily intended to act on the Token and its variables, but can also act on the represented Character Sheet. A single Character Sheet can have multiple Tokens representing it, and each of these are able to do individual actions using the data on the Character Sheet jointly represented. However, if such multi-token Characters / NPCs / creatures are likely to encounter effects that will affect the Character Sheet they must be split with each Token representing a separate Character Sheet, or else the one effect will affect all tokens associated with the Character Sheet, whether they were targeted or not! In fact, it is recommended that tokens and character sheets are 1-to-1 to keep things simple.
' + +'Note: Effect macros are heavily dependent upon the ChatSetAttr API and the Tokenmod API, both from the Roll20 Script Library, and they must be loaded. It is also highly recommended to load all the other RPGMaster series APIs: InitiativeMaster, AttackMaster, MagicMaster and CommandMaster. This will provide the most immersive game-support environment
' + +'The RoundMaster API is distributed with an Effect Database containing effects to support items provided in other RPGMaster series APIs. If you want to replace any Effect macro in the provided database, you can do so simply by creating an Ability Macro in one of your own Effect databases with exactly the same name as the provided item to be replaced. The API gives preference to Ability Macros in user-defined databases, so yours will be selected in preference to the one provided with the APIs.
' + +'The recommended Token Bar assignments for all APIs in the Master Series are:
' + +'Bar1 (Green Circle): | Armour Class (AC field) - only current value |
---|---|
Bar2 (Blue Circle): | Base Thac0 (thac0-base field) before adjustments - only current value |
Bar3 (Red Circle): | Hit Points (HP field) - current & max |
It is recommended to use these assignments, and they are the bar assignments set by the CommandMaster API if its facilities are used to set up the tokens. All tokens must be set the same way, whatever way you eventually choose.
' + +'These assignments can be changed in the RoundMaster API, by changing the fields object near the top of the API script. See the RPGMaster Character Sheet setup Handout for details of how to do this. However, when using the Effect place holders in the effect macros, the APIs will always search the token and character sheet for the most appropriate field assignments - if you link the token bars differently, the APIs will look at the fields so linked and attempt to use/change/maintain the appropriate ones you have assigned.
' + +'Dynamic parameters are identified in the macros by bracketing them with two carets: ^^parameter^^. The standard Roll20 syntax of @{selected|...} is not available, as at the time the macros run the targeted token may not be selected, and @{character_name|...} will not enable the token to be affected (especially where the Character Sheet is represented by more than one token). The ^^...^^ parameters always relate to the token on which a status has been set, and the Character Sheet it represents. Currently available parameters are:
' + +'Place holder | Replaced with |
---|---|
^^tid^^ | TokenID |
^^tname^^ | Token_name |
^^cid^^ | CharacterID |
^^cname^^ | Character_name |
^^ac^^ | Armour Class value (order looked for: a token bar linked to an appropriate field, Character Sheet AC field, MonsterAC - see note) |
^^ac_max^^ | Maximum value of AC, wherever it is found |
^^token_ac^^ | The token field name for AC value field, if set as a token bar |
^^token_ac_max^^ | The token field name for AC max field, if set as a token bar |
^^thac0^^ | Thac0 value (order looking: a token bar linked to an appropriate field, Character Sheet Thac0_base field, MonsterThac0 - see note) |
^^thac0_max^^ | Maximum value of Thac0, wherever it is found |
^^token_thac0^^ | The token field name for Thac0 value field, if set as a token bar |
^^token_thac0_max^^ | The token field name for Thac0 max field, if set as a token bar |
^^hp^^ | HP value (order looked for: a token bar linked to an appropriate field, Character Sheet HP field - see note) |
^^hp_max^^ | Maximum value of HP, wherever it is found |
^^token_hp^^ | The token field name for HP value field, if set as a token bar |
^^token_hp_max^^ | The token field name for HP max field, if set as a token bar |
^^bar1_current^^ | Value of the token Bar1_value field |
^^bar2_current^^ | Value of the token Bar2_value field |
^^bar3_current^^ | Value of the token Bar3_value field |
Note: If a legal value is not found in any of these fields, the value in the token bar specified in the API fields object will be used as a last resort.
' + +'This allows most data on both the token and the character sheet to be accessed. For example @{^^cname^^|strength} will return the strength value from the represented character sheet. Of course all loaded RPGMaster series API commands are available, along with commands for any other APIs you have loaded.
' + +'Two other APIs from the Roll20 Script Library are extremely useful for these macros, and indeed are used by many of the provided APIs: ChatSetAttr API from joesinghaus allows easy and flexible setting of Character Sheet attributes. Tokenmod API from The Aaron supports easy setting and modifying of Token attributes. Combined with the dynamic parameters above, these make for exceptionally powerful real-time effects in game-play.
' + +'Each effect macro runs when a particular status event occurs. Here is the complete list of effect macro status name qualifiers that can be used. Each of these is appended to the status whenever the status experiences the relevant event, and an effect macro with that name searched for and run if found:
' + +'statusname-start | The status is created on a token |
---|---|
statusname-turn | Each round the status has a duration that is not zero |
statusname-end | The status duration reaches zero |
These effect macros are triggered for weapons when certain events take place:
' + +'weaponname-inhand | A weapon is taken in-hand (triggered by AttackMaster API --weapon command) |
---|---|
weaponname-dancing | A weapon starts dancing (triggered by AttackMaster API --dance command) |
weaponname-sheathed | A weapon is sheathed (out of hand - triggered by AttackMaster --weapon cmd) |
Here is an example of an effect macro that runs when a Faerie fire (twilight form) status is placed on a token. The following --target command might be run to set this status, with the caster token selected:
' + +'!rounds --target area|@{selected|token_id}|@{target|Select first target|token_id}|Faerie-Fire-twilight|[[4*@{selected|Casting-Level}]]|-1|Outlined in dim Faerie Fire, 1 penalty to AC|aura
' + +'(See the RoundMaster Help handout for an explanation of the --target command and its parameters). This command will result in the following effect macro being run when the first token is targeted:
' + +'!token-mod --ignore-selected --ids ^^tid^^ --set ^^token_ac^^|+1
'
+ +'^^tname^^ is surrounded by Faerie Fire, and becomes easier to hit
This uses the Tokenmod API to increase the AC number of the targeted token by 1 (making it 1 wose), and then display a message to all Players stating the name of the targeted token, and the effect on it. This will be run for each token targeted, and will be individual to each. Note: the tokens are not \'selected\' in Roll20 terms, and so @{selected|...} will not work
' + +'When the Faerie Fire status counts down to zero, the following effect macro will be run on each of the tokens it was applied to:
' + +'!token-mod --ignore-selected --ids ^^tid^^ --set ^^token_ac^^|-1
'
+ +'^^tname^^ has lost that glow and is now harder to aim at
Again, the Tokenmod API is used to decrease the token AC and a message issued confirming what has happened. If messages should only be sent to the Player(s) controlling the character represented by the token, use /w "^^cname^^" before the message. If the message is only for the gm, use /w gm.
' + +'A more complex example is a Quarterstaff of Dancing, that uses the complete suite of possible effect macros and certain aspects of the AttackMaster API functionality triggered by Weapon table field settings. The first macro is triggered by AttackMaster API when a Character takes a Quarterstaff-of-Dancing in hand to use as a weapon:
' + +'!rounds --addtargetstatus ^^tid^^|Quarterstaff-of-Dancing|4|-1|Quarterstaff not yet dancing so keep using it|stopwatch
' + +'This command sets a status marker on the Token of the Character taking the Quarterstaff in hand, and sets a countdown of 4 rounds, running the next effect macro in each of those rounds:
' + +''
+ +'!attk --quiet-modweap ^^tid^^|quarterstaff-of-dancing|melee|+:+1 --quiet-modweap ^^tid^^|quarterstaff-of-dancing|dmg|+:+1
'
+ +'/w "^^cname^^" Updating the quarterstaff +1 to attk & dmg
This command then runs each round as the Quarterstaff-of-Dancing status counts down, and uses the !attk --quiet-modweap command to gradually increment the magical to-hit and dmg plus, round by round. Once the countdown reaches zero, the next effect macro is run:
' + +'' + +'!attk --dance ^^tid^^|Quarterstaff-of-Dancing
' + +'This calls an AttackMaster API command to start the weapon dancing, resets the weapon to its specs that it starts dancing with, and the AttackMaster API then automatically calls the next effect macro:
' + +''
+ +'!rounds --addtargetstatus ^^tid^^|Dancing-Quarterstaff|4|-1|The Quarterstaff is Dancing by itself. Use this time wisely!|all-for-one
'
+ +'!attk --quiet-modweap ^^tid^^|quarterstaff-of-dancing|melee|sb:0 --quiet-modweap ^^tid^^|quarterstaff-of-dancing|dmg|sb:0
This places a new status marker on the token representing the Character with the dancing weapon (note the new status name Dancing-Quarterstaff), and resets the Strength Bonus flags for the weapon - a dancing weapon can\'t have the Strength Bonus of the wielder. As each round now passes, the following different status effect macro is run:
' + +'' + +'!attk --quiet-modweap ^^tid^^|quarterstaff-of-dancing|melee|+:+1 --quiet-modweap ^^tid^^|quarterstaff-of-dancing|dmg|+:+1
' + +'As per the previous -turn effect macro, this increments the magical plusses on To-Hit and Dmg, round by round. It has to have a different name, as the -end effect macro does different actions:
' + +'' + +'!attk --dance ^^tid^^|Quarterstaff-of-Dancing|stop
' + +'This uses the AttackMaster API command to stop the Quarterstaff from dancing. As can be seen from the above, quite complex sequences of effect macros can be created.
' + +'' + + '<%= confirm_button %>' + + ' | ' + + '' + + '<%= reject_button %>' + + ' | ' + + '
Edit Group Status "'+statusName+'" |
'+(/_([^_]+)_?/.exec(statusArgs.name) || ['',statusArgs.name])[1] + ' ' + (dir === 0 ? '': (dur <= 0 ? 'Expiring':((!isGM && dir < -1) ? '' : dur)))
+ + (dir===0 ? '\u221E' : (dir > 0 ? '\u25B2(+'+dir+')':'\u25BC'+((!isGM && dir < -1) ? '' : ('('+dir+')'))+''))
+ + ((statusArgs.msg) ? (' ' + getFormattedRoll(parseStr(statusArgs.msg)) + ''):'')+' | '
+ + '
'
+ + ' | '
+ + '' + + name + '\'s initiative is ' + parseInt(initiative) + ' ' + msg + + ' | ' + + ''
+ + ' | '
+ + '
'+ (favored ? 'Edit Favorite' :('Edit "'+statusName+'" for'))+' | '+(favored ? (''+statusName+' | ') : ('
'
+ + ' Name '
+ + ''+''+statusName+' | '
+ + ''
+ + ' | '
+ + '
'
+ + ' Marker '
+ + ''+''+mImg+' | '
+ + ''
+ + ' | '
+ + '
'
+ + ' Duration '
+ + ''+''+status.duration+' | '
+ + ''
+ + ' | '
+ + '
'
+ + ' Direction '
+ + ''+''+status.direction+' | '
+ + ''
+ + ' | '
+ + '
'
+ + ' Message '
+ + ''+''+status.msg+' | '
+ + ''
+ + ' | '
+ + '
' + //+ 'cookies' + //+ ' Add to Favorites' + + RoundMaster_tmp.getTemplate({command: '!rounds --addfav '+statusName+' %% '+status.duration+' %% '+status.direction+' %% '+status.msg+' %% '+globalStatus.marker, text: 'Add to Favorites'},'button') + + + ' | ' + + '
Statuses for |
Edit Group Status "'+statusName+'" |
' + + RoundMaster_tmp.getTemplate({command: '!rounds --relay hc% ' + hashes[0], text: 'Confirm'},'button') + + ' | ' + + '' + + RoundMaster_tmp.getTemplate({command: '!rounds --relay hc% ' + hashes[1], text: 'Reject'},'button') + + ' | ' + + '
'+(curToken ? ('Editing "'+statusName+'" for'):('Editing Favorite ' + statusName))+' | '+ (tokenId ? ('
${cmdStr}
Token Marker Effects Macro Library
Change Log:
Legacy Token Marker Effects Macro Library
Change Log:
Token Marker Effects Macro Library
Change Log:
New for v5.058: Improved feedback for GM
' + +'New for v5.058: Added support to migrate to new Universal Mods Table
' + +'New for v5.057: Fixes to bugs in --addStatus and related commands
' +'New for v5.056: --state-extract & --state-load support campaign copying
' - +'New for v5.055: RPGM maths for duration & direction of --target
' - +'New for v5.055: Saving throw mods & prompts for --target commands
' - +'New for v5.055: --target multi to target status changes for multiple tokens
' - +'New for v5.055: --target-nosave and --target-save to deny/force GM confirm
' - +'New for v5.055: --nosave on/off configures --target-nosave behaviour
' +'RoundMaster is an API for the Roll20 RPG-DS. Its purpose is to extend the functionality of the Turn Tracker capability already built in to Roll20. It is one of several other similar APIs available on the platform that support the Turn Tracker and manage token and character statuses related to the passing of time: the USP of this one is the full richness of its functionality and the degree of user testing that has occurred over a 12 month period.
' +'RoundMaster is based on the much older TrackerJacker API, and many thanks to Ken L. for creating TrackerJacker. However, roundMaster is a considerable fix and extension to TrackerJacker, suited to many different applications in many different RPG scenarios. RoundMaster is also the first release as part of the wider RPGMaster series of APIs for Roll20, composed of RoundMaster, CommandMaster, InitiativeMaster, AttackMaster, MagicMaster and MoneyMaster - other than RoundMaster (which is generic) these initially support only the AD&D2e RPG.
' +'Note: For some aspects of the APIs to work, the ChatSetAttr API and the Tokenmod API, both from the Roll20 Script Library, must be loaded. It is also highly recommended to load all the other RPGMaster series APIs listed above. This will provide the most immersive game-support environment
' @@ -857,12 +1222,12 @@ var RoundMaster = (function() { +'--addToTracker name|tokenID/-1|priority|[qualifier]|[message]|[detail]Update: --addstatus status|duration|direction|[message]|[marker]|[savemod]
' - +'Update: --addtargetstatus tokenID|status|duration|direction|[message]|[marker]|[savemod]
' + +'Fix: --addstatus status|duration|direction|[message]|[marker]|[savemod]
' + +'Fix: --addtargetstatus tokenID|status|duration|direction|[message]|[marker]|[savemod]
' +'--edit
' - +'Update: --target[-save/-nosave] CASTER/MULTI|casterID|status|duration|direction|[message]|[marker]|[savemod]
' - +'Update: --target[-save/-nosave] SINGLE/AREA|casterID|targetID|status|duration|direction|[message]|[marker]|[savemod]
' - +'New: --nosave (ON/OFF)
' + +'--target[-save/-nosave] CASTER/MULTI|casterID|status|duration|direction|[message]|[marker]|[savemod]
' + +'--target[-save/-nosave] SINGLE/AREA|casterID|targetID|status|duration|direction|[message]|[marker]|[savemod]
' + +'--nosave (ON/OFF)
' +'--gm-target CASTER|casterID|status|duration|direction|[message]|[marker]
' +'--gm-target SINGLE/AREA|casterID|targetID|status|duration|direction|[message]|[marker]
' +'--aoe tokenID|[shape]|[units]|[range]|[length]|[width]|[image]|[confirmed]
' @@ -934,7 +1299,7 @@ var RoundMaster = (function() { +'
The duration and direction values (as well as any numbers in a save specification) can use Roll20 maths using the [[...]] syntax. However, sometimes it is not possible or desirable to calculate the value using Roll20 maths, especially when using the --target multi command with maths including the number of targeted tokens using the # attribute. RoundMaster provides an alternative maths capability as follows:
' +'+-*/ | Standard operators can be used to do maths |
---|
'+(/_([^_]+)_?/.exec(statusArgs.name) || ['',statusArgs.name])[1] + ' ' + (dir === 0 ? '': (dur <= 0 ? 'Expiring':((!isGM && dir < -1) ? '' : dur)))
+ (dir===0 ? '\u221E' : (dir > 0 ? '\u25B2(+'+dir+')':'\u25BC'+((!isGM && dir < -1) ? '' : ('('+dir+')'))+''))
+ ((statusArgs.msg) ? (' ' + getFormattedRoll(parseStr(statusArgs.msg)) + ''):'')+' | '
@@ -3140,7 +3539,7 @@ var RoundMaster = (function() {
+ 'Name: ' + ''+fav.name+''
+ '||