From 48b41ca8854cff4b85ab095c282ed80604f906e5 Mon Sep 17 00:00:00 2001 From: David Kruger Date: Tue, 20 May 2025 21:40:33 -0700 Subject: [PATCH] Parse spell duration --- dnd5etools/db/spells.py | 54 ++++++++++++++++++++++++++- dnd5etools/scripts/spell_templates.py | 3 +- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/dnd5etools/db/spells.py b/dnd5etools/db/spells.py index f7d7ad0..3775fb6 100644 --- a/dnd5etools/db/spells.py +++ b/dnd5etools/db/spells.py @@ -14,12 +14,54 @@ class Ability(enum.StrEnum): Charisma = "charisma" +class DurationType(enum.StrEnum): + Timed = "timed" + Instant = "instant" + Permanent = "permanent" + Special = "special" + + +class DurationTimeType(enum.StrEnum): + Round = "round" + Minute = "minute" + Hour = "hour" + Day = "day" + + +class Duration(typing.NamedTuple): + type: DurationType + time_type: typing.Optional[DurationTimeType] + time_value: typing.Optional[int] + concentration: bool + end_condition: typing.Set[str] + + @classmethod + def from_json(cls, json_data) -> typing.Self: + dt = DurationType(json_data["type"]) + return cls( + type=dt, + time_type=( + DurationTimeType(json_data["duration"]["type"]) + if dt == DurationType.Timed + else None + ), + time_value=( + int(json_data["duration"]["amount"]) + if dt == DurationType.Timed + else None + ), + concentration=json_data.get("concentration", False), + end_condition=set(json_data.get("end", [])), + ) + + class Spell(typing.NamedTuple): name: str source: str description: str ability_check: typing.Set[Ability] - # remaining: 'affectsCreatureType', 'alias', 'areaTags', 'basicRules2024', 'components', 'conditionImmune', 'conditionInflict', 'damageImmune', 'damageInflict', 'damageResist', 'damageVulnerable', 'duration', 'entriesHigherLevel', 'hasFluffImages', 'level', 'meta', 'miscTags', 'page', 'range', 'savingThrow', 'scalingLevelDice', 'school', 'spellAttack', 'srd52', 'time' + duration: typing.List[Duration] + # remaining: 'affectsCreatureType', 'alias', 'areaTags', 'basicRules2024', 'components', 'conditionImmune', 'conditionInflict', 'damageImmune', 'damageInflict', 'damageResist', 'damageVulnerable', 'entriesHigherLevel', 'hasFluffImages', 'level', 'meta', 'miscTags', 'page', 'range', 'savingThrow', 'scalingLevelDice', 'school', 'spellAttack', 'srd52', 'time' @classmethod def from_json(cls, json_data) -> typing.Self: @@ -33,11 +75,16 @@ class Spell(typing.NamedTuple): description = cls.description_from_json(json_data) except Exception as e: raise Exception(f"Unable to parse description for {name}: {e}") + try: + duration = cls.duration_from_json(json_data) + except Exception as e: + raise Exception(f"Unable to parse duration for {name}: {e}") return cls( name=name, source=source, description=description, ability_check=ability_check, + duration=duration, ) @classmethod @@ -50,6 +97,10 @@ class Spell(typing.NamedTuple): def description_from_json(cls, json_data) -> str: return " ".join([str(e) for e in json_data["entries"]]) + @classmethod + def duration_from_json(cls, json_data) -> typing.List[Duration]: + return [Duration.from_json(d) for d in json_data["duration"]] + class SpellList: def __init__(self, code: str, filepath: pathlib.Path) -> None: @@ -65,6 +116,7 @@ class SpellList: except Exception as e: raise Exception(f"Failed to read spells file {filepath}: {e}") spells: typing.List[Spell] = [] + dbg_duration_type = set() for spell_data in spell_list_data["spell"]: if "_copy" in spell_data: continue # Ignore spells that are just tweaks or reprints diff --git a/dnd5etools/scripts/spell_templates.py b/dnd5etools/scripts/spell_templates.py index 6379432..9e08fa3 100644 --- a/dnd5etools/scripts/spell_templates.py +++ b/dnd5etools/scripts/spell_templates.py @@ -14,4 +14,5 @@ def main(): codes = args.source_code for code in codes: spell_list = db.get_spell_list(code) - pprint.pprint(spell_list.spells) + # pprint.pprint(spell_list.spells) + print(f"Found {len(spell_list.spells)} spells")