npcsManager.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. ########################################################################
  2. # Hello Worlds - Libre 3D RPG game.
  3. # Copyright (C) 2020 CYBERDEViL
  4. #
  5. # This file is part of Hello Worlds.
  6. #
  7. # Hello Worlds is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # Hello Worlds is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  19. #
  20. ########################################################################
  21. from panda3d.core import Vec3
  22. from panda3d.bullet import BulletRigidBodyNode
  23. from panda3d.ai import AIWorld, AICharacter
  24. from core.db import NPCs
  25. from core.worldMouse import NPCMouse
  26. from core.models import SelectedNpcModel
  27. from core.character import Character, CharacterModel
  28. import random
  29. class AI:
  30. def __init__(self, npc, aiWorld):
  31. """
  32. @param npc:
  33. @type npc: core.npcsManager.NPC
  34. @param aiWorld:
  35. @type aiWorld: panda3d.ai.AIWorld
  36. """
  37. self._npc = npc
  38. self._aiWorld = aiWorld
  39. self._activeSteering = None
  40. self._selectedSpell = None
  41. self._selectedAttacker = None
  42. self._beenUnderAttack = False
  43. self._onWayHome = False
  44. self.AIchar = AICharacter(
  45. "spawnId_{0}".format(npc.characterData.spawnData.id),
  46. npc.characterNP, 100, 6, 10)
  47. self.AIbehaviors = self.AIchar.getAiBehaviors()
  48. self._aiWorld.addAiChar(self.AIchar)
  49. self.AIchar.setMaxForce(6)
  50. def destroy(self):
  51. self._aiWorld.removeAiChar("spawnId_{0}".format(self._npc.characterData.spawnData.id))
  52. #del self.AIchar
  53. self.AIchar = None
  54. self._activeSteering = None
  55. self._selectedSpell = None
  56. self._selectedAttacker = None
  57. self._beenUnderAttack = False
  58. self._onWayHome = False
  59. self._npc = None
  60. self._aiWorld = None
  61. def lookAt(self, attacker):
  62. """
  63. @param attacker:
  64. @type attacker: core.character.Character
  65. """
  66. self._npc.characterNP.lookAt(attacker.characterNP)
  67. def getFirstAttacker(self):
  68. """ Note: do check if it has any attackers before calling this.
  69. @rtype: core.character.Character
  70. @return:
  71. """
  72. return self.getAttacker(self._npc._underAttackBy[0])
  73. def getRandomAttacker(self):
  74. """ Note: do check if it has any attackers before calling this.
  75. @rtype: core.character.Character
  76. @return:
  77. """
  78. return self.getAttacker(
  79. random.choice(self._npc._underAttackBy)
  80. )
  81. def getAttacker(self, spawnId):
  82. """ Get attacker it's core.character.Character object
  83. @param spawnId: Attacker it's spawnId
  84. @type spawnId: int
  85. @rtype: core.character.Character
  86. @return:
  87. """
  88. if spawnId > 0: return base.world.npcsManager.getSpawn(spawnId)
  89. return base.world.player
  90. def getRandomSpell(self):
  91. """ Returns a random spell to cast.
  92. @rtype: core.models.SpellModel
  93. @return:
  94. """
  95. return random.choice(list(self._npc.characterData.spells))
  96. def distanceBetween(self, pos, pos2):
  97. # TODO make this a util function or something ..
  98. # This doesn't consider height or Z axis.
  99. diffVec = pos - pos2
  100. diffVecXY = diffVec.getXy()
  101. return diffVecXY.length()
  102. def removeSteering(self):
  103. if self._activeSteering:
  104. self.AIbehaviors.removeAi('all')
  105. self._activeSteering = None
  106. def update(self):
  107. if self._npc._underAttackBy:
  108. if not list(self._npc.characterData.spells): return
  109. if self._onWayHome: self.AIbehaviors.removeAi('pathfollow')
  110. if not self._selectedAttacker:
  111. self._selectedAttacker = self.getFirstAttacker()
  112. if not self._selectedAttacker: return
  113. if self._selectedAttacker.isDead():
  114. self._selectedAttacker = None
  115. return
  116. if (self._npc.characterData.spells.isCasting or
  117. self._npc.isDead()): return
  118. self._beenUnderAttack = True
  119. if not self._selectedSpell:
  120. self._selectedSpell = self.getRandomSpell()
  121. if self._selectedSpell.isCooling():
  122. return
  123. if self._npc.characterData.stats.energy.value < self._selectedSpell.data.energyCost:
  124. return
  125. self.lookAt(self._selectedAttacker)
  126. if self._selectedSpell.data.rangeEnd:
  127. distance = self.distanceBetween(
  128. self._npc.getGlobalPos(),
  129. self._selectedAttacker.getGlobalPos()
  130. )
  131. ## Get closer
  132. if distance >= self._selectedSpell.data.rangeEnd - 1:
  133. if self._activeSteering != 'persue':
  134. self.removeSteering()
  135. self.AIbehaviors.pursue(self._selectedAttacker.characterNP, 10)
  136. self._activeSteering = 'persue'
  137. return
  138. ## Take more distance
  139. if distance <= self._selectedSpell.data.rangeStart + 0.5:
  140. if self._activeSteering != 'flee':
  141. self.removeSteering()
  142. self.AIbehaviors.flee(self._selectedAttacker.characterNP, 10)
  143. self._activeSteering = 'flee'
  144. return
  145. self.removeSteering()
  146. self._npc.startCast(self._selectedSpell.data.id, targetSpawnId=int(self._selectedAttacker.characterData.spawnData.id))
  147. self._selectedSpell = None
  148. self._selectedAttacker = None
  149. elif self._beenUnderAttack:
  150. # Return to spawn point
  151. self.AIbehaviors.pathFollow(1)
  152. self.AIbehaviors.addToPath(Vec3(*self._npc.characterData.spawnData.pos))
  153. self.AIbehaviors.startFollow()
  154. self._beenUnderAttack = False
  155. self._onWayHome = True
  156. elif self._onWayHome:
  157. # Check if NPC is at spawn point
  158. distanceFromHome = self.distanceBetween(
  159. self._npc.getGlobalPos(),
  160. Vec3(*self._npc.characterData.spawnData.pos)
  161. )
  162. if distanceFromHome < 0.5:
  163. self._onWayHome = False
  164. self.AIbehaviors.removeAi('pathfollow')
  165. class NPC(Character):
  166. def __init__(self, world, worldNP, spawnData, aiWorld):
  167. """
  168. @param world:
  169. @type world: BulletWorld
  170. @param worldNP:
  171. @type worldNP: NodePath
  172. @param spawnData:
  173. @type spawnData: core.db.NPCSpawnData
  174. """
  175. self._aiWorld = aiWorld
  176. model = CharacterModel(NPCs[spawnData.characterId] , spawnData)
  177. Character.__init__(self, world, worldNP, model)
  178. def __hash__(self): return self.characterData.spawnData.id
  179. def __lt__(self, other):
  180. return not other < self.characterData.spawnData.id
  181. def setup(self):
  182. Character.setup(self)
  183. self.ai = AI(self, self._aiWorld)
  184. def destroy(self):
  185. ## First check if characterNP is not None (already destroyed)
  186. ## this can happen when a npc goes out of range and gets
  187. ## killed at the same time. TODO fix this when implementing
  188. ## proper die/death system.
  189. if self.characterNP:
  190. Character.destroy(self)
  191. self.ai.destroy()
  192. self.ai = None
  193. def die(self):
  194. Character.die(self)
  195. if self.characterData.spawnData.respawnTime:
  196. # set respawn time to 0 to not respawn.
  197. taskMgr.doMethodLater(self.characterData.spawnData.respawnTime, self.respawn, 'respawn')
  198. def addSelectPlane(self):
  199. self.groundPlane = loader.loadModel('assets/other/select_plane.egg') #TODO use AssetsPath
  200. self.groundPlane.reparentTo(self.actorNP)
  201. self.groundPlane.setPos(0, 0, 0)
  202. self.groundPlane.setScale(2)
  203. def removeSelectPlane(self):
  204. self.groundPlane.removeNode()
  205. self.groundPlane = None
  206. class NPCsManager:
  207. def __init__(self, world, worldNP):
  208. """
  209. @param world:
  210. @type world: BulletWorld
  211. @param worldNP:
  212. @type worldNP: NodePath
  213. """
  214. self._world = world
  215. self._worldNP = worldNP
  216. self._player = None
  217. self._spawnData = None # List with db.SpawnData's from assets/maps/{map_name}/spawns.json
  218. self._np = self._worldNP.attachNewNode(BulletRigidBodyNode('NPCs'))
  219. self._AIworld = AIWorld(self._worldNP)
  220. self._spawns = {} # spawnId : spawned npc
  221. self._selectModel = SelectedNpcModel()
  222. self.mouseHandler = NPCMouse(self._np, self._selectModel)
  223. taskMgr.add(self.update, 'updateNPCs')
  224. @property
  225. def selectedNpcModel(self):
  226. """
  227. @rtype: core.models.SelectedNpcModel
  228. @return: Return the selected npc model.
  229. """
  230. return self._selectModel
  231. @property
  232. def node(self): return self._np
  233. @property
  234. def spawns(self): return list(self._spawns.values())
  235. def clear(self):
  236. # Remove all npcs from the world.
  237. # TODO Remove objects
  238. # ..
  239. self._spawnData = None
  240. for spawnId in self._spawns:
  241. # TODO make sure everything is proper deleted. (same happends in self.update)
  242. self._spawns[spawnId].destroy()
  243. self._spawns.clear()
  244. def setPlayer(self, player):
  245. self._player = player
  246. def setSpawnData(self, spawns):
  247. self.clear()
  248. self._spawnData = spawns
  249. for spawn in self._spawnData: self.spawn(spawn)
  250. def spawn(self, spawnData): # spawn
  251. self._spawns.update({spawnData.id : NPC(self._world, self._np, spawnData, self._AIworld)})
  252. def getSpawn(self, spawnId):
  253. """ Returns NPC
  254. @param spawnId:
  255. @type spawnId: int
  256. """
  257. return self._spawns.get(str(spawnId))
  258. def distanceBetween(self, pos, pos2):
  259. # This doesn't consider height or Z axis.
  260. diffVec = pos - pos2
  261. diffVecXY = diffVec.getXy()
  262. return diffVecXY.length()
  263. def update(self, task):
  264. if self._player:
  265. if base.world.player.isMoving:
  266. # Unspawn out of range, spawn in range.
  267. for spawn in self._spawnData:
  268. distance = self.distanceBetween(
  269. Vec3(*spawn.pos),
  270. base.world.player.characterNP.getPos()
  271. )
  272. if distance > 100: # Out of range
  273. if spawn.id in self._spawns:
  274. # TODO make sure everything is proper deleted.
  275. self._spawns.pop(spawn.id).destroy()
  276. elif distance < 80: # In range
  277. if spawn.id not in self._spawns:
  278. self.spawn(spawn)
  279. # Update AI's
  280. for spawnId, npc in self._spawns.items():
  281. if not npc.isDead(): npc.ai.update()
  282. self._AIworld.update()
  283. return task.cont