from direct.gui.DirectGui import *
from panda3d.core import *
from direct.interval.IntervalGlobal import *
from direct.distributed.ClockDelta import *
from direct.fsm import FSM
from direct.distributed import DistributedObject
from direct.showutil import Rope
from direct.showbase import PythonUtil
from direct.task import Task
from toontown.toonbase import ToontownGlobals
from toontown.toonbase import TTLocalizer
from otp.otpbase import OTPGlobals
from toontown.cogdominium import CogdoCraneGameConsts as GameConsts
import random

class DistCogdoCrane(DistributedObject.DistributedObject, FSM.FSM):
    notify = DirectNotifyGlobal.directNotify.newCategory('DistCogdoCrane')
    firstMagnetBit = 21
    craneMinY = 8
    craneMaxY = 25
    armMinH = -45
    armMaxH = 45
    shadowOffset = 7
    emptySlideSpeed = 10
    emptyRotateSpeed = 20
    lookAtPoint = Point3(0.3, 0, 0.1)
    lookAtUp = Vec3(0, -1, 0)
    neutralStickHinge = VBase3(0, 90, 0)

    def __init__(self, cr):
        DistributedObject.DistributedObject.__init__(self, cr)
        FSM.FSM.__init__(self, 'DistCogdoCrane')
        self.craneGame = None
        self.index = None
        self.avId = 0
        self.cableLength = 20
        self.numLinks = 3
        self.initialArmPosition = (0, 20, 0)
        self.slideSpeed = self.emptySlideSpeed
        self.rotateSpeed = self.emptyRotateSpeed
        self.changeSeq = 0
        self.lastChangeSeq = 0
        self.moveSound = None
        self.links = []
        self.activeLinks = []
        self.collisions = NodePathCollection()
        self.physicsActivated = 0
        self.snifferActivated = 0
        self.magnetOn = 0
        self.root = NodePath('root')
        self.hinge = self.root.attachNewNode('hinge')
        self.hinge.setPos(0, -17.6, 38.5)
        self.controls = self.root.attachNewNode('controls')
        self.controls.setPos(0, -4.9, 0)
        self.arm = self.hinge.attachNewNode('arm')
        self.crane = self.arm.attachNewNode('crane')
        self.cable = self.hinge.attachNewNode('cable')
        self.topLink = self.crane.attachNewNode('topLink')
        self.topLink.setPos(0, 0, -1)
        self.shadow = None
        self.p0 = Point3(0, 0, 0)
        self.v1 = Vec3(1, 1, 1)
        self.armSmoother = SmoothMover()
        self.armSmoother.setSmoothMode(SmoothMover.SMOn)
        self.linkSmoothers = []
        self.smoothStarted = 0
        self.__broadcastPeriod = 0.2
        self.cable.node().setFinal(1)
        self.crane.setPos(*self.initialArmPosition)
        self.heldObject = None
        self.craneAdviceLabel = None
        self.magnetAdviceLabel = None
        self.atLimitSfx = base.loadSfx('phase_4/audio/sfx/MG_cannon_adjust.ogg')
        self.magnetOnSfx = base.loadSfx('phase_10/audio/sfx/CBHQ_CFO_magnet_on.ogg')
        self.magnetLoopSfx = base.loadSfx('phase_10/audio/sfx/CBHQ_CFO_magnet_loop.ogg')
        self.magnetSoundInterval = Parallel(SoundInterval(self.magnetOnSfx), Sequence(Wait(0.5), Func(base.playSfx, self.magnetLoopSfx, looping=1)))
        self.craneMoveSfx = base.loadSfx('phase_9/audio/sfx/CHQ_FACT_elevator_up_down.ogg')
        self.fadeTrack = None
        return

    def announceGenerate(self):
        DistributedObject.DistributedObject.announceGenerate(self)
        self.name = 'crane-%s' % self.doId
        self.root.setName(self.name)
        self.root.setPosHpr(*GameConsts.CranePosHprs[self.index])
        self.rotateLinkName = self.uniqueName('rotateLink')
        self.snifferEvent = self.uniqueName('sniffer')
        self.triggerName = self.uniqueName('trigger')
        self.triggerEvent = 'enter%s' % self.triggerName
        self.shadowName = self.uniqueName('shadow')
        self.flickerName = self.uniqueName('flicker')
        self.smoothName = self.uniqueName('craneSmooth')
        self.posHprBroadcastName = self.uniqueName('craneBroadcast')
        self.craneAdviceName = self.uniqueName('craneAdvice')
        self.magnetAdviceName = self.uniqueName('magnetAdvice')
        self.controlModel = self.craneGame.controls.copyTo(self.controls)
        self.cc = NodePath('cc')
        column = self.controlModel.find('**/column')
        column.getChildren().reparentTo(self.cc)
        self.cc.reparentTo(column)
        self.stickHinge = self.cc.attachNewNode('stickHinge')
        self.stick = self.craneGame.stick.copyTo(self.stickHinge)
        self.stickHinge.setHpr(self.neutralStickHinge)
        self.stick.setHpr(0, -90, 0)
        self.stick.flattenLight()
        self.bottom = self.controlModel.find('**/bottom')
        self.bottom.wrtReparentTo(self.cc)
        self.bottomPos = self.bottom.getPos()
        cs = CollisionSphere(0, -5, -2, 3)
        cs.setTangible(0)
        cn = CollisionNode(self.triggerName)
        cn.addSolid(cs)
        cn.setIntoCollideMask(OTPGlobals.WallBitmask)
        self.trigger = self.root.attachNewNode(cn)
        self.trigger.stash()
        cs = CollisionTube(0, 2.7, 0, 0, 2.7, 3, 1.2)
        cn = CollisionNode('tube')
        cn.addSolid(cs)
        cn.setIntoCollideMask(OTPGlobals.WallBitmask)
        self.tube = self.controlModel.attachNewNode(cn)
        cs = CollisionSphere(0, 0, 2, 3)
        cn = CollisionNode('safetyBubble')
        cn.addSolid(cs)
        cn.setIntoCollideMask(ToontownGlobals.PieBitmask)
        self.controls.attachNewNode(cn)
        arm = self.craneGame.craneArm.copyTo(self.crane)
        self.craneGame.cranes[self.index] = self

    def disable(self):
        DistributedObject.DistributedObject.disable(self)
        del self.craneGame.cranes[self.index]
        self.cleanup()

    def cleanup(self):
        if self.state != 'Off':
            self.demand('Off')
        self.craneGame = None
        return

    def accomodateToon(self, toon):
        origScale = self.controlModel.getSz()
        origCcPos = self.cc.getPos()
        origBottomPos = self.bottom.getPos()
        origStickHingeHpr = self.stickHinge.getHpr()
        scale = toon.getGeomNode().getChild(0).getSz(render)
        self.controlModel.setScale(scale)
        self.cc.setPos(0, 0, 0)
        toon.setPosHpr(self.controls, 0, 0, 0, 0, 0, 0)
        toon.pose('leverNeutral', 0)
        toon.update()
        pos = toon.rightHand.getPos(self.cc)
        self.cc.setPos(pos[0], pos[1], pos[2] - 1)
        self.bottom.setZ(toon, 0.0)
        self.bottom.setPos(self.bottomPos[0], self.bottomPos[1], self.bottom.getZ())
        self.stickHinge.lookAt(toon.rightHand, self.lookAtPoint, self.lookAtUp)
        lerpTime = 0.5
        return Parallel(self.controlModel.scaleInterval(lerpTime, scale, origScale, blendType='easeInOut'), self.cc.posInterval(lerpTime, self.cc.getPos(), origCcPos, blendType='easeInOut'), self.bottom.posInterval(lerpTime, self.bottom.getPos(), origBottomPos, blendType='easeInOut'), self.stickHinge.quatInterval(lerpTime, self.stickHinge.getHpr(), origStickHingeHpr, blendType='easeInOut'))

    def getRestoreScaleInterval(self):
        lerpTime = 1
        return Parallel(self.controlModel.scaleInterval(lerpTime, 1, blendType='easeInOut'), self.cc.posInterval(lerpTime, Point3(0, 0, 0), blendType='easeInOut'), self.bottom.posInterval(lerpTime, self.bottomPos, blendType='easeInOut'), self.stickHinge.quatInterval(lerpTime, self.neutralStickHinge, blendType='easeInOut'))

    def makeToonGrabInterval(self, toon):
        origPos = toon.getPos()
        origHpr = toon.getHpr()
        a = self.accomodateToon(toon)
        newPos = toon.getPos()
        newHpr = toon.getHpr()
        origHpr.setX(PythonUtil.fitSrcAngle2Dest(origHpr[0], newHpr[0]))
        toon.setPosHpr(origPos, origHpr)
        walkTime = 0.2
        reach = ActorInterval(toon, 'leverReach')
        if reach.getDuration() < walkTime:
            reach = Sequence(ActorInterval(toon, 'walk', loop=1, duration=walkTime - reach.getDuration()), reach)
        i = Sequence(Parallel(toon.posInterval(walkTime, newPos, origPos), toon.hprInterval(walkTime, newHpr, origHpr), reach), Func(self.startWatchJoystick, toon))
        i = Parallel(i, a)
        return i

    def __toonPlayWithCallback(self, animName, numFrames):
        duration = numFrames / 24.0
        self.toon.play(animName)
        taskMgr.doMethodLater(duration, self.__toonPlayCallback, self.uniqueName('toonPlay'))

    def __toonPlayCallback(self, task):
        if self.changeSeq == self.lastChangeSeq:
            self.__toonPlayWithCallback('leverNeutral', 40)
        else:
            self.__toonPlayWithCallback('leverPull', 40)
            self.lastChangeSeq = self.changeSeq

    def startWatchJoystick(self, toon):
        self.toon = toon
        taskMgr.add(self.__watchJoystick, self.uniqueName('watchJoystick'))
        self.__toonPlayWithCallback('leverNeutral', 40)
        self.accept(toon.uniqueName('disable'), self.__handleUnexpectedExit, extraArgs=[toon.doId])

    def stopWatchJoystick(self):
        taskMgr.remove(self.uniqueName('toonPlay'))
        taskMgr.remove(self.uniqueName('watchJoystick'))
        if self.toon:
            self.ignore(self.toon.uniqueName('disable'))
        self.toon = None
        return

    def __watchJoystick(self, task):
        self.toon.setPosHpr(self.controls, 0, 0, 0, 0, 0, 0)
        self.toon.update()
        self.stickHinge.lookAt(self.toon.rightHand, self.lookAtPoint, self.lookAtUp)
        return Task.cont

    def __handleUnexpectedExit(self, toonId):
        self.notify.warning('%s: unexpected exit for %s' % (self.doId, toonId))
        if self.toon and self.toon.doId == toonId:
            self.stopWatchJoystick()

    def __activatePhysics(self):
        if not self.physicsActivated:
            for an, anp, cnp in self.activeLinks:
                self.craneGame.physicsMgr.attachPhysicalNode(an)
                base.cTrav.addCollider(cnp, self.handler)

            self.collisions.unstash()
            self.physicsActivated = 1

    def __deactivatePhysics(self):
        if self.physicsActivated:
            for an, anp, cnp in self.activeLinks:
                self.craneGame.physicsMgr.removePhysicalNode(an)
                base.cTrav.removeCollider(cnp)

            self.collisions.stash()
            self.physicsActivated = 0

    def __straightenCable(self):
        for linkNum in xrange(self.numLinks):
            an, anp, cnp = self.activeLinks[linkNum]
            an.getPhysicsObject().setVelocity(0, 0, 0)
            z = float(linkNum + 1) / float(self.numLinks) * self.cableLength
            anp.setPos(self.crane.getPos(self.cable))
            anp.setZ(-z)

    def setCableLength(self, length):
        self.cableLength = length
        linkWidth = float(length) / float(self.numLinks)
        self.shell.setRadius(linkWidth + 1)

    def setupCable(self):
        activated = self.physicsActivated
        self.clearCable()
        self.handler = PhysicsCollisionHandler()
        self.handler.setStaticFrictionCoef(0.1)
        self.handler.setDynamicFrictionCoef(GameConsts.Settings.EmptyFrictionCoef.get())
        linkWidth = float(self.cableLength) / float(self.numLinks)
        self.shell = CollisionInvSphere(0, 0, 0, linkWidth + 1)
        self.links = []
        self.links.append((self.topLink, Point3(0, 0, 0)))
        anchor = self.topLink
        for linkNum in xrange(self.numLinks):
            anchor = self.__makeLink(anchor, linkNum)

        self.collisions.stash()
        self.bottomLink = self.links[-1][0]
        self.middleLink = self.links[-2][0]
        self.magnet = self.bottomLink.attachNewNode('magnet')
        self.wiggleMagnet = self.magnet.attachNewNode('wiggleMagnet')
        taskMgr.add(self.__rotateMagnet, self.rotateLinkName)
        magnetModel = self.craneGame.magnet.copyTo(self.wiggleMagnet)
        magnetModel.setHpr(90, 45, 90)
        self.gripper = magnetModel.attachNewNode('gripper')
        self.gripper.setPos(0, 0, -4)
        cn = CollisionNode('sniffer')
        self.sniffer = magnetModel.attachNewNode(cn)
        self.sniffer.stash()
        cs = CollisionSphere(0, 0, -10, 6)
        cs.setTangible(0)
        cn.addSolid(cs)
        cn.setIntoCollideMask(BitMask32(0))
        cn.setFromCollideMask(ToontownGlobals.CashbotBossObjectBitmask)
        self.snifferHandler = CollisionHandlerEvent()
        self.snifferHandler.addInPattern(self.snifferEvent)
        self.snifferHandler.addAgainPattern(self.snifferEvent)
        rope = self.makeSpline()
        rope.reparentTo(self.cable)
        rope.setTexture(self.craneGame.cableTex)
        ts = TextureStage.getDefault()
        rope.setTexScale(ts, 0.15, 0.13)
        rope.setTexOffset(ts, 0.83, 0.01)
        if activated:
            self.__activatePhysics()

    def clearCable(self):
        self.__deactivatePhysics()
        taskMgr.remove(self.rotateLinkName)
        self.links = []
        self.activeLinks = []
        self.linkSmoothers = []
        self.collisions.clear()
        self.cable.getChildren().detach()
        self.topLink.getChildren().detach()
        self.gripper = None
        return

    def makeSpline(self):
        rope = Rope.Rope()
        rope.setup(min(len(self.links), 4), self.links)
        rope.curve.normalizeKnots()
        rn = rope.ropeNode
        rn.setRenderMode(RopeNode.RMTube)
        rn.setNumSlices(3)
        rn.setTubeUp(Vec3(0, -1, 0))
        rn.setUvMode(RopeNode.UVParametric)
        rn.setUvDirection(1)
        rn.setThickness(0.5)
        return rope

    def startShadow(self):
        self.shadow = self.craneGame.geomRoot.attachNewNode('%s-shadow' % self.name)
        self.shadow.setColor(1, 1, 1, 0.3)
        self.shadow.setDepthWrite(0)
        self.shadow.setTransparency(1)
        self.shadow.setBin('shadow', 0)
        self.shadow.node().setFinal(1)
        self.magnetShadow = loader.loadModel('phase_3/models/props/drop_shadow')
        self.magnetShadow.reparentTo(self.shadow)
        self.craneShadow = loader.loadModel('phase_3/models/props/square_drop_shadow')
        self.craneShadow.setScale(0.5, 4, 1)
        self.craneShadow.setPos(0, -12, 0)
        self.craneShadow.flattenLight()
        self.craneShadow.reparentTo(self.shadow)
        taskMgr.add(self.__followShadow, self.shadowName)
        rope = self.makeSpline()
        rope.reparentTo(self.shadow)
        rope.setColor(1, 1, 1, 0.2)
        tex = self.craneShadow.findTexture('*')
        rope.setTexture(tex)
        rn = rope.ropeNode
        rn.setRenderMode(RopeNode.RMTape)
        rn.setNumSubdiv(6)
        rn.setThickness(0.8)
        rn.setTubeUp(Vec3(0, 0, 1))
        rn.setMatrix(Mat4.translateMat(0, 0, self.shadowOffset) * Mat4.scaleMat(1, 1, 0.01))

    def stopShadow(self):
        if self.shadow:
            self.shadow.removeNode()
            self.shadow = None
            self.magnetShadow = None
            self.craneShadow = None
        taskMgr.remove(self.shadowName)
        return

    def __followShadow(self, task):
        p = self.magnet.getPos(self.craneGame.geomRoot)
        self.magnetShadow.setPos(p[0], p[1], self.shadowOffset)
        self.craneShadow.setPosHpr(self.crane, 0, 0, 0, 0, 0, 0)
        self.craneShadow.setZ(self.shadowOffset)
        return Task.cont

    def __makeLink(self, anchor, linkNum):
        an = ActorNode('link%s' % linkNum)
        an.getPhysicsObject().setMass(GameConsts.Settings.RopeLinkMass.get())
        anp = NodePath(an)
        cn = CollisionNode('cn')
        sphere = CollisionSphere(0, 0, 0, 1)
        cn.addSolid(sphere)
        cnp = anp.attachNewNode(cn)
        self.handler.addCollider(cnp, anp)
        self.activeLinks.append((an, anp, cnp))
        self.linkSmoothers.append(SmoothMover())
        anp.reparentTo(self.cable)
        z = float(linkNum + 1) / float(self.numLinks) * self.cableLength
        anp.setPos(self.crane.getPos())
        anp.setZ(-z)
        mask = BitMask32.bit(self.firstMagnetBit + linkNum)
        cn.setFromCollideMask(mask)
        cn.setIntoCollideMask(BitMask32(0))
        shellNode = CollisionNode('shell%s' % linkNum)
        shellNode.addSolid(self.shell)
        shellNP = anchor.attachNewNode(shellNode)
        shellNode.setIntoCollideMask(mask)
        self.collisions.addPath(shellNP)
        self.collisions.addPath(cnp)
        self.links.append((anp, Point3(0, 0, 0)))
        return anp

    def __rotateMagnet(self, task):
        self.magnet.lookAt(self.middleLink, self.p0, self.v1)
        return Task.cont

    def __enableControlInterface(self):
        gui = loader.loadModel('phase_3.5/models/gui/avatar_panel_gui')
        self.accept('control', self.__controlPressed)
        self.accept('control-up', self.__controlReleased)
        self.accept('InputState-forward', self.__upArrow)
        self.accept('InputState-reverse', self.__downArrow)
        self.accept('InputState-turnLeft', self.__leftArrow)
        self.accept('InputState-turnRight', self.__rightArrow)
        taskMgr.add(self.__watchControls, 'watchCraneControls')
        taskMgr.doMethodLater(5, self.__displayCraneAdvice, self.craneAdviceName)
        taskMgr.doMethodLater(10, self.__displayMagnetAdvice, self.magnetAdviceName)
        NametagGlobals.setOnscreenChatForced(1)
        self.arrowVert = 0
        self.arrowHorz = 0

    def __disableControlInterface(self):
        self.__turnOffMagnet()
        self.__cleanupCraneAdvice()
        self.__cleanupMagnetAdvice()
        self.ignore('escape')
        self.ignore('control')
        self.ignore('control-up')
        self.ignore('InputState-forward')
        self.ignore('InputState-reverse')
        self.ignore('InputState-turnLeft')
        self.ignore('InputState-turnRight')
        self.arrowVert = 0
        self.arrowHorz = 0
        NametagGlobals.setOnscreenChatForced(0)
        taskMgr.remove('watchCraneControls')
        self.__setMoveSound(None)
        return

    def __displayCraneAdvice(self, task):
        if self.craneAdviceLabel == None:
            self.craneAdviceLabel = DirectLabel(text=TTLocalizer.CashbotCraneAdvice, text_fg=VBase4(1, 1, 1, 1), text_align=TextNode.ACenter, relief=None, pos=(0, 0, 0.69), scale=0.1)
        return

    def __cleanupCraneAdvice(self):
        if self.craneAdviceLabel:
            self.craneAdviceLabel.destroy()
            self.craneAdviceLabel = None
        taskMgr.remove(self.craneAdviceName)
        return

    def __displayMagnetAdvice(self, task):
        if self.magnetAdviceLabel == None:
            self.magnetAdviceLabel = DirectLabel(text=TTLocalizer.CashbotMagnetAdvice, text_fg=VBase4(1, 1, 1, 1), text_align=TextNode.ACenter, relief=None, pos=(0, 0, 0.55), scale=0.1)
        return

    def __cleanupMagnetAdvice(self):
        if self.magnetAdviceLabel:
            self.magnetAdviceLabel.destroy()
            self.magnetAdviceLabel = None
        taskMgr.remove(self.magnetAdviceName)
        return

    def __watchControls(self, task):
        if self.arrowHorz or self.arrowVert:
            self.__moveCraneArcHinge(self.arrowHorz, self.arrowVert)
        else:
            self.__setMoveSound(None)
        return Task.cont

    def __incrementChangeSeq(self):
        self.changeSeq = self.changeSeq + 1 & 255

    def __controlPressed(self):
        self.__cleanupMagnetAdvice()
        self.__turnOnMagnet()

    def __controlReleased(self):
        self.__turnOffMagnet()

    def __turnOnMagnet(self):
        if not self.magnetOn:
            self.__incrementChangeSeq()
            self.magnetOn = 1
            if not self.heldObject:
                self.__activateSniffer()

    def __turnOffMagnet(self):
        if self.magnetOn:
            self.magnetOn = 0
            self.__deactivateSniffer()
            self.releaseObject()

    def __upArrow(self, pressed):
        self.__incrementChangeSeq()
        self.__cleanupCraneAdvice()
        if pressed:
            self.arrowVert = 1
        elif self.arrowVert > 0:
            self.arrowVert = 0

    def __downArrow(self, pressed):
        self.__incrementChangeSeq()
        self.__cleanupCraneAdvice()
        if pressed:
            self.arrowVert = -1
        elif self.arrowVert < 0:
            self.arrowVert = 0

    def __rightArrow(self, pressed):
        self.__incrementChangeSeq()
        self.__cleanupCraneAdvice()
        if pressed:
            self.arrowHorz = 1
        elif self.arrowHorz > 0:
            self.arrowHorz = 0

    def __leftArrow(self, pressed):
        self.__incrementChangeSeq()
        self.__cleanupCraneAdvice()
        if pressed:
            self.arrowHorz = -1
        elif self.arrowHorz < 0:
            self.arrowHorz = 0

    def __moveCraneArcHinge(self, xd, yd):
        dt = globalClock.getDt()
        h = self.arm.getH() - xd * self.rotateSpeed * dt
        limitH = max(min(h, self.armMaxH), self.armMinH)
        self.arm.setH(limitH)
        y = self.crane.getY() + yd * self.slideSpeed * dt
        limitY = max(min(y, self.craneMaxY), self.craneMinY)
        atLimit = (limitH != h or limitY != y)
        if atLimit:
            now = globalClock.getFrameTime()
            x = math.sin(now * 79) * 0.05
            z = math.sin(now * 70) * 0.02
            self.crane.setPos(x, limitY, z)
            self.__setMoveSound(self.atLimitSfx)
        else:
            self.crane.setPos(0, limitY, 0)
            self.__setMoveSound(self.craneMoveSfx)

    def __setMoveSound(self, sfx):
        if sfx != self.moveSound:
            if self.moveSound:
                self.moveSound.stop()
            self.moveSound = sfx
            if self.moveSound:
                base.playSfx(self.moveSound, looping=1, volume=0.5)

    def __activateSniffer(self):
        if not self.snifferActivated:
            self.sniffer.unstash()
            base.cTrav.addCollider(self.sniffer, self.snifferHandler)
            self.accept(self.snifferEvent, self.__sniffedSomething)
            self.startFlicker()
            self.snifferActivated = 1

    def __deactivateSniffer(self):
        if self.snifferActivated:
            base.cTrav.removeCollider(self.sniffer)
            self.sniffer.stash()
            self.ignore(self.snifferEvent)
            self.stopFlicker()
            self.snifferActivated = 0

    def startFlicker(self):
        self.magnetSoundInterval.start()
        self.lightning = []
        for i in xrange(4):
            t = float(i) / 3.0 - 0.5
            l = self.craneGame.lightning.copyTo(self.gripper)
            l.setScale(random.choice([1, -1]), 1, 5)
            l.setZ(random.uniform(-5, -5.5))
            l.flattenLight()
            l.setTwoSided(1)
            l.setBillboardAxis()
            l.setScale(random.uniform(0.5, 1.0))
            if t < 0:
                l.setX(t - 0.7)
            else:
                l.setX(t + 0.7)
            l.setR(-20 * t)
            l.setP(random.uniform(-20, 20))
            self.lightning.append(l)

        taskMgr.add(self.__flickerLightning, self.flickerName)

    def stopFlicker(self):
        self.magnetSoundInterval.finish()
        self.magnetLoopSfx.stop()
        taskMgr.remove(self.flickerName)
        for l in self.lightning:
            l.detachNode()

        self.lightning = None
        return

    def __flickerLightning(self, task):
        for l in self.lightning:
            if random.random() < 0.5:
                l.hide()
            else:
                l.show()

        return Task.cont

    def __sniffedSomething(self, entry):
        np = entry.getIntoNodePath()
        doId = int(np.getNetTag('object'))
        obj = base.cr.doId2do.get(doId)
        if obj and obj.state != 'LocalDropped' and (obj.state != 'Dropped' or obj.craneId != self.doId):
            obj.d_requestGrab()
            obj.demand('LocalGrabbed', localAvatar.doId, self.doId)

    def grabObject(self, obj):
        if self.state == 'Off':
            return
        if self.heldObject != None:
            self.releaseObject()
        self.__deactivateSniffer()
        obj.wrtReparentTo(self.gripper)
        if obj.lerpInterval:
            obj.lerpInterval.finish()
        obj.lerpInterval = Parallel(obj.posInterval(ToontownGlobals.CashbotBossToMagnetTime, Point3(*obj.grabPos)), obj.quatInterval(ToontownGlobals.CashbotBossToMagnetTime, VBase3(obj.getH(), 0, 0)), obj.toMagnetSoundInterval)
        obj.lerpInterval.start()
        self.heldObject = obj
        self.handler.setDynamicFrictionCoef(obj.craneFrictionCoef)
        self.slideSpeed = obj.craneSlideSpeed
        self.rotateSpeed = obj.craneRotateSpeed
        if self.avId == localAvatar.doId and not self.magnetOn:
            self.releaseObject()
        return

    def dropObject(self, obj):
        if obj.lerpInterval:
            obj.lerpInterval.finish()
        obj.wrtReparentTo(render)
        obj.lerpInterval = Parallel(obj.quatInterval(ToontownGlobals.CashbotBossFromMagnetTime, VBase3(obj.getH(), 0, 0), blendType='easeOut'))
        obj.lerpInterval.start()
        p1 = self.bottomLink.node().getPhysicsObject()
        v = render.getRelativeVector(self.bottomLink, p1.getVelocity())
        obj.physicsObject.setVelocity(v * 1.5)
        if self.heldObject == obj:
            self.heldObject = None
            self.handler.setDynamicFrictionCoef(GameConsts.Settings.EmptyFrictionCoef.get())
            self.slideSpeed = self.emptySlideSpeed
            self.rotateSpeed = self.emptyRotateSpeed
        return

    def releaseObject(self):
        if self.heldObject:
            obj = self.heldObject
            obj.d_requestDrop()
            if obj.state == 'Grabbed':
                obj.demand('LocalDropped', localAvatar.doId, self.doId)

    def __hitTrigger(self, event):
        pass

    def setCraneGameId(self, craneGameId):
        self.craneGameId = craneGameId
        self.craneGame = base.cr.doId2do[craneGameId]

    def setIndex(self, index):
        self.index = index

    def setState(self, state, avId):
        if state == 'C':
            self.demand('Controlled', avId)
        elif state == 'F':
            self.demand('Free')
        else:
            self.notify.error('Invalid state from AI: %s' % state)

    def d_requestControl(self):
        self.sendUpdate('requestControl')

    def d_requestFree(self):
        self.sendUpdate('requestFree')

    def b_clearSmoothing(self):
        self.d_clearSmoothing()
        self.clearSmoothing()

    def d_clearSmoothing(self):
        self.sendUpdate('clearSmoothing', [0])

    def clearSmoothing(self, bogus = None):
        self.armSmoother.clearPositions(1)
        for smoother in self.linkSmoothers:
            smoother.clearPositions(1)

    def reloadPosition(self):
        self.armSmoother.clearPositions(0)
        self.armSmoother.setPos(self.crane.getPos())
        self.armSmoother.setHpr(self.arm.getHpr())
        self.armSmoother.setPhonyTimestamp()
        for linkNum in xrange(self.numLinks):
            smoother = self.linkSmoothers[linkNum]
            an, anp, cnp = self.activeLinks[linkNum]
            smoother.clearPositions(0)
            smoother.setPos(anp.getPos())
            smoother.setPhonyTimestamp()

    def doSmoothTask(self, task):
        self.armSmoother.computeAndApplySmoothPosHpr(self.crane, self.arm)
        for linkNum in xrange(self.numLinks):
            smoother = self.linkSmoothers[linkNum]
            anp = self.activeLinks[linkNum][1]
            smoother.computeAndApplySmoothPos(anp)

        return Task.cont

    def startSmooth(self):
        if not self.smoothStarted:
            taskName = self.smoothName
            taskMgr.remove(taskName)
            self.reloadPosition()
            taskMgr.add(self.doSmoothTask, taskName)
            self.smoothStarted = 1

    def stopSmooth(self):
        if self.smoothStarted:
            taskName = self.smoothName
            taskMgr.remove(taskName)
            self.forceToTruePosition()
            self.smoothStarted = 0

    def forceToTruePosition(self):
        if self.armSmoother.getLatestPosition():
            self.armSmoother.applySmoothPos(self.crane)
            self.armSmoother.applySmoothHpr(self.arm)
        self.armSmoother.clearPositions(1)
        for linkNum in xrange(self.numLinks):
            smoother = self.linkSmoothers[linkNum]
            an, anp, cnp = self.activeLinks[linkNum]
            if smoother.getLatestPosition():
                smoother.applySmoothPos(anp)
            smoother.clearPositions(1)

    def setCablePos(self, changeSeq, y, h, links, timestamp):
        self.changeSeq = changeSeq
        if self.smoothStarted:
            now = globalClock.getFrameTime()
            local = globalClockDelta.networkToLocalTime(timestamp, now)
            self.armSmoother.setY(y)
            self.armSmoother.setH(h)
            self.armSmoother.setTimestamp(local)
            self.armSmoother.markPosition()
            for linkNum in xrange(self.numLinks):
                smoother = self.linkSmoothers[linkNum]
                lp = links[linkNum]
                smoother.setPos(*lp)
                smoother.setTimestamp(local)
                smoother.markPosition()

        else:
            self.crane.setY(y)
            self.arm.setH(h)

    def d_sendCablePos(self):
        timestamp = globalClockDelta.getFrameNetworkTime()
        links = []
        for linkNum in xrange(self.numLinks):
            an, anp, cnp = self.activeLinks[linkNum]
            p = anp.getPos()
            links.append((p[0], p[1], p[2]))

        self.sendUpdate('setCablePos', [self.changeSeq,
         self.crane.getY(),
         self.arm.getH(),
         links,
         timestamp])

    def stopPosHprBroadcast(self):
        taskName = self.posHprBroadcastName
        taskMgr.remove(taskName)

    def startPosHprBroadcast(self):
        taskName = self.posHprBroadcastName
        self.b_clearSmoothing()
        self.d_sendCablePos()
        taskMgr.remove(taskName)
        taskMgr.doMethodLater(self.__broadcastPeriod, self.__posHprBroadcast, taskName)

    def __posHprBroadcast(self, task):
        self.d_sendCablePos()
        taskName = self.posHprBroadcastName
        taskMgr.doMethodLater(self.__broadcastPeriod, self.__posHprBroadcast, taskName)
        return Task.done

    def enterOff(self):
        self.clearCable()
        self.root.detachNode()

    def exitOff(self):
        if self.craneGame:
            self.setupCable()
        self.root.reparentTo(render)

    def enterControlled(self, avId):
        self.avId = avId
        toon = base.cr.doId2do.get(avId)
        if not toon:
            return
        self.grabTrack = self.makeToonGrabInterval(toon)
        if avId == localAvatar.doId:
            self.craneGame.toCraneMode()
            camera.reparentTo(self.hinge)
            camera.setPosHpr(0, -20, -5, 0, -20, 0)
            self.tube.stash()
            localAvatar.setPosHpr(self.controls, 0, 0, 0, 0, 0, 0)
            localAvatar.sendCurrentPosition()
            self.__activatePhysics()
            self.__enableControlInterface()
            self.startPosHprBroadcast()
            self.startShadow()
        else:
            self.startSmooth()
            toon.stopSmooth()
            self.grabTrack = Sequence(self.grabTrack, Func(toon.startSmooth))
        self.grabTrack.start()

    def exitControlled(self):
        self.grabTrack.finish()
        del self.grabTrack
        if self.toon and not self.toon.isDisabled():
            self.toon.loop('neutral')
            self.toon.startSmooth()
        self.stopWatchJoystick()
        self.stopPosHprBroadcast()
        self.stopShadow()
        self.stopSmooth()
        if self.avId == localAvatar.doId:
            self.__disableControlInterface()
            self.__deactivatePhysics()
            self.tube.unstash()
            camera.reparentTo(base.localAvatar)
            camera.setPos(base.localAvatar.cameraPositions[0][0])
            camera.setHpr(0, 0, 0)
        self.__straightenCable()

    def enterFree(self):
        if self.fadeTrack:
            self.fadeTrack.finish()
            self.fadeTrack = None
        self.restoreScaleTrack = Sequence(Wait(6), self.getRestoreScaleInterval())
        self.restoreScaleTrack.start()
        if self.avId == localAvatar.doId:
            self.controlModel.setAlphaScale(0.3)
            self.controlModel.setTransparency(1)
            taskMgr.doMethodLater(5, self.__allowDetect, self.triggerName)
            self.fadeTrack = Sequence(Func(self.controlModel.setTransparency, 1), self.controlModel.colorScaleInterval(0.2, VBase4(1, 1, 1, 0.3)))
            self.fadeTrack.start()
        else:
            self.trigger.unstash()
            self.accept(self.triggerEvent, self.__hitTrigger)
        self.avId = 0
        return

    def __allowDetect(self, task):
        if self.fadeTrack:
            self.fadeTrack.finish()
        self.fadeTrack = Sequence(self.controlModel.colorScaleInterval(0.2, VBase4(1, 1, 1, 1)), Func(self.controlModel.clearColorScale), Func(self.controlModel.clearTransparency))
        self.fadeTrack.start()
        self.trigger.unstash()
        self.accept(self.triggerEvent, self.__hitTrigger)

    def exitFree(self):
        if self.fadeTrack:
            self.fadeTrack.finish()
            self.fadeTrack = None
        self.restoreScaleTrack.pause()
        del self.restoreScaleTrack
        taskMgr.remove(self.triggerName)
        self.controlModel.clearColorScale()
        self.controlModel.clearTransparency()
        self.trigger.stash()
        self.ignore(self.triggerEvent)
        return

    def enterMovie(self):
        self.__activatePhysics()

    def exitMovie(self):
        self.__deactivatePhysics()
        self.__straightenCable()