npcsManager.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  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 BitMask32, Vec3
  22. from panda3d.bullet import BulletCharacterControllerNode, BulletRigidBodyNode
  23. from panda3d.bullet import BulletCapsuleShape, ZUp
  24. from direct.actor.Actor import Actor
  25. from panda3d.ai import AIWorld, AICharacter
  26. from core.db import NPCs
  27. from core.worldMouse import NPCMouse
  28. from core.models import SelectedNpcModel, StatsModel
  29. class NPC:
  30. def __init__(self, spawn, world, worldNP, aiWorld):
  31. """NPC
  32. @param spawn: Object that contains data about this NPC spawn.
  33. @type spawn: db.SpawnData
  34. @param world:
  35. @type world: BulletWorld
  36. @param worldNP:
  37. @type worldNP: NodePath
  38. @param aiWorld:
  39. @type aiWorld: AIWorld
  40. """
  41. self._id = spawn.id # unique spawn id, not to confuse with npc id.
  42. self._world = world
  43. self._worldNP = worldNP
  44. self._aiWorld = aiWorld
  45. self._data = NPCs[spawn.characterId] # default data
  46. self._spawn = spawn
  47. self._healing = False
  48. self._healingRate = 1 # in seconds
  49. self._healingUnit = 1 # in healing units
  50. self.setup()
  51. def __hash__(self): return self._id
  52. def __lt__(self, other):
  53. return not other < self._id
  54. def isDead(self):
  55. return not bool(self.characterNP)
  56. def _healthChanged(self, value):
  57. if value <= 0:
  58. self.die()
  59. elif not self._healing and value < self.stats.health.max: # heal
  60. self._healing = True
  61. taskMgr.doMethodLater(self._healingRate, self._heal, '{}_heal'.format(self._id))
  62. def _heal(self, task):
  63. if not self.isDead() and self.stats.health.value < self.stats.health.max:
  64. self.stats.health.value += self._healingUnit
  65. return task.again
  66. self._healing = False
  67. return task.done
  68. def resetStats(self):
  69. self._stats = StatsModel(self._data.stats)
  70. @property
  71. def stats(self): return self._stats
  72. def die(self):
  73. self.destroy()
  74. if self._spawn.respawnTime:
  75. # set respawn time to 0 to not respawn.
  76. taskMgr.doMethodLater(self._spawn.respawnTime, self.respawn, 'respawn')
  77. def respawn(self, task):
  78. self.setup()
  79. return task.done
  80. @property
  81. def id(self):
  82. # Returns unique NPC spawn id
  83. return self._id
  84. @property
  85. def data(self):
  86. # Returns db.NPCData model
  87. return self._data
  88. @property
  89. def spawnData(self):
  90. # Returns db.SpawnData model
  91. return self._spawn
  92. def _distanceTo(self, pos): # vec3 pos
  93. # This doesn't consider height or Z axis.
  94. diffVec = pos - self.characterNP.getPos()
  95. diffVecXY = diffVec.getXy()
  96. return diffVecXY.length()
  97. def distanceToPlayer(self):
  98. return self._distanceTo(base.world.player.characterNP.getPos())
  99. def distanceToSpawnPoint(self):
  100. return self._distanceTo(Vec3(*self.spawnData.pos))
  101. def destroy(self):
  102. self.stats.health.valueChanged.disconnect(self._healthChanged)
  103. self._aiWorld.removeAiChar("npc_{0}".format(self._id))
  104. self.actorNP.cleanup()
  105. self.actorNP.removeNode()
  106. self.characterNP.removeNode()
  107. self._world.remove(self.characterCont)
  108. self.actorNP = None
  109. self.characterNP = None
  110. self.characterCont = None
  111. def setup(self):
  112. self.resetStats()
  113. h = 0.1
  114. w = 0.3
  115. # BulletCapsuleShape(float radius, float height, BulletUpAxis up)
  116. shape = BulletCapsuleShape(w, h, ZUp)
  117. # BulletCharacterControllerNode(BulletShape shape, float step_height, str name)
  118. self.characterCont = BulletCharacterControllerNode(shape, 0.5, "npc_{0}".format(self._id))
  119. self.characterCont.setGravity(18.0)
  120. self.characterCont.setMaxSlope(0.5)
  121. self.characterNP = self._worldNP.attachNewNode(self.characterCont)
  122. self.characterNP.setPos(*self.spawnData.pos)
  123. self.characterNP.setH(self.spawnData.orientation)
  124. self.characterNP.setCollideMask(BitMask32.allOn())
  125. self._world.attach(self.characterCont)
  126. # Character model
  127. self.actorNP = Actor(self._data.filePath)
  128. self.actorNP.setTag('spawnId', str(self.spawnData.id))
  129. self.actorNP.setTag('npcId', str(self.spawnData.characterId))
  130. # TODO animations
  131. #self.actorNP = Actor(
  132. # "self._data.file,
  133. # self._data.animations)
  134. self.actorNP.setPlayRate(1.1, 'run')
  135. self.actorNP.reparentTo(self.characterNP)
  136. self.actorNP.setScale(0.3048) # 1ft = 0.3048m
  137. # Model to collision mesh offset TODO make dynamic
  138. self.actorNP.setPos(0, 0, -.25)
  139. # Setup AI
  140. self.AIchar = AICharacter("spawnId_{0}".format(self._id), self.characterNP, 100, 3, 3)
  141. self._aiWorld.addAiChar(self.AIchar)
  142. AIbehaviors = self.AIchar.getAiBehaviors()
  143. # evade (NodePath target_object, double panic_distance, double relax_distance, float evade_wt)
  144. # wander (double wander_radius, int flag, double aoe, float wander_weight)
  145. # pursue (NodePath target_object, float pursue_wt)
  146. #
  147. AIbehaviors.evade(base.world.player.characterNP, 10, 25, 10)
  148. AIbehaviors.wander(25, 0, 45, 5)
  149. #AIbehaviors.pursue(base.world.player.characterNP, 2)
  150. # Connections
  151. self.stats.health.valueChanged.connect(self._healthChanged)
  152. def addSelectPlane(self):
  153. self.groundPlane = loader.loadModel('assets/other/select_plane.egg') #TODO use AssetsPath
  154. self.groundPlane.reparentTo(self.actorNP)
  155. self.groundPlane.setPos(0, 0, 0)
  156. self.groundPlane.setScale(2)
  157. def removeSelectPlane(self):
  158. self.groundPlane.removeNode()
  159. class NPCsManager:
  160. def __init__(self, world, worldNP):
  161. """
  162. @param world:
  163. @type world: BulletWorld
  164. @param worldNP:
  165. @type worldNP: NodePath
  166. """
  167. self._world = world
  168. self._worldNP = worldNP
  169. self._player = None
  170. self._spawnData = None # List with db.SpawnData's from assets/maps/{map_name}/spawns.json
  171. self._np = self._worldNP.attachNewNode(BulletRigidBodyNode('NPCs'))
  172. self._AIworld = AIWorld(self._worldNP)
  173. self._spawns = {} # spawned npcs
  174. self._selectModel = SelectedNpcModel()
  175. self.mouseHandler = NPCMouse(self._np, self._selectModel)
  176. taskMgr.add(self.update, 'updateNPCs')
  177. @property
  178. def selectedNpcModel(self): return self._selectModel
  179. @property
  180. def node(self): return self._np
  181. def clear(self):
  182. # Remove all npcs from the world.
  183. # TODO Remove objects
  184. # ..
  185. self._spawnData = None
  186. for spawnId in self._spawns:
  187. # TODO make sure everything is proper deleted. (same happends in self.update)
  188. self._spawns[spawnId].destroy()
  189. self._spawns.clear()
  190. def setPlayer(self, player):
  191. self._player = player
  192. def setSpawnData(self, spawns):
  193. self.clear()
  194. self._spawnData = spawns
  195. for spawn in self._spawnData: self.spawn(spawn)
  196. def spawn(self, spawn): # spawn
  197. self._spawns.update({spawn.id : NPC(spawn, self._world, self._np, self._AIworld)})
  198. def getSpawn(self, spawnId): # spawn id
  199. return self._spawns.get(str(spawnId))
  200. def distanceBetween(self, pos, pos2):
  201. # This doesn't consider height or Z axis.
  202. diffVec = pos - pos2
  203. diffVecXY = diffVec.getXy()
  204. return diffVecXY.length()
  205. def update(self, task):
  206. if self._player:
  207. self._AIworld.update()
  208. if base.world.player.isMoving:
  209. # Remove out of range
  210. for spawn in self._spawnData:
  211. distance = self.distanceBetween(
  212. Vec3(*spawn.pos),
  213. base.world.player.characterNP.getPos()
  214. )
  215. if distance > 100: # Out of range
  216. if spawn.id in self._spawns:
  217. # TODO make sure everything is proper deleted.
  218. self._spawns.pop(spawn.id).destroy()
  219. elif distance < 80: # In range
  220. if spawn.id not in self._spawns:
  221. self._spawns.update({spawn.id : NPC(spawn, self._world, self._np, self._AIworld)})
  222. return task.cont