2024-04-28 14:30:47 -07:00
|
|
|
from sqlalchemy import ForeignKey, Text, UniqueConstraint
|
2024-02-23 10:45:38 -08:00
|
|
|
from sqlalchemy.ext.associationproxy import association_proxy
|
2024-04-28 14:30:47 -07:00
|
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
2024-02-18 19:30:41 -08:00
|
|
|
|
2024-04-28 14:30:47 -07:00
|
|
|
from ttfrog.db.base import BaseObject, SavingThrowsMixin, SkillsMixin, SlugMixin
|
|
|
|
from ttfrog.db.schema.classes import CharacterClass, ClassAttribute
|
2024-04-29 01:09:58 -07:00
|
|
|
from ttfrog.db.schema.modifiers import Modifier, ModifierMixin, Stat
|
2024-02-18 19:30:41 -08:00
|
|
|
|
|
|
|
__all__ = [
|
2024-03-26 00:53:21 -07:00
|
|
|
"Ancestry",
|
|
|
|
"AncestryTrait",
|
|
|
|
"AncestryTraitMap",
|
|
|
|
"CharacterClassMap",
|
|
|
|
"CharacterClassAttributeMap",
|
|
|
|
"Character",
|
2024-04-21 02:17:47 -07:00
|
|
|
"Modifier",
|
2024-02-18 19:30:41 -08:00
|
|
|
]
|
|
|
|
|
2024-02-26 01:12:45 -08:00
|
|
|
|
2024-02-23 10:45:38 -08:00
|
|
|
def class_map_creator(fields):
|
|
|
|
if isinstance(fields, CharacterClassMap):
|
|
|
|
return fields
|
|
|
|
return CharacterClassMap(**fields)
|
|
|
|
|
2024-03-24 16:56:13 -07:00
|
|
|
|
2024-02-26 01:12:45 -08:00
|
|
|
def attr_map_creator(fields):
|
|
|
|
if isinstance(fields, CharacterClassAttributeMap):
|
|
|
|
return fields
|
|
|
|
return CharacterClassAttributeMap(**fields)
|
|
|
|
|
2024-02-18 19:30:41 -08:00
|
|
|
|
|
|
|
class AncestryTraitMap(BaseObject):
|
|
|
|
__tablename__ = "trait_map"
|
2024-04-20 20:35:24 -07:00
|
|
|
__table_args__ = (UniqueConstraint("ancestry_id", "ancestry_trait_id"),)
|
2024-04-28 14:30:47 -07:00
|
|
|
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
|
|
|
|
ancestry_id: Mapped[int] = mapped_column(ForeignKey("ancestry.id"))
|
|
|
|
ancestry_trait_id: Mapped[int] = mapped_column(ForeignKey("ancestry_trait.id"), init=False)
|
|
|
|
trait: Mapped["AncestryTrait"] = relationship(uselist=False, lazy="immediate")
|
|
|
|
level: Mapped[int] = mapped_column(nullable=False, info={"min": 1, "max": 20})
|
2024-02-18 19:30:41 -08:00
|
|
|
|
|
|
|
|
2024-04-21 21:30:24 -07:00
|
|
|
class Ancestry(BaseObject, ModifierMixin):
|
2024-02-18 19:30:41 -08:00
|
|
|
"""
|
2024-04-21 21:30:24 -07:00
|
|
|
A character ancestry ("race"), which has zero or more AncestryTraits and Modifiers.
|
2024-02-18 19:30:41 -08:00
|
|
|
"""
|
2024-03-26 00:53:21 -07:00
|
|
|
|
2024-02-18 19:30:41 -08:00
|
|
|
__tablename__ = "ancestry"
|
2024-04-28 14:30:47 -07:00
|
|
|
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
|
|
|
|
name: Mapped[str] = mapped_column(unique=True, nullable=False)
|
|
|
|
|
|
|
|
creature_type: Mapped[str] = mapped_column(nullable=False, default="humanoid")
|
|
|
|
size: Mapped[str] = mapped_column(nullable=False, default="medium")
|
|
|
|
walk_speed: Mapped[int] = mapped_column(nullable=False, default=30, info={"min": 0, "max": 99})
|
|
|
|
|
|
|
|
_fly_speed: Mapped[int] = mapped_column(init=False, nullable=True, info={"min": 0, "max": 99})
|
|
|
|
_climb_speed: Mapped[int] = mapped_column(init=False, nullable=True, info={"min": 0, "max": 99})
|
|
|
|
_swim_speed: Mapped[int] = mapped_column(init=False, nullable=True, info={"min": 0, "max": 99})
|
|
|
|
|
|
|
|
_traits = relationship(
|
|
|
|
"AncestryTraitMap", init=False, uselist=True, cascade="all,delete,delete-orphan", lazy="immediate"
|
|
|
|
)
|
2024-02-18 19:30:41 -08:00
|
|
|
|
2024-04-20 23:27:47 -07:00
|
|
|
@property
|
|
|
|
def traits(self):
|
|
|
|
return [mapping.trait for mapping in self._traits]
|
|
|
|
|
2024-04-23 00:15:13 -07:00
|
|
|
@property
|
|
|
|
def speed(self):
|
|
|
|
return self.walk_speed
|
|
|
|
|
|
|
|
@property
|
|
|
|
def climb_speed(self):
|
|
|
|
return self._climb_speed or int(self.speed / 2)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def swim_speed(self):
|
|
|
|
return self._swim_speed or int(self.speed / 2)
|
|
|
|
|
2024-04-20 23:27:47 -07:00
|
|
|
def add_trait(self, trait, level=1):
|
2024-04-28 14:30:47 -07:00
|
|
|
if not self._traits or trait not in self._traits:
|
|
|
|
mapping = AncestryTraitMap(ancestry_id=self.id, trait=trait, level=level)
|
|
|
|
if not self._traits:
|
|
|
|
self._traits = [mapping]
|
|
|
|
else:
|
|
|
|
self._traits.append(mapping)
|
2024-04-20 23:27:47 -07:00
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2024-02-18 19:30:41 -08:00
|
|
|
def __repr__(self):
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
|
2024-04-23 00:15:13 -07:00
|
|
|
class AncestryTrait(BaseObject, ModifierMixin):
|
2024-02-18 19:30:41 -08:00
|
|
|
"""
|
|
|
|
A trait granted to a character via its Ancestry.
|
|
|
|
"""
|
2024-03-26 00:53:21 -07:00
|
|
|
|
2024-02-18 19:30:41 -08:00
|
|
|
__tablename__ = "ancestry_trait"
|
2024-04-28 14:30:47 -07:00
|
|
|
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
|
|
|
|
name: Mapped[str] = mapped_column(nullable=False)
|
|
|
|
description: Mapped[Text] = mapped_column(Text, default="")
|
2024-02-18 19:30:41 -08:00
|
|
|
|
2024-03-24 16:56:13 -07:00
|
|
|
def __repr__(self):
|
|
|
|
return self.name
|
|
|
|
|
2024-02-18 19:30:41 -08:00
|
|
|
|
2024-04-20 20:35:07 -07:00
|
|
|
class CharacterClassMap(BaseObject):
|
2024-02-18 19:30:41 -08:00
|
|
|
__tablename__ = "class_map"
|
2024-04-20 20:35:24 -07:00
|
|
|
__table_args__ = (UniqueConstraint("character_id", "character_class_id"),)
|
2024-04-28 14:30:47 -07:00
|
|
|
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
|
|
|
|
character: Mapped["Character"] = relationship(uselist=False, viewonly=True)
|
|
|
|
character_class: Mapped["CharacterClass"] = relationship(lazy="immediate")
|
2024-02-23 10:45:38 -08:00
|
|
|
|
2024-04-28 14:30:47 -07:00
|
|
|
character_id: Mapped[int] = mapped_column(ForeignKey("character.id"), init=False, nullable=False)
|
|
|
|
character_class_id: Mapped[int] = mapped_column(ForeignKey("character_class.id"), init=False, nullable=False)
|
|
|
|
|
|
|
|
level: Mapped[int] = mapped_column(nullable=False, info={"min": 1, "max": 20}, default=1)
|
2024-03-24 16:56:13 -07:00
|
|
|
|
|
|
|
def __repr__(self):
|
2024-04-14 11:37:34 -07:00
|
|
|
return "{self.character.name}, {self.character_class.name}, level {self.level}"
|
2024-02-18 19:30:41 -08:00
|
|
|
|
|
|
|
|
2024-04-20 20:35:07 -07:00
|
|
|
class CharacterClassAttributeMap(BaseObject):
|
2024-02-18 19:30:41 -08:00
|
|
|
__tablename__ = "character_class_attribute_map"
|
2024-04-20 20:35:24 -07:00
|
|
|
__table_args__ = (UniqueConstraint("character_id", "class_attribute_id"),)
|
2024-04-28 14:30:47 -07:00
|
|
|
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
|
|
|
|
character_id: Mapped[int] = mapped_column(ForeignKey("character.id"), nullable=False)
|
|
|
|
class_attribute_id: Mapped[int] = mapped_column(ForeignKey("class_attribute.id"), nullable=False)
|
|
|
|
option_id: Mapped[int] = mapped_column(ForeignKey("class_attribute_option.id"), nullable=False)
|
2024-02-26 01:12:45 -08:00
|
|
|
|
2024-04-28 14:30:47 -07:00
|
|
|
class_attribute: Mapped["ClassAttribute"] = relationship(lazy="immediate")
|
2024-03-26 00:53:21 -07:00
|
|
|
option = relationship("ClassAttributeOption", lazy="immediate")
|
2024-02-18 19:30:41 -08:00
|
|
|
|
2024-03-24 16:56:13 -07:00
|
|
|
character_class = relationship(
|
|
|
|
"CharacterClass",
|
|
|
|
secondary="class_map",
|
|
|
|
primaryjoin="CharacterClassAttributeMap.character_id == CharacterClassMap.character_id",
|
|
|
|
secondaryjoin="CharacterClass.id == CharacterClassMap.character_class_id",
|
2024-03-26 00:53:21 -07:00
|
|
|
viewonly=True,
|
2024-03-26 21:58:04 -07:00
|
|
|
uselist=False,
|
2024-03-24 16:56:13 -07:00
|
|
|
)
|
|
|
|
|
2024-02-18 19:30:41 -08:00
|
|
|
|
2024-04-29 01:09:58 -07:00
|
|
|
class Character(BaseObject, SlugMixin, SavingThrowsMixin, SkillsMixin, ModifierMixin):
|
2024-02-18 19:30:41 -08:00
|
|
|
__tablename__ = "character"
|
2024-04-29 01:09:58 -07:00
|
|
|
|
2024-04-28 14:30:47 -07:00
|
|
|
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
|
|
|
|
|
|
|
|
name: Mapped[str] = mapped_column(default="New Character", nullable=False)
|
2024-04-29 01:09:58 -07:00
|
|
|
_armor_class: Mapped[int] = mapped_column(default=10, nullable=False, info={"min": 1, "max": 99, "modify": True})
|
|
|
|
_hit_points: Mapped[int] = mapped_column(default=1, nullable=False, info={"min": 0, "max": 999, "modify": True})
|
|
|
|
_max_hit_points: Mapped[int] = mapped_column(
|
|
|
|
default=10, nullable=False, info={"min": 0, "max": 999, "modify": True}
|
|
|
|
)
|
2024-04-28 14:30:47 -07:00
|
|
|
temp_hit_points: Mapped[int] = mapped_column(default=0, nullable=False, info={"min": 0, "max": 999})
|
2024-04-29 01:09:58 -07:00
|
|
|
_strength: Mapped[int] = mapped_column(
|
|
|
|
nullable=False, default=10, info={"min": 0, "max": 30, "modify": True, "modify_class": Stat}
|
|
|
|
)
|
|
|
|
_dexterity: Mapped[int] = mapped_column(
|
|
|
|
nullable=False, default=10, info={"min": 0, "max": 30, "modify": True, "modify_class": Stat}
|
|
|
|
)
|
|
|
|
_constitution: Mapped[int] = mapped_column(
|
|
|
|
nullable=False, default=10, info={"min": 0, "max": 30, "modify": True, "modify_class": Stat}
|
|
|
|
)
|
|
|
|
_intelligence: Mapped[int] = mapped_column(
|
|
|
|
nullable=False, default=10, info={"min": 0, "max": 30, "modify": True, "modify_class": Stat}
|
|
|
|
)
|
|
|
|
_wisdom: Mapped[int] = mapped_column(
|
|
|
|
nullable=False, default=10, info={"min": 0, "max": 30, "modify": True, "modify_class": Stat}
|
|
|
|
)
|
|
|
|
_charisma: Mapped[int] = mapped_column(
|
|
|
|
nullable=False, default=10, info={"min": 0, "max": 30, "modify": True, "modify_class": Stat}
|
|
|
|
)
|
2024-04-28 14:30:47 -07:00
|
|
|
|
|
|
|
_vision: Mapped[int] = mapped_column(default=None, nullable=True, info={"min": 0})
|
|
|
|
|
|
|
|
proficiencies: Mapped[str] = mapped_column(nullable=False, default="")
|
2024-02-18 19:30:41 -08:00
|
|
|
|
2024-03-26 00:53:21 -07:00
|
|
|
class_map = relationship("CharacterClassMap", cascade="all,delete,delete-orphan")
|
2024-04-14 11:37:34 -07:00
|
|
|
class_list = association_proxy("class_map", "id", creator=class_map_creator)
|
2024-02-23 10:45:38 -08:00
|
|
|
|
2024-03-26 00:53:21 -07:00
|
|
|
character_class_attribute_map = relationship("CharacterClassAttributeMap", cascade="all,delete,delete-orphan")
|
2024-04-14 11:37:34 -07:00
|
|
|
attribute_list = association_proxy("character_class_attribute_map", "id", creator=attr_map_creator)
|
2024-02-18 19:30:41 -08:00
|
|
|
|
2024-04-28 14:30:47 -07:00
|
|
|
ancestry_id: Mapped[int] = mapped_column(ForeignKey("ancestry.id"), nullable=False, default="1")
|
|
|
|
ancestry: Mapped["Ancestry"] = relationship(uselist=False, default=None)
|
2024-03-24 16:56:13 -07:00
|
|
|
|
2024-04-21 02:17:47 -07:00
|
|
|
@property
|
|
|
|
def modifiers(self):
|
2024-04-21 21:30:24 -07:00
|
|
|
unified = {}
|
|
|
|
unified.update(**self.ancestry.modifiers)
|
2024-04-23 00:15:13 -07:00
|
|
|
for trait in self.traits:
|
|
|
|
unified.update(**trait.modifiers)
|
2024-04-21 21:30:24 -07:00
|
|
|
unified.update(**super().modifiers)
|
|
|
|
return unified
|
2024-04-21 02:17:47 -07:00
|
|
|
|
2024-03-26 21:58:04 -07:00
|
|
|
@property
|
|
|
|
def classes(self):
|
|
|
|
return dict([(mapping.character_class.name, mapping.character_class) for mapping in self.class_map])
|
|
|
|
|
2024-03-24 16:56:13 -07:00
|
|
|
@property
|
|
|
|
def traits(self):
|
2024-04-20 23:27:47 -07:00
|
|
|
return self.ancestry.traits
|
2024-03-24 16:56:13 -07:00
|
|
|
|
2024-04-20 23:33:36 -07:00
|
|
|
@property
|
|
|
|
def speed(self):
|
2024-04-21 02:17:47 -07:00
|
|
|
return self.apply_modifiers("speed", self.ancestry.speed)
|
|
|
|
|
2024-04-23 00:15:13 -07:00
|
|
|
@property
|
|
|
|
def climb_speed(self):
|
|
|
|
return self.apply_modifiers("climb_speed", self.ancestry.climb_speed)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def swim_speed(self):
|
|
|
|
return self.apply_modifiers("swim_speed", self.ancestry.swim_speed)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def fly_speed(self):
|
2024-04-29 01:09:58 -07:00
|
|
|
return self.apply_modifiers("fly_speed", self.ancestry.fly_speed)
|
2024-04-23 00:15:13 -07:00
|
|
|
|
2024-04-21 02:17:47 -07:00
|
|
|
@property
|
|
|
|
def size(self):
|
2024-04-29 01:09:58 -07:00
|
|
|
return self._apply_modifiers("size", self.ancestry.size)
|
2024-04-23 00:15:13 -07:00
|
|
|
|
|
|
|
@property
|
|
|
|
def vision_in_darkness(self):
|
|
|
|
return self.apply_modifiers("vision_in_darkness", self.vision if self.vision is not None else 0)
|
|
|
|
|
2024-03-24 16:56:13 -07:00
|
|
|
@property
|
|
|
|
def level(self):
|
|
|
|
return sum(mapping.level for mapping in self.class_map)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def levels(self):
|
|
|
|
return dict([(mapping.character_class.name, mapping.level) for mapping in self.class_map])
|
|
|
|
|
2024-03-26 21:58:04 -07:00
|
|
|
@property
|
|
|
|
def class_attributes(self):
|
|
|
|
return dict([(mapping.class_attribute.name, mapping.option) for mapping in self.character_class_attribute_map])
|
|
|
|
|
2024-03-24 16:56:13 -07:00
|
|
|
def add_class(self, newclass, level=1):
|
|
|
|
if level == 0:
|
|
|
|
return self.remove_class(newclass)
|
|
|
|
level_in_class = [mapping for mapping in self.class_map if mapping.character_class_id == newclass.id]
|
|
|
|
if level_in_class:
|
|
|
|
level_in_class = level_in_class[0]
|
|
|
|
level_in_class.level = level
|
2024-04-14 11:37:34 -07:00
|
|
|
else:
|
2024-04-28 14:30:47 -07:00
|
|
|
self.class_list.append(CharacterClassMap(character=self, character_class=newclass, level=level))
|
2024-04-14 11:37:34 -07:00
|
|
|
for lvl in range(1, level + 1):
|
|
|
|
if not newclass.attributes_by_level[lvl]:
|
|
|
|
continue
|
|
|
|
for attr_name, attr in newclass.attributes_by_level[lvl].items():
|
|
|
|
self.add_class_attribute(attr, attr.options[0])
|
2024-03-24 16:56:13 -07:00
|
|
|
|
|
|
|
def remove_class(self, target):
|
|
|
|
self.class_map = [m for m in self.class_map if m.id != target.id]
|
2024-03-26 21:58:04 -07:00
|
|
|
for mapping in self.character_class_attribute_map:
|
|
|
|
if mapping.character_class.id == target.id:
|
|
|
|
self.remove_class_attribute(mapping.class_attribute)
|
|
|
|
|
|
|
|
def remove_class_attribute(self, attribute):
|
|
|
|
self.character_class_attribute_map = [m for m in self.character_class_attribute_map if m.id != attribute.id]
|
|
|
|
|
|
|
|
def add_class_attribute(self, attribute, option):
|
|
|
|
for thisclass in self.classes.values():
|
2024-04-20 20:35:07 -07:00
|
|
|
current_level = self.levels[thisclass.name]
|
|
|
|
current_attributes = thisclass.attributes_by_level.get(current_level, {})
|
|
|
|
if attribute.name in current_attributes:
|
|
|
|
if attribute.name in self.class_attributes:
|
|
|
|
return True
|
2024-04-14 11:37:34 -07:00
|
|
|
self.attribute_list.append(
|
2024-04-23 00:15:13 -07:00
|
|
|
CharacterClassAttributeMap(character_id=self.id, class_attribute=attribute, option=option)
|
2024-03-26 21:58:04 -07:00
|
|
|
)
|
|
|
|
return True
|
|
|
|
return False
|