spells.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. from core.db import Spells
  2. class SpellStatus:
  3. none = 0
  4. casting = 1
  5. fire = 2
  6. cooling = 3
  7. done = 4
  8. cancelled = 5
  9. class CasterCheckStatus:
  10. clear = 0 ## Ready to cast
  11. busy = 1 ## Already casting
  12. lowEnergy = 2 ## Not enough energy
  13. dead = 3 ## Caster is dead
  14. cooling = 4 ## Spell is cooling
  15. class TargetCheckStatus:
  16. clear = 0
  17. notFacing = 1
  18. notInRange = 2
  19. dead = 3
  20. maxHealth = 4
  21. selfAttack = 5
  22. casterDead = 6
  23. class BasicCast:
  24. def __init__(self, caster, spell, targetSpawnId):
  25. self._caster = caster
  26. self._spell = spell
  27. self._targetSpawnId = targetSpawnId
  28. self._targets = []
  29. self._targetCheckTask = None
  30. self._casterCheckTask = None
  31. self._castingTask = None
  32. self.startCast()
  33. @property
  34. def caster(self): return self._caster
  35. @property
  36. def spell(self): return self._spell
  37. @property
  38. def targetSpawnId(self): return self._targetSpawnId
  39. @property
  40. def target(self):
  41. if self._targets: return self._targets[0]
  42. @property
  43. def targets(self): return self._targets
  44. @targets.setter
  45. def targets(self, targets):
  46. """
  47. @type target: core.character.Character
  48. """
  49. self._targets = targets
  50. def casterCheck(self):
  51. """ Checks the following:
  52. - If the caster isn't already casting.
  53. - If the caster has enough energy.
  54. - If the caster is dead.
  55. - If the spell isn't in cooldown.
  56. @rtype: int
  57. @return: See CasterCheckStatus
  58. """
  59. if self.caster.characterData.spells.isCasting:
  60. if self.caster == base.world.playerController.character:
  61. base.uiManager.actionMessenger.print("Already busy!", 2)
  62. return CasterCheckStatus.busy
  63. if self.spell.isCooling():
  64. if self.caster == base.world.playerController.character:
  65. base.uiManager.actionMessenger.print("Spell is cooling..", 2)
  66. return CasterCheckStatus.cooling
  67. if self.caster.characterData.stats.energy.value < self.spell.data.energyCost:
  68. if self.caster == base.world.playerController.character:
  69. base.uiManager.actionMessenger.print("Not enough energy", 2)
  70. return CasterCheckStatus.lowEnergy
  71. if self.caster.isDead(): return CasterCheckStatus.dead
  72. return CasterCheckStatus.clear
  73. def targetCheck(self, target):
  74. """
  75. - target is not dead
  76. - caster is not dead
  77. - caster is facing target
  78. - caster is in range of target
  79. - if impactPoints are positive
  80. - - check if target doesn't have max health
  81. - if impactPoints are negative
  82. - - check if target is not caster (cannot attack self)
  83. """
  84. if target.isDead(): return TargetCheckStatus.dead
  85. if self.caster.isDead(): return TargetCheckStatus.casterDead
  86. if self.spell.data.impactPoints < 0: # Attacking
  87. if (target == self.caster):
  88. if self.caster == base.world.playerController.character:
  89. base.uiManager.actionMessenger.print("Cannot attack self.", 2)
  90. return TargetCheckStatus.selfAttack
  91. elif self.spell.data.impactPoints > 0: # healing
  92. if (target.characterData.stats.health.value ==
  93. target.characterData.stats.health.max
  94. ):
  95. if self.caster == base.world.playerController.character:
  96. base.uiManager.actionMessenger.print("Max health reached!", 2)
  97. return TargetCheckStatus.maxHealth
  98. if (self.spell.data.facingAngle and not
  99. self.caster.canSee(
  100. target,
  101. self.spell.data.facingAngle
  102. )
  103. ):
  104. if self.caster == base.world.playerController.character:
  105. base.uiManager.actionMessenger.print("You are not facing the NPC.", 2)
  106. return TargetCheckStatus.notFacing
  107. if self.spell.data.rangeEnd and not self.inRange(target):
  108. if self.caster == base.world.playerController.character:
  109. base.uiManager.actionMessenger.print("Not in range.", 2)
  110. return TargetCheckStatus.notInRange
  111. return TargetCheckStatus.clear
  112. def targetIntervalCheck(self, task):
  113. if self.targetCheck(self.target) != TargetCheckStatus.clear:
  114. self.cancelCast()
  115. return task.done
  116. return task.again
  117. def inRange(self, target):
  118. diffVec = self.caster.getGlobalPos() - target.getGlobalPos()
  119. if (diffVec.getXy().length() >= self.spell.data.rangeStart and
  120. diffVec.getXy().length() <= self.spell.data.rangeEnd):
  121. return True
  122. return False
  123. def stopAllTasks(self):
  124. if self._targetCheckTask:
  125. taskMgr.remove(self._targetCheckTask)
  126. self._targetCheckTask = None
  127. if self._casterCheckTask:
  128. taskMgr.remove(self._casterCheckTask)
  129. self._casterCheckTask = None
  130. if self._castingTask:
  131. taskMgr.remove(self._castingTask)
  132. self._castingTask = None
  133. def cancelCast(self, spawnId=None):
  134. self.caster.hasDied.disconnect(self.cancelCast)
  135. if self.target and self.spell.data.targetType == Spells.targetTypes.selected:
  136. self.target.hasDied.disconnect(self.cancelCast)
  137. self.spell.setStatus(SpellStatus.cancelled)
  138. self.stopAllTasks()
  139. self.caster.characterData.spells.isCasting = False
  140. self.caster.actorNP.loop("idle")
  141. self.finalizeCast()
  142. def getTarget(self):
  143. target = base.world.player
  144. if self.targetSpawnId > 0: # None or player selected
  145. target = base.world.npcsManager.getSpawn(self.targetSpawnId)
  146. return target
  147. def getTargets(self):
  148. """
  149. Get multiple targets in range of the spell, excluding the caster.
  150. """
  151. targets = []
  152. if not self.caster.isDead():
  153. for character in base.world.npcsManager.spawns + [base.world.player]:
  154. if (character != self.caster and
  155. self.caster.characterData.fraction in character.characterData.enemies and
  156. not character.isDead() and
  157. self.inRange(character)
  158. ):
  159. targets.append(character)
  160. return targets
  161. def startCast(self):
  162. if self.casterCheck() != CasterCheckStatus.clear: return
  163. if self.spell.data.targetType in [Spells.targetTypes.selected, Spells.targetTypes.self]:
  164. if self.spell.data.targetType == Spells.targetTypes.selected:
  165. self.targets = [self.getTarget()]
  166. else:
  167. self.targets = [self.caster]
  168. if self.targetCheck(self.target) != TargetCheckStatus.clear:
  169. return
  170. if self.spell.data.targetType == Spells.targetTypes.selected:
  171. self.target.hasDied.connect(self.cancelCast)
  172. self._targetCheckTask = taskMgr.doMethodLater(
  173. .1,
  174. self.targetIntervalCheck,
  175. '{}_castTargetCheck'.format(self.caster.characterData.spawnData.id),
  176. )
  177. self.caster.hasDied.connect(self.cancelCast)
  178. self.doCastingTime()
  179. def doCastingTime(self):
  180. self.caster.characterData.spells.isCasting = True # TODO spells should know this by watching it's spell's
  181. if self.spell.data.castTime:
  182. self._castingTask = taskMgr.doMethodLater(
  183. self.spell.data.castTime,
  184. self.applySpell,
  185. '{}_cast'.format(self.caster.characterData.spawnData.id),
  186. extraArgs=[],
  187. )
  188. self.spell.setStatus(SpellStatus.casting)
  189. ## Start casting animation.
  190. self.caster.actorNP.loop("casting")
  191. else: self.applySpell()
  192. def applySpell(self):
  193. """ Apply spell (after casting-time)
  194. """
  195. self.caster.hasDied.disconnect(self.cancelCast)
  196. self.stopAllTasks()
  197. ## Reset animation.
  198. self.caster.actorNP.loop("idle")
  199. if self.caster.isDead():
  200. self.cancelCast()
  201. return
  202. ## Subtract used energy from caster.
  203. if self.spell.data.energyCost:
  204. self.caster.characterData.stats.energy.value -= self.spell.data.energyCost
  205. if self.spell.data.targetType == Spells.targetTypes.range:
  206. # Get all targets in range
  207. self.targets = self.getTargets()
  208. elif self.target and self.spell.data.targetType == Spells.targetTypes.selected:
  209. self.target.hasDied.disconnect(self.cancelCast)
  210. if self.spell.data.impactPoints:
  211. for target in self.targets:
  212. target.characterData.stats.health.value += self.spell.data.impactPoints
  213. if self.spell.data.impactPoints < 0 and not target.isDead():
  214. target.underAttackBy(int(self.caster.characterData.spawnData.id))
  215. #else: # healing
  216. self.spell.setStatus(SpellStatus.fire)
  217. self.caster.characterData.spells.isCasting = False # TODO spells should know this by watching it's spell's
  218. self.targets.clear()
  219. self.doCooldown()
  220. def doCooldown(self):
  221. """ Wait for cooldown time before calling self.finalizeCast()
  222. """
  223. if self.spell.data.coolDown:
  224. self.spell.setStatus(SpellStatus.cooling)
  225. taskMgr.doMethodLater(
  226. self.spell.data.coolDown,
  227. self.finalizeCast,
  228. '{}_finalizeCast'.format(self.caster.characterData.spawnData.id),
  229. extraArgs=[]
  230. )
  231. else: self.finalizeCast()
  232. def finalizeCast(self):
  233. """ Finalize the cast.
  234. """
  235. self.spell.setStatus(SpellStatus.done)
  236. ## Note: make sure made connections are disconnected, else
  237. ## they leave a reference and this instance won't get destructed.
  238. # ..