123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286 |
- ########################################################################
- # Hello Worlds - Libre 3D RPG game.
- # Copyright (C) 2020 CYBERDEViL
- #
- # This file is part of Hello Worlds.
- #
- # Hello Worlds is free software: you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # Hello Worlds is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program. If not, see <https://www.gnu.org/licenses/>.
- #
- ########################################################################
- from panda3d.core import BitMask32, Vec3
- from panda3d.bullet import BulletCharacterControllerNode, BulletRigidBodyNode
- from panda3d.bullet import BulletCapsuleShape, ZUp
- from direct.actor.Actor import Actor
- from panda3d.ai import AIWorld, AICharacter
- from core.db import NPCs
- from core.worldMouse import NPCMouse
- from core.models import SelectedNpcModel, StatsModel
- class NPC:
- def __init__(self, spawn, world, worldNP, aiWorld):
- """NPC
- @param spawn: Object that contains data about this NPC spawn.
- @type spawn: db.SpawnData
- @param world:
- @type world: BulletWorld
- @param worldNP:
- @type worldNP: NodePath
- @param aiWorld:
- @type aiWorld: AIWorld
- """
- self._id = spawn.id # unique spawn id, not to confuse with npc id.
- self._world = world
- self._worldNP = worldNP
- self._aiWorld = aiWorld
- self._data = NPCs[spawn.characterId] # default data
- self._spawn = spawn
- self._healing = False
- self._healingRate = 1 # in seconds
- self._healingUnit = 1 # in healing units
- self.setup()
- def __hash__(self): return self._id
- def __lt__(self, other):
- return not other < self._id
- def isDead(self):
- return not bool(self.characterNP)
- def _healthChanged(self, value):
- if value <= 0:
- self.die()
- elif not self._healing and value < self.stats.health.max: # heal
- self._healing = True
- taskMgr.doMethodLater(self._healingRate, self._heal, '{}_heal'.format(self._id))
- def _heal(self, task):
- if not self.isDead() and self.stats.health.value < self.stats.health.max:
- self.stats.health.value += self._healingUnit
- return task.again
- self._healing = False
- return task.done
- def resetStats(self):
- self._stats = StatsModel(self._data.stats)
- @property
- def stats(self): return self._stats
- def die(self):
- self.destroy()
- if self._spawn.respawnTime:
- # set respawn time to 0 to not respawn.
- taskMgr.doMethodLater(self._spawn.respawnTime, self.respawn, 'respawn')
- def respawn(self, task):
- self.setup()
- return task.done
- @property
- def id(self):
- # Returns unique NPC spawn id
- return self._id
- @property
- def data(self):
- # Returns db.NPCData model
- return self._data
- @property
- def spawnData(self):
- # Returns db.SpawnData model
- return self._spawn
- def _distanceTo(self, pos): # vec3 pos
- # This doesn't consider height or Z axis.
- diffVec = pos - self.characterNP.getPos()
- diffVecXY = diffVec.getXy()
- return diffVecXY.length()
- def distanceToPlayer(self):
- return self._distanceTo(base.world.player.characterNP.getPos())
- def distanceToSpawnPoint(self):
- return self._distanceTo(Vec3(*self.spawnData.pos))
- def destroy(self):
- self.stats.health.valueChanged.disconnect(self._healthChanged)
- self._aiWorld.removeAiChar("npc_{0}".format(self._id))
- self.actorNP.cleanup()
- self.actorNP.removeNode()
- self.characterNP.removeNode()
- self._world.remove(self.characterCont)
- self.actorNP = None
- self.characterNP = None
- self.characterCont = None
- def setup(self):
- self.resetStats()
- h = 0.1
- w = 0.3
- # BulletCapsuleShape(float radius, float height, BulletUpAxis up)
- shape = BulletCapsuleShape(w, h, ZUp)
- # BulletCharacterControllerNode(BulletShape shape, float step_height, str name)
- self.characterCont = BulletCharacterControllerNode(shape, 0.5, "npc_{0}".format(self._id))
- self.characterCont.setGravity(18.0)
- self.characterCont.setMaxSlope(0.5)
- self.characterNP = self._worldNP.attachNewNode(self.characterCont)
- self.characterNP.setPos(*self.spawnData.pos)
- self.characterNP.setH(self.spawnData.orientation)
- self.characterNP.setCollideMask(BitMask32.allOn())
- self._world.attach(self.characterCont)
- # Character model
- self.actorNP = Actor(self._data.filePath)
- self.actorNP.setTag('spawnId', str(self.spawnData.id))
- self.actorNP.setTag('npcId', str(self.spawnData.characterId))
- # TODO animations
- #self.actorNP = Actor(
- # "self._data.file,
- # self._data.animations)
- self.actorNP.setPlayRate(1.1, 'run')
- self.actorNP.reparentTo(self.characterNP)
- self.actorNP.setScale(0.3048) # 1ft = 0.3048m
- # Model to collision mesh offset TODO make dynamic
- self.actorNP.setPos(0, 0, -.25)
- # Setup AI
- self.AIchar = AICharacter("spawnId_{0}".format(self._id), self.characterNP, 100, 3, 3)
- self._aiWorld.addAiChar(self.AIchar)
- AIbehaviors = self.AIchar.getAiBehaviors()
- # evade (NodePath target_object, double panic_distance, double relax_distance, float evade_wt)
- # wander (double wander_radius, int flag, double aoe, float wander_weight)
- # pursue (NodePath target_object, float pursue_wt)
- #
- AIbehaviors.evade(base.world.player.characterNP, 10, 25, 10)
- AIbehaviors.wander(25, 0, 45, 5)
- #AIbehaviors.pursue(base.world.player.characterNP, 2)
- # Connections
- self.stats.health.valueChanged.connect(self._healthChanged)
- def addSelectPlane(self):
- self.groundPlane = loader.loadModel('assets/other/select_plane.egg') #TODO use AssetsPath
- self.groundPlane.reparentTo(self.actorNP)
- self.groundPlane.setPos(0, 0, 0)
- self.groundPlane.setScale(2)
- def removeSelectPlane(self):
- self.groundPlane.removeNode()
- class NPCsManager:
- def __init__(self, world, worldNP):
- """
- @param world:
- @type world: BulletWorld
- @param worldNP:
- @type worldNP: NodePath
- """
- self._world = world
- self._worldNP = worldNP
- self._player = None
- self._spawnData = None # List with db.SpawnData's from assets/maps/{map_name}/spawns.json
- self._np = self._worldNP.attachNewNode(BulletRigidBodyNode('NPCs'))
- self._AIworld = AIWorld(self._worldNP)
- self._spawns = {} # spawned npcs
- self._selectModel = SelectedNpcModel()
- self.mouseHandler = NPCMouse(self._np, self._selectModel)
- taskMgr.add(self.update, 'updateNPCs')
- @property
- def selectedNpcModel(self): return self._selectModel
- @property
- def node(self): return self._np
- def clear(self):
- # Remove all npcs from the world.
- # TODO Remove objects
- # ..
- self._spawnData = None
- for spawnId in self._spawns:
- # TODO make sure everything is proper deleted. (same happends in self.update)
- self._spawns[spawnId].destroy()
- self._spawns.clear()
- def setPlayer(self, player):
- self._player = player
- def setSpawnData(self, spawns):
- self.clear()
- self._spawnData = spawns
- for spawn in self._spawnData: self.spawn(spawn)
- def spawn(self, spawn): # spawn
- self._spawns.update({spawn.id : NPC(spawn, self._world, self._np, self._AIworld)})
- def getSpawn(self, spawnId): # spawn id
- return self._spawns.get(str(spawnId))
- def distanceBetween(self, pos, pos2):
- # This doesn't consider height or Z axis.
- diffVec = pos - pos2
- diffVecXY = diffVec.getXy()
- return diffVecXY.length()
- def update(self, task):
- if self._player:
- self._AIworld.update()
- if base.world.player.isMoving:
- # Remove out of range
- for spawn in self._spawnData:
- distance = self.distanceBetween(
- Vec3(*spawn.pos),
- base.world.player.characterNP.getPos()
- )
- if distance > 100: # Out of range
- if spawn.id in self._spawns:
- # TODO make sure everything is proper deleted.
- self._spawns.pop(spawn.id).destroy()
- elif distance < 80: # In range
- if spawn.id not in self._spawns:
- self._spawns.update({spawn.id : NPC(spawn, self._world, self._np, self._AIworld)})
- return task.cont
|