You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

401 lines
12 KiB

  1. import base64
  2. from flask_sqlalchemy import SQLAlchemy
  3. from ..models import db, BaseModel
  4. class CharacterClass(BaseModel):
  5. __tablename__ = 'character_class'
  6. name = db.Column(db.String(50), unique=True, nullable=False)
  7. spellcaster = db.Column(db.String(10))
  8. bucket = db.Column(db.String(50))
  9. frequency_modifier = db.Column(db.Integer, default=1)
  10. prime_requisite = db.Column(db.String(3))
  11. hit_die_size = db.Column(db.Integer)
  12. maximum_level = db.Column(db.Integer)
  13. armour_modifier = db.Column(db.Integer)
  14. melee_light = db.Column(db.Integer)
  15. melee_medium = db.Column(db.Integer)
  16. melee_heavy = db.Column(db.Integer)
  17. ranged_light = db.Column(db.Integer)
  18. ranged_heavy = db.Column(db.Integer)
  19. def __repr__(self):
  20. return '<CharacterClass: {0}>'.format(self.name)
  21. @property
  22. def is_divine_spellcaster(self):
  23. return self.spellcaster == 'Divine'
  24. @property
  25. def is_arcane_spellcaster(self):
  26. return self.spellcaster == 'Arcane'
  27. class ClassLevelProgression(BaseModel):
  28. __tablename__ = 'class_progression'
  29. level = db.Column(db.Integer)
  30. attack_throw = db.Column(db.Integer)
  31. save_petrification_paralysis = db.Column(db.Integer)
  32. save_poison_death = db.Column(db.Integer)
  33. save_blast_breath = db.Column(db.Integer)
  34. save_staffs_wands = db.Column(db.Integer)
  35. save_spells = db.Column(db.Integer)
  36. spellslots1 = db.Column(db.Integer)
  37. spellslots2 = db.Column(db.Integer)
  38. spellslots3 = db.Column(db.Integer)
  39. spellslots4 = db.Column(db.Integer)
  40. spellslots5 = db.Column(db.Integer)
  41. spellslots6 = db.Column(db.Integer)
  42. guild_id = db.Column(db.Integer, db.ForeignKey('character_class.id'), nullable=False)
  43. guild = db.relationship('CharacterClass', backref=db.backref('progressions', lazy=True))
  44. def spell_slots(self, level=None):
  45. if level > self.guild.maximum_level:
  46. level = self.guild.maximum_level
  47. spellslots = [
  48. self.spellslots1,
  49. self.spellslots2,
  50. self.spellslots3,
  51. self.spellslots4,
  52. self.spellslots5,
  53. self.spellslots6
  54. ]
  55. if level:
  56. return spellslots[level - 1]
  57. return spellslots
  58. def __repr__(self):
  59. return '<LevelProgression: {0} {1}>'.format(self.level, self.guild.name)
  60. class Spell(BaseModel):
  61. __tablename__ = 'spells'
  62. name = db.Column(db.String(50), unique=True, nullable=False)
  63. range = db.Column(db.String(50))
  64. duration = db.Column(db.String(50))
  65. arcane = db.Column(db.Integer, nullable=False)
  66. divine = db.Column(db.Integer, nullable=False)
  67. description = db.Column(db.Text(1000), nullable=False)
  68. def __repr__(self):
  69. return '<Spell: {0} ({1})>'.format(self.name, self.school)
  70. def level_for(self, npc):
  71. return self.arcane if npc.is_arcane_spellcaster else self.divine
  72. @property
  73. def school(self):
  74. if self.is_arcane and self.is_divine:
  75. return 'Multi'
  76. if self.is_arcane:
  77. return 'Arcane'
  78. if self.is_divine:
  79. return 'Divine'
  80. @property
  81. def is_divine(self):
  82. return bool(self.divine and self.divine > 0)
  83. @property
  84. def is_arcane(self):
  85. return bool(self.arcane and self.arcane > 0)
  86. @property
  87. def dom_id(self):
  88. return self.name.lower().replace("*", "").replace("'", "").replace(",", "").replace(" ", "_")
  89. @property
  90. def roll20_format(self):
  91. spell_dict = {
  92. 'id': self.id,
  93. 'name': self.name,
  94. 'range': self.range,
  95. 'duration': self.duration,
  96. 'divine': self.divine,
  97. 'is_divine': self.is_divine,
  98. 'arcane': self.arcane,
  99. 'is_arcane': self.is_arcane,
  100. 'description': base64.b64encode(self.description.encode('ascii')).decode('ascii')
  101. }
  102. return spell_dict
  103. class EquipmentArmour(BaseModel):
  104. __tablename__ = 'eq_armour'
  105. name = db.Column(db.String(50), unique=True, nullable=False)
  106. gp_value = db.Column(db.Integer, nullable=False)
  107. ac_mod = db.Column(db.Integer, nullable=False)
  108. def __repr__(self):
  109. return '<EquipmentArmour: {0}>'.format(self.name)
  110. class EquipmentRangedWeapon(BaseModel):
  111. __tablename__ = 'eq_ranged_wep'
  112. name = db.Column(db.String(50), unique=True, nullable=False)
  113. bucket = db.Column(db.String(20), nullable=False)
  114. gp_value = db.Column(db.Integer, nullable=False)
  115. damage_die = db.Column(db.String(10), nullable=False)
  116. def __repr__(self):
  117. return '<EquipmentRangedWeapon: {0}>'.format(self.name)
  118. class EquipmentMeleeWeapon(BaseModel):
  119. __tablename__ = 'eq_melee_wep'
  120. name = db.Column(db.String(50), unique=False, nullable=False)
  121. bucket = db.Column(db.String(20), nullable=False)
  122. gp_value = db.Column(db.Integer, nullable=False)
  123. damage_die = db.Column(db.String(10), nullable=False)
  124. two_handed = db.Column(db.Boolean, nullable=False)
  125. def __repr__(self):
  126. return '<EquipmentMeleeWeapon: {0}>'.format(self.name)
  127. class CharacterNPC(BaseModel):
  128. __tablename__ = 'npcs'
  129. name = db.Column(db.String(50), unique=False, nullable=True)
  130. level = db.Column(db.Integer, nullable=False)
  131. alignment = db.Column(db.String(20), unique=False, nullable=False)
  132. hit_points = db.Column(db.Integer, nullable=False)
  133. strength = db.Column(db.Integer, nullable=False)
  134. intelligence = db.Column(db.Integer, nullable=False)
  135. wisdom = db.Column(db.Integer, nullable=False)
  136. dexterity = db.Column(db.Integer, nullable=False)
  137. constitution = db.Column(db.Integer, nullable=False)
  138. charisma = db.Column(db.Integer, nullable=False)
  139. spells = db.Column(db.String(200))
  140. guild_id = db.Column(db.Integer, db.ForeignKey('character_class.id'), nullable=False)
  141. guild = db.relationship('CharacterClass', backref=db.backref('npcs', lazy=True))
  142. melee_id = db.Column(db.Integer, db.ForeignKey('eq_melee_wep.id'), nullable=False)
  143. melee = db.relationship('EquipmentMeleeWeapon')
  144. ranged_id = db.Column(db.Integer, db.ForeignKey('eq_ranged_wep.id'), nullable=True)
  145. ranged = db.relationship('EquipmentRangedWeapon')
  146. armour_id = db.Column(db.Integer, db.ForeignKey('eq_armour.id'), nullable=False)
  147. armour = db.relationship('EquipmentArmour')
  148. def __repr__(self):
  149. return '<CharacterNPC: {0}>'.format(self.name)
  150. @staticmethod
  151. def calculate_attr_mod(attr):
  152. mod = 0
  153. if attr <= 3:
  154. mod = -3
  155. elif attr >= 4 and attr <= 5:
  156. mod = -2
  157. elif attr >= 6 and attr <= 8:
  158. mod = -1
  159. elif attr >= 9 and attr <= 12:
  160. mod = 0
  161. elif attr >= 13 and attr <= 15:
  162. mod = 1
  163. elif attr >= 16 and attr <= 16:
  164. mod = 2
  165. elif attr >= 18:
  166. mod = 3
  167. return mod
  168. def spell_slots(self, level=None):
  169. if level > self.guild.maximum_level:
  170. level = self.guild.maximum_level
  171. return self.current_progression.spell_slots(level)
  172. def spells_known(self, level=None):
  173. if level:
  174. if level > self.guild.maximum_level:
  175. level = self.guild.maximum_level
  176. # Return for a specific level
  177. if self.is_arcane_spellcaster:
  178. return (self.spell_slots(level) + self.wis_mod)
  179. return self.spell_slots(level)
  180. # Return for all levels
  181. if self.is_arcane_spellcaster:
  182. return [self.spell_slots(x) + self.wis_mod for x in range(1,6)]
  183. return [self.spell_slots(x) for x in range(1,6)]
  184. def spell_list(self, level=None):
  185. spell_ids = self.spells.split(',')
  186. spells = Spell.query.filter(Spell.id.in_(spell_ids)).all()
  187. spells_by_level = {}
  188. for spell in spells:
  189. spells_by_level.setdefault(spell.level_for(self), []).append(spell)
  190. return spells_by_level
  191. @property
  192. def roll20_format(self):
  193. npc_dict = {
  194. 'name': self.name,
  195. 'level': self.level,
  196. 'hp': self.hit_points,
  197. 'guild': self.guild.name,
  198. 'alignment': self.alignment,
  199. "attack_throw": self.attack_throw,
  200. 'attributes': {
  201. 'str': self.strength,
  202. 'int': self.intelligence,
  203. 'wis': self.wisdom,
  204. 'dex': self.dexterity,
  205. 'con': self.constitution,
  206. 'chr': self.charisma
  207. },
  208. 'attribute_mods': {
  209. 'str': self.str_mod,
  210. 'int': self.int_mod,
  211. 'wis': self.wis_mod,
  212. 'dex': self.dex_mod,
  213. 'con': self.con_mod,
  214. 'chr': self.chr_mod
  215. },
  216. "saves": {
  217. "pp": self.save_pp,
  218. "pd": self.save_pd,
  219. "bb": self.save_bb,
  220. "sw": self.save_sw,
  221. "sp": self.save_sp
  222. },
  223. 'armour': {
  224. 'name': self.armour.name,
  225. 'value': self.armour.gp_value,
  226. 'ac_mod': self.armour.ac_mod
  227. },
  228. 'melee': {
  229. 'name': self.melee.name,
  230. 'value': self.melee.gp_value,
  231. 'damage': self.melee.damage_die,
  232. 'throw_mod': 0,
  233. 'two_handed': self.melee.two_handed
  234. }
  235. }
  236. if self.guild.is_divine_spellcaster or self.guild.is_arcane_spellcaster:
  237. npc_dict['spells'] = []
  238. spell_list = self.spell_list()
  239. for level in spell_list:
  240. for spell in spell_list[level]:
  241. npc_dict['spells'].append(spell.roll20_format)
  242. if self.ranged:
  243. npc_dict['ranged'] = {
  244. 'name': self.ranged.name,
  245. 'value': self.ranged.gp_value,
  246. 'damage': self.ranged.damage_die,
  247. 'throw_mod': 0
  248. }
  249. return npc_dict
  250. @property
  251. def str(self):
  252. return self.strength
  253. @property
  254. def int(self):
  255. return self.intelligence
  256. @property
  257. def wis(self):
  258. return self.wisdom
  259. @property
  260. def dex(self):
  261. return self.dexterity
  262. @property
  263. def con(self):
  264. return self.constitution
  265. @property
  266. def chr(self):
  267. return self.charisma
  268. @property
  269. def str_mod(self):
  270. return self.calculate_attr_mod(self.strength)
  271. @property
  272. def int_mod(self):
  273. return self.calculate_attr_mod(self.intelligence)
  274. @property
  275. def wis_mod(self):
  276. return self.calculate_attr_mod(self.wisdom)
  277. @property
  278. def dex_mod(self):
  279. return self.calculate_attr_mod(self.dexterity)
  280. @property
  281. def con_mod(self):
  282. return self.calculate_attr_mod(self.constitution)
  283. @property
  284. def chr_mod(self):
  285. return self.calculate_attr_mod(self.charisma)
  286. @property
  287. def current_progression(self):
  288. return ClassLevelProgression.query.filter_by(guild_id=self.guild.id, level=self.level).first()
  289. @property
  290. def attack_throw(self):
  291. return self.current_progression.attack_throw
  292. @property
  293. def save_pp(self):
  294. return self.current_progression.save_petrification_paralysis
  295. @property
  296. def save_pd(self):
  297. return self.current_progression.save_petrification_paralysis
  298. @property
  299. def save_bb(self):
  300. return self.current_progression.save_blast_breath
  301. @property
  302. def save_sw(self):
  303. return self.current_progression.save_staffs_wands
  304. @property
  305. def save_sp(self):
  306. return self.current_progression.save_spells
  307. @property
  308. def hp(self):
  309. return self.hit_points
  310. @property
  311. def is_divine_spellcaster(self):
  312. return self.guild.is_divine_spellcaster
  313. @property
  314. def is_arcane_spellcaster(self):
  315. return self.guild.is_arcane_spellcaster
  316. def update(self, kwargs):
  317. # Allows us to update like a dict, used for inserting attributes zip
  318. self.__dict__.update(kwargs)
  319. admin_models = [CharacterClass, ClassLevelProgression, EquipmentArmour, EquipmentRangedWeapon, EquipmentMeleeWeapon, CharacterNPC, Spell]