Rod's D&D 5e Framework

Framework(s) for D&D 5e.

Moderators: Azhrei, dorpond, trevor, giliath, Gamerdude, jay, Mr.Ice

Nikkor89
Kobold
Posts: 3
Joined: Sun Apr 19, 2020 5:11 pm

Re: Rod's D&D 5e Framework

Post by Nikkor89 »

AndyBigDM wrote:
Fri Apr 24, 2020 4:53 am
Nikkor89 wrote:
Wed Apr 22, 2020 9:04 am
I've now added all the races, their variants and monster races (PH, SCAG and Volo) in your framework so if someone is interested I can link the Lib:Character Creation and Lib:Character Tokens (you need the feats with the explanation for some features).
That said this is still a Work in Progress since I plan to start working on the backgrounds options if someone is not doing it already.
Interested!!!

I've already added a couple of extra races for my players (Aarakocra, Aasimar & Tortle) - but if you've gone to so much effort getting everything else in I can't just let you keep that to youself now can I!?
Though If you're still working on adding more I will happily wait until you're ready to share...
It's a work in progress but I've stopped working on the backgrounds at the moment since I was working on the 2.0 version and I've read Rod has been working on them in the 2.3 so I'll wait and see what he already did.
For the races I've already done the full list is this one: (you can copy/paste the list in the races section in settings to make them selecta
Human,Human (Variant),Elf (High Elf),Elf (Wood Elf),Elf (Dark Elf),Dwarf (Hill Dwarf),Dwarf (Mountain Dwarf),Dwarf (Gray Dwarf),Halfling (Lightfoot),Halfling (Stout),Gnome (Forest Gnome),Gnome (Rock Gnome),Gnome (Deep Gnome),Half-Elf,Half-Elf (Variant),Half-Orc,Dragonborn,Tiefling,Tiefling (Variant),Aasimar (Protector),Aasimar (Scourge),Aasimar (Fallen),Firbolg,Goliath,Kenku,Lizardfolk,Tabaxi,Triton,Aarakocra,Air Genasi,Earth Genasi,Fire Genasi,Water Genasi,Bugbear,Goblin,Hobgoblin,Kobold,Orc,Yuan-ti Pureblood
These are the races from: PH, SCAG, EEPC and VGTM.
I've not added the other ones since I was not interested in them. Maybe I'll add them later.

files
the four files are the expanded feats, equip and spells requested by some races and the macroset for the races to import in the Lib:Character Creation token.
I've also checked if they work in the 2.3 version and from what I've seen the only problem is the missing tables for the races I've added

AndyBigDM
Kobold
Posts: 3
Joined: Wed Apr 22, 2020 2:06 pm

Re: Rod's D&D 5e Framework

Post by AndyBigDM »

Thanks for sharing!

Have you tested the dice rollers from a player perspective?... That's my only issue and I can't figure it out. Don't know if it's something I've done it an actual bug...

OokOok
Kobold
Posts: 15
Joined: Wed Nov 13, 2019 12:27 am

Re: Rod's D&D 5e Framework

Post by OokOok »

Just finished upgrading from FW 2.0x to 2.3. I like the new DM page graphics, and really like the multiple new windows into things like Races, Feats, and particularly spell lists. Basically, the new lib:Tables stuff looks interesting. I'll have to study up on how to integrate the 3x new subclasses I added into 2.0 to see how they have dealt with in 2.3.

I'm receiving Stack Overflow errors attempting to open any of the Class windows, but I'm assuming I'll be able to address that with startup memory size mods. Has anyone else experienced this?

A "wishlist" item: I don't know enough to know whether this is possible in MT, but is it possible to separate out a lot of the static data into tokens that would NOT normally be part of any updates you do? I had to re-import the following things:

a) (Compendium) Equipment
b) (Compendium) Feats
c) (Compendium) Additional Feats
d) (Compendium) Manage Spells

e) (Character Sheet) Feats
f) (Character Sheet) Races
g) (Character Sheet) Backgrounds
h) (Character Sheet) Classes
i) (Character Sheet) Spell Lists
j) (Character Sheet) Spells

While a-d are less troublesome because you can do a bulk JSON merge, e-j are a bit more fiddly because you basically have to cut/paste data from old to new FW one field at a time. Doing j), in particularly, is tedious.

Merudo
Giant
Posts: 228
Joined: Wed Jun 05, 2019 7:06 am

Re: Rod's D&D 5e Framework

Post by Merudo »

Awesome work! This is shaping up into something wonderful :)

With regard to the spells, backgrounds, etc. it might be worth looking at 5etools. They have the near complete data of 5e including every spell, background, race, monster, etc, all of it in JSON form.

I think it wouldn't be too hard to modify your framework so that part of this data can be imported directly into it. That might save you some serious time.

OokOok
Kobold
Posts: 15
Joined: Wed Nov 13, 2019 12:27 am

Re: Rod's D&D 5e Framework

Post by OokOok »

Indeed, I've been playing around with mass import of various things using 5etools-style json files as input. Here's a PowerShell script that will take background definitions and create a file whose contents can be copy/pasted into the Feats table. I still need to work on a couple of enhancements (see the "not yet implemented" notes in the script comments), and there are 3 bugs - 2 minor, 1 major - in the code that are also called out in comments. I hope someone can really help me figure out the BIG bug. Even with bugs, however, the output generated is valid. (Some Backgrounds will only show one paragraph of text in sections where there should be multiple paragraphs.)

As always, be safe and backup your cmpgn file before you play with this!

I also used a combo of spell data exported from 5etools (rather than raw json data) + Excel to massage data into Rod's markdown format to populate the spells database, and also an Excel table that generates all the strings needed to specify which spells are available to each class at each specific level. (It was faster for me to do this in Excel than to write a program to do it.) I'll try to figure out a way to share that later.

Code: Select all


function Quote-Literals {
	# Encode any string literals that have to be protected
	
	param( [string]$txt )
	$txt = $txt -replace '"', '\"'				# protect embedded quotes
	$txt = $txt -replace " d(\d+?)", ' 1d$1'	# replace " dN" with "1dN"
	$txt = $txt -replace " (\d+?d\d+?)",  '[$1](roll \"$1\")'	# encode die rolls
	$txt = $txt -replace "^Prerequisite:(.*)",'*Prerequisite: $1*'	# KLUDGE: not sure why the replacement in Remove-EncodedText not working?
	return $txt
}
	
function Remove-EncodedText {
	param( [string]$link )
	# Remove @<text> through to space (inclusive)
	# Remove { and }
	# Remove anything from first | through to } (inclusive)

	$link = $link -replace "\|\D+?}",""		# Remove ALL lists from <pipe> through end bracket (inclusive)
	$link = $link -replace "{@\D+? ",""		# Remove ALL {@<tag><space>
	$link = $link -replace "}+?",""
	$link = $link -replace "\)\|.*$", ")"	# KLUDGE: handles things like "pole (10-foot)|<blah" by removing from just after the ) to the end
	$link = $link -replace "{@i (.*)}", '*$1*' # Convert italic directive to italic markup

	return ($link)
}

# Iterate through the 5e Tools backgrounds.json file and produce the following output
#
# 1) json strings to be imported as Feats to Rod's 5e Framework
# 2) (not yet implemented) a macro script to be saved to Character Creation as part of the Background frame
# 3) (not yet implemented) a text fragment to be added to the list of Backgrounds in the Settings dialog

# Open 5etools JSON source file

$devPath = "C:\Users\dstei\OneDrive\Projects\MapTool\Rod's 5E Framework\Tools\"
$srcFile = $devPath + "ALL-backgrounds.json"
$dstFile = $devPath + "parsed-backgrounds.json"
$jsondata = Get-Content -Raw -Path $srcFile -Encoding UTF8 | ConvertFrom-Json -NoEnumerate
$TextInfo = (Get-Culture).TextInfo
$q = '"'			# literal quote

# $jsondata.background.count = # of records available

$bgout = "{" 
$otherFeatures = ""		# holds the collection of Features defined in all the backgrounds

foreach ($bg in $jsondata.background) { 
	$bg.name 
	$out = $q + $bg.name + $q + ":" + $q + "---\n"

	# Code doesn't currently handle backgrounds that have _copy keys, so we're going to skip all those entries
	if ([bool]($bg.PSobject.Properties.name -match "_copy")) {
		$copy = $true
	} else {
		foreach ($item in $bg.entries[0].items) {
			
			# BUG: "Dissenter" doesn't have any item keys in $bg.entries.  It has $bg.entries[0].entries, with three values that
			# aren't picked up by this code.  So, for now, we get "Dissenter":"", in our output.
			if ($item.name -eq "Skill Proficiencies") {
				$temp = Remove-EncodedText $item.entry
				$out = $out + "**Skill Proficiencies** " + $temp + "  \n"
			}
			if ($item.name -eq "Tool Proficiencies") {
				$temp = Remove-EncodedText $item.entry
				$out = $out + "**Tool Proficiencies** " + $(Remove-EncodedText $item.entry ) + "  \n"
			}
			if ($item.name -eq "Languages") {
				$out = $out + "**Language** " + $item.entry + "  \n"
			}
			if ($item.name -eq "Equipment") {
				$temp = Remove-EncodedText $item.entry
				$out = $out + "**Equipment** " + $temp  + "\n\n"
			}
		}
	
		for($node=1;$node -lt $bg.entries.count;$node++) {
			# Node 0: Proficiencies, languages, equipment
			# Node 1-n: Features, tables, etc.
			foreach ($item in $bg.entries[$node]) {
				if ($item.name -like "Feature: *") {
					# Create a separate JSON entry for features
					# All entries of this node are part of the feature
					$otherFeatures = $otherFeatures + $q + $item.name.TrimStart("Feature: ") + $q + ':"'
					for($subnode=0;$subnode -lt $item.entries.count;$subnode++) {
						$otherFeatures = $otherFeatures + $(Quote-Literals $item.entries[$subnode]) + "\n\n"	
					}
					$otherFeatures = $otherFeatures + $q + ",`r`n"
				} else {
					# ...otherwise, compile section header...
					$out = $out + "#### " + $item.name + "\n\n"

					# Huge kludge.  $item is often a mix of raw strings + complex objects (like tables).  The
					# raw strings are hard to figure out, so basically we look at all items here and capture those
					# that are of type string, leaving the things like tables for other processes.
					for ($i=0 ; $i -lt $item.entries.count; $i++) {
						if ($item.entries[$i] -is [string]) {
							$out = $out + $(Quote-Literals $(Remove-EncodedText($item.entries[$i]))) + "\n\n"	
						}
					}
						
					#$out = $out + $(Quote-Literals $(Remove-EncodedText($item.entries[0]))) + "\n\n"	
				
					
					# ...and then check for table entries
					# BUG: "House Agent" has a table at $bg.entries[$node]...NOT at ...$[node].entries.  This code
					# throws a visible error on-screen during execution because it can't find the non-existent 
					# ...$[node].entries keys.  This is NOT fatal - a valid House Agent json entry is created, but it
					# is missing one of the background's tables.

					foreach ($tbl in $bg.entries[$node].entries) {
						if ($tbl.type -eq "table") {			

							if ($tbl.caption -ne $null) {
								$out = $out + "##### " + $tbl.caption + "\n\n"
							}
							
							# Column headers
							$out = $out + "|[" + $tbl.colLabels[0] + "](roll \" + $q + "1" + $tbl.colLabels[0] + "\" + $q + ")|" + $tbl.colLabels[1] + "|\n"
							$out = $out + "|:---:|---|\n"
							
							# Table rows
							foreach ($row in $tbl.rows) {
								$out = $out + "|" + $row[0] + "|" + $(Quote-Literals($(Remove-EncodedText $row[1]))) + "|\n"							
							}
							$out = $out + "\n"
						}
					}
					
				}
				
			}
		}

		$bgout = $bgout + $out + $q + ","
		$bgout = $bgout + "`r`n"
		
	}
}
$bgout = $bgout + $otherFeatures.TrimEnd(",`r`n") + "`r`n}"
$bgout | Out-File $dstFile 



EDIT: I have figured out a work-around for the big bug - seems kludgy but it works. I've updated the code above.

User avatar
rtakehara
Cave Troll
Posts: 37
Joined: Mon Nov 11, 2019 5:11 pm
Contact:

Re: Rod's D&D 5e Framework

Post by rtakehara »

OokOok wrote:
Thu Apr 23, 2020 10:47 pm
While you track this down, is there a simple was to reset the database of spells so I can re-import a clean copy? EDIT 4/24: OK - I found the property where the spells are stored on the lib token and manually removed the two offending entries.
I did a 2.3.1 fix that fix that, I mean, I didn't do a way to delete the problematic item, but I figured the problem happens because there is a space in the beginning of the json object key (on the end also breaks it), but I added a code that removes spaces from beginning and ending so it wont happen again, hopefully... thanks for the report!

User avatar
rtakehara
Cave Troll
Posts: 37
Joined: Mon Nov 11, 2019 5:11 pm
Contact:

Re: Rod's D&D 5e Framework

Post by rtakehara »

AndyBigDM wrote:
Fri Apr 24, 2020 4:48 am
We came across a bug that completely froze up maptools and required a forced shutdown of the application.
We retested to confirm, and it seems that the dice rollers in the 'Campaign' macros window are broken.
Oh, other people reported the same! I did a 2.3.1 patch that probably fixes it (I couldn't test with many players yet) but the link in the roll was auto executing, so one extra player was making it repeat once, with two, it probably retro actively feeds itself into an infinite loop. mb, thanks for the report!

Also, super glad to see it worked (mostly) well for you!

OokOok
Kobold
Posts: 15
Joined: Wed Nov 13, 2019 12:27 am

Re: Rod's D&D 5e Framework

Post by OokOok »

I've been playing around with backgrounds that have apostrophes in them. I cannot figure out how to properly escape them in the Backgrounds list, however. All text prior to the single quote are dropped on output.

Example: in AI there's a background called "Celebrity Adventurer's Scion" The backgrounds list string should therefore be Celebrity Adventurer's Scion=Name Dropping. This string is accepted by the input field, but when selecting a background during PC creation the selection list shows

's Scion

I've tried prefixing the ' with \ and double-quoting the background, but that simply suppresses the display of this choice entirely.

OokOok
Kobold
Posts: 15
Joined: Wed Nov 13, 2019 12:27 am

Re: Rod's D&D 5e Framework

Post by OokOok »

This is a massively revised and improved version of my PowerShell tool to convert background definitions exported from 5etools into things compatible with Rod's great framework.

There's a bunch of notes in the opening comments. Check and adjust the path definitions around line 75 for your environment. To prepare your source JSON, select the background(s) you want in 5etools, right-click and Pin them. Then go to your Pin list, right-click, and choose "Download JSON Data". THAT file becomes the input used by my tool. The tool is fast - on a decent PC it will process several dozen backgrounds in just a few seconds. Most of the effort is a lot of copy/paste of text from tool output into the FW.

I have run this tool against several dozen backgrounds, but I have NOT exhaustively tested every one of them inside the FW. I tested about a dozen to ensure that the basic class of functions worked as expected - or, in some cases, to determine that my tool is not yet capable of creating a small handful of the backgrounds available. If anyone comes across a problem that's not already discussed in the tool's comments please let me know so I can look into it!

Code: Select all

#
# Import-Background.ps1
#
# This tool is designed to work in support of Rod's D&D 5e Framework for 
# Maptool, found at https://forums.rptools.net/viewtopic.php?f=85&t=28435.
# 
# It takes as input a file containing D&D 5e Background descriptions 
# exported from 5eTools as JSON definitions.  It will generate as output
# the following:
#
# 1) For EACH background in the JSON source, a file called <background>.mt.
#    The contents of this file are meant to be copy/pasted into the 
#    framework as a background defined at Lib:Character Creation.
# 2) A single file called parsed-backgrounds.json.
#    The contents of this file are meant to be copy/pasted into the
#    Menu->Settings->Feats (compendium) Import field.  What you are
#    pasting in are all the text descriptions of the Backgrounds plus
#    the special Feature associated with each Background.
# 3) A single (potentially LONG) line of text is printed to the screen
#    after the tool runs.  It is a list of all the Background=Feature
#    pairs just processed.  You should copy/paste this into the 
#    Menu->Settings->Backgrounds link of the framework.

# Things this tool currently does NOT do:
#   o Doesn't check for duplicate Features, which, when imported, will cause
#	  at least one of the related Backgrounds to generate errors in MT.  
#	  Example: Charlatan, Dimir Operative, and Baldur's Gate Charlatan all 
#	  have a Feature called "False Identity".  As a result, these three 
#	  backgrounds could result in three DIFFERENT JSON entries, all duping
#	  the key of "False Identity".  SOLUTION: The user will need to manually 
#	  modify the input file to differentiate these better.  PROBABLY Note
#     GOING TO FIX THIS IN THE TOOL.  THE PROBLEM IS THAT THE SOURCES ARE
#     NON-UNIQUE.
#
#  o At least one Background has a Language description of "Dwarvish, or one
#	 lanuage of choice is Dwarvish is already known". I don't know what the 
#	 MT macro code would need to look like to a) figure out if a language is 
#	 already selected and b) perform an appropriate "if <langknown> then 
#	 <select language>".  (I know how to do the <select language> portion. If
#    someone could help me with a) and the syntax for "if x then" I'll add it.
#
#  o For starting equipment, we can only recognize gear that is tagged in the
#    JSON sources.  For example, the Acolyte's background shows this starting
#	 gear:
#    A {@item holy symbol|phb} (a gift to you when you entered the priesthood), 
#	 a prayer book or prayer wheel, 5 sticks of incense, vestments, a set of 
#	 {@item common clothes|phb}, and a belt {@item pouch|phb} containing 15 gp
#
#    The prayer book or prayer wheel, the incense, and the vestments are not
# 	 tagged as @item in the source, and are therefore not added to the 
#	 inventory of the Acolyte.  (I will be adding support for defining 
#    supplemental equipment later.)  ON THE LIST TO FIX
#
#  o For starting equipment, only grants Qty 1 of items regardless of what the
#	 background text might say.  EXCEPTION: Always grants 20 arrows or 
#	 10 torches when the item is spec'ed in the background.  (So far, these 
#	 are the only exceptions I've encountered in backgrounds.)
#
#  o At least one background (Archaeologist) grants ONE tool proficiency, but 
#    it is from a choice of two options.  I don't yet handle this - and any
#    .mt file generated will not work correctly.  ON THE LIST TO FIX
#
#  o At least one background (Far Traveler) requires a Tool Proficiency choice
#    of Musical Instrument OR Gaming Set.  I don't currently handle this and
#    the output .mt file that is created will NOT work.  ON THE LIST TO FIX.
#
#  o Backgrounds that have 's in them don't display correctly in the list
#    when selecting a background.  Everything prior to the 's is dropped.
#    As a result, choosing these options results in the matching macro to
#    not be found.  I have a query out to Rod re: whether I can solve this
#    by better string quoting.  I'll wait to see if this is a bug in the FW
#    that can be fixed.  If not, the only solution will be to manually rename
#    things to avoid use of 's.

$devPath = "C:\Users\dstei\OneDrive\Projects\MapTool\Rod's 5E Framework\Tools\"
$srcFile = $devPath + "ALL-backgrounds.json"
$dstFile = $devPath + "parsed-backgrounds.json"

$jsondata = Get-Content -RAW -Path $srcFile -Encoding UTF8 | ConvertFrom-Json -NoEnumerate
$TextInfo = (Get-Culture).TextInfo
$q = '"'			# literal quote

function Make-Macro {

	# Let's see if this background grants any kind of tools proficiencies
	if ($tools.count -ne 0) {

		# Common tools preamble for all backgrounds
		$tp = "`r`n`r`n<!-----------------Tools------------------->`r`n"
		$tp = $tp + '[h:tools=getLibProperty("Tools","Lib:Character Creation")]' + "`r`n`r`n"
		$tp = $tp + '[h:atr=getProperty("Tool Proficiency")]' + "`r`n"
		$tp = $tp + '[h:value=getStrProp(atr,"value")]' + "`r`n`r`n"

		# Check to see if there's a list of choices that we'll have to query the PC about (example: Clasp Member)
		# For each item that the PC can choose we'll also have to make sure that the macro can process their choices
		# in the case of generic selections.  For example, if there's a choice between "musical instrument" and "gaming set"
		# the macro not only has to offer this choice, but will also have to include code to process EITHER choice after selection.
		#
		#	NOT YET IMPLEMENTED
		#
		#

		# Let's look for GENERIC proficiencies and add macro code for each one found
		for ($i=0; $i -lt $tools.count; $i++) {
			$t = $tools[$i].ToLower()

			if ($t -eq "musical instrument") {
				$tp = $tp + "<!-----------------Choose Instrument Proficiency------------------->`r`n"
				$tp = $tp + "[h:res=input(" + $q + "skill|Background Instrument Proficiency||label|span=true" +$q + ",`r`n"
				$tp = $tp + $q + "music|Bagpipes,Drum,Ducimer,Flute,Horn,Lute,Lyre,Pan Flute,Sharm,Viol|Instrument Choice|list|value=string" + $q + ")]`r`n"
				$tp = $tp + "[h,if(listfind(value,music)==-1):value=listappend(value,music)]`r`n"		
				$tp = $tp + '[h:atr=setStrProp(atr,"value",value)]' +"`r`n"
				$tp = $tp + '[h:setProperty("Tool Proficiency",atr)]' +"`r`n"
				$tp = $tp + "`r`n"		
			
			} elseif ($t -eq "gaming set") {
				$tp = $tp + "<!-----------------Choose Gaming Proficiency------------------->`r`n"
				$tp = $tp + "[h:res=input(" + $q + "skill|Background Gaming Set Proficiency||label|span=true" +$q + ",`r`n"
				$tp = $tp + $q + "game|Dice Set,Dragonchess Set,Playing Card Set,Three-Dragon Ante Set|Gaming Set Choice|list|value=string" + $q + ")]`r`n"
				$tp = $tp + "[h,if(listfind(value,game)==-1):value=listappend(value,game)]`r`n"		
				$tp = $tp + '[h:atr=setStrProp(atr,"value",value)]' + "`r`n"
				$tp = $tp + '[h:setProperty("Tool Proficiency",atr)]' +"`r`n"
				$tp = $tp + "`r`n"				

			} elseif ($t -eq "artisan's tools") {
				$tp = $tp + "<!-----------------Choose Artisan's Proficiency------------------->`r`n"
				$tp = $tp + "[h:res=input(" + $q + "skill|Background Artisan Tool Proficiency||label|span=true" +$q + ",`r`n"
				$tp = $tp + $q + "artisan|Alchemist's Supplies,Brewer's Supplies,Calligrapher's Supplies,Carprenter's Tools,Cartographer's Tools,Cobbler's Tools,Cook's Tools,Glassblower's Tools,Jeweler's Tools,Leatherworker's Tools,Mason's Tools,Painter's Tools,Smith's Tools,Tinker's Tools,Weaver's Tools,Woodcarver's Tools|Tool Choice|list|value=string" + $q + ")]`r`n"
				$tp = $tp + "[h,if(listfind(value,artisan)==-1):value=listappend(value,artisan)]`r`n"		
				$tp = $tp + '[h:atr=setStrProp(atr,"value",value)]' + "`r`n"
				$tp = $tp + '[h:setProperty("Tool Proficiency",atr)]' +"`r`n"
				$tp = $tp + "`r`n"	
				
			} elseif ($t -eq "vehicles (land)") {
				$tp = $tp + "<!-----------------Choose Vehicle Proficiency------------------->`r`n"
				$tp = $tp + "[h:res=input(" + $q + "skill|Background Land Vehicle Proficiency||label|span=true" +$q + ",`r`n"
				$tp = $tp + $q + "skillchoice|Carriage,Cart,Chariot,Sled,Wagon|Vehicle Choice|list|value=string" + $q + ")]`r`n"
				$tp = $tp + "[h,if(listfind(value,skillchoice)==-1):value=listappend(value,skillchoice)]`r`n"		
				$tp = $tp + '[h:atr=setStrProp(atr,"value",value)]' + "`r`n"
				$tp = $tp + '[h:setProperty("Tool Proficiency",atr)]' +"`r`n"
				$tp = $tp + "`r`n"			

			} elseif ($t -eq "vehicles (water)") {
				$tp = $tp + "<!-----------------Choose Vehicle Proficiency------------------->`r`n"
				$tp = $tp + "[h:res=input(" + $q + "skill|Background Water Vehicle Proficiency||label|span=true" +$q + ",`r`n"
				$tp = $tp + $q + "skillchoice|Galley,Keelboat,Longship,Rowboat,Sailing Ship,Warship|Vehicle Choice|list|value=string" + $q + ")]`r`n"
				$tp = $tp + "[h,if(listfind(value,skillchoice)==-1):value=listappend(value,skillchoice)]`r`n"		
				$tp = $tp + '[h:atr=setStrProp(atr,"value",value)]' + "`r`n"
				$tp = $tp + '[h:setProperty("Tool Proficiency",atr)]' +"`r`n"
				$tp = $tp + "`r`n"			

			} else {
				# This tool is NOT a generic tool, so let's compile macro code that grants SPECIFIC proficiency
				$tp = $tp + "<!-----------------" + $t + "Proficiency------------------->`r`n"
				$tp = $tp + "[h:tool=" + $q + $t + $q +"]`r`n"
				$tp = $tp + "[h,if(listfind(value,tool)==-1):value=listappend(value,tool)]`r`n"
				$tp = $tp + '[h:atr=setStrProp(atr,"value",value)]' + "`r`n"
				$tp = $tp + '[h:setProperty("Tool Proficiency",atr)]' +"`r`n"
				$tp = $tp + "`r`n"		
			}
		}	
	}
	
	if ($langChoiceCount -gt 0) {
		$addSave = $true
		$lc = $lc + "`r`n<!-----------------Choose any Standard------------------->`r`n"
		$lc = $lc + "[h:res=input(" + $q +"lang|Background Languages||label|span=true" + $q + ",`r`n"
		for ($i=1; $i -le $langChoiceCount ; $i++) {
			$lc = $lc + $q + "language" + $i + "|Choose one," +$q + "+languages+" +$q + "|Language " + $i + "|list|value=string" + $q +",`r`n"
		}
		$lc = $lc.TrimEnd(",`r`n") + ")]`r`n"
		$lc = $lc + "[h:abort(res)]`r`n`r`n"
		for ($i=1; $i -le $langChoiceCount ; $i++) {
			$lc = $lc + "[h,if(listfind(value,language" + $i +")==-1):value=listappend(value,language" +$i + ")]`r`n"
		}
		$lc = $lc + "`r`n"
	}	
	if ($langChoiceList.count -gt 0) {
		$addSave = $true
		$lc = $lc + "<!-----------------Choose from a Subset of Languages------------------->`r`n"
		$lc = $lc + "[h:res=input(" + $q + "lang|Background Languages||label|span=true" +$q + ",`r`n"
		$lc = $lc + $q + "langchoice|LANGCHOICELIST|Language Choices|list|value=string" + $q + ")]`r`n"
		$lc = $lc + "[h,if(listfind(value,langchoice)==-1):value=listappend(value,langchoice)]`r`n"		
		$lc = $lc + "`r`n"
	}
	if ($forcedLangName -ne "") {
		$lc = $lc + "<!-----------------Know a SPECIFIC Language------------------->`r`n"
		$lc = $lc + "[h,if(listfind(value," + $q + "FORCEDLANGNAME" + $q + ")==-1):value=listappend(value," + $q + "FORCEDLANGNAME" + $q +")]`r`n"		
		$lc = $lc + "`r`n"
	}	
	if ($addSave) {
		$lc = $lc + "<!-----------------Save Languages------------------->`r`n"	
		$lc = $lc + "[h:atr=setStrProp(atr," + $q + "value" + $q + ",value)]`r`n`r`n"
		$lc = $lc + "[h:setProperty(" + $q + "Language Proficiency" + $q + ",atr)]`r`n"
	}
	
	# Let's see if this background grants any kind of starting equipment
	if ($equiplist.count -gt 0) {
		# Common equipment preamble for all backgrounds
		$ec = "`r`n<!-----------------Equipment------------------->`r`n"
		$ec = $ec + '[h:group="Equipment"]' + "`r`n"
		$ec = $ec + '[h:inputList=getLibProperty(group,"Lib:Character")]' + "`r`n"
		$ec = $ec + "[h:inputList=json.fields(inputList)]`r`n"
		$ec = $ec + '[h:inputList=listSort(inputList,"N")]' + "`r`n"
		$ec = $ec + "[h:Property=getProperty(group)]`r`n`r`n"
		$ec = $ec + '[h,if(json.type(Property)=="UNKNOWN"):Property="{}";""]' + "`r`n`r`n"
		$ec = $ec + '[h:AddItem="Add [email protected]:Character Creation"]' + "`r`n`r`n"
		
		# Iterate through the list of equipment.  When necessary, create drop-down lists when a choice is required.
		for ($i=0; $i -lt $equiplist.count; $i++) {
			$it = $TextInfo.ToTitleCase($equiplist[$i])
			$custom = ""
			
			if ($it -eq "holy symbol") {
				# Pick what kind of holy symbol to have
				$ec = $ec + '[h:res=input("holysym|Background Equipment||label|span=true",' + "`r`n"
				$ec = $ec + '"holy|Amulet,Emblem,Reliquary|Holy Symbol|list|value=string")]' + "`r`n"
				$ec = $ec + "[h:abort(res)]`r`n"
				$ec = $ec + '[macro(AddItem):"tokenName="+tokenName+";item="+holy+";Quantity=1;customName="]' + "`r`n"
			
			} elseif ($it -eq "artisan's tools") {
				# Grant the tool that we previously took a proficiency in
				$ec = $ec + '[macro(AddItem):"tokenName="+tokenName+";item="+artisan+";Quantity=1;customName="]' + "`r`n"
			
			} elseif ($it -eq "musical instrument") {
				# Grant the tool that we previously took a proficiency in
				$ec = $ec + '[macro(AddItem):"tokenName="+tokenName+";item="+music+";Quantity=1;customName="]' + "`r`n"
			
			} elseif ($it -eq "gaming set") {
				# Grant the tool that we previously took a proficiency in
				$ec = $ec + '[macro(AddItem):"tokenName="+tokenName+";item="+game+";Quantity=1;customName="]' + "`r`n"
			
			} else {
				# Add a specific named item to inventory
				# We're going to kludge up some code to handle items typically granted in quantity.  We don't have any
				# knowledge of how many to grant, so we'll assume it defaults to 1 but then call out some well-known
				# overrides here.
				$qty = 1
				if ($it.ToLower() -eq "arrow") { $qty = 20 }
				if ($it.ToLower() -eq "torches") { $qty = 10 }
				$ec = $ec + '[macro(AddItem):"tokenName="+tokenName+";item=' + $it + ';Quantity=' + $qty + ';customName="]' + "`r`n"
			}
		}
	}
	
	$sc = $sc + "`r`n<!-----------------Skill------------------->`r`n"
	$sc = $sc + '[h:attributeList=getLibProperty("Skills", "Lib:Character")]' + "`r`n"
	$sc = $sc + "[h:repeat=countStrProp(attributeList)]`r`n"
	$sc = $sc + '[h:skillList=""]' + "`r`n"
	$sc = $sc + '[h,count(repeat,""),code:{' + "`r`n"
	$sc = $sc + "	[h:skillList=listappend(skillList,indexKeyStrProp(attributeList,roll.count))]`r`n"
	$sc = $sc + "}]`r`n`r`n"

	if ($skillChoiceList -ne "") {
		# We have to CHOOSE one or more of our skills
		# $skill1 is always (assumed to be) a single, named skill and therefore isn't relevant for preparing our selection code
		# $skill2 will contain the number of selections to be made (aka the # of linkes in our input statement)
		# $skillChoiceList will contain the CSV list of choices to select from
		
		$sc = $sc + "<!-----------------Skill Selection------------------->`r`n"
		$sc = $sc + '[h:res=input("skill|Skills||label|span=true",' + "`r`n"
		$sc = $sc + '"skillchoice2|Choose one,LISTOFSKILLCHOICES|Skill 2|list|value=string"'

		if ($skill2 -eq 2) {
			# Handle the need to select TWO skills at time of background selection
			$sc = $sc + ",`r`n" + '"skillchoice3|Choose one,"+LISTOFSKILLCHOICES+"|Skill 3|list|value=string")]' + "`r`n"
			$sc = $sc + "[h:abort(res)]`r`n"
			if ($skill1 -eq "") {
				# There were ZERO pre-assigned skills, so we're adding only the two chosen skills
				$sc = $sc + '[h:skill="skillchoice2+","+skillchoice3]' + "`r`n"
			} else {
				# There was ONE pre-assigned skill plus our two choices
				$sc = $sc + '[h:skill="SKILL1,"+skillchoice2+","+skillchoice3]' + "`r`n"
			}
		} else {
			# Handle the need to select ONE skill at time of background selection
			$sc = $sc + ")]`r`n"	
			$sc = $sc + "[h:abort(res)]`r`n"
			if ($skill1 -eq "") {
				# There were ZERO pre-assigned skills, so we're adding only the one chosen skill
				$sc = $sc + '[h:skill=skillchoice2]' + "`r`n" 
			} else {
				# There was ONE pre-assigned skill plus our one choice
				$sc = $sc + '[h:skill="SKILL1," +skillchoice2]' + "`r`n"
			}
		}	
		
	} else {
		# ALL of our background skills are explicitly named
		if ($skill2 -eq "" ) {
			# We are pre-assigned ONE skill.  (Haven't yet come across this IRL, but safety valve...)
			$sc = $sc + '[h:skill="SKILL1"]' + "`r`n"		
		} else {
			# We were pre-assigned TWO skills.
			$sc = $sc + '[h:skill="SKILL1,SKILL2"]' + "`r`n"
		}
	}


	$mt = $macro
	$mt = $mt -replace "BGNAME", $bgName
	$mt = $mt -replace "BGFEATURE", $bgFeature
	$mt = $mt -replace "SKILLCODE", $sc
	$mt = $mt -replace "SKILL1", $skill1
	$mt = $mt -replace "SKILL2", $skill2
	$mt = $mt -replace "LISTOFSKILLCHOICES", $skillChoiceList
	$mt = $mt -replace "LANGCODE", $lc
	$mt = $mt -replace "LANGCHOICELIST", $langChoiceList
	$mt = $mt -replace "FORCEDLANGNAME", $TextInfo.ToTitleCase($forcedLangName)
	$mt = $mt -replace "SKILLCODE", $skillCode
	$mt = $mt -replace "EQUIPCODE", $ec
	$mt = $mt -replace "TOOLCODE", $tp
	$mt = $mt -replace "GOLD", $money
	
	$fname = $devPath,$bgName,".mt" -join ""

	$mt | Out-File "$fname"
	
	
}
$macro = @'
[h:tokenName=macro.args]
[h:id=findToken(tokenName)]
[h:switchToken(id)]


<!-----------------Background------------------->
[h:atr=setStrProp("","value","BGNAME")]
[h:setProperty("Background",atr)]


<!-----------------Set Skills if empty------------------->
[h:skillList=getLibProperty("Skills", "Lib:Character")]
[h:SkillObject=getProperty("Skills")]
[h:array=json.fromList(skillList,";")]
[h:object=""]
[h,if(json.type(SkillObject)=="UNKNOWN"),count(countStrProp(skillList),"<br><br>"),code:{
	[h:skillName=indexKeyStrProp(skillList,roll.count)]
	[h:skillAttribute=indexValueStrProp(skillList,roll.count)]
	[h:object=json.set(object,"name",skillName)]
	[h:object=json.set(object,"prof",0)]
	[h:object=json.set(object,"attribute",skillAttribute)]
	[h:object=json.set(object,"other",0)]
	[r:array=json.set(array,roll.count,object)]
};{}]

[h,if(json.type(SkillObject)=="UNKNOWN"),code:{
	[h:setProperty("Skills",array)]
	[h:SkillObject=array]
};{}]
SKILLCODE
[h:skills=getProperty("Skills")]
[h,count(listcount(skill)),code:{

	[h:currentSkill=listget(skill,roll.count)]
	[h:index=listfind(skillList,currentSkill)]

	[h:chosenskill=json.get(skills,index)]
	[h:value=json.get(chosenskill,"prof")]
	[h:chosenskill=json.set(chosenskill,"prof",if(value>1,value,1))]
	[h:skills=json.set(skills,index,chosenskill)]

}]
[h:setProperty("Skills",skills)]


<!-----------------FEATURE------------------->
[h:group="Feats"]
[h:inputList=getLibProperty(group,"Lib:Character")]
[h:inputList=json.fields(inputList)]
[h:inputList=listSort(inputList,"N")]
[h:Property=getProperty(group)]

[h:Property=json.set(Property,"BGNAME","Background")]
[h:Property=json.set(Property,"BGFEATURE","Background")]
[h:setProperty(group,Property)]
TOOLCODE

<!-----------------Languages------------------->
[h:atr=getProperty("Language Proficiency")]
[h:value=getStrProp(atr,"value")]

[h:languages=getLibProperty("Languages","Lib:Character Creation")]

[h,count(listcount(value),""),code:{
	[h:item=listget(value,roll.count)]
	[h:itemFind=listfind(languages,item)]
	[h,if(itemFind==-1):"";languages=listdelete(languages,listfind(languages,item))]
}]
LANGCODE
EQUIPCODE

<!-----------------Currency------------------->
[h:currentmoney=getProperty("Currency")]
[h:gp=getStrProp(currentmoney,"GP")]
[h:gp=if(gp=="",0,gp)]
[h:currentmoney=setStrProp(currentmoney,"GP",gp+GOLD)]
[h:setProperty("Currency",currentmoney)]
'@

function Get-EquipmentList {
	param ([string]$txt)
	
	# Given a string with embedded equipment as in this form:
	#  "A set of {@item fine clothes|phb}, a {@item disguise kit|phb}, tools of the con of your choice (ten stoppered bottles filled with colored liquid, a set of weighted dice, a deck of marked cards, or a signet ring of an imaginary duke), and a belt {@item pouch|phb} containing 15 gp"
	# return an array of all equipment granted.  Note that the item MUST be represented in $txt with the @item tag.  SO in the example
	# above, the liquid/dice/cards/ring will NOT be part of the array.
	
	# This fancy regex basically strips out all the text OUTSIDE of {}, plus all the @<tag><space> text.  It still leaves the |<tag>
	# text because I can't figure out the secret regex to do that...so I simply brute-force the removal of that at the end.
	## $el = [Regex]::Matches($txt, '(?<={@item )(.*?)(?=\}') | Select -ExpandProperty Value
	$el = @($txt | Select-String '(?<={@\w+ )(.*?)(?=})' -AllMatches |% matches)
	for ($i=0; $i -lt $el.count; $i++) {
		$el[$i] = $el[$i] -replace  "\|.*$", ""		# remove the trailing (e.g.) |phb tag
	}
	return ,$el		#NOTE: the comma before $el forces the function to return arrays ALWAYS, even if only a single element
}

function Get-Money {
	param ([string]$txt)
	# Given a string with embedded equipment as in this form:
	#  "A set of {@item fine clothes|phb}, a {@item disguise kit|phb}, tools of the con of your choice (ten stoppered bottles filled with colored liquid, a set of weighted dice, a deck of marked cards, or a signet ring of an imaginary duke), and a belt {@item pouch|phb} containing 15 gp"
	# return the number of GP granted.
	#$g = [Regex]::Matches($txt, '(?<=containing )(\d+)(?= gp)') | Select -ExpandProperty Value
	$g = ($txt | Select-String '(?<=containing )(\d+)(?= gp)' |% matches)
	
	return $g.value
}

function Key-Exists {
	# Test to see if the specified key exists in our object

	param (	[object]$obj) 

	[bool]($obj.psobject.properties.Count -ne 0)
}

function Quote-Literals {
	# Encode any string literals that have to be protected
	
	param( [string]$txt )
	$txt = $txt -replace '"', '\"'				# protect embedded quotes
	$txt = $txt -replace " d(\d+?)", ' 1d$1'	# replace " dN" with "1dN"
	$txt = $txt -replace " (\d+?d\d+?)",  '[$1](roll \"$1\")'	# encode die rolls
	$txt = $txt -replace "^Prerequisite:(.*)",'*Prerequisite: $1*'	# KLUDGE: not sure why the replacement in Remove-EncodedText not working?
	return $txt
}
	
function Remove-EncodedText {
	param( [string]$link )
	# Remove @<text> through to space (inclusive)
	# Remove { and }
	# Remove anything from first | through to } (inclusive)

	$link = $link -replace "\|\D+?}",""		# Remove ALL lists from <pipe> through end bracket (inclusive)
	$link = $link -replace "{@\D+? ",""		# Remove ALL {@<tag><space>
	$link = $link -replace "}+?",""
	$link = $link -replace "\)\|.*$", ")"	# KLUDGE: handles things like "pole (10-foot)|<blah" by removing from just after the ) to the end
	$link = $link -replace "{@i (.*)}", '*$1*' # Convert italic directive to italic markup

	return ($link)
}

$bgout = "{" 
$otherFeatures = ""		# holds the collection of Features defined in all the backgrounds
$bgList = @()			# list of background=feature; entries

foreach ($bg in $jsondata) { 
	
	# BUG: "Dissenter" has a totally mangled source json record.  Just drop it quietly from our output.
	if ($bg.name -eq "Dissenter") {
		continue
	}
	$bg.name
	
	$bgName = $bg.name 
	$equiplist = @()
	$langChoiceCount = 0	# number of languages this background can select
	$langChoiceList = @()	# in cases where the list of choices is constrained, these are the options
	$forcedLangName = ""	# A specific language we must choose (unless we already know it)
	$tools = @()			# A list of any tools we gain proficiency in as a result of this background
	$toolsChoiceList = @()	# A list of tools we have to choose from
	$skill1 = ""
	$skill2 = ""
	$skillChoiceList = @()
	$money = 0
	
	$out = $q + $bg.name + $q + ":" + $q + "---\n"

	# Code doesn't currently handle backgrounds that have _copy keys, so we're going to skip all those entries
	if ([bool]($bg.PSobject.Properties.name -match "_copy")) {
		$copy = $true
	} else {
		foreach ($item in $bg.entries[0].items) {
			# Sigh.  Once again, the TalDorei json sources are a mess.  Everyone else places "{@skill skilla},{@skill skillb}" into
			# $bg.entries[0].items...but TDCS puts tons of extraneous text here.  So going to use skillProficiencies instead (which
			# I should have done from the start, anyway.

			if ($item.name -eq "Skill Proficiencies") {
				$skills = $bg.skillProficiencies | gm -membertype NoteProperty | foreach-object { "$_" }
				# If one of the skills do NOT start with "bool" then is is some kind of choice string
				if ($skills.count -gt 1) {
					if (($skills[0].StartsWith("bool")) -and ($skills[1].StartsWith("bool"))) {
						# a normal record with two specified skills
						$skill1 = $(Remove-EncodedText $skills[0])

						$skill2 = $(Remove-EncodedText $skills[1])
						$skill2 = $skill2 -replace "bool ", ""
						$skill2 = $skill2 -replace "=true", ""					
						$skill2 = $skill2.Trim()
						$skill2 = $TextInfo.ToTitleCase($skill2)
					} elseif ($skills[0].StartsWith("bool")) {
						# $skills[1] is some kind of non-standard format
						$skill1 = $(Remove-EncodedText $skills[0])						
						$skill2 = 1
					} elseif ($skills[1].StartsWith("bool")) {
						# $skills[0] is some kind of non-standard format
						$skill1 = $(Remove-EncodedText $skills[1])
						$skill2 = 1
					} 

				} else {
					if ($skills.StartsWith("bool")) {
						# OK - there was only a SINGLE explicit skills assignment.
						$skill1 = $(Remove-EncodedText $skills[0])						
					} else {
						# Wow.  The only thing that came back is some kind of list that we're supposed to select from.
						# This is something like a really braindead entry for "Lyceum Student", where we're supposed	
						# to select TWO choices and have NONE granted outright.  
						$skill1 = ""
						$skill2 = 2
					}
				}
				
				if ($skill1 -ne "") {
					# We for back (at least one) explicit skill grant, so clean it up
					$skill1 = $skill1 -replace "bool ", ""
					$skill1 = $skill1 -replace "=true", ""					
					$skill1 = $skill1.Trim()
					$skill1 = $TextInfo.ToTitleCase($skill1)
				}
			
				if ($skill2 -ge 1) {
					# If skill2 is "", then we only had a single explicit skill grant.
					# If skill2 is a number, then at least one of our skill choices must be made at runtime
					if (Key-Exists $bg.skillProficiencies[0].choose.from) {			
						# This is a mangled TalDorei-style entry.  In fact, it is so stupid that we can't detect how many
						# choices are to be made because they called the key "count", and anytime we do <path>.count we don't get
						# the count attribute, we instead get the count of elements under the "from" key.  So HORRIBLE 
						# brute force here.
						$skillChoiceList = $bg.skillProficiencies[0].choose.from					
						$skillChoiceList = $TextInfo.ToTitleCase($skillChoiceList -join ",")
					}				
				} 
				
				$skillList = $skill1
				if ($skillChoiceList -ne "") {
					# If we've got a list, then $skill2 = [int]# of choices to be made...
					# skillList is a text string printed next to Skill Proficiencies in the background header
					# skillChoiceList is a text string containing CSV values used to construct dropdown lists in the macro
					$skillList = $skillList + " plus your choice of "
					if ($skill2 -eq 2) {
						$skillList = $skillList + "two from "
					} else {
						$skillList = $skillList + "one from "
					}
					$skillList = $skillList + $skillChoiceList
				} else {
					#...otherwise, $skill2 is a single, named skill
					$skillList = $skillList + $skill2
				}
				
				$out = $out + "**Skill Proficiencies** " + $skillList + "  \n"
			}

			if ($item.name -eq "Tool Proficiencies") {
				$out = $out + "**Tool Proficiencies** " + $(Remove-EncodedText $item.entry ) + "  \n"
				
				# Figure out what our proficiencies are
				$tools = @($bg.toolProficiencies | gm -membertype NoteProperty | foreach-object { "$_" })
				for ($i=0; $i -lt $tools.count; $i++) {
					# Ugh.  This is SO fuggly...but I don't yet understand how to better deal with NoteProperties, so
					# I'm just brute-forcing my way to an answer.
					$temp = $tools[$i]
					$temp = $temp -replace "bool ", ""
					$temp = $temp -replace "=true", ""
					$tools[$i] = $temp

				}
				# Check to see whether this background has a list of tools you can choose from
				$toolsChoiceList = $bg.toolProficiencies.choose.from

			}
			if ($item.name -eq "Languages") {	
				$out = $out + "**Language** " + $(Remove-EncodedText $item.entry ) + "  \n"
				
				# Do we have any forced languages? 
				# Some entries (notably the TDCS ones like Clasp Member) are missing LanguageProficiencies keys.
				if (-not(Key-Exists($bg.languageProficiencies))) {
					if ($bg.source -eq "TalDorei") {
						for ($i=0; $i -lt $bg.entries[0].count; $i++) {
							# Let's hunt for a Languages tag
							if(Key-Exists $bg.entries[0].items[$i] -and $bg.entries[0].items[$i] -eq "Languages") {
								$temp = $bg.entries[0].items[$i].entry
								if ($temp -eq "Thieves' Cant") { $forcedLangName = "Thieves' Cant" }
								if ($temp -eq "One of your choice") { $langChoiceCount = 1 }
								if ($temp -eq "Two of your choice") { $langChoiceCount = 2 }
							}
						}
					}
				} else {
					$forcedLangName = $bg.languageProficiencies[0].psobject.properties.name
					if ($forcedLangName -eq "anyStandard") {
						# No - we can pick N number of languages.  Example: Acolyte
						$forcedLangName = ""
						$langChoiceCount = $bg.languageProficiencies[0].anyStandard
					} elseif ($bg.languageProficiencies[1].anyStandard -gt 0) {
						# Yes - we can pick N language IF we already know the forced language.  Example: Clan Crafter (24)
						$langChoiceCount = $bg.languageProficiencies[1].anyStandard
					} elseif ($bg.languageProficiencies.choose.from.count -ne 0) {
						# We have a small list from which we can choose languages: Example: Izzet Engineer
						$forcedLangName = ""
						$langChoiceList = $bg.languageProficiencies.choose.from
						$langChoiceList = $TextInfo.ToTitleCase($langChoiceList -join ",")
					}
				}
			} 
			if ($item.name -eq "Equipment") {
				$out = $out + "**Equipment** " + $(Remove-EncodedText $item.entry )  + "\n\n"
				$equiplist = Get-EquipmentList ($item.entry)				
				$money = Get-Money ($item.entry)
				if ($money -le 0) { $money = 0 }
			}
		}

		for($node=1;$node -lt $bg.entries.count;$node++) {
			# Node 0: Proficiencies, languages, equipment
			# Node 1-n: Features, tables, etc.
			foreach ($item in $bg.entries[$node]) {
				if (-not (Key-Exists($item.name)))  {
					# Issue: House Agent has a node that doesn't have a .name entry that will be encountered BEFORE
					# getting to the Feature node.  This causes a "null-valued expression" error but this error is benign.
					continue 
				}
				if ($item.name.StartsWith("Feature: ")) {
					# Issue: House Agent has a node that doesn't have a .name entry that will be encountered BEFORE
					# getting to the Feature node.  This causes a "null-valued expression" error but this error is benign.
					# Create a separate JSON entry for features
					# All entries of this node are part of the feature
					$bgFeature = $item.name.Replace("Feature: ","")
					$otherFeatures = $otherFeatures + $q + $bgFeature + $q + ':"'
					$bgList += ($bg.name + "=" + $bgFeature)
					for($subnode=0;$subnode -lt $item.entries.count;$subnode++) {
						$otherFeatures = $otherFeatures + $(Quote-Literals $item.entries[$subnode]) + "\n\n"	
					}
					$otherFeatures = $otherFeatures + $q + ",`r`n"
					
					#BUG: "Inheritance" has a table in the Features section which isn't correctly handled here.  This
					# makes both Inheritor and Inheritance unusable.
					# BUG: "Trail of the Five Gods" feature has a bunch of fancy stuff...not handled here.
					
				} else {
					# ...otherwise, compile section header...
					$out = $out + "#### " + $item.name + "\n\n"

					# Huge kludge.  $item is often a mix of raw strings + complex objects (like tables).  The
					# raw strings are hard to figure out, so basically we look at all items here and capture those
					# that are of type string, leaving the things like tables for other processes.
					
					# BUG: Izzet Engineer has an entry node with text, table, and text.  The 2nd batch of text is 
					# *supposed* to appear AFTER the table is rendered, but currently appears BEFORE (and also messes
					# up the table).  May need to stop looking for more strings as soon as we find our first table,
					# and then continue looking for strings after we render each table.
					for ($i=0 ; $i -lt $item.entries.count; $i++) {
						if ($item.entries[$i] -is [string]) {
							$out = $out + $(Quote-Literals $(Remove-EncodedText($item.entries[$i]))) + "\n\n"	
						}
					}
					
					# ...and then check for table entries			
					$HAkludge = $false
					foreach ($tbl in $bg.entries[$node].entries) {

						# Kludge: House Agent has a table in a non-standard place that we don't normally look at
						# If doing house agent, the first time through this loop look at our non-standard table
						# NOTE: This table will appear later in our output than in the original source material, but
						# the diff is minor and I can live with it.
						if ($bg.name -eq "House Agent" -and -not $HAkludge) {
							$tbl = $bg.entries[1]
							$HAkludge = $true
						}
						
						if ($tbl.type -eq "table") {			

							if ($tbl.caption -ne $null) {
								$out = $out + "##### " + $tbl.caption + "\n\n"
							}
							
							# Column headers
							$out = $out + "|[" + $tbl.colLabels[0] + "](roll \" + $q + "1" + $tbl.colLabels[0] + "\" + $q + ")|" + $tbl.colLabels[1] + "|\n"
							$out = $out + "|:---:|---|\n"
							
							# Table rows
							foreach ($row in $tbl.rows) {
								$out = $out + "|" + $row[0] + "|" + $(Quote-Literals($(Remove-EncodedText $row[1]))) + "|\n"							
							}
							$out = $out + "\n"
						}
					}
					
				}
				
			}
		}

		$bgout = $bgout + $out + $q + ","
		$bgout = $bgout + "`r`n"
		
	}
	Make-Macro
}
$bgout = $bgout + $otherFeatures.TrimEnd(",`r`n") + "`r`n}"
$bgout | Out-File $dstFile 
Write-Host "`r`n"
Write-Host "Done!  Copy/paste the following string of text into the framework Settings-->Background entry:`r`n"
$(($bgList | Sort-Object) -join ";")





User avatar
Rukbat
Kobold
Posts: 4
Joined: Fri May 01, 2020 6:13 am

Re: Rod's D&D 5e Framework

Post by Rukbat »

FIrst of all, I would like to thank you Rod for making this framework, it looks great!

I have been able to mass import spells with the Import function. I'd rather not share my file as it is not SRD-compliant, but there is nothing against sharing the method I used!
I first exported the spell list from maptools to see the format they needed to be in (I know nothing about java, I should mention).
I then downloaded the complete spellcards from dnd-spells[dot]com, pasted the text from the pdf into a txt, and with some regex magic in notepad++ formatted them as needed, including the dice roll macros.
Tips: there are some " in the text of a few spells that will need to be escaped. Also I first put in all the \n signs and then did a mass unwrap text (using the TextFX plugin). Last but not least, the whole thing is too big for the import box and it crashed maptool. I had to import one spell level at a time.
This took me a couple hours, but some of that time was wasted figuring out why certain sections would not import (because of the unescaped " in the text).

Now I would like to import subclasses and backgrounds. I started with subclasses and the artificer by pasting them into the "Classes" bit and hit a snag. While the link to their entries were created in the library, no macro was created in the tables lib, and if I click on the new entries I get this error:
Unknown macro "Path of the Ancestral [email protected]:Tables".
Unknown macro "[email protected]:Tables".

User avatar
Rukbat
Kobold
Posts: 4
Joined: Fri May 01, 2020 6:13 am

Re: Rod's D&D 5e Framework

Post by Rukbat »

I am trying to create new NPCs from scratch using a token, but if the variant rules option is checked, normal features (the ones that go with the feats field) are not shown and are entirely replaced by the variant rules. Not sure whether this is a bug or intentional?

User avatar
Rukbat
Kobold
Posts: 4
Joined: Fri May 01, 2020 6:13 am

Re: Rod's D&D 5e Framework

Post by Rukbat »

Well, I have finally finished editing my collection of SRD monsters for this framework, though I can't seem to be able to import more than one at a time.
Of course, that could be caused by a buggy one in the mix.

Anyway, I'll post it here, hope it can be of use to someone :mrgreen:

BIG BIG WARNING:
Do NOT paste the whole thing into an import window. It will likely crash your Maptool.
(That happened to me when I tried to mass import my spell list, which was a little smaller.)
I wanted to divide it into manageable chunks, but actually my maptool is not letting me import more than one monster at once. It's not crashing, but it just won't do it. And I'd rather not create 320 files, though other users are welcome to do so if they have the patience. :mrgreen:

Disclaimer: to speed up the process, every stat block has all the options ticked (such as Damage Resistance, etc.). They can be turned off manually from within the framework.
Dice rolls and spell links that I have tested worked, there could be the odd buggy one in the mix, but they should work. Of course, the spell links only work if you have the spells loaded and with the exact same title as mine, but they should be easy enough to correct if your spell list is different. (and let me tell you, I thoroughly regretted the decision to include full spell links about halfway through, but I decided not to quit and here we are)
SRDmonsters-final.zip
(77.9 KiB) Downloaded 15 times

User avatar
wolph42
Deity
Posts: 9706
Joined: Fri Mar 20, 2009 5:40 am
Location: Netherlands
Contact:

Re: Rod's D&D 5e Framework

Post by wolph42 »

what i did was cram everything in excel, clean it up and then created json code to in excel and copy pasted the lot in a macro: run that and voila, you have a complete library. Here's the file as reference:
https://www.dropbox.com/s/b441wgxmwre1v ... .xlsx?dl=1
have a look at the tabs like: weapons, armour, talents, etc.

tarduini
Kobold
Posts: 1
Joined: Tue May 05, 2020 7:53 pm

Re: Rod's D&D 5e Framework

Post by tarduini »

Fixed a couple of json issues in Rukbat's excellent monster library. You should now be able to import the entire json file into Rod's beastiary library. :D
SRDmonsters-final v2.zip
(77.9 KiB) Downloaded 21 times

OokOok
Kobold
Posts: 15
Joined: Wed Nov 13, 2019 12:27 am

Re: Rod's D&D 5e Framework

Post by OokOok »

This PowerShell script will convert the entire 5etools item files available in the downloaded 5etools source into a JSON string that can be directly pasted into the Import field of Rod's item database. The two files are in the data subdir. I was able to fully convert and import 777 items with full support of all layout features. I did not hyperlink spells or monster, although that wouldn't be that hard to do. I've commented the code so you can hopefully make whatever changes you want. My recommendation would be to run the program twice with these args:
Import-Equipment base $true --> cut/paste the contents of the output file into the FW, then repeat as follows:
Import-Equiment all $true

This will convert all NON-WONDROUS items, which I think are best left out of the FW inventory. You can introduce selected wondrous items as needed. (Passing $false will also convert wondrous items.) You can optionally add a 3rd argument matching one of the acceptable itemType values. For example, adding WD as a third arg would convert ONLY wands. Here are the acceptable type codes:

Code: Select all

Types:
	$		Treasure				
	A		Ammunition					
	AIR		Vehicle (air)				
	AT 		Artisan's tool				
	G		Adventuring gear			
	GS 		Gaming set					
	HA 		Heavy armor					
	INS 		Instrument					
	LA 		Light armor					
	M 		Melee weapon				
	MA 		Medium armor				
	MNT		Mount						
	OTH		Other						
	P 		Potion						
	R 		Ranged weapon				
	RG 		Ring						
	S 		Shield						
	SCF		Spellcasting focus			
	SHP		Vehicle (water)				
	T 		Tool						
	TAH 		Tack and harness			
	TG		Trade good					
	VEH		Vehicle (land)	
	WD		Wand


Note that magical items are often NOT tagged as wondrous. So even with the $true flag you're going to get a LOT of magic items. I suggest looking over the output and deleting the ones you don't want prior to importing to the FW.

Note that the items schema is the most complex in 5etools. You can't just export item JSON from 5etools, which only exports a subset of what's needed. An item might need access to several different JSON structures, including baseItem, item, itemType, itemProperties, and/or itemTypeAdditionalEntries. 5etools only downloads the baseItem or item table entries. So if you want to import your own homebrew items you'll need to study the data files distributed by 5e tools and structure your JSON like that. (It may be easier/faster to hand-enter your homebrew rather than try to coerce it into acceptable JSON.)

Code: Select all

param(
	[string]$itemList = "all",			# Spec whether to import "base" (aka $baseFile) or "all" (aka $allFile)
	[string]$noWondrous = $false,	# if you set this to $true, no Wondrous items will be converted.
	[string]$typeFilter = "" 		# you can (optionally) spec an item type to output only those finds of items.
									# for example, adding "HA" will output ONLY Heavy Armor.

)

if (($itemList -ine "base") -and ($itemList -ine "all")) {
	Write-Host 'Missing itemList spec: your first arg must be either "base" or "all"'
	Write-Host '	"base" will import the contents of $baseFile, while'
	Write-Host '	"all"  will import the contents of $allFile'
	exit
}

$devPath = "C:\Users\dstei\OneDrive\Projects\MapTool\Rod's 5E Framework\Tools\"
$allFile = $devPath + "ALL-items.json"				# Normally, the 5etools master items.json file
$baseFile = $devPath + "ALL-items-base.json"		# Normally, the 5etools items-base.json file
$dstFile = $devPath + "parsed-equipment.json"		# Copy/paste the contents of this file into Rod's FW


$TextInfo = (Get-Culture).TextInfo
$q = '"'			# literal quote

$all = Get-Content -RAW -Path $allFile -Encoding UTF8 | ConvertFrom-Json 
$base = Get-Content -RAW -Path $baseFile -Encoding UTF8 | ConvertFrom-Json 

$baseItem = $base.baseItem
$itemProperty = $base.itemProperty
$itemType = $base.itemType
$itemTypeAdditionalEntries = $base.itemTypeAdditionalEntries

$allItem = $all.item
$allItemGroup = $all.itemGroup


function Key-Exists {
	# Test to see if the specified key exists in our object

	param (	[object]$obj ) 

	[bool]($obj.psobject.properties.Count -ne 0)
}


function Lookup-DamageType {
	# Given a damage type attribute, return the full name of the attribute
	
	param ( [string]$dmgAttr )
	
	$private:ret = "dmgTypeNotFound"
	
	# dmgTags and dmgTypes MUST be listed in the same order!!
	$dmgTags = "ABCFOLNPIYRST"
	[email protected]("acid","bludgeoning","cold","fire","force","lightning","necrotic","piercing","poison","psychic","radiant","slashing","thunder")
	
	$idx = $dmgTags.indexOf($dmgAttr)
	if ($idx -ne -1) {
		$ret = $dmgTypes[$idx]
	}
	
	return $ret
}	

function Lookup-ItemType {
	# Given a parent item record, lookup the type and return back a string
	# that describes the type.  We may have to consult multiple keys of the
	# parent record to figure out the final result.
	
	param ( [object]$obj )
	
	$private:ret = ""
	
	if (-not(Key-Exists $obj)) {
		"Lookup-ItemType: $obj isn't a valid item key."
	} elseif (-not(Key-Exists $obj.Type)) {
		# NOTE TO SELF:  This _copy clause works as expected...but I'm having problems with _copy
		# in other places so I'm going to globally skip _copy records for now.  Later, when I 
		# decide to work on _copy I should NOT need to worry about this code
		if (Key-Exists $obj._copy) {
			# Find the index number for the master record and then look that up instead
			$tmp = $($masterList.name).IndexOf($obj._copy.name)
			if ($tmp -ne -1) {
				Lookup-ItemType $masterList[$tmp]
			} else {
				"Lookup-Item: _copy can't find original item"
			}
		
		# Many items, particularly Wondrous items and staves, don't have .type keys
		} elseif ((Key-Exists $obj.wondrous) -and ($obj.wondrous)) { 
			$ret = $ret + "wondrous item, " 

		} elseif ((Key-Exists $obj.staff) -and ($obj.staff)) { 
			$ret = $ret +  $ret + "staff, " 

		# Handle a bunch of one-off exceptions
		} elseif ($obj.name -ieq "Blood of the Lycanthrope (Injury)") { #noop ; 
		} elseif ($obj.name -ieq "Dust of the Mummy (Inhaled)") 		 { #noop ; 
		} elseif ($obj.name -ieq "Thessaltoxin (Ingested or Injury)") { #noop ;
		} else {
			"Lookup-ItemType: The passed object doesn't have a .Type key."
		}

	} else {
	
		# the following designation take precendence over $obj.Type, and apply in the indicated order
		if ((Key-Exists $obj.wondrous) -and ($obj.wondrous)) 	{ $ret = $ret + "wondrous item, " }
		if ((Key-Exists $obj.staff) -and ($obj.staff)) 			{ $ret = $ret +  $ret + "staff, " }

		# lookup the basic item type
		switch -exact ($obj.Type)  {
			"A"  { $ret = $ret + "ammunition, " ; break }
			"AF" { 
					if (Key-Exists $obj.age) {
						if ($obj.age -ieq "futuristic") {
							$ret = $ret + "futuristic, ammunition, " 
						} elseif ($obj.age -ieq "modern") {
							$ret = $ret + "modern, ammunition, "
						} elseif ($obj.age -ieq "renaissance") {
							$ret = $ret + "renaissance, ammunition, "
						} else {
							$ret = $ret + "unknown type ammunition, "
						}
					} else {
						$ret = $ret + "ammunition, "
					}
					break 
				 }
			"AIR"{ $ret = $ret + "vehicle (air), " ; break }
			"AT" { $ret = $ret + "artisan's tool, "; break }
			"G"  { $ret = $ret + "adventuring gear, " ; break }
			"EXP"{
					if (Key-Exists $obj.age) {
						if ($obj.age -ieq "futuristic") {
							$ret = $ret + "futuristic, explosive, " 
						} elseif ($obj.age -ieq "modern") {
							$ret = $ret + "modern, explosive, "
						} elseif ($obj.age -ieq "renaissance") {
							$ret = $ret + "renaissance, explosive, "
						} else {
							$ret = $ret + "unknown type explosive, "
						}
					} else {
						$ret = $ret + "explosive, "
					}
					break 
				 }
			"GS" { $ret = $ret + "gaming set, " ; break }
			"HA" { $ret = $ret + "heavy armor, " ; break }
			"M"  {
					if (Key-Exists $obj.weaponCategory) {
						if ($obj.weaponCategory -ieq "martial") {
							$ret = $ret + "martial weapon, melee weapon, "
						} elseif ($obj.weaponCategory -ieq "simple") {
							$ret = $ret + "simple weapon, melee weapon, "
						} elseif ($obj.weaponCategory -ieq "exotic") {
							$ret = $ret + "exotic weapon, melee weapon, "
						} else {
							$ret = $ret + "unknown type, melee weapon, "
						}
					} else {
						$ret = $ret + "melee weapon, "
					}
					break

				 }
			"INS"{ $ret = $ret + "instrument, " ; break }
			"LA" { $ret = $ret + "light armor, " ; break }
			"MA" { $ret = $ret + "medium armor, " ; break }
			"MNT"{ $ret = $ret + "mount, " ; break }
			"OTH"{ $ret = $ret + "other, " ; break }
			"P"	 { $ret = $ret + "potion, " ; break }
			"R"  { 
					if (Key-Exists $obj.weaponCategory) {
						if ($obj.weaponCategory -ieq "martial") {
							$ret = $ret + "martial weapon, ranged weapon, "
						} elseif ($obj.weaponCategory -ieq "simple") {
							$ret = $ret + "simple weapon, ranged weapon, "
						} elseif ($obj.weaponCategory -ieq "exotic") {
							$ret = $ret + "exotic weapon, ranged weapon, "
						} else {
							$ret = $ret + "ranged weapon, "
						}
					}
					break
				 }			
			"RD" { $ret = $ret + "rod, " ; break }
			"RG" { $ret = $ret + "ring, " ; break }
			"S"  { $ret = $ret + "shield, " ; break }
			"SC" { $ret = $ret + "scroll, " ; break }
			"SCF"{ $ret = $ret + "spell casting focus, " ; break }
			"SHP"{ $ret = $ret + "vehicle (water), " ; break }
			"T"  { $ret = $ret + "tool, " ; break }
			"TAH"{ $ret = $ret + "tack and harness, " ; break }
			"TG" { $ret = $ret + "trade good, " ; break }
			"VEH"{ $ret = $ret + "vehicle (land), " ; break }
			"WD" { $ret = $ret + "wand, " ; break }
			"$"  { $ret = $ret + "treasure, " ; break }
			Default { "Didn't kind a match for type $obj.Type in function Lookup-ItemType" }
		}
		
	}
	
	# Check for additional modifiers
	if ((Key-Exists $obj.poison) -and ($obj.poison)) { $ret = $ret +  $ret + "poison, " }
	
	if ((Key-Exists $obj.tier) -and ($obj.tier)) { $ret = $ret + $obj.tier + " tier, " }
	If (($obj.rarity -ne "none") -and ((-not($obj.rarity.StartsWith("unknown"))))) { $ret = $ret + $obj.rarity + ", " }
	if (Key-Exists $obj.reqAttune) { 
		if ($obj.reqAttune -ieq "true") {
			# If requires attunement, remove the extraneous punctuation before noting this
			$ret = $ret.TrimEnd(", ") + " (requires attunement), "
		} else {
			$ret = $ret.TrimEnd(", ") + " (requires attunement " + $obj.reqAttune + "), " 
		}
	}

	# capitalize the first letter, and remove the extraneous trailing punctuation
	$ret = $ret.substring(0,1).toupper()+$ret.substring(1).tolower()   
	$ret = $ret.TrimEnd(", ")
	
	return $ret
}

	
function Process-Entries {
	# Given a key of .entries, process through and dispatch to various decoding functions
	
	param ( [object]$obj )
	
	$private:ret = ""
	
	for ($i=0; $i -lt $obj.count; $i++ ) {
		if (-not(Key-Exists $obj[$i].type)) {
			# When no type is present it is because we have a string element in our .entries array. 
			$ret = $ret + $(Process-Strings $obj[$i])
			
		} elseif ($obj[$i].type -ieq "list") {
			$ret = $ret + $(Process-List $obj[$i])

		} elseif ($obj[$i].type -ieq "table") {
			$ret = $ret + $(Process-Table $obj[$i])
			
		} elseif ($obj[$i].type -ieq "inset") {
			$ret = $ret + "``````\n"			# insert 3 back-ticks (have to escape each one)
			if (Key-Exists $obj.name) {
				$ret = $ret + "##### " + $obj.name + "\n\n"
				if (Key-Exists $obj.entries) {
					$ret = $ret + $(Process-Entries $obj[$i].entries)
				}
			}
			$ret = $ret.TrimEnd("\n") + "\n``````"
			
		} elseif (Key-Exists $obj[$i].name) {
			# A name key is normally printed in bold italics before one (or more) paragraphs of text.
			# Collective, this text describes some kind of special feature of the object.
			$ret = $ret + $(Process-Feature $obj[$i])

		} else {
			Write-Debug "Unhandled subkey in Process-Entries"
		}
		
	}
	
	return $ret
}


function Process-Feature {
	# Given a key of .entries[x], where a .name subkey exists, process a feature.
	# A feature starts with .name in old italics, and is followed by lines of text.
	# .name is printed ONLY once, at the start of the text.
	
	param ( [object]$obj )
	
	$private:ret = ""

	# Ideally, the following line should work here...but it didn't so I just
	# gave up and put it later.
	#$ret = $ret + "***" + $obj.name + ".*** "		# markdown for bold-italic name<period><space>

	if (Key-Exists $obj.entries) {
		# Recurse if necessary
		$ret = $ret + $(Process-Entries $obj.entries $ret)
 	} else {
		Write-Debug "Unhandled subkey in Process-Feature"
	}

	# I used to do the obvious and insert this bold-italic sequence at the
	# start of this function...but for reasons I still haven't figured out
	# it kept doubling the tag, no doubt due to recursion issues.  I 
	# finally just gave in and stuffed it into the front of the return
	# string at the end here and this made all the difference.
	$ret = "***" + $obj.name + ".*** " + $ret		# markdown for bold-italic name<period><space>
	
	# Originally retured $ret + "  \n\n"...but prior calls take care of this already.
	return $ret 
}


function Process-List {
	# Given a key of .entries[x], all items in .entries[x].items are 
	# expected to be list elements.  Add bullets to them
	
	param ( [object]$obj )
	
	$private:ret = ""

	for ($i=0; $i -lt $obj.items.count; $i++) {
	
		# List bullets are separated by a single \n
		$ret = $ret + "- " + $(Quote-Literals $(Remove-Tags $obj.items[$i])) + "\n"
	}
	return $ret + "\n"		# The last bullet needs a 2nd \n
}


function Process-Properties {
	# Given an item, see if it has properties
	
	param ( [object]$obj )
	
	$private:ret = ""
	$private:skipBase = $false		# if $true, we don't check baseItem
	
	# An item might have properties coming from up to two different places.
	# A non-baseItem might have properties.  If so, we print those and IGNORE
	# the baseItem; otherwise, we check the baseItem and then print those if found.
	
	# We start by checking the object we PASSED INTO THIS function.
	# This MIGHT be a baseItem, but we don't care right now.
	if (Key-Exists $obj.property) {
		# This object has properties, so don't bother looking at the baseItem list later.
		# This assumes that the specific object ALWAYS has a complete list of properties
		# which supercede the generic item properties.  (If properties are supposed to be
		# additive, then this logic is wrong.)

		$skipBase = $true	
		for ($i=0; $i -lt $obj.property.count; $i++) {
			$tmp = $itemProperty.abbreviation.IndexOf($obj.property[$i])
			if ($tmp -ne -1) {
				for ($j=0; $j -lt $itemProperty[$tmp].entries.count; $j++) {
					$ret = $ret + $(Process-Feature $itemProperty[$tmp].entries[$j]).TrimEnd("\n") + "\n\n"
				}
			}
		}
	}
	
	# NOW we look at the baseItem, but only if we didn't find any properties when
	# checking the master item.
	if (-not($skipBase) -and (Key-Exists $obj.baseItem)) {
		# strip off the |<text> from our value before doing the lookup
		$tmp = $baseItem.name.IndexOf($obj.baseItem -replace "\|.*$", "")
		if ($tmp -ne -1) {
			$ret = $ret + $(Process-Properties $baseItem[$tmp]) 
		}
	}	
	
	return $ret.TrimEnd("\n") + "\n\n"
}


function Process-Strings {
	# Given a key of .entries[x], all children are expected to be strings.  Process them.
	
	param ( [object]$obj )
	
	$private:ret = ""	
	
	$ret = Remove-Tags $obj
	$ret = Quote-Literals $ret
	
	return $ret  + "\n\n"
}
	
	
function Process-Table {
	# Given a key of .type = "table", built a markdown table

	param ( [object]$obj )
	
	$private:ret = "|"
	
	# Check for a table caption
	if (Key-Exists $obj.caption) {
		# If we have a caption, throw away the table header we initialized into $ret and 
		# put our caption as the first entry in $ret instead...then tack on the | mark.
		$ret = "### " + $obj.caption + "\n|"
	}

	# Decode the table headers
	for ($i=0; $i -lt $obj.colLabels.count; $i++ ) {
		$ret = $ret + $(Quote-Literals $(Remove-Tags $obj.colLabels[$i])) + "|"	
	}
	$ret = $ret + "\n|"
	
	# Decode the column formatting
	for ($i=0; $i -lt $obj.colStyles.count; $i++ ) {
		if ($obj.colStyles[$i] -match "left") {
			$ret = $ret + ":---|"
		} elseif ($obj.colStyles[$i] -match "right") {
			$ret = $ret + "---:|"
		} elseif ($obj.colStyles[$i] -match "center") {
			$ret = $ret + ":---:|"
		} else {
			$ret = $ret + "---|"
		}
	}
	$ret = $ret + "\n|"
	
	# Decode the rows
	for ($i=0; $i -lt $obj.rows.count; $i++) {	
		for ($j=0; $j -lt $obj.rows[$i].count; $j++) {
			if (Key-Exists $obj.rows[$i][$j].type) {
				$tmp1 = ""
				$tmp2 = ""
				if ($obj.rows[$i][$j].type -ieq "cell") {
					if (Key-Exists $obj.rows[$i][$j].roll.exact) { $tmp1 = $obj.rows[$i][$j].roll.exact.ToString() }
					if (Key-Exists $obj.rows[$i][$j].roll.entry) { $tmp2 = $obj.rows[$i][$j].roll.entry }
					$tmp3 = $tmp1,$tmp2 -join " or "
					if ($tmp3.length -gt 4) { $tmp3 = $tmp3.Trim(" or ") }
					$ret = $ret + $tmp3 + "|"
				}
			} else {
				$ret = $ret + $(Quote-Literals $(Remove-Tags $obj.rows[$i][$j])) + "|"
			}
		}
		$ret = $ret +  "\n|"
	}
	
	# Remove the last extraneous table delimiter and as many \n as are there, then add back 2x \n
	return $ret.TrimEnd("|\n") + "|\n\n"
	
}

function Process-TypeFeatures {
	# Given an item, determine whether it has any generic type features and add them to the text block.
	
	param ( [object]$obj )
	
	$private:ret = ""
	
	if (Key-Exists $obj.type) {
		# Lookup this item's type in the itemType array.  If found, we have some type-specific text.
		# There's a problem in the base data files.  The type "AIR" SHOULD have an entry in the itemType
		# table that duplicates the "SHP" itemType...but it is missing.  So we have to brute-force it
		# here.
		if ($obj.type -ieq "AIR" ) {
			$tobjtype = "SHP"
		} else {
			$tobjtype = $obj.type
		}
		$tmp = $itemType.abbreviation.IndexOf($tobjtype)
		if ($tmp -ne -1) {
			#for ($i=0; $i -lt $itemType[$tmp].entries.count; $i++) {
				$ret = $ret + $(Process-Entries $itemType[$tmp].entries)
			#}
		}
		
		# Some items (e.g. "AT" artisan tools like Alchemist's Supplies) have (internal)
		# additional entries which need processing.  This check MIGHT need to happen
		# earlier/later than TypeFeature checking.  TBD.
		if (Key-Exists $obj.additionalEntries) {
			$ret = $ret + $(Process-Entries $obj.additionalEntries)
		}
		
		# Some item types also have (external) additional entries which need processing
		$tmp = $itemTypeAdditionalEntries.appliesTo.IndexOf($obj.type)
		if ($tmp -ne -1) {
			$ret = $ret + $(Process-Entries $itemTypeAdditionalEntries[$tmp].entries) + "\n\n"
		}
	}
	
	return $ret			# don't tack on any add'l \n
}

	
function Process-WeaponDmg {
	# Given an (weapon) item, build up the damage string that appears above the horizontal rule
	
	param ( [object]$obj )
	
	$private:ret = ""
	
	# Weapon damage strings are in the format of <dmg> - <properties>.  In other words, there'same
	# a "before the dash" and "after the dash" layout.  Let's start with before the dash, which
	# is reasonably simple.
	
	if (Key-Exists $obj.dmg1) {
		$dmg1 = $obj.dmg1
	}
	if (Key-Exists $obj.dmgType) {
		$dmgType = Lookup-DamageType $obj.dmgType
	}
	$tmp = $dmg1,$dmgType -join " "
	$tmp = $tmp.Trim(" ")
	
	# Add the dash
	$ret = $(Quote-Literals $(Remove-Tags $tmp)) + " - "
	
	# Do post-dash stuff
	# Essentially, we lookup .property vs the itemProperty table in the order we encounter them
	# and keep tagging on $bi.entries[0].name onto our string.  EXCEPTION: when "S" (special) OR
	# the abbreviation is an entry that includes a template, we have to override default behavior
	# and handle the output specially.
	
	if (Key-Exists $obj.property) {
		for ($i=0; $i -lt $obj.property.count; $i++) {
			if ($obj.property[$i] -eq "S") {
				$ret = $ret + "special, "
			
			} elseif ($obj.property[$i] -eq "A" -or $obj.property[$i] -eq "AF") {
				$ret = $ret + "ammunition (" +$obj.range + " ft.), "
				
			} elseif ($obj.property[$i] -eq "RLD") {
				$ret = $ret + "reload (" +$obg.reload + " shots), "			
				
			} elseif ($obj.property[$i] -eq "T") {
				$ret = $ret + "thrown (" +$obj.range + " ft.), "
				
			} elseif ($obj.property[$i] -eq "T") {
				$ret = $ret + "versatile (" +$(Quote-Literals $(Remove-Tags $obj.dmg2p)) + "), "
				
			} else {
				# Whatever property tag we have doesn't have any output template, so let'same
				# just look it up in the itemProperty table
				$tmp = $itemProperty.abbreviation.IndexOf($obj.property[$i])
				if ($tmp -ne -1) {
					if (Key-Exists $itemProperty[$tmp].entries[0].name) {
						$ret = $ret + $itemProperty[$tmp].entries[0].name.ToLower() + ", "
					} else {
						$ret = $ret + $tmp + ":undefined, "
					}
				}
			}
		}
	}

	return $ret.TrimEnd(", ") + "\n"
}

function Quote-Literals {
	# Encode any string literals that have to be protected
	
	param( [string]$txt )

	$txt = $txt -replace '"', '\"'								# protect embedded quotes
	$txt = $txt -replace " d(\d+?)", ' 1d$1'					# replace " dN" with " 1dN"
	# The following replaces many formats of dice rolling strings with encoded strings
	# Handles 1d10, 10d10, 1d10+5, 10d10 + 5, 1d10-5, 10d10 + 5
	# Regex testing tool https://regexr.com/3d70r
	$txt = $txt -replace "(\d+)?d(\d+) ?([\+\-] ?\d+)?", '[$1d$2 $3](roll \"$1d$2 $3\") '
	
	$txt = $txt -replace "^Prerequisite:(.*)",'*Prerequisite: $1*'	# KLUDGE: not sure why the replacement in Remove-EncodedText not working?

	return $txt
}
	

function Remove-Tags {
	# Remove @<text> through to space (inclusive)
	# Remove { and }
	# Remove anything from first | through to } (inclusive)

	param( [string]$txt )

	$txt = $txt -replace "\|\D+?}",""		# Remove ALL lists from <pipe> through end bracket (inclusive)
	$txt = $txt -replace "{@\D+? ",""		# Remove ALL {@<tag><space>
	$txt = $txt -replace "}+?",""
	$txt = $txt -replace "\)\|.*$", ")"	# KLUDGE: handles things like "pole (10-foot)|<blah" by removing from just after the ) to the end
	$txt = $txt -replace "{@i (.*)}", '*$1*' # Convert italic directive to italic markup
	return $txt
}




#===================================
#=              MAIN 			   =
#===================================


$jo = "{"			# our JSON output string.  We start with our opening bracket.

# select which file we're importing
if ($itemList -ieq "base") {
	$masterList = $baseItem
} else {
	$masterList = $allItem
}

#===== Convert base items first
for ($i=0; $i -lt $masterList.count; $i++) {
	$bi = $masterList[$i]

	$cost = 0		# general work variable
	$weight = 0		# general work variable

	
	if (($typeFilter -ne "") -and ($bi.type -ne $typefilter)) {
		# If we're filtering and the current record isn't one we care about...skip
		continue
	} elseif (Key-Exists $bi._copy ) {
		# For now, skip the 6-10 items that are copies of other items.  Too much effort!
		continue
	} else {
	# Let's try to convert this elements
	
	# if this is a Wondrous item and we're supposed to skip those, move to the next item
	if ($noWondrous -and ((Key-Exists $bi.wondrous) -and ($bi.wondrous))) { "Skipping Wondrous item: " + $bi.name ; continue }
	
	"Converting: " + $bi.name
	

	# Every item starts the same: "Item.name":"*Item.type*  \nItem.value gp, Item.weight lb.  \n"
	$tmp = $bi.name -replace ",", ""
	$jo = $jo + $q + $tmp + $q + ":" + $q			# create "key":"   (we leave an open quote hanging)
	$jo = $jo + "\n*" + $(Lookup-ItemType $bi) + "*  \n"
	
	# Get item cost and weight (if they exist)
	$value = ""
	if (Key-Exists $bi.value) {
		$value = ($bi.value)
		if ($value -ge 100) {
			$value = $($value/100).ToString("N0") + " gp"
		} elseif ($value -ge 10)  {
			$value = $($value/10).ToString("N0") + " sp"
		} else {
			$value = $($value).ToString("N0") + " cp"
		}
	}
	
	$weight = ""
	if (Key-Exists $bi.weight) {
		# We have a weight
		$weight = $bi.weight.ToString() + " lb."
	}

	# Build our cost/weight string
	$tmp = $value,$weight -join ", "
	if ($tmp.length -gt 2) {
		$tmp = $tmp.Trim(", ")
		$tmp = $tmp.substring(0,1).toupper()+$tmp.substring(1).tolower() 
		$jo = $jo + $tmp + "  \n"
	}	

	# Some items have additional lines before the HR.  Look for them here.
	# Armor: has AC notes
	if (Key-Exists $bi.ac) {
		$jo = $jo + "AC " + $bi.ac + "\n"
		if ($bi.type -ieq "MA") { $jo = $jo.TrimEnd("\n") + " + Dex (max 2)  \n" }
		if ($bi.type -ieq "LA") { $jo = $jo.TrimEnd("\n") + " + Dex  \n" }
	}
	
	# Melee and ranged weapons have damage (and range) information
	if ((Key-Exists $bi.type) -and ($bi.type -ieq "M" -or $bi.type -ieq "R")) {
		$jo = $jo + $(Process-WeaponDmg $bi)
	}
	
	# Mounts: Have speed and carrying capacity
	if ((Key-Exists $bi.type) -and ($bi.type -ieq "MNT")) {
		$tmp = ""
		if (Key-Exists $bi.speed) { $tmp = $tmp + "speed: " + $bi.speed + ", " }
		if (Key-Exists $bi.carryingCapacity) {$tmp = $tmp + "carrying capacity: " + $bi.carryingCapacity + ", " }
		$tmp = $tmp.TrimEnd(", ")
		if ($tmp.length -ge 2) {
			$tmp = $tmp.substring(0,1).toupper()+$tmp.substring(1).tolower() 
			$jo = $jo + $tmp + "  \n"
		}
	}
	
	# Shields: Have AC modifiers
	if ((Key-Exists $bi.type) -and ($bi.type -ieq "S")) {
	
		if (Key-Exists $bi.ac) {
			if ($bi.ac -ge 0) { 
				$jo = $jo + "AC +" + $bi.ac + "  \n"
			} else {  
				$jo = $jo + "AC " + $bi.ac + "  \n"
			}
		}
	}
	
	# Tack and Harness: Have multipliers
	if ((Key-Exists $bi.type) -and ($bi.type -ieq "TAH")) {
		$tmp = ""
		if (Key-Exists $bi.valueMult)  { $tmp = $tmp + "base value x" + $bi.valueMult + ", " }
		if (Key-Exists $bi.weightMult) { $tmp = $tmp + "base weight x" + $bi.weightMult + ", " }
		$tmp = $tmp.TrimEnd(", ")
		if ($tmp.length -ge 2) {
			$tmp = $tmp.substring(0,1).toupper()+$tmp.substring(1).tolower() 
			$jo = $jo + $tmp + "  \n"
		}
	}
	
	# (Air)ships: Movement, capacity, crew
	if ((Key-Exists $bi.type) -and ($bi.type -ieq "SHP" -or $bi.type -ieq "AIR" -or $bi.type -ieq "VEH")) {
		if (Key-Exists $bi.vehSpeed ) { $jo = $jo + "Speed: " + $bi.vehSpeed + " mph  \n" }
		if (Key-Exists $bi.capCargo -or Key-Exists $bi.capPassenger) { 
			$tmp1 = ""
			$tmp2 = ""
			if (Key-Exists $bi.capCargo) 	 { $tmp1 = $bi.capCargo.ToString() + " tons cargo" }
			if (Key-Exists $bi.capPassenger) { $tmp2 = $bi.capPassenger.ToString() + " passengers" }
			$tmp = $tmp1,$tmp2 -join ", "
			$tmp = $tmp.Trim(" ,")
			if ($tmp.length -ge 2) {
				$tmp = $tmp.substring(0,1).toupper()+$tmp.substring(1).tolower() 	
			}
			$jo = $jo + $tmp + "  \n"
		}
		if (Key-Exists $bi.crew -or Key-Exists $bi.vehAC -or Key-Exists $bi.vehHP -or Key-Exists $bi.vehDmgThresh) {
			$tmp1 = ""
			$tmp2 = ""
			$tmp3 = ""
			$tmp4 = ""
			if (Key-Exists $bi.crew)   { $tmp1 = $bi.crew.ToString() + " crew" }
			if (Key-Exists $bi.vehAC)  { $tmp2 = "AC " + $bi.vehAc.ToString() }
			if (Key-Exists $bi.vehHP)  { $tmp3 = "HP " + $bi.vehHp.ToString() }
			if (Key-Exists $bi.vehDmgThresh) { $tmp4 = "Damage Threshold " + $bi.vehDmgThresh.ToString() }
			$tmp = $tmp1,$tmp2,$tmp3,$tmp4 -join ", "
			$tmp = $tmp.Trim(", ")
			if ($tmp.length -ge 2) {
				$tmp = $tmp.substring(0,1).toupper()+$tmp.substring(1).tolower() 	
			}
			$jo = $jo + $tmp + "  \n"
		}		
	}


	# Add the horizontal rule
	$jo = $jo + "\n---\n\n"
	
	# Add stuff below the horizontal rule
	
	# Process through all .entries
	if (Key-Exists $bi.entries) {
		$jo = $jo + $(Process-Entries $bi.entries)	
	}
	
	# Some items (such as armor) have additional strings that need to be printed, but
	# those strings are defined statically (or, at the very least, do NOT appear in 
	# any of the item tables, so the source of those strings is unknown.
	if (Key-Exists $bi.stealth) {
		$jo = $jo + "The wearer has disadvantage on Stealth (Dexterity) checks.\n\n"
	}
	if (Key-Exists $bi.strentgh) {
		$jo = $jo + "If the wearer has a Strength score lower than " + $bi.strength.ToString + ", their speed is reduced by 10 feet.\n\n"
	}

	# See if this type of item has special .type Features, and if yes process through them
	$jo = $jo + $(Process-TypeFeatures $bi)
		
	# See if this item has any Properties, and if yes process through them.
	if (Key-Exists $bi.property) {
		$jo = $jo + $(Process-Properties $bi)
	}
		
	# Add the source information
	$jo = $jo.TrimEnd("\n") + "\n\n**Source:**  *" + $bi.source +"*, page " + $bi.page + ". "
	if ((Key-Exists $bi.srd) -and ($bi.srd -ieq "true")) { 
		$jo = $jo + "Available in SRD. " 
	}
	$jo = $jo + " \n"
	
	
	# Add the closing quote and comma for this particular JSON record.  
	# When we're all done, we'll have to strip off the extraneous ","
	$jo = $jo + $q + ","
	
	}
}

#===== Now convert non-base items

# Finish off our JSON string.  Remove the extraneous punctuation and add the close bracket.
$jo = $jo.TrimEnd(",") + "}"

$jo | Out-File $dstFile 
{/code]

Post Reply

Return to “D&D 5e Frameworks”