123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578 |
- ########################################################################
- # 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 core.models import PlayerStatsModel, StatsModel, SpellsModel
- from core.signals import Signal
- from core.db import Spells, Fractions
- from core.spells import BasicCast
- class CharacterModel:
- def __init__(self, characterData, spawnData):
- """ Data model for a character.
- @param characterData:
- @type characterData: core.db.CharacterData
- @param spawnData:
- @type spawnData: core.db.GenericSpawnData
- """
- self._characterData = characterData
- self._spawnData = spawnData
- self._spells = SpellsModel(characterData.spells)
- self.resetStats()
- """ Expose dynamic attributes
- """
- @property
- def spells(self):
- """ Returns the character it's SpellsModel
- @rtype: core.models.SpellsModel
- @return: Character it's current spells
- """
- return self._spells
- @property
- def stats(self):
- """ Returns the character it's StatsModel
- @rtype: core.models.StatsModel
- @return: Current stats
- """
- return self._stats
- def resetStats(self):
- """ Resets the stats.
- """
- self._stats = StatsModel(self._characterData.stats)
- """ Expose CharacterData
- """
- @property
- def id(self):
- """ Returns the character it's ID.
- @rtype: int
- @return: Character ID
- """
- return self._characterData.id
- @property
- def name(self):
- """ Returns the character it's name.
- @rtype: int
- @return: Character ID
- """
- return self._characterData.name
- @property
- def file(self):
- """ Returns filename of the character it's model.
- @rtype: str
- @return: Filename (.egg)
- """
- return self._characterData.file
- @property
- def filePath(self):
- """ Returns the full filepath to the model file (.egg).
- @rtype: str
- @return: Full filepath to model (.egg)
- """
- return self._characterData.filePath
- @property
- def speciesId(self):
- """ Returns character it's species ID.
- @rtype: int
- @return: The character it's species ID.
- """
- return self._characterData.speciesId
- @property
- def fraction(self):
- """ Returns character it's fraction ID.
- @rtype: int
- @return: The character it's fraction ID.
- """
- return self._characterData.fractionId
- @property
- def enemies(self):
- """ Returns character it's enemies fraction ID's.
- @rtype: int
- @return: The character it's enemies fraction ID's.
- """
- fraction = Fractions[self._characterData.fractionId]
- return fraction.enemies
- """ Expose SpawnData
- """
- @property
- def spawnData(self):
- """ Returns character it's spawn data.
- @rtype: core.db.GenericSpawnData
- @return: The character it's spawn data.
- """
- return self._spawnData
- class PlayerCharacterModel(CharacterModel):
- def __init__(self, characterData, spawnData):
- """ Player character data model
- @param characterData:
- @type characterData: core.db.PlayerData
- @param spawnData:
- @type spawnData: core.db.GenericSpawnData
- """
- CharacterModel.__init__(self, characterData, spawnData)
- def resetStats(self):
- """ Resets the player it's stats.
- """
- self._stats = PlayerStatsModel(self._characterData.stats)
- # Panda3d
- from panda3d.core import BitMask32, Vec3, lookAt
- from panda3d.bullet import BulletCharacterControllerNode
- from panda3d.bullet import BulletCapsuleShape, ZUp
- from direct.actor.Actor import Actor
- class CharacterProto:
- """ Bones of character, only collision mesh (no Actor).
- """
- def __init__(self, world, worldNP, characterModel):
- """
- @param world:
- @type world: panda3d.bullet.BulletWorld
- @param worldNP:
- @type worldNP: panda3d.core.NodePath
- @param characterModel:
- @type characterModel: core.character.CharacterModel
- """
- ## Emits spawn id
- self.hasDied = Signal(int)
- ## Emits spawn id
- self.destroyed = Signal(int)
- self._world = world
- self._worldNP = worldNP
- self._characterData = characterModel
- self.crouching = False
- self.isMoving = False
- self.isCasting = False
- self._underAttackBy = []
- self._restoringHealth = False
- self._healingRate = 3 # in seconds
- self._healingUnit = 1 # in health units
- self._restoringEnergy = False
- self._energyRestoreRate = 2 # in seconds
- self._energyRestoreUnit = 3 # in energy units
- self._previousCharacterPos = Vec3()
- def canSee(self, other, angle=45):
- """
- @param other: Other character to test against.
- @type other: core.character.Character
- @param angle: Maximum viewing angle.
- @type angle: int
- @rtype: bool
- @return: True if we can see the other character, False if not.
- """
- quat = self.characterNP.getQuat()
- lookAt(quat, other.getGlobalPos() - self.getGlobalPos(), Vec3.up())
- diffHeading = quat.getHpr()[0] - self.getOrientation()
- if diffHeading > angle / 2 or diffHeading < -(angle / 2):
- return False
- return True
- def die(self):
- self.hasDied.emit(int(self.characterData.spawnData.id))
- self.destroy()
- def respawn(self, task):
- self.setup()
- return task.done
- def isDead(self):
- return bool(self.characterNP == None)
- def startCast(self, spellId, targetSpawnId=-1):
- """ Do some basic checks if we may cast this spell;
- if so start the process of casting the spell.
- @param spellId: Spell ID to cast.
- @type spellId: str
- @param targetSpawnId: Spawn id of the target
- @type targetSpawnId: str
- """
- spell = self.characterData.spells[spellId]
- # Select method
- if spell.data.method == Spells.methods.basic:
- BasicCast(self, spell, targetSpawnId)
- def takeHit(self, spellId, targetSpawnId=-1):
- """
- """
- pass
- def underAttackBy(self, spawnId):
- """
- @param spawnId: Character spawn id that is attacking
- @type spawnId: int
- """
- if self.isDead(): return
- if spawnId not in self._underAttackBy:
- attacker = base.world.player
- if spawnId > 0:
- attacker = base.world.npcsManager.getSpawn(spawnId)
- if not attacker: return
- if attacker.isDead(): return
- print("[{} {}] underAttackBy [{} {}]".format(
- self.characterData.spawnData.id, self.characterData.name, spawnId, attacker.characterData.name))
- attacker.hasDied.connect(self._attackerDied)
- self._underAttackBy.append(spawnId)
- def _attackerDied(self, spawnId):
- if spawnId in self._underAttackBy:
- print("[{} {}] Attacker died, removing it from the list.".format(
- self.characterData.spawnData.id, self.characterData.name))
- self._underAttackBy.remove(spawnId)
- attacker = base.world.player
- if spawnId > 0:
- attacker = base.world.npcsManager.getSpawn(spawnId)
- if attacker:
- attacker.hasDied.disconnect(self._attackerDied)
- """ Auto health/energy restore
- """
- def _healthChanged(self, value):
- if value <= 0:
- self.die()
- elif not self._restoringHealth and value < self.characterData.stats.health.max: # restore
- self._restoringHealth = True
- taskMgr.doMethodLater(self._healingRate, self._restoreHealth, '{}_restoreHealth'.format(self.characterData.spawnData.id))
- def _restoreHealth(self, task):
- if not self.isDead() and self.characterData.stats.health.value < self.characterData.stats.health.max:
- self.characterData.stats.health.value += self._healingUnit
- return task.again
- self._restoringHealth = False
- return task.done
- def _energyChanged(self, value):
- if not self._restoringEnergy and value < self.characterData.stats.energy.max: # restore
- self._restoringEnergy = True
- taskMgr.doMethodLater(self._energyRestoreRate, self._restoreEnergy, '{}_restoreEnergy'.format(self.characterData.spawnData.id))
- def _restoreEnergy(self, task):
- if not self.isDead() and self.characterData.stats.energy.value < self.characterData.stats.energy.max:
- self.characterData.stats.energy.value += self._energyRestoreUnit
- return task.again
- self._restoringEnergy = False
- return task.done
- @property
- def characterData(self):
- """ characterData
- @rtype: core.db.CharacterData
- @return: The character it's data.
- """
- return self._characterData
- def destroy(self):
- """ Call this to destruct / remove the character from the world.
- """
- self.destroyed.emit(int(self.characterData.spawnData.id))
- for spawnId in self._underAttackBy:
- attacker = base.world.player
- if spawnId > 0:
- attacker = base.world.npcsManager.getSpawn(spawnId)
- if attacker:
- attacker.hasDied.disconnect(self._attackerDied)
- self._underAttackBy.clear()
- self.characterData.stats.health.valueChanged.disconnect(self._healthChanged)
- self.characterData.stats.energy.valueChanged.disconnect(self._energyChanged)
- self.characterNP.removeNode()
- self._world.remove(self.characterCont) # (BulletWorld.remove())
- del self.characterNP
- del self.characterCont
- self.characterCont = None
- self.characterNP = None
- self.crouching = False
- self.isMoving = False
- self.isCasting = False
- self._restoringEnergy = False
- self._restoringHealth = False
- self._previousCharacterPos = Vec3()
- def setup(self):
- """ Call this to setup (re-init).
- """
- self.characterData.resetStats()
- h = 0.6
- 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, "character_{0}".format(self.characterData.spawnData.id))
- self.characterCont.setGravity(18.0)
- self.characterCont.setMaxSlope(0.5)
- self.characterNP = self._worldNP.attachNewNode(self.characterCont)
- self.characterNP.setPos(*self.characterData.spawnData.pos)
- self.characterNP.setH(self.characterData.spawnData.orientation)
- self.characterNP.setCollideMask(BitMask32.allOn())
- self._world.attach(self.characterCont)
- self.characterData.stats.health.valueChanged.connect(self._healthChanged)
- self.characterData.stats.energy.valueChanged.connect(self._energyChanged)
- def rotate(self, omega):
- """ Rotate the character.
- @param omega:
- @type omega: float
- """
- self.characterNP.setH(self.characterNP, omega)
- def rotateLeft(self, dt):
- """ Rotate the character left.
- @param dt: Delta-time
- @type dt: float
- """
- omega = 120 * dt
- self.characterNP.setH(self.characterNP, omega)
- return omega
- def rotateRight(self, dt):
- """ Rotate the character right.
- @param dt: Delta-time
- @type dt: float
- """
- omega = -120 * dt
- self.characterNP.setH(self.characterNP, omega)
- return omega
- def forward(self, dt, moveVec):
- """ Modifies the given Vec3 to a forward movement.
- @param dt: Delta-time
- @type dt: float
- @param moveVec: Vec3 to modify with dt.
- @type moveVec: panda3d.core.Vec3
- """
- moveVec.setY(6 * dt)
- def backward(self, dt, moveVec):
- """ Modifies the given Vec3 to a backward movement.
- @param dt: Delta-time
- @type dt: float
- @param moveVec: Vec3 to modify with dt.
- @type moveVec: panda3d.core.Vec3
- """
- moveVec.setY(-3 * dt)
- def shuffleLeft(self, dt, moveVec):
- """ Modifies the given Vec3 to shuffle left.
- @param dt: Delta-time
- @type dt: float
- @param moveVec: Vec3 to modify with dt.
- @type moveVec: panda3d.core.Vec3
- """
- moveVec.setX(-3 * dt)
- def shuffleRight(self, dt, moveVec):
- """ Modifies the given Vec3 to shuffle right.
- @param dt: Delta-time
- @type dt: float
- @param moveVec: Vec3 to modify with dt.
- @type moveVec: panda3d.core.Vec3
- """
- moveVec.setX(3 * dt)
- def setPos(self, moveVec):
- """ Applies the moveVec to the character.
- @param moveVec: Position modifier.
- @type moveVec: panda3d.core.Vec3
- """
- self.characterNP.setPos(self.characterNP, moveVec)
- def setGlobalPos(self, pos):
- """ Sets new global position for the character.
- @param pos: New global position.
- @type pos: panda3d.core.Vec3
- """
- self.characterNP.setPos(pos)
- def setOrientation(self, o):
- """ Sets new global orientation for the character.
- @param o: New global orientation.
- @type o: float
- """
- self.characterNP.setH(o)
- def setGlobalX(self, x):
- """ Sets new global x position for the character.
- @param x: New x position.
- @type x: float
- """
- self.characterNP.setX(x)
- def setGlobalY(self, y):
- """ Sets new global y position for the character.
- @param y: New y position.
- @type y: float
- """
- self.characterNP.setY(y)
- def setGlobalZ(self, z):
- """ Sets new global z position for the character.
- @param z: New z position.
- @type z: float
- """
- self.characterNP.setZ(z)
- def getGlobalPos(self):
- """ Returns global position of the character.
- @rtype: panda3d.core.Vec3
- @return: The character it's global position.
- """
- return self.characterNP.getPos()
- def getOrientation(self):
- """ Returns global orientation of the character.
- @rtype: float
- @return: The character it's global orientation.
- """
- return self.characterNP.getH()
- def updatePreviousPos(self): # TODO find better name for this
- """ Sets self.isMoving - TODO this needs to change.
- """
- characterPos = self.characterNP.getPos()
- if characterPos[2] < -10:
- # reset pos (character is fallen of the map)
- self.characterNP.setPos(*self.characterData.spawnData.pos) # TODO create function for this, it doesnt belong here
- if characterPos != self._previousCharacterPos:
- self._previousCharacterPos = characterPos
- self.isMoving = True
- else: self.isMoving = False
- def doJump(self):
- """ Makes the character jump.
- """
- if self.characterCont.isOnGround() and self.isMoving:
- self.characterCont.setMaxJumpHeight(1.25)
- self.characterCont.setJumpSpeed(5.6)
- self.characterCont.setFallSpeed(16)
- self.characterCont.doJump()
- def doCrouch(self):
- """ Should make the character crouch. TODO this doesn't work.
- """
- self.crouching = not self.crouching
- #sz = self.crouching and 1.2 or 1.0
- # eh this does not work
- # https://www.panda3d.org/manual/?title=Bullet_Character_Controller#Crouching
- #self.characterNP.setScale(Vec3(1, 1, sz))
- #self.characterCont.getShape().setLocalScale(Vec3(1, 1, sz))
- class Character(CharacterProto):
- """ This extends CharacterProto with a Actor (3d model/animations)
- """
- def __init__(self, world, worldNP, characterModel):
- CharacterProto.__init__(self, world, worldNP, characterModel)
- self._animationSet = False
- self.setup()
- def destroy(self):
- CharacterProto.destroy(self)
- self.actorNP.cleanup()
- self.actorNP.removeNode()
- self._animationSet = False
- def setup(self):
- # Create character
- CharacterProto.setup(self)
- # Character model
- self.actorNP = Actor(
- self.characterData.filePath
- )
- #self.actorNP = Actor(
- # "assets/characters/{0}".format(self.character.model.file),
- # self.character.model.actions)
- self.actorNP.loop('idle')
- self.actorNP.setTag('spawnId', str(self.characterData.spawnData.id))
- self.actorNP.setTag('characterId', str(self.characterData.spawnData.characterId))
- self.actorNP.setPlayRate(1.1, 'run')
- self.actorNP.reparentTo(self.characterNP)
- self.actorNP.setScale(0.3048) # 1ft = 0.3048m
- self.actorNP.setH(180)
- # Model to collision mesh offset TODO make dynamic
- self.actorNP.setPos(0, 0, -0.55)
|