import base64 from flask_sqlalchemy import SQLAlchemy from ..models import db, BaseModel class CharacterClass(BaseModel): __tablename__ = 'character_class' name = db.Column(db.String(50), unique=True, nullable=False) spellcaster = db.Column(db.String(10)) bucket = db.Column(db.String(50)) frequency_modifier = db.Column(db.Integer, default=1) prime_requisite = db.Column(db.String(3)) hit_die_size = db.Column(db.Integer) maximum_level = db.Column(db.Integer) armour_modifier = db.Column(db.Integer) melee_light = db.Column(db.Integer) melee_medium = db.Column(db.Integer) melee_heavy = db.Column(db.Integer) ranged_light = db.Column(db.Integer) ranged_heavy = db.Column(db.Integer) def __repr__(self): return ''.format(self.name) @property def is_divine_spellcaster(self): return self.spellcaster == 'Divine' @property def is_arcane_spellcaster(self): return self.spellcaster == 'Arcane' class ClassLevelProgression(BaseModel): __tablename__ = 'class_progression' level = db.Column(db.Integer) attack_throw = db.Column(db.Integer) save_petrification_paralysis = db.Column(db.Integer) save_poison_death = db.Column(db.Integer) save_blast_breath = db.Column(db.Integer) save_staffs_wands = db.Column(db.Integer) save_spells = db.Column(db.Integer) spellslots1 = db.Column(db.Integer) spellslots2 = db.Column(db.Integer) spellslots3 = db.Column(db.Integer) spellslots4 = db.Column(db.Integer) spellslots5 = db.Column(db.Integer) spellslots6 = db.Column(db.Integer) guild_id = db.Column(db.Integer, db.ForeignKey('character_class.id'), nullable=False) guild = db.relationship('CharacterClass', backref=db.backref('progressions', lazy=True)) def spell_slots(self, level=None): if level > self.guild.maximum_level: level = self.guild.maximum_level spellslots = [ self.spellslots1, self.spellslots2, self.spellslots3, self.spellslots4, self.spellslots5, self.spellslots6 ] if level: return spellslots[level - 1] return spellslots def __repr__(self): return ''.format(self.level, self.guild.name) class Spell(BaseModel): __tablename__ = 'spells' name = db.Column(db.String(50), unique=True, nullable=False) range = db.Column(db.String(50)) duration = db.Column(db.String(50)) arcane = db.Column(db.Integer, nullable=False) divine = db.Column(db.Integer, nullable=False) description = db.Column(db.Text(1000), nullable=False) def __repr__(self): return ''.format(self.name, self.school) def level_for(self, npc): return self.arcane if npc.is_arcane_spellcaster else self.divine @property def school(self): if self.is_arcane and self.is_divine: return 'Multi' if self.is_arcane: return 'Arcane' if self.is_divine: return 'Divine' @property def is_divine(self): return bool(self.divine and self.divine > 0) @property def is_arcane(self): return bool(self.arcane and self.arcane > 0) @property def roll20_format(self): spell_dict = { 'id': self.id, 'name': self.name, 'range': self.range, 'duration': self.duration, 'divine': self.divine, 'is_divine': self.is_divine, 'arcane': self.arcane, 'is_arcane': self.is_arcane, 'description': base64.b64encode(self.description.encode('ascii')).decode('ascii') } return spell_dict class EquipmentArmour(BaseModel): __tablename__ = 'eq_armour' name = db.Column(db.String(50), unique=True, nullable=False) gp_value = db.Column(db.Integer, nullable=False) ac_mod = db.Column(db.Integer, nullable=False) def __repr__(self): return ''.format(self.name) class EquipmentRangedWeapon(BaseModel): __tablename__ = 'eq_ranged_wep' name = db.Column(db.String(50), unique=True, nullable=False) bucket = db.Column(db.String(20), nullable=False) gp_value = db.Column(db.Integer, nullable=False) damage_die = db.Column(db.String(10), nullable=False) def __repr__(self): return ''.format(self.name) class EquipmentMeleeWeapon(BaseModel): __tablename__ = 'eq_melee_wep' name = db.Column(db.String(50), unique=False, nullable=False) bucket = db.Column(db.String(20), nullable=False) gp_value = db.Column(db.Integer, nullable=False) damage_die = db.Column(db.String(10), nullable=False) two_handed = db.Column(db.Boolean, nullable=False) def __repr__(self): return ''.format(self.name) class CharacterNPC(BaseModel): __tablename__ = 'npcs' name = db.Column(db.String(50), unique=False, nullable=True) level = db.Column(db.Integer, nullable=False) alignment = db.Column(db.String(20), unique=False, nullable=False) hit_points = db.Column(db.Integer, nullable=False) strength = db.Column(db.Integer, nullable=False) intelligence = db.Column(db.Integer, nullable=False) wisdom = db.Column(db.Integer, nullable=False) dexterity = db.Column(db.Integer, nullable=False) constitution = db.Column(db.Integer, nullable=False) charisma = db.Column(db.Integer, nullable=False) spells = db.Column(db.String(200)) guild_id = db.Column(db.Integer, db.ForeignKey('character_class.id'), nullable=False) guild = db.relationship('CharacterClass', backref=db.backref('npcs', lazy=True)) melee_id = db.Column(db.Integer, db.ForeignKey('eq_melee_wep.id'), nullable=False) melee = db.relationship('EquipmentMeleeWeapon') ranged_id = db.Column(db.Integer, db.ForeignKey('eq_ranged_wep.id'), nullable=True) ranged = db.relationship('EquipmentRangedWeapon') armour_id = db.Column(db.Integer, db.ForeignKey('eq_armour.id'), nullable=False) armour = db.relationship('EquipmentArmour') def __repr__(self): return ''.format(self.name) @staticmethod def calculate_attr_mod(attr): mod = 0 if attr <= 3: mod = -3 elif attr >= 4 and attr <= 5: mod = -2 elif attr >= 6 and attr <= 8: mod = -1 elif attr >= 9 and attr <= 12: mod = 0 elif attr >= 13 and attr <= 15: mod = 1 elif attr >= 16 and attr <= 16: mod = 2 elif attr >= 18: mod = 3 return mod def spell_slots(self, level=None): if level > self.guild.maximum_level: level = self.guild.maximum_level return self.current_progression.spell_slots(level) def spells_known(self, level=None): if level: if level > self.guild.maximum_level: level = self.guild.maximum_level # Return for a specific level if self.is_arcane_spellcaster: return (self.spell_slots(level) + self.wis_mod) return self.spell_slots(level) # Return for all levels if self.is_arcane_spellcaster: return [self.spell_slots(x) + self.wis_mod for x in range(1,6)] return [self.spell_slots(x) for x in range(1,6)] def spell_list(self, level=None): spell_ids = self.spells.split(',') spells = Spell.query.filter(Spell.id.in_(spell_ids)).all() spells_by_level = {} for spell in spells: spells_by_level.setdefault(spell.level_for(self), []).append(spell) return spells_by_level @property def roll20_format(self): npc_dict = { 'name': self.name, 'level': self.level, 'hp': self.hit_points, 'guild': self.guild.name, 'alignment': self.alignment, "attack_throw": self.attack_throw, 'attributes': { 'str': self.strength, 'int': self.intelligence, 'wis': self.wisdom, 'dex': self.dexterity, 'con': self.constitution, 'chr': self.charisma }, 'attribute_mods': { 'str': self.str_mod, 'int': self.int_mod, 'wis': self.wis_mod, 'dex': self.dex_mod, 'con': self.con_mod, 'chr': self.chr_mod }, "saves": { "pp": self.save_pp, "pd": self.save_pd, "bb": self.save_bb, "sw": self.save_sw, "sp": self.save_sp }, 'armour': { 'name': self.armour.name, 'value': self.armour.gp_value, 'ac_mod': self.armour.ac_mod }, 'melee': { 'name': self.melee.name, 'value': self.melee.gp_value, 'damage': self.melee.damage_die, 'throw_mod': 0, 'two_handed': self.melee.two_handed } } if self.guild.is_divine_spellcaster or self.guild.is_arcane_spellcaster: npc_dict['spells'] = [] spell_list = self.spell_list() for level in spell_list: for spell in spell_list[level]: npc_dict['spells'].append(spell.roll20_format) if self.ranged: npc_dict['ranged'] = { 'name': self.ranged.name, 'value': self.ranged.gp_value, 'damage': self.ranged.damage_die, 'throw_mod': 0 } return npc_dict @property def str(self): return self.strength @property def int(self): return self.intelligence @property def wis(self): return self.wisdom @property def dex(self): return self.dexterity @property def con(self): return self.constitution @property def chr(self): return self.charisma @property def str_mod(self): return self.calculate_attr_mod(self.strength) @property def int_mod(self): return self.calculate_attr_mod(self.intelligence) @property def wis_mod(self): return self.calculate_attr_mod(self.wisdom) @property def dex_mod(self): return self.calculate_attr_mod(self.dexterity) @property def con_mod(self): return self.calculate_attr_mod(self.constitution) @property def chr_mod(self): return self.calculate_attr_mod(self.charisma) @property def current_progression(self): return ClassLevelProgression.query.filter_by(guild_id=self.guild.id, level=self.level).first() @property def attack_throw(self): return self.current_progression.attack_throw @property def save_pp(self): return self.current_progression.save_petrification_paralysis @property def save_pd(self): return self.current_progression.save_petrification_paralysis @property def save_bb(self): return self.current_progression.save_blast_breath @property def save_sw(self): return self.current_progression.save_staffs_wands @property def save_sp(self): return self.current_progression.save_spells @property def hp(self): return self.hit_points @property def is_divine_spellcaster(self): return self.guild.is_divine_spellcaster @property def is_arcane_spellcaster(self): return self.guild.is_arcane_spellcaster def update(self, kwargs): # Allows us to update like a dict, used for inserting attributes zip self.__dict__.update(kwargs) admin_models = [CharacterClass, ClassLevelProgression, EquipmentArmour, EquipmentRangedWeapon, EquipmentMeleeWeapon, CharacterNPC, Spell]