Compare commits
9 Commits
4253495dc5
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b7b9e00f19 | |||
| 96624631b2 | |||
| 632a8c4f1b | |||
| 13458a0173 | |||
| 27eb488e78 | |||
| 6cab3b289a | |||
| 48b41ca885 | |||
| b992873072 | |||
| 1ad7b2c502 |
+150
-8
@@ -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
|
||||||
|
|||||||
@@ -1,8 +1,33 @@
|
|||||||
|
import collections
|
||||||
import pprint
|
import pprint
|
||||||
|
import re
|
||||||
|
import typing
|
||||||
import dnd5etools.db.spells
|
import dnd5etools.db.spells
|
||||||
import dnd5etools.scripts.argparse
|
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<size>\d+)-foot-radius[ -](sphere|circle)", flags=re.IGNORECASE
|
||||||
|
)
|
||||||
|
radius_size_re = re.compile(r"(?P<size>\d+)-foot radius", flags=re.IGNORECASE)
|
||||||
|
circle_diam_size_re = re.compile(
|
||||||
|
r"(?P<size>\d+)-foot-diameter[ -](sphere|circle)", flags=re.IGNORECASE
|
||||||
|
)
|
||||||
|
cylinder_size_re = re.compile(
|
||||||
|
r"(?P<size>\d+)-foot-radius, \d+-foot[ -](high|tall) cylinder", flags=re.IGNORECASE
|
||||||
|
)
|
||||||
|
alt_cylinder_size_re = re.compile(
|
||||||
|
r"\d+-foot[ -](high|tall), (?P<size>\d+)-foot-radius cylinder", flags=re.IGNORECASE
|
||||||
|
)
|
||||||
|
square_size_re = re.compile(r"(?P<size>\d+)-foot[ -](cube|square)", flags=re.IGNORECASE)
|
||||||
|
cone_size_re = re.compile(r"(?P<size>\d+)-foot cone", flags=re.IGNORECASE)
|
||||||
|
line_size_re = re.compile(
|
||||||
|
r"\d+-foot-wide, (?P<size>\d+)-foot[ -]long line", flags=re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
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 +37,80 @@ 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)
|
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, 2)
|
||||||
|
print("squares/size")
|
||||||
|
print_sizes(squares, 2)
|
||||||
|
print("cones/size")
|
||||||
|
print_sizes(cones, 2)
|
||||||
|
print("lines/length")
|
||||||
|
print_sizes(lines, 2)
|
||||||
|
|
||||||
|
|
||||||
|
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: int) -> None:
|
||||||
|
indent_str = " " * indent
|
||||||
|
for s in sorted(sizes_dict.items(), key=lambda kv: len(kv[1]), reverse=True):
|
||||||
|
print(f"{indent_str} {s[0]}ft/{s[0]/5}in => {len(s[1])}")
|
||||||
|
print(
|
||||||
|
f"{indent_str}{indent_str}{pprint.pformat(s[1], indent=3*indent, compact=True)}"
|
||||||
|
)
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[project]
|
[project]
|
||||||
#urls = { repository = "https://gitlab.krugerlabs.us/krugd/dnd_transcribe" }
|
urls = { repository = "https://gitlab.krugerlabs.us/krugd/dnd_5etools_utils" }
|
||||||
authors = [{ name = "David Kruger" }]
|
authors = [{ name = "David Kruger" }]
|
||||||
name = "dnd_5etools_utils"
|
name = "dnd_5etools_utils"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user