Compare commits

...

5 Commits

2 changed files with 199 additions and 10 deletions

View File

@ -14,12 +14,113 @@ class Ability(enum.StrEnum):
Charisma = "charisma" 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] # TODO: replace with a full enum
@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 SpellAttackType(enum.StrEnum):
Melee = "M"
Ranged = "R"
Other = "O"
class SpellAreaType(enum.StrEnum):
SingleTarget = "ST"
MultipleTargets = "MT"
Cube = "C"
Cone = "N"
Cylinder = "Y"
Sphere = "S"
Circle = "R"
Square = "Q"
Line = "L"
Hemisphere = "H"
Wall = "W"
class SpellRangeType(enum.StrEnum):
Special = "special"
Point = "point"
Line = "line"
Cube = "cube"
Cone = "cone"
EmanatioN = "emanation"
Radius = "radius"
Sphere = "sphere"
Hemisphere = "hemisphere"
Cylinder = "cylinder"
Self = "self"
Sight = "sight"
Unlimited = "unlimited"
UnlimitedSamePlane = "plane"
Touch = "touch"
class SpellRange(typing.NamedTuple):
type: SpellRangeType
distance_type: str # TODO Replace with a full enum
distance_value: typing.Optional[int]
@classmethod
def from_json(cls, json_data) -> typing.Self:
return cls(
type=SpellRangeType(json_data["type"]),
distance_type=json_data["distance"]["type"],
distance_value=(
int(json_data["distance"]["amount"])
if "amount" in json_data["distance"]
else None
),
)
class Spell(typing.NamedTuple): class Spell(typing.NamedTuple):
name: str name: str
source: str source: str
description: str description: str
ability_check: typing.Optional[Ability] 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]
attack_type: typing.Set[SpellAttackType]
area_type: typing.Set[SpellAreaType]
range: SpellRange
# remaining: 'affectsCreatureType', 'alias', 'basicRules2024', 'components', 'conditionImmune', 'conditionInflict', 'damageImmune', 'damageInflict', 'damageResist', 'damageVulnerable', 'entriesHigherLevel', 'hasFluffImages', 'level', 'meta', 'miscTags', 'page', 'savingThrow', 'scalingLevelDice', 'school', 'srd52', 'time'
@classmethod @classmethod
def from_json(cls, json_data) -> typing.Self: def from_json(cls, json_data) -> typing.Self:
@ -33,25 +134,65 @@ class Spell(typing.NamedTuple):
description = cls.description_from_json(json_data) description = cls.description_from_json(json_data)
except Exception as e: except Exception as e:
raise Exception(f"Unable to parse description for {name}: {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}")
try:
attack_type = cls.attack_type_from_json(json_data)
except Exception as e:
raise Exception(f"Unable to parse spellAttack for {name}: {e}")
try:
area_type = cls.area_type_from_json(json_data)
except Exception as e:
raise Exception(f"Unable to parse areaTags for {name}: {e}")
try:
range = cls.range_from_json(json_data)
except Exception as e:
raise Exception(f"Unable to parse range for {name}: {e}")
return cls( return cls(
name=name, name=name,
source=source, source=source,
description=description, description=description,
ability_check=ability_check, ability_check=ability_check,
duration=duration,
attack_type=attack_type,
area_type=area_type,
range=range,
) )
@classmethod @classmethod
def ability_check_from_json(cls, json_data) -> typing.Optional[Ability]: def ability_check_from_json(cls, json_data) -> typing.Set[Ability]:
if "abilityCheck" not in json_data: return cls.json_str_enum_list(json_data, "abilityCheck", Ability)
return None
elif len(json_data["abilityCheck"]) > 1: @classmethod
raise Exception(f"Unexpected abilityCheck length") def attack_type_from_json(cls, json_data) -> typing.Set[SpellAttackType]:
return Ability(json_data["abilityCheck"][0]) return cls.json_str_enum_list(json_data, "spellAttack", SpellAttackType)
@classmethod
def area_type_from_json(cls, json_data) -> typing.Set[SpellAreaType]:
return cls.json_str_enum_list(json_data, "areaTags", SpellAreaType)
@classmethod
def json_str_enum_list(
cls, json_data, key: str, enum_cls: typing.Type[enum.StrEnum]
) -> typing.Set[enum.StrEnum]:
if key not in json_data:
return set()
return {enum_cls(c) for c in json_data[key]}
@classmethod @classmethod
def description_from_json(cls, json_data) -> str: def description_from_json(cls, json_data) -> str:
return " ".join([str(e) for e in json_data["entries"]]) 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"]]
@classmethod
def range_from_json(cls, json_data) -> typing.List[SpellRange]:
return SpellRange.from_json(json_data["range"])
class SpellList: class SpellList:
def __init__(self, code: str, filepath: pathlib.Path) -> None: def __init__(self, code: str, filepath: pathlib.Path) -> None:
@ -67,6 +208,7 @@ class SpellList:
except Exception as e: except Exception as e:
raise Exception(f"Failed to read spells file {filepath}: {e}") raise Exception(f"Failed to read spells file {filepath}: {e}")
spells: typing.List[Spell] = [] spells: typing.List[Spell] = []
dbg_duration_type = set()
for spell_data in spell_list_data["spell"]: for spell_data in spell_list_data["spell"]:
if "_copy" in spell_data: if "_copy" in spell_data:
continue # Ignore spells that are just tweaks or reprints continue # Ignore spells that are just tweaks or reprints

View File

@ -1,8 +1,16 @@
import pprint import pprint
import re
import dnd5etools.db.spells import dnd5etools.db.spells
import dnd5etools.scripts.argparse import dnd5etools.scripts.argparse
area_size_re = re.compile(r"\d+-foot")
sphere_size_re = re.compile(r"(?P<size>\d+)-foot-radius sphere")
cylinder_size_re = re.compile(r"(?P<size>\d+)-foot-radius, \d+-foot-high cylinder")
cube_size_re = re.compile(r"(?P<size>\d+)-foot cube")
cone_size_re = re.compile(r"(?P<size>\d+)-foot cone")
def main(): def main():
args = dnd5etools.scripts.argparse.build_argument_parser( args = dnd5etools.scripts.argparse.build_argument_parser(
"Summarizes spell templates", "Summarizes spell templates",
@ -12,6 +20,45 @@ def main():
codes = list(db.db_index.source_index.keys()) codes = list(db.db_index.source_index.keys())
else: else:
codes = args.source_code codes = args.source_code
all_template_spells = []
for code in codes: for code in codes:
spell_list = db.get_spell_list(code) all_template_spells += filter(is_template_spell, db.get_spell_list(code).spells)
pprint.pprint(spell_list.spells) # pprint.pprint(all_template_spells)
spheres = set()
cylinders = set()
cubes = set()
cones = set()
for spell in all_template_spells:
found = False
for search_re, dest in [
(sphere_size_re, spheres),
(cylinder_size_re, cylinders),
(cube_size_re, cubes),
(cone_size_re, cones),
]:
m = search_re.search(spell.description)
if m is not None:
dest.add(m.group("size"))
found = True
break
if not found:
print(spell)
print("spheres", spheres)
print("cylinders", cylinders)
print("cubes", cubes)
print("cones", cones)
def is_template_spell(spell: dnd5etools.db.spells.Spell) -> bool:
return (
len(spell.area_type) > 0
and has_timed_duration(spell)
and area_size_re.search(spell.description) is not None
)
def has_timed_duration(spell: dnd5etools.db.spells.Spell) -> bool:
for sd in spell.duration:
if sd.type != dnd5etools.db.spells.DurationType.Instant:
return True
return False