adding hit dice and defenses
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
from collections import defaultdict
|
||||
|
||||
from sqlalchemy import ForeignKey, String, Text, UniqueConstraint
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
@@ -36,6 +38,24 @@ def attr_map_creator(fields):
|
||||
return CharacterClassAttributeMap(**fields)
|
||||
|
||||
|
||||
class HitDie(BaseObject):
|
||||
__tablename__ = "hit_die"
|
||||
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
|
||||
character_id: Mapped[int] = mapped_column(ForeignKey("character.id"))
|
||||
character_class_id: Mapped[int] = mapped_column(ForeignKey("character_class.id"))
|
||||
character_class = relationship("CharacterClass", lazy="immediate")
|
||||
|
||||
spent: Mapped[bool] = mapped_column(nullable=False, default=False)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.character_class.hit_die_name
|
||||
|
||||
@property
|
||||
def stat(self):
|
||||
return self.character_class.hit_die_stat_name
|
||||
|
||||
|
||||
class AncestryTraitMap(BaseObject):
|
||||
__tablename__ = "trait_map"
|
||||
__table_args__ = (UniqueConstraint("ancestry_id", "ancestry_trait_id"),)
|
||||
@@ -205,6 +225,19 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||
ancestry_id: Mapped[int] = mapped_column(ForeignKey("ancestry.id"), nullable=False, default="1")
|
||||
ancestry: Mapped["Ancestry"] = relationship(uselist=False, default=None)
|
||||
|
||||
_hit_dice = relationship("HitDie", uselist=True, cascade="all,delete,delete-orphan", lazy="immediate")
|
||||
|
||||
@property
|
||||
def hit_dice(self):
|
||||
pool = defaultdict(list)
|
||||
for die in self._hit_dice:
|
||||
pool[die.character_class.name].append(die)
|
||||
return pool
|
||||
|
||||
@property
|
||||
def hit_dice_available(self):
|
||||
return [die for die in self._hit_dice if die.spent is False]
|
||||
|
||||
@property
|
||||
def proficiency_bonus(self):
|
||||
return 1 + int(0.5 + self.level / 4)
|
||||
@@ -281,6 +314,24 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||
return None
|
||||
return mapping[0]
|
||||
|
||||
def immune(self, damage_type: str, magical: bool = False):
|
||||
return self.defense(damage_type, magical) == 'immune'
|
||||
|
||||
def resistant(self, damage_type: str, magical: bool = False):
|
||||
return self.defense(damage_type, magical) == 'resistant'
|
||||
|
||||
def vulnerable(self, damage_type: str, magical: bool = False):
|
||||
return self.defense(damage_type, magical) == 'vulnerable'
|
||||
|
||||
def absorbs(self, damage_type: str, magical: bool = False):
|
||||
return self.defense(damage_type, magical) == 'absorbs'
|
||||
|
||||
def defense(self, damage_type: str, magical: bool = False):
|
||||
attr_name = damage_type
|
||||
if magical:
|
||||
attr_name = f"magical_{attr_name}"
|
||||
return self._apply_modifiers(f"defenses.{attr_name}", None)
|
||||
|
||||
def check_modifier(self, skill: Skill, save: bool = False):
|
||||
# if the skill is not assigned, but we have modifiers, apply them to zero.
|
||||
if skill not in self.skills:
|
||||
@@ -302,7 +353,6 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||
# if the skill is a proficiency, apply the bonus to the initial value
|
||||
if skill in self.skills:
|
||||
mapping = [mapping for mapping in self._skills if mapping.skill_id == skill.id][-1]
|
||||
print(f"Found mapping: {mapping}")
|
||||
if mapping.expert and not save:
|
||||
initial += 2 * self.proficiency_bonus
|
||||
elif mapping.proficient:
|
||||
@@ -331,13 +381,19 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||
for skill in newclass.skills[: newclass.starting_skills]:
|
||||
self.add_skill(skill, proficient=True, character_class=newclass)
|
||||
|
||||
# add hit dice
|
||||
existing = len(self.hit_dice[newclass.name])
|
||||
for lvl in range(level - existing):
|
||||
self._hit_dice.append(HitDie(character_id=self.id, character_class_id=newclass.id))
|
||||
|
||||
def remove_class(self, target):
|
||||
self.class_map = [m for m in self.class_map if m.character_class != target]
|
||||
for mapping in self.character_class_attribute_map:
|
||||
if mapping.character_class.id == target.id:
|
||||
if mapping.character_class == target:
|
||||
self.remove_class_attribute(mapping.class_attribute)
|
||||
for skill in target.skills:
|
||||
self.remove_skill(skill, proficient=True, expert=False, character_class=target)
|
||||
self.remove_skill(skill, proficient=True, expert=False, character_class=target)
|
||||
self._hit_dice = [die for die in self._hit_dice if die.character_class != target]
|
||||
self.class_map = [m for m in self.class_map if m.character_class != target]
|
||||
|
||||
def remove_class_attribute(self, attribute):
|
||||
self.character_class_attribute_map = [
|
||||
@@ -415,6 +471,34 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||
self._skills = [m for m in self._skills if m not in to_delete]
|
||||
return True
|
||||
|
||||
def apply_healing(self, value: int):
|
||||
self.hit_points = min(self.hit_points + value, self._max_hit_points)
|
||||
|
||||
def apply_damage(self, value: int, damage_type: str, magical=False):
|
||||
total = value
|
||||
if self.absorbs(damage_type, magical):
|
||||
return self.apply_healing(total)
|
||||
if self.immune(damage_type, magical):
|
||||
return
|
||||
if self.resistant(damage_type, magical):
|
||||
total = int(value / 2)
|
||||
elif self.vulnerable(damage_type, magical):
|
||||
total = value * 2
|
||||
|
||||
if total <= self.temp_hit_points:
|
||||
self.temp_hit_points -= total
|
||||
return
|
||||
|
||||
self.hit_points = max(0, self.hit_points - (total - self.temp_hit_points))
|
||||
self.temp_hit_points = 0
|
||||
return
|
||||
|
||||
def spend_hit_die(self, die):
|
||||
die.spent = True
|
||||
|
||||
def reset_hit_die(self, die):
|
||||
die.spent = False
|
||||
|
||||
def __after_insert__(self, session):
|
||||
"""
|
||||
Called by the session after_flush event listener to add default joins in other tables.
|
||||
|
||||
@@ -76,8 +76,8 @@ class CharacterClass(BaseObject):
|
||||
__tablename__ = "character_class"
|
||||
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(index=True, unique=True)
|
||||
hit_dice: Mapped[str] = mapped_column(default="1d6")
|
||||
hit_dice_stat: Mapped[str] = mapped_column(default="")
|
||||
hit_die_name: Mapped[str] = mapped_column(default="1d6")
|
||||
hit_die_stat_name: Mapped[str] = mapped_column(default="")
|
||||
starting_skills: int = mapped_column(nullable=False, default=0)
|
||||
|
||||
attributes = relationship("ClassAttributeMap", cascade="all,delete,delete-orphan", lazy="immediate")
|
||||
@@ -88,7 +88,7 @@ class CharacterClass(BaseObject):
|
||||
def add_skill(self, skill, expert=False):
|
||||
if not self.skills or skill not in self.skills:
|
||||
if not self.id:
|
||||
raise Exception(f"Cannot add a skill before the class has been persisted.")
|
||||
raise Exception("Cannot add a skill before the class has been persisted.")
|
||||
mapping = ClassSkillMap(character_class_id=self.id, skill_id=skill.id, proficient=True, expert=expert)
|
||||
self.skills.append(mapping)
|
||||
return True
|
||||
|
||||
@@ -198,7 +198,6 @@ class ModifierMixin:
|
||||
return get_attr(self, attr_name.split("."))
|
||||
|
||||
def _apply_one_modifier(self, modifier, initial, modified):
|
||||
print(f"Trying to apply {modifier}")
|
||||
if modifier.new_value is not None:
|
||||
return modifier.new_value
|
||||
elif modifier.absolute_value is not None:
|
||||
@@ -275,4 +274,4 @@ class ModifierMixin:
|
||||
self._get_modifiable_base(col.info.get("modifiable_base", col.name)),
|
||||
modifiable_class=col.info.get("modifiable_class", None),
|
||||
)
|
||||
raise AttributeError(f"No such attribute: {attr_name}.")
|
||||
raise AttributeError(f"No such attribute on {self.__class__.__name__} object: {attr_name}.")
|
||||
|
||||
Reference in New Issue
Block a user