class DistributedEagleSuit(DistributedSuit):
    notify = directNotify.newCategory('DistributedEagleSuit')

    def __init__(self, cr):
        DistributedSuit.__init__(self, cr)
        self.eagleCry = base.audio3d.loadSfx(
            'phase_5/audio/sfx/tt_s_ara_cfg_eagleCry.ogg')
        base.audio3d.attachSoundToObject(self.eagleCry, self)
        self.fallWhistle = base.audio3d.loadSfx(
            'phase_5/audio/sfx/incoming_whistleALT.ogg')
        base.audio3d.attachSoundToObject(self.fallWhistle, self)
        self.explode = base.audio3d.loadSfx(
            'phase_3.5/audio/sfx/ENC_cogfall_apart.ogg')
        base.audio3d.attachSoundToObject(self.explode, self)
        self.eventSphereNodePath = None
        self.fallingPropeller = None
        self.fallingPropProjectile = None
        self.mg = None
        self.flySpeed = 0.0
        return

    def enterNeutral(self, ts=0):
        self.show()
        self.timestampAnimTrack = Sequence(Wait(ts),
                                           Func(self.loop, 'flyNeutral'))
        self.timestampAnimTrack.start()

    def makeStateDict(self):
        self.suitFSM.addState(
            State('eagleFly', self.enterEagleFly, self.exitEagleFly))
        self.suitFSM.addState(
            State('eagleFall', self.enterEagleFall, self.exitEagleFall))
        self.stateIndex2suitState = {
            0: self.suitFSM.getStateNamed('off'),
            1: self.suitFSM.getStateNamed('walking'),
            2: self.suitFSM.getStateNamed('flyingDown'),
            3: self.suitFSM.getStateNamed('flyingUp'),
            4: self.suitFSM.getStateNamed('lured'),
            5: self.suitFSM.getStateNamed('eagleFly'),
            6: self.suitFSM.getStateNamed('eagleFall')
        }
        self.suitState2stateIndex = {}
        for stateId, state in self.stateIndex2suitState.items():
            self.suitState2stateIndex[state.getName()] = stateId

    def setFlySpeed(self, value):
        self.flySpeed = value

    def getFlySpeed(self):
        return self.flySpeed

    def enterEagleFly(self, startIndex, endIndex, ts=0.0):
        durationFactor = self.getFlySpeed()
        if startIndex > -1:
            startPos = EGG.EAGLE_FLY_POINTS[startIndex]
        else:
            startPos = self.getPos(render)
        endPos = EGG.EAGLE_FLY_POINTS[endIndex]
        if self.moveIval:
            self.moveIval.pause()
            self.moveIval = None
        self.moveIval = NPCWalkInterval(self,
                                        endPos,
                                        durationFactor=durationFactor,
                                        startPos=startPos,
                                        fluid=1)
        self.moveIval.start(ts)
        return

    def exitEagleFly(self):
        if self.moveIval:
            self.moveIval.pause()
            self.moveIval = None
        return

    def enterEagleFall(self, startIndex, endIndex, ts=0.0):
        self.moveIval = LerpPosInterval(self,
                                        duration=4.0,
                                        pos=self.getPos(render) - (0, 0, 75),
                                        startPos=self.getPos(render),
                                        blendType='easeIn')
        self.moveIval.start(ts)

    def exitEagleFall(self):
        if self.moveIval:
            self.moveIval.finish()
            self.moveIval = None
        return

    def fallAndExplode(self):
        self.cleanupPropeller()
        self.fallingPropeller = Actor(
            'phase_4/models/props/propeller-mod.bam',
            {'chan': 'phase_4/models/props/propeller-chan.bam'})
        self.fallingPropeller.reparentTo(render)
        self.fallingPropeller.loop('chan', fromFrame=0, toFrame=3)
        parentNode = self.attachNewNode('fallingPropParentNode')
        h = random.randint(0, 359)
        parentNode.setH(h)
        dummyNode = parentNode.attachNewNode('dummyNode')
        dummyNode.setPos(0, 10, -50)
        self.fallingPropProjectile = FlightProjectileInterval(
            self.fallingPropeller,
            startPos=self.find('**/joint_head').getPos(render),
            endPos=dummyNode.getPos(render),
            duration=5.0,
            gravityMult=0.25)
        self.fallingPropProjectile.start()
        dummyNode.removeNode()
        del dummyNode
        parentNode.removeNode()
        del parentNode
        self.updateHealthBar(0)
        self.ignoreHit()
        base.playSfx(self.fallWhistle, node=self)
        taskMgr.doMethodLater(4.0, self.doExplodeSound,
                              self.uniqueName('DEagleSuit-doExplodeSound'))

    def doExplodeSound(self, task):
        base.playSfx(self.explode, node=self)
        return Task.done

    def __initializeEventSphere(self):
        sphere = CollisionSphere(0, 0, 0, 2)
        sphere.setTangible(0)
        node = CollisionNode(self.uniqueName('DEagleSuit-eventSphere'))
        node.addSolid(sphere)
        node.setCollideMask(CIGlobals.WallBitmask)
        np = self.attachNewNode(node)
        np.setSz(2.5)
        np.setZ(5.5)
        self.eventSphereNodePath = np

    def removeEventSphere(self):
        if self.eventSphereNodePath:
            self.eventSphereNodePath.removeNode()
            self.eventSphereNodePath = None
        return

    def acceptHit(self):
        self.acceptOnce('enter' + self.eventSphereNodePath.node().getName(),
                        self.__handleHit)

    def ignoreHit(self):
        self.ignore('enter' + self.eventSphereNodePath.node().getName())

    def __handleHit(self, entry):
        messenger.send(EGG.EAGLE_HIT_EVENT, [self.doId])

    def setSuit(self, arg, variant):
        DistributedSuit.setSuit(self, arg, 3)
        self.deleteShadow()
        self.disableBodyCollisions()
        self.disableRay()
        self.__initializeEventSphere()
        self.show()
        self.setAnimState('flyNeutral')

    def __doEagleCry(self, task):
        base.playSfx(self.eagleCry, node=self)
        task.delayTime = random.uniform(3, 30)
        return Task.again

    def announceGenerate(self):
        DistributedSuit.announceGenerate(self)
        taskMgr.doMethodLater(random.uniform(5, 25), self.__doEagleCry,
                              self.uniqueName('DEagleSuit-doEagleCry'))
        self.acceptHit()

    def disable(self):
        self.ignoreHit()
        self.removeEventSphere()
        taskMgr.remove(self.uniqueName('DEagleSuit-doExplodeSound'))
        taskMgr.remove(self.uniqueName('DEagleSuit-doEagleCry'))
        if self.fallingPropProjectile:
            self.fallingPropProjectile.finish()
            self.fallingPropProjectile = None
        if self.fallingPropeller:
            self.fallingPropeller.cleanup()
            self.fallingPropeller = None
        base.audio3d.detachSound(self.fallWhistle)
        del self.fallWhistle
        base.audio3d.detachSound(self.explode)
        del self.explode
        base.audio3d.detachSound(self.eagleCry)
        del self.eagleCry
        self.mg = None
        DistributedSuit.disable(self)
        return
class DistributedSuit(Suit, DistributedAvatar, DistributedSmoothNode,
                      DelayDeletable):
    notify = directNotify.newCategory('DistributedSuit')

    def __init__(self, cr):
        Suit.__init__(self)
        DistributedAvatar.__init__(self, cr)
        DistributedSmoothNode.__init__(self, cr)

        self.anim = None
        self._state = SuitState.ALIVE
        self.dept = None
        self.variant = None
        self.suitPlan = None
        self.level = None
        self.moveIval = None
        self.hpFlash = None

        # For PythonCTMusicManager:
        # Are we in range of the localAvatar?
        self.isInRange = False

        self.chaseTarget = 0

        self.suitFSM = ClassicFSM('DistributedSuit', [
            State('off', self.enterSuitOff, self.exitSuitOff),
            State('walking', self.enterWalking, self.exitWalking),
            State('flyingDown', self.enterFlyingDown, self.exitFlyingDown),
            State('flyingUp', self.enterFlyingUp, self.exitFlyingUp),
            State('lured', self.enterLured, self.exitLured)
        ], 'off', 'off')
        self.stateIndex2suitState = {}
        self.suitFSM.enterInitialState()
        self.makeStateDict()

    def setChaseTarget(self, avId):
        if avId != base.localAvatar.doId:
            if self.chaseTarget == base.localAvatar.doId:
                messenger.send(PCTMM.getCogLostTargetEvent())
        else:
            messenger.send(PCTMM.getCogChasingEvent())

        self.chaseTarget = avId

    def setWalkPath(self, path, timestamp):
        elapsedT = globalClockDelta.localElapsedTime(timestamp)
        self.suitFSM.request('walking', [path, elapsedT])

    def showAvId(self):
        self.setDisplayName(self.getName() + "\n" + str(self.doId))

    def showName(self):
        self.setDisplayName(self.getName())

    def setDisplayName(self, name):
        self.setupNameTag(tempName=name)

    def enterWalking(self, path, elapsedT):
        # path: A list of point2s.
        #
        # We will make a sequence of NPCWalkIntervals for each point2 in the path.

        self.clearMoveTrack()
        self.moveIval = getMoveIvalFromPath(self, path, elapsedT, True,
                                            'suitMoveIval')
        self.moveIval.start(elapsedT)

    def clearMoveTrack(self):
        if self.moveIval:
            self.ignore(self.moveIval.getDoneEvent())
            self.moveIval.pause()
            self.moveIval = None
        if not self.isDead():
            self.animFSM.request('neutral')

    def exitWalking(self):
        self.clearMoveTrack()
        if not self.isDead():
            self.animFSM.request('neutral')

    def enterFlyingDown(self, startIndex, endIndex, ts=0.0):
        if self.getHood() != '' and startIndex != -1 and endIndex != -1:
            duration = 3.5
            startPoint = CIGlobals.SuitSpawnPoints[
                self.getHood()].keys()[startIndex]
            startPos = CIGlobals.SuitSpawnPoints[
                self.getHood()][startPoint] + (0, 0, 6.5 * 4.8)
            endPoint = CIGlobals.SuitSpawnPoints[
                self.getHood()].keys()[endIndex]
            endPos = CIGlobals.SuitSpawnPoints[self.getHood()][endPoint]
            self.stopMoving(finish=1)
            groundF = 28
            dur = self.getDuration('land')
            fr = self.getFrameRate('land')
            if fr:
                animTimeInAir = groundF / fr
            else:
                animTimeInAir = groundF
            impactLength = dur - animTimeInAir
            timeTillLanding = 6.5 - impactLength
            self.moveIval = LerpPosInterval(self,
                                            duration=timeTillLanding,
                                            pos=endPos,
                                            startPos=startPos,
                                            fluid=1)
            self.moveIval.start(ts)
        self.animFSM.request('flyDown', [ts])

    def exitFlyingDown(self):
        self.stopMoving(finish=1)
        self.animFSM.request('neutral')

    def enterFlyingUp(self, startIndex, endIndex, ts=0.0):
        if self.getHood() != '':
            duration = 3
            if startIndex > -1:
                startPoint = CIGlobals.SuitSpawnPoints[
                    self.getHood()].keys()[startIndex]
                startPos = CIGlobals.SuitSpawnPoints[
                    self.getHood()][startPoint]
            else:
                startPos = self.getPos(render)
            if endIndex > -1:
                endPoint = CIGlobals.SuitSpawnPoints[
                    self.getHood()].keys()[endIndex]
                endPos = CIGlobals.SuitSpawnPoints[
                    self.getHood()][endPoint] + (0, 0, 6.5 * 4.8)
            else:
                endPos = self.getPos(render) + (0, 0, 6.5 * 4.8)

            self.stopMoving(finish=1)
            groundF = 28
            dur = self.getDuration('land')
            fr = self.getFrameRate('land')
            if fr:
                animTimeInAir = groundF / fr
            else:
                animTimeInAir = groundF
            impactLength = dur - animTimeInAir
            timeTillLanding = 6.5 - impactLength
            self.moveIval = Sequence(
                Wait(impactLength),
                LerpPosInterval(self,
                                duration=timeTillLanding,
                                pos=endPos,
                                startPos=startPos,
                                fluid=1))
            self.moveIval.start(ts)
        self.animFSM.request('flyAway', [ts, 1])

    def exitFlyingUp(self):
        if self.moveIval:
            self.moveIval.finish()
            self.moveIval = None
        self.animFSM.request('neutral')

    def enterLured(self, _, __, ___):
        self.loop('lured')

    def exitLured(self):
        self.stop()

    def enterSuitOff(self, foo1=None, foo2=None, foo3=None):
        pass

    def exitSuitOff(self):
        pass

    def setName(self, name):
        Suit.setName(self, name, self.suitPlan.getName())

    def setLevel(self, level):
        self.level = level
        if self.level == 12:
            self.maxHealth = 200
        elif self.level > 0:
            self.maxHealth = (self.level + 1) * (self.level + 2)
        else:
            self.maxHealth = 1
        self.health = self.maxHealth
        self.updateHealthBar(self.health)

    def getLevel(self):
        return self.level

    def startMoveInterval(self, startX, startY, startZ, endX, endY, endZ,
                          duration):
        self.stopMoving()
        endPos = Point3(endX, endY, endZ)
        self.moveIval = NPCWalkInterval(self,
                                        endPos,
                                        durationFactor=duration,
                                        fluid=1)
        self.moveIval.start()

    def stopMoveInterval(self, andTurnAround=0):
        if self.moveIval:
            self.moveIval.pause()
            self.moveIval = None
        if andTurnAround == 1:
            if self.health > 0:
                self.animFSM.request('neutral')
            self.setH(self.getH() - 180)

    def toggleRay(self, ray=1):
        if ray:
            Suit.initializeRay(self, self.avatarType, 2)
        else:
            Suit.disableRay(self)

    def startProjInterval(self,
                          startX,
                          startY,
                          startZ,
                          endX,
                          endY,
                          endZ,
                          duration,
                          gravityMult,
                          ts=0):
        if isinstance(ts, int) and ts != 0:
            ts = globalClockDelta.localElapsedTime(ts)

        self.disableRay()
        self.stopMoveInterval()
        startPos = Point3(startX, startY, startZ)
        endPos = Point3(endX, endY, endZ)
        oldHpr = self.getHpr(render)
        self.headsUp(endPos)
        newHpr = self.getHpr(render)
        self.setHpr(oldHpr)
        self.moveIval = Parallel(
            LerpHprInterval(self,
                            duration=0.5,
                            hpr=newHpr,
                            startHpr=oldHpr,
                            blendType='easeInOut'),
            Sequence(Func(self.animFSM.request, 'flyAway', [ts]), Wait(3.5),
                     Func(self.animFSM.request, 'flyDown', [1.0])),
            Sequence(
                Wait(2.0), Func(self.headsUp, endPos),
                ProjectileInterval(self,
                                   startPos=startPos,
                                   endPos=endPos,
                                   gravityMult=gravityMult,
                                   duration=duration)))
        self.moveIval.start(ts)

    def startPosInterval(self,
                         startX,
                         startY,
                         startZ,
                         endX,
                         endY,
                         endZ,
                         duration,
                         blendType,
                         ts=0.0):
        if ts != 0.0:
            ts = globalClockDelta.localElapsedTime(ts)
        self.stopMoveInterval()
        startPos = Point3(startX, startY, startZ)
        endPos = Point3(endX, endY, endZ)
        self.moveIval = LerpPosInterval(self,
                                        duration=duration,
                                        pos=endPos,
                                        startPos=startPos,
                                        blendType=blendType)
        self.moveIval.start(ts)

    def stopMoving(self, finish=0):
        if self.moveIval:
            if finish:
                self.moveIval.finish()
            else:
                self.moveIval.pause()
            self.moveIval = None

    def d_disableMovement(self, wantRay=False):
        self.sendUpdate('disableMovement', [])
        self.interruptAttack()
        self.stopMoving()
        if not wantRay:
            Suit.disableRay(self)

    def d_enableMovement(self):
        self.sendUpdate('enableMovement', [])
        Suit.initializeRay(self, self.avatarType, 2)

    def startRay(self):
        Suit.initializeRay(self, self.avatarType, 2)

    def setHealth(self, health):
        if health > self.health:
            # We got an hp boost. Flash green.
            flashColor = VBase4(0, 1, 0, 1)
        elif health < self.health:
            # We got an hp loss. Flash red.
            flashColor = VBase4(1, 0, 0, 1)
        DistributedAvatar.setHealth(self, health)

        def doBossFlash():
            if not self.isEmpty():
                LerpColorScaleInterval(self, 0.2, flashColor).start()

        def clearBossFlash():
            if not self.isEmpty():
                self.clearColorScale()

        if self.isDead():
            self.setChaseTarget(0)
            base.taskMgr.remove(self.uniqueName('monitorLocalAvDistance'))
            if self.isInRange:
                messenger.send(PCTMM.getCogOutOfRangeEvent())
                self.isInRange = False
            self.interruptAttack()

        if self.getLevel() > 12:
            if self.hpFlash:
                self.hpFlash.finish()
                self.hpFlash = None
            self.hpFlash = Sequence(Func(doBossFlash), Wait(0.2),
                                    Func(clearBossFlash))
            self.hpFlash.start()
        self.updateHealthBar(health)

    def announceHealth(self, level, hp):
        DistributedAvatar.announceHealth(self, level, hp)
        if level == 1:
            healthSfx = base.audio3d.loadSfx(SuitGlobals.healedSfx)
            base.audio3d.attachSoundToObject(healthSfx, self)
            SoundInterval(healthSfx, node=self).start()
            del healthSfx

    #
    #    'setSuit' sets the suit type and generates it.
    #    'arg' is an id for a SuitPlan as defined in SuitBank or
    #        an instance of SuitPlan.
    #    'variant' is an optional argument that sets the variant.
    #        It takes an id for the variant or an instance of Variant.
    #        Default is Variant.NORMAL.

    def setSuit(self, arg, variant=0):
        if isinstance(arg, SuitPlan):
            plan = arg
        else:
            plan = SuitBank.getSuitById(arg)

        voice = Voice.NORMAL
        if variant:
            if isinstance(variant, (int, long, float, complex)):
                variant = Variant.getVariantById(variant)

        if plan.getForcedVoice():
            voice = plan.getForcedVoice()

        Suit.generate(self, plan, variant, voice=voice)
        self.suitPlan = plan
        self.variant = Variant.getVariantById(variant)

    def getSuit(self):
        return tuple((self.suitPlan, self.variant))

    def spawn(self, startIndex, endIndex, spawnMode=SpawnMode.FLYDOWN):
        if spawnMode == SpawnMode.FLYDOWN:
            startPoint = CIGlobals.SuitSpawnPoints[
                self.getHood()].keys()[startIndex]
            startPos = CIGlobals.SuitSpawnPoints[
                self.getHood()][startPoint] + (0, 0, 50)
            endPoint = CIGlobals.SuitSpawnPoints[
                self.getHood()].keys()[endIndex]
            endPos = CIGlobals.SuitSpawnPoints[self.getHood()][endPoint]
            if self.moveIval:
                self.moveIval.finish()
                self.moveIval = None
            self.moveIval = LerpPosInterval(self,
                                            duration=3,
                                            pos=endPos,
                                            startPos=startPos,
                                            fluid=1)

    def makeStateDict(self):
        self.stateIndex2suitState = {
            0: self.suitFSM.getStateNamed('off'),
            1: self.suitFSM.getStateNamed('walking'),
            2: self.suitFSM.getStateNamed('flyingDown'),
            3: self.suitFSM.getStateNamed('flyingUp'),
            4: self.suitFSM.getStateNamed('lured')
        }
        self.suitState2stateIndex = {}
        for stateId, state in self.stateIndex2suitState.items():
            self.suitState2stateIndex[state.getName()] = stateId

    def setSuitState(self, index, startPoint, endPoint, timestamp=None):
        if timestamp != None:
            ts = globalClockDelta.localElapsedTime(timestamp)
        else:
            ts = 0.0

        self.suitState = self.stateIndex2suitState[index]
        self.startPoint = startPoint
        self.endPoint = endPoint

        self.suitFSM.request(self.suitState, [startPoint, endPoint, ts])

    def getSuitState(self):
        return self.suitState

    def setAnimState(self, anim, loop=1, timestamp=None):
        prevAnim = self.anim
        self.anim = anim

        if timestamp == None:
            ts = 0.0
        else:
            ts = globalClockDelta.localElapsedTime(timestamp)

        if type(anim) == types.IntType:
            if anim != 44 and anim != 45:
                anim = SuitGlobals.getAnimById(anim)
                animName = anim.getName()
            elif anim == 44:
                animName = 'die'
            elif anim == 45:
                animName = 'flyNeutral'
        elif type(anim) == types.StringType:
            animName = anim

        if self.animFSM.hasStateNamed(animName):
            self.animFSM.request(animName, [ts])
        else:
            if loop:
                self.loop(animName)
            else:
                self.play(animName)
        messenger.send(SuitGlobals.animStateChangeEvent % (self.uniqueName),
                       [anim, prevAnim])

    def doAttack(self, attackId, avId, timestamp=None):
        if timestamp == None:
            ts = 0.0
        else:
            ts = globalClockDelta.localElapsedTime(timestamp)
        attackName = SuitAttacks.SuitAttackLengths.keys()[attackId]
        attackTaunt = CIGlobals.SuitAttackTaunts[attackName][random.randint(
            0,
            len(CIGlobals.SuitAttackTaunts[attackName]) - 1)]
        avatar = self.cr.doId2do.get(avId)
        shouldChat = 0
        if self.suitPlan in [SuitBank.VicePresident, SuitBank.LucyCrossbill]:
            shouldChat = random.randint(0, 2)
        if shouldChat == 0:
            self.setChat(attackTaunt)

        self.animFSM.request('attack', [attackName, avatar, 0.0])

    def throwObject(self):
        self.acceptOnce('enter' + self.wsnp.node().getName(),
                        self.__handleWeaponCollision)
        Suit.throwObject(self)

    def __handleWeaponCollision(self, entry):
        self.sendUpdate('toonHitByWeapon',
                        [self.attack, base.localAvatar.doId])
        base.localAvatar.handleHitByWeapon(self.attack, self)
        self.b_handleWeaponTouch()

    def b_handleWeaponTouch(self):
        self.sendUpdate('handleWeaponTouch', [])
        self.handleWeaponTouch()

    def __monitorLocalAvDistance(self, task):
        if self.getDistance(base.localAvatar) <= PCTMM.getCogInRangeDistance(
        ) and not self.isInRange:
            self.isInRange = True
            messenger.send(PCTMM.getCogInRangeEvent())
        elif self.getDistance(base.localAvatar) > PCTMM.getCogInRangeDistance(
        ) and self.isInRange:
            self.isInRange = False
            messenger.send(PCTMM.getCogOutOfRangeEvent())

        return task.cont

    def announceGenerate(self):
        DistributedAvatar.announceGenerate(self)
        self.setAnimState('neutral')
        base.taskMgr.add(self.__monitorLocalAvDistance,
                         self.uniqueName('monitorLocalAvDistance'))

    def generate(self):
        DistributedAvatar.generate(self)
        DistributedSmoothNode.generate(self)

    def disable(self):
        base.taskMgr.remove(self.uniqueName('monitorLocalAvDistance'))
        self.anim = None
        self._state = None
        self.dept = None
        self.variant = None
        self.suitPlan = None
        if self.hpFlash:
            self.hpFlash.finish()
            self.hpFlash = None
        if self.moveIval:
            self.moveIval.pause()
            self.moveIval = None
        Suit.disable(self)
        DistributedAvatar.disable(self)

    def delete(self):
        Suit.delete(self)
        del self.anim
        del self._state
        del self.dept
        del self.variant
        del self.suitPlan
        del self.moveIval
        DistributedAvatar.delete(self)
        DistributedSmoothNode.delete(self)
class InventoryGui(DirectObject):
    directNotify = DirectNotify().newCategory('InventoryGui')
    HiddenPos = (0.2, 0, 0)
    VisiblePos = (-0.1725, 0, 0)
    SwitchTime = 0.3
    AutoShowTime = 1.5
    DELETED = False

    def __init__(self):
        DirectObject.__init__(self)
        self.backpack = base.localAvatar.backpack
        if not self.backpack:
            return
        self.backpack.loadoutGUI = self
        self.oneSlotPos = [(0, 0, 0)]
        self.twoSlotsPos = [(0, 0, 0.3), (0, 0, -0.2)]
        self.threeSlotsPos = [(0, 0, 0.5), (0, 0, 0), (0, 0, -0.5)]
        self.fourSlotPos = [(0, 0, 0.5), (0, 0, 0.15), (0, 0, -0.2),
                            (0, 0, -0.55)]
        self.availableSlot = 0
        self.slots = []
        self.activeSlot = None
        self.defaultSlots = 3
        self.prevSlot = None
        self.ammoLabel = None
        self.inventoryFrame = DirectFrame(parent=base.a2dRightCenter,
                                          pos=(-0.1725, 0, 0))
        self.visibilityBtn = DirectButton(text='',
                                          relief=None,
                                          text_bg=(1, 1, 1, 0),
                                          parent=base.a2dRightCenter,
                                          pos=(-0.1725, 0, 0),
                                          frameSize=(-0.2, 0.2, -0.725, 0.7),
                                          clickSound=None,
                                          rolloverSound=None)
        self.visibilityBtn.bind(DGG.WITHIN, self.__handleVisEnter)
        self.visibilityBtn.bind(DGG.WITHOUT, self.__handleVisExit)
        self.visibilityBtn.setBin('background', 10)
        self.visibilityBtn = None
        self.visibilityBtnStatus = 0
        self.switchSound = True
        self.switchSoundSfx = base.loadSfx(
            'phase_3/audio/sfx/GUI_balloon_popup.ogg')
        self.visibilityFSM = ClassicFSM('InventoryGui-VisibilityFSM', [
            State('off', self.enterOff, self.exitOff),
            State('hidden', self.enterHidden, self.exitHidden),
            State('hidden2visible', self.enterHidden2Visible,
                  self.exitHidden2Visible),
            State('visible', self.enterVisible, self.exitVisible),
            State('visible2hidden', self.enterVisible2Hidden,
                  self.exitVisible2Hidden)
        ], 'off', 'off')
        self.visibilityFSM.enterInitialState()
        self.visibilityFSM.request('hidden')
        return

    def enterOff(self):
        pass

    def exitOff(self):
        pass

    def enterHidden(self):
        self.inventoryFrame.setPos(InventoryGui.HiddenPos)
        self.inventoryFrame.hide()

    def exitHidden(self):
        pass

    def enterVisible(self, autoShow=False):
        self.inventoryFrame.setPos(InventoryGui.VisiblePos)
        self.inventoryFrame.show()
        if self.visibilityBtnStatus == 0:
            if autoShow is False:
                self.visibilityFSM.request('visible2hidden')

    def exitVisible(self):
        pass

    def enterHidden2Visible(self, autoShow=False):
        self.inventoryFrame.show()
        self.moveIval = LerpPosInterval(self.inventoryFrame,
                                        duration=InventoryGui.SwitchTime,
                                        pos=InventoryGui.VisiblePos,
                                        startPos=InventoryGui.HiddenPos)
        self.moveIval.setDoneEvent('hidden2visible')
        self.acceptOnce('hidden2visible', self.visibilityFSM.request,
                        ['visible', [autoShow]])
        self.moveIval.start()

    def exitHidden2Visible(self):
        self.ignore('hidden2visible')
        self.moveIval.finish()
        del self.moveIval

    def enterVisible2Hidden(self):
        self.moveIval = LerpPosInterval(self.inventoryFrame,
                                        duration=InventoryGui.SwitchTime,
                                        pos=InventoryGui.HiddenPos,
                                        startPos=InventoryGui.VisiblePos)
        self.moveIval.setDoneEvent('visible2hidden')
        self.acceptOnce('visible2hidden', self.visibilityFSM.request,
                        ['hidden'])
        self.moveIval.start()

    def exitVisible2Hidden(self):
        self.ignore('visible2hidden')
        self.moveIval.finish()
        del self.moveIval

    def click_setWeapon(self, slot, cmd):
        self.setWeapon(slot, playSound=False)

    def setWeapon(self, slot, playSound=True, showUpIfHidden=False):
        if isinstance(slot, str):
            for iSlot in self.slots:
                if iSlot.getGag():
                    if iSlot.getGag().getID() == slot:
                        slot = iSlot

        if self.activeSlot and slot != self.activeSlot:
            self.activeSlot.setOutlineImage('idle')
            self.prevSlot = self.activeSlot
        if slot.getGag() and self.backpack.getSupply(slot.getGag().getID(
        )) > 0 and not slot.getGag().getState() == GagState.RECHARGING:
            if self.activeSlot != slot:
                gagId = slot.getGag().getID()
                base.localAvatar.needsToSwitchToGag = gagId
                if base.localAvatar.gagsTimedOut == False:
                    base.localAvatar.b_equip(gagId)
                    base.localAvatar.enableGagKeys()
                slot.setOutlineImage('selected')
                self.activeSlot = slot
            else:
                if self.activeSlot == slot and slot.getGag().getState() in [
                        GagState.LOADED, GagState.RECHARGING
                ]:
                    base.localAvatar.needsToSwitchToGag = 'unequip'
                    if base.localAvatar.gagsTimedOut == False:
                        base.localAvatar.b_unEquip()
                        base.localAvatar.enableGagKeys()
                    self.activeSlot = None
            self.update()
            if self.switchSound and playSound:
                base.playSfx(self.switchSoundSfx)
        if showUpIfHidden:
            base.taskMgr.remove('showUpIfHidden')
            self.__autoVisEnter()
            base.taskMgr.doMethodLater(InventoryGui.AutoShowTime,
                                       self.__autoVisExitTask,
                                       'showUpIfHidden')
        return

    def __autoVisExitTask(self, task):
        if self.visibilityBtnStatus == 0:
            self.__handleVisExit(None, updateBtnStatus=False)
        return task.done

    def __autoVisEnter(self):
        self.__handleVisEnter(None, True, False)
        return

    def __handleVisEnter(self, foo, autoShow=False, updateBtnStatus=True):
        if updateBtnStatus:
            self.visibilityBtnStatus = 1
        if self.visibilityFSM.getCurrentState().getName() == 'hidden':
            self.visibilityFSM.request('hidden2visible', [autoShow])
        else:
            if self.visibilityFSM.getCurrentState().getName(
            ) == 'visible2hidden':
                self.visibilityFSM.request('visible')

    def __handleVisExit(self, foo, updateBtnStatus=True):
        if updateBtnStatus:
            self.visibilityBtnStatus = 0
        base.taskMgr.remove('showUpIfHidden')
        if self.visibilityFSM.getCurrentState().getName() == 'visible':
            self.visibilityFSM.request('visible2hidden')

    def createGui(self):
        self.deleteGui()
        posGroup = self.threeSlotsPos
        if self.defaultSlots == 4:
            posGroup = self.fourSlotPos
        for slot in range(len(posGroup) + 1):
            if slot == 3:
                posGroup = self.fourSlotPos
            slotObj = Slot(self, slot + 1, posGroup[slot], self.inventoryFrame)
            self.slots.append(slotObj)
            if slot == 3:
                slotObj.hide()

        self.ammoLabel = DirectLabel(text='Ammo: 0',
                                     text_fg=(1, 1, 1, 1),
                                     relief=None,
                                     text_shadow=(0, 0, 0, 1),
                                     text_scale=0.08,
                                     pos=(0.2, 0, 0.35),
                                     parent=base.a2dBottomLeft)
        self.ammoLabel.hide()
        self.enableWeaponSwitch()
        self.resetScroll()
        self.update()
        return

    def deleteGui(self):
        self.disableWeaponSwitch()
        for slot in self.slots:
            slot.destroy()

        self.slots = []
        if self.ammoLabel:
            self.ammoLabel.destroy()
            self.ammoLabel = None
        self.DELETED = True
        return

    def resetScroll(self):
        nextGag = 0
        prevGag = -1
        curGag = -1
        if self.prevSlot:
            prevGag = self.slots.index(self.prevSlot)
        if self.activeSlot:
            curGag = self.slots.index(self.activeSlot)
        if curGag == len(self.slots) - 1:
            nextGag = 0
            prevGag = curGag - 1
        else:
            if curGag == 0:
                nextGag = 1
                prevGag = len(self.slots) - 1
            else:
                if curGag == -1:
                    prevGag = len(self.slots) - 1
                else:
                    nextGag = curGag + 1
                    prevGag = curGag - 1
        self.accept('wheel_down',
                    self.setWeapon,
                    extraArgs=[self.slots[nextGag], True, True])
        self.accept('wheel_up',
                    self.setWeapon,
                    extraArgs=[self.slots[prevGag], True, True])

    def update(self):
        if not self.backpack:
            return
        for element in [self.ammoLabel, self.inventoryFrame]:
            if not element:
                return

        updateSlots = list(self.slots)
        for slot in self.slots:
            gag = slot.getGag()
            if not gag:
                updateSlots.remove(slot)
                slot.hide()
                continue
            supply = self.backpack.getSupply(gag.getID())
            index = self.slots.index(slot)
            if not gag and len(self.backpack.getGags()) - 1 >= index:
                gag = self.backpack.getGagByIndex(index)
                slot.setGag(gag)
                if self.backpack.getSupply(gag.getID(
                )) > 0 and not gag.getState() == GagState.RECHARGING:
                    slot.setOutlineImage('idle')
                else:
                    slot.setOutlineImage('no_ammo')
            elif slot == self.activeSlot:
                self.ammoLabel['text_fg'] = (1, 1, 1, 1)
                if supply > 0 and not gag.getState() == GagState.RECHARGING:
                    slot.setOutlineImage('selected')
                else:
                    if supply <= 0:
                        self.ammoLabel['text_fg'] = (0.9, 0, 0, 1)
                    slot.setOutlineImage('no_ammo')
                    self.activeSlot = None
                self.ammoLabel.show()
                self.ammoLabel['text'] = 'Ammo: %s' % self.backpack.getSupply(
                    slot.getGag().getID())
            elif self.backpack.getSupply(slot.getGag().getID(
            )) > 0 and not gag.getState() == GagState.RECHARGING:
                slot.setOutlineImage('idle')
            else:
                slot.setOutlineImage('no_ammo')

        numSlots = len(updateSlots)
        posGroup = {
            1: self.oneSlotPos,
            2: self.twoSlotsPos,
            3: self.threeSlotsPos,
            4: self.fourSlotPos
        }.get(numSlots)
        for i in xrange(len(updateSlots)):
            updateSlots[i].setPos(posGroup[i])
            updateSlots[i].show()

        if self.activeSlot == None:
            self.ammoLabel.hide()
            self.ammoLabel['text'] = 'Ammo: 0'
        self.resetScroll()
        return

    def setBackpack(self, backpack):
        self.backpack = backpack

    def updateLoadout(self):
        if self.backpack:
            loadout = self.backpack.getLoadout()
            if len(loadout) <= 3:
                self.reseatSlots()
            else:
                if len(loadout) == 4:
                    self.reseatSlots(slots=4)
            for i in range(len(self.slots)):
                slot = self.slots[i]
                if i < len(loadout):
                    slot.setGag(loadout[i])
                else:
                    slot.setGag(None)

            self.update()
        return

    def reseatSlots(self, slots=3):
        for slot in range(len(self.slots) - 1):
            if slots == 4:
                self.slots[slot].setPos(self.fourSlotPos[slot])
            else:
                self.slots[slot].setPos(self.threeSlotsPos[slot])

    def enableWeaponSwitch(self):
        for index in range(len(self.slots)):
            self.accept(str(index + 1),
                        self.setWeapon,
                        extraArgs=[self.slots[index], True, True])

    def disableWeaponSwitch(self):
        for key in ['1', '2', '3', '4', 'wheel_down', 'wheel_up']:
            self.ignore(key)

    def getSlots(self):
        return self.slots

    def getActiveSlot(self):
        return self.activeSlot

    def isDeleted(self):
        return self.DELETED
Beispiel #4
0
class GUnit(GEntity):
   
   def __init__(self,conf):
      GEntity.__init__(self,conf)
      self.p3dobject.reparentTo(self.gmap.units_node)
      self.p3dobject.setTransparency(TransparencyAttrib.MAlpha)
      #to be put under condition for non pickable units (bonuses npc for instance)
      self.p3dobject.setTag('GUnit-pickable','1')
      self.p3dobject.setPos(self.gmap.root.find('**/tile_'+str(conf['tileid'])),0,0,0)
      #supposedly already a float, but will screw up if not, so just making sure.
      self.move_speed=float(conf['move_speed'])
      self.path=[]
      self.popout_when_move_over=False
      self.pid=conf['pid']
      #highlight
      self.ts_highlighted=TextureStage('ts_highlighted')
      self.ts_highlighted.setMode(TextureStage.MDecal)
      self.ts_highlighted.setSort(2)
      #highlight
      self.ts_selected=TextureStage('ts_selected')
      self.ts_selected.setMode(TextureStage.MDecal)
      self.ts_selected.setSort(3)
               
   @staticmethod
   def load_resources():
      GUnit.textures={   'highlighted':loader.loadTexture('data/models/highlighted.tex.png'),
                        'selected':loader.loadTexture('data/models/selected.tex.png'),
                     }
      
   def dispose(self):
      '''del method'''
      GEntity.dispose(self)
      self.popout_sequence.finish()
      del self.popout_sequence

   def add_path(self,data):
      '''
      adds tile to pass by.
      '''
      #check for data completeness
      if not 'path' in data:
         out('WARNING in GUnit.add_path: incomplete data:\n'+str(data))
         return
      elif not isinstance(data['path'],list):
         out('WARNING in GUnit.add_path: invalid data:\n'+str(data))
         return
      #data considered valid
      self.path.extend([self.instances[eid] for eid in data['path']])
      if not self.update_move in update_list: 
         update_list.append(self.update_move)
      #out('GUnit.add_path:'+str(data))
   
   def finish_move_to(self,data):
      '''triggered by server side unit, to indicate the need to popout at end of move.'''
      out('GUnit.finish_move_to()'+str(data))
      if self.update_move in update_list:
         self.popout_when_move_over=True
      else:
         self.popout()
         
   def popout(self):
      '''sets up the popout animation at end of unit's mission'''
      scale=self.p3dobject.scaleInterval(.5,(.1,.1,.1))
      finish=Func(lambda:dispose_list.append(self))
      self.popout_sequence=Sequence(scale,finish)
      self.popout_sequence.start()
      
   def set_highlighted(self):
      self.p3dobject.setTexture(self.ts_highlighted,self.textures['highlighted'])
      
   def unset_highlighted(self):
      self.p3dobject.clearTexture(self.ts_highlighted)
      
   def set_selected(self):
      self.p3dobject.setTexture(self.ts_selected,self.textures['selected'])
      
   def unset_selected(self):
      self.p3dobject.clearTexture(self.ts_selected)
      
   def update_move(self):
      '''called every frame during while a move.'''
      if len(self.path)==0:
         out('WARNING in GUnit.update_move: path is empty, but method still called. removing it.')
         update_list.remove(self.update_move)
         return
      if not hasattr(self,'move_interval'):
         #start moving
         #first 3 args=model,duration,pos, the duration=1/... is relative to server side tile side size
         self.move_interval=LerpPosInterval(self.p3dobject,
                                    (1/(self.move_speed*ConfigVariableDouble('clock-frame-rate').getValue())),
                                    self.path[0].p3dobject.getPos(),
                                    name='interval_unit_move_'+str(self.eid)
                                    )
         self.p3dobject.lookAt(self.path[0].p3dobject.getPos())
         self.p3dobject.loop('run')
         self.move_interval.start()
      else:
         #is move ~over ?
         #t=self.move_interval.getT()
         #d=self.move_interval.getDuration()
         #d=d-t
         d=dist3(self.p3dobject,self.path[0].p3dobject)
         #out('client '+str(t*100./d)+'%')
         #arrived
         if d<self.move_speed:
            #out('client '+str(self.path[0].eid)+'@'+str(self.frame_no))
            self.p3dobject.setPos(self.path[0].p3dobject,0,0,0)
            self.path.pop(0)
            if len(self.path)==0:
               self.p3dobject.stop()
               self.move_interval.finish()
               del self.move_interval
               update_list.remove(self.update_move)
               if self.popout_when_move_over:
                  self.popout()
            else:
               #first 3 args=model,duration,pos
               self.move_interval.finish()
               self.move_interval=LerpPosInterval(self.p3dobject,
                                          (1/(self.move_speed*ConfigVariableDouble('clock-frame-rate').getValue())),
                                          self.path[0].p3dobject.getPos(),
                                          name='interval_unit_move_'+str(self.eid)
                                          )
               self.p3dobject.lookAt(self.path[0].p3dobject.getPos())
               self.move_interval.start()
class DistributedEagleSuit(DistributedSuit):
    notify = directNotify.newCategory('DistributedEagleSuit')

    def __init__(self, cr):
        DistributedSuit.__init__(self, cr)
        self.eagleCry = base.audio3d.loadSfx('phase_5/audio/sfx/tt_s_ara_cfg_eagleCry.mp3')
        base.audio3d.attachSoundToObject(self.eagleCry, self)
        self.fallWhistle = base.audio3d.loadSfx('phase_5/audio/sfx/incoming_whistleALT.mp3')
        base.audio3d.attachSoundToObject(self.fallWhistle, self)
        self.explode = base.audio3d.loadSfx('phase_3.5/audio/sfx/ENC_cogfall_apart.mp3')
        base.audio3d.attachSoundToObject(self.explode, self)
        self.eventSphereNodePath = None
        self.fallingPropeller = None
        self.fallingPropProjectile = None
        self.mg = None
        self.flySpeed = 0.0
        return

    def enterNeutral(self, ts = 0):
        self.show()
        self.timestampAnimTrack = Sequence(Wait(ts), Func(self.loop, 'flyNeutral'))
        self.timestampAnimTrack.start()

    def makeStateDict(self):
        self.suitFSM.addState(State('eagleFly', self.enterEagleFly, self.exitEagleFly))
        self.suitFSM.addState(State('eagleFall', self.enterEagleFall, self.exitEagleFall))
        self.stateIndex2suitState = {0: self.suitFSM.getStateNamed('off'),
         1: self.suitFSM.getStateNamed('walking'),
         2: self.suitFSM.getStateNamed('flyingDown'),
         3: self.suitFSM.getStateNamed('flyingUp'),
         4: self.suitFSM.getStateNamed('lured'),
         5: self.suitFSM.getStateNamed('eagleFly'),
         6: self.suitFSM.getStateNamed('eagleFall')}
        self.suitState2stateIndex = {}
        for stateId, state in self.stateIndex2suitState.items():
            self.suitState2stateIndex[state.getName()] = stateId

    def setFlySpeed(self, value):
        self.flySpeed = value

    def getFlySpeed(self):
        return self.flySpeed

    def enterEagleFly(self, startIndex, endIndex, ts = 0.0):
        durationFactor = self.getFlySpeed()
        if startIndex > -1:
            startPos = EGG.EAGLE_FLY_POINTS[startIndex]
        else:
            startPos = self.getPos(render)
        endPos = EGG.EAGLE_FLY_POINTS[endIndex]
        if self.moveIval:
            self.moveIval.pause()
            self.moveIval = None
        self.moveIval = NPCWalkInterval(self, endPos, durationFactor=durationFactor, startPos=startPos, fluid=1)
        self.moveIval.start(ts)
        return

    def exitEagleFly(self):
        if self.moveIval:
            self.moveIval.pause()
            self.moveIval = None
        return

    def enterEagleFall(self, startIndex, endIndex, ts = 0.0):
        self.moveIval = LerpPosInterval(self, duration=4.0, pos=self.getPos(render) - (0, 0, 75), startPos=self.getPos(render), blendType='easeIn')
        self.moveIval.start(ts)

    def exitEagleFall(self):
        if self.moveIval:
            self.moveIval.finish()
            self.moveIval = None
        return

    def fallAndExplode(self):
        self.cleanupPropeller()
        self.fallingPropeller = Actor('phase_4/models/props/propeller-mod.bam', {'chan': 'phase_4/models/props/propeller-chan.bam'})
        self.fallingPropeller.reparentTo(render)
        self.fallingPropeller.loop('chan', fromFrame=0, toFrame=3)
        parentNode = self.attachNewNode('fallingPropParentNode')
        h = random.randint(0, 359)
        parentNode.setH(h)
        dummyNode = parentNode.attachNewNode('dummyNode')
        dummyNode.setPos(0, 10, -50)
        self.fallingPropProjectile = FlightProjectileInterval(self.fallingPropeller, startPos=self.find('**/joint_head').getPos(render), endPos=dummyNode.getPos(render), duration=5.0, gravityMult=0.25)
        self.fallingPropProjectile.start()
        dummyNode.removeNode()
        del dummyNode
        parentNode.removeNode()
        del parentNode
        self.updateHealthBar(0)
        self.ignoreHit()
        self.fallWhistle.play()
        taskMgr.doMethodLater(4.0, self.doExplodeSound, self.uniqueName('DEagleSuit-doExplodeSound'))

    def doExplodeSound(self, task):
        self.explode.play()
        return Task.done

    def __initializeEventSphere(self):
        sphere = CollisionSphere(0, 0, 0, 2)
        sphere.setTangible(0)
        node = CollisionNode(self.uniqueName('DEagleSuit-eventSphere'))
        node.addSolid(sphere)
        node.setCollideMask(CIGlobals.WallBitmask)
        np = self.attachNewNode(node)
        np.setSz(2.5)
        np.setZ(5.5)
        self.eventSphereNodePath = np

    def removeEventSphere(self):
        if self.eventSphereNodePath:
            self.eventSphereNodePath.removeNode()
            self.eventSphereNodePath = None
        return

    def acceptHit(self):
        self.acceptOnce('enter' + self.eventSphereNodePath.node().getName(), self.__handleHit)

    def ignoreHit(self):
        self.ignore('enter' + self.eventSphereNodePath.node().getName())

    def __handleHit(self, entry):
        messenger.send(EGG.EAGLE_HIT_EVENT, [self.doId])

    def setSuit(self, arg, variant):
        DistributedSuit.setSuit(self, arg, 3)
        self.deleteShadow()
        self.disableBodyCollisions()
        self.disableRay()
        self.__initializeEventSphere()
        self.show()
        self.setAnimState('flyNeutral')

    def __doEagleCry(self, task):
        self.eagleCry.play()
        task.delayTime = random.uniform(3, 30)
        return Task.again

    def announceGenerate(self):
        DistributedSuit.announceGenerate(self)
        taskMgr.doMethodLater(random.uniform(5, 25), self.__doEagleCry, self.uniqueName('DEagleSuit-doEagleCry'))
        self.acceptHit()

    def disable(self):
        self.ignoreHit()
        self.removeEventSphere()
        taskMgr.remove(self.uniqueName('DEagleSuit-doExplodeSound'))
        taskMgr.remove(self.uniqueName('DEagleSuit-doEagleCry'))
        if self.fallingPropProjectile:
            self.fallingPropProjectile.finish()
            self.fallingPropProjectile = None
        if self.fallingPropeller:
            self.fallingPropeller.cleanup()
            self.fallingPropeller = None
        base.audio3d.detachSound(self.fallWhistle)
        del self.fallWhistle
        base.audio3d.detachSound(self.explode)
        del self.explode
        base.audio3d.detachSound(self.eagleCry)
        del self.eagleCry
        self.mg = None
        DistributedSuit.disable(self)
        return
class EnemyUnit(Unit):
    """Base class of enemy units.

    Args:
        id_ (int): Enemy unit id.
        class_ (str): Enemy class name.
        class_data (dict): Enemy class description.
        model (actor.Actor): Enemy character model.
        y_positions (list): Free positions along Y.
        enemy_handler (CollisionHandlerEvent): Enemy collisions handler.
    """

    def __init__(self, id_, class_, class_data, model, y_positions, enemy_handler):
        Unit.__init__(self, "enemy_" + str(id_), class_, class_data)

        self.transport_snd = None
        self._move_int = None
        self._rb_node = None
        self._tooltip = "Skinhead - " + self.class_

        self._y_positions = y_positions
        self._y_pos = take_random(self._y_positions)

        self._x_range = (-0.3, 0.4) if self.class_data["part"] == "side" else (0.6, 1.3)

        self.node = render.attachNewNode(self.id + "_node")  # noqa: F821
        self.node.setPos(self._io_dist, -7, 0)

        self.model = model
        self.model.pose("ride", 1)
        self.model.reparentTo(self.node)

        # organize movement and aiming tasks
        time_to_overtake = random.randint(33, 50)

        self._move(time_to_overtake, (self._y_pos, random.uniform(*self._x_range), 0))
        taskMgr.doMethodLater(  # noqa: F821
            time_to_overtake + 2, self._float_move, self.id + "_float_move"
        )

    @abc.abstractmethod
    def _explode(self):
        raise NotImplementedError(
            "Every enemy unit class must implement _explode() method."
        )

    @property
    def _io_dist(self):
        """Enemy Y-distance for approach and back off."""
        if self._y_pos > 0:
            return self._y_pos + 0.45
        return self._y_pos - 0.45

    @property
    def clear_delay(self):
        """Delay between this unit death and its clearing.

        Returns:
            int: Seconds to hold the unit before delete.
        """
        return 15

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

        Returns:
            float: Delay between shots in seconds.
        """
        return 1.7 + random.uniform(0.1, 0.9)

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

        Returns:
            str: This unit fraction and class.
        """
        return self._tooltip

    def _die(self):
        """Make this enemy unit die.

        Play death sequence of movements and sounds,
        stop all the tasks for this enemy, plan clearing.

        Returns:
            bool: True, if the unit dies for the first time.
        """
        if not Unit._die(self):
            return False

        self.model.setColorScale(1, 1, 1, 1)
        self._stop_tasks("_float_move")
        self._move_int.pause()

        self.model.play("die")
        if self.id in base.world.enemy.active_units:  # noqa: F821
            base.world.enemy.active_units.pop(self.id)  # noqa: F821
            if self.current_part:
                self.current_part.enemies.remove(self)

        self._explode()
        self.transport_snd.stop()
        self._y_positions.append(self._y_pos)

        base.add_head(self.class_data["class"].__name__)  # noqa: F821
        return True

    def _float_move(self, task):
        """Make enemy floatly move along the Train."""
        if chance(80):
            shift = random.choice((-0.05, 0.05))
            if self._y_pos + shift in self._y_positions:
                self._y_positions.append(self._y_pos)
                self._y_pos = self._y_pos + shift
                self._y_positions.remove(self._y_pos)

        self._move(
            random.randint(3, 6), (self._y_pos, random.uniform(*self._x_range), 0)
        )
        task.delayTime = random.randint(7, 9)
        return task.again

    def _move(self, period, new_pos):
        """Run a new movement interval with the given parameters.

        Args:
            period (tuple): Interval duration bounds.
            new_pos (tuple): New enemy position.
        """
        if self._move_int is not None:
            self._move_int.pause()

        self._move_int = LerpPosInterval(
            self.node, period, new_pos, blendType="easeInOut"
        )
        self._move_int.start()

    def capture_train(self):
        """The Train got critical damage - stop near it."""
        self._stop_tasks("_float_move")

    def clear(self, task=None):
        """Clear all the graphical, physical and sound data of this unit."""
        base.sound_mgr.detach_sound(self.transport_snd)  # noqa: F821
        if getattr(self, "_cry_snd", None):
            base.sound_mgr.detach_sound(self._cry_snd)  # noqa: F821

        self._move_int.finish()
        self.model.cleanup()
        self.node.removeNode()

        if self._rb_node is not None:
            base.world.phys_mgr.removeRigidBody(self._rb_node)  # noqa: F821

        if task is not None:
            return task.done

    def get_damage(self, damage):
        """Take damage points and change model color.

        The more damage the unit taken the more red it'll be.

        Args:
            damage (int): Damage points to get.
        """
        Unit.get_damage(self, damage)
        self.model.setColorScale(self.model.getColorScale()[0] + 0.018, 1, 1, 1)

    def stop(self):
        """Smoothly stop this unit following the Train."""
        self._stop_tasks("_float_move")
        self._move(random.randint(9, 11), (self._io_dist, -7, 0))
        self._y_positions.append(self._y_pos)

    def stop_ride(self):
        """Stop riding actions."""
        self.transport_snd.stop()