adding docs to dnd_item.weapons

This commit is contained in:
evilchili 2023-12-31 11:55:54 -08:00
parent 4548623831
commit 6220e04553

View File

@ -9,15 +9,25 @@ from dnd_item import types
def random_from_csv(csv: str) -> str:
"""
Split a comma-separated value string into a list and return a random value.
"""
return random.choice(csv.split(",")).strip()
class Weapon(types.Item):
""" """
"""
An Item subclass representing weapons, both magical and mundane.
Much of this subclass is devoted to generating descriptive and entertaining
weapon names. It also implements a number of handy properties for presentation.
"""
def _descriptors(self) -> tuple:
"""
Collect the nouns and adjectives from the properties of this item.
Collect the 'nouns' and 'adjectives' properties from the Item's
'properties' attribute. This is used by _random_descriptors to choose a
set of random nouns and adjectives to apply to a weapon name.
"""
nouns = dict()
adjectives = dict()
@ -31,6 +41,20 @@ class Weapon(types.Item):
return (nouns, adjectives)
def _name_template(self, with_adjectives: bool, with_nouns: bool) -> str:
"""
Generate a WeightedSet of potential name template strings and pick one.
The possible templates are determined by whether we have selected
nouns, adjectives, or both to describe the item; we can't use a
template that includes {adjectives} if no adjectives are defined in the
Item's properties, for example.
The weighted distribution is tuned so that we mostly get names of the
typical form ("Venomous Shortsowrd", "Dagger of Shocks"). Occasionally
we will select templates resulting in very long names ("Frosty +3 Mace
of Mighty Striking") or repeitive descriptions ("Flaming Spear of
Flames"), but not so often as to make all generated items too silly.
"""
num_properties = len(self.properties)
options = []
if with_nouns and not with_adjectives:
@ -51,7 +75,15 @@ class Weapon(types.Item):
def _random_descriptors(self):
"""
Select random nouns and adjectives from the object
Select random nouns and adjectives from the available descriptors in
the properties attribute.
This method ensures that if a property adding fire damage to the wepaon
is chosen, 'Flames' or 'Flaming' or 'Fire' (or whatever is defined on
that enchantement) is used when naming the Item.
The randomly-selected set of nouns and/or adjectives will then govern
the naming template selected; see _random_template() above.
"""
random_nouns = []
random_adjectives = []
@ -65,8 +97,12 @@ class Weapon(types.Item):
seen_nouns = dict()
for prop_name in set(list(nouns.keys()) + list(adjectives.keys())):
if prop_name in nouns and prop_name not in adjectives:
if prop_name not in seen_nouns:
# Ensure we only add one noun for each property so taht we
# don't end up with Items named like "Mace of Venomous
# Venom Poison".
add_word(prop_name, random_nouns, nouns)
seen_nouns[prop_name] = True
@ -89,12 +125,25 @@ class Weapon(types.Item):
add_word(prop_name, random_nouns, nouns)
add_word(prop_name, random_adjectives, adjectives)
# Join multiple nouns together, so that instead of "Staff of Strikes
# Cold" we get "Staff of Strikes and Cold." Adjectives we just join
# together ("Staff of Frosty Striking")
random_nouns = " and ".join(random_nouns)
random_adjectives = " ".join(random_adjectives)
return (random_nouns, random_adjectives)
@cached_property
def name(self) -> str:
"""
Generate and cache a random name for the Weapon based on its properties.
This method selects a subset of random nouns and adjectives from the
Weapon's properties, a random name string template compatible with
those descriptors, and returns a formatted title string. The return
value is cached because there are multiple possisble names for a Weapon
with the same set of properties, and we don't want multiple references
to self.name to return different values on the same instance.
"""
base_name = super().name
(nouns, adjectives) = self._random_descriptors()
if not (nouns or adjectives):
@ -108,6 +157,10 @@ class Weapon(types.Item):
@property
def to_hit(self):
"""
Return a string summarizing the total bonus to hit from this weapon and its properties. This
could be either a single number (+2), a die roll (1d6), or a combination of both (1d6+2).
"""
bonus_val = 0
bonus_dice = ""
if not hasattr(self, "properties"):
@ -124,6 +177,15 @@ class Weapon(types.Item):
@property
def damage_dice(self):
"""
Return a string summarizing the damage done by a hit with this weapon
by combining all the damage types and dice from the Weapon's
properties. Examples:
- 5 Bludgeoning
- 1d6 Piercing
- 1d8+1 Thunder
- 1d6+1 Slashing + 1d4 Thunder + 3 Poison
"""
if not hasattr(self, "properties"):
return ""
dmg = {self.damage_type: str(self.damage) or ""}
@ -143,12 +205,17 @@ class Weapon(types.Item):
@property
def summary(self):
"""
Return a one-line summary of the Weapon's attack. For example:
+2 to hit, 5 ft., 1 tgts. 1d6+2 Piercing + 1d6 Fire
"""
return f"{self.to_hit} to hit, {self.range} ft., {self.targets} tgts. {self.damage_dice}"
@property
def details(self):
"""
Format the item properties as nested bullet lists.
Return details of the Weapon as a multi-line string.
"""
props = ", ".join(self.get("properties", dict()).keys())
return "\n".join(
@ -160,8 +227,18 @@ class Weapon(types.Item):
]
)
@property
@cached_property
def id(self):
"""
Generate a unique ID for this weapon. Unlike self.name, which may have
any of several possisble generated values based on the Weapon's
properties, this property will generate the same ID every for every
instance with the same properties.
This is useful for asserting equality between instances, which may
be helpful when (for example) generating weapon look-up tables,
web pages, item cards, and so on.
"""
sha1bytes = hashlib.sha1(
"".join(
[
@ -171,10 +248,19 @@ class Weapon(types.Item):
]
).encode()
)
# Only use the first ten characteres of the encoded value. This
# increases the likelihood of hash collisions, but 10 characters is far
# more than is necessary to encode all possible weapons generated by
# this package, and 10 is friendlier for user-facing use casees, such
# as stub URLs.
return base64.urlsafe_b64encode(sha1bytes.digest()).decode("ascii")[:10]
class WeaponGenerator(types.ItemGenerator):
"""
A subclass of ItemGenerator that generates Weapon instances.
"""
item_class = Weapon
def __init__(
@ -186,7 +272,8 @@ class WeaponGenerator(types.ItemGenerator):
super().__init__(bases=bases, rarity=rarity, properties_by_rarity=properties_by_rarity)
def random_properties(self) -> dict:
# add missing base weapon defaults (TODO: update the sources)
# add missing base weapon defaults
# TODO: update the sources then delete this method
item = super().random_properties()
item["targets"] = 1
if item["category"] == "Martial":
@ -194,9 +281,12 @@ class WeaponGenerator(types.ItemGenerator):
item["range"] = ""
return item
# handlers for extra properties
def get_enchantment(self, **attrs) -> dict:
"""
PROPERTIES_BY_RARITY includes references to enchamentments, so make
sure we know how to generate a random enchantment when it is referenced
by a template string.
"""
prop = types.ENCHANTMENT.random()
prop["adjectives"] = random_from_csv(prop["adjectives"])
prop["nouns"] = random_from_csv(prop["nouns"])