import collections import pprint import re import typing import dnd5etools.db.spells import dnd5etools.scripts.argparse area_size_re = re.compile(r"\d+-foot", flags=re.IGNORECASE) light_size_re = re.compile(r"(light|light\|\w+\}) in a \d+-foot", flags=re.IGNORECASE) circle_size_re = re.compile( r"(?P\d+)-foot-radius[ -](sphere|circle)", flags=re.IGNORECASE ) radius_size_re = re.compile(r"(?P\d+)-foot radius", flags=re.IGNORECASE) circle_diam_size_re = re.compile( r"(?P\d+)-foot-diameter[ -](sphere|circle)", flags=re.IGNORECASE ) cylinder_size_re = re.compile( r"(?P\d+)-foot-radius, \d+-foot[ -](high|tall) cylinder", flags=re.IGNORECASE ) alt_cylinder_size_re = re.compile( r"\d+-foot[ -](high|tall), (?P\d+)-foot-radius cylinder", flags=re.IGNORECASE ) square_size_re = re.compile(r"(?P\d+)-foot[ -](cube|square)", flags=re.IGNORECASE) cone_size_re = re.compile(r"(?P\d+)-foot cone", flags=re.IGNORECASE) line_size_re = re.compile( r"\d+-foot-wide, (?P\d+)-foot[ -]long line", flags=re.IGNORECASE ) def main(): args = dnd5etools.scripts.argparse.build_argument_parser( "Summarizes spell templates", ).parse_args() db = dnd5etools.db.spells.SpellsDb(args.data_dir) if args.source_code is None: codes = list(db.db_index.source_index.keys()) else: codes = args.source_code all_template_spells = [] for code in codes: all_template_spells += filter(is_template_spell, db.get_spell_list(code).spells) num_matches = 0 circles = collections.defaultdict(list) squares = collections.defaultdict(list) cones = collections.defaultdict(list) lines = collections.defaultdict(list) for spell in all_template_spells: found = False for search_re, dest in [ (line_size_re, lines), (circle_size_re, circles), (cylinder_size_re, circles), (circle_diam_size_re, circles), (alt_cylinder_size_re, circles), (square_size_re, squares), (cone_size_re, cones), (radius_size_re, circles), ]: m = search_re.search(spell.description) if m is not None: size = int(m.group("size")) if search_re not in ( cone_size_re, square_size_re, circle_diam_size_re, line_size_re, ): size *= 2 dest[size].append(spell.name) found = True num_matches += 1 break print(f"{num_matches} matched from {len(all_template_spells)}") print("circles/diameter") print_sizes(circles, " ") print("squares/size") print_sizes(squares, " ") print("cones/size") print_sizes(cones, " ") print("lines/length") print_sizes(lines, " ") def is_template_spell(spell: dnd5etools.db.spells.Spell) -> bool: num_area_sizes = 0 for _ in area_size_re.finditer(spell.description): num_area_sizes += 1 num_light_sizes = 0 for _ in light_size_re.finditer(spell.description): num_light_sizes += 1 return ( len(spell.area_type) > 0 and not "Wall" in spell.name and has_timed_duration(spell) and spell.range.type != dnd5etools.db.spells.SpellRangeType.Emanation and spell.range.distance_type != "self" and num_area_sizes > 0 and num_light_sizes < num_area_sizes ) 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 def print_sizes(sizes_dict: typing.Mapping[int, int], indent: str) -> None: for s in sorted(sizes_dict.items(), key=lambda kv: len(kv[1]), reverse=True): print(f"{indent} {s[0]}ft => {len(s[1])}") print(f"{indent}{indent}{s[1]}")