&{template:RPGMarmour}{{name=Shield+2}}{{subtitle=Shield}}{{Shield=1-handed +2 Medium Shield made of wood & metal}}Specs=[Medium Shield,Shield,1H,Shields]{{AC=+[[2]] against all attacks from the front}}ACData=[a:Medium Shield+2, st:Shield, +:2,sz:M, wt:10] {{Speed=[[0]]}} {{Size=M}} {{Immunity=None}} {{Saves=No effect}} {{desc=All shields improve a character\'s Armor Class by 1 or more against a specified number of attacks. A shield is useful only to protect the front and flanks of the user. Attacks from the rear or rear flanks cannot be blocked by a shield (exception: a shield slung across the back does help defend against rear attacks). The reference to the size of the shield is relative to the size of the character. Thus, a human\'s small shield would have all the effects of a medium shield when used by a gnome.
'
+ +'*The medium shield* is carried on the forearm and gripped with the hand. Its weight prevents the character from using his shield hand for other purposes. With a medium shield, a character can protect against any frontal or flank attacks.}}
'
+ +'As can be seen here, the specification for a Shield is almost identical in structure to that of any other armour, the major difference being in the Specs section type field.
'
+ +'&{template:RPGMarmour}{{name=Field Plate Armour of Vulnerability+/-3}}{{subtitle=Cursed Armour}}{{Armour=+/-3 selectively magical Field Plate}}Specs=[Armour-of-Vulnerability|Armour-of-Resistance,Armour,0H,Plate]{{AC=[[2]][[0-3]] better AC against Slashing damage'
+ +'+[[3]] worse AC against any other type}}ACData=[a:Armour-of-Vulnerability+-3,st:Mail,+S:3,+P:-3,+B:-3,ac:2,sz:L,wt:60,sp:0,rc:cursed]{{Speed=0}}{{Size=Large}}{{Immunity=None}}{{Saves=No effect}}{{desc=***Curse.*** This armor is cursed, a fact that is revealed only when an identify spell is cast on the armor or you attune to it. Attuning to the armor curses you until you are targeted by the remove curse spell or similar magic; removing the armor fails to end the curse. While cursed, you have vulnerability to two of the three damage types associated with the armor (not the one to which it grants resistance).}}{{desc1=This armour provides resistance to Slashing damage only, but vulnerability to Piercing and Bludgeoning damage.
'
+ +'This armor is a combination of chain or brigandine with metal plates (cuirass, epaulettes, elbow guards, gauntlets, tasets, and greaves) covering vital areas. The weight is distributed over the whole body and the whole thing is held together by buckles and straps. This is the most common form of heavy armor.
'
+ +'For each +1 bonus to armor, regardless of the type of armor, the wearer\'s Armor Class moves downward (toward AC 2 . . . to 1 . . . to 0, -1, -2, and so on). Note, however, that Armor Class can never be improved beyond -10}}
'
+ +'This is a slightly more complex type of armour. It is a cursed item, and generally appears initially as Armour-of-Resistance+3, hence the Specs first parameter of armour type having two possible values, separated by \'|\'.
'
+ +'The use of the damage type specific magical adjustment fields can be seen in the data section, along with the use of the rc: field tag with the value \'cursed\'. See section 4 for a complete list of rc: field values.
'
+ +'Below are lists of the current possible values for the item database Ability macro sections.
'
+ +'There is an infinite list of weapon types: generally the type is the weapon name without any reference to magical plusses, so the Type of a Longsword+2 is Longsword. This Type is used to check for Proficiency.
'
+ +'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:
'
+ +'Types and Group-Types that can be used by various Character Classes are defined in the Class-DB class database for each class type:
'
+ +'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.
'
+ +' 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):
'
+ +'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:''
+ +'
Magic Item Database Help
'
+ +'
for MagicMaster v'+version+' and later
'
+ +'
'
+ +'
New in this Help Handout
'
+ +'
'
+ +'- New Items that affect Rogue skills
'
+ +'- New Items that affect AC, Thac0, Damage and/or HP
'
+ +'
'
+ +'[General DB Help]'
+ +'[Item Inheritance]'
+ +'
Note: Help for Spells & Powers has been split out to its own help handout.
'
+ +'
2. Magic Item Databases
'
+ +'
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.
'
+ +'
2.1 Simple Magic Items
'
+ +'
The Ability Macro may look something like this:
'
+ +'
Oil-of-Etherealness
'
+ +'
&{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.
'
+ +'
2.2 Items that Protect
'
+ +'
Items like a Ring of Protection or a Luck Blade protect the possessor by improving their saving throws and/or armour class.
'
+ +'
Ring of Protection+2
'
+ +'
&{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.
'
+ +'
2.3 Items that change initiative
'
+ +'
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:
'
+ +'
Rod of Alertness
'
+ +'
&{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.
'
+ +'
2.4 Items that change Rogue skills
'
+ +'
Items can also affect the scores required for a Rogue (or other character class) to perform various skills.
'
+ +'
Fine Thieves Tools
'
+ +'
&{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) |
'
+ +'
'
+ +'
2.5 More Complex Items
'
+ +'
Other magic items might use different structures, and be more complex:
'
+ +'
Ring of Human Influence
'
+ +'
&{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 |
'
+ +'
'
+ +'
New Pick: and Put: field tags
'
+ +'
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 |
'
+ +'
'
+ +'
2.6 Weaponised Items using variable charges
'
+ +'
Some more complex items can be used as weapons that have different effects or damage depending on how many charges are expended:
'
+ +'
Staff of Striking
'
+ +'
&{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.
'
+ +'
2.7 Magic Items that must be taken in-hand
'
+ +'
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:
'
+ +'
Wand of Frost
'
+ +'
&{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.
'
+ +'
2.8 Hiding Magic Item Details
'
+ +'
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
'
+ +'
Flask of Curses
'
+ +'
&{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 splitting of the title & name of the definition: title is always displayed, and name is only added to the title when hidden details are made displayable by the GM.
'
+ + '- The optional addition of the st: attribute in the MiscData specification, which specifies the name of the item displayed on buttons while details are hidden. If not specified, defaults to the item class.
'
+ + '- The addition of the Looks Like tag which marks text that replaces the description while details are hidden and until the GM reveals the details.
'
+ + '- The Hide item as other item function in the GM\'s Add Items dialog, which will be available for items that have the Looks Like tag, or for which the GM chooses another item to hide this one as.'
+ +'
'
+ +'
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 |
'
+ +'
'
+ +'
2.9 Configurable Items and the Query Attribute
'
+ +'
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.
'
+ +'
Armour of Blending
'
+ +'
&{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) |
'
+ +'
'
+ +'
'
+ +'
3. Magic Items with Powers or Spell-Storing
'
+ +'
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.
'
+ +'
3.1 Powerful Magic Item
'
+ +'
Here is an example of a power wielding magic item:
'
+ +'
Ring-of-Shooting-Stars
'
+ +'
!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.
'
+ +'
3.2 Spell Storing Magic Item
'
+ +'
Here is an example of a spell-storing magic item:
'
+ +'
Ring-of-Spell-Storing-HHSLS
'
+ +'
&{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.
'
+ +'
3.3 Flexible Spell-Storing Magic Items
'
+ +'
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):
'
+ +'
Vibrant Purple Ioun Stone
'
+ +'
&{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 %{...|...}.
'
+ +'
3.4 Magic-Item-storing Items
'
+ +'
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.
'
+ +'
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).
'
+ +'
'
+ +'
4. Weapons (if using AttackMaster API)
'
+ +'
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.
'
+ +'
'
+ +'
5. Armour & Shields
'
+ +'
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.
'
+ +'
'
+ +'
6. Specs & Data field values
'
+ +'
Below are lists of the current possible values for the item database Ability macro sections.
'
+ +'
6.1 Specs 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.
'
+ +'
7.1(a) Magic Item Types
'
+ +'
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 \'|\'
'
+ +'
7.1(b) Magic Item Classes
'
+ +'
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 |
'
+ +'
'
+ +'
7.1(c) Armour Handedness
'
+ +'
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.
'
+ +'
7.1(d) Item Schools
'
+ +'
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.
'
+ +'
7.2 Data Sections
'
+ +'
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 | | |
'
+ +'
'
+ +'
'
+ +'
7.3 Character Sheet data fields
'
+ +'
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.
'
+ +'
',
+ },
+ ClassDatabase_Help: {name:'Class & Race Database Help',
+ version:2.05,
+ avatar:'https://s3.amazonaws.com/files.d20.io/images/257656656/ckSHhNht7v3u60CRKonRTg/thumb.png?1638050703',
+ bio:''
+ +'
Character Class & Race Databases
'
+ +'
for RPGMaster APIs v'+version+' and later
'
+ +'
'
+ +'
New in this Help Handout
'
+ +'
'
+ +'- New Rogue class base skill modifiers
'
+ +'- New Race modifiers for rogue skills
'
+ +'- New Drag & Drop NPCs with calculated attributes, class, race & levels
'
+ +'- New Drag & Drop NPCs and Creatures can have any number of random equip/magic items auto-allocated
'
+ +'- New Drag & Drop NPC and Creature weapon allocations can have proficiency specified in definition
'
+ +'- New Drag & Drop Rogue NPCs can have a specified level of randomness for rogue skill level mods
'
+ +'
'
+ +'[General DB Help]'
+ +'[Item Inheritance]'
+ +'
2. Character Class Database
'
+ +'
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.
'
+ +'
Standard / Simple Class definitions
'
+ +'
The Ability Macro for a Class may look something like this:
'
+ +'
Thief
'
+ +'
&{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 (# / #) |
'
+ +'
'
+ +'
Changing the Default Saving Throws
'
+ +'
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.
'
+ +'
Dwarven Defender
'
+ +'
&{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:.
'
+ +'
Restricting the Schools and Spheres of Spells available
'
+ +'
While standard Wizards and Priests are very similar to the standard specification above, the definitions of specialist spellcaster classes is slightly more complex.
'
+ +'
Conjurer
'
+ +'
&{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.
'
+ +'
Default spells and spells per level
'
+ +'
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).
'
+ +'
Non-standard spells per level
'
+ +'
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:
'
+ +'
Priest of Magic
'
+ +'
&{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\' |
'
+ +'
'
+
+ +'
New Base Rogue Skill values
'
+ +'
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.
'
+
+ +'
Classes with Specific Powers
'
+ +'
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.
'
+ +'
Priest of Light
'
+ +'
&{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.
'
+ +'
3. Race & Creature Databases
'
+ +'
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.
'
+ +'
Standard / Simple Race Definitions
'
+ +'
The database definition for a simple, standard Race might look something like this:
'
+ +'
Human
'
+ +'
&{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!
'
+ +'
Save and To-Hit Modifiers
'
+ +'
There are a number of additional data fields relevant to Races:
'
+ +'
Halfling
'
+ +'
&{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.
'
+
+ +'
New Rogue Race Modifiers
'
+ +'
Each race might have different modifiers to the success percentage for rogue skills. Such differences can be specified in the Race data.
'
+ +'
Gnome
'
+ +'
&{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.
'
+
+ +'
Race Powers
'
+ +'
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.
'
+ +'
Dwarf
'
+ +'
&{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.
'
+ +'
4. Creatures
'
+ +'
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.
'
+ +'
Vampire
'
+ +'
&{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.
'
+
+ +'
New 4.1 Non-Player Characters (NPCs)
'
+ +'
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.
'
+ +'
Human Abjurer Wizard NPC
'
+ +'
&{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.
'
+
+ +'
Updated 4.2 Specifying Creature & NPC Items, Weapons & Armour
'
+ +'
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:
'
+ +'
Drow Fighter NPC
'
+ +'
&{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. |
'
+ +'
'
+ +'
4.3 NPCs/Creatures with Spell-Casting Ability
'
+ +'
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.
'
+ +'
4.4 Complex Creatures with Multiple Forms
'
+ +'
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:
'
+ +'
- the two instances of ??1 will be replaced by the first value after the selected text in the roll query,
'
+ +'- then the parentheses that hold numbers are calculated (??1-4),
'
+ +'- then any ranges are resolved by rolling a number in the range (none here),
'
+ +'- next dice rolls are made (none here),
'
+ +'- then the max & min are resolved and
'
+ +'- the final parentheses calculated.
'
+ +'
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.
'
+ +'
5. Specs & Data field values
'
+ +'
Below are lists of the current possible values for the Class and Race database Ability macro sections.
'
+ +'
5.1 Specs 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.
'
+ +'
5.1(a) Class & Race Types
'
+ +'
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.
'
+ +'
5.1(b) Macro Type
'
+ +'
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.
'
+ +'
5.1(c) Handedness
'
+ +'
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)
'
+ +'
5.1(d) Base Classes
'
+ +'
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.
'
+ +'
'
+ +'
5.2 Data Sections
'
+ +'
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 |
'
+ +'
'
+ +'
'
+ +'
5.3 Character Sheet data fields
'
+ +'
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.
'
+ +'
',
+ },
+ AttacksDatabase_Help:{name:'Attacks Database Help',
+ version:1.06,
+ avatar:'https://s3.amazonaws.com/files.d20.io/images/257656656/ckSHhNht7v3u60CRKonRTg/thumb.png?1638050703',
+ bio:''
+ +'
Fighting Styles Database
'
+ +'
for AttackMaster API v'+version+' and later
'
+ +'
'
+ +'[General DB Help]'
+ +'
2. Fighting Style Databases
'
+ +'
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:
'
+ +'
Weapon & Shield Style
'
+ +'
&{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:
'
+ +'
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:
'
+ +'
Bowyer Style
'
+ +'
&{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 errorMsgDiv = '
' ;
+
+ const reCastMIspellCmd = /!magic\s+--cast-spell\s+MI\s*\|/im;
+ const reCastMIpowerCmd = /!magic\s+--cast-spell\s+MI-POWERS?\s*\|/im;
+ const reSpecs = /}}\s*Specs=\s*?(\[[^{]*?\])\s*?{{/im;
+ const reSpecsAll = /\[\s*?(\w[-\+\s\w\|]*?)\s*?,\s*?(\w[-\s\w\|]*?\w)\s*?,\s*?(\w[\s\w\|]*?\w)\s*?,\s*?(\w[-\+\s\w\|]*?\w)\s*?(?:,\s*?(\w[-\+\s\w\|]*?\w)\s*?)?\]/g;
+ const reData = /}}\s*?\w*?data\s*?=(.*?){{/im;
+ const reDataAll = /\[.*?\]/g;
+ const reSpecClass = /\[\s*?\w[\s\|\w\-\+]*?\s*?,\s*?(\w[\s\|\w\-]*?)\s*?,.*?\]/g;
+ const reSpecSuperType = /}}\s*Specs=\s*?\[\s*?\w[\s\|\w\-\+]*?\s*?,\s*?\w[\s\|\w\-]*?\w\s*?,\s*?\d+H(?:\|\d*H)\s*?,\s*?(\w[\s\|\w\-]*?\w)\s*?(?:,\s*?(\w[-\+\s\w\|]*?\w)\s*?)?\]/im;
+ const reDataSpeed = /}}\s*?\w*?data\s*?=.*?[\[,]\s*?sp:([d\d\+\-\*\/.]+?)[,\s\]]/im;
+ const reDataCharge = /}}\s*?\w*?data\s*?=.*?[\[,]\s*?rc:([\w\+\-]+?)[,\s\]]/im;
+ const reDataCost = /}}\s*?\w*?data\s*?=.*?[\[,]\s*?gp:(\d+?\.?\d*?)[,\s\]]/im;
+ const reDataLevel = /}}\s*?\w*?data\s*?=.*?[\[,]\s*?lv:(\d+?)[,\s\]]/im;
+ const reACData = /}}\s*acdata\s*=(.*?){{/im;
+ const reName = /[\[,]\s*?w:\s*[^\],]+?[,\]]/im;
+ const reLevel = /[\[,]\s*?lv:(\d+?)[,\s\]]/im;
+ const reClassData = /}}\s*?ClassData\s*?=(.*?){{/im;
+ const reClassRaceData = /}}\s*?(?:Class|Race)Data\s*?=.*?{{/im;
+ const reSpellData = /}}\s*?SpellData\s*?=(.*?){{/im;
+ const reRepeatingTable = /^(repeating_.*)_\$(\d+)_.*$/;
+ const reNumSpellsData = /}}[\s\w\-]*?(?"],
+ [/\\lt;?/gm, "<"],
+ [/<<|«/g, "["],
+ [/\\lbrak;?/g, "["],
+ [/>>|»/g, "]"],
+ [/\\rbrak;?/g, "]"],
+ [/\\\^/g, "?"],
+ [/\\ques;?/g, "?"],
+ [/`/g, "@"],
+ [/\\at;?/g, "@"],
+ [/~/g, "-"],
+ [/\\dash;?/g, "-"],
+ [/\\n/g, "\n"],
+ [/¦/g, "|"],
+ [/\\vbar;?/g, "|"],
+ [/\\clon;?/g, ":"],
+ [/\\amp;?/g, "&"],
+ [/\\lpar;?/g, "("],
+ [/\\rpar;?/g, ")"],
+ [/\\cr;?/g, "
"],
+ [/&&/g, "/"],
+ [/%%/g, "%"],
+ [/\\comma;?/g, ","],
+ [/\\fs;?/g, "\\"],
+ ];
+
+ const dbEncoders = [
+ [/\\/gm,"\\\\"],
+ [/\r?\n/gm,'\\n'],
+ [/'/gm,"\\'"],
+ [/&/gm,"\\\\amp"],
+ [/>/gm,"\\\\gt"],
+ [/"],
+ [/\\lt/gm, "<"],
+ ];
+
+ const pallet = Object.freeze({
+ fancy: {
+ def: {outer:'yellow',outerpad:'',shadow:'rgba(0,0,0,0) 0px 0px',titlebox:'transparent',titletext:'white; text-shadow: 1px 1px 1px gray',titlepad:'5% 5% 2px 5%',bodypad:'4px 5% 10% 10%',bodybox:'',rowbox:'purple',rowdark:'transparent',rowdarktext:'black',rowlight:'transparent',rowlighttext:'black',rowpad:'4px',outerimg:'https://s3.amazonaws.com/files.d20.io/images/279722596/LxsTe-cbwk5j9L0ipM3GLw/thumb.jpg?1649510600',titleimg:'https://s3.amazonaws.com/files.d20.io/images/279800986/SqFez5dbn2roAsokDaBAPw/thumb.jpg?1649536002',bodyimg:'https://s3.amazonaws.com/files.d20.io/images/279800959/KyHThjxjXeZQ-b_uC6yCjQ/thumb.jpg?1649535995'},
+ spell: {outer:'black',outerpad:'',shadow:'rgba(0,0,0,0) 0px 0px',titlebox:'transparent',titletext:'white; text-shadow: 1px 1px 1px gray',titlepad:'20% 10% 2px 10%',bodypad:'4px 12% 5% 12%',bodybox:'',rowbox:'purple',rowdark:'transparent',rowdarktext:'black',rowlight:'transparent',rowlighttext:'black',rowpad:'4px',outerimg:'',titleimg:'https://s3.amazonaws.com/files.d20.io/images/279801150/vQ_1KKR72-7DTAusJzkt0w/thumb.png?1649536043',bodyimg:'https://s3.amazonaws.com/files.d20.io/images/279801125/opM7Y6m20DGLPeP-hrXpCA/thumb.png?1649536037'},
+ potion: {outer:'black',outerpad:'',shadow:'rgba(0,0,0,0) 0px 0px',titlebox:'transparent',titletext:'black; text-shadow: 1px 1px 1px gray',titlepad:'60% 10% 2px 10%',bodypad:'10% 5% 15% 8%',bodybox:'',rowbox:'mediumturquoise',rowdark:'transparent',rowdarktext:'white',rowlight:'transparent',rowlighttext:'white',rowpad:'4px',outerimg:'',titleimg:'https://s3.amazonaws.com/files.d20.io/images/279798022/Qgs1fGmOup8_9mtzoEeSxw/thumb.png?1649535031',bodyimg:'https://s3.amazonaws.com/files.d20.io/images/279798050/hQ4nWnVGPDINtjidt8-1eg/thumb.png?1649535040'},
+ weapon: {outer:'yellow',outerpad:'',shadow:'rgba(0,0,0,0) 0px 0px',titlebox:'transparent',titletext:'white; text-shadow: 1px 1px 1px gray',titlepad:'5% 5% 2px 5%',bodypad:'4px 5% 10% 10%',bodybox:'',rowbox:'purple',rowdark:'transparent',rowdarktext:'black; text-shadow: 1px 1px 1px white',rowlight:'transparent',rowlighttext:'black; text-shadow: 1px 1px 1px white',rowpad:'4px',outerimg:'https://s3.amazonaws.com/files.d20.io/images/279722596/LxsTe-cbwk5j9L0ipM3GLw/thumb.jpg?1649510600',titleimg:'',bodyimg:'https://s3.amazonaws.com/files.d20.io/images/257648113/iUlG62xcBc6AdUj5lv32Ww/max.png?1638047575'},
+ attack: {outer:'purple',outerpad:'',shadow:'rgba(0,0,0,0.4) 3px 3px',titlebox:'4px solid maroon',titletext:'white; text-shadow: 1px 1px 1px gray',titlepad:'4px 4px 1px 4px',bodypad:'0px',bodybox:'4px solid gray',rowbox:'none',rowdark:'white',rowdarktext:'black',rowlight:'white',rowlighttext:'black',rowpad:'4px',outerimg:'',titleimg:'',bodyimg:'',dmgslabel:'S/M',dmgllabel:'L'},
+ menu: {outer:'black',outerpad:'',shadow:'rgba(0,0,0,0.4) 3px 3px',titlebox:'blue',titletext:'white; text-shadow: 1px 1px 1px gray',titlepad:'1px 1px 1px 1px',bodypad:'4px 4px 4px 4px',bodybox:'',rowbox:'teal',rowdark:'lightblue',rowdarktext:'black',rowlight:'lightcyan',rowlighttext:'black',rowpad:'4px',outerimg:'',titleimg:'',bodyimg:''},
+ message: {outer:'black',outerpad:'',shadow:'rgba(0,0,0,0.4) 3px 3px',titlebox:'white',titletext:'black',titlepad:'1px 1px 1px 1px',bodypad:'4px 4px 4px 4px',bodybox:'1px solid black',rowbox:'',rowdark:'white',rowdarktext:'black',rowlight:'white',rowlighttext:'black',rowpad:'4px',outerimg:'',titleimg:'',bodyimg:''},
+ warning: {outer:'black',outerpad:'',shadow:'rgba(0,0,0,0.4) 3px 3px',titlebox:'crimson',titletext:'white',titlepad:'1px 1px 1px 1px',bodypad:'4px 4px 4px 4px',bodybox:'',rowbox:'purple',rowdark:'pink',rowdarktext:'black',rowlight:'mistyrose',rowlighttext:'black',rowpad:'4px',outerimg:'',titleimg:'',bodyimg:'',rowimg:''},
+ },
+ plain: {
+ def: {outer:'blue',outerpad:'',shadow:'rgba(0,0,0,0.4) 3px 3px',titlebox:'blue',titletext:'white; text-shadow: 1px 1px 1px gray',titlepad:'1px 1px 1px 1px',bodypad:'4px 4px 4px 4px',bodybox:'',rowbox:'purple',rowdark:'lightblue',rowdarktext:'black',rowlight:'lightcyan',rowlighttext:'black',rowpad:'4px',outerimg:'',titleimg:'',bodyimg:'',rowimg:''},
+ spell: {outer:'black',outerpad:'',shadow:'rgba(0,0,0,0) 0px 0px',titlebox:'firebrick',titletext:'white; text-shadow: 1px 1px 1px gray',titlepad:'1px 1px 1px 1px',bodypad:'4px 4px 4px 4px',bodybox:'',rowbox:'purple',rowdark:'pink',rowdarktext:'black',rowlight:'mistyrose',rowlighttext:'black',rowpad:'4px',outerimg:'',titleimg:'',bodyimg:'',rowimg:''},
+ potion: {outer:'black',outerpad:'',shadow:'rgba(0,0,0,0) 0px 0px',titlebox:'forestgreen',titletext:'white; text-shadow: 1px 1px 1px gray',titlepad:'1px 1px 1px 1px',bodypad:'4px 4px 4px 4px',bodybox:'',rowbox:'mediumturquoise',rowdark:'khaki',rowdarktext:'black',rowlight:'lightgoldenrodyellow',rowlighttext:'black',rowpad:'4px',outerimg:'',titleimg:'',bodyimg:'',rowimg:''},
+ weapon: {outer:'yellow',outerpad:'',shadow:'rgba(0,0,0,0) 0px 0px',titlebox:'gray',titletext:'white; text-shadow: 1px 1px 1px gray',titlepad:'1px 1px 1px 1px',bodypad:'4px 4px 4px 4px',bodybox:'',rowbox:'purple',rowdark:'gainsboro',rowdarktext:'black; text-shadow: 1px 1px 1px white',rowlight:'ghostwhite',rowlighttext:'black; text-shadow: 1px 1px 1px white',rowpad:'4px',outerimg:'',titleimg:'',bodyimg:'',rowimg:''},
+ attack: {outer:'purple',outerpad:'',shadow:'rgba(0,0,0,0.4) 3px 3px',titlebox:'4px solid maroon',titletext:'white; text-shadow: 1px 1px 1px gray',titlepad:'4px 4px 1px 4px',bodypad:'0px',bodybox:'4px solid gray',rowbox:'none',rowdark:'white',rowdarktext:'black',rowlight:'white',rowlighttext:'black',rowpad:'4px',outerimg:'',titleimg:'',bodyimg:'',dmgslabel:'S/M',dmgllabel:'L'},
+ menu: {outer:'black',outerpad:'',shadow:'rgba(0,0,0,0.4) 3px 3px',titlebox:'blue',titletext:'white; text-shadow: 1px 1px 1px gray',titlepad:'1px 1px 1px 1px',bodypad:'4px 4px 4px 4px',bodybox:'',rowbox:'teal',rowdark:'lightblue',rowdarktext:'black',rowlight:'lightcyan',rowlighttext:'black',rowpad:'4px',outerimg:'',titleimg:'',bodyimg:'',rowimg:''},
+ message: {outer:'black',outerpad:'',shadow:'rgba(0,0,0,0) 0px 0px',titlebox:'white',titletext:'black',titlepad:'1px 1px 1px 1px',bodypad:'4px 4px 4px 4px',bodybox:'1px solid black',rowbox:'',rowdark:'white',rowdarktext:'black',rowlight:'white',rowlighttext:'black',rowpad:'4px',outerimg:'',titleimg:'',bodyimg:''},
+ warning: {outer:'black',outerpad:'',shadow:'rgba(0,0,0,0.4) 3px 3px',titlebox:'crimson',titletext:'white',titlepad:'1px 1px 1px 1px',bodypad:'4px 4px 4px 4px',bodybox:'',rowbox:'purple',rowdark:'pink',rowdarktext:'black',rowlight:'mistyrose',rowlighttext:'black',rowpad:'4px',outerimg:'',titleimg:'',bodyimg:'',rowimg:''},
+ },
+ dark: {
+ def: {outer:'darkgoldenrod',outerpad:'',shadow:'rgba(0,0,0,0.4) 3px 3px',titlebox:'blue',titletext:'white; text-shadow: 1px 1px 1px gray',titlepad:'1px 1px 1px 1px',bodypad:'4px 4px 4px 4px',bodybox:'',rowbox:'purple',rowdark:'navy',rowdarktext:'white',rowlight:'blueviolet',rowlighttext:'white',rowpad:'4px',outerimg:'',titleimg:'',bodyimg:'',rowimg:''},
+ spell: {outer:'black',outerpad:'',shadow:'rgba(0,0,0,0) 0px 0px',titlebox:'firebrick',titletext:'white; text-shadow: 1px 1px 1px gray',titlepad:'1px 1px 1px 1px',bodypad:'4px 4px 4px 4px',bodybox:'',rowbox:'purple',rowdark:'darkmagenta',rowdarktext:'white',rowlight:'darkorchid',rowlighttext:'white',rowpad:'4px',outerimg:'',titleimg:'',bodyimg:'',rowimg:''},
+ potion: {outer:'black',outerpad:'',shadow:'rgba(0,0,0,0) 0px 0px',titlebox:'forestgreen',titletext:'white; text-shadow: 1px 1px 1px gray',titlepad:'1px 1px 1px 1px',bodypad:'4px 4px 4px 4px',bodybox:'',rowbox:'mediumturquoise',rowdark:'darkgoldenrod',rowdarktext:'white; text-shadow: 1px 1px 1px black',rowlight:'goldenrod',rowlighttext:'white; text-shadow: 1px 1px 1px black',rowpad:'4px',outerimg:'',titleimg:'',bodyimg:'',rowimg:''},
+ weapon: {outer:'darkgoldenrod',outerpad:'',shadow:'rgba(0,0,0,0) 0px 0px',titlebox:'gray',titletext:'white; text-shadow: 1px 1px 1px gray',titlepad:'1px 1px 1px 1px',bodypad:'4px 4px 4px 4px',bodybox:'',rowbox:'purple',rowdark:'dimgray',rowdarktext:'white; text-shadow: 1px 1px 1px black',rowlight:'darkgray',rowlighttext:'white; text-shadow: 1px 1px 1px black',rowpad:'4px',outerimg:'',titleimg:'',bodyimg:'',rowimg:''},
+ attack: {outer:'purple',outerpad:'',shadow:'rgba(0,0,0,0.4) 3px 3px',titlebox:'4px solid maroon',titletext:'white; text-shadow: 1px 1px 1px gray',titlepad:'4px 4px 1px 4px',bodypad:'0px',bodybox:'4px solid gray',rowbox:'none',rowdark:'dimgray',rowdarktext:'white; text-shadow: 1px 1px 1px black',rowlight:'dimgray',rowlighttext:'white; text-shadow: 1px 1px 1px black',rowpad:'4px',outerimg:'',titleimg:'',bodyimg:'',dmgslabel:'S/M',dmgllabel:'L'},
+ menu: {outer:'black',outerpad:'',shadow:'rgba(0,0,0,0.4) 3px 3px',titlebox:'blue',titletext:'white; text-shadow: 1px 1px 1px gray',titlepad:'1px 1px 1px 1px',bodypad:'4px 4px 4px 4px',bodybox:'',rowbox:'teal',rowdark:'navy',rowdarktext:'white',rowlight:'blueviolet',rowlighttext:'white',rowpad:'4px',outerimg:'',titleimg:'',bodyimg:'',rowimg:''},
+ message: {outer:'white',outerpad:'',shadow:'rgba(0,0,0,0) 0px 0px',titlebox:'black',titletext:'white',titlepad:'1px 1px 1px 1px',bodypad:'4px 4px 4px 4px',bodybox:'1px solid white',rowbox:'',rowdark:'black',rowdarktext:'white',rowlight:'black',rowlighttext:'white',rowpad:'4px',outerimg:'',titleimg:'',bodyimg:''},
+ warning: {outer:'black',outerpad:'',shadow:'rgba(0,0,0,0.4) 3px 3px',titlebox:'crimson',titletext:'white',titlepad:'1px 1px 1px 1px',bodypad:'4px 4px 4px 4px',bodybox:'',rowbox:'purple',rowdark:'darkmagenta',rowdarktext:'white',rowlight:'darkorchid',rowlighttext:'white',rowpad:'4px',outerimg:'',titleimg:'',bodyimg:'',rowimg:''},
+ }
+ });
+
+ const acImg = 'https://s3.amazonaws.com/files.d20.io/images/280889787/N6NbbkLDe92C4e5DDtmkaw/thumb.png?1650135426';
+ const dmgImg = 'https://s3.amazonaws.com/files.d20.io/images/280890292/ZBDEOKwQHCPeY2yQJuhkeA/thumb.png?1650135612';
+ const hpImg = 'https://s3.amazonaws.com/files.d20.io/images/281063429/1ySUC06qy_MuhY-_Be_pVQ/thumb.png?1650223020';
+ const slashImg = 'https://s3.amazonaws.com/files.d20.io/images/281331848/XnspIFctdnld8LVG3_m5RQ/thumb.png?1650393752';
+ const pierceImg = 'https://s3.amazonaws.com/files.d20.io/images/281331832/DYgW_xqlORNJ77oigkFqAA/thumb.png?1650393745';
+ const bludgeonImg = 'https://s3.amazonaws.com/files.d20.io/images/281331818/rSZVRXYkRNR4K9Ru6CXbVw/thumb.png?1650393737';
+ const sacImg = 'https://s3.amazonaws.com/files.d20.io/images/281054605/oNYktYKEmF9_ngePXyUcPw/thumb.png?1650219538';
+ const pacImg = 'https://s3.amazonaws.com/files.d20.io/images/281054578/FeeVqF8X-rgeEP6fg4CWKg/thumb.png?1650219526';
+ const bacImg = 'https://s3.amazonaws.com/files.d20.io/images/281054552/-i1SuQ4Rx1OO7cXPtlggNg/thumb.png?1650219515';
+ const heart = ['https://s3.amazonaws.com/files.d20.io/images/281063429/1ySUC06qy_MuhY-_Be_pVQ/thumb.png?1650223020',
+ 'https://s3.amazonaws.com/files.d20.io/images/281334582/74iFWTTF47pFyvmGd1WWnw/thumb.png?1650395033',
+ 'https://s3.amazonaws.com/files.d20.io/images/281334596/8yhpUHhYL7bQIlLZfHGqjw/thumb.png?1650395041',
+ 'https://s3.amazonaws.com/files.d20.io/images/281334612/FpyTBj_oaJS6_GaZmlsfvA/thumb.png?1650395047',
+ 'https://s3.amazonaws.com/files.d20.io/images/281334628/-4nbQ36qch58EK0BWgytRA/thumb.png?1650395055',
+ 'https://s3.amazonaws.com/files.d20.io/images/281334655/OH7HS1U-xYFUTJjIAypqFQ/thumb.png?1650395061',
+ 'https://s3.amazonaws.com/files.d20.io/images/281334666/CByDv4kCKWcaV9LabxwV-A/thumb.png?1650395067',
+ 'https://s3.amazonaws.com/files.d20.io/images/281334682/xbcpGhbmeEAf3ZmKEgs3Ow/thumb.png?1650395074',
+ 'https://s3.amazonaws.com/files.d20.io/images/281334693/xTlsD3NHddK4g3-nlQVCDg/thumb.png?1650395080'
+ ];
+
+ var DBindex;
+ var classesParsed = false;
+ var magicList = {};
+ var RPGMap = {};
+ var apis = {magic:false,attk:false,init:false};
+ var lastMsg = [];
+ var doneRNmsg = false;
+ var waitList = {};
+ var msg_orig = {};
+ var showMoreObj = {};
+
+ const isString = (s) => 'string' === typeof s || s instanceof String;
+ const isArray = (a) => Array.isArray(a);
+ const flatten = (a) => isArray(a) ? a.reduce((m,e)=>[...m, ...flatten(e)],[]) : [a];
+
+ /**
+ * In the inline roll evaluator from ChatSetAttr script v1.9
+ * by Joe Singhaus and C Levett.
+ **/
+
+ var processInlinerolls = function (msg) {
+ if (msg.inlinerolls && msg.inlinerolls.length) {
+ return msg.inlinerolls.map(v => {
+ const ti = v.results.rolls.filter(v2 => v2.table)
+ .map(v2 => v2.results.map(v3 => v3.tableItem.name).join(", "))
+ .join(", ");
+ return (ti.length && ti) || v.results.total || 0;
+ })
+ .reduce((m, v, k) => m.replace(new RegExp('\\$\\[\\['+k+'\\]\\]','img'),'[['+v+'['+msg.inlinerolls[k].expression+'] ]]'), msg.content);
+ } else {
+ return msg.content;
+ }
+ };
+
+ /*
+ * Check the version of a Character Sheet database against
+ * the current version in the API. Delete old versions so
+ * API versions are used and indexed.
+ */
+
+ var del_Old_DBs = function() {
+
+ var update = false;
+
+ _.each( dbNames, (db,dbName) => {
+ let dbFullName = dbName.replace(/_/g,'-'),
+ dbCS = findObjs({ type:'character', name:dbFullName },{caseInsensitive:true}),
+ dbVersion = 0.0,
+ msg, versionObj;
+
+ if (!dbCS || !dbCS.length) return;
+
+ dbCS = dbCS[0];
+
+ if (_.isUndefined(LibFunctions.attrLookup( dbCS, fields.dbVersion ))) {
+ setTimeout( () => del_Old_DBs(), 5000 );
+ return;
+
+ } else {
+
+ dbVersion = parseFloat(LibFunctions.attrLookup( dbCS, fields.dbVersion )) || dbVersion;
+
+ if (dbVersion >= (parseFloat(db.version) || 0)) return;
+
+ log('Deleting '+dbFullName+' v'+dbVersion);
+ dbCS.remove();
+ update = true;
+ }
+ });
+ if (update) LibFunctions.updateDBindex();
+ return;
+ }
+
+ /*
+ * Clear any waiting timer action saved in the playerConfig
+ */
+
+ var clearWaitTimer = function(pid) {
+ if (pid && waitList[pid]) {
+ clearTimeout(waitList[pid]);
+ waitList[pid] = undefined;
+ } else if (!pid) {
+ _.each(waitList,w=>clearTimeout(w));
+ waitList = {};
+ }
+ return;
+ };
+
+
+ /*
+ * Function to generate unique IDs for creating objects in Roll20
+ */
+
+ const generateUUID = function () {
+ var a = 0,
+ b = [];
+ return function () {
+ var c = (new Date()).getTime() + 0,
+ d = c === a;
+ a = c;
+ for (var e = new Array(8), f = 7; 0 <= f; f--) {
+ e[f] = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".charAt(c % 64);
+ c = Math.floor(c / 64);
+ }
+ c = e.join("");
+ if (d) {
+ for (f = 11; 0 <= f && 63 === b[f]; f--) {
+ b[f] = 0;
+ }
+ b[f]++;
+ } else {
+ for (f = 0; 12 > f; f++) {
+ b[f] = Math.floor(64 * Math.random());
+ }
+ }
+ for (f = 0; 12 > f; f++) {
+ c += "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".charAt(b[f]);
+ }
+ return c;
+ };
+ }();
+ const generateRowID = function () {
+ return generateUUID().replace(/_/g, "Z");
+ };
+
+ /**
+ * Find the GM, generally when a player can't be found
+ */
+
+ var findTheGM = function() {
+ var playerGM,
+ players = findObjs({ _type:'player' });
+
+ if (players.length !== 0) {
+ if (!_.isUndefined(playerGM = _.find(players, function(p) {
+ var player = p;
+ if (player) {
+ if (playerIsGM(player.id)) {
+ return player.id;
+ }
+ }
+ }))) {
+ return playerGM.id;
+ }
+ }
+ return undefined;
+ }
+
+ /*
+ * Display a message with a link to the Release Notes
+ */
+
+ var displayReleaseNotesLink = function() {
+ var handoutIDs = LibFunctions.getHandoutIDs();
+ if (!doneRNmsg) {
+ doneRNmsg = true;
+ LibFunctions.sendFeedback(waitMsgDiv+'You can read the latest **[Release Notes here]('+fields.journalURL+handoutIDs.RPGMReleaseNotes+')**, version '+handouts.RPGM_Release_Notes.version+' updated '+(new Date(lastUpdate*1000).toDateString())+'
');
+ }
+ }
+
+ String.prototype.dbName = function() {
+ return this.toLowerCase().replace(reIgnore,'');
+ }
+
+ String.prototype.dispName = function() {
+ return (this || '').replace(/[-_]([^\d])/g,' $1');
+ }
+
+ String.prototype.hyphened = function() {
+ return (this || '').replace(/\s/g,'-');
+ }
+
+ String.prototype.trueCompare = function(txt) {
+ return (this || '').dbName() === (String(txt) || '').dbName();
+ }
+
+ class AbilityObj {
+ constructor( dBname, abilityObj, ctObj, source ) {
+ this.dB = dBname;
+ this.obj = (_.isUndefined(abilityObj) || !_.isArray(abilityObj) || abilityObj.length < 2) ? abilityObj : [_.clone(abilityObj[0]),_.clone(abilityObj[1])];
+ this.ct = ctObj;
+ this.source = source;
+ this.api = (!!abilityObj && !!abilityObj[1]) ? (abilityObj[1].body.trim()[0] == '!') : false;
+ }
+
+ specs(re = reSpecs) {
+ let specStr = (!this.obj || !this.obj[1]) ? undefined : this.obj[1].body.match(re);
+ return specStr ? [...('['+specStr[0]+']').matchAll(reSpecsAll)] : undefined;
+ }
+ data(re = reData) {
+ let specStr = (!this.obj || !this.obj[1]) ? undefined : this.obj[1].body.match(re);
+ return specStr ? [...('['+specStr[0]+']').matchAll(reDataAll)] : undefined;
+ }
+ hands(re = reSpecs) {
+ let specStr = (!this.obj || !this.obj[1]) ? undefined : this.obj[1].body.match(re);
+ return specStr ? [...('['+specStr[0]+']').matchAll(reHands)].concat([...('['+specStr[0]+']').matchAll(reHands2)] || []) : undefined;
+ }
+ classes() {
+ /**
+ * Search a database object body for the object "class"
+ **/
+ let objType = [],
+ specs = (!this.obj || !this.obj[1]) ? undefined : this.obj[1].body.match(/}}\s*?specs\s*?=(.*?){{/im);
+ specs = specs ? [...('['+specs[0]+']').matchAll(reSpecClass)] : [];
+ for (let i=0; i < specs.length; i++) {
+ objType.push(specs[i][1]);
+ }
+ return _.uniq(objType.join('|').toLowerCase().split('|')).join('|');
+ }
+
+
+ }
+
+ class CharTable {
+ constructor( property, attrs, defaultVal ) {
+ if (!property || !isArray(property) || property.length < 2) throw new Error('Invalid attribute definition in table constructor');
+ this.property = property;
+ this.attrs = attrs || {};
+ this.defaultVal = defaultVal || {current:'',max:''};
+ }
+ }
+
+ class CharTableArray {
+ constructor( character, table, col ) {
+ if (!character) throw new Error('Invalid character object in table constructor');
+ if (!table || !isArray(table) || table.length < 2) throw new Error('Invalid table definition in table constructor');
+ this.character = character;
+ this.table = table;
+ this.tableType;
+ this.fieldGroup;
+ this.values = {};
+ this.sortKeys;
+ this.col = (_.isUndefined(col) || _.isNull(col) || (table && !_.isNull(table) && !table[1] && col && col==1)) ? '' : col;
+ }
+
+ /*
+ * A method to get the whole of a repeating table in
+ * two parts: an array of objects indexed by Roll20 object IDs,
+ * and an array of object IDs indexed by repeating table row number.
+ * Returns an object containing the table, and all parameters defining
+ * that table and where it came from.
+ */
+
+ addTable(attrDef,defaultVal=null,caseSensitive) {
+ if (_.isUndefined(attrDef) || !isArray(attrDef) || attrDef.length < 2) throw new Error('No table attribute supplied to addTable() for '+this.table[0]+', attrDef '+attrDef);
+ let rowName, index = 0, name = attrDef[0];
+ if (this.table && !_.isNull(this.table)) {
+ rowName = this.table[0]+this.col+'_$0_'+attrDef[0]+this.col;
+ } else {
+ rowName = name;
+ }
+
+ if (_.isUndefined(defaultVal) || _.isNull(defaultVal)) {
+ defaultVal=attrDef[2];
+ }
+
+ if (!this.hasOwnProperty(name)) {
+ this[name] = new CharTable( attrDef );
+ }
+ if (attrDef[1] === 'max' && _.isUndefined(this[name].defaultVal.current)) this[name].defaultVal.current = '';
+ if (attrDef[1] === 'current' && _.isUndefined(this[name].defaultVal.max)) this[name].defaultVal.max = '';
+ this[name].defaultVal[attrDef[1]] = defaultVal;
+ let match=rowName.match(/^(repeating_.*)_\$(\d+)_.*$/);
+
+ if(match){
+ let createOrderKeys=[],
+ attrMatcher=new RegExp(`^${rowName.replace(/_\$\d+_/,'_([-\\da-zA-Z]+)_')}$`,(caseSensitive?'i':'')),
+ orderMatcher=new RegExp(`^${this.table[0]}${this.col}_([-\\da-zA-Z]+)_`,(caseSensitive?'i':'')),
+ attrs=_.chain(findObjs({type:'attribute', characterid:this.character.id}))
+ .map((a)=>{
+ let orderKey = (a.get('name').match(orderMatcher)||['',''])[1];
+ if (orderKey && !createOrderKeys.includes(orderKey)) {
+ createOrderKeys.push(orderKey);
+ };
+ return {attr:a,match:a.get('name').match(attrMatcher)};
+ })
+ .filter((o)=>o.match)
+ .reduce((m,o)=>{ m[o.match[1]]=o.attr; return m;},{})
+ .value(),
+ sortOrderKeys = _.chain( ((findObjs({
+ type:'attribute',
+ characterid:this.character.id,
+ name: `_reporder_${match[1]}`
+ })[0]||{get:_.noop}).get('current') || '' ).split(/\s*,\s*/))
+ .intersection(createOrderKeys)
+ .union(createOrderKeys)
+ .value();
+
+ if (_.isUndefined(this.sortKeys)) {
+ this.sortKeys = sortOrderKeys;
+ } else {
+ this.sortKeys = (sortOrderKeys.length > this.sortKeys.length) ? _.union(sortOrderKeys,this.sortKeys) : _.union(this.sortKeys,sortOrderKeys);
+ }
+ this[name].attrs=attrs;
+ if (_.isUndefined(this.values[attrDef[0]])) {
+ this.values[attrDef[0]] = Object.create({current:'',max:''});
+ }
+ this.values[attrDef[0]][attrDef[1]] = attrDef[2] || '';
+ } else {
+ this[name].attrs=[];
+ if (_.isUndefined(this.sortKeys)) {
+ this.sortKeys = [];
+ }
+ }
+ return this;
+ }
+
+ /*
+ * Find all the necessary tables to manage a repeating
+ * section of a character sheet. Dynamically driven by
+ * the table field definitions in the 'fields' object.
+ */
+
+ addAllTables( fieldGroup, caseSensitive ) {
+
+ var rows = {};
+
+ this.fieldGroup = fieldGroup;
+ this.values = {};
+ _.each( fields, (elem,key) => {
+ if (key.startsWith(fieldGroup)
+ && ['current','max'].includes(String(elem[1]).toLowerCase())) {
+ rows[key]=elem;
+ };
+ });
+ _.each(rows, (elem,key) => {
+ this.addTable( elem, elem[2], caseSensitive );
+ });
+ return this;
+ }
+
+ /**
+ * A function to take a table obtained using getTableField() and a row number, and
+ * safely return the value of the table row, or undefined. Uses the table object
+ * parameters such as the character object it came from and the field property.
+ * If the row entry is undefined use a default value if set in the getTableField() call,
+ * which can be overridden with an optional parameter. Can just return the row
+ * object or can return a different property of the object using the second optional parameter.
+ */
+
+ tableLookup( attrDef, index, defVal, retObj ) {
+
+ if (!attrDef || !isArray(attrDef) || attrDef.length < 2) throw new Error('No table attribute supplied to tableLookup() for '+this.table[0]+', attrDef '+attrDef);
+ var val, name = attrDef[0];
+ if (_.isUndefined(retObj)) {
+ retObj=false;
+ } else if (retObj === true) {
+ defVal=false;
+ }
+ if (_.isUndefined(defVal)) {
+ defVal=true;
+ }
+ if (this[name] && !_.isUndefined(index)) {
+ let property = (retObj === true) ? null : ((retObj === false) ? attrDef : retObj);
+ defVal = (defVal===false) ? (undefined) : ((defVal===true) ? this[name].defaultVal[attrDef[1]] : defVal);
+// if (!_.isUndefined(defVal)) defVal = String(defVal);
+ if (index>=0) {
+ let attrs = this[name].attrs,
+ sortOrderKeys = this.sortKeys;
+ if (index
{
+ if (_.isUndefined(elem.attrs)) return;
+ currentVal = (!rowVals || _.isUndefined(rowVals[key])) ? elem.defaultVal['current'] : rowVals[key]['current'];
+ maxVal = (!rowVals || _.isUndefined(rowVals[key])) ? elem.defaultVal['max'] : rowVals[key]['max'];
+ this.tableSet( [key,'current'], index, currentVal );
+ this.tableSet( [key,'max'], index, maxVal );
+ });
+ } else {
+ if (index > this.sortKeys.length) {
+ this.addTableRow( index-1, undefined );
+ }
+ let rowObjID = generateRowID();
+ let namePt1 = this.table[0]+this.col+'_'+rowObjID+'_';
+ let gotVals = !!rowVals && _.pairs(rowVals).length > 0;
+ _.each( list, (elem,key) => {
+ if (_.isUndefined(elem.attrs)) return;
+ rowObj = createObj( "attribute", {characterid: this.character.id, name: (namePt1+key+this.col)} );
+ if (!gotVals) {
+ newVal = _.isUndefined(this.values[key]) ? elem.defaultVal : this.values[key] ;
+ } else {
+ newVal = rowVals[key];
+ }
+ rowObj.set(newVal);
+ this[key].attrs[rowObjID] = rowObj;
+ this.sortKeys[index] = rowObjID;
+ });
+ }
+ return this;
+ }
+
+ /*
+ * Delete / remove a table row completely
+ */
+
+ delTableRow( index ) {
+
+ let rowObj, newVal, currentVal, maxVal, list = this;
+
+ let fieldGroup = this.fieldGroup;
+ if (!fieldGroup) throw new Error('undefined addTableRow fieldGroup');
+ if (index) index = parseInt(index);
+ if (_.isUndefined(index) || isNaN(index)) return this;
+
+ if ((index < 0) || ((index >= this.sortKeys.length) || _.isUndefined(this.tableLookup( fields[fieldGroup+'name'], index, false )))) return this;
+
+ _.each( list, (elem,key) => {
+ if (_.isUndefined(elem.attrs)) return;
+ elem.attrs[this.sortKeys[index]].remove();
+ _.omit(elem.attrs,this.sortKeys[index]);
+ });
+ this.sortKeys.splice(index,1);
+ return this;
+ };
+
+ /*
+ * A function to find the index of a matching entry in a table
+ */
+
+ tableFind( attrDef, val, def=true, every=false ) {
+
+ let findRE = _.isRegExp(val);
+ let findArray = _.isArray(val);
+ let findVal = !findRE && !findArray && (_.isString(val) || _.isNumber(val) || _.isBoolean(val));
+ if (!(findRE || findArray || findVal)) {
+ LibFunctions.sendError('Invalid search term "'+val+'" of type '+typeof val+' for tableFind while searching '+this.table[0]);
+ return undefined;
+ };
+ if (findVal) val = String(val).dbName() || '-';
+ if (findArray) val = val.map(v => String(v).dbName() || '-');
+ let property = attrDef[1];
+ let attrVal = LibFunctions.attrLookup( this.character, attrDef );
+ let indexArray = [], tableIndex = -1;
+
+ if ((this.table[1] < 0) && (findVal ? (val === String(attrVal.dbName() || '-')) :
+ (findRE ? (attrVal.search(val) >= 0) :
+ (findArray ? (val.includes(attrVal.dbName() || '-')) :
+ false )))) {
+ if (!every) return tableIndex;
+ indexArray.push(tableIndex);
+ }
+/* if (!every) {
+ tableIndex = this.sortKeys.indexOf(
+ _.findKey(this[attrDef[0]].attrs, function( elem ) {
+ elem = (!def || _.isUndefined(attrDef[2])) ? undefined : String(attrDef[2]);
+ return (_.isUndefined(elem) ? false :
+ (findVal ? (val === (String(elem.get(property)).dbName() || '-')) :
+ (findRE ? String((elem.get(property)).search(val) >= 0) :
+ (findArray ? (val.includes(String(elem.get(property)).dbName() || '-')) :
+ false ))));
+ })
+ );
+ } else {
+*/ let attrs = this[attrDef[0]].attrs;
+ for (let i=0; i < this.sortKeys.length; i++) {
+ let elem = _.has(attrs,this.sortKeys[i]) ? attrs[this.sortKeys[i]].get(property) : (def ? attrDef[2] : undefined);
+// log('tableFind: row '+i+' value = '+elem+(findVal ? ' against '+val : ''));
+ if (!_.isUndefined(elem)) {
+ if (findVal ? (val === (String(elem).dbName() || '-')) :
+ (findRE ? (String(elem).search(val) >= 0) :
+ (findArray ? (val.includes(String(elem).dbName() || '-')) :
+ false ))) {
+
+ indexArray.push(i);
+ if (tableIndex < 0) tableIndex = i;
+// log('tableFind: found at row '+i+', every is '+every);
+ if (!every) break;
+ };
+ };
+ };
+// };
+
+ if (tableIndex < 0 && ((findVal && val === '-') || (findArray && val.includes('-')))) {
+ tableIndex = this.sortKeys.length;
+ indexArray.push(tableIndex);
+ }
+// log('tableFind: indexArray.length = '+indexArray.length+' so returning '+(!indexArray.length ? undefined : (every ? indexArray : tableIndex)));
+ return !indexArray.length ? undefined : (every ? indexArray : tableIndex)
+ };
+
+ /*
+ * Another way of calling tableFind() with the every parameter set to true
+ */
+
+ tableFindAll( attrDef, val, def ) {
+ return this.tableFind( attrDef, val, def, true );
+ };
+
+ /*
+ * A function to set all rows of just one field of a table to
+ * a provided value, or its default if value not provided
+ */
+
+ tableDefault( attrDef, val ) {
+ if (!attrDef || !isArray(attrDef) || attrDef.length < 2) throw new Error('No table attribute supplied to tableDefault() for '+this.table[0]+', attrDef '+attrDef);
+ if (_.isUndefined(val) || _.isNull(val)) val = this[attrDef[0]].defaultVal[attrDef[1]];
+ if (!this[attrDef[0]]) throw new Error('Invalid table attribute '+attrDef[0]+' supplied for '+this.table[0]);
+ _.each(this[attrDef[0]].attrs, obj => {
+ obj.set(attrDef[1],val);
+ });
+ return this;
+ }
+
+ /*
+ * Make a copy of the default values for the table
+ */
+
+ copyValues() {
+ let newValues = {};
+ _.each( this.values, (v,k) => newValues[k] = Object.create(v));
+ return newValues;
+ }
+
+ }
+
+ class CSdbIndex {
+ constructor() {
+ this.mu_spells_db = {};
+ this.pr_spells_db = {};
+ this.powers_db = {};
+ this.mi_db = {};
+ this.race_db = {};
+ this.class_db = {};
+ this.attacks_db = {};
+ }
+ }
+
+
+ class LibFunctions {
+
+ static init(){
+
+ /** ------------------------------- Table Management ---------------------------- **/
+
+ /*
+ * A function to get the whole of a repeating table in
+ * two parts: an array of objects indexed by Roll20 object IDs,
+ * and an array of object IDs indexed by repeating table row number.
+ * Returns an object containing the table, and all parameters defining
+ * that table and where it came from.
+ */
+
+ LibFunctions.getTableField = function(character,tableObj,tableDef,attrDef,col,defaultVal=null,caseSensitive) {
+ if (_.isUndefined(tableObj) || _.isUndefined(tableObj.table)) tableObj = new CharTableArray( character, tableDef, col );
+ tableObj.addTable( attrDef, defaultVal, caseSensitive );
+ return tableObj;
+ }
+
+ /*
+ * Find all the necessary tables to manage a repeating
+ * section of a character sheet. Dynamically driven by
+ * the table field definitions in the 'fields' object.
+ */
+
+ LibFunctions.getTable = function( character, fieldGroup, col, tableObj, caseSensitive ) {
+ if (!fieldGroup) return undefined;
+ let tableDef = fieldGroup.tableDef;
+ if (_.isUndefined(tableObj) || _.isUndefined(tableObj.table)) tableObj = new CharTableArray( character, tableDef, col );
+ tableObj.addAllTables( fieldGroup.prefix, caseSensitive );
+ return tableObj;
+ }
+
+ /*
+ * Get all tables in a particular numbered group of tables,
+ * based not on columns but on a numbered sequence of prefixes
+ */
+
+ LibFunctions.getLvlTable = function( character, fieldGroup, lvl, tableObj, caseSensitive ) {
+ if (_.isUndefined(lvl) || _.isNull(lvl)) lvl = '';
+ let tableDef = [fieldGroup.tableDef[0]+lvl,fieldGroup.tableDef[1]];
+ if (_.isUndefined(tableObj) || _.isUndefined(tableObj.table)) tableObj = new CharTableArray( character, tableDef, null );
+ tableObj.addAllTables( fieldGroup.prefix, caseSensitive );
+ return tableObj;
+ }
+
+ /*
+ * Function to initialise a values[] array to hold data for
+ * setting a table row to.
+ */
+
+ LibFunctions.initValues = function( fieldGroup, values ) {
+
+// let values = [new Set()];
+ if (_.isUndefined(values)) values = {};
+ let rows = _.filter( fields, (elem,f) => f.startsWith(fieldGroup))
+ .map(elem => {
+ if (_.isUndefined(values[elem[0]])) {
+ values[elem[0]] = {current:'',max:''};
+ }
+ values[elem[0]][elem[1]] = elem[2] || '';
+ });
+ return values;
+ }
+
+ /** ------------------------ Attribute Management ------------------------------ **/
+
+ /**
+ * A function to return the handle for the 'fields' object for the represented
+ * character sheet mapping, and an object of handles for other game-specific values.
+ **/
+
+ LibFunctions.getRPGMap = function() {
+ RPGMap.dbNames = dbNames;
+ RPGMap.fieldGroups = fieldGroups;
+ RPGMap.miTypeLists = miTypeLists;
+ RPGMap.clTypeLists = clTypeLists;
+ RPGMap.spTypeLists = spTypeLists;
+ RPGMap.classMap = classMap;
+ RPGMap.baseThac0table = baseThac0table;
+ RPGMap.spellsPerLevel = spellsPerLevel;
+ RPGMap.spellLevels = spellLevels;
+ RPGMap.specMU = specMU;
+ RPGMap.ordMU = ordMU;
+ RPGMap.wisdomSpells = wisdomSpells;
+ RPGMap.casterLevels = casterLevels;
+ RPGMap.primeClasses = primeClasses;
+ RPGMap.classLevels = classLevels;
+ RPGMap.rangedWeapMods = rangedWeapMods;
+ RPGMap.saveLevels = saveLevels;
+ RPGMap.baseSaves = baseSaves;
+ RPGMap.classSaveMods = classSaveMods;
+ RPGMap.raceSaveMods = raceSaveMods;
+ RPGMap.defaultNonProfPenalty = defaultNonProfPenalty;
+ RPGMap.classNonProfPenalty = classNonProfPenalty;
+ RPGMap.raceToHitMods = raceToHitMods;
+ RPGMap.classAllowedWeaps = classAllowedWeaps;
+ RPGMap.classAllowedArmour = classAllowedArmour;
+ RPGMap.weapMultiAttks = weapMultiAttks;
+ RPGMap.punchWrestle = punchWrestle;
+ RPGMap.saveFormat = saveFormat;
+ RPGMap.rogueSkills = rogueSkills;
+ RPGMap.thiefSkillFactors = thiefSkillFactors;
+ RPGMap.rogueDexMods = rogueDexMods;
+ RPGMap.reSpellSpecs = reSpellSpecs;
+ RPGMap.reWeapSpecs = reWeapSpecs;
+ RPGMap.reACSpecs = reACSpecs;
+ RPGMap.reModSpecs = reModSpecs;
+ RPGMap.reClassSpecs = reClassSpecs;
+ RPGMap.reThiefSpecs = reThiefSpecs;
+ RPGMap.reNPCThiefSpecs = reNPCThiefSpecs;
+ RPGMap.reSaveSpecs = reSaveSpecs;
+ RPGMap.reAttr = reAttr;
+ RPGMap.showMoreObj = showMoreObj;
+ return [fields,RPGMap];
+ }
+
+ /**
+ * A function to lookup the value of any attribute, including repeating rows, without errors
+ * thus avoiding the issues with getAttrByName()
+ *
+ * Thanks to The Aaron for this, which I have modded to split and
+ * allow tables to be loaded once rather than multiple times.
+ */
+
+ LibFunctions.attrLookup = function(character,attrDef,tableDef,r,c='',caseSensitive=false, defValParam=true) {
+ let name, match,
+ property = attrDef[1];
+
+ if (!character || !character.id || (tableDef && isNaN(r))) return undefined;
+
+ if (tableDef && (tableDef[1] || r >= 0)) {
+ c = (tableDef[1] || c != 1) ? c : '';
+ name = tableDef[0] + c + '_$' + r + '_' + attrDef[0] + c;
+ } else {
+ name = attrDef[0];
+ }
+ let defVal = (defValParam === false ? undefined : (defValParam === true ? attrDef[2] : defValParam));
+ if (!_.isUndefined(defVal)) defVal = String(defVal);
+ match=name.match(/^(repeating_.*)_\$(\d+)_.*$/);
+ if(match){
+ let index=match[2];
+ let tableObj = new CharTableArray( character, tableDef, c );
+ tableObj.addTable(attrDef,null,caseSensitive);
+ return tableObj.tableLookup(attrDef,index,defValParam,!attrDef[1]);
+ } else {
+ let attrObj = findObjs({ type:'attribute', characterid:character.id, name:name}, {caseInsensitive: !caseSensitive});
+ if (!attrObj || attrObj.length == 0) {
+ return (_.isUndefined(property) || _.isNull(property)) ? undefined : defVal;
+ } else if (_.isUndefined(property) || _.isNull(property)) {
+ return getObj('attribute',attrObj[0].id);
+ } else {
+ let value = getObj('attribute',attrObj[0].id).get(property);
+ return (_.isUndefined(value) ? defVal : String(value));
+ }
+ }
+ }
+
+ /**
+ * Check that an attribute exists, set it if it does, or
+ * create it if it doesn't using !setAttr
+ **/
+
+ LibFunctions.setAttr = function( character, attrDef, attrValue, tableDef, r, c, caseSensitive ) {
+
+ var name, attrObj, match;
+
+ if (_.isUndefined(attrDef)) {log('setAttr attrDef undefined:'+attrDef);return undefined;}
+ try {
+ name = attrDef[0];
+ } catch {
+ return undefined;
+ }
+
+ if (tableDef && (tableDef[1] || r >= 0)) {
+ c = (c && (tableDef[1] || c != 1)) ? c : '';
+ name = tableDef[0] + c + '_$' + r + '_' + attrDef[0] + c;
+ } else {
+ name = attrDef[0];
+ }
+ match=name.match(/^(repeating_.*)_\$(\d+)_.*$/);
+ if(match){
+ let tableObj = new CharTableArray( character, tableDef, c );
+ tableObj.addTable(attrDef,null,caseSensitive);
+ if (tableObj) {
+ attrObj = tableObj.tableLookup(attrDef,r,false,true);
+ }
+ } else {
+ attrObj = LibFunctions.attrLookup( character, [name, null], null, null, null, caseSensitive );
+ if (!attrObj) {
+ attrObj = createObj( 'attribute', {characterid:character.id, name:attrDef[0], current:'', max:''} );
+ }
+ };
+ if (attrObj) {
+ if (_.isUndefined(attrValue)) attrValue = _.isUndefined(attrDef[2]) ? '' : attrDef[2];
+ if (attrDef[3]) {
+ attrObj.setWithWorker(attrDef[1],String(attrValue));
+ } else {
+ attrObj.set(attrDef[1],String(attrValue));
+ }
+ }
+ return attrObj;
+ }
+
+ /** --------------------------- Ability Management Functions ------------------------------ **/
+
+
+ /**
+ * Find an ability macro with the specified name in any
+ * macro database with the specified root name, returning
+ * the database name, and the matching "ct-" object.
+ * If can't find a matching ability macro or "ct-" object
+ * then return undefined objects
+ * RED v2.044: Updated to use a database index of object IDs
+ * to speed up lookups.
+ **/
+
+ LibFunctions.abilityLookup = function( rootDB, ability, charCS, silent=false, def=true, isGM=false, trueAbility='' ) {
+
+ var charID, obj, ct, db, spells, items, objIndex, abilityName, action,
+ trueAbilityName = String(trueAbility || '').dbName(),
+ source = 'charDB',
+ notFound = false,
+ abilityObj = [],
+ ctObj = [],
+ rDB = rootDB.toLowerCase().replace(/-/g,'_');
+
+ var getTypes = function( body ) {
+ let objType = [],
+ specs = body.match(/}}\s*?specs\s*?=(.*?){{/im);
+ specs = specs ? [...('['+specs[0]+']').matchAll(reSpecClass)] : [];
+ for (let i=0; i < specs.length; i++) {
+ objType.push(specs[i][1]);
+ }
+ return _.uniq(objType.join('|').toLowerCase().split('|')).join('|');
+ };
+
+ if (_.isUndefined(DBindex[rDB])) {
+ for (db of _.keys(DBindex)) {
+ if (rDB.startsWith(db)) {
+ rDB = db;
+ break;
+ }
+ }
+ }
+ if (!ability || ability.length==0 || ability === '-') {
+ return (!def ? new AbilityObj( rootDB, undefined, undefined, undefined) : new AbilityObj( rDB, [undefined,blankItem], [undefined,0], 'apiDB'));
+ }
+
+ do {
+ abilityName = String(ability || '').dbName();
+ if (!_.isUndefined(DBindex[rDB]) && !_.isUndefined(DBindex[rDB][abilityName])) {
+ objIndex = DBindex[rDB][abilityName];
+ if (objIndex[0].length) {
+ obj = getObj('ability',objIndex[0]);
+ }
+ }
+ if (charCS && (!objIndex || (objIndex[0].length && !obj))) {
+ obj = findObjs({ type:'ability', characterid:charCS.id, name:(ability.replace(/\s/g,'-')) });
+ if (obj && obj.length) {
+ source = 'sheet';
+ obj = obj[0];
+ objIndex = [];
+ objIndex.push(obj.id);
+ ct = findObjs({ type:'attribute', characterid:charCS.id, name:'ct-'+ability });
+ }
+ }
+ notFound = notFound || (!objIndex || (objIndex[0].length && !obj));
+ if (notFound) ability = trueAbility;
+ } while (notFound && abilityName !== trueAbilityName && ability && ability.length);
+
+ if (!objIndex || (objIndex[0].length && !obj)) {
+// if (!silent) log('Not found ability '+abilityName+' in any '+rootDB+' database');
+ return new AbilityObj( rootDB, undefined, undefined, undefined);
+ } else if (!objIndex[0].length || !obj) {
+ source = 'apiDB';
+ db = rootDB;
+ obj = dbNames[objIndex[2]].db[objIndex[3]];
+ if (!obj) return new AbilityObj( rootDB, undefined, undefined, undefined);
+ obj.body = LibFunctions.parseStr(obj.body,dbReplacers);
+ abilityObj = [undefined,obj];
+ ctObj = [undefined,obj.ct];
+ } else {
+ charID = obj.get('characterid');
+ db = getObj('character',charID).get('name');
+ spells = db.startsWith(fields.MU_SpellsDB) || db.startsWith(fields.PR_SpellsDB) || db.startsWith(fields.Powers_DB);
+ items = db.startsWith(fields.MagicItemDB);
+ abilityObj[0] = obj;
+ action = obj.get('action');
+ ct = !ct ? getObj('attribute',objIndex[1]) : ct[0];
+ abilityObj[1] = {name:obj.get('name'),
+ type:getTypes(action),
+ ct:(!ct ? 0 : ct.get('current')),
+ charge:(!ct || spells ? 'uncharged' : ct.get('max')),
+ cost:(!ct || items ? '0' : ct.get('max')),
+ body:action};
+ ctObj = [ct,abilityObj[1].ct];
+ };
+// if (!notFound && !isGM) abilityObj[1].body = abilityObj[1].body.replace(/{{\s*?Looks Like\s*=.*?}}/img,'');
+ return new AbilityObj( db, abilityObj, ctObj, source );
+ }
+
+ /*
+ * Create or update an ability on a character sheet
+ */
+
+ LibFunctions.setAbility = function( charCS, abilityName, abilityMacro, actionBar=false ) {
+
+ if (!charCS) {log('setAbility error: invalid character sheet');return;}
+ abilityName = !abilityName ? '-' : abilityName.hyphened();
+ var abilityObj = findObjs({type: 'ability',
+ characterid: charCS.id,
+ name: abilityName},
+ {caseInsensitive:true});
+ if (!abilityObj || abilityObj.length == 0 || !abilityObj[0] || abilityObj[0].get('name') !== abilityName) {
+ abilityObj = createObj( 'ability', {characterid: charCS.id,
+ name: abilityName,
+ action: abilityMacro,
+ istokenaction: actionBar});
+ } else {
+ abilityObj = abilityObj[0];
+ abilityObj.set( 'action', abilityMacro );
+ abilityObj.set( 'istokenaction', actionBar );
+ }
+ return abilityObj;
+ }
+
+ /*
+ * Handle displaying an Ability Macro
+ */
+
+ LibFunctions.doDisplayAbility = function( args, selected, senderId, as, img ) {
+ if (!args) return;
+ if (!args[0] && selected && selected.length) {
+ args[0] = selected[0]._id;
+ }
+ if (args.length < 3) {
+ LibFunctions.sendError('Incorrect RPGMaster command syntax',msg_orig[senderId]);
+ return;
+ }
+ if ((charCS = LibFunctions.getCharacter(args[0]))) args.unshift('standard');
+ var cmd = (args[0] || 'standard').toLowerCase(),
+ tokenID = args[1],
+ db = args[2],
+ ability = args[3],
+ diceRoll1 = args[4] || '',
+ diceRoll2 = args[5] || '',
+ targetID = args[6] || '',
+ charCS = LibFunctions.getCharacter(tokenID),
+ targetToken = getObj('graphic',targetID),
+ targetCS = (targetToken ? getObj('character',targetToken.get('represents')) : undefined),
+ isView = cmd.includes('view'),
+ abObj, abilityMacro;
+
+ var diceRoll = function( rollTxt ) {
+ if (!rollTxt) return randomInteger(20);
+ var retVal = rollTxt.match(/\d+d\d+/i);
+ retVal = (!retVal) ? parseInt((rollTxt.match(/\((\d+)\)/) || rollTxt.match(/(\d+)/) || [0,randomInteger(20)])[1]) : '[['+retVal+']]';
+ return retVal;
+ };
+
+ if (!charCS) {
+ LibFunctions.sendError('The token identified does not represent a character sheet',msg_orig[senderId]);
+ return;
+ }
+
+ if (db.toLowerCase().includes('-db')) {
+ abObj = LibFunctions.getAbility( db, ability, charCS );
+ if (!abObj.obj || !abObj.obj[1]) {
+ LibFunctions.sendError(('The provided ability does not exist in any '+db+' database'),msg_orig[senderId]);
+ return;
+ }
+ abilityMacro = abObj.obj[1].body;
+ } else {
+ var abilityCS = findObjs({type:'character',_id:db});
+ if (!abilityCS || !abilityCS.length) abilityCS = findObjs({type:'character',name:db}, {caseInsensitive: true});
+ if (abilityCS && abilityCS[0]) {
+ abObj = findObjs({type:'ability',characterid:abilityCS[0].id,name:ability}, {caseInsensitive: true});
+ }
+ if (!abObj || !abObj.length) {
+ LibFunctions.sendError(('Not found ability '+ability+' for character '+db),msg_orig[senderId]);
+ return;
+ }
+ abilityMacro = abObj[0].get('action');
+ }
+ diceRoll1 = diceRoll(diceRoll1);
+ diceRoll2 = diceRoll(diceRoll2);
+
+ abilityMacro = abilityMacro.replace(/\}\}\}/g,'} }}')
+ .replace(/%%diceRoll1%%/img,diceRoll1)
+ .replace(/%%diceRoll2%%/img,diceRoll2)
+ .replace(/@{selected\|token_id}/img,tokenID)
+ .replace(/@{selected/img,'@{'+charCS.get('name'));
+ if (targetToken && targetCS) {
+ let targetHP = LibFunctions.getTokenValue( targetToken, fields.token_HP, fields.HP, null, fields.Thac0_base, false );
+ let targetMaxHP = LibFunctions.getTokenValue( targetToken, fields.token_MaxHP, fields.MaxHP, null, fields.Thac0_base, false );
+ let targetAC = LibFunctions.getTokenValue( targetToken, fields.token_AC, fields.AC, fields.MonsterAC, fields.Thac0_base, false );
+ let tokenName = targetToken.get('name');
+ let targetName = targetCS.get('name');
+ let heart = abilityMacro.match(/\{\{\s*Token[_\s]Heart\s*=.*?\}\}/im);
+ if (heart) {
+ abilityMacro = abilityMacro.replace(heart[0],'{{Token_Heart='+Math.ceil(8*Math.max(0,targetHP.val)/targetMaxHP.val)+'}}');
+ }
+ abilityMacro = abilityMacro.replace(/@\{\s*target\s*\|?[^\{\}]*?\|\s*token_id\s*\}/img,targetID)
+ .replace(/(?:\[\[)?\s*(?:0\s*\+)?\s*@\{target\|?[^\{\}]*?\|hp\|max\}\s*(?:\&\{noerror\})?\s*(?:\]\])?/img,targetMaxHP.val+' ')
+ .replace(/(?:\[\[)?\s*(?:0\s*\+)?\s*@\{target\|?[^\{\}]*?\|hp\}\s*(?:\&\{noerror\})?\s*(?:\]\])?/img,targetHP.val+' ')
+ .replace(new RegExp('(?:\\[\\[)?\\s*(?:0\\s*\\+)?\\s*@\\{target\\|?[^\\{\\}]*?\\|'+targetMaxHP.name+'\\|max\\}\\s*(?:\\&\\{noerror\\})?\\s*(?:\\]\\])?','img'),targetMaxHP.val+' ')
+ .replace(new RegExp('(?:\\[\\[)?\\s*(?:0\\s*\\+)?\\s*@\\{target\\|?[^\\{\\}]*?\\|'+targetHP.name+'\\}\s*(?:\\&\\{noerror\\})?\\s*(?:\\]\\])?','img'),targetHP.val+' ')
+ .replace(/(?:\[\[)?\s*(?:0\s*\+)?\s*@\{target\|?[^\{\}]*?\|ac\}\s*(?:\&\{noerror\})?\s*(?:\]\])?/img,targetAC+' ')
+ .replace(/@\{target\|?[^\{\}]*?\|token_name\}(?:\s*\&\{noerror\})?/img,tokenName+' ');
+ let targetFields = [...abilityMacro.matchAll(/(?:\[\[)?(?:\s*0\s*\+)?\s*(@\{target\|.*?\})\s*(?:\&\{noerror\})?\s*(?:\]\])?/img)];
+ _.each(targetFields, f => abilityMacro = abilityMacro.replace(f[0],f[1]));
+ abilityMacro = abilityMacro.replace(/@\{target\|?[^\{\}]*?\|/img,'@{'+targetName+'|');
+ }
+ cmd = cmd.replace(/\-?view/,'');
+ if (isView && !state.MagicMaster.viewActions) abilityMacro = abilityMacro.replace(/(? LibFunctions.parseStr((LibFunctions.attrLookup( charCS, [fields.ItemVar[0]+name+'+'+row+'-'+w,'current'] ) || '').split('/')[v] || '');
+
+ if (abObj.obj && abObj.obj[1]) {
+ do {
+ extra = abObj.obj[1].body.match(/%{([^\|]+?)\|([^}]+?)}/);
+ if (extra) {
+ if (!extraList.includes(extra[2].dbName())) {
+ extraList.push(extra[2].dbName());
+ extraDef = LibFunctions.abilityLookup( extra[1], extra[2], charCS, silent );
+ } else {
+ extraDef.obj = undefined;
+ }
+ if (extraDef.obj) {
+ abObj.obj[1].body = abObj.obj[1].body.replace(/%{([^\|]+?)\|([^}]+?)}/,extraDef.obj[1].body.replace('$$','$$$$'));
+ } else {
+ abObj.obj[1].body = abObj.obj[1].body.replace(/%{([^\|]+?)\|([^}]+?)}/,'');
+ }
+ }
+ } while (extra && extraDef.obj);
+
+ trueName = (trueName || '').trim();
+ if (!isGM && trueName && trueName.length && (name.dbName() === trueName.dbName())) {
+ abObj.obj[1].body = abObj.obj[1].body.replace(/{{\s*?Looks\s?Like\s*=/img,'{{Appearance=');
+ }
+ if (charCS) {
+ if (trueName && trueName.length && name.dbName() !== trueName.dbName()) {
+ let cmd = '{{GM Info=[Reveal Now](!magic --button GM-ResetSingleMI|'+charCS.id+'|'+(name)
+ + ' --message gm|'+charCS.id+'|Revealing '+(trueName.dispName())+'|The item '+(trueName.dispName())+' which was hidden as '+(name.dispName())+' has been revealed)';
+ if (/{{\s*GM\s?Info\s*=/im.test(abObj.obj[1].body)) {
+ abObj.obj[1].body = abObj.obj[1].body.replace(/{{\s*GM\s?Info\s*=([^\[])/im,(cmd + ' $1'));
+ } else {
+ abObj.obj[1].body += cmd + '}}';
+ }
+ }
+ while (reVars.test(abObj.obj[1].body)) abObj.obj[1].body = abObj.obj[1].body.replace(reVars,varRes);
+ abObj.obj[0] = LibFunctions.setAbility( charCS, name, abObj.obj[1].body );
+ LibFunctions.setAttr( charCS, [fields.CastingTimePrefix[0]+name,'current'], abObj.obj[1].ct );
+ LibFunctions.setAttr( charCS, [fields.CastingTimePrefix[0]+name,'max'], abObj.obj[1].charge );
+ abObj.dB = charCS.get('name');
+ }
+ }
+ return abObj;
+ }
+
+ /** -------------------------------------------- send messages to chat ----------------------------------------- **/
+
+ LibFunctions.parseTemplate = function( txt ) {
+// return LibFunctions.parseOutput( '', '', '', txt, null, null, null, false );
+ };
+
+ LibFunctions.redisplayOutput = function(senderId) {
+ if (senderId && senderId.length && !_.isUndefined(lastMsg[senderId])) {
+ let args = [...lastMsg[senderId]];
+ if (args.length > 3) {
+ return LibFunctions.parseOutput( args[0], args[1], args[2], args[3], senderId );
+ }
+ }
+ }
+
+ /*
+ * Parse the standard Roll Template structure for RPGMaster
+ * templates and return the converted text for display in the
+ * chat window.
+ */
+
+ LibFunctions.parseOutput = function( as, preamble, template, txt, senderId ) {
+
+ var isGM = false;
+ var originalTxt = txt;
+ if (senderId && senderId.length) {
+ for (const playerId of senderId.split(',')) {
+ lastMsg[playerId] = arguments;
+ isGM = isGM || playerIsGM(playerId);
+ }
+ }
+
+ clearWaitTimer(senderId);
+
+ txt = txt.replace(/}}\s*?k/img,'} }k')
+ .replace(/{{=/img,'{{ =')
+ .replace(/</img,'<')
+ .replace(/>/img,'>')
+ .replace(/{{\s*}}/img,'');
+
+ var colours, colourSet;
+
+ switch (template.toLowerCase()) {
+ case 'rpgmattack':
+ colourSet = 'attack';
+ break;
+ case 'rpgmweapon':
+ case 'rpgmammo':
+ colourSet = 'weapon';
+ break;
+ case 'rpgmpotion':
+ colourSet = 'potion';
+ break;
+ case 'rpgmspell':
+ case 'rpgmitemspell':
+ case 'rpgmwandspell':
+ case 'rpgmscroll':
+ colourSet = 'spell';
+ break;
+ case 'rpgmmenu':
+ colourSet = 'menu';
+ break;
+ case 'rpgmmessage':
+ colourSet = 'message';
+ break;
+ case 'rpgmwarning':
+ colourSet = 'warning';
+ break;
+ case 'rpgmarmour':
+ case 'rpgmitem':
+ case 'rpgmring':
+ case 'rpgmwand':
+ case 'rpgmclass':
+ case 'rpgmdefault':
+ default:
+ colourSet = 'def';
+ break;
+ }
+ if (_.isUndefined(state.MagicMaster) || _.isUndefined(state.attackMaster)) {
+ colours = Object.create(pallet.plain[colourSet]);
+ } else if (!senderId || _.isUndefined(state.MagicMaster.playerConfig) || _.isUndefined(state.MagicMaster.playerConfig[senderId])) {
+ colours = Object.create((state.attackMaster.fancy || state.MagicMaster.fancy) ? pallet.fancy[colourSet] : pallet.plain[colourSet]);
+ } else {
+ let config = state.MagicMaster.playerConfig[senderId];
+ colours = Object.create(config.menuPlain ? pallet.plain[colourSet] : (config.menuDark ? pallet.dark[colourSet] : pallet.fancy[colourSet]));
+ }
+ if (template) {
+ const txtObj = _.object([...txt.replace(/[\r\n]/g,'').matchAll(/\{\{(.+?)=(.*?)\}\}/g)].map(v => v.slice(1)).map(v => [v[0].dbName(),v[1]]));
+ _.each( txtObj, (t,k) => {
+ if (!_.isUndefined(colours[k])) {
+ colours[k] = t;
+ txt = txt.replace(new RegExp(`{{\\s*${k}\\s*=.*?}}`,'img'),'');
+ }
+ });
+ };
+ const outerFrame = '';
+ const endOuterFrame = '
';
+ const headerFrame = '';
+ const endHeaderFrame = ' |
';
+ const header1 = '';
+ const endHeader1 = '';
+ const header2 = '';
+ const endHeader2 = '';
+ const subtitle1 = '
';
+ const endSubtitle1 = '';
+ const subtitle2 = '
';
+ const endSubtitle2 = '';
+ const settings = ' ';
+
+ const bodyFrame = '';
+ const fullBodyFrame = '';
+ const lastBodyFrame = '';
+ const endBodyFrame = ' ';
+ const row1col = ['',
+ ' '];
+ const rowResult = ['',
+ ' '];
+ const endRowResult = ' '
+ const endRow1col = ' ';
+ const rowHeader = ' ';
+ const endRowHeader = ' | ';
+ const rowBodyC = ' ';
+ const endRowBodyC = ' | ';
+ const rowBody = ' ';
+ const endRowBody = ' | ';
+ const row1 = ' ';
+ const endRow1 = ' | ';
+ const row1C = ' ';
+ const endRow1C = ' | ';
+ const row2col = [' ',
+ ' '];
+ const endRow2col = ' ';
+ const rowL = ' ';
+ const endRowL = ' | ';
+ const rowR = ' ';
+ const endRowR = ' | ';
+ const rowC = ' ';
+ const endRowC = ' | ';
+ const row2 = ' ';
+ const endRow2 = ' | ';
+ const rowC2 = ' ';
+ const endRowC2 = ' | ';
+ const titleDmgSM = ' '+colours.dmgslabel+' | ';
+ const rowDmgSM = ' ';
+ const endRowDmgSM = ' | ';
+ const titleAC = ' AC Hit | ';
+ const rowAC = ' ';
+ const endRowAC = ' | ';
+ const rowType = ' ';
+ const endRowType = ' | ';
+ const titleDmgL = ' '+colours.dmgllabel+' | ';
+ const rowDmgL = ' ';
+ const endRowDmgL = ' | ';
+ const sImg = '  '
+ const pImg = '  '
+ const bImg = '  '
+ const rowTargetAC = ' Target | ';
+ const endRowTargetAC = ' | AC | ';
+ const rowTargetSAC = ' ';
+ const endRowTargetSAC = ' | ';
+ const rowTargetPAC = ' ';
+ const endRowTargetPAC = ' | ';
+ const rowTargetBAC = ' ';
+ const endRowTargetBAC = ' | ';
+ const titleTargetHP = ' Target HP | '
+ const rowTargetHP = ' ';
+ const endTableStyle = ' | ';
+
+ const addDescs = function( txtObj, j, showMore='', rowCols=row1col, rowFrame=row1 ) {
+ let content = '';
+ if (!_.isUndefined(txtObj.desc)) content += (rowCols[(j++)%2]+rowFrame+ txtObj.desc +showMore +endRow1+endRow1col);
+ for (let i=1; i<=9; ++i) {
+ if (!_.isUndefined(txtObj['desc'+i])) content += (rowCols[(j++)%2]+rowFrame+ txtObj['desc'+i] +endRow1+endRow1col);
+ };
+ return content;
+ };
+
+ const maxDiceRoll = function( diceRoll ) {
+ var rollData = diceRoll.match(/(\d+)d(\d+)/i)||fields.ToHitRoll.match(/(\d+)d(\d+)/i)||['1d20',1,20];
+ return {min:(parseInt(rollData[1])||1), max:((parseInt(rollData[1]) * parseInt(rollData[2]))||20)};
+ };
+
+ const RPGMattack = function( txt ) {
+
+ const arReplace = function( txt, ac ) {
+ let arAdj = txt.match(/([-+]?\d+)\[([\s\w\d]+?)=([-+\d\|]+?)\]/i);
+ if (!arAdj || !arAdj.length) return txt;
+ txt = arAdj[3].split('|')[arAdj[1]];
+ txt = '+-'.includes(txt[0]) ? txt : '+'+txt;
+ return '[['+(ac && ac.length ? ac : arAdj[1])+txt+' ['+txt+' ['+arAdj[2]+'] ] ]]';
+ }
+
+ const varReplace = function( str, field ) {
+ var value = '';
+ if (field) {
+ field = field.replace(/[-\s]/g,'_').toLowerCase();
+ value = (/^[-+]?[\d.]+/.test(field)|| _.isUndefined(txtObj[field])) ? parseFloat(field) : parseFloat(txtObj[field].match(/[-+]?[\d.]+/));
+ }
+ return value;
+ }
+
+ const attkDefaults = {title:'', name:'', subtitle:'', ac_hit:'', target_ac:'', attk_type:'', target_sac:'', target_pac:'', target_bac:'', dmg_s:'', dmg_l:'', target_hp:'', target_maxhp:''};
+ const txtObj = _.object([...txt.replace(/[\r\n]/g,'').replace(/\}\}\}/g,'} }}').matchAll(/\{\{(.+?)=(.*?)\}\}/g)].map(v => v.slice(1)).map(v => [v[0].replace(/[-\s]/g,'_').toLowerCase(),v[1]]));
+ const dice_roll = parseInt((txtObj.ac_hit.match(/(\d+)\[Dice roll\]/i) || ['',''])[1]);
+ const toHitRoll = (txt.match(/specs=\[.*?(\d+d\d+),.*\]/im)||['',fields.ToHitRoll])[1];
+ const minMaxRoll = maxDiceRoll(toHitRoll);
+ const isMax = state.attackMaster.naturalRolls && !isNaN(dice_roll) && dice_roll >= minMaxRoll.max;
+ const isMin = state.attackMaster.naturalRolls && !isNaN(dice_roll) && dice_roll <= minMaxRoll.min;
+ const hasDescs = /{{\s*desc\d?\s*=/im.test(txt);
+ var crit = false;
+ var fumble = false;
+ _.defaults(txtObj,attkDefaults);
+ txtObj.target_sac = arReplace( txtObj.target_sac, txtObj.target_ac );
+ txtObj.target_pac = arReplace( txtObj.target_pac, txtObj.target_ac );
+ txtObj.target_bac = arReplace( txtObj.target_bac, txtObj.target_ac );
+ txtObj.attk_type = txtObj.attk_type.toLowerCase();
+ var content = outerFrame;
+
+ if (txtObj.title.length || txtObj.name.length) {
+ content += headerFrame
+ +header1+ txtObj.title+' '+txtObj.name +endHeader1
+ +(txtObj.subtitle ? (subtitle1+ txtObj.subtitle +endSubtitle1) : '')
+ +settings
+ +endHeaderFrame;
+ }
+ if (txtObj.ac_hit != '') {
+ content += ((txtObj.crit_roll || txtObj.fumble_roll || txtObj.ar_adjust || txtObj.target_ac != '' || txtObj.result || hasDescs) ? bodyFrame : lastBodyFrame)
+ +row1col[0]
+ +titleDmgSM
+ +rowAC+ txtObj.ac_hit +endRowAC
+ +titleDmgL
+ +endRow1col
+ +row1col[1]
+ +rowDmgSM+ txtObj.dmg_s +endRowDmgSM
+ +rowDmgL+ txtObj.dmg_l +endRowDmgL
+ +endRow1col
+ +row1col[0]
+ +titleAC
+ +endRow1col
+ +row1col[1]
+ +rowType+ [sImg,pImg,bImg].filter((e,i) => txtObj.attk_type.includes(['s','p','b'][i])).join('') +endRowType
+ +endRow1col
+ +endBodyFrame;
+ }
+ if (txtObj.ar_adjust) {
+ content += ((txtObj.target_ac != '' || txtObj.result || hasDescs) ? bodyFrame : lastBodyFrame)
+ +row1col[0]
+ + rowC + txtObj.ar_adjust + endRowC
+ +endRow1col
+ +endBodyFrame;
+ }
+ if ((txtObj.crit_roll || txtObj.fumble_roll) && !isNaN(dice_roll)) {
+ const crit_roll = parseInt(txtObj.crit_roll);
+ const fumble_roll = parseInt(txtObj.fumble_roll);
+ crit = (crit_roll && (crit_roll <= dice_roll));
+ fumble = (fumble_roll && (fumble_roll >= dice_roll));
+ if (crit || fumble) {
+ content += ((txtObj.target_ac != '' || txtObj.result || hasDescs) ? bodyFrame : lastBodyFrame)
+ +(!crit ? '' : (rowResult[0]+rowC+ (txtObj.crit || 'Critical Hit!') +endRowC+endRowResult))
+ +(!fumble ? '' : (rowResult[1]+rowC+ (txtObj.fumble || 'Fumbled!') +endRowC+endRowResult))
+ +endBodyFrame;
+ }
+ }
+ if (txtObj.target_ac != '') {
+ const target_hp = parseInt(txtObj.target_hp.match(/[-+]?\d+/));
+ const target_maxhp = parseInt(txtObj.target_maxhp.match(/[-+]?\d+/));
+ const heart_url = !(isNaN(target_hp) || isNaN(target_maxhp)) ? heart[Math.min(Math.ceil(8*Math.max(target_hp,0)/target_maxhp),8)] : '';
+ content += ((txtObj.result) ? bodyFrame : lastBodyFrame)
+ +row1col[0]
+ +rowTargetAC+ txtObj.target_ac +endRowTargetAC
+ +titleTargetHP
+ +endRow1col
+ +row1col[1]
+ +rowTargetSAC+ txtObj.target_sac +endRowTargetSAC
+ +rowTargetPAC+ txtObj.target_pac +endRowTargetPAC
+ +rowTargetBAC+ txtObj.target_bac +endRowTargetBAC
+ +rowTargetHP + 'background-image: url('+heart_url+');">' +endRowTargetHP
+ +endRow1col
+ +endBodyFrame;
+ }
+ if (txtObj.result || ((isMax || isMin) && !(crit || fumble))) {
+ let result = isMax || crit;
+ if (txtObj.result) {
+ const test = txtObj.result.match(/([\w\s_.+-]+?|[-+]?[\d.]+?)((?:<=|>=|<|>|=|<>|!=))(.+)/);
+ if (test) {
+ const field1 = test[1].replace(/[-\s]/g,'_').toLowerCase();
+ const field2 = test[3].replace(/[-\s]/g,'_').toLowerCase();
+ const value1 = (/^[-+]?[\d.]+/.test(test[1])|| _.isUndefined(txtObj[field1])) ? parseFloat(test[1]) : parseFloat(txtObj[field1].match(/[-+]?[\d.]+/));
+ const value2 = (/^[-+]?[\d.]+/.test(test[3])|| _.isUndefined(txtObj[field2])) ? parseFloat(test[3]) : parseFloat(txtObj[field2].match(/[-+]?[\d.]+/));
+ switch (test[2]) {
+ case '=': result = value1 == value2; break;
+ case '<': result = value1 < value2; break;
+ case '>': result = value1 > value2; break;
+ case '<=': result = value1 <= value2; break;
+ case '>=': result = value1 >= value2; break;
+ case '<>': result = value1 != value2; break;
+ case '!=': result = value1 != value2; break;
+ default: result = false;
+ }
+ if (state.attackMaster.weapRules.naturals) result = (result || isMax) && !isMin;
+ if (state.attackMaster.weapRules.criticals) result = (result || crit) && !fumble;
+ if (txtObj.successcmd && txtObj.successcmd.length && result) {
+ while (/%%[_\d\w\+-]+?%%/.test(txtObj.successcmd)) txtObj.successcmd = txtObj.successcmd.replace( /%%([_\d\w\+-]+?)%%/, varReplace );
+ _.each(txtObj.successcmd.split(' '),cmd => LibFunctions.sendAPI(LibFunctions.parseStr(cmd)));
+ } else if (txtObj.failcmd && txtObj.failcmd.length && !result) {
+ while (/%%[_\d\w\+-]+?%%/.test(txtObj.failcmd)) txtObj.failcmd = txtObj.failcmd.replace( /%%([_\d\w\+-]+?)%%/, varReplace );
+ _.each(txtObj.failcmd.split(' '),cmd => LibFunctions.sendAPI(LibFunctions.parseStr(cmd)));
+ };
+ }
+ };
+ content += (hasDescs ? bodyFrame : lastBodyFrame)
+ +rowResult[result ? 0 : 1]+rowC+''+ ((isMax || crit) ? 'Natural '+dice_roll : ((isMin || fumble) ? 'Natural '+dice_roll : (result ? 'Success' : 'Failure'))) +''+endRowC+endRowResult
+ +endBodyFrame;
+ }
+ if (hasDescs) {
+ content += lastBodyFrame
+ +addDescs(txtObj,1)
+ +endBodyFrame;
+ }
+ content += endOuterFrame;
+ return content;
+ };
+
+ const RPGMspell = function( txt, preamble ) {
+ const spellDefaults = {prefix:'', title:'', name:'', splevel:'', school:'', range:'', components:'', duration:'', time:'', aoe:'', save:'', effects:''};
+ let k=1;
+ const txtObj = _.object([...txt.replace(/[\r\n]/g,'').matchAll(/\{\{(.+?)=(.*?)\}\}/g)].map(v => v.slice(1)).map(v => [v[0].replace(/[-\s]/g,'_').toLowerCase(),v[1]]));
+ const isLooksLike = !isGM && !!txtObj.looks_like;
+ _.defaults(txtObj,spellDefaults);
+ const showMore = /{{hide\d=/img.test(originalTxt);
+ const showLess = /{{desc\d=/img.test(originalTxt);
+ const txtRowID = generateRowID();
+ if (showMore || showLess) showMoreObj[txtRowID] = (preamble+'&{template:'+template+'}'+originalTxt.replace(/{{hide(\d)=/img,'{{reveal$1=').replace(/{{desc(\d)=/img,'{{hide$1=').replace(/{{reveal(\d)=/img,'{{desc$1=') );
+ const showMoreButton = (showMore || showLess) ? (' *show '+(showMore ? 'more' : 'less')+'...*') : '';
+ const hasDescs = !isLooksLike && /{{\s*desc\d?\s*=/im.test(txt);
+ var content = outerFrame
+ +headerFrame
+ +header2+ (!isLooksLike ? txtObj.prefix : '')+' '+txtObj.title+' '+(!isLooksLike ? txtObj.name : '')+endHeader2
+ +(!isLooksLike ? (subtitle2+ txtObj.splevel +' * '+ txtObj.school +endSubtitle2) : '')
+ +settings
+ +endHeaderFrame
+ +lastBodyFrame
+ +(!isLooksLike ? (
+ row2col[++k%2]+rowL+'Range '+ txtObj.range +endRowL
+ +rowR+'Components '+ txtObj.components +endRowR+endRow2col
+ +row2col[++k%2]+rowL+'Duration '+ txtObj.duration +endRowL
+ +rowR+'Casting Time '+ txtObj.time +endRowR+endRow2col
+ +row2col[++k%2]+rowL+'Area of Effect '+ txtObj.aoe +endRowL
+ +rowR+'Saving Throw '+ txtObj.save +endRowR+endRow2col
+ +(txtObj.healing ? (row2col[++k%2]+rowC2+'Healing: '+ txtObj.healing +endRowC2+endRow2col) : '')
+ +(txtObj.damage ? (row2col[++k%2]+rowC2+'Damage: '+ txtObj.damage +endRowC2+endRow2col) : '')
+ +(txtObj.reference ? (row2col[++k%2]+rowC2+'Reference: '+ txtObj.reference +endRowC2+endRow2col) : '')
+ +(txtObj.materials ? (row2col[++k%2]+rowC2+'Materials: '+ txtObj.materials +endRowC2+endRow2col) : '')
+ +(txtObj.use ? (row2col[++k%2]+row2+'Use: '+ txtObj.use +endRow2+endRow2col) : '')
+ +(txtObj.learn ? (row2col[++k%2]+row2+'Learn spell: '+ txtObj.learn +endRow2+endRow2col) : '')
+ +(isGM && txtObj.gm_info ? (row2col[++k%2]+row2+'GM Info: '+ txtObj.gm_info +endRow2+endRow2col) : '')
+ ) : '')
+ +(txtObj.looks_like || txtObj.appearance ? (row2col[++k%2]+row2+(!isLooksLike ? 'Looks Like: ' : '')+ (txtObj.looks_like ? txtObj.looks_like : txtObj.appearance) +endRow2+endRow2col) : '')
+ +(!isLooksLike ? (
+ row2col[++k%2]+row2+'Effects: '+ txtObj.effects +showMoreButton +endRow2+endRow2col
+ +addDescs(txtObj,++k,'',row2col,row2)
+ ) : '');
+ +endBodyFrame
+ +endOuterFrame;
+ return content;
+ }
+
+ const RPGMmessage = function( txt ) {
+
+ const txtObj = _.object([...txt.replace(/[\r\n]/g,'').matchAll(/\{\{(.+?)=(.*?)\}\}/g)].map(v => v.slice(1)).map(v => [v[0].replace(/[-\s]/g,'_').toLowerCase(),v[1]]));
+ let content = outerFrame;
+ if (txtObj.name || txtObj.title) {
+ content += headerFrame
+ +header1+ (txtObj.title || '')+(txtObj.name || '') +endHeader1
+ +settings
+ +endHeaderFrame
+ +lastBodyFrame;
+ } else {
+ content += fullBodyFrame;
+ }
+ content += addDescs(txtObj,1)
+ +endBodyFrame
+ +endOuterFrame;
+ return content;
+ }
+
+ const RPGMdefault = function( txt, preamble, isShowMore=true ) {
+
+ var value1, value2;
+
+ const resultTest = function( t ) {
+ let result = false;
+ const test = t.match(/([\w\s_.+-]+?|[-+]?[\d.]+?)((?:<=|>=|<|>|=|<>|!=))(.+)/);
+ if (test) {
+ value1 = (/^[-+]?[\d.]+/.test(test[1])|| _.isUndefined(txtObj[test[1]])) ? parseFloat(test[1]) : parseFloat(txtObj[test[1]].match(/[-+]?[\d.]+/));
+ value2 = (/^[-+]?[\d.]+/.test(test[3])|| _.isUndefined(txtObj[test[3]])) ? parseFloat(test[3]) : parseFloat(txtObj[test[3]].match(/[-+]?[\d.]+/));
+ switch (test[2]) {
+ case '=': result = value1 == value2; break;
+ case '<': result = value1 < value2; break;
+ case '>': result = value1 > value2; break;
+ case '<=': result = value1 <= value2; break;
+ case '>=': result = value1 >= value2; break;
+ case '<>': result = value1 != value2; break;
+ case '!=': result = value1 != value2; break;
+ default: result = false;
+ }
+ }
+ return result;
+ };
+
+ const rollVal = (m,v) => LibFunctions.evalAttr(v);
+
+ const defDefaults = {prefix:'', title:'', name:'', success:'', failure:''};
+ const txtObj = _.object([...txt.replace(/[\r\n]/g,'').matchAll(/\{\{(.+?)=(.*?)\}\}/g)].map(v => v.slice(1)));
+ const isLooksLike = !isGM && /{{\s*Looks\s?Like\s*=.*?}}/im.test(txt);
+ _.defaults(txtObj,defDefaults);
+ const showMore = /{{hide\d=/img.test(originalTxt);
+ const showLess = /{{desc\d=/img.test(originalTxt);
+ const txtRowID = generateRowID();
+ if (isShowMore && (showMore || showLess)) showMoreObj[txtRowID] = (preamble+' &{template:'+template+'}'+originalTxt.replace(/{{hide(\d)=/img,'{{reveal$1=').replace(/{{desc(\d)=/img,'{{hide$1=').replace(/{{reveal(\d)=/img,'{{desc$1=') );
+ const showMoreButton = (isShowMore && (showMore || showLess)) ? (' *show '+(showMore ? 'more' : 'less')+'...*') : '';
+ let content = outerFrame
+ +headerFrame
+ +header1+ (!isLooksLike ? txtObj.prefix : '')+' '+txtObj.title+' '+(!isLooksLike ? txtObj.name : '')+endHeader1
+ +(txtObj.subtitle && !isLooksLike ? (subtitle1+ txtObj.subtitle +endSubtitle1) : '')
+ +settings
+ +endHeaderFrame;
+ content += lastBodyFrame;
+ let j=1, crit=false, fumble=false, result=false;
+
+ _.each(txtObj,(t,k) => {
+ switch (k.dbName()) {
+ case 'result': result = resultTest(t); break;
+ case 'critroll': crit = state.attackMaster.weapRules.criticals && resultTest(t); break;
+ case 'fumbleroll': fumble = state.attackMaster.weapRules.criticals && resultTest(t); break;
+ default: break;
+ }
+ });
+
+ result = !fumble && (crit || result);
+// log('RPGMdefault: result = '+result+', crit = '+crit+', fumble = '+fumble);
+ _.each(txtObj,(t,k) => {
+ t = t.replace(/\/img,tableStyle);
+ t = t.replace(/\<\/table\>/img,endTableStyle);
+ txtObj[k] = t;
+ if (!t || !t.length) return;
+ let key = k.toLowerCase().replace(/\s/g,'');
+ if (key === 'lookslike') {
+ if (!isGM) {
+ content += (row2col[(j++)%2]+row2+ t +endRow2+endRow2col);
+ } else {
+// content += (row1col[(j++)%2]+rowHeader+ k +endRowHeader+rowBodyC+ t +endRowBodyC+endRow1col);
+ content += (row2col[(j++)%2]+row2+ '**'+k+'**: '+t +endRow2+endRow2col);
+ }
+ } else if (isLooksLike) {
+ return;
+ } else if (key === 'result' || key === 'crit_roll' || key === 'fumble_roll') {
+ switch (key) {
+ case 'result':
+ if (result) {txtObj.success = (txtObj.Success || txtObj.success).replace(/value1/ig,value1).replace(/value2/ig,value2).replace(/\[\[\d*?\[(.+?)\]\s?\]\]/g,rollVal);}
+ else {txtObj.failure = (txtObj.Failure || txtObj.failure).replace(/value1/ig,value1).replace(/value2/ig,value2).replace(/\[\[\d*?\[(.+?)\]\s?\]\]/g,rollVal);}
+ let resultTxt = (result ? !!txtObj.success.length : !!txtObj.failure.length) ? ' ' : '';
+ content += rowResult[result ? 0 : 1]+row1C+''+ (result ? 'Success' : 'Failure') +''
+ + resultTxt+(result ? txtObj.success : txtObj.failure)+(resultTxt.length?'':'')+endRow1C+endRowResult;
+ if (txtObj.successcmd && txtObj.successcmd.length && result && !crit) {
+ LibFunctions.sendAPI(LibFunctions.parseStr(txtObj.successcmd));
+ } else if (txtObj.failcmd && txtObj.failcmd.length && !result && !fumble) {
+ LibFunctions.sendAPI(LibFunctions.parseStr(txtObj.failcmd));
+ };
+ break;
+ case 'crit_roll':
+ content += (!crit ? '' : (rowResult[0]+row1C+ (txtObj.crit || 'Critical Success!') +endRow1C+endRowResult));
+ if (txtObj.critcmd && txtObj.critcmd.length && crit) LibFunctions.sendAPI(LibFunctions.parseStr(txtObj.critcmd));
+ break;
+ case 'fumble_roll':
+ content += (!fumble ? '' : (rowResult[1]+row1C+ (txtObj.fumble || 'Critical Failure!') +endRow1C+endRowResult));
+ if (txtObj.fumblecmd && txtObj.fumblecmd.length && fumble) LibFunctions.sendAPI(LibFunctions.parseStr(txtObj.fumblecmd));
+ break;
+ }
+ } else if (key === 'use') {
+ content += (row2col[(j++)%2]+row2+ '**'+k+'**: '+t +endRow2+endRow2col);
+ } else if (key.startsWith('hide')) {
+ return;
+ } else if (key.startsWith('section')) {
+ content += row1col[(j++)%2]+row1C+ t +endRow1C+endRow1col;
+ } else if (key === 'gminfo') {
+// if (isGM) content += (row1col[(j++)%2]+rowHeader+ k +endRowHeader+rowBodyC+ t +endRowBodyC+endRow1col);
+ if (isGM) content += (row2col[(j++)%2]+row2+ '**'+k+'**: '+t +endRow2+endRow2col);
+ } else if (!['prefix','name','title','subtitle','successcmd','failcmd','success','failure','crit','fumble','critcmd','fumblecmd','gmdesc'].includes(key) && !key.startsWith('desc')) {
+ content += row1col[(j++)%2]+rowHeader+ k +endRowHeader+rowBodyC+ t +endRowBodyC+endRow1col;
+ }
+ });
+ content += (isLooksLike ? '' : addDescs(txtObj,j,showMoreButton))
+ + endBodyFrame + endOuterFrame;
+ return content;
+ }
+
+ let content;
+ switch (template.toLowerCase()) {
+ case 'rpgmattack':
+ content = RPGMattack( txt );
+ break;
+ case 'rpgmspell':
+ case 'rpgmpotion':
+ case 'rpgmitemspell':
+ case 'rpgmwandspell':
+ case 'rpgmscroll':
+ content = RPGMspell( txt, preamble );
+ break;
+ case 'rpgmmessage':
+ content = RPGMmessage( txt );
+ break;
+ case 'rpgmwarning':
+ case 'rpgmmenu':
+ content = RPGMdefault( txt, preamble, false );
+ break;
+ case 'rpgmweapon':
+ case 'rpgmammo':
+ case 'rpgmarmour':
+ case 'rpgmitem':
+ case 'rpgmring':
+ case 'rpgmwand':
+ case 'rpgmclass':
+ case 'rpgmdefault':
+ content = RPGMdefault( txt, preamble, true );
+ break;
+ default:
+ content = (template ? '&{template:'+template+'}' : '' ) + txt;
+ break;
+ }
+ while (/
/.test(content)) {content = content.replace(/
/mg,' ')};
+ content = (content[0] === '!' ? '' : preamble) + content;
+ setTimeout(() => sendChat(as?as:defaultAs,content,null,{noarchive:!archive, use3d:use3Ddice}), 0);
+ return content;
+ }
+
+ /*
+ * Determine who to send a Response to: use who controls
+ * the character - if no one or if none of the controlling
+ * players are on-line send the response to the GM
+ */
+
+ LibFunctions.sendToWho = function(charCS,senderId,makePublic=false,embedded=false) {
+
+ var to, controlledBy, players, viewerID, isPlayer=LibFunctions.checkPlayersLive( charCS );
+ controlledBy = (!charCS ? '' : charCS.get('controlledby'));
+ if (controlledBy.includes('all')) {
+ to = '';
+ } else if (playerIsGM(senderId) || !charCS || !isPlayer) {
+ to = embedded ? '/w gm ' : '/w gm ';
+ } else if (makePublic) {
+ to = '';
+ } else {
+ to = (embedded ? ('/w "'+charCS.get('name')+'" ') : ('/w "' + charCS.get('name') + '" '));
+ }
+ return to;
+ }
+
+ /*
+ * A more reliable form of function to determine who
+ * to send a Response to: use who controls
+ * the character - if no one or if none of the controlling
+ * players are on-line send the response to the GM
+ */
+
+ LibFunctions.sendMsgToWho = function(charCS,senderId,msg,div='',makePublic=false,embedded=false) {
+
+ var to, controlledBy, players, viewerID, isPlayer=false;
+// match = !embedded ? /(?<=^|}}\s*)^(?!\!|\/)/mg : /(? 0) {
+ controlledBy = controlledBy.split(',');
+ viewerID = (state.roundMaster && state.roundMaster.viewer && state.roundMaster.viewer.is_set) ? (state.roundMaster.viewer.pid || null) : null;
+ players = controlledBy.filter(id => id != viewerID);
+ if (players.length) {
+ isPlayer = _.some( controlledBy, function(playerID) {
+ players = findObjs({_type: 'player', _id: playerID, _online: true});
+ return (players && players.length > 0);
+ });
+ };
+ };
+ if (controlledBy.includes('all')) {
+ to = '';
+ } else if (playerIsGM(senderId) || !charCS || controlledBy.length == 0 || !isPlayer) {
+ to = embedded ? '/w gm ' : '/w gm ';
+ } else if (makePublic) {
+ to = '';
+ } else {
+ to = (embedded ? ('/w "'+charCS.get('name')+'" ') : ('/w "' + charCS.get('name') + '" '));
+ }
+ if (!embedded) msg = msg.replace(/^&{template:/img,(to+div+'$&'))
+ .replace(/^(?!\!|\/)/,('$&'+to+div))
+ .replace(/^\!.*^(?!\!|\/)/mg,('$&'+to+div))
+ .replace(/^\/(?:w|em|ooc|talktomyself|fx|desc|as|emas)\s.*?^(?!\!|\/)/img,('$&'+to+div));
+
+ return embedded ? to : msg;
+ }
+
+ /**
+ * Insert a whisper into a body with a template.
+ * If no template, inserts the whisper at the start of
+ * the first line not starting with an API call.
+ **/
+
+ LibFunctions.insertWhisper = function(to, msg='') {
+ let splitMsg = msg.match(/([^]*?)^.*?((?:&|\\amp|\\amp;){template:.*)/msi);
+ if (!splitMsg || !splitMsg.length > 2) return to+' '+msg;
+ return splitMsg[1]+'\n'+to+' '+splitMsg[2];
+ }
+
+ /**
+ * Send public message with 3d dice rolls (if enabled)
+ */
+
+ LibFunctions.sendPublic = function(msg,charCS,senderId) {
+ if (!msg)
+ {return undefined;}
+ var who;
+
+ if (charCS) {
+ who = 'character|'+charCS.id;
+ } else {
+ who = '';
+ }
+ clearWaitTimer();
+ setTimeout(() => sendChat(who,msg,null,{use3d:use3Ddice}), 0);
+ };
+
+ /**
+ * Send API command to chat
+ */
+ LibFunctions.sendAPI = function(msg, senderId, from='', noSplit=false) {
+ var as;
+ if (!msg) {
+ log('sendMagicAPI: no msg');
+ return undefined;
+ }
+ if (!senderId || senderId.length == 0) {
+ as = '';
+ } else {
+ as = 'player|' + senderId;
+ }
+ let msgArray = noSplit ? [msg] : msg.split(/(?:
|\n)/);
+ _.each(msgArray, m => sendChat(as,m, null,{noarchive:!archive, use3d:use3Ddice}));
+ };
+
+ /**
+ * Send locally parsed feedback to the GM only!
+ */
+ LibFunctions.sendFeedback = function(msg,as,img) {
+ if (!msg)
+ {return;}
+ var gm = findTheGM(),
+ div = ''
+ + '  + ') '
+ + ' ';
+ clearWaitTimer(gm);
+ setTimeout(() => sendChat(('player|'+gm),LibFunctions.sendMsgToWho(null,null,msg,div),null,{noarchive:!archive,use3d:false}), 100); //,use3d:false
+ };
+
+ /**
+ * Sends a response to everyone who controls the character
+ * RED: v0.003 Check the player(s) controlling the character are valid for this campaign
+ * if they are not, send to the GM instead - Transmogrifier can introduce invalid IDs
+ * Also check if the controlling player(s) are online. If they are not
+ * assume the GM is doing some testing and send the message to them.
+ */
+
+ LibFunctions.sendResponse = function(charCS,msg,senderId,as,img) {
+ if (!msg)
+ {return;}
+ if (!charCS || (senderId && playerIsGM(senderId))) {
+ LibFunctions.sendFeedback( msg, as, img );
+ } else {
+ var div = ''
+ + '  + ') '
+ + ' ';
+ clearWaitTimer(senderId);
+ setTimeout(() => sendChat((senderId ? 'player|'+senderId : charCS.get('name')),LibFunctions.sendMsgToWho(charCS,senderId,msg,div),null,{noarchive:!archive, use3d:use3Ddice}), 100);
+ }
+ };
+
+ /*
+ * Send a message to the player (rather than the character)
+ */
+
+ LibFunctions.sendResponseError = function(pid,msg,as,img) {
+ msg = '&{template:'+fields.warningTemplate+'}{{title=Warning!}}{{desc='+msg+'}}';
+ LibFunctions.sendResponsePlayer(pid,msg,as,img);
+ return;
+ }
+
+ /*
+ * Send an error message to the identified player.
+ * If that player is not online, send to the GM
+ */
+
+ LibFunctions.sendResponsePlayer = function(pid,msg,as,img) {
+ if (!pid || !msg)
+ {return null;}
+ var player = getObj('player',pid),
+ to;
+ if (player && player.get('_online')) {
+ to = '/w "' + player.get('_displayname') + '" ';
+ } else {
+ to = '/w gm ';
+ }
+ var content = to
+ + ''
+ + '  + ') '
+ + ' '+msg;
+ clearWaitTimer(pid);
+ setTimeout(() => sendChat((as?as:defaultAs),content,null,{noarchive:false, use3d:use3Ddice}), 100);
+ };
+
+ /*
+ * Send to all players other than those that control the specified character
+ * and/or other than the specified player
+ */
+
+ LibFunctions.sendToOthers = function(pid,msg,as,img,charCS) {
+ if (!msg || (!pid && !charCS))
+ {return null;}
+ let controllers = charCS ? charCS.get('controlledby').split(',') : [];
+ let players = filterObjs(obj => {
+ if (obj.get('_type') != 'player' || obj.id == pid) return false;
+ if (controllers.includes(obj.id)) return false;
+ return obj.get('_online');
+ });
+ _.each(players, p => LibFunctions.sendResponsePlayer(p,msg,as,img));
+ };
+
+ /**
+ * Send a simple error
+ */
+
+ LibFunctions.sendError = function(msg, cmd) {
+ var postErrorMsg = function( msg, cmd ) {
+ var content = '/w GM '
+ + ''
+ + '  '
+ + ' '
+ + errorMsgDiv + 'Error: ' + msg
+ + (cmd ? (' while processing command
' + 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 = ''};
+ if (!cmdStr) cmdStr = msg.content;
+ log(apiName + ' error: ' + e.name + ', ' + e.message + ' when processing command ' + cmdStr);
+ let who=(getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname');
+ sendChat(apiName,`/w gm `+
+ ``+
+ ` There was an error while trying to run ${who}'s command: `+
+ ` ${cmdStr}
`+
+ ` Please send me this information so I can make sure this doesn't happen again (triple click for easy select in most browsers.): `+
+ ` `+
+ JSON.stringify({msg:msg, version:version, stack: e.stack, API_Meta})+
+ ` `+
+ ` `
+ )
+ };
+ setTimeout(postCatchMsg,500,apiName,msg,e,cmdStr);
+ };
+
+ /**
+ * Pare a message with ^^...^^ parameters in it and send to chat
+ * This allows character and token names for selected characters to be sent
+ * Must be called with a validated tokenID
+ */
+
+ LibFunctions.sendParsedMsg = function( tid, msg, senderId, msgFrom, t2id ) {
+ var cid, tname, charCS, cname, curToken,
+ parsedMsg = msg;
+
+ curToken = getObj( 'graphic', tid );
+ tname = (curToken ? curToken.get('name') : '');
+ cid = (curToken ? curToken.get('represents') : '');
+ charCS = getObj('character',cid);
+ cname = (charCS ? charCS.get('name') : '');
+
+ parsedMsg = parsedMsg.replace( /\^\^cid\^\^/gi , cid );
+ parsedMsg = parsedMsg.replace( /\^\^tid\^\^/gi , tid );
+ parsedMsg = parsedMsg.replace( /\^\^cname\^\^/gi , cname );
+ parsedMsg = parsedMsg.replace( /\^\^tname\^\^/gi , tname );
+
+ if (t2id) {
+ curToken = getObj( 'graphic', t2id );
+ tname = curToken.get('name');
+ cid = curToken.get('represents');
+ charCS = getObj('character',cid);
+ cname = charCS.get('name');
+
+ parsedMsg = parsedMsg.replace( /\^\^c2id\^\^/gi , cid );
+ parsedMsg = parsedMsg.replace( /\^\^t2id\^\^/gi , t2id );
+ parsedMsg = parsedMsg.replace( /\^\^c2name\^\^/gi , cname );
+ parsedMsg = parsedMsg.replace( /\^\^t2name\^\^/gi , tname );
+ }
+ LibFunctions.sendResponse( charCS, parsedMsg, senderId, msgFrom, null );
+ };
+
+ /*
+ * Check to see if a command string includes a gm roll query. If so,
+ * convert it to a normal roll query and send it to the GM to answer.
+ * Return true if a gm query has been found.
+ */
+
+ LibFunctions.sendGMquery = function( api, command, senderId ) {
+ var rollQuery;
+ if (command.toLowerCase().includes('gm{')) {
+ while ((rollQuery = command.match(/gm{.+?}/i))) {
+ if (!rollQuery || !rollQuery.length) break;
+ rollQuery = rollQuery[0].replace(/gm{/i,'?{').replace(/\//g,'|');
+ rollQuery = LibFunctions.parseStr(rollQuery);
+ command = command.replace(/gm{.+?}/i,rollQuery);
+ };
+ LibFunctions.sendFeedback( '&{template:'+fields.warningTemplate+'}{{title=DM Selection}}{{desc=As DM, you need to make [selections](!'+api+' '+senderId+' --'+command+') for '+getObj('player',senderId).get('_displayname')+'. Press the button and the selections and their reasons will be presented to you in Roll Querys in the centre of the screen.}}');
+ LibFunctions.sendResponsePlayer( senderId, '&{template:'+fields.messageTemplate+'}{{title=DM Selection}}{{desc=Please wait while the DM makes a choice or dice roll.}}' );
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ /*
+ * Send a formatted "please wait" message to the specified player.
+ */
+
+ LibFunctions.sendWait = function(senderId,timer=500,source='') {
+ if (timer === 0) {
+ clearWaitTimer(senderId);
+ return;
+ } else if (waitList[senderId]) {
+ clearWaitTimer(senderId);
+ }
+ if (playerIsGM(senderId)) {
+ waitList[senderId] = setTimeout(() => {sendChat(defaultAs,('/w GM ' + waitMsgDiv + 'Gathering data - please wait'),null,{noarchive:!archive});
+ clearWaitTimer(senderId);
+ }, timer);
+ } else {
+ var player = getObj('player',senderId),
+ to = '/w "' + (!player ? 'GM' : player.get('_displayname')) + '" ';
+ waitList[senderId] = setTimeout(() => {sendChat('player|'+senderId,(to + waitMsgDiv + 'Gathering data - please wait'),null,{noarchive:!archive, use3d:false});
+ clearWaitTimer(senderId);
+ }, timer);
+ }
+ };
+
+
+ /* ------------------------------- Character Sheet Database Management -------------------------- */
+
+ /*
+ * Check the version of a Character Sheet database against
+ * the current version in the API. Return true if needs updating
+ */
+
+ LibFunctions.checkDBver = function( dbFullName, dbObj, silent ) {
+
+ dbFullName = dbFullName.replace(/_/g,'-');
+
+ var dbCS = findObjs({ type:'character', name:dbFullName },{caseInsensitive:true}),
+ dbVersion = 0.0,
+ msg, versionObj;
+
+ if (!dbCS || !dbCS.length) return true;
+
+ dbCS = dbCS[0];
+ dbVersion = parseFloat(LibFunctions.attrLookup( dbCS, fields.dbVersion ) || dbVersion);
+
+ if (dbVersion < (parseFloat(dbObj.version) || 0)) {log('checkDBver: dB '+dbFullName+' API version='+(parseFloat(dbObj.version) || 0)+', CS version='+dbVersion); return true;}
+
+ msg = dbFullName+' v'+dbVersion+' not updated as is already latest version';
+ if (!silent) LibFunctions.sendFeedback(msg,fields.feedbackName);
+ return false;
+ }
+
+ /*
+ * A function to read the abilities of a database character sheet
+ * and write them to a handout, so they can be cut&pasted to an API
+ * for saving as a new version.
+ */
+
+ LibFunctions.saveDBtoHandout = function( dbName, version, typeFilter='' ) {
+
+ var dbCS = findObjs({ type: 'character', name: dbName })[0] || undefined,
+ objDef,
+ objHeader = '',
+ foundItems = [],
+ dbHandout,csDBlist,
+ reDBdata = {speed:reSpellSpecs.speed,cost:reSpellSpecs.cost,recharge:reSpellSpecs.recharge};
+
+ var encodeStr = (str,encoders=dbEncoders) => encoders.reduce((m, rep) => m.replace(rep[0], rep[1]), str);
+
+ if (!dbCS) {
+ LibFunctions.sendError(('Database '+dbName+' not found'),null);
+ return undefined;
+ }
+ if (!version || !version.length) {
+ version = (parseFloat(LibFunctions.attrLookup( dbCS, fields.dbVersion ) || '1.0') + 0.01).toFixed(2).toString();
+ } else if (version === '=') {
+ version = parseFloat(LibFunctions.attrLookup( dbCS, fields.dbVersion ) || '1.0');
+ }
+ dbHandout = findObjs({ type: 'handout', name: dbName+'-object v'+version });
+
+ if (!dbHandout || !dbHandout.length) {
+ dbHandout = createObj('handout',{name:(dbName+'-object v'+version)});
+ } else {
+ dbHandout = dbHandout[0];
+ }
+
+ objHeader = 'avatar:\''+dbCS.get('avatar')+'\', '
+ + 'version:'+version+', ';
+ objDef = 'db:[';
+ csDBlist = findObjs({ type: 'ability', characterid: dbCS.id });
+
+ _.each( _.sortBy(csDBlist,item => item.get('name')), function( item ) {
+ let itemName = item.get('name');
+ if (foundItems.includes(itemName)) return;
+ foundItems.push(itemName);
+
+ let objData = LibFunctions.resolveData(itemName,dbName,reNotAttackData,null,reDBdata).parsed,
+ objHitData = LibFunctions.resolveData(itemName,dbName,reToHitData,null,reDBdata).parsed,
+ objBody = encodeStr(item.get('action')),
+ objCT = objData.speed || objHitData.speed || 0,
+ objChg = (objData.type !== 'uncharged') ? objData.type : objHitData.type,
+ objCost = objData.cost || objHitData.cost || 0,
+ objType = '',
+ specs = objBody.match(/}}\s*?specs\s*?=(.*?){{/im);
+
+ specs = specs ? [...('['+specs[0]+']').matchAll(reSpecClass)] : [];
+ for (let i=0; i < specs.length; i++) {
+ objType += (objType && objType.length) ? ('|' + specs[i][1]) : specs[i][1];
+ }
+ objType = _.uniq(objType.toLowerCase().split('|')).join('|');
+ if (typeFilter && typeFilter.length && !objType.includes(typeFilter)) return;
+
+ objBody = objBody.replace(/template:2Edefault/i,'template:\'+fields.CSdefaultTemplate+\'')
+ .replace(/template:2Espell/i,'template:\'+fields.CSspellTemplate+\'')
+ .replace(/template:2Eattack/i,'template:\'+fields.CSweaponTemplate+\'')
+ .replace(/template:RPGMdefault/i,'template:\'+fields.defaultTemplate+\'')
+ .replace(/template:RPGMspell/i,'template:\'+fields.spellTemplate+\'')
+ .replace(/template:RPGMweapon/i,'template:\'+fields.weaponTemplate+\'')
+ .replace(/template:RPGMpotion/i,'template:\'+fields.potionTemplate+\'')
+ .replace(/template:RPGMattack/i,'template:\'+fields.targetTemplate+\'')
+ .replace(/template:RPGMammo/i,'template:\'+fields.ammoTemplate+\'')
+ .replace(/template:RPGMarmour/i,'template:\'+fields.armourTemplate+\'')
+ .replace(/template:RPGMitem/i,'template:\'+fields.itemTemplate+\'')
+ .replace(/template:RPGMitemSpell/i,'template:\'+fields.itemSpellTemplate+\'')
+ .replace(/template:RPGMring/i,'template:\'+fields.ringTemplate+\'')
+ .replace(/template:RPGMscroll/i,'template:\'+fields.scrollTemplate+\'')
+ .replace(/template:RPGMwand/i,'template:\'+fields.wandTemplate+\'')
+ .replace(/template:RPGMwandSpell/i,'template:\'+fields.wandSpellTemplate+\'')
+ .replace(/template:RPGMmessage/i,'template:\'+fields.messageTemplate+\'')
+ .replace(/template:RPGMwarning/i,'template:\'+fields.warningTemplate+\'')
+ .replace(/template:RPGMclass/i,'template:\'+fields.classTemplate+\'');
+
+ objDef += '{name:\''+itemName+'\','
+ + 'type:\''+objType+'\','
+ + 'ct:\''+objCT+'\','
+ + 'charge:\''+objChg+'\','
+ + 'cost:\''+objCost+'\','
+ + 'body:\''+objBody+'\'}, ';
+ });
+ objDef += ']}, ';
+ dbHandout.set('notes',objHeader+objDef);
+ LibFunctions.sendFeedback('Extracted '+dbName+' v'+version,fields.feedbackName);
+ LibFunctions.setAttr( dbCS, fields.dbVersion, version );
+ return dbHandout;
+ }
+
+ /*
+ * Check the version of a Character Sheet database and, if
+ * it is earlier than the static data held in this API, update
+ * it to the latest version.
+ */
+
+ LibFunctions.buildCSdb = function( dbFullName, dbObj, typeList, silent ) {
+
+ dbFullName = dbFullName.replace(/_/g,'-');
+
+ const spells = dbObj.type.includes('spell') || dbObj.type.includes('power'),
+ charClass = dbObj.type.includes('class'),
+ rootDB = dbObj.root.toLowerCase();
+
+ var dbVersion = 0.0,
+ dbCS = findObjs({ type:'character', name:dbFullName },{caseInsensitive:true}),
+ errFlag = false,
+ lists = {},
+ foundItems = [],
+ csDBlist, specs, objType, objBody,
+ msg, versionObj, curDB;
+
+ if (LibFunctions.checkDBver( dbFullName, dbObj, silent )) {
+
+ if (dbCS && dbCS.length) {
+ let abilities = findObjs({ _type:'ability', _characterid:dbCS[0].id });
+ _.each( abilities, a => a.remove() );
+ dbCS = dbCS[0];
+ } else {
+ dbCS = createObj( 'character', {name:dbFullName} );
+ }
+
+ let sorted = _.sortBy(dbObj.db,'name');
+
+ _.each(sorted, item => {
+ if (!!item.body && !foundItems.includes(item.name)) {
+ foundItems.push(item.name);
+ item.body = LibFunctions.parseStr(item.body,dbReplacers);
+ if (!LibFunctions.setAbility( dbCS, item.name, item.body )) {
+ errFlag = true;
+ } else {
+ LibFunctions.setAttr( dbCS, [fields.CastingTimePrefix[0]+item.name, 'current'], item.ct );
+ LibFunctions.setAttr( dbCS, [fields.CastingTimePrefix[0]+item.name, 'max'], (spells ? item.cost : item.charge) );
+ LibFunctions.addMIspells( dbCS, item );
+ item.type.dbName().split('|').filter(t => !!t).map(t => {
+ let listType = typeList[t] ? typeList[t].type.toLowerCase() : (typeList.miscellaneous ? typeList.miscellaneous.type.toLowerCase() : undefined);
+ if (listType) {
+ if (!lists[listType]) lists[listType] = [];
+ if (!lists[listType].includes(item.name)) {
+ lists[listType].push(item.name);
+ }
+ } else if (_.isUndefined(listType)) {
+ LibFunctions.sendError(('Unable to identify item type '+t+' when updating '+item.name+' in database '+dbFullName));
+ };
+ });
+ };
+ };
+ });
+ if (errFlag) {
+ LibFunctions.sendError( ('Unable to completely update database '+dbFullName) );
+ } else {
+ _.each(typeList, dbList => dbList.field[0].length ? LibFunctions.setAttr( dbCS, [dbList.field[0],'current'], (lists[dbList.type.toLowerCase()] || ['']).join('|')) : '');
+ LibFunctions.setAttr( dbCS, fields.dbVersion, (dbObj.version || 1.0));
+ dbCS.set('avatar',(dbObj.avatar || ''));
+ dbCS.set('bio',(dbObj.bio || ''));
+ dbCS.set('controlledby',(dbObj.controlledby || 'All'));
+ dbCS.set('gmnotes',(dbObj.gmnotes || ''));
+ let msg = 'Updated database '+dbFullName+' to version '+String(dbObj.version);
+ if (!silent) LibFunctions.sendFeedback( msg, fields.feedbackName ); else log(msg);
+ }
+ }
+ return (errFlag);
+ }
+
+ /**
+ * Create an internal index of items in the databases
+ * to make searches much faster. Index entries indexed by
+ * database root name & short name (name in lower case with
+ * '-', '_' and ' ' ignored). index[0] = abilityID,
+ * index[1] = ct-attributeID
+ * v3.051 Check that other database-handling APIs have finished
+ * updating their databases and performed a handshake
+ **/
+
+ LibFunctions.updateDBindex = function() {
+ var rootDB, magicDB, validDB,
+ db, shortName, attrName, objList,
+ rootList = ['mu_spells_db','pr_spells_db','powers_db','mi_db','race_db','class_db','attacks_db','styles_db','locks_traps_db'],
+ index = {};
+
+ _.each( dbNames, (dbFields, db) => {
+ if (state.MagicMaster.spellRules.denyCustom && db.toLowerCase().includes('custom')) return;
+ _.each( dbFields.db, (item, i) => {
+ rootDB = db.toLowerCase().match( /[a-z_]+?_db/i );
+ if (!item || !item.name) log('updateDBindex: item='+item.name+', i='+i+', unable to create shortName');
+ shortName = item.name.dbName();
+ if (_.isUndefined(index[rootDB])) index[rootDB] = {};
+ if (_.isUndefined(index[rootDB][shortName])) index[rootDB][shortName] = ['',String(item.ct),db,i];
+ });
+ });
+
+ objList = filterObjs( function(obj) {
+ if (obj.get('type') != 'ability') return false;
+ if (!(magicDB = getObj('character',obj.get('characterid')))) {
+ return false;
+ }
+ db = magicDB.get('name').toLowerCase().replace(/-/g,'_');
+ if (/\s*v\d*\.\d*/.test(db)) {
+ return false;
+ }
+ let validDB = false;
+ for (const rDB of rootList) {
+ if (db.startsWith(rDB)) {
+ validDB = true;
+ rootDB = rDB;
+ break;
+ }
+ }
+ if (!validDB) {return false;}
+ let shortName = obj.get('name').dbName();
+
+ if (_.isUndefined(index[rootDB])) {index[rootDB] = {};}
+ if (_.isUndefined(index[rootDB][shortName]) || !index[rootDB][shortName][0].length || !stdDB.includes(db)) {
+ index[rootDB][shortName] = [obj.id,''];
+ }
+ return true;
+ });
+ objList = filterObjs( function(obj) {
+ if (obj.get('type') != 'attribute') {return false;}
+ attrName = obj.get('name');
+ if (!attrName || !attrName.toLowerCase().startsWith('ct-')) {return false;}
+ if (!(magicDB = getObj('character',obj.get('characterid')))) {
+ return false;
+ }
+ db = magicDB.get('name').toLowerCase().replace(/-/g,'_');
+ if (/\s*v\d*\.\d*/.test(db)) {return false;}
+ let validDB = false;
+ for (const rDB of rootList) {
+ if (db.startsWith(rDB)) {
+ validDB = true;
+ rootDB = rDB;
+ }
+ }
+ if (!validDB)
+ {return false;}
+ let shortName = attrName.dbName().substring(2);
+
+ if (!!!index[rootDB][shortName]) {
+ return false;
+ }
+ if (!stdDB.includes(db) || (!!!index[rootDB][shortName][1]) || (index[rootDB][shortName][1].length === 0)) {
+ index[rootDB][shortName][1] = obj.id;
+ };
+ return true;
+ });
+ magicList = {}; // Blank the internal index of items, as it might have changed and needs rebuilding
+// LibFunctions.sendFeedback( waitMsgDiv+'RPGMaster is now ready.' );
+
+ return index;
+ }
+
+ /*
+ * Check a character sheet database and update/create the
+ * required attributes from the definitions. This should
+ * be run after updating or adding item or spell definitions.
+ */
+
+ LibFunctions.checkCSdb = function( dbFullName ) {
+
+ var db = dbFullName.toLowerCase(),
+ lists = {},
+ spellsDB,
+ dbCSlist,
+ dbTypeList;
+
+ var checkObj = function( obj ) {
+ var objCS, objCSname, objName, objBody, type, objCT, objChg, objCost, specs, spellsDB, classDB;
+
+ if (!obj || obj.get('type') !== 'ability') return false;
+ objCS = getObj('character',obj.get('characterid'));
+ if (!objCS) {log('checkObj: not found database object');return false;}
+ objCSname = objCS.get('name').toLowerCase();
+ if (db && db.length && (db !== '-db' && !objCSname.startsWith(db))) return false;
+ if (!objCSname.includes('-db') || (/\s*v\d*\.\d*/.test(objCSname))) return false;
+ objBody = obj.get('action');
+ spellsDB = objCSname.includes('spells') || objCSname.includes('powers');
+ classDB = objCSname.includes('class') || objCSname.includes('race');
+ specs = objBody.match(reSpecs);
+ objName = obj.get('name');
+ if (specs) {
+ dbTypeList = (spellsDB ? spTypeLists : (classDB ? clTypeLists : miTypeLists));
+ specs = specs ? [...('['+specs[0]+']').matchAll(reSpecClass)] : [];
+ for (const i of specs) {
+ type = i[1];
+ if (type && type.length) {
+ let typeList = type.dbName().split('|');
+ for (const t of typeList) {
+ let itemType = dbTypeList[t] ? dbTypeList[t].type : (dbTypeList.miscellaneous ? dbTypeList.miscellaneous.type : undefined);
+ if (itemType) {
+ if (!lists[objCS.id]) lists[objCS.id] = {};
+ if (!lists[objCS.id][itemType]) lists[objCS.id][itemType] = [];
+ if (!lists[objCS.id][itemType].includes(objName)) {
+ lists[objCS.id][itemType].push(objName);
+ };
+ };
+ };
+ };
+ };
+ };
+ objCT = (objBody.match(reDataSpeed) || ['',0])[1];
+ objChg = (objBody.match(reDataCharge) || ['','uncharged'])[1];
+ objCost = (objBody.match(reDataCost) || ['',0])[1];
+ LibFunctions.setAttr( objCS, [fields.CastingTimePrefix[0]+objName, 'current'], objCT );
+ LibFunctions.setAttr( objCS, [fields.CastingTimePrefix[0]+objName, 'max'], (spellsDB ? objCost : objChg) );
+ LibFunctions.addMIspells( objCS, {name:objName,body:objBody} );
+ return true;
+ };
+
+ dbCSlist = filterObjs( obj => checkObj(obj) );
+ if (!dbCSlist || !dbCSlist.length) {
+ LibFunctions.sendFeedback('No databases found with a name that includes '+db,fields.feedbackName);
+ } else {
+ _.each(lists,(types,dbID) => {
+ let dbCS = getObj('character',dbID);
+ _.each(dbTypeList, dbList => {
+ if (types[dbList.type]) {
+ LibFunctions.setAttr( dbCS, [dbList.field[0],'current'], (types[dbList.type].sort().join('|') || '' ));
+ }
+ });
+ });
+ LibFunctions.sendFeedback(((!db || !db.length || db === '-db') ? 'All databases have' : ('Database '+dbFullName+' has')) + ' been updated',fields.feedbackName);
+ }
+ return;
+ }
+
+ /**
+ * Get a new DB index of all Ability Objects stored in
+ * database character sheets
+ **/
+
+ LibFunctions.getDBindex = function(forceUpdate = false) {
+ if (_.isUndefined(DBindex) || forceUpdate) {
+ DBindex = LibFunctions.updateDBindex();
+ }
+ return DBindex;
+ }
+
+ /**
+ * Update or create the help handouts
+ **/
+
+ LibFunctions.updateHandouts = function(handouts,silent,senderId) {
+
+ let classHelp = findObjs({ _type:'handout', name:'Class Database Help' });
+ if (classHelp && classHelp[0]) classHelp[0].remove();
+ _.each(handouts,(obj,k) => {
+ let dbCS = findObjs({ type:'handout', name:obj.name },{caseInsensitive:true});
+ if (!dbCS || !dbCS[0]) {
+ log(obj.name+' not found. Creating version '+obj.version);
+ if (!silent) LibFunctions.sendFeedback(obj.name+' not found. Creating version '+obj.version);
+ dbCS = createObj('handout',{name:obj.name,inplayerjournals:(_.isUndefined(senderId) ? '' : senderId)});
+ dbCS.set('notes',obj.bio.replace('[General DB Help]',General_DB_Help)
+ .replace('[Item Inheritance]',itemInheritance)
+ .replace('[General API Help]',General_API_Help));
+ dbCS.set('avatar',obj.avatar);
+ } else {
+ dbCS = dbCS[0];
+ dbCS.get('notes',function(note) {
+ let reVersion = new RegExp(obj.name+'\\s*?v(\\d+?.\\d*?)', 'im');
+ let version = note.match(reVersion);
+ version = (version && version.length) ? (parseFloat(version[1]) || 0) : 0;
+ if (version >= obj.version) {
+ if (!silent) LibFunctions.sendFeedback('Not updating handout '+obj.name+' as is already version '+obj.version);
+ return;
+ }
+ dbCS.set('notes',obj.bio.replace('[General DB Help]',General_DB_Help)
+ .replace('[Item Inheritance]',itemInheritance)
+ .replace('[General API Help]',General_API_Help));
+ dbCS.set('avatar',obj.avatar);
+ if (!silent) LibFunctions.sendFeedback(obj.name+' handout updated to version '+obj.version);
+ log(obj.name+' handout updated to version '+obj.version);
+ });
+ }
+ });
+ return;
+ }
+
+ /**
+ * Get the handout IDs for all handouts
+ **/
+
+ LibFunctions.getHandoutIDs = function() {
+
+ var handoutObjs = findObjs({ type: 'handout' }),
+ handoutIDs = {};
+ _.each( handoutObjs, h => {
+ let name = h.get('name').replace(/[-_&\s]/g,'');
+ handoutIDs[name] = h.id;
+ });
+ return handoutIDs;
+ };
+
+ /* -------------------------------- Utility Functions ---------------------------- */
+
+ /**
+ * Calculate/roll an attribute value that has a range
+ * Always tries to create a 3 dice bell curve for the value
+ **/
+
+ LibFunctions.calcAttr = function( attr='3:18' ) {
+ let attrRange = attr.split(':'),
+ low = parseInt(attrRange[0]),
+ high = parseInt(attrRange[1]);
+ if (high && !isNaN(low) && !isNaN(high)) {
+ let range = high - (low - 1);
+ if (range === 2) {
+ return low - 1 + randomInteger(2);
+ } else if (range === 3) {
+ return low - 2 + randomInteger(2) + randomInteger(2);
+ } else if (range === 5) {
+ return low - 2 + randomInteger(3) + randomInteger(3);
+ } else if ((range-2)%3 === 0) {
+ return low - 3 + randomInteger(Math.ceil(range/3)+1) + randomInteger(Math.floor(range/3)+1) + randomInteger(Math.floor(range/3)+1);
+ } else if ((range-1)%3 === 0) {
+ return low - 3 + randomInteger(Math.ceil(range/3)) + randomInteger(Math.ceil(range/3)) + randomInteger(Math.ceil(range/3));
+ } else if ((range)%3 === 0) {
+ return low - 3 + randomInteger((range/3)+1) + randomInteger((range/3)+1) + randomInteger(range/3);
+ }
+ }
+ return attr;
+ }
+
+ /**
+ * A function to calculate an internal dice roll
+ */
+
+ LibFunctions.rollDice = function( count, dice, reroll ) {
+ count = parseInt(count || 1);
+ dice = parseInt(dice || 8);
+ reroll = parseInt(reroll || 0);
+ let total = 0,
+ roll;
+ for (let d=0; d= 30) {
+ log('evalAttr: loop count exceeded, returning '+v);
+ return v;
+ }
+ while (rePar.test(v)) v = v.replace(rePar,eval).replace(/\-\-/g,'+').replace(/\+\-/g,'-');
+ v = v.replace(reRange,LibFunctions.calcAttr).replace(/\-\-/g,'+').replace(/\+\-/g,'-');
+ } while (rePar.test(v) || reRange.test(v));
+ v = v.replace(reDice,reRoll).replace(/\-\-/g,'+').replace(/\+\-/g,'-');
+ } while (rePar.test(v) || reRange.test(v) || reDice.test(v));
+ v = v.replace(reMinMax,eval).replace(/\-\-/g,'+').replace(/\+\-/g,'-');
+ } while (rePar.test(v) || reRange.test(v) || reDice.test(v) || reMinMax.test(v));
+ return String(v)+(orig[2] || '');
+ };
+ } catch (e) {
+ LibFunctions.sendError('Invalid attribute value given: calculating "'+orig[0]+'" but only **\'+ - * / ( ) : d r f c ^ v , ;\'** can be used. Current evaluation is '+v);
+// LibFunctions.sendCatchError('LibFunctions',null,e);
+ return v;
+ };
+ };
+
+ /**
+ * Find the player's ID from a player name or a character name,
+ * or if no player name provided, return the GM's ID
+ **/
+
+ LibFunctions.findThePlayer = function(who) {
+ let playerObjs = findObjs({_type:'player',_displayname:who});
+ let GMid = findTheGM();
+ if (!playerObjs || !playerObjs.length) {
+ let charObj = LibFunctions.findCharacter(who);
+ if (charObj) {
+ let playerIds = charObj.get('controlledby').split(',').filter(id => id !== '' && id !== GMid);
+ if (!playerIds || !playerIds.length) return GMid;
+ if (playerIds[0] !== 'all') {
+ let pid = playerIds.find( p => (getObj('player',p) && !!getObj('player',p).get('_online')));
+ if (pid) return pid;
+ }
+ playerObjs = filterObjs(p => (p.get('_type') === 'player' && !!p.get('_online') && p.id !== GMid));
+ }
+ }
+ return (!playerObjs || !playerObjs.length) ? GMid : playerObjs[0].id;
+ };
+
+ /**
+ * Find a Character object given a name only,
+ * returning the first match or undefined
+ */
+
+ LibFunctions.findCharacter = function( name ) {
+ var charObj = findObjs({ _type: 'character' , name: name },{caseInsensitive: true});
+ return ((charObj && charObj.length) ? charObj[0] : undefined);
+ }
+
+ /**
+ * Function to find the ID of a live player
+ * that controls the specified character
+ */
+
+ LibFunctions.checkPlayersLive = function( charCS ) {
+ let playerID, controlledBy = (!charCS ? '' : charCS.get('controlledby'));
+ if (controlledBy.length > 0) {
+ controlledBy = controlledBy.split(',');
+ let viewerID = (state.roundMaster && state.roundMaster.viewer && state.roundMaster.viewer.is_set) ? (state.roundMaster.viewer.pid || null) : null;
+ let players = controlledBy.filter(id => id != viewerID);
+ if (players.length) {
+ playerID = _.find( controlledBy, function(playerID) {
+ players = findObjs({_type: 'player', _id: playerID, _online: true});
+ return (players && players.length > 0);
+ });
+ };
+ };
+ return playerID;
+ };
+
+ /**
+ * A function to return the specified player ID, or
+ * the first live player who controls the character,
+ * or the first live player who controls the token
+ * representing a character, or senderId, or the GM.
+ */
+
+ LibFunctions.fixSenderId = function( args, selected, senderId ) {
+
+ let playerID = args[0] || (selected && selected.length ? selected[0]._id : senderId),
+ playerObj = getObj('player',playerID);
+ if (!playerObj) playerID = LibFunctions.checkPlayersLive( getObj('character',args[0]) );
+ if (!playerID) playerID = LibFunctions.checkPlayersLive( LibFunctions.getCharacter(args[0]) );
+
+ return playerID || senderId;
+ };
+
+ /*
+ * Parse a data string for attribute settings
+ */
+
+ LibFunctions.parseData = function( attributes, reSpecs, def=true, charCS, item='', row='' ) {
+
+ var parsedData = {},
+ val,
+ varRes = ( m, w, v = 'current' ) => LibFunctions.parseStr((LibFunctions.attrLookup( charCS, [fields.ItemVar[0]+item+'+'+row+'-'+w,'current'] ) || '').split('/')[v] || '');
+
+ if (charCS) while (reVars.test(attributes)) attributes = attributes.replace(reVars,varRes);
+ _.each( reSpecs, spec => {
+ if (_.isUndefined(spec) || _.isUndefined(spec.re)) return;
+ val = attributes.match(spec.re);
+ if (!!val && val.length>1 && val[1].length) {
+ parsedData[spec.field] = (val.length == 3 && val[2]) ? [val[1],val[2]] : val[1];
+ } else if (!def) {
+ parsedData[spec.field] = undefined;
+ } else {
+ parsedData[spec.field] = spec.def;
+ }
+ });
+ return parsedData;
+ }
+
+ /*
+ * Follow an inheritance chain of Class or Race database objects and
+ * consolidate their parsed data and attribute specifications
+ */
+
+ LibFunctions.resolveData = function( name, dBase, reThisData, charCS, reParseTable, row='', quals=[], defBase=true, doneList=[], topItem, debugging=false ) {
+
+ try {
+
+ if (_.isEmpty(reParseTable)) reParseTable = undefined;
+
+ var thisObj, thisSpecs, baseObj, thisData, thisAttr, parsedData,
+ rDB = dBase.toLowerCase().replace(/-/g,'_'),
+ isSpell = rDB.includes('spells_db'),
+ isMI = rDB.startsWith('mi_db'),
+ isRC = !isSpell && !isMI,
+ parseTable = reClassSpecs,
+ baseData = [['']],
+ baseParsed = LibFunctions.parseData( '', (reParseTable || (!isRC ? reSpellSpecs : reClassSpecs)), defBase ),
+ baseAttr = LibFunctions.parseData( '', reAttr, defBase ),
+ debugging = debugging || false, // name.dbName().includes('berserking'),
+ varRes = ( m, w, v = 0 ) => LibFunctions.parseStr((LibFunctions.attrLookup( charCS, [fields.ItemVar[0]+(topItem || name)+'+'+row+'-'+w,'current'] ) || '').split('/')[v] || '');
+
+ if (!name || !name.trim().length || doneList.includes(name.dbName())) throw new Error('resolveData: no name or already processed '+name);
+ thisObj = LibFunctions.abilityLookup( dBase, name, charCS, true );
+ if (!thisObj.obj || !thisObj.obj[1]) throw new Error('resolveData: no definition of '+name+' in '+dBase);
+ doneList.push(name.dbName());
+ thisSpecs = thisObj.specs();
+ if (!thisSpecs || !thisSpecs[0]) throw new Error('resolveData: no Specs in definition of '+name);
+ if (debugging) log('resolveData: weapon '+name+' thisSpecs = '+thisSpecs[0]+', item body = '+thisObj.obj[1].body);
+ baseObj = LibFunctions.resolveData( ((isMI ? thisSpecs[0][5] : thisSpecs[0][4]) || ''), dBase, reThisData, charCS, reParseTable, row, quals, defBase, doneList, (topItem || name), debugging );
+ baseParsed = baseObj.parsed; baseAttr = baseObj.attrs; baseData = baseObj.raw;
+ thisData = thisObj.data(reThisData);
+ if (!thisData || !thisData[0]) thisData = [['']];
+ thisData.forEach( td => {_.each( quals, (q,k) => td[0] = td[0].replace(new RegExp('\\?\\?'+k,'g'),q));});
+ if (debugging) log('resolveData: weapon '+name+' quals = '+quals+', after ??# replacement, thisData.length = '+thisData.length+', thisData = '+thisData);
+ thisData[0][0] = thisData[0][0].replace(/\?\?\d/g,'0');
+ if (isMI || isSpell) {
+ if (debugging && !miTypeLists[thisSpecs[0][2].dbName().split('|')[0]]) log('resolveData: unable to find '+thisSpecs[0][2].dbName().split('|')[0]);
+ switch ((miTypeLists[thisSpecs[0][2].dbName().split('|')[0]] || {type:''}).type) {
+ case 'weapon':
+ case 'ammo':
+ parseTable = reWeapSpecs;
+ break;
+ case 'armour':
+ case 'armor':
+ parseTable = reACSpecs;
+ break;
+ default:
+ parseTable = reSpellSpecs;
+ break;
+ };
+ };
+ while (reVars.test(thisData[0][0])) thisData[0][0] = thisData[0][0].replace(reVars,varRes);
+ parsedData = LibFunctions.parseData( thisData[0][0], (reParseTable || parseTable), false, charCS, name, row );
+ thisAttr = LibFunctions.parseData( (parsedData.cattr || '')+',', reAttr, false, charCS, name, row );
+ if (baseParsed) {
+ if (!parsedData.cattr) {
+ parsedData.cattr = baseParsed.cattr;
+ thisAttr = baseAttr;
+ } else if (baseAttr) {
+ thisAttr = _.mapObject(Object.assign(baseAttr,_.pick(thisAttr,a => !!a)), attr => attr !== '-' ? attr : '');
+ }
+ if (debugging) log('resolveData: baseParsed = '+_.pairs(baseParsed).flat()+', parsedData = '+_.pairs(parsedData).flat());
+ parsedData = _.mapObject(Object.assign(baseParsed,_.pick(parsedData,a => !!a)), attr => attr !== '-' ? attr : '');
+ let dataCount = reIsAttackData.test(thisData[0][0]) ? thisData.length : 1;
+ for (let i=0; i < dataCount; i++) {
+ while (reVars.test(thisData[i][0])) thisData[i][0] = thisData[i][0].replace(reVars,varRes);
+ if (!!baseData.length && i < thisSpecs.length) {
+ thisData[i][0] = '['+_.pairs(Object.assign(
+ _.object(baseData[0][0].replace(/^.*?=\[/,'').replace(/[\[\]]/g,'').split(',').map(v => {v = v.trim().split(':');v[0] = v[0].toLowerCase();return v})),
+ _.object(thisData[i][0].replace(/^.*?=\[/,'').replace(/[\[\]]/g,'').split(',').map(v => {v = v.trim().split(':');v[0] = v[0].toLowerCase();return v}))
+ )
+ ).map(v => v.join(':')).filter(v => v !== ':').join()+']';
+ if (baseData.length > 1) {baseData.shift();} // else {baseData = [['']]};
+ };
+ };
+ }
+ if (debugging)log('resolveData: merged data for '+name+' thisData = '+thisData);
+ if (parsedData.bag || (parsedData.numpowers && parsedData.numpowers[0]!=='=')) thisData = thisData.concat(baseData);
+ if (debugging)log('resolveData: result is '+thisData.length+' long = '+thisData);
+ return {parsed:parsedData, attrs:thisAttr, raw:((thisData[0].length === 1 && thisData[0][0].trim() === '[]') ? '' : thisData)};
+
+ } catch (err) {
+ if (err.message.startsWith('resolveData')) {
+ if (debugging) log(err.message);
+ return {parsed:baseParsed, attrs:baseAttr, raw:baseData};
+ } else {
+ LibFunctions.sendCatchError( 'RPGM Library',null,err,'RPGM Library resolveData()');
+ }
+ }
+ };
+
+ /*
+ * Function to replace special characters in a string
+ */
+
+ LibFunctions.parseStr = function(str='',replaced=replacers){
+ return replaced.reduce((m, rep) => m.replace(rep[0], rep[1]), str);
+ }
+
+ /**
+ * Get valid character from a tokenID
+ */
+
+ LibFunctions.getCharacter = function( tokenID, silent=true ) {
+
+ var curToken,
+ charID,
+ charCS;
+
+ if (!tokenID) {
+ if (!silent) LibFunctions.sendError('Invalid token_id in arguments');
+ return undefined;
+ };
+
+ charCS = getObj( 'character', tokenID );
+ if (charCS) return charCS;
+
+ curToken = getObj( 'graphic', tokenID );
+
+ if (!curToken) {
+ if (!silent) LibFunctions.sendError('Invalid token_id in arguments');
+ return undefined;
+ };
+
+ charID = curToken.get('represents');
+
+ if (!charID) {
+ if (!silent) LibFunctions.sendError(('The token "'+curToken.get('name')+'" does not represent a character sheet'));
+ return undefined;
+ };
+
+ charCS = getObj('character',charID);
+
+ if (!charCS) {
+ if (!silent) LibFunctions.sendError(('The token "'+curToken.get('name')+'" does not represent a character sheet'));
+ return undefined;
+ };
+ return charCS;
+ };
+
+ /*
+ * Get linked values from the right place for this token. These can be
+ * re-mapped by the GM so need to check all links to assess correct
+ * source for a token value
+ */
+
+ LibFunctions.getTokenValue = function( curToken, tokenBar, field, altField, thac0_base ) {
+
+ if (!curToken) return undefined;
+ var charCS = LibFunctions.getCharacter(curToken.id),
+ attr = field[0].toLowerCase(),
+ altAttr = altField ? altField[0].toLowerCase() : 'EMPTY',
+ property = field[1],
+ token_property = (property.toLowerCase() == 'current' ? 'value' : 'max'),
+ linkedToken = false,
+ barName, attrVal, attrObj, attrName, tokenField,
+ fieldIndex = _.isUndefined(state.RPGMaster.tokenFields) ? -1 : state.RPGMaster.tokenFields.indexOf( field[0] );
+
+ if (!charCS) {return undefined;}
+
+ if (_.some( ['bar2_link','bar1_link','bar3_link'], linkName=>{
+ let linkID = curToken.get(linkName);
+ tokenField = linkName;
+ barName == '';
+ if (linkID && linkID.length) {
+ attrObj = getObj('attribute',linkID);
+ if (attrObj) {
+ attrName = attrObj.get('name').toLowerCase();
+ barName = tokenField.substring(0,4);
+ return (attrName == attr) || (attrName == altAttr);
+ }
+ }
+ return false;
+ })) {
+ linkedToken = true;
+ attrVal = parseFloat(curToken.get(barName+'_'+token_property));
+ attrVal = !isNaN(attrVal) ? parseFloat(attrVal) : undefined;
+ }
+ if (isNaN(attrVal) && !linkedToken && fieldIndex >= 0) {
+ attrVal = parseFloat(curToken.get('bar'+(fieldIndex+1)+'_'+token_property));
+ attrName = barName = 'bar'+(fieldIndex+1);
+ }
+ if (isNaN(attrVal) && attr.includes('thac0')) {
+ if (!thac0_base) thac0_base = ['thac0-base','current','20'];
+ attrVal = parseFloat(LibFunctions.attrLookup( charCS, thac0_base ));
+ attrName = thac0_base[0];
+ barName = undefined;
+ }
+ if (isNaN(attrVal)) {
+ attrVal = parseFloat(LibFunctions.attrLookup( charCS, field ));
+ attrName = field[0];
+ barName = undefined;
+ }
+ if (isNaN(attrVal) && altField) {
+ attrVal = parseFloat(LibFunctions.attrLookup( charCS, altField ));
+ attrName = altField[0];
+ }
+ return {val:attrVal, name:(isNaN(attrVal) ? undefined : attrName), barName:(barName || attrName)};
+ }
+
+ /*
+ * Create an array of class objects for the classes
+ * of the specified character.
+ */
+
+ LibFunctions.classObjects = function( charCS, senderId, parseTable ) {
+
+ try {
+ var charLevels = ((_.filter( fields, (elem,l) => {return l.toLowerCase().includes('_level')}).filter( elem => 0 < (LibFunctions.attrLookup( charCS, elem ) || 0))) || fields.Fighter_level);
+ var charClass, baseClass, charLevel, isCreature = false, isClass = false, dB = fields.ClassDB;
+
+ var classDef = _.filter( classLevels, a => {
+ return _.some( charLevels, b => {
+ return (a[1].includes(b[0]))
+ })
+ })
+ .map( elem => {
+ charClass = LibFunctions.attrLookup(charCS,elem[0]) || '';
+ charLevel = LibFunctions.attrLookup( charCS, elem[1] ) || 0;
+ if (elem[0][0] == fields.Wizard_class[0]) {
+ baseClass = 'wizard';
+ } else if (elem[0][0] == fields.Priest_class[0]) {
+ baseClass = 'priest';
+ } else if (elem[0][0] == fields.Rogue_class[0]) {
+ baseClass = 'rogue';
+ } else if (elem[0][0] == fields.Psion_class[0]) {
+ baseClass = 'psion';
+ } else if (elem[1][0] == fields.Fighter_level[0] && charLevel > 0) {
+ baseClass = 'warrior';
+ } else if (elem[1][0] == fields.Monster_hitDice[0]) {
+ let monsterHD = parseInt(LibFunctions.attrLookup( charCS, fields.Monster_hitDice )) || 0,
+ monsterHPplus = parseInt(LibFunctions.attrLookup( charCS, fields.Monster_hpExtra )) || 0,
+ monsterIntField = LibFunctions.attrLookup( charCS, fields.Monster_int ) || '',
+ monsterIntNum = parseInt((monsterIntField.match(/\d+/)||["1"])[0]) || 0,
+ monsterInt = monsterIntField.toLowerCase().includes('non') ? 0 : monsterIntNum;
+ charLevel = Math.ceil((monsterHD + Math.ceil(monsterHPplus/4)) / (monsterInt != 0 ? 1 : 2)); // Calculation based on p65 of DMG
+ baseClass = 'creature';
+ isCreature = true;
+ if (!charClass || !charClass.length) {
+ charClass = LibFunctions.attrLookup(charCS,fields.Race);
+ dB = fields.RaceDB;
+ };
+ if (!charClass || !charClass.length) {
+ charClass = 'creature';
+ dB = fields.ClassDB;
+ };
+
+ } else {
+ baseClass = 'warrior';
+ }
+ isClass = isClass || baseClass !== 'creature';
+ let classObj = LibFunctions.abilityLookup( dB, charClass, charCS, true );
+ if (!charClass.length || !classObj.obj) {
+ charClass = baseClass;
+ classObj = LibFunctions.abilityLookup( dB, baseClass, charCS, true );
+ }
+ return {name:charClass.dbName(), dB:classObj.dB, base:baseClass.dbName(), dBbase:fields.ClassDB, level:charLevel, obj:classObj.obj};
+ });
+ if (isCreature && isClass) {classDef = classDef.filter( c => c.base !== 'creature')};
+ if (_.isUndefined(classDef) || !classDef.length) classDef = [{name:'creature', dB:fields.RaceDB, base:'warrior', dBbase:fields.ClassDB, level:0, obj:LibFunctions.abilityLookup( fields.ClassDB, 'creature', charCS ).obj}];
+ classDef = classDef.map(c => {let d = LibFunctions.resolveData((c.name || charClass), (c.dB || dB), reData, null, parseTable); c.classData = d.parsed; c.attrData = d.attrs; c.rawData = d.raw; return c});
+
+ } catch (e) {
+ LibFunctions.sendCatchError( 'RPGM Library',(senderId ? msg_orig[senderId] : null),e,'RPGM Library classObjects()');
+ }
+ return classDef;
+ };
+
+ /*
+ * Determine if a particular item type or superType is an
+ * allowed type for a specific class.
+ */
+
+ LibFunctions.classAllowedItem = function( charCS, wname, wt, wst, allowedItemsByClass ) {
+
+ wt = wt ? wt.dbName() : '-';
+ wst = wst ? wst.dbName() : '-';
+ wname = wname ? wname.dbName() : '-';
+ allowedItemsByClass = allowedItemsByClass.dbName();
+
+ var typeDefaults = {weaps:'any',ac:'any',sps:'any',spm:'',spb:'',align:'any',race:'any'},
+ itemType = !_.isUndefined(typeDefaults[allowedItemsByClass]) ? allowedItemsByClass : 'weaps',
+ forceFalse = false,
+ reItemSpecs = { weapons: reClassSpecs.weapons,
+ armour: reClassSpecs.armour,
+ majorsphere:reClassSpecs.majorsphere,
+ minorsphere:reClassSpecs.minorsphere,
+ bannedsphere:reClassSpecs.bannedsphere,
+ alignment: reClassSpecs.alignment,
+ race: reClassSpecs.race,
+ };
+
+ var classAllowed = LibFunctions.classObjects( charCS ).some( elem => {
+ if (wt.includes('innate') || wst.includes('innate')) return true;
+
+ if (!elem.obj) return false;
+ let allowedItems = (elem.classData[itemType] || typeDefaults[itemType]).toLowerCase().replace(reIgnore,'').split('|');
+ return allowedItems.reduce((p,c) => {
+ let item = '!+'.includes(c[0]) ? c.slice(1) : c,
+ found = item.includes('any') || (wt.includes(item) || wst.includes(item) || (wt=='-' && wst=='-' && wname.includes(item)));
+ forceFalse = (forceFalse || (c[0] === '!' && found)) && !(c[0] === '+' && found);
+ return (p || found) && !forceFalse;
+ }, false);
+ }),
+ raceAllowed = true;
+
+ forceFalse = false;
+ let allowedItems = LibFunctions.resolveData( (LibFunctions.attrLookup( charCS, fields.Race ) || 'human'), fields.RaceDB, reClassRaceData, charCS, reItemSpecs, '', [], true, [] ).parsed[itemType];
+ if (!allowedItems || !allowedItems.length) {
+ allowedItems = typeDefaults[itemType];
+ }
+ allowedItems = allowedItems.dbName().split('|');
+ raceAllowed = allowedItems.reduce((p,c) => {
+ let item = '!+'.includes(c[0]) ? c.slice(1) : c,
+ found = item.includes('any') || (wt.includes(item) || wst.includes(item) || (wt=='-' && wst=='-' && wname.includes(item)));
+ forceFalse = (forceFalse || (c[0] === '!' && found)) && !(c[0] === '+' && found);
+ return (p || found) && !forceFalse;
+ }, false);
+ return (classAllowed && raceAllowed);
+ };
+
+ /*
+ * For magic items that have stored spells or powers, extract
+ * these from the MI definition and create or update the
+ * related character sheet database attribute.
+ */
+
+ LibFunctions.addMIspells = function( dbCS, dbItem ) {
+
+ var itemData = LibFunctions.resolveData( dbItem.name, fields.MagicItemDB, reNumSpellsData ).raw,
+ itemSpells = itemData ? [...('['+itemData+']').matchAll(/\[.+?\]/g)] : [],
+ spellSet = {MU:[[],[]],PR:[[],[]],PW:[[],[]],AB:[[],[]]};
+
+ _.each(itemSpells, spell => {
+ let parsedData = LibFunctions.parseData( spell[0], reSpellSpecs );
+ if (parsedData && parsedData.spell && ['MU','PR','PW','AB'].includes(parsedData.spell.toUpperCase())) {
+ let spellType = parsedData.spell.toUpperCase();
+ spellSet[spellType][0].push(parsedData.name);
+ spellSet[spellType][1].push((spellType == 'PW') ? (parsedData.perDay+'.'+parsedData.perDay) : (parsedData.level+'.0'));
+ }
+ });
+ if (spellSet.PW[0].length) {
+ LibFunctions.setAttr( dbCS, [fields.ItemPowersList[0]+dbItem.name,fields.ItemPowersList[1]], spellSet.PW[0].join() );
+ if (spellSet.PW[1].length) {
+ LibFunctions.setAttr( dbCS, [fields.ItemPowerValues[0]+dbItem.name,fields.ItemPowerValues[1]], spellSet.PW[1].join() );
+ }
+ }
+ if (spellSet.PR[0].length) {
+ LibFunctions.setAttr( dbCS, [fields.ItemPRspellsList[0]+dbItem.name,fields.ItemPRspellsList[1]], spellSet.PR[0].join() );
+ if (spellSet.PR[1].length) {
+ LibFunctions.setAttr( dbCS, [fields.ItemPRspellValues[0]+dbItem.name,fields.ItemPRspellValues[1]], spellSet.PR[1].join() );
+ }
+ }
+ if (spellSet.MU[0].length) {
+ LibFunctions.setAttr( dbCS, [fields.ItemMUspellsList[0]+dbItem.name,fields.ItemMUspellsList[1]], spellSet.MU[0].join() );
+ if (spellSet.MU[1].length) {
+ LibFunctions.setAttr( dbCS, [fields.ItemMUspellValues[0]+dbItem.name,fields.ItemMUspellValues[1]], spellSet.MU[1].join() );
+ }
+ }
+ return spellSet;
+ }
+
+ /**
+ * String together the value of the specified item type from
+ * all databases with the specified root name, separated
+ * by |. This is used to get a complete list of available
+ * magic spell or item macros across all databases of a
+ * specific type.
+ **/
+
+ LibFunctions.getMagicList = function( rootDB, mapObj, objType, senderId, defList='', other=false, otherMsg='Specify', alphabet=false ) {
+
+ objType = _.isArray(objType) ? objType.join('-').toLowerCase() : objType.toLowerCase();
+ if (!magicList[rootDB] || !magicList[rootDB][objType] || magicList[rootDB][objType].alpha != alphabet) {
+
+ var list = [],
+ alphaList = [],
+ rDB = rootDB.toLowerCase().replace(/-/g,'_'),
+ typeList;
+
+ var addItemToList = function( objIndex, objName, mapObj, objType ) {
+ var error = false;
+ try {
+ var typeList;
+ if (objIndex[0].length) {
+ let obj = getObj('ability',objIndex[0]);
+ if (!obj) return true;
+ let objDef = obj.get('action');
+ let specs = objDef.match(reSpecs);
+ specs = specs ? [...('['+specs[0]+']').matchAll(reSpecsAll)] : [];
+
+ outer_block: {
+ for (const s of specs) {
+ typeList = s[2].dbName().split('|');
+ if (typeList.includes('format') || typeList.includes('hide')) continue;
+ for (const t of typeList) {
+ if (t==='magic') continue;
+ if ((mapObj[t] && !!mapObj[t].type && objType.includes(mapObj[t].type))
+ || (!mapObj[t] && mapObj.miscellaneous && objType.includes(mapObj.miscellaneous.type))) {
+ let listName = obj.get('name').dispName();
+ let listQuery = !mapObj[t] ? '' : (!mapObj[t].query ? '' : '%%'+mapObj[t].query).replace(/%/g,'%%')
+ .replace(/\&/g,'&')
+ .replace(/\?/g,'?')
+ .replace(/{/g,'&#123;')
+ .replace(/}/g,'&#125;')
+ .replace(/\|/g,'&#124;')
+ .replace(/\,/g,'&#44;');
+ list.push(listName+(alphabet ? ',' : ',')+listName+listQuery);
+ break outer_block;
+ }
+ }
+ }
+ }
+ } else {
+ typeList = dbNames[objIndex[2]].db[objIndex[3]].type.dbName().split('|');
+ if (typeList.includes('format') || typeList.includes('hide')) return error;
+
+ for (const t of typeList) {
+ if (t === 'magic') continue;
+ if ((mapObj[t] && !!mapObj[t].type && objType.includes(mapObj[t].type))
+ || (!mapObj[t] && mapObj.miscellaneous && objType.includes(mapObj.miscellaneous.type))) {
+ let listName = dbNames[objIndex[2]].db[objIndex[3]].name.dispName();
+ let listQuery = !mapObj[t] ? '' : (!mapObj[t].query ? '' : '%%'+mapObj[t].query).replace(/%/g,'%%')
+ .replace(/\&/g,'&')
+ .replace(/\?/g,'?')
+ .replace(/{/g,'&#123;')
+ .replace(/}/g,'&#125;')
+ .replace(/\|/g,'&#124;')
+ .replace(/\,/g,'&#44;');
+ list.push(listName+(alphabet ? ',' : ',')+listName+listQuery);
+ break;
+ }
+ }
+ }
+ } catch (e) {
+ log('LibFunction getMagicList: JavaScript '+e.name+': '+e.message+' while processing object '+objName);
+ LibFunctions.sendCatchError('RPGMaster Library',(senderId ? msg_orig[senderId] : null),e);
+ error = true;
+
+ } finally {
+ return error;
+ }
+ };
+
+ if (_.isUndefined(DBindex[rDB])) {
+ for (const db of _.keys(DBindex)) {
+ if (rDB.startsWith(db)) {
+ rDB = db;
+ break;
+ }
+ }
+ }
+ _.each( DBindex[rDB], (objIndex,objName) => {
+ addItemToList( objIndex, objName, mapObj, objType );
+ });
+ if (!list.length || !list[0].length) {
+ list = defList.split('|');
+ }
+ list = _.uniq(list.filter( list => !!list ).sort(),true);
+
+ if (alphabet) {
+ if (_.isBoolean(alphabet)) {
+ for (let i=65; i<=90; i++) {
+ let subList = list.filter( n => n.toUpperCase().charCodeAt(0)==i )
+ .concat(list.filter( n => n.toUpperCase().startsWith('POTION OF ') && n.toUpperCase().charCodeAt(10)==i ),
+ list.filter( n => n.toUpperCase().startsWith('RING OF ') && n.toUpperCase().charCodeAt(8)==i ),
+ list.filter( n => n.toUpperCase().startsWith('SCROLL OF ') && n.toUpperCase().charCodeAt(10)==i ));
+ if (subList && subList.length) {
+ if (subList.length === 1) subList.push(subList[0]);
+ alphaList.push(String.fromCharCode(i)+',?{Choose from |'+(subList.join('|'))+'}');
+ }
+ }
+ } else if (_.isArray(alphabet)) {
+ for (const group of alphabet) {
+ let subList = list.filter( n => n.dbName().startsWith(group.dbName()));
+ if (subList && subList.length) {
+ if (subList.length === 1) subList.push(subList[0]);
+ alphaList.push(group+',?{Choose from |'+(subList.join('|'))+'}');
+ };
+ };
+ };
+ list = alphaList;
+ }
+ if (other) {
+ list.push('Other,?{'+otherMsg+'}');
+ }
+
+ if (!magicList[rootDB]) magicList[rootDB] = {};
+ if (!magicList[rootDB][objType]) magicList[rootDB][objType] = {};
+ magicList[rootDB][objType].list = list.join('|');
+ magicList[rootDB][objType].alpha = alphabet;
+ }
+ return magicList[rootDB][objType].list;
+ };
+
+ /**
+ * Get the displayable type of an item, derived from the
+ * item's database "Specs" field, for display in search-able
+ * containers
+ **/
+
+ LibFunctions.getShownType = function( miObj, row, miAlt ) {
+ var specs = miObj.specs(),
+ mi, miType, miAlt, data;
+ if (specs) {
+ let miClasses = specs.reduce((a,b) => a.concat([b[2]]), []).join('|');
+ let lowerMI = miClasses.toLowerCase();
+
+ mi = miClasses.split('|').find(itemClass => _.isUndefined(miTypeLists[itemClass.dbName()]) || !(['weapon','ammo','armour','armor'].includes(miTypeLists[itemClass.dbName()].type)));
+ if (!mi) {
+ mi = miClasses.split('|').find(itemClass => ['weapon','ammo','armour','armor'].includes(miTypeLists[itemClass.dbName()].type));
+ }
+ miType = miTypeLists[mi.dbName()] ? miTypeLists[mi.dbName()].type : 'miscellaneous';
+ if (!miAlt) miAlt = LibFunctions.resolveData(miObj.obj[1].name,fields.MagicItemDB,reNotAttackData,null,{itemType:reSpellSpecs.itemType},row,null,false).parsed.itemType;
+ specs = specs.find(itemSpecs => itemSpecs[2].toLowerCase().includes(mi.toLowerCase()));
+ switch (miType) {
+ case 'weapon':
+ case 'ammo':
+ mi = miAlt || ((specs || ['','','','','item'])[4]);
+ break;
+ case 'armour':
+ case 'armor':
+ mi = miAlt || ((specs || ['','mi'])[1]);
+ break;
+ case 'miscellaneous':
+ mi = miAlt || mi;
+ break;
+ default:
+ if (mi.toLowerCase() === 'magic') {
+ mi = miAlt || ((specs || ['','','','','item'])[4]);
+ }
+ break;
+ }
+ }
+ return mi.replace(/[-_]/g,' ').replace(/\|/g,'/');
+ };
+
+ /**
+ * Find an item identified as a Power, but which might actually
+ * be in a different database, as powers can be anything magical
+ **/
+
+ LibFunctions.findPower = function( charCS, power, silent=false, def=true ) {
+
+ if (!power || !power.length) return LibFunctions.abilityLookup( fields.PowersDB, '', charCS, true, false );
+
+ const dbList = [['PW-',fields.PowersDB],['MU-',fields.MU_SpellsDB],['PR-',fields.PR_SpellsDB],['MI-',fields.MagicItemDB]];
+
+ var powerType = power.substring(0,3),
+ powerLib;
+
+ if (_.some(dbList,dB=>dB[0]===powerType.toUpperCase())) power = power.slice(powerType.length);
+
+ if (!_.some(dbList, dB => {
+ if (powerType.toUpperCase() === dB[0]) {
+ powerLib = LibFunctions.abilityLookup( dB[1], power, null, true, def );
+ return true;
+ } else {
+ return false;
+ }
+ })) {
+ _.some(dbList, dB => {
+ powerLib = LibFunctions.abilityLookup( dB[1], power, null, true, false );
+ return !_.isUndefined(powerLib.obj);
+ });
+ };
+ if (!powerLib.obj) {
+ powerLib = LibFunctions.abilityLookup( fields.PowersDB, power, charCS, silent, def );
+ }
+ powerLib.name = power;
+ return powerLib;
+ }
+
+ /**
+ * Find and return total level of a character
+ **/
+
+ LibFunctions.characterLevel = function( charCS ) {
+// var level = parseInt((LibFunctions.attrLookup( charCS, fields.Total_level ) || 0),10);
+// if (!level) {
+ var level = Math.max(((parseInt((LibFunctions.attrLookup( charCS, fields.Monster_hitDice ) || 0),10)
+ + ((parseInt((LibFunctions.attrLookup( charCS, fields.Monster_hpExtra ) || 0),10) >= 3) ? 1 : 0)) || 0),
+ ((parseInt((LibFunctions.attrLookup( charCS, fields.Fighter_level ) || 0),10)
+ + parseInt((LibFunctions.attrLookup( charCS, fields.Wizard_level ) || 0),10)
+ + parseInt((LibFunctions.attrLookup( charCS, fields.Priest_level ) || 0),10)
+ + parseInt((LibFunctions.attrLookup( charCS, fields.Rogue_level ) || 0),10)
+ + parseInt((LibFunctions.attrLookup( charCS, fields.Psion_level ) || 0),10)) || 0));
+// }
+ return level;
+ }
+
+ /*
+ * Find and return the level for spell casting.
+ * MU: Wizard_level
+ * PR: Priest_level
+ * POWER or MI: all levels added
+ */
+
+ LibFunctions.caster = function( charCS, casterType ) {
+
+ var level=0, castingLevel=0, charClass, castingClass;
+
+ casterType = casterType.toUpperCase();
+
+ if (casterType == 'MI' || casterType == 'POWER' || casterType == 'PW') {
+ level = LibFunctions.characterLevel( charCS );
+ return {lv:level,cl:'',clv:level,ccl:''};
+ }
+
+ for (const casterData of casterLevels) {
+ charClass = (LibFunctions.attrLookup( charCS, casterData[0] ) || '');
+ castingClass = charClass.dbName();
+ level = LibFunctions.attrLookup(charCS,casterData[1]) || 0;
+ if (level > 0 && (_.isUndefined(spellsPerLevel[castingClass]) || _.isUndefined(spellsPerLevel[castingClass][casterType]))) {
+ if (casterType == 'MU' && casterData[0][0] == fields.Wizard_class[0]) {
+ castingClass = 'wizard';
+ } else if (casterType == 'PR' && casterData[0][0] == fields.Priest_class[0]) {
+ castingClass ='priest';
+ } else {
+ level = 0;
+ }
+ }
+ if (level > 0) break;
+ }
+ if (level>0 && castingClass) {
+ castingLevel = Math.min(Math.max((1+parseInt(level) - spellsPerLevel[castingClass][casterType][0][1]),0),spellsPerLevel[castingClass][casterType][0][2]);
+ if (castingLevel <= 0) castingLevel = -1;
+ };
+ return {lv:level,cl:charClass,clv:castingLevel,ccl:castingClass};
+ };
+
+ /* ---------------------------- Game Rule-Specific Functions -------------------------------- */
+
+ /*
+ * Set the attributes and the dependent mods for a character / NPC
+ * that has not had them previously set, using either the range/roll
+ * specifications in their class & race definitions or default ranges
+ * set by configuration.
+ */
+
+ LibFunctions.handleSetNPCAttributes = function( charCS ) {
+
+ if (state.attackMaster.attrRoll && !LibFunctions.attrLookup( charCS, fields.Strength )) {
+ let monInt = LibFunctions.attrLookup( charCS, fields.Monster_int );
+ let attrData = LibFunctions.resolveData( (LibFunctions.attrLookup( charCS, fields.Race ) || 'human'), fields.RaceDB, reRaceData, charCS, {name:reClassSpecs.name} ).attrs;
+ if (!LibFunctions.attrLookup( charCS, fields.Strength )) LibFunctions.setAttr( charCS, fields.Strength, LibFunctions.evalAttr(attrData.str || (state.attackMaster.attrRestrict ? '8:15' : '3d6')) );
+ if (!LibFunctions.attrLookup( charCS, fields.Dexterity )) LibFunctions.setAttr( charCS, fields.Dexterity, LibFunctions.evalAttr(attrData.dex || (state.attackMaster.attrRestrict ? '7:14' : '3d6')) );
+ if (!LibFunctions.attrLookup( charCS, fields.Constitution )) LibFunctions.setAttr( charCS, fields.Constitution, LibFunctions.evalAttr(attrData.con || (state.attackMaster.attrRestrict ? '7:14' : '3d6')) );
+ if (!LibFunctions.attrLookup( charCS, fields.Intelligence )) LibFunctions.setAttr( charCS, fields.Intelligence, LibFunctions.evalAttr(monInt || attrData.int || (state.attackMaster.attrRestrict ? '7:15' : '3d6')) );
+ if (!LibFunctions.attrLookup( charCS, fields.Wisdom )) LibFunctions.setAttr( charCS, fields.Wisdom, LibFunctions.evalAttr(attrData.wis || (state.attackMaster.attrRestrict ? '7:15' : '3d6')) );
+ if (!LibFunctions.attrLookup( charCS, fields.Charisma )) LibFunctions.setAttr( charCS, fields.Charisma, LibFunctions.evalAttr(attrData.chr || (state.attackMaster.attrRestrict ? '7:15' : '3d6')) );
+ };
+
+ let str = LibFunctions.attrLookup( charCS, fields.Strength ) || '';
+ let baseStr = (parseInt((str.match(/(\d+)(?:\(\d*\))?/) || ['','0'])[1]) || 0);
+ let exStr = parseInt(str.match(/d+\((\d*)\)/));
+ if (exStr === 0 || baseStr > 18) {
+ exStr = 100;
+ } else {
+ exStr = (parseInt((exStr || ['','0'])[1]) || 0);
+ }
+ let strIndex = baseStr + Math.max(0,exstrIndex.findIndex(x => x >= exStr));
+ let dexIndex = parseInt(LibFunctions.attrLookup( charCS, fields.Dexterity )) || 0;
+ let conIndex = parseInt(LibFunctions.attrLookup( charCS, fields.Constitution )) || 0;
+ let intIndex = parseInt(LibFunctions.attrLookup( charCS, fields.Intelligence )) || 0;
+ let wisIndex = parseInt(LibFunctions.attrLookup( charCS, fields.Wisdom )) || 0;
+ let chrIndex = parseInt(LibFunctions.attrLookup( charCS, fields.Charisma )) || 0;
+ let fighterFlag = LibFunctions.attrLookup( charCS, fields.Fighter_level ) || 0;
+ _.each( attrMods.str, (s,i) => {
+ if (i === 'opendoor') {
+ LibFunctions.setAttr( charCS, s.field, (String(s.data[0][strIndex]) + (s.data[1][strIndex] ? ('('+s.data[1][strIndex]+')') : '' )));
+ } else {
+ LibFunctions.setAttr( charCS, s.field, s.data[strIndex]);
+ };
+ });
+ _.each( attrMods.dex, d => {
+ LibFunctions.setAttr( charCS, d.field, d.data[dexIndex] );
+ });
+ _.each( attrMods.con, (c,i) => {
+ if (i === (fighterFlag ? 'hpadj' : 'fighthp')) return;
+ LibFunctions.setAttr( charCS, c.field, c.data[strIndex] );
+ });
+ _.each( attrMods.int, (j,i) => {
+ if (i === 'illusion') {
+ for (let lv=1; lv <= intIndex; lv++) {
+ if (j.data[lv]) LibFunctions.setAttr( charCS, [j.field[0]+j.data[lv],j.field[1]], j.data[lv]+'-level' );
+ };
+ } else {
+ LibFunctions.setAttr( charCS, j.field, j.data[intIndex] );
+ };
+ });
+ _.each( attrMods.wis, (w,i) => {
+ if (i === 'wisbonus') {
+ let count = [0,0,0,0,0,0,0,0];
+ for (let lv=1; lv<=wisIndex; lv++) {
+ count[w.data[0][lv]]++;
+ count[w.data[1][lv]]++;
+ };
+ let content = [];
+ for (let lv=1; lv<=7; lv++) {
+ if (count[lv]) content.push(String(count[lv])+'x'+numNames[lv]+'-level');
+ };
+ LibFunctions.setAttr( charCS, w.field, content.join(', ') );
+ } else {
+ LibFunctions.setAttr( charCS, w.field, w.data[wisIndex] );
+ }
+ });
+ _.each( attrMods.chr, c => {
+ LibFunctions.setAttr( charCS, c.field, c.data[intIndex] );
+ });
+ };
+
+ /*
+ * Return the base Thac0 of a character based on class & level
+ */
+
+ LibFunctions.handleGetBaseThac0 = function( charCS, type ) {
+
+ if (!type) {
+ return Math.max(1,Math.min( parseInt(LibFunctions.attrLookup( charCS, fields.MonsterThac0 )) || 20,
+ baseThac0table[0][LibFunctions.attrLookup( charCS, fields.Fighter_level ) || 0],
+ baseThac0table[1][LibFunctions.attrLookup( charCS, fields.Wizard_level ) || 0],
+ baseThac0table[2][LibFunctions.attrLookup( charCS, fields.Priest_level ) || 0],
+ baseThac0table[3][LibFunctions.attrLookup( charCS, fields.Rogue_level ) || 0],
+ baseThac0table[4][LibFunctions.attrLookup( charCS, fields.Psion_level ) || 0]
+ ));
+ } else if (!isNaN(type)) {
+ return parseInt(type);
+ } else {
+ type = type.split('=');
+ let fromType = type[0].split('|'),
+ toType = (type[1].toUpperCase() === 'WARRIOR') ? 'F' : (type[1].toUpperCase() || 'F')[0],
+ classNum = toType === 'O' ? 4
+ :(toType === 'W' ? 1
+ :(toType === 'P' ? 2
+ :(toType === 'R' ? 3
+ : 0 ))),
+ thac0 = 20,
+ field;
+
+ _.each( fromType, t => {
+ t = t.toUpperCase()
+ switch ((t.toUpperCase() === 'WARRIOR') ? 'F' : t[0].toUpperCase()) {
+ default: field = fields.Fighter_level;break;
+ case 'W': field = fields.Wizard_level; break;
+ case 'P': field = fields.Priest_level; break;
+ case 'R': field = fields.Rogue_level; break;
+ case 'O': field = fields.Psion_level; break;
+ }
+ thac0 = Math.max(1,Math.min( thac0, baseThac0table[classNum][parseInt(LibFunctions.attrLookup( charCS, field )) || 0]));
+ });
+ return thac0;
+ }
+ }
+
+ /*
+ * Return the rogue level points available to the character specified
+ */
+
+ LibFunctions.rogueLevelPoints = function( charCS, classes ) {
+
+ let rogue = _.find( classes, c => c.base === 'rogue' ),
+ startPts = 0,
+ ptsPerLevel = 0;
+ if (rogue && !isNaN(rogue.classData.roguePts) && (rogue.classData.roguePts != 0)) {
+ startPts = parseInt(rogue.classData.roguePts);
+ ptsPerLevel = (parseFloat(rogue.classData.roguePts) * 100) % 100;
+ } else if (rogue) {
+ switch (rogue.charClass) {
+ case 'bard':
+ startPts = 20;
+ ptsPerLevel = 15;
+ break;
+ case 'assassin':
+ startPts = 40;
+ ptsPerLevel = 20;
+ break;
+ default:
+ startPts = 60;
+ ptsPerLevel = 30;
+ break;
+ };
+ } else {
+ startPts = 0;
+ ptsPerLevel = 0;
+ };
+ LibFunctions.setAttr( charCS, fields.RogueLevelPts, ((startPts-ptsPerLevel)+'+('+ptsPerLevel+'*@{'+fields.Rogue_level[0]+')}'));
+ return (rogue ? ((startPts - ptsPerLevel) + (ptsPerLevel * rogue.level)) : 0);
+ };
+
+ /**
+ * Handle automatically checking the thief skill modifiers for
+ * current race, dexterity & armour.
+ **/
+
+ LibFunctions.handleCheckThiefMods = function( args, senderId, silent = false ) {
+
+ var tokenID = args[0],
+ silent = silent || String(args[1] || '').dbName() === 'silent',
+ charCS = LibFunctions.getCharacter( tokenID ),
+ skillFlags = {pp:false,ol:false,rt:false,ms:false,hs:false,dn:false,cw:false,rl:false,ib:false},
+ blankSkills = {pp:0,ol:0,rt:0,ms:0,hs:0,dn:0,cw:0,rl:0,ib:0,rules:[]},
+ disguiseACData = {pp:-70,ol:-40,rt:-40,ms:-60,hs:-60,dn:-40,cw:-80,rl:0,ib:0},
+ skillMods = {},
+ mods = {},
+ skillText = '',
+ itemText = {},
+ startPts = 0,
+ ptsPerLevel = 0;
+
+ if (!charCS) {
+ // Handle internal error
+ return;
+ };
+
+ var raceData = LibFunctions.resolveData( LibFunctions.attrLookup( charCS, fields.Race ), fields.RaceDB, reRaceData, charCS, reThiefSpecs ).parsed || {},
+ classes = LibFunctions.classObjects( charCS, senderId ),
+ dexData = rogueDexMods[Math.max(0,( LibFunctions.attrLookup( charCS, fields.Dexterity ) - rogueDexMods[0].lv ))] || {},
+ rogue = _.find( classes, c => c.base === 'rogue' ),
+ armourData = LibFunctions.resolveData( LibFunctions.attrLookup( charCS, fields.Armor_trueName ), fields.MagicItemDB, reACData, charCS, reThiefSpecs ).parsed || disguiseACData,
+ maxTotal = 95,
+ minTotal = state.attackMaster.thieveCrit;
+
+ [mods,skillMods,skillFlags,itemText] = LibFunctions.scanItemMods( charCS, {}, {}, blankSkills, skillFlags, reThiefSpecs, {}, silent, false );
+
+ _.each(skillMods, function(s,c) {
+ mods = _.mapObject(mods, function(v,k) {
+ return (_.isUndefined(s[k]) ? v : (skillFlags[k] ? s[k] : (v + s[k])));
+ });
+ });
+ _.each(rogueSkills, skill => {
+ let base = -100;
+ classes.forEach( c => base = Math.max( base, LibFunctions.evalAttr(c.classData[skill.tag] || 0) ));
+ LibFunctions.setAttr( charCS, [skill.factors[0], 'current'], base );
+ LibFunctions.setAttr( charCS, [skill.factors[1], 'current'], LibFunctions.evalAttr(raceData[skill.tag] || 0) );
+ LibFunctions.setAttr( charCS, [skill.factors[2], 'current'], LibFunctions.evalAttr(dexData[skill.tag] || 0) );
+ LibFunctions.setAttr( charCS, [skill.factors[4], 'current'], LibFunctions.evalAttr(armourData[skill.tag] || 0) );
+ LibFunctions.setAttr( charCS, [skill.factors[5], 'current'], (mods[skill.tag] || 0) );
+
+ LibFunctions.setAttr( charCS, skill.save, Math.min(maxTotal,Math.max(minTotal,
+ (base + parseInt(raceData[skill.tag] || 0)
+ + parseInt(dexData[skill.tag] || 0)
+ + parseInt(armourData[skill.tag] || 0)
+ + parseInt(mods[skill.tag] || 0)
+ + parseInt(LibFunctions.attrLookup(charCS,[skill.factors[3], 'current']) || 0)
+ + parseInt(LibFunctions.attrLookup(charCS,[skill.factors[6], 'current']) || 0))
+ )));
+ });
+
+ LibFunctions.rogueLevelPoints( charCS, classes );
+ skillText += _.reduce(itemText, (t,i) => (t + i));
+ return skillText;
+ };
+ /*
+ * Parse the Class Databases to update internal rule tables with
+ * any changes held for specific Class definitions
+ */
+
+ LibFunctions.parseClassDB = function(forceUpdate=false) {
+
+ var doParse = function( rootDB, saveMods ) {
+ let isClass = rootDB === fields.ClassDB,
+ indexDB = rootDB.toLowerCase().replace(/-/g,'_');
+ if (!DBindex[indexDB]) return;
+ for (const ClassName in DBindex[indexDB]) {
+ let def = LibFunctions.abilityLookup(rootDB, ClassName),
+ type = !def.obj ? '' : def.obj[1].type,
+ classSpecs = def.specs(reSpecs) || [['','','','','']],
+ isCreature = type.toLowerCase().includes('creature') || (classSpecs && classSpecs[0] && classSpecs[0][4] && String(classSpecs[0][4]).toLowerCase().includes('creature')),
+ dataObj = LibFunctions.resolveData( ClassName, rootDB, /}}\s*?(?:Class|Race)Data\s*?=(.*?){{/im, null, null, '', [], false );
+
+ if (isClass) {
+ let classType;
+ if (classSpecs && !_.isNull(classSpecs)) {
+ if (classSpecs.some( s => {
+ if (s && s.length >= 5) {
+ classType = String(s[1]||'').dbName();
+ return (((s[4]||'').dbName() == 'wizard' ) && !ordMU.includes(classType) && (dataObj.parsed.specmu == 1));
+ }
+ return false;
+ })) {
+ if (!specMU.includes(classType)) specMU.push(classType);
+ } else {
+ if (!ordMU.includes(classType)) ordMU.push(classType);
+ };
+ };
+ }
+
+ if (dataObj.raw) {
+ for (let r=0; r {
+ pen = pen.toLowerCase().split('=');
+ pen[0] = pen[0].dbName();
+ pen[1] = parseInt(pen[1]) || 0;
+ return pen;
+ });
+ }
+ let rowArray = rowData.toLowerCase().replace(/\[/g,'').replace(/\]/g,'').split(','),
+ svlArray = rowArray.filter(elem => elem.startsWith('svl'));
+
+ if (svlArray && svlArray.length) {
+ svlArray.sort((a,b)=>{parseInt((a.match(/svl(\d+):/)||[0,0])[1])-parseInt((b.match(/svl(\d+):/)||[0,0])[1]);});
+ saveLevels[name] = [];
+ baseSaves[name] = [];
+ let oldLevel = 0,
+ baseIndex = 0;
+ svlArray.forEach(svl => {
+ let sv = svl.match(/svl(\d+):([\d\|]+)/),
+ level = parseInt(sv[1] || 0),
+ saves = (sv[2] || '20|20|20|20|20').split('|');
+ saveLevels[name].length = level+1;
+ saveLevels[name].fill(baseIndex,oldLevel,level+1);
+ if (baseIndex == 0 && level != 0) {
+ baseSaves[name].push([16,18,17,20,19]);
+ baseIndex++;
+ }
+ saves.length = 5;
+ baseSaves[name].push(saves);
+ baseIndex++
+ oldLevel = level+1;
+ });
+ };
+ svlArray = rowArray.filter(elem => {return ((/^\s*sv[a-z0-9]{3}:/.test(elem)) && !(/^\s*svl\d\d:/.test(elem)));});
+ if (svlArray && svlArray.length) {
+ saveMods[name] = {att:'con',par:0.0,poi:0.0,dea:0.0,rod:0.0,sta:0.0,wan:0.0,pet:0.0,pol:0.0,bre:0.0,spe:0.0,str:0.0,con:0.0,dex:0.0,int:0.0,wis:0.0,chr:0.0};
+ svlArray.forEach(svm => {
+ let sv = svm.match(/sv([a-z0-9]{3}):([+-]?\d+\.?\d*|\w{3})(L\d+)?/i);
+ if (sv[1] == 'all') {
+ saveMods[name] = _.mapObject(saveMods[name], (v,k) => {return k != 'att' ? v + (parseFloat(sv[2] || 0) || 0) : v;});
+ } else if (['sav','atr','chk'].includes(sv[1])) {
+ let saves = sv[1] === 'sav' ? saveFormat.Saves : (sv[1] === 'atr' ? saveFormat.Attributes : saveFormat.Checks);
+ _.each(saves, s => saveMods[name][s.tag] = saveMods[name][s.tag] + (parseFloat(sv[2] || 0) || 0));
+ } else {
+ let plv = parseInt(sv[3]) || 1;
+ saveMods[name][sv[1]] = (sv[1] != 'att') ? (String(parseFloat(sv[2] || 0) || 0)+(sv[3] || '')) : (sv[2] || 'con').dbName();
+ };
+ });
+ };
+ svlArray = rowArray.filter(elem => {return /^\s*sv[a-z0-9]{3}\+:/.test(elem);});
+ if (svlArray && svlArray.length) {
+ classSaveMods[name] = {att:'con',par:0.0,poi:0.0,dea:0.0,rod:0.0,sta:0.0,wan:0.0,pet:0.0,pol:0.0,bre:0.0,spe:0.0,str:0.0,con:0.0,dex:0.0,int:0.0,wis:0.0,chr:0.0};
+ svlArray.forEach(svm => {
+ let sv = svm.match(/sv([a-z0-9]{3})\+:([+-]?\d+\.?\d*|\w{3})/);
+ if (_.isUndefined(sv)) return;
+ if (sv[1] == 'all') {
+ classSaveMods[name] = _.mapObject(classSaveMods[name], (v,k) => {return k != 'att' ? v + (parseFloat(sv[2] || 0) || 0) : v;});
+ } else if (['sav','atr','chk'].includes(sv[1])) {
+ let saves = sv[1] === 'sav' ? saveFormat.Saves : (sv[1] === 'atr' ? saveFormat.Attributes : saveFormat.Checks);
+ _.each(saves, s => classSaveMods[name][s.tag] = classSaveMods[name][s.tag] + (parseFloat(sv[2] || 0) || 0));
+ } else {
+ classSaveMods[name][sv[1]] = (sv[1] != 'att') ? (parseFloat(sv[2] || 0) || 0) : (sv[2] || 'con').dbName();
+ }
+ });
+ };
+ };
+ };
+ if (isCreature) {
+ if (!clTypeLists[classSpecs[0][2].toLowerCase()]) clTypeLists[classSpecs[0][2].toLowerCase()] = {type:'creature',field:fields.RaceCreatureList,query:''};
+ if (dataObj.parsed.query && dataObj.parsed.query.length) {
+ let storedQuestions = {NPClevel:['What level NPC?','0%0%0%Not-Prof','1%1%1%Specialist','2%1%1%Specialist','3%1%1%Specialist','4%2%1%Specialist','5%2%1%Specialist','6%2%1%Specialist','7%2%1%Mastery','8%3%2%Mastery','9%3%2%Mastery','10%3%2%Mastery','11%3%2%Mastery','12%4%2%Mastery','13%4%2%Mastery','14%4%3%Mastery','15%4%3%Mastery','16%5%3%Mastery','17%5%3%Mastery','18%5%3%Mastery','19%5%3%Mastery','20%5%3%Mastery']};
+ let query = LibFunctions.parseStr(dataObj.parsed.query).split('|');
+ if (query.length === 1 && !!storedQuestions[query[0]]) query = storedQuestions[query[0]];
+ let question = query.shift();
+ clTypeLists[classSpecs[0][2].toLowerCase()].query = '?{'+question + '|'
+ + query.map( q => {
+ let sq = q.split('%');
+ return sq[0]+','+sq.join('%%');
+ }).join('|')
+ + '}';
+ };
+ };
+ };
+ return;
+ };
+ if (classesParsed && !forceUpdate) return;
+ doParse( fields.ClassDB, classSaveMods );
+ doParse( fields.RaceDB, raceSaveMods );
+ classesParsed = true;
+ LibFunctions.sendFeedback( waitMsgDiv+'RPGMaster is now ready.' );
+
+ return;
+ };
+
+ /*
+ * Scan a particular item definition for effects on saves, or thieving
+ * skills, or any other set of tags passed in.
+ */
+
+ LibFunctions.scanForMods = function( charCS, item, trueItem, classArray, specsArray, dataArray, mods, itemMods, blanks, setFlags, reValues, addedText, silent=false ) { // mods
+ let modsClass = classArray[((classArray.length === 1 || classArray[0] !== 'magic') ? 0 : 1)],
+ dispItem = item.dispName(),
+ types = {par:'sav',poi:'sav',dea:'sav',rod:'sav',sta:'sav',wan:'sav',pet:'sav',pol:'sav',bre:'sav',spe:'sav',str:'atr',con:'atr',dex:'atr',int:'atr',wis:'atr',chr:'atr',pp:'th',ol:'th',rt:'th',ms:'th',hs:'th',dn:'th',cw:'th',rl:'th',ib:'th'},
+ reRules = /[,\[\s]rules:(.*?)[,\s\]]/i,
+ saveMods = [];
+// if (modsClass.toLowerCase() === 'magic') log('scanForItems: item '+item+' is magic');
+
+ _.each( dataArray, data => {
+ if (!data) return;
+ if (!itemMods[modsClass] || _.size(itemMods[modsClass]) < _.size(blanks)) itemMods[modsClass] = JSON.parse(JSON.stringify(blanks));
+
+ let svRules = (data[0].match(reRules) || ['',''])[1].toLowerCase().replace(/[_\s]/g,'').split('|').map(r => r.replace(/\-/g,(match,i,s)=>(i>0?'':match))),
+ inHand = !svRules.includes('+inhand') || !_.isUndefined(LibFunctions.getTableField( charCS, {}, fields.InHand_table, fields.InHand_trueName ).tableFind( fields.InHand_trueName, trueItem )),
+ worn = !svRules.includes('+worn') || LibFunctions.classAllowedItem( charCS, trueItem, specsArray[0][1].dbName(), specsArray[0][4].dbName(), 'ac' ),
+ conflict = '',
+ testRules = [];
+ let adds = !(_.some(itemMods,(mi,c) => {conflict=c;return (svRules.includes( '-'+c ) || _.some(classArray,mic => {return (mi.rules || []).includes('-'+mic);}))}));
+ if (_.isUndefined(addedText[modsClass]) && (!inHand || !worn || !adds)) addedText[modsClass] = '';
+ if (!inHand && !silent) addedText[modsClass] += '{{'+dispItem+'=Is not currently in hand}}';
+ if (!worn && !silent) addedText[modsClass] += '{{'+dispItem+'=Is not of a usable type}}';
+ if (!adds && !silent) addedText[modsClass] += '{{'+dispItem+'=Does not combine with items of class '+conflict+'}}';
+ if (!inHand || !worn || !adds) return;
+ _.each(_.pick(LibFunctions.parseData( data[0], reValues, false, charCS ), (val,key) => !_.isUndefined(val)), (val,key) => saveMods.push( [(key+':'+val),key,val] ));
+
+ if (!silent && !!saveMods && saveMods.length) {
+ if (!addedText[modsClass]) addedText[modsClass]='';
+ addedText[modsClass] += '{{'+dispItem+'=';
+ };
+
+ _.each( saveMods, m => {
+
+ let n = LibFunctions.evalAttr('-+='.includes(m[2][0]) ? m[2].substring(1) : m[2]);
+ m[2] = ('-+='.includes(m[2][0]) ? m[2][0] : '') + String(n);
+ let saveSpec;
+ let every = m[1] === 'all' || m[1] === 'th',
+ attr = m[1] === 'atr',
+ save = m[1] === 'sav';
+ if (save || every || attr) {
+ let msg = 'All '+(every ? '' : (attr ? 'attribute ' : 'save '))+'mods: '+m[2];
+ if (!silent) addedText[modsClass] += msg;
+ let tempObj = itemMods[modsClass];
+ itemMods[modsClass] = _.mapObject(tempObj, function(v,k) {
+ if (k != 'att' && k != 'rules' && !_.isUndefined(blanks[k])) {
+ if (every || (!!types[k] && types[k] == m[1])) {
+ if ('+-'.includes(m[2][0]) && !setFlags[k]) {
+ return (v+(parseInt(m[2]) || 0));
+ } else if (m[2][0] == '=') {
+ let newVal = parseInt(m[2].substring(1)) || 0;
+ if (setFlags[k]) {
+ return Math.max(v,newVal);
+ } else {
+ setFlags[k] = true;
+ return newVal;
+ }
+ } else if (!setFlags[k]) {
+ return Math.max(v,(parseInt(m[2]) || 0));
+ } else {
+ return v;
+ }
+ } else {
+ return v;
+ }
+ } else {
+ return v;
+ }
+ log('scanForMods: checked everything, should not get here');
+ return v;
+ });
+ } else {
+ if (_.isUndefined(mods[m[1]])) mods[m[1]] = 0;
+ if (_.isUndefined(itemMods[modsClass][m[1]])) itemMods[modsClass][m[1]] = 0;
+ if (_.isUndefined(setFlags[m[1]])) setFlags[m[1]] = false;
+ let v = itemMods[modsClass][m[1]] || 0;
+ if (m[1] != 'att') {
+ if (!silent) addedText[modsClass] += (xlateSave[m[1]] || trueItem)+': '+m[2]+', ';
+ if ('+-'.includes(m[2][0]) && !setFlags[m[1]]) {
+ itemMods[modsClass][m[1]] += (parseInt(m[2]) || 0);
+ } else if (m[2][0] === '=') {
+ let newVal = parseInt(m[2].substring(1)) || 0;
+ if (setFlags[m[1]]) {
+ itemMods[modsClass][m[1]] = Math.max(v,newVal);
+ } else {
+ setFlags[m[1]] = true;
+ itemMods[modsClass][m[1]] = newVal;
+ }
+ } else if (!setFlags[m[1]]) {
+ itemMods[modsClass][m[1]] = Math.max(v,(parseInt(m[2]) || 0));
+ }
+ } else {
+ itemMods[modsClass].att = v;
+ }
+ };
+ });
+ if (!silent && !!saveMods && saveMods.length) addedText[modsClass] += '}}';
+ itemMods[modsClass].rules = itemMods[modsClass].rules ? itemMods[modsClass].rules.concat(svRules) : svRules;
+ });
+ return [mods,itemMods,setFlags,addedText];
+ };
+
+ /*
+ * Scan all items in the character's possession for effects
+ * on saving throws or thieving skills (or any other list of
+ * possible mods passed in).
+ */
+
+ LibFunctions.scanItemMods = function( charCS, mods, itemMods, blanks, itemFlags, reValues, itemText, silent=false, scanArmour=true ) {
+ let ItemNames = LibFunctions.getTableField( charCS, {}, fields.Items_table, fields.Items_name ),
+ currentArmour = LibFunctions.attrLookup( charCS, fields.Armor_trueName ) || 'No Armor',
+ item;
+ for (let itemRow = ItemNames.table[1]; !_.isUndefined(item = ItemNames.tableLookup( fields.Items_name, itemRow, false )); itemRow++) {
+// if (item == 'Ring-of-Protection+2') log('scanItemMods: item = '+item);
+ if (item && item.length && item != '-') {
+ let trueItem = ItemNames.tableLookup( fields.Items_trueName, itemRow );
+ let itemObj = LibFunctions.abilityLookup( fields.MagicItemDB, trueItem, charCS );
+ if (itemObj.obj) {
+ let specsArray = itemObj.specs(/}}\s*specs=\s*?(.*?)\s*?{{/im),
+ miClass = specsArray ? (specsArray[0][2].dbName() || 'magicitem') : 'magicitem';
+
+ if ((miClass.includes('armour') || miClass.includes('armor')) && (!scanArmour || trueItem !== currentArmour)) continue;
+ if (miClass.includes('ring') && miClass.includes('protection')) {
+ let leftRing = LibFunctions.attrLookup( charCS, fields.Equip_leftTrueRing ) || '-',
+ rightRing = LibFunctions.attrLookup( charCS, fields.Equip_rightTrueRing ) || '-';
+ if (![leftRing,rightRing].includes(trueItem)) {
+ if (!silent) itemText.Not_Worn += '{{'+item+'=Is not currently worn}}';
+ continue;
+ }
+ }
+ let itemData = LibFunctions.resolveData( trueItem, fields.MagicItemDB, reNotAttackData, charCS, reValues );
+// if (item == 'Ring-of-Protection+2') log('scanItemMods: about to scan '+item+' for mods. Data is '+_.pairs(itemData.parsed));
+ [mods,itemMods,itemFlags,itemText] = LibFunctions.scanForMods( charCS, item, trueItem, miClass.dbName().split('|'), specsArray, itemData.raw, mods, itemMods, blanks, itemFlags, reValues, itemText );
+// if (item == 'Ring-of-Protection+2') log('scanItemMods: scanned and revealed '+_.pairs(itemText));
+ };
+ };
+ };
+ return [mods,itemMods,itemFlags,itemText];
+ };
+
+ /*
+ * Scan Race, Class, Level and MI data to set the saving throws table
+ * for a particular Token
+ */
+
+ LibFunctions.handleCheckSaves = function( args, senderId, selected, silent=false ) {
+
+ const blankMods = {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,rules:[]},
+ reSave = /[,\[\s]sv([a-z0-9]{3}):([-\+\*\/\=\^vfc\d\.;\(\)]+)[,\s\]]/g;
+
+ var tokenID,
+ charCS,
+ attkMenu,
+ msg = '';
+
+ var checkThisSave = function(attkMenu,curToken,senderId,silent,selected) {
+
+ return new Promise(resolve => {
+ try {
+ tokenID = attkMenu ? curToken.id : curToken._id;
+ charCS = LibFunctions.getCharacter( tokenID, true );
+
+ if (!charCS) {
+ log('checkThisSave: invalid charCS');
+ return;
+ }
+
+ var tokenName = getObj('graphic',tokenID).get('name'),
+ classes = LibFunctions.classObjects( charCS ),
+ race = (LibFunctions.attrLookup( charCS, fields.Race ) || 'human').dbName(),
+ ItemNames = LibFunctions.getTableField( charCS, {}, fields.Items_table, fields.Items_name ),
+ saves = [],
+ classSaves, classMods,
+ SaveMods = LibFunctions.getTable( charCS, fieldGroups.MODS ),
+ mods = _.isUndefined(raceSaveMods[race]) ? (_.find(raceSaveMods, (m,k) => race.includes(k)) || raceSaveMods.human) : raceSaveMods[race],
+ raceBonus = _.isUndefined(classSaveMods[race]) ? (_.find(classSaveMods, (m,k) => race.includes(k)) || _.create(blankMods)) : classSaveMods[race],
+ setFlags = {att:false,par:false,poi:false,dea:false,rod:false,sta:false,wan:false,pet:false,pol:false,bre:false,spe:false,str:false,con:false,dex:false,int:false,wis:false,chr:false},
+ miMods = {},
+ skillMods = {},
+ modName,
+ attribute, attrVal, item,
+ addedText = {},
+ itemText = '',
+ content = silent ? '' : '&{template:'+fields.defaultTemplate+'}';
+
+ content += silent ? '' : '{{name='+tokenName+'\'s Saving Throws}}';
+
+ classes.forEach( c => {
+ if (!saveLevels[c.name]) {
+ classSaves = baseSaves[c.base][saveLevels[c.base][Math.min(c.level,saveLevels[c.base].length-1)]];
+ } else {
+ classSaves = baseSaves[c.name][saveLevels[c.name][Math.min(c.level,saveLevels[c.name].length-1)]];
+ }
+ if (!saves || !saves.length) {
+ saves = classSaves;
+ } else {
+ saves = saves.map((v,k)=> Math.min(v,classSaves[k]));
+ }
+ if (!silent) itemText += '{{'+c.obj[1].name+'=Level '+c.level+'='+classSaves+'}}';
+ });
+
+ switch (mods.att.toLowerCase()) {
+ case 'str':
+ attribute = fields.Strength;
+ break;
+ case 'dex':
+ attribute = fields.Dexterity;
+ break;
+ case 'con':
+ attribute = fields.Constitution;
+ break;
+ case 'int':
+ attribute = fields.Intelligence;
+ break;
+ case 'wis':
+ attribute = fields.Wisdom;
+ break;
+ case 'chr':
+ attribute = fields.Charisma;
+ break;
+ default:
+ attribute = undefined;
+ };
+ if (attribute) {
+ attrVal = parseInt(LibFunctions.attrLookup( charCS, attribute )) || -1;
+ } else {
+ attrVal = -1;
+ }
+ let dispBonus = (!silent && (_.some(mods,(m,k)=>!!m && k!='att') || _.some(raceBonus,(m,k)=>!!m && k!='att')));
+ if (dispBonus) itemText += '{{'+LibFunctions.attrLookup( charCS, fields.Race )+'=';
+ mods = _.mapObject(mods,(v,k) => {
+ if (k == 'att') {
+ return v;
+ } else {
+ let saveVal = Math.floor(v != 0 ? (attrVal != -1 ? (attrVal/v) : v) : 0)+raceBonus[k];
+ if (!silent && saveVal != 0) itemText += xlateSave[k]+':'+(saveVal >= 0 ? '+' : '')+saveVal+', ';
+ return saveVal;
+ }
+ });
+ if (dispBonus) itemText += '}}';
+
+ let dexBonus = 0-(parseInt(LibFunctions.attrLookup( charCS, fields.Dex_acBonus )) || 0);
+ if (dexBonus) {
+ mods.dex += dexBonus;
+ itemText += '{{Dexterity of '+LibFunctions.attrLookup( charCS, fields.Dexterity )+'='+(dexBonus > 0 ? 'Bonus' : 'Penalty')+' of '+dexBonus+'}}';
+ }
+
+ classes.forEach( c => {
+ if (c.name === race) c.name = c.base;
+ classMods = classSaveMods[c.name] || classSaveMods[c.base] || classSaveMods.undefined;
+ classMods = _.mapObject(classMods,v=>{
+ let plv = String(v || '').match(/([-\+]?\d+)L(\d+)/i);
+ if (plv && plv[2] != 0) v = plv[1] * Math.ceil(c.level/plv[2]);
+ return parseInt(v);
+ });
+ if (!mods && !mods.length) {
+ mods = classMods;
+ } else {
+ mods = _.mapObject(mods,(v,k)=>{return k != 'att' ? v+classMods[k] : v;});
+ }
+ if (classMods.att) classMods.att = classMods.par;
+ if (!silent && _.some(classMods)) {
+ itemText += '{{'+c.name+' Mods=';
+ let vals = _.chain(classMods).values().uniq().value();
+ if (vals.length == 1) {
+ itemText += 'All mods:'+vals[0];
+ } else {
+ _.mapObject(classMods,(v,k)=> ((k!='att' && v) ? (itemText += xlateSave[k]+':'+v+' ') : ''));
+ }
+ itemText += '}}';
+ }
+ });
+ [mods,miMods,setFlags,addedText] = LibFunctions.scanItemMods( charCS, mods, miMods, blankMods, setFlags, reSaveSpecs, addedText, silent );
+
+ for (let modRow = SaveMods.table[1]; !_.isUndefined(modName = SaveMods.tableLookup( fields.Mods_name, modRow, false )); modRow++) {
+ let saveToken = SaveMods.tableLookup( fields.Mods_tokenID, modRow);
+ let modType = SaveMods.tableLookup( fields.Mods_modType, modRow);
+ if (modName === '-' || (saveToken.length && saveToken !== tokenID) || (modType.length && modType !== 'save')) continue;
+ let curRound = parseInt(SaveMods.tableLookup(fields.Mods_curRound,modRow)) || 0,
+ toRound = parseInt(SaveMods.tableLookup(fields.Mods_round,modRow)) || 0,
+ diff = state.initMaster.round - curRound;
+ if (diff < 0 && !isNaN(toRound) && toRound !== 0) toRound += diff;
+ curRound += diff;
+ let saveCount = SaveMods.tableLookup(fields.Mods_modCount,modRow);
+ if ((saveCount !== '' && saveCount <= 0) || (!isNaN(toRound) && toRound > 0 && toRound < state.initMaster.round)) {
+ SaveMods.addTableRow(modRow);
+ continue;
+ } else if (diff !== 0 && !isNaN(toRound) && toRound > 0) {
+ SaveMods.tableSet(fields.Mods_curRound,modRow,curRound);
+ SaveMods.tableSet(fields.Mods_round,modRow,toRound);
+ };
+ let spellName = SaveMods.tableLookup(fields.Mods_spellName,modRow);
+ let itemName = spellName;
+ if (spellName.trueCompare(modName)) itemName += ':' + modName;
+ [mods,miMods,setFlags,addedText] = LibFunctions.scanForMods( charCS, spellName, modName, spellName.toLowerCase().split('|'), [['','','','','']], [['['+SaveMods.tableLookup(fields.Mods_saveSpec,modRow)+']']], mods, miMods, blankMods, setFlags, reSaveSpecs, addedText, silent );
+ };
+
+ _.each(miMods, function(s,c) {
+ mods = _.mapObject(mods, function(v,k) {
+ return (_.isUndefined(s[k]) ? v : (setFlags[k] ? s[k] : (v + s[k])));
+ });
+ });
+
+ _.each( saveFormat.Saves, (s,k) => {
+ LibFunctions.setAttr( charCS, s.mon, saves[s.index] );
+ LibFunctions.setAttr( charCS, s.save, saves[s.index] );
+ LibFunctions.setAttr( charCS, s.mod, mods[s.tag] );
+ });
+
+ for (let modRow = SaveMods.table[1]; !_.isUndefined(modName = SaveMods.tableLookup( fields.Mods_name, modRow, false )); modRow++) {
+ let saveToken = SaveMods.tableLookup( fields.Mods_tokenID, modRow);
+ let modType = SaveMods.tableLookup( fields.Mods_modType, modRow);
+ if (modName === '-' || (saveToken.length && saveToken !== tokenID) || (modType.length && modType !== 'save')) continue;
+ let tag = SaveMods.tableLookup( fields.Mods_tag, modRow ),
+ basis = SaveMods.tableLookup( fields.Mods_basis, modRow, false ),
+ i = SaveMods.tableLookup( fields.Mods_index, modRow );
+ if (_.isUndefined(mods[tag])) {
+ mods[tag] = 0;
+ setFlags[tag] = false;
+ }
+ if (!_.isUndefined(basis) && !setFlags[tag]) mods[tag] += mods[basis];
+ LibFunctions.setAttr( charCS, [SaveMods.tableLookup( fields.Mods_saveField, modRow ),'current'], saves[i] );
+ LibFunctions.setAttr( charCS, [SaveMods.tableLookup( fields.Mods_modField, modRow ),'current'], mods[tag] );
+ };
+
+ if (!silent) {
+ itemText += _.reduce(addedText, (t,i) => (t + i));
+ content +='{{Saves=';
+ let i = -1,
+ a = [];
+ _.each( saveFormat.Saves, (s,k) => {
+ if (s.index != i) {
+ content += a.join(', ');
+ a = [];
+ content += (i>0?'':'')+'**'+saves[(i=s.index)]+'** | ';
+ }
+ a.push(k+'('+(mods[s.tag]>=0?'+':'')+mods[s.tag]+')');
+ });
+ content += a.join(', ')+' | ';
+ for (let modRow = SaveMods.table[1]; !_.isUndefined(modName = SaveMods.tableLookup( fields.Mods_name, modRow, false )); modRow++) {
+ let saveToken = SaveMods.tableLookup( fields.Mods_tokenID, modRow);
+ let modType = SaveMods.tableLookup( fields.Mods_modType, modRow);
+ if (modName === '-' || (saveToken.length && saveToken !== tokenID) || (modType.length && modType !== 'save')) continue;
+ let tag = SaveMods.tableLookup( fields.Mods_tag, modRow );
+ if (!tag || !tag.length) continue;
+ let index = SaveMods.tableLookup( fields.Mods_index, modRow );
+ content += '**'+saves[index]+'** | '+modName+'('+(mods[tag]>=0?'+':'')+mods[tag]+') | ';
+ };
+
+ content += ' }}';
+ content += '{{Attribute Checks=';
+ _.each( saveFormat.Attributes, (a,k) => content += '**'+LibFunctions.attrLookup(charCS,a.save)+'** | '+k+'('+(mods[a.tag]>=0?'+':'')+mods[a.tag]+') | ');
+ content +=' }}'
+ + ((selected.length == 1) ? itemText : '');
+ };
+// log('checkThisSave: finished checking '+charCS.get('name'));
+// return;
+ } catch (e) {
+ sendCatchError('RPGM Library',null,e,'RPGM Library handleCheckSaves()');
+ content = '';
+ } finally {
+ setTimeout(() => {
+ resolve(content);
+ }, 1000);
+ };
+ });
+ };
+
+ async function checkAllSaves( args, selected, senderId, silent ) {
+ try {
+ var who = LibFunctions.sendToWho(null,senderId);
+
+ if (attkMenu = (args && args[0])) {
+ selected = [];
+ selected.push(getObj('graphic',args[0]));
+ }
+ let nomenu = args && ((args[2] || '') === 'nomenu');
+
+ for (const token of selected) {
+ if (msg && msg.length) msg += '\n'+who;
+// if (!token || (token.get('name').dbName() === 'thorg')) continue;
+ msg += await checkThisSave( attkMenu, token, senderId, silent, selected );
+ };
+// log('checkAllSaves: finished checking all selected tokens');
+
+ if (!silent && !nomenu && (attkMenu || (args && args[1]))) {
+ if (!msg) msg = '&{template:'+fields.defaultTemplate+'}';
+ msg += '{{desc=[Return to Menu]('+(attkMenu ? ('!attk --button '+(args[1] || 'SAVES')+'|'+args[0]) : ('!cmd --button '+args[1]))+')}}';
+ }
+ if (!silent) {
+ LibFunctions.sendResponse( charCS, msg, senderId );
+ } else {
+ clearWaitTimer(senderId);
+ }
+ return;
+ } catch (e) {
+ sendCatchError( 'RPGM Library', msg_orig[senderId], e);
+ }
+ };
+
+ checkAllSaves( args, selected, senderId, silent );
+ return;
+ }
+
+ /*
+ * Reload all weapons in the InHand tables, to set correct
+ * data after a race, class or level change. Will not work
+ * for weapons entered manually into the weapon tables
+ */
+
+ LibFunctions.handleCheckWeapons = function( tokenID, charCS ) {
+
+ var InHand = LibFunctions.getTable( charCS, fieldGroups.INHAND ),
+ itemIndex = InHand.tableLookup( fields.InHand_index, 0 );
+ if (itemIndex.length && !isNaN(itemIndex)) {
+ LibFunctions.sendAPI('!attk --button PRIMARY|'+tokenID+'|'+itemIndex+'|0||silent');
+ }
+ itemIndex = InHand.tableLookup( fields.InHand_index, 1 );
+ if (itemIndex.length && !isNaN(itemIndex)) {
+ LibFunctions.sendAPI('!attk --button OFFHAND|'+tokenID+'|'+itemIndex+'|1||silent');
+ }
+ itemIndex = InHand.tableLookup( fields.InHand_index, 2 );
+ if (itemIndex.length && !isNaN(itemIndex)) {
+ LibFunctions.sendAPI('!attk --button BOTH|'+tokenID+'|'+itemIndex+'|2||silent');
+ }
+
+ for (let r=3; !_.isUndefined(itemIndex = InHand.tableLookup( fields.InHand_index, r, false )); r++) {
+ if (itemIndex.length && !isNaN(itemIndex)) {
+ LibFunctions.sendAPI('!attk --button HAND|'+tokenID+'|'+itemIndex+'|'+r+'||silent');
+ }
+ }
+ return;
+ }
+
+/* ------------------------------------------------------------ Configuration ------------------------------------------------ */
+
+ /**
+ * Get the configuration for the player who's ID is passed in
+ * or, if the config is passed back in, set it in the state variable
+ **/
+
+ LibFunctions.getSetPlayerConfig = function( playerID, configObj ) {
+
+ if (!state.MagicMaster.playerConfig[playerID]) {
+ state.MagicMaster.playerConfig[playerID]={};
+ }
+ if (!_.isUndefined(configObj)) {
+ state.MagicMaster.playerConfig[playerID] = configObj;
+ };
+ return state.MagicMaster.playerConfig[playerID];
+ };
+
+ /*
+ * Make a configuration menu to allow the DM to select:
+ * - strict mode: follow the rules precisely,
+ * - house rules mode: follow "old fogies" house rules
+ * - no restrictions: allow anything goes
+ */
+
+ LibFunctions.makeConfigMenu = function( args, msg='' ) {
+
+ var configButtons = function( flag, txtOn, cmdOn, txtOff, cmdOff ) {
+ const liveButton = (txt) =>''+txt+' | ',
+ selButton = (txt,cmd) => ''+txt+' | ';
+ var buttons = (flag ? (selButton(txtOn,cmdOn)+liveButton(txtOff)) : (liveButton(txtOn)+selButton(txtOff,cmdOff)));
+// + (flag ? ('['+txtOn+']('+cmdOn+')'+txtOff+'')
+// : (''+txtOn+' | ['+txtOff+']('+cmdOff+')'))
+ return buttons;
+ };
+
+ var content = '&{template:'+fields.menuTemplate+'}{{name=Configure RPGMaster}}{{subtitle=AttackMaster}}'
+ + (msg.length ? '{{ ='+msg+'}}' : '')
+ + '{{desc=Select which configuration you wish for this campaign using the toggle buttons below.}}'
+ + '{{desc1=';
+
+// content += ('undefined' !== typeof MagicMaster ? ('Menus | '+configButtons(state.MagicMaster.fancy, 'Plain menus', '!magic --config fancy-menus|false', 'Fancy menus', '!magic --config fancy-menus|true')+' ') : '');
+ if ('undefined' !== typeof attackMaster) {
+ content += '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')+' '
+ + (state.attackMaster.weapRules.allowAll ? '' : ('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')+' '
+ + ((state.attackMaster.thieveCrit > 0) ? ('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')+' '
+ + ((state.attackMaster.attrRoll) ? ('NPC Attr Range | '+configButtons(state.attackMaster.attrRestrict, 'Full Range', '!attk --config attr-restrict|false', 'Restrict', '!attk --config attr-restrict|true')+' ') : '');
+ }
+ if ('undefined' !== typeof MagicMaster) {
+ content += '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')+' ';
+ }
+ content += ('undefined' !== typeof CommandMaster ? ('[Set Default Token Bars](!cmd --button AB_ASK_TOKENBARS|) | ') : '')
+ + ' }}';
+ LibFunctions.sendFeedback( content );
+ return;
+ };
+
+/* -------------------------------------------------- Code stubs for alternate versions -------------------------------------- */
+
+ LibFunctions.creatureAttkDefs = function() {};
+ LibFunctions.creatureWeapDefs = function() {};
+ LibFunctions.updateClassLevel = function() {};
+ LibFunctions.displayClassLevel = function() {};
+
+/* --------------------------------------------------- End of Library Functions ---------------------------------------------------- */
+
+
+/* ---------------------------------------------------- Finish Initialisation ---------------------------------------------- */
+
+ LibFunctions.sendFeedback( waitMsgDiv+'Please wait while RPGMaster initialises...' );
+ apis.magic = ('undefined' !== typeof MagicMaster);
+ apis.attk = ('undefined' !== typeof attackMaster);
+ apis.init = ('undefined' !== typeof initMaster);
+ DBindex = undefined;
+
+ if (_.isUndefined(state.RPGMaster)) state.RPGMaster = {};
+ if (_.isUndefined(state.RPGMaster.tokenFields)) {
+ state.RPGMaster.tokenFields = [fields.AC[0],fields.Thac0_base[0],fields.HP[0]];
+ };
+ if (_.isUndefined(state.MagicMaster)) state.MagicMaster = {};
+ if (_.isUndefined(state.MagicMaster.spellRules)) state.MagicMaster.spellRules = {};
+
+ setTimeout( del_Old_DBs, 5000 );
+
+ // RED: v1.036 create help handouts from stored data
+ setTimeout( () => LibFunctions.updateHandouts(handouts,true,findTheGM()),300);
+ setTimeout( () => displayReleaseNotesLink(), 5000 );
+ setTimeout( () => LibFunctions.sendAPI('!token-mod --api-as '+findTheGM()+' --config players-can-ids|on',findTheGM()), 10000);
+ }
+ }
+
+ const handleChatMessage = (msg) => {
+ try {
+ var preamble, targetid,
+ playerid = msg.playerid;
+
+ msg_orig[playerid] = msg;
+
+ if (msg.type === "api") {
+ return;
+ } else if (msg.content.trim().startsWith('!')) {
+ log('lib handleChatMessage: msg not api but starts with ! so re-send. Msg = '+msg.content);
+ return;
+ }
+ if (msg.rolltemplate && msg.rolltemplate.startsWith('RPGM')) {
+
+ targetid = findTheGM();
+ if (msg.target) {
+ if (msg.target != 'gm') {
+ let targetObjs = findObjs({_type:'player',_displayname:msg.who});
+ targetid = (!targetObjs || !targetObjs.length) ? targetid : targetObjs[0].id;
+ }
+ }
+ let newMsg = Object.create(msg);
+ newMsg = processInlinerolls(newMsg);
+ const template = newMsg.match(/^([^{]*)({{[^]*}}).*?$([^]*)/im);
+ switch (msg.type.toLowerCase()) {
+ case 'emote':
+ preamble = '/em';
+ break;
+ case 'desc':
+ preamble = '/desc';
+ break;
+ case 'whisper':
+ preamble = '/w "'+msg.target_name+'"';
+ break;
+ default:
+ preamble = '';
+ break;
+ }
+ if (/^\s*\/i.test(template[1])) preamble += ' '+template[1]; else if (template[1].trim().length) log('RPGM output parser: extra preamble = '+template[1]);
+ LibFunctions.parseOutput( msg.who, preamble, msg.rolltemplate, template[2], targetid );
+ }
+ return;
+ } catch (e) {
+ log('RPGMaster Library handleChatMessage: JavaScript '+e.name+': '+e.message+' while processing a chat message');
+ LibFunctions.sendCatchError('RPGMaster Library',msg_orig[playerid],e);
+ }
+ };
+
+ const tryInit = ()=>{
+ if(Campaign()) {
+ LibFunctions.init();
+ } else {
+ setTimeout(tryInit,10);
+ }
+ };
+ setTimeout(tryInit,0);
+
+ const checkInstall = () => {
+ log('-=> libRPGMaster v'+version+' <=- ['+(new Date(lastUpdate*1000))+']');
+
+ if( ! state.hasOwnProperty('libRPGMaster') || state.libRPGMaster.version !== schemaVersion) {
+ switch(state.libRPGMaster && state.libRPGMaster.version) {
+
+ case 0.1:
+ /* break; // intentional dropthrough */
+
+ case 'UpdateSchemaVersion':
+ state.libRPGMaster.version = schemaVersion;
+ break;
+
+ default:
+ state.libRPGMaster = {
+ version: schemaVersion
+ };
+ break;
+ }
+ }
+ };
+
+ const registerLib = () => {
+ on('chat:message',handleChatMessage);
+ };
+
+ on('ready', function () {
+ checkInstall();
+ registerLib();
+ });
+
+ return {
+ getRPGMap: (...a) => LibFunctions.getRPGMap(...a),
+ getTableField: (...a) => LibFunctions.getTableField(...a),
+ getTable: (...a) => LibFunctions.getTable(...a),
+ getLvlTable: (...a) => LibFunctions.getLvlTable(...a),
+ initValues: (...a) => LibFunctions.initValues(...a),
+ attrLookup: (...a) => LibFunctions.attrLookup(...a),
+ setAttr: (...a) => LibFunctions.setAttr(...a),
+ abilityLookup: (...a) => LibFunctions.abilityLookup(...a),
+ setAbility: (...a) => LibFunctions.setAbility(...a),
+ doDisplayAbility: (...a) => LibFunctions.doDisplayAbility(...a),
+ getAbility: (...a) => LibFunctions.getAbility(...a),
+ parseTemplate: (...a) => LibFunctions.parseTemplate(...a),
+ redisplayOutput: (...a) => LibFunctions.redisplayOutput(...a),
+ parseOutput: (...a) => LibFunctions.parseOutput(...a),
+ sendToWho: (...a) => LibFunctions.sendToWho(...a),
+ sendMsgToWho: (...a) => LibFunctions.sendMsgToWho(...a),
+ sendPublic: (...a) => LibFunctions.sendPublic(...a),
+ sendAPI: (...a) => LibFunctions.sendAPI(...a),
+ sendFeedback: (...a) => LibFunctions.sendFeedback(...a),
+ sendResponse: (...a) => LibFunctions.sendResponse(...a),
+ sendResponsePlayer: (...a) => LibFunctions.sendResponsePlayer(...a),
+ sendResponseError: (...a) => LibFunctions.sendResponseError(...a),
+ sendToOthers: (...a) => LibFunctions.sendToOthers(...a),
+ sendError: (...a) => LibFunctions.sendError(...a),
+ sendCatchError: (...a) => LibFunctions.sendCatchError(...a),
+ sendParsedMsg: (...a) => LibFunctions.sendParsedMsg(...a),
+ sendGMquery: (...a) => LibFunctions.sendGMquery(...a),
+ sendWait: (...a) => LibFunctions.sendWait(...a),
+ checkDBver: (...a) => LibFunctions.checkDBver(...a),
+ saveDBtoHandout: (...a) => LibFunctions.saveDBtoHandout(...a),
+ buildCSdb: (...a) => LibFunctions.buildCSdb(...a),
+ checkCSdb: (...a) => LibFunctions.checkCSdb(...a),
+ getDBindex: (...a) => LibFunctions.getDBindex(...a),
+ updateHandouts: (...a) => LibFunctions.updateHandouts(...a),
+ findThePlayer: (...a) => LibFunctions.findThePlayer(...a),
+ findCharacter: (...a) => LibFunctions.findCharacter(...a),
+ fixSenderId: (...a) => LibFunctions.fixSenderId(...a),
+ calcAttr: (...a) => LibFunctions.calcAttr(...a),
+ rollDice: (...a) => LibFunctions.rollDice(...a),
+ evalAttr: (...a) => LibFunctions.evalAttr(...a),
+ getCharacter: (...a) => LibFunctions.getCharacter(...a),
+ getTokenValue: (...a) => LibFunctions.getTokenValue(...a),
+ classObjects: (...a) => LibFunctions.classObjects(...a),
+ addMIspells: (...a) => LibFunctions.addMIspells(...a),
+ getMagicList: (...a) => LibFunctions.getMagicList(...a),
+ getShownType: (...a) => LibFunctions.getShownType(...a),
+ parseClassDB: (...a) => LibFunctions.parseClassDB(...a),
+ rogueLevelPoints: (...a) => LibFunctions.rogueLevelPoints(...a),
+ handleCheckThiefMods: (...a) => LibFunctions.handleCheckThiefMods(...a),
+ scanItemMods: (...a) => LibFunctions.scanItemMods(...a),
+ scanForMods: (...a) => LibFunctions.scanForMods(...a),
+ handleCheckSaves: (...a) => LibFunctions.handleCheckSaves(...a),
+ handleCheckWeapons: (...a) => LibFunctions.handleCheckWeapons(...a),
+ handleSetNPCAttributes: (...a) => LibFunctions.handleSetNPCAttributes(...a),
+ getHandoutIDs: (...a) => LibFunctions.getHandoutIDs(...a),
+ classAllowedItem: (...a) => LibFunctions.classAllowedItem(...a),
+ parseData: (...a) => LibFunctions.parseData(...a),
+ parseStr: (...a) => LibFunctions.parseStr(...a),
+ resolveData: (...a) => LibFunctions.resolveData(...a),
+ findPower: (...a) => LibFunctions.findPower(...a),
+ handleGetBaseThac0: (...a) => LibFunctions.handleGetBaseThac0(...a),
+ characterLevel: (...a) => LibFunctions.characterLevel(...a),
+ caster: (...a) => LibFunctions.caster(...a),
+ creatureAttkDefs: (...a) => LibFunctions.creatureAttkDefs(...a),
+ creatureWeapDefs: (...a) => LibFunctions.creatureWeapDefs(...a),
+ getSetPlayerConfig: (...a) => LibFunctions.getSetPlayerConfig(...a),
+ makeConfigMenu: (...a) => LibFunctions.makeConfigMenu(...a),
+ displayClassLevel: (...a) => LibFunctions.displayClassLevel(...a),
+ updateClassLevel: (...a) => LibFunctions.updateClassLevel(...a),
+ };
+
+})();
+
+{try{throw new Error('');}catch(e){API_Meta.libRPGMaster.lineCount=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-API_Meta.libRPGMaster.offset);}}
diff --git a/RPGMlibrary AD+D2e/libRPGMaster2e.js b/RPGMlibrary AD+D2e/libRPGMaster2e.js
index e5b06928d..824dce002 100644
--- a/RPGMlibrary AD+D2e/libRPGMaster2e.js
+++ b/RPGMlibrary AD+D2e/libRPGMaster2e.js
@@ -95,13 +95,14 @@ API_Meta.libRPGMaster={offset:Number.MAX_SAFE_INTEGER,lineCount:-1};
* for multi-line data sections. Fixed classObjects() resolution of certain creature
* classes. Fixed handleGetBaseThac0() for Warrior classes. Added new database for
* treasure items.
+ * v4.0.2 25/02/2025 Fixed error with AoE for the Bless spell.
**/
const libRPGMaster = (() => { // eslint-disable-line no-unused-vars
'use strict';
- const version = '4.0.1';
+ const version = '4.0.2';
API_Meta.libRPGMaster.version = version;
- const lastUpdate = 1738351019;
+ const lastUpdate = 1740502257;
const schemaVersion = 0.1;
log('now in seconds is '+Date.now()/1000);
@@ -4115,7 +4116,7 @@ const libRPGMaster = (() => { // eslint-disable-line no-unused-vars
version:8.03,
db:[{name:'Animal-Friendship',type:'prspelll1',ct:'600',charge:'uncharged',cost:'0.05',body:'\\amp{template:'+fields.spellTemplate+'}{{title=@{selected|Casting-name} casts\nAnimal Friendship\nas a level @{selected|pr-casting-level} caster}}{{splevel=Level 1 Priest}}{{school=Enchantment/Charm}}{{sphere=Animal}}Specs=[Animal-Friendship,PRspellL1,1H,Enchantment-Charm]{{components=V,S,M}}{{time=[[1]] hour}}{{range=[10 yards](!rounds --aoe @{selected|token_id}|circle|yards|0|20||magic|true)}}{{duration=Permanent}}{{aoe=[[1]] animal per spell casting}}{{save=Negates}}{{reference=PHB p198}}SpellData=[w:Animal-Friendship,lv:1,sp:600,gp:0.05,cs:VSM,sph:Animal]{{effects=Attracts up to [[2*@{selected|pr-casting-level}]] HD animals. Max number of HD of animals as friends at any time is 2x caster\'s level at that time.}}{{hide1=Able to show any animal of animal intelligence to semi-intelligence (i.e., Intelligence 1-4) that he desires friendship. If the animal does not roll a successful saving throw vs. spell immediately when the spell is begun, it stands quietly while the caster finishes the spell. Thereafter, it follows the caster about. The spell functions only if the caster actually wishes to be the animal\'s friend. If the caster has ulterior motives, the animal always senses them (for example, the caster intends to eat the animal, send it ahead to set off traps, etc.).\nThe caster can teach the befriended animal three specific tricks or tasks for each point of Intelligence it possesses. Typical tasks are those taught to a dog or similar pet (i.e., they cannot be complex). Training for each such trick must be done over a period of one week, and all must be done within three months of acquiring the creature. During the three-month period, the animal will not harm the caster, but if the creature is left alone for more than a week, it will revert to its natural state and act accordingly.\nOnly unaligned animals can be attracted, befriended, and trained.}}{{materials=Caster\'s holy symbol and food that the animal likes worth 5cp.}}'},
{name:'Attack-target',type:'',ct:'0',charge:'uncharged',cost:'0',body:'**@{selected|token_name} hits AC [[( ([[@{selected|thac0-base}]]) - ([[@{selected|strengthhit}]]) - [[1d20]] )]] with his spell.**\n/w "@{selected|character_name}" If hit, [Cast on them](!magic --target single|@{selected|token_id}|@{target|Who are you Attacking?|token_id}|@{selected|Spell-cast}|@{selected|Spell-duration}|@{selected|Spell-direction}|@{selected|Spell-msg}|@{selected|Spell-marker})'},
- {name:'Bless',type:'prspelll1',ct:'10',charge:'uncharged',cost:'1',body:'\\amp{template:'+fields.spellTemplate+'}{{title=@{selected|Casting-name} casts\nBless\nas a level @{selected|pr-casting-level} caster}}{{splevel=Level 1 Priest}}{{school=Conjuration/Summoning}}{{sphere=All}}Specs=[Bless,PRspellL1,1H,Conjuration-Summoning]{{components=V,S,M}}{{time=[[1]] round}}{{range=[[60]] yards}}{{duration=[[6]] rounds}}{{aoe=[50ft cube](!range --aoe @{selected|token_id}|square|feet|180|50||light)}}{{save=None}}{{reference=PHB p198}}{{Use=[Bless them](!rounds --target area|@{selected|token_id}|\\amp#64;{target|Select first supplicant|token_id}|Bless|6|-1|Blessed, attk+1, save vs fear+1|angel-outfit) applies attack bonus for duration, but apply fear save benefit manually}}SpellData=[w:Bless,dur:6 rounds,lv:1,sp:10,r:60 yds,aoe:50ft cube,sav:None,gp:1,cs:VSM,sph:All]{{effects=Raises morale of friendly creatures *not yet in melee combat* adding +[[1]] to saves vs. fear effects and raises attack rolls by +[[1]]. Or can cast on a single object weighing no more than [[@{selected|pr-casting-level}]] pounds and the plus lasts until the item is used or the duration ends}}{{hide1=The caster determines at what range (up to 60 yards) he will cast the spell. At the instant the spell is completed, it affects all creatures in a 50-foot cube centered on the point selected by the caster (thus, affected creatures leaving the area are still subject to the spell\'s effect; those entering the area after the casting is completed are not).\nA second use of this spell is to bless a single item (for example, a crossbow bolt for use against a rakshasa). The weight of the item is limited to one pound per caster level and the effect lasts until the item is used or the spell duration ends.\nMultiple *bless* spells are not cumulative. In addition to the verbal and somatic gesture components, the bless spell requires holy water.}}{{marterials=Holy Water}}'},
+ {name:'Bless',type:'prspelll1',ct:'10',charge:'uncharged',cost:'1',body:'\\amp{template:'+fields.spellTemplate+'}{{title=@{selected|Casting-name} casts\nBless\nas a level @{selected|pr-casting-level} caster}}{{splevel=Level 1 Priest}}{{school=Conjuration/Summoning}}{{sphere=All}}Specs=[Bless,PRspellL1,1H,Conjuration-Summoning]{{components=V,S,M}}{{time=[[1]] round}}{{range=[[60]] yards}}{{duration=[[6]] rounds}}{{aoe=[50ft cube](!rounds --aoe @{selected|token_id}|square|feet|180|50||light)}}{{save=None}}{{reference=PHB p198}}{{Use=[Bless them](!rounds --target area|@{selected|token_id}|\\amp#64;{target|Select first supplicant|token_id}|Bless|6|-1|Blessed, attk+1, save vs fear+1|angel-outfit) applies attack bonus for duration, but apply fear save benefit manually}}SpellData=[w:Bless,dur:6 rounds,lv:1,sp:10,r:60 yds,aoe:50ft cube,sav:None,gp:1,cs:VSM,sph:All]{{effects=Raises morale of friendly creatures *not yet in melee combat* adding +[[1]] to saves vs. fear effects and raises attack rolls by +[[1]]. Or can cast on a single object weighing no more than [[@{selected|pr-casting-level}]] pounds and the plus lasts until the item is used or the duration ends}}{{hide1=The caster determines at what range (up to 60 yards) he will cast the spell. At the instant the spell is completed, it affects all creatures in a 50-foot cube centered on the point selected by the caster (thus, affected creatures leaving the area are still subject to the spell\'s effect; those entering the area after the casting is completed are not).\nA second use of this spell is to bless a single item (for example, a crossbow bolt for use against a rakshasa). The weight of the item is limited to one pound per caster level and the effect lasts until the item is used or the spell duration ends.\nMultiple *bless* spells are not cumulative. In addition to the verbal and somatic gesture components, the bless spell requires holy water.}}{{marterials=Holy Water}}'},
{name:'CLW',type:'prspelll1',ct:'5',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.spellTemplate+'}{{title=@{selected|Casting-name} casts\nCure Light Wounds\nas a level @{selected|pr-casting-level} caster}}{{splevel=Level 1 Priest}}{{school=Necromancy}}{{sphere=Healing}}Specs=[CLW,PRspellL1,1H,Necromancy]{{components=V,S}}{{time=[[5]]}}{{range=Touch}}{{duration=Permanent}}{{aoe=Creature touched}}{{save=None}}{{reference=PHB p199}}{{healing=[1d8](!\\amp#13;\\amp#47;r 1d8) HP,\nor [1d8+3](!\\amp#13;\\amp#47;r 1d8+3) for a Priest of Life}}SpellData=[w:CLW,lv:1,sp:5,gp:0,cs:VS,sph:Healing]{{effects=Cannot cure non-corporeal or nonliving or extra-planar creatures.}}'},
{name:'Cause-Fear',type:'prspelll1',ct:'1',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.spellTemplate+'}{{title=@{selected|Casting-name} casts\nCause Fear\nas a level @{selected|pr-casting-level} caster}}{{splevel=Level 1 Priest (reversable)}}{{school=Abjuration}}{{sphere=Charm}}Specs=[Cause-Fear,PRspellL1,1H,Abjuration]{{components=V,S}}{{time=[[1]]}}{{range=[10 yds](!rounds --aoe @{selected|token_id}|circle|yards|0|20||dark|true)}}{{duration=1d4 rounds}}{{aoe=[[1]] creature}}{{save=Negates}}{{reference=PHB p202 (reverse Remove Fear)}}{{Use=[Scare Them](!rounds --target single|@{selected|token_id}|\\amp#64;{target|Select a victim|token_id}|Cause-Fear|\\amp#91;[1d4]\\amp#93;|-1|Frightened, flee at maximum rate|screaming|svspe\\clon;+0)}}SpellData=[w:Cause-Fear,lv:1,sp:1,gp:0,cs:VS,sph:Charm]{{effects=Cause fear causes one creature to flee in panic at maximum movement speed away from the caster for 1d4 rounds. A successful saving throw negates it, and any Wisdom adjustment also applies. Of course, *cause fear* can be automatically countered by *remove fear* and vice versa.\nNeither spell has any effect on undead of any sort.}}'},
{name:'Cause-Light-Wounds',type:'innate-melee|prspelll1',ct:'5',charge:'uncharged',cost:'0',body:'\\amp{template:'+fields.spellTemplate+'}{{title=@{selected|Casting-name} casts\nCause Light Wounds\nas a level @{selected|pr-casting-level} caster}}{{splevel=Level 1 Priest}}{{school=Necromancy}}{{sphere=Healing}}Specs=[Cause-Light-Wounds,Innate-Melee|PRspellL1,1H,Necromancy]{{components=V,S}}ToHitData=[w:Cause Light Wounds,sp:5,r:5,ty:SPB,touch:1]{{time=[[5]]}}DmgData=[w:Cause Light Wounds,sb:0,SM:1d8,L:1d8]{{range=Touch}}{{duration=Permanent}}{{aoe=Creature touched}}{{save=None}}{{damage=[1d8](!\\amp#13;\\amp#47;r 1d8) HP}}{{reference=PHB p199 (reverse Cure Light Wounds)}}SpellData=[w:Cause-Light-Wounds,lv:1,sp:5,gp:0,cs:VS,sph:Healing]{{Use=Take the spell in-hand as a weapon and attack with it.}}{{effects=Operates in the same manner as Cure Light Wounds, but inflicting 1d8 points of damage instead of curing. If a creature is avoiding this touch, a Touch attack is required to inflict the damage. Cannot damage non-corporeal or nonliving or extra-planar creatures.}}'},
diff --git a/RPGMlibrary AD+D2e/script.json b/RPGMlibrary AD+D2e/script.json
index 1cb6da05c..be4779465 100644
--- a/RPGMlibrary AD+D2e/script.json
+++ b/RPGMlibrary AD+D2e/script.json
@@ -2,8 +2,8 @@
"$schema": "https://github.com/DameryDad/roll20-api-scripts/blob/RPGMlibrary/RPGMlibrary AD+D2e/Script.json",
"name": "RPGMaster library AD+D2e",
"script": "libRPGMaster2e.js",
- "version": "4.0.1",
- "previousversions": ["1.3.00","1.3.01","1.3.02","1.3.03","1.3.04","1.4.01","1.4.02","1.4.03","1.4.04","1.4.05","1.4.06","1.4.07","1.5.01","1.5.02","1.5.03","1.5.04","1.5.05","1.5.06","2.1.0","2.2.0","2.2.1","2.2.2","2.3.0","2.3.1","2.3.2","2.3.3","2.3.4","3.0.0","3.0.1","3.0.2","3.1.3","3.2.0","3.2.1","3.3.0","3.4.0","3.5.0","3.5.1","3.5.2"],
+ "version": "4.0.2",
+ "previousversions": ["1.3.00","1.3.01","1.3.02","1.3.03","1.3.04","1.4.01","1.4.02","1.4.03","1.4.04","1.4.05","1.4.06","1.4.07","1.5.01","1.5.02","1.5.03","1.5.04","1.5.05","1.5.06","2.1.0","2.2.0","2.2.1","2.2.2","2.3.0","2.3.1","2.3.2","2.3.3","2.3.4","3.0.0","3.0.1","3.0.2","3.1.3","3.2.0","3.2.1","3.3.0","3.4.0","3.5.0","3.5.1","3.5.2","4.0.1"],
"description": "RPGMaster Library for AD&D2e provides all of the game-version-specific data and rule processing for the RPGMaster series of APIs to work with the Advanced Dungeon & Dragons 2nd Edition rule set, and with the Advanced D&D2e Character Sheet by Peter B. Other versions of the library will support other gave versions in future. In order for versions of the RPGMaster series APIs later than 1.0.0 to work (i.e. those that have version numbers with three segments) they require one of the RPGMaster Libraries to be loaded with them: which one determines which rule set and character sheet they work with. The Library does not support any API commands itself (the other RPGMaster APIs provide those), but it supports a unique set of Roll Templates, and provides API Authors with a number of callable functions that might be of use. See the RPGMaster Library Help handout that the Library creates in your campaign when initially loaded for details.\n\n[RPGMaster Documentation](https://wiki.roll20.net/RPGMaster) \n### Getting Started\n1. When all APIs in the RPGMaster suite are loaded, run `!cmd --initialise` and add the player macros created to the Macro Bar, then\n2. 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) \n3. Once steps 1 & 2 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",
| |