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.

397 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 roll20_format(self):
  88. spell_dict = {
  89. 'id': self.id,
  90. 'name': self.name,
  91. 'range': self.range,
  92. 'duration': self.duration,
  93. 'divine': self.divine,
  94. 'is_divine': self.is_divine,
  95. 'arcane': self.arcane,
  96. 'is_arcane': self.is_arcane,
  97. 'description': base64.b64encode(self.description.encode('ascii')).decode('ascii')
  98. }
  99. return spell_dict
  100. class EquipmentArmour(BaseModel):
  101. __tablename__ = 'eq_armour'
  102. name = db.Column(db.String(50), unique=True, nullable=False)
  103. gp_value = db.Column(db.Integer, nullable=False)
  104. ac_mod = db.Column(db.Integer, nullable=False)
  105. def __repr__(self):
  106. return '<EquipmentArmour: {0}>'.format(self.name)
  107. class EquipmentRangedWeapon(BaseModel):
  108. __tablename__ = 'eq_ranged_wep'
  109. name = db.Column(db.String(50), unique=True, nullable=False)
  110. bucket = db.Column(db.String(20), nullable=False)
  111. gp_value = db.Column(db.Integer, nullable=False)
  112. damage_die = db.Column(db.String(10), nullable=False)
  113. def __repr__(self):
  114. return '<EquipmentRangedWeapon: {0}>'.format(self.name)
  115. class EquipmentMeleeWeapon(BaseModel):
  116. __tablename__ = 'eq_melee_wep'
  117. name = db.Column(db.String(50), unique=False, nullable=False)
  118. bucket = db.Column(db.String(20), nullable=False)
  119. gp_value = db.Column(db.Integer, nullable=False)
  120. damage_die = db.Column(db.String(10), nullable=False)
  121. two_handed = db.Column(db.Boolean, nullable=False)
  122. def __repr__(self):
  123. return '<EquipmentMeleeWeapon: {0}>'.format(self.name)
  124. class CharacterNPC(BaseModel):
  125. __tablename__ = 'npcs'
  126. name = db.Column(db.String(50), unique=False, nullable=True)
  127. level = db.Column(db.Integer, nullable=False)
  128. alignment = db.Column(db.String(20), unique=False, nullable=False)
  129. hit_points = db.Column(db.Integer, nullable=False)
  130. strength = db.Column(db.Integer, nullable=False)
  131. intelligence = db.Column(db.Integer, nullable=False)
  132. wisdom = db.Column(db.Integer, nullable=False)
  133. dexterity = db.Column(db.Integer, nullable=False)
  134. constitution = db.Column(db.Integer, nullable=False)
  135. charisma = db.Column(db.Integer, nullable=False)
  136. spells = db.Column(db.String(200))
  137. guild_id = db.Column(db.Integer, db.ForeignKey('character_class.id'), nullable=False)
  138. guild = db.relationship('CharacterClass', backref=db.backref('npcs', lazy=True))
  139. melee_id = db.Column(db.Integer, db.ForeignKey('eq_melee_wep.id'), nullable=False)
  140. melee = db.relationship('EquipmentMeleeWeapon')
  141. ranged_id = db.Column(db.Integer, db.ForeignKey('eq_ranged_wep.id'), nullable=True)
  142. ranged = db.relationship('EquipmentRangedWeapon')
  143. armour_id = db.Column(db.Integer, db.ForeignKey('eq_armour.id'), nullable=False)
  144. armour = db.relationship('EquipmentArmour')
  145. def __repr__(self):
  146. return '<CharacterNPC: {0}>'.format(self.name)
  147. @staticmethod
  148. def calculate_attr_mod(attr):
  149. mod = 0
  150. if attr <= 3:
  151. mod = -3
  152. elif attr >= 4 and attr <= 5:
  153. mod = -2
  154. elif attr >= 6 and attr <= 8:
  155. mod = -1
  156. elif attr >= 9 and attr <= 12:
  157. mod = 0
  158. elif attr >= 13 and attr <= 15:
  159. mod = 1
  160. elif attr >= 16 and attr <= 16:
  161. mod = 2
  162. elif attr >= 18:
  163. mod = 3
  164. return mod
  165. def spell_slots(self, level=None):
  166. if level > self.guild.maximum_level:
  167. level = self.guild.maximum_level
  168. return self.current_progression.spell_slots(level)
  169. def spells_known(self, level=None):
  170. if level:
  171. if level > self.guild.maximum_level:
  172. level = self.guild.maximum_level
  173. # Return for a specific level
  174. if self.is_arcane_spellcaster:
  175. return (self.spell_slots(level) + self.wis_mod)
  176. return self.spell_slots(level)
  177. # Return for all levels
  178. if self.is_arcane_spellcaster:
  179. return [self.spell_slots(x) + self.wis_mod for x in range(1,6)]
  180. return [self.spell_slots(x) for x in range(1,6)]
  181. def spell_list(self, level=None):
  182. spell_ids = self.spells.split(',')
  183. spells = Spell.query.filter(Spell.id.in_(spell_ids)).all()
  184. spells_by_level = {}
  185. for spell in spells:
  186. spells_by_level.setdefault(spell.level_for(self), []).append(spell)
  187. return spells_by_level
  188. @property
  189. def roll20_format(self):
  190. npc_dict = {
  191. 'name': self.name,
  192. 'level': self.level,
  193. 'hp': self.hit_points,
  194. 'guild': self.guild.name,
  195. 'alignment': self.alignment,
  196. "attack_throw": self.attack_throw,
  197. 'attributes': {
  198. 'str': self.strength,
  199. 'int': self.intelligence,
  200. 'wis': self.wisdom,
  201. 'dex': self.dexterity,
  202. 'con': self.constitution,
  203. 'chr': self.charisma
  204. },
  205. 'attribute_mods': {
  206. 'str': self.str_mod,
  207. 'int': self.int_mod,
  208. 'wis': self.wis_mod,
  209. 'dex': self.dex_mod,
  210. 'con': self.con_mod,
  211. 'chr': self.chr_mod
  212. },
  213. "saves": {
  214. "pp": self.save_pp,
  215. "pd": self.save_pd,
  216. "bb": self.save_bb,
  217. "sw": self.save_sw,
  218. "sp": self.save_sp
  219. },
  220. 'armour': {
  221. 'name': self.armour.name,
  222. 'value': self.armour.gp_value,
  223. 'ac_mod': self.armour.ac_mod
  224. },
  225. 'melee': {
  226. 'name': self.melee.name,
  227. 'value': self.melee.gp_value,
  228. 'damage': self.melee.damage_die,
  229. 'throw_mod': 0,
  230. 'two_handed': self.melee.two_handed
  231. }
  232. }
  233. if self.guild.is_divine_spellcaster or self.guild.is_arcane_spellcaster:
  234. npc_dict['spells'] = []
  235. spell_list = self.spell_list()
  236. for level in spell_list:
  237. for spell in spell_list[level]:
  238. npc_dict['spells'].append(spell.roll20_format)
  239. if self.ranged:
  240. npc_dict['ranged'] = {
  241. 'name': self.ranged.name,
  242. 'value': self.ranged.gp_value,
  243. 'damage': self.ranged.damage_die,
  244. 'throw_mod': 0
  245. }
  246. return npc_dict
  247. @property
  248. def str(self):
  249. return self.strength
  250. @property
  251. def int(self):
  252. return self.intelligence
  253. @property
  254. def wis(self):
  255. return self.wisdom
  256. @property
  257. def dex(self):
  258. return self.dexterity
  259. @property
  260. def con(self):
  261. return self.constitution
  262. @property
  263. def chr(self):
  264. return self.charisma
  265. @property
  266. def str_mod(self):
  267. return self.calculate_attr_mod(self.strength)
  268. @property
  269. def int_mod(self):
  270. return self.calculate_attr_mod(self.intelligence)
  271. @property
  272. def wis_mod(self):
  273. return self.calculate_attr_mod(self.wisdom)
  274. @property
  275. def dex_mod(self):
  276. return self.calculate_attr_mod(self.dexterity)
  277. @property
  278. def con_mod(self):
  279. return self.calculate_attr_mod(self.constitution)
  280. @property
  281. def chr_mod(self):
  282. return self.calculate_attr_mod(self.charisma)
  283. @property
  284. def current_progression(self):
  285. return ClassLevelProgression.query.filter_by(guild_id=self.guild.id, level=self.level).first()
  286. @property
  287. def attack_throw(self):
  288. return self.current_progression.attack_throw
  289. @property
  290. def save_pp(self):
  291. return self.current_progression.save_petrification_paralysis
  292. @property
  293. def save_pd(self):
  294. return self.current_progression.save_petrification_paralysis
  295. @property
  296. def save_bb(self):
  297. return self.current_progression.save_blast_breath
  298. @property
  299. def save_sw(self):
  300. return self.current_progression.save_staffs_wands
  301. @property
  302. def save_sp(self):
  303. return self.current_progression.save_spells
  304. @property
  305. def hp(self):
  306. return self.hit_points
  307. @property
  308. def is_divine_spellcaster(self):
  309. return self.guild.is_divine_spellcaster
  310. @property
  311. def is_arcane_spellcaster(self):
  312. return self.guild.is_arcane_spellcaster
  313. def update(self, kwargs):
  314. # Allows us to update like a dict, used for inserting attributes zip
  315. self.__dict__.update(kwargs)
  316. admin_models = [CharacterClass, ClassLevelProgression, EquipmentArmour, EquipmentRangedWeapon, EquipmentMeleeWeapon, CharacterNPC, Spell]