Esempio n. 1
0
 def __init__(self, cr):
     DistributedObject.DistributedObject.__init__(self, cr)
     FSM.FSM.__init__(self, 'DistributedGolfSpot')
     self.boss = None
     self.index = None
     self.avId = 0
     self.toon = None
     self.golfSpotSmoother = SmoothMover()
     self.golfSpotSmoother.setSmoothMode(SmoothMover.SMOn)
     self.smoothStarted = 0
     self.__broadcastPeriod = 0.2
     if self.index > len(self.positions):
         self.notify.error('Invalid index %d' % index)
     self.fadeTrack = None
     self.setupPowerBar()
     self.aimStart = None
     self.golfSpotAdviceLabel = None
     self.changeSeq = 0
     self.lastChangeSeq = 0
     self.controlKeyAllowed = False
     self.flyBallTracks = {}
     self.splatTracks = {}
     self.__flyBallBubble = None
     self.flyBallHandler = None
     self.__flyBallSequenceNum = 0
     self.swingInterval = None
     self.lastHitSequenceNum = -1
     self.goingToReward = False
     self.gotHitByBoss = False
     self.releaseTrack = None
     self.grabTrack = None
     self.restoreScaleTrack = None
     return
 def __init__(self, cr):
     DistributedObject.DistributedObject.__init__(self, cr)
     FSM.FSM.__init__(self, 'DistributedGolfSpot')
     self.boss = None
     self.index = None
     self.avId = 0
     self.toon = None
     self.golfSpotSmoother = SmoothMover()
     self.golfSpotSmoother.setSmoothMode(SmoothMover.SMOn)
     self.smoothStarted = 0
     self.__broadcastPeriod = 0.2
     if self.index > len(self.positions):
         self.notify.error('Invalid index %d' % index)
     self.fadeTrack = None
     self.setupPowerBar()
     self.aimStart = None
     self.golfSpotAdviceLabel = None
     self.changeSeq = 0
     self.lastChangeSeq = 0
     self.controlKeyAllowed = False
     self.flyBallTracks = {}
     self.splatTracks = {}
     self.__flyBallBubble = None
     self.flyBallHandler = None
     self.__flyBallSequenceNum = 0
     self.swingInterval = None
     self.lastHitSequenceNum = -1
     self.goingToReward = False
     self.gotHitByBoss = False
     self.releaseTrack = None
     self.grabTrack = None
     self.restoreScaleTrack = None
     return
 def __init__(self, cr):
     DistributedObject.DistributedObject.__init__(self, cr)
     FSM.FSM.__init__(self, 'DistributedBanquetTable')
     self.boss = None
     self.index = -1
     self.diners = {}
     self.dinerStatus = {}
     self.serviceLocs = {}
     self.chairLocators = {}
     self.sitLocators = {}
     self.activeIntervals = {}
     self.dinerStatusIndicators = {}
     self.preparedForPhaseFour = False
     self.avId = 0
     self.toon = None
     self.pitcherSmoother = SmoothMover()
     self.pitcherSmoother.setSmoothMode(SmoothMover.SMOn)
     self.smoothStarted = 0
     self.__broadcastPeriod = 0.2
     self.changeSeq = 0
     self.lastChangeSeq = 0
     self.pitcherAdviceLabel = None
     self.fireLength = 250
     self.fireTrack = None
     self.hitObject = None
     self.setupPowerBar()
     self.aimStart = None
     self.toonPitcherPosition = Point3(0, -2, 0)
     self.allowLocalRequestControl = True
     self.fadeTrack = None
     self.grabTrack = None
     self.gotHitByBoss = False
     self.keyTTL = []
     self.keyRate = 0
     self.buttons = [0, 1]
     self.lastPowerFired = 0
     self.moveSound = None
     self.releaseTrack = None
     return
 def __init__(self, cr):
     DistributedObject.DistributedObject.__init__(self, cr)
     FSM.FSM.__init__(self, 'DistributedBanquetTable')
     self.boss = None
     self.index = -1
     self.diners = {}
     self.dinerStatus = {}
     self.serviceLocs = {}
     self.chairLocators = {}
     self.sitLocators = {}
     self.activeIntervals = {}
     self.dinerStatusIndicators = {}
     self.preparedForPhaseFour = False
     self.avId = 0
     self.toon = None
     self.pitcherSmoother = SmoothMover()
     self.pitcherSmoother.setSmoothMode(SmoothMover.SMOn)
     self.smoothStarted = 0
     self.__broadcastPeriod = 0.2
     self.changeSeq = 0
     self.lastChangeSeq = 0
     self.pitcherAdviceLabel = None
     self.fireLength = 250
     self.fireTrack = None
     self.hitObject = None
     self.setupPowerBar()
     self.aimStart = None
     self.toonPitcherPosition = Point3(0, -2, 0)
     self.allowLocalRequestControl = True
     self.fadeTrack = None
     self.grabTrack = None
     self.gotHitByBoss = False
     self.keyTTL = []
     self.keyRate = 0
     self.buttons = [0, 1]
     self.lastPowerFired = 0
     self.moveSound = None
     self.releaseTrack = None
     return
class DistributedBanquetTable(DistributedObject.DistributedObject, FSM.FSM, BanquetTableBase.BanquetTableBase):
    notify = DirectNotifyGlobal.directNotify.newCategory('DistributedBanquetTable')
    rotationsPerSeatIndex = [90,
     90,
     0,
     0,
     -90,
     -90,
     180,
     180]
    pitcherMinH = -360
    pitcherMaxH = 360
    rotateSpeed = 30
    waterPowerSpeed = base.config.GetDouble('water-power-speed', 15)
    waterPowerExponent = base.config.GetDouble('water-power-exponent', 0.75)
    useNewAnimations = True
    TugOfWarControls = False
    OnlyUpArrow = True
    if OnlyUpArrow:
        BASELINE_KEY_RATE = 3
    else:
        BASELINE_KEY_RATE = 6
    UPDATE_KEY_PRESS_RATE_TASK = 'BanquetTableUpdateKeyPressRateTask'
    YELLOW_POWER_THRESHOLD = 0.75
    RED_POWER_THRESHOLD = 0.97

    def __init__(self, cr):
        DistributedObject.DistributedObject.__init__(self, cr)
        FSM.FSM.__init__(self, 'DistributedBanquetTable')
        self.boss = None
        self.index = -1
        self.diners = {}
        self.dinerStatus = {}
        self.serviceLocs = {}
        self.chairLocators = {}
        self.sitLocators = {}
        self.activeIntervals = {}
        self.dinerStatusIndicators = {}
        self.preparedForPhaseFour = False
        self.avId = 0
        self.toon = None
        self.pitcherSmoother = SmoothMover()
        self.pitcherSmoother.setSmoothMode(SmoothMover.SMOn)
        self.smoothStarted = 0
        self.__broadcastPeriod = 0.2
        self.changeSeq = 0
        self.lastChangeSeq = 0
        self.pitcherAdviceLabel = None
        self.fireLength = 250
        self.fireTrack = None
        self.hitObject = None
        self.setupPowerBar()
        self.aimStart = None
        self.toonPitcherPosition = Point3(0, -2, 0)
        self.allowLocalRequestControl = True
        self.fadeTrack = None
        self.grabTrack = None
        self.gotHitByBoss = False
        self.keyTTL = []
        self.keyRate = 0
        self.buttons = [0, 1]
        self.lastPowerFired = 0
        self.moveSound = None
        self.releaseTrack = None
        return

    def disable(self):
        DistributedObject.DistributedObject.disable(self)
        taskMgr.remove(self.triggerName)
        taskMgr.remove(self.smoothName)
        taskMgr.remove(self.watchControlsName)
        taskMgr.remove(self.pitcherAdviceName)
        taskMgr.remove(self.posHprBroadcastName)
        taskMgr.remove(self.waterPowerTaskName)
        if self.releaseTrack:
            self.releaseTrack.finish()
            self.releaseTrack = None
        if self.fireTrack:
            self.fireTrack.finish()
            self.fireTrack = None
        self.cleanupIntervals()
        return

    def delete(self):
        DistributedObject.DistributedObject.delete(self)
        self.boss = None
        self.ignoreAll()
        for indicator in list(self.dinerStatusIndicators.values()):
            indicator.delete()

        self.dinerStatusIndicators = {}
        for diner in list(self.diners.values()):
            diner.delete()

        self.diners = {}
        self.powerBar.destroy()
        self.powerBar = None
        self.pitcherMoveSfx.stop()
        return

    def announceGenerate(self):
        DistributedObject.DistributedObject.announceGenerate(self)
        self.loadAssets()
        self.smoothName = self.uniqueName('pitcherSmooth')
        self.pitcherAdviceName = self.uniqueName('pitcherAdvice')
        self.posHprBroadcastName = self.uniqueName('pitcherBroadcast')
        self.waterPowerTaskName = self.uniqueName('updateWaterPower')
        self.triggerName = self.uniqueName('trigger')
        self.watchControlsName = self.uniqueName('watchControls')

    def setBossCogId(self, bossCogId):
        self.bossCogId = bossCogId
        self.boss = base.cr.doId2do[bossCogId]
        self.boss.setTable(self, self.index)

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

    def setState(self, state, avId, extraInfo):
        self.gotHitByBoss = extraInfo
        if state == 'F':
            self.demand('Off')
        elif state == 'N':
            self.demand('On')
        elif state == 'I':
            self.demand('Inactive')
        elif state == 'R':
            self.demand('Free')
        elif state == 'C':
            self.demand('Controlled', avId)
        elif state == 'L':
            self.demand('Flat', avId)
        else:
            self.notify.error('Invalid state from AI: %s' % state)

    def setNumDiners(self, numDiners):
        self.numDiners = numDiners

    def setDinerInfo(self, hungryDurations, eatingDurations, dinerLevels):
        self.dinerInfo = {}
        for i in range(len(hungryDurations)):
            hungryDur = hungryDurations[i]
            eatingDur = eatingDurations[i]
            dinerLevel = dinerLevels[i]
            self.dinerInfo[i] = (hungryDur, eatingDur, dinerLevel)

    def loadAssets(self):
        self.tableGroup = loader.loadModel('phase_12/models/bossbotHQ/BanquetTableChairs')
        tableLocator = self.boss.geom.find('**/TableLocator_%d' % (self.index + 1))
        if tableLocator.isEmpty():
            self.tableGroup.reparentTo(render)
            self.tableGroup.setPos(0, 75, 0)
        else:
            self.tableGroup.reparentTo(tableLocator)
        self.tableGeom = self.tableGroup.find('**/Geometry')
        self.setupDiners()
        self.setupChairCols()
        self.squirtSfx = loader.loadSfx('phase_4/audio/sfx/AA_squirt_seltzer_miss.ogg')
        self.hitBossSfx = loader.loadSfx('phase_5/audio/sfx/SA_watercooler_spray_only.ogg')
        self.hitBossSoundInterval = SoundInterval(self.hitBossSfx, node=self.boss, volume=1.0)
        self.serveFoodSfx = loader.loadSfx('phase_4/audio/sfx/MG_sfx_travel_game_bell_for_trolley.ogg')
        self.pitcherMoveSfx = base.loader.loadSfx('phase_4/audio/sfx/MG_cannon_adjust.ogg')

    def setupDiners(self):
        for i in range(self.numDiners):
            newDiner = self.createDiner(i)
            self.diners[i] = newDiner
            self.dinerStatus[i] = self.HUNGRY

    def createDiner(self, i):
        diner = Suit.Suit()
        diner.dna = SuitDNA.SuitDNA()
        level = self.dinerInfo[i][2]
        level -= 4
        diner.dna.newSuitRandom(level=level, dept='c')
        diner.setDNA(diner.dna)
        diner.nametag.setNametag2d(None)
        diner.nametag.setNametag3d(None)
        if self.useNewAnimations:
            diner.loop('sit', fromFrame=i)
        else:
            diner.pose('landing', 0)
        locator = self.tableGroup.find('**/chair_%d' % (i + 1))
        locatorScale = locator.getNetTransform().getScale()[0]
        correctHeadingNp = locator.attachNewNode('correctHeading')
        self.chairLocators[i] = correctHeadingNp
        heading = self.rotationsPerSeatIndex[i]
        correctHeadingNp.setH(heading)
        sitLocator = correctHeadingNp.attachNewNode('sitLocator')
        base.sitLocator = sitLocator
        pos = correctHeadingNp.getPos(render)
        if SuitDNA.getSuitBodyType(diner.dna.name) == 'c':
            sitLocator.setPos(0.5, 3.65, -3.75)
        else:
            sitLocator.setZ(-2.4)
            sitLocator.setY(2.5)
            sitLocator.setX(0.5)
        self.sitLocators[i] = sitLocator
        diner.setScale(1.0 / locatorScale)
        diner.reparentTo(sitLocator)
        newLoc = NodePath('serviceLoc-%d-%d' % (self.index, i))
        newLoc.reparentTo(correctHeadingNp)
        newLoc.setPos(0, 3.0, 1)
        self.serviceLocs[i] = newLoc
        base.serviceLoc = newLoc
        head = diner.find('**/joint_head')
        newIndicator = DinerStatusIndicator.DinerStatusIndicator(parent=head, pos=Point3(0, 0, 3.5), scale=5.0)
        newIndicator.wrtReparentTo(diner)
        self.dinerStatusIndicators[i] = newIndicator
        return diner

    def setupChairCols(self):
        for i in range(self.numDiners):
            chairCol = self.tableGroup.find('**/collision_chair_%d' % (i + 1))
            colName = 'ChairCol-%d-%d' % (self.index, i)
            chairCol.setTag('chairIndex', str(i))
            chairCol.setName(colName)
            chairCol.setCollideMask(ToontownGlobals.WallBitmask)
            self.accept('enter' + colName, self.touchedChair)

    def touchedChair(self, colEntry):
        chairIndex = int(colEntry.getIntoNodePath().getTag('chairIndex'))
        if chairIndex in self.dinerStatus:
            status = self.dinerStatus[chairIndex]
            if status in (self.HUNGRY, self.ANGRY):
                self.boss.localToonTouchedChair(self.index, chairIndex)

    def serveFood(self, food, chairIndex):
        self.removeFoodModel(chairIndex)
        serviceLoc = self.serviceLocs.get(chairIndex)
        if not food or food.isEmpty():
            foodModel = loader.loadModel('phase_12/models/bossbotHQ/canoffood')
            foodModel.setScale(ToontownGlobals.BossbotFoodModelScale)
            foodModel.reparentTo(serviceLoc)
        else:
            food.wrtReparentTo(serviceLoc)
            tray = food.find('**/tray')
            if not tray.isEmpty():
                tray.hide()
            ivalDuration = 1.5
            foodMoveIval = Parallel(SoundInterval(self.serveFoodSfx, node=food), ProjectileInterval(food, duration=ivalDuration, startPos=food.getPos(serviceLoc), endPos=serviceLoc.getPos(serviceLoc)), LerpHprInterval(food, ivalDuration, Point3(0, -360, 0)))
            intervalName = 'serveFood-%d-%d' % (self.index, chairIndex)
            foodMoveIval.start()
            self.activeIntervals[intervalName] = foodMoveIval

    def setDinerStatus(self, chairIndex, status):
        if chairIndex in self.dinerStatus:
            oldStatus = self.dinerStatus[chairIndex]
            self.dinerStatus[chairIndex] = status
            if oldStatus != status:
                if status == self.EATING:
                    self.changeDinerToEating(chairIndex)
                elif status == self.HUNGRY:
                    self.changeDinerToHungry(chairIndex)
                elif status == self.ANGRY:
                    self.changeDinerToAngry(chairIndex)
                elif status == self.DEAD:
                    self.changeDinerToDead(chairIndex)
                elif status == self.HIDDEN:
                    self.changeDinerToHidden(chairIndex)

    def removeFoodModel(self, chairIndex):
        serviceLoc = self.serviceLocs.get(chairIndex)
        if serviceLoc:
            for i in range(serviceLoc.getNumChildren()):
                serviceLoc.getChild(0).removeNode()

    def changeDinerToEating(self, chairIndex):
        indicator = self.dinerStatusIndicators.get(chairIndex)
        eatingDuration = self.dinerInfo[chairIndex][1]
        if indicator:
            indicator.request('Eating', eatingDuration)
        diner = self.diners[chairIndex]
        intervalName = 'eating-%d-%d' % (self.index, chairIndex)
        eatInTime = 32.0 / 24.0
        eatOutTime = 21.0 / 24.0
        eatLoopTime = 19 / 24.0
        rightHand = diner.getRightHand()
        waitTime = 5
        loopDuration = eatingDuration - eatInTime - eatOutTime - waitTime
        serviceLoc = self.serviceLocs[chairIndex]

        def foodAttach(self = self, diner = diner):
            if self.serviceLocs[chairIndex].getNumChildren() < 1:
                return
            foodModel = self.serviceLocs[chairIndex].getChild(0)
            (foodModel.reparentTo(diner.getRightHand()),)
            (foodModel.setHpr(Point3(0, -94, 0)),)
            (foodModel.setPos(Point3(-0.15, -0.7, -0.4)),)
            scaleAdj = 1
            if SuitDNA.getSuitBodyType(diner.dna.name) == 'c':
                scaleAdj = 0.6
                (foodModel.setPos(Point3(0.1, -0.25, -0.31)),)
            else:
                scaleAdj = 0.8
                (foodModel.setPos(Point3(-0.25, -0.85, -0.34)),)
            oldScale = foodModel.getScale()
            newScale = oldScale * scaleAdj
            foodModel.setScale(newScale)

        def foodDetach(self = self, diner = diner):
            if diner.getRightHand().getNumChildren() < 1:
                return
            foodModel = diner.getRightHand().getChild(0)
            (foodModel.reparentTo(serviceLoc),)
            (foodModel.setPosHpr(0, 0, 0, 0, 0, 0),)
            scaleAdj = 1
            if SuitDNA.getSuitBodyType(diner.dna.name) == 'c':
                scaleAdj = 0.6
            else:
                scakeAdj = 0.8
            oldScale = foodModel.getScale()
            newScale = oldScale / scaleAdj
            foodModel.setScale(newScale)

        eatIval = Sequence(ActorInterval(diner, 'sit', duration=waitTime), ActorInterval(diner, 'sit-eat-in', startFrame=0, endFrame=6), Func(foodAttach), ActorInterval(diner, 'sit-eat-in', startFrame=6, endFrame=32), ActorInterval(diner, 'sit-eat-loop', duration=loopDuration, loop=1), ActorInterval(diner, 'sit-eat-out', startFrame=0, endFrame=12), Func(foodDetach), ActorInterval(diner, 'sit-eat-out', startFrame=12, endFrame=21))
        eatIval.start()
        self.activeIntervals[intervalName] = eatIval

    def changeDinerToHungry(self, chairIndex):
        intervalName = 'eating-%d-%d' % (self.index, chairIndex)
        if intervalName in self.activeIntervals:
            self.activeIntervals[intervalName].finish()
        self.removeFoodModel(chairIndex)
        indicator = self.dinerStatusIndicators.get(chairIndex)
        if indicator:
            indicator.request('Hungry', self.dinerInfo[chairIndex][0])
        diner = self.diners[chairIndex]
        if random.choice([0, 1]):
            diner.loop('sit-hungry-left')
        else:
            diner.loop('sit-hungry-right')

    def changeDinerToAngry(self, chairIndex):
        self.removeFoodModel(chairIndex)
        indicator = self.dinerStatusIndicators.get(chairIndex)
        if indicator:
            indicator.request('Angry')
        diner = self.diners[chairIndex]
        diner.loop('sit-angry')

    def changeDinerToDead(self, chairIndex):

        def removeDeathSuit(suit, deathSuit):
            if not deathSuit.isEmpty():
                deathSuit.detachNode()
                suit.cleanupLoseActor()

        self.removeFoodModel(chairIndex)
        indicator = self.dinerStatusIndicators.get(chairIndex)
        if indicator:
            indicator.request('Dead')
        diner = self.diners[chairIndex]
        deathSuit = diner
        locator = self.tableGroup.find('**/chair_%d' % (chairIndex + 1))
        deathSuit = diner.getLoseActor()
        ival = Sequence(Func(self.notify.debug, 'before actorinterval sit-lose'), ActorInterval(diner, 'sit-lose'), Func(self.notify.debug, 'before deathSuit.setHpr'), Func(deathSuit.setHpr, diner.getHpr()), Func(self.notify.debug, 'before diner.hide'), Func(diner.hide), Func(self.notify.debug, 'before deathSuit.reparentTo'), Func(deathSuit.reparentTo, self.chairLocators[chairIndex]), Func(self.notify.debug, 'befor ActorInterval lose'), ActorInterval(deathSuit, 'lose', duration=MovieUtil.SUIT_LOSE_DURATION), Func(self.notify.debug, 'before remove deathsuit'), Func(removeDeathSuit, diner, deathSuit, name='remove-death-suit-%d-%d' % (chairIndex, self.index)), Func(self.notify.debug, 'diner.stash'), Func(diner.stash))
        spinningSound = base.loader.loadSfx('phase_3.5/audio/sfx/Cog_Death.ogg')
        deathSound = base.loader.loadSfx('phase_3.5/audio/sfx/ENC_cogfall_apart.ogg')
        deathSoundTrack = Sequence(Wait(0.8), SoundInterval(spinningSound, duration=1.2, startTime=1.5, volume=0.2, node=deathSuit), SoundInterval(spinningSound, duration=3.0, startTime=0.6, volume=0.8, node=deathSuit), SoundInterval(deathSound, volume=0.32, node=deathSuit))
        intervalName = 'dinerDie-%d-%d' % (self.index, chairIndex)
        deathIval = Parallel(ival, deathSoundTrack)
        deathIval.start()
        self.activeIntervals[intervalName] = deathIval

    def changeDinerToHidden(self, chairIndex):
        self.removeFoodModel(chairIndex)
        indicator = self.dinerStatusIndicators.get(chairIndex)
        if indicator:
            indicator.request('Inactive')
        diner = self.diners[chairIndex]
        diner.hide()

    def setAllDinersToSitNeutral(self):
        startFrame = 0
        for diner in list(self.diners.values()):
            if not diner.isHidden():
                diner.loop('sit', fromFrame=startFrame)
                startFrame += 1

    def cleanupIntervals(self):
        for interval in list(self.activeIntervals.values()):
            interval.finish()

        self.activeIntervals = {}

    def clearInterval(self, name, finish = 1):
        if name in self.activeIntervals:
            ival = self.activeIntervals[name]
            if finish:
                ival.finish()
            else:
                ival.pause()
            if name in self.activeIntervals:
                del self.activeIntervals[name]
        else:
            self.notify.debug('interval: %s already cleared' % name)

    def finishInterval(self, name):
        if name in self.activeIntervals:
            interval = self.activeIntervals[name]
            interval.finish()

    def getNotDeadInfo(self):
        notDeadList = []
        for i in range(self.numDiners):
            if self.dinerStatus[i] != self.DEAD:
                notDeadList.append((self.index, i, 12))

        return notDeadList

    def enterOn(self):
        pass

    def exitOn(self):
        pass

    def enterInactive(self):
        for chairIndex in range(self.numDiners):
            indicator = self.dinerStatusIndicators.get(chairIndex)
            if indicator:
                indicator.request('Inactive')
            self.removeFoodModel(chairIndex)

    def exitInactive(self):
        pass

    def enterFree(self):
        self.resetPowerBar()
        if self.fadeTrack:
            self.fadeTrack.finish()
            self.fadeTrack = None
        self.prepareForPhaseFour()
        if self.avId == localAvatar.doId:
            self.tableGroup.setAlphaScale(0.3)
            self.tableGroup.setTransparency(1)
            taskMgr.doMethodLater(5, self.__allowDetect, self.triggerName)
            self.fadeTrack = Sequence(Func(self.tableGroup.setTransparency, 1), self.tableGroup.colorScaleInterval(0.2, VBase4(1, 1, 1, 0.3)))
            self.fadeTrack.start()
            self.allowLocalRequestControl = False
        else:
            self.allowLocalRequestControl = True
        self.avId = 0
        return

    def exitFree(self):
        pass

    def touchedTable(self, colEntry):
        tableIndex = int(colEntry.getIntoNodePath().getTag('tableIndex'))
        if self.state == 'Free' and self.avId == 0 and self.allowLocalRequestControl:
            self.d_requestControl()

    def prepareForPhaseFour(self):
        if not self.preparedForPhaseFour:
            for i in range(8):
                chair = self.tableGroup.find('**/chair_%d' % (i + 1))
                if not chair.isEmpty():
                    chair.hide()
                colChairs = self.tableGroup.findAllMatches('**/ChairCol*')
                for i in range(colChairs.getNumPaths()):
                    col = colChairs.getPath(i)
                    col.stash()

                colChairs = self.tableGroup.findAllMatches('**/collision_chair*')
                for i in range(colChairs.getNumPaths()):
                    col = colChairs.getPath(i)
                    col.stash()

            tableCol = self.tableGroup.find('**/collision_table')
            colName = 'TableCol-%d' % self.index
            tableCol.setTag('tableIndex', str(self.index))
            tableCol.setName(colName)
            tableCol.setCollideMask(ToontownGlobals.WallBitmask | ToontownGlobals.BanquetTableBitmask)
            self.accept('enter' + colName, self.touchedTable)
            self.preparedForPhaseFour = True
            self.waterPitcherModel = loader.loadModel('phase_12/models/bossbotHQ/tt_m_ara_bhq_seltzerBottle')
            lampNode = self.tableGroup.find('**/lamp_med_5')
            pos = lampNode.getPos(self.tableGroup)
            lampNode.hide()
            bottleLocator = self.tableGroup.find('**/bottle_locator')
            pos = bottleLocator.getPos(self.tableGroup)
            self.waterPitcherNode = self.tableGroup.attachNewNode('pitcherNode')
            self.waterPitcherNode.setPos(pos)
            self.waterPitcherModel.reparentTo(self.waterPitcherNode)
            self.nozzle = self.waterPitcherModel.find('**/nozzle_tip')
            self.handLocator = self.waterPitcherModel.find('**/hand_locator')
            self.handPos = self.handLocator.getPos()

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

    def d_requestFree(self, gotHitByBoss):
        self.sendUpdate('requestFree', [gotHitByBoss])

    def enterControlled(self, avId):
        self.prepareForPhaseFour()
        self.avId = avId
        toon = base.cr.doId2do.get(avId)
        if not toon:
            return
        self.toon = toon
        self.grabTrack = self.makeToonGrabInterval(toon)
        self.notify.debug('grabTrack=%s' % self.grabTrack)
        self.pitcherCamPos = Point3(0, -50, 40)
        self.pitcherCamHpr = Point3(0, -21, 0)
        if avId == localAvatar.doId:
            self.boss.toMovieMode()
            self.__enableControlInterface()
            self.startPosHprBroadcast()
            self.grabTrack = Sequence(self.grabTrack, Func(camera.wrtReparentTo, localAvatar), LerpPosHprInterval(camera, 1, self.pitcherCamPos, self.pitcherCamHpr), Func(self.boss.toCraneMode))
            if self.TugOfWarControls:
                self.__spawnUpdateKeyPressRateTask()
            self.accept('exitCrane', self.gotBossZapped)
        else:
            self.startSmooth()
            toon.stopSmooth()
        self.grabTrack.start()

    def exitControlled(self):
        self.ignore('exitCrane')
        if self.grabTrack:
            self.grabTrack.finish()
            self.grabTrack = None
        nextState = self.getCurrentOrNextState()
        self.notify.debug('nextState=%s' % nextState)
        if nextState == 'Flat':
            place = base.cr.playGame.getPlace()
            self.notify.debug('%s' % place.fsm)
            if self.avId == localAvatar.doId:
                self.__disableControlInterface()
        else:
            if self.toon and not self.toon.isDisabled():
                self.toon.loop('neutral')
                self.toon.startSmooth()
            self.releaseTrack = self.makeToonReleaseInterval(self.toon)
            self.stopPosHprBroadcast()
            self.stopSmooth()
            if self.avId == localAvatar.doId:
                localAvatar.wrtReparentTo(render)
                self.__disableControlInterface()
                camera.reparentTo(base.localAvatar)
                camera.setPos(base.localAvatar.cameraPositions[0][0])
                camera.setHpr(0, 0, 0)
                self.goToFinalBattle()
                self.safeBossToFinalBattleMode()
            else:
                toon = base.cr.doId2do.get(self.avId)
                if toon:
                    toon.wrtReparentTo(render)
            self.releaseTrack.start()
        return

    def safeBossToFinalBattleMode(self):
        if self.boss:
            self.boss.toFinalBattleMode()

    def goToFinalBattle(self):
        if self.cr:
            place = self.cr.playGame.getPlace()
            if place and hasattr(place, 'fsm'):
                if place.fsm.getCurrentState().getName() == 'crane':
                    place.setState('finalBattle')

    def makeToonGrabInterval(self, toon):
        toon.pose('leverNeutral', 0)
        toon.update()
        rightHandPos = toon.rightHand.getPos(toon)
        self.toonPitcherPosition = Point3(self.handPos[0] - rightHandPos[0], self.handPos[1] - rightHandPos[1], 0)
        destZScale = rightHandPos[2] / self.handPos[2]
        grabIval = Sequence(Func(toon.wrtReparentTo, self.waterPitcherNode), Func(toon.loop, 'neutral'), Parallel(ActorInterval(toon, 'jump'), Sequence(Wait(0.43), Parallel(ProjectileInterval(toon, duration=0.9, startPos=toon.getPos(self.waterPitcherNode), endPos=self.toonPitcherPosition), LerpHprInterval(toon, 0.9, Point3(0, 0, 0)), LerpScaleInterval(self.waterPitcherModel, 0.9, Point3(1, 1, destZScale))))), Func(toon.setPos, self.toonPitcherPosition), Func(toon.loop, 'leverNeutral'))
        return grabIval

    def makeToonReleaseInterval(self, toon):
        temp1 = self.waterPitcherNode.attachNewNode('temp1')
        temp1.setPos(self.toonPitcherPosition)
        temp2 = self.waterPitcherNode.attachNewNode('temp2')
        temp2.setPos(0, -10, -self.waterPitcherNode.getZ())
        startPos = temp1.getPos(render)
        endPos = temp2.getPos(render)
        temp1.removeNode()
        temp2.removeNode()

        def getSlideToPos(toon = toon):
            return render.getRelativePoint(toon, Point3(0, -10, 0))

        if self.gotHitByBoss:
            self.notify.debug('creating zap interval instead')
            grabIval = Sequence(Func(toon.loop, 'neutral'), Func(toon.wrtReparentTo, render), Parallel(ActorInterval(toon, 'slip-backward'), toon.posInterval(0.5, getSlideToPos, fluid=1)))
        else:
            grabIval = Sequence(Func(toon.loop, 'neutral'), Func(toon.wrtReparentTo, render), Parallel(ActorInterval(toon, 'jump'), Sequence(Wait(0.43), ProjectileInterval(toon, duration=0.9, startPos=startPos, endPos=endPos))))
        return grabIval

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

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

    def clearSmoothing(self, bogus = None):
        self.pitcherSmoother.clearPositions(1)

    def doSmoothTask(self, task):
        self.pitcherSmoother.computeAndApplySmoothHpr(self.waterPitcherNode)
        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 __enableControlInterface(self):
        gui = loader.loadModel('phase_3.5/models/gui/avatar_panel_gui')
        self.closeButton = DirectButton(image=(gui.find('**/CloseBtn_UP'),
         gui.find('**/CloseBtn_DN'),
         gui.find('**/CloseBtn_Rllvr'),
         gui.find('**/CloseBtn_UP')), relief=None, scale=2, text=TTLocalizer.BossbotPitcherLeave, text_scale=0.04, text_pos=(0, -0.07), text_fg=VBase4(1, 1, 1, 1), pos=(1.05, 0, -0.82), command=self.__exitPitcher)
        self.accept('escape', self.__exitPitcher)
        self.accept(base.JUMP, 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)
        self.accept(base.MOVE_UP, self.__upArrowKeyPressed)
        self.accept(base.MOVE_DOWN, self.__downArrowKeyPressed)
        taskMgr.add(self.__watchControls, self.watchControlsName)
        taskMgr.doMethodLater(5, self.__displayPitcherAdvice, self.pitcherAdviceName)
        self.arrowVert = 0
        self.arrowHorz = 0
        self.powerBar.show()
        return

    def __disableControlInterface(self):
        if self.closeButton:
            self.closeButton.destroy()
            self.closeButton = None
        self.__cleanupPitcherAdvice()
        self.ignore('escape')
        self.ignore(base.JUMP)
        self.ignore('control-up')
        self.ignore('InputState-forward')
        self.ignore('InputState-reverse')
        self.ignore('InputState-turnLeft')
        self.ignore('InputState-turnRight')
        self.ignore(base.MOVE_UP)
        self.ignore(base.MOVE_DOWN)
        self.arrowVert = 0
        self.arrowHorz = 0
        taskMgr.remove(self.watchControlsName)
        taskMgr.remove(self.waterPowerTaskName)
        self.resetPowerBar()
        self.aimStart = None
        self.powerBar.hide()
        if self.TugOfWarControls:
            self.__killUpdateKeyPressRateTask()
            self.keyTTL = []
        self.__setMoveSound(None)
        return

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

    def __cleanupPitcherAdvice(self):
        if self.pitcherAdviceLabel:
            self.pitcherAdviceLabel.destroy()
            self.pitcherAdviceLabel = None
        taskMgr.remove(self.pitcherAdviceName)
        return

    def showExiting(self):
        if self.closeButton:
            self.closeButton.destroy()
            self.closeButton = DirectLabel(relief=None, text=TTLocalizer.BossbotPitcherLeaving, pos=(1.05, 0, -0.88), text_pos=(0, 0), text_scale=0.06, text_fg=VBase4(1, 1, 1, 1))
        self.__cleanupPitcherAdvice()
        return

    def __exitPitcher(self):
        self.showExiting()
        self.d_requestFree(False)

    def __controlPressed(self):
        self.__cleanupPitcherAdvice()
        if self.TugOfWarControls:
            if self.power:
                self.aimStart = 1
                self.__endFireWater()
        elif self.state == 'Controlled':
            self.__beginFireWater()

    def __controlReleased(self):
        if self.TugOfWarControls:
            pass
        elif self.state == 'Controlled':
            self.__endFireWater()

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

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

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

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

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

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

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

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

    def d_sendPitcherPos(self):
        timestamp = globalClockDelta.getFrameNetworkTime()
        self.sendUpdate('setPitcherPos', [self.changeSeq, self.waterPitcherNode.getH(), timestamp])

    def setPitcherPos(self, changeSeq, h, timestamp):
        self.changeSeq = changeSeq
        if self.smoothStarted:
            now = globalClock.getFrameTime()
            local = globalClockDelta.networkToLocalTime(timestamp, now)
            self.pitcherSmoother.setH(h)
            self.pitcherSmoother.setTimestamp(local)
            self.pitcherSmoother.markPosition()
        else:
            self.waterPitcherNode.setH(h)

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

    def __movePitcher(self, xd):
        dt = globalClock.getDt()
        h = self.waterPitcherNode.getH() - xd * self.rotateSpeed * dt
        h %= 360
        self.notify.debug('rotSpeed=%.2f curH=%.2f  xd =%.2f, dt = %.2f, h=%.2f' % (self.rotateSpeed,
         self.waterPitcherNode.getH(),
         xd,
         dt,
         h))
        limitH = h
        self.waterPitcherNode.setH(limitH)
        if xd:
            self.__setMoveSound(self.pitcherMoveSfx)

    def reloadPosition(self):
        self.pitcherSmoother.clearPositions(0)
        self.pitcherSmoother.setHpr(self.waterPitcherNode.getHpr())
        self.pitcherSmoother.setPhonyTimestamp()

    def forceToTruePosition(self):
        if self.pitcherSmoother.getLatestPosition():
            self.pitcherSmoother.applySmoothHpr(self.waterPitcherNode)
        self.pitcherSmoother.clearPositions(1)

    def getSprayTrack(self, color, origin, target, dScaleUp, dHold, dScaleDown, horizScale = 1.0, vertScale = 1.0, parent = render):
        track = Sequence()
        SPRAY_LEN = 1.5
        sprayProp = MovieUtil.globalPropPool.getProp('spray')
        sprayScale = hidden.attachNewNode('spray-parent')
        sprayRot = hidden.attachNewNode('spray-rotate')
        spray = sprayRot
        spray.setColor(color)
        if color[3] < 1.0:
            spray.setTransparency(1)

        def showSpray(sprayScale, sprayRot, sprayProp, origin, target, parent):
            if callable(origin):
                origin = origin()
            if callable(target):
                target = target()
            sprayRot.reparentTo(parent)
            sprayRot.clearMat()
            sprayScale.reparentTo(sprayRot)
            sprayScale.clearMat()
            sprayProp.reparentTo(sprayScale)
            sprayProp.clearMat()
            sprayRot.setPos(origin)
            sprayRot.lookAt(Point3(target))

        track.append(Func(showSpray, sprayScale, sprayRot, sprayProp, origin, target, parent))

        def calcTargetScale(target = target, origin = origin, horizScale = horizScale, vertScale = vertScale):
            if callable(target):
                target = target()
            if callable(origin):
                origin = origin()
            distance = Vec3(target - origin).length()
            yScale = distance / SPRAY_LEN
            targetScale = Point3(yScale * horizScale, yScale, yScale * vertScale)
            return targetScale

        track.append(LerpScaleInterval(sprayScale, dScaleUp, calcTargetScale, startScale=Point3(0.01, 0.01, 0.01)))
        track.append(Func(self.checkHitObject))
        track.append(Wait(dHold))

        def prepareToShrinkSpray(spray, sprayProp, origin, target):
            if callable(target):
                target = target()
            if callable(origin):
                origin = origin()
            sprayProp.setPos(Point3(0.0, -SPRAY_LEN, 0.0))
            spray.setPos(target)

        track.append(Func(prepareToShrinkSpray, spray, sprayProp, origin, target))
        track.append(LerpScaleInterval(sprayScale, dScaleDown, Point3(0.01, 0.01, 0.01)))

        def hideSpray(spray, sprayScale, sprayRot, sprayProp, propPool):
            sprayProp.detachNode()
            MovieUtil.removeProp(sprayProp)
            sprayRot.removeNode()
            sprayScale.removeNode()

        track.append(Func(hideSpray, spray, sprayScale, sprayRot, sprayProp, MovieUtil.globalPropPool))
        return track

    def checkHitObject(self):
        if not self.hitObject:
            return
        if self.avId != base.localAvatar.doId:
            return
        tag = self.hitObject.getNetTag('pieCode')
        pieCode = int(tag)
        if pieCode == ToontownGlobals.PieCodeBossCog:
            self.hitBossSoundInterval.start()
            self.sendUpdate('waterHitBoss', [self.index])
            if self.TugOfWarControls:
                damage = 1
                if self.lastPowerFired < self.YELLOW_POWER_THRESHOLD:
                    damage = 1
                elif self.lastPowerFired < self.RED_POWER_THRESHOLD:
                    damage = 2
                else:
                    damage = 3
                self.boss.d_hitBoss(damage)
            else:
                damage = 1
                if self.lastPowerFired < self.YELLOW_POWER_THRESHOLD:
                    damage = 1
                elif self.lastPowerFired < self.RED_POWER_THRESHOLD:
                    damage = 2
                else:
                    damage = 3
                self.boss.d_hitBoss(damage)

    def waterHitBoss(self, tableIndex):
        if self.index == tableIndex:
            self.hitBossSoundInterval.start()

    def setupPowerBar(self):
        self.powerBar = DirectWaitBar(pos=(0.0, 0, -0.94), relief=DGG.SUNKEN, frameSize=(-2.0,
         2.0,
         -0.2,
         0.2), borderWidth=(0.02, 0.02), scale=0.25, range=1, sortOrder=50, frameColor=(0.5, 0.5, 0.5, 0.5), barColor=(0.75, 0.75, 1.0, 0.8), text='', text_scale=0.26, text_fg=(1, 1, 1, 1), text_align=TextNode.ACenter, text_pos=(0, -0.05))
        self.power = 0
        self.powerBar['value'] = self.power
        self.powerBar.hide()

    def resetPowerBar(self):
        self.power = 0
        self.powerBar['value'] = self.power
        self.powerBar['text'] = ''
        self.keyTTL = []

    def __beginFireWater(self):
        if self.fireTrack and self.fireTrack.isPlaying():
            return
        if self.aimStart != None:
            return
        if not self.state == 'Controlled':
            return
        if not self.avId == localAvatar.doId:
            return
        time = globalClock.getFrameTime()
        self.aimStart = time
        messenger.send('wakeup')
        taskMgr.add(self.__updateWaterPower, self.waterPowerTaskName)
        return

    def __endFireWater(self):
        if self.aimStart == None:
            return
        if not self.state == 'Controlled':
            return
        if not self.avId == localAvatar.doId:
            return
        taskMgr.remove(self.waterPowerTaskName)
        messenger.send('wakeup')
        self.aimStart = None
        origin = self.nozzle.getPos(render)
        target = self.boss.getPos(render)
        angle = deg2Rad(self.waterPitcherNode.getH() + 90)
        x = math.cos(angle)
        y = math.sin(angle)
        fireVector = Point3(x, y, 0)
        if self.power < 0.001:
            self.power = 0.001
        self.lastPowerFired = self.power
        fireVector *= self.fireLength * self.power
        target = origin + fireVector
        segment = CollisionSegment(origin[0], origin[1], origin[2], target[0], target[1], target[2])
        fromObject = render.attachNewNode(CollisionNode('pitcherColNode'))
        fromObject.node().addSolid(segment)
        fromObject.node().setFromCollideMask(ToontownGlobals.PieBitmask | ToontownGlobals.CameraBitmask | ToontownGlobals.FloorBitmask)
        fromObject.node().setIntoCollideMask(BitMask32.allOff())
        queue = CollisionHandlerQueue()
        base.cTrav.addCollider(fromObject, queue)
        base.cTrav.traverse(render)
        queue.sortEntries()
        self.hitObject = None
        if queue.getNumEntries():
            entry = queue.getEntry(0)
            target = entry.getSurfacePoint(render)
            self.hitObject = entry.getIntoNodePath()
        base.cTrav.removeCollider(fromObject)
        fromObject.removeNode()
        self.d_firingWater(origin, target)
        self.fireWater(origin, target)
        self.resetPowerBar()
        return

    def __updateWaterPower(self, task):
        if not self.powerBar:
            print('### no power bar!!!')
            return task.done
        newPower = self.__getWaterPower(globalClock.getFrameTime())
        self.power = newPower
        self.powerBar['value'] = newPower
        if self.power < self.YELLOW_POWER_THRESHOLD:
            self.powerBar['barColor'] = VBase4(0.75, 0.75, 1.0, 0.8)
        elif self.power < self.RED_POWER_THRESHOLD:
            self.powerBar['barColor'] = VBase4(1.0, 1.0, 0.0, 0.8)
        else:
            self.powerBar['barColor'] = VBase4(1.0, 0.0, 0.0, 0.8)
        return task.cont

    def __getWaterPower(self, time):
        elapsed = max(time - self.aimStart, 0.0)
        t = elapsed / self.waterPowerSpeed
        exponent = self.waterPowerExponent
        if t > 1:
            t = t % 1
        power = 1 - math.pow(1 - t, exponent)
        if power > 1.0:
            power = 1.0
        return power

    def d_firingWater(self, origin, target):
        self.sendUpdate('firingWater', [origin[0],
         origin[1],
         origin[2],
         target[0],
         target[1],
         target[2]])

    def firingWater(self, startX, startY, startZ, endX, endY, endZ):
        origin = Point3(startX, startY, startZ)
        target = Point3(endX, endY, endZ)
        self.fireWater(origin, target)

    def fireWater(self, origin, target):
        color = VBase4(0.75, 0.75, 1, 0.8)
        dScaleUp = 0.1
        dHold = 0.3
        dScaleDown = 0.1
        horizScale = 0.1
        vertScale = 0.1
        sprayTrack = self.getSprayTrack(color, origin, target, dScaleUp, dHold, dScaleDown, horizScale, vertScale)
        duration = self.squirtSfx.length()
        if sprayTrack.getDuration() < duration:
            duration = sprayTrack.getDuration()
        soundTrack = SoundInterval(self.squirtSfx, node=self.waterPitcherModel, duration=duration)
        self.fireTrack = Parallel(sprayTrack, soundTrack)
        self.fireTrack.start()

    def getPos(self, wrt = render):
        return self.tableGroup.getPos(wrt)

    def getLocator(self):
        return self.tableGroup

    def enterFlat(self, avId):
        self.prepareForPhaseFour()
        self.resetPowerBar()
        self.notify.debug('enterFlat %d' % self.index)
        if self.avId:
            toon = base.cr.doId2do.get(self.avId)
            if toon:
                toon.wrtReparentTo(render)
                toon.setZ(0)
        self.tableGroup.setScale(1, 1, 0.01)
        if self.avId and self.avId == localAvatar.doId:
            localAvatar.b_squish(ToontownGlobals.BossCogDamageLevels[ToontownGlobals.BossCogMoveAttack])

    def exitFlat(self):
        self.tableGroup.setScale(1.0)
        if self.avId:
            toon = base.cr.doId2do.get(self.avId)
            if toon:
                if toon == localAvatar:
                    self.boss.toCraneMode()
                    toon.b_setAnimState('neutral')
                toon.setAnimState('neutral')
                toon.loop('leverNeutral')

    def __allowDetect(self, task):
        if self.fadeTrack:
            self.fadeTrack.finish()
        self.fadeTrack = Sequence(self.tableGroup.colorScaleInterval(0.2, VBase4(1, 1, 1, 1)), Func(self.tableGroup.clearColorScale), Func(self.tableGroup.clearTransparency))
        self.fadeTrack.start()
        self.allowLocalRequestControl = True

    def gotBossZapped(self):
        self.showExiting()
        self.d_requestFree(True)

    def __upArrowKeyPressed(self):
        if self.TugOfWarControls:
            self.__pressHandler(0)

    def __downArrowKeyPressed(self):
        if self.TugOfWarControls:
            self.__pressHandler(1)

    def __pressHandler(self, index):
        if index == self.buttons[0]:
            self.keyTTL.insert(0, 1.0)
            if not self.OnlyUpArrow:
                self.buttons.reverse()

    def __spawnUpdateKeyPressRateTask(self):
        taskMgr.remove(self.taskName(self.UPDATE_KEY_PRESS_RATE_TASK))
        taskMgr.doMethodLater(0.1, self.__updateKeyPressRateTask, self.taskName(self.UPDATE_KEY_PRESS_RATE_TASK))

    def __killUpdateKeyPressRateTask(self):
        taskMgr.remove(self.taskName(self.UPDATE_KEY_PRESS_RATE_TASK))

    def __updateKeyPressRateTask(self, task):
        if self.state not in 'Controlled':
            return Task.done
        for i in range(len(self.keyTTL)):
            self.keyTTL[i] -= 0.1

        for i in range(len(self.keyTTL)):
            if self.keyTTL[i] <= 0:
                a = self.keyTTL[0:i]
                del self.keyTTL
                self.keyTTL = a
                break

        self.keyRate = len(self.keyTTL)
        keyRateDiff = self.keyRate - self.BASELINE_KEY_RATE
        diffPower = keyRateDiff / 300.0
        if self.power < 1 and diffPower > 0:
            diffPower = diffPower * math.pow(1 - self.power, 1.25)
        newPower = self.power + diffPower
        if newPower > 1:
            newPower = 1
        elif newPower < 0:
            newPower = 0
        self.notify.debug('diffPower=%.2f keyRate = %d, newPower=%.2f' % (diffPower, self.keyRate, newPower))
        self.power = newPower
        self.powerBar['value'] = newPower
        if self.power < self.YELLOW_POWER_THRESHOLD:
            self.powerBar['barColor'] = VBase4(0.75, 0.75, 1.0, 0.8)
        elif self.power < self.RED_POWER_THRESHOLD:
            self.powerBar['barColor'] = VBase4(1.0, 1.0, 0.0, 0.8)
        else:
            self.powerBar['barColor'] = VBase4(1.0, 0.0, 0.0, 0.8)
        self.__spawnUpdateKeyPressRateTask()
        return Task.done

    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)
Esempio n. 6
0
class DistributedGolfSpot(DistributedObject.DistributedObject, FSM.FSM):
    notify = DirectNotifyGlobal.directNotify.newCategory('DistributedGolfSpot')
    positions = ((-45, 100, GolfGlobals.GOLF_BALL_RADIUS),
                 (-15, 100, GolfGlobals.GOLF_BALL_RADIUS),
                 (15, 100, GolfGlobals.GOLF_BALL_RADIUS),
                 (45, 100, GolfGlobals.GOLF_BALL_RADIUS))
    toonGolfOffsetPos = Point3(-2, 0, -GolfGlobals.GOLF_BALL_RADIUS)
    toonGolfOffsetHpr = Point3(-90, 0, 0)
    rotateSpeed = 20
    golfPowerSpeed = base.config.GetDouble('golf-power-speed', 3)
    golfPowerExponent = base.config.GetDouble('golf-power-exponent', 0.75)

    def __init__(self, cr):
        DistributedObject.DistributedObject.__init__(self, cr)
        FSM.FSM.__init__(self, 'DistributedGolfSpot')
        self.boss = None
        self.index = None
        self.avId = 0
        self.toon = None
        self.golfSpotSmoother = SmoothMover()
        self.golfSpotSmoother.setSmoothMode(SmoothMover.SMOn)
        self.smoothStarted = 0
        self.__broadcastPeriod = 0.2
        if self.index > len(self.positions):
            self.notify.error('Invalid index %d' % index)
        self.fadeTrack = None
        self.setupPowerBar()
        self.aimStart = None
        self.golfSpotAdviceLabel = None
        self.changeSeq = 0
        self.lastChangeSeq = 0
        self.controlKeyAllowed = False
        self.flyBallTracks = {}
        self.splatTracks = {}
        self.__flyBallBubble = None
        self.flyBallHandler = None
        self.__flyBallSequenceNum = 0
        self.swingInterval = None
        self.lastHitSequenceNum = -1
        self.goingToReward = False
        self.gotHitByBoss = False
        self.releaseTrack = None
        self.grabTrack = None
        self.restoreScaleTrack = None
        return

    def setBossCogId(self, bossCogId):
        self.bossCogId = bossCogId
        self.boss = base.cr.doId2do[bossCogId]
        self.boss.setGolfSpot(self, self.index)

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

    def disable(self):
        DistributedObject.DistributedObject.disable(self)
        self.ignoreAll()

    def delete(self):
        DistributedObject.DistributedObject.delete(self)
        self.ignoreAll()
        self.boss = None
        return

    def announceGenerate(self):
        DistributedObject.DistributedObject.announceGenerate(self)
        self.triggerName = self.uniqueName('trigger')
        self.triggerEvent = 'enter%s' % self.triggerName
        self.smoothName = self.uniqueName('golfSpotSmooth')
        self.golfSpotAdviceName = self.uniqueName('golfSpotAdvice')
        self.posHprBroadcastName = self.uniqueName('golfSpotBroadcast')
        self.ballPowerTaskName = self.uniqueName('updateGolfPower')
        self.adjustClubTaskName = self.uniqueName('adjustClub')
        self.loadAssets()
        self.accept('flyBallHit-%d' % self.index, self.__flyBallHit)

    def loadAssets(self):
        self.root = render.attachNewNode('golfSpot-%d' % self.index)
        self.root.setPos(*self.positions[self.index])
        self.ballModel = loader.loadModel('phase_6/models/golf/golf_ball')
        self.ballColor = VBase4(1, 1, 1, 1)
        if self.index < len(GolfGlobals.PlayerColors):
            self.ballColor = VBase4(*GolfGlobals.PlayerColors[self.index])
            self.ballModel.setColorScale(self.ballColor)
        self.ballModel.reparentTo(self.root)
        self.club = loader.loadModel('phase_6/models/golf/putter')
        self.clubLookatSpot = self.root.attachNewNode('clubLookat')
        self.clubLookatSpot.setY(-(GolfGlobals.GOLF_BALL_RADIUS + 0.1))
        cs = CollisionSphere(0, 0, 0, 1)
        cs.setTangible(0)
        cn = CollisionNode(self.triggerName)
        cn.addSolid(cs)
        cn.setIntoCollideMask(ToontownGlobals.WallBitmask)
        self.trigger = self.root.attachNewNode(cn)
        self.trigger.stash()
        self.hitBallSfx = loader.loadSfx('phase_6/audio/sfx/Golf_Hit_Ball.ogg')

    def cleanup(self):
        if self.swingInterval:
            self.swingInterval.finish()
            self.swingInterval = None
        if self.releaseTrack:
            self.releaseTrack.finish()
            self.releaseTrack = None
        flyTracks = self.flyBallTracks.values()
        for track in flyTracks:
            track.finish()

        if self.fadeTrack:
            self.fadeTrack.finish()
            self.fadeTrack = None
        if self.restoreScaleTrack:
            self.restoreScaleTrack.finish()
            self.restoreScaleTrack = None
        self.root.removeNode()
        self.ballModel.removeNode()
        self.club.removeNode()
        if self.powerBar:
            self.powerBar.destroy()
            self.powerBar = None
        taskMgr.remove(self.triggerName)
        self.boss = None
        return

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

    def enterOff(self):
        pass

    def exitOff(self):
        pass

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

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

    def enterControlled(self, avId):
        self.avId = avId
        toon = base.cr.doId2do.get(avId)
        if not toon:
            return
        self.enableControlKey()
        self.toon = toon
        self.grabTrack = self.makeToonGrabInterval(toon)
        if avId == localAvatar.doId:
            self.boss.toCraneMode()
            camera.reparentTo(self.root)
            camera.setPosHpr(0, -10, 3, 0, 0, 0)
            localAvatar.setPos(self.root, self.toonGolfOffsetPos)
            localAvatar.setHpr(self.root, self.toonGolfOffsetHpr)
            localAvatar.sendCurrentPosition()
            self.__enableControlInterface()
            self.startPosHprBroadcast()
            self.accept('exitCrane', self.gotBossZapped)
        self.grabTrack.start()

    def exitControlled(self):
        self.grabTrack.finish()
        del self.grabTrack
        if self.swingInterval:
            self.swingInterval.finish()
            self.swingInterval = None
        if not self.ballModel.isEmpty():
            if self.ballModel.isHidden():
                self.notify.debug('ball is hidden scale =%s' %
                                  self.ballModel.getScale())
            else:
                self.notify.debug('ball is showing scale=%s' %
                                  self.ballModel.getScale())
        if self.toon and not self.toon.isDisabled():
            self.toon.startSmooth()
        self.releaseTrack = self.makeToonReleaseInterval(self.toon)
        self.stopPosHprBroadcast()
        self.stopSmooth()
        if self.avId == localAvatar.doId:
            self.__disableControlInterface()
            if not self.goingToReward:
                camera.reparentTo(base.localAvatar)
                camera.setPos(base.localAvatar.cameraPositions[0][0])
                camera.setHpr(0, 0, 0)
        self.stopAdjustClubTask()
        self.releaseTrack.start()
        self.enableControlKey()
        return

    def __allowDetect(self, task):
        if self.fadeTrack:
            self.fadeTrack.finish()
        self.fadeTrack = Sequence(self.ballModel.colorScaleInterval(
            0.2, self.ballColor),
                                  Func(self.ballModel.clearTransparency),
                                  name='fadeTrack-allowDetect')
        self.fadeTrack.start()
        self.trigger.unstash()
        self.accept(self.triggerEvent, self.__hitTrigger)

    def __hitTrigger(self, event):
        self.d_requestControl()

    def getRestoreScaleInterval(self):
        return Sequence()

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

    def d_requestFree(self, gotHitByBoss):
        self.sendUpdate('requestFree', [gotHitByBoss])

    def makeToonGrabInterval(self, toon):
        origPos = toon.getPos(self.root)
        origHpr = toon.getHpr(self.root)
        a = self.accomodateToon(toon)
        newPos = toon.getPos()
        newHpr = toon.getHpr()
        origHpr.setX(PythonUtil.fitSrcAngle2Dest(origHpr[0], newHpr[0]))
        self.notify.debug('toon.setPosHpr %s %s' % (origPos, origHpr))
        toon.setPosHpr(origPos, origHpr)
        walkTime = 0.2
        reach = Sequence()
        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(toon.stopLookAround))
        if toon == base.localAvatar:
            i.append(Func(self.switchToAnimState, 'GolfPuttLoop'))
        i.append(Func(self.startAdjustClubTask))
        i = Parallel(i, a)
        return i

    def accomodateToon(self, toon):
        toon.wrtReparentTo(self.root)
        toon.setPos(self.toonGolfOffsetPos)
        toon.setHpr(self.toonGolfOffsetHpr)
        return Sequence()

    def switchToAnimState(self, animStateName, forced=False):
        curAnimState = base.localAvatar.animFSM.getCurrentState()
        curAnimStateName = ''
        if curAnimState:
            curAnimStateName = curAnimState.getName()
        if curAnimStateName != animStateName or forced:
            base.localAvatar.b_setAnimState(animStateName)

    def __enableControlInterface(self):
        gui = loader.loadModel('phase_3.5/models/gui/avatar_panel_gui')
        self.closeButton = DirectButton(image=(gui.find('**/CloseBtn_UP'),
                                               gui.find('**/CloseBtn_DN'),
                                               gui.find('**/CloseBtn_Rllvr'),
                                               gui.find('**/CloseBtn_UP')),
                                        relief=None,
                                        scale=2,
                                        text=TTLocalizer.BossbotGolfSpotLeave,
                                        text_scale=0.04,
                                        text_pos=(0, -0.07),
                                        text_fg=VBase4(1, 1, 1, 1),
                                        pos=(1.05, 0, -0.82),
                                        command=self.__exitGolfSpot)
        self.accept('escape', self.__exitGolfSpot)
        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, 'watchGolfSpotControls')
        taskMgr.doMethodLater(5, self.__displayGolfSpotAdvice,
                              self.golfSpotAdviceName)
        self.arrowVert = 0
        self.arrowHorz = 0
        if self.powerBar:
            self.powerBar.show()
        return

    def __disableControlInterface(self):
        if self.closeButton:
            self.closeButton.destroy()
            self.closeButton = None
        self.__cleanupGolfSpotAdvice()
        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
        taskMgr.remove('watchGolfSpotControls')
        if self.powerBar:
            self.powerBar.hide()
        else:
            self.notify.debug('self.powerBar is none')
        return

    def setupPowerBar(self):
        self.powerBar = DirectWaitBar(pos=(0.0, 0, -0.94),
                                      relief=DGG.SUNKEN,
                                      frameSize=(-2.0, 2.0, -0.2, 0.2),
                                      borderWidth=(0.02, 0.02),
                                      scale=0.25,
                                      range=100,
                                      sortOrder=50,
                                      frameColor=(0.5, 0.5, 0.5, 0.5),
                                      barColor=(1.0, 0.0, 0.0, 1.0),
                                      text='',
                                      text_scale=0.26,
                                      text_fg=(1, 1, 1, 1),
                                      text_align=TextNode.ACenter,
                                      text_pos=(0, -0.05))
        self.power = 0
        self.powerBar['value'] = self.power
        self.powerBar.hide()

    def resetPowerBar(self):
        self.power = 0
        self.powerBar['value'] = self.power
        self.powerBar['text'] = ''

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

    def __cleanupGolfSpotAdvice(self):
        if self.golfSpotAdviceLabel:
            self.golfSpotAdviceLabel.destroy()
            self.golfSpotAdviceLabel = None
        taskMgr.remove(self.golfSpotAdviceName)
        return

    def showExiting(self):
        if self.closeButton:
            self.closeButton.destroy()
            self.closeButton = DirectLabel(
                relief=None,
                text=TTLocalizer.BossbotGolfSpotLeaving,
                pos=(1.05, 0, -0.88),
                text_pos=(0, 0),
                text_scale=0.06,
                text_fg=VBase4(1, 1, 1, 1))
        self.__cleanupGolfSpotAdvice()
        return

    def __exitGolfSpot(self):
        self.d_requestFree(False)

    def __controlPressed(self):
        if self.controlKeyAllowed:
            self.__beginFireBall()

    def __controlReleased(self):
        if self.controlKeyAllowed:
            self.__endFireBall()

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

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

    def __rightArrow(self, pressed):
        self.__incrementChangeSeq()
        self.__cleanupGolfSpotAdvice()
        if pressed:
            self.arrowHorz = 1
            self.switchToAnimState('GolfRotateLeft')
        elif self.arrowHorz > 0:
            self.arrowHorz = 0
            self.switchToAnimState('GolfPuttLoop')

    def __leftArrow(self, pressed):
        self.__incrementChangeSeq()
        self.__cleanupGolfSpotAdvice()
        if pressed:
            self.arrowHorz = -1
            self.switchToAnimState('GolfRotateRight')
        elif self.arrowHorz < 0:
            self.arrowHorz = 0
            self.switchToAnimState('GolfPuttLoop')

    def __watchControls(self, task):
        if self.arrowHorz:
            self.__moveGolfSpot(self.arrowHorz)
        return Task.cont

    def __moveGolfSpot(self, xd):
        dt = globalClock.getDt()
        h = self.root.getH() - xd * self.rotateSpeed * dt
        h %= 360
        limitH = h
        self.root.setH(limitH)

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

    def __beginFireBall(self):
        if self.aimStart != None:
            return
        if not self.state == 'Controlled':
            return
        if not self.avId == localAvatar.doId:
            return
        time = globalClock.getFrameTime()
        self.aimStart = time
        messenger.send('wakeup')
        taskMgr.add(self.__updateBallPower, self.ballPowerTaskName)
        return

    def __endFireBall(self):
        if self.aimStart == None:
            return
        if not self.state == 'Controlled':
            return
        if not self.avId == localAvatar.doId:
            return
        taskMgr.remove(self.ballPowerTaskName)
        self.disableControlKey()
        messenger.send('wakeup')
        self.aimStart = None
        power = self.power
        angle = self.root.getH()
        self.notify.debug('incrementing self.__flyBallSequenceNum')
        self.__flyBallSequenceNum = (self.__flyBallSequenceNum + 1) % 255
        self.sendSwingInfo(power, angle, self.__flyBallSequenceNum)
        self.setSwingInfo(power, angle, self.__flyBallSequenceNum)
        self.resetPowerBar()
        return

    def __updateBallPower(self, task):
        if not self.powerBar:
            print '### no power bar!!!'
            return task.done
        newPower = self.__getBallPower(globalClock.getFrameTime())
        self.power = newPower
        self.powerBar['value'] = newPower
        return task.cont

    def __getBallPower(self, time):
        elapsed = max(time - self.aimStart, 0.0)
        t = elapsed / self.golfPowerSpeed
        t = math.pow(t, self.golfPowerExponent)
        power = int(t * 100) % 200
        if power > 100:
            power = 200 - power
        return power

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

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

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

    def d_sendGolfSpotPos(self):
        timestamp = globalClockDelta.getFrameNetworkTime()
        self.sendUpdate(
            'setGolfSpotPos',
            [self.changeSeq, self.root.getH(), timestamp])

    def setGolfSpotPos(self, changeSeq, h, timestamp):
        self.changeSeq = changeSeq
        if self.smoothStarted:
            now = globalClock.getFrameTime()
            local = globalClockDelta.networkToLocalTime(timestamp, now)
            self.golfSpotSmoother.setH(h)
            self.golfSpotSmoother.setTimestamp(local)
            self.golfSpotSmoother.markPosition()
        else:
            self.root.setH(h)

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

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

    def clearSmoothing(self, bogus=None):
        self.golfSpotSmoother.clearPositions(1)

    def doSmoothTask(self, task):
        self.golfSpotSmoother.computeAndApplySmoothHpr(self.root)
        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 makeToonReleaseInterval(self, toon):
        def getSlideToPos(toon=toon):
            return render.getRelativePoint(toon, Point3(0, -5, 0))

        if self.gotHitByBoss:
            grabIval = Sequence(Func(self.detachClub),
                                name='makeToonReleaseInterval-gotHitByBoss')
            if not toon.isEmpty():
                toonIval = Sequence(Func(toon.wrtReparentTo, render),
                                    Parallel(
                                        ActorInterval(toon, 'slip-backward'),
                                        toon.posInterval(0.5,
                                                         getSlideToPos,
                                                         fluid=1)),
                                    name='makeToonReleaseInterval-toonIval')
                grabIval.append(toonIval)
        else:
            grabIval = Sequence(Func(self.detachClub))
            if not toon.isEmpty():
                toonIval = Sequence(
                    Parallel(
                        ActorInterval(toon,
                                      'walk',
                                      duration=1.0,
                                      playRate=-1.0),
                        LerpPosInterval(toon,
                                        duration=1.0,
                                        pos=Point3(-10, 0, 0))),
                    Func(toon.wrtReparentTo, render))
                grabIval.append(toonIval)
        if localAvatar.doId == toon.doId:
            if not self.goingToReward and toon.hp > 0:
                grabIval.append(Func(self.goToFinalBattle))
                grabIval.append(
                    Func(self.notify.debug, 'goingToFinalBattlemode'))
                grabIval.append(Func(self.safeBossToFinalBattleMode))
        return grabIval

    def safeBossToFinalBattleMode(self):
        if self.boss:
            self.boss.toFinalBattleMode()

    def goToFinalBattle(self):
        if self.cr:
            place = self.cr.playGame.getPlace()
            if place and hasattr(place, 'fsm'):
                curState = place.fsm.getCurrentState().getName()
                if place.fsm.getCurrentState().getName() == 'crane':
                    place.setState('finalBattle')
                else:
                    self.notify.debug('NOT going to final battle, state=%s' %
                                      curState)

    def attachClub(self, avId, pointToBall=False):
        club = self.club
        if club:
            av = base.cr.doId2do.get(avId)
            if av:
                av.useLOD(1000)
                lHand = av.getLeftHands()[0]
                club.setPos(0, 0, 0)
                club.reparentTo(lHand)
                netScale = club.getNetTransform().getScale()[1]
                counterActToonScale = lHand.find('**/counteractToonScale')
                if counterActToonScale.isEmpty():
                    counterActToonScale = lHand.attachNewNode(
                        'counteractToonScale')
                    counterActToonScale.setScale(1 / netScale)
                    self.notify.debug('creating counterActToonScale for %s' %
                                      av.getName())
                club.reparentTo(counterActToonScale)
                club.setX(-0.25 * netScale)
                if pointToBall:
                    club.lookAt(self.clubLookatSpot)

    def detachClub(self):
        if not self.club.isEmpty():
            self.club.reparentTo(self.root)
            self.club.setZ(-20)
            self.club.setScale(1)

    def adjustClub(self):
        club = self.club
        if club:
            distance = club.getDistance(self.clubLookatSpot)
            scaleFactor = distance / 2.058
            club.setScale(1, scaleFactor, 1)

    def startAdjustClubTask(self):
        taskMgr.add(self.adjustClubTask, self.adjustClubTaskName)

    def stopAdjustClubTask(self):
        taskMgr.remove(self.adjustClubTaskName)

    def adjustClubTask(self, task):
        self.attachClub(self.avId, True)
        self.adjustClub()
        return task.cont

    def enableControlKey(self):
        self.controlKeyAllowed = True

    def disableControlKey(self):
        self.controlKeyAllowed = False

    def sendSwingInfo(self, power, angle, sequenceNum):
        self.sendUpdate('setSwingInfo', [power, angle, sequenceNum])

    def startBallPlayback(self, power, angle, sequenceNum):
        flyBall = self.ballModel.copyTo(NodePath())
        flyBall.setScale(1.0)
        flyBallBubble = self.getFlyBallBubble().instanceTo(NodePath())
        flyBallBubble.reparentTo(flyBall)
        flyBall.setTag('pieSequence', str(sequenceNum))
        flyBall.setTag('throwerId', str(self.avId))
        t = power / 100.0
        t = 1.0 - t
        dist = 300 - 200 * t
        time = 1.5 + 0.5 * t
        proj = ProjectileInterval(None,
                                  startPos=Point3(0, 0, 0),
                                  endPos=Point3(0, dist, 0),
                                  duration=time)
        relVel = proj.startVel

        def getVelocity(root=self.root, relVel=relVel):
            return render.getRelativeVector(root, relVel)

        fly = Sequence(
            Func(flyBall.reparentTo, render),
            Func(flyBall.setPosHpr, self.root, 0, 0, 0, 0, 0, 0),
            Func(base.cTrav.addCollider, flyBallBubble, self.flyBallHandler),
            ProjectileInterval(flyBall, startVel=getVelocity, duration=3),
            Func(flyBall.detachNode),
            Func(base.cTrav.removeCollider, flyBallBubble),
            Func(self.notify.debug, 'removed collider'),
            Func(self.flyBallFinishedFlying, sequenceNum))
        flyWithSound = Parallel(fly,
                                SoundInterval(self.hitBallSfx, node=self.root),
                                name='flyWithSound')
        self.notify.debug('starting flyball track')
        flyWithSound.start()
        self.flyBallTracks[sequenceNum] = flyWithSound
        return

    def setSwingInfo(self, power, angle, sequenceNum):
        av = base.cr.doId2do.get(self.avId)
        self.swingInterval = Sequence()
        if av:
            self.stopAdjustClubTask()
            self.swingInterval = Sequence(
                ActorInterval(av,
                              'swing-putt',
                              startFrame=0,
                              endFrame=GolfGlobals.BALL_CONTACT_FRAME),
                Func(self.startBallPlayback, power, angle, sequenceNum),
                Func(self.ballModel.hide),
                ActorInterval(av,
                              'swing-putt',
                              startFrame=GolfGlobals.BALL_CONTACT_FRAME,
                              endFrame=24), Func(self.ballModel.setScale, 0.1),
                Func(self.ballModel.show),
                LerpScaleInterval(self.ballModel, 1.0, Point3(1, 1, 1)),
                Func(self.enableControlKey))
            if av == localAvatar:
                self.swingInterval.append(
                    Func(self.switchToAnimState, 'GolfPuttLoop', True))
        self.swingInterval.start()

    def getFlyBallBubble(self):
        if self.__flyBallBubble == None:
            bubble = CollisionSphere(0, 0, 0, GolfGlobals.GOLF_BALL_RADIUS)
            node = CollisionNode('flyBallBubble')
            node.addSolid(bubble)
            node.setFromCollideMask(ToontownGlobals.PieBitmask
                                    | ToontownGlobals.CameraBitmask
                                    | ToontownGlobals.FloorBitmask)
            node.setIntoCollideMask(BitMask32.allOff())
            self.__flyBallBubble = NodePath(node)
            self.flyBallHandler = CollisionHandlerEvent()
            self.flyBallHandler.addInPattern('flyBallHit-%d' % self.index)
        return self.__flyBallBubble

    def __flyBallHit(self, entry):
        print entry

    def flyBallFinishedFlying(self, sequence):
        if self.flyBallTracks.has_key(sequence):
            del self.flyBallTracks[sequence]

    def __finishFlyBallTrack(self, sequence):
        if self.flyBallTracks.has_key(sequence):
            flyBallTrack = self.flyBallTracks[sequence]
            del self.flyBallTracks[sequence]
            flyBallTrack.finish()

    def flyBallFinishedSplatting(self, sequence):
        if self.splatTracks.has_key(sequence):
            del self.splatTracks[sequence]

    def __flyBallHit(self, entry):
        if not entry.hasSurfacePoint() or not entry.hasInto():
            return
        if not entry.getInto().isTangible():
            return
        sequence = int(entry.getFromNodePath().getNetTag('pieSequence'))
        self.__finishFlyBallTrack(sequence)
        if self.splatTracks.has_key(sequence):
            splatTrack = self.splatTracks[sequence]
            del self.splatTracks[sequence]
            splatTrack.finish()
        flyBallCode = 0
        flyBallCodeStr = entry.getIntoNodePath().getNetTag('pieCode')
        if flyBallCodeStr:
            flyBallCode = int(flyBallCodeStr)
        pos = entry.getSurfacePoint(render)
        timestamp32 = globalClockDelta.getFrameNetworkTime(bits=32)
        throwerId = int(entry.getFromNodePath().getNetTag('throwerId'))
        splat = self.getFlyBallSplatInterval(pos[0], pos[1], pos[2],
                                             flyBallCode, throwerId)
        splat = Sequence(splat, Func(self.flyBallFinishedSplatting, sequence))
        self.splatTracks[sequence] = splat
        splat.start()
        self.notify.debug(
            'doId=%d into=%s flyBallCode=%d, throwerId=%d' %
            (self.doId, entry.getIntoNodePath(), flyBallCode, throwerId))
        if flyBallCode == ToontownGlobals.PieCodeBossCog and self.avId == localAvatar.doId and self.lastHitSequenceNum != self.__flyBallSequenceNum:
            self.lastHitSequenceNum = self.__flyBallSequenceNum
            self.boss.d_ballHitBoss(10)
        elif flyBallCode == ToontownGlobals.PieCodeToon and self.avId == localAvatar.doId and self.lastHitSequenceNum != self.__flyBallSequenceNum:
            self.lastHitSequenceNum = self.__flyBallSequenceNum
            avatarDoId = entry.getIntoNodePath().getNetTag('avatarDoId')
            if avatarDoId == '':
                self.notify.warning('Toon %s has no avatarDoId tag.' %
                                    repr(entry.getIntoNodePath()))
                return
            doId = int(avatarDoId)
            if doId != localAvatar.doId:
                pass

    def getFlyBallSplatInterval(self, x, y, z, flyBallCode, throwerId):
        from toontown.toonbase import ToontownBattleGlobals
        from toontown.battle import BattleProps
        splatName = 'dust'
        splat = BattleProps.globalPropPool.getProp(splatName)
        splat.setBillboardPointWorld(2)
        color = ToontownGlobals.PieCodeColors.get(flyBallCode)
        if color:
            splat.setColor(*color)
        if flyBallCode == ToontownGlobals.PieCodeBossCog:
            self.notify.debug('changing color to %s' % self.ballColor)
            splat.setColor(self.ballColor)
        sound = loader.loadSfx('phase_11/audio/sfx/LB_evidence_miss.ogg')
        vol = 1.0
        if flyBallCode == ToontownGlobals.PieCodeBossCog:
            sound = loader.loadSfx('phase_4/audio/sfx/Golf_Hit_Barrier_1.ogg')
        soundIval = SoundInterval(sound, node=splat, volume=vol)
        if flyBallCode == ToontownGlobals.PieCodeBossCog and localAvatar.doId == throwerId:
            vol = 1.0
            soundIval = SoundInterval(sound, node=localAvatar, volume=vol)
        ival = Parallel(
            Func(splat.reparentTo, render), Func(splat.setPos, x, y, z),
            soundIval,
            Sequence(ActorInterval(splat, splatName), Func(splat.detachNode)))
        return ival

    def setGoingToReward(self):
        self.goingToReward = True

    def gotBossZapped(self):
        self.showExiting()
        self.d_requestFree(True)
class DistributedBanquetTable(DistributedObject.DistributedObject, FSM.FSM, BanquetTableBase.BanquetTableBase):
    notify = DirectNotifyGlobal.directNotify.newCategory("DistributedBanquetTable")
    rotationsPerSeatIndex = [90, 90, 0, 0, -90, -90, 180, 180]
    pitcherMinH = -360
    pitcherMaxH = 360
    rotateSpeed = 30
    waterPowerSpeed = base.config.GetDouble("water-power-speed", 15)
    waterPowerExponent = base.config.GetDouble("water-power-exponent", 0.75)
    useNewAnimations = True
    TugOfWarControls = False
    OnlyUpArrow = True
    if OnlyUpArrow:
        BASELINE_KEY_RATE = 3
    else:
        BASELINE_KEY_RATE = 6
    UPDATE_KEY_PRESS_RATE_TASK = "BanquetTableUpdateKeyPressRateTask"
    YELLOW_POWER_THRESHOLD = 0.75
    RED_POWER_THRESHOLD = 0.96999999999999997

    def __init__(self, cr):
        DistributedObject.DistributedObject.__init__(self, cr)
        FSM.FSM.__init__(self, "DistributedBanquetTable")
        self.boss = None
        self.index = -1
        self.diners = {}
        self.dinerStatus = {}
        self.serviceLocs = {}
        self.chairLocators = {}
        self.sitLocators = {}
        self.activeIntervals = {}
        self.dinerStatusIndicators = {}
        self.preparedForPhaseFour = False
        self.avId = 0
        self.toon = None
        self.pitcherSmoother = SmoothMover()
        self.pitcherSmoother.setSmoothMode(SmoothMover.SMOn)
        self.smoothStarted = 0
        self._DistributedBanquetTable__broadcastPeriod = 0.20000000000000001
        self.changeSeq = 0
        self.lastChangeSeq = 0
        self.pitcherAdviceLabel = None
        self.fireLength = 250
        self.fireTrack = None
        self.hitObject = None
        self.setupPowerBar()
        self.aimStart = None
        self.toonPitcherPosition = Point3(0, -2, 0)
        self.allowLocalRequestControl = True
        self.fadeTrack = None
        self.grabTrack = None
        self.gotHitByBoss = False
        self.keyTTL = []
        self.keyRate = 0
        self.buttons = [0, 1]
        self.lastPowerFired = 0
        self.moveSound = None
        self.releaseTrack = None

    def disable(self):
        DistributedObject.DistributedObject.disable(self)
        taskMgr.remove(self.triggerName)
        taskMgr.remove(self.smoothName)
        taskMgr.remove(self.watchControlsName)
        taskMgr.remove(self.pitcherAdviceName)
        taskMgr.remove(self.posHprBroadcastName)
        taskMgr.remove(self.waterPowerTaskName)
        if self.releaseTrack:
            self.releaseTrack.finish()
            self.releaseTrack = None

        if self.fireTrack:
            self.fireTrack.finish()
            self.fireTrack = None

        self.cleanupIntervals()

    def delete(self):
        DistributedObject.DistributedObject.delete(self)
        self.boss = None
        self.ignoreAll()
        for indicator in self.dinerStatusIndicators.values():
            indicator.delete()

        self.dinerStatusIndicators = {}
        for diner in self.diners.values():
            diner.delete()

        self.diners = {}
        self.powerBar.destroy()
        self.powerBar = None
        self.pitcherMoveSfx.stop()

    def announceGenerate(self):
        DistributedObject.DistributedObject.announceGenerate(self)
        self.loadAssets()
        self.smoothName = self.uniqueName("pitcherSmooth")
        self.pitcherAdviceName = self.uniqueName("pitcherAdvice")
        self.posHprBroadcastName = self.uniqueName("pitcherBroadcast")
        self.waterPowerTaskName = self.uniqueName("updateWaterPower")
        self.triggerName = self.uniqueName("trigger")
        self.watchControlsName = self.uniqueName("watchControls")

    def setBossCogId(self, bossCogId):
        self.bossCogId = bossCogId
        self.boss = base.cr.doId2do[bossCogId]
        self.boss.setTable(self, self.index)

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

    def setState(self, state, avId, extraInfo):
        self.gotHitByBoss = extraInfo
        if state == "F":
            self.demand("Off")
        elif state == "N":
            self.demand("On")
        elif state == "I":
            self.demand("Inactive")
        elif state == "R":
            self.demand("Free")
        elif state == "C":
            self.demand("Controlled", avId)
        elif state == "L":
            self.demand("Flat", avId)
        else:
            self.notify.error("Invalid state from AI: %s" % state)

    def setNumDiners(self, numDiners):
        self.numDiners = numDiners

    def setDinerInfo(self, hungryDurations, eatingDurations, dinerLevels):
        self.dinerInfo = {}
        for i in xrange(len(hungryDurations)):
            hungryDur = hungryDurations[i]
            eatingDur = eatingDurations[i]
            dinerLevel = dinerLevels[i]
            self.dinerInfo[i] = (hungryDur, eatingDur, dinerLevel)

    def loadAssets(self):
        self.tableGroup = loader.loadModel("phase_12/models/bossbotHQ/BanquetTableChairs")
        tableLocator = self.boss.geom.find("**/TableLocator_%d" % (self.index + 1))
        if tableLocator.isEmpty():
            self.tableGroup.reparentTo(render)
            self.tableGroup.setPos(0, 75, 0)
        else:
            self.tableGroup.reparentTo(tableLocator)
        self.tableGeom = self.tableGroup.find("**/Geometry")
        self.setupDiners()
        self.setupChairCols()
        self.squirtSfx = loader.loadSfx("phase_4/audio/sfx/AA_squirt_seltzer_miss.mp3")
        self.hitBossSfx = loader.loadSfx("phase_5/audio/sfx/SA_watercooler_spray_only.mp3")
        self.hitBossSoundInterval = SoundInterval(self.hitBossSfx, node=self.boss, volume=1.0)
        self.serveFoodSfx = loader.loadSfx("phase_4/audio/sfx/MG_sfx_travel_game_bell_for_trolley.mp3")
        self.pitcherMoveSfx = base.loadSfx("phase_4/audio/sfx/MG_cannon_adjust.mp3")

    def setupDiners(self):
        for i in xrange(self.numDiners):
            newDiner = self.createDiner(i)
            self.diners[i] = newDiner
            self.dinerStatus[i] = self.HUNGRY

    def createDiner(self, i):
        diner = Suit.Suit()
        diner.dna = SuitDNA.SuitDNA()
        level = self.dinerInfo[i][2]
        level -= 4
        diner.dna.newSuitRandom(level=level, dept="c")
        diner.setDNA(diner.dna)
        if self.useNewAnimations:
            diner.loop("sit", fromFrame=i)
        else:
            diner.pose("landing", 0)
        locator = self.tableGroup.find("**/chair_%d" % (i + 1))
        locatorScale = locator.getNetTransform().getScale()[0]
        correctHeadingNp = locator.attachNewNode("correctHeading")
        self.chairLocators[i] = correctHeadingNp
        heading = self.rotationsPerSeatIndex[i]
        correctHeadingNp.setH(heading)
        sitLocator = correctHeadingNp.attachNewNode("sitLocator")
        base.sitLocator = sitLocator
        pos = correctHeadingNp.getPos(render)
        if SuitDNA.getSuitBodyType(diner.dna.name) == "c":
            sitLocator.setPos(0.5, 3.6499999999999999, -3.75)
        else:
            sitLocator.setZ(-2.3999999999999999)
            sitLocator.setY(2.5)
            sitLocator.setX(0.5)
        self.sitLocators[i] = sitLocator
        diner.setScale(1.0 / locatorScale)
        diner.reparentTo(sitLocator)
        newLoc = NodePath("serviceLoc-%d-%d" % (self.index, i))
        newLoc.reparentTo(correctHeadingNp)
        newLoc.setPos(0, 3.0, 1)
        self.serviceLocs[i] = newLoc
        base.serviceLoc = newLoc
        head = diner.find("**/joint_head")
        newIndicator = DinerStatusIndicator.DinerStatusIndicator(parent=head, pos=Point3(0, 0, 3.5), scale=5.0)
        newIndicator.wrtReparentTo(diner)
        self.dinerStatusIndicators[i] = newIndicator
        return diner

    def setupChairCols(self):
        for i in xrange(self.numDiners):
            chairCol = self.tableGroup.find("**/collision_chair_%d" % (i + 1))
            colName = "ChairCol-%d-%d" % (self.index, i)
            chairCol.setTag("chairIndex", str(i))
            chairCol.setName(colName)
            chairCol.setCollideMask(ToontownGlobals.WallBitmask)
            self.accept("enter" + colName, self.touchedChair)

    def touchedChair(self, colEntry):
        chairIndex = int(colEntry.getIntoNodePath().getTag("chairIndex"))
        if chairIndex in self.dinerStatus:
            status = self.dinerStatus[chairIndex]
            if status in (self.HUNGRY, self.ANGRY):
                self.boss.localToonTouchedChair(self.index, chairIndex)

    def serveFood(self, food, chairIndex):
        self.removeFoodModel(chairIndex)
        serviceLoc = self.serviceLocs.get(chairIndex)
        if not food or food.isEmpty():
            foodModel = loader.loadModel("phase_12/models/bossbotHQ/canoffood")
            foodModel.setScale(ToontownGlobals.BossbotFoodModelScale)
            foodModel.reparentTo(serviceLoc)
        else:
            food.wrtReparentTo(serviceLoc)
            tray = food.find("**/tray")
            if not tray.isEmpty():
                tray.hide()

            ivalDuration = 1.5
            foodMoveIval = Parallel(
                SoundInterval(self.serveFoodSfx, node=food),
                ProjectileInterval(
                    food, duration=ivalDuration, startPos=food.getPos(serviceLoc), endPos=serviceLoc.getPos(serviceLoc)
                ),
                LerpHprInterval(food, ivalDuration, Point3(0, -360, 0)),
            )
            intervalName = "serveFood-%d-%d" % (self.index, chairIndex)
            foodMoveIval.start()
            self.activeIntervals[intervalName] = foodMoveIval

    def setDinerStatus(self, chairIndex, status):
        if chairIndex in self.dinerStatus:
            oldStatus = self.dinerStatus[chairIndex]
            self.dinerStatus[chairIndex] = status
            if oldStatus != status:
                if status == self.EATING:
                    self.changeDinerToEating(chairIndex)
                elif status == self.HUNGRY:
                    self.changeDinerToHungry(chairIndex)
                elif status == self.ANGRY:
                    self.changeDinerToAngry(chairIndex)
                elif status == self.DEAD:
                    self.changeDinerToDead(chairIndex)
                elif status == self.HIDDEN:
                    self.changeDinerToHidden(chairIndex)

    def removeFoodModel(self, chairIndex):
        serviceLoc = self.serviceLocs.get(chairIndex)
        if serviceLoc:
            for i in xrange(serviceLoc.getNumChildren()):
                serviceLoc.getChild(0).removeNode()

    def changeDinerToEating(self, chairIndex):
        indicator = self.dinerStatusIndicators.get(chairIndex)
        eatingDuration = self.dinerInfo[chairIndex][1]
        if indicator:
            indicator.request("Eating", eatingDuration)

        diner = self.diners[chairIndex]
        intervalName = "eating-%d-%d" % (self.index, chairIndex)
        eatInTime = 32.0 / 24.0
        eatOutTime = 21.0 / 24.0
        eatLoopTime = 19 / 24.0
        rightHand = diner.getRightHand()
        waitTime = 5
        loopDuration = eatingDuration - eatInTime - eatOutTime - waitTime
        serviceLoc = self.serviceLocs[chairIndex]

        def foodAttach(self=self, diner=diner):
            foodModel = self.serviceLocs[chairIndex].getChild(0)
            (foodModel.reparentTo(diner.getRightHand()),)
            (foodModel.setHpr(Point3(0, -94, 0)),)
            (foodModel.setPos(Point3(-0.14999999999999999, -0.69999999999999996, -0.40000000000000002)),)
            scaleAdj = 1
            if SuitDNA.getSuitBodyType(diner.dna.name) == "c":
                scaleAdj = 0.59999999999999998
                (foodModel.setPos(Point3(0.10000000000000001, -0.25, -0.31)),)
            else:
                scaleAdj = 0.80000000000000004
                (foodModel.setPos(Point3(-0.25, -0.84999999999999998, -0.34000000000000002)),)
            oldScale = foodModel.getScale()
            newScale = oldScale * scaleAdj
            foodModel.setScale(newScale)

        def foodDetach(self=self, diner=diner):
            foodModel = diner.getRightHand().getChild(0)
            (foodModel.reparentTo(serviceLoc),)
            (foodModel.setPosHpr(0, 0, 0, 0, 0, 0),)
            scaleAdj = 1
            if SuitDNA.getSuitBodyType(diner.dna.name) == "c":
                scaleAdj = 0.59999999999999998
            else:
                scakeAdj = 0.80000000000000004
            oldScale = foodModel.getScale()
            newScale = oldScale / scaleAdj
            foodModel.setScale(newScale)

        eatIval = Sequence(
            ActorInterval(diner, "sit", duration=waitTime),
            ActorInterval(diner, "sit-eat-in", startFrame=0, endFrame=6),
            Func(foodAttach),
            ActorInterval(diner, "sit-eat-in", startFrame=6, endFrame=32),
            ActorInterval(diner, "sit-eat-loop", duration=loopDuration, loop=1),
            ActorInterval(diner, "sit-eat-out", startFrame=0, endFrame=12),
            Func(foodDetach),
            ActorInterval(diner, "sit-eat-out", startFrame=12, endFrame=21),
        )
        eatIval.start()
        self.activeIntervals[intervalName] = eatIval

    def changeDinerToHungry(self, chairIndex):
        intervalName = "eating-%d-%d" % (self.index, chairIndex)
        if intervalName in self.activeIntervals:
            self.activeIntervals[intervalName].finish()

        self.removeFoodModel(chairIndex)
        indicator = self.dinerStatusIndicators.get(chairIndex)
        if indicator:
            indicator.request("Hungry", self.dinerInfo[chairIndex][0])

        diner = self.diners[chairIndex]
        if random.choice([0, 1]):
            diner.loop("sit-hungry-left")
        else:
            diner.loop("sit-hungry-right")

    def changeDinerToAngry(self, chairIndex):
        self.removeFoodModel(chairIndex)
        indicator = self.dinerStatusIndicators.get(chairIndex)
        if indicator:
            indicator.request("Angry")

        diner = self.diners[chairIndex]
        diner.loop("sit-angry")

    def changeDinerToDead(self, chairIndex):
        def removeDeathSuit(suit, deathSuit):
            if not deathSuit.isEmpty():
                deathSuit.detachNode()
                suit.cleanupLoseActor()

        self.removeFoodModel(chairIndex)
        indicator = self.dinerStatusIndicators.get(chairIndex)
        if indicator:
            indicator.request("Dead")

        diner = self.diners[chairIndex]
        deathSuit = diner
        locator = self.tableGroup.find("**/chair_%d" % (chairIndex + 1))
        deathSuit = diner.getLoseActor()
        ival = Sequence(
            Func(self.notify.debug, "before actorinterval sit-lose"),
            ActorInterval(diner, "sit-lose"),
            Func(self.notify.debug, "before deathSuit.setHpr"),
            Func(deathSuit.setHpr, diner.getHpr()),
            Func(self.notify.debug, "before diner.hide"),
            Func(diner.hide),
            Func(self.notify.debug, "before deathSuit.reparentTo"),
            Func(deathSuit.reparentTo, self.chairLocators[chairIndex]),
            Func(self.notify.debug, "befor ActorInterval lose"),
            ActorInterval(deathSuit, "lose", duration=MovieUtil.SUIT_LOSE_DURATION),
            Func(self.notify.debug, "before remove deathsuit"),
            Func(removeDeathSuit, diner, deathSuit, name="remove-death-suit-%d-%d" % (chairIndex, self.index)),
            Func(self.notify.debug, "diner.stash"),
            Func(diner.stash),
        )
        spinningSound = base.loadSfx("phase_3.5/audio/sfx/Cog_Death.mp3")
        deathSound = base.loadSfx("phase_3.5/audio/sfx/ENC_cogfall_apart.mp3")
        deathSoundTrack = Sequence(
            Wait(0.80000000000000004),
            SoundInterval(spinningSound, duration=1.2, startTime=1.5, volume=0.20000000000000001, node=deathSuit),
            SoundInterval(
                spinningSound, duration=3.0, startTime=0.59999999999999998, volume=0.80000000000000004, node=deathSuit
            ),
            SoundInterval(deathSound, volume=0.32000000000000001, node=deathSuit),
        )
        intervalName = "dinerDie-%d-%d" % (self.index, chairIndex)
        deathIval = Parallel(ival, deathSoundTrack)
        deathIval.start()
        self.activeIntervals[intervalName] = deathIval

    def changeDinerToHidden(self, chairIndex):
        self.removeFoodModel(chairIndex)
        indicator = self.dinerStatusIndicators.get(chairIndex)
        if indicator:
            indicator.request("Inactive")

        diner = self.diners[chairIndex]
        diner.hide()

    def setAllDinersToSitNeutral(self):
        startFrame = 0
        for diner in self.diners.values():
            if not diner.isHidden():
                diner.loop("sit", fromFrame=startFrame)
                startFrame += 1
                continue

    def cleanupIntervals(self):
        for interval in self.activeIntervals.values():
            interval.finish()

        self.activeIntervals = {}

    def clearInterval(self, name, finish=1):
        if self.activeIntervals.has_key(name):
            ival = self.activeIntervals[name]
            if finish:
                ival.finish()
            else:
                ival.pause()
            if self.activeIntervals.has_key(name):
                del self.activeIntervals[name]

        else:
            self.notify.debug("interval: %s already cleared" % name)

    def finishInterval(self, name):
        if self.activeIntervals.has_key(name):
            interval = self.activeIntervals[name]
            interval.finish()

    def getNotDeadInfo(self):
        notDeadList = []
        for i in xrange(self.numDiners):
            if self.dinerStatus[i] != self.DEAD:
                notDeadList.append((self.index, i, 12))
                continue

        return notDeadList

    def enterOn(self):
        pass

    def exitOn(self):
        pass

    def enterInactive(self):
        for chairIndex in xrange(self.numDiners):
            indicator = self.dinerStatusIndicators.get(chairIndex)
            if indicator:
                indicator.request("Inactive")

            self.removeFoodModel(chairIndex)

    def exitInactive(self):
        pass

    def enterFree(self):
        self.resetPowerBar()
        if self.fadeTrack:
            self.fadeTrack.finish()
            self.fadeTrack = None

        self.prepareForPhaseFour()
        if self.avId == localAvatar.doId:
            self.tableGroup.setAlphaScale(0.29999999999999999)
            self.tableGroup.setTransparency(1)
            taskMgr.doMethodLater(5, self._DistributedBanquetTable__allowDetect, self.triggerName)
            self.fadeTrack = Sequence(
                Func(self.tableGroup.setTransparency, 1),
                self.tableGroup.colorScaleInterval(0.20000000000000001, VBase4(1, 1, 1, 0.29999999999999999)),
            )
            self.fadeTrack.start()
            self.allowLocalRequestControl = False
        else:
            self.allowLocalRequestControl = True
        self.avId = 0

    def exitFree(self):
        pass

    def touchedTable(self, colEntry):
        tableIndex = int(colEntry.getIntoNodePath().getTag("tableIndex"))
        if self.state == "Free" and self.avId == 0 and self.allowLocalRequestControl:
            self.d_requestControl()

    def prepareForPhaseFour(self):
        if not self.preparedForPhaseFour:
            for i in xrange(8):
                chair = self.tableGroup.find("**/chair_%d" % (i + 1))
                if not chair.isEmpty():
                    chair.hide()

                colChairs = self.tableGroup.findAllMatches("**/ChairCol*")
                for i in xrange(colChairs.getNumPaths()):
                    col = colChairs.getPath(i)
                    col.stash()

                colChairs = self.tableGroup.findAllMatches("**/collision_chair*")
                for i in xrange(colChairs.getNumPaths()):
                    col = colChairs.getPath(i)
                    col.stash()

            tableCol = self.tableGroup.find("**/collision_table")
            colName = "TableCol-%d" % self.index
            tableCol.setTag("tableIndex", str(self.index))
            tableCol.setName(colName)
            tableCol.setCollideMask(ToontownGlobals.WallBitmask | ToontownGlobals.BanquetTableBitmask)
            self.accept("enter" + colName, self.touchedTable)
            self.preparedForPhaseFour = True
            self.waterPitcherModel = loader.loadModel("phase_12/models/bossbotHQ/tt_m_ara_bhq_seltzerBottle")
            lampNode = self.tableGroup.find("**/lamp_med_5")
            pos = lampNode.getPos(self.tableGroup)
            lampNode.hide()
            bottleLocator = self.tableGroup.find("**/bottle_locator")
            pos = bottleLocator.getPos(self.tableGroup)
            self.waterPitcherNode = self.tableGroup.attachNewNode("pitcherNode")
            self.waterPitcherNode.setPos(pos)
            self.waterPitcherModel.reparentTo(self.waterPitcherNode)
            self.waterPitcherModel.ls()
            self.nozzle = self.waterPitcherModel.find("**/nozzle_tip")
            self.handLocator = self.waterPitcherModel.find("**/hand_locator")
            self.handPos = self.handLocator.getPos()

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

    def d_requestFree(self, gotHitByBoss):
        self.sendUpdate("requestFree", [gotHitByBoss])

    def enterControlled(self, avId):
        self.prepareForPhaseFour()
        self.avId = avId
        toon = base.cr.doId2do.get(avId)
        if not toon:
            return None

        self.toon = toon
        self.grabTrack = self.makeToonGrabInterval(toon)
        self.notify.debug("grabTrack=%s" % self.grabTrack)
        self.pitcherCamPos = Point3(0, -50, 40)
        self.pitcherCamHpr = Point3(0, -21, 0)
        if avId == localAvatar.doId:
            self.boss.toMovieMode()
            self._DistributedBanquetTable__enableControlInterface()
            self.startPosHprBroadcast()
            self.grabTrack = Sequence(
                self.grabTrack,
                Func(camera.wrtReparentTo, localAvatar),
                LerpPosHprInterval(camera, 1, self.pitcherCamPos, self.pitcherCamHpr),
                Func(self.boss.toCraneMode),
            )
            if self.TugOfWarControls:
                self._DistributedBanquetTable__spawnUpdateKeyPressRateTask()

            self.accept("exitCrane", self.gotBossZapped)
        else:
            self.startSmooth()
            toon.stopSmooth()
        self.grabTrack.start()

    def exitControlled(self):
        self.ignore("exitCrane")
        if self.grabTrack:
            self.grabTrack.finish()
            self.grabTrack = None

        nextState = self.getCurrentOrNextState()
        self.notify.debug("nextState=%s" % nextState)
        if nextState == "Flat":
            place = base.cr.playGame.getPlace()
            self.notify.debug("%s" % place.fsm)
            if self.avId == localAvatar.doId:
                self._DistributedBanquetTable__disableControlInterface()

        elif self.toon and not self.toon.isDisabled():
            self.toon.loop("neutral")
            self.toon.startSmooth()

        self.releaseTrack = self.makeToonReleaseInterval(self.toon)
        self.stopPosHprBroadcast()
        self.stopSmooth()
        if self.avId == localAvatar.doId:
            localAvatar.wrtReparentTo(render)
            self._DistributedBanquetTable__disableControlInterface()
            camera.reparentTo(base.localAvatar)
            camera.setPos(base.localAvatar.cameraPositions[0][0])
            camera.setHpr(0, 0, 0)
            self.goToFinalBattle()
            self.safeBossToFinalBattleMode()
        else:
            toon = base.cr.doId2do.get(self.avId)
            if toon:
                toon.wrtReparentTo(render)

        self.releaseTrack.start()

    def safeBossToFinalBattleMode(self):
        if self.boss:
            self.boss.toFinalBattleMode()

    def goToFinalBattle(self):
        if self.cr:
            place = self.cr.playGame.getPlace()
            if place and hasattr(place, "fsm"):
                if place.fsm.getCurrentState().getName() == "crane":
                    place.setState("finalBattle")

    def makeToonGrabInterval(self, toon):
        toon.pose("leverNeutral", 0)
        toon.update()
        rightHandPos = toon.rightHand.getPos(toon)
        self.toonPitcherPosition = Point3(self.handPos[0] - rightHandPos[0], self.handPos[1] - rightHandPos[1], 0)
        destZScale = rightHandPos[2] / self.handPos[2]
        grabIval = Sequence(
            Func(toon.wrtReparentTo, self.waterPitcherNode),
            Func(toon.loop, "neutral"),
            Parallel(
                ActorInterval(toon, "jump"),
                Sequence(
                    Wait(0.42999999999999999),
                    Parallel(
                        ProjectileInterval(
                            toon,
                            duration=0.90000000000000002,
                            startPos=toon.getPos(self.waterPitcherNode),
                            endPos=self.toonPitcherPosition,
                        ),
                        LerpHprInterval(toon, 0.90000000000000002, Point3(0, 0, 0)),
                        LerpScaleInterval(self.waterPitcherModel, 0.90000000000000002, Point3(1, 1, destZScale)),
                    ),
                ),
            ),
            Func(toon.setPos, self.toonPitcherPosition),
            Func(toon.loop, "leverNeutral"),
        )
        return grabIval

    def makeToonReleaseInterval(self, toon):
        temp1 = self.waterPitcherNode.attachNewNode("temp1")
        temp1.setPos(self.toonPitcherPosition)
        temp2 = self.waterPitcherNode.attachNewNode("temp2")
        temp2.setPos(0, -10, -self.waterPitcherNode.getZ())
        startPos = temp1.getPos(render)
        endPos = temp2.getPos(render)
        temp1.removeNode()
        temp2.removeNode()

        def getSlideToPos(toon=toon):
            return render.getRelativePoint(toon, Point3(0, -10, 0))

        if self.gotHitByBoss:
            self.notify.debug("creating zap interval instead")
            grabIval = Sequence(
                Func(toon.loop, "neutral"),
                Func(toon.wrtReparentTo, render),
                Parallel(ActorInterval(toon, "slip-backward"), toon.posInterval(0.5, getSlideToPos, fluid=1)),
            )
        else:
            grabIval = Sequence(
                Func(toon.loop, "neutral"),
                Func(toon.wrtReparentTo, render),
                Parallel(
                    ActorInterval(toon, "jump"),
                    Sequence(
                        Wait(0.42999999999999999),
                        ProjectileInterval(toon, duration=0.90000000000000002, startPos=startPos, endPos=endPos),
                    ),
                ),
            )
        return grabIval

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

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

    def clearSmoothing(self, bogus=None):
        self.pitcherSmoother.clearPositions(1)

    def doSmoothTask(self, task):
        self.pitcherSmoother.computeAndApplySmoothHpr(self.waterPitcherNode)
        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 _DistributedBanquetTable__enableControlInterface(self):
        gui = loader.loadModel("phase_3.5/models/gui/avatar_panel_gui")
        self.closeButton = DirectButton(
            image=(
                gui.find("**/CloseBtn_UP"),
                gui.find("**/CloseBtn_DN"),
                gui.find("**/CloseBtn_Rllvr"),
                gui.find("**/CloseBtn_UP"),
            ),
            relief=None,
            scale=2,
            text=TTLocalizer.BossbotPitcherLeave,
            text_scale=0.040000000000000001,
            text_pos=(0, -0.070000000000000007),
            text_fg=VBase4(1, 1, 1, 1),
            pos=(1.05, 0, -0.81999999999999995),
            command=self._DistributedBanquetTable__exitPitcher,
        )
        self.accept("escape", self._DistributedBanquetTable__exitPitcher)
        self.accept("control", self._DistributedBanquetTable__controlPressed)
        self.accept("control-up", self._DistributedBanquetTable__controlReleased)
        self.accept("InputState-forward", self._DistributedBanquetTable__upArrow)
        self.accept("InputState-reverse", self._DistributedBanquetTable__downArrow)
        self.accept("InputState-turnLeft", self._DistributedBanquetTable__leftArrow)
        self.accept("InputState-turnRight", self._DistributedBanquetTable__rightArrow)
        self.accept("arrow_up", self._DistributedBanquetTable__upArrowKeyPressed)
        self.accept("arrow_down", self._DistributedBanquetTable__downArrowKeyPressed)
        taskMgr.add(self._DistributedBanquetTable__watchControls, self.watchControlsName)
        taskMgr.doMethodLater(5, self._DistributedBanquetTable__displayPitcherAdvice, self.pitcherAdviceName)
        self.arrowVert = 0
        self.arrowHorz = 0
        self.powerBar.show()

    def _DistributedBanquetTable__disableControlInterface(self):
        if self.closeButton:
            self.closeButton.destroy()
            self.closeButton = None

        self._DistributedBanquetTable__cleanupPitcherAdvice()
        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.ignore("arrow_up")
        self.ignore("arrow_down")
        self.arrowVert = 0
        self.arrowHorz = 0
        taskMgr.remove(self.watchControlsName)
        taskMgr.remove(self.waterPowerTaskName)
        self.resetPowerBar()
        self.aimStart = None
        self.powerBar.hide()
        if self.TugOfWarControls:
            self._DistributedBanquetTable__killUpdateKeyPressRateTask()
            self.keyTTL = []

        self._DistributedBanquetTable__setMoveSound(None)

    def _DistributedBanquetTable__displayPitcherAdvice(self, task):
        if self.pitcherAdviceLabel == None:
            self.pitcherAdviceLabel = DirectLabel(
                text=TTLocalizer.BossbotPitcherAdvice,
                text_fg=VBase4(1, 1, 1, 1),
                text_align=TextNode.ACenter,
                relief=None,
                pos=(0, 0, 0.68999999999999995),
                scale=0.10000000000000001,
            )

    def _DistributedBanquetTable__cleanupPitcherAdvice(self):
        if self.pitcherAdviceLabel:
            self.pitcherAdviceLabel.destroy()
            self.pitcherAdviceLabel = None

        taskMgr.remove(self.pitcherAdviceName)

    def showExiting(self):
        if self.closeButton:
            self.closeButton.destroy()
            self.closeButton = DirectLabel(
                relief=None,
                text=TTLocalizer.BossbotPitcherLeaving,
                pos=(1.05, 0, -0.88),
                text_pos=(0, 0),
                text_scale=0.059999999999999998,
                text_fg=VBase4(1, 1, 1, 1),
            )

        self._DistributedBanquetTable__cleanupPitcherAdvice()

    def _DistributedBanquetTable__exitPitcher(self):
        self.showExiting()
        self.d_requestFree(False)

    def _DistributedBanquetTable__controlPressed(self):
        self._DistributedBanquetTable__cleanupPitcherAdvice()
        if self.TugOfWarControls:
            if self.power:
                self.aimStart = 1
                self._DistributedBanquetTable__endFireWater()

        elif self.state == "Controlled":
            self._DistributedBanquetTable__beginFireWater()

    def _DistributedBanquetTable__controlReleased(self):
        if self.TugOfWarControls:
            pass
        1
        if self.state == "Controlled":
            self._DistributedBanquetTable__endFireWater()

    def _DistributedBanquetTable__upArrow(self, pressed):
        self._DistributedBanquetTable__incrementChangeSeq()
        self._DistributedBanquetTable__cleanupPitcherAdvice()
        if pressed:
            self.arrowVert = 1
        elif self.arrowVert > 0:
            self.arrowVert = 0

    def _DistributedBanquetTable__downArrow(self, pressed):
        self._DistributedBanquetTable__incrementChangeSeq()
        self._DistributedBanquetTable__cleanupPitcherAdvice()
        if pressed:
            self.arrowVert = -1
        elif self.arrowVert < 0:
            self.arrowVert = 0

    def _DistributedBanquetTable__rightArrow(self, pressed):
        self._DistributedBanquetTable__incrementChangeSeq()
        self._DistributedBanquetTable__cleanupPitcherAdvice()
        if pressed:
            self.arrowHorz = 1
        elif self.arrowHorz > 0:
            self.arrowHorz = 0

    def _DistributedBanquetTable__leftArrow(self, pressed):
        self._DistributedBanquetTable__incrementChangeSeq()
        self._DistributedBanquetTable__cleanupPitcherAdvice()
        if pressed:
            self.arrowHorz = -1
        elif self.arrowHorz < 0:
            self.arrowHorz = 0

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

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

    def startPosHprBroadcast(self):
        taskName = self.posHprBroadcastName
        self.b_clearSmoothing()
        self.d_sendPitcherPos()
        taskMgr.remove(taskName)
        taskMgr.doMethodLater(
            self._DistributedBanquetTable__broadcastPeriod, self._DistributedBanquetTable__posHprBroadcast, taskName
        )

    def _DistributedBanquetTable__posHprBroadcast(self, task):
        self.d_sendPitcherPos()
        taskName = self.posHprBroadcastName
        taskMgr.doMethodLater(
            self._DistributedBanquetTable__broadcastPeriod, self._DistributedBanquetTable__posHprBroadcast, taskName
        )
        return Task.done

    def d_sendPitcherPos(self):
        timestamp = globalClockDelta.getFrameNetworkTime()
        self.sendUpdate("setPitcherPos", [self.changeSeq, self.waterPitcherNode.getH(), timestamp])

    def setPitcherPos(self, changeSeq, h, timestamp):
        self.changeSeq = changeSeq
        if self.smoothStarted:
            now = globalClock.getFrameTime()
            local = globalClockDelta.networkToLocalTime(timestamp, now)
            self.pitcherSmoother.setH(h)
            self.pitcherSmoother.setTimestamp(local)
            self.pitcherSmoother.markPosition()
        else:
            self.waterPitcherNode.setH(h)

    def _DistributedBanquetTable__watchControls(self, task):
        if self.arrowHorz:
            self._DistributedBanquetTable__movePitcher(self.arrowHorz)
        else:
            self._DistributedBanquetTable__setMoveSound(None)
        return Task.cont

    def _DistributedBanquetTable__movePitcher(self, xd):
        dt = globalClock.getDt()
        h = self.waterPitcherNode.getH() - xd * self.rotateSpeed * dt
        h %= 360
        self.notify.debug(
            "rotSpeed=%.2f curH=%.2f  xd =%.2f, dt = %.2f, h=%.2f"
            % (self.rotateSpeed, self.waterPitcherNode.getH(), xd, dt, h)
        )
        limitH = h
        self.waterPitcherNode.setH(limitH)
        if xd:
            self._DistributedBanquetTable__setMoveSound(self.pitcherMoveSfx)

    def reloadPosition(self):
        self.pitcherSmoother.clearPositions(0)
        self.pitcherSmoother.setHpr(self.waterPitcherNode.getHpr())
        self.pitcherSmoother.setPhonyTimestamp()

    def forceToTruePosition(self):
        if self.pitcherSmoother.getLatestPosition():
            self.pitcherSmoother.applySmoothHpr(self.waterPitcherNode)

        self.pitcherSmoother.clearPositions(1)

    def getSprayTrack(
        self, color, origin, target, dScaleUp, dHold, dScaleDown, horizScale=1.0, vertScale=1.0, parent=render
    ):
        track = Sequence()
        SPRAY_LEN = 1.5
        sprayProp = MovieUtil.globalPropPool.getProp("spray")
        sprayScale = hidden.attachNewNode("spray-parent")
        sprayRot = hidden.attachNewNode("spray-rotate")
        spray = sprayRot
        spray.setColor(color)
        if color[3] < 1.0:
            spray.setTransparency(1)

        def showSpray(sprayScale, sprayRot, sprayProp, origin, target, parent):
            if callable(origin):
                origin = origin()

            if callable(target):
                target = target()

            sprayRot.reparentTo(parent)
            sprayRot.clearMat()
            sprayScale.reparentTo(sprayRot)
            sprayScale.clearMat()
            sprayProp.reparentTo(sprayScale)
            sprayProp.clearMat()
            sprayRot.setPos(origin)
            sprayRot.lookAt(Point3(target))

        track.append(Func(showSpray, sprayScale, sprayRot, sprayProp, origin, target, parent))

        def calcTargetScale(target=target, origin=origin, horizScale=horizScale, vertScale=vertScale):
            if callable(target):
                target = target()

            if callable(origin):
                origin = origin()

            distance = Vec3(target - origin).length()
            yScale = distance / SPRAY_LEN
            targetScale = Point3(yScale * horizScale, yScale, yScale * vertScale)
            return targetScale

        track.append(LerpScaleInterval(sprayScale, dScaleUp, calcTargetScale, startScale=Point3(0.01, 0.01, 0.01)))
        track.append(Func(self.checkHitObject))
        track.append(Wait(dHold))

        def prepareToShrinkSpray(spray, sprayProp, origin, target):
            if callable(target):
                target = target()

            if callable(origin):
                origin = origin()

            sprayProp.setPos(Point3(0.0, -SPRAY_LEN, 0.0))
            spray.setPos(target)

        track.append(Func(prepareToShrinkSpray, spray, sprayProp, origin, target))
        track.append(LerpScaleInterval(sprayScale, dScaleDown, Point3(0.01, 0.01, 0.01)))

        def hideSpray(spray, sprayScale, sprayRot, sprayProp, propPool):
            sprayProp.detachNode()
            MovieUtil.removeProp(sprayProp)
            sprayRot.removeNode()
            sprayScale.removeNode()

        track.append(Func(hideSpray, spray, sprayScale, sprayRot, sprayProp, MovieUtil.globalPropPool))
        return track

    def checkHitObject(self):
        if not self.hitObject:
            return None

        if self.avId != base.localAvatar.doId:
            return None

        tag = self.hitObject.getNetTag("pieCode")
        pieCode = int(tag)
        if pieCode == ToontownGlobals.PieCodeBossCog:
            self.hitBossSoundInterval.start()
            self.sendUpdate("waterHitBoss", [self.index])
            if self.TugOfWarControls:
                damage = 1
                if self.lastPowerFired < self.YELLOW_POWER_THRESHOLD:
                    damage = 1
                elif self.lastPowerFired < self.RED_POWER_THRESHOLD:
                    damage = 2
                else:
                    damage = 3
                self.boss.d_hitBoss(damage)
            else:
                damage = 1
                if self.lastPowerFired < self.YELLOW_POWER_THRESHOLD:
                    damage = 1
                elif self.lastPowerFired < self.RED_POWER_THRESHOLD:
                    damage = 2
                else:
                    damage = 3
                self.boss.d_hitBoss(damage)

    def waterHitBoss(self, tableIndex):
        if self.index == tableIndex:
            self.hitBossSoundInterval.start()

    def setupPowerBar(self):
        self.powerBar = DirectWaitBar(
            pos=(0.0, 0, -0.93999999999999995),
            relief=DGG.SUNKEN,
            frameSize=(-2.0, 2.0, -0.20000000000000001, 0.20000000000000001),
            borderWidth=(0.02, 0.02),
            scale=0.25,
            range=1,
            sortOrder=50,
            frameColor=(0.5, 0.5, 0.5, 0.5),
            barColor=(0.75, 0.75, 1.0, 0.80000000000000004),
            text="",
            text_scale=0.26000000000000001,
            text_fg=(1, 1, 1, 1),
            text_align=TextNode.ACenter,
            text_pos=(0, -0.050000000000000003),
        )
        self.power = 0
        self.powerBar["value"] = self.power
        self.powerBar.hide()

    def resetPowerBar(self):
        self.power = 0
        self.powerBar["value"] = self.power
        self.powerBar["text"] = ""
        self.keyTTL = []

    def _DistributedBanquetTable__beginFireWater(self):
        if self.fireTrack and self.fireTrack.isPlaying():
            return None

        if self.aimStart != None:
            return None

        if not self.state == "Controlled":
            return None

        if not self.avId == localAvatar.doId:
            return None

        time = globalClock.getFrameTime()
        self.aimStart = time
        messenger.send("wakeup")
        taskMgr.add(self._DistributedBanquetTable__updateWaterPower, self.waterPowerTaskName)

    def _DistributedBanquetTable__endFireWater(self):
        if self.aimStart == None:
            return None

        if not self.state == "Controlled":
            return None

        if not self.avId == localAvatar.doId:
            return None

        taskMgr.remove(self.waterPowerTaskName)
        messenger.send("wakeup")
        self.aimStart = None
        origin = self.nozzle.getPos(render)
        target = self.boss.getPos(render)
        angle = deg2Rad(self.waterPitcherNode.getH() + 90)
        x = math.cos(angle)
        y = math.sin(angle)
        fireVector = Point3(x, y, 0)
        if self.power < 0.001:
            self.power = 0.001

        self.lastPowerFired = self.power
        fireVector *= self.fireLength * self.power
        target = origin + fireVector
        segment = CollisionSegment(origin[0], origin[1], origin[2], target[0], target[1], target[2])
        fromObject = render.attachNewNode(CollisionNode("pitcherColNode"))
        fromObject.node().addSolid(segment)
        fromObject.node().setFromCollideMask(
            ToontownGlobals.PieBitmask | ToontownGlobals.CameraBitmask | ToontownGlobals.FloorBitmask
        )
        fromObject.node().setIntoCollideMask(BitMask32.allOff())
        queue = CollisionHandlerQueue()
        base.cTrav.addCollider(fromObject, queue)
        base.cTrav.traverse(render)
        queue.sortEntries()
        self.hitObject = None
        if queue.getNumEntries():
            entry = queue.getEntry(0)
            target = entry.getSurfacePoint(render)
            self.hitObject = entry.getIntoNodePath()

        base.cTrav.removeCollider(fromObject)
        fromObject.removeNode()
        self.d_firingWater(origin, target)
        self.fireWater(origin, target)
        self.resetPowerBar()

    def _DistributedBanquetTable__updateWaterPower(self, task):
        if not self.powerBar:
            print "### no power bar!!!"
            return task.done

        newPower = self._DistributedBanquetTable__getWaterPower(globalClock.getFrameTime())
        self.power = newPower
        self.powerBar["value"] = newPower
        if self.power < self.YELLOW_POWER_THRESHOLD:
            self.powerBar["barColor"] = VBase4(0.75, 0.75, 1.0, 0.80000000000000004)
        elif self.power < self.RED_POWER_THRESHOLD:
            self.powerBar["barColor"] = VBase4(1.0, 1.0, 0.0, 0.80000000000000004)
        else:
            self.powerBar["barColor"] = VBase4(1.0, 0.0, 0.0, 0.80000000000000004)
        return task.cont

    def _DistributedBanquetTable__getWaterPower(self, time):
        elapsed = max(time - self.aimStart, 0.0)
        t = elapsed / self.waterPowerSpeed
        exponent = self.waterPowerExponent
        if t > 1:
            t = t % 1

        power = 1 - math.pow(1 - t, exponent)
        if power > 1.0:
            power = 1.0

        return power

    def d_firingWater(self, origin, target):
        self.sendUpdate("firingWater", [origin[0], origin[1], origin[2], target[0], target[1], target[2]])

    def firingWater(self, startX, startY, startZ, endX, endY, endZ):
        origin = Point3(startX, startY, startZ)
        target = Point3(endX, endY, endZ)
        self.fireWater(origin, target)

    def fireWater(self, origin, target):
        color = VBase4(0.75, 0.75, 1, 0.80000000000000004)
        dScaleUp = 0.10000000000000001
        dHold = 0.29999999999999999
        dScaleDown = 0.10000000000000001
        horizScale = 0.10000000000000001
        vertScale = 0.10000000000000001
        sprayTrack = self.getSprayTrack(color, origin, target, dScaleUp, dHold, dScaleDown, horizScale, vertScale)
        duration = self.squirtSfx.length()
        if sprayTrack.getDuration() < duration:
            duration = sprayTrack.getDuration()

        soundTrack = SoundInterval(self.squirtSfx, node=self.waterPitcherModel, duration=duration)
        self.fireTrack = Parallel(sprayTrack, soundTrack)
        self.fireTrack.start()

    def getPos(self, wrt=render):
        return self.tableGroup.getPos(wrt)

    def getLocator(self):
        return self.tableGroup

    def enterFlat(self, avId):
        self.prepareForPhaseFour()
        self.resetPowerBar()
        self.notify.debug("enterFlat %d" % self.index)
        if self.avId:
            toon = base.cr.doId2do.get(self.avId)
            if toon:
                toon.wrtReparentTo(render)
                toon.setZ(0)

        self.tableGroup.setScale(1, 1, 0.01)
        if self.avId and self.avId == localAvatar.doId:
            localAvatar.b_squish(ToontownGlobals.BossCogDamageLevels[ToontownGlobals.BossCogMoveAttack])

    def exitFlat(self):
        self.tableGroup.setScale(1.0)
        if self.avId:
            toon = base.cr.doId2do.get(self.avId)
            if toon:
                if toon == localAvatar:
                    self.boss.toCraneMode()
                    toon.b_setAnimState("neutral")

                toon.setAnimState("neutral")
                toon.loop("leverNeutral")

    def _DistributedBanquetTable__allowDetect(self, task):
        if self.fadeTrack:
            self.fadeTrack.finish()

        self.fadeTrack = Sequence(
            self.tableGroup.colorScaleInterval(0.20000000000000001, VBase4(1, 1, 1, 1)),
            Func(self.tableGroup.clearColorScale),
            Func(self.tableGroup.clearTransparency),
        )
        self.fadeTrack.start()
        self.allowLocalRequestControl = True

    def gotBossZapped(self):
        self.showExiting()
        self.d_requestFree(True)

    def _DistributedBanquetTable__upArrowKeyPressed(self):
        if self.TugOfWarControls:
            self._DistributedBanquetTable__pressHandler(0)

    def _DistributedBanquetTable__downArrowKeyPressed(self):
        if self.TugOfWarControls:
            self._DistributedBanquetTable__pressHandler(1)

    def _DistributedBanquetTable__pressHandler(self, index):
        if index == self.buttons[0]:
            self.keyTTL.insert(0, 1.0)
            if not self.OnlyUpArrow:
                self.buttons.reverse()

    def _DistributedBanquetTable__spawnUpdateKeyPressRateTask(self):
        taskMgr.remove(self.taskName(self.UPDATE_KEY_PRESS_RATE_TASK))
        taskMgr.doMethodLater(
            0.10000000000000001,
            self._DistributedBanquetTable__updateKeyPressRateTask,
            self.taskName(self.UPDATE_KEY_PRESS_RATE_TASK),
        )

    def _DistributedBanquetTable__killUpdateKeyPressRateTask(self):
        taskMgr.remove(self.taskName(self.UPDATE_KEY_PRESS_RATE_TASK))

    def _DistributedBanquetTable__updateKeyPressRateTask(self, task):
        if self.state not in "Controlled":
            return Task.done

        for i in range(len(self.keyTTL)):
            self.keyTTL[i] -= 0.10000000000000001

        for i in range(len(self.keyTTL)):
            if self.keyTTL[i] <= 0:
                a = self.keyTTL[0:i]
                del self.keyTTL
                self.keyTTL = a
                break
                continue

        self.keyRate = len(self.keyTTL)
        keyRateDiff = self.keyRate - self.BASELINE_KEY_RATE
        diffPower = keyRateDiff / 300.0
        if self.power < 1 and diffPower > 0:
            diffPower = diffPower * math.pow(1 - self.power, 1.25)

        newPower = self.power + diffPower
        if newPower > 1:
            newPower = 1
        elif newPower < 0:
            newPower = 0

        self.notify.debug("diffPower=%.2f keyRate = %d, newPower=%.2f" % (diffPower, self.keyRate, newPower))
        self.power = newPower
        self.powerBar["value"] = newPower
        if self.power < self.YELLOW_POWER_THRESHOLD:
            self.powerBar["barColor"] = VBase4(0.75, 0.75, 1.0, 0.80000000000000004)
        elif self.power < self.RED_POWER_THRESHOLD:
            self.powerBar["barColor"] = VBase4(1.0, 1.0, 0.0, 0.80000000000000004)
        else:
            self.powerBar["barColor"] = VBase4(1.0, 0.0, 0.0, 0.80000000000000004)
        self._DistributedBanquetTable__spawnUpdateKeyPressRateTask()
        return Task.done

    def _DistributedBanquetTable__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)
class DistributedGolfSpot(DistributedObject.DistributedObject, FSM.FSM):
    notify = DirectNotifyGlobal.directNotify.newCategory('DistributedGolfSpot')
    positions = ((-45, 100, GolfGlobals.GOLF_BALL_RADIUS),
     (-15, 100, GolfGlobals.GOLF_BALL_RADIUS),
     (15, 100, GolfGlobals.GOLF_BALL_RADIUS),
     (45, 100, GolfGlobals.GOLF_BALL_RADIUS))
    toonGolfOffsetPos = Point3(-2, 0, -GolfGlobals.GOLF_BALL_RADIUS)
    toonGolfOffsetHpr = Point3(-90, 0, 0)
    rotateSpeed = 20
    golfPowerSpeed = base.config.GetDouble('golf-power-speed', 3)
    golfPowerExponent = base.config.GetDouble('golf-power-exponent', 0.75)

    def __init__(self, cr):
        DistributedObject.DistributedObject.__init__(self, cr)
        FSM.FSM.__init__(self, 'DistributedGolfSpot')
        self.boss = None
        self.index = None
        self.avId = 0
        self.toon = None
        self.golfSpotSmoother = SmoothMover()
        self.golfSpotSmoother.setSmoothMode(SmoothMover.SMOn)
        self.smoothStarted = 0
        self.__broadcastPeriod = 0.2
        if self.index > len(self.positions):
            self.notify.error('Invalid index %d' % index)
        self.fadeTrack = None
        self.setupPowerBar()
        self.aimStart = None
        self.golfSpotAdviceLabel = None
        self.changeSeq = 0
        self.lastChangeSeq = 0
        self.controlKeyAllowed = False
        self.flyBallTracks = {}
        self.splatTracks = {}
        self.__flyBallBubble = None
        self.flyBallHandler = None
        self.__flyBallSequenceNum = 0
        self.swingInterval = None
        self.lastHitSequenceNum = -1
        self.goingToReward = False
        self.gotHitByBoss = False
        self.releaseTrack = None
        self.grabTrack = None
        self.restoreScaleTrack = None
        return

    def setBossCogId(self, bossCogId):
        self.bossCogId = bossCogId
        self.boss = base.cr.doId2do[bossCogId]
        self.boss.setGolfSpot(self, self.index)

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

    def disable(self):
        DistributedObject.DistributedObject.disable(self)
        self.ignoreAll()

    def delete(self):
        DistributedObject.DistributedObject.delete(self)
        self.ignoreAll()
        self.boss = None
        return

    def announceGenerate(self):
        DistributedObject.DistributedObject.announceGenerate(self)
        self.triggerName = self.uniqueName('trigger')
        self.triggerEvent = 'enter%s' % self.triggerName
        self.smoothName = self.uniqueName('golfSpotSmooth')
        self.golfSpotAdviceName = self.uniqueName('golfSpotAdvice')
        self.posHprBroadcastName = self.uniqueName('golfSpotBroadcast')
        self.ballPowerTaskName = self.uniqueName('updateGolfPower')
        self.adjustClubTaskName = self.uniqueName('adjustClub')
        self.loadAssets()
        self.accept('flyBallHit-%d' % self.index, self.__flyBallHit)

    def loadAssets(self):
        self.root = render.attachNewNode('golfSpot-%d' % self.index)
        self.root.setPos(*self.positions[self.index])
        self.ballModel = loader.loadModel('phase_6/models/golf/golf_ball')
        self.ballColor = VBase4(1, 1, 1, 1)
        if self.index < len(GolfGlobals.PlayerColors):
            self.ballColor = VBase4(*GolfGlobals.PlayerColors[self.index])
            self.ballModel.setColorScale(self.ballColor)
        self.ballModel.reparentTo(self.root)
        self.club = loader.loadModel('phase_6/models/golf/putter')
        self.clubLookatSpot = self.root.attachNewNode('clubLookat')
        self.clubLookatSpot.setY(-(GolfGlobals.GOLF_BALL_RADIUS + 0.1))
        cs = CollisionSphere(0, 0, 0, 1)
        cs.setTangible(0)
        cn = CollisionNode(self.triggerName)
        cn.addSolid(cs)
        cn.setIntoCollideMask(ToontownGlobals.WallBitmask)
        self.trigger = self.root.attachNewNode(cn)
        self.trigger.stash()
        self.hitBallSfx = loader.loadSfx('phase_6/audio/sfx/Golf_Hit_Ball.ogg')

    def cleanup(self):
        if self.swingInterval:
            self.swingInterval.finish()
            self.swingInterval = None
        if self.releaseTrack:
            self.releaseTrack.finish()
            self.releaseTrack = None
        flyTracks = self.flyBallTracks.values()
        for track in flyTracks:
            track.finish()

        if self.fadeTrack:
            self.fadeTrack.finish()
            self.fadeTrack = None
        if self.restoreScaleTrack:
            self.restoreScaleTrack.finish()
            self.restoreScaleTrack = None
        self.root.removeNode()
        self.ballModel.removeNode()
        self.club.removeNode()
        if self.powerBar:
            self.powerBar.destroy()
            self.powerBar = None
        taskMgr.remove(self.triggerName)
        self.boss = None
        return

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

    def enterOff(self):
        pass

    def exitOff(self):
        pass

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

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

    def enterControlled(self, avId):
        self.avId = avId
        toon = base.cr.doId2do.get(avId)
        if not toon:
            return
        self.enableControlKey()
        self.toon = toon
        self.grabTrack = self.makeToonGrabInterval(toon)
        if avId == localAvatar.doId:
            self.boss.toCraneMode()
            camera.reparentTo(self.root)
            camera.setPosHpr(0, -10, 3, 0, 0, 0)
            localAvatar.setPos(self.root, self.toonGolfOffsetPos)
            localAvatar.setHpr(self.root, self.toonGolfOffsetHpr)
            localAvatar.sendCurrentPosition()
            self.__enableControlInterface()
            self.startPosHprBroadcast()
            self.accept('exitCrane', self.gotBossZapped)
        self.grabTrack.start()

    def exitControlled(self):
        self.grabTrack.finish()
        del self.grabTrack
        if self.swingInterval:
            self.swingInterval.finish()
            self.swingInterval = None
        if not self.ballModel.isEmpty():
            if self.ballModel.isHidden():
                self.notify.debug('ball is hidden scale =%s' % self.ballModel.getScale())
            else:
                self.notify.debug('ball is showing scale=%s' % self.ballModel.getScale())
        if self.toon and not self.toon.isDisabled():
            self.toon.startSmooth()
        self.releaseTrack = self.makeToonReleaseInterval(self.toon)
        self.stopPosHprBroadcast()
        self.stopSmooth()
        if self.avId == localAvatar.doId:
            self.__disableControlInterface()
            if not self.goingToReward:
                camera.reparentTo(base.localAvatar)
                camera.setPos(base.localAvatar.cameraPositions[0][0])
                camera.setHpr(0, 0, 0)
        self.stopAdjustClubTask()
        self.releaseTrack.start()
        self.enableControlKey()
        return

    def __allowDetect(self, task):
        if self.fadeTrack:
            self.fadeTrack.finish()
        self.fadeTrack = Sequence(self.ballModel.colorScaleInterval(0.2, self.ballColor), Func(self.ballModel.clearTransparency), name='fadeTrack-allowDetect')
        self.fadeTrack.start()
        self.trigger.unstash()
        self.accept(self.triggerEvent, self.__hitTrigger)

    def __hitTrigger(self, event):
        self.d_requestControl()

    def getRestoreScaleInterval(self):
        return Sequence()

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

    def d_requestFree(self, gotHitByBoss):
        self.sendUpdate('requestFree', [gotHitByBoss])

    def makeToonGrabInterval(self, toon):
        origPos = toon.getPos(self.root)
        origHpr = toon.getHpr(self.root)
        a = self.accomodateToon(toon)
        newPos = toon.getPos()
        newHpr = toon.getHpr()
        origHpr.setX(PythonUtil.fitSrcAngle2Dest(origHpr[0], newHpr[0]))
        self.notify.debug('toon.setPosHpr %s %s' % (origPos, origHpr))
        toon.setPosHpr(origPos, origHpr)
        walkTime = 0.2
        reach = Sequence()
        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(toon.stopLookAround))
        if toon == base.localAvatar:
            i.append(Func(self.switchToAnimState, 'GolfPuttLoop'))
        i.append(Func(self.startAdjustClubTask))
        i = Parallel(i, a)
        return i

    def accomodateToon(self, toon):
        toon.wrtReparentTo(self.root)
        toon.setPos(self.toonGolfOffsetPos)
        toon.setHpr(self.toonGolfOffsetHpr)
        return Sequence()

    def switchToAnimState(self, animStateName, forced = False):
        curAnimState = base.localAvatar.animFSM.getCurrentState()
        curAnimStateName = ''
        if curAnimState:
            curAnimStateName = curAnimState.getName()
        if curAnimStateName != animStateName or forced:
            base.localAvatar.b_setAnimState(animStateName)

    def __enableControlInterface(self):
        gui = loader.loadModel('phase_3.5/models/gui/avatar_panel_gui')
        self.closeButton = DirectButton(image=(gui.find('**/CloseBtn_UP'),
         gui.find('**/CloseBtn_DN'),
         gui.find('**/CloseBtn_Rllvr'),
         gui.find('**/CloseBtn_UP')), relief=None, scale=2, text=TTLocalizer.BossbotGolfSpotLeave, text_scale=0.04, text_pos=(0, -0.07), text_fg=VBase4(1, 1, 1, 1), pos=(1.05, 0, -0.82), command=self.__exitGolfSpot)
        self.accept('escape', self.__exitGolfSpot)
        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, 'watchGolfSpotControls')
        taskMgr.doMethodLater(5, self.__displayGolfSpotAdvice, self.golfSpotAdviceName)
        self.arrowVert = 0
        self.arrowHorz = 0
        if self.powerBar:
            self.powerBar.show()
        return

    def __disableControlInterface(self):
        if self.closeButton:
            self.closeButton.destroy()
            self.closeButton = None
        self.__cleanupGolfSpotAdvice()
        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
        taskMgr.remove('watchGolfSpotControls')
        if self.powerBar:
            self.powerBar.hide()
        else:
            self.notify.debug('self.powerBar is none')
        return

    def setupPowerBar(self):
        self.powerBar = DirectWaitBar(pos=(0.0, 0, -0.94), relief=DGG.SUNKEN, frameSize=(-2.0,
         2.0,
         -0.2,
         0.2), borderWidth=(0.02, 0.02), scale=0.25, range=100, sortOrder=50, frameColor=(0.5, 0.5, 0.5, 0.5), barColor=(1.0, 0.0, 0.0, 1.0), text='', text_scale=0.26, text_fg=(1, 1, 1, 1), text_align=TextNode.ACenter, text_pos=(0, -0.05))
        self.power = 0
        self.powerBar['value'] = self.power
        self.powerBar.hide()

    def resetPowerBar(self):
        self.power = 0
        self.powerBar['value'] = self.power
        self.powerBar['text'] = ''

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

    def __cleanupGolfSpotAdvice(self):
        if self.golfSpotAdviceLabel:
            self.golfSpotAdviceLabel.destroy()
            self.golfSpotAdviceLabel = None
        taskMgr.remove(self.golfSpotAdviceName)
        return

    def showExiting(self):
        if self.closeButton:
            self.closeButton.destroy()
            self.closeButton = DirectLabel(relief=None, text=TTLocalizer.BossbotGolfSpotLeaving, pos=(1.05, 0, -0.88), text_pos=(0, 0), text_scale=0.06, text_fg=VBase4(1, 1, 1, 1))
        self.__cleanupGolfSpotAdvice()
        return

    def __exitGolfSpot(self):
        self.d_requestFree(False)

    def __controlPressed(self):
        if self.controlKeyAllowed:
            self.__beginFireBall()

    def __controlReleased(self):
        if self.controlKeyAllowed:
            self.__endFireBall()

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

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

    def __rightArrow(self, pressed):
        self.__incrementChangeSeq()
        self.__cleanupGolfSpotAdvice()
        if pressed:
            self.arrowHorz = 1
            self.switchToAnimState('GolfRotateLeft')
        elif self.arrowHorz > 0:
            self.arrowHorz = 0
            self.switchToAnimState('GolfPuttLoop')

    def __leftArrow(self, pressed):
        self.__incrementChangeSeq()
        self.__cleanupGolfSpotAdvice()
        if pressed:
            self.arrowHorz = -1
            self.switchToAnimState('GolfRotateRight')
        elif self.arrowHorz < 0:
            self.arrowHorz = 0
            self.switchToAnimState('GolfPuttLoop')

    def __watchControls(self, task):
        if self.arrowHorz:
            self.__moveGolfSpot(self.arrowHorz)
        return Task.cont

    def __moveGolfSpot(self, xd):
        dt = globalClock.getDt()
        h = self.root.getH() - xd * self.rotateSpeed * dt
        h %= 360
        limitH = h
        self.root.setH(limitH)

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

    def __beginFireBall(self):
        if self.aimStart != None:
            return
        if not self.state == 'Controlled':
            return
        if not self.avId == localAvatar.doId:
            return
        time = globalClock.getFrameTime()
        self.aimStart = time
        messenger.send('wakeup')
        taskMgr.add(self.__updateBallPower, self.ballPowerTaskName)
        return

    def __endFireBall(self):
        if self.aimStart == None:
            return
        if not self.state == 'Controlled':
            return
        if not self.avId == localAvatar.doId:
            return
        taskMgr.remove(self.ballPowerTaskName)
        self.disableControlKey()
        messenger.send('wakeup')
        self.aimStart = None
        power = self.power
        angle = self.root.getH()
        self.notify.debug('incrementing self.__flyBallSequenceNum')
        self.__flyBallSequenceNum = (self.__flyBallSequenceNum + 1) % 255
        self.sendSwingInfo(power, angle, self.__flyBallSequenceNum)
        self.setSwingInfo(power, angle, self.__flyBallSequenceNum)
        self.resetPowerBar()
        return

    def __updateBallPower(self, task):
        if not self.powerBar:
            print '### no power bar!!!'
            return task.done
        newPower = self.__getBallPower(globalClock.getFrameTime())
        self.power = newPower
        self.powerBar['value'] = newPower
        return task.cont

    def __getBallPower(self, time):
        elapsed = max(time - self.aimStart, 0.0)
        t = elapsed / self.golfPowerSpeed
        t = math.pow(t, self.golfPowerExponent)
        power = int(t * 100) % 200
        if power > 100:
            power = 200 - power
        return power

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

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

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

    def d_sendGolfSpotPos(self):
        timestamp = globalClockDelta.getFrameNetworkTime()
        self.sendUpdate('setGolfSpotPos', [self.changeSeq, self.root.getH(), timestamp])

    def setGolfSpotPos(self, changeSeq, h, timestamp):
        self.changeSeq = changeSeq
        if self.smoothStarted:
            now = globalClock.getFrameTime()
            local = globalClockDelta.networkToLocalTime(timestamp, now)
            self.golfSpotSmoother.setH(h)
            self.golfSpotSmoother.setTimestamp(local)
            self.golfSpotSmoother.markPosition()
        else:
            self.root.setH(h)

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

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

    def clearSmoothing(self, bogus = None):
        self.golfSpotSmoother.clearPositions(1)

    def doSmoothTask(self, task):
        self.golfSpotSmoother.computeAndApplySmoothHpr(self.root)
        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 makeToonReleaseInterval(self, toon):

        def getSlideToPos(toon = toon):
            return render.getRelativePoint(toon, Point3(0, -5, 0))

        if self.gotHitByBoss:
            grabIval = Sequence(Func(self.detachClub), name='makeToonReleaseInterval-gotHitByBoss')
            if not toon.isEmpty():
                toonIval = Sequence(Func(toon.wrtReparentTo, render), Parallel(ActorInterval(toon, 'slip-backward'), toon.posInterval(0.5, getSlideToPos, fluid=1)), name='makeToonReleaseInterval-toonIval')
                grabIval.append(toonIval)
        else:
            grabIval = Sequence(Func(self.detachClub))
            if not toon.isEmpty():
                toonIval = Sequence(Parallel(ActorInterval(toon, 'walk', duration=1.0, playRate=-1.0), LerpPosInterval(toon, duration=1.0, pos=Point3(-10, 0, 0))), Func(toon.wrtReparentTo, render))
                grabIval.append(toonIval)
        if localAvatar.doId == toon.doId:
            if not self.goingToReward and toon.hp > 0:
                grabIval.append(Func(self.goToFinalBattle))
                grabIval.append(Func(self.notify.debug, 'goingToFinalBattlemode'))
                grabIval.append(Func(self.safeBossToFinalBattleMode))
        return grabIval

    def safeBossToFinalBattleMode(self):
        if self.boss:
            self.boss.toFinalBattleMode()

    def goToFinalBattle(self):
        if self.cr:
            place = self.cr.playGame.getPlace()
            if place and hasattr(place, 'fsm'):
                curState = place.fsm.getCurrentState().getName()
                if place.fsm.getCurrentState().getName() == 'crane':
                    place.setState('finalBattle')
                else:
                    self.notify.debug('NOT going to final battle, state=%s' % curState)

    def attachClub(self, avId, pointToBall = False):
        club = self.club
        if club:
            av = base.cr.doId2do.get(avId)
            if av:
                av.useLOD(1000)
                lHand = av.getLeftHands()[0]
                club.setPos(0, 0, 0)
                club.reparentTo(lHand)
                netScale = club.getNetTransform().getScale()[1]
                counterActToonScale = lHand.find('**/counteractToonScale')
                if counterActToonScale.isEmpty():
                    counterActToonScale = lHand.attachNewNode('counteractToonScale')
                    counterActToonScale.setScale(1 / netScale)
                    self.notify.debug('creating counterActToonScale for %s' % av.getName())
                club.reparentTo(counterActToonScale)
                club.setX(-0.25 * netScale)
                if pointToBall:
                    club.lookAt(self.clubLookatSpot)

    def detachClub(self):
        if not self.club.isEmpty():
            self.club.reparentTo(self.root)
            self.club.setZ(-20)
            self.club.setScale(1)

    def adjustClub(self):
        club = self.club
        if club:
            distance = club.getDistance(self.clubLookatSpot)
            scaleFactor = distance / 2.058
            club.setScale(1, scaleFactor, 1)

    def startAdjustClubTask(self):
        taskMgr.add(self.adjustClubTask, self.adjustClubTaskName)

    def stopAdjustClubTask(self):
        taskMgr.remove(self.adjustClubTaskName)

    def adjustClubTask(self, task):
        self.attachClub(self.avId, True)
        self.adjustClub()
        return task.cont

    def enableControlKey(self):
        self.controlKeyAllowed = True

    def disableControlKey(self):
        self.controlKeyAllowed = False

    def sendSwingInfo(self, power, angle, sequenceNum):
        self.sendUpdate('setSwingInfo', [power, angle, sequenceNum])

    def startBallPlayback(self, power, angle, sequenceNum):
        flyBall = self.ballModel.copyTo(NodePath())
        flyBall.setScale(1.0)
        flyBallBubble = self.getFlyBallBubble().instanceTo(NodePath())
        flyBallBubble.reparentTo(flyBall)
        flyBall.setTag('pieSequence', str(sequenceNum))
        flyBall.setTag('throwerId', str(self.avId))
        t = power / 100.0
        t = 1.0 - t
        dist = 300 - 200 * t
        time = 1.5 + 0.5 * t
        proj = ProjectileInterval(None, startPos=Point3(0, 0, 0), endPos=Point3(0, dist, 0), duration=time)
        relVel = proj.startVel

        def getVelocity(root = self.root, relVel = relVel):
            return render.getRelativeVector(root, relVel)

        fly = Sequence(Func(flyBall.reparentTo, render), Func(flyBall.setPosHpr, self.root, 0, 0, 0, 0, 0, 0), Func(base.cTrav.addCollider, flyBallBubble, self.flyBallHandler), ProjectileInterval(flyBall, startVel=getVelocity, duration=3), Func(flyBall.detachNode), Func(base.cTrav.removeCollider, flyBallBubble), Func(self.notify.debug, 'removed collider'), Func(self.flyBallFinishedFlying, sequenceNum))
        flyWithSound = Parallel(fly, SoundInterval(self.hitBallSfx, node=self.root), name='flyWithSound')
        self.notify.debug('starting flyball track')
        flyWithSound.start()
        self.flyBallTracks[sequenceNum] = flyWithSound
        return

    def setSwingInfo(self, power, angle, sequenceNum):
        av = base.cr.doId2do.get(self.avId)
        self.swingInterval = Sequence()
        if av:
            self.stopAdjustClubTask()
            self.swingInterval = Sequence(ActorInterval(av, 'swing-putt', startFrame=0, endFrame=GolfGlobals.BALL_CONTACT_FRAME), Func(self.startBallPlayback, power, angle, sequenceNum), Func(self.ballModel.hide), ActorInterval(av, 'swing-putt', startFrame=GolfGlobals.BALL_CONTACT_FRAME, endFrame=24), Func(self.ballModel.setScale, 0.1), Func(self.ballModel.show), LerpScaleInterval(self.ballModel, 1.0, Point3(1, 1, 1)), Func(self.enableControlKey))
            if av == localAvatar:
                self.swingInterval.append(Func(self.switchToAnimState, 'GolfPuttLoop', True))
        self.swingInterval.start()

    def getFlyBallBubble(self):
        if self.__flyBallBubble == None:
            bubble = CollisionSphere(0, 0, 0, GolfGlobals.GOLF_BALL_RADIUS)
            node = CollisionNode('flyBallBubble')
            node.addSolid(bubble)
            node.setFromCollideMask(ToontownGlobals.PieBitmask | ToontownGlobals.CameraBitmask | ToontownGlobals.FloorBitmask)
            node.setIntoCollideMask(BitMask32.allOff())
            self.__flyBallBubble = NodePath(node)
            self.flyBallHandler = CollisionHandlerEvent()
            self.flyBallHandler.addInPattern('flyBallHit-%d' % self.index)
        return self.__flyBallBubble

    def __flyBallHit(self, entry):
        print entry

    def flyBallFinishedFlying(self, sequence):
        if self.flyBallTracks.has_key(sequence):
            del self.flyBallTracks[sequence]

    def __finishFlyBallTrack(self, sequence):
        if self.flyBallTracks.has_key(sequence):
            flyBallTrack = self.flyBallTracks[sequence]
            del self.flyBallTracks[sequence]
            flyBallTrack.finish()

    def flyBallFinishedSplatting(self, sequence):
        if self.splatTracks.has_key(sequence):
            del self.splatTracks[sequence]

    def __flyBallHit(self, entry):
        if not entry.hasSurfacePoint() or not entry.hasInto():
            return
        if not entry.getInto().isTangible():
            return
        sequence = int(entry.getFromNodePath().getNetTag('pieSequence'))
        self.__finishFlyBallTrack(sequence)
        if self.splatTracks.has_key(sequence):
            splatTrack = self.splatTracks[sequence]
            del self.splatTracks[sequence]
            splatTrack.finish()
        flyBallCode = 0
        flyBallCodeStr = entry.getIntoNodePath().getNetTag('pieCode')
        if flyBallCodeStr:
            flyBallCode = int(flyBallCodeStr)
        pos = entry.getSurfacePoint(render)
        timestamp32 = globalClockDelta.getFrameNetworkTime(bits=32)
        throwerId = int(entry.getFromNodePath().getNetTag('throwerId'))
        splat = self.getFlyBallSplatInterval(pos[0], pos[1], pos[2], flyBallCode, throwerId)
        splat = Sequence(splat, Func(self.flyBallFinishedSplatting, sequence))
        self.splatTracks[sequence] = splat
        splat.start()
        self.notify.debug('doId=%d into=%s flyBallCode=%d, throwerId=%d' % (self.doId,
         entry.getIntoNodePath(),
         flyBallCode,
         throwerId))
        if flyBallCode == ToontownGlobals.PieCodeBossCog and self.avId == localAvatar.doId and self.lastHitSequenceNum != self.__flyBallSequenceNum:
            self.lastHitSequenceNum = self.__flyBallSequenceNum
            self.boss.d_ballHitBoss(1)
        elif flyBallCode == ToontownGlobals.PieCodeToon and self.avId == localAvatar.doId and self.lastHitSequenceNum != self.__flyBallSequenceNum:
            self.lastHitSequenceNum = self.__flyBallSequenceNum
            avatarDoId = entry.getIntoNodePath().getNetTag('avatarDoId')
            if avatarDoId == '':
                self.notify.warning('Toon %s has no avatarDoId tag.' % repr(entry.getIntoNodePath()))
                return
            doId = int(avatarDoId)
            if doId != localAvatar.doId:
                pass

    def getFlyBallSplatInterval(self, x, y, z, flyBallCode, throwerId):
        from toontown.toonbase import ToontownBattleGlobals
        from toontown.battle import BattleProps
        splatName = 'dust'
        splat = BattleProps.globalPropPool.getProp(splatName)
        splat.setBillboardPointWorld(2)
        color = ToontownGlobals.PieCodeColors.get(flyBallCode)
        if color:
            splat.setColor(*color)
        if flyBallCode == ToontownGlobals.PieCodeBossCog:
            self.notify.debug('changing color to %s' % self.ballColor)
            splat.setColor(self.ballColor)
        sound = loader.loadSfx('phase_11/audio/sfx/LB_evidence_miss.ogg')
        vol = 1.0
        if flyBallCode == ToontownGlobals.PieCodeBossCog:
            sound = loader.loadSfx('phase_4/audio/sfx/Golf_Hit_Barrier_1.ogg')
        soundIval = SoundInterval(sound, node=splat, volume=vol)
        if flyBallCode == ToontownGlobals.PieCodeBossCog and localAvatar.doId == throwerId:
            vol = 1.0
            soundIval = SoundInterval(sound, node=localAvatar, volume=vol)
        ival = Parallel(Func(splat.reparentTo, render), Func(splat.setPos, x, y, z), soundIval, Sequence(ActorInterval(splat, splatName), Func(splat.detachNode)))
        return ival

    def setGoingToReward(self):
        self.goingToReward = True

    def gotBossZapped(self):
        self.showExiting()
        self.d_requestFree(True)
Esempio n. 9
0
 def __init__(self, scenefile, capturefile=None, showstats=False, screenshot_dir=None):
     
     self.scenefile = scenefile
     self.capturefile = capturefile
     self.scene = scene.Scene.fromfile(scenefile)
     self.unique_models = set(m.slug for m in self.scene)
     self.multiplexer = pool.MultiplexPool()
     self.screenshot_dir = screenshot_dir
     
     print '%d objects in scene, %d unique' % (len(self.scene), len(self.unique_models))
     
     # turns on lazy loading of textures
     p3d.loadPrcFileData('', 'preload-textures 0')
     p3d.loadPrcFileData('', 'preload-simple-textures 1')
     p3d.loadPrcFileData('', 'compressed-textures 1')
     p3d.loadPrcFileData('', 'allow-incomplete-render 0')
     
     
     # window size to 1024x768
     p3d.loadPrcFileData('', 'win-size 1024 768')
     
     ShowBase.ShowBase.__init__(self)
     
     # background color sky blue
     self.win.setClearColorActive(True)
     self.win.setClearColor(p3d.VBase4(0.5294, 0.8078, 0.9215, 0))
     
     # create a nodepath for each unique model
     self.unique_nodepaths = dict((m, p3d.NodePath(m)) for m in self.unique_models)
     
     self.rigid_body_combiners = {}
     self.rigid_body_combiner_np = {}
     for m in self.unique_models:
         #rbc = p3d.RigidBodyCombiner(m)
         rbc = p3d.NodePath(m)
         self.rigid_body_combiners[m] = rbc
         np = p3d.NodePath(rbc)
         np.reparentTo(self.render)
         self.rigid_body_combiner_np[m] = np
     
     # find out how many objects are going to be instanced for each node
     self.instance_count = collections.defaultdict(int)
     for model in self.scene:
         self.instance_count[model.slug] += 1
     
     # then instance each unique model to its instantiation in the actual scene
     self.nodepaths = {}
     self.obj_bounds = {}
     self.nodepaths_byslug = collections.defaultdict(list)
     for model in self.scene:
         unique_np = self.unique_nodepaths[model.slug]
         node_name = model.slug + "_%.7g_%.7g_%.7g" % (model.x, model.y, model.z)
         
         if self.instance_count[model.slug] == 1:
             unique_np.setName(node_name)
             unique_np.reparentTo(self.render)
             np = unique_np
         else:
             np = self.rigid_body_combiner_np[model.slug].attachNewNode(node_name)
         
         self.nodepaths[model] = np
         self.nodepaths_byslug[model.slug].append(np)
         np.setPos(model.x, model.y, model.z)
         np.setScale(model.scale, model.scale, model.scale)
         q = p3d.Quat()
         q.setI(model.orient_x)
         q.setJ(model.orient_y)
         q.setK(model.orient_z)
         q.setR(model.orient_w)
         np.setQuat(q)
         self.obj_bounds[np] = p3d.BoundingSphere(np.getPos(), np.getScale()[0])
     
     #for rbc in self.rigid_body_combiners.itervalues():
     #    rbc.collect()
     
     self.waiting = []
     self.pm_waiting = collections.defaultdict(list)
     self.models_loaded = set()
     self.loading_priority = 2147483647
     
     for m in self.unique_models:
         t = metadata.MetadataDownloadTask(m)
         self.multiplexer.add_task(t)
     
     self.disableMouse()
     pcore.attachLights(self.render)
     self.render.setShaderAuto()
     self.render.setTransparency(p3d.TransparencyAttrib.MNone)
     
     self.camLens.setFar(sys.maxint)
     self.camLens.setNear(8.0)
     self.render.setAntialias(p3d.AntialiasAttrib.MAuto)
     
     self.showstats = showstats
     if showstats:
         self.num_metadata_loaded = 0
         self.num_models_loaded = 0
         self.num_texture_updates = 0
         self.num_mesh_refinements = 0
         self.total_texture_updates = 0
         self.total_mesh_refinements = 0
         
         self.txtMetadataLoaded = gui.OnscreenText.OnscreenText(text='', style=1, pos=(0.01, -0.05),
                                                              parent=self.a2dTopLeft, align=p3d.TextNode.ALeft,
                                                              scale=0.05, fg=(0.1, 0.1, 0.1, 1), shadow=(0.9, 0.9, 0.9, 1))
         self.txtUniqueLoaded = gui.OnscreenText.OnscreenText(text='', style=1, pos=(0.01, -0.11),
                                                              parent=self.a2dTopLeft, align=p3d.TextNode.ALeft,
                                                              scale=0.05, fg=(0.1, 0.1, 0.1, 1), shadow=(0.9, 0.9, 0.9, 1))
         self.txtTextureUpdates = gui.OnscreenText.OnscreenText(text='', style=1, pos=(0.01, -0.17),
                                                              parent=self.a2dTopLeft, align=p3d.TextNode.ALeft,
                                                              scale=0.05, fg=(0.1, 0.1, 0.1, 1), shadow=(0.9, 0.9, 0.9, 1))
         self.txtMeshRefinements = gui.OnscreenText.OnscreenText(text='', style=1, pos=(0.01, -0.23),
                                                              parent=self.a2dTopLeft, align=p3d.TextNode.ALeft,
                                                              scale=0.05, fg=(0.1, 0.1, 0.1, 1), shadow=(0.9, 0.9, 0.9, 1))
         
         self.update_stats()
     
     self.globalClock = p3d.ClockObject.getGlobalClock()
     
     self.smooth_mover = SmoothMover()
     self.smooth_mover.setPredictionMode(SmoothMover.PMOn)
     self.smooth_mover.setSmoothMode(SmoothMover.SMOn)
     self.smooth_mover.setMaxPositionAge(10.0)
     self.smooth_mover.setAcceptClockSkew(False)
     self.smooth_mover.setDelay(0)
     
     self.pandastate = katasked.panda.PandaState(self.cam,
                                                 self.unique_nodepaths,
                                                 self.nodepaths,
                                                 self.smooth_mover,
                                                 self.globalClock,
                                                 self.obj_bounds)
     
     if self.capturefile is not None:
         self.capturedata = json.load(self.capturefile)
         self.duration = self.capturedata['duration']
         self.positions = self.capturedata['positions']
         self.rotations = self.capturedata['rotations']
         
         self.curve_creator = motioncap.CreateNurbsCurve()
         for pos, rot in zip(self.positions, self.rotations):
             self.curve_creator.addPoint(pos, rot)
         self.mopath = self.curve_creator.getMotionPath()
         
         self.interval = MopathInterval.MopathInterval(self.mopath, self.cam, duration=self.duration, name="Camera Replay")
     else:
         controls.KeyboardMovement()
         controls.MouseCamera()
     
     self.update_camera_predictor_task = self.taskMgr.doMethodLater(0.1, self.update_camera_predictor, 'update_camera_predictor')
     self.update_priority_task = self.taskMgr.doMethodLater(0.5, self.check_pool, 'check_pool')
     self.load_waiting_task = self.taskMgr.doMethodLater(0.1, self.load_waiting, 'load_waiting')
Esempio n. 10
0
class ProgressiveLoader(ShowBase.ShowBase):
    
    def __init__(self, scenefile, capturefile=None, showstats=False, screenshot_dir=None):
        
        self.scenefile = scenefile
        self.capturefile = capturefile
        self.scene = scene.Scene.fromfile(scenefile)
        self.unique_models = set(m.slug for m in self.scene)
        self.multiplexer = pool.MultiplexPool()
        self.screenshot_dir = screenshot_dir
        
        print '%d objects in scene, %d unique' % (len(self.scene), len(self.unique_models))
        
        # turns on lazy loading of textures
        p3d.loadPrcFileData('', 'preload-textures 0')
        p3d.loadPrcFileData('', 'preload-simple-textures 1')
        p3d.loadPrcFileData('', 'compressed-textures 1')
        p3d.loadPrcFileData('', 'allow-incomplete-render 0')
        
        
        # window size to 1024x768
        p3d.loadPrcFileData('', 'win-size 1024 768')
        
        ShowBase.ShowBase.__init__(self)
        
        # background color sky blue
        self.win.setClearColorActive(True)
        self.win.setClearColor(p3d.VBase4(0.5294, 0.8078, 0.9215, 0))
        
        # create a nodepath for each unique model
        self.unique_nodepaths = dict((m, p3d.NodePath(m)) for m in self.unique_models)
        
        self.rigid_body_combiners = {}
        self.rigid_body_combiner_np = {}
        for m in self.unique_models:
            #rbc = p3d.RigidBodyCombiner(m)
            rbc = p3d.NodePath(m)
            self.rigid_body_combiners[m] = rbc
            np = p3d.NodePath(rbc)
            np.reparentTo(self.render)
            self.rigid_body_combiner_np[m] = np
        
        # find out how many objects are going to be instanced for each node
        self.instance_count = collections.defaultdict(int)
        for model in self.scene:
            self.instance_count[model.slug] += 1
        
        # then instance each unique model to its instantiation in the actual scene
        self.nodepaths = {}
        self.obj_bounds = {}
        self.nodepaths_byslug = collections.defaultdict(list)
        for model in self.scene:
            unique_np = self.unique_nodepaths[model.slug]
            node_name = model.slug + "_%.7g_%.7g_%.7g" % (model.x, model.y, model.z)
            
            if self.instance_count[model.slug] == 1:
                unique_np.setName(node_name)
                unique_np.reparentTo(self.render)
                np = unique_np
            else:
                np = self.rigid_body_combiner_np[model.slug].attachNewNode(node_name)
            
            self.nodepaths[model] = np
            self.nodepaths_byslug[model.slug].append(np)
            np.setPos(model.x, model.y, model.z)
            np.setScale(model.scale, model.scale, model.scale)
            q = p3d.Quat()
            q.setI(model.orient_x)
            q.setJ(model.orient_y)
            q.setK(model.orient_z)
            q.setR(model.orient_w)
            np.setQuat(q)
            self.obj_bounds[np] = p3d.BoundingSphere(np.getPos(), np.getScale()[0])
        
        #for rbc in self.rigid_body_combiners.itervalues():
        #    rbc.collect()
        
        self.waiting = []
        self.pm_waiting = collections.defaultdict(list)
        self.models_loaded = set()
        self.loading_priority = 2147483647
        
        for m in self.unique_models:
            t = metadata.MetadataDownloadTask(m)
            self.multiplexer.add_task(t)
        
        self.disableMouse()
        pcore.attachLights(self.render)
        self.render.setShaderAuto()
        self.render.setTransparency(p3d.TransparencyAttrib.MNone)
        
        self.camLens.setFar(sys.maxint)
        self.camLens.setNear(8.0)
        self.render.setAntialias(p3d.AntialiasAttrib.MAuto)
        
        self.showstats = showstats
        if showstats:
            self.num_metadata_loaded = 0
            self.num_models_loaded = 0
            self.num_texture_updates = 0
            self.num_mesh_refinements = 0
            self.total_texture_updates = 0
            self.total_mesh_refinements = 0
            
            self.txtMetadataLoaded = gui.OnscreenText.OnscreenText(text='', style=1, pos=(0.01, -0.05),
                                                                 parent=self.a2dTopLeft, align=p3d.TextNode.ALeft,
                                                                 scale=0.05, fg=(0.1, 0.1, 0.1, 1), shadow=(0.9, 0.9, 0.9, 1))
            self.txtUniqueLoaded = gui.OnscreenText.OnscreenText(text='', style=1, pos=(0.01, -0.11),
                                                                 parent=self.a2dTopLeft, align=p3d.TextNode.ALeft,
                                                                 scale=0.05, fg=(0.1, 0.1, 0.1, 1), shadow=(0.9, 0.9, 0.9, 1))
            self.txtTextureUpdates = gui.OnscreenText.OnscreenText(text='', style=1, pos=(0.01, -0.17),
                                                                 parent=self.a2dTopLeft, align=p3d.TextNode.ALeft,
                                                                 scale=0.05, fg=(0.1, 0.1, 0.1, 1), shadow=(0.9, 0.9, 0.9, 1))
            self.txtMeshRefinements = gui.OnscreenText.OnscreenText(text='', style=1, pos=(0.01, -0.23),
                                                                 parent=self.a2dTopLeft, align=p3d.TextNode.ALeft,
                                                                 scale=0.05, fg=(0.1, 0.1, 0.1, 1), shadow=(0.9, 0.9, 0.9, 1))
            
            self.update_stats()
        
        self.globalClock = p3d.ClockObject.getGlobalClock()
        
        self.smooth_mover = SmoothMover()
        self.smooth_mover.setPredictionMode(SmoothMover.PMOn)
        self.smooth_mover.setSmoothMode(SmoothMover.SMOn)
        self.smooth_mover.setMaxPositionAge(10.0)
        self.smooth_mover.setAcceptClockSkew(False)
        self.smooth_mover.setDelay(0)
        
        self.pandastate = katasked.panda.PandaState(self.cam,
                                                    self.unique_nodepaths,
                                                    self.nodepaths,
                                                    self.smooth_mover,
                                                    self.globalClock,
                                                    self.obj_bounds)
        
        if self.capturefile is not None:
            self.capturedata = json.load(self.capturefile)
            self.duration = self.capturedata['duration']
            self.positions = self.capturedata['positions']
            self.rotations = self.capturedata['rotations']
            
            self.curve_creator = motioncap.CreateNurbsCurve()
            for pos, rot in zip(self.positions, self.rotations):
                self.curve_creator.addPoint(pos, rot)
            self.mopath = self.curve_creator.getMotionPath()
            
            self.interval = MopathInterval.MopathInterval(self.mopath, self.cam, duration=self.duration, name="Camera Replay")
        else:
            controls.KeyboardMovement()
            controls.MouseCamera()
        
        self.update_camera_predictor_task = self.taskMgr.doMethodLater(0.1, self.update_camera_predictor, 'update_camera_predictor')
        self.update_priority_task = self.taskMgr.doMethodLater(0.5, self.check_pool, 'check_pool')
        self.load_waiting_task = self.taskMgr.doMethodLater(0.1, self.load_waiting, 'load_waiting')
        
    def run(self):
        if self.screenshot_dir is not None:
            self.start_time = None
            self.screenshot_info = []
            self.screenshot_task = self.taskMgr.doMethodLater(0, self.trigger_screenshot, 'screenshot_task', sort=-1)
        
        if self.capturefile is not None:
            self.interval.start()
            self.taskMgr.doMethodLater(self.duration + 2.0, self.finished, 'exiter')
        
        ShowBase.ShowBase.run(self)
    
    def update_camera_predictor(self, task):
        curtime = self.globalClock.getFrameTime()
        self.smooth_mover.setPos(self.cam.getPos())
        self.smooth_mover.setHpr(self.cam.getHpr())
        self.smooth_mover.setTimestamp(curtime)
        self.smooth_mover.markPosition()
        
        return task.again
    
    def check_pool(self, task):
        t0 = time.time()
        finished_tasks = self.multiplexer.poll(self.pandastate)
        t1 = time.time()
        time_took = t1-t0
        time_wait = time_took * 2.0
        time_wait = min(time_wait, 1)
        time_wait = max(time_wait, 0.1)
        task.delayTime = time_wait
        
        if len(finished_tasks) == 0 and self.multiplexer.empty():
            print
            print 'FINISHED LOADING'
            print
            return task.done
        
        for t in finished_tasks:
            if isinstance(t, meshtask.MeshLoadTask):
                self.loader.loadModel(t.bam_file, callback=self.model_loaded, extraArgs=[t.modelslug], priority=self.loading_priority)
            elif isinstance(t, texturetask.TextureDownloadTask):
                self.loader.loadModel(t.bam_file, callback=self.texture_loaded, extraArgs=[t.modelslug], priority=self.loading_priority)
            elif isinstance(t, refinementtask.MeshRefinementDownloadTask):
                if t.modelslug in self.models_loaded:
                    self.waiting.append((LOAD_TYPE.MESH_REFINEMENT, t.modelslug, t.pm_refinements))
                else:
                    self.pm_waiting[t.modelslug].append(t.pm_refinements)
            elif isinstance(t, metadata.MetadataDownloadTask):
                if self.showstats:
                    self.num_metadata_loaded += 1
                    
                    progressive = t.metadata['metadata']['types']['progressive']
                    byte_ranges = progressive['mipmaps']['./atlas.jpg']['byte_ranges']
                    baselevel = len(byte_ranges)
                    for i, levelinfo in enumerate(byte_ranges):
                        if levelinfo['width'] >= 128 or levelinfo['height'] >= 128:
                            baselevel = i
                            break
                    self.total_texture_updates += len(byte_ranges[baselevel+1:])
                    
                    if 'progressive_stream_size' in progressive:
                        self.total_mesh_refinements += int(math.ceil(progressive['progressive_stream_size'] / float(percepfilter.PM_CHUNK_SIZE)))
                    
                    self.update_stats()
                
            self.loading_priority -= 1
        
        
        return task.again

    def model_loaded(self, modelpath, modelslug):
        self.models_loaded.add(modelslug)
        self.waiting.append((LOAD_TYPE.INITIAL_MODEL, modelpath, modelslug))
        for pm_refinements in self.pm_waiting[modelslug]:
            self.waiting.append((LOAD_TYPE.MESH_REFINEMENT, modelslug, pm_refinements))
        del self.pm_waiting[modelslug]
    
    def texture_loaded(self, modelpath, modelslug):
        newtex = modelpath.getChild(0).getTexture()
        np = self.unique_nodepaths[modelslug]
        self.waiting.append((LOAD_TYPE.TEXTURE_UPDATE, np, newtex))
        
    def load_waiting(self, task):
        time_wait = 0.1
        
        if len(self.waiting) > 0:
            t0 = time.time()
            args = self.waiting.pop(0)
            if args[0] == LOAD_TYPE.INITIAL_MODEL:
                modelpath, modelslug = args[1], args[2]
                
                np = self.unique_nodepaths[modelslug]
                modelnode = modelpath.getChild(0)
                geomnode = modelnode.getChild(0)
                trans = modelnode.getTransform()
                geomnode.setTransform(trans)
                self.unique_nodepaths[modelslug] = geomnode
                
                if self.instance_count[modelslug] == 1:
                    geomnode.reparentTo(np)
                else:
                    for instance_np in self.nodepaths_byslug[modelslug]:
                        geomnode.instanceTo(instance_np)
                    #self.rigid_body_combiners[modelslug].collect()
                
                if self.showstats:
                    self.num_models_loaded += 1
                    self.update_stats()
                
            elif args[0] == LOAD_TYPE.TEXTURE_UPDATE:
                np, newtex = args[1], args[2]
                np.setTextureOff(1)
                np.setTexture(newtex, 1)
                if self.showstats:
                    self.num_texture_updates += 1
                    self.update_stats()
            elif args[0] == LOAD_TYPE.MESH_REFINEMENT:
                modelslug, pm_refinements = args[1], args[2]
                np = self.unique_nodepaths[modelslug]
                pdae_updater.update_nodepath(np.node(), pm_refinements)
                
                #if self.instance_count[modelslug] > 1:
                #    self.rigid_body_combiners[modelslug].collect()
                
                if self.showstats:
                    self.num_mesh_refinements += 1
                    self.update_stats()
            
            t1 = time.time()
            time_took = t1-t0
            time_wait = time_took * 2.0
            time_wait = min(time_wait, 1)
            time_wait = max(time_wait, 0.1)
        
        task.delayTime = time_wait
        return task.again

    def trigger_screenshot(self, task):
        if self.start_time is None:
            self.start_time = self.globalClock.getLongTime()
            task.delayTime = 1.0
            return task.again
        
        run_time = self.globalClock.getLongTime() - self.start_time
        fname = ('%07.2f' % run_time) + '.tiff'
        self.screenshot_info.append({'filename': fname,
                                     'position': list(self.cam.getPos()),
                                     'hpr': list(self.cam.getHpr())})
        self.win.saveScreenshot(os.path.join(self.screenshot_dir, 'realtime', fname))
        return task.again

    def finished(self, task):
        if self.screenshot_dir is not None:
            with open(os.path.join(self.screenshot_dir, 'info.json'), 'w') as f:
                json.dump(self.screenshot_info, f, indent=2)
        
            sys.exit(len(self.screenshot_info))
        
        sys.exit(0)

    def update_stats(self):
        self.txtMetadataLoaded.setText('Metadata Loaded: %d/%d' % (self.num_metadata_loaded, len(self.unique_models)))
        self.txtUniqueLoaded.setText('Base Mesh Loaded: %d/%d' % (self.num_models_loaded, len(self.unique_models)))
        self.txtTextureUpdates.setText('Texture Updates: %d/%d' % (self.num_texture_updates, self.total_texture_updates))
        self.txtMeshRefinements.setText('Mesh Refinements: %d/%d' % (self.num_mesh_refinements, self.total_mesh_refinements))