Beispiel #1
0
 def enableMixing(self):
     if self.__mixer != self.animationMixer:
         self.disableMixing()
     self.__mixer = self.animationMixer
     if self.__mixer:
         Actor.enableBlend(self)
         self.__mixer.cleanup()
    def enableMixing(self):
        if self._UsesAnimationMixer__mixer != self.animationMixer:
            self.disableMixing()

        self._UsesAnimationMixer__mixer = self.animationMixer
        if self._UsesAnimationMixer__mixer:
            Actor.enableBlend(self)
            self._UsesAnimationMixer__mixer.cleanup()
Beispiel #3
0
class World:
    def __init__(self):
        base.disableMouse()

        base.camera.setPos(0, -5, 1)

        self.setupLight()

        self.kid = Actor(
            "../Models/Kid.egg", {
                "Walk": "../Animations/Walk.egg",
                "Thoughtful": "../Animations/Thoughtful.egg"
            })
        self.kid.reparentTo(render)
        self.kid.enableBlend()
        self.kid.setControlEffect("Walk", 1)
        self.kid.setControlEffect("Thoughtful", 1)
        self.kid.loop("Thoughtful")
        self.kid.loop("Walk")
        self.kid.setH(180)

    def setupLight(self):
        primeL = DirectionalLight("prime")
        primeL.setColor(VBase4(.6, .6, .6, 1))
        # Creates the primary directional light and sets it's color to 60%.
        self.dirLight = render.attachNewNode(primeL)
        self.dirLight.setHpr(45, -60, 0)
        # Assigns the light to a nodePath and rotates that nodePath to aim the light.

        render.setLight(self.dirLight)
        # Sets the directional light to illuminate everything attached to the render node.

        ambL = AmbientLight("amb")
        ambL.setColor(VBase4(.2, .2, .2, 1))
        self.ambLight = render.attachNewNode(ambL)
        # Creates an ambient light to fill in the shadows and sets it's color to 20%.
        # also places it in a NodePath.

        render.setLight(self.ambLight)
        # Sets the ambient light to illuminate the scene.

        return
Beispiel #4
0
class Person(DirectObject):

    person = None
    personActor = None
    state_key = None    

    def __init__(self):
        self.load_char()
        self.catch_events()

    def catch_events(self):
        pass

    def load_char(self):
        self.person = render.attachNewNode('persona')

        self.personActor = Actor('resources/eggs/personagem.egg',
                             {'idle':'resources/eggs/personagem-parado',
                              'run' :'resources/eggs/personagem-correr',
                              'jump':'resources/eggs/personagem-pular'}
                             )

        #self.personActor.setScale(.3)
        self.personActor.loop('idle')
        self.personActor.reparentTo(self.person)
        self.personActor.setPos(0,0,1.5)
        self.state_key = {
                            'right':0, 'left':0,'jump': False, 'fall':0,
                            'speed_walk' : 0.8, 'speed_side' : 0.8,'up' : 0,
                            'down': 0, 'moving' : False, 'walk_right' : 0,
                            'walk_left' : 0, 'crouch' : 0,'crouching' : False,
                            'aim' : 0
                         }
                          
        self.personActor.enableBlend()
        self.personActor.loop('idle')
        self.personActor.loop('run')
        self.personActor.loop('jump')

        self.person.setPos(0,30,0)
        self.person.setH(0)

        taskMgr.add( self.walk, 'movePerson' )
        taskMgr.add( self.crouch, 'crouchPerson')

    def walk(self,task):

        elapsed = globalClock.getDt()

        #roda o sprite em seu propio eixo
        if (self.state_key['left']!=0):
            self.person.setH(self.person.getH() + ((elapsed*300) * self.state_key['speed_side'] ))
        if (self.state_key['right']!=0):
            self.person.setH(self.person.getH() - ((elapsed*300) * self.state_key['speed_side'] ))
        if (self.state_key['up']!=0):
            self.person.setY(self.person, -((elapsed*25) * self.state_key['speed_walk'] ))
        if (self.state_key['down']!=0):
            self.person.setY(self.person, +((elapsed*25) * self.state_key['speed_walk'] ))
        if (self.state_key['walk_right'] != 0):
            self.person.setX(self.person, - ((elapsed*25) * self.state_key['speed_walk'] ))
        if (self.state_key['walk_left'] != 0):
            self.person.setX(self.person, + ((elapsed*25) * self.state_key['speed_walk'] ))


        if ((self.state_key['up'] == 1) or (self.state_key['down'] == 1)):
            if (not self.state_key['moving']):                
                self.state_key['moving'] = True
        else:
            if (self.state_key['moving']):                
                self.state_key['moving'] = False

        self.animation()
        return task.cont

    def animation(self):
        if (self.state_key['crouching'] == True):
            blendAnim = [0, 0, 0, 1.0]
        elif (self.state_key['moving'] == False) and (self.state_key['jump'] == False):
            blendAnim = [1.0, 0.0, 0.0, 0.0]
        elif (self.state_key['moving'] == True) and (self.state_key['jump'] == False):
            blendAnim = [0.0, 1.0, 0.0, 0.0]
        elif (self.state_key['jump'] == True):
            blendAnim = [0.0, 0.0, 1.0, 0.0]
        else:
            blendAnim = [1.0, 0.0, 0.0, 0.0]

        self.personActor.setControlEffect( 'idle', blendAnim[0] )
        self.personActor.setControlEffect( 'run', blendAnim[1] )
        self.personActor.setControlEffect( 'jump', blendAnim[2] )
        self.personActor.setControlEffect( 'jump', blendAnim[3] )


    def attack(self):
        pass

    def jump(self):
        pass

    def aim(self):
        pass

    def shoot(self):
        pass

    def crouch(self, task):
        if self.state_key['crouch'] == 1:
            self.state_key['crouching'] = True
        else:
            self.state_key['crouching'] = False

        self.animation()

        return task.cont
Beispiel #5
0
class World(DirectObject): #necessary to accept events
    def __init__(self):
        print "START THE GAAAAAME!"
        self.accept("space", self.StartGame)
        self.titleScreen = OnscreenImage(image = '../images/Title.png', scale = (1.3333333,1,1))
        self.titleScreen.setTransparency(TransparencyAttrib.MAlpha)
        self.loadSound()
        
        
                

    def StartGame(self):

        self.titleScreen.destroy()
        #turn off default mouse control
        base.disableMouse()
        #add update tasks to taskManager
        taskMgr.add(self.keyEvents, "keyEventTask")        
        taskMgr.add(self.checkPipes2, "checkPipesTask")
        taskMgr.add(self.loopMusic, "loopMusicTask")
        taskMgr.add(self.updateTimer, "timeTask")
        
        #Enables particle effects
        base.enableParticles()
        
        camera.setPosHpr(0, -18, 3, 0, 0, 0)
        self.keyMap = {"moveLeft":0, "moveRight":0, "moveUp":0, "moveDown":0, "drop":0, \
        "actionLeft":0, "actionRight":0, "actionDown":0, "actionUp":0}
        self.prevTime = 0
        
        #Sets initial collision state, will change name later
        self.numCollisions = 0
        self.currentPipe = False
        
        self.loadModels()            
        self.setupLights()                
        #self.loadSound()                
        self.setupCollisions()
      
        self.accept("escape", sys.exit) #message name, function to call, (optional) list of arguments to that function
        #useful interval methods:
        # loop, pause, resume, finish
        # start can optionally take arguments: starttime, endtime, playrate
        
        
        #for "continuous" control
        self.accept("space", self.setKey, ["drop", 0]) #disables debug stopping
        self.accept("space-up", self.setKey, ["drop", 0])
        
        self.accept("arrow_up", self.setKey, ["actionUp", 1])
        self.accept("arrow_down", self.setKey, ["actionDown", 1])
        self.accept("arrow_left", self.setKey, ["actionLeft", 1])
        self.accept("arrow_right", self.setKey, ["actionRight", 1])
        self.accept("arrow_up-up", self.setKey, ["actionUp", 0])
        self.accept("arrow_down-up", self.setKey, ["actionDown", 0])
        self.accept("arrow_left-up", self.setKey, ["actionLeft", 0])
        self.accept("arrow_right-up", self.setKey, ["actionRight", 0])
        
        self.accept("w", self.setKey, ["moveUp", 1])
        self.accept("s", self.setKey, ["moveDown", 1])
        self.accept("a", self.setKey, ["moveLeft", 1])
        self.accept("d", self.setKey, ["moveRight", 1])
        self.accept("w-up", self.setKey, ["moveUp", 0])
        self.accept("s-up", self.setKey, ["moveDown", 0])
        self.accept("a-up", self.setKey, ["moveLeft", 0])
        self.accept("d-up", self.setKey, ["moveRight", 0])
        
        self.accept("spider-and-tube_collision", self.pipeCollide)
        
        self.DefaultTime = 1#.8
        self.TimeLeft = self.DefaultTime
        self.TimerGoing = False
        
        #Set game over thing
        self.GameOver = False

        #Set gameplay variables to keep track of
        self.gameScore = 0
        self.playerStability = 100
        self.currentActionCommand = ActionCommand(0,"")
        self.currentActionCommand.isEmpty()

        #Create HUD and add it to task thing
        self.Hud = GameHUD()
        taskMgr.add(self.update_game_Hud, "updateHudTask")

        #Fog and changing background
        myFog = Fog("Fog Name")
        f = 0.05
        myFog.setColor(f,f,f)
        myFog.setExpDensity(.01)
        render.setFog(myFog)
        base.setBackgroundColor(f,f,f)

    def updateTimer(self,task):
        if self.TimerGoing == True:
            self.TimeLeft = self.TimeLeft - globalClock.getDt()
            if self.TimeLeft <= 0:
                print"-----------TIME UP!!!!!!!!!---------"
                print self.currentActionCommand.getCommand()
                if self.currentActionCommand.isEmpty() == False:
                    self.playerStability = self.playerStability - 10
                    if self.playerStability <= 0:
                        if self.GameOver == False:
                            #REPLACE TITLE.PNG WITH THE GAME OVER IMAGE!!!!!!
                            self.gameOverScreen = OnscreenImage(image = '../images/GameOver.png', scale = (1.3333333,1,1))
                            self.gameOverScreen.setTransparency(TransparencyAttrib.MAlpha)
                            self.GameOver = True
                            self.Hud.hide = True
                print "***Blanked Action Command***"
                self.currentActionCommand = ActionCommand(0,"")
                self.TimeLeft = self.DefaultTime
                self.TimerGoing = False
        return Task.cont  
    
    def setKey(self, key, value):
        self.keyMap[key] = value
        
    def loadModels(self):
        """loads initial models into the world"""
        #load pipes
        self.numPipes = 6 #number appearing on the stage at any given time
        self.numGenericTypes = 5 #number of normal pipe models
        self.numSpecialTypes = 4 #number of 'broken' pipe models
        self.pipeGenericBag = GrabBag(self.numGenericTypes)
        self.pipeSpecialBag = GrabBag(self.numSpecialTypes)
        self.pipeList = []
        self.pipeInterval = 20.25*3.05#*.98 #length*timesLonger*overlapConstant
        self.pipeDepth = 0
        self.pipeCycle = 1
        
        #create initial pipes
        for i in range(self.numPipes):
            self.createPipe2(i)
            #print self.pipeList[i].model.getY()
        
        #Enable initial shaders
        self.pipeList[0].addShader()
        self.pipeList[1].addShader()
        
        #load spider
        #self.spider = loader.loadModel("../models/spider.egg")
        self.spider = Actor("../models/spider.egg", 
            {"aniFall":"../models/animation_fall cycle.egg",
            "aniFix1":"../models/animation_fixattack1.egg",}
        )
        #setup animation
        self.spider.enableBlend()
        self.spider.setControlEffect('aniFall', 1)        
        self.spider.loop("aniFall")   

        self.spider.reparentTo(render)
        self.spider.setShaderAuto()
        self.spider.setScale(.045)
        self.spider.setZ(4.25)
        self.spider.setH(180)
        self.spider.setP(-65)

        #load back panal
        self.backPanal = loader.loadModel("../models/infinity.egg")
        self.backPanal.reparentTo(render)
        self.backPanal.setZ(4.25)
        self.backPanal.setY(self.pipeInterval*5*.90)
        
            
    def loadSound(self):
        self.openingMusic = loader.loadSfx("../audio/opening.wav")
        self.mainLoopMusic = loader.loadSfx("../audio/mainLoop.wav")
        SoundInterval(self.openingMusic).start()
        
        #repair sound effects
        self.fixSound1 = loader.loadSfx("../audio/repair1.wav")
        self.fixSound2 = loader.loadSfx("../audio/repair2.wav")
        self.fixSound3 = loader.loadSfx("../audio/repair3.wav")
        self.fixSound4 = loader.loadSfx("../audio/repair4.wav")
        self.explosionSound = loader.loadSfx("../audio/explosion.wav")
        
            
    def setupLights(self):
        """loads initial lighting"""
        self.ambientLight = AmbientLight("ambientLight")
        #for setting colors, alpha is largely irrelevant
        self.ambientLight.setColor((.25, .25, .35, 1.0))
        #self.ambientLight.setColor((.25, .25, .25, 1.0))
        #create a NodePath, and attach it directly into the scene
        self.ambientLightNP = render.attachNewNode(self.ambientLight)
        #the node that calls setLight is what's illuminated by the given light
        #you can use clearLight() to turn it off
        render.setLight(self.ambientLightNP)
        
        self.dirLight = DirectionalLight("dirLight")
        self.dirLight.setColor((.7, .5, .5, 1))
        #self.dirLight.setColor((.7, .7, 1, 1))
        self.dirLightNP = render.attachNewNode(self.dirLight)
        self.dirLightNP.setHpr(0, -25, 0)
        render.setLight(self.dirLightNP)
        
    def setupCollisions(self):
        #make a collision traverser, set it to default
        base.cTrav = CollisionTraverser()
        self.cHandler = CollisionHandlerEvent()
        #set the pattern for the event sent on collision
        # "%in" is substituted with the name of the into object
        self.cHandler.setInPattern("%fn-and-%in")
        
        cSphere = CollisionSphere((0,-5,0), 30)
        cNode = CollisionNode("spider")
        cNode.addSolid(cSphere)
        #spider is *only* a from object
        cNode.setIntoCollideMask(BitMask32.allOff())
        self.spiderCollisionNode = self.spider.attachNewNode(cNode)
        
        #self.spiderCollisionNode.show()
        base.cTrav.addCollider(self.spiderCollisionNode, self.cHandler)
        
        
    def pipeCollide(self, cEntry):
        self.numCollisions += 1
        print self.numCollisions
        #print cEntry.getIntoNodePath().getParent().getParent().getName()
        #print "\n\n\n"
        modelKey = cEntry.getIntoNodePath().getParent().getParent().getKey()
        self.currentPipe = self.getPipe(modelKey)
        if self.currentPipe.actionCommand.isEmpty() == False:
            print "Pipe Action Command = " + str(self.currentPipe.actionCommand.getCommand())
            self.currentActionCommand = ActionCommand(2,"",self.currentPipe.actionCommand.getCommand())
            print "Player Action Command = " + str(self.currentActionCommand.getCommand())
            self.currentPipe.actionCommand.blankCommand()
            print "Player Action Command = " + str(self.currentActionCommand.getCommand())

            print "------!!!!!!!!!!!!!------"

            if self.TimerGoing == False:
                self.TimeLeft = self.DefaultTime
                self.TimerGoing = True
           
            
    def getPipe(self, model):
        modelPath = model
        print modelPath
        print "CollideKey: " + str(modelPath)
        #KeyTestDebug
        for i in range(self.pipeList.__len__()):
            print "Key "+ str(i) +":" + str(self.pipeList[i].key)
        for i in range(self.pipeList.__len__()):
            print "Testing Key: " + str(self.pipeList[i].key)
            if self.pipeList[i].key == modelPath: return(self.pipeList[i])
        
    def update_game_Hud(self,task):
        self.gameScore = self.gameScore + globalClock.getDt()
        tempScore = int(self.gameScore * 100)
        self.Hud.updateHud(self.playerStability,tempScore,self.currentActionCommand.getOriginal())
        return Task.cont    
Beispiel #6
0
class Tricker(object):
    def __init__(self):
        ## tricker-model taken from tutorial files from
        # https://www.digitaltutors.com/tutorial/1478-Animating-an-Acrobatic-Fight-Scene-in-Maya
        self.actor = Actor(
            "tp/models/tricker-model", {
                "gainer": "tp/anims/tricker-gainer",
                "gainer_bad": "tp/anims/tricker-gainer-bad",
                "gswitch": "tp/anims/tricker-gswitch",
                "gswitch_bad": "tp/anims/tricker-gswitch-bad",
                "btwist": "tp/anims/tricker-btwist",
                "btwist_bad": "tp/anims/tricker-btwist",
                "cork": "tp/anims/tricker-cork",
                "cork_bad": "tp/anims/tricker-cork-bad",
                "doublecork": "tp/anims/tricker-dubcork",
                "doublecork_bad": "tp/anims/tricker-dubcork",
                "fall_swing": "tp/anims/tricker-fall-left",
                "initial_swing": "tp/anims/tricker-initial-swing",
                "end_swing": "tp/anims/tricker-end-swing",
                "fall_reversal": "tp/anims/tricker-fall-right",
                "raiz": "tp/anims/tricker-raiz",
                "raiz_bad": "tp/anims/tricker-raiz-bad",
                "fiveForty": "tp/anims/tricker-540",
                "fiveForty_bad": "tp/anims/tricker-540-bad"
            })

        #saveDict contains all info to be saved to json
        self.saveDict = {
            'name': '',
            'level': 0,
            'totalStam': 100,
            'skills': {
                "gainer": 1,
                "gswitch": 1,
                "btwist": 1,
                "cork": 1,
                "doublecork": 1,
                "raiz": 1,
                "fiveForty": 1
            }
        }
        self.updateAttributes()

        # trickMap is different - this shit maps trick names to their classes
        # in order to get class data just given an animation name
        self.trickMap = {
            'gainer': self.gainer,
            'gswitch': self.gswitch,
            'btwist': self.btwist,
            'cork': self.cork,
            'doublecork': self.doublecork,
            'raiz': self.raiz,
            'fiveForty': self.fiveForty
        }

        # runtime traits, to be reset with reset function
        # NOTE: You MUST add any vals here to the reset function below
        self.prevTrick = None
        self.stamina = self.totalStam
        self.grade = ''
        self.timing = ''
        self.score = 0
        self.comboLength = 0
        self.comboEnded = False
        self.comboContinuing = False
        self.falling = False
        self.midTrick = False

    def comboHasEnded(self):
        return self.comboEnded

    def hasName(self):
        return self.name != ''

    def isFalling(self):
        return self.falling

    def isMidTrick(self):
        return self.midTrick

    def getName(self):
        if self.hasName(): return self.name
        else: return "NewPlayer"

    def getLevel(self):
        return str(self.saveDict['level'])

    def getTotalStam(self):
        return str(self.totalStam)

    def getSaveDict(self):
        return self.saveDict

    def getSkillDict(self):
        return self.skillDict

    def getComboLength(self):
        return str(int(self.comboLength))

    def getScore(self):
        return str(int(self.score))

    def getTiming(self):
        return self.timing

    def updateStamina(self, stamCost):
        self.stamina -= stamCost

    def updateComboLength(self):
        self.comboLength += 1

    def updateScore(self, trick, goodPercentage, comboLength):
        b = trick.getDifficulty() * 100
        score = b + (b * goodPercentage) + (b * (comboLength / 5))
        self.score += score

    def getTimingBarPercentage(self):
        currAnim = self.actor.getCurrentAnim()

        if currAnim and 'fall' not in currAnim and 'initial' not in currAnim and 'end' not in currAnim:
            trick = self.trickMap[currAnim.split('_')[0]]
            sweetspot = trick.getSweetSpot()
            currFrame = self.actor.getCurrentFrame()
            dist = abs(sweetspot - currFrame)
            eMargin = trick.getDuration() * .2 / trick.getDifficulty()

            if dist > eMargin: gp = 0
            else: gp = 1 - (dist / eMargin)

            return gp
        else:
            return 0

    def getTrueStam(self):
        return self.stamina

    def stamPercentage(self):
        sp = self.stamina / self.totalStam
        if sp < 0: sp = 0
        return sp

    def getGrade(self):
        return self.grade

    def increaseSkill(self, trick, grade):
        if grade == 0: b = 2
        elif grade == 1: b = 1.66
        elif grade == 2: b = 1.33
        elif grade == 3: b = 1
        elif grade == 4: b = 1

        # This line makes the exp curve and prevents leveling over 100
        exp = b - math.log(self.saveDict['skills'][str(trick)])

        if exp < 0: exp = 0

        self.saveDict['skills'][str(trick)] += exp
        self.updateAttributes()

    """
    debug animation procedure:
    comment out:
    return under if self.comboEnded in self.tryTrick
    both moveInterval.start() in self.doTrickTask
    
    
    """

    def tryTrick(self, trick, taskMgr):
        if self.midTrick:
            print("already received an input")
            return
        self.midTrick = True
        if self.comboEnded:
            print("combo ended - no more tricking 4 u")
            return

        if self.falling:
            print("can't trick once you've fallen boi")
            return

        if self.stamina <= 0:
            self.comboEnded = True
            print("no stamina to throw trick!! ending combo")
            return

        self.comboContinuing = True
        distance = (0, 0, 0)

        self.grade = 0
        currAnim = self.actor.getCurrentAnim()
        goodPercentage = trick.getGoodPercentage(self.grade)

        if self.prevTrick: distance = self.prevTrick.getDistance()

        if currAnim and self.prevTrick:
            if (self.prevTrick.getExitTransition() !=
                    trick.getEntryTransition()):
                self.comboEnded = True
                print("invalid transition - ended combo")
                return

            distance = self.prevTrick.getDistance()

            currFrame = self.actor.getCurrentFrame(currAnim)
            numFrames = self.actor.getNumFrames(currAnim)
            framesLeft = numFrames - currFrame

            (self.grade, self.timing) = self.prevTrick.getGrade(currFrame)
            if self.grade == 4:
                self.falling = True
                print("Combo failed - falling")
                self.prevTrick = None

            stamCost = trick.getStamCost(self.grade)
            self.updateStamina(stamCost)

            goodPercentage = trick.getGoodPercentage(self.grade)

            # 0.06 is the time it takes for 2 frames - smooth blending
            delayTime = framesLeft / 30 - 0.09
            taskMgr.doMethodLater(
                delayTime,
                self.doTrickTask,
                'doTrick',
                extraArgs=[str(trick), goodPercentage, distance, taskMgr],
                appendTask=True)
        else:
            stamCost = trick.getStamCost(self.grade)
            self.updateStamina(stamCost)

            if trick.entryTransition == 'swing':
                self.actor.play('initial_swing')
                distance = (-2, 2, 0)
                taskMgr.doMethodLater(
                    21 / 30,
                    self.doTrickTask,
                    'doTrick',
                    extraArgs=[str(trick), goodPercentage, distance, taskMgr],
                    appendTask=True)
            else:
                taskMgr.add(
                    self.doTrickTask,
                    'doTrick',
                    extraArgs=[str(trick), goodPercentage, distance, taskMgr],
                    appendTask=True)

        if self.getTrueStam() < 0:
            self.falling = True
            return ("Ran out of stamina mid-trick - falling!")

        if not self.falling:
            self.updateScore(trick, goodPercentage, self.comboLength)
            self.updateComboLength()
            self.prevTrick = trick

        # this is tricking - you still learn from your falls!
        self.increaseSkill(trick, self.grade)

    def doTrickTask(self, animation, goodPercentage, distance, taskMgr, task):
        print("goodp:", goodPercentage)
        self.actor.setPos(self.actor, distance)
        badAnim = str(animation + "_bad")

        if not self.isFalling():
            self.actor.enableBlend()
            self.actor.setControlEffect(badAnim, 1 - goodPercentage)
            self.actor.setControlEffect(animation, goodPercentage)
            self.actor.play(badAnim)
            self.actor.play(animation)
            self.actor.disableBlend()

            self.comboContinuing = False
            self.midTrick = False
            taskMgr.add(self.checkTrickStateTask,
                        'checkTrickState',
                        extraArgs=[animation],
                        appendTask=True)

        elif self.isFalling():
            trick = self.trickMap[animation]
            fallDist = trick.getDistance()
            exitTrans = trick.getExitTransition()
            fallingAnim = "fall_" + exitTrans

            fallStartFrame = trick.getDuration() - trick.getSweetSpot()
            regFrames = self.actor.getNumFrames(animation) - fallStartFrame

            fallSeq = Sequence(
                self.actor.actorInterval(badAnim, endFrame=regFrames),
                Func(self.playFall, fallingAnim, fallDist, exitTrans))
            fallSeq.start()

    # if moveInterval: moveInterval.start()
        return Task.done

    def playFall(self, fallingAnim, distance, exitTrans):
        # if fallingAnim == 'fall_swing':
        #     self.actor.enableBlend()
        #     self.actor.setControlEffect(badAnim, .5)
        #     self.actor.setControlEffect(fallingAnim, .5)
        #     self.actor.play(badAnim, fromFrame=startFrame)
        #     self.actor.play(fallingAnim)
        #     self.actor.disableBlend()
        # elif fallingAnim == 'fall_reversal':
        if exitTrans == 'swing':
            self.actor.setPos(self.actor, distance)
        self.actor.play(fallingAnim)

    def playSwingExit(self, distance):
        self.actor.setPos(self.actor, distance)
        self.actor.play('end_swing')

    def checkTrickStateTask(self, animation, task):
        # has to be -1 otherwise the currFrame never gets to the last frame. IDK why
        totalFrames = self.actor.getNumFrames(animation) - 1
        currFrame = self.actor.getCurrentFrame(animation)

        if self.comboContinuing or self.falling:
            print("comboContinuing or falling")
            return Task.done
        self.comboContinuing = False
        if currFrame == totalFrames:
            self.comboEnded = True
            print("no input received - combo ended")
            trick = self.trickMap[animation]
            distance = trick.getDistance()
            if trick.getExitTransition() == 'swing':
                self.playSwingExit(distance)
            return Task.done
        return Task.cont

    def reset(self):
        self.prevTrick = None
        self.stamina = 100
        self.grade = None
        self.timing = ''
        self.score = 0
        self.comboLength = 0
        self.comboEnded = False
        self.comboContinuing = False
        self.falling = False
        self.midTrick = False

    def setName(self, name):
        self.saveDict['name'] = name
        self.updateAttributes()

    def loadToSaveDict(self, indata):
        self.saveDict = indata
        self.updateAttributes()

    def updateAttributes(self):
        self.name = self.saveDict['name']
        self.totalStam = self.saveDict['totalStam']
        self.skillDict = self.saveDict['skills']

        # set tricker's level based on proficiency in all skills
        numTricks = totalSP = 0
        for trick in self.skillDict:
            numTricks += 1
            totalSP += self.skillDict[trick]
        self.level = int(totalSP / numTricks)
        self.saveDict['level'] = self.level

        # Load tricks
        self.gainer = Gainer(self)
        self.gswitch = Gswitch(self)
        self.btwist = Btwist(self)
        self.cork = Cork(self)
        self.doublecork = DoubleCork(self)
        self.raiz = Raiz(self)
        self.fiveForty = FiveForty(self)
Beispiel #7
0
class Vehicle:
    def __init__(self, app, model_name):
        model_file_name = 'assets/cars/{}/{}.bam'.format(
            model_name, model_name)
        self.app = app

        def animation_path(model, animation):
            base_path = 'assets/cars/animations/{}/{}-{}.bam'
            return base_path.format(model, model, animation)

        self.model = Actor(
            model_file_name,
            {
                #    AIRBRAKE: 'assets/cars/animations/{}-{}.bam'.format(model_name, AIRBRAKE),
                #    AIRBRAKE: animation_path(model_name, AIRBRAKE),
                #    STABILIZER_FINS: animation_path(model_name, STABILIZER_FINS),
            })
        self.model.enableBlend()
        self.model.setControlEffect(AIRBRAKE, 1)
        self.model.setControlEffect(STABILIZER_FINS, 1)
        # FIXME: This code fails due to a bug in Actor
        # airbrake_joints = [joint.name
        #                    for joint in self.model.getJoints()
        #                    if joint.name.startswith(AIRBRAKE)
        # ]
        # self.model.makeSubpart(AIRBRAKE, airbrake_joints)
        # stabilizer_joints = [joint.name
        #                      for joint in self.model.getJoints()
        #                      if joint.name.startswith(STABILIZER_FINS)
        # ]
        # self.model.makeSubpart(STABILIZER_FINS, stabilizer_joints)

        puppet = self.app.loader.load_model(model_file_name)
        puppet.find("**/armature").hide()
        puppet.reparentTo(self.model)

        # Get the vehicle data
        self.vehicle_data = VehicleData(puppet, model_name, 'cars')

        # Configure the physics node
        self.physics_node = BulletRigidBodyNode('vehicle')
        self.physics_node.set_friction(self.vehicle_data.friction)
        self.physics_node.set_linear_sleep_threshold(0)
        self.physics_node.set_angular_sleep_threshold(0)
        self.physics_node.setCcdMotionThreshold(1e-7)
        self.physics_node.setCcdSweptSphereRadius(0.5)
        self.physics_node.setMass(self.vehicle_data.mass)
        shape = BulletConvexHullShape()
        for geom_node in self.model.find_all_matches("**/+GeomNode"):
            for geom in geom_node.node().get_geoms():
                vertices = GeomVertexReader(geom.get_vertex_data(), 'vertex')
                while not vertices.is_at_end():
                    v_geom = vertices.getData3f()
                    v_model = self.model.get_relative_point(geom_node, v_geom)
                    shape.add_point(v_model)
        self.physics_node.add_shape(shape)
        self.vehicle = NodePath(self.physics_node)
        self.vehicle.set_collide_mask(CM_VEHICLE | CM_COLLIDE)
        self.model.reparent_to(self.vehicle)

        # Navigational aids
        self.target_node = self.app.loader.load_model('models/zup-axis')
        self.target_node.reparent_to(self.model)
        self.target_node.set_scale(1)
        self.target_node.set_render_mode_wireframe()
        self.target_node.hide()

        self.delta_node = self.app.loader.load_model('models/smiley')
        self.delta_node.set_pos(1, 10, 1)
        self.delta_node.reparent_to(base.cam)
        self.delta_node.hide()

        self.airbrake_state = 0.0
        self.airbrake_factor = 0.5
        self.airbrake_speed = 1 / self.vehicle_data.airbrake_duration
        self.stabilizer_fins_state = 0.0
        self.stabilizer_fins_speed = 1 / self.vehicle_data.stabilizer_fins_duration

        self.centroid = base.loader.load_model('models/smiley')
        self.centroid.reparent_to(self.vehicle)
        self.centroid.hide()

        # Gyro sound
        sound_file = 'assets/cars/{}/{}.wav'.format(
            model_name,
            GYROSCOPE_SOUND,
        )
        sound = base.audio3d.load_sfx(sound_file)
        self.model.set_python_tag(GYROSCOPE_SOUND, sound)
        base.audio3d.attach_sound_to_object(sound, self.model)
        sound.set_volume(0)
        sound.set_play_rate(0)
        sound.set_loop(True)
        sound.play()

        # Thruster limiting
        self.thruster_state = 0.0
        self.thruster_heat = 0.0

        for repulsor in self.vehicle_data.repulsor_nodes:
            self.add_repulsor(repulsor, model_name)
        for thruster in self.vehicle_data.thruster_nodes:
            self.add_thruster(thruster, model_name)

        # ECU data storage from frame to frame
        self.last_flight_height = None

        # FIXME: Move into a default controller
        self.inputs = {
            # Repulsors
            REPULSOR_ACTIVATION: 0.0,
            ACCELERATE: 0.0,
            TURN: 0.0,
            STRAFE: 0.0,
            HOVER: 0.0,
            FULL_REPULSORS: False,
            # Gyro
            ACTIVE_STABILIZATION_ON_GROUND: PASSIVE,
            ACTIVE_STABILIZATION_CUTOFF_ANGLE: PASSIVE,
            ACTIVE_STABILIZATION_IN_AIR: PASSIVE,
            TARGET_ORIENTATION: Vec3(0, 0, 0),
            # Thrust
            THRUST: 0.0,
            # Air foils
            AIRBRAKE: 0.0,
            STABILIZER_FINS: 0.0,
        }
        self.sensors = {}
        self.commands = {}

    def np(self):
        return self.vehicle

    def place(self, spawn_point):
        # FIXME: Pass a root node to __init__ instead
        self.vehicle.reparent_to(self.app.environment.model)
        connector = self.model.find("**/" + SPAWN_POINT_CONNECTOR)
        self.vehicle.set_hpr(-connector.get_hpr(spawn_point))
        self.vehicle.set_pos(-connector.get_pos(spawn_point))
        self.app.environment.add_physics_node(self.physics_node)

    def set_inputs(self, inputs):
        self.inputs = inputs

    def add_repulsor(self, node, model_name):
        ground_contact = self.app.loader.load_model(
            "assets/effect/repulsorhit.egg")
        ground_contact.set_scale(1)
        ground_contact.reparent_to(self.app.render)
        node.set_python_tag('ray_end', ground_contact)

        sound_file = 'assets/cars/{}/{}.wav'.format(
            model_name,
            REPULSOR_SOUND,
        )
        sound = base.audio3d.load_sfx(sound_file)
        node.set_python_tag(REPULSOR_SOUND, sound)
        base.audio3d.attach_sound_to_object(sound, node)
        sound.set_volume(0)
        sound.set_play_rate(0)
        sound.set_loop(True)
        sound.play()

    def add_thruster(self, node, model_name):
        node.set_python_tag(THRUSTER_POWER, 0.0)
        node.set_python_tag(THRUSTER_OVERHEATED, False)

        # Basic jet sound
        sound_file = 'assets/cars/{}/{}.wav'.format(
            model_name,
            THRUSTER_SOUND,
        )
        sound = base.audio3d.load_sfx(sound_file)
        node.set_python_tag(THRUSTER_SOUND, sound)
        base.audio3d.attach_sound_to_object(sound, node)
        sound.set_volume(0)
        sound.set_play_rate(0)
        sound.set_loop(True)
        sound.play()

        # Overheating sound
        sound_file = 'assets/cars/{}/{}.wav'.format(
            model_name,
            THRUSTER_OVERHEAT_SOUND,
        )
        sound = base.audio3d.load_sfx(sound_file)
        node.set_python_tag(THRUSTER_OVERHEAT_SOUND, sound)
        base.audio3d.attach_sound_to_object(sound, node)

    def game_loop(self):
        self.gather_sensors()
        self.ecu()
        self.apply_air_drag()
        self.apply_repulsors()
        self.apply_gyroscope()
        self.apply_thrusters()
        self.apply_airbrake()
        self.apply_stabilizer_fins()

    def gather_sensors(self):
        # Gather data repulsor ray collisions with ground
        repulsor_data = []
        for node in self.vehicle_data.repulsor_nodes:
            data = RepulsorData()
            repulsor_data.append(data)
            max_distance = node.get_python_tag(ACTIVATION_DISTANCE)
            data.position = node.get_pos(self.vehicle)
            data.direction = self.app.render.get_relative_vector(
                node,
                Vec3(0, 0, -max_distance),
            )
            # FIXME: `self.app.environment.physics_world` is ugly.
            feeler = self.app.environment.physics_world.ray_test_closest(
                base.render.get_relative_point(self.vehicle, data.position),
                base.render.get_relative_point(self.vehicle,
                                               data.position + data.direction),
                CM_TERRAIN,
            )
            if feeler.has_hit():
                data.active = True
                data.fraction = feeler.get_hit_fraction()
                data.contact = self.vehicle.get_relative_point(
                    self.app.render,
                    feeler.get_hit_pos(),
                )
            else:
                data.active = False
                data.fraction = 1.0

        # Find the local ground's normal vector
        local_up = False
        contacts = [
            data.contact for data in repulsor_data
            if data.active and self.inputs[REPULSOR_ACTIVATION]
        ]
        if len(contacts) > 2:
            local_up = True
            target_mode = self.inputs[ACTIVE_STABILIZATION_ON_GROUND]
            # We can calculate a local up as the smallest base vector of the
            # point cloud of contacts.
            centroid = reduce(lambda a, b: a + b, contacts) / len(contacts)
            covariance = [
                [c.x, c.y, c.z]
                for c in [contact - centroid for contact in contacts]
            ][0:3]  # We need exactly 3, no PCA yet. :(
            eigenvalues, eigenvectors = numpy.linalg.eig(
                numpy.array(covariance))
            # FIXME: These few lines look baaaad...
            indexed_eigenvalues = enumerate(eigenvalues)

            def get_magnitude(indexed_element):
                index, value = indexed_element
                return abs(value)

            sorted_indexed_eigenvalues = sorted(
                indexed_eigenvalues,
                key=get_magnitude,
            )
            # The smallest eigenvalue leads to the ground plane's normal
            up_vec_idx, _ = sorted_indexed_eigenvalues[0]
            up_vec = VBase3(*eigenvectors[:, up_vec_idx])
            # Point into the upper half-space
            if up_vec.z < 0:
                up_vec *= -1
            # Calculate the forward of the centroid
            centroid_forward = Vec3(0, 1,
                                    0) - up_vec * (Vec3(0, 1, 0).dot(up_vec))
            # FIXME: Huh?
            forward_planar = centroid_forward - up_vec * (
                centroid_forward.dot(up_vec))
            # Now let's orient and place the centroid
            self.centroid.set_pos(self.vehicle, (0, 0, 0))
            self.centroid.heads_up(forward_planar, up_vec)
            self.centroid.set_pos(self.vehicle, centroid)

            # Flight height for repulsor attenuation
            flight_height = -self.centroid.get_z(self.vehicle)
            if self.last_flight_height is not None:
                climb_speed = (flight_height -
                               self.last_flight_height) / globalClock.dt
            else:
                climb_speed = 0
            self.last_flight_height = flight_height
        else:
            local_up = False
            self.last_flight_height = None
            flight_height = 0.0
            climb_speed = 0.0

        # Air drag
        movement = self.physics_node.get_linear_velocity()

        drag_coefficient = self.vehicle_data.drag_coefficient
        drag_area = self.vehicle_data.drag_area + \
                    self.vehicle_data.airbrake_area * self.airbrake_state + \
                    self.vehicle_data.stabilizer_fins_area * self.stabilizer_fins_state
        air_density = self.app.environment.env_data.air_density
        air_speed = -self.vehicle.get_relative_vector(
            base.render,
            movement,
        )

        # drag_force = 1/2*drag_coefficient*mass_density*area*flow_speed**2
        result = Vec3(air_speed)
        result = (result**3) / result.length()
        result.componentwise_mult(drag_area)
        result.componentwise_mult(self.vehicle_data.drag_coefficient)
        drag_force = result * 1 / 2 * air_density

        self.sensors = {
            CURRENT_ORIENTATION: self.vehicle.get_hpr(self.app.render),
            CURRENT_MOVEMENT: movement,
            CURRENT_ROTATION: self.physics_node.get_angular_velocity(),
            LOCAL_DRAG_FORCE: drag_force,
            REPULSOR_DATA: repulsor_data,
            LOCAL_UP: local_up,
            FLIGHT_HEIGHT: flight_height,
            CLIMB_SPEED: climb_speed,
        }

    def ecu(self):
        repulsor_target_angles = self.ecu_repulsor_reorientation()
        repulsor_activation, delta_height, projected_delta_height, \
            power_frac_needed = self.ecu_repulsor_activation()
        gyro_rotation = self.ecu_gyro_stabilization()
        thruster_activation = [
            self.inputs[THRUST] for _ in self.vehicle_data.thruster_nodes
        ]
        airbrake = self.inputs[AIRBRAKE]
        stabilizer_fins = self.inputs[STABILIZER_FINS]

        self.commands = {
            # Steering commands
            REPULSOR_TARGET_ORIENTATIONS: repulsor_target_angles,
            REPULSOR_ACTIVATION: repulsor_activation,
            GYRO_ROTATION: gyro_rotation,
            THRUSTER_ACTIVATION: thruster_activation,
            AIRBRAKE: airbrake,
            STABILIZER_FINS: stabilizer_fins,
            # ECU data output; Interesting numbers we found along the way.
            HEIGHT_OVER_TARGET: delta_height,
            HEIGHT_OVER_TARGET_PROJECTED: projected_delta_height,
            REPULSOR_POWER_FRACTION_NEEDED: power_frac_needed,
        }

    def ecu_repulsor_reorientation(self):
        # Calculate effective repulsor motion blend values
        accelerate = self.inputs[ACCELERATE]
        turn = self.inputs[TURN]
        strafe = self.inputs[STRAFE]
        hover = self.inputs[HOVER]
        length = sum([abs(accelerate), abs(turn), abs(strafe), hover])
        if length > 1:
            accelerate /= length
            turn /= length
            strafe /= length
            hover /= length
        # Split the turn signal into animation blend factors
        if turn > 0.0:
            turn_left = 0.0
            turn_right = turn
        else:
            turn_left = -turn
            turn_right = 0.0
        # Blend the repulsor angle
        repulsor_target_angles = []
        for node in self.vehicle_data.repulsor_nodes:
            acc_angle = -(node.get_python_tag(ACCELERATE)) * accelerate
            turn_left_angle = node.get_python_tag(TURN_LEFT) * turn_left
            turn_right_angle = node.get_python_tag(TURN_RIGHT) * turn_right
            strafe_angle = node.get_python_tag(STRAFE) * strafe
            hover_angle = node.get_python_tag(HOVER) * hover
            angle = acc_angle + turn_left_angle + turn_right_angle + \
                    strafe_angle + hover_angle
            repulsor_target_angles.append(angle)
        return repulsor_target_angles

    def ecu_repulsor_activation(self):
        # Do we know how high we are flying?
        if self.sensors[LOCAL_UP]:
            tau = self.inputs[TARGET_FLIGHT_HEIGHT_TAU]
            # What would we be at in tau seconds if we weren't using repulsors?
            flight_height = self.sensors[FLIGHT_HEIGHT]
            target_flight_height = self.inputs[TARGET_FLIGHT_HEIGHT]
            delta_height = flight_height - target_flight_height
            gravity_z = self.centroid.get_relative_vector(
                self.app.render,
                self.app.environment.physics_world.get_gravity(),
            ).get_z()
            # Since gravity is an acceleration
            gravity_h = 1 / 2 * gravity_z * tau**2
            climb_rate = self.sensors[CLIMB_SPEED] * tau
            projected_delta_height = delta_height + gravity_h + climb_rate
            # Are we sinking?
            if projected_delta_height <= 0:
                # Our projected height will be under our target height, so we
                # will need to apply the repulsors to make up the difference.
                # How much climb can each repulsor provide at 100% power right
                # now?
                max_powers = [
                    node.get_python_tag(FORCE)
                    for node in self.vehicle_data.repulsor_nodes
                ]
                transferrable_powers = [
                    max_power * cos(0.5 * pi * data.fraction)
                    for max_power, data in zip(
                        max_powers,
                        self.sensors[REPULSOR_DATA],
                    )
                ]
                angle_ratios = [
                    cos(node.get_quat(self.vehicle).get_angle_rad())
                    for node in self.vehicle_data.repulsor_nodes
                ]
                angled_powers = [
                    power * ratio
                    for power, ratio in zip(transferrable_powers, angle_ratios)
                ]
                # We don't want to activate the repulsors unevenly, so we'll
                # have to go by the weakest link.
                total_angled_power = min(angled_powers) * len(angled_powers)
                # How high can we climb under 100% repulsor power?
                max_climb = 1 / 2 * total_angled_power * tau**2 / self.vehicle_data.mass
                # The fraction of power needed to achieve the desired climb
                power_frac_needed = -projected_delta_height / max_climb
                # ...and store it.
                repulsor_activation = [
                    power_frac_needed for _ in self.vehicle_data.repulsor_nodes
                ]
            else:
                # We're not sinking.
                repulsor_activation = [
                    0.0 for _ in self.vehicle_data.repulsor_nodes
                ]
                power_frac_needed = 0.0
        else:  # We do not have ground contact.
            repulsor_activation = [
                0.0 for _ in self.vehicle_data.repulsor_nodes
            ]
            delta_height = 0.0
            projected_delta_height = 0.0
            power_frac_needed = 0.0
        # The driver gives 100% repulsor power, no matter how high we are, or
        # whether we even have ground contact.
        if self.inputs[FULL_REPULSORS]:
            repulsor_activation = [
                self.inputs[REPULSOR_ACTIVATION]
                for _ in self.vehicle_data.repulsor_nodes
            ]
        return repulsor_activation, delta_height, projected_delta_height,\
            power_frac_needed

    def ecu_gyro_stabilization(self):
        # Active stabilization and angular dampening
        # FIXME: Get from self.inputs
        tau = 0.2  # Seconds until target orientation is reached

        if self.sensors[LOCAL_UP]:
            target_mode = self.inputs[ACTIVE_STABILIZATION_ON_GROUND]
        else:
            target_mode = self.inputs[ACTIVE_STABILIZATION_IN_AIR]

        if target_mode == TO_HORIZON:
            # Stabilize to the current heading, but in a horizontal
            # orientation
            self.target_node.set_hpr(
                self.app.render,
                self.vehicle.get_h(),
                0,
                0,
            )
        elif target_mode == TO_GROUND:
            # Stabilize towards the local up
            self.target_node.set_hpr(self.centroid, (0, 0, 0))

        if target_mode != PASSIVE:
            xyz_driver_modification = self.inputs[TARGET_ORIENTATION]
            hpr_driver_modification = VBase3(
                xyz_driver_modification.z,
                xyz_driver_modification.x,
                xyz_driver_modification.y,
            )
            self.target_node.set_hpr(
                self.target_node,
                hpr_driver_modification,
            )

            # Now comes the math.
            orientation = self.vehicle.get_quat(self.app.render)
            target_orientation = self.target_node.get_quat(self.app.render)
            delta_orientation = target_orientation * invert(orientation)
            self.delta_node.set_quat(invert(delta_orientation))

            delta_angle = delta_orientation.get_angle_rad()
            if abs(delta_angle) < (pi / 360 * 0.1) or isnan(delta_angle):
                delta_angle = 0
                axis_of_torque = VBase3(0, 0, 0)
            else:
                axis_of_torque = delta_orientation.get_axis()
                axis_of_torque.normalize()
                axis_of_torque = self.app.render.get_relative_vector(
                    self.vehicle,
                    axis_of_torque,
                )
            if delta_angle > pi:
                delta_angle -= 2 * pi

            # If the mass was standing still, this would be the velocity that
            # has to be reached to achieve the targeted orientation in tau
            # seconds.
            target_angular_velocity = axis_of_torque * delta_angle / tau
        else:  # `elif target_mode == PASSIVE:`, since we might want an OFF mode
            # Passive stabilization, so this is the pure commanded impulse
            target_angular_velocity = self.app.render.get_relative_vector(
                self.vehicle,
                self.inputs[TARGET_ORIENTATION] * tau / pi,
            )

        # But we also have to cancel out the current velocity for that.
        angular_velocity = self.physics_node.get_angular_velocity()
        countering_velocity = -angular_velocity

        # An impulse of 1 causes an angular velocity of 2.5 rad on a unit mass,
        # so we have to adjust accordingly.
        target_impulse = target_angular_velocity / 2.5 * self.vehicle_data.mass
        countering_impulse = countering_velocity / 2.5 * self.vehicle_data.mass

        # Now just sum those up, and we have the impulse that needs to be
        # applied to steer towards target.
        impulse = target_impulse + countering_impulse
        return impulse

    def apply_air_drag(self):
        drag_force = self.sensors[LOCAL_DRAG_FORCE]
        global_drag_force = base.render.get_relative_vector(
            self.vehicle,
            drag_force,
        )
        if not isnan(global_drag_force.x):
            self.physics_node.apply_central_impulse(
                global_drag_force * globalClock.dt, )

    def apply_repulsors(self):
        dt = globalClock.dt
        repulsor_data = zip(self.vehicle_data.repulsor_nodes,
                            self.sensors[REPULSOR_DATA],
                            self.commands[REPULSOR_ACTIVATION],
                            self.commands[REPULSOR_TARGET_ORIENTATIONS])
        for node, data, activation, angle in repulsor_data:
            active = data.active
            frac = data.fraction
            # Repulse in current orientation
            if active and activation:
                if activation > 1.0:
                    activation = 1.0
                if activation < 0.0:
                    activation = 0.0
                # Repulsor power at zero distance
                base_strength = node.get_python_tag(FORCE)
                # Effective fraction of repulsors force
                transfer_frac = cos(0.5 * pi * frac)
                # Effective repulsor force
                strength = base_strength * activation * transfer_frac
                # Resulting impulse
                impulse_dir = Vec3(0, 0, 1)
                impulse_dir_world = self.app.render.get_relative_vector(
                    node,
                    impulse_dir,
                )
                impulse = impulse_dir_world * strength
                # Apply
                repulsor_pos = node.get_pos(self.vehicle)
                # FIXME! The position at which an impulse is applied seems to be
                # centered around node it is applied to, but offset in the world
                # orientation. So, right distance, wrong angle. This is likely a
                # bug in Panda3D's Bullet wrapper. Or an idiosyncracy of Bullet.
                self.physics_node.apply_impulse(
                    impulse * dt,
                    self.app.render.get_relative_vector(
                        self.vehicle,
                        repulsor_pos,
                    ),
                )

                # Contact visualization node
                max_distance = node.get_python_tag(ACTIVATION_DISTANCE)
                contact_distance = -impulse_dir_world * max_distance * frac
                contact_node = node.get_python_tag('ray_end')
                contact_node.set_pos(
                    node.get_pos(self.app.render) + contact_distance, )
                #contact_node.set_hpr(node, 0, -90, 0) # Look towards repulsor
                #contact_node.set_hpr(0, -90, 0) # Look up
                contact_node.set_hpr(0, -90,
                                     contact_node.getR() +
                                     4)  # Look up and rotate
                contact_node.set_scale(1 - frac)

                contact_node.show()
                # Sound
                sound = node.get_python_tag(REPULSOR_SOUND)
                sound.set_volume(activation * 20)
                sound.set_play_rate((1 + activation / 2) * 2)
            else:
                node.get_python_tag('ray_end').hide()
                sound = node.get_python_tag(REPULSOR_SOUND)
                sound.set_volume(0.0)
                sound.set_play_rate(0.0)
            # Reorient
            old_hpr = node.get_python_tag(REPULSOR_OLD_ORIENTATION)
            want_hpr = VBase3(angle.z, angle.x, angle.y)
            delta_hpr = want_hpr - old_hpr
            max_angle = node.get_python_tag(REPULSOR_TURNING_ANGLE) * dt
            if delta_hpr.length() > max_angle:
                delta_hpr = delta_hpr / delta_hpr.length() * max_angle
            new_hpr = old_hpr + delta_hpr
            node.set_hpr(new_hpr)
            node.set_python_tag(REPULSOR_OLD_ORIENTATION, new_hpr)

    def apply_gyroscope(self):
        impulse = self.commands[GYRO_ROTATION]
        # Clamp the impulse to what the "motor" can produce.
        max_impulse = self.vehicle_data.max_gyro_torque
        if impulse.length() > max_impulse:
            clamped_impulse = impulse / impulse.length() * max_impulse
        else:
            clamped_impulse = impulse

        self.physics_node.apply_torque_impulse(clamped_impulse)

        impulse_ratio = clamped_impulse.length() / max_impulse
        sound = self.model.get_python_tag(GYROSCOPE_SOUND)
        sound.set_volume(0.5 * impulse_ratio)
        sound.set_play_rate(0.9 + 0.1 * impulse_ratio)

    def apply_thrusters(self):
        dt = globalClock.dt
        thruster_data = zip(
            self.vehicle_data.thruster_nodes,
            self.commands[THRUSTER_ACTIVATION],
        )
        for node, thrust in thruster_data:
            if self.thruster_heat >= 1.0:
                thrust = 0.0
            current_power = node.get_python_tag(THRUSTER_POWER)
            # Adjust thrust to be within bounds of thruster's ability to adjust
            # thrust.
            ramp_time = node.get_python_tag(THRUSTER_RAMP_TIME)
            ramp_step = (1 / ramp_time) * globalClock.dt
            thrust = adjust_within_limit(thrust, current_power, ramp_step)
            node.set_python_tag(THRUSTER_POWER, thrust)
            max_force = node.get_python_tag(FORCE)
            real_force = max_force * thrust
            # FIXME: See repulsors above for the shortcoming that this suffers
            thruster_pos = base.render.get_relative_vector(
                self.vehicle,
                node.get_pos(self.vehicle),
            )
            thrust_direction = self.app.render.get_relative_vector(
                node, Vec3(0, 0, 1))
            self.physics_node.apply_impulse(
                thrust_direction * real_force * dt,
                thruster_pos,
            )
            heating = node.get_python_tag(THRUSTER_HEATING)
            cooling = node.get_python_tag(THRUSTER_COOLING)
            effective_heating = -cooling + (heating - cooling) * thrust
            self.thruster_heat += effective_heating * dt
            if self.thruster_heat < 0.0:
                self.thruster_heat = 0.0

            # Sound
            sound = node.get_python_tag(THRUSTER_SOUND)
            sound.set_volume(thrust * 5)
            sound.set_play_rate((1 + thrust / 20) / 3)

            was_overheated = node.get_python_tag(THRUSTER_OVERHEATED)
            is_overheated = self.thruster_heat > 1.0
            if is_overheated and not was_overheated:
                sound = node.get_python_tag(THRUSTER_OVERHEAT_SOUND)
                sound.set_volume(5)
                sound.play()
                node.set_python_tag(THRUSTER_OVERHEATED, True)
            if not is_overheated:
                node.set_python_tag(THRUSTER_OVERHEATED, False)

    def apply_airbrake(self):
        # Animation and state update only, since the effect is in air drag
        dt = globalClock.dt
        target = self.commands[AIRBRAKE]
        max_movement = self.airbrake_speed * dt
        # Clamp change to available speed
        delta = target - self.airbrake_state
        if abs(delta) > max_movement:
            self.airbrake_state += copysign(max_movement, delta)
        else:
            self.airbrake_state = target
        if self.airbrake_state > 1.0:
            self.airbrake_state = 1.0
        if self.airbrake_state < 0.0:
            self.airbrake_state = 0.0
        #self.model.pose(AIRBRAKE, self.airbrake_state, partName=AIRBRAKE)
        self.model.pose(AIRBRAKE, self.airbrake_state)

    def apply_stabilizer_fins(self):
        # Animation and state update only, since the effect is in air drag
        dt = globalClock.dt
        target = self.commands[STABILIZER_FINS]
        max_movement = self.stabilizer_fins_speed * dt
        # Clamp change to available speed
        delta = target - self.stabilizer_fins_state
        if abs(delta) > max_movement:
            self.stabilizer_fins_state += copysign(max_movement, delta)
        else:
            self.stabilizer_fins_state = target
        if self.stabilizer_fins_state > 1.0:
            self.stabilizer_fins_state = 1.0
        if self.stabilizer_fins_state < 0.0:
            self.stabilizer_fins_state = 0.0
        self.model.pose(
            STABILIZER_FINS,
            self.stabilizer_fins_state,
            #partName=STABILIZER_FINS,
        )
        # FIXME: Implement stabilizing effect

    def shock(self, x=0, y=0, z=0):
        self.physics_node.apply_impulse(
            Vec3(0, 0, 0),
            Vec3(random(), random(), random()) * 10,
        )
        self.physics_node.apply_torque_impulse(Vec3(x, y, z))
Beispiel #8
0
class Character(Shooter, Unit):
    """A game character.

    Represents a unit controlled by a player.

    Args:
        id_ (int): This character unique id.
        name (str): This character name.
        class_ (str): This character class.
        sex (str): This character gender.
        team (crew.Crew): The Crew object.
        desc (dict):
            Optional. This character description (used
            for loading character from a saved game).
    """

    def __init__(self, id_, name, class_, sex, team, desc=None):
        Unit.__init__(
            self, "character_" + str(id_), class_, CLASSES[sex + "_" + class_]
        )
        Shooter.__init__(self)

        self._team = team
        self._current_pos = None
        self._current_anim = None
        self._idle_seq = None
        self._cohesion_ball = None
        self._is_stunned = False
        self._is_stimulated = False
        self._health_bar = None

        self.inhale = 15
        self.damage_range = [5, 8]
        self.clear_damage = [5, 8]
        self.effects = {}
        self._yeah_snds = []

        self.name = name
        self.sex = sex
        if sex == "male":
            self.heshe = base.labels.PRONOUNS[0]  # noqa: F821
            self.hisher = base.labels.PRONOUNS[1]  # noqa: F821
            self.himher = base.labels.PRONOUNS[2]  # noqa: F821
        else:
            self.heshe = base.labels.PRONOUNS[3]  # noqa: F821
            self.hisher = base.labels.PRONOUNS[4]  # noqa: F821
            self.himher = base.labels.PRONOUNS[5]  # noqa: F821

        if desc:
            self._energy = desc["energy"]
            self.is_diseased = desc["is_diseased"]
            self.get_well_score = desc["get_well_score"]
            self.traits = desc["traits"]
            self.disabled_traits = desc["disabled_traits"]

            if self.is_diseased:
                taskMgr.doMethodLater(  # noqa: F821
                    60, self.get_well, self.id + "_get_well"
                )
        else:
            self._energy = 100
            self.is_diseased = False
            self.get_well_score = 0

            self.disabled_traits = []
            self.traits = []
            traits = copy.copy(base.labels.TRAITS)  # noqa: F821
            for _ in range(random.randint(0, 2)):
                self.traits.append(random.choice(take_random(traits)))

    @property
    def clear_delay(self):
        """
        Delay between this character death
        and clearing the character object.

        Returns:
            float: Seconds to wait before clearing.
        """
        return 3.5

    @property
    def damage(self):
        """This character one-time calculated damage.

        Returns:
            float: Damage one-time made by this character.
        """
        return random.uniform(*self.damage_range) * self.damage_factor

    @property
    def damage_factor(self):
        """This character damage factor.

        The damage factor depends on cohesion with another
        character on the same part of the Train.

        Returns:
            float: Damage one-time made by this character.
        """
        factor = self._team.calc_cohesion_factor(self.current_part.chars, self)
        if (
            # Loner
            base.labels.TRAITS[4][1] in self.traits  # noqa: F821
            and len(self.current_part.chars) == 1
        ):
            return factor * 1.3

        return factor

    @property
    def description(self):
        """This character state in saveable form.

        Used for saving the character.

        Returns:
            dict: This character description.
        """
        return {
            "id": int(self.id.split("_")[1]),
            "name": self.name,
            "sex": self.sex,
            "class": self.class_,
            "health": self.health,
            "energy": self.energy,
            "place": self.current_part.name,
            "traits": self.traits,
            "disabled_traits": self.disabled_traits,
            "is_diseased": self.is_diseased,
            "get_well_score": self.get_well_score,
        }

    @property
    def energy(self):
        """This character energy.

        Returns:
            int: This character energy points.
        """
        return self._energy

    @energy.setter
    def energy(self, value):
        """This character energy setter.

        Args:
            value (int): New energy value.
        """
        self._energy = min(max(value, 0), 80 if self.is_diseased else 100)

    @property
    def shooting_speed(self):
        """Delay between shots of this unit.

        Returns:
            float: Delay between shots in seconds.
        """
        if base.labels.TRAITS[0][0] in self.traits:  # noqa: F821
            # Fast hands
            return 1.1 + random.uniform(0.1, 0.8)

        if base.labels.TRAITS[0][1] in self.traits:  # noqa: F821
            # Snail
            return 2 + random.uniform(0.1, 1.1)

        return 1.7 + random.uniform(0.1, 0.9)

    @property
    def statuses(self):
        """Return strings describing the current character status.

        Returns:
            list: Status description lines.
        """
        statuses = []
        if base.world.sun.is_dark:  # noqa: F821
            if base.labels.TRAITS[1][0] in self.traits:  # noqa: F821
                statuses.append(base.labels.STATUSES[0])  # noqa: F821
            elif base.train.lights_on:  # noqa: F821
                if "Floodlights" not in base.train.upgrades:  # noqa: F821
                    statuses.append(base.labels.STATUSES[1])  # noqa: F821
            else:
                statuses.append(base.labels.STATUSES[2])  # noqa: F821

        if self.energy <= 95:
            statuses.append(
                base.labels.STATUSES[3].format((100 - self.energy) // 5)  # noqa: F821
            )

        if not base.world.is_in_city and self.current_part is not None:  # noqa: F821
            factor = round(self.damage_factor, 2)
            if factor != 1:
                statuses.append(base.labels.STATUSES[4].format(factor))  # noqa: F821

        if self.health < 50 and base.labels.TRAITS[2][1] in self.traits:  # noqa: F821
            statuses.append(base.labels.STATUSES[5])  # noqa: F821

        if self.is_diseased:
            statuses.append(base.labels.STATUSES[6])  # noqa: F821

        if (
            base.labels.TRAITS[6][1] in self.traits  # noqa: F821
            and base.train.ctrl.current_speed > 0.75  # noqa: F821
        ):
            statuses.append(base.labels.STATUSES[7])  # noqa: F821

        return statuses[:4]

    @property
    def tooltip(self):
        """Tooltip to show on this character mouse pointing.

        Returns:
            str: This character name.
        """
        return self.name

    def attack(self, enemy_unit):
        """Make the given enemy unit this character's target.

        Args:
            enemy_unit (units.enemy.EnemyUnit): Enemy unit to attack.
        """
        if enemy_unit in self.current_part.enemies:
            self._target = enemy_unit

    def prepare(self):
        """Load the character model and positionate it.

        Tweak collision solid and sounds.
        """
        animations = {
            name: address(self.class_ + "-" + name)
            for name in (
                "die",
                "gun_up",
                "incline1",
                "release_gun",
                "stand_and_aim",
                "stand",
                "surrender",
                "tread1",
                "turn_head1",
                "stunned",
                "cough",
                "celebrate",
            )
        }
        self.model = Actor(address(self.sex + "_" + self.class_), animations)
        self.model.enableBlend()
        self.model.setControlEffect("stand", 1)

        self.model.setPlayRate(0.8, "stand_and_aim")
        self.model.setPlayRate(0.6, "stand")
        self.model.loop("stand")
        self._current_anim = "stand"

        self._health_bar = HealthBar(self)

        base.team.init_relations(self)  # noqa: F821

        taskMgr.doMethodLater(  # noqa: F821
            random.randint(40, 60), self._idle_animation, self.id + "_idle_anim"
        )
        self._col_node = self._init_col_node(
            NO_MASK, MOUSE_MASK, CollisionCapsule(0, 0, 0, 0, 0, 0.035, 0.035)
        )
        self.shot_snd = self._set_shoot_snd(self.class_data["shot_snd"])
        self._cough_snd = base.sound_mgr.loadSfx(  # noqa: F821
            "sounds/{sex}_cough.ogg".format(sex=self.sex)
        )
        base.sound_mgr.attachSoundToObject(self._cough_snd, self.model)  # noqa: F821
        self._die_snd = base.sound_mgr.loadSfx(  # noqa: F821
            "sounds/combat/{sex}_death.ogg".format(sex=self.sex)
        )
        base.sound_mgr.attachSoundToObject(self._die_snd, self.model)  # noqa: F821

        for i in range(1, 4):
            yeah_snd1 = base.sound_mgr.loadSfx(  # noqa: F821
                "sounds/{sex}_yes{num}.ogg".format(sex=self.sex, num=str(i))
            )
            base.sound_mgr.attachSoundToObject(yeah_snd1, self.model)  # noqa: F821

            self._yeah_snds.append(yeah_snd1)

        if self.class_ == "soldier":
            z = 0.064 if self.sex == "male" else 0.062
        elif self.class_ == "raider":
            z = 0.047
        elif self.class_ == "anarchist":
            z = 0.06 if self.sex == "male" else 0.057

        self._shoot_anim = self._set_shoot_anim(
            (0.004, 0.045, z), 97, self.class_data["shots_num"]
        )
        taskMgr.doMethodLater(  # noqa: F821
            self.class_data["energy_spend"],
            self._reduce_energy,
            self.id + "_reduce_energy",
        )
        self._prepare_cohesion_particles()
        self._prepare_auras()

    def _prepare_cohesion_particles(self):
        """Prepare cohesion skills particle effects."""
        effect = ParticleEffect()
        effect.loadConfig("effects/recall_the_past.ptf")

        self.effects["recall_the_past"] = {"length": 0.7, "effect": effect}

        effect = ParticleEffect()
        effect.loadConfig("effects/not_leaving_ours.ptf")

        self.effects["not_leaving_ours"] = {"length": 2, "effect": effect}

        effect = ParticleEffect()
        effect.loadConfig("effects/common_rage.ptf")

        self.effects["common_rage"] = {"length": 90, "effect": effect}

    def _prepare_auras(self):
        """Prepare cohesion skills aura effects."""
        aura = loader.loadModel(address("cover_fire_aura"))  # noqa: F821
        aura.hide()
        aura.reparentTo(self.model)
        aura.setY(0.007)

        self.effects["cover_fire"] = {
            "model": aura,
            "seq": Sequence(
                LerpScaleInterval(aura, 1.5, (2, 2, 2), (1, 1, 1)),
                LerpScaleInterval(aura, 1.5, (1, 1, 1), (2, 2, 2)),
            ),
        }

        aura = loader.loadModel(address("common_rage_aura"))  # noqa: F821
        aura.hide()
        aura.reparentTo(self.model)
        aura.setY(0.007)

        self.effects["common_rage_aura"] = {
            "model": aura,
            "seq": Sequence(
                LerpScaleInterval(aura, 1.5, (2, 2, 2), (1, 1, 1)),
                LerpScaleInterval(aura, 1.5, (1, 1, 1), (2, 2, 2)),
            ),
        }

        aura = loader.loadModel(address("hold_together_aura"))  # noqa: F821
        aura.hide()
        aura.reparentTo(self.model)
        aura.setY(0.007)

        self.effects["hold_together"] = {
            "model": aura,
            "seq": Sequence(
                LerpScaleInterval(aura, 1.5, (2, 2, 2), (1, 1, 1)),
                LerpScaleInterval(aura, 1.5, (1, 1, 1), (2, 2, 2)),
            ),
        }

    def play_cohesion_effect(self, name):
        """Play the given cohesion skill effect.

        Args:
            name (str): Name of the skill.
        """
        self.effects[name]["effect"].start(self.model, self.model)
        self.effects[name]["effect"].softStart()

        taskMgr.doMethodLater(  # noqa: F821
            self.effects[name]["length"],
            self.effects[name]["effect"].softStop,
            "stop_cohesion_effect",
            extraArgs=[],
        )

    def play_cohesion_aura(self, name):
        """Play the given aura effect.

        Args:
            name (str): The effect name.
        """
        self.effects[name]["model"].show()
        self.effects[name]["seq"].loop()

    def play_yes(self):
        """Play a voice sound, when the character is chosen."""
        random.choice(self._yeah_snds).play()

    def stop_aura_effect(self, name):
        """Stop the given aura effect.

        Args:
            name (str): The effect name.
        """
        self.effects[name]["model"].hide()
        self.effects[name]["seq"].finish()

    def celebrate(self, task):
        """Play victory celebration animation."""
        if self.current_part.name == "part_rest":
            return task.done

        LerpAnimInterval(self.model, 0.1, self._current_anim, "celebrate").start()
        self._current_anim = "celebrate"
        self.model.play("celebrate")
        return task.done

    def move_to(self, part, to_pos=None):
        """Move this Character to the given Train part.

        If the movement is done with a positions exchange
        between two characters, the method doesn't
        take or release characters cells, just replaces
        one character with another one.

        Args:
            part (train.part.TrainPart):
                Train part to move this Character to.
            to_pos (dict):
                Position to move this Character to.

        Returns:
            bool: True, if the character was moved.
        """
        if to_pos:
            part.chars.append(self)
            pos = to_pos
        else:
            pos = part.give_cell(self)

        if not pos:  # no free cells on the chosen part
            return

        if self.current_part is not None:
            if to_pos:
                self.current_part.chars.remove(self)
            else:
                self.current_part.release_cell(self._current_pos, self)

            if self.current_part.name == "part_rest":
                self.stop_rest()

        if part.name == "part_rest":
            self.rest()

        self.model.wrtReparentTo(part.parent)
        self.model.setPos(pos)
        self.model.setH(part.angle)

        self.current_part = part
        self._current_pos = pos

        if part.name == "part_rest" and base.char_gui.rest_list_shown:  # noqa: F821
            base.char_gui.update_resting_chars(part)  # noqa: F821

        return True

    def do_effects(self, effects):
        """Do outing effects to this character.

        Effects are: adding traits, changing health and energy.

        Args:
            effects (dict): Effects and their values.
        """
        if effects is None:
            return

        effects = copy.deepcopy(effects)
        self.get_damage(-effects.pop("health", 0))

        if "add_trait" in effects and len(self.traits + self.disabled_traits) < 3:
            ind1, ind2 = effects["add_trait"]
            trait = base.labels.TRAITS[ind1][ind2]  # noqa: F821

            if trait and trait not in self.traits + self.disabled_traits:
                self.traits.append(trait)
                base.char_gui.move_status_label(-1)  # noqa: F821
                effects.pop("add_trait")

        for key, value in effects.items():
            if hasattr(self, key):
                setattr(self, key, getattr(self, key) + value)

    def prepare_to_fight(self):
        """Prepare this character for a fight.

        Switch animations and run a task to choose a target.
        """
        self._stop_tasks("_idle_anim")
        if self._idle_seq is not None:
            self._idle_seq.finish()

        self.model.loop("stand_and_aim")
        LerpAnimInterval(self.model, 0.8, self._current_anim, "stand_and_aim").start()
        LerpAnimInterval(self.model, 0.8, "stand", "stand_and_aim").start()
        self._current_anim = "stand_and_aim"

        taskMgr.doMethodLater(  # noqa: F821
            0.5, self._choose_target, self.id + "_choose_target"
        )
        self._health_bar.show_health()

    def rest(self):
        """Make this character rest.

        Stops all the active tasks and starts energy regaining/healing.
        """
        self._stop_tasks(
            "_reduce_energy", "_shoot", "_aim", "_choose_target", "_idle_anim"
        )
        self.model.hide()
        self._col_node.stash()

        taskMgr.doMethodLater(  # noqa: F821
            0.05, self._calm_down, self.id + "_calm_down", extraArgs=[True],
        )
        taskMgr.doMethodLater(  # noqa: F821
            self.class_data["energy_gain"], self._gain_energy, self.id + "_gain_energy"
        )
        if self.health < self.class_data["health"]:
            taskMgr.doMethodLater(  # noqa: F821
                self.class_data["healing"], self._heal, self.id + "_heal"
            )

    def surrender(self):
        """Stop fighting, surrender."""
        self._stop_tasks("_shoot", "_aim", "_choose_target")
        self._col_node.removeNode()
        self._shoot_anim.finish()

        LerpAnimInterval(self.model, 0.5, "stand_and_aim", "surrender").start()
        self.model.play("surrender")

    def stop_rest(self):
        """Stop this character rest."""
        self.model.show()
        self._col_node.unstash()
        base.char_gui.destroy_char_button(self.id)  # noqa: F821

        self._stop_tasks("_gain_energy", "_heal")
        taskMgr.doMethodLater(  # noqa: F821
            self.class_data["energy_spend"],
            self._reduce_energy,
            self.id + "_reduce_energy",
        )
        if base.world.enemy.active_units:  # noqa: F821
            self.prepare_to_fight()

    def _reduce_energy(self, task):
        """
        Reduce the character energy according to
        day part and status: fighting or not.
        """
        if base.world.sun.is_dark:  # noqa: F821
            if not base.train.lights_on:  # noqa: F821
                task.delayTime = 15
            elif "Floodlights" in base.train.upgrades:  # noqa: F821
                task.delayTime = self.class_data["energy_spend"]
            else:
                task.delayTime = 20

            if base.labels.TRAITS[1][1] in self.traits:  # noqa: F821
                # Fear of dark
                task.delayTime /= 2

        elif self._target:
            # Nervousness
            task.delayTime = (
                15 if base.labels.TRAITS[5][1] in self.traits else 20  # noqa: F821
            )
        else:
            task.delayTime = self.class_data["energy_spend"]

        if (
            base.labels.TRAITS[2][1] in self.traits  # noqa: F821
            and self.health < self.class_data["health"] / 2
        ):
            # Hemophobia
            task.delayTime *= 0.75

        self.energy -= 1
        if base.labels.TRAITS[7][0] in self.traits:  # noqa: F821
            # Mechanic
            base.train.get_damage(-3)  # noqa: F821

        return task.again

    def _gain_energy(self, task):
        """Regain this character energy.

        Used as a timed task while the character is resting.
        """
        if (
            base.labels.TRAITS[6][1] in self.traits  # noqa: F821
            and base.train.ctrl.current_speed > 0.75  # noqa: F821
        ):
            # Motion sickness
            return task.again

        self.energy += 3

        for char in self.current_part.chars:
            if char.sex == "female" and self.id != char.id:
                task.delayTime = self.class_data["energy_gain"] - 5
                return task.again

        return task.again

    def _heal(self, task):
        """Regain this character health.

        Used as a timed task while the character is resting.
        """
        if (
            base.labels.TRAITS[6][1] in self.traits  # noqa: F821
            and base.train.ctrl.current_speed > 0.75  # noqa: F821
        ):
            # Motion sickness
            return task.again

        if base.labels.TRAITS[7][1] in self.traits and chance(40):  # noqa: F821
            # Pharmacophobia
            return task.again

        if self.health < self.class_data["health"]:
            self.health += 1
            return task.again

        return task.done

    def _choose_target(self, task):
        """Choose an enemy to shoot.

        Only an enemy from the Train part shooting
        range can be chosen as a target.
        """
        if self.current_part and self.current_part.enemies:
            self._target = random.choice(self.current_part.enemies)
            taskMgr.doMethodLater(0.1, self._aim, self.id + "_aim")  # noqa: F821
            taskMgr.doMethodLater(1, self._shoot, self.id + "_shoot")  # noqa: F821
            return task.done

        # enemies retreated - return to passive state
        if not base.world.enemy.active_units:  # noqa: F821
            taskMgr.doMethodLater(  # noqa: F821
                7, self._calm_down, self.id + "_calm_down", extraArgs=[False],
            )
            return task.done

        return task.again

    def _aim(self, task):
        """Rotate the character to aim on enemy."""
        if (
            self._target
            and self._target.is_dead
            and base.labels.TRAITS[5][0] in self.traits  # noqa: F821
        ):
            # Bloodthirsty
            self.health += 7

        if self.current_part and self._target in self.current_part.enemies:
            self.model.headsUp(self._target.model)
            return task.again

        self._stop_tasks("_shoot")
        self._target = None

        if base.world.enemy.active_units:  # noqa: F821
            taskMgr.doMethodLater(  # noqa: F821
                0.5, self._choose_target, self.id + "_choose_target"
            )
            return task.done

        taskMgr.doMethodLater(  # noqa: F821
            7, self._calm_down, self.id + "_calm_down", extraArgs=[False]
        )
        return task.done

    def _calm_down(self, force):
        """Return to passive state on a combat over.

        Args:
            force (bool):
                If True, change to passive animation fast.
                Change animation in usual speed otherwise.
        """
        self._stop_tasks("_shoot", "_calm_down")
        if self.current_part:
            self.model.hprInterval(
                0.1 if force else 2, (self.current_part.angle, 0, 0)
            ).start()

        if force:
            for anim in self.model.get_anim_names():
                self.model.setControlEffect(anim, 0)

            self.model.stop()
            self.model.setControlEffect("stand", 1)
            self.model.loop("stand")
        else:
            LerpAnimInterval(self.model, 2, self._current_anim, "stand").start()

        self._current_anim = "stand"
        taskMgr.doMethodLater(  # noqa: F821
            random.randint(40, 60), self._idle_animation, self.id + "_idle_anim"
        )
        self._health_bar.hide_health()

    def _idle_animation(self, task):
        """Play one of the idle animations.

        Args:
            task (panda3d.core.PythonTask): Task object.
        """
        if self.is_diseased and chance(80):
            self._current_anim = "cough"
            self._cough_snd.play()
        else:
            self._current_anim = random.choice(
                ("incline1", "gun_up", "release_gun", "tread1", "turn_head1")
            )
        LerpAnimInterval(self.model, 0.2, "stand", self._current_anim).start()

        self._idle_seq = Sequence(
            self.model.actorInterval(self._current_anim, playRate=0.75),
            Func(self._stand),
        )
        self._idle_seq.start()

        task.delayTime = random.randint(40, 60)
        return task.again

    def _stand(self):
        """Return to the idle stand animation."""
        LerpAnimInterval(self.model, 0.2, self._current_anim, "stand").start()
        self._current_anim = "stand"

    def _die(self):
        """Character death sequence.

        Stop all the character's tasks, play death animation
        and plan the character object clearing.
        """
        if self._team.hold_together:
            self.health = 1
            return False

        if not Shooter._die(self):
            return False

        Unit._die(self)

        if base.common_ctrl.chosen_char == self:  # noqa: F821
            base.common_ctrl.deselect(clear_resting=False)  # noqa: F821

        self._health_bar.hide_health()
        self._stop_tasks(
            "_reduce_energy", "_get_well", "_infect", "_stop_stimul", "_gain_energy"
        )

        base.char_gui.destroy_char_button(self.id)  # noqa: F821
        self._team.delete_relations(self.id)

        LerpAnimInterval(self.model, 0.3, "stand_and_aim", "die").start()
        LerpAnimInterval(self.model, 0.3, self._current_anim, "die").start()
        self.model.hprInterval(1, (self.current_part.angle, 0, 0)).start()
        self.model.play("die")
        self._die_snd.play()

        taskMgr.doMethodLater(3, self._hide, self.id + "_hide")  # noqa: F821

    def _hide(self, task):
        """Hide the main model."""
        self.model.hide()
        return task.done

    def leave(self):
        """Make this character leave, plan clearing.

        Used only when sending a character away in a city.
        """
        self._stop_tasks("_calm_down", "_gain_energy", "_heal", "_infect", "_get_well")
        taskMgr.doMethodLater(0.05, self.clear, self.id + "_clear")  # noqa: F821

    def clear(self, task):
        """Clear this character.

        Release models and sounds memory, release the part
        cell and delete the character from the team list.
        """
        self.model.cleanup()
        self._health_bar.removeNode()
        self.model.removeNode()
        base.sound_mgr.detach_sound(self.shot_snd)  # noqa: F821
        base.sound_mgr.detach_sound(self._cough_snd)  # noqa: F821
        base.sound_mgr.detach_sound(self._die_snd)  # noqa: F821
        for snd in self._yeah_snds:
            base.sound_mgr.detach_sound(snd)  # noqa: F821

        self._team.chars.pop(self.id)
        base.res_gui.update_chars()  # noqa: F821
        self.current_part.release_cell(self._current_pos, self)
        self.current_part = None

        return task.done

    def exchange_pos(self, char):
        """Exchange positions of this Character and the given one.

        Args:
            char (units.character.Character):
                Character to exchange the positions with.
        """
        o_part, o_pos = self.current_part, self._current_pos
        self.move_to(char.current_part, char._current_pos)
        char.move_to(o_part, o_pos)

    def hide_relations_ball(self):
        """Hide the relations ball of this character."""
        if self._cohesion_ball is not None:
            self._cohesion_ball.removeNode()
            self._cohesion_ball = None

    def show_relation(self, cohesion):
        """Show a relations ball above this character.

        The color of the ball shows the strength of cohesion
        between the chosen character and this one.

        Args:
            cohesion (float):
                Value of the cohesion between the
                chosen character and this one.
        """
        if self._cohesion_ball is None:
            self._cohesion_ball = loader.loadModel(  # noqa: F821
                address("relation_ball")
            )
            self._cohesion_ball.clearLight()
            base.world.sun.ignore_shadows(self._cohesion_ball)  # noqa: F821

            self._cohesion_ball.reparentTo(self.model)
            self._cohesion_ball.setZ(0.14)

        self._cohesion_ball.setColorScale(*RELATION_COLORS[cohesion // 20])

    def _missed_shot(self):
        """Calculate if character missed the current shot.

        Returns:
            bool: True if character missed, False otherwise.
        """
        miss_chance = 0
        if self.class_ == "soldier":
            if (
                abs(self._target.node.getX()) < 0.56
                and abs(self._target.node.getY()) < 0.95
            ):
                miss_chance += 20
        elif self.class_ == "raider":
            if (
                abs(self._target.node.getX()) > 0.56
                and abs(self._target.node.getY()) > 0.95
            ):
                miss_chance += 20

        if base.world.sun.is_dark:  # noqa: F821
            if base.labels.TRAITS[1][0] in self.traits:  # noqa: F821
                # Cat eyes
                miss_chance -= 5
            elif base.train.lights_on:  # noqa: F821
                if "Floodlights" not in base.train.upgrades:  # noqa: F821
                    miss_chance += 10
            else:
                miss_chance += 20

        miss_chance += (100 - self.energy) // 5
        if self._team.cover_fire:
            miss_chance = min(100, max(0, miss_chance - 25))

        return chance(miss_chance)

    def get_stunned(self):
        """Make this character stunned for some time.

        Stunned unit can't shoot.
        """
        if self._is_stunned or self._is_stimulated:
            return

        self._is_stunned = True
        self._stop_tasks("_aim", "_shoot")

        self._current_anim = "stunned"
        self.model.play("stunned")
        LerpAnimInterval(self.model, 0.05, "stand_and_aim", "stunned").start()

        taskMgr.doMethodLater(  # noqa: F821
            6, self._stop_stunning, self.id + "_stop_stunning"
        )

    def get_sick(self, is_infect=False):
        """Calculations to get this character sick.

        The worse condition the character has the higher is the chance
        for him to get sick. Sick character has lower energy maximum,
        and all his positive traits are disabled until getting well.

        Args:
            is_infect (bool):
                True if the disease came from a
                character on the same Train part.
        """
        if self.is_diseased:
            return

        cond_percent = (self.energy + self.health) / (100 + self.class_data["health"])
        percent = (1 - cond_percent) * 30

        sick_chance = max(
            0,  # not less than zero
            (
                percent
                + (20 if is_infect else 0)
                # Immunity
                - (40 if base.labels.TRAITS[3][0] in self.traits else 0)  # noqa: F821
                # Weak immunity
                + (20 if base.labels.TRAITS[3][1] in self.traits else 0)  # noqa: F821
            ),
        )

        if chance(sick_chance):
            self.is_diseased = True
            self.get_well_score = 0

            for traits_pair in base.labels.TRAITS:  # noqa: F821
                if traits_pair[0] in self.traits:

                    self.traits.remove(traits_pair[0])
                    self.disabled_traits.append(traits_pair[0])

            taskMgr.doMethodLater(  # noqa: F821
                60, self.get_well, self.id + "_get_well"
            )
            taskMgr.doMethodLater(240, self.infect, self.id + "_infect")  # noqa: F821
            self.energy = min(self.energy, 80)

    def get_stimulated(self):
        """Use a stimulator on this character.

        Disables all the negative traits for some time.
        """
        for traits_pair in base.labels.TRAITS:  # noqa: F821
            if traits_pair[1] in self.traits:
                self.traits.remove(traits_pair[1])
                self.disabled_traits.append(traits_pair[1])

        self._is_stimulated = True
        taskMgr.doMethodLater(  # noqa: F821
            300, self._stop_stimul, self.id + "_stop_stimul"
        )

    def _stop_stimul(self, task):
        """Stop stimulation of this character.

        Returns back all the disabled negative traits.
        """
        for trait_pair in base.labels.TRAITS:  # noqa: F821
            if trait_pair[1] in self.disabled_traits:
                self.traits.append(trait_pair[1])
                self.disabled_traits.remove(trait_pair[1])

        self._is_stimulated = False
        return task.done

    def get_well(self, task):
        """Get this character well.

        When the character got well, his energy maximum is restored to
        the default value, and his positive traits are back operational.
        """
        self.get_well_score += 1
        if self.get_well_score < 20:
            return task.again

        self.is_diseased = False
        for trait_pair in base.labels.TRAITS:  # noqa: F821
            if trait_pair[0] in self.disabled_traits:
                self.traits.append(trait_pair[0])
                self.disabled_traits.remove(trait_pair[0])

        self._stop_tasks("_infect")
        return task.done

    def infect(self, task):
        """Infect other characters on the same Train part."""
        for char in self.current_part.chars:
            if char.id != self.id:
                char.get_sick(is_infect=True)

        return task.again

    def _shoot(self, task):
        """Play shooting animation and sound, make damage."""
        if self.current_part and not self.current_part.is_covered:
            return Shooter._shoot(self, task)

        return task.again

    def _stop_stunning(self, task):
        """Stop this character stun and continue fighting."""
        self._is_stunned = False

        LerpAnimInterval(self.model, 0.8, self._current_anim, "stand_and_aim").start()
        self._current_anim = "stand_and_aim"
        self.model.loop("stand_and_aim")

        taskMgr.doMethodLater(0.1, self._aim, self.id + "_aim")  # noqa: F821
        taskMgr.doMethodLater(1, self._shoot, self.id + "_shoot")  # noqa: F821

        return task.done

    def get_damage(self, damage):
        """Getting damage.

        Args:
            damage (int): Damage points to get.
        """
        if base.labels.TRAITS[2][0] in self.traits:  # noqa: F821
            # Masochism
            self.energy += 1

        Unit.get_damage(self, damage)

    def get_stench_damage(self):
        """Get damage from the Stench."""
        if base.labels.TRAITS[6][0] in self.traits and self.inhale > 0:  # noqa: F821
            # Deep breath
            self.inhale -= 1
            return

        if (
            self.current_part
            and self.current_part.name == "part_rest"
            and "Window Frames" in base.train.upgrades  # noqa: F821
        ):
            return

        self.get_damage(1)
Beispiel #9
0
class Player(DirectObject):
    
    #----------------
    # Initialization
    #----------------
    
    def __init__(self):
        self.pNode = render.attachNewNode('playerRoot')     # The root node of the player
        self.eyeHeight = 1.5                                # The z height of the camera
        self.selectedBlock = 0
        self.adjacentBlock = 0
        self.selectedPlayer = None
        self.playerState = PlayerState(self)
        self.inputBuffer = InputBuffer()
        self.currentItem = None
        self.itemModels = {}
        self.playerModel = None
        
        # The bounding box of the player for collision with environment geometry
        self.boundingBoxCenterNode = self.pNode.attachNewNode('bboxCenter')
        self.boundingBoxCenterNode.setPos(0, 0, 0.9)
        self.boundingBox = BoundingBox(self.boundingBoxCenterNode, [0.2, 0.2, 0.9])
        
        self.animFSM = PlayerAnimationFSM(self)
        self.animFSM.request('Idle')
        
        self.camNode = self.pNode.attachNewNode('cam')      # The position of the player's eyes
        self.camNode.setPos(0, 0, self.eyeHeight)
        
        self.playerParent = self.pNode.attachNewNode('playerParent')
        
        # Change this back so items follow player turning
        #self.itemNode = self.playerParent.attachNewNode('3rdPersonItem')
        self.itemNode = self.pNode.attachNewNode('3rdPersonItem')
        self.itemNode.setPos(0, 0, 1.2)
        
        self.camItemNode = self.camNode.attachNewNode('1stPersonItem')
        
        # Node for name text
        self.nameText = None
        self.nameNode = self.pNode.attachNewNode('NameNode')
        self.nameNode.setPos(0, 0, 1.97)
        
        self.lookingRayNode = self.pNode.attachNewNode('lookingRayNode')
        self.lookingRayNode.setPos(0, 0, self.eyeHeight)
        
        lookingSeg = CollisionSegment(0, 0, 0, 0, 100, 0)
        self.lookingRay = self.lookingRayNode.attachNewNode(CollisionNode('lookingRay'))
        self.lookingRay.node().addSolid(lookingSeg)
        self.lookingRay.node().setFromCollideMask(Globals.BLOCK_PICKER_BITMASK | Globals.PLAYER_BITMASK)
        self.lookingRay.node().setIntoCollideMask(BitMask32.allOff())
        
        self.lookingRayCollisionEntry = None
        
        self.pickerCollisionHandler = CollisionHandlerQueue()
        self.pickerTraverser = CollisionTraverser('pickerTraverser')
        self.pickerTraverser.addCollider(self.lookingRay, self.pickerCollisionHandler)
        if(not Settings.IS_SERVER):
            self.selectionGeom = SelectionGeom()
        else:
            self.selectionGeom = None
        
        self.CreateCollisionGeoms()
        self.LoadModels()
        
    def CreateCollisionGeoms(self):
        self.collisionNode = self.pNode.attachNewNode('CollisionNode')
        
        collisionGeom = self.collisionNode.attachNewNode(CollisionNode("legsPlayer"))
        collisionGeom.node().addSolid(CollisionSphere(0, 0, 0.35, 0.3))
        collisionGeom.node().setFromCollideMask(Globals.ITEM_BITMASK)
        collisionGeom.node().setIntoCollideMask(Globals.PLAYER_BITMASK)
        #collisionGeom.show()
        collisionGeom.setPythonTag(Globals.TAG_COLLISION, Globals.COLLISION_LEG)
        collisionGeom.setPythonTag(Globals.TAG_PLAYER, self)
        
        collisionGeom = self.collisionNode.attachNewNode(CollisionNode("bodyPlayer"))
        collisionGeom.node().addSolid(CollisionSphere(0, 0, 0.9, 0.3))
        collisionGeom.node().setFromCollideMask(BitMask32.allOff())
        collisionGeom.node().setIntoCollideMask(Globals.PLAYER_BITMASK)
        #collisionGeom.show()
        collisionGeom.setPythonTag(Globals.TAG_COLLISION, Globals.COLLISION_BODY)
        collisionGeom.setPythonTag(Globals.TAG_PLAYER, self)
        
        collisionGeom = self.collisionNode.attachNewNode(CollisionNode("headPlayer"))
        collisionGeom.node().addSolid(CollisionSphere(0, 0, 1.45, 0.35))
        collisionGeom.node().setFromCollideMask(BitMask32.allOff())
        collisionGeom.node().setIntoCollideMask(Globals.PLAYER_BITMASK)
        collisionGeom.setPythonTag(Globals.TAG_COLLISION, Globals.COLLISION_HEAD)
        collisionGeom.setPythonTag(Globals.TAG_PLAYER, self)
        
    def CreateNameTextNode(self, name):
        self.nameText = PlayerName(name, self.nameNode)
        
    def RemoveSelectionGeom(self):
        if(self.selectionGeom is not None):
            self.selectionGeom.Destroy()
            self.selectionGeom = None
        
    def LoadModels(self, teamId = Game.SPECTATE):
        if(self.playerModel is not None):
            self.playerModel.cleanup()
        
        if(teamId == Game.TEAM_1):
            self.playerModel = Actor('Assets/Models/Players/ralph',
                                     {"run":"Assets/Models/Players/ralph-run",
                                      "walk":"Assets/Models/Players/ralph-walk"})
        #elif(teamId == Game.TEAM_2):
        else:
            self.playerModel = Actor('Assets/Models/Players/ralph2',
                                     {"run":"Assets/Models/Players/ralph-run",
                                      "walk":"Assets/Models/Players/ralph-walk"})
        self.playerModel.setScale(0.36)
        self.playerModel.reparentTo(self.playerParent)
        self.playerModel.setH(180)
        #self.playerModel.hide()
        
        self.playerModel.enableBlend() 
        self.playerModel.loop('run')
        self.playerModel.pose('walk', 5)
        
#        self.outlineFilter = CommonFilters(base.win, base.cam)
#        if(not self.outlineFilter.setCartoonInk(0.5)):
#            print 'ERROR CARTOON FILTER'
        
    #--------------------
    # Controlling models
    #--------------------
        
    def HidePlayerModel(self):
        self.playerModel.hide()
        
    def ShowPlayerModel(self):
        self.playerModel.show()
        
    def LoopAnimation(self, animation):
        self.playerModel.setPlayRate(1.0, animation)
        self.playerModel.loop(animation)
        
    def RequestFSMTransition(self, animFSMState):
        self.animFSM.request(animFSMState)
        
    #--------------------
    # Updating
    #--------------------
        
    def Update(self, deltaTime):
        if(self.currentItem):
            self.currentItem.IncreaseTimeSinceLastUse(deltaTime)
                
    def UpdateLookingDirection(self, lookingDirection):
        if(not self.playerModel.isHidden() and self.GetPlayerState().GetValue(PlayerState.PLAYING_STATE) == PlayerState.PS_PLAYING):
            facingDirection = Vec3(lookingDirection.getX(), lookingDirection.getY(), 0)
            facingDirection.normalize()
            self.playerParent.lookAt(Point3(facingDirection))
            self.itemNode.lookAt(Point3(self.itemNode.getPos() + lookingDirection))
            
    def UpdateLookingRayDirection(self, lookingDirection):
        self.lookingRayNode.lookAt(Point3(lookingDirection.getX(), lookingDirection.getY(), lookingDirection.getZ() + self.eyeHeight))
    
    def MovePosToPriorSnapshot(self, numFramesBack):
        self.pNode.setPos(self.GetPlayerState().GetValue(PlayerState.POSITION_HISTORY).GetPosition(numFramesBack))
        
    def ReturnToCurrentPosition(self):
        self.pNode.setPos(self.currentPosition)
    
    #-----------------------
    # Weapons / items
    #-----------------------
    
    def OnSelectedItemChangeEvent(self, event):
        if(event.GetItemStack()):
            self.ChangeItem(event.GetItemStack().GetItem())
        else:
            self.ChangeItem(None)
        
    def ChangeItem(self, item):
        print 'CHANGE ITEM curr', self.currentItem, 'new', item, self.GetPlayerState().GetValue(PlayerState.PID)
        
#        if(self.currentItem):
#            self.playerState.UpdateValue(PlayerState.CURRENT_ITEM, self.currentItem.GetItemId())
#        else:
#            self.playerState.UpdateValue(PlayerState.CURRENT_ITEM, ItemId.NoItem)
        
        if(item != self.currentItem):
        
            oldItem = self.currentItem
            self.currentItem = item
                
            if(self.currentItem):
                self.playerState.UpdateValue(PlayerState.CURRENT_ITEM, self.currentItem.GetItemId())
            else:
                self.playerState.UpdateValue(PlayerState.CURRENT_ITEM, ItemId.NoItem)
                
            
            #If both are item objects, equip dequip
            if(self.currentItem and oldItem):
                if(not Settings.IS_SERVER):
                    self.currentItem.LoadContent()
                    self.currentItem.ReparentTo(self.camItemNode)
                self.currentItem.EquipDequip(oldItem)
                
            
            # If just the old item is an obj, dequip it
            elif(oldItem):
                oldItem.Dequip()
                
            # If just the new item is an obj, equip it
            elif(self.currentItem):
                if(not Settings.IS_SERVER):
                    self.currentItem.LoadContent()
                    self.currentItem.ReparentTo(self.camItemNode)
                self.currentItem.Equip()
            
    # Change the item as though this is ourself, but then
    # reparent it so that it shows up properly in 3rd person
    def ThirdPersonChangeItem(self, item):
        self.ChangeItem(item)
        if(item and not Settings.IS_SERVER):
            self.currentItem.ReparentTo(self.itemNode)
            
    def ChangeBlockToPlace(self, direction):
        if(isinstance(self.currentItem, Builder)):
            if(direction > 0):
                self.currentItem.IncreaseBlock()
            else:
                self.currentItem.DecreaseBlock()
                
    def Reload(self):
        if(isinstance(self.currentItem, Firearm)):
            self.currentItem.Reload()
            
            # If we reloaded, tell fire an event
            if(self.GetPlayerState().GetValue(PlayerState.PID) == Globals.MY_PID):
                PlayerReloadEvent(self).Fire()
                
    def OnTeamChange(self, teamId):
        self.GetPlayerState().UpdateValue(PlayerState.TEAM, teamId)
        self.LoadModels(teamId)
        if(self.GetPlayerState().GetValue(PlayerState.PID) == Globals.MY_PID):
            self.HidePlayerModel()
        else:  
            if(teamId != Globals.MY_TEAM):
                self.nameText.reshowOnInView = False
                self.nameText.Hide()
                
            else:
                self.nameText.reshowOnInView = True
                self.ShowNameAboveHead()
                
            self.nameText.SetColor(Globals.TEAM_COLORS[teamId])
        
        #self.nameTextNode.setTextColor(Globals.TEAM_COLORS[teamId])
        
    #------------------
    # Death / Respawn
    #-------------------
    
    def OnDeath(self):
        self.collisionNode.stash()
        LerpHprInterval(self.playerParent, 0.7, (self.playerParent.getH(), 90, self.playerParent.getR())).start()
        self.itemNode.hide()
        self.camItemNode.hide()
        self.GetPlayerState().UpdateValue(PlayerState.PLAYING_STATE, PlayerState.PS_PLAYING_DEAD)
        
        # If this is the actual player
        if(self.GetPlayerState().GetValue(PlayerState.PID) == Globals.MY_PID):
            self.ShowPlayerModel()
            if(isinstance(self.currentItem, Firearm) and self.currentItem.IsADS()):
                self.currentItem.ToggleADS()
        
    def OnRespawn(self):
        self.collisionNode.unstash()
        self.GetInputBuffer().Clear()
        self.itemNode.show()
        self.camItemNode.show()
        self.GetPlayerState().UpdateValue(PlayerState.PLAYING_STATE, PlayerState.PS_PLAYING)
        
        if(self.GetPlayerState().GetValue(PlayerState.PID) == Globals.MY_PID):
            print 'Respawn event for me'
            self.HidePlayerModel()
            SelectedItemChangeEvent(None, ItemStack(self.currentItem)).Fire()
            
        else:
            self.ShowPlayerModel()
            
    #----------------------
    # Accessor / Mutators
    #----------------------
        
    def GetPos(self):
        return self.pNode.getPos()
    
    def SetPos(self, value):
        self.pNode.setPos(value)
        
    def GetZ(self):
        return self.pNode.getZ()
        
    def LerpTo(self, pos):
        LerpPosInterval(self.pNode, 0.1, pos).start()
        
    #---------------------------------------
    # Playernames on screen
    #---------------------------------------
    
    def ShowNameAboveHead(self):
        self.nameText.Show()
        
    def FadeOutNameAboveHead(self, task = None):
        self.nameText.FadeOut()
        
    #---------------------------------------
    # Picking things with looking direction
    #----------------------------------------
        
    #@pstat
    def PerfromLookingRayCollision(self, isSelf = True):
        self.pickerTraverser.traverse(render)
        
        entries = []
        for i in range(self.pickerCollisionHandler.getNumEntries()):
            entry = self.pickerCollisionHandler.getEntry(i)
            entries.append(entry)
            
        numEntries = len(entries)
        
        if(numEntries > 0):
            entries.sort(lambda x,y: cmp(self.DistanceFromCam(x.getSurfacePoint(render)),
                                         self.DistanceFromCam(y.getSurfacePoint(render))))
            
            self.lookingRayCollisionEntry = None
            playerEntry = None
            blockCollisionEntry = None
            
            for i in xrange(numEntries):
                if(entries[i].getIntoNodePath().getParent() != self.collisionNode):
                    target = entries[i].getIntoNodePath().__str__()
                    if(target.endswith('blockCollision')):
                        blockCollisionEntry = entries[i]
                        self.lookingRayCollisionEntry = blockCollisionEntry
                        break
                    elif(target.endswith('Player')):
                        playerEntry = entries[i]
                        self.lookingRayCollisionEntry = playerEntry
                        break
            
            if(blockCollisionEntry and isinstance(self.currentItem, Builder)):
                point = blockCollisionEntry.getSurfacePoint(render)
                
                if(self.DistanceFromCam(point) <= Globals.MAX_SELECT_DISTANCE):
                    
                    norm = blockCollisionEntry.getSurfaceNormal(render)
                    self.adjacentBlock = point + norm * 0.2
                    point -=  norm * 0.2
                    
                    x = point.getX()
                    y = point.getY()
                    z = point.getZ()
                    
                    if(self.selectionGeom):
                        self.selectionGeom.SetPos(int(x), int(y), int(z))
                        self.selectionGeom.Show()
                        self.selectedBlock = self.selectionGeom.GetPos() 
                    else:
                        self.selectedBlock = VBase3(int(x), int(y), int(z))
                else:
                    self.selectedBlock = None
                    self.adjacentBlock = None
                    if(self.selectionGeom):
                        self.selectionGeom.Hide()                
            else:
                self.selectedBlock = None
                self.adjacentBlock = None
                if(self.selectionGeom):
                    self.selectionGeom.Hide()
            
            if(playerEntry):
                player = playerEntry.getIntoNodePath().getPythonTag(Globals.TAG_PLAYER)
                if(player):
                    # Each frame, if the crosshairs are hovering a player, fire the event
                    # so the HUD can respond.
                    PlayerSelectedEvent(player).Fire()
                    self.selectedPlayer = player
                    
#                    # Before, we were only firing on changes
#                    if(self.selectedPlayer != player):
#                        self.selectedPlayer = player
#                        if(not Settings.IS_SERVER and isSelf):
#                            PlayerSelectedEvent(player).Fire()
                            
            else:
                if(self.selectedPlayer != None):
                    self.selectedPlayer = None
                    if(not Settings.IS_SERVER and isSelf):
                        PlayerSelectedEvent(None).Fire()
                
    def DistanceFromCam(self, otherPoint):
        eye = self.camNode.getPos(render)
        return (otherPoint - eye).length()
    
    def GetSelectedBlock(self):
        return self.selectedBlock
    
    def GetAdjacentBlock(self):
        return self.adjacentBlock
    
    def GetPlayerState(self):
        return self.playerState
    
    def GetInputBuffer(self):
        return self.inputBuffer
    
    def GetPlayerOverheadName(self):
        return self.nameText
    
    def GetCurrentItem(self):
        return self.currentItem
        
    #---------------
    # Cleanup
    #---------------
    
    def Destroy(self):
        for child in self.pNode.getChildren():
            child.removeNode()
        self.pNode.removeNode()
        if(self.playerModel):
            self.playerModel.delete()