diff --git a/docs/templates/dnd5e/QuteBackground.md b/docs/templates/dnd5e/QuteBackground.md index 6f4862c7..489d88f8 100644 --- a/docs/templates/dnd5e/QuteBackground.md +++ b/docs/templates/dnd5e/QuteBackground.md @@ -6,12 +6,20 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[fluffImages](#fluffimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [prerequisite](#prerequisite), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [prerequisite](#prerequisite), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### fluffImages -List of images for this background (as [ImageRef](../ImageRef.md)) +List of images as [ImageRef](../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present ### hasSections @@ -33,6 +41,21 @@ Formatted text listing other prerequisite conditions (optional) List of content superceded by this note (as [Reprinted](../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteBastion/README.md b/docs/templates/dnd5e/QuteBastion/README.md index c3dfa3b1..3d1c4591 100644 --- a/docs/templates/dnd5e/QuteBastion/README.md +++ b/docs/templates/dnd5e/QuteBastion/README.md @@ -6,12 +6,20 @@ Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). ## Attributes -[fluffImages](#fluffimages), [hasSections](#hassections), [hirelingDescription](#hirelingdescription), [hirelings](#hirelings), [labeledSource](#labeledsource), [level](#level), [name](#name), [orders](#orders), [prerequisite](#prerequisite), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [space](#space), [spaceDescription](#spacedescription), [tags](#tags), [text](#text), [type](#type), [vaultPath](#vaultpath) +[fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hirelingDescription](#hirelingdescription), [hirelings](#hirelings), [labeledSource](#labeledsource), [level](#level), [name](#name), [orders](#orders), [prerequisite](#prerequisite), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [space](#space), [spaceDescription](#spacedescription), [tags](#tags), [text](#text), [type](#type), [vaultPath](#vaultpath) ### fluffImages -List of images for this bastion (as [ImageRef](../../ImageRef.md), optional) +List of images as [ImageRef](../../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present ### hasSections @@ -50,6 +58,21 @@ Formatted text listing other prerequisite conditions (optional) List of content superceded by this note (as [Reprinted](../../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteClass/HitPointDie.md b/docs/templates/dnd5e/QuteClass/HitPointDie.md new file mode 100644 index 00000000..10f64175 --- /dev/null +++ b/docs/templates/dnd5e/QuteClass/HitPointDie.md @@ -0,0 +1,39 @@ +# HitPointDie + +Describes the hit point die used by the class. + +If referenced as a unit (ignoring inner attributes), it will render +formatted strings based on the class version (2024 or not). + +## Attributes + +[average](#average), [classic](#classic), [face](#face), [isClassic](#isclassic), [isSidekick](#issidekick), [name](#name), [number](#number), [sidekick](#sidekick) + + +### average + +The average value of a hit dice roll + +### classic + + +### face + +Die to roll (8, 10); This will be 0 for sidekicks + +### isClassic + +True if this is a 2014 class + +### isSidekick + +Explicit test for sidekick (alternate to 0 face) + +### name + + +### number + +How many dice to roll (pretty much always 1) + +### sidekick diff --git a/docs/templates/dnd5e/QuteClass/Multiclassing.md b/docs/templates/dnd5e/QuteClass/Multiclassing.md new file mode 100644 index 00000000..4637f39d --- /dev/null +++ b/docs/templates/dnd5e/QuteClass/Multiclassing.md @@ -0,0 +1,58 @@ +# Multiclassing + +Describes the multiclassing information for the class. + +If referenced as a unit (ignoring inner attributes), it will render +formatted text describing multiclassing requirements and proficiencies. + +## Attributes + +[armor](#armor), [classic](#classic), [isClassic](#isclassic), [primaryAbility](#primaryability), [requirements](#requirements), [requirementsSpecial](#requirementsspecial), [skills](#skills), [text](#text), [tools](#tools), [weapons](#weapons) + + +### armor + +Armor proficiencies gained as formatted string +(optional) + +### classic + + +### isClassic + +True if this class is from the 2014 edition + +### primaryAbility + +Primary ability for multiclassing as formatted +string (optional) + +### requirements + +Prerequisites for multiclassing as formatted +string (optional) + +### requirementsSpecial + +Special prerequisites for multiclassing as +formatted string (optional) + +### skills + +Skill proficiencies gained as formatted string +(optional) + +### text + +Formatted text describing this multiclass +(optional) + +### tools + +Tool proficiencies gained as formatted string +(optional) + +### weapons + +Weapon proficiencies gained as formatted string +(optional) diff --git a/docs/templates/dnd5e/QuteClass/README.md b/docs/templates/dnd5e/QuteClass/README.md new file mode 100644 index 00000000..13086717 --- /dev/null +++ b/docs/templates/dnd5e/QuteClass/README.md @@ -0,0 +1,104 @@ +# QuteClass + +5eTools class attributes (`class2md.txt`) + +Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). + +## Attributes + +[classProgression](#classprogression), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hitDice](#hitdice), [hitPointDie](#hitpointdie), [hitRollAverage](#hitrollaverage), [labeledSource](#labeledsource), [multiclassing](#multiclassing), [name](#name), [primaryAbility](#primaryability), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [startingEquipment](#startingequipment), [tags](#tags), [text](#text), [vaultPath](#vaultpath) + + +### classProgression + +Formatted callout containing class and feature progressions. + +### fluffImages + +List of images as [ImageRef](../../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present + +### hasSections + +True if the content (text) contains sections + +### hitDice + +Hit dice for this class as a single digit: 8 + +### hitPointDie + +Hit point die for this class as +[HitPointDie](HitPointDie.md) + +### hitRollAverage + +Average Hit dice roll as a single digit + +### labeledSource + +Formatted string describing the content's source(s): `_Source: _` + +### multiclassing + +Multiclassing requirements and proficiencies for this class as +[Multiclassing](Multiclassing.md) + +### name + +Note name + +### primaryAbility + +Formatted string describing the primary abilities for this class + +### reprintOf + +List of content superceded by this note (as [Reprinted](../../Reprinted.md)) + +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + +### source + +String describing the content's source(s) + +### sourceAndPage + +Book sources as list of [SourceAndPage](../../SourceAndPage.md) + +### startingEquipment + +Formatted text describing starting equipment as +[StartingEquipment](StartingEquipment.md) + +### tags + +Collected tags for inclusion in frontmatter + +### text + +Formatted text. For most templates, this is the bulk of the content. + +### vaultPath + +Path to this note in the vault diff --git a/docs/templates/dnd5e/QuteClass/StartingEquipment.md b/docs/templates/dnd5e/QuteClass/StartingEquipment.md new file mode 100644 index 00000000..2e47375f --- /dev/null +++ b/docs/templates/dnd5e/QuteClass/StartingEquipment.md @@ -0,0 +1,56 @@ +# StartingEquipment + +Describes the starting equipment for the class. + +If referenced as a unit (ignoring inner attributes), it will render +structured text describing starting proficiencies and equipment *2014* vs +*2024*. + +## Attributes + +[armor](#armor), [armorString](#armorstring), [classic](#classic), [equipment](#equipment), [isClassic](#isclassic), [joinOrDefault](#joinordefault), [proficiencies](#proficiencies), [savingThrows](#savingthrows), [skills](#skills), [tools](#tools), [weapons](#weapons) + + +### armor + +List of armor as formatted strings (links) + +### armorString + +Create a structured string describing armor training. +Slighly different formatting and joining for 2014 vs 2024 materials. + +### classic + + +### equipment + +List of equipment as formatted strings (links) + +### isClassic + +True if this class is from the 2014 edition + +### joinOrDefault + +Given a list of strings, return a formatted string with a conjunction. + +### proficiencies + +Formatted string of class proficiencies + +### savingThrows + +List of saving throws + +### skills + +List of skills as formatted strings (links) + +### tools + +List of tools as formatted strings (links) + +### weapons + +List of weapons as formatted strings (links) diff --git a/docs/templates/dnd5e/QuteDeck/README.md b/docs/templates/dnd5e/QuteDeck/README.md index 337c335b..d9603bdc 100644 --- a/docs/templates/dnd5e/QuteDeck/README.md +++ b/docs/templates/dnd5e/QuteDeck/README.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). ## Attributes -[cardBack](#cardback), [cards](#cards), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[cardBack](#cardback), [cards](#cards), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### cardBack @@ -17,6 +17,18 @@ Image from the back of the card as [ImageRef](../../ImageRef.md) (optional) List of cards in the deck +### fluffImages + +List of images as [ImageRef](../../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present + ### hasSections True if the content (text) contains sections @@ -33,6 +45,21 @@ Note name List of content superceded by this note (as [Reprinted](../../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteDeity.md b/docs/templates/dnd5e/QuteDeity.md index 6386943c..cc4e89df 100644 --- a/docs/templates/dnd5e/QuteDeity.md +++ b/docs/templates/dnd5e/QuteDeity.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[alignment](#alignment), [altNames](#altnames), [category](#category), [domains](#domains), [hasSections](#hassections), [image](#image), [labeledSource](#labeledsource), [name](#name), [pantheon](#pantheon), [province](#province), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [symbol](#symbol), [tags](#tags), [text](#text), [title](#title), [vaultPath](#vaultpath) +[alignment](#alignment), [altNames](#altnames), [category](#category), [domains](#domains), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [image](#image), [labeledSource](#labeledsource), [name](#name), [pantheon](#pantheon), [province](#province), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [symbol](#symbol), [tags](#tags), [text](#text), [title](#title), [vaultPath](#vaultpath) ### alignment @@ -25,6 +25,18 @@ Category of this deity: Lesser Idols, Prime Deities Category of this deity: Nature, Tempest +### fluffImages + +List of images as [ImageRef](../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present + ### hasSections True if the content (text) contains sections @@ -53,6 +65,21 @@ Province of this deity: Discovery, Luck, Storms, Travel, ... List of content superceded by this note (as [Reprinted](../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteFeat.md b/docs/templates/dnd5e/QuteFeat.md index 07c4bf4a..bbbbcd92 100644 --- a/docs/templates/dnd5e/QuteFeat.md +++ b/docs/templates/dnd5e/QuteFeat.md @@ -6,9 +6,21 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [prerequisite](#prerequisite), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [prerequisite](#prerequisite), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +### fluffImages + +List of images as [ImageRef](../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present + ### hasSections True if the content (text) contains sections @@ -33,6 +45,21 @@ Formatted text listing other prerequisite conditions (optional) List of content superceded by this note (as [Reprinted](../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteHazard.md b/docs/templates/dnd5e/QuteHazard.md index 7637565d..0edfaf17 100644 --- a/docs/templates/dnd5e/QuteHazard.md +++ b/docs/templates/dnd5e/QuteHazard.md @@ -6,9 +6,21 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[hasSections](#hassections), [hazardType](#hazardtype), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hazardType](#hazardtype), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +### fluffImages + +List of images as [ImageRef](../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present + ### hasSections True if the content (text) contains sections @@ -29,6 +41,21 @@ Note name List of content superceded by this note (as [Reprinted](../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteItem/README.md b/docs/templates/dnd5e/QuteItem/README.md index 081d054b..6727caa1 100644 --- a/docs/templates/dnd5e/QuteItem/README.md +++ b/docs/templates/dnd5e/QuteItem/README.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). ## Attributes -[armorClass](#armorclass), [cost](#cost), [costCp](#costcp), [damage](#damage), [damage2h](#damage2h), [detail](#detail), [fluffImages](#fluffimages), [hasSections](#hassections), [labeledSource](#labeledsource), [mastery](#mastery), [name](#name), [prerequisite](#prerequisite), [properties](#properties), [range](#range), [reprintOf](#reprintof), [rootVariant](#rootvariant), [source](#source), [sourceAndPage](#sourceandpage), [stealthPenalty](#stealthpenalty), [strengthRequirement](#strengthrequirement), [subtypeString](#subtypestring), [tags](#tags), [text](#text), [variantAliases](#variantaliases), [variantSectionLinks](#variantsectionlinks), [variants](#variants), [vaultPath](#vaultpath), [weight](#weight) +[armorClass](#armorclass), [cost](#cost), [costCp](#costcp), [damage](#damage), [damage2h](#damage2h), [detail](#detail), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [mastery](#mastery), [name](#name), [prerequisite](#prerequisite), [properties](#properties), [range](#range), [reprintOf](#reprintof), [rootVariant](#rootvariant), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [stealthPenalty](#stealthpenalty), [strengthRequirement](#strengthrequirement), [subtypeString](#subtypestring), [tags](#tags), [text](#text), [variantAliases](#variantaliases), [variantSectionLinks](#variantsectionlinks), [variants](#variants), [vaultPath](#vaultpath), [weight](#weight) ### armorClass @@ -35,7 +35,15 @@ Formatted string of item details. Will include some combination of tier, rarity, ### fluffImages -List of images for this item as [ImageRef](../../ImageRef.md) +List of images as [ImageRef](../../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present ### hasSections @@ -73,6 +81,21 @@ List of content superceded by this note (as [Reprinted](../../Reprinted.md)) Detailed information about this item as [Variant](Variant.md) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteMonster/README.md b/docs/templates/dnd5e/QuteMonster/README.md index 8b1bff4b..00d81537 100644 --- a/docs/templates/dnd5e/QuteMonster/README.md +++ b/docs/templates/dnd5e/QuteMonster/README.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). ## Attributes -[5eInitiativeYaml](#5einitiativeyaml), [5eStatblockYaml](#5estatblockyaml), [ac](#ac), [acHp](#achp), [acText](#actext), [action](#action), [alignment](#alignment), [bonusAction](#bonusaction), [books](#books), [conditionImmune](#conditionimmune), [cr](#cr), [description](#description), [environment](#environment), [fluffImages](#fluffimages), [fullType](#fulltype), [hasSections](#hassections), [hitDice](#hitdice), [hp](#hp), [hpText](#hptext), [immune](#immune), [immuneResist](#immuneresist), [isNpc](#isnpc), [labeledSource](#labeledsource), [languages](#languages), [legendary](#legendary), [legendaryGroup](#legendarygroup), [legendaryGroupLink](#legendarygrouplink), [name](#name), [passive](#passive), [pb](#pb), [reaction](#reaction), [reprintOf](#reprintof), [resist](#resist), [savesSkills](#savesskills), [savingThrows](#savingthrows), [scores](#scores), [senses](#senses), [size](#size), [skills](#skills), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [spellcasting](#spellcasting), [subtype](#subtype), [tags](#tags), [text](#text), [token](#token), [trait](#trait), [type](#type), [vaultPath](#vaultpath), [vulnerable](#vulnerable) +[5eInitiativeYaml](#5einitiativeyaml), [5eStatblockYaml](#5estatblockyaml), [ac](#ac), [acHp](#achp), [acText](#actext), [action](#action), [alignment](#alignment), [bonusAction](#bonusaction), [books](#books), [conditionImmune](#conditionimmune), [cr](#cr), [description](#description), [environment](#environment), [fluffImages](#fluffimages), [fullType](#fulltype), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hitDice](#hitdice), [hp](#hp), [hpText](#hptext), [immune](#immune), [immuneResist](#immuneresist), [isNpc](#isnpc), [labeledSource](#labeledsource), [languages](#languages), [legendary](#legendary), [legendaryGroup](#legendarygroup), [legendaryGroupLink](#legendarygrouplink), [name](#name), [passive](#passive), [pb](#pb), [reaction](#reaction), [reprintOf](#reprintof), [resist](#resist), [savesSkills](#savesskills), [savingThrows](#savingthrows), [scores](#scores), [senses](#senses), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [size](#size), [skills](#skills), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [spellcasting](#spellcasting), [subtype](#subtype), [tags](#tags), [text](#text), [token](#token), [trait](#trait), [type](#type), [vaultPath](#vaultpath), [vulnerable](#vulnerable) ### 5eInitiativeYaml @@ -66,12 +66,20 @@ Formatted text describing the creature's environment. Usually a single word. ### fluffImages -List of [ImageRef](../../ImageRef.md) related to the creature +List of images as [ImageRef](../../ImageRef.md) (optional) ### fullType Creature type (lowercase) and subtype if present: `{resource.type} ({resource.subtype})` +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present + ### hasSections True if the content (text) contains sections @@ -163,6 +171,21 @@ Creature ability scores as [AbilityScores](../AbilityScores.md) Comma-separated string of creature senses (if present). +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### size Creature size (capitalized) diff --git a/docs/templates/dnd5e/QuteObject.md b/docs/templates/dnd5e/QuteObject.md index 8fae1d30..fcc39cba 100644 --- a/docs/templates/dnd5e/QuteObject.md +++ b/docs/templates/dnd5e/QuteObject.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[5eInitiativeYaml](#5einitiativeyaml), [5eStatblockYaml](#5estatblockyaml), [ac](#ac), [acHp](#achp), [acText](#actext), [action](#action), [books](#books), [conditionImmune](#conditionimmune), [creatureType](#creaturetype), [fluffImages](#fluffimages), [hasSections](#hassections), [hitDice](#hitdice), [hp](#hp), [hpText](#hptext), [immune](#immune), [immuneResist](#immuneresist), [isNpc](#isnpc), [labeledSource](#labeledsource), [name](#name), [objectType](#objecttype), [reprintOf](#reprintof), [resist](#resist), [scores](#scores), [senses](#senses), [size](#size), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [tags](#tags), [text](#text), [token](#token), [vaultPath](#vaultpath), [vulnerable](#vulnerable) +[5eInitiativeYaml](#5einitiativeyaml), [5eStatblockYaml](#5estatblockyaml), [ac](#ac), [acHp](#achp), [acText](#actext), [action](#action), [books](#books), [conditionImmune](#conditionimmune), [creatureType](#creaturetype), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hitDice](#hitdice), [hp](#hp), [hpText](#hptext), [immune](#immune), [immuneResist](#immuneresist), [isNpc](#isnpc), [labeledSource](#labeledsource), [name](#name), [objectType](#objecttype), [reprintOf](#reprintof), [resist](#resist), [scores](#scores), [senses](#senses), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [size](#size), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [tags](#tags), [text](#text), [token](#token), [vaultPath](#vaultpath), [vulnerable](#vulnerable) ### 5eInitiativeYaml @@ -50,7 +50,15 @@ Creature type (lowercase); optional ### fluffImages -List of [ImageRef](../ImageRef.md) related to the creature +List of images as [ImageRef](../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present ### hasSections @@ -108,6 +116,21 @@ Object ability scores as [AbilityScores](AbilityScores.md)) Comma-separated string of object senses (if present). +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### size Object size (capitalized) diff --git a/docs/templates/dnd5e/QutePsionic.md b/docs/templates/dnd5e/QutePsionic.md index 258e320e..438cbf89 100644 --- a/docs/templates/dnd5e/QutePsionic.md +++ b/docs/templates/dnd5e/QutePsionic.md @@ -6,13 +6,25 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[focus](#focus), [hasSections](#hassections), [labeledSource](#labeledsource), [modes](#modes), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [typeOrder](#typeorder), [vaultPath](#vaultpath) +[fluffImages](#fluffimages), [focus](#focus), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [modes](#modes), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [typeOrder](#typeorder), [vaultPath](#vaultpath) +### fluffImages + +List of images as [ImageRef](../ImageRef.md) (optional) + ### focus Psionic focus (string) +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present + ### hasSections True if the content (text) contains sections @@ -33,6 +45,21 @@ Note name List of content superceded by this note (as [Reprinted](../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteRace.md b/docs/templates/dnd5e/QuteRace.md index 150c9b43..5ff714a2 100644 --- a/docs/templates/dnd5e/QuteRace.md +++ b/docs/templates/dnd5e/QuteRace.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[ability](#ability), [description](#description), [fluffImages](#fluffimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [size](#size), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [spellcasting](#spellcasting), [tags](#tags), [text](#text), [traits](#traits), [type](#type), [vaultPath](#vaultpath) +[ability](#ability), [description](#description), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [size](#size), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [spellcasting](#spellcasting), [tags](#tags), [text](#text), [traits](#traits), [type](#type), [vaultPath](#vaultpath) ### ability @@ -19,7 +19,15 @@ Formatted text describing the race. Optional. Same as {resource.text} ### fluffImages -List of images for this race (as [ImageRef](../ImageRef.md)) +List of images as [ImageRef](../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present ### hasSections @@ -37,6 +45,21 @@ Note name List of content superceded by this note (as [Reprinted](../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### size Size: Small or Medium diff --git a/docs/templates/dnd5e/QuteReward.md b/docs/templates/dnd5e/QuteReward.md index bbe12bab..b6af1f0d 100644 --- a/docs/templates/dnd5e/QuteReward.md +++ b/docs/templates/dnd5e/QuteReward.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[ability](#ability), [detail](#detail), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [signatureSpells](#signaturespells), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[ability](#ability), [detail](#detail), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [signatureSpells](#signaturespells), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### ability @@ -17,6 +17,18 @@ Description of special ability granted by this reward, if defined separately. Th Reward detail string (similar to item detail). May include the reward type and rarity if either are defined. +### fluffImages + +List of images as [ImageRef](../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present + ### hasSections True if the content (text) contains sections @@ -33,6 +45,21 @@ Note name List of content superceded by this note (as [Reprinted](../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### signatureSpells Formatted text describing sigature spells. Not commonly used. diff --git a/docs/templates/dnd5e/QuteSpell.md b/docs/templates/dnd5e/QuteSpell.md index 418bb5fe..4851be78 100644 --- a/docs/templates/dnd5e/QuteSpell.md +++ b/docs/templates/dnd5e/QuteSpell.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[classList](#classlist), [classes](#classes), [components](#components), [duration](#duration), [fluffImages](#fluffimages), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [range](#range), [reprintOf](#reprintof), [ritual](#ritual), [school](#school), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [time](#time), [vaultPath](#vaultpath) +[classList](#classlist), [classes](#classes), [components](#components), [duration](#duration), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [range](#range), [reprintOf](#reprintof), [ritual](#ritual), [school](#school), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [time](#time), [vaultPath](#vaultpath) ### classList @@ -27,7 +27,15 @@ Formatted: spell range ### fluffImages -List of images for this spell (as [ImageRef](../ImageRef.md)) +List of images as [ImageRef](../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present ### hasSections @@ -61,6 +69,21 @@ true for ritual spells Spell school +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteSubclass.md b/docs/templates/dnd5e/QuteSubclass.md index 8d70d7f2..63bdd02e 100644 --- a/docs/templates/dnd5e/QuteSubclass.md +++ b/docs/templates/dnd5e/QuteSubclass.md @@ -6,13 +6,25 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[classProgression](#classprogression), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [parentClass](#parentclass), [parentClassLink](#parentclasslink), [parentClassSource](#parentclasssource), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [subclassTitle](#subclasstitle), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[classProgression](#classprogression), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [parentClass](#parentclass), [parentClassLink](#parentclasslink), [parentClassSource](#parentclasssource), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [subclassTitle](#subclasstitle), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### classProgression A pre-foramatted markdown callout describing subclass spell or feature progression +### fluffImages + +List of images as [ImageRef](../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present + ### hasSections True if the content (text) contains sections @@ -41,6 +53,21 @@ Source of the parent class (abbreviation) List of content superceded by this note (as [Reprinted](../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteVehicle/README.md b/docs/templates/dnd5e/QuteVehicle/README.md index 142e5a78..0a8548a7 100644 --- a/docs/templates/dnd5e/QuteVehicle/README.md +++ b/docs/templates/dnd5e/QuteVehicle/README.md @@ -10,7 +10,7 @@ Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). ## Attributes -[action](#action), [fluffImages](#fluffimages), [hasSections](#hassections), [immuneResist](#immuneresist), [isCreature](#iscreature), [isObject](#isobject), [isShip](#isship), [isSpelljammer](#isspelljammer), [isWarMachine](#iswarmachine), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [scores](#scores), [shipCrewCargoPace](#shipcrewcargopace), [shipSections](#shipsections), [sizeDimension](#sizedimension), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [terrain](#terrain), [text](#text), [token](#token), [vaultPath](#vaultpath), [vehicleType](#vehicletype) +[action](#action), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [immuneResist](#immuneresist), [isCreature](#iscreature), [isObject](#isobject), [isShip](#isship), [isSpelljammer](#isspelljammer), [isWarMachine](#iswarmachine), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [scores](#scores), [shipCrewCargoPace](#shipcrewcargopace), [shipSections](#shipsections), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [sizeDimension](#sizedimension), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [terrain](#terrain), [text](#text), [token](#token), [vaultPath](#vaultpath), [vehicleType](#vehicletype) ### action @@ -19,7 +19,15 @@ List of vehicle actions as a collection of [NamedText](../../NamedText.md) ### fluffImages -List of [ImageRef](../../ImageRef.md) related to the creature +List of images as [ImageRef](../../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present ### hasSections @@ -75,6 +83,21 @@ Ship capacity and pace attributes as [ShipCrewCargoPace](ShipCrewCargoPace.md). Ship sections and traits as [ShipAcHp](ShipAcHp.md) (hull, sails, oars, .. ) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### sizeDimension Ship size and dimensions. Used by Ship, Infernal War Machine diff --git a/docs/templates/dnd5e/README.md b/docs/templates/dnd5e/README.md index 35e4c254..51f42743 100644 --- a/docs/templates/dnd5e/README.md +++ b/docs/templates/dnd5e/README.md @@ -15,7 +15,7 @@ This data object provides a default mechanism for creating a marked up string based on the attributes that are present. - [QuteBackground](QuteBackground.md): 5eTools background attributes (`background2md.txt`). - [QuteBastion](QuteBastion/README.md): 5eTools background attributes (`bastion2md.txt`). -- [QuteClass](QuteClass.md): 5eTools class attributes (`class2md.txt`) +- [QuteClass](QuteClass/README.md): 5eTools class attributes (`class2md.txt`) Extension of [Tools5eQuteBase](Tools5eQuteBase.md). - [QuteDeck](QuteDeck/README.md): 5eTools deck attributes (`deck2md.txt`) diff --git a/docs/templates/dnd5e/Tools5eQuteBase.md b/docs/templates/dnd5e/Tools5eQuteBase.md index ea39f714..99bc792b 100644 --- a/docs/templates/dnd5e/Tools5eQuteBase.md +++ b/docs/templates/dnd5e/Tools5eQuteBase.md @@ -8,9 +8,21 @@ for the type. For example, `QuteBackground` will use `background2md.txt`. ## Attributes -[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +### fluffImages + +List of images as [ImageRef](../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present + ### hasSections True if the content (text) contains sections @@ -27,6 +39,21 @@ Note name List of content superceded by this note (as [Reprinted](../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/examples/templates/tools5e/images-background2md.txt b/examples/templates/tools5e/images-background2md.txt index e67ed4b4..edcd1404 100644 --- a/examples/templates/tools5e/images-background2md.txt +++ b/examples/templates/tools5e/images-background2md.txt @@ -11,17 +11,14 @@ aliases: ["{resource.name}"] --- # {resource.name} *Source: {resource.source}* -{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} -{first.getEmbeddedLink("right")} -{/let}{/if} +{#if resource.hasImages }{resource.showPortraitImage}{/if} {#if resource.prerequisite} ***Prerequisites*** {resource.prerequisite} {/if} {resource.text} -{#if resource.fluffImages.size > 1 } +{#if resource.hasMoreImages } -{#each resource.fluffImages}{#if it_index != 0}{it.getEmbeddedLink} -{/if}{/each} +{resource.showMoreImages} {/if} diff --git a/examples/templates/tools5e/images-class2md.txt b/examples/templates/tools5e/images-class2md.txt new file mode 100644 index 00000000..9772a389 --- /dev/null +++ b/examples/templates/tools5e/images-class2md.txt @@ -0,0 +1,47 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-class +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} +*Source: {resource.source}* + +{#if resource.classProgression } +{resource.classProgression} + +{/if} +{#if resource.hasImages }{resource.showPortraitImage} + +{/if} +## Hit Points + +{#if resource.hitDice } +- **Hit Dice**: 1d{resource.hitDice} per {resource.name} level +- **Hit Points at First Level:** {resource.hitDice} + CON +- **Hit Points at Higher Levels:** add {resource.hitRollAverage} OR 1d{resource.hitDice} + CON (minimum of 1) +{#else} +- **Hit Dice**: *x* = hit dice specified in the sidekick's statblock (human, gnome, kobold, etc.) +- **Hit Points at First Level:** *x* + CON +- **Hit Points at Higher Levels:** add 1d*x* + CON (minimum of 1) +{/if} + +## Starting {resource.name} + +{resource.startingEquipment} + +{#if resource.multiclassing } +## Multiclassing {resource.name} + +{resource.multiclassing} +{/if}{#if resource.hasMoreImages } + +{resource.showMoreImages} + +{/if} +{resource.text} diff --git a/examples/templates/tools5e/images-item2md.txt b/examples/templates/tools5e/images-item2md.txt index 18721da8..9b0552f4 100644 --- a/examples/templates/tools5e/images-item2md.txt +++ b/examples/templates/tools5e/images-item2md.txt @@ -15,9 +15,7 @@ aliases: --- # {resource.name} {#if resource.detail }*{resource.detail}* -{/if}{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} -{first.getEmbeddedLink("right")} -{/let}{/if} +{/if}{#if resource.hasImages }{resource.showPortraitImage}{/if} {#if resource.prerequisite} - **Prerequisites**: {resource.prerequisite} @@ -44,12 +42,10 @@ aliases: {/if}{#if resource.text } {resource.text} -{/if} -{#if resource.fluffImages.size > 1 } +{/if}{#if resource.hasMoreImages } + +{resource.showMoreImages} -{#each resource.fluffImages}{#if it_index != 0}{it.getEmbeddedLink} - -{/if}{/each} {/if}{#if resource.variants } **Variants**: diff --git a/examples/templates/tools5e/images-monster2md.txt b/examples/templates/tools5e/images-monster2md.txt index 7c0cc504..5fe5d7be 100644 --- a/examples/templates/tools5e/images-monster2md.txt +++ b/examples/templates/tools5e/images-monster2md.txt @@ -13,18 +13,14 @@ aliases: ["{resource.name}"] *Source: {resource.source}* {#if resource.description } -{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} -![{first.title}]({first.vaultPath}#right) -{/let}{/if} +{#if resource.hasImages }{resource.showPortraitImage}{/if} {resource.description} +{#if resource.hasMoreImages } -{#each resource.fluffImages}{#if it_index != 0} -{it.getEmbeddedLink} -{/if}{/each} -{#else} -{#each resource.fluffImages} -{it.getEmbeddedLink} -{/each} +{resource.showMoreImages} +{/if} +{#else if resource.hasImages} +{resource.showAllImages} {/if}{#if resource.hasSections } ## Statblock diff --git a/examples/templates/tools5e/images-object2md.txt b/examples/templates/tools5e/images-object2md.txt index f8d09dd9..5776ccbc 100644 --- a/examples/templates/tools5e/images-object2md.txt +++ b/examples/templates/tools5e/images-object2md.txt @@ -9,20 +9,20 @@ tags: aliases: ["{resource.name}"] --- # {resource.name} -*Source: {resource.source}* +*Source: {resource.source}* {#if resource.text } -{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} -{first.getEmbeddedLink("right")} -{/let}{/if} +{#if resource.hasImages }{resource.showPortraitImage} + +{/if} {resource.text} -{#each resource.fluffImages}{#if it_index != 0} -{it.getEmbeddedLink()} -{/if}{/each} -{#else} -{#each resource.fluffImages} -{it.getEmbeddedLink()} -{/each} +{#if resource.hasMoreImages } + +{resource.showMoreImages} +{/if} +{#else if resource.hasImages } +{resource.showAllImages} + {/if}{#if resource.hasSections } ## Statblock diff --git a/examples/templates/tools5e/images-race2md.txt b/examples/templates/tools5e/images-race2md.txt index 090910d9..08b76940 100644 --- a/examples/templates/tools5e/images-race2md.txt +++ b/examples/templates/tools5e/images-race2md.txt @@ -11,9 +11,7 @@ aliases: ["{resource.name}"] --- # {resource.name} *Source: {resource.source}* -{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} -{first.getEmbeddedLink("right")} -{/let}{/if} +{#if resource.hasImages }{resource.showPortraitImage}{/if} - **Ability Scores**: {resource.ability} {#if resource.type} @@ -35,8 +33,7 @@ aliases: ["{resource.name}"] {resource.description} {/if} -{#if resource.fluffImages.size > 1 } +{#if resource.hasMoreImages } -{#each resource.fluffImages}{#if it_index != 0}{it.getEmbeddedLink} -{/if}{/each} +{resource.showMoreImages} {/if} diff --git a/examples/templates/tools5e/images-spell2md.txt b/examples/templates/tools5e/images-spell2md.txt index 5f53b942..f1f6131a 100644 --- a/examples/templates/tools5e/images-spell2md.txt +++ b/examples/templates/tools5e/images-spell2md.txt @@ -18,9 +18,7 @@ aliases: ["{resource.name}"] # {resource.name} %%-- Embedded content starts on the next line. --%% *{resource.level}, {resource.school}{#if resource.ritual} (ritual){/if}* -{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} -{first.getEmbeddedLink("right")} -{/let}{/if} +{#if resource.hasImages }{resource.showPortraitImage}{/if} - **Casting time:** {resource.time}{#if resource.ritual} unless cast as a ritual{/if} - **Range:** {resource.range} @@ -32,11 +30,8 @@ aliases: ["{resource.name}"] {#if resource.hasSections } ## Summary -{/if} -{#if resource.fluffImages.size > 1 } - -{#each resource.fluffImages}{#if it_index != 0}{it.getEmbeddedLink} -{/if}{/each} +{/if}{#if resource.hasMoreImages } +{resource.showMoreImages} {/if}{#if resource.classes } **Classes**: {resource.classes} diff --git a/examples/templates/tools5e/images-subclass2md.txt b/examples/templates/tools5e/images-subclass2md.txt new file mode 100644 index 00000000..c47b319b --- /dev/null +++ b/examples/templates/tools5e/images-subclass2md.txt @@ -0,0 +1,31 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-class +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} +*{resource.parentClassLink}{#if resource.subclassTitle}: {resource.subclassTitle}{/if}* +*Source: {resource.source}* + +{#if resource.classProgression } +{resource.classProgression} + +{/if} +{#if resource.text } +{#if resource.hasImages }{resource.showPortraitImage} + +{/if} +{resource.text} +{#if resource.hasMoreImages } + +{resource.showMoreImages} +{/if} +{#else if resource.hasImages} +{resource.showAllImages} +{/if} diff --git a/examples/templates/tools5e/images-vehicle2md.txt b/examples/templates/tools5e/images-vehicle2md.txt index e82a3997..ed733a72 100644 --- a/examples/templates/tools5e/images-vehicle2md.txt +++ b/examples/templates/tools5e/images-vehicle2md.txt @@ -12,20 +12,20 @@ aliases: ["{resource.name}"] %%-- Embedded content starts on the next line. --%% *Source: {resource.source}* {#if resource.text && !resource.isObject }{! ----- Fluff for non-OBJECT vehicles ----- !} -{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} -{first.getEmbeddedLink("right")} -{/let}{/if} +{#if resource.hasImages }{resource.showPortraitImage}{/if} {resource.text} -{#if resource.fluffImages.size > 1 } +{#if resource.hasMoreImages } -{#each resource.fluffImages}{#if it_index != 0}{it.getEmbeddedLink} -{/if}{/each}{/if} +{resource.showMoreImages} + +{/if} {#else}{! ----- Fluff images for OBJECT vehicles (or no text) ----- !} +{#if resource.hasImages } -{#each resource.fluffImages} -{it.getEmbeddedLink} -{/each} +{resource.showAllImages} + +{/if} {/if}{#if !resource.isObject && resource.hasSections } ## Statblock diff --git a/examples/templates/tools5e/mixed/mixed-background2md.txt b/examples/templates/tools5e/mixed/mixed-background2md.txt new file mode 100644 index 00000000..90cc64d4 --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-background2md.txt @@ -0,0 +1,26 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-background +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} +{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} +{first.getEmbeddedLink("right")} +{/let}{/if} + +{resource.text} +{#if resource.fluffImages.size > 1 } + +{#each resource.fluffImages}{#if it_index != 0}{it.getEmbeddedLink} +{/if}{/each} +{/if}{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-class2md.txt b/examples/templates/tools5e/mixed/mixed-class2md.txt new file mode 100644 index 00000000..98a32812 --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-class2md.txt @@ -0,0 +1,44 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-class +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} + +{#if resource.classProgression } +{resource.classProgression} + +{/if} +## Hit Points + +{#if resource.hitDice } +- **Hit Dice**: 1d{resource.hitDice} per {resource.name} level +- **Hit Points at First Level:** {resource.hitDice} + CON +- **Hit Points at Higher Levels:** add {resource.hitRollAverage} OR 1d{resource.hitDice} + CON (minimum of 1) +{#else} +- **Hit Dice**: *x* = hit dice specified in the sidekick's statblock (human, gnome, kobold, etc.) +- **Hit Points at First Level:** *x* + CON +- **Hit Points at Higher Levels:** add 1d*x* + CON (minimum of 1) +{/if} + +## Starting a {resource.name} + +{resource.startingEquipment} + +{#if resource.multiclassing } +## Multiclassing {resource.name} + +{resource.multiclassing} +{/if} + +{resource.text}{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-deity2md.txt b/examples/templates/tools5e/mixed/mixed-deity2md.txt new file mode 100644 index 00000000..e05f7ef7 --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-deity2md.txt @@ -0,0 +1,36 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-deity +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"{#each resource.altNames}, "{it}"{/each}] +--- +# {resource.name} +{#if resource.image} +{resource.image.getEmbeddedLink("symbol")}{/if} + +{#if resource.altNames } +- **Alternate Names**: {#each resource.altNames}{it}{#if it_hasNext}, {/if}{/each} +{/if}{#if resource.alignment } +- **Alignment**: {resource.alignment} +{/if}{#if resource.category } +- **Category**: {resource.category} +{/if}{#if resource.domains } +- **Domains**: {resource.domains} +{/if}{#if resource.pantheon } +- **Pantheon**: {resource.pantheon} +{/if}{#if resource.province } +- **Province**: {resource.province} +{/if}{#if resource.symbol } +- **Symbol**: {resource.symbol} +{/if} + +{resource.text}{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-feat2md.txt b/examples/templates/tools5e/mixed/mixed-feat2md.txt new file mode 100644 index 00000000..e48fdf63 --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-feat2md.txt @@ -0,0 +1,27 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-feat +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} + +{#if resource.level || resource.prerequisite} +{#if resource.prerequisite} +***Prerequisites*** {resource.prerequisite} +{/if} +{#if resource.level} +***Level*** {resource.level} +{/if} + +{/if} +{resource.text}{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-hazard2md.txt b/examples/templates/tools5e/mixed/mixed-hazard2md.txt new file mode 100644 index 00000000..5a7c5017 --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-hazard2md.txt @@ -0,0 +1,21 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-hazard +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} +{#if resource.hazardType }*{resource.hazardType}* +{/if}{#if resource.text } + +{resource.text} +{/if}{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-item2md.txt b/examples/templates/tools5e/mixed/mixed-item2md.txt new file mode 100644 index 00000000..d33bd191 --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-item2md.txt @@ -0,0 +1,51 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-item +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} +{#if resource.detail }*{resource.detail}* +{/if}{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} +{first.getEmbeddedLink("right")} +{/let}{/if} + +{#if resource.armorClass } +- **Armor Class**: {resource.armorClass} +{#else if resource.damage }{#if resource.damage2h } +- **Damage**: + - One-handed: {resource.damage} + - Two-handed: {resource.damage2h} +{#else} +- **Damage**: {resource.damage} +{/if}{#if resource.range } +- **Range**: {resource.range} +{/if}{/if}{#if resource.properties } +- **Properties**: {resource.properties} +{/if}{#if resource.strengthRequirement } +- **Strength**: Requires {resource.strengthRequirement} STR. +{/if}{#if resource.stealthPenalty } +- **Stealth**: The wearer has disadvantage on Stealth (DEX) checks. +{/if} +- **Cost**: {#if resource.cost }{resource.cost}{#else}⏤{/if} +- **Weight**: {#if resource.weight }{resource.weight} lbs.{#else}⏤{/if} +{#if resource.text } + +{resource.text} +{/if} +{#if resource.fluffImages.size > 1 } + +{#each resource.fluffImages}{#if it_index != 0}{it.getEmbeddedLink} + +{/if}{/each} +{/if}{#if resource.source } + +## Sources + +*{resource.source}*{/if} + diff --git a/examples/templates/tools5e/mixed/mixed-monster2md.txt b/examples/templates/tools5e/mixed/mixed-monster2md.txt new file mode 100644 index 00000000..25058a62 --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-monster2md.txt @@ -0,0 +1,113 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-monster +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +statblock: true +statblock-link: "#^statblock" +{resource.5eInitiativeYaml} +aliases: ["{resource.name}"] +--- +# {resource.name} + +{#if resource.description } +{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} +![{first.title}]({first.vaultPath}#right) +{/let}{/if} +{resource.description} + +{#each resource.fluffImages}{#if it_index != 0} +{it.getEmbeddedLink} +{/if}{/each} +{#else} +{#each resource.fluffImages} +{it.getEmbeddedLink} +{/each} +{/if}{#if resource.hasSections } + +## Statblock +{/if} + +```ad-statblock +title: {resource.name}{#if resource.token} +![{resource.token.title}]({resource.token.vaultPath}#token){/if} +*{resource.size} {resource.fullType}, {resource.alignment}* + +- **Armor Class** {#if resource.ac }{resource.ac} {/if}{#if resource.acText }({resource.acText}){/if} +- **Hit Points** {resource.hp} {#if resource.hitDice }({resource.hitDice}){/if} {#if resource.hpText }({resource.hpText}){/if} +- **Speed** {resource.speed} + +|STR|DEX|CON|INT|WIS|CHA| +|:---:|:---:|:---:|:---:|:---:|:---:| +|{resource.scores}| + +- **Proficiency Bonus** {resource.pb} +- **Saving Throws** {#if resource.savingThrows }{resource.savingThrows}{#else}⏤{/if} +- **Skills** {#if resource.skills }{resource.skills}{#else}⏤{/if} +- **Senses** {#if resource.senses }{resource.senses}, {/if}passive Perception {resource.passive} +{#if resource.vulnerable } +- **Damage Vulnerabilities** {resource.vulnerable} +{/if}{#if resource.resist} +- **Damage Resistances** {resource.resist} +{/if}{#if resource.immune} +- **Damage Immunities** {resource.immune} +{/if}{#if resource.conditionImmune} +- **Condition Immunities** {resource.conditionImmune} +{/if} +- **Languages** {#if resource.languages }{resource.languages}{#else}—{/if} +- **Challenge** {resource.cr} +{#if resource.trait} + +## Traits +{#for trait in resource.trait} + +{#if trait.name }***{trait.name}.*** {/if}{trait.desc} +{/for}{/if}{#if resource.spellcasting}{#for spellcasting in resource.spellcasting} + +***{spellcasting.name}.*** {spellcasting.desc} +{/for}{/if}{#if resource.action} + +## Actions +{#for action in resource.action} + +{#if action.name }***{action.name}.*** {/if}{action.desc} +{/for}{/if}{#if resource.bonusAction} + +## Bonus Actions +{#for bonusAction in resource.bonusAction} + +{#if bonusAction.name }***{bonusAction.name}.*** {/if}{bonusAction.desc} +{/for}{/if}{#if resource.reaction} + +## Reactions +{#for reaction in resource.reaction} + +{#if reaction.name }***{reaction.name}.*** {/if}{reaction.desc} +{/for}{/if}{#if resource.legendary} + +## Legendary Actions +{#for legendary in resource.legendary} + +{#if legendary.name }***{legendary.name}.*** {/if}{legendary.desc} +{/for}{/if}{#if resource.legendaryGroup}{#for group in resource.legendaryGroup} + +## {group.name} + +{group.desc} +{/for}{/if} +``` +^statblock +{#if resource.environment } + +## Environment + +{resource.environment} +{/if}{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-object2md.txt b/examples/templates/tools5e/mixed/mixed-object2md.txt new file mode 100644 index 00000000..9dfaed90 --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-object2md.txt @@ -0,0 +1,65 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-object +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for}{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} + +{#if resource.text } +{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} +{first.getEmbeddedLink("right")} +{/let}{/if} +{resource.text} +{#each resource.fluffImages}{#if it_index != 0} +{it.getEmbeddedLink()} +{/if}{/each} +{#else} +{#each resource.fluffImages} +{it.getEmbeddedLink()} +{/each} +{/if}{#if resource.hasSections } +## Statblock + +{/if} +```ad-statblock +title: {resource.name}{#if resource.token} +![{resource.token.title}]({resource.token.vaultPath}#token){/if} +*{resource.size} {resource.objectType}{#if resource.creatureType } ({resource.creatureType}){/if}* + +- **Armor Class** {#if resource.ac }{resource.ac} {/if}{#if resource.acText }({resource.acText}){/if} +- **Hit Points** {resource.hp or ' '} {#if resource.hpText }({resource.hpText}){/if} +- **Speed** {resource.speed} + +|STR|DEX|CON|INT|WIS|CHA| +|:---:|:---:|:---:|:---:|:---:|:---:| +|{resource.scores}| + +{#if resource.senses } +- **Senses** {resource.senses} +{/if}{#if resource.vulnerable } +- **Damage Vulnerabilities** {resource.vulnerable} +{/if}{#if resource.resist} +- **Damage Resistances** {resource.resist} +{/if}{#if resource.immune} +- **Damage Immunities** {resource.immune} +{/if}{#if resource.conditionImmune} +- **Condition Immunities** {resource.conditionImmune} +{/if} +{#if resource.action} + +## Actions +{#for action in resource.action} + +{#if action.name }***{action.name}.*** {/if}{action.desc} +{/for}{/if} +``` +^statblock{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-race2md.txt b/examples/templates/tools5e/mixed/mixed-race2md.txt new file mode 100644 index 00000000..467fb1cf --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-race2md.txt @@ -0,0 +1,46 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-race +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} + +{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} +{first.getEmbeddedLink("right")} +{/let}{/if} + +- **Ability Scores**: {resource.ability} +{#if resource.type} +- **Type**: {resource.type} +{/if} +- **Size**: {resource.size} +- **Speed**: {resource.speed} +{#if resource.spellcasting} +- **Spellcasting**: {resource.spellcasting} +{/if} + +## Traits + +{resource.traits} +{#if resource.description} + +## Description + +{resource.description} + +{/if} +{#if resource.fluffImages.size > 1 } + +{#each resource.fluffImages}{#if it_index != 0}{it.getEmbeddedLink} +{/if}{/each} +{/if}{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-reward2md.txt b/examples/templates/tools5e/mixed/mixed-reward2md.txt new file mode 100644 index 00000000..043f973d --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-reward2md.txt @@ -0,0 +1,24 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-reward +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} +{#if resource.detail }*{resource.detail}* +{/if}{#if resource.signatureSpells } + +- **Signature Spells**: {resource.signatureSpells} +{/if}{#if resource.text } + +{resource.text} +{/if}{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-spell2md.txt b/examples/templates/tools5e/mixed/mixed-spell2md.txt new file mode 100644 index 00000000..504ffb77 --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-spell2md.txt @@ -0,0 +1,42 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-spell +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} +%%-- Embedded content starts on the next line. --%% +*{resource.level}, {resource.school}{#if resource.ritual} (ritual){/if}* +{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} +{first.getEmbeddedLink("right")} +{/let}{/if} + +- **Casting time:** {resource.time}{#if resource.ritual} unless cast as a ritual{/if} +- **Range:** {resource.range} +- **Components:** {resource.components} +- **Duration:** {resource.duration} + +{resource.text} + +{#if resource.hasSections } +## Summary + +{/if} +{#if resource.fluffImages.size > 1 } + +{#each resource.fluffImages}{#if it_index != 0}{it.getEmbeddedLink} +{/if}{/each} + +{/if}{#if resource.classes } +**Classes**: {resource.classes} + +{/if}{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-subclass2md.txt b/examples/templates/tools5e/mixed/mixed-subclass2md.txt new file mode 100644 index 00000000..0b92cca9 --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-subclass2md.txt @@ -0,0 +1,24 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-class +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} +*{resource.parentClassLink}{#if resource.subclassTitle}: {resource.subclassTitle}{/if}* + +{#if resource.classProgression } +{resource.classProgression} + +{/if} + +{resource.text}{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-vehicle2md.txt b/examples/templates/tools5e/mixed/mixed-vehicle2md.txt new file mode 100644 index 00000000..c86287a6 --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-vehicle2md.txt @@ -0,0 +1,114 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-vehicle +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for}{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} +%%-- Embedded content starts on the next line. --%% +*Source: {resource.source}* +{#if resource.text && !resource.isObject }{! ----- Fluff for non-OBJECT vehicles ----- !} +{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} +{first.getEmbeddedLink("right")} +{/let}{/if} + +{resource.text} +{#if resource.fluffImages.size > 1 } + +{#each resource.fluffImages}{#if it_index != 0}{it.getEmbeddedLink} +{/if}{/each}{/if} +{#else}{! ----- Fluff images for OBJECT vehicles (or no text) ----- !} + +{#each resource.fluffImages} +{it.getEmbeddedLink} +{/each} + +{/if}{#if !resource.isObject && resource.hasSections } +## Statblock + +{/if} +```ad-statblock +title: {resource.name}{#if resource.token} +![{resource.token.title}]({resource.token.vaultPath}#token){/if} +*{resource.sizeDimension}; {resource.terrain}* +{#if resource.shipCrewCargoPace} + +{resource.shipCrewCargoPace} +{/if}{#if resource.isObject }{! ----- BEGIN OBJECT (type) ----- !} +{#if resource.scores} + +|STR|DEX|CON|INT|WIS|CHA| +|:---:|:---:|:---:|:---:|:---:|:---:| +|{resource.scores}| + +{/if}{#if resource.immuneResist } +{resource.immuneResist} +{/if}{#if resource.text } + +{resource.text} + +{/if}{#if resource.shipSections}{#each resource.shipSections} + +{it} +{/each}{/if} +{#else if resource.isCreature }{! ----- BEGIN CREATURE (type) ----- !} +{#if resource.scores} + +|STR|DEX|CON|INT|WIS|CHA| +|:---:|:---:|:---:|:---:|:---:|:---:| +|{resource.scores}| + +{/if}{#if resource.immuneResist } +{resource.immuneResist} +{/if}{#if resource.shipSections}{#each resource.shipSections} + +{it} +{/each}{/if} +{#else if resource.isWarMachine }{! ----- BEGIN INFERNAL WAR MACHINE (type) ----- !} +{#if resource.scores} + +|STR|DEX|CON|INT|WIS|CHA| +|:---:|:---:|:---:|:---:|:---:|:---:| +|{resource.scores}| + +{/if}{#if resource.immuneResist } +{resource.immuneResist} +{/if}{#if resource.shipSections}{#each resource.shipSections} + +{it} +{/each}{/if} +{#else if resource.isSpelljammer }{! ----- BEGIN SPELLJAMMER (type) ----- !} +{#if resource.shipSections}{#each resource.shipSections} + +{it} +{/each}{/if} +{#else if resource.isShip }{! ----- BEGIN SHIP (type) ----- !} +{#if resource.scores} + +|STR|DEX|CON|INT|WIS|CHA| +|:---:|:---:|:---:|:---:|:---:|:---:| +|{resource.scores}| + +{/if}{#if resource.immuneResist } +{resource.immuneResist} +{/if}{#if resource.action} + +## Actions +{#each resource.action} + +{it} +{/each}{/if}{#if resource.shipSections}{#each resource.shipSections} + +{it} +{/each}{/if} +{/if}{! END SHIP (type) !} +``` +^statblock{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/src/main/java/dev/ebullient/convert/StringUtil.java b/src/main/java/dev/ebullient/convert/StringUtil.java index 89238dbc..02886126 100644 --- a/src/main/java/dev/ebullient/convert/StringUtil.java +++ b/src/main/java/dev/ebullient/convert/StringUtil.java @@ -373,6 +373,20 @@ public static String toOrdinal(String level) { } } + public static String toAnchorTag(String x) { + return x.replace(" ", "%20") + .replace(":", "") + .replace(".", "") + .replace('‑', '-'); + } + + // markdown link to href + public static String markdownLinkToHtml(String x) { + return x.replaceAll("\\[([^\\]]+)\\]\\(([^\\s)]+)(?:\\s\"[^\"]*\")?\\)", + "$1"); + + } + /** * A {@link java.util.stream.Collector} which converts the elements to strings, and joins the non-empty, non-null * strings into a single string. Allows providing an optional final delimiter that will be inserted before the diff --git a/src/main/java/dev/ebullient/convert/config/TtrpgConfig.java b/src/main/java/dev/ebullient/convert/config/TtrpgConfig.java index 179ac1ae..8590dad6 100644 --- a/src/main/java/dev/ebullient/convert/config/TtrpgConfig.java +++ b/src/main/java/dev/ebullient/convert/config/TtrpgConfig.java @@ -12,6 +12,7 @@ import java.util.Optional; import java.util.Set; import java.util.TreeSet; +import java.util.function.Consumer; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; @@ -117,6 +118,15 @@ public static void includeAdditionalSource(String src) { } } + public static void addReferenceEntries(Consumer callback) { + if (datasource == Datasource.tools5e) { + JsonNode srdEntries = TtrpgConfig.activeGlobalConfig("srdEntries"); + for (JsonNode property : ConfigKeys.properties.iterateArrayFrom(srdEntries)) { + callback.accept(property); + } + } + } + public static class ImageRoot { final String internalImageRoot; final boolean copyInternal; diff --git a/src/main/java/dev/ebullient/convert/io/MarkdownWriter.java b/src/main/java/dev/ebullient/convert/io/MarkdownWriter.java index 7d7bc913..6cc26431 100644 --- a/src/main/java/dev/ebullient/convert/io/MarkdownWriter.java +++ b/src/main/java/dev/ebullient/convert/io/MarkdownWriter.java @@ -71,7 +71,8 @@ public void writeFiles(Path basePath, List elements) { for (Map.Entry> pathEntry : pathMap.entrySet()) { if (pathEntry.getValue().size() > 1) { - tui.warnf("Conflict: several entries would write to the same file:\n %s", + tui.warnf("Conflict: several entries would write to the same file: (%s)\n %s", + pathEntry.getKey().fileName, pathEntry.getValue().stream().map(x -> String.format("[%s]: %s", x.getName(), x.sources().getKey())) diff --git a/src/main/java/dev/ebullient/convert/io/Tui.java b/src/main/java/dev/ebullient/convert/io/Tui.java index 09bcf75d..37d5af99 100644 --- a/src/main/java/dev/ebullient/convert/io/Tui.java +++ b/src/main/java/dev/ebullient/convert/io/Tui.java @@ -173,13 +173,6 @@ public static String slugify(String s) { return slugifier().slugify(s); } - public static String toAnchorTag(String x) { - return x.replace(" ", "%20") - .replace(":", "") - .replace(".", "") - .replace('‑', '-'); - } - static final boolean picocliDebugEnabled = "DEBUG".equalsIgnoreCase(System.getProperty("picocli.trace")); Ansi ansi; @@ -324,6 +317,12 @@ private void verboseMsg(Msg msgWrap, String output, Object... params) { } } + public void log(Throwable t, boolean keepException) { + if (log != null) { + log.println(captureStackTrace(t, keepException)); + } + } + public void logf(String output, Object... params) { logf(Msg.NOOP, output, params); } @@ -380,7 +379,7 @@ private void error(Throwable ex, String errorMsg) { .replace("java.nio.file.NoSuchFileException: ", "File not found: ")); errLine(message, colors.errorText(message)); if (ex != null && log != null) { - ex.printStackTrace(log); + log.println(captureStackTrace(ex, true)); } } @@ -505,7 +504,6 @@ private void copyRemoteImage(ImageRef image, Path targetPath) { public boolean readFile(Path p, List fixes, BiConsumer callback) { inputRoot.add(p.getParent().toAbsolutePath()); try { - progressf("Reading %s", p); File f = p.toFile(); String contents = Files.readString(p); for (Fix fix : fixes) { @@ -644,4 +642,24 @@ public static JsonNode readTreeFromResource(String resource) { public static String jsonStringify(Object o) { return Tui.MAPPER.valueToTree(o).toPrettyString(); } + + public static String captureStackTrace(Throwable t, boolean keepException) { + var stackTrace = t.getStackTrace(); + if (stackTrace == null || stackTrace.length == 0) { + return keepException ? t.toString() : ""; + } + StringBuilder sb = new StringBuilder(); + if (keepException) { + sb.append(Msg.DEBUG.wrap(t.toString())).append("\n"); + } else { + sb.append(Msg.DEBUG.wrap(t.getMessage())).append("\n"); + } + for (StackTraceElement e : stackTrace) { + if (e.getClassName().startsWith("picocli")) { + break; + } + sb.append("\tat ").append(e.toString()).append("\n"); + } + return sb.toString(); + } } diff --git a/src/main/java/dev/ebullient/convert/qute/QuteUtil.java b/src/main/java/dev/ebullient/convert/qute/QuteUtil.java index 6968b999..5fa21ed7 100644 --- a/src/main/java/dev/ebullient/convert/qute/QuteUtil.java +++ b/src/main/java/dev/ebullient/convert/qute/QuteUtil.java @@ -33,6 +33,12 @@ default void addIntegerUnlessEmpty(Map map, String key, Integer } } + default void maybeAddBlankLine(List content) { + if (content.size() > 0 && !content.get(content.size() - 1).isBlank()) { + content.add(""); + } + } + /** Remove leading '+' */ default Map mapOfNumbers(Map map) { Map result = new HashMap<>(); diff --git a/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java b/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java index f258dfdf..10d9e607 100644 --- a/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java +++ b/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java @@ -186,20 +186,31 @@ default JsonNode getFieldFrom(JsonNode source, JsonNodeReader field) { default List getListOfStrings(JsonNode source, Tui tui) { JsonNode target = getFrom(source); - if (target == null) { + if (target == null || target.isNull()) { return List.of(); - } else if (target.isTextual()) { + } + if (target.isTextual()) { return List.of(target.asText()); } + if (target.isObject()) { + throw new IllegalArgumentException( + "Unexpected object when creating list of strings: %s".formatted( + source)); + } List list = fieldFromTo(source, Tui.LIST_STRING, tui); return list == null ? List.of() : list; } default Map getMapOfStrings(JsonNode source, Tui tui) { JsonNode target = getFrom(source); - if (target == null) { + if (target == null || target.isNull()) { return Map.of(); } + if (target.isTextual() || target.isArray()) { + throw new IllegalArgumentException( + "Unexpected text or array when creating map of strings: %s".formatted( + source)); + } Map map = fieldFromTo(source, Tui.MAP_STRING_STRING, tui); return map == null ? Map.of() : map; } @@ -387,10 +398,6 @@ default String value() { return name(); } - default String toAnchorTag(String x) { - return Tui.toAnchorTag(x); - } - default boolean isValueOfField(JsonNode source, JsonNodeReader field) { return matches(field.getTextOrEmpty(source)); } @@ -450,7 +457,7 @@ default ArrayNode ensureArrayIn(JsonNode target) { if (target == null) { return Tui.MAPPER.createArrayNode(); } - return target.withArray(this.nodeName()); + return target.withArrayProperty(this.nodeName()); } default JsonNode copyFrom(JsonNode source) { diff --git a/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java b/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java index f951017c..e80dfc21 100644 --- a/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java +++ b/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java @@ -2,6 +2,7 @@ import static dev.ebullient.convert.StringUtil.isPresent; import static dev.ebullient.convert.StringUtil.join; +import static dev.ebullient.convert.StringUtil.toAnchorTag; import java.nio.file.Path; import java.util.ArrayDeque; @@ -664,18 +665,21 @@ default Stream streamOfFieldNames(JsonNode source) { return StreamSupport.stream(iterableFieldNames(source).spliterator(), false); } + default Stream> streamProps(JsonNode source) { + return streamPropsExcluding(source, (JsonNodeReader[]) null); + } + default Stream> streamPropsExcluding(JsonNode source, JsonNodeReader... excludingKeys) { if (source == null || !source.isObject()) { return Stream.of(); } + if (excludingKeys == null || excludingKeys.length == 0) { + return source.properties().stream(); + } return source.properties().stream() .filter(e -> Arrays.stream(excludingKeys).noneMatch(s -> e.getKey().equalsIgnoreCase(s.name()))); } - default String toAnchorTag(String x) { - return Tui.toAnchorTag(x); - } - /** {@link #createLink(String, Path, String, String)} with an empty title */ default String createLink(String displayText, Path target, String anchor) { return createLink(displayText, target, anchor, null); diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemMastery.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemMastery.java index c82f3085..81acbd8b 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemMastery.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemMastery.java @@ -1,6 +1,7 @@ package dev.ebullient.convert.tools.dnd5e; import static dev.ebullient.convert.StringUtil.isPresent; +import static dev.ebullient.convert.StringUtil.toAnchorTag; import java.util.Collection; import java.util.Comparator; @@ -12,6 +13,7 @@ import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.JsonTextConverter.SourceField; +import dev.ebullient.convert.tools.ToolsIndex.TtrpgValue; public record ItemMastery( String name, @@ -32,24 +34,19 @@ public String linkify(String linkText) { boolean included = isPresent(indexKey) ? index.isIncluded(indexKey) - : index.customRulesIncluded(); + : index.customContentIncluded(); return included ? "[%s](%sitem-mastery.md#%s)".formatted( - linkText, index.rulesVaultRoot(), Tui.toAnchorTag(name)) + linkText, index.rulesVaultRoot(), toAnchorTag(name)) : linkText; } public static final Comparator comparator = Comparator.comparing(ItemMastery::name); private static final Map masteryMap = new HashMap<>(); - public static ItemMastery fromKey(String key, Tools5eIndex index) { - String finalKey = index.getAliasOrDefault(key); - JsonNode node = index.getNode(finalKey); - return node == null ? null : fromNode(finalKey, node); - } - - public static ItemMastery fromNode(String key, JsonNode mastery) { + public static ItemMastery fromNode(JsonNode mastery) { + String key = TtrpgValue.indexKey.getTextOrEmpty(mastery); // Create the ItemType object once return masteryMap.computeIfAbsent(key, k -> { String name = SourceField.name.getTextOrEmpty(mastery); @@ -69,4 +66,8 @@ public static List asLinks(Collection itemMasteries) { .map(ItemMastery::linkify) .toList(); } + + public static void clear() { + masteryMap.clear(); + } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemProperty.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemProperty.java index ea0455ba..6d91c08a 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemProperty.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemProperty.java @@ -1,6 +1,7 @@ package dev.ebullient.convert.tools.dnd5e; import static dev.ebullient.convert.StringUtil.isPresent; +import static dev.ebullient.convert.StringUtil.toAnchorTag; import java.util.Collection; import java.util.Comparator; @@ -13,6 +14,7 @@ import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.JsonTextConverter.SourceField; +import dev.ebullient.convert.tools.ToolsIndex.TtrpgValue; import dev.ebullient.convert.tools.dnd5e.Json2QuteItem.ItemTag; import dev.ebullient.convert.tools.dnd5e.JsonSource.Tools5eFields; @@ -37,12 +39,12 @@ public String linkify(String linkText) { boolean included = isPresent(indexKey) ? index.isIncluded(indexKey) - : index.customRulesIncluded(); + : index.customContentIncluded(); return included ? "[%s](%sitem-properties.md#%s)".formatted( linkText, index.rulesVaultRoot(), - Tui.toAnchorTag(isPresent(sectionName) ? sectionName : name)) + toAnchorTag(isPresent(sectionName) ? sectionName : name)) : linkText; } @@ -53,13 +55,12 @@ public String linkify(String linkText) { public static final ItemProperty SILVERED = ItemProperty.customProperty("Silvered", "Silvered Weapons", "="); public static final ItemProperty POISON = ItemProperty.customProperty("Poison", "="); - public static ItemProperty fromKey(String key, Tools5eIndex index) { - String finalKey = index.getAliasOrDefault(key); - JsonNode node = index.getNode(finalKey); - return node == null ? null : ItemProperty.fromNode(finalKey, node); - } - - public static ItemProperty fromNode(String key, JsonNode property) { + public static ItemProperty fromNode(JsonNode property) { + String key = TtrpgValue.indexKey.getTextOrEmpty(property); + if (key.isEmpty()) { + Tui.instance().warnf(Msg.NOT_SET.wrap("Index key not found for property %s"), property); + return null; + } // Create the ItemType object once return ItemProperty.propertyMap.computeIfAbsent(key, k -> { String abbreviation = Tools5eFields.abbreviation.getTextOrEmpty(property); @@ -138,6 +139,10 @@ public static ItemProperty customProperty(String name, String sectionName, Strin public static ItemProperty customProperty(String name, String abbreviation) { return ItemProperty.customProperty(name, name, abbreviation); } + + public static void clear() { + propertyMap.clear(); + } } // Parser.ITM_PROP_ABV__TWO_HANDED = "2H"; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemType.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemType.java index 4947d23e..5e33e01f 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemType.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemType.java @@ -1,6 +1,7 @@ package dev.ebullient.convert.tools.dnd5e; import static dev.ebullient.convert.StringUtil.isPresent; +import static dev.ebullient.convert.StringUtil.toAnchorTag; import java.util.HashMap; import java.util.Map; @@ -10,6 +11,7 @@ import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.JsonTextConverter.SourceField; +import dev.ebullient.convert.tools.ToolsIndex.TtrpgValue; import dev.ebullient.convert.tools.dnd5e.Json2QuteItem.ItemTag; import dev.ebullient.convert.tools.dnd5e.JsonSource.Tools5eFields; @@ -41,23 +43,22 @@ public String linkify(String linkText) { boolean included = isPresent(indexKey) ? index.isIncluded(indexKey) - : index.customRulesIncluded(); + : index.customContentIncluded(); return included ? "[%s](%sitem-types.md#%s)".formatted( - linkText, index.rulesVaultRoot(), Tui.toAnchorTag(name)) + linkText, index.rulesVaultRoot(), toAnchorTag(name)) : linkText; } public static final Map typeMap = new HashMap<>(); - public static ItemType fromKey(String key, Tools5eIndex index) { - String finalKey = index.getAliasOrDefault(key); - JsonNode node = index.getNode(finalKey); - return node == null ? null : fromNode(finalKey, node); - } - - public static ItemType fromNode(String typeKey, JsonNode typeNode) { + public static ItemType fromNode(JsonNode typeNode) { + String typeKey = TtrpgValue.indexKey.getTextOrEmpty(typeNode); + if (typeKey.isEmpty()) { + Tui.instance().warnf(Msg.NOT_SET.wrap("Index key not found for property %s"), typeNode); + return null; + } // Create the ItemType object once return typeMap.computeIfAbsent(typeKey, k -> { String abbreviation = Tools5eFields.abbreviation.getTextOrEmpty(typeNode); @@ -160,6 +161,10 @@ private static ItemTypeGroup mapGroup(String abbreviation, String lowercase, Jso } }; } + + public static void clear() { + typeMap.clear(); + } } // Parser.ITM_TYP_ABV__TREASURE = "$"; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBackground.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBackground.java index 55905d74..28b9a859 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBackground.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBackground.java @@ -56,8 +56,8 @@ protected Tools5eQuteBase buildQuteResource() { backgroundName, getSourceText(sources), listPrerequisites(rootNode), - String.join("\n", text), images, + String.join("\n", text), tags); } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBastion.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBastion.java index 4322a79f..5dd9fbd3 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBastion.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBastion.java @@ -33,7 +33,7 @@ protected Tools5eQuteBase buildQuteResource() { List fluffImages = new ArrayList<>(); List text = getFluff(Tools5eIndexType.facilityFluff, "##", fluffImages); - appendToText(text, SourceField.entries.getFrom(rootNode), "##"); + appendToText(text, rootNode, "##"); String type = BastionFields.facilityType.getTextOrThrow(rootNode); diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java index cca389a8..37f94664 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java @@ -1,23 +1,33 @@ package dev.ebullient.convert.tools.dnd5e; +import static dev.ebullient.convert.StringUtil.isPresent; +import static dev.ebullient.convert.StringUtil.join; +import static dev.ebullient.convert.StringUtil.joinConjunct; +import static dev.ebullient.convert.StringUtil.markdownLinkToHtml; +import static dev.ebullient.convert.StringUtil.toAnchorTag; +import static dev.ebullient.convert.StringUtil.toTitleCase; +import static dev.ebullient.convert.StringUtil.uppercaseFirst; + import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; import dev.ebullient.convert.io.Msg; +import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.OptionalFeatureIndex.OptionalFeatureType; import dev.ebullient.convert.tools.dnd5e.qute.QuteClass; +import dev.ebullient.convert.tools.dnd5e.qute.QuteClass.HitPointDie; +import dev.ebullient.convert.tools.dnd5e.qute.QuteClass.Multiclassing; +import dev.ebullient.convert.tools.dnd5e.qute.QuteClass.StartingEquipment; import dev.ebullient.convert.tools.dnd5e.qute.QuteSubclass; import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; @@ -26,35 +36,22 @@ public class Json2QuteClass extends Json2QuteCommon { final static Map keyToClassFeature = new HashMap<>(); final Map> startingText = new HashMap<>(); - final List classFeatures = new ArrayList<>(); - final List subclasses = new ArrayList<>(); - boolean additionalFromBackground; + final boolean isSidekick; + final String decoratedClassName; final String classSource; final String subclassTitle; - final String decoratedClassName; + final String primaryAbility; String filename = null; + SidekickProficiencies sidekickProficiencies = null; Json2QuteClass(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) { super(index, type, jsonNode); - boolean pushed = parseState().push(getSources(), rootNode); // store state - try { - if (!isSidekick()) { - findClassHitDice(); - findStartingEquipment(); - findClassProficiencies(); - } - - decoratedClassName = type.decoratedName(jsonNode); - classSource = jsonNode.get("source").asText(); - subclassTitle = getTextOrEmpty(rootNode, "subclassTitle"); - - // class features can be text elements or object elements (classFeature field) - findClassFeatures(Tools5eIndexType.classfeature, jsonNode.get("classFeatures"), classFeatures, "classFeature"); - findSubclasses(); - } finally { - parseState().pop(pushed); // restore state - } + decoratedClassName = type.decoratedName(jsonNode); + classSource = jsonNode.get("source").asText(); + isSidekick = ClassFields.isSidekick.booleanOrDefault(jsonNode, false); + subclassTitle = ClassFields.subclassTitle.getTextOrEmpty(jsonNode); + primaryAbility = buildPrimaryAbility(); } @Override @@ -66,691 +63,955 @@ public String getFileName() { @Override protected QuteClass buildQuteResource() { + // Some compensation for sidekicks. Do this first + List features = findClassFeatures( + Tools5eIndexType.classfeature, + ClassFields.classFeatures.ensureArrayIn(rootNode), + ClassFields.classFeature); + Tags tags = new Tags(getSources()); tags.add("class", getName()); - // May have fluff text. Does not have separate fluff images - List text = getFluff(Tools5eIndexType.classFluff, "##", new ArrayList<>()); + List progression = buildProgressionTable(features, rootNode, ClassFields.classTableGroups); + + List images = new ArrayList<>(); + List text = getFluff(Tools5eIndexType.classFluff, "##", images); maybeAddBlankLine(text); text.add("## Class Features"); - for (ClassFeature cf : classFeatures) { + for (ClassFeature cf : features) { cf.appendText(this, text, getSources().primarySource()); } - addOptionalFeatureText(rootNode, text); - - List progression = new ArrayList<>(); - buildFeatureProgression(progression); - maybeAddBlankLine(progression); - buildClassProgression(rootNode, progression, "classTableGroups"); + addOptionalFeatureText(rootNode, getSources().primarySource(), text); return new QuteClass(getSources(), decoratedClassName, getSourceText(getSources()), - startingHitDice(), String.join("\n", progression), + primaryAbility, + buildHitDie(), buildStartingEquipment(), - buildStartMulticlassing(), + buildMulticlassing(), String.join("\n", text), + images, tags); } public List buildSubclasses() { List quteSc = new ArrayList<>(); - for (Subclass sc : subclasses) { - boolean pushed = parseState().push(sc.sources); - filename = Tools5eQuteBase.fixFileName(sc.getName(), sc.sources); + for (String scKey : ClassFields.subclassKeys.getListOfStrings(rootNode, tui())) { + JsonNode scNode = index.getNode(scKey); + Tools5eSources scSources = Tools5eSources.findSources(scKey); + String scName = scSources.getName(); + String scShortName = ClassFields.shortName.getTextOrDefault(scNode, scName); + + List scFeatures = findClassFeatures( + Tools5eIndexType.subclassFeature, + ClassFields.subclassFeatures.ensureArrayIn(scNode), + ClassFields.subclassFeature); + + filename = Tools5eQuteBase.fixFileName(scName, scSources); + boolean pushed = parseState().push(scSources); try { - Tags tags = new Tags(sc.sources); - tags.add("subclass", getName(), sc.shortName); + Tags tags = new Tags(scSources); + tags.add("subclass", getName(), scShortName); - if (tags.toString().contains("cleric")) { - tags.add("domain", sc.shortName); + if (getName().matches(".*[Cc]leric.*")) { + tags.add("domain", scShortName); } - List text = new ArrayList<>(); + List progression = buildProgressionTable(scFeatures, scNode, ClassFields.subclassTableGroups); + + List images = new ArrayList<>(); + List text = getFluff(scNode, Tools5eIndexType.subclassFluff, "##", images); text.add("## Class Features"); - for (ClassFeature scf : sc.classFeatures) { - scf.appendText(this, text, sc.sources.primarySource()); + for (ClassFeature scf : scFeatures) { + scf.appendText(this, text, scSources.primarySource()); } - addOptionalFeatureText(sc.subclassNode, text); - - List progression = new ArrayList<>(); - buildClassProgression(sc.subclassNode, progression, "subclassTableGroups"); + addOptionalFeatureText(scNode, scSources.primarySource(), text); - quteSc.add(new QuteSubclass(sc.sources, - sc.getName(), - getSourceText(sc.sources), + quteSc.add(new QuteSubclass(scSources, + scName, + getSourceText(scSources), getName(), // parentClassName String.format("[%s](%s.md)", decoratedClassName, - Tools5eQuteBase.getClassResource(getName(), sc.parentClassSource)), - sc.parentClassSource, // parentClassSource + Tools5eQuteBase.getClassResource(getName(), getSources().primarySource())), + getSources().primarySource(), subclassTitle, String.join("\n", progression), String.join("\n", text), + images, tags)); + } finally { parseState().pop(pushed); filename = null; } } - return quteSc; } - void buildFeatureProgression(List progression) { - List> row_levels = new ArrayList<>(21); - for (int i = 0; i < 21; i++) { - row_levels.add(new ArrayList<>()); + private ClassFeature getClassFeature(String featureKey, Tools5eIndexType featureType) { + return keyToClassFeature.computeIfAbsent(featureKey, k -> { + JsonNode featureNode = index().getOriginNoFallback(featureKey); + return new ClassFeature(featureType, featureKey, featureNode); + }); + } + + List findClassFeatures(Tools5eIndexType featureType, JsonNode featureElements, + ClassFields field) { + List features = new ArrayList<>(); + for (JsonNode featureNode : featureElements) { + + String featureKey = featureNode.isTextual() + ? featureType.fromTagReference(featureNode.asText()) + : featureType.fromTagReference(ClassFields.classFeature.getTextOrEmpty(featureNode)); + + var classFeature = getClassFeature(featureKey, featureType); + + if (isSidekick && "1".equals(classFeature.level()) + && "Bonus Proficiencies".equalsIgnoreCase(classFeature.getName())) { + // sidekick classes don't have startingProficiencies, they have a bonus + // proficiency feature instead. + sidekickProficiencies = new SidekickProficiencies(classFeature.cfNode()); + } + // Add to list of features (for content rendering later) + features.add(classFeature); } + return features; + } - Map> levelToFeature = classFeatures.stream() - .collect(Collectors.groupingBy(x -> Integer.valueOf(x.level))); + void addOptionalFeatureText(JsonNode optFeatures, String primarySource, List text) { + JsonNode optionalFeatureProgression = ClassFields.optionalfeatureProgression.getFrom(optFeatures); + if (optionalFeatureProgression == null) { + return; + } - // Headings are in row 0 - row_levels.get(0).add("Level"); - row_levels.get(0).add("PB"); - row_levels.get(0).add("Features"); - // Values - for (int level = 1; level < row_levels.size(); level++) { - final int featureLevel = level; - row_levels.get(level).add(JsonSource.levelToString(level)); - row_levels.get(level).add("+" + levelToPb(level)); + String relativePath = Tools5eIndexType.optionalFeatureTypes.getRelativePath(); + String source = SourceField.source.getTextOrDefault(optionalFeatureProgression, primarySource); - List features = levelToFeature.get(level); - if (features == null || features.isEmpty()) { - row_levels.get(level).add("⏤"); - } else { - row_levels.get(level).add(features.stream() - .map(x -> markdownLinkify(x.getName(), featureLevel)) - .collect(Collectors.joining(", "))); + maybeAddBlankLine(text); + text.add("## Optional Features"); + for (JsonNode ofp : iterableElements(optionalFeatureProgression)) { + for (String featureType : Tools5eFields.featureType.getListOfStrings(ofp, tui())) { + OptionalFeatureType oft = index.getOptionalFeatureType(featureType, source); + if (oft != null) { + maybeAddBlankLine(text); + text.add("> [!example]- " + oft.title); + text.add(String.format("> ![%s](%s%s/%s.md#%s)", + oft.title, + index().compendiumVaultRoot(), relativePath, + oft.getFilename(), + toAnchorTag(oft.title))); + text.add("^list-" + slugify(oft.title)); + } else { + tui().errorf( + Msg.UNRESOLVED, "Can not find optional feature type %s for progression. Source: %s; Reference: %s", + featureType, ofp, parseState().getSource()); + } } } + } + + String buildPrimaryAbility() { + // primary ability only exists in 2024 class versions + if (!ClassFields.primaryAbility.existsIn(rootNode)) { + return null; + } + + // Array of objects with multiple properties + List abilities = ClassFields.primaryAbility.streamProps(rootNode) + .map(e -> { + return streamPropsExcluding(rootNode) + .filter(x -> x.getValue().asBoolean()) + .map(x -> SkillOrAbility.format(x.getKey(), index(), getSources())) + .toList(); + }) + .map(l -> joinConjunct("and", l)) + .toList(); + return joinConjunct("or", abilities); + } + + HitPointDie buildHitDie() { + if (isSidekick) { + // Sidekicks do not have a hit die. Hit points depend on creature statblock + return new HitPointDie(getName(), 0, 0, this.sources.isClassic(), isSidekick); + } + JsonNode hdNode = ClassFields.hd.getFrom(rootNode); + if (hdNode != null) { + // both attributes are required. Default should not be necessary + return new HitPointDie( + getName(), + ClassFields.number.intOrDefault(hdNode, 1), + ClassFields.faces.intOrDefault(hdNode, 1), + this.sources.isClassic(), + isSidekick); + } + return null; + } + + StartingEquipment buildStartingEquipment() { + if (isSidekick) { + if (sidekickProficiencies == null) { + tui().warnf(Msg.UNKNOWN, "Sidekick class %s has no proficiencies", getName()); + return null; + } + return new StartingEquipment( + sidekickProficiencies.savingThrows(), + sidekickProficiencies.skills(), + sidekickProficiencies.weapons(), + sidekickProficiencies.tools(), + sidekickProficiencies.armor(), + "", + sources.isClassic()); + } + + List savingThrows = ClassFields.proficiency.streamFrom(rootNode) + .map(n -> SkillOrAbility.format(n.asText(), index(), getSources())) + .sorted() + .toList(); + + JsonNode startingProficiencies = ClassFields.startingProficiencies.getFrom(rootNode); - progression.addAll( - convertRowsToTable(row_levels, "Feature progression", - List.of("- PB: Proficiency Bonus"), - "feature-progression")); - maybeAddBlankLine(progression); + var armor = listOfArmorProficiencies(startingProficiencies); + var skills = listOfSkillProfiencies(startingProficiencies); + var tools = listOfToolProfiencies(startingProficiencies); + var weapons = listOfWeaponProfiencies(startingProficiencies); + + return new StartingEquipment(savingThrows, skills, weapons, tools, armor, + equipmentDescription(ClassFields.startingEquipment.getFrom(rootNode)), + sources.isClassic()); } - void buildClassProgression(JsonNode node, List progression, String field) { - if (!node.has(field)) { - return; + Multiclassing buildMulticlassing() { + JsonNode multiclassing = ClassFields.multiclassing.getFrom(rootNode); + if (multiclassing == null || multiclassing.isEmpty()) { + return null; } + // primary ablity only exists in 2024 class versions (or homebrew) + // requirements only exist in 2014 class versions (or homebrew) + String requirements = null; + if (ClassFields.requirements.existsIn(multiclassing)) { + List reqContents = new ArrayList<>(); - List> row_levels = new ArrayList<>(21); - for (int i = 0; i < 21; i++) { - row_levels.add(new ArrayList<>()); - if (i == 0) { - row_levels.get(i).add("Level"); + JsonNode reqNode = ClassFields.requirements.getFrom(multiclassing); + JsonNode orNode = ClassFields.or.getFrom(reqNode); + if (orNode == null) { + reqContents.add("**Ability Score Minimum:**" + abilityRequirements(reqNode, ", ")); } else { - row_levels.get(i).add(JsonSource.levelToString(i)); + reqContents.add("**Ability Score Minimum:**" + streamOf(orNode) + .map(n -> abilityRequirements(n, " or ")) + .collect(Collectors.joining("; "))); } + appendToText(reqContents, SourceField.entries.getFrom(reqNode), null); + requirements = String.join("\n", reqContents); } - for (JsonNode table : iterableElements(node.get(field))) { - // Headings - for (JsonNode c : iterableElements(table.get("colLabels"))) { - String label = c.asText(); - if (label.contains("|spells|")) { - row_levels.get(0).add( - label.substring(label.indexOf(" ") + 1, label.indexOf("|"))); - } else { - row_levels.get(0).add(replaceText(label)); + JsonNode reqSpecialNode = ClassFields.requirementsSpecial.getFrom(multiclassing); + String requirementsSpecial = reqSpecialNode == null + ? null + : replaceText(reqSpecialNode); + + JsonNode profGained = ClassFields.proficienciesGained.getFrom(multiclassing); + + List skillsGained = listOfSkillProfiencies(profGained); + List weaponsGained = listOfWeaponProfiencies(profGained); + List toolsGained = listOfToolProfiencies(profGained); + List armorGained = listOfArmorProficiencies(profGained); + + return new Multiclassing( + primaryAbility, + requirements, + requirementsSpecial, + join(", ", skillsGained), + join(", ", weaponsGained), + join(", ", toolsGained), + join(", ", armorGained), + flattenToString(SourceField.entries.getFrom(multiclassing), "\n"), + getSources().isClassic()); + } + + List buildProgressionTable(List features, JsonNode sourceNode, ClassFields field) { + List headings = new ArrayList<>(); + List spellCasting = new ArrayList<>(); + + Map levels = new HashMap<>(); + for (int i = 1; i < 21; i++) { + // Create LevelProgression for each level + // this sets level and proficiency bonus for level + levels.put(i, new LevelProgression(i)); + } + + for (ClassFeature feature : features) { + var lp = levels.get(Integer.valueOf(feature.level())); + lp.addFeature(feature); + } + + for (JsonNode tableNode : field.iterateArrayFrom(sourceNode)) { + if (ClassFields.rows.existsIn(tableNode)) { + // headings for other/middle columns + for (JsonNode label : ClassFields.colLabels.iterateArrayFrom(tableNode)) { + headings.add(markdownLinkToHtml(replaceText(label))); } - } - // Values - if (table.has("rows")) { - ArrayNode rows = table.withArray("rows"); - for (int i = 0; i < rows.size(); i++) { - int level = i + 1; - if (level >= row_levels.size()) { - tui().errorf("Badly formed class-progression table in %s: %s", sources, table.toString()); - break; + // values for other/middle columns + int i = 1; + for (JsonNode row : ClassFields.rows.iterateArrayFrom(tableNode)) { + var lp = levels.get(Integer.valueOf(i)); + for (JsonNode col : iterableElements(row)) { + lp.addValue(progressionColumnValue(col)); } - rows.get(i).forEach(c -> row_levels.get(level).add(columnValue(c))); + i++; } - } else if (table.has("rowsSpellProgression")) { - ArrayNode rows = table.withArray("rowsSpellProgression"); - for (int i = 0; i < rows.size(); i++) { - int level = i + 1; - if (level >= row_levels.size()) { - tui().errorf("Badly formed spell-progression table in %s: %s", sources, table.toString()); - break; + } else if (ClassFields.rowsSpellProgression.existsIn(tableNode)) { + // headings for other/middle columns + for (JsonNode label : ClassFields.colLabels.iterateArrayFrom(tableNode)) { + spellCasting.add(replaceText(label)); + } + int i = 1; + for (JsonNode row : ClassFields.rowsSpellProgression.iterateArrayFrom(tableNode)) { + var lp = levels.get(Integer.valueOf(i)); + for (JsonNode col : iterableElements(row)) { + lp.addSpellSlot(progressionColumnValue(col)); } - rows.get(i).forEach(c -> row_levels.get(level).add(columnValue(c))); + i++; } } } - progression.addAll(convertRowsToTable(row_levels, "Class progression", - List.of("- 1st-9th: Spell slots per level"), "class-progression")); + return progressionAsTable(headings, spellCasting, levels); } - List convertRowsToTable(List> row_levels, String title, List footer, String blockid) { + List progressionAsTable(List headings, + List spellCasting, + Map levels) { List text = new ArrayList<>(); - // Convert each row to markdown columns - row_levels.forEach(r -> text.add("| " + String.join(" | ", r) + " |")); - - // insert a header delimiting row (copy row 0, replace everything not a "|" with a "-") - text.add(1, text.get(0).replaceAll("[^|]", "-")); - - if (footer != null && !footer.isEmpty()) { - maybeAddBlankLine(text); - text.addAll(footer); - } + text.add("[!tldr] Class and Feature Progression"); + text.add(""); + text.add(""); + text.add(""); + // Top-level heading row to group spell casting columns + text.add("%s" + .formatted( + 3 + headings.size(), + spellCasting.isEmpty() + ? "" + : "" + .formatted(spellCasting.size()))); + + text.add( + "%s%s" + .formatted( + headings.isEmpty() + ? "" + : "", + spellCasting.isEmpty() + ? "" + : "")); + + text.add(""); + + for (int i = 1; i < 21; i++) { + var lp = levels.get(Integer.valueOf(i)); + text.add( + "%s%s" + .formatted( + lp.level, lp.pb, + join(", ", lp.features), + lp.values.isEmpty() + ? "" + : "", + spellCasting.isEmpty() + ? "" + : "")); + } + + text.add("
Spell Slots per Spell Level
LevelPBFeatures
" + join("", headings) + + "" + + join("", spellCasting) + "
%s%s%s
" + join("", lp.values) + + "" + + join("", lp.spellSlots) + "
"); // Move everything into a callout box text.replaceAll(s -> "> " + s); - - // add start of block - text.add(0, "> [!tldr]- " + title); - text.add(1, "> "); // must have a blank line before table starts - - text.add("^" + blockid); + text.add("^class-progession"); return text; } - String buildStartingEquipment() { - List startingEquipment = new ArrayList<>(); - startingEquipment.add(String.format("You are proficient with the following items%s.", - additionalFromBackground - ? ", in addition to any proficiencies provided by your race or background" - : "")); - maybeAddBlankLine(startingEquipment); - - if (startingText.containsKey("saves")) { - startingEquipment.add(String.format("- **Saving Throws**: %s", startingTextJoinOrDefault("saves", "none"))); - } - startingEquipment.add(String.format("- **Armor**: %s", startingTextJoinOrDefault("armor", "none"))); - startingEquipment.add(String.format("- **Weapons**: %s", startingTextJoinOrDefault("weapons", "none"))); - startingEquipment.add(String.format("- **Tools**: %s", startingTextJoinOrDefault("tools", "none"))); - startingEquipment.add(String.format("- **Skills**: %s", startingTextJoinOrDefault("skills", "none"))); - - if (!isSidekick()) { - maybeAddBlankLine(startingEquipment); - startingEquipment.add(String.format("You begin play with the following equipment%s.", - additionalFromBackground ? ", in addition to any equipment provided by your background" : "")); - maybeAddBlankLine(startingEquipment); - List equipment = startingText.get("equipment"); - if (equipment == null) { - startingEquipment.add("- None"); - } else { - startingEquipment.addAll(equipment); - } - String wealth = startingTextJoinOrDefault("wealth", ""); - if (!wealth.isEmpty()) { - maybeAddBlankLine(startingEquipment); - startingEquipment.add(String.format("Alternatively, you may start with %s gp and choose your own equipment.", - startingTextJoinOrDefault("wealth", "3d4 x 10"))); - } - } - return String.join("\n", startingEquipment); - } - - String buildStartMulticlassing() { - JsonNode multiclassing = rootNode.get("multiclassing"); - if (multiclassing == null) { - return null; + String progressionColumnValue(JsonNode c) { + if (c == null || c.isNull()) { + return "⏤"; } + if (c.isObject()) { + String type = ClassFields.type.getTextOrEmpty(c); + return switch (type) { + case "dice" -> { + List rolls = new ArrayList<>(); + for (JsonNode roll : ClassFields.toRoll.iterateArrayFrom(c)) { + rolls.add("%sd%s".formatted( + ClassFields.number.getTextOrEmpty(roll), + ClassFields.faces.getTextOrEmpty(roll))); + } + yield join(",", rolls); + } + case "bonus", "bonusSpeed" -> { + yield "+" + ClassFields.value.getTextOrEmpty(c); + } + default -> throw new IllegalArgumentException("Unknown column object value: " + c.toPrettyString()); + }; + } + String value = c.asText(); + return value.isEmpty() || value.equals("0") + ? "⏤" + : replaceText(value); + } + + String abilityRequirements(JsonNode reqNode, String joiner) { + return streamProps(reqNode) + .filter(n -> SkillOrAbility.fromTextValue(n.getKey()) != null) + .map(e -> "%s %s".formatted( + SkillOrAbility.format(e.getKey(), index(), getSources()), + e.getValue().asText())) + .sorted() + .collect(Collectors.joining(joiner)); + } + + List listOfArmorProficiencies(JsonNode containingNode) { + return ClassFields.armor.streamFrom(containingNode) + .map(n -> { + if (n.isTextual()) { + return armorToLink(n.asText()); + } + return armorToLink(ClassFields.full.getTextOrDefault(n, + ClassFields.proficiency.getTextOrEmpty(n))); + }) + .toList(); + } + + List listOfSkillProfiencies(JsonNode containingNode) { + if (isSidekick) { + return List.of(); + } + + return ClassFields.skills.streamFrom(containingNode) + // ARRAY of objects + .map(n -> { + String choose = null; + List baseSkills = new ArrayList<>(); + // any: integer + // choose: { + // count: integer, + // from: [ + // ... + // ] + // } + // skillName: true + for (var x : iterableFields(n)) { + if ("any".equals(x.getKey())) { + choose = skillChoices(List.of(), + x.getValue().asInt()); + } else if ("choose".equals(x.getKey())) { + choose = skillChoices( + ClassFields.from.getListOfStrings(x.getValue(), tui()), + ClassFields.count.intOrDefault(x.getValue(), 1)); + } else { + SkillOrAbility skill = index.findSkillOrAbility(n.asText(), getSources()); + if (skill != null) { + baseSkills.add(linkifySkill(skill)); + } + } + } - final List startMulticlass = new ArrayList<>(); - startMulticlass.add(String.format("To multiclass as a %s, you must meet the following prerequisites:", getName())); + String allSkills = joinConjunct("and", baseSkills); + if (baseSkills.size() > 0 && choose != null) { + return "%s; and %s".formatted(allSkills, choose); + } + return choose == null ? allSkills : choose; + }) + .toList(); + } - maybeAddBlankLine(startMulticlass); - JsonNode requirements = multiclassing.get("requirements"); - if (requirements == null) { - tui().warnf(Msg.NOT_SET, "No requirements specified to multiclass %s: %s", getSources().getKey(), multiclassing); - } else if (requirements.has("or")) { - List options = new ArrayList<>(); - requirements.get("or").get(0).fields().forEachRemaining(ability -> options.add(String.format("%s %s", - SkillOrAbility.format(ability.getKey(), index(), getSources()), ability.getValue().asText()))); - startMulticlass.add("- " + String.join(", or ", options)); - } else { - requirements.fields().forEachRemaining( - ability -> startMulticlass.add(String.format("- %s %s", - SkillOrAbility.format(ability.getKey(), index(), getSources()), ability.getValue().asText()))); - } - - JsonNode gained = multiclassing.get("proficienciesGained"); - if (gained != null) { - maybeAddBlankLine(startMulticlass); - startMulticlass.add("You gain the following proficiencies:"); - maybeAddBlankLine(startMulticlass); - - startMulticlass.add(String.format("- **Armor**: %s", - startingTextJoinOrDefault(gained, "armor"))); - startMulticlass.add(String.format("- **Weapons**: %s", - startingTextJoinOrDefault(gained, "weapons"))); - startMulticlass.add(String.format("- **Tools**: %s", - startingTextJoinOrDefault(gained, "tools"))); - - if (gained.has("skills")) { - startMulticlass.add(String.format("- **Skills**: %s", - classSkills(gained, sources))); - } - } - return String.join("\n", startMulticlass); + List listOfWeaponProfiencies(JsonNode containingNode) { + return ClassFields.weapons.streamFrom(containingNode) + .map(n -> { + if (ClassFields.optional.booleanOrDefault(n, false)) { + return "%s (optional)".formatted(replaceText(ClassFields.proficiency.getTextOrEmpty(n))); + } + String weaponType = n.asText(); + if (weaponType.matches("(?i)(simple|martial)")) { + return "%s weapons".formatted( + sources.isClassic() ? weaponType : toTitleCase(weaponType)); + } + return replaceText(weaponType); + }) + .toList(); } - boolean isSidekick() { - return getSources().getName().toLowerCase().contains("sidekick"); + List listOfToolProfiencies(JsonNode containingNode) { + return ClassFields.tools.streamFrom(containingNode) + .map(this::replaceText) + .toList(); } - void findClassHitDice() { - JsonNode hd = rootNode.get("hd"); - if (hd != null) { - put("hd", List.of(hd.get("faces").asText())); - } + String armorToLink(String armor) { + return armor + .replaceAll("^light", linkify(Tools5eIndexType.itemType, + sources.isClassic() ? "la|PHB|light armor" : "la|XPHB|Light armor")) + .replaceAll("^medium", linkify(Tools5eIndexType.itemType, + sources.isClassic() ? "ma|PHB|medium armor" : "ma|XPHB|Medium armor")) + .replaceAll("^heavy", linkify(Tools5eIndexType.itemType, + sources.isClassic() ? "ha|PHB|heavy armor" : "ha|XPHB|Heavy armor")) + .replaceAll("^shields?", linkify(Tools5eIndexType.item, + sources.isClassic() ? "shield|PHB|shields" : "shield|XPHB|Shields")); } - void findClassFeatures(Tools5eIndexType type, JsonNode arrayElement, List features, String fieldName) { - for (JsonNode cf : iterableElements(arrayElement)) { + String skillChoices(Collection skills, int numSkills) { + if (skills.isEmpty() || skills.size() >= 18) { + String link = "||skill%s".formatted(numSkills == 1 ? "" : "s"); + String linkToSkills = linkifyRules(Tools5eIndexType.skill, link, "skills"); + return sources.isClassic() + ? "choose any %s %s".formatted(numSkills, linkToSkills) + : "Choose %s %s".formatted(numSkills, linkToSkills); + } + + List formatted = skills.stream().map(x -> index.findSkillOrAbility(x, getSources())) + .filter(x -> x != null) + .sorted(SkillOrAbility.comparator) + .map(x -> linkifySkill(x)) + .toList(); + return sources.isClassic() + ? "choose %s from %s".formatted(numSkills, + joinConjunct(" and ", formatted)) + : "*Choose %s:* %s".formatted(numSkills, + joinConjunct(" or ", formatted)); + } + + String equipmentDescription(JsonNode startingEquipment) { + List text = new ArrayList<>(); - ClassFeature feature = findClassFeature(this, type, cf, fieldName); - if (feature == null) { - continue; + if (ClassFields.additionalFromBackground.existsIn(startingEquipment) + && ClassFields.defaultEquipment.existsIn(startingEquipment)) { + // Older default format. + if (ClassFields.additionalFromBackground.booleanOrDefault(startingEquipment, false)) { + text.add("You start with the following items, plus anything provided by your background."); + text.add(""); } - features.add(feature); + for (JsonNode item : ClassFields.defaultEquipment.iterateArrayFrom(startingEquipment)) { + text.add("- %s".formatted(replaceText(item))); + } - if (isSidekick() && "1".equals(feature.level) && feature.getName().equals("Bonus Proficiencies")) { - sidekickProficiencies(feature.cfNode); + String goldAlternative = ClassFields.goldAlternative.getTextOrNull(startingEquipment); + if (isPresent(goldAlternative)) { + text.add(""); + text.add("Alternatively, you may start with %s gp to buy your own equipment." + .formatted(replaceText(goldAlternative))); } + } else { + JsonNode entries = SourceField.entries.getFrom(startingEquipment); + appendToText(text, entries, null); } + return String.join("\n", text); } static ClassFeature findClassFeature(JsonSource converter, Tools5eIndexType type, JsonNode cf, String fieldName) { String lookup = cf.isTextual() ? cf.asText() : cf.get(fieldName).asText(); String finalKey = type.fromTagReference(lookup); - JsonNode featureJson = finalKey == null ? null : converter.index().resolveClassFeatureNode(finalKey); - - if (featureJson == null) { - return null; // skipped or not found - } - ClassFeature feature = keyToClassFeature.get(finalKey); if (feature == null) { - feature = new ClassFeature(); - feature.cfNode = featureJson; - feature.cfType = type; - feature.level = lookup.replaceAll(".*\\|(\\d+)\\|?.*", "$1"); - feature.cfSources = Tools5eSources.findSources(finalKey); - - keyToClassFeature.put(finalKey, feature); - } - // check inclusion of class feature sources - if (!converter.cfg().sourceIncluded(feature.cfSources)) { - return null; // skipped + JsonNode cfNode = converter.index().getNode(finalKey); + if (cfNode == null) { + return null; // skipped or not found + } + feature = new ClassFeature(type, finalKey, cfNode); + keyToClassFeature.putIfAbsent(finalKey, feature); } return feature; } - void findSubclasses() { - Map scNodes = new HashMap<>(); - for (JsonNode x : index().classElementsMatching(Tools5eIndexType.subclass, getSources().getName(), classSource)) { - scNodes.put(x, classSource); + static record ClassFeature( + Tools5eIndexType cfType, + JsonNode cfNode, + Tools5eSources cfSources, + KeyData keyData) { + public ClassFeature(Tools5eIndexType cfType, String key, JsonNode cfNode) { + this(cfType, cfNode, Tools5eSources.findSources(key), + cfType == Tools5eIndexType.classfeature + ? new ClassFeatureKeyData(key) + : new SubclassFeatureKeyData(key)); } - for (String aliasKey : index.getAliasesFor(getSources().getKey())) { - int lastSegment = aliasKey.lastIndexOf('|'); - String aliasSource = aliasKey.substring(lastSegment + 1); - for (JsonNode x : index().classElementsMatching(Tools5eIndexType.subclass, getSources().getName(), aliasSource)) { - scNodes.put(x, aliasSource); - } + + public String getName() { + return cfSources.getName(); } - for (JsonNode scNode : scNodes.keySet()) { - String parentClassSource = scNodes.get(scNode); - String scKey = Tools5eIndexType.subclass.createKey(scNode); - JsonNode resolved = index.resolveClassFeatureNode(scKey, scNode); + public String level() { + return keyData.level(); + } - Subclass sc = new Subclass(); - sc.subclassNode = resolved; - sc.parentClassSource = parentClassSource; // e.g. PHB or DMG - sc.parentKey = getSources().getKey(); - sc.shortName = resolved.get("shortName").asText(); - sc.sources = Tools5eSources.findSources(scKey); + void appendLink(JsonSource converter, List text, String pageSource) { + converter.maybeAddBlankLine(text); + String x = converter.decoratedFeatureTypeName(cfSources, cfNode); + text.add(String.format("[%s](#%s)", x, toAnchorTag(x + " (Level " + level() + ")"))); + } - // If parent sources does not contain subclass source... - if (!getSources().contains(sc.sources) && index.isExcluded(scKey)) { - continue; // excluded + public void appendListItemText(JsonSource converter, List text, String pageSource) { + boolean pushed = converter.parseState().pushFeatureType(); + try { + text.add("**" + converter.decoratedFeatureTypeName(cfSources, cfNode) + "**"); + if (!cfSources.primarySource().equalsIgnoreCase(pageSource)) { + text.add(converter.getLabeledSource(cfSources)); + } + text.add(""); + converter.appendToText(text, SourceField.entries.getFrom(cfNode), null); + text.add(""); + } finally { + converter.parseState().pop(pushed); } + } - // subclass features are text elements (null field) - findClassFeatures(Tools5eIndexType.subclassFeature, resolved.get("subclassFeatures"), sc.classFeatures, null); - - subclasses.add(sc); + void appendText(JsonSource converter, List text, String primarySource) { + boolean pushed = converter.parseState().pushFeatureType(); + try { + converter.maybeAddBlankLine(text); + text.add("### " + converter.decoratedFeatureTypeName(cfSources, cfNode) + " (Level " + level() + ")"); + if (!cfSources.primarySource().equalsIgnoreCase(primarySource)) { + text.add(converter.getLabeledSource(cfSources)); + } + converter.maybeAddBlankLine(text); + converter.appendToText(text, SourceField.entries.getFrom(cfNode), "####"); + } finally { + converter.parseState().pop(pushed); + } } } - void addOptionalFeatureText(JsonNode entry, List text) { - JsonNode optionalFeatureProgession = entry.get("optionalfeatureProgression"); - if (optionalFeatureProgession == null) { - return; - } + static interface KeyData { + String name(); - maybeAddBlankLine(text); - text.add("## Optional Features"); + String parentName(); - String relativePath = Tools5eIndexType.optionalFeatureTypes.getRelativePath(); - String source = entry.get("source").asText(); - for (JsonNode ofp : iterableElements(optionalFeatureProgession)) { - for (String featureType : Tools5eFields.featureType.getListOfStrings(ofp, tui())) { - OptionalFeatureType oft = index.getOptionalFeatureType(featureType, source); + String parentSource(); - if (oft != null) { - maybeAddBlankLine(text); - text.add("> [!example]- " + oft.title); - text.add(String.format("> ![%s](%s%s/%s.md#%s)", - oft.title, - index().compendiumVaultRoot(), relativePath, - oft.getFilename(), - toAnchorTag(oft.title))); - text.add("^list-" + slugify(oft.title)); - } else { - tui().errorf( - Msg.UNRESOLVED, "Can not find optional feature type %s for progression. Source: %s", - featureType, ofp); - } - } - } + String level(); + + String itemSource(); } - void findStartingEquipment() { - JsonNode equipment = rootNode.get("startingEquipment"); - if (equipment != null) { - String wealth = getWealth(equipment); - put("wealth", List.of(wealth)); - put("equipment", defaultEquipment(equipment)); - additionalFromBackground = booleanOrDefault(equipment, "additionalFromBackground", true); + static class ClassFeatureKeyData implements KeyData { + final String cfName; + final String className; + final String classSource; + final String level; + final String cfSource; + + public ClassFeatureKeyData(String key) { + String[] parts = key.split("\\|"); + this.cfName = parts[1]; + this.className = parts[2]; + this.classSource = parts[3]; + this.level = parts[4]; + this.cfSource = parts[5]; } - } - public void findClassProficiencies() { - if (rootNode.has("proficiency")) { - List savingThrows = new ArrayList<>(); - rootNode.withArray("proficiency").forEach(n -> savingThrows.add(asAbilityEnum(n))); - put("saves", savingThrows); + @Override + public String name() { + return cfName; } - JsonNode startingProf = rootNode.get("startingProficiencies"); - if (startingProf == null) { - tui().errorf("%s has no starting proficiencies", sources); - } else { - if (startingProf.has("armor")) { - put("armor", findAndReplace(startingProf, "armor")); - } - if (startingProf.has("weapons")) { - put("weapons", findAndReplace(startingProf, "weapons")); - } - if (startingProf.has("tools")) { - put("tools", findAndReplace(startingProf, "tools")); - } - if (startingProf.has("skills")) { - put("skills", List.of(classSkills(startingProf, sources))); - } + @Override + public String parentName() { + return className; } - } - void sidekickProficiencies(JsonNode sidekickClassFeature) { - for (JsonNode e : iterableEntries(sidekickClassFeature)) { - String line = e.asText(); - if (line.contains("saving throw")) { - //"The sidekick gains proficiency in one saving throw of your choice: Dexterity, Intelligence, or Charisma.", - //"The sidekick gains proficiency in one saving throw of your choice: Wisdom, Intelligence, or Charisma.", - //"The sidekick gains proficiency in one saving throw of your choice: Strength, Dexterity, or Constitution.", - String text = line.replaceAll(".*in one saving throw of your choice: (.*)", "$1") - .replaceAll("or ", "").replace(".", ""); - put("saves", List.of(text)); - } - if (line.contains("skills")) { - // "In addition, the sidekick gains proficiency in five skills of your choice, and it gains proficiency with light armor. If it is a humanoid or has a simple or martial weapon in its stat block, it also gains proficiency with all simple weapons and with two tools of your choice." - // "In addition, the sidekick gains proficiency in two skills of your choice from the following list: {@skill Arcana}, {@skill History}, {@skill Insight}, {@skill Investigation}, {@skill Medicine}, {@skill Performance}, {@skill Persuasion}, and {@skill Religion}.", - // "In addition, the sidekick gains proficiency in two skills of your choice from the following list: {@skill Acrobatics}, {@skill Animal Handling}, {@skill Athletics}, {@skill Intimidation}, {@skill Nature}, {@skill Perception}, and {@skill Survival}.", - String numSkills = line.replaceAll(".* proficiency in (.*) skills .*", "$1"); - int count = Integer.parseInt(textToInt(numSkills)); - put("numSkills", List.of(count + "")); - - Collection skills; - int start = line.indexOf("list:"); - if (start >= 0) { - int end = line.indexOf('.'); - String text = line.substring(start + 5, end).trim() - .replaceAll("\\{@skill ([^}]+)}", "$1") - .replace(".", "") - .replace("and ", ""); - skills = Set.of(text.split("\\s*,\\s*")); - } else { - skills = SkillOrAbility.allSkills; - } - put("skills", List.of(skillChoices(skills, count))); - } - if (line.contains("armor")) { - // "In addition, the sidekick gains proficiency in five skills of your choice, and it gains proficiency with light armor. If it is a humanoid or has a simple or martial weapon in its stat block, it also gains proficiency with all simple weapons and with two tools of your choice." - // "The sidekick gains proficiency with light armor, and if it is a humanoid or has a simple or martial weapon in its stat block, it also gains proficiency with all simple weapons." - // "The sidekick gains proficiency with all armor, and if it is a humanoid or has a simple or martial weapon in its stat block, it gains proficiency with shields and all simple and martial weapons." - if (line.contains("all armor")) { // Warrior Sidekick - put("armor", List.of("light, medium, heavy, shields")); - put("weapons", List.of("martial")); - } else { - put("armor", List.of("light")); - put("weapons", List.of("simple")); - } - } - if (line.contains("tools")) { - put("tools", List.of("two tools of your choice")); - } + @Override + public String parentSource() { + return classSource; } - } - String skillChoices(Collection skills, int numSkills) { - return String.format("Choose %s from %s", - numSkills, - skills.stream().map(x -> index.findSkillOrAbility(x, getSources())) - .filter(x -> x != null) - .sorted(SkillOrAbility.comparator) - .map(x -> "*" + x.value() + "*") - .collect(Collectors.joining(", "))); + @Override + public String level() { + return level; + } + + @Override + public String itemSource() { + return cfSource; + } } - String chooseSkillListFrom(JsonNode choose) { - int count = choose.has("count") - ? choose.get("count").asInt() - : 1; + // Unpack a subclass key + static class SubclassKeyData implements KeyData { + final String scName; + final String className; + final String classSource; + final String scSource; - ArrayNode from = choose.withArray("from"); - return skillChoices(toListOfStrings(from), count); - } + public SubclassKeyData(String key) { + String[] parts = key.split("\\|"); + this.scName = parts[1]; + this.className = parts[2]; + this.classSource = parts[3]; + this.scSource = parts[4]; + } - String classSkills(JsonNode source, Tools5eSources sources) { - List result = new ArrayList<>(); + @Override + public String name() { + return scName; + } - ArrayNode skillNode = source.withArray("skills"); - if (skillNode.size() > 1) { - tui().errorf("Multivalue skill array in %s: %s", sources, source.toPrettyString()); + @Override + public String parentName() { + return className; } - JsonNode skills = skillNode.get(0); - for (Entry e : iterableFields(skills)) { - String skill = e.getKey(); - if ("choose".equals(skill)) { - result.add(chooseSkillListFrom(e.getValue())); - } else if ("any".equals(skill)) { - int count = skills.get("any").asInt(); - result.add(skillChoices(SkillOrAbility.allSkills, count)); - } else { - SkillOrAbility custom = index.findSkillOrAbility(skill, getSources()); - if (custom == null) { - tui().errorf("Unexpected skills in starting proficiencies for %s: %s", - sources, source.toPrettyString()); - } - result.add("*" + custom.value() + "*"); - } + @Override + public String parentSource() { + return classSource; } - return String.join("; ", result); - } - List defaultEquipment(JsonNode equipment) { - List text = new ArrayList<>(); - appendList(text, equipment.withArray("default"), ListType.unordered); - return text; - } + @Override + public String level() { + return ""; + } - String getWealth(JsonNode equipment) { - return replaceText(getTextOrEmpty(equipment, "goldAlternative")); + @Override + public String itemSource() { + return scSource; + } } - void put(String key, List value) { - startingText.put(key, value); - } + // Unpack a subclass feature key + static class SubclassFeatureKeyData implements KeyData { + final String scfName; + final String className; + final String classSource; + final String scName; + final String scSource; + final String level; + final String scfSource; - int startingHitDice() { - List text = startingText.get("hd"); - if (text == null || text.isEmpty()) { - return 0; + public SubclassFeatureKeyData(String key) { + String[] parts = key.split("\\|"); + this.scfName = parts[1]; + this.className = parts[2]; + this.classSource = parts[3]; + this.scName = parts[4]; + this.scSource = parts[5]; + this.level = parts[6]; + this.scfSource = parts[7]; } - if (text.size() > 1) { - throw new IllegalArgumentException("Unable to parse int from starting text field: " + text); + + @Override + public String name() { + return scfName; } - return Integer.parseInt(text.get(0)); - } - String startingTextJoinOrDefault(String field, String value) { - List text = startingText.get(field); - return text == null ? value : String.join(", ", text); - } + @Override + public String parentName() { + return scName; + } - String startingTextJoinOrDefault(JsonNode source, String field) { - return startingTextJoinOrDefault(source, field, s -> s); - } + @Override + public String parentSource() { + return scSource; + } - String startingTextJoinOrDefault(JsonNode source, String field, Function replacements) { - List text = findAndReplace(source, field, replacements); - return text == null || text.isEmpty() ? "none" : String.join(", ", text); - } + @Override + public String level() { + return level; + } - String textToInt(String text) { - switch (text) { - case "two" -> { - return "2"; - } - case "three" -> { - return "3"; - } - case "five" -> { - return "5"; - } - default -> { - tui().errorf("Unknown number of skills (%s) listed in sidekick class features (%s)", text, sources); - return "1"; - } + @Override + public String itemSource() { + return scfSource; } } - static class ClassFeature { - JsonNode cfNode; - String level; + static class LevelProgression { + final String level; + final String pb; + List features = new ArrayList<>(); + List values = new ArrayList<>(); + List spellSlots = new ArrayList<>(); - Tools5eSources cfSources; - Tools5eIndexType cfType; + LevelProgression(int level) { + this.level = JsonSource.levelToString(level); + this.pb = "+" + JsonSource.levelToPb(level); + } - public String getName() { - return cfSources.getName(); + void addFeature(ClassFeature cf) { + features.add(toHtmlLink(cf.getName(), cf.level())); } - void appendLink(JsonSource converter, List text, String pageSource) { - converter.maybeAddBlankLine(text); - String x = converter.decoratedFeatureTypeName(cfSources, cfNode); - text.add(String.format("[%s](#%s)", x, converter.toAnchorTag(x + " (Level " + level + ")"))); + void addValue(String value) { + values.add(value); } - void appendText(JsonSource converter, List text, String pageSource) { - boolean pushed = converter.parseState().pushFeatureType(); - try { - converter.maybeAddBlankLine(text); - text.add("### " + converter.decoratedFeatureTypeName(cfSources, cfNode) + " (Level " + level + ")"); - if (!cfSources.primarySource().equalsIgnoreCase(pageSource)) { - text.add(converter.getLabeledSource(cfSources)); - } - text.add(""); - converter.appendToText(text, cfNode.get("entries"), "####"); - } finally { - converter.parseState().pop(pushed); - } + void addSpellSlot(String value) { + spellSlots.add(value); } - public void appendListItemText(JsonSource converter, List text, String pageSource) { - boolean pushed = converter.parseState().pushFeatureType(); - try { - text.add("**" + converter.decoratedFeatureTypeName(cfSources, cfNode) + "**"); - if (!cfSources.primarySource().equalsIgnoreCase(pageSource)) { - text.add(converter.getLabeledSource(cfSources)); - } - text.add(""); - converter.appendToText(text, cfNode.get("entries"), null); - text.add(""); - } finally { - converter.parseState().pop(pushed); - } + String toHtmlLink(String x, String level) { + return String.format("%s", + toAnchorTag(x + " (Level " + level + ")"), + x); } } - static class Subclass { - public String parentKey; - JsonNode subclassNode; - String shortName; + // Not static. Relies on Json2QuteClass members + class SidekickProficiencies { + static final Pattern sidekickArmor = Pattern.compile("(?<=with ).*? armor"); + static final Pattern sidekickHumanoid = Pattern.compile("[Ii]f it is a humanoid[^.]+\\.($| )"); + static final Pattern sidekickSavingThrows = Pattern.compile("\\b[^ ]+ saving throw of your choice.*"); + static final Pattern sidekickSkills = Pattern + .compile("\\b[^ ]+ skills of your choice(,| from the following list.*)"); + static final Pattern sidekickTools = Pattern.compile("\\b[^ ]+ tools of your choice"); + static final Pattern sidekickWeapons = Pattern.compile("(?<=(with|and) )all simple( and martial)? weapons"); - Tools5eSources sources; - String parentClassSource; + private String armor; + private String skills; + private String savingThrows; + private String tools; + private String weapons; - final List classFeatures = new ArrayList<>(); + SidekickProficiencies(JsonNode node) { + String text = String.join("\n", SourceField.entries.replaceTextFromList(node, index())); + String humanoidClause = null; - public String getName() { - return sources.getName(); - } - } + Matcher humanoidMatcher = sidekickHumanoid.matcher(text); + if (humanoidMatcher.find()) { + humanoidClause = humanoidMatcher.group(0); + // Remove the humanoid clause from the text + text = humanoidMatcher.replaceAll(""); + } - String markdownLinkify(String x, int level) { - return String.format("[%s](#%s)", x, toAnchorTag(x + " (Level " + level + ")")); - } + Matcher armorMatcher = sidekickArmor.matcher(text); + if (armorMatcher.find()) { + armor = uppercaseFirst(armorMatcher.group()); + if (humanoidClause.contains("shields")) { + armor += "; and shields if [humanoid](%s)".formatted(toAnchorTag("Bonus Progression (Level 1)")); + } + } - String columnValue(JsonNode c) { - if (c.isTextual() || c.isIntegralNumber()) { - String value = c.asText(); - if (value.isEmpty() || value.equals("0")) { - return "⏤"; - } else { - return replaceText(value); + Matcher savingThrowsMatcher = sidekickSavingThrows.matcher(text); + if (savingThrowsMatcher.find()) { + savingThrows = uppercaseFirst(savingThrowsMatcher.group()); } - } else if (c.isObject()) { - String type = getTextOrEmpty(c, "type"); - switch (type) { - case "dice" -> { - JsonNode toRoll = c.get("toRoll"); - List rolls = new ArrayList<>(); - toRoll.forEach(f -> rolls.add(String.format("%sd%s", f.get("number").asText(), f.get("faces").asText()))); - return String.join(", ", rolls); - } - case "bonus", "bonusSpeed" -> { - return "+" + c.get("value").asText(); - } + + Matcher skillsMatcher = sidekickSkills.matcher(text); + if (skillsMatcher.find()) { + skills = uppercaseFirst(skillsMatcher.group()).replaceAll(",$", ""); } - throw new IllegalArgumentException("Unknown column object value: " + c.toPrettyString()); - } else { - throw new IllegalArgumentException("Unknown column value: " + c.toPrettyString()); + + // Only present in the humanoid clause + Matcher toolMatcher = sidekickTools.matcher(humanoidClause); + if (toolMatcher.find()) { + tools = "%s if [humanoid](%s)".formatted( + uppercaseFirst(toolMatcher.group()), + toAnchorTag("Bonus Progression (Level 1)")); + } + + // Only present in the humanoid clause + Matcher weaponsMatcher = sidekickWeapons.matcher(humanoidClause); + if (weaponsMatcher.find()) { + weapons = "%s if [humanoid](%s)".formatted( + uppercaseFirst(weaponsMatcher.group()), + toAnchorTag("Bonus Progression (Level 1)")); + } + } + + List armor() { + return isPresent(armor) ? List.of(armor) : List.of(); + } + + List savingThrows() { + return isPresent(savingThrows) ? List.of(savingThrows) : List.of(); + } + + List skills() { + return isPresent(skills) ? List.of(skills) : List.of(); + } + + List tools() { + return isPresent(tools) ? List.of(tools) : List.of(); + } + + List weapons() { + return isPresent(weapons) ? List.of(weapons) : List.of(); } } enum ClassFields implements JsonNodeReader { + additionalFromBackground, + additionalProperties, + any, + armor, + choose, + classFeature, + classFeatures, + classTableGroups, + colLabels, + count, + defaultEquipment("default"), // default is a reserved word + faces, + featureKeys, + from, + full, + gainSubclassFeature, + goldAlternative, + hd, + isSidekick, + multiclassing, + number, + optional, optionalfeatureProgression, - subclassSource, + or, + primaryAbility, + proficienciesGained, + proficiency, + properties, + required, + requirements, + requirementsSpecial, + rows, + rowsSpellProgression, + shortName, + skills, + startingEquipment, + startingProficiencies, + subclassFeature, + subclassFeatures, + subclassKeys, subclassShortName, + subclassSource, + subclassTableGroups, + subclassTitle, + toRoll, + tools, + type, + value, + weapons, + ; + + final String nodeName; + + ClassFields() { + nodeName = name(); + } + + ClassFields(String nodeName) { + this.nodeName = nodeName; + } + + @Override + public String nodeName() { + return nodeName; + } } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java index 3e0bcf31..c6443b6d 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java @@ -95,27 +95,35 @@ public String getText(String heading) { } public String getFluffDescription(Tools5eIndexType fluffType, String heading, List images) { - List text = getFluff(fluffType, heading, images); + return getFluffDescription(rootNode, fluffType, heading, images); + } + + public String getFluffDescription(JsonNode fromNode, Tools5eIndexType fluffType, String heading, List images) { + List text = getFluff(fromNode, fluffType, heading, images); return text.isEmpty() ? null : String.join("\n", text); } public List getFluff(Tools5eIndexType fluffType, String heading, List images) { + return getFluff(rootNode, fluffType, heading, images); + } + + public List getFluff(JsonNode fromNode, Tools5eIndexType fluffType, String heading, List images) { List text = new ArrayList<>(); JsonNode fluffNode = null; - if (TtrpgValue.indexFluffKey.existsIn(rootNode)) { + if (TtrpgValue.indexFluffKey.existsIn(fromNode)) { // Specific variant - String fluffKey = TtrpgValue.indexFluffKey.getTextOrEmpty(rootNode); + String fluffKey = TtrpgValue.indexFluffKey.getTextOrEmpty(fromNode); fluffNode = index.getOrigin(fluffKey); - } else if (Tools5eFields.fluff.existsIn(rootNode)) { - fluffNode = Tools5eFields.fluff.getFrom(rootNode); + } else if (Tools5eFields.fluff.existsIn(fromNode)) { + fluffNode = Tools5eFields.fluff.getFrom(fromNode); JsonNode monsterFluff = Tools5eFields._monsterFluff.getFrom(fluffNode); if (monsterFluff != null) { String fluffKey = fluffType.createKey(monsterFluff); fluffNode = index.getOrigin(fluffKey); } - } else if (Tools5eFields.hasFluff.booleanOrDefault(rootNode, false) - || Tools5eFields.hasFluffImages.booleanOrDefault(rootNode, false)) { - String fluffKey = fluffType.createKey(rootNode); + } else if (Tools5eFields.hasFluff.booleanOrDefault(fromNode, false) + || Tools5eFields.hasFluffImages.booleanOrDefault(fromNode, false)) { + String fluffKey = fluffType.createKey(fromNode); fluffNode = index.getOrigin(fluffKey); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCompose.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCompose.java index 1ed19301..bbb45a10 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCompose.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCompose.java @@ -228,15 +228,19 @@ private void appendItemProperties(List text, Tags tags) { final JsonNode srdEntries = TtrpgConfig.activeGlobalConfig("srdEntries").get("properties"); for (JsonNode srdEntry : iterableElements(srdEntries)) { - // FIXME: "edition" test for srd entries - currentSources = Tools5eSources.findOrTemporary(srdEntry); + String finalKey = TtrpgValue.indexKey.getTextOrEmpty(srdEntry); + if (index().isExcluded(finalKey)) { + continue; + } + currentSources = Tools5eSources.findSources(finalKey); + boolean p2 = parseState().push(srdEntry); try { - String name = srdEntry.get("name").asText(); + String name = currentSources.getName(); maybeAddBlankLine(text); text.add("## " + name); - if (!srdEntry.has("srd")) { + if (!currentSources.isSrdOrFreeRules()) { text.add(getLabeledSource(srdEntry)); } text.add(""); @@ -254,10 +258,15 @@ private void appendItemProperties(List text, Tags tags) { } maybeAddBlankLine(text); text.add("### " + propName); - if (!property.has("srd")) { + if (!Tools5eSources.isSrd(property)) { text.add(getLabeledSource(property)); } - appendToText(text, SourceField.entries.getFrom(property), null); + List inner = new ArrayList<>(); + appendToText(inner, SourceField.entries.getFrom(property), null); + if (!inner.isEmpty()) { + inner.set(0, inner.get(0).replaceAll("^\\*\\*.*?\\.\\*\\* ", "")); + text.addAll(inner); + } } } else { appendToText(text, SourceField.entries.getFrom(srdEntry), "###"); diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteFeat.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteFeat.java index 3f5ba141..b7e40bf0 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteFeat.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteFeat.java @@ -1,7 +1,11 @@ package dev.ebullient.convert.tools.dnd5e; +import java.util.ArrayList; +import java.util.List; + import com.fasterxml.jackson.databind.JsonNode; +import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.qute.QuteFeat; @@ -17,13 +21,18 @@ protected Tools5eQuteBase buildQuteResource() { Tags tags = new Tags(getSources()); tags.add("feat"); + List images = new ArrayList<>(); + List text = getFluff(Tools5eIndexType.featFluff, "##", images); + appendToText(text, SourceField.entries.getFrom(rootNode), "##"); + // TODO: update w/ category, ability, additionalSpells return new QuteFeat(sources, type.decoratedName(rootNode), getSourceText(sources), listPrerequisites(rootNode), null, // Level coming someday.. - getText("##"), + images, + String.join("\n", text), tags); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteHazard.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteHazard.java index 4f8a2585..e28d72c8 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteHazard.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteHazard.java @@ -1,7 +1,11 @@ package dev.ebullient.convert.tools.dnd5e; +import java.util.ArrayList; +import java.util.List; + import com.fasterxml.jackson.databind.JsonNode; +import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.qute.QuteHazard; @@ -22,11 +26,16 @@ protected Tools5eQuteBase buildQuteResource() { tags.add("hazard", hazardType); } + List images = new ArrayList<>(); + List text = getFluff(Tools5eIndexType.trapFluff, "##", images); + appendToText(text, SourceField.entries.getFrom(rootNode), "##"); + return new QuteHazard(getSources(), getSources().getName(), getSourceText(getSources()), getHazardType(hazardType), - getText("##"), + images, + String.join("\n", text), tags); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteItem.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteItem.java index e090c379..53aac414 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteItem.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteItem.java @@ -57,9 +57,9 @@ protected Tools5eQuteBase buildQuteResource() { return new QuteItem(sources, getSourceText(sources), rootVariant, - text, - fluffImages, variants, + fluffImages, + text, tags); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteObject.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteObject.java index 374e2c4d..7de1bd78 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteObject.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteObject.java @@ -30,7 +30,7 @@ protected Tools5eQuteBase buildQuteResource() { } List fluffImages = new ArrayList<>(); - List text = getFluff(Tools5eIndexType.monsterFluff, "##", fluffImages); + List text = getFluff(Tools5eIndexType.objectFluff, "##", fluffImages); appendToText(text, SourceField.entries.getFrom(rootNode), "##"); return new QuteObject(sources, diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteOptionalFeature.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteOptionalFeature.java index 0c3b26ff..8ad07bde 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteOptionalFeature.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteOptionalFeature.java @@ -1,7 +1,11 @@ package dev.ebullient.convert.tools.dnd5e; +import java.util.ArrayList; +import java.util.List; + import com.fasterxml.jackson.databind.JsonNode; +import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.qute.QuteFeat; import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; @@ -19,13 +23,17 @@ protected Tools5eQuteBase buildQuteResource() { tags.add("optional-feature", featureType); } - // set the template to use + List images = new ArrayList<>(); + List text = getFluff(Tools5eIndexType.optionalfeatureFluff, "##", images); + appendToText(text, SourceField.entries.getFrom(rootNode), "##"); + return new QuteFeat(getSources(), getSources().getName(), getSourceText(sources), listPrerequisites(rootNode), null, - getText("##"), + images, + String.join("\n", text), tags); } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteReward.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteReward.java index 83ac498c..779e8c88 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteReward.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteReward.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.JsonNode; +import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.qute.QuteReward; @@ -23,8 +24,11 @@ protected QuteReward buildQuteResource() { tags.add("reward", type); } - List details = new ArrayList<>(); + List images = new ArrayList<>(); + List text = getFluff(Tools5eIndexType.rewardFluff, "##", images); + appendToText(text, SourceField.entries.getFrom(rootNode), "##"); + List details = new ArrayList<>(); String type = RewardField.type.getTextOrNull(rootNode); if (type != null) { details.add(type); @@ -41,7 +45,8 @@ protected QuteReward buildQuteResource() { RewardField.ability.transformTextFrom(rootNode, "\n", index), getSources().getName().startsWith(detail) ? "" : detail, RewardField.signaturespells.transformTextFrom(rootNode, "\n", index), - getText("##"), + images, + String.join("\n", text), tags); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpell.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpell.java index 97dc5f11..9ec9a3b1 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpell.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpell.java @@ -60,8 +60,8 @@ protected Tools5eQuteBase buildQuteResource() { spellComponents(), spellDuration(), String.join(", ", classes), - String.join("\n", text), getFluffImages(Tools5eIndexType.spellFluff), + String.join("\n", text), tags); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java index b588f575..9379d7e6 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java @@ -292,7 +292,7 @@ default void appendClassFeatureRef(List text, JsonNode entry, Tools5eInd return; // skipped or not found } if (parseState().featureTypeDepth() > 2) { - tui().errorf("Cycle in class or subclass features found in %s", cf.cfSources); + tui().errorf("Cycle in class or subclass features found in %s", cf.cfSources()); // this is within an existing feature description. Emit as a link cf.appendLink(this, text, parseState().getSource(featureType)); } else if (parseState().inList()) { @@ -648,7 +648,7 @@ default void embedReference(List text, JsonNode entry, Tools5eIndexType } } else { text.add(link); - tui().warnf(Msg.UNRESOLVED, "unable to find statblock target: %s", entry); + tui().warnf(Msg.UNRESOLVED, "unable to find statblock target %s from %s", entry, getSources()); } } @@ -961,7 +961,7 @@ default String mapAlignmentToString(String a) { }; } - default int levelToPb(int level) { + static int levelToPb(int level) { // 2 + (¼ * (Level – 1)) return 2 + ((int) (.25 * (level - 1))); } @@ -1284,6 +1284,7 @@ enum Tools5eFields implements JsonNodeReader { number, // speed optionalfeature, otherSources, + parentSource, prop, // statblock race, regionalEffects, // legendary group diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java index 4a370720..6e6da9ca 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java @@ -1,10 +1,13 @@ package dev.ebullient.convert.tools.dnd5e; import static dev.ebullient.convert.StringUtil.isPresent; +import static dev.ebullient.convert.StringUtil.toAnchorTag; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.function.Function; import java.util.regex.MatchResult; import java.util.regex.Matcher; @@ -41,10 +44,12 @@ public interface JsonTextReplacement extends JsonTextConverter static final Pattern quickRefPattern = Pattern.compile("\\{@quickref ([^}]+)}"); static final Pattern notePattern = Pattern.compile("\\{@(note|tip) ([^}]+)}"); static final Pattern footnotePattern = Pattern.compile("\\{@footnote ([^}]+)}"); - static final Pattern abilitySavePattern = Pattern.compile("\\{@(ability|savingThrow) ([^}]+)}"); // {@ability str 20} + static final Pattern abilitySavePattern = Pattern.compile("\\{@(ability|savingThrow) ([^}]+)}"); // {@ability str + // 20} static final Pattern savingThrowPattern = Pattern.compile("\\{@actSave ([^}]+)}"); static final Pattern attackPattern = Pattern.compile("\\{@atkr? ([^}]+)}"); - static final Pattern skillCheckPattern = Pattern.compile("\\{@skillCheck ([^}]+)}"); // {@skillCheck animal_handling 5} + static final Pattern skillCheckPattern = Pattern.compile("\\{@skillCheck ([^}]+)}"); // {@skillCheck animal_handling + // 5} static final Pattern optionalFeaturesFilter = Pattern.compile("\\{@filter ([^|}]+)\\|optionalfeatures\\|([^}]*)}"); static final Pattern featureTypePattern = Pattern.compile("(?:[Ff]eature )?[Tt]ype=([^|}]+)"); static final Pattern featureSourcePattern = Pattern.compile("source=([^|}]+)"); @@ -52,6 +57,8 @@ public interface JsonTextReplacement extends JsonTextConverter static final Pattern promptPattern = Pattern.compile("#\\$prompt_number(?::(.*?))?\\$#"); static final String subclassFeatureMask = "subclassfeature\\|(.*)\\|.*?\\|.*?\\|.*?\\|.*?\\|(\\d+)\\|.*"; + static final Set missingKeys = new HashSet<>(); + Tools5eIndex index(); Tools5eSources getSources(); @@ -178,12 +185,14 @@ default String _replaceTokenText(String input, boolean nested) { try { result = replacePromptStrings(result); - // {@dice .. }, {@damage ..}{@hit ..}, {@d20 ..}, {@initiative ...}, {@scaledice..}, {@scaledamage..} + // {@dice .. }, {@damage ..}{@hit ..}, {@d20 ..}, {@initiative ...}, + // {@scaledice..}, {@scaledamage..} result = replaceWithDiceRoller(result); result = chancePattern.matcher(result).replaceAll((match) -> { // "Chance tags; similar to dice roller tags, but output success/failure. - // {@chance 50}; {@chance 50|display text}; {@chance 50|display text|rolled by name}; + // {@chance 50}; {@chance 50|display text}; {@chance 50|display text|rolled by + // name}; // {@chance 50|display text|rolled by name|on success text}; // {@chance 50|display text|rolled by name|on success text|on failure text}.", String[] parts = match.group(1).split("\\|"); @@ -213,7 +222,8 @@ default String _replaceTokenText(String input, boolean nested) { }); result = homebrewPattern.matcher(result).replaceAll((match) -> { - // {@homebrew changes|modifications}, {@homebrew additions} or {@homebrew |removals} + // {@homebrew changes|modifications}, {@homebrew additions} or {@homebrew + // |removals} String s = match.group(1); int pos = s.indexOf('|'); if (pos == 0) { // removal @@ -281,8 +291,10 @@ default String _replaceTokenText(String input, boolean nested) { List type = new ArrayList<>(); String method = ""; // render.js Renderer.attackTagToFull - // const ptType = tags.includes("m") ? "Melee " : tags.includes("r") ? "Ranged " : tags.includes("g") ? "Magical " : tags.includes("a") ? "Area " : ""; - // const ptMethod = tags.includes("w") ? "Weapon " : tags.includes("s") ? "Spell " : tags.includes("p") ? "Power " : ""; + // const ptType = tags.includes("m") ? "Melee " : tags.includes("r") ? "Ranged " + // : tags.includes("g") ? "Magical " : tags.includes("a") ? "Area " : ""; + // const ptMethod = tags.includes("w") ? "Weapon " : tags.includes("s") ? "Spell + // " : tags.includes("p") ? "Power " : ""; if (match.group(1).contains("m")) { type.add("Melee "); } @@ -311,7 +323,8 @@ default String _replaceTokenText(String input, boolean nested) { .replaceAll("\\{@hitYourSpellAttack ([^}]+)}", "$1") .replaceAll("\\{@hitYourSpellAttack}", "the summoner's spell attack modifier") // "Internal links: {@5etools This Is Your Life|lifegen.html}", - // "External links: {@link https://discord.gg/5etools} or {@link Discord|https://discord.gg/5etools}" + // "External links: {@link https://discord.gg/5etools} or {@link + // Discord|https://discord.gg/5etools}" .replaceAll("\\{@link ([^}|]+)\\|([^}]+)}", "$1 ($2)") // this must come first .replaceAll("\\{@link ([^}|]+)}", "$1") // this must come first .replaceAll("\\{@5etools ([^}|]+)\\|?[^}]*}", "$1") @@ -375,9 +388,12 @@ default String _replaceTokenText(String input, boolean nested) { } result = footnotePattern.matcher(result).replaceAll((match) -> { - // {@footnote directly in text|This is primarily for homebrew purposes, as the official texts (so far) avoid using footnotes}, - // {@footnote optional reference information|This is the footnote. References are free text.|Footnote 1, page 20}.", - // We're converting these to _inline_ markdown footnotes, as numbering is difficult to track + // {@footnote directly in text|This is primarily for homebrew purposes, as the + // official texts (so far) avoid using footnotes}, + // {@footnote optional reference information|This is the footnote. References + // are free text.|Footnote 1, page 20}.", + // We're converting these to _inline_ markdown footnotes, as numbering is + // difficult to track String[] parts = match.group(1).split("\\|"); if (parts[0].contains("")) { // This already assumes what the footnote name will be @@ -436,9 +452,9 @@ default String replaceSavingThrow(MatchResult match) { default String replaceSkillOrAbility(MatchResult match) { // format: {@ability str 20} or {@ability str 20|Display Text} - // or {@ability str 20|Display Text|Roll Name Text} + // or {@ability str 20|Display Text|Roll Name Text} // format: {@savingThrow str 5} or {@savingThrow str 5|Display Text} - // or {@savingThrow str 5|Display Text|Roll Name Text} + // or {@savingThrow str 5|Display Text|Roll Name Text} String[] parts = match.group(2).split("\\|"); String[] score = parts[0].split(" "); @@ -469,8 +485,9 @@ default String replaceSkillOrAbility(MatchResult match) { } default String replaceSkillCheck(MatchResult match) { - // format: {@skillCheck animal_handling 5} or {@skillCheck animal_handling 5|Display Text} - // or {@skillCheck animal_handling 5|Display Text|Roll Name Text} + // format: {@skillCheck animal_handling 5} or {@skillCheck animal_handling + // 5|Display Text} + // or {@skillCheck animal_handling 5|Display Text|Roll Name Text} String[] parts = match.group(1).split("\\|"); String[] score = parts[0].split(" "); SkillOrAbility skill = index().findSkillOrAbility(score[0], getSources()); @@ -493,16 +510,27 @@ default String replaceSkillCheck(MatchResult match) { return "%s (%s)".formatted(text, dice); } + default String linkifySkill(SkillOrAbility skill) { + String source = skill.source(); + return linkify(Tools5eIndexType.skill, + "%s|%s|%s".formatted(skill.value(), source, skill.value())); + } + default String linkifyRules(Tools5eIndexType type, String text, String rules) { // {@condition stunned} assumes PHB by default, - // {@condition stunned|PHB} can have sources added with a pipe (not that it's ever useful), + // {@condition stunned|PHB} can have sources added with a pipe (not that it's + // ever useful), // {@condition stunned|PHB|and optional link text added with another pipe}.", String[] parts = text.split("\\|"); String name = parts[0]; - String source = parts.length > 1 ? parts[1] : "PHB"; + String source = parts.length > 1 ? parts[1] : type.defaultSourceString(); String linkText = parts.length > 2 ? parts[2] : name; + if (name.isBlank()) { + return "[%s](%s%s.md)".formatted(linkText, index().rulesVaultRoot(), rules); + } + String aliasKey = index().getAliasOrDefault(type.createKey(name, source)); if (index().isExcluded(aliasKey)) { return linkText; @@ -536,7 +564,8 @@ default String linkify(Tools5eIndexType type, String s) { return switch (type) { // {@background Charlatan} assumes PHB by default, // {@background Anthropologist|toa} can have sources added with a pipe, - // {@background Anthropologist|ToA|and optional link text added with another pipe}.", + // {@background Anthropologist|ToA|and optional link text added with another + // pipe}.", // {@feat Alert} assumes PHB by default, // {@feat Elven Accuracy|xge} can have sources added with a pipe, // {@feat Elven Accuracy|xge|and optional link text added with another pipe}.", @@ -554,10 +583,13 @@ default String linkify(Tools5eIndexType type, String s) { // {@object Ballista|DMG|and optional link text added with another pipe}.", // {@optfeature Agonizing Blast} assumes PHB by default, // {@optfeature Aspect of the Moon|xge} can have sources added with a pipe, - // {@optfeature Aspect of the Moon|xge|and optional link text added with another pipe}.", + // {@optfeature Aspect of the Moon|xge|and optional link text added with another + // pipe}.", // {@psionic Mastery of Force} assumes UATheMysticClass by default - // {@psionic Mastery of Force|UATheMysticClass} can have sources added with a pipe - // {@psionic Mastery of Force|UATheMysticClass|and optional link text added with another pipe}.", + // {@psionic Mastery of Force|UATheMysticClass} can have sources added with a + // pipe + // {@psionic Mastery of Force|UATheMysticClass|and optional link text added with + // another pipe}.", // {@race Human} assumes PHB by default, // {@race Aasimar (Fallen)|VGM} // {@race Aasimar|DMG|racial traits for the aasimar} @@ -566,16 +598,19 @@ default String linkify(Tools5eIndexType type, String s) { // {@race dwarf (hill)||Dwarf, hill} // {@reward Blessing of Health} assumes DMG by default, // {@reward Blessing of Health} can have sources added with a pipe, - // {@reward Blessing of Health|DMG|and optional link text added with another pipe}.", + // {@reward Blessing of Health|DMG|and optional link text added with another + // pipe}.", // {@spell acid splash} assumes PHB by default, // {@spell tiny servant|xge} can have sources added with a pipe, // {@spell tiny servant|xge|and optional link text added with another pipe}.", // {@table 25 gp Art Objects} assumes DMG by default, // {@table Adventuring Gear|phb} can have sources added with a pipe, - // {@table Adventuring Gear|phb|and optional link text added with another pipe}.", + // {@table Adventuring Gear|phb|and optional link text added with another + // pipe}.", // {@trap falling net} assumes DMG by default, // {@trap falling portcullis|xge} can have sources added with a pipe, - // {@trap falling portcullis|xge|and optional link text added with another pipe}.", + // {@trap falling portcullis|xge|and optional link text added with another + // pipe}.", // {@vehicle Galley} assumes GoS by default, // {@vehicle Galley|UAOfShipsAndSea} can have sources added with a pipe, // {@vehicle Galley|GoS|and optional link text added with another pipe}.", @@ -588,6 +623,7 @@ default String linkify(Tools5eIndexType type, String s) { legendaryGroup, object, optfeature, + psionic, race, reward, spell, @@ -638,12 +674,17 @@ default String linkifyType(Tools5eIndexType type, String aliasKey, String linkTe String dirName = type.getRelativePath(); JsonNode jsonSource = index().getNode(aliasKey); // filtered if (jsonSource == null) { - if (index().getOrigin(aliasKey) == null) { - // sources can be excluded, that's fine.. but if this is something that doesn't exist at all.. - tui().debugf(Msg.UNRESOLVED, "unresolvable {@%s %s} as [%s] from %s", - type, match, aliasKey, parseState().getSource()); + jsonSource = index().getHomebrewNode(type, aliasKey, parseState().getSource()); + if (jsonSource == null) { + if (index().getOrigin(aliasKey) == null) { + // sources can be excluded, that's fine.. but if this is something that doesn't + // exist at all.. + tui().debugf(Msg.UNRESOLVED, "unresolvable {@%s %s} as [%s] from %s", + type, match, aliasKey, parseState().getSource()); + // log a stack trace of how we got here + } + return linkText; } - return linkText; } Tools5eSources linkSource = Tools5eSources.findSources(jsonSource); return linkOrText(linkText, aliasKey, dirName, @@ -701,7 +742,8 @@ default String linkifyClass(String match) { // {@class artificer|uaartificer} can have sources added with a pipe, // {@class fighter|phb|optional link text added with another pipe}, // {@class fighter|phb|subclasses added|Eldritch Knight} with another pipe, - // {@class fighter|phb|and class feature added|Eldritch Knight|phb|2-0} with another pipe + // {@class fighter|phb|and class feature added|Eldritch Knight|phb|2-0} with + // another pipe // {@class Barbarian|phb|Path of the Ancestral Guardian|Ancestral Guardian|xge} // {@class Fighter|phb|Samurai|Samurai|xge} String[] parts = match.split("\\|"); @@ -714,7 +756,8 @@ default String linkifyClass(String match) { String relativePath = Tools5eIndexType.classtype.getRelativePath(); if (subclass != null) { String key = index() - .getAliasOrDefault(Tools5eIndexType.getSubclassKey(className, classSource, subclass, subclassSource)); + .getAliasOrDefault( + Tools5eIndexType.getSubclassKey(className, classSource, subclass, subclassSource)); // "subclass|path of wild magic|barbarian|phb|" int first = key.indexOf('|'); int second = key.indexOf('|', first + 1); @@ -729,7 +772,8 @@ default String linkifyClass(String match) { } default String linkifyClassFeature(String match) { - // "Class Features: Class source is assumed to be PHB, class feature source is assumed to be the same as class source" + // "Class Features: Class source is assumed to be PHB, class feature source is + // assumed to be the same as class source" // {@classFeature Rage|Barbarian||1}, // {@classFeature Infuse Item|Artificer|TCE|2}, // {@classFeature Survival Instincts|Barbarian||2|UAClassFeatureVariants}, @@ -789,13 +833,16 @@ default String linkifyOptionalFeatureType(MatchResult match) { } default String linkifySubclassFeature(String match) { - //"Subclass Features: + // "Subclass Features: // {@subclassFeature Path of the Berserker|Barbarian||Berserker||3}, // {@subclassFeature Alchemist|Artificer|TCE|Alchemist|TCE|3}, - // {@subclassFeature Path of the Battlerager|Barbarian||Battlerager|SCAG|3}, --> "barbarian-path-of-the-... " - // {@subclassFeature Blessed Strikes|Cleric||Life||8|UAClassFeatureVariants}, --> "-domain" + // {@subclassFeature Path of the Battlerager|Barbarian||Battlerager|SCAG|3}, --> + // "barbarian-path-of-the-... " + // {@subclassFeature Blessed Strikes|Cleric||Life||8|UAClassFeatureVariants}, + // --> "-domain" // {@subclassFeature Blessed Strikes|Cleric|PHB|Twilight|TCE|8|TCE} - // {@subclassFeature Path of the Berserker|Barbarian||Berserker||3||optional display text}. + // {@subclassFeature Path of the Berserker|Barbarian||Berserker||3||optional + // display text}. // Class source is assumed to be PHB. // Subclass source is assumed to be PHB. // Subclass feature source is assumed to be the same as subclass source.", @@ -824,8 +871,10 @@ default String linkifySubclassFeature(String match) { String subclassKey = Tools5eIndexType.subclass.fromChildKey(featureKey); // look up alias for subclass so link is correct, but don't follow reprints - // "subclass|redemption|paladin|phb|" : "subclass|oath of redemption|paladin|phb|", - // "subclass|twilight|cleric|phb|tce" : "subclass|twilight domain|cleric|phb|tce" + // "subclass|redemption|paladin|phb|" : "subclass|oath of + // redemption|paladin|phb|", + // "subclass|twilight|cleric|phb|tce" : "subclass|twilight + // domain|cleric|phb|tce" subclassKey = index().getAliasOrDefault(subclassKey, false); JsonNode subclassNode = index().getNode(subclassKey); @@ -839,8 +888,10 @@ default String linkifySubclassFeature(String match) { return linkText; } // Examine new subclass node's features, to see if there is a match - // e.g. for "subclassfeature|primal companion|ranger|phb|beast master|phb|3|tce", - // consider "subclassfeature|primal companion|ranger|xphb|beast master|xphb|3|xphb" + // e.g. for "subclassfeature|primal companion|ranger|phb|beast + // master|phb|3|tce", + // consider "subclassfeature|primal companion|ranger|xphb|beast + // master|xphb|3|xphb" String test = featureKey.replaceAll(subclassFeatureMask, "$1-$2"); boolean found = false; for (String fkey : Tools5eFields.classFeatureKeys.getListOfStrings(subclassNode, tui())) { @@ -852,7 +903,8 @@ default String linkifySubclassFeature(String match) { } } if (!found) { - tui().warnf(Msg.UNRESOLVED, "No equivalent subclass feature found for {@subclassfeature %s} in %s (from %s)", + tui().warnf(Msg.UNRESOLVED, + "No equivalent subclass feature found for {@subclassfeature %s} in %s (from %s)", match, subclassKey, getSources().getKey()); return linkText; } @@ -922,28 +974,40 @@ default String linkifyItemAttribute(Tools5eIndexType type, String s) { String source = parts.length > 1 ? parts[1] : type.defaultSourceString(); String linkText = parts.length > 2 ? parts[2] : parts[0]; String lookup = "%s|%s".formatted(parts[0], source); + if (missingKeys.contains(lookup)) { + return linkText; + } return switch (type) { case itemType -> { - ItemType itemType = index().findItemType(lookup, getSources()); + String key = Tools5eIndexType.itemType.fromTagReference(lookup); + ItemType itemType = index().findItemType(key, getSources()); if (itemType == null) { - tui().warnf(Msg.UNRESOLVED, "Item type %s not found from %s", s, getSources().getKey()); - yield s; + if (missingKeys.add(lookup) && index().isIncluded(key)) { + tui().warnf(Msg.UNRESOLVED, "Item type %s not found from %s", s, getSources().getKey()); + } + yield linkText; } yield itemType.linkify(linkText); } case itemProperty -> { - ItemProperty itemProperty = index().findItemProperty(lookup, getSources()); + String key = Tools5eIndexType.itemProperty.fromTagReference(lookup); + ItemProperty itemProperty = index().findItemProperty(key, getSources()); if (itemProperty == null) { - tui().warnf(Msg.UNRESOLVED, "Item property %s not found from %s", s, getSources().getKey()); - yield s; + if (missingKeys.add(lookup) && index().isIncluded(key)) { + tui().warnf(Msg.UNRESOLVED, "Item property %s not found from %s", s, getSources().getKey()); + } + yield linkText; } yield itemProperty.linkify(linkText); } case itemMastery -> { - ItemMastery itemMastery = index().findItemMastery(lookup, getSources()); + String key = Tools5eIndexType.itemMastery.fromTagReference(lookup); + ItemMastery itemMastery = index().findItemMastery(key, getSources()); if (itemMastery == null) { - tui().warnf(Msg.UNRESOLVED, "Item type %s not found from %s", s, getSources().getKey()); - yield s; + if (missingKeys.add(lookup) && index().isIncluded(key)) { + tui().warnf(Msg.UNRESOLVED, "Item mastery %s not found from %s", s, getSources().getKey()); + } + yield linkText; } yield itemMastery.linkify(linkText); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java index 193f8384..15218548 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java @@ -127,10 +127,13 @@ public Map getMap() { * This is included in all-index.json */ static class OptionalFeatureType { + + @JsonIgnore + final HomebrewMetaTypes homebrewMeta; + final String lookupKey; final String featureTypeKey; final String abbreviation; - final HomebrewMetaTypes homebrewMeta; final String title; final String source; final List features = new ArrayList<>(); diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/SkillOrAbility.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/SkillOrAbility.java index e9c03b8d..06d8c624 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/SkillOrAbility.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/SkillOrAbility.java @@ -1,5 +1,7 @@ package dev.ebullient.convert.tools.dnd5e; +import static dev.ebullient.convert.StringUtil.toTitleCase; + import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @@ -16,6 +18,8 @@ public interface SkillOrAbility { String value(); + String source(); + int ordinal(); public static SkillOrAbility fromTextValue(String v) { @@ -53,17 +57,20 @@ public class CustomSkillOrAbility implements SkillOrAbility { final String name; final String lower; final String key; + final String source; public CustomSkillOrAbility(String name) { this.name = name; this.lower = name.toLowerCase(); this.key = null; + this.source = ""; } public CustomSkillOrAbility(JsonNode skill) { - this.name = SourceField.name.getTextOrEmpty(skill); + this.name = toTitleCase(SourceField.name.getTextOrEmpty(skill)); this.lower = this.name.toLowerCase(); this.key = Tools5eIndexType.skill.createKey(skill); + this.source = SourceField.source.getTextOrEmpty(skill); } @Override @@ -71,6 +78,11 @@ public String value() { return name; } + @Override + public String source() { + return source; + } + public int ordinal() { return 99; } @@ -120,5 +132,9 @@ enum SkillOrAbilityEnum implements SkillOrAbility { public String value() { return longValue; } + + public String source() { + return Tools5eIndexType.skill.defaultSourceString(); + } } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eHomebrewIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eHomebrewIndex.java index 56dd5011..6fac93fb 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eHomebrewIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eHomebrewIndex.java @@ -62,8 +62,7 @@ public ItemType findHomebrewType(String fragment, Tools5eSources sources) { if (meta != null) { JsonNode homebrewNode = meta.getItemProperty(fragment); if (homebrewNode != null) { - String key = Tools5eIndexType.itemType.createKey(homebrewNode); - return ItemType.fromNode(key, homebrewNode); + return ItemType.fromNode(homebrewNode); } } return null; @@ -74,8 +73,7 @@ public ItemMastery findHomebrewMastery(String fragment, Tools5eSources sources) if (meta != null) { JsonNode homebrewNode = meta.getItemMastery(fragment); if (homebrewNode != null) { - String key = Tools5eIndexType.itemMastery.createKey(homebrewNode); - return ItemMastery.fromNode(key, homebrewNode); + return ItemMastery.fromNode(homebrewNode); } } return null; @@ -86,8 +84,7 @@ public ItemProperty findHomebrewProperty(String fragment, Tools5eSources sources if (meta != null) { JsonNode homebrewNode = meta.getItemProperty(fragment); if (homebrewNode != null) { - String key = Tools5eIndexType.itemProperty.createKey(homebrewNode); - return ItemProperty.fromNode(key, homebrewNode); + return ItemProperty.fromNode(homebrewNode); } } return null; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java index 0a3335c9..1afc6a3a 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java @@ -14,10 +14,10 @@ import java.util.TreeMap; import java.util.TreeSet; import java.util.function.BiConsumer; -import java.util.function.Function; import java.util.stream.Collectors; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.BooleanNode; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -30,6 +30,7 @@ import dev.ebullient.convert.qute.SourceAndPage; import dev.ebullient.convert.tools.MarkdownConverter; import dev.ebullient.convert.tools.ToolsIndex; +import dev.ebullient.convert.tools.dnd5e.Json2QuteClass.ClassFields; import dev.ebullient.convert.tools.dnd5e.Json2QuteItem.ItemField; import dev.ebullient.convert.tools.dnd5e.Json2QuteRace.RaceFields; import dev.ebullient.convert.tools.dnd5e.OptionalFeatureIndex.OptionalFeatureType; @@ -125,7 +126,9 @@ private void indexTypes(String filename, JsonNode node) { Tools5eIndexType.objectFluff.withArrayFrom(node, this::addToIndex); Tools5eIndexType.optionalfeatureFluff.withArrayFrom(node, this::addToIndex); Tools5eIndexType.raceFluff.withArrayFrom(node, this::addToIndex); + Tools5eIndexType.rewardFluff.withArrayFrom(node, this::addToIndex); Tools5eIndexType.spellFluff.withArrayFrom(node, this::addToIndex); + Tools5eIndexType.subclassFluff.withArrayFrom(node, this::addToIndex); Tools5eIndexType.trapFluff.withArrayFrom(node, this::addToIndex); Tools5eIndexType.vehicleFluff.withArrayFrom(node, this::addToIndex); @@ -288,9 +291,10 @@ public void prepare() { // Add missing/frequently-used aliases TtrpgConfig.addDefaultAliases(aliases); + TtrpgConfig.addReferenceEntries((n) -> addToIndex(Tools5eIndexType.reference, n)); - tui().progressf("Importing homebrew sources"); // Properly import homebrew sources + tui().progressf("Importing homebrew sources"); homebrewIndex.importBrew(this::importHomebrewTree); tui().debugf("Preparing index using configuration:\n%s", Tui.jsonStringify(config)); @@ -307,6 +311,19 @@ public void prepare() { .filter(n -> !ItemField.packContents.existsIn(n)) .toList(); + // Bring in parent adventures (before sources are created) + nodeIndex.values().stream() + .filter(n -> Tools5eFields.parentSource.existsIn(n)) + .forEach(n -> { + String source = SourceField.source.getTextOrEmpty(n); + String parentSource = Tools5eFields.parentSource.getTextOrNull(n); + if (TtrpgConfig.getConfig().sourceIncluded(source)) { + // include the parent source if you include an adventure (related rules) + tui().debugf(Msg.SOURCE, "including %s due to %s", parentSource, source); + TtrpgConfig.includeAdditionalSource(parentSource); + } + }); + List variants = new ArrayList<>(); // For each node: handle copies, link sources @@ -392,7 +409,7 @@ public void prepare() { filteredIndex.put(key, e.getValue()); } else if (sources.includedByConfig()) { filteredIndex.put(key, e.getValue()); - logThis.accept(msgType, "(KEEP) " + key); + logThis.accept(msgType, "------ " + key); } else if (type.isOutputType()) { logThis.accept(msgType, "(drop) " + key); } @@ -407,6 +424,33 @@ public void prepare() { tui().progressf("Removing dependent and dangling resources"); filteredIndex.keySet().removeIf(k -> otherwiseExcluded(k)); + // Populate classes and subclasses with related (included) + // features + for (var entry : filteredIndex.entrySet()) { + String entryKey = entry.getKey(); + var type = Tools5eIndexType.getTypeFromKey(entryKey); + if (type == Tools5eIndexType.subclass + || type == Tools5eIndexType.classfeature + || type == Tools5eIndexType.subclassFeature) { + var parentType = switch (type) { + case subclass -> Tools5eIndexType.classtype; + case classfeature -> Tools5eIndexType.classtype; + case subclassFeature -> Tools5eIndexType.subclass; + default -> null; + }; + ClassFields targetField = switch (type) { + case subclass -> ClassFields.subclassKeys; + case classfeature -> ClassFields.featureKeys; + case subclassFeature -> ClassFields.featureKeys; + default -> null; + }; + String parentKey = getAliasOrDefault(parentType.fromChildKey(entryKey)); + JsonNode parent = getOriginNoFallback(parentKey); + ArrayNode target = targetField.ensureArrayIn(parent); + target.add(entryKey); + } + } + // Deities have their own glorious reprint mess, which we only need to deal with // when we aren't hoarding all the things. if (config.reprintBehavior() != ReprintBehavior.all) { @@ -593,34 +637,39 @@ private boolean otherwiseExcluded(String key) { Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key); return switch (type) { - case card -> removeIfParentExcluded(key, Tools5eIndexType.deck, Msg.DECK); - case classfeature, subclassFeature -> removeIfParentExcluded(key, Tools5eIndexType.classtype, - Msg.CLASSES); + case card -> removeIfParentExcluded(key, type, Tools5eIndexType.deck, Msg.DECK); + case classfeature -> removeIfParentExcluded(key, type, + Tools5eIndexType.classtype, Msg.CLASSES); + case subclassFeature -> removeIfParentExcluded(key, type, + Tools5eIndexType.subclass, Msg.CLASSES) + || removeIfParentExcluded(key, type, + Tools5eIndexType.classtype, Msg.CLASSES); case optfeature, optionalFeatureTypes -> removeUnusedOptionalFeatures(type, key); - case subclass -> !sources.includedByConfig() || removeIfParentExcluded(key, Tools5eIndexType.classtype, - Msg.CLASSES); - case subrace -> !sources.includedByConfig() || removeIfParentExcluded(key, Tools5eIndexType.race, Msg.RACES); + case subclass -> !sources.includedByConfig() + || removeIfParentExcluded(key, type, Tools5eIndexType.classtype, Msg.CLASSES); + case subrace -> !sources.includedByConfig() + || removeIfParentExcluded(key, type, Tools5eIndexType.race, Msg.RACES); default -> false; // does not have a parent }; } - private boolean removeIfParentExcluded(String key, Tools5eIndexType parentType, Msg msg) { + private boolean removeIfParentExcluded(String key, Tools5eIndexType type, Tools5eIndexType parentType, Msg msg) { String parentKey = parentType.fromChildKey(key); Tools5eSources parentSources = Tools5eSources.findSources(parentKey); if (parentSources == null) { - tui().warnf(Msg.UNRESOLVED, "%35s :: unresolved parent of [%s]", parentKey, key); // allow for corrections (aliases), not reprints parentKey = getAliasOrDefault(parentKey, false); parentSources = Tools5eSources.findSources(parentKey); if (parentSources == null) { + tui().warnf(Msg.UNRESOLVED, "%35s :: unresolved parent of [%s]", parentKey, key); return true; // has a parent, it is missing (dangling resource) } } - boolean included = parentSources.includedByConfig(); - if (!included) { + boolean filterIncluded = filteredIndex.containsKey(parentKey); + if (!filterIncluded) { tui().debugf(msg, "(drop) %43s :: %s", parentKey, key); } - return !included; + return !filterIncluded; } private boolean removeUnusedOptionalFeatures(Tools5eIndexType type, String key) { @@ -651,12 +700,6 @@ public boolean notPrepared() { return filteredIndex == null; } - public List classElementsMatching(Tools5eIndexType type, String className, String classSource) { - String pattern = String.format("%s\\|[^|]+\\|%s\\|%s\\|.*", type, className, classSource) - .toLowerCase(); - return nodesMatching(pattern); - } - public List elementsMatching(Tools5eIndexType type, String middle) { String pattern = String.format("%s\\|%s\\|.*", type, middle) .toLowerCase(); @@ -747,59 +790,64 @@ public JsonNode getNode(String finalKey) { return filteredIndex.get(finalKey); } - public ItemProperty findItemProperty(String fragment, Tools5eSources sources) { - if (fragment == null || fragment.isEmpty()) { + public JsonNode getHomebrewNode(Tools5eIndexType type, String finalKey, String currentSource) { + HomebrewMetaTypes meta = homebrewIndex.getHomebrewMetaTypes(currentSource); + if (meta == null) { return null; } - if (fragment.contains("|")) { - String finalKey = Tools5eIndexType.itemProperty.fromTagReference(fragment); - return ItemProperty.fromKey(finalKey, this); - } + String adaptKey = finalKey.replace(type.defaultSourceString().toLowerCase(), currentSource.toLowerCase()); + return filteredIndex.get(adaptKey); + } - // We could have a default property (phb), or we could have a homebrew property - ItemProperty property = homebrewIndex.findHomebrewProperty(fragment, sources); - if (property == null) { - // Then we'll try the default source - String key = Tools5eIndexType.itemProperty.fromTagReference(fragment); - return ItemProperty.fromKey(key, this); + public ItemProperty findItemProperty(String key, Tools5eSources activeSources) { + if (key == null || key.isEmpty()) { + return null; + } + JsonNode propertyNode = findTypePropertyNode(key); // check alias & phb/xphb + if (propertyNode != null) { + return ItemProperty.fromNode(propertyNode); } - return property; + // try homebrew property + return homebrewIndex.findHomebrewProperty(key, activeSources); } - public ItemType findItemType(String fragment, Tools5eSources sources) { - if (fragment == null || fragment.isEmpty()) { + public ItemType findItemType(String key, Tools5eSources activeSources) { + if (key == null || key.isEmpty()) { return null; } - if (fragment.contains("|")) { - String finalKey = Tools5eIndexType.itemType.fromTagReference(fragment); - return ItemType.fromKey(finalKey, this); - } - // We could have a default property (phb), or we could have a homebrew property - ItemType type = homebrewIndex.findHomebrewType(fragment, sources); - if (type == null) { - // Then we'll try the default source - String key = Tools5eIndexType.itemType.fromTagReference(fragment); - return ItemType.fromKey(key, this); + JsonNode typeNode = findTypePropertyNode(key); // check alias & phb/xphb + if (typeNode != null) { + return ItemType.fromNode(typeNode); } - return type; + // try homebrew property + return homebrewIndex.findHomebrewType(key, activeSources); } - public ItemMastery findItemMastery(String fragment, Tools5eSources sources) { - if (fragment == null || fragment.isEmpty()) { + public ItemMastery findItemMastery(String key, Tools5eSources activeSources) { + if (key == null || key.isEmpty()) { return null; } - if (fragment.contains("|")) { - String finalKey = Tools5eIndexType.itemMastery.fromTagReference(fragment); - return ItemMastery.fromKey(finalKey, this); + JsonNode masteryNode = getNode(getAliasOrDefault(key)); + if (masteryNode != null) { + return ItemMastery.fromNode(masteryNode); } - // We could have a default property, or we could have a homebrew property - ItemMastery mastery = homebrewIndex.findHomebrewMastery(fragment, sources); - if (mastery == null) { - // Then we'll try the default source - String key = Tools5eIndexType.itemMastery.fromTagReference(fragment); - return ItemMastery.fromKey(key, this); + // try homebrew property + return homebrewIndex.findHomebrewMastery(key, activeSources); + } + + private JsonNode findTypePropertyNode(String key) { + String aliasKey = getAliasOrDefault(key); + JsonNode node = getNode(aliasKey); + if (node == null && aliasKey.endsWith("phb")) { + aliasKey = aliasKey.contains("|xphb") + ? aliasKey.replace("|xphb", "|phb") + : aliasKey.replace("|phb", "|xphb"); + node = getNode(aliasKey); + if (node != null) { + addAlias(key, aliasKey); + } } - return mastery; + return node; } public HomebrewMetaTypes getHomebrewMetaTypes(Tools5eSources sources) { @@ -861,13 +909,6 @@ private Optional matchTable(String rowData, JsonNode table) { return Optional.empty(); } - public List originNodesMatching(Function filter) { - return nodeIndex.entrySet().stream() - .filter(e -> filter.apply(e.getValue())) - .map(Entry::getValue) - .collect(Collectors.toList()); - } - public JsonNode getOriginNoFallback(String finalKey) { JsonNode result = nodeIndex.get(finalKey); return result; @@ -898,10 +939,9 @@ public JsonNode getOrigin(String finalKey) { result = nodeIndex.get(lookup); } } - } - if (result == null) { - tui().debugf(Msg.UNRESOLVED, "No element found for %s", - finalKey); + if (result == null) { + tui().log(new Exception("No element found for " + finalKey), false); + } } return result; } @@ -962,7 +1002,7 @@ public String linkifyByName(Tools5eIndexType type, String name) { }); } - public boolean customRulesIncluded() { + public boolean customContentIncluded() { // The biggest hack of all time (not really). // I have some custom content for types/property/mastery that // should be included, but only if: @@ -1006,20 +1046,6 @@ public Set> includedEntries() { return filteredIndex.entrySet(); } - public JsonNode resolveClassFeatureNode(String finalKey) { - JsonNode featureNode = getOrigin(finalKey); - if (featureNode == null) { - tui().debugf(Msg.UNRESOLVED, "unresolved class feature %s", finalKey); - return null; // skip this - } - return resolveClassFeatureNode(finalKey, featureNode); - } - - public JsonNode resolveClassFeatureNode(String finalKey, JsonNode featureNode) { - // TODO: Handle copies or other fill-in / fluff? - return featureNode; - } - public Collection classesForSpell(String spellKey) { return spellClassIndex.get(spellKey); } @@ -1132,6 +1158,9 @@ public void cleanup() { // affiliated sources cache, too Tools5eSources.clear(); + ItemMastery.clear(); + ItemProperty.clear(); + ItemType.clear(); } static class Tuple { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java index 79a44b53..275c8e30 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java @@ -49,6 +49,7 @@ public enum Tools5eIndexType implements IndexType, JsonNodeReader { itemType, itemTypeAdditionalEntries, language, + languageFluff, legendaryGroup, magicvariant, monster, @@ -75,6 +76,7 @@ public enum Tools5eIndexType implements IndexType, JsonNodeReader { status, subclass, subclassFeature, + subclassFluff, subrace("race"), table, tableGroup, @@ -381,7 +383,7 @@ public String decoratedName(JsonNode entry) { public String decoratedName(String name, JsonNode entry) { Tools5eSources sources = Tools5eSources.findOrTemporary(entry); if (sources.isPrimarySource("DMG") - && !sources.type.defaultSourceString().equalsIgnoreCase("DMG") + && !sources.getType().defaultSourceString().equalsIgnoreCase("DMG") && !name.contains("(DMG)")) { return name + " (DMG)"; } @@ -620,16 +622,18 @@ boolean hasVariants() { boolean isFluffType() { return switch (this) { case backgroundFluff, - facilityFluff, classFluff, conditionFluff, + facilityFluff, featFluff, itemFluff, + languageFluff, monsterFluff, objectFluff, optionalfeatureFluff, raceFluff, rewardFluff, + subclassFluff, trapFluff, vehicleFluff -> true; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java index 6f961979..c9e60ca1 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java @@ -186,15 +186,15 @@ public static String srdName(JsonNode node) { return "true".equalsIgnoreCase(name) ? null : name; } - final boolean srd; - final boolean basicRules; - final boolean srd52; - final boolean freeRules2024; - final Tools5eIndexType type; - final String edition; + private final boolean srd; + private final boolean basicRules; + private final boolean srd52; + private final boolean freeRules2024; + private final Tools5eIndexType type; + private final String edition; - boolean filterRule; - boolean cfgIncluded; + private boolean filterRule; + private boolean cfgIncluded; private Tools5eSources(Tools5eIndexType type, String key, JsonNode jsonElement) { super(type, key, jsonElement); @@ -207,6 +207,10 @@ private Tools5eSources(Tools5eIndexType type, String key, JsonNode jsonElement) testSourceRules(); } + public boolean isSrdOrFreeRules() { + return srd || basicRules || srd52 || freeRules2024; + } + /** * Is this included by configuration (source list, include/exclude rules)? * Content may be suppressed for other reasons (reprints) diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBackground.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBackground.java index 17bcccce..241291e0 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBackground.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBackground.java @@ -14,16 +14,13 @@ */ @TemplateData public class QuteBackground extends Tools5eQuteBase { - /** List of images for this background (as {@link dev.ebullient.convert.qute.ImageRef}) */ - public final List fluffImages; /** Formatted text listing other prerequisite conditions (optional) */ public final String prerequisite; public QuteBackground(Tools5eSources sources, String name, String source, String prerequisite, - String text, List images, Tags tags) { - super(sources, name, source, text, tags); - this.fluffImages = images; + List images, String text, Tags tags) { + super(sources, name, source, images, text, tags); this.prerequisite = prerequisite; // optional } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBastion.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBastion.java index 70cde921..bef221e8 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBastion.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBastion.java @@ -36,15 +36,13 @@ public class QuteBastion extends Tools5eQuteBase { public final String type; /** Formatted text listing other prerequisite conditions (optional) */ public final String prerequisite; - /** List of images for this bastion (as {@link dev.ebullient.convert.qute.ImageRef}, optional) */ - public final List fluffImages; public QuteBastion(Tools5eSources sources, String name, String source, List hirelings, String level, List orders, String prerequisite, List space, String type, String text, List images, Tags tags) { - super(sources, name, source, text, tags); - this.fluffImages = images; // optional + super(sources, name, source, images, text, tags); + this.hirelings = hirelings; // optional this.level = level; // optional this.orders = orders; // optional diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteClass.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteClass.java index 79361541..d5f1b2b9 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteClass.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteClass.java @@ -1,5 +1,14 @@ package dev.ebullient.convert.tools.dnd5e.qute; +import static dev.ebullient.convert.StringUtil.join; +import static dev.ebullient.convert.StringUtil.joinConjunct; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import dev.ebullient.convert.qute.ImageRef; +import dev.ebullient.convert.qute.QuteUtil; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.Tools5eSources; import io.quarkus.qute.TemplateData; @@ -12,34 +21,365 @@ @TemplateData public class QuteClass extends Tools5eQuteBase { + /** Formatted string describing the primary abilities for this class */ + public final String primaryAbility; + /** Hit dice for this class as a single digit: 8 */ public final int hitDice; + /** Average Hit dice roll as a single digit */ + public final int hitRollAverage; + + /** + * Hit point die for this class as + * {@link dev.ebullient.convert.tools.dnd5e.qute.QuteClass.HitPointDie} + */ + public final HitPointDie hitPointDie; + /** Formatted callout containing class and feature progressions. */ public final String classProgression; - /** Formatted text describing starting equipment */ - public final String startingEquipment; + /** + * Formatted text describing starting equipment as + * {@link dev.ebullient.convert.tools.dnd5e.qute.QuteClass.StartingEquipment} + */ + public final StartingEquipment startingEquipment; - /** Formatted text section describing how to multiclass with this class */ - public final String multiclassing; + /** + * Multiclassing requirements and proficiencies for this class as + * {@link dev.ebullient.convert.tools.dnd5e.qute.QuteClass.Multiclassing} + */ + public final Multiclassing multiclassing; public QuteClass(Tools5eSources sources, String name, String source, - int hitDice, String classProgression, - String startingEquipment, String multiclassing, - String text, Tags tags) { - super(sources, name, source, text, tags); - - this.hitDice = hitDice; + String classProgression, + String primaryAbility, HitPointDie hitPointDie, + StartingEquipment startingEquipment, Multiclassing multiclassing, + String text, List images, Tags tags) { + super(sources, name, source, images, text, tags); + this.primaryAbility = primaryAbility; + this.hitPointDie = hitPointDie; + // compat with previous version. Sidekicks do not have a hitPointDie + this.hitDice = hitPointDie == null || hitPointDie.isSidekick() + ? 0 + : hitPointDie.face(); + this.hitRollAverage = hitPointDie == null || hitPointDie.isSidekick() + ? 0 + : hitPointDie.average(); this.classProgression = classProgression; this.startingEquipment = startingEquipment; this.multiclassing = multiclassing; } /** - * The average roll for a hit die of this class, for example: `add {resource.hitRollAverage}...` + * Describes the multiclassing information for the class. + * + * If referenced as a unit (ignoring inner attributes), it will render + * formatted text describing multiclassing requirements and proficiencies. + * + * @param primaryAbility Primary ability for multiclassing as formatted + * string (optional) + * @param requirements Prerequisites for multiclassing as formatted + * string (optional) + * @param requirementsSpecial Special prerequisites for multiclassing as + * formatted string (optional) + * @param skills Skill proficiencies gained as formatted string + * (optional) + * @param weapons Weapon proficiencies gained as formatted string + * (optional) + * @param tools Tool proficiencies gained as formatted string + * (optional) + * @param armor Armor proficiencies gained as formatted string + * (optional) + * @param text Formatted text describing this multiclass + * (optional) + * @param isClassic True if this class is from the 2014 edition + */ + @TemplateData + public record Multiclassing( + String primaryAbility, + String requirements, + String requirementsSpecial, + String skills, + String weapons, + String tools, + String armor, + String text, + boolean isClassic) implements QuteUtil { + public String prereq() { + if (isPresent(this.primaryAbility)) { + return "To qualify for a new class, you must have a score of at least 13 in the primary ability of the new class (%s) and your current classes." + .formatted(primaryAbility); + } + if (isPresent(requirements)) { + return requirements; + } + return ""; + } + + public String prereqSpecial() { + if (isPresent(requirementsSpecial)) { + List content = new ArrayList<>(); + if (isPresent(requirements)) { + content.add( + "To qualify for a new class, you must meet the %sprerequisites for both your current class and your new one." + .formatted(isPresent(requirementsSpecial) ? "" : "ability score ")); + } + maybeAddBlankLine(content); + content.add("**%sPrerequisites:** %s".formatted( + isPresent(requirements) ? "Other " : "", + requirementsSpecial)); + return String.join("\n", content); + } + return ""; + } + + public String profIntro() { + return "When you gain a level in a class other than your first, you gain only some of that class's starting proficiencies."; + } + + @Override + public String toString() { + boolean hasRequirements = isPresent(primaryAbility) || isPresent(requirements) + || isPresent(requirementsSpecial); + boolean hasProficiencies = isPresent(armor) || isPresent(weapons) || isPresent(tools) || isPresent(skills); + + List content = new ArrayList<>(); + if (hasRequirements) { + content.add(prereq()); + if (isPresent(requirementsSpecial)) { + maybeAddBlankLine(content); + content.add(prereqSpecial()); + } + } + if (isPresent(text)) { + maybeAddBlankLine(content); + content.add(text); + } + if (hasProficiencies) { + if (isPresent(requirements)) { + maybeAddBlankLine(content); + content.add(profIntro()); + } + maybeAddBlankLine(content); + if (isClassic) { + if (isPresent(armor)) { + content.add("- **Armor**: " + armor); + } + if (isPresent(weapons)) { + content.add("- **Weapons**: " + weapons); + } + if (isPresent(tools)) { + content.add("- **Tools**: " + tools); + } + if (isPresent(skills)) { + content.add("- **Skills**: " + skills); + } + } else { + if (isPresent(skills)) { + content.add("- **Skill Proficiencies**: " + skills); + } + if (isPresent(weapons)) { + content.add("- **Weapon Proficiencies**: " + weapons); + } + if (isPresent(tools)) { + content.add("- **Tool Proficiencies**: " + tools); + } + if (isPresent(armor)) { + content.add("- **Armor Training**: " + armor); + } + } + } + + return String.join("\n", content); + } + } + + /** + * Describes the starting equipment for the class. + * + * If referenced as a unit (ignoring inner attributes), it will render + * structured text describing starting proficiencies and equipment *2014* vs + * *2024*. + * + * @param savingThrows List of saving throws + * @param skills List of skills as formatted strings (links) + * @param weapons List of weapons as formatted strings (links) + * @param tools List of tools as formatted strings (links) + * @param armor List of armor as formatted strings (links) + * @param equipment List of equipment as formatted strings (links) + * @param isClassic True if this class is from the 2014 edition */ - public int getHitRollAverage() { - return (hitDice + 1) / 2; + @TemplateData + public record StartingEquipment( + List savingThrows, + List skills, + List weapons, + List tools, + List armor, + String equipment, + boolean isClassic) implements QuteUtil { + + @Override + public String toString() { + List text = new ArrayList<>(); + text.add(getProficiencies()); + if (isPresent(equipment)) { + maybeAddBlankLine(text); + text.add((isClassic ? "" : "**Starting Equipment:** ") + equipment); + } + return String.join("\n", text); + } + + /** Formatted string of class proficiencies */ + public String getProficiencies() { + List text = new ArrayList<>(); + if (isClassic) { + text.add("- **Saving Throws**: " + getJoinOrDefault(savingThrows, null)); + text.add("- **Armor**: " + (isPresent(armor) ? getArmorString() : "none")); + text.add("- **Weapons**: " + getJoinOrDefault(weapons, isClassic ? null : " and ")); + text.add("- **Tools**: " + getJoinOrDefault(tools, isClassic ? null : " and ")); + text.add("- **Skills**: " + join(" *or* ", skills)); + } else { + text.add("- **Saving Throw Proficiencies**: " + getJoinOrDefault(savingThrows, null)); + text.add("- **Skill Proficiencies**: " + join(" *or* ", skills)); + text.add("- **Weapon Proficiencies**: " + getJoinOrDefault(weapons, isClassic ? null : " and ")); + if (isPresent(tools)) { + text.add("- **Tool Proficiencies**: " + getJoinOrDefault(tools, isClassic ? null : " and ")); + } + if (isPresent(armor)) { + text.add("- **Armor Training**: " + getArmorString()); + } + } + return String.join("\n", text); + } + + /** + * Create a structured string describing armor training. + * Slighly different formatting and joining for 2014 vs 2024 materials. + * + * @return formatted string with links to armor item types and shield items + */ + public String getArmorString() { + if (isClassic) { + return join(", ", armor); + } + List armorLinks = armor.stream() + .filter(s -> s.matches("Light|Medium|Heavy")) + .collect(Collectors.toCollection(ArrayList::new)); + List otherLinks = armor.stream() + .filter(s -> !s.matches("Light|Medium|Heavy")) + .toList(); + if (armorLinks.size() > 1) { + // remove " armor" from all but the last item + for (int i = 0; i < armorLinks.size() - 1; i++) { + armorLinks.set(i, armorLinks.get(i).replace(" armor", "")); + } + String joined = joinConjunct("and", armorLinks); + armorLinks.clear(); + armorLinks.add(joined); + } + armorLinks.addAll(otherLinks); + return joinConjunct(" and ", armorLinks); + } + + /** + * Given a list of strings, return a formatted string with a conjunction. + * + * @param value List of strings. + * @param conjunct Conjunction (and, or). If null, elements will be + * comma-separated. + * Otherwise, the first n elements comma-separated and the last + * element will be joined with conjunction. + * @return Formatted string. If value is empty, will return "none". + */ + public String getJoinOrDefault(List value, String conjunct) { + if (value == null || value.isEmpty()) { + return "none"; + } + return conjunct == null + ? join(", ", value) + : joinConjunct(conjunct, value); + } + } + + /** + * Describes the hit point die used by the class. + * + * If referenced as a unit (ignoring inner attributes), it will render + * formatted strings based on the class version (2024 or not). + * + * @param number How many dice to roll (pretty much always 1) + * @param face Die to roll (8, 10); This will be 0 for sidekicks + * @param average The average value of a hit dice roll + * @param isClassic True if this is a 2014 class + * @param isSidekick Explicit test for sidekick (alternate to 0 face) + */ + @TemplateData + public record HitPointDie( + String name, + int number, + int face, + int average, + boolean isClassic, + boolean isSidekick) { + public HitPointDie(String name, int number, int face, boolean isClassic, boolean isSidekick) { + this(name, number, face, (number * face) / 2 + 1, isClassic, isSidekick); + } + + @Override + public String toString() { + // return + // `
Hit Point Die: + // ${renderer.render(Renderer.class.getHitDiceEntry(cls.hd, {styleHint}))} per + // ${cls.name} level
+ //
Hit Points at Level 1: + // ${Renderer.class.getHitPointsAtFirstLevel(cls.hd, {styleHint})}
+ //
Hit Points per additional ${cls.name} Level: + // ${Renderer.class.getHitPointsAtHigherLevels(cls.name, cls.hd, + // {styleHint})}
`; + // return styleHint === "classic" -- hit dice entry + // ? `{@dice ${clsHd.number}d${clsHd.faces}||Hit die}` + // : `{@dice ${clsHd.number}d${clsHd.faces}|${clsHd.number === 1 ? "" : + // clsHd.number}D${clsHd.faces}|Hit die}`; + if (isSidekick) { + String suffix = isClassic ? "its Constitution modifier" : "its Con. modifier"; + return """ + - **Hit Point Die**: *x*; specified in the sidekick's statblock (human, gnome, kobold, etc.) + - **Hit Points at Level 1:** 1d*x* + %s + - **Hit Points per additional %s lvel:** 1d*x* + %s (minimum of 1 hit point per level) + """ + .stripIndent() + .formatted(suffix, name, suffix); + } + + String dieEntry = isClassic + ? "%sd%s".formatted(number, face) + : "%sD%s".formatted(number == 1 ? "" : number, face); + + // classic ? `${clsHd.number * clsHd.faces} + your Constitution modifier` + // : `${clsHd.number * clsHd.faces} + Con. modifier`; + String level1 = "%s + %s".formatted( + number * face, + isClassic ? "your Constitution modifier" : "Con. modifier"); + + // classic ? `${Renderer.get().render(Renderer.class.getHitDiceEntry(clsHd, + // {styleHint}))} (or ${((clsHd.number * clsHd.faces) / 2 + 1)}) + your + // Constitution modifier per ${className} level after 1st` + // : `${Renderer.get().render(Renderer.class.getHitDiceEntry(clsHd, + // {styleHint}))} + your Con. modifier, or, ${((clsHd.number * clsHd.faces) / 2 + // + 1)} + your Con. modifier`; + String levelUp = isClassic + ? "%s (or %s) + your Constitution modifier".formatted( + dieEntry, average) + : "%s + your Con. modifier or %s + your Con. modifier".formatted( + dieEntry, average); // average + + return """ + - **Hit Point Die:** %s per %s level + - **Hit Points at Level 1:** %s + - **Hit Points per additional %s Level:** %s + """.formatted(dieEntry, name, level1, name, levelUp); + } } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeck.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeck.java index 47095e47..43a8a82a 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeck.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeck.java @@ -25,7 +25,7 @@ public class QuteDeck extends Tools5eQuteBase { public QuteDeck(CompendiumSources sources, String name, String source, ImageRef cardBack, List cards, String text, Tags tags) { - super(sources, name, source, text, tags); + super(sources, name, source, List.of(), text, tags); this.cardBack = cardBack; this.cards = cards; } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeity.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeity.java index 7a40cd9a..e27743a1 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeity.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeity.java @@ -39,7 +39,7 @@ public QuteDeity(Tools5eSources sources, String name, String source, String title, String cateogry, String domains, String province, String symbol, ImageRef symbolImg, String text, Tags tags) { - super(sources, name, source, text, tags); + super(sources, name, source, List.of(), text, tags); this.altNames = altNames; this.pantheon = pantheon; this.alignment = alignment; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteFeat.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteFeat.java index 84d02cf2..877f8ecb 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteFeat.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteFeat.java @@ -1,5 +1,8 @@ package dev.ebullient.convert.tools.dnd5e.qute; +import java.util.List; + +import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.Tools5eSources; import io.quarkus.qute.TemplateData; @@ -19,8 +22,8 @@ public class QuteFeat extends Tools5eQuteBase { public QuteFeat(Tools5eSources sources, String name, String source, String prerequisite, String level, - String text, Tags tags) { - super(sources, name, source, text, tags); + List images, String text, Tags tags) { + super(sources, name, source, images, text, tags); withTemplate("feat2md.txt"); // Feat and OptionalFeature this.level = level; this.prerequisite = prerequisite; // optional diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteHazard.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteHazard.java index 573791d2..4db9fb51 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteHazard.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteHazard.java @@ -1,5 +1,8 @@ package dev.ebullient.convert.tools.dnd5e.qute; +import java.util.List; + +import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.CompendiumSources; import dev.ebullient.convert.tools.Tags; import io.quarkus.qute.TemplateData; @@ -17,8 +20,8 @@ public class QuteHazard extends Tools5eQuteBase { public QuteHazard(CompendiumSources sources, String name, String source, String hazardType, - String text, Tags tags) { - super(sources, name, source, text, tags); + List images, String text, Tags tags) { + super(sources, name, source, images, text, tags); this.hazardType = hazardType; withTemplate("hazard2md.txt"); // not trap or hazard (types) } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteItem.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteItem.java index b17e23e9..913217f6 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteItem.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteItem.java @@ -1,11 +1,11 @@ package dev.ebullient.convert.tools.dnd5e.qute; import static dev.ebullient.convert.StringUtil.join; +import static dev.ebullient.convert.StringUtil.toAnchorTag; import java.util.List; import java.util.stream.Collectors; -import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.Tools5eSources; @@ -22,19 +22,16 @@ public class QuteItem extends Tools5eQuteBase { /** Detailed information about this item as {@link dev.ebullient.convert.tools.dnd5e.qute.QuteItem.Variant} */ public final Variant rootVariant; - /** List of images for this item as {@link dev.ebullient.convert.qute.ImageRef} */ - public final List fluffImages; /** List of magic item variants as {@link dev.ebullient.convert.tools.dnd5e.qute.QuteItem.Variant}. Optional. */ public final List variants; public QuteItem(Tools5eSources sources, String source, - Variant rootVariant, String text, List images, - List variants, Tags tags) { - super(sources, rootVariant.name, source, text, tags); + Variant rootVariant, List variants, List images, + String text, Tags tags) { + super(sources, rootVariant.name, source, images, text, tags); withTemplate("item2md.txt"); this.rootVariant = rootVariant; - this.fluffImages = images == null ? List.of() : images; this.variants = variants == null ? List.of() : variants; } @@ -130,7 +127,7 @@ public String getVariantSectionLinks() { return ""; } return variants.stream() - .map(x -> String.format("- [%s](#%s)", x.name, Tui.toAnchorTag(x.name))) + .map(x -> String.format("- [%s](#%s)", x.name, toAnchorTag(x.name))) .collect(Collectors.joining("\n")); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java index 7d1e64e7..d38a558b 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java @@ -91,8 +91,6 @@ public class QuteMonster extends Tools5eQuteBase { public final String environment; /** Token image as {@link dev.ebullient.convert.qute.ImageRef} */ public final ImageRef token; - /** List of {@link dev.ebullient.convert.qute.ImageRef} related to the creature */ - public final List fluffImages; public QuteMonster(Tools5eSources sources, String name, String source, boolean isNpc, String size, String type, String subtype, String alignment, @@ -105,9 +103,9 @@ public QuteMonster(Tools5eSources sources, String name, String source, boolean i Collection legendary, Collection legendaryGroup, String legendaryGroupLink, List spellcasting, String description, String environment, - ImageRef tokenImage, List fluffImages, Tags tags) { + ImageRef tokenImage, List images, Tags tags) { - super(sources, name, source, description, tags); + super(sources, name, source, images, description, tags); this.isNpc = isNpc; this.size = size; @@ -137,7 +135,6 @@ public QuteMonster(Tools5eSources sources, String name, String source, boolean i this.description = description; this.environment = environment; this.token = tokenImage; - this.fluffImages = fluffImages; } @Override diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteObject.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteObject.java index d0d12231..059d1d18 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteObject.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteObject.java @@ -45,8 +45,6 @@ public class QuteObject extends Tools5eQuteBase { /** Token image as {@link dev.ebullient.convert.qute.ImageRef} */ public final ImageRef token; - /** List of {@link dev.ebullient.convert.qute.ImageRef} related to the creature */ - public final List fluffImages; public QuteObject(CompendiumSources sources, String name, String source, @@ -57,9 +55,9 @@ public QuteObject(CompendiumSources sources, String senses, ImmuneResist immuneResist, Collection actions, - ImageRef tokenImage, List fluffImages, + ImageRef tokenImage, List images, String text, Tags tags) { - super(sources, name, source, text, tags); + super(sources, name, source, images, text, tags); this.isNpc = isNpc; this.size = size; this.creatureType = creatureType; @@ -74,7 +72,6 @@ public QuteObject(CompendiumSources sources, this.action = actions; this.token = tokenImage; - this.fluffImages = fluffImages; } /** List of source books (abbreviated name). Fantasy statblock uses this list. */ diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QutePsionic.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QutePsionic.java index 1adf6cc6..602e0f4f 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QutePsionic.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QutePsionic.java @@ -1,6 +1,7 @@ package dev.ebullient.convert.tools.dnd5e.qute; import java.util.Collection; +import java.util.List; import dev.ebullient.convert.qute.NamedText; import dev.ebullient.convert.tools.CompendiumSources; @@ -25,7 +26,7 @@ public class QutePsionic extends Tools5eQuteBase { public QutePsionic(CompendiumSources sources, String name, String source, String typeOrder, String focus, Collection modes, String text, Tags tags) { - super(sources, name, source, text, tags); + super(sources, name, source, List.of(), text, tags); this.typeOrder = typeOrder; this.focus = focus; this.modes = modes; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteRace.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteRace.java index 895b079c..5668a640 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteRace.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteRace.java @@ -29,14 +29,12 @@ public class QuteRace extends Tools5eQuteBase { public final String traits; /** Formatted text describing the race. Optional. Same as {resource.text} */ public final String description; - /** List of images for this race (as {@link dev.ebullient.convert.qute.ImageRef}) */ - public final List fluffImages; public QuteRace(Tools5eSources sources, String name, String source, String ability, String type, String size, String speed, String spellcasting, String traits, String description, List images, Tags tags) { - super(sources, name, source, description, tags); + super(sources, name, source, images, description, tags); this.ability = ability; this.type = type; this.size = size; @@ -44,6 +42,5 @@ public QuteRace(Tools5eSources sources, String name, String source, this.spellcasting = spellcasting; this.traits = traits; this.description = description; - this.fluffImages = images; } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteReward.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteReward.java index 104bbfaf..bfc92f79 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteReward.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteReward.java @@ -1,5 +1,8 @@ package dev.ebullient.convert.tools.dnd5e.qute; +import java.util.List; + +import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.CompendiumSources; import dev.ebullient.convert.tools.Tags; import io.quarkus.qute.TemplateData; @@ -23,8 +26,8 @@ public class QuteReward extends Tools5eQuteBase { public QuteReward(CompendiumSources sources, String name, String source, String ability, String detail, String signatureSpells, - String text, Tags tags) { - super(sources, name, source, text, tags); + List images, String text, Tags tags) { + super(sources, name, source, images, text, tags); withTemplate("reward2md.txt"); this.ability = ability; this.detail = detail; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSpell.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSpell.java index 62dcc85a..0182b332 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSpell.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSpell.java @@ -32,14 +32,12 @@ public class QuteSpell extends Tools5eQuteBase { public final String duration; /** String: rendered list of links to classes that can use this spell. May be incomplete or empty. */ public final String classes; - /** List of images for this spell (as {@link dev.ebullient.convert.qute.ImageRef}) */ - public final List fluffImages; public QuteSpell(Tools5eSources sources, String name, String source, String level, String school, boolean ritual, String time, String range, String components, String duration, - String classes, String text, List fluffImages, Tags tags) { - super(sources, name, source, text, tags); + String classes, List images, String text, Tags tags) { + super(sources, name, source, images, text, tags); this.level = level; this.school = school; @@ -49,7 +47,6 @@ public QuteSpell(Tools5eSources sources, String name, String source, String leve this.components = components; this.duration = duration; this.classes = classes; - this.fluffImages = fluffImages; } /** List of class names that can use this spell. May be incomplete or empty. */ diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSubclass.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSubclass.java index d98f7e6b..37837f42 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSubclass.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSubclass.java @@ -1,5 +1,8 @@ package dev.ebullient.convert.tools.dnd5e.qute; +import java.util.List; + +import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.Tools5eSources; import io.quarkus.qute.TemplateData; @@ -11,7 +14,6 @@ */ @TemplateData public class QuteSubclass extends Tools5eQuteBase { - /** Name of the parent class */ public final String parentClass; /** Markdown link to the parent class */ @@ -30,9 +32,8 @@ public QuteSubclass(Tools5eSources sources, String parentClassSource, String subclassTitle, String classProgression, - String text, Tags tags) { - super(sources, name, source, text, tags); - + String text, List images, Tags tags) { + super(sources, name, source, images, text, tags); this.parentClass = parentClass; this.parentClassLink = parentClassLink; this.parentClassSource = parentClassSource; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteVehicle.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteVehicle.java index 57c650e5..8cb95742 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteVehicle.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteVehicle.java @@ -54,8 +54,6 @@ public class QuteVehicle extends Tools5eQuteBase { /** Token image as {@link dev.ebullient.convert.qute.ImageRef} */ public final ImageRef token; - /** List of {@link dev.ebullient.convert.qute.ImageRef} related to the creature */ - public final List fluffImages; public QuteVehicle(CompendiumSources sources, String name, String source, String vehicleType, String terrain, @@ -64,9 +62,8 @@ public QuteVehicle(CompendiumSources sources, String name, String source, ShipCrewCargoPace shipCrewCargoPace, List shipSections, Collection action, - ImageRef token, List fluffImages, - String text, Tags tags) { - super(sources, name, source, text, tags); + ImageRef token, List images, String text, Tags tags) { + super(sources, name, source, images, text, tags); this.vehicleType = vehicleType; this.terrain = terrain; @@ -81,7 +78,6 @@ public QuteVehicle(CompendiumSources sources, String name, String source, this.action = action; this.token = token; - this.fluffImages = fluffImages; } /** True if this vehicle is a Ship */ diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteBase.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteBase.java index 31cd42c8..7179a8e1 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteBase.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteBase.java @@ -1,11 +1,13 @@ package dev.ebullient.convert.tools.dnd5e.qute; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import com.fasterxml.jackson.databind.JsonNode; import dev.ebullient.convert.io.Tui; +import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.qute.QuteBase; import dev.ebullient.convert.tools.CompendiumSources; import dev.ebullient.convert.tools.Tags; @@ -24,12 +26,76 @@ @TemplateData public class Tools5eQuteBase extends QuteBase { + /** List of images as {@link dev.ebullient.convert.qute.ImageRef} (optional) */ + public final List fluffImages; + String targetPath; String filename; String template; - public Tools5eQuteBase(CompendiumSources sources, String name, String source, String text, Tags tags) { + public Tools5eQuteBase(CompendiumSources sources, String name, String source, List fluffImages, String text, + Tags tags) { super(sources, name, source, text, tags); + this.fluffImages = isPresent(fluffImages) ? fluffImages : List.of(); + } + + /** + * Return true if any images are present + */ + public boolean getHasImages() { + return !fluffImages.isEmpty(); + } + + /** + * Return true if more than one image is present + */ + public boolean getHasMoreImages() { + return fluffImages.size() > 1; + } + + /** + * Return an embedded wikilink to the first image + * Will have the "right" anchor tag. + */ + public String getShowPortraitImage() { + if (fluffImages.isEmpty()) { + return ""; + } + return fluffImages.get(0).getEmbeddedLink("right"); + } + + /** + * Return embedded wikilinks for all images + * If there is more than one, they will be displayed in a gallery. + */ + public String getShowAllImages() { + return createImageLinks(false); + } + + /** + * Return embedded wikilinks for all but the first image + * If there is more than one, they will be displayed in a gallery. + */ + public String getShowMoreImages() { + return createImageLinks(true); + } + + private String createImageLinks(boolean omitFirst) { + if (fluffImages.isEmpty()) { + return ""; + } + if (fluffImages.size() == 1 && !omitFirst) { + return fluffImages.get(0).getEmbeddedLink("center"); + } + if (fluffImages.size() == 2 && omitFirst) { + return fluffImages.get(1).getEmbeddedLink("center"); + } + List lines = new ArrayList<>(); + lines.add("> [!gallery]"); + for (int i = omitFirst ? 1 : 0; i < fluffImages.size(); i++) { + lines.add(fluffImages.get(i).getEmbeddedLink("")); // no anchor + } + return String.join("\n", lines); } public static String fixFileName(String name, Tools5eSources sources) { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBook.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBook.java index f1104d55..684cb185 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBook.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBook.java @@ -1,5 +1,6 @@ package dev.ebullient.convert.tools.pf2e; +import static dev.ebullient.convert.StringUtil.toAnchorTag; import static dev.ebullient.convert.StringUtil.toTitleCase; import java.nio.file.Path; diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCompose.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCompose.java index 9d05096b..f9710693 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCompose.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCompose.java @@ -1,5 +1,7 @@ package dev.ebullient.convert.tools.pf2e; +import static dev.ebullient.convert.StringUtil.toAnchorTag; + import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/JsonTextReplacement.java b/src/main/java/dev/ebullient/convert/tools/pf2e/JsonTextReplacement.java index 8d890ec4..24dc0c4b 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/JsonTextReplacement.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/JsonTextReplacement.java @@ -1,6 +1,7 @@ package dev.ebullient.convert.tools.pf2e; import static dev.ebullient.convert.StringUtil.join; +import static dev.ebullient.convert.StringUtil.toAnchorTag; import static dev.ebullient.convert.StringUtil.toTitleCase; import java.util.ArrayList; diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteTraitIndex.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteTraitIndex.java index 7dd32c94..f5c01b97 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteTraitIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteTraitIndex.java @@ -1,12 +1,13 @@ package dev.ebullient.convert.tools.pf2e.qute; +import static dev.ebullient.convert.StringUtil.toAnchorTag; + import java.util.Collection; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.stream.Collectors; -import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.pf2e.Pf2eIndexType; import dev.ebullient.convert.tools.pf2e.Pf2eSources; import io.quarkus.qute.TemplateData; @@ -43,7 +44,7 @@ public QuteTraitIndex(Pf2eSources sources, Map> categ /** List of category anchor links */ public List getCategoryLinks() { return categoryToTraits.keySet().stream() - .map(x -> "[" + x + "](#" + Tui.toAnchorTag(x) + ")") + .map(x -> "[" + x + "](#" + toAnchorTag(x) + ")") .toList(); } diff --git a/src/main/resources/convertData.json b/src/main/resources/convertData.json index fb68a284..30dfa088 100644 --- a/src/main/resources/convertData.json +++ b/src/main/resources/convertData.json @@ -30,6 +30,16 @@ "type": "entries", "name": "Attunement", "edition": "classic", + "source": "DMG", + "page": 136, + "basicRules": true, + "srd": true, + "reprintedAs": [ + { + "tag": "reference", + "uid": "Attunement|XPHB" + } + ], "entries": [ "Some magic items require a creature to form a bond with them before their magical properties can be used. This bond is called attunement, and certain items have a prerequisite for it. If the prerequisite is a class, a creature must be a member of that class to attune to the item. (If the class is a spellcasting class, a monster qualifies if it has spell slots and uses that class's spell list.) If the prerequisite is to be a spellcaster, a creature qualifies if it can cast at least one spell using its traits or features, not using a magic item or the like.", "Without becoming attuned to an item that requires attunement, a creature gains only its nonmagical benefits, unless its description states otherwise. For example, a magic shield that requires attunement provides the benefits of a normal shield to a creature not attuned to it, but none of its magical properties.", @@ -59,6 +69,8 @@ "source": "XPHB", "page": 232, "id": "717", + "freerules2024": true, + "srd52": true, "entries": [ "Some magic items require a creature to form a bond\u2014called Attunement\u2014with them before the creature can use an item's magical properties. Without becoming attuned to an item that requires Attunement, you gain only its nonmagical benefits unless its description states otherwise. For example, a magic Shield that requires Attunement provides the benefits of a normal Shield if you aren't attuned to it, but none of its magical properties.", { @@ -107,7 +119,9 @@ { "name": "General and Weapon Properties", "srd": true, + "srd52": true, "basicRules": true, + "freeRules2024": true, "entries": [ ] }, @@ -118,6 +132,12 @@ "page": 147, "srd": true, "basicRules": true, + "reprintedAs": [ + { + "tag": "reference", + "uid": "Improvised Weapons|XPHB" + } + ], "entries": [ "Sometimes characters don't have their weapons and have to attack with whatever is close at hand. An improvised weapon includes any object you can wield in one or two hands, such as broken glass, a table leg, a frying pan, a wagon wheel, or a dead goblin.", "In many cases, an improvised weapon is similar to an actual weapon and can be treated as such. For example, a table leg is akin to a club. At the DM's option, a character proficient with a weapon can use a similar object as if it were that weapon and use his or her proficiency bonus.", @@ -174,16 +194,41 @@ "name": "Cursed Items", "source": "DMG", "page": 138, + "basicRules": true, + "reprintedAs": [ + { + "tag": "reference", + "uid": "Cursed Items|XDMG" + } + ], "entries": [ "Some magic items bear curses that bedevil their users, sometimes long after a user has stopped using an item. Most methods of identifying items, including the identify spell, fail to reveal the presence of a curse, although lore might hint at it.", "Attunement to a cursed item can't be ended voluntarily unless the curse is broken first, such as with the {@spell remove curse} spell." ] }, + { + "type": "entries", + "name": "Cursed Items", + "source": "XDMG", + "page": 220, + "freeRules2024": true, + "entries": [ + "A magic item’s description specifies whether it bears a curse. Most methods of identifying items, including the Identify spell, fail to reveal such a curse.", + "Attunement to a cursed item can't be ended voluntarily unless the curse is broken first, such as with the {@spell Remove Curse} spell." + ] + }, { "type": "entries", "name": "Poison", "source": "DMG", "page": 257, + "srd": true, + "reprintedAs": [ + { + "tag": "reference", + "uid": "Poison|XDMG" + } + ], "entries": [ "Given their insidious and deadly nature, poisons are illegal in most societies but are a favorite tool among assassins, drow, and other evil creatures.", "Poisons come in the following four types.", @@ -224,6 +269,59 @@ "id": "2fc" } ] + }, + { + "name": "Poison", + "source": "XDMG", + "page": 90, + "entries": [ + "Given their insidious and deadly nature, poisons are a favorite tool among assassins and evil creatures.", + "Poisons come in the following four types:", + { + "type": "list", + "style": "list-hang-notitle", + "items": [ + { + "type": "item", + "name": "Contact", + "entry": "Contact poison can be smeared on an object and remains potent until it is touched or washed off. A creature that touches contact poison with exposed skin suffers its effects." + }, + { + "type": "item", + "name": "Ingested", + "entry": "A creature must swallow an entire dose of ingested poison to suffer its effects. The dose can be delivered in food or a liquid. You may decide that a partial dose has a reduced effect, such as allowing {@variantrule Advantage|XPHB} on the saving throw or dealing only half as much damage on a failed save." + }, + { + "type": "item", + "name": "Inhaled", + "entry": "Poisonous powders and gases take effect when inhaled. Blowing the powder or releasing the gas subjects creatures in a 5-foot {@variantrule Cube [Area of Effect]|XPHB|Cube} to its effect. The resulting cloud dissipates immediately afterward. Holding one's breath is ineffective against inhaled poisons, as they affect nasal membranes, tear ducts, and other parts of the body." + }, + { + "type": "item", + "name": "Injury", + "entry": "Injury poison can be applied as a Bonus Action to a weapon, a piece of ammunition, or similar object. The poison remains potent until delivered through a wound or washed off. A creature that takes Piercing or Slashing damage from an object coated with the poison is exposed to its effects." + } + ] + }, + { + "type": "entries", + "name": "Purchasing Poison", + "page": 90, + "id": "1be", + "entries": [ + "In some settings, laws prohibit the possession and use of poison, but an illicit dealer or unscrupulous apothecary might keep a hidden stash. Characters with criminal contacts might be able to acquire poison easily. Other characters might have to make extensive inquiries and pay bribes before they acquire the poison they seek." + ] + }, + { + "type": "entries", + "name": "Harvesting Poison", + "page": 90, + "id": "1bf", + "entries": [ + "A character can attempt to harvest poison from a venomous creature that is dead or has the {@condition Incapacitated|XPHB} condition. The effort takes {@dice 1d6} minutes, after which the character makes a {@dc 20} Intelligence ({@skill Nature|XPHB}) check using a {@item Poisoner's Kit|XPHB}. On a successful check, the character harvests enough poison for a single dose, and no additional poison can be harvested from that creature. On a failed check, the character is unable to extract any poison. If the character fails the check by 5 or more, the character is subjected to the creature's poison." + ] + } + ] } ] }, @@ -565,6 +663,7 @@ "aliases": { "item|alchemist's tools|phb": "item|alchemist's supplies|phb", "item|alchemists' supplies|phb": "item|alchemist's supplies|phb", + "item|arrow of slaying (generic)|dmg": "item|arrow of slaying|dmg", "item|backpack|dmg": "item|backpack|phb", "item|breastplate|dmg": "item|breastplate|phb", "item|caltrops (20)|phb": "item|caltrops (bag of 20)|phb", @@ -589,6 +688,7 @@ "spell|deception|phb": "skill|deception|phb", "spell|detect good and evil|phb": "spell|detect evil and good|phb", "spell|enlarge|phb": "spell|enlarge/reduce|phb", + "spell|fire wall|phb": "spell|wall of fire|phb", "spell|frostbite|phb": "spell|frostbite|xge", "spell|history|phb": "skill|history|phb", "spell|infestation|phb": "spell|infestation|xge", @@ -605,52 +705,6 @@ "trap|quicksand|dmg": "hazard|quicksand|dmg" }, "fixes": { - "adventure/adventure-lr.json": [ - { - "match": " \\(see the \\\"\\{@condition Bluerot\\|GoS}\\\" sidebar\\)", - "replace": "" - } - ], - "adventure/adventure-rot.json": [ - { - "match": "\\{@i \\{@i The Rise of Tiamat}.}", - "replace": "{@i The Rise of Tiamat}." - } - ], - "class/class-artificer.json": [ - { - "match": "(\"page\":\\s*\\d+,)([\\r\\n\\s]+)(\"subclassFeatures\": \\[[\\s\\r\\n]+\"Alchemist)", - "replace": "$1$2\"optionalfeatureProgression\": [ { \"name\": \"Alchemical Formula\", \"featureType\": [ \"AF\" ] } ],$2$3" - } - ], - "class/class-bard.json": [ - { - "match": "(\"isReprinted\": true,)([\\r\\n\\s]+)(\"subclassFeatures\": \\[[\\s\\r\\n]+\"College of Swords)", - "replace": "$1$2\"optionalfeatureProgression\": [ { \"name\": \"Fighting Style\", \"featureType\": [ \"FS:B\" ], \"progression\": { \"3\": 1 } } ],$2$3" - }, - { - "match": "(\"page\":\\s*\\d+,)([\\r\\n\\s]+)(\"subclassFeatures\": \\[[\\s\\r\\n]+\"College of Swords)", - "replace": "$1$2\"optionalfeatureProgression\": [ { \"name\": \"Fighting Style\", \"featureType\": [ \"FS:B\" ], \"progression\": { \"3\": 1 } } ],$2$3" - } - ], - "class/class-wizard.json": [ - { - "match": "(\\],)([\\r\\n\\s]+)(\"subclassFeatures\": \\[[\\s\\r\\n]+\"Onomancy)", - "replace": "$1$2\"optionalfeatureProgression\": [ { \"name\": \"Onomancy Resonant\", \"featureType\": [ \"OR\" ] } ],$2$3" - } - ], - "class/D&D Wiki; Swashbuckler.json": [ - { - "match": "\t\t\t\t\"MB\"", - "replace": "\t\t\t\t\"TB\"" - } - ], - "deities.json": [ - { - "match": "deities/TDCSR/StormLord.webp", - "replace": "deities/TDCSR/Stormlord.webp" - } - ] }, "sources": [ "actions.json", @@ -663,6 +717,7 @@ "books.json", "class", "conditionsdiseases.json", + "cultsboons.json", "decks.json", "deities.json", "feats.json", @@ -671,6 +726,7 @@ "fluff-conditionsdiseases.json", "fluff-feats.json", "fluff-items.json", + "fluff-languages.json", "fluff-objects.json", "fluff-optionalfeatures.json", "fluff-races.json", @@ -680,6 +736,7 @@ "generated/gendata-tables.json", "items-base.json", "items.json", + "languages.json", "magicvariants.json", "objects.json", "optionalfeatures.json", diff --git a/src/test/java/dev/ebullient/convert/CustomTemplatesTest.java b/src/test/java/dev/ebullient/convert/CustomTemplatesTest.java index e7ad85b4..ab6f1d98 100644 --- a/src/test/java/dev/ebullient/convert/CustomTemplatesTest.java +++ b/src/test/java/dev/ebullient/convert/CustomTemplatesTest.java @@ -2,12 +2,18 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import dev.ebullient.convert.io.Tui; @@ -19,9 +25,11 @@ @QuarkusMainTest public class CustomTemplatesTest { - static Path testOutput; + static Path rootTestOutput; static Tui tui; + Path testOutput; + @BeforeAll public static void setupDir() { setupDir("templates"); @@ -30,8 +38,8 @@ public static void setupDir() { public static void setupDir(String name) { tui = new Tui(); tui.init(null, false, false); - testOutput = TestUtils.OUTPUT_ROOT_5E.resolve(name); - testOutput.toFile().mkdirs(); + rootTestOutput = TestUtils.OUTPUT_ROOT_5E.resolve(name); + rootTestOutput.toFile().mkdirs(); } @AfterAll @@ -39,9 +47,33 @@ public static void cleanup() { System.out.println("Done."); } + @BeforeEach + public void setup() { + testOutput = null; // test should set this to something readable + } + + @AfterEach + public void moveLogFile() throws IOException { + assertThat(testOutput).isNotNull(); // make sure test set this + + Path logFile = Path.of("ttrpg-convert.out.txt"); + if (Files.exists(logFile) && Files.exists(testOutput)) { + String content = Files.readString(logFile, StandardCharsets.UTF_8); + + Path filePath = testOutput.resolve(logFile); + Files.move(logFile, filePath, StandardCopyOption.REPLACE_EXISTING); + + if (content.contains("Exception")) { + tui.errorf("Exception found in %s", filePath); + } + } + TestUtils.cleanupReferences(); + } + @Test @Launch({ "--help" }) void testCommandHelp(LaunchResult result) { + testOutput = rootTestOutput.resolve("help"); result.echoSystemOut(); assertThat(result.getOutput()) .withFailMessage("Command failed. Output:%n%s", TestUtils.dump(result)) @@ -51,6 +83,7 @@ void testCommandHelp(LaunchResult result) { @Test @Launch({ "--version" }) void testCommandVersion(LaunchResult result) { + testOutput = rootTestOutput.resolve("version"); result.echoSystemOut(); assertThat(result.getOutput()) .withFailMessage("Command failed. Output:%n%s", TestUtils.dump(result)) @@ -58,13 +91,13 @@ void testCommandVersion(LaunchResult result) { } @Test - void testCommandBadTemplates(QuarkusMainLauncher launcher) { + void testCommandBadTemplates(QuarkusMainLauncher launcher) throws IOException { + testOutput = rootTestOutput.resolve("bad-templates"); if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { - Path target = testOutput.resolve("bad-templates"); - + TestUtils.deleteDir(testOutput); LaunchResult result = launcher.launch("--index", "--background=garbage.txt", - "-o", target.toString(), + "-o", testOutput.toString(), TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString(), TestUtils.PATH_5E_TOOLS_DATA.toString()); @@ -75,12 +108,13 @@ void testCommandBadTemplates(QuarkusMainLauncher launcher) { } @Test - void testCommandBadTemplatesInJson(QuarkusMainLauncher launcher) { + void testCommandBadTemplatesInJson(QuarkusMainLauncher launcher) throws IOException { + testOutput = rootTestOutput.resolve("bad-templates-json"); if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { - Path target = testOutput.resolve("bad-templates-json"); + TestUtils.deleteDir(testOutput); LaunchResult result = launcher.launch("--index", - "-o", target.toString(), + "-o", testOutput.toString(), TestUtils.PATH_5E_TOOLS_DATA.toString(), TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString(), TestUtils.TEST_RESOURCES.resolve("sources-bad-template.json").toString()); @@ -92,13 +126,13 @@ void testCommandBadTemplatesInJson(QuarkusMainLauncher launcher) { } @Test - void testCommandTemplates_5e(QuarkusMainLauncher launcher) { + void testCommandTemplates_5e(QuarkusMainLauncher launcher) throws IOException { + testOutput = rootTestOutput.resolve("srd-templates"); if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { - Path target = testOutput.resolve("srd-templates"); - TestUtils.deleteDir(target); + TestUtils.deleteDir(testOutput); // SRD only, just templates - LaunchResult result = launcher.launch( + LaunchResult result = launcher.launch("--log", "--index", "--background", TestUtils.TEST_RESOURCES.resolve("other/background.txt").toString(), "--class", TestUtils.TEST_RESOURCES.resolve("other/class.txt").toString(), "--deity", TestUtils.TEST_RESOURCES.resolve("other/deity.txt").toString(), @@ -108,7 +142,7 @@ void testCommandTemplates_5e(QuarkusMainLauncher launcher) { "--race", TestUtils.TEST_RESOURCES.resolve("other/race.txt").toString(), "--spell", TestUtils.TEST_RESOURCES.resolve("other/spell.txt").toString(), "--subclass", TestUtils.TEST_RESOURCES.resolve("other/subclass.txt").toString(), - "-o", target.toString(), + "-o", testOutput.toString(), TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString(), TestUtils.PATH_5E_TOOLS_DATA.toString()); @@ -117,14 +151,14 @@ void testCommandTemplates_5e(QuarkusMainLauncher launcher) { .isEqualTo(0); List.of( - target.resolve("compendium/backgrounds"), - target.resolve("compendium/classes"), - target.resolve("compendium/deities"), - target.resolve("compendium/feats"), - target.resolve("compendium/items"), - target.resolve("compendium/races"), - target.resolve("compendium/spells"), - target.resolve("rules")) + testOutput.resolve("compendium/backgrounds"), + testOutput.resolve("compendium/classes"), + testOutput.resolve("compendium/deities"), + testOutput.resolve("compendium/feats"), + testOutput.resolve("compendium/items"), + testOutput.resolve("compendium/races"), + testOutput.resolve("compendium/spells"), + testOutput.resolve("rules")) .forEach(directory -> TestUtils.assertDirectoryContents(directory, tui, (p, content) -> { List errors = new ArrayList<>(); boolean index = false; @@ -155,14 +189,14 @@ void testCommandTemplates_5e(QuarkusMainLauncher launcher) { } @Test - void testCommandTemplates_5eJson(QuarkusMainLauncher launcher) { + void testCommandTemplates_5eJson(QuarkusMainLauncher launcher) throws IOException { + testOutput = rootTestOutput.resolve("json-templates"); if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { - Path target = testOutput.resolve("json-templates"); - TestUtils.deleteDir(target); + TestUtils.deleteDir(testOutput); - LaunchResult result = launcher.launch("--debug", "--index", + LaunchResult result = launcher.launch("--log", "--index", "-c", TestUtils.TEST_RESOURCES.resolve("5e/sources-templates.json").toString(), - "-o", target.toString(), + "-o", testOutput.toString(), TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString(), TestUtils.PATH_5E_TOOLS_DATA.toString()); @@ -171,19 +205,19 @@ void testCommandTemplates_5eJson(QuarkusMainLauncher launcher) { .isEqualTo(0); // test extra cp value attribute in yaml frontmatter - Path abacus = target.resolve("compendium/items/abacus.md"); + Path abacus = testOutput.resolve("compendium/items/abacus.md"); assertThat(abacus).exists(); assertThat(abacus).content().contains("cost: 200"); List.of( - target.resolve("compendium/backgrounds"), - target.resolve("compendium/classes"), - target.resolve("compendium/deities"), - target.resolve("compendium/feats"), - target.resolve("compendium/items"), - target.resolve("compendium/races"), - target.resolve("compendium/spells"), - target.resolve("rules")) + testOutput.resolve("compendium/backgrounds"), + testOutput.resolve("compendium/classes"), + testOutput.resolve("compendium/deities"), + testOutput.resolve("compendium/feats"), + testOutput.resolve("compendium/items"), + testOutput.resolve("compendium/races"), + testOutput.resolve("compendium/spells"), + testOutput.resolve("rules")) .forEach(directory -> TestUtils.assertDirectoryContents(directory, tui, (p, content) -> { List errors = new ArrayList<>(); boolean frontmatter = false; diff --git a/src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java b/src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java index 26b5ca0a..106f83a2 100644 --- a/src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java +++ b/src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java @@ -293,7 +293,7 @@ void testLiveData_5eUA(QuarkusMainLauncher launcher) { } @Test - void testCommand_5eBookAdventureInJson(QuarkusMainLauncher launcher) { + void testLiveData_5eBookAdventureInJson(QuarkusMainLauncher launcher) { testOutput = rootTestOutput.resolve("json-book-adventure"); if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { TestUtils.deleteDir(testOutput); @@ -340,7 +340,7 @@ void testCommand_5eBookAdventureInJson(QuarkusMainLauncher launcher) { } @Test - void testCommand_5eBookAdventureMinimalYaml(QuarkusMainLauncher launcher) { + void testLiveData_5eBookAdventureMinimalYaml(QuarkusMainLauncher launcher) { testOutput = rootTestOutput.resolve("yaml-adventure"); if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { TestUtils.deleteDir(testOutput); @@ -365,4 +365,23 @@ void testCommand_5eBookAdventureMinimalYaml(QuarkusMainLauncher launcher) { }); } } + + @Test + void testLiveData_Sample(QuarkusMainLauncher launcher) { + testOutput = rootTestOutput.resolve("sample"); + if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { + + TestUtils.deleteDir(testOutput); + + Tui.instance().infof("--- Sample content ----- "); + + LaunchResult result = launcher.launch("--log", "--index", + "-o", testOutput.toString(), + "-c", TestUtils.TEST_RESOURCES.resolve("5e/sample.yaml").toString(), + TestUtils.PATH_5E_TOOLS_DATA.toString()); + assertThat(result.exitCode()) + .withFailMessage("Command failed. Output:%n%s", TestUtils.dump(result)) + .isEqualTo(0); + } + } } diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java index 9cdbdc7e..59fe8c6b 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java @@ -40,8 +40,8 @@ public void cleanup() throws Exception { public void testKeyIndex() throws Exception { commonTests.testKeyIndex(outputPath); - // All sources, but reprints will be followed. - // PHB elements should be missing/replaced by XPHB equivalents (e.g.) + // All sources, but things that have been reprinted will be replaced by the newest version + // e.g. PHB elements should be missing/replaced by XPHB equivalents if (commonTests.dataPresent) { commonTests.assert_MISSING("action|attack|phb"); commonTests.assert_Present("action|attack|xphb"); @@ -51,6 +51,7 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("background|sage|phb"); commonTests.assert_Present("background|sage|xphb"); commonTests.assert_Present("background|baldur's gate acolyte|bgdia"); + commonTests.assert_Present("classtype|artificer|tce"); commonTests.assert_MISSING("classtype|bard|phb"); commonTests.assert_Present("classtype|bard|xphb"); commonTests.assert_MISSING("condition|blinded|phb"); @@ -197,7 +198,7 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("subrace|genasi (air)|genasi|mpmm|mpmm"); commonTests.assert_MISSING("subrace|human|human|phb|phb"); commonTests.assert_Present("subrace|luma (sable)|luma|hwcs|hwcs"); - commonTests.assert_Present("subrace|tiefling (zariel)|tiefling|phb|mtf"); + commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); commonTests.assert_MISSING("subrace|tiefling|tiefling|phb|phb"); commonTests.assert_Present("subrace|vampire (ixalan)|vampire|psz|psx"); commonTests.assert_MISSING("trap|collapsing roof|dmg"); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java index 0c4e9cc1..f4614a76 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java @@ -56,6 +56,7 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("background|sage|phb"); commonTests.assert_Present("background|sage|xphb"); commonTests.assert_Present("background|baldur's gate acolyte|bgdia"); + commonTests.assert_Present("classtype|artificer|tce"); commonTests.assert_Present("classtype|bard|phb"); commonTests.assert_Present("classtype|bard|xphb"); commonTests.assert_Present("condition|blinded|phb"); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java index e9e7b376..88da1889 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java @@ -49,6 +49,7 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("background|sage|phb"); commonTests.assert_Present("background|sage|xphb"); commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); + commonTests.assert_MISSING("classtype|artificer|tce"); commonTests.assert_Present("classtype|bard|phb"); commonTests.assert_Present("classtype|bard|xphb"); commonTests.assert_MISSING("condition|blinded|phb"); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java index c6db87ec..c13a72d8 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java @@ -49,6 +49,7 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("background|sage|phb"); commonTests.assert_Present("background|sage|xphb"); commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); + commonTests.assert_MISSING("classtype|artificer|tce"); commonTests.assert_MISSING("classtype|bard|phb"); commonTests.assert_Present("classtype|bard|xphb"); commonTests.assert_MISSING("condition|blinded|phb"); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java index 1a75c0e4..ec8fe990 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java @@ -49,6 +49,7 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("background|sage|phb"); commonTests.assert_MISSING("background|sage|xphb"); commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); + commonTests.assert_MISSING("classtype|artificer|tce"); commonTests.assert_Present("classtype|bard|phb"); commonTests.assert_MISSING("classtype|bard|xphb"); commonTests.assert_Present("condition|blinded|phb"); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java index 44706f0c..e7f763f5 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java @@ -49,6 +49,7 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("background|sage|phb"); commonTests.assert_Present("background|sage|xphb"); commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); + commonTests.assert_MISSING("classtype|artificer|tce"); commonTests.assert_MISSING("classtype|bard|phb"); commonTests.assert_Present("classtype|bard|xphb"); commonTests.assert_MISSING("condition|blinded|phb"); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java index 68fd4f0c..440a86aa 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java @@ -49,6 +49,7 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("background|sage|phb"); commonTests.assert_MISSING("background|sage|xphb"); commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); + commonTests.assert_Present("classtype|artificer|tce"); commonTests.assert_Present("classtype|bard|phb"); commonTests.assert_MISSING("classtype|bard|xphb"); commonTests.assert_Present("condition|blinded|phb"); diff --git a/src/test/resources/5e/sample.yaml b/src/test/resources/5e/sample.yaml new file mode 100644 index 00000000..cb9b5165 --- /dev/null +++ b/src/test/resources/5e/sample.yaml @@ -0,0 +1,93 @@ +sources: + adventure: + - CM + - DC + - DIP + - FS + - GoS + - IDRotF + - LMoP + - LOX + - OoW + - PotA + - SDW + - SLW + - TftYP-AtG + - TftYP-DiT + - TftYP-TFoF + - TftYP-THSoT + - TftYP-TSC + - TftYP-ToH + - TftYP-WPM + - WBtW + - WDH + - WDMM + book: + - AAG + - AI + - BAM + - DMG + - DoD + - EGW + - FTD + - MaBJoV + - MM + - MPMM + - MTF + - PHB + - SCAG + - SCC + - TCE + - TDCSR + - VGM + - XGE + reference: + - AWM + - EEPC + - ESK + - TftYP + - SaF + homebrew: + - sources/5e-homebrew/collection/MCDM Productions; Strongholds and Followers.json + +paths: + compendium: /compendium/5e/ + rules: /compendium/5e/rules/ + +include: + - race|genasi|eepc + - racefluff|genasi|eepc + - subrace|air|genasi|eepc + - subrace|earth|genasi|eepc + - subrace|fire|genasi|eepc + - subrace|water|genasi|eepc + +excludePattern: + - race\|.*\|dmg + +exclude: + - monster|expert|dc + - monster|expert|sdw + - monster|expert|slw + +template: + background: examples/templates/tools5e/mixed/mixed-background2md.txt + class: examples/templates/tools5e/mixed/mixed-class2md.txt + deity: examples/templates/tools5e/mixed/mixed-deity2md.txt + feat: examples/templates/tools5e/mixed/mixed-feat2md.txt + hazard: examples/templates/tools5e/mixed/mixed-hazard2md.txt + item: examples/templates/tools5e/mixed/mixed-item2md.txt + monster: examples/templates/tools5e/mixed/mixed-monster2md.txt + object: examples/templates/tools5e/mixed/mixed-object2md.txt + race: examples/templates/tools5e/mixed/mixed-race2md.txt + reward: examples/templates/tools5e/mixed/mixed-reward2md.txt + spell: examples/templates/tools5e/mixed/mixed-spell2md.txt + subclass: examples/templates/tools5e/mixed/mixed-subclass2md.txt + vehicle: examples/templates/tools5e/mixed/mixed-vehicle2md.txt + +images: + copyInternal: true + internalRoot: sources/5etools-img + +useDiceRoller: true +yamlStatblocks: false diff --git a/src/test/resources/5e/sources-book-adventure.json b/src/test/resources/5e/sources-book-adventure.json index 2da661e0..561706b0 100644 --- a/src/test/resources/5e/sources-book-adventure.json +++ b/src/test/resources/5e/sources-book-adventure.json @@ -6,7 +6,8 @@ "convert": { "adventure": [ "WBtW", - "MOT-NSS" + "MOT-NSS", + "DIP" ], "book": [ "PHB", diff --git a/src/test/resources/5e/sources-images.yaml b/src/test/resources/5e/sources-images.yaml index 3dc9c700..8e193221 100644 --- a/src/test/resources/5e/sources-images.yaml +++ b/src/test/resources/5e/sources-images.yaml @@ -34,10 +34,12 @@ include: - classtype|wizard|xphb template: background: "examples/templates/tools5e/images-background2md.txt" + class: "examples/templates/tools5e/images-class2md.txt" item: "examples/templates/tools5e/images-item2md.txt" monster: "examples/templates/tools5e/images-monster2md.txt" object: "examples/templates/tools5e/images-object2md.txt" race: "examples/templates/tools5e/images-race2md.txt" spell: "examples/templates/tools5e/images-spell2md.txt" + subclass: "examples/templates/tools5e/images-subclass2md.txt" vehicle: "examples/templates/tools5e/images-vehicle2md.txt" useDiceRoller: true