diff --git a/src/ttfrog/db/schema/character.py b/src/ttfrog/db/schema/character.py index c0b0789..e3f0086 100644 --- a/src/ttfrog/db/schema/character.py +++ b/src/ttfrog/db/schema/character.py @@ -96,6 +96,7 @@ class CharacterClassAttributeMap(BaseObject, IterableMixin): primaryjoin="CharacterClassAttributeMap.character_id == CharacterClassMap.character_id", secondaryjoin="CharacterClass.id == CharacterClassMap.character_class_id", viewonly=True, + uselist=False, ) @@ -117,14 +118,18 @@ class Character(*Bases, SavingThrowsMixin, SkillsMixin): proficiencies = Column(String) class_map = relationship("CharacterClassMap", cascade="all,delete,delete-orphan") - classes = association_proxy("class_map", "id", creator=class_map_creator) + _classes = association_proxy("class_map", "id", creator=class_map_creator) character_class_attribute_map = relationship("CharacterClassAttributeMap", cascade="all,delete,delete-orphan") - class_attributes = association_proxy("character_class_attribute_map", "id", creator=attr_map_creator) + _class_attributes = association_proxy("character_class_attribute_map", "id", creator=attr_map_creator) ancestry_id = Column(Integer, ForeignKey("ancestry.id"), nullable=False, default="1") ancestry = relationship("Ancestry", uselist=False) + @property + def classes(self): + return dict([(mapping.character_class.name, mapping.character_class) for mapping in self.class_map]) + @property def traits(self): return [mapping.trait for mapping in self.ancestry.traits] @@ -137,6 +142,10 @@ class Character(*Bases, SavingThrowsMixin, SkillsMixin): def levels(self): return dict([(mapping.character_class.name, mapping.level) for mapping in self.class_map]) + @property + def class_attributes(self): + return dict([(mapping.class_attribute.name, mapping.option) for mapping in self.character_class_attribute_map]) + def add_class(self, newclass, level=1): if level == 0: return self.remove_class(newclass) @@ -145,7 +154,24 @@ class Character(*Bases, SavingThrowsMixin, SkillsMixin): level_in_class = level_in_class[0] level_in_class.level = level return - self.classes.append(CharacterClassMap(character_id=self.id, character_class_id=newclass.id, level=level)) + self._classes.append(CharacterClassMap(character_id=self.id, character_class_id=newclass.id, level=level)) def remove_class(self, target): self.class_map = [m for m in self.class_map if m.id != target.id] + 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(): + if attribute.name in thisclass.attributes_by_level.get(self.levels[thisclass.name], {}): + self._class_attributes.append( + CharacterClassAttributeMap( + character_id=self.id, class_attribute_id=attribute.id, option_id=option.id + ) + ) + return True + return False diff --git a/src/ttfrog/db/schema/classes.py b/src/ttfrog/db/schema/classes.py index 21e4550..0f20482 100644 --- a/src/ttfrog/db/schema/classes.py +++ b/src/ttfrog/db/schema/classes.py @@ -16,12 +16,14 @@ class ClassAttributeMap(BaseObject, IterableMixin): class_attribute_id = Column(Integer, ForeignKey("class_attribute.id"), primary_key=True) character_class_id = Column(Integer, ForeignKey("character_class.id"), primary_key=True) level = Column(Integer, nullable=False, info={"min": 1, "max": 20}, default=1) + attribute = relationship("ClassAttribute", uselist=False, viewonly=True, lazy="immediate") class ClassAttribute(BaseObject, IterableMixin): __tablename__ = "class_attribute" id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String, nullable=False) + options = relationship("ClassAttributeOption", cascade="all,delete,delete-orphan", lazy="immediate") def __repr__(self): return f"{self.id}: {self.name}" @@ -32,7 +34,6 @@ class ClassAttributeOption(BaseObject, IterableMixin): id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String, nullable=False) attribute_id = Column(Integer, ForeignKey("class_attribute.id"), nullable=False) - # attribute = relationship("ClassAttribute", uselist=False) class CharacterClass(*Bases, SavingThrowsMixin, SkillsMixin): @@ -42,4 +43,11 @@ class CharacterClass(*Bases, SavingThrowsMixin, SkillsMixin): hit_dice = Column(String, default="1d6") hit_dice_stat = Column(Enum(StatsEnum)) proficiencies = Column(String) - attributes = relationship("ClassAttributeMap") + attributes = relationship("ClassAttributeMap", cascade="all,delete,delete-orphan", lazy="immediate") + + @property + def attributes_by_level(self): + by_level = {} + for mapping in self.attributes: + by_level[mapping.level] = {mapping.attribute.name: mapping.attribute} + return by_level diff --git a/test/fixtures/classes.json b/test/fixtures/classes.json index 3d1abc3..5e9ea29 100644 --- a/test/fixtures/classes.json +++ b/test/fixtures/classes.json @@ -1,6 +1,7 @@ { "CharacterClass": [ { + "id": 1, "name": "fighter", "hit_dice": "1d10", "hit_dice_stat": "CON", @@ -9,6 +10,7 @@ "skills": ["Acrobatics", "Animal Handling", "Athletics", "History", "Insight", "Intimidation", "Perception", "Survival"] }, { + "id": 2, "name": "rogue", "hit_dice": "1d8", "hit_dice_stat": "DEX", @@ -16,5 +18,28 @@ "saving_throws": ["DEX", "INT"], "skills": ["Acrobatics", "Athletics", "Deception", "Insight", "Intimidation", "Investigation", "Perception", "Performance", "Persuasion", "Sleight of Hand", "Stealth"] } + ], + "ClassAttribute": [ + { + "id": 1, + "name": "Fighting Style" + } + ], + "ClassAttributeMap": [ + { + "class_attribute_id": 1, + "character_class_id": 1, + "level": 2 + } + ], + "ClassAttributeOption": [ + { + "attribute_id": 1, + "name": "Archery" + }, + { + "attribute_id": 1, + "name": "Battlemaster" + } ] } diff --git a/test/test_schema.py b/test/test_schema.py index 08c36ea..bbe5df5 100644 --- a/test/test_schema.py +++ b/test/test_schema.py @@ -26,16 +26,26 @@ def test_create_character(db, classes, ancestries): db.add(char) assert char.levels == {"fighter": 1} assert char.level == 1 - assert char.class_attributes == [] + assert char.class_attributes == {} + + # 'fighting style' is available, but not at this level + fighting_style = char.classes["fighter"].attributes_by_level[2]["Fighting Style"] + assert char.add_class_attribute(fighting_style, fighting_style.options[0]) is False + db.add(char) + assert char.class_attributes == {} # level up char.add_class(classes["fighter"], level=2) db.add(char) assert char.levels == {"fighter": 2} assert char.level == 2 - assert char.class_attributes == [] - # multiclass + # Assign the fighting style + assert char.add_class_attribute(fighting_style, fighting_style.options[0]) + db.add(char) + assert char.class_attributes[fighting_style.name] == fighting_style.options[0] + + # classes char.add_class(classes["rogue"], level=1) db.add(char) assert char.level == 3 @@ -51,6 +61,7 @@ def test_create_character(db, classes, ancestries): char.remove_class(classes["fighter"]) db.add(char) - # ensure we're not persisting any orphan records in the map table + # ensure we're not persisting any orphan records in the map tables dump = db.dump() assert dump["class_map"] == [] + assert dump["character_class_attribute_map"] == []