diff --git a/docs/templates/dnd5e/QuteSpell.md b/docs/templates/dnd5e/QuteSpell.md index ac95d9271..d801e31df 100644 --- a/docs/templates/dnd5e/QuteSpell.md +++ b/docs/templates/dnd5e/QuteSpell.md @@ -6,7 +6,19 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[backgrounds](#backgrounds), [books](#books), [classList](#classlist), [classes](#classes), [components](#components), [duration](#duration), [feats](#feats), [fluffImages](#fluffimages), [getAliases](#getaliases), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [optionalfeatures](#optionalfeatures), [races](#races), [range](#range), [references](#references), [reprintOf](#reprintof), [ritual](#ritual), [school](#school), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [time](#time), [vaultPath](#vaultpath) +[abilityChecks](#abilitychecks), [affectsCreatureTypes](#affectscreaturetypes), [areaTags](#areatags), [backgrounds](#backgrounds), [books](#books), [classList](#classlist), [classes](#classes), [components](#components), [conditionImmune](#conditionimmune), [conditionInflict](#conditioninflict), [damageImmune](#damageimmune), [damageInflict](#damageinflict), [damageResist](#damageresist), [damageVulnerable](#damagevulnerable), [duration](#duration), [feats](#feats), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [higherLevels](#higherlevels), [labeledSource](#labeledsource), [level](#level), [miscTags](#misctags), [name](#name), [optionalfeatures](#optionalfeatures), [races](#races), [range](#range), [references](#references), [reprintOf](#reprintof), [ritual](#ritual), [savingThrows](#savingthrows), [scalingLevelDice](#scalingleveldice), [school](#school), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [spellAttacks](#spellattacks), [tags](#tags), [text](#text), [time](#time), [vaultPath](#vaultpath) + +### abilityChecks + +Formatted: Ability checks + +### affectsCreatureTypes + +Formatted: Creature types + +### areaTags + +Formatted/mapped: Areas ### backgrounds @@ -28,6 +40,30 @@ String: rendered list of links to classes that can use this spell. May be incomp Formatted: spell components +### conditionImmune + +Formatted: Condition immunities + +### conditionInflict + +Formatted: Conditions + +### damageImmune + +Formatted: Damage immunities + +### damageInflict + +Formatted: Damage types + +### damageResist + +Formatted: Damage resistances + +### damageVulnerable + +Formatted: Damage vulnerabilities + ### duration Formatted: spell range @@ -68,6 +104,10 @@ Return true if more than one image is present True if the content (text) contains sections +### higherLevels + +At higher levels text + ### labeledSource Formatted string describing the content's source(s): `_Source: _` @@ -76,6 +116,10 @@ Formatted string describing the content's source(s): `_Source: _` Spell level +### miscTags + +Formatted/mapped: Misc tags + ### name Note name @@ -104,6 +148,14 @@ List of content superceded by this note (as [Reprinted](../Reprinted.md)) true for ritual spells +### savingThrows + +Formatted: Saving throws + +### scalingLevelDice + +Formatted: Scaling damage dice entries + ### school Spell school @@ -139,6 +191,10 @@ Calling this method will return an italicised string with the primary source followed by a footnote listing all other sources. Useful for types that tend to have many sources. +### spellAttacks + +Formatted: Spell attack forms + ### tags Collected tags for inclusion in frontmatter 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 102e2d564..b0c77c67e 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpell.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpell.java @@ -1,13 +1,16 @@ package dev.ebullient.convert.tools.dnd5e; import static dev.ebullient.convert.StringUtil.pluralize; +import static dev.ebullient.convert.StringUtil.toOrdinal; import static dev.ebullient.convert.StringUtil.uppercaseFirst; import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Set; import java.util.TreeSet; @@ -22,6 +25,42 @@ public class Json2QuteSpell extends Json2QuteCommon { final String decoratedName; + private static final Map AREA_TAG_LABELS = Map.ofEntries( + Map.entry("C", "Cube"), + Map.entry("H", "Hemisphere"), + Map.entry("L", "Line"), + Map.entry("MT", "Multiple Targets"), + Map.entry("N", "Cone"), + Map.entry("Q", "Square"), + Map.entry("R", "Circle"), + Map.entry("S", "Sphere"), + Map.entry("ST", "Single Target"), + Map.entry("W", "Wall"), + Map.entry("Y", "Cylinder")); + + private static final Map MISC_TAG_LABELS = Map.ofEntries( + Map.entry("AAD", "Additional Attack Damage"), + Map.entry("ADV", "Grants Advantage"), + Map.entry("DFT", "Difficult Terrain"), + Map.entry("FMV", "Forced Movement"), + Map.entry("HL", "Healing"), + Map.entry("LGT", "Creates Light"), + Map.entry("LGTS", "Creates Sunlight"), + Map.entry("MAC", "Modifies AC"), + Map.entry("OBJ", "Affects Objects"), + Map.entry("OBS", "Obscures Vision"), + Map.entry("PRM", "Permanent Effects"), + Map.entry("PIR", "Permanent If Repeated"), + Map.entry("PS", "Plane Shifting"), + Map.entry("RO", "Rollable Effects"), + Map.entry("SCL", "Scaling Effects"), + Map.entry("SCT", "Scaling Targets"), + Map.entry("SGT", "Requires Sight"), + Map.entry("SMN", "Summons Creature"), + Map.entry("THP", "Grants Temporary Hit Points"), + Map.entry("TP", "Teleportation"), + Map.entry("UBA", "Uses Bonus Action")); + Json2QuteSpell(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) { super(index, type, jsonNode); decoratedName = linkifier().decoratedName(type, jsonNode); @@ -59,7 +98,6 @@ protected Tools5eQuteBase buildQuteResource() { appendToText(text, SpellFields.entriesHigherLevel.getFrom(rootNode), textContains(text, "## ") ? "##" : null); } - return new QuteSpell(sources, decoratedName, getSourceText(sources), @@ -70,6 +108,20 @@ protected Tools5eQuteBase buildQuteResource() { spellRange(), spellComponents(), spellDuration(), + spellAbilityChecks(), + spellAffectsCreatureTypes(), + spellAreaTags(), + spellConditionImmune(), + spellConditionInflict(), + spellDamageImmune(), + spellDamageInflict(), + spellDamageResist(), + spellDamageVulnerable(), + spellMiscTags(), + spellSavingThrows(), + spellScalingLevelDice(), + spellAttacks(), + spellHigherLevelEntries(), referenceLinks, getFluffImages(Tools5eIndexType.spellFluff), String.join("\n", text), @@ -214,30 +266,241 @@ String spellCastingTime() { return pluralize(result.toString(), Integer.valueOf(number)); } + String spellAreaTags() { + return joinList(SpellFields.areaTags.getListOfStrings(rootNode, tui()).stream() + .map(tag -> AREA_TAG_LABELS.getOrDefault(tag, uppercaseFirst(tag))) + .filter(Objects::nonNull) + .distinct() + .toList()); + } + + String spellDamageInflict() { + return joinList(SpellFields.damageInflict.getListOfStrings(rootNode, tui()).stream() + .map(this::formatKeyword) + .filter(Objects::nonNull) + .distinct() + .toList()); + } + + String spellSavingThrows() { + return joinList(SpellFields.savingThrow.getListOfStrings(rootNode, tui()).stream() + .map(this::formatKeyword) + .filter(Objects::nonNull) + .distinct() + .toList()); + } + + String spellConditionInflict() { + return joinList(SpellFields.conditionInflict.getListOfStrings(rootNode, tui()).stream() + .map(this::formatKeyword) + .filter(Objects::nonNull) + .distinct() + .toList()); + } + + String spellAbilityChecks() { + return joinList(SpellFields.abilityCheck.getListOfStrings(rootNode, tui()).stream() + .map(this::formatAbility) + .filter(Objects::nonNull) + .distinct() + .toList()); + } + + String spellMiscTags() { + return joinList(SpellFields.miscTags.getListOfStrings(rootNode, tui()).stream() + .map(Json2QuteSpell::formatMiscTag) + .filter(Objects::nonNull) + .distinct() + .toList()); + } + + String spellAffectsCreatureTypes() { + return joinList(SpellFields.affectsCreatureType.getListOfStrings(rootNode, tui()).stream() + .map(this::formatKeyword) + .filter(Objects::nonNull) + .distinct() + .toList()); + } + + String spellConditionImmune() { + return joinList(SpellFields.conditionImmune.getListOfStrings(rootNode, tui()).stream() + .map(this::formatKeyword) + .filter(Objects::nonNull) + .distinct() + .toList()); + } + + String spellDamageImmune() { + return joinList(SpellFields.damageImmune.getListOfStrings(rootNode, tui()).stream() + .map(this::formatKeyword) + .filter(Objects::nonNull) + .distinct() + .toList()); + } + + String spellDamageResist() { + return joinList(SpellFields.damageResist.getListOfStrings(rootNode, tui()).stream() + .map(this::formatKeyword) + .filter(Objects::nonNull) + .distinct() + .toList()); + } + + String spellDamageVulnerable() { + return joinList(SpellFields.damageVulnerable.getListOfStrings(rootNode, tui()).stream() + .map(this::formatKeyword) + .filter(Objects::nonNull) + .distinct() + .toList()); + } + + String spellAttacks() { + return joinList(SpellFields.spellAttack.getListOfStrings(rootNode, tui()).stream() + .map(Json2QuteSpell::formatSpellAttack) + .filter(Objects::nonNull) + .distinct() + .toList()); + } + + String spellScalingLevelDice() { + JsonNode node = SpellFields.scalingLevelDice.getFrom(rootNode); + if (node == null || node.isNull()) { + return null; + } + + List blocks = new ArrayList<>(); + if (node.isArray()) { + for (JsonNode n : node) { + blocks.add(n); + } + } else if (node.isObject()) { + blocks.add(node); + } else { + return null; + } + + List result = new ArrayList<>(); + for (JsonNode scalingNode : blocks) { + JsonNode scaling = SpellFields.scaling.getFrom(scalingNode); + if (scaling == null || scaling.isEmpty()) { + continue; + } + + List> entries = new ArrayList<>(); + for (Entry e : iterableFields(scaling)) { + entries.add(e); + } + entries.sort(Comparator.comparingInt(e -> Integer.parseInt(e.getKey()))); + + List pieces = new ArrayList<>(); + for (Entry entry : entries) { + String level = entry.getKey(); + String dice = entry.getValue().asText(); + if (dice == null || dice.isBlank()) { + continue; + } + pieces.add("%s level: %s".formatted(toOrdinal(level), dice)); + } + + if (pieces.isEmpty()) { + continue; + } + + String label = SpellFields.label.getTextOrEmpty(scalingNode); + if (label == null || label.isBlank()) { + result.add(String.join("; ", pieces)); + } else { + result.add("%s: %s".formatted(uppercaseFirst(label.trim()), String.join("; ", pieces))); + } + } + + return result.isEmpty() ? null : String.join(", ", result); + } + + String spellHigherLevelEntries() { + if (!SpellFields.entriesHigherLevel.existsIn(rootNode)) { + return null; + } + String value = SpellFields.entriesHigherLevel.transformTextFrom(rootNode, "\n", this, null).strip(); + return value.isBlank() ? null : value; + } + + private String formatKeyword(String value) { + if (value == null || value.isBlank()) { + return null; + } + return uppercaseFirst(value.trim()); + } + + private String formatAbility(String ability) { + if (ability == null || ability.isBlank()) { + return null; + } + return uppercaseFirst(ability.trim()); + } + + private static String formatMiscTag(String tag) { + if (tag == null || tag.isBlank()) { + return null; + } + return MISC_TAG_LABELS.getOrDefault(tag, uppercaseFirst(tag.trim())); + } + + private static String formatSpellAttack(String code) { + if (code == null || code.isBlank()) { + return null; + } + return switch (code.trim()) { + case "M" -> "Melee Spell Attack"; + case "R" -> "Ranged Spell Attack"; + default -> null; + }; + } + enum SpellFields implements JsonNodeReader { + abilityCheck, + affectsCreatureType, amount, + areaTags, className, classSource, classes, components, + conditionImmune, + conditionInflict, + damageImmune, + damageInflict, + damageResist, + damageVulnerable, distance, duration, entriesHigherLevel, + label, level, meta, + miscTags, number, range, ritual, + savingThrow, + scaling, + scalingLevelDice, school, self, sight, special, + spellAttack, text, touch, type, unit, unlimited, - definedInSource, - spellAttack, + } + + private static String joinList(List values) { + if (values == null || values.isEmpty()) { + return null; + } + return String.join(", ", values); } } 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 304974cb9..2dba28c7d 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 @@ -31,6 +31,34 @@ public class QuteSpell extends Tools5eQuteBase { public final String components; /** Formatted: spell range */ public final String duration; + /** Formatted: Ability checks */ + public final String abilityChecks; + /** Formatted: Creature types */ + public final String affectsCreatureTypes; + /** Formatted/mapped: Areas */ + public final String areaTags; + /** Formatted: Condition immunities */ + public final String conditionImmune; + /** Formatted: Conditions */ + public final String conditionInflict; + /** Formatted: Damage immunities */ + public final String damageImmune; + /** Formatted: Damage types */ + public final String damageInflict; + /** Formatted: Damage resistances */ + public final String damageResist; + /** Formatted: Damage vulnerabilities */ + public final String damageVulnerable; + /** Formatted/mapped: Misc tags */ + public final String miscTags; + /** Formatted: Saving throws */ + public final String savingThrows; + /** Formatted: Scaling damage dice entries */ + public final String scalingLevelDice; + /** Formatted: Spell attack forms */ + public final String spellAttacks; + /** At higher levels text */ + public final String higherLevels; /** String: rendered list of links to classes that grant access to this spell. May be incomplete or empty. */ public final String backgrounds; /** String: rendered list of links to classes that can use this spell. May be incomplete or empty. */ @@ -47,7 +75,14 @@ public class QuteSpell extends Tools5eQuteBase { public QuteSpell(Tools5eSources sources, String name, String source, String level, String school, boolean ritual, String time, String range, String components, String duration, - Collection references, List images, String text, Tags tags) { + String abilityChecks, String affectsCreatureTypes, + String areaTags, String conditionImmune, + String conditionInflict, String damageImmune, + String damageInflict, String damageResist, + String damageVulnerable, String miscTags, + String savingThrows, String scalingLevelDice, + String spellAttacks, + String higherLevels, Collection references, List images, String text, Tags tags) { super(sources, name, source, images, text, tags); this.level = level; @@ -57,6 +92,20 @@ public QuteSpell(Tools5eSources sources, String name, String source, String leve this.range = range; this.components = components; this.duration = duration; + this.abilityChecks = abilityChecks; + this.affectsCreatureTypes = affectsCreatureTypes; + this.areaTags = areaTags; + this.conditionImmune = conditionImmune; + this.conditionInflict = conditionInflict; + this.damageImmune = damageImmune; + this.damageInflict = damageInflict; + this.damageResist = damageResist; + this.damageVulnerable = damageVulnerable; + this.miscTags = miscTags; + this.savingThrows = savingThrows; + this.scalingLevelDice = scalingLevelDice; + this.spellAttacks = spellAttacks; + this.higherLevels = higherLevels == null || higherLevels.isBlank() ? null : higherLevels; this.references = references; this.backgrounds = references.stream() .filter(s -> s.contains("background"))