123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- ########################################################################
- # 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 Vec3
- from panda3d.bullet import BulletRigidBodyNode
- from panda3d.ai import AIWorld, AICharacter
- from core.db import NPCs
- from core.worldMouse import NPCMouse
- from core.models import SelectedNpcModel
- from core.character import Character, CharacterModel
- import random
- class AI:
- def __init__(self, npc, aiWorld):
- """
- @param npc:
- @type npc: core.npcsManager.NPC
- @param aiWorld:
- @type aiWorld: panda3d.ai.AIWorld
- """
- self._npc = npc
- self._aiWorld = aiWorld
- self._activeSteering = None
- self._selectedSpell = None
- self._selectedAttacker = None
- self._beenUnderAttack = False
- self._onWayHome = False
- self.AIchar = AICharacter(
- "spawnId_{0}".format(npc.characterData.spawnData.id),
- npc.characterNP, 100, 6, 10)
- self.AIbehaviors = self.AIchar.getAiBehaviors()
- self._aiWorld.addAiChar(self.AIchar)
- self.AIchar.setMaxForce(6)
- def destroy(self):
- self._aiWorld.removeAiChar("spawnId_{0}".format(self._npc.characterData.spawnData.id))
- #del self.AIchar
- self.AIchar = None
- self._activeSteering = None
- self._selectedSpell = None
- self._selectedAttacker = None
- self._beenUnderAttack = False
- self._onWayHome = False
- self._npc = None
- self._aiWorld = None
- def lookAt(self, attacker):
- """
- @param attacker:
- @type attacker: core.character.Character
- """
- self._npc.characterNP.lookAt(attacker.characterNP)
- def getFirstAttacker(self):
- """ Note: do check if it has any attackers before calling this.
- @rtype: core.character.Character
- @return:
- """
- return self.getAttacker(self._npc._underAttackBy[0])
- def getRandomAttacker(self):
- """ Note: do check if it has any attackers before calling this.
- @rtype: core.character.Character
- @return:
- """
- return self.getAttacker(
- random.choice(self._npc._underAttackBy)
- )
- def getAttacker(self, spawnId):
- """ Get attacker it's core.character.Character object
- @param spawnId: Attacker it's spawnId
- @type spawnId: int
- @rtype: core.character.Character
- @return:
- """
- if spawnId > 0: return base.world.npcsManager.getSpawn(spawnId)
- return base.world.player
- def getRandomSpell(self):
- """ Returns a random spell to cast.
- @rtype: core.models.SpellModel
- @return:
- """
- return random.choice(list(self._npc.characterData.spells))
- def distanceBetween(self, pos, pos2):
- # TODO make this a util function or something ..
- # This doesn't consider height or Z axis.
- diffVec = pos - pos2
- diffVecXY = diffVec.getXy()
- return diffVecXY.length()
- def removeSteering(self):
- if self._activeSteering:
- self.AIbehaviors.removeAi('all')
- self._activeSteering = None
- def update(self):
- if self._npc._underAttackBy:
- if not list(self._npc.characterData.spells): return
- if self._onWayHome: self.AIbehaviors.removeAi('pathfollow')
- if not self._selectedAttacker:
- self._selectedAttacker = self.getFirstAttacker()
- if not self._selectedAttacker: return
- if self._selectedAttacker.isDead():
- self._selectedAttacker = None
- return
- if (self._npc.characterData.spells.isCasting or
- self._npc.isDead()): return
- self._beenUnderAttack = True
- if not self._selectedSpell:
- self._selectedSpell = self.getRandomSpell()
- if self._selectedSpell.isCooling():
- return
- if self._npc.characterData.stats.energy.value < self._selectedSpell.data.energyCost:
- return
- self.lookAt(self._selectedAttacker)
- if self._selectedSpell.data.rangeEnd:
- distance = self.distanceBetween(
- self._npc.getGlobalPos(),
- self._selectedAttacker.getGlobalPos()
- )
- ## Get closer
- if distance >= self._selectedSpell.data.rangeEnd - 1:
- if self._activeSteering != 'persue':
- self.removeSteering()
- self.AIbehaviors.pursue(self._selectedAttacker.characterNP, 10)
- self._activeSteering = 'persue'
- return
- ## Take more distance
- if distance <= self._selectedSpell.data.rangeStart + 0.5:
- if self._activeSteering != 'flee':
- self.removeSteering()
- self.AIbehaviors.flee(self._selectedAttacker.characterNP, 10)
- self._activeSteering = 'flee'
- return
- self.removeSteering()
- self._npc.startCast(self._selectedSpell.data.id, targetSpawnId=int(self._selectedAttacker.characterData.spawnData.id))
- self._selectedSpell = None
- self._selectedAttacker = None
- elif self._beenUnderAttack:
- # Return to spawn point
- self.AIbehaviors.pathFollow(1)
- self.AIbehaviors.addToPath(Vec3(*self._npc.characterData.spawnData.pos))
- self.AIbehaviors.startFollow()
- self._beenUnderAttack = False
- self._onWayHome = True
- elif self._onWayHome:
- # Check if NPC is at spawn point
- distanceFromHome = self.distanceBetween(
- self._npc.getGlobalPos(),
- Vec3(*self._npc.characterData.spawnData.pos)
- )
- if distanceFromHome < 0.5:
- self._onWayHome = False
- self.AIbehaviors.removeAi('pathfollow')
- class NPC(Character):
- def __init__(self, world, worldNP, spawnData, aiWorld):
- """
- @param world:
- @type world: BulletWorld
- @param worldNP:
- @type worldNP: NodePath
- @param spawnData:
- @type spawnData: core.db.NPCSpawnData
- """
- self._aiWorld = aiWorld
- model = CharacterModel(NPCs[spawnData.characterId] , spawnData)
- Character.__init__(self, world, worldNP, model)
- def __hash__(self): return self.characterData.spawnData.id
- def __lt__(self, other):
- return not other < self.characterData.spawnData.id
- def setup(self):
- Character.setup(self)
- self.ai = AI(self, self._aiWorld)
- def destroy(self):
- ## First check if characterNP is not None (already destroyed)
- ## this can happen when a npc goes out of range and gets
- ## killed at the same time. TODO fix this when implementing
- ## proper die/death system.
- if self.characterNP:
- Character.destroy(self)
- self.ai.destroy()
- self.ai = None
- def die(self):
- Character.die(self)
- if self.characterData.spawnData.respawnTime:
- # set respawn time to 0 to not respawn.
- taskMgr.doMethodLater(self.characterData.spawnData.respawnTime, self.respawn, 'respawn')
- 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()
- self.groundPlane = None
- 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 = {} # spawnId : spawned npc
- self._selectModel = SelectedNpcModel()
- self.mouseHandler = NPCMouse(self._np, self._selectModel)
- taskMgr.add(self.update, 'updateNPCs')
- @property
- def selectedNpcModel(self):
- """
- @rtype: core.models.SelectedNpcModel
- @return: Return the selected npc model.
- """
- return self._selectModel
- @property
- def node(self): return self._np
- @property
- def spawns(self): return list(self._spawns.values())
- 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, spawnData): # spawn
- self._spawns.update({spawnData.id : NPC(self._world, self._np, spawnData, self._AIworld)})
- def getSpawn(self, spawnId):
- """ Returns NPC
- @param spawnId:
- @type spawnId: int
- """
- 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:
- if base.world.player.isMoving:
- # Unspawn out of range, spawn in 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.spawn(spawn)
- # Update AI's
- for spawnId, npc in self._spawns.items():
- if not npc.isDead(): npc.ai.update()
- self._AIworld.update()
- return task.cont
|