Skip to content

feat(2024): validate and reshape generated monster data - #1228

Open
bagelbits wants to merge 11 commits into
add2024Monsters-basefrom
claude/pr-data-validation-ad996c
Open

bagelbits wants to merge 11 commits into
add2024Monsters-basefrom
claude/pr-data-validation-ad996c

Conversation

@bagelbits

@bagelbits bagelbits commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

What does this do?

Stacked on #1226. Base branch add2024Monsters-base is copy of that PR head. Retarget to main after #1226 merge.

Script output now match MonsterSchema. New test check it. CI now run on PRs to any base branch, so stacked PRs get lint + tests.

  • Normalize PDF text: ability scores on one line, wrapped header lines joined, merged lines split. Write monster-text-data.normalized.txt.
  • Pair text block to monster by ability scores + CR, not array position. Old way swap skills/PB for 7 monsters (giant-crab, giant-crocodile, winter-wolf, worg, allosaurus, ankylosaurus, ape).
  • Reshape: numeric CR, xp, xp_in_lair, armor_class array, condition references, senses parsing, hit dice minus sign.
  • Parse damage (all types, one entry each) and dc (saving throw, escape, ability check) from descriptions for traits, actions, bonus actions, reactions, legendary actions.
  • Build spellcasting. List entries (48): spell lists come from stat block text, gist stop at intro line; match list to entry by ability + save DC + to-hit since PDF put some blocks inside previous block text. Single-spell casts (Invisibility, Fear, Coven Magic, Divine Aid, and similar): link spell, inherit ability/DC from main Spellcasting when text say "same spellcasting ability", per-day usage from action name. Every spell resolve against 5e-SRD-Spells.json; script throw on unknown spell or unused list. (level N version) set spell level, other parentheticals become capitalized notes.
  • Clean PDF debris from entries: drop spell list lines gist parsed as entries, strip next block title from last entry, split merged entries, complete descriptions cut at page breaks from stat block text (only when gist text is exact prefix of text paragraph). 1460 entries down to 1330.
  • Schema: add optional damage to ReactionSchema. Add optional spellcasting to reactions and legendary actions. Add optional skills, gear, bonus_actions to MonsterSchema. Omit skills/gear when source say None.
  • Split script into modules: monsterText.mjs (normalize, parse blocks, pair to monsters), monsterEntries.mjs (damage, dc, debris cleanup), monsterSpellcasting.mjs, monsterCommon.mjs. Output byte-identical before and after split.
  • Structure all 178 Multiattack entries: multiattack_type, actions, action_options. Same Choice shape as 2014 and existing 2024 monsters. Attack type from the monster's own action (melee/ranged/special), or the spell's attack_type for casts. Description text unchanged.
  • New convention for "any combination": inside a Choice, an action option count is the most times that option can be picked. "Three attacks, Claw or Tail in any combination" = choose: 3, Claw count: 3, Tail count: 3. Replacements join the choice (count = attacks replaced). No existing data use count > 1 inside choose > 1, so this is new. Please check it fits API consumers.
  • Split form monsters like 2014: werebear, wereboar, wererat, weretiger, werewolf into human/hybrid/animal, vampire into vampire/bat/mist. 329 monsters become 341. Same index, name and image as 2014, linked by forms. Entries tagged "Form Only" go only to their forms (tag removed), untagged go to every form. Multiattack options pruned to actions the form has. Size from Shape-Shift. Speed from text Speed line (gist had animal-form value, e.g. werewolf 40 ft. for every form) or from Shape-Shift for vampire bat and mist. Human and vampire form keep base size "Medium or Small".
  • Merge fake entries gist made from tail of Failure:/Success: lines (lycanthrope curse, steam mephit Steam Breath).
  • Regenerate 5e-SRD-Monsters-New.json.

How was it tested?

New src/2024/tests/monsterGeneration.test.ts. Run script into temp dir, no overwrite of real files. Check: count + unique index, schema, hit dice format, passive perception, sense format, XP, PB vs CR, skill bonus = mod + PB or 2 x PB, every save DC and damage roll in description present in output, every spell cast has valid spell URL, no spell list entries or unterminated descriptions. Every Multiattack must be structured and simple counts must round trip. Forms split, linked and pruned; werewolf human/wolf entries and speeds checked. Full suite 106 pass.

Known gaps

  • armor_class.type is guess: gear = armor, no gear = natural. Gist has no type.
  • spell save DC not put in dc. It live in spellcasting.dc.
  • Dryad Multiattack read as one choice of Vine Lash, Thorn Burst or Charm Monster. Text say "and it can use Spellcasting" so could mean attack plus cast.
  • Clay golem Multiattack condition ("if it used Hasten this turn") only in Choice desc, not structured.
  • components_required computed from text: "no Material components" give ["V","S"].
  • Reference adult-blue-dragon (5e-SRD-Monsters.json) has extra acid-arrow and modifier: 5. Book text has neither. Output follow book text.
  • Charmed (with Mind Blank) on archmage become plain charmed.
  • Escape DC and first-named ability of check DC map to one ability. Book allow choice.
  • shambling-mound and giant-frog Stealth match book, not mod + PB. Listed as quirks in test.
  • Mist form gets every untagged entry (Charm, legendary actions, traits) since tags followed strictly. Book say mist form can't take actions.
  • Mimic Adhesive (Object Form Only) keeps tag. Not split in 2014.
  • Some words keep gist compound artifacts like 5-footwide.
  • Merged-entry split also cover (... Form Only) headings (wererat Hand Crossbow).

Is there a Github issue this is resolving?

NA

Did you update the docs in the API? Please link an associated PR if applicable.

No

🤖 Generated with Claude Code

bagelbits and others added 11 commits September 20, 2026 11:55
Normalize the PDF text (ability scores, wrapped and merged header lines)
and pair text blocks to monsters by ability scores plus CR instead of
array position, which had swapped several monsters' skills and PB.

Reshape output to MonsterSchema: numeric CR, xp/xp_in_lair, armor_class
array, condition references, senses parsing, dc and multi-type damage
parsed from descriptions. Add optional damage to reactions.

Add monsterGeneration test that runs the script into a temp dir and
checks schema, PB vs CR, skill bonuses, senses, XP, DC and damage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Read At Will and N/Day spell lists from the stat block text, since the gist
descriptions stop at the intro line. Match each list to its entry by the
intro's ability, save DC and to-hit bonus, because the PDF layout can put
one block's spellcasting inside the previous block's text.

Resolve every spell against 5e-SRD-Spells.json, map "(level N version)" to
the spell level and other parentheticals to capitalized notes. Fail the
script on any unknown spell or unused list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Validate the generated monsters against the real schema instead of a
test-only extension. Omit skills and gear when the source says None.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Build spellcasting for entries that cast named spells inline, including
Coven Magic and bonus actions, reactions and legendary actions. Inherit
ability, DC and to-hit from the monster's Spellcasting entry when the text
says "the same spellcasting ability". Take per-day usage from the action
name. Allow spellcasting on reactions and legendary actions in the schema.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Stacked PRs target a non-main base, so the main-only filter skipped lint
and tests on them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Drop spell list lines the gist parsed as entries, strip the next block's
title from the last entry of a block, split entries the gist merged, and
complete descriptions truncated at page breaks from the stat block text.
A completion is only accepted when the gist text is an exact prefix of the
text paragraph. Link Nightmare Haunting's Dream cast.

chain-devil Unnerving Gaze is cut off in the source text itself and stays
listed as a known truncation in the test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The gist split the reaction in two. The text has the full sentence; the
em dash in the gist stopped the prefix match. Normalize dashes and drop
the known-truncation allow-list from the test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Move text normalization, stat block parsing, entry cleanup and
spellcasting out of processMonsterData.mjs. Generated JSON and normalized
text are byte-identical before and after.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Parse Multiattack descriptions into multiattack_type, actions and
action_options, using the same Choice shape as the 2014 and existing 2024
monsters. Handle fixed sequences, ability uses, alternatives, "any
combination" choices and "can replace one attack with" clauses. Attack
type comes from the monster's own entry, or the spell for casts.

Shapes that do not fit stay as plain descriptions and are listed in the
test: any-combination plus replacement, and three one-off phrasings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
In a Choice, an action option's count is the most times it can be picked,
so "three attacks, Claw or Tail in any combination" is choose 3 with each
option at count 3. Replacements join the choice with count 1 (or the
number of attacks they replace), which structures the remaining any-combo
plus replacement entries, oni, dryad and guardian naga. Description text
is unchanged.

Also split Hand Crossbow (... Form Only) out of the wererat's Scratch,
where the gist had merged them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mirror the 2014 data: werebear, wereboar, wererat, weretiger and werewolf
become human, hybrid and animal forms; vampire becomes vampire, bat and
mist. Each form links the others through forms and reuses the 2014 index,
name and image.

Entries tagged "Form Only" go only to their forms and lose the tag.
Multiattack options for actions a form lacks are dropped. Sizes come from
Shape-Shift; speeds come from the Speed line in the text (the gist had the
animal-form value) or from Shape-Shift for the vampire's bat and mist.

Also merge fake entries the gist made from the tail of a Failure: or
Success: line, such as the lycanthropes' curse and steam mephit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant