character.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  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 core.models import PlayerStatsModel, StatsModel, SpellsModel
  22. from core.signals import Signal
  23. from core.db import Spells, Fractions
  24. from core.spells import BasicCast
  25. class CharacterModel:
  26. def __init__(self, characterData, spawnData):
  27. """ Data model for a character.
  28. @param characterData:
  29. @type characterData: core.db.CharacterData
  30. @param spawnData:
  31. @type spawnData: core.db.GenericSpawnData
  32. """
  33. self._characterData = characterData
  34. self._spawnData = spawnData
  35. self._spells = SpellsModel(characterData.spells)
  36. self.resetStats()
  37. """ Expose dynamic attributes
  38. """
  39. @property
  40. def spells(self):
  41. """ Returns the character it's SpellsModel
  42. @rtype: core.models.SpellsModel
  43. @return: Character it's current spells
  44. """
  45. return self._spells
  46. @property
  47. def stats(self):
  48. """ Returns the character it's StatsModel
  49. @rtype: core.models.StatsModel
  50. @return: Current stats
  51. """
  52. return self._stats
  53. def resetStats(self):
  54. """ Resets the stats.
  55. """
  56. self._stats = StatsModel(self._characterData.stats)
  57. """ Expose CharacterData
  58. """
  59. @property
  60. def id(self):
  61. """ Returns the character it's ID.
  62. @rtype: int
  63. @return: Character ID
  64. """
  65. return self._characterData.id
  66. @property
  67. def name(self):
  68. """ Returns the character it's name.
  69. @rtype: int
  70. @return: Character ID
  71. """
  72. return self._characterData.name
  73. @property
  74. def file(self):
  75. """ Returns filename of the character it's model.
  76. @rtype: str
  77. @return: Filename (.egg)
  78. """
  79. return self._characterData.file
  80. @property
  81. def filePath(self):
  82. """ Returns the full filepath to the model file (.egg).
  83. @rtype: str
  84. @return: Full filepath to model (.egg)
  85. """
  86. return self._characterData.filePath
  87. @property
  88. def speciesId(self):
  89. """ Returns character it's species ID.
  90. @rtype: int
  91. @return: The character it's species ID.
  92. """
  93. return self._characterData.speciesId
  94. @property
  95. def fraction(self):
  96. """ Returns character it's fraction ID.
  97. @rtype: int
  98. @return: The character it's fraction ID.
  99. """
  100. return self._characterData.fractionId
  101. @property
  102. def enemies(self):
  103. """ Returns character it's enemies fraction ID's.
  104. @rtype: int
  105. @return: The character it's enemies fraction ID's.
  106. """
  107. fraction = Fractions[self._characterData.fractionId]
  108. return fraction.enemies
  109. """ Expose SpawnData
  110. """
  111. @property
  112. def spawnData(self):
  113. """ Returns character it's spawn data.
  114. @rtype: core.db.GenericSpawnData
  115. @return: The character it's spawn data.
  116. """
  117. return self._spawnData
  118. class PlayerCharacterModel(CharacterModel):
  119. def __init__(self, characterData, spawnData):
  120. """ Player character data model
  121. @param characterData:
  122. @type characterData: core.db.PlayerData
  123. @param spawnData:
  124. @type spawnData: core.db.GenericSpawnData
  125. """
  126. CharacterModel.__init__(self, characterData, spawnData)
  127. def resetStats(self):
  128. """ Resets the player it's stats.
  129. """
  130. self._stats = PlayerStatsModel(self._characterData.stats)
  131. # Panda3d
  132. from panda3d.core import BitMask32, Vec3, lookAt
  133. from panda3d.bullet import BulletCharacterControllerNode
  134. from panda3d.bullet import BulletCapsuleShape, ZUp
  135. from direct.actor.Actor import Actor
  136. class CharacterProto:
  137. """ Bones of character, only collision mesh (no Actor).
  138. """
  139. def __init__(self, world, worldNP, characterModel):
  140. """
  141. @param world:
  142. @type world: panda3d.bullet.BulletWorld
  143. @param worldNP:
  144. @type worldNP: panda3d.core.NodePath
  145. @param characterModel:
  146. @type characterModel: core.character.CharacterModel
  147. """
  148. ## Emits spawn id
  149. self.hasDied = Signal(int)
  150. ## Emits spawn id
  151. self.destroyed = Signal(int)
  152. self._world = world
  153. self._worldNP = worldNP
  154. self._characterData = characterModel
  155. self.crouching = False
  156. self.isMoving = False
  157. self.isCasting = False
  158. self._underAttackBy = []
  159. self._restoringHealth = False
  160. self._healingRate = 3 # in seconds
  161. self._healingUnit = 1 # in health units
  162. self._restoringEnergy = False
  163. self._energyRestoreRate = 2 # in seconds
  164. self._energyRestoreUnit = 3 # in energy units
  165. self._previousCharacterPos = Vec3()
  166. def canSee(self, other, angle=45):
  167. """
  168. @param other: Other character to test against.
  169. @type other: core.character.Character
  170. @param angle: Maximum viewing angle.
  171. @type angle: int
  172. @rtype: bool
  173. @return: True if we can see the other character, False if not.
  174. """
  175. quat = self.characterNP.getQuat()
  176. lookAt(quat, other.getGlobalPos() - self.getGlobalPos(), Vec3.up())
  177. diffHeading = quat.getHpr()[0] - self.getOrientation()
  178. if diffHeading > angle / 2 or diffHeading < -(angle / 2):
  179. return False
  180. return True
  181. def die(self):
  182. self.hasDied.emit(int(self.characterData.spawnData.id))
  183. self.destroy()
  184. def respawn(self, task):
  185. self.setup()
  186. return task.done
  187. def isDead(self):
  188. return bool(self.characterNP == None)
  189. def startCast(self, spellId, targetSpawnId=-1):
  190. """ Do some basic checks if we may cast this spell;
  191. if so start the process of casting the spell.
  192. @param spellId: Spell ID to cast.
  193. @type spellId: str
  194. @param targetSpawnId: Spawn id of the target
  195. @type targetSpawnId: str
  196. """
  197. spell = self.characterData.spells[spellId]
  198. # Select method
  199. if spell.data.method == Spells.methods.basic:
  200. BasicCast(self, spell, targetSpawnId)
  201. def takeHit(self, spellId, targetSpawnId=-1):
  202. """
  203. """
  204. pass
  205. def underAttackBy(self, spawnId):
  206. """
  207. @param spawnId: Character spawn id that is attacking
  208. @type spawnId: int
  209. """
  210. if self.isDead(): return
  211. if spawnId not in self._underAttackBy:
  212. attacker = base.world.player
  213. if spawnId > 0:
  214. attacker = base.world.npcsManager.getSpawn(spawnId)
  215. if not attacker: return
  216. if attacker.isDead(): return
  217. print("[{} {}] underAttackBy [{} {}]".format(
  218. self.characterData.spawnData.id, self.characterData.name, spawnId, attacker.characterData.name))
  219. attacker.hasDied.connect(self._attackerDied)
  220. self._underAttackBy.append(spawnId)
  221. def _attackerDied(self, spawnId):
  222. if spawnId in self._underAttackBy:
  223. print("[{} {}] Attacker died, removing it from the list.".format(
  224. self.characterData.spawnData.id, self.characterData.name))
  225. self._underAttackBy.remove(spawnId)
  226. attacker = base.world.player
  227. if spawnId > 0:
  228. attacker = base.world.npcsManager.getSpawn(spawnId)
  229. if attacker:
  230. attacker.hasDied.disconnect(self._attackerDied)
  231. """ Auto health/energy restore
  232. """
  233. def _healthChanged(self, value):
  234. if value <= 0:
  235. self.die()
  236. elif not self._restoringHealth and value < self.characterData.stats.health.max: # restore
  237. self._restoringHealth = True
  238. taskMgr.doMethodLater(self._healingRate, self._restoreHealth, '{}_restoreHealth'.format(self.characterData.spawnData.id))
  239. def _restoreHealth(self, task):
  240. if not self.isDead() and self.characterData.stats.health.value < self.characterData.stats.health.max:
  241. self.characterData.stats.health.value += self._healingUnit
  242. return task.again
  243. self._restoringHealth = False
  244. return task.done
  245. def _energyChanged(self, value):
  246. if not self._restoringEnergy and value < self.characterData.stats.energy.max: # restore
  247. self._restoringEnergy = True
  248. taskMgr.doMethodLater(self._energyRestoreRate, self._restoreEnergy, '{}_restoreEnergy'.format(self.characterData.spawnData.id))
  249. def _restoreEnergy(self, task):
  250. if not self.isDead() and self.characterData.stats.energy.value < self.characterData.stats.energy.max:
  251. self.characterData.stats.energy.value += self._energyRestoreUnit
  252. return task.again
  253. self._restoringEnergy = False
  254. return task.done
  255. @property
  256. def characterData(self):
  257. """ characterData
  258. @rtype: core.db.CharacterData
  259. @return: The character it's data.
  260. """
  261. return self._characterData
  262. def destroy(self):
  263. """ Call this to destruct / remove the character from the world.
  264. """
  265. self.destroyed.emit(int(self.characterData.spawnData.id))
  266. for spawnId in self._underAttackBy:
  267. attacker = base.world.player
  268. if spawnId > 0:
  269. attacker = base.world.npcsManager.getSpawn(spawnId)
  270. if attacker:
  271. attacker.hasDied.disconnect(self._attackerDied)
  272. self._underAttackBy.clear()
  273. self.characterData.stats.health.valueChanged.disconnect(self._healthChanged)
  274. self.characterData.stats.energy.valueChanged.disconnect(self._energyChanged)
  275. self.characterNP.removeNode()
  276. self._world.remove(self.characterCont) # (BulletWorld.remove())
  277. del self.characterNP
  278. del self.characterCont
  279. self.characterCont = None
  280. self.characterNP = None
  281. self.crouching = False
  282. self.isMoving = False
  283. self.isCasting = False
  284. self._restoringEnergy = False
  285. self._restoringHealth = False
  286. self._previousCharacterPos = Vec3()
  287. def setup(self):
  288. """ Call this to setup (re-init).
  289. """
  290. self.characterData.resetStats()
  291. h = 0.6
  292. w = 0.3
  293. # BulletCapsuleShape(float radius, float height, BulletUpAxis up)
  294. shape = BulletCapsuleShape(w, h, ZUp)
  295. # BulletCharacterControllerNode(BulletShape shape, float step_height, str name)
  296. self.characterCont = BulletCharacterControllerNode(shape, 0.5, "character_{0}".format(self.characterData.spawnData.id))
  297. self.characterCont.setGravity(18.0)
  298. self.characterCont.setMaxSlope(0.5)
  299. self.characterNP = self._worldNP.attachNewNode(self.characterCont)
  300. self.characterNP.setPos(*self.characterData.spawnData.pos)
  301. self.characterNP.setH(self.characterData.spawnData.orientation)
  302. self.characterNP.setCollideMask(BitMask32.allOn())
  303. self._world.attach(self.characterCont)
  304. self.characterData.stats.health.valueChanged.connect(self._healthChanged)
  305. self.characterData.stats.energy.valueChanged.connect(self._energyChanged)
  306. def rotate(self, omega):
  307. """ Rotate the character.
  308. @param omega:
  309. @type omega: float
  310. """
  311. self.characterNP.setH(self.characterNP, omega)
  312. def rotateLeft(self, dt):
  313. """ Rotate the character left.
  314. @param dt: Delta-time
  315. @type dt: float
  316. """
  317. omega = 120 * dt
  318. self.characterNP.setH(self.characterNP, omega)
  319. return omega
  320. def rotateRight(self, dt):
  321. """ Rotate the character right.
  322. @param dt: Delta-time
  323. @type dt: float
  324. """
  325. omega = -120 * dt
  326. self.characterNP.setH(self.characterNP, omega)
  327. return omega
  328. def forward(self, dt, moveVec):
  329. """ Modifies the given Vec3 to a forward movement.
  330. @param dt: Delta-time
  331. @type dt: float
  332. @param moveVec: Vec3 to modify with dt.
  333. @type moveVec: panda3d.core.Vec3
  334. """
  335. moveVec.setY(6 * dt)
  336. def backward(self, dt, moveVec):
  337. """ Modifies the given Vec3 to a backward movement.
  338. @param dt: Delta-time
  339. @type dt: float
  340. @param moveVec: Vec3 to modify with dt.
  341. @type moveVec: panda3d.core.Vec3
  342. """
  343. moveVec.setY(-3 * dt)
  344. def shuffleLeft(self, dt, moveVec):
  345. """ Modifies the given Vec3 to shuffle left.
  346. @param dt: Delta-time
  347. @type dt: float
  348. @param moveVec: Vec3 to modify with dt.
  349. @type moveVec: panda3d.core.Vec3
  350. """
  351. moveVec.setX(-3 * dt)
  352. def shuffleRight(self, dt, moveVec):
  353. """ Modifies the given Vec3 to shuffle right.
  354. @param dt: Delta-time
  355. @type dt: float
  356. @param moveVec: Vec3 to modify with dt.
  357. @type moveVec: panda3d.core.Vec3
  358. """
  359. moveVec.setX(3 * dt)
  360. def setPos(self, moveVec):
  361. """ Applies the moveVec to the character.
  362. @param moveVec: Position modifier.
  363. @type moveVec: panda3d.core.Vec3
  364. """
  365. self.characterNP.setPos(self.characterNP, moveVec)
  366. def setGlobalPos(self, pos):
  367. """ Sets new global position for the character.
  368. @param pos: New global position.
  369. @type pos: panda3d.core.Vec3
  370. """
  371. self.characterNP.setPos(pos)
  372. def setOrientation(self, o):
  373. """ Sets new global orientation for the character.
  374. @param o: New global orientation.
  375. @type o: float
  376. """
  377. self.characterNP.setH(o)
  378. def setGlobalX(self, x):
  379. """ Sets new global x position for the character.
  380. @param x: New x position.
  381. @type x: float
  382. """
  383. self.characterNP.setX(x)
  384. def setGlobalY(self, y):
  385. """ Sets new global y position for the character.
  386. @param y: New y position.
  387. @type y: float
  388. """
  389. self.characterNP.setY(y)
  390. def setGlobalZ(self, z):
  391. """ Sets new global z position for the character.
  392. @param z: New z position.
  393. @type z: float
  394. """
  395. self.characterNP.setZ(z)
  396. def getGlobalPos(self):
  397. """ Returns global position of the character.
  398. @rtype: panda3d.core.Vec3
  399. @return: The character it's global position.
  400. """
  401. return self.characterNP.getPos()
  402. def getOrientation(self):
  403. """ Returns global orientation of the character.
  404. @rtype: float
  405. @return: The character it's global orientation.
  406. """
  407. return self.characterNP.getH()
  408. def updatePreviousPos(self): # TODO find better name for this
  409. """ Sets self.isMoving - TODO this needs to change.
  410. """
  411. characterPos = self.characterNP.getPos()
  412. if characterPos[2] < -10:
  413. # reset pos (character is fallen of the map)
  414. self.characterNP.setPos(*self.characterData.spawnData.pos) # TODO create function for this, it doesnt belong here
  415. if characterPos != self._previousCharacterPos:
  416. self._previousCharacterPos = characterPos
  417. self.isMoving = True
  418. else: self.isMoving = False
  419. def doJump(self):
  420. """ Makes the character jump.
  421. """
  422. if self.characterCont.isOnGround() and self.isMoving:
  423. self.characterCont.setMaxJumpHeight(1.25)
  424. self.characterCont.setJumpSpeed(5.6)
  425. self.characterCont.setFallSpeed(16)
  426. self.characterCont.doJump()
  427. def doCrouch(self):
  428. """ Should make the character crouch. TODO this doesn't work.
  429. """
  430. self.crouching = not self.crouching
  431. #sz = self.crouching and 1.2 or 1.0
  432. # eh this does not work
  433. # https://www.panda3d.org/manual/?title=Bullet_Character_Controller#Crouching
  434. #self.characterNP.setScale(Vec3(1, 1, sz))
  435. #self.characterCont.getShape().setLocalScale(Vec3(1, 1, sz))
  436. class Character(CharacterProto):
  437. """ This extends CharacterProto with a Actor (3d model/animations)
  438. """
  439. def __init__(self, world, worldNP, characterModel):
  440. CharacterProto.__init__(self, world, worldNP, characterModel)
  441. self._animationSet = False
  442. self.setup()
  443. def destroy(self):
  444. CharacterProto.destroy(self)
  445. self.actorNP.cleanup()
  446. self.actorNP.removeNode()
  447. self._animationSet = False
  448. def setup(self):
  449. # Create character
  450. CharacterProto.setup(self)
  451. # Character model
  452. self.actorNP = Actor(
  453. self.characterData.filePath
  454. )
  455. #self.actorNP = Actor(
  456. # "assets/characters/{0}".format(self.character.model.file),
  457. # self.character.model.actions)
  458. self.actorNP.loop('idle')
  459. self.actorNP.setTag('spawnId', str(self.characterData.spawnData.id))
  460. self.actorNP.setTag('characterId', str(self.characterData.spawnData.characterId))
  461. self.actorNP.setPlayRate(1.1, 'run')
  462. self.actorNP.reparentTo(self.characterNP)
  463. self.actorNP.setScale(0.3048) # 1ft = 0.3048m
  464. self.actorNP.setH(180)
  465. # Model to collision mesh offset TODO make dynamic
  466. self.actorNP.setPos(0, 0, -0.55)