From d2696de43eb1528a8e66841513baf6752971ee05 Mon Sep 17 00:00:00 2001 From: Kelly Stewart Date: Mon, 29 Apr 2024 23:13:48 +1000 Subject: [PATCH] Add bestiary support (#423) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ 🐉 Pf2e Bestiary * Add ability to export creatures with the CLI tool - just the name and level for now * Add defenses export to Pf2e creature support * Add support for perception to pf2e creature * Put AC note text in "notes" through tokenization Previously only note text in the "note" key was tokenized. This makes tests fail in creature stat blocks, which use e.g. ac: "notes": "{@spell mage armor}" * Add a creature2md template file * Add tests for the creature type * Add `std` as an enum type rather than using the string directly. * Add support for parsing languages for creatures. Added a `CreatureLanguages` type as an intermediate object to hold the language data. Not sure whether this is the desired style for these helper classes, or whether I should be prefacing the names with `Qute` and putting them in e.g. `QuteCreature` instead. * Move CreatureLanguages into QuteCreature, and fix up generated documentation. Add support for `MarkdownDoclet` to correctly format and parse record components. This was actually a pain to figure out, as it turns out that these aren't accessible as an Element and instead have to be parsed through the @param tags of the class comment. * Add support for skills to the creature importer * Collapse spaces in docstrings Prevents docstring spacing from rendering weird in markdown (such as indentation in @param tags) * Add a ^statblock block tag to the default creature template Also fix some import order formatting that I accidentally changed earlier. * Add support for parsing languages for creatures. Add support for `MarkdownDoclet` to correctly format and parse record components. This was actually a pain to figure out, as it turns out that these aren't accessible as an Element and instead have to be parsed through the @param tags of the class comment. * Use admonition block in the creature2md template --- docs/templates/pf2e/QuteCreature.md | 68 ++++++++ docs/templates/pf2e/README.md | 1 + .../ebullient/convert/io/MarkdownDoclet.java | 36 ++-- .../convert/tools/pf2e/Json2QuteBase.java | 10 ++ .../convert/tools/pf2e/Json2QuteCreature.java | 159 ++++++++++++++++++ .../convert/tools/pf2e/Json2QuteDeity.java | 6 - .../convert/tools/pf2e/Pf2eIndex.java | 1 + .../convert/tools/pf2e/Pf2eIndexType.java | 2 + .../convert/tools/pf2e/Pf2eTypeReader.java | 60 ++++++- .../convert/tools/pf2e/qute/QuteCreature.java | 116 +++++++++++++ .../tools/pf2e/qute/QuteDataArmorClass.java | 19 ++- .../tools/pf2e/qute/QuteDataSkillBonus.java | 48 ++++++ src/main/resources/convertData.json | 3 +- .../templates/toolsPf2e/creature2md.txt | 42 +++++ .../tools/pf2e/Pf2eJsonDataNoneTest.java | 5 + .../tools/pf2e/Pf2eJsonDataSubsetTest.java | 5 + .../convert/tools/pf2e/Pf2eJsonDataTest.java | 5 + 17 files changed, 557 insertions(+), 29 deletions(-) create mode 100644 docs/templates/pf2e/QuteCreature.md create mode 100644 src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCreature.java create mode 100644 src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteCreature.java create mode 100644 src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataSkillBonus.java create mode 100644 src/main/resources/templates/toolsPf2e/creature2md.txt diff --git a/docs/templates/pf2e/QuteCreature.md b/docs/templates/pf2e/QuteCreature.md new file mode 100644 index 00000000..cb749ce6 --- /dev/null +++ b/docs/templates/pf2e/QuteCreature.md @@ -0,0 +1,68 @@ +# QuteCreature + +Pf2eTools Creature attributes (`creature2md.txt`) + +Use `%%--` to mark the end of the preamble (frontmatter and other leading content only appropriate to the standalone case). + +Extension of [Pf2eQuteBase](Pf2eQuteBase.md) + +## Attributes + +[aliases](#aliases), [defenses](#defenses), [description](#description), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [perception](#perception), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [traits](#traits), [vaultPath](#vaultpath) + + +### aliases + +Aliases for this note (optional) + +### defenses + +Defenses (AC, saves, etc) as [QuteDataDefenses](QuteDataDefenses.md) + +### description + +Short creature description (optional) + +### hasSections + +True if the content (text) contains sections + +### labeledSource + +Formatted string describing the content's source(s): `_Source: _` + +### level + +Creature level (number, optional) + +### name + +Note name + +### perception + +Creature perception (number, optional) + +### source + +String describing the content's source(s) + +### sourceAndPage + +Book sources as list of [SourceAndPage](../SourceAndPage.md) + +### tags + +Collected tags for inclusion in frontmatter + +### text + +Formatted text. For most templates, this is the bulk of the content. + +### traits + +Collection of traits (decorated links, optional) + +### vaultPath + +Path to this note in the vault diff --git a/docs/templates/pf2e/README.md b/docs/templates/pf2e/README.md index f03b4e3e..36a6dcf5 100644 --- a/docs/templates/pf2e/README.md +++ b/docs/templates/pf2e/README.md @@ -11,6 +11,7 @@ - [QuteArchetype](QuteArchetype.md): Pf2eTools Archetype attributes (`archetype2md.txt`) - [QuteBackground](QuteBackground.md): Pf2eTools Background attributes (`background2md.txt`) - [QuteBook](QuteBook/README.md): Pf2eTools Book attributes (`book2md.txt`) +- [QuteCreature](QuteCreature.md): Pf2eTools Creature attributes (`creature2md.txt`) - [QuteDataActivity](QuteDataActivity.md): Pf2eTools activity attributes - [QuteDataArmorClass](QuteDataArmorClass.md): Pf2eTools armor class attributes - [QuteDataDefenses](QuteDataDefenses/README.md): Pf2eTools Armor class, Saving Throws, and other attributes describing defenses diff --git a/src/main/java/dev/ebullient/convert/io/MarkdownDoclet.java b/src/main/java/dev/ebullient/convert/io/MarkdownDoclet.java index 7103d9ad..80b0db69 100644 --- a/src/main/java/dev/ebullient/convert/io/MarkdownDoclet.java +++ b/src/main/java/dev/ebullient/convert/io/MarkdownDoclet.java @@ -17,13 +17,7 @@ import java.util.stream.Collectors; import javax.lang.model.SourceVersion; -import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.Modifier; -import javax.lang.model.element.NestingKind; -import javax.lang.model.element.PackageElement; -import javax.lang.model.element.QualifiedNameable; -import javax.lang.model.element.TypeElement; +import javax.lang.model.element.*; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; @@ -36,6 +30,7 @@ import com.sun.source.doctree.DocTree; import com.sun.source.doctree.LinkTree; import com.sun.source.doctree.LiteralTree; +import com.sun.source.doctree.ParamTree; import com.sun.source.doctree.TextTree; import com.sun.source.util.DocTrees; @@ -196,11 +191,24 @@ protected void writeReferenceFile(DocTrees docTrees, TypeElement t) throws IOExc .collect(Collectors.joining(", "))); aggregator.add("\n\n"); - for (Map.Entry entry : members.entrySet()) { - aggregator.add("\n\n### " + entry.getKey() + "\n\n"); - aggregator.addFullBody(docTrees.getDocCommentTree(entry.getValue())); + if (t.getKind() == ElementKind.RECORD) { + // If it's a record, then we can't retrieve the attributes as Elements, so we have to parse them from + // the comment tree instead. + docTrees.getDocCommentTree(t) + .getBlockTags().stream() + .filter(e -> e.getKind() == DocTree.Kind.PARAM) + .map(param -> (ParamTree) param) + .forEach(param -> { + aggregator.add("\n\n### " + param.getName() + "\n\n"); + aggregator.addAll(param.getDescription()); + }); + } else { + for (Map.Entry entry : members.entrySet()) { + aggregator.add("\n\n### " + entry.getKey() + "\n\n"); + aggregator.addFullBody(docTrees.getDocCommentTree(entry.getValue())); + } } - out.println(aggregator.toString()); + out.println(aggregator); } } @@ -218,11 +226,11 @@ protected void processElement(DocTrees docTrees, Map members, E if (e.getAnnotation(Deprecated.class) != null) { return; } - } else if (!kind.isField()) { + } else if (!kind.isField() && kind != ElementKind.RECORD_COMPONENT) { return; } - if (e.getKind() == ElementKind.METHOD) { + if (kind == ElementKind.METHOD) { name = name.replaceFirst("(get|is)", ""); name = name.substring(0, 1).toLowerCase() + name.substring(1); } @@ -409,7 +417,7 @@ void add(DocTree docTree) { void add(String text) { if (htmlEntity.isEmpty()) { - content.add(text); + content.add(text.replaceAll(" +", " ")); } else { htmlEntity.peek().add(text); } diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBase.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBase.java index c3bf1505..de234365 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBase.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBase.java @@ -1,7 +1,11 @@ package dev.ebullient.convert.tools.pf2e; +import java.util.List; +import java.util.stream.Collectors; + import com.fasterxml.jackson.databind.JsonNode; +import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase; import dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteNote; @@ -32,6 +36,12 @@ public Pf2eSources getSources() { return sources; } + List toAlignments(JsonNode alignNode, JsonNodeReader alignmentField) { + return alignmentField.getListOfStrings(alignNode, tui()).stream() + .map(a -> a.length() > 2 ? a : linkify(Pf2eIndexType.trait, a.toUpperCase())) + .collect(Collectors.toList()); + } + public Pf2eQuteBase build() { boolean pushed = parseState().push(getSources(), rootNode); try { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCreature.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCreature.java new file mode 100644 index 00000000..532fb914 --- /dev/null +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCreature.java @@ -0,0 +1,159 @@ +package dev.ebullient.convert.tools.pf2e; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import com.fasterxml.jackson.databind.JsonNode; + +import dev.ebullient.convert.tools.JsonNodeReader; +import dev.ebullient.convert.tools.Tags; +import dev.ebullient.convert.tools.pf2e.qute.QuteCreature; +import dev.ebullient.convert.tools.pf2e.qute.QuteDataDefenses; + +public class Json2QuteCreature extends Json2QuteBase { + + public Json2QuteCreature(Pf2eIndex index, JsonNode rootNode) { + super(index, Pf2eIndexType.creature, rootNode); + } + + @Override + protected QuteCreature buildQuteResource() { + List text = new ArrayList<>(); + Tags tags = new Tags(sources); + + appendToText(text, SourceField.entries.getFrom(rootNode), "##"); + + Collection traits = collectTraitsFrom(rootNode, tags); + if (Pf2eCreature.alignment.existsIn(rootNode)) { + traits.addAll(toAlignments(rootNode, Pf2eCreature.alignment)); + } + Optional level = Pf2eCreature.level.getIntFrom(rootNode); + + return new QuteCreature(sources, text, tags, + traits, + Field.alias.replaceTextFromList(rootNode, this), + Pf2eCreature.description.replaceTextFrom(rootNode, this), + level.orElse(null), + getPerception(), + buildDefenses(), + Pf2eCreatureLanguages.createCreatureLanguages(Pf2eCreature.languages.getFrom(rootNode), this), + buildSkills()); + } + + /** + * Example JSON input: + * + *
+     *     "perception": {
+     *         "std": 6
+     *     }
+     * 
+ */ + private Integer getPerception() { + JsonNode perceptionNode = Pf2eCreature.perception.getFrom(rootNode); + if (perceptionNode == null || !perceptionNode.isObject()) { + return null; + } + return Pf2eCreature.std.getIntOrThrow(perceptionNode); + } + + /** + * Example JSON input: + * + *
+     *     "defenses": { ... }
+     * 
+ */ + private QuteDataDefenses buildDefenses() { + JsonNode defenseNode = Pf2eCreature.defenses.getFrom(rootNode); + if (defenseNode == null || !defenseNode.isObject()) { + return null; + } + return Pf2eDefenses.createInlineDefenses(defenseNode, this); + } + + /** + * Example JSON input: + * + *
+     *     "skills": {
+     *         "athletics": 30,
+     *         "stealth": {
+     *             "std": 36,
+     *             "in forests": 42,
+     *             "note": "additional note"
+     *         },
+     *         "notes": [
+     *             "some note"
+     *         ]
+     *     }
+     * 
+ */ + private QuteCreature.CreatureSkills buildSkills() { + JsonNode skillsNode = Pf2eCreature.skills.getFrom(rootNode); + if (skillsNode == null || !skillsNode.isObject()) { + return null; + } + return new QuteCreature.CreatureSkills( + skillsNode.properties().stream() + .filter(e -> !e.getKey().equals(Pf2eCreature.notes.name())) + .map(e -> Pf2eTypeReader.Pf2eSkillBonus.createSkillBonus(e.getKey(), e.getValue(), this)) + .toList(), + Pf2eCreature.notes.replaceTextFromList(rootNode, this)); + } + + /** + * Example JSON input: + * + *
+     *     "languages": {
+     *         "languages": ["Common", "Sylvan"],
+     *         "abilities": ["{@ability telepathy} 100 feet"],
+     *         "notes": ["some other notes"],
+     *     }
+     * 
+ */ + enum Pf2eCreatureLanguages implements JsonNodeReader { + languages, + abilities, + notes; + + static QuteCreature.CreatureLanguages createCreatureLanguages(JsonNode node, Pf2eTypeReader convert) { + if (node == null) { + return null; + } + return new QuteCreature.CreatureLanguages( + languages.getListOfStrings(node, convert.tui()), + abilities.getListOfStrings(node, convert.tui()).stream().map(convert::replaceText).toList(), + notes.getListOfStrings(node, convert.tui()).stream().map(convert::replaceText).toList()); + } + } + + enum Pf2eCreature implements JsonNodeReader { + abilities, + abilityMods, + alignment, + attacks, + defenses, + description, + hasImages, + inflicts, + isNpc, + items, + languages, + level, + notes, + perception, + rarity, + rituals, + senses, + size, + skills, + speed, + spellcasting, + std, + traits, + } +} diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteDeity.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteDeity.java index b605ecb3..cf185039 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteDeity.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteDeity.java @@ -199,12 +199,6 @@ private String findRange(JsonNode actionNode) { return replaceText(rangeString); } - private List toAlignments(JsonNode alignNode, Pf2eDeity alignmentField) { - return alignmentField.getListOfStrings(alignNode, tui()).stream() - .map(a -> a.length() > 2 ? a : linkify(Pf2eIndexType.trait, a.toUpperCase())) - .collect(Collectors.toList()); - } - String commandmentToString(List edictOrAnathema) { if (edictOrAnathema.stream().anyMatch(x -> x.contains(","))) { return String.join("; ", edictOrAnathema); diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eIndex.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eIndex.java index 2485ab05..6c5c564f 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eIndex.java @@ -63,6 +63,7 @@ public Pf2eIndex importTree(String filename, JsonNode node) { Pf2eIndexType.action.withArrayFrom(node, this::addToIndex); Pf2eIndexType.archetype.withArrayFrom(node, this::addToIndex); Pf2eIndexType.background.withArrayFrom(node, this::addToIndex); + Pf2eIndexType.creature.withArrayFrom(node, this::addToIndex); Pf2eIndexType.curse.withArrayFrom(node, this::addToIndex); Pf2eIndexType.condition.withArrayFrom(node, this::addToIndex); Pf2eIndexType.deity.withArrayFrom(node, this::addToIndex); diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eIndexType.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eIndexType.java index fcbec97f..a25239be 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eIndexType.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eIndexType.java @@ -173,6 +173,8 @@ public Pf2eQuteBase convertJson2QuteBase(Pf2eIndex index, JsonNode node) { return new Json2QuteSpell(index, node).build(); case trait: return new Json2QuteTrait(index, node).build(); + case creature: + return new Json2QuteCreature(index, node).build(); default: return null; } diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eTypeReader.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eTypeReader.java index 47d1d58c..bb6ebda9 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eTypeReader.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eTypeReader.java @@ -1,6 +1,7 @@ package dev.ebullient.convert.tools.pf2e; import java.util.ArrayList; +import java.util.Arrays; import java.util.Comparator; import java.util.HashSet; import java.util.List; @@ -8,6 +9,7 @@ import java.util.Map.Entry; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; import com.fasterxml.jackson.databind.JsonNode; @@ -21,6 +23,7 @@ import dev.ebullient.convert.tools.pf2e.qute.QuteDataDefenses; import dev.ebullient.convert.tools.pf2e.qute.QuteDataDefenses.QuteSavingThrows; import dev.ebullient.convert.tools.pf2e.qute.QuteDataHpHardness; +import dev.ebullient.convert.tools.pf2e.qute.QuteDataSkillBonus; import dev.ebullient.convert.tools.pf2e.qute.QuteItem.QuteItemWeaponData; import io.quarkus.runtime.annotations.RegisterForReflection; @@ -246,16 +249,27 @@ public static QuteDataArmorClass getAcString(JsonNode source, Pf2eTypeReader con QuteDataArmorClass ac = new QuteDataArmorClass(); NamedText.SortedBuilder namedText = new NamedText.SortedBuilder(); for (Entry e : convert.iterableFields(acNode)) { - if (e.getKey().equals(note.name()) || e.getKey().equals(abilities.name())) { - continue; // skip these two + if (e.getKey().equals(note.name()) || + e.getKey().equals(abilities.name()) || + e.getKey().equals(notes.name())) { + continue; // skip these three } namedText.add( (e.getKey().equals("std") ? "AC" : e.getKey() + " AC"), "" + e.getValue()); } ac.armorClass = namedText.build(); - ac.note = convert.replaceText(note.getTextOrEmpty(acNode)); ac.abilities = convert.replaceText(abilities.getTextOrEmpty(acNode)); + + // Consolidate "note" and "notes" into different representations of the same data + List acNotes = notes.getListOfStrings(acNode, convert.tui()); + ac.notes = Stream.concat(acNotes.stream(), Stream.of(note.getTextOrEmpty(acNode))) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(convert::replaceText) + .toList(); + ac.note = String.join("; ", ac.notes); + return ac; } @@ -420,6 +434,46 @@ static Pf2eSpellComponent valueFromEncoding(String value) { } } + enum Pf2eSkillBonus implements JsonNodeReader { + std, + note; + + /** + * Example JSON object input: + * + *
+         * {
+         *     "std": 10,
+         *     "in woods": 12,
+         *     "note": "some note"
+         * }
+         * 
+ * + * @param skillName The name of the skill + * @param source Either a single integer bonus, or an object (see above example) + */ + public static QuteDataSkillBonus createSkillBonus( + String skillName, JsonNode source, Pf2eTypeReader convert) { + String displayName = Arrays.stream(skillName.split(" ")) + .map(s -> s.substring(0, 1).toUpperCase() + s.substring(1)) + .collect(Collectors.joining(" ")); + + if (source.isInt()) { + return new QuteDataSkillBonus(displayName, source.asInt()); + } + + return new QuteDataSkillBonus( + displayName, + std.getIntOrThrow(source), + source.properties().stream() + .filter(e -> !e.getKey().equals(std.name()) && !e.getKey().equals(note.name())) // skip these + .collect( + Collectors.toUnmodifiableMap( + e -> convert.replaceText(e.getKey()), e -> e.getValue().asInt())), + convert.replaceText(note.getTextOrNull(source))); + } + } + @RegisterForReflection class Speed { public Integer walk; diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteCreature.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteCreature.java new file mode 100644 index 00000000..dfe50310 --- /dev/null +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteCreature.java @@ -0,0 +1,116 @@ +package dev.ebullient.convert.tools.pf2e.qute; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import dev.ebullient.convert.qute.QuteUtil; +import dev.ebullient.convert.tools.Tags; +import dev.ebullient.convert.tools.pf2e.Pf2eSources; +import io.quarkus.qute.TemplateData; + +/** + * Pf2eTools Creature attributes ({@code creature2md.txt}) + *

+ * Use `%%--` to mark the end of the preamble (frontmatter and + * other leading content only appropriate to the standalone case). + *

+ *

+ * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase} + *

+ */ +@TemplateData +public class QuteCreature extends Pf2eQuteBase { + + /** Aliases for this note (optional) */ + public final List aliases; + /** Collection of traits (decorated links, optional) */ + public final Collection traits; + /** Short creature description (optional) */ + public final String description; + /** Creature level (number, optional) */ + public final Integer level; + /** Creature perception (number, optional) */ + public final Integer perception; + /** + * Languages as {@link dev.ebullient.convert.tools.pf2e.qute.QuteCreature.CreatureLanguages CreatureLanguages} + */ + public final CreatureLanguages languages; + /** Defenses (AC, saves, etc) as {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataDefenses QuteDataDefenses} */ + public final QuteDataDefenses defenses; + /** + * Skill bonuses as {@link dev.ebullient.convert.tools.pf2e.qute.QuteCreature.CreatureSkills CreatureSkills} + */ + public final CreatureSkills skills; + + public QuteCreature(Pf2eSources sources, List text, Tags tags, + Collection traits, List aliases, + String description, Integer level, Integer perception, + QuteDataDefenses defenses, CreatureLanguages languages, CreatureSkills skills) { + super(sources, text, tags); + this.traits = traits; + this.aliases = aliases; + this.description = description; + this.level = level; + this.perception = perception; + this.languages = languages; + this.defenses = defenses; + this.skills = skills; + } + + /** + * The languages and language features known by a creature. + * + *

+ * Referencing this object directly provides a default markup which includes all data. Example: + * {@code "Common, Sylvan; telepathy 100ft; knows any language the summoner does" } + *

+ * + * @param languages Languages known (optional) + * @param notes Language-related notes (optional) + * @param abilities Language-related abilities (optional) + */ + @TemplateData + public record CreatureLanguages( + List languages, + List notes, + List abilities) implements QuteUtil { + + @Override + public String toString() { + return Stream.of( + languages != null ? List.of(String.join(", ", languages)) : List. of(), + abilities, notes) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .dropWhile(String::isEmpty) + .collect(Collectors.joining("; ")); + } + } + + /** + * A creature's skill information. + * + *

+ * Referencing this object directly provides a default markup which includes all data. Example: + * {@code "Athletics +10, Cult Lore +10 (lore on their cult), Stealth +10 (+12 in forests); Some skill note" } + *

+ * + * @param skills Skill bonuses for the creature, as a list of + * {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataSkillBonus QuteDataSkillBonus} + * @param notes Notes for the creature's skills (list of strings, optional) + */ + @TemplateData + public record CreatureSkills( + List skills, + List notes) { + + @Override + public String toString() { + return skills.stream().map(QuteDataSkillBonus::toString).collect(Collectors.joining(", ")) + + (notes == null ? "" : " " + String.join("; ", notes)); + } + } +} diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataArmorClass.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataArmorClass.java index 40b32bed..e25e58c9 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataArmorClass.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataArmorClass.java @@ -1,24 +1,33 @@ package dev.ebullient.convert.tools.pf2e.qute; -import java.util.Collection; -import java.util.stream.Collectors; - import dev.ebullient.convert.qute.NamedText; import dev.ebullient.convert.qute.QuteUtil; import io.quarkus.qute.TemplateData; +import java.util.Collection; +import java.util.stream.Collectors; + /** * Pf2eTools armor class attributes */ @TemplateData public class QuteDataArmorClass implements QuteUtil { public Collection armorClass; - public String note; public String abilities; + /** Notes associated with the armor class, e.g. "with mage armor". */ + public Collection notes; + + /** + * Any notes associated with the armor class. This contains the same data as + * {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataArmorClass#notes notes}, but as a single + * semicolon-delimited string. + */ + public String note; + public String toString() { return armorClass.stream() - .map(e -> e.toString()) + .map(NamedText::toString) .collect(Collectors.joining("; ")) + (isPresent(note) ? " " + note.trim() : "") + (isPresent(abilities) ? "; " + abilities : ""); diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataSkillBonus.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataSkillBonus.java new file mode 100644 index 00000000..35efb4c2 --- /dev/null +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataSkillBonus.java @@ -0,0 +1,48 @@ +package dev.ebullient.convert.tools.pf2e.qute; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import dev.ebullient.convert.qute.QuteUtil; +import io.quarkus.qute.TemplateData; + +/** + * A Pathfinder 2e skill and associated bonuses. + * + *

+ * Using this directly provides a default representation, e.g. + * {@code Stealth +36 (+42 in forests) (some other note)} + *

+ * + * @param name The name of the skill + * @param standardBonus The standard bonus associated with this skill + * @param otherBonuses Any additional bonuses, as a map of descriptions to bonuses. Iterate over all map entries to + * display the values: {@code {#each resource.skills.otherBonuses}{it.key}: {it.value}{/each}} + * @param note Any note associated with this skill bonus + */ +@TemplateData +public record QuteDataSkillBonus( + String name, + Integer standardBonus, + Map otherBonuses, + String note) implements QuteUtil { + + public QuteDataSkillBonus(String name, Integer standardBonus) { + this(name, standardBonus, null, null); + } + + @Override + public String toString() { + return Stream.of( + List.of(String.format("%s %+d", name, standardBonus)), + otherBonuses.entrySet().stream().map(e -> String.format("(%+d %s)", e.getValue(), e.getKey())).toList(), + note == null ? List. of() : List.of("(" + note + ")")) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .collect(Collectors.joining(" ")); + } +} diff --git a/src/main/resources/convertData.json b/src/main/resources/convertData.json index 96c545b5..4454e5b6 100644 --- a/src/main/resources/convertData.json +++ b/src/main/resources/convertData.json @@ -360,7 +360,7 @@ "spell|ceremony|phb": "spell|ceremony|xge", "spell|charm monster|phb": "spell|charm monster|xge", "spell|deception|phb": "skill|deception|phb", - "spell|detect good and evil|phb":"spell|detect evil and good|phb", + "spell|detect good and evil|phb": "spell|detect evil and good|phb", "spell|enlarge|phb": "spell|enlarge/reduce|phb", "spell|history|phb": "skill|history|phb", "spell|infestation|phb": "spell|infestation|xge", @@ -461,6 +461,7 @@ "archetype", "background", "book", + "creature", "deity", "feat", "hazard", diff --git a/src/main/resources/templates/toolsPf2e/creature2md.txt b/src/main/resources/templates/toolsPf2e/creature2md.txt new file mode 100644 index 00000000..26ba841a --- /dev/null +++ b/src/main/resources/templates/toolsPf2e/creature2md.txt @@ -0,0 +1,42 @@ +--- +obsidianUIMode: preview +cssclasses: pf2e,pf2e-creature +{#if resource.tags} +tags: +{#each resource.tags} +- {it} +{/each} +{/if} +{#if resource.aliases} +aliases: +{#each resource.aliases} +- {it} +{/each} +{/if} +--- +# {resource.name} *Creature {resource.level}* +{#if resource.traits}{#each resource.traits}{it} {/each}{/if} + +```ad-statblock +{#if resource.perception != null} +- **Perception** {#if resource.perception >= 0}+{#else}-{/if}{resource.perception} +{/if}{#if resource.languages} +- **Languages** {resource.languages} +{/if}{#if resource.skills} +- **Skills** {resource.skills} +{/if}{#if resource.defenses} +{resource.defenses} +{/if} +``` +^statblock +{#if resource.hasSections} + +## Summary +{/if} +{#if resource.description} + +{resource.description} + +{/if} + +*Source: {resource.source}* diff --git a/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataNoneTest.java b/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataNoneTest.java index 24dca018..18be2108 100644 --- a/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataNoneTest.java +++ b/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataNoneTest.java @@ -55,6 +55,11 @@ public void testBackground_p2fe() throws Exception { commonTests.generateNotesForType(Pf2eIndexType.background); } + @Test + public void testCreature_pf2e() throws Exception { + commonTests.generateNotesForType(Pf2eIndexType.creature); + } + @Test public void testDeity_p2fe() throws Exception { commonTests.generateNotesForType(Pf2eIndexType.deity); diff --git a/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataSubsetTest.java b/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataSubsetTest.java index c3f28de3..4e113817 100644 --- a/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataSubsetTest.java +++ b/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataSubsetTest.java @@ -55,6 +55,11 @@ public void testBackground_p2fe() throws Exception { commonTests.generateNotesForType(Pf2eIndexType.background); } + @Test + public void testCreature_pf2e() throws Exception { + commonTests.generateNotesForType(Pf2eIndexType.creature); + } + @Test public void testDeity_p2fe() throws Exception { commonTests.generateNotesForType(Pf2eIndexType.deity); diff --git a/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataTest.java b/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataTest.java index a1806810..3053ff60 100644 --- a/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataTest.java +++ b/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataTest.java @@ -55,6 +55,11 @@ public void testBackground_p2fe() throws Exception { commonTests.generateNotesForType(Pf2eIndexType.background); } + @Test + public void testCreature_pf2e() throws Exception { + commonTests.generateNotesForType(Pf2eIndexType.creature); + } + @Test public void testDeity_p2fe() throws Exception { commonTests.generateNotesForType(Pf2eIndexType.deity);