def enterPlay(self):
        self.notify.debug('enterPlay')
        for i in xrange(self.numPlayers):
            avId = self.avIdList[i]
            avName = self.getAvatarName(avId)
            scorePanel = MinigameAvatarScorePanel.MinigameAvatarScorePanel(
                avId, avName)
            scorePanel.setPos(1.1200000000000001, 0.0,
                              0.5 - 0.28000000000000003 * i)
            self.scorePanels.append(scorePanel)

        self.goalBar.show()
        self.goalBar['value'] = 0.0
        toonbase.setCellsAvailable(toonbase.rightCells, 0)
        self._DistributedMazeGame__spawnUpdateSuitsTask()
        orthoDrive = OrthoDrive(self.TOON_SPEED,
                                maxFrameMove=self.MAX_FRAME_MOVE,
                                customCollisionCallback=self.
                                _DistributedMazeGame__doMazeCollisions)
        self.orthoWalk = OrthoWalk(orthoDrive,
                                   broadcast=not self.isSinglePlayer())
        self.orthoWalk.start()
        self.accept(MazeSuit.COLLISION_EVENT_NAME,
                    self._DistributedMazeGame__hitBySuit)
        self.accept(self.TREASURE_GRAB_EVENT_NAME,
                    self._DistributedMazeGame__treasureGrabbed)
        self.timer = ToontownTimer.ToontownTimer()
        self.timer.posInTopRightCorner()
        self.timer.setTime(MazeGameGlobals.GAME_DURATION)
        self.timer.countdown(MazeGameGlobals.GAME_DURATION, self.timerExpired)
        self.accept('resetClock', self._DistributedMazeGame__resetClock)
        base.playMusic(self.music, looping=0, volume=0.80000000000000004)
    def initOrthoWalk(self):
        self.notify.debug('startOrthoWalk')
        
        def doCollisions(oldPos, newPos, self = self):
            x = bound(newPos[0], self.StageHalfWidth, -(self.StageHalfWidth))
            y = bound(newPos[1], self.StageHalfHeight, -(self.StageHalfHeight))
            newPos.setX(x)
            newPos.setY(y)
            return newPos

        orthoDrive = OrthoDrive(self.ToonSpeed, customCollisionCallback = doCollisions)
        self.orthoWalk = OrthoWalk(orthoDrive, broadcast = not self.isSinglePlayer())
Exemple #3
0
    def enterPlay(self):
        self.notify.debug("enterPlay")

        # Initialize the scoreboard
        for i in xrange(self.numPlayers):
            avId = self.avIdList[i]
            avName = self.getAvatarName(avId)
            scorePanel = \
                       MinigameAvatarScorePanel.MinigameAvatarScorePanel(avId,
                                                                         avName)
            scorePanel.setPos(1.12, 0.0, .5 - 0.28*i)
            self.scorePanels.append(scorePanel)

        self.goalBar.show()
        self.goalBar['value'] = 0.

        # We need the right edge of the screen for display of the
        # scoreboard, so we can't have any offscreen popups there.
        base.setCellsAvailable(base.rightCells, 0)

        self.__spawnUpdateSuitsTask()

        orthoDrive = OrthoDrive(
            self.TOON_SPEED,
            maxFrameMove=self.MAX_FRAME_MOVE,
            customCollisionCallback=self.__doMazeCollisions,
            priority = 1
            )
        self.orthoWalk = OrthoWalk(orthoDrive,
                                   broadcast=not self.isSinglePlayer())
        self.orthoWalk.start()

        # listen for collisions with the suits
        self.accept(MazeSuit.COLLISION_EVENT_NAME, self.__hitBySuit)

        # listen for treasure pickups
        self.accept(self.TREASURE_GRAB_EVENT_NAME, self.__treasureGrabbed)

        # Start counting down the game clock,
        # call timerExpired when it reaches 0
        self.timer = ToontownTimer.ToontownTimer()
        self.timer.posInTopRightCorner()
        self.timer.setTime(MazeGameGlobals.GAME_DURATION)
        self.timer.countdown(MazeGameGlobals.GAME_DURATION, self.timerExpired)

        # listen for resetClock messages so we can keep our clock in sync
        self.accept("resetClock", self.__resetClock)

        base.playMusic(self.music, looping = 0, volume = .8)
    def initOrthoWalk(self):
        self.notify.debug('startOrthoWalk')

        def doCollisions(oldPos, newPos, self = self):
            x = bound(newPos[0], self.StageHalfWidth, -self.StageHalfWidth)
            y = bound(newPos[1], self.StageHalfHeight, -self.StageHalfHeight)
            newPos.setX(x)
            newPos.setY(y)
            return newPos

        orthoDrive = OrthoDrive(self.ToonSpeed, customCollisionCallback=doCollisions)
        self.orthoWalk = OrthoWalk(orthoDrive, broadcast=not self.isSinglePlayer())
Exemple #5
0
class DistributedCatchGame(DistributedMinigame):
    DropTaskName = 'dropSomething'
    EndGameTaskName = 'endCatchGame'
    SuitWalkTaskName = 'catchGameSuitWalk'
    DropObjectPlurals = {
        'apple': TTLocalizer.CatchGameApples,
        'orange': TTLocalizer.CatchGameOranges,
        'pear': TTLocalizer.CatchGamePears,
        'coconut': TTLocalizer.CatchGameCoconuts,
        'watermelon': TTLocalizer.CatchGameWatermelons,
        'pineapple': TTLocalizer.CatchGamePineapples,
        'anvil': TTLocalizer.CatchGameAnvils
    }

    def __init__(self, cr):
        DistributedMinigame.__init__(self, cr)
        self.gameFSM = ClassicFSM.ClassicFSM('DistributedCatchGame', [
            State.State('off', self.enterOff, self.exitOff, ['play']),
            State.State('play', self.enterPlay, self.exitPlay, ['cleanup']),
            State.State('cleanup', self.enterCleanup, self.exitCleanup, [])
        ], 'off', 'cleanup')
        self.addChildGameFSM(self.gameFSM)
        self.setUsesSmoothing()
        self.setUsesLookAround()

    def getTitle(self):
        return TTLocalizer.CatchGameTitle

    def getInstructions(self):
        return TTLocalizer.CatchGameInstructions % {
            'fruit': self.DropObjectPlurals[self.fruitName],
            'badThing': self.DropObjectPlurals['anvil']
        }

    def getMaxDuration(self):
        return CatchGameGlobals.GameDuration + 5

    def load(self):
        self.notify.debug('load')
        DistributedMinigame.load(self)
        self.defineConstants()
        groundModels = [
            'phase_4/models/minigames/treehouse_2players',
            'phase_4/models/minigames/treehouse_2players',
            'phase_4/models/minigames/treehouse_3players',
            'phase_4/models/minigames/treehouse_4players'
        ]
        index = self.getNumPlayers() - 1
        self.ground = loader.loadModel(groundModels[index])
        self.ground.setHpr(180, -90, 0)
        self.dropShadow = loader.loadModel('phase_3/models/props/drop_shadow')
        self.dropObjModels = {}
        for objType in DropObjectTypes:
            if objType.name not in ['anvil', self.fruitName]:
                continue
            model = loader.loadModel(objType.modelPath)
            self.dropObjModels[objType.name] = model
            modelScales = {
                'apple': 0.7,
                'orange': 0.7,
                'pear': 0.5,
                'coconut': 0.7,
                'watermelon': 0.6,
                'pineapple': 0.45
            }
            if objType.name in modelScales:
                model.setScale(modelScales[objType.name])
            if objType == Name2DropObjectType['pear']:
                model.setZ(-.6)
            if objType == Name2DropObjectType['coconut']:
                model.setP(180)
            if objType == Name2DropObjectType['watermelon']:
                model.setH(135)
                model.setZ(-.5)
            if objType == Name2DropObjectType['pineapple']:
                model.setZ(-1.7)
            if objType == Name2DropObjectType['anvil']:
                model.setZ(-self.ObjRadius)
            model.flattenMedium()

        self.music = base.loadMusic('phase_4/audio/bgm/MG_toontag.ogg')
        self.sndGoodCatch = base.loadSfx(
            'phase_4/audio/sfx/SZ_DD_treasure.ogg')
        self.sndOof = base.loadSfx('phase_4/audio/sfx/MG_cannon_hit_dirt.ogg')
        self.sndAnvilLand = base.loadSfx(
            'phase_4/audio/sfx/AA_drop_anvil_miss.ogg')
        self.sndPerfect = base.loadSfx('phase_4/audio/sfx/ring_perfect.ogg')
        self.toonSDs = {}
        avId = self.localAvId
        toonSD = CatchGameToonSD.CatchGameToonSD(avId, self)
        self.toonSDs[avId] = toonSD
        toonSD.load()
        if self.WantSuits:
            suitTypes = ['f', 'tm', 'pp', 'dt']
            self.suits = []
            for type in suitTypes:
                suit = Suit.Suit()
                d = SuitDNA.SuitDNA()
                d.newSuit(type)
                suit.setDNA(d)
                suit.nametag3d.stash()
                suit.nametag.destroy()
                suit.pose('walk', 0)
                self.suits.append(suit)

        self.__textGen = TextNode('ringGame')
        self.__textGen.setFont(ToontownGlobals.getSignFont())
        self.__textGen.setAlign(TextNode.ACenter)
        self.introMovie = self.getIntroMovie()

    def unload(self):
        self.notify.debug('unload')
        DistributedMinigame.unload(self)
        self.removeChildGameFSM(self.gameFSM)
        del self.gameFSM
        self.introMovie.finish()
        del self.introMovie
        del self.__textGen
        for avId in self.toonSDs.keys():
            toonSD = self.toonSDs[avId]
            toonSD.unload()

        del self.toonSDs
        for suit in self.suits:
            suit.reparentTo(hidden)
            suit.delete()

        del self.suits
        self.ground.removeNode()
        del self.ground
        self.dropShadow.removeNode()
        del self.dropShadow
        for model in self.dropObjModels.values():
            model.removeNode()

        del self.dropObjModels
        del self.music
        del self.sndGoodCatch
        del self.sndOof
        del self.sndAnvilLand
        del self.sndPerfect

    def getObjModel(self, objName):
        return self.dropObjModels[objName].copyTo(hidden)

    def __genText(self, text):
        self.__textGen.setText(text)
        return self.__textGen.generate()

    def calcDifficultyConstants(self, difficulty, numPlayers):
        ToonSpeedRange = [16.0, 25.0]
        self.ToonSpeed = lerp(ToonSpeedRange[0], ToonSpeedRange[1], difficulty)
        self.SuitSpeed = self.ToonSpeed / 2.0
        self.SuitPeriodRange = [
            lerp(5.0, 3.0, self.getDifficulty()),
            lerp(15.0, 8.0, self.getDifficulty())
        ]

        def scaledDimensions(widthHeight, scale):
            w, h = widthHeight
            return [math.sqrt(scale * w * w), math.sqrt(scale * h * h)]

        BaseStageDimensions = [20, 15]
        areaScales = [1.0, 1.0, 3.0 / 2, 4.0 / 2]
        self.StageAreaScale = areaScales[numPlayers - 1]
        self.StageLinearScale = math.sqrt(self.StageAreaScale)
        self.notify.debug('StageLinearScale: %s' % self.StageLinearScale)
        self.StageDimensions = scaledDimensions(BaseStageDimensions,
                                                self.StageAreaScale)
        self.notify.debug('StageDimensions: %s' % self.StageDimensions)
        self.StageHalfWidth = self.StageDimensions[0] / 2.0
        self.StageHalfHeight = self.StageDimensions[1] / 2.0
        MOHs = [24] * 2 + [26, 28]
        self.MinOffscreenHeight = MOHs[self.getNumPlayers() - 1]
        distance = math.sqrt(self.StageDimensions[0] *
                             self.StageDimensions[0] +
                             self.StageDimensions[1] * self.StageDimensions[1])
        distance /= self.StageLinearScale
        if self.DropPlacerType == PathDropPlacer:
            distance /= 1.5
        ToonRunDuration = distance / self.ToonSpeed
        offScreenOnScreenRatio = 1.0
        fraction = 1.0 / 3 * 0.85
        self.BaselineOnscreenDropDuration = ToonRunDuration / (
            fraction * (1.0 + offScreenOnScreenRatio))
        self.notify.debug('BaselineOnscreenDropDuration=%s' %
                          self.BaselineOnscreenDropDuration)
        self.OffscreenTime = offScreenOnScreenRatio * self.BaselineOnscreenDropDuration
        self.notify.debug('OffscreenTime=%s' % self.OffscreenTime)
        self.BaselineDropDuration = self.BaselineOnscreenDropDuration + self.OffscreenTime
        self.MaxDropDuration = self.BaselineDropDuration
        self.DropPeriod = self.BaselineDropDuration / 2.0
        scaledNumPlayers = (numPlayers - 1.0) * 0.75 + 1.0
        self.DropPeriod /= scaledNumPlayers
        typeProbs = {'fruit': 3, 'anvil': 1}
        probSum = reduce(lambda x, y: x + y, typeProbs.values())
        for key in typeProbs.keys():
            typeProbs[key] = float(typeProbs[key]) / probSum

        scheduler = DropScheduler(CatchGameGlobals.GameDuration,
                                  self.FirstDropDelay, self.DropPeriod,
                                  self.MaxDropDuration, self.FasterDropDelay,
                                  self.FasterDropPeriodMult)
        self.totalDrops = 0
        while not scheduler.doneDropping():
            scheduler.stepT()
            self.totalDrops += 1

        self.numFruits = int(self.totalDrops * typeProbs['fruit'])
        self.numAnvils = int(self.totalDrops - self.numFruits)

    def getNumPlayers(self):
        return self.numPlayers

    def defineConstants(self):
        self.notify.debug('defineConstants')
        self.DropPlacerType = RegionDropPlacer
        fruits = {
            ToontownGlobals.ToontownCentral: 'apple',
            ToontownGlobals.DonaldsDock: 'orange',
            ToontownGlobals.DaisyGardens: 'pear',
            ToontownGlobals.MinniesMelodyland: 'coconut',
            ToontownGlobals.TheBrrrgh: 'watermelon',
            ToontownGlobals.DonaldsDreamland: 'pineapple'
        }
        self.fruitName = fruits[self.getSafezoneId()]
        self.ShowObjSpheres = 0
        self.ShowToonSpheres = 0
        self.ShowSuitSpheres = 0
        self.PredictiveSmoothing = 1
        self.UseGravity = 1
        self.TrickShadows = 1
        self.WantSuits = 1
        self.FirstDropDelay = 0.5
        self.FasterDropDelay = int(2.0 / 3 * CatchGameGlobals.GameDuration)
        self.notify.debug('will start dropping fast after %s seconds' %
                          self.FasterDropDelay)
        self.FasterDropPeriodMult = 0.5
        self.calcDifficultyConstants(self.getDifficulty(),
                                     self.getNumPlayers())
        self.notify.debug('ToonSpeed: %s' % self.ToonSpeed)
        self.notify.debug('total drops: %s' % self.totalDrops)
        self.notify.debug('numFruits: %s' % self.numFruits)
        self.notify.debug('numAnvils: %s' % self.numAnvils)
        self.ObjRadius = 1.0
        dropGridDimensions = [[5, 5], [5, 5], [6, 6], [7, 7]]
        self.DropRows, self.DropColumns = dropGridDimensions[
            self.getNumPlayers() - 1]
        self.cameraPosTable = [[0, -29.36, 28.17]] * 2 + [[0, -32.87, 30.43],
                                                          [0, -35.59, 32.1]]
        self.cameraHpr = [0, -35, 0]
        self.CameraPosHpr = self.cameraPosTable[self.getNumPlayers() -
                                                1] + self.cameraHpr
        for objType in DropObjectTypes:
            self.notify.debug('*** Object Type: %s' % objType.name)
            objType.onscreenDuration = objType.onscreenDurMult * self.BaselineOnscreenDropDuration
            self.notify.debug('onscreenDuration=%s' % objType.onscreenDuration)
            v_0 = 0.0
            t = objType.onscreenDuration
            x_0 = self.MinOffscreenHeight
            x = 0.0
            g = 2.0 * (x - x_0 - v_0 * t) / (t * t)
            self.notify.debug('gravity=%s' % g)
            objType.trajectory = Trajectory.Trajectory(
                0,
                Vec3(0, 0, x_0),
                Vec3(0, 0, v_0),
                gravMult=abs(g / Trajectory.Trajectory.gravity))
            objType.fallDuration = objType.onscreenDuration + self.OffscreenTime

    def grid2world(self, column, row):
        x = column / float(self.DropColumns - 1)
        y = row / float(self.DropRows - 1)
        x = x * 2.0 - 1.0
        y = y * 2.0 - 1.0
        x *= self.StageHalfWidth
        y *= self.StageHalfHeight
        return (x, y)

    def showPosts(self):
        self.hidePosts()
        self.posts = [Toon.Toon(), Toon.Toon(), Toon.Toon(), Toon.Toon()]
        for i in xrange(len(self.posts)):
            toon = self.posts[i]
            toon.setDNA(base.localAvatar.getStyle())
            toon.reparentTo(render)
            x = self.StageHalfWidth
            y = self.StageHalfHeight
            if i > 1:
                x = -x
            if i % 2:
                y = -y
            toon.setPos(x, y, 0)

    def hidePosts(self):
        if hasattr(self, 'posts'):
            for toon in self.posts:
                toon.removeNode()

            del self.posts

    def showDropGrid(self):
        self.hideDropGrid()
        self.dropMarkers = []
        print 'dropRows: %s' % self.DropRows
        print 'dropCols: %s' % self.DropColumns
        for row in xrange(self.DropRows):
            self.dropMarkers.append([])
            rowList = self.dropMarkers[row]
            for column in xrange(self.DropColumns):
                toon = Toon.Toon()
                toon.setDNA(base.localAvatar.getStyle())
                toon.reparentTo(render)
                toon.setScale(1.0 / 3)
                x, y = self.grid2world(column, row)
                toon.setPos(x, y, 0)
                rowList.append(toon)

    def hideDropGrid(self):
        if hasattr(self, 'dropMarkers'):
            for row in self.dropMarkers:
                for marker in row:
                    marker.removeNode()

            del self.dropMarkers

    def onstage(self):
        self.notify.debug('onstage')
        DistributedMinigame.onstage(self)
        self.ground.reparentTo(render)
        self.scorePanels = []
        camera.reparentTo(render)
        camera.setPosHpr(*self.CameraPosHpr)
        lt = base.localAvatar
        lt.reparentTo(render)
        self.__placeToon(self.localAvId)
        lt.setSpeed(0, 0)
        toonSD = self.toonSDs[self.localAvId]
        toonSD.enter()
        toonSD.fsm.request('normal')
        self.orthoWalk.stop()
        radius = 0.7
        handler = CollisionHandlerEvent()
        handler.setInPattern('ltCatch%in')
        self.ltLegsCollNode = CollisionNode('catchLegsCollNode')
        self.ltLegsCollNode.setCollideMask(ToontownGlobals.CatchGameBitmask)
        self.ltHeadCollNode = CollisionNode('catchHeadCollNode')
        self.ltHeadCollNode.setCollideMask(ToontownGlobals.CatchGameBitmask)
        self.ltLHandCollNode = CollisionNode('catchLHandCollNode')
        self.ltLHandCollNode.setCollideMask(ToontownGlobals.CatchGameBitmask)
        self.ltRHandCollNode = CollisionNode('catchRHandCollNode')
        self.ltRHandCollNode.setCollideMask(ToontownGlobals.CatchGameBitmask)
        legsCollNodepath = lt.attachNewNode(self.ltLegsCollNode)
        legsCollNodepath.hide()
        head = base.localAvatar.getHeadParts().getPath(2)
        headCollNodepath = head.attachNewNode(self.ltHeadCollNode)
        headCollNodepath.hide()
        lHand = base.localAvatar.getLeftHands()[0]
        lHandCollNodepath = lHand.attachNewNode(self.ltLHandCollNode)
        lHandCollNodepath.hide()
        rHand = base.localAvatar.getRightHands()[0]
        rHandCollNodepath = rHand.attachNewNode(self.ltRHandCollNode)
        rHandCollNodepath.hide()
        lt.cTrav.addCollider(legsCollNodepath, handler)
        lt.cTrav.addCollider(headCollNodepath, handler)
        lt.cTrav.addCollider(lHandCollNodepath, handler)
        lt.cTrav.addCollider(lHandCollNodepath, handler)
        if self.ShowToonSpheres:
            legsCollNodepath.show()
            headCollNodepath.show()
            lHandCollNodepath.show()
            rHandCollNodepath.show()
        self.ltLegsCollNode.addSolid(CollisionSphere(0, 0, radius, radius))
        self.ltHeadCollNode.addSolid(CollisionSphere(0, 0, 0, radius))
        self.ltLHandCollNode.addSolid(
            CollisionSphere(0, 0, 0, 2 * radius / 3.0))
        self.ltRHandCollNode.addSolid(
            CollisionSphere(0, 0, 0, 2 * radius / 3.0))
        self.toonCollNodes = [
            legsCollNodepath, headCollNodepath, lHandCollNodepath,
            rHandCollNodepath
        ]
        if self.PredictiveSmoothing:
            DistributedSmoothNode.activateSmoothing(1, 1)
        self.introMovie.start()

    def offstage(self):
        self.notify.debug('offstage')
        DistributedSmoothNode.activateSmoothing(1, 0)
        self.introMovie.finish()
        for avId in self.toonSDs.keys():
            self.toonSDs[avId].exit()

        self.hidePosts()
        self.hideDropGrid()
        for collNode in self.toonCollNodes:
            while collNode.node().getNumSolids():
                collNode.node().removeSolid(0)

            base.localAvatar.cTrav.removeCollider(collNode)

        del self.toonCollNodes
        for panel in self.scorePanels:
            panel.cleanup()

        del self.scorePanels
        self.ground.reparentTo(hidden)
        DistributedMinigame.offstage(self)

    def handleDisabledAvatar(self, avId):
        self.notify.debug('handleDisabledAvatar')
        self.notify.debug('avatar ' + str(avId) + ' disabled')
        self.toonSDs[avId].exit(unexpectedExit=True)
        del self.toonSDs[avId]
        DistributedMinigame.handleDisabledAvatar(self, avId)

    def __placeToon(self, avId):
        toon = self.getAvatar(avId)
        idx = self.avIdList.index(avId)
        x = lineupPos(idx, self.numPlayers, 4.0)
        toon.setPos(x, 0, 0)
        toon.setHpr(180, 0, 0)

    def setGameReady(self):
        if not self.hasLocalToon:
            return
        self.notify.debug('setGameReady')
        if DistributedMinigame.setGameReady(self):
            return
        headCollNP = base.localAvatar.find('**/catchHeadCollNode')
        if headCollNP and not headCollNP.isEmpty():
            headCollNP.hide()
        for avId in self.remoteAvIdList:
            toon = self.getAvatar(avId)
            if toon:
                toon.reparentTo(render)
                self.__placeToon(avId)
                toonSD = CatchGameToonSD.CatchGameToonSD(avId, self)
                self.toonSDs[avId] = toonSD
                toonSD.load()
                toonSD.enter()
                toonSD.fsm.request('normal')
                toon.startSmooth()

    def setGameStart(self, timestamp):
        if not self.hasLocalToon:
            return
        self.notify.debug('setGameStart')
        DistributedMinigame.setGameStart(self, timestamp)
        self.introMovie.finish()
        camera.reparentTo(render)
        camera.setPosHpr(*self.CameraPosHpr)
        self.gameFSM.request('play')

    def enterOff(self):
        self.notify.debug('enterOff')

    def exitOff(self):
        pass

    def enterPlay(self):
        self.notify.debug('enterPlay')
        self.orthoWalk.start()
        for suit in self.suits:
            suitCollSphere = CollisionSphere(0, 0, 0, 1.0)
            suit.collSphereName = 'suitCollSphere%s' % self.suits.index(suit)
            suitCollSphere.setTangible(0)
            suitCollNode = CollisionNode(self.uniqueName(suit.collSphereName))
            suitCollNode.setIntoCollideMask(ToontownGlobals.WallBitmask)
            suitCollNode.addSolid(suitCollSphere)
            suit.collNodePath = suit.attachNewNode(suitCollNode)
            suit.collNodePath.hide()
            if self.ShowSuitSpheres:
                suit.collNodePath.show()
            self.accept(self.uniqueName('enter' + suit.collSphereName),
                        self.handleSuitCollision)

        self.scores = [0] * self.numPlayers
        spacing = 0.4
        for i in xrange(self.numPlayers):
            avId = self.avIdList[i]
            avName = self.getAvatarName(avId)
            scorePanel = MinigameAvatarScorePanel.MinigameAvatarScorePanel(
                avId, avName)
            scorePanel.setScale(0.9)
            scorePanel.setPos(-0.583 - spacing * (self.numPlayers - 1 - i),
                              0.0, -0.15)
            scorePanel.reparentTo(base.a2dTopRight)
            scorePanel.makeTransparent(0.75)
            self.scorePanels.append(scorePanel)

        self.fruitsCaught = 0
        self.droppedObjCaught = {}
        self.dropIntervals = {}
        self.droppedObjNames = []
        self.dropSchedule = []
        self.numItemsDropped = 0
        self.scheduleDrops()
        self.startDropTask()
        if self.WantSuits:
            self.startSuitWalkTask()
        self.timer = ToontownTimer.ToontownTimer()
        self.timer.posInTopRightCorner()
        self.timer.setTime(CatchGameGlobals.GameDuration)
        self.timer.countdown(CatchGameGlobals.GameDuration, self.timerExpired)
        self.timer.setTransparency(1)
        self.timer.setColorScale(1, 1, 1, 0.75)
        base.playMusic(self.music, looping=0, volume=0.9)

    def exitPlay(self):
        self.stopDropTask()
        self.stopSuitWalkTask()
        if hasattr(self, 'perfectIval'):
            self.perfectIval.pause()
            del self.perfectIval
        self.timer.stop()
        self.timer.destroy()
        del self.timer
        self.music.stop()
        for suit in self.suits:
            self.ignore(self.uniqueName('enter' + suit.collSphereName))
            suit.collNodePath.removeNode()

        for ival in self.dropIntervals.values():
            ival.finish()

        del self.dropIntervals
        del self.droppedObjNames
        del self.droppedObjCaught
        del self.dropSchedule
        taskMgr.remove(self.EndGameTaskName)

    def timerExpired(self):
        pass

    def __handleCatch(self, objNum):
        self.notify.debug('catch: %s' % objNum)
        self.showCatch(self.localAvId, objNum)
        objName = self.droppedObjNames[objNum]
        objTypeId = CatchGameGlobals.Name2DOTypeId[objName]
        self.sendUpdate('claimCatch', [objNum, objTypeId])
        self.finishDropInterval(objNum)

    def showCatch(self, avId, objNum):
        isLocal = avId == self.localAvId
        objName = self.droppedObjNames[objNum]
        objType = Name2DropObjectType[objName]
        if objType.good:
            if objNum not in self.droppedObjCaught:
                if isLocal:
                    base.playSfx(self.sndGoodCatch)
                fruit = self.getObjModel(objName)
                toon = self.getAvatar(avId)
                rHand = toon.getRightHands()[0]
                self.toonSDs[avId].eatFruit(fruit, rHand)
        else:
            self.toonSDs[avId].fsm.request('fallForward')
        self.droppedObjCaught[objNum] = 1

    def setObjectCaught(self, avId, objNum):
        if not self.hasLocalToon:
            return
        if self.gameFSM.getCurrentState().getName() != 'play':
            self.notify.warning('ignoring msg: object %s caught by %s' %
                                (objNum, avId))
            return
        isLocal = avId == self.localAvId
        if not isLocal:
            self.notify.debug('AI: avatar %s caught %s' % (avId, objNum))
            self.finishDropInterval(objNum)
            self.showCatch(avId, objNum)
        objName = self.droppedObjNames[objNum]
        if Name2DropObjectType[objName].good:
            i = self.avIdList.index(avId)
            self.scores[i] += 1
            self.scorePanels[i].setScore(self.scores[i])
            self.fruitsCaught += 1

    def finishDropInterval(self, objNum):
        if objNum in self.dropIntervals:
            self.dropIntervals[objNum].finish()

    def scheduleDrops(self):
        self.droppedObjNames = [self.fruitName
                                ] * self.numFruits + ['anvil'] * self.numAnvils
        self.randomNumGen.shuffle(self.droppedObjNames)
        dropPlacer = self.DropPlacerType(self, self.getNumPlayers(),
                                         self.droppedObjNames)
        while not dropPlacer.doneDropping():
            self.dropSchedule.append(dropPlacer.getNextDrop())

    def startDropTask(self):
        taskMgr.add(self.dropTask, self.DropTaskName)

    def stopDropTask(self):
        taskMgr.remove(self.DropTaskName)

    def dropTask(self, task):
        curT = self.getCurrentGameTime()
        while self.dropSchedule[0][0] <= curT:
            drop = self.dropSchedule[0]
            self.dropSchedule = self.dropSchedule[1:]
            dropTime, objName, dropCoords = drop
            objNum = self.numItemsDropped
            lastDrop = len(self.dropSchedule) == 0
            x, y = self.grid2world(*dropCoords)
            dropIval = self.getDropIval(x, y, objName, objNum)

            def cleanup(self=self, objNum=objNum, lastDrop=lastDrop):
                del self.dropIntervals[objNum]
                if lastDrop:
                    self.sendUpdate('reportDone')

            dropIval.append(Func(cleanup))
            self.dropIntervals[objNum] = dropIval
            self.numItemsDropped += 1
            dropIval.start(curT - dropTime)
            if lastDrop:
                return Task.done

        return Task.cont

    def setEveryoneDone(self):
        if not self.hasLocalToon:
            return
        if self.gameFSM.getCurrentState().getName() != 'play':
            self.notify.warning('ignoring setEveryoneDone msg')
            return
        self.notify.debug('setEveryoneDone')

        def endGame(task, self=self):
            if not CatchGameGlobals.EndlessGame:
                self.gameOver()
            return Task.done

        self.notify.debug('num fruits: %s' % self.numFruits)
        self.notify.debug('num catches: %s' % self.fruitsCaught)
        self.timer.hide()

        if self.fruitsCaught >= self.numFruits:
            self.notify.debug('perfect game!')
            perfectTextSubnode = hidden.attachNewNode(
                self.__genText(TTLocalizer.CatchGamePerfect))
            perfectText = hidden.attachNewNode('perfectText')
            perfectTextSubnode.reparentTo(perfectText)
            frame = self.__textGen.getCardActual()
            offsetY = -abs(frame[2] + frame[3]) / 2.0
            perfectTextSubnode.setPos(0, 0, offsetY)
            perfectText.setColor(1, 0.1, 0.1, 1)

            def fadeFunc(t, text=perfectText):
                text.setColorScale(1, 1, 1, t)

            def destroyText(text=perfectText):
                text.removeNode()

            textTrack = Sequence(
                Func(perfectText.reparentTo, aspect2d),
                Parallel(
                    LerpScaleInterval(perfectText,
                                      duration=0.5,
                                      scale=0.3,
                                      startScale=0.0),
                    LerpFunctionInterval(fadeFunc,
                                         fromData=0.0,
                                         toData=1.0,
                                         duration=0.5)), Wait(2.0),
                Parallel(
                    LerpScaleInterval(perfectText, duration=0.5, scale=1.0),
                    LerpFunctionInterval(fadeFunc,
                                         fromData=1.0,
                                         toData=0.0,
                                         duration=0.5,
                                         blendType='easeIn')),
                Func(destroyText), WaitInterval(0.5), Func(endGame, None))
            soundTrack = SoundInterval(self.sndPerfect)
            self.perfectIval = Parallel(textTrack, soundTrack)
            self.perfectIval.start()
        else:
            taskMgr.doMethodLater(1, endGame, self.EndGameTaskName)
        return

    def getDropIval(self, x, y, dropObjName, num):
        objType = Name2DropObjectType[dropObjName]
        dropNode = hidden.attachNewNode('catchDropNode%s' % num)
        dropNode.setPos(x, y, 0)
        shadow = self.dropShadow.copyTo(dropNode)
        shadow.setZ(0.2)
        shadow.setColor(1, 1, 1, 1)
        object = self.getObjModel(dropObjName)
        object.reparentTo(dropNode)
        if dropObjName in ['watermelon', 'anvil']:
            objH = object.getH()
            absDelta = {'watermelon': 12, 'anvil': 15}[dropObjName]
            delta = (self.randomNumGen.random() * 2.0 - 1.0) * absDelta
            newH = objH + delta
        else:
            newH = self.randomNumGen.random() * 360.0
        object.setH(newH)
        sphereName = 'FallObj%s' % num
        radius = self.ObjRadius
        if objType.good:
            radius *= lerp(1.0, 1.3, self.getDifficulty())
        collSphere = CollisionSphere(0, 0, 0, radius)
        collSphere.setTangible(0)
        collNode = CollisionNode(sphereName)
        collNode.setCollideMask(ToontownGlobals.CatchGameBitmask)
        collNode.addSolid(collSphere)
        collNodePath = object.attachNewNode(collNode)
        collNodePath.hide()
        if self.ShowObjSpheres:
            collNodePath.show()
        catchEventName = 'ltCatch' + sphereName

        def eatCollEntry(forward, collEntry):
            forward()

        self.accept(catchEventName,
                    Functor(eatCollEntry, Functor(self.__handleCatch, num)))

        def cleanup(self=self,
                    dropNode=dropNode,
                    num=num,
                    event=catchEventName):
            self.ignore(event)
            dropNode.removeNode()

        duration = objType.fallDuration
        onscreenDuration = objType.onscreenDuration
        dropHeight = self.MinOffscreenHeight
        targetShadowScale = 0.3
        if self.TrickShadows:
            intermedScale = targetShadowScale * (self.OffscreenTime /
                                                 self.BaselineDropDuration)
            shadowScaleIval = Sequence(
                LerpScaleInterval(shadow,
                                  self.OffscreenTime,
                                  intermedScale,
                                  startScale=0))
            shadowScaleIval.append(
                LerpScaleInterval(shadow,
                                  duration - self.OffscreenTime,
                                  targetShadowScale,
                                  startScale=intermedScale))
        else:
            shadowScaleIval = LerpScaleInterval(shadow,
                                                duration,
                                                targetShadowScale,
                                                startScale=0)
        targetShadowAlpha = 0.4
        shadowAlphaIval = LerpColorScaleInterval(
            shadow,
            self.OffscreenTime,
            Point4(1, 1, 1, targetShadowAlpha),
            startColorScale=Point4(1, 1, 1, 0))
        shadowIval = Parallel(shadowScaleIval, shadowAlphaIval)
        if self.UseGravity:

            def setObjPos(t, objType=objType, object=object):
                z = objType.trajectory.calcZ(t)
                object.setZ(z)

            setObjPos(0)
            dropIval = LerpFunctionInterval(setObjPos,
                                            fromData=0,
                                            toData=onscreenDuration,
                                            duration=onscreenDuration)
        else:
            startPos = Point3(0, 0, self.MinOffscreenHeight)
            object.setPos(startPos)
            dropIval = LerpPosInterval(object,
                                       onscreenDuration,
                                       Point3(0, 0, 0),
                                       startPos=startPos,
                                       blendType='easeIn')
        ival = Sequence(Func(Functor(dropNode.reparentTo, render)),
                        Parallel(
                            Sequence(WaitInterval(self.OffscreenTime),
                                     dropIval), shadowIval),
                        Func(cleanup),
                        name='drop%s' % num)
        landSound = None
        if objType == Name2DropObjectType['anvil']:
            landSound = self.sndAnvilLand
        if landSound:
            ival.append(SoundInterval(landSound))
        return ival

    def startSuitWalkTask(self):
        ival = Parallel(name='catchGameMetaSuitWalk')
        rng = RandomNumGen(self.randomNumGen)
        delay = 0.0
        while delay < CatchGameGlobals.GameDuration:
            delay += lerp(self.SuitPeriodRange[0], self.SuitPeriodRange[0],
                          rng.random())
            walkIval = Sequence(name='catchGameSuitWalk')
            walkIval.append(Wait(delay))

            def pickY(self=self, rng=rng):
                return lerp(-self.StageHalfHeight, self.StageHalfHeight,
                            rng.random())

            m = [2.5, 2.5, 2.3, 2.1][self.getNumPlayers() - 1]
            startPos = Point3(-(self.StageHalfWidth * m), pickY(), 0)
            stopPos = Point3(self.StageHalfWidth * m, pickY(), 0)
            if rng.choice([0, 1]):
                startPos, stopPos = stopPos, startPos
            walkIval.append(self.getSuitWalkIval(startPos, stopPos, rng))
            ival.append(walkIval)

        ival.start()
        self.suitWalkIval = ival

    def stopSuitWalkTask(self):
        self.suitWalkIval.finish()
        del self.suitWalkIval

    def getSuitWalkIval(self, startPos, stopPos, rng):
        data = {}
        lerpNP = render.attachNewNode('catchGameSuitParent')

        def setup(self=self,
                  startPos=startPos,
                  stopPos=stopPos,
                  data=data,
                  lerpNP=lerpNP,
                  rng=rng):
            if len(self.suits) == 0:
                return
            suit = rng.choice(self.suits)
            data['suit'] = suit
            self.suits.remove(suit)
            suit.reparentTo(lerpNP)
            suit.loop('walk')
            suit.setPlayRate(self.SuitSpeed / ToontownGlobals.SuitWalkSpeed,
                             'walk')
            suit.setPos(0, 0, 0)
            lerpNP.setPos(startPos)
            suit.lookAt(stopPos)

        def cleanup(self=self, data=data, lerpNP=lerpNP):
            if 'suit' in data:
                suit = data['suit']
                suit.reparentTo(hidden)
                self.suits.append(suit)
            lerpNP.removeNode()

        distance = Vec3(stopPos - startPos).length()
        duration = distance / self.SuitSpeed
        ival = Sequence(FunctionInterval(setup),
                        LerpPosInterval(lerpNP, duration, stopPos),
                        FunctionInterval(cleanup))
        return ival

    def handleSuitCollision(self, collEntry):
        self.toonSDs[self.localAvId].fsm.request('fallBack')
        timestamp = globalClockDelta.localToNetworkTime(
            globalClock.getFrameTime())
        self.sendUpdate('hitBySuit', [self.localAvId, timestamp])

    def hitBySuit(self, avId, timestamp):
        if not self.hasLocalToon:
            return
        if self.gameFSM.getCurrentState().getName() != 'play':
            self.notify.warning('ignoring msg: av %s hit by suit' % avId)
            return
        toon = self.getAvatar(avId)
        if toon == None:
            return
        self.notify.debug('avatar %s hit by a suit' % avId)
        if avId != self.localAvId:
            self.toonSDs[avId].fsm.request('fallBack')
        return

    def enterCleanup(self):
        self.notify.debug('enterCleanup')

    def exitCleanup(self):
        pass

    def initOrthoWalk(self):
        self.notify.debug('startOrthoWalk')

        def doCollisions(oldPos, newPos, self=self):
            x = bound(newPos[0], self.StageHalfWidth, -self.StageHalfWidth)
            y = bound(newPos[1], self.StageHalfHeight, -self.StageHalfHeight)
            newPos.setX(x)
            newPos.setY(y)
            return newPos

        orthoDrive = OrthoDrive(self.ToonSpeed,
                                customCollisionCallback=doCollisions)
        self.orthoWalk = OrthoWalk(orthoDrive,
                                   broadcast=not self.isSinglePlayer())

    def destroyOrthoWalk(self):
        self.notify.debug('destroyOrthoWalk')
        self.orthoWalk.destroy()
        del self.orthoWalk

    def getIntroMovie(self):
        locNode = self.ground.find('**/locator_tree')
        treeNode = locNode.attachNewNode('treeNode')
        treeNode.setHpr(render, 0, 0, 0)

        def cleanupTree(treeNode=treeNode):
            treeNode.removeNode()

        initialCamPosHpr = (-0.21, -19.56, 13.94, 0.0, 26.57, 0.0)
        suitViewCamPosHpr = (0, -11.5, 13, 0, -35, 0)
        finalCamPosHpr = self.CameraPosHpr
        cameraIval = Sequence(
            Func(camera.reparentTo, render),
            Func(camera.setPosHpr, treeNode, *initialCamPosHpr),
            WaitInterval(4.0),
            LerpPosHprInterval(camera,
                               2.0,
                               Point3(*suitViewCamPosHpr[:3]),
                               Point3(*suitViewCamPosHpr[3:]),
                               blendType='easeInOut',
                               name='lerpToSuitView'), WaitInterval(4.0),
            LerpPosHprInterval(camera,
                               3.0,
                               Point3(*finalCamPosHpr[:3]),
                               Point3(*finalCamPosHpr[3:]),
                               blendType='easeInOut',
                               name='lerpToPlayView'))

        def getIntroToon(toonProperties, parent, pos):
            toon = Toon.Toon()
            dna = ToonDNA.ToonDNA()
            dna.newToonFromProperties(*toonProperties)
            toon.setDNA(dna)
            toon.reparentTo(parent)
            toon.setPos(*pos)
            toon.setH(180)
            toon.startBlink()
            return toon

        def cleanupIntroToon(toon):
            toon.detachNode()
            toon.stopBlink()
            toon.delete()

        def getThrowIval(toon, hand, object, leftToon, isAnvil=0):
            anim = 'catch-intro-throw'
            grabFrame = 12
            fullSizeFrame = 30
            framePeriod = 1.0 / toon.getFrameRate(anim)
            objScaleDur = (fullSizeFrame - grabFrame) * framePeriod
            releaseFrame = 35
            trajDuration = 1.6
            trajDistance = 4
            if leftToon:
                releaseFrame = 34
                trajDuration = 1.0
                trajDistance = 1
            animIval = ActorInterval(toon, anim, loop=0)

            def getThrowDest(object=object, offset=trajDistance):
                dest = object.getPos(render)
                dest += Point3(0, -offset, 0)
                dest.setZ(0)
                return dest

            if leftToon:
                trajIval = ProjectileInterval(object,
                                              startVel=Point3(0, 0, 0),
                                              duration=trajDuration)
            else:
                trajIval = ProjectileInterval(object,
                                              endPos=getThrowDest,
                                              duration=trajDuration)
            trajIval = Sequence(Func(object.wrtReparentTo, render), trajIval,
                                Func(object.wrtReparentTo, hidden))
            if isAnvil:
                trajIval.append(SoundInterval(self.sndAnvilLand))
            objIval = Track(
                (grabFrame * framePeriod,
                 Sequence(
                     Func(object.reparentTo, hand),
                     Func(object.setPosHpr, 0.05, -.13, 0.62, 0, 0, 336.8),
                     LerpScaleInterval(object,
                                       objScaleDur,
                                       1.0,
                                       startScale=0.1,
                                       blendType='easeInOut'))),
                (releaseFrame * framePeriod, trajIval))

            def cleanup(object=object):
                object.reparentTo(hidden)
                object.removeNode()

            throwIval = Sequence(Parallel(animIval, objIval), Func(cleanup))
            return throwIval

        tY = -4.0
        tZ = 19.5
        props = ['css', 'md', 'm', 'f', 9, 0, 9, 9, 13, 5, 11, 5, 8, 7]
        leftToon = getIntroToon(props, treeNode, [-2.3, tY, tZ])
        props = ['mss', 'ls', 'l', 'm', 6, 0, 6, 6, 3, 5, 3, 5, 5, 0]
        rightToon = getIntroToon(props, treeNode, [1.8, tY, tZ])
        fruit = self.getObjModel(self.fruitName)
        if self.fruitName == 'pineapple':
            fruit.setZ(0.42)
            fruit.flattenMedium()
        anvil = self.getObjModel('anvil')
        anvil.setH(100)
        anvil.setZ(0.42)
        anvil.flattenMedium()
        leftToonIval = getThrowIval(leftToon,
                                    leftToon.getRightHands()[0],
                                    fruit,
                                    leftToon=1)
        rightToonIval = getThrowIval(rightToon,
                                     rightToon.getLeftHands()[0],
                                     anvil,
                                     leftToon=0,
                                     isAnvil=1)
        animDur = leftToon.getNumFrames(
            'catch-intro-throw') / leftToon.getFrameRate('catch-intro-throw')
        toonIval = Sequence(
            Parallel(
                Sequence(leftToonIval, Func(leftToon.loop, 'neutral')),
                Sequence(Func(rightToon.loop, 'neutral'),
                         WaitInterval(animDur / 2.0), rightToonIval,
                         Func(rightToon.loop, 'neutral')),
                WaitInterval(cameraIval.getDuration())),
            Func(cleanupIntroToon, leftToon), Func(cleanupIntroToon,
                                                   rightToon))
        self.treeNode = treeNode
        self.fruit = fruit
        self.anvil = anvil
        self.leftToon = leftToon
        self.rightToon = rightToon
        introMovie = Sequence(Parallel(cameraIval, toonIval),
                              Func(cleanupTree))
        return introMovie
class DistributedCatchGame(DistributedMinigame):
    DropTaskName = 'dropSomething'
    EndGameTaskName = 'endCatchGame'
    SuitWalkTaskName = 'catchGameSuitWalk'
    DropObjectPlurals = {'apple': TTLocalizer.CatchGameApples,
     'orange': TTLocalizer.CatchGameOranges,
     'pear': TTLocalizer.CatchGamePears,
     'coconut': TTLocalizer.CatchGameCoconuts,
     'watermelon': TTLocalizer.CatchGameWatermelons,
     'pineapple': TTLocalizer.CatchGamePineapples,
     'anvil': TTLocalizer.CatchGameAnvils}

    def __init__(self, cr):
        DistributedMinigame.__init__(self, cr)
        self.gameFSM = ClassicFSM.ClassicFSM('DistributedCatchGame', [State.State('off', self.enterOff, self.exitOff, ['play']), State.State('play', self.enterPlay, self.exitPlay, ['cleanup']), State.State('cleanup', self.enterCleanup, self.exitCleanup, [])], 'off', 'cleanup')
        self.addChildGameFSM(self.gameFSM)
        self.setUsesSmoothing()
        self.setUsesLookAround()

    def getTitle(self):
        return TTLocalizer.CatchGameTitle

    def getInstructions(self):
        return TTLocalizer.CatchGameInstructions % {'fruit': self.DropObjectPlurals[self.fruitName],
         'badThing': self.DropObjectPlurals['anvil']}

    def getMaxDuration(self):
        return CatchGameGlobals.GameDuration + 5

    def load(self):
        self.notify.debug('load')
        DistributedMinigame.load(self)
        self.defineConstants()
        groundModels = ['phase_4/models/minigames/treehouse_2players',
         'phase_4/models/minigames/treehouse_2players',
         'phase_4/models/minigames/treehouse_3players',
         'phase_4/models/minigames/treehouse_4players']
        index = self.getNumPlayers() - 1
        self.ground = loader.loadModel(groundModels[index])
        self.ground.setHpr(180, -90, 0)
        self.dropShadow = loader.loadModel('phase_3/models/props/drop_shadow')
        self.dropObjModels = {}
        for objType in DropObjectTypes:
            if objType.name not in ['anvil', self.fruitName]:
                continue
            model = loader.loadModel(objType.modelPath)
            self.dropObjModels[objType.name] = model
            modelScales = {'apple': 0.7,
             'orange': 0.7,
             'pear': 0.5,
             'coconut': 0.7,
             'watermelon': 0.6,
             'pineapple': 0.45}
            if modelScales.has_key(objType.name):
                model.setScale(modelScales[objType.name])
            if objType == Name2DropObjectType['pear']:
                model.setZ(-.6)
            if objType == Name2DropObjectType['coconut']:
                model.setP(180)
            if objType == Name2DropObjectType['watermelon']:
                model.setH(135)
                model.setZ(-.5)
            if objType == Name2DropObjectType['pineapple']:
                model.setZ(-1.7)
            if objType == Name2DropObjectType['anvil']:
                model.setZ(-self.ObjRadius)
            model.flattenMedium()

        self.music = base.loadMusic('phase_4/audio/bgm/MG_toontag.ogg')
        self.sndGoodCatch = base.loadSfx('phase_4/audio/sfx/SZ_DD_treasure.ogg')
        self.sndOof = base.loadSfx('phase_4/audio/sfx/MG_cannon_hit_dirt.ogg')
        self.sndAnvilLand = base.loadSfx('phase_4/audio/sfx/AA_drop_anvil_miss.ogg')
        self.sndPerfect = base.loadSfx('phase_4/audio/sfx/ring_perfect.ogg')
        self.toonSDs = {}
        avId = self.localAvId
        toonSD = CatchGameToonSD.CatchGameToonSD(avId, self)
        self.toonSDs[avId] = toonSD
        toonSD.load()
        if self.WantSuits:
            suitTypes = ['f',
             'tm',
             'pp',
             'dt']
            self.suits = []
            for type in suitTypes:
                suit = Suit.Suit()
                d = SuitDNA.SuitDNA()
                d.newSuit(type)
                suit.setDNA(d)
                suit.pose('walk', 0)
                self.suits.append(suit)

        self.__textGen = TextNode('ringGame')
        self.__textGen.setFont(ToontownGlobals.getSignFont())
        self.__textGen.setAlign(TextNode.ACenter)
        self.introMovie = self.getIntroMovie()

    def unload(self):
        self.notify.debug('unload')
        DistributedMinigame.unload(self)
        self.removeChildGameFSM(self.gameFSM)
        del self.gameFSM
        self.introMovie.finish()
        del self.introMovie
        del self.__textGen
        for avId in self.toonSDs.keys():
            toonSD = self.toonSDs[avId]
            toonSD.unload()

        del self.toonSDs
        for suit in self.suits:
            suit.reparentTo(hidden)
            suit.delete()

        del self.suits
        self.ground.removeNode()
        del self.ground
        self.dropShadow.removeNode()
        del self.dropShadow
        for model in self.dropObjModels.values():
            model.removeNode()

        del self.dropObjModels
        del self.music
        del self.sndGoodCatch
        del self.sndOof
        del self.sndAnvilLand
        del self.sndPerfect

    def getObjModel(self, objName):
        return self.dropObjModels[objName].copyTo(hidden)

    def __genText(self, text):
        self.__textGen.setText(text)
        return self.__textGen.generate()

    def calcDifficultyConstants(self, difficulty, numPlayers):
        ToonSpeedRange = [16.0, 25.0]
        self.ToonSpeed = lerp(ToonSpeedRange[0], ToonSpeedRange[1], difficulty)
        self.SuitSpeed = self.ToonSpeed / 2.0
        self.SuitPeriodRange = [lerp(5.0, 3.0, self.getDifficulty()), lerp(15.0, 8.0, self.getDifficulty())]

        def scaledDimensions(widthHeight, scale):
            w, h = widthHeight
            return [math.sqrt(scale * w * w), math.sqrt(scale * h * h)]

        BaseStageDimensions = [20, 15]
        areaScales = [1.0,
         1.0,
         3.0 / 2,
         4.0 / 2]
        self.StageAreaScale = areaScales[numPlayers - 1]
        self.StageLinearScale = math.sqrt(self.StageAreaScale)
        self.notify.debug('StageLinearScale: %s' % self.StageLinearScale)
        self.StageDimensions = scaledDimensions(BaseStageDimensions, self.StageAreaScale)
        self.notify.debug('StageDimensions: %s' % self.StageDimensions)
        self.StageHalfWidth = self.StageDimensions[0] / 2.0
        self.StageHalfHeight = self.StageDimensions[1] / 2.0
        MOHs = [24] * 2 + [26, 28]
        self.MinOffscreenHeight = MOHs[self.getNumPlayers() - 1]
        distance = math.sqrt(self.StageDimensions[0] * self.StageDimensions[0] + self.StageDimensions[1] * self.StageDimensions[1])
        distance /= self.StageLinearScale
        if self.DropPlacerType == PathDropPlacer:
            distance /= 1.5
        ToonRunDuration = distance / self.ToonSpeed
        offScreenOnScreenRatio = 1.0
        fraction = 1.0 / 3 * 0.85
        self.BaselineOnscreenDropDuration = ToonRunDuration / (fraction * (1.0 + offScreenOnScreenRatio))
        self.notify.debug('BaselineOnscreenDropDuration=%s' % self.BaselineOnscreenDropDuration)
        self.OffscreenTime = offScreenOnScreenRatio * self.BaselineOnscreenDropDuration
        self.notify.debug('OffscreenTime=%s' % self.OffscreenTime)
        self.BaselineDropDuration = self.BaselineOnscreenDropDuration + self.OffscreenTime
        self.MaxDropDuration = self.BaselineDropDuration
        self.DropPeriod = self.BaselineDropDuration / 2.0
        scaledNumPlayers = (numPlayers - 1.0) * 0.75 + 1.0
        self.DropPeriod /= scaledNumPlayers
        typeProbs = {'fruit': 3,
         'anvil': 1}
        probSum = reduce(lambda x, y: x + y, typeProbs.values())
        for key in typeProbs.keys():
            typeProbs[key] = float(typeProbs[key]) / probSum

        scheduler = DropScheduler(CatchGameGlobals.GameDuration, self.FirstDropDelay, self.DropPeriod, self.MaxDropDuration, self.FasterDropDelay, self.FasterDropPeriodMult)
        self.totalDrops = 0
        while not scheduler.doneDropping():
            scheduler.stepT()
            self.totalDrops += 1

        self.numFruits = int(self.totalDrops * typeProbs['fruit'])
        self.numAnvils = int(self.totalDrops - self.numFruits)

    def getNumPlayers(self):
        return self.numPlayers

    def defineConstants(self):
        self.notify.debug('defineConstants')
        self.DropPlacerType = RegionDropPlacer
        fruits = {ToontownGlobals.ToontownCentral: 'apple',
         ToontownGlobals.DonaldsDock: 'orange',
         ToontownGlobals.DaisyGardens: 'pear',
         ToontownGlobals.MinniesMelodyland: 'coconut',
         ToontownGlobals.TheBrrrgh: 'watermelon',
         ToontownGlobals.DonaldsDreamland: 'pineapple'}
        self.fruitName = fruits[self.getSafezoneId()]
        self.ShowObjSpheres = 0
        self.ShowToonSpheres = 0
        self.ShowSuitSpheres = 0
        self.PredictiveSmoothing = 1
        self.UseGravity = 1
        self.TrickShadows = 1
        self.WantSuits = 1
        self.FirstDropDelay = 0.5
        self.FasterDropDelay = int(2.0 / 3 * CatchGameGlobals.GameDuration)
        self.notify.debug('will start dropping fast after %s seconds' % self.FasterDropDelay)
        self.FasterDropPeriodMult = 0.5
        self.calcDifficultyConstants(self.getDifficulty(), self.getNumPlayers())
        self.notify.debug('ToonSpeed: %s' % self.ToonSpeed)
        self.notify.debug('total drops: %s' % self.totalDrops)
        self.notify.debug('numFruits: %s' % self.numFruits)
        self.notify.debug('numAnvils: %s' % self.numAnvils)
        self.ObjRadius = 1.0
        dropGridDimensions = [[5, 5],
         [5, 5],
         [6, 6],
         [7, 7]]
        self.DropRows, self.DropColumns = dropGridDimensions[self.getNumPlayers() - 1]
        self.cameraPosTable = [[0, -29.36, 28.17]] * 2 + [[0, -32.87, 30.43], [0, -35.59, 32.1]]
        self.cameraHpr = [0, -35, 0]
        self.CameraPosHpr = self.cameraPosTable[self.getNumPlayers() - 1] + self.cameraHpr
        for objType in DropObjectTypes:
            self.notify.debug('*** Object Type: %s' % objType.name)
            objType.onscreenDuration = objType.onscreenDurMult * self.BaselineOnscreenDropDuration
            self.notify.debug('onscreenDuration=%s' % objType.onscreenDuration)
            v_0 = 0.0
            t = objType.onscreenDuration
            x_0 = self.MinOffscreenHeight
            x = 0.0
            g = 2.0 * (x - x_0 - v_0 * t) / (t * t)
            self.notify.debug('gravity=%s' % g)
            objType.trajectory = Trajectory.Trajectory(0, Vec3(0, 0, x_0), Vec3(0, 0, v_0), gravMult=abs(g / Trajectory.Trajectory.gravity))
            objType.fallDuration = objType.onscreenDuration + self.OffscreenTime

    def grid2world(self, column, row):
        x = column / float(self.DropColumns - 1)
        y = row / float(self.DropRows - 1)
        x = x * 2.0 - 1.0
        y = y * 2.0 - 1.0
        x *= self.StageHalfWidth
        y *= self.StageHalfHeight
        return x, y

    def showPosts(self):
        self.hidePosts()
        self.posts = [Toon.Toon(),
         Toon.Toon(),
         Toon.Toon(),
         Toon.Toon()]
        for i in range(len(self.posts)):
            toon = self.posts[i]
            toon.setDNA(base.localAvatar.getStyle())
            toon.reparentTo(render)
            x = self.StageHalfWidth
            y = self.StageHalfHeight
            if i > 1:
                x = -x
            if i % 2:
                y = -y
            toon.setPos(x, y, 0)

    def hidePosts(self):
        if hasattr(self, 'posts'):
            for toon in self.posts:
                toon.removeNode()

            del self.posts

    def showDropGrid(self):
        self.hideDropGrid()
        self.dropMarkers = []
        print 'dropRows: %s' % self.DropRows
        print 'dropCols: %s' % self.DropColumns
        for row in range(self.DropRows):
            self.dropMarkers.append([])
            rowList = self.dropMarkers[row]
            for column in range(self.DropColumns):
                toon = Toon.Toon()
                toon.setDNA(base.localAvatar.getStyle())
                toon.reparentTo(render)
                toon.setScale(1.0 / 3)
                x, y = self.grid2world(column, row)
                toon.setPos(x, y, 0)
                rowList.append(toon)

    def hideDropGrid(self):
        if hasattr(self, 'dropMarkers'):
            for row in self.dropMarkers:
                for marker in row:
                    marker.removeNode()

            del self.dropMarkers

    def onstage(self):
        self.notify.debug('onstage')
        DistributedMinigame.onstage(self)
        self.ground.reparentTo(render)
        self.scorePanels = []
        camera.reparentTo(render)
        camera.setPosHpr(*self.CameraPosHpr)
        lt = base.localAvatar
        lt.reparentTo(render)
        self.__placeToon(self.localAvId)
        lt.setSpeed(0, 0)
        toonSD = self.toonSDs[self.localAvId]
        toonSD.enter()
        toonSD.fsm.request('normal')
        self.orthoWalk.stop()
        radius = 0.7
        handler = CollisionHandlerEvent()
        handler.setInPattern('ltCatch%in')
        self.ltLegsCollNode = CollisionNode('catchLegsCollNode')
        self.ltLegsCollNode.setCollideMask(ToontownGlobals.CatchGameBitmask)
        self.ltHeadCollNode = CollisionNode('catchHeadCollNode')
        self.ltHeadCollNode.setCollideMask(ToontownGlobals.CatchGameBitmask)
        self.ltLHandCollNode = CollisionNode('catchLHandCollNode')
        self.ltLHandCollNode.setCollideMask(ToontownGlobals.CatchGameBitmask)
        self.ltRHandCollNode = CollisionNode('catchRHandCollNode')
        self.ltRHandCollNode.setCollideMask(ToontownGlobals.CatchGameBitmask)
        legsCollNodepath = lt.attachNewNode(self.ltLegsCollNode)
        legsCollNodepath.hide()
        head = base.localAvatar.getHeadParts().getPath(2)
        headCollNodepath = head.attachNewNode(self.ltHeadCollNode)
        headCollNodepath.hide()
        lHand = base.localAvatar.getLeftHands()[0]
        lHandCollNodepath = lHand.attachNewNode(self.ltLHandCollNode)
        lHandCollNodepath.hide()
        rHand = base.localAvatar.getRightHands()[0]
        rHandCollNodepath = rHand.attachNewNode(self.ltRHandCollNode)
        rHandCollNodepath.hide()
        lt.cTrav.addCollider(legsCollNodepath, handler)
        lt.cTrav.addCollider(headCollNodepath, handler)
        lt.cTrav.addCollider(lHandCollNodepath, handler)
        lt.cTrav.addCollider(lHandCollNodepath, handler)
        if self.ShowToonSpheres:
            legsCollNodepath.show()
            headCollNodepath.show()
            lHandCollNodepath.show()
            rHandCollNodepath.show()
        self.ltLegsCollNode.addSolid(CollisionSphere(0, 0, radius, radius))
        self.ltHeadCollNode.addSolid(CollisionSphere(0, 0, 0, radius))
        self.ltLHandCollNode.addSolid(CollisionSphere(0, 0, 0, 2 * radius / 3.0))
        self.ltRHandCollNode.addSolid(CollisionSphere(0, 0, 0, 2 * radius / 3.0))
        self.toonCollNodes = [legsCollNodepath,
         headCollNodepath,
         lHandCollNodepath,
         rHandCollNodepath]
        if self.PredictiveSmoothing:
            DistributedSmoothNode.activateSmoothing(1, 1)
        self.introMovie.start()

    def offstage(self):
        self.notify.debug('offstage')
        DistributedSmoothNode.activateSmoothing(1, 0)
        self.introMovie.finish()
        for avId in self.toonSDs.keys():
            self.toonSDs[avId].exit()

        self.hidePosts()
        self.hideDropGrid()
        for collNode in self.toonCollNodes:
            while collNode.node().getNumSolids():
                collNode.node().removeSolid(0)

            base.localAvatar.cTrav.removeCollider(collNode)

        del self.toonCollNodes
        for panel in self.scorePanels:
            panel.cleanup()

        del self.scorePanels
        self.ground.reparentTo(hidden)
        DistributedMinigame.offstage(self)

    def handleDisabledAvatar(self, avId):
        self.notify.debug('handleDisabledAvatar')
        self.notify.debug('avatar ' + str(avId) + ' disabled')
        self.toonSDs[avId].exit(unexpectedExit=True)
        del self.toonSDs[avId]
        DistributedMinigame.handleDisabledAvatar(self, avId)

    def __placeToon(self, avId):
        toon = self.getAvatar(avId)
        idx = self.avIdList.index(avId)
        x = lineupPos(idx, self.numPlayers, 4.0)
        toon.setPos(x, 0, 0)
        toon.setHpr(180, 0, 0)

    def setGameReady(self):
        if not self.hasLocalToon:
            return
        self.notify.debug('setGameReady')
        if DistributedMinigame.setGameReady(self):
            return
        headCollNP = base.localAvatar.find('**/catchHeadCollNode')
        if headCollNP and not headCollNP.isEmpty():
            headCollNP.hide()
        for avId in self.remoteAvIdList:
            toon = self.getAvatar(avId)
            if toon:
                toon.reparentTo(render)
                self.__placeToon(avId)
                toonSD = CatchGameToonSD.CatchGameToonSD(avId, self)
                self.toonSDs[avId] = toonSD
                toonSD.load()
                toonSD.enter()
                toonSD.fsm.request('normal')
                toon.startSmooth()

    def setGameStart(self, timestamp):
        if not self.hasLocalToon:
            return
        self.notify.debug('setGameStart')
        DistributedMinigame.setGameStart(self, timestamp)
        self.introMovie.finish()
        camera.reparentTo(render)
        camera.setPosHpr(*self.CameraPosHpr)
        self.gameFSM.request('play')

    def enterOff(self):
        self.notify.debug('enterOff')

    def exitOff(self):
        pass

    def enterPlay(self):
        self.notify.debug('enterPlay')
        self.orthoWalk.start()
        for suit in self.suits:
            suitCollSphere = CollisionSphere(0, 0, 0, 1.0)
            suit.collSphereName = 'suitCollSphere%s' % self.suits.index(suit)
            suitCollSphere.setTangible(0)
            suitCollNode = CollisionNode(self.uniqueName(suit.collSphereName))
            suitCollNode.setIntoCollideMask(ToontownGlobals.WallBitmask)
            suitCollNode.addSolid(suitCollSphere)
            suit.collNodePath = suit.attachNewNode(suitCollNode)
            suit.collNodePath.hide()
            if self.ShowSuitSpheres:
                suit.collNodePath.show()
            self.accept(self.uniqueName('enter' + suit.collSphereName), self.handleSuitCollision)

        self.scores = [0] * self.numPlayers
        spacing = 0.4
        for i in xrange(self.numPlayers):
            avId = self.avIdList[i]
            avName = self.getAvatarName(avId)
            scorePanel = MinigameAvatarScorePanel.MinigameAvatarScorePanel(avId, avName)
            scorePanel.setScale(0.9)
            scorePanel.setPos(-0.583 - spacing * (self.numPlayers - 1 - i), 0.0, -0.15)
            scorePanel.reparentTo(base.a2dTopRight)
            scorePanel.makeTransparent(0.75)
            self.scorePanels.append(scorePanel)

        self.fruitsCaught = 0
        self.droppedObjCaught = {}
        self.dropIntervals = {}
        self.droppedObjNames = []
        self.dropSchedule = []
        self.numItemsDropped = 0
        self.scheduleDrops()
        self.startDropTask()
        if self.WantSuits:
            self.startSuitWalkTask()
        self.timer = ToontownTimer.ToontownTimer()
        self.timer.posInTopRightCorner()
        self.timer.setTime(CatchGameGlobals.GameDuration)
        self.timer.countdown(CatchGameGlobals.GameDuration, self.timerExpired)
        self.timer.setTransparency(1)
        self.timer.setColorScale(1, 1, 1, 0.75)
        base.playMusic(self.music, looping=0, volume=0.9)

    def exitPlay(self):
        self.stopDropTask()
        self.stopSuitWalkTask()
        if hasattr(self, 'perfectIval'):
            self.perfectIval.pause()
            del self.perfectIval
        self.timer.stop()
        self.timer.destroy()
        del self.timer
        self.music.stop()
        for suit in self.suits:
            self.ignore(self.uniqueName('enter' + suit.collSphereName))
            suit.collNodePath.removeNode()

        for ival in self.dropIntervals.values():
            ival.finish()

        del self.dropIntervals
        del self.droppedObjNames
        del self.droppedObjCaught
        del self.dropSchedule
        taskMgr.remove(self.EndGameTaskName)

    def timerExpired(self):
        pass

    def __handleCatch(self, objNum):
        self.notify.debug('catch: %s' % objNum)
        self.showCatch(self.localAvId, objNum)
        objName = self.droppedObjNames[objNum]
        objTypeId = CatchGameGlobals.Name2DOTypeId[objName]
        self.sendUpdate('claimCatch', [objNum, objTypeId])
        self.finishDropInterval(objNum)

    def showCatch(self, avId, objNum):
        isLocal = avId == self.localAvId
        objName = self.droppedObjNames[objNum]
        objType = Name2DropObjectType[objName]
        if objType.good:
            if not self.droppedObjCaught.has_key(objNum):
                if isLocal:
                    base.playSfx(self.sndGoodCatch)
                fruit = self.getObjModel(objName)
                toon = self.getAvatar(avId)
                rHand = toon.getRightHands()[0]
                self.toonSDs[avId].eatFruit(fruit, rHand)
        else:
            self.toonSDs[avId].fsm.request('fallForward')
        self.droppedObjCaught[objNum] = 1

    def setObjectCaught(self, avId, objNum):
        if not self.hasLocalToon:
            return
        if self.gameFSM.getCurrentState().getName() != 'play':
            self.notify.warning('ignoring msg: object %s caught by %s' % (objNum, avId))
            return
        isLocal = avId == self.localAvId
        if not isLocal:
            self.notify.debug('AI: avatar %s caught %s' % (avId, objNum))
            self.finishDropInterval(objNum)
            self.showCatch(avId, objNum)
        objName = self.droppedObjNames[objNum]
        if Name2DropObjectType[objName].good:
            i = self.avIdList.index(avId)
            self.scores[i] += 1
            self.scorePanels[i].setScore(self.scores[i])
            self.fruitsCaught += 1

    def finishDropInterval(self, objNum):
        if self.dropIntervals.has_key(objNum):
            self.dropIntervals[objNum].finish()

    def scheduleDrops(self):
        self.droppedObjNames = [self.fruitName] * self.numFruits + ['anvil'] * self.numAnvils
        self.randomNumGen.shuffle(self.droppedObjNames)
        dropPlacer = self.DropPlacerType(self, self.getNumPlayers(), self.droppedObjNames)
        while not dropPlacer.doneDropping():
            self.dropSchedule.append(dropPlacer.getNextDrop())

    def startDropTask(self):
        taskMgr.add(self.dropTask, self.DropTaskName)

    def stopDropTask(self):
        taskMgr.remove(self.DropTaskName)

    def dropTask(self, task):
        curT = self.getCurrentGameTime()
        while self.dropSchedule[0][0] <= curT:
            drop = self.dropSchedule[0]
            self.dropSchedule = self.dropSchedule[1:]
            dropTime, objName, dropCoords = drop
            objNum = self.numItemsDropped
            lastDrop = len(self.dropSchedule) == 0
            x, y = self.grid2world(*dropCoords)
            dropIval = self.getDropIval(x, y, objName, objNum)

            def cleanup(self = self, objNum = objNum, lastDrop = lastDrop):
                del self.dropIntervals[objNum]
                if lastDrop:
                    self.sendUpdate('reportDone')

            dropIval.append(Func(cleanup))
            self.dropIntervals[objNum] = dropIval
            self.numItemsDropped += 1
            dropIval.start(curT - dropTime)
            if lastDrop:
                return Task.done

        return Task.cont

    def setEveryoneDone(self):
        if not self.hasLocalToon:
            return
        if self.gameFSM.getCurrentState().getName() != 'play':
            self.notify.warning('ignoring setEveryoneDone msg')
            return
        self.notify.debug('setEveryoneDone')

        def endGame(task, self = self):
            if not CatchGameGlobals.EndlessGame:
                self.gameOver()
            return Task.done

        self.notify.debug('num fruits: %s' % self.numFruits)
        self.notify.debug('num catches: %s' % self.fruitsCaught)
        self.timer.hide()

        #For the Alpha Blueprint ARG
        if config.GetBool('want-blueprint4-ARG', False):
            MinigameGlobals.generateDebugARGPhrase()

        if self.fruitsCaught >= self.numFruits:
            self.notify.debug('perfect game!')
            perfectTextSubnode = hidden.attachNewNode(self.__genText(TTLocalizer.CatchGamePerfect))
            perfectText = hidden.attachNewNode('perfectText')
            perfectTextSubnode.reparentTo(perfectText)
            frame = self.__textGen.getCardActual()
            offsetY = -abs(frame[2] + frame[3]) / 2.0
            perfectTextSubnode.setPos(0, 0, offsetY)
            perfectText.setColor(1, 0.1, 0.1, 1)

            def fadeFunc(t, text = perfectText):
                text.setColorScale(1, 1, 1, t)

            def destroyText(text = perfectText):
                text.removeNode()

            textTrack = Sequence(Func(perfectText.reparentTo, aspect2d), Parallel(LerpScaleInterval(perfectText, duration=0.5, scale=0.3, startScale=0.0), LerpFunctionInterval(fadeFunc, fromData=0.0, toData=1.0, duration=0.5)), Wait(2.0), Parallel(LerpScaleInterval(perfectText, duration=0.5, scale=1.0), LerpFunctionInterval(fadeFunc, fromData=1.0, toData=0.0, duration=0.5, blendType='easeIn')), Func(destroyText), WaitInterval(0.5), Func(endGame, None))
            soundTrack = SoundInterval(self.sndPerfect)
            self.perfectIval = Parallel(textTrack, soundTrack)
            self.perfectIval.start()
        else:
            taskMgr.doMethodLater(1, endGame, self.EndGameTaskName)
        return

    def getDropIval(self, x, y, dropObjName, num):
        objType = Name2DropObjectType[dropObjName]
        dropNode = hidden.attachNewNode('catchDropNode%s' % num)
        dropNode.setPos(x, y, 0)
        shadow = self.dropShadow.copyTo(dropNode)
        shadow.setZ(0.2)
        shadow.setColor(1, 1, 1, 1)
        object = self.getObjModel(dropObjName)
        object.reparentTo(dropNode)
        if dropObjName in ['watermelon', 'anvil']:
            objH = object.getH()
            absDelta = {'watermelon': 12,
             'anvil': 15}[dropObjName]
            delta = (self.randomNumGen.random() * 2.0 - 1.0) * absDelta
            newH = objH + delta
        else:
            newH = self.randomNumGen.random() * 360.0
        object.setH(newH)
        sphereName = 'FallObj%s' % num
        radius = self.ObjRadius
        if objType.good:
            radius *= lerp(1.0, 1.3, self.getDifficulty())
        collSphere = CollisionSphere(0, 0, 0, radius)
        collSphere.setTangible(0)
        collNode = CollisionNode(sphereName)
        collNode.setCollideMask(ToontownGlobals.CatchGameBitmask)
        collNode.addSolid(collSphere)
        collNodePath = object.attachNewNode(collNode)
        collNodePath.hide()
        if self.ShowObjSpheres:
            collNodePath.show()
        catchEventName = 'ltCatch' + sphereName

        def eatCollEntry(forward, collEntry):
            forward()

        self.accept(catchEventName, Functor(eatCollEntry, Functor(self.__handleCatch, num)))

        def cleanup(self = self, dropNode = dropNode, num = num, event = catchEventName):
            self.ignore(event)
            dropNode.removeNode()

        duration = objType.fallDuration
        onscreenDuration = objType.onscreenDuration
        dropHeight = self.MinOffscreenHeight
        targetShadowScale = 0.3
        if self.TrickShadows:
            intermedScale = targetShadowScale * (self.OffscreenTime / self.BaselineDropDuration)
            shadowScaleIval = Sequence(LerpScaleInterval(shadow, self.OffscreenTime, intermedScale, startScale=0))
            shadowScaleIval.append(LerpScaleInterval(shadow, duration - self.OffscreenTime, targetShadowScale, startScale=intermedScale))
        else:
            shadowScaleIval = LerpScaleInterval(shadow, duration, targetShadowScale, startScale=0)
        targetShadowAlpha = 0.4
        shadowAlphaIval = LerpColorScaleInterval(shadow, self.OffscreenTime, Point4(1, 1, 1, targetShadowAlpha), startColorScale=Point4(1, 1, 1, 0))
        shadowIval = Parallel(shadowScaleIval, shadowAlphaIval)
        if self.UseGravity:

            def setObjPos(t, objType = objType, object = object):
                z = objType.trajectory.calcZ(t)
                object.setZ(z)

            setObjPos(0)
            dropIval = LerpFunctionInterval(setObjPos, fromData=0, toData=onscreenDuration, duration=onscreenDuration)
        else:
            startPos = Point3(0, 0, self.MinOffscreenHeight)
            object.setPos(startPos)
            dropIval = LerpPosInterval(object, onscreenDuration, Point3(0, 0, 0), startPos=startPos, blendType='easeIn')
        ival = Sequence(Func(Functor(dropNode.reparentTo, render)), Parallel(Sequence(WaitInterval(self.OffscreenTime), dropIval), shadowIval), Func(cleanup), name='drop%s' % num)
        landSound = None
        if objType == Name2DropObjectType['anvil']:
            landSound = self.sndAnvilLand
        if landSound:
            ival.append(SoundInterval(landSound))
        return ival

    def startSuitWalkTask(self):
        ival = Parallel(name='catchGameMetaSuitWalk')
        rng = RandomNumGen(self.randomNumGen)
        delay = 0.0
        while delay < CatchGameGlobals.GameDuration:
            delay += lerp(self.SuitPeriodRange[0], self.SuitPeriodRange[0], rng.random())
            walkIval = Sequence(name='catchGameSuitWalk')
            walkIval.append(Wait(delay))

            def pickY(self = self, rng = rng):
                return lerp(-self.StageHalfHeight, self.StageHalfHeight, rng.random())

            m = [2.5,
             2.5,
             2.3,
             2.1][self.getNumPlayers() - 1]
            startPos = Point3(-(self.StageHalfWidth * m), pickY(), 0)
            stopPos = Point3(self.StageHalfWidth * m, pickY(), 0)
            if rng.choice([0, 1]):
                startPos, stopPos = stopPos, startPos
            walkIval.append(self.getSuitWalkIval(startPos, stopPos, rng))
            ival.append(walkIval)

        ival.start()
        self.suitWalkIval = ival

    def stopSuitWalkTask(self):
        self.suitWalkIval.finish()
        del self.suitWalkIval

    def getSuitWalkIval(self, startPos, stopPos, rng):
        data = {}
        lerpNP = render.attachNewNode('catchGameSuitParent')

        def setup(self = self, startPos = startPos, stopPos = stopPos, data = data, lerpNP = lerpNP, rng = rng):
            if len(self.suits) == 0:
                return
            suit = rng.choice(self.suits)
            data['suit'] = suit
            self.suits.remove(suit)
            suit.reparentTo(lerpNP)
            suit.loop('walk')
            suit.setPlayRate(self.SuitSpeed / ToontownGlobals.SuitWalkSpeed, 'walk')
            suit.setPos(0, 0, 0)
            lerpNP.setPos(startPos)
            suit.lookAt(stopPos)

        def cleanup(self = self, data = data, lerpNP = lerpNP):
            if data.has_key('suit'):
                suit = data['suit']
                suit.reparentTo(hidden)
                self.suits.append(suit)
            lerpNP.removeNode()

        distance = Vec3(stopPos - startPos).length()
        duration = distance / self.SuitSpeed
        ival = Sequence(FunctionInterval(setup), LerpPosInterval(lerpNP, duration, stopPos), FunctionInterval(cleanup))
        return ival

    def handleSuitCollision(self, collEntry):
        self.toonSDs[self.localAvId].fsm.request('fallBack')
        timestamp = globalClockDelta.localToNetworkTime(globalClock.getFrameTime())
        self.sendUpdate('hitBySuit', [self.localAvId, timestamp])

    def hitBySuit(self, avId, timestamp):
        if not self.hasLocalToon:
            return
        if self.gameFSM.getCurrentState().getName() != 'play':
            self.notify.warning('ignoring msg: av %s hit by suit' % avId)
            return
        toon = self.getAvatar(avId)
        if toon == None:
            return
        self.notify.debug('avatar %s hit by a suit' % avId)
        if avId != self.localAvId:
            self.toonSDs[avId].fsm.request('fallBack')
        return

    def enterCleanup(self):
        self.notify.debug('enterCleanup')

    def exitCleanup(self):
        pass

    def initOrthoWalk(self):
        self.notify.debug('startOrthoWalk')

        def doCollisions(oldPos, newPos, self = self):
            x = bound(newPos[0], self.StageHalfWidth, -self.StageHalfWidth)
            y = bound(newPos[1], self.StageHalfHeight, -self.StageHalfHeight)
            newPos.setX(x)
            newPos.setY(y)
            return newPos

        orthoDrive = OrthoDrive(self.ToonSpeed, customCollisionCallback=doCollisions)
        self.orthoWalk = OrthoWalk(orthoDrive, broadcast=not self.isSinglePlayer())

    def destroyOrthoWalk(self):
        self.notify.debug('destroyOrthoWalk')
        self.orthoWalk.destroy()
        del self.orthoWalk

    def getIntroMovie(self):
        locNode = self.ground.find('**/locator_tree')
        treeNode = locNode.attachNewNode('treeNode')
        treeNode.setHpr(render, 0, 0, 0)

        def cleanupTree(treeNode = treeNode):
            treeNode.removeNode()

        initialCamPosHpr = (-0.21,
         -19.56,
         13.94,
         0.0,
         26.57,
         0.0)
        suitViewCamPosHpr = (0, -11.5, 13, 0, -35, 0)
        finalCamPosHpr = self.CameraPosHpr
        cameraIval = Sequence(Func(camera.reparentTo, render), Func(camera.setPosHpr, treeNode, *initialCamPosHpr), WaitInterval(4.0), LerpPosHprInterval(camera, 2.0, Point3(*suitViewCamPosHpr[:3]), Point3(*suitViewCamPosHpr[3:]), blendType='easeInOut', name='lerpToSuitView'), WaitInterval(4.0), LerpPosHprInterval(camera, 3.0, Point3(*finalCamPosHpr[:3]), Point3(*finalCamPosHpr[3:]), blendType='easeInOut', name='lerpToPlayView'))

        def getIntroToon(toonProperties, parent, pos):
            toon = Toon.Toon()
            dna = ToonDNA.ToonDNA()
            dna.newToonFromProperties(*toonProperties)
            toon.setDNA(dna)
            toon.reparentTo(parent)
            toon.setPos(*pos)
            toon.setH(180)
            toon.startBlink()
            return toon

        def cleanupIntroToon(toon):
            toon.detachNode()
            toon.stopBlink()
            toon.delete()

        def getThrowIval(toon, hand, object, leftToon, isAnvil = 0):
            anim = 'catch-intro-throw'
            grabFrame = 12
            fullSizeFrame = 30
            framePeriod = 1.0 / toon.getFrameRate(anim)
            objScaleDur = (fullSizeFrame - grabFrame) * framePeriod
            releaseFrame = 35
            trajDuration = 1.6
            trajDistance = 4
            if leftToon:
                releaseFrame = 34
                trajDuration = 1.0
                trajDistance = 1
            animIval = ActorInterval(toon, anim, loop=0)

            def getThrowDest(object = object, offset = trajDistance):
                dest = object.getPos(render)
                dest += Point3(0, -offset, 0)
                dest.setZ(0)
                return dest

            if leftToon:
                trajIval = ProjectileInterval(object, startVel=Point3(0, 0, 0), duration=trajDuration)
            else:
                trajIval = ProjectileInterval(object, endPos=getThrowDest, duration=trajDuration)
            trajIval = Sequence(Func(object.wrtReparentTo, render), trajIval, Func(object.wrtReparentTo, hidden))
            if isAnvil:
                trajIval.append(SoundInterval(self.sndAnvilLand))
            objIval = Track((grabFrame * framePeriod, Sequence(Func(object.reparentTo, hand), Func(object.setPosHpr, 0.05, -.13, 0.62, 0, 0, 336.8), LerpScaleInterval(object, objScaleDur, 1.0, startScale=0.1, blendType='easeInOut'))), (releaseFrame * framePeriod, trajIval))

            def cleanup(object = object):
                object.reparentTo(hidden)
                object.removeNode()

            throwIval = Sequence(Parallel(animIval, objIval), Func(cleanup))
            return throwIval

        tY = -4.0
        tZ = 19.5
        props = ['css',
         'md',
         'm',
         'f',
         9,
         0,
         9,
         9,
         13,
         5,
         11,
         5,
         8,
         7]
        leftToon = getIntroToon(props, treeNode, [-2.3, tY, tZ])
        props = ['mss',
         'ls',
         'l',
         'm',
         6,
         0,
         6,
         6,
         3,
         5,
         3,
         5,
         5,
         0]
        rightToon = getIntroToon(props, treeNode, [1.8, tY, tZ])
        fruit = self.getObjModel(self.fruitName)
        if self.fruitName == 'pineapple':
            fruit.setZ(0.42)
            fruit.flattenMedium()
        anvil = self.getObjModel('anvil')
        anvil.setH(100)
        anvil.setZ(0.42)
        anvil.flattenMedium()
        leftToonIval = getThrowIval(leftToon, leftToon.getRightHands()[0], fruit, leftToon=1)
        rightToonIval = getThrowIval(rightToon, rightToon.getLeftHands()[0], anvil, leftToon=0, isAnvil=1)
        animDur = leftToon.getNumFrames('catch-intro-throw') / leftToon.getFrameRate('catch-intro-throw')
        toonIval = Sequence(Parallel(Sequence(leftToonIval, Func(leftToon.loop, 'neutral')), Sequence(Func(rightToon.loop, 'neutral'), WaitInterval(animDur / 2.0), rightToonIval, Func(rightToon.loop, 'neutral')), WaitInterval(cameraIval.getDuration())), Func(cleanupIntroToon, leftToon), Func(cleanupIntroToon, rightToon))
        self.treeNode = treeNode
        self.fruit = fruit
        self.anvil = anvil
        self.leftToon = leftToon
        self.rightToon = rightToon
        introMovie = Sequence(Parallel(cameraIval, toonIval), Func(cleanupTree))
        return introMovie
Exemple #7
0
class DistributedMazeGame(DistributedMinigame):
    notify = DirectNotifyGlobal.directNotify.newCategory('DistributedMazeGame')
    # define constants that you won't want to tweak here
    CAMERA_TASK = "MazeGameCameraTask"
    UPDATE_SUITS_TASK = "MazeGameUpdateSuitsTask"

    TREASURE_GRAB_EVENT_NAME = "MazeTreasureGrabbed"

    def __init__(self, cr):
        DistributedMinigame.__init__(self, cr)

        self.gameFSM = ClassicFSM.ClassicFSM('DistributedMazeGame',
                               [
                                State.State('off',
                                            self.enterOff,
                                            self.exitOff,
                                            ['play']),
                                State.State('play',
                                            self.enterPlay,
                                            self.exitPlay,
                                            ['cleanup',
                                             'showScores']),
                                State.State('showScores',
                                            self.enterShowScores,
                                            self.exitShowScores,
                                            ['cleanup']),
                                State.State('cleanup',
                                            self.enterCleanup,
                                            self.exitCleanup,
                                            []),
                                ],
                               # Initial State
                               'off',
                               # Final State
                               'cleanup',
                               )

        # Add our game ClassicFSM to the framework ClassicFSM
        self.addChildGameFSM(self.gameFSM)

        # we make the toons look around during the intro movie
        self.usesLookAround = 1

    def getTitle(self):
        return TTLocalizer.MazeGameTitle

    def getInstructions(self):
        return TTLocalizer.MazeGameInstructions

    def getMaxDuration(self):
        return MazeGameGlobals.GAME_DURATION

    def __defineConstants(self):
        self.TOON_SPEED = 8.

        self.TOON_Z = 0

        # actually lower the minimum speed for higher difficulty
        # levels; a really slow suit will get in the way and make
        # it harder to get all the coins
        self.MinSuitSpeedRange = [.8 * self.TOON_SPEED, .6 * self.TOON_SPEED]
        self.MaxSuitSpeedRange = [1.1 * self.TOON_SPEED, 2. * self.TOON_SPEED]

        # set these to true to allocate suit speeds on a curve,
        # where more suits are closer to the median speed and
        # fewer suits are closer to the max and min suit speeds
        self.FASTER_SUIT_CURVE = 1
        # for easier difficulties, there are fewer REALLY slow suits
        self.SLOWER_SUIT_CURVE = (self.getDifficulty() < .5)

        # All of the suits operate entirely independently of the
        # AI server; the server doesn't even really know that they
        # exist. Each suit walks around the maze at a fixed rate,
        # using a seeded random number generator to decide what
        # path to take. Identical seeds are used on all clients.

        # To ensure that the suits on various clients behave
        # exactly the same, it is necessary to avoid the use of
        # floating point numbers in any calculations that affect the
        # paths of the suits. Due to the spatial interactions between
        # suits that can significantly affect their decisions, it is
        # very important that the suits are told to make path decisions
        # in exactly the same order on every client.

        # Therefore, rather than intuitively storing the walking speed
        # of each suit as a feet-per-second floating point number, we
        # store the suits' walk periods, or the number of 'tics' that
        # pass while a suit walks from one position to the next. (Suits
        # only make path decisions when they arrive at new grid positions)
        # We choose an arbitrary number (MazeGameGlobals.SUIT_TIC_FREQ)
        # of 'suit tics' per second. Higher numbers of tics per
        # second give greater suit speed granularity, at the cost of
        # overflowing Python's int range more quickly. For the purposes
        # of a one-minute game, we can choose a fairly high value without
        # worrying about overflow.

        # Each suit is assigned a number of tics that represents the amount
        # of time that it will take that suit to walk from one grid cell
        # to the next grid cell. This is the suit's 'walk period'. If a
        # suit's walk period is the same as MazeGameGlobals.SUIT_TIC_FREQ,
        # that suit will take one second to walk the length of a cell. If
        # the walk period is equal to .5 * SUIT_TIC_FREQ, it will take
        # 1/2 second, etc.

        # compute the suit periods
        if __debug__:
            def printPeriodTable(name, numSuitList, fasterSuits,
                                 tTransFunc=None):
                str = '%s = {\n' % name
                # cycle through the safezones and calculate the
                # corresponding suit speeds
                for zone in MinigameGlobals.SafeZones:
                    str += '%s%s : {' % (' '*4, zone)

                    difficulty = MinigameGlobals.getDifficulty(zone)
                    minSpeed = lerp(self.MinSuitSpeedRange[0],
                                    self.MinSuitSpeedRange[1],
                                    difficulty)
                    maxSpeed = lerp(self.MaxSuitSpeedRange[0],
                                    self.MaxSuitSpeedRange[1],
                                    difficulty)
                    # all the 'slower' suits will be slower than this speed,
                    # all the 'faster' suits will be faster.
                    midSpeed = (minSpeed + maxSpeed) / 2.

                    # cycle through the suit counts (how many suits
                    # will be in play)
                    for numSuits in numSuitList:
                        # there must be an even number of suits
                        assert not numSuits % 2
                        speeds = []
                        for i in xrange(numSuits/2):
                            if fasterSuits:
                                i += numSuits/2
                            t = i / float(numSuits-1)
                            # map t into 0..1
                            if fasterSuits:
                                t -= .5
                            t *= 2.
                            # apply any time transformation function
                            if tTransFunc != None:
                                t = tTransFunc(t)

                            if fasterSuits:
                                speed = lerp(midSpeed, maxSpeed, t)
                            else:
                                speed = lerp(minSpeed, midSpeed, t)
                            speeds.append(speed)

                        # calculate the corresponding suit periods
                        def calcUpdatePeriod(speed):
                            # result in tics
                            # SUIT_TIC_FREQ: tics/sec
                            # CELL_WIDTH: feet
                            # speed: feet/sec
                            # tics = ((tics/sec) * feet) / (feet/sec)
                            return int((float(MazeGameGlobals.SUIT_TIC_FREQ) * \
                                        float(MazeData.CELL_WIDTH)) / speed)

                        periods = map(calcUpdatePeriod, speeds)

                        filler = ""
                        if numSuits < 10:
                            filler = " "

                        str += '%s%s : %s,\n%s%s' % (numSuits, filler, periods,
                                                     ' '*4, ' '*8)
                    str += '},\n'
                str += '%s}' % (' '*4)
                print str

            # these helper functions are used to distort the t time value.
            def rampIntoCurve(t):
                t = 1. - t
                t = t * t * t
                t = 1. - t
                return t
            def rampOutOfCurve(t):
                return t * t * t

            numSuitList = [4,8,12,16]
            printPeriodTable("self.slowerSuitPeriods", numSuitList, 0)
            printPeriodTable("self.slowerSuitPeriodsCurve", numSuitList, 0,
                             rampIntoCurve)
            printPeriodTable("self.fasterSuitPeriods", numSuitList, 1)
            printPeriodTable("self.fasterSuitPeriodsCurve", numSuitList, 1,
                             rampOutOfCurve)

        # these tables were generated from the code above
        # and pasted in
        self.slowerSuitPeriods = {
            2000 : {4  : [128, 76],
                    8  : [128, 99, 81, 68],
                    12 : [128, 108, 93, 82, 74, 67],
                    16 : [128, 112, 101, 91, 83, 76, 71, 66],
                    },
            1000 : {4  : [110, 69],
                    8  : [110, 88, 73, 62],
                    12 : [110, 95, 83, 74, 67, 61],
                    16 : [110, 98, 89, 81, 75, 69, 64, 60],
                    },
            5000 : {4  : [96, 63],
                    8  : [96, 79, 66, 57],
                    12 : [96, 84, 75, 67, 61, 56],
                    16 : [96, 87, 80, 73, 68, 63, 59, 55],
                    },
            4000 : {4  : [86, 58],
                    8  : [86, 71, 61, 53],
                    12 : [86, 76, 68, 62, 56, 52],
                    16 : [86, 78, 72, 67, 62, 58, 54, 51],
                    },
            3000 : {4  : [78, 54],
                    8  : [78, 65, 56, 49],
                    12 : [78, 69, 62, 57, 52, 48],
                    16 : [78, 71, 66, 61, 57, 54, 51, 48],
                    },
            9000 : {4  : [71, 50],
                    8  : [71, 60, 52, 46],
                    12 : [71, 64, 58, 53, 49, 45],
                    16 : [71, 65, 61, 57, 53, 50, 47, 45],
                    },
            }
        self.slowerSuitPeriodsCurve = {
            2000 : {4  : [128, 65],
                    8  : [128, 78, 66, 64],
                    12 : [128, 88, 73, 67, 64, 64],
                    16 : [128, 94, 79, 71, 67, 65, 64, 64],
                    },
            1000 : {4  : [110, 59],
                    8  : [110, 70, 60, 58],
                    12 : [110, 78, 66, 61, 59, 58],
                    16 : [110, 84, 72, 65, 61, 59, 58, 58],
                    },
            5000 : {4  : [96, 55],
                    8  : [96, 64, 56, 54],
                    12 : [96, 71, 61, 56, 54, 54],
                    16 : [96, 76, 65, 59, 56, 55, 54, 54],
                    },
            4000 : {4  : [86, 51],
                    8  : [86, 59, 52, 50],
                    12 : [86, 65, 56, 52, 50, 50],
                    16 : [86, 69, 60, 55, 52, 51, 50, 50],
                    },
            3000 : {4  : [78, 47],
                    8  : [78, 55, 48, 47],
                    12 : [78, 60, 52, 48, 47, 47],
                    16 : [78, 63, 55, 51, 49, 47, 47, 47],
                    },
            9000 : {4  : [71, 44],
                    8  : [71, 51, 45, 44],
                    12 : [71, 55, 48, 45, 44, 44],
                    16 : [71, 58, 51, 48, 45, 44, 44, 44],
                    },
            }
        self.fasterSuitPeriods = {
            2000 : {4  : [54, 42],
                    8  : [59, 52, 47, 42],
                    12 : [61, 56, 52, 48, 45, 42],
                    16 : [61, 58, 54, 51, 49, 46, 44, 42],
                    },
            1000 : {4  : [50, 40],
                    8  : [55, 48, 44, 40],
                    12 : [56, 52, 48, 45, 42, 40],
                    16 : [56, 53, 50, 48, 45, 43, 41, 40],
                    },
            5000 : {4  : [47, 37],
                    8  : [51, 45, 41, 37],
                    12 : [52, 48, 45, 42, 39, 37],
                    16 : [52, 49, 47, 44, 42, 40, 39, 37],
                    },
            4000 : {4  : [44, 35],
                    8  : [47, 42, 38, 35],
                    12 : [48, 45, 42, 39, 37, 35],
                    16 : [49, 46, 44, 42, 40, 38, 37, 35],
                    },
            3000 : {4  : [41, 33],
                    8  : [44, 40, 36, 33],
                    12 : [45, 42, 39, 37, 35, 33],
                    16 : [45, 43, 41, 39, 38, 36, 35, 33],
                    },
            9000 : {4  : [39, 32],
                    8  : [41, 37, 34, 32],
                    12 : [42, 40, 37, 35, 33, 32],
                    16 : [43, 41, 39, 37, 35, 34, 33, 32],
                    },
            }
        self.fasterSuitPeriodsCurve = {
            2000 : {4  : [62, 42],
                    8  : [63, 61, 54, 42],
                    12 : [63, 63, 61, 56, 50, 42],
                    16 : [63, 63, 62, 60, 57, 53, 48, 42],
                    },
            1000 : {4  : [57, 40],
                    8  : [58, 56, 50, 40],
                    12 : [58, 58, 56, 52, 46, 40],
                    16 : [58, 58, 57, 56, 53, 49, 45, 40],
                    },
            5000 : {4  : [53, 37],
                    8  : [54, 52, 46, 37],
                    12 : [54, 53, 52, 48, 43, 37],
                    16 : [54, 54, 53, 51, 49, 46, 42, 37],
                    },
            4000 : {4  : [49, 35],
                    8  : [50, 48, 43, 35],
                    12 : [50, 49, 48, 45, 41, 35],
                    16 : [50, 50, 49, 48, 46, 43, 39, 35],
                    },
            3000 : {4  : [46, 33],
                    8  : [47, 45, 41, 33],
                    12 : [47, 46, 45, 42, 38, 33],
                    16 : [47, 46, 46, 45, 43, 40, 37, 33],
                    },
            9000 : {4  : [43, 32],
                    8  : [44, 42, 38, 32],
                    12 : [44, 43, 42, 40, 36, 32],
                    16 : [44, 44, 43, 42, 40, 38, 35, 32],
                    },
            }

        self.CELL_WIDTH = MazeData.CELL_WIDTH
        self.MAX_FRAME_MOVE = self.CELL_WIDTH/2 # maximum movement in one frame

        startOffset = 3
        self.startPosHTable = [
            [Point3(0, startOffset,self.TOON_Z),  0],
            [Point3(0,-startOffset,self.TOON_Z),180],
            [Point3( startOffset,0,self.TOON_Z),270],
            [Point3(-startOffset,0,self.TOON_Z), 90],
            ]

        self.camOffset = Vec3(0, -19, 45)

    def load(self):
        self.notify.debug("load")
        DistributedMinigame.load(self)
        # load resources and create objects here

        self.__defineConstants()

        mazeName = MazeGameGlobals.getMazeName(self.doId, self.numPlayers,
                                               MazeData.mazeNames)
        self.maze = Maze.Maze(mazeName)

        model = loader.loadModel("phase_3.5/models/props/mickeySZ")
        self.treasureModel = model.find("**/mickeySZ")
        model.removeNode()
        self.treasureModel.setScale(1.6)
        #self.treasureModel.setP(-80) # tilt the mickey heads toward the camera
        self.treasureModel.setP(-90)

        self.music = base.loadMusic(
            "phase_4/audio/bgm/MG_toontag.mid"
            #"phase_4/audio/bgm/TC_SZ.mid"
            )

        # make a dictionary of tracks for showing each toon
        # getting hit by a suit
        self.toonHitTracks = {}

        self.scorePanels = []

        if __debug__:
            # this flag will allow you to walk right through suits
            self.cheat = config.GetBool('maze-game-cheat', 0)

    def unload(self):
        self.notify.debug("unload")
        DistributedMinigame.unload(self)
        # unload resources and delete objects from load() here

        del self.toonHitTracks

        self.maze.destroy()
        del self.maze

        self.treasureModel.removeNode()
        del self.treasureModel

        del self.music
        
        # remove our game ClassicFSM from the framework ClassicFSM
        self.removeChildGameFSM(self.gameFSM)
        del self.gameFSM

    def onstage(self):
        self.notify.debug("onstage")
        DistributedMinigame.onstage(self)

        # start up the minigame; parent things to render, start playing
        # music...
        # at this point we cannot yet show the remote players' toons
        self.maze.onstage()

        # place the toons in random starting lineups by
        # shuffling the starting position list
        self.randomNumGen.shuffle(self.startPosHTable)

        lt = base.localAvatar
        lt.reparentTo(render)
        lt.hideName()
        self.__placeToon(self.localAvId)
        lt.setAnimState('Happy', 1.0)
        lt.setSpeed(0,0)

        self.camParent = render.attachNewNode('mazeGameCamParent')
        self.camParent.reparentTo(base.localAvatar)
        self.camParent.setPos(0,0,0)
        self.camParent.setHpr(render, 0,0,0)
        camera.reparentTo(self.camParent)
        camera.setPos(self.camOffset)

        self.__spawnCameraTask()

        # create random num generators for each toon
        self.toonRNGs = []
        for i in xrange(self.numPlayers):
            self.toonRNGs.append(RandomNumGen.RandomNumGen(self.randomNumGen))

        # create the treasures
        self.treasures = []
        for i in xrange(self.maze.numTreasures):
            self.treasures.append(MazeTreasure.MazeTreasure(
                self.treasureModel, self.maze.treasurePosList[i], i, self.doId))

        self.__loadSuits()
        for suit in self.suits:
            suit.onstage()

        # create an instance of each sound so that they can be
        # played simultaneously, one for each toon
        # these sounds must be loaded here (and not in load()) because
        # we don't know how many players there will be until the
        # minigame has recieved all required fields
        self.sndTable = {
            "hitBySuit" : [None] * self.numPlayers,
            "falling"   : [None] * self.numPlayers,
            }
        for i in xrange(self.numPlayers):
            self.sndTable["hitBySuit"][i] =  base.loadSfx(
                "phase_4/audio/sfx/MG_Tag_C.mp3"
                #"phase_4/audio/sfx/MG_cannon_fire_alt.mp3"
                )
            self.sndTable["falling"][i] = base.loadSfx(
                "phase_4/audio/sfx/MG_cannon_whizz.mp3")

        # load a few copies of the grab sound
        self.grabSounds = []
        for i in xrange(5):
            self.grabSounds.append(base.loadSfx(
                "phase_4/audio/sfx/MG_maze_pickup.mp3"
                ))
        # play the sounds round-robin
        self.grabSoundIndex = 0

        # fill in the toonHitTracks dict with bogus tracks
        for avId in self.avIdList:
            self.toonHitTracks[avId] = Wait(0.1)

        self.scores = [0] * self.numPlayers

        # this will show what percentage of the treasures
        # have been picked up
        self.goalBar = DirectWaitBar(
            parent = render2d,
            relief = DGG.SUNKEN,
            frameSize = (-0.35, 0.35, -0.15, 0.15),
            borderWidth = (0.02, 0.02),
            scale = 0.42,
            pos = (.84, 0, (0.5 - .28*self.numPlayers) + .05),
            barColor = (0, 0.7, 0, 1),
            )
        self.goalBar.setBin('unsorted', 0)
        self.goalBar.hide()

        self.introTrack = self.getIntroTrack()
        self.introTrack.start()

    def offstage(self):
        self.notify.debug("offstage")
        # stop the minigame; parent things to hidden, stop the
        # music...

        if self.introTrack.isPlaying():
            self.introTrack.finish()
        del self.introTrack

        for avId in self.toonHitTracks.keys():
            track = self.toonHitTracks[avId]
            if track.isPlaying():
                track.finish()

        self.__killCameraTask()

        camera.wrtReparentTo(render)
        self.camParent.removeNode()
        del self.camParent

        for panel in self.scorePanels:
            panel.cleanup()
        self.scorePanels = []

        self.goalBar.destroy()
        del self.goalBar

        # Restore the offscreen popups.
        base.setCellsAvailable(base.rightCells, 1)

        for suit in self.suits:
            suit.offstage()
        self.__unloadSuits()

        for treasure in self.treasures:
            treasure.destroy()
        del self.treasures

        del self.sndTable
        del self.grabSounds

        del self.toonRNGs

        self.maze.offstage()
        
        base.localAvatar.showName()

        # this parents toons to hidden, so do it last
        # just to be sure
        DistributedMinigame.offstage(self)

    def __placeToon(self, avId):
        """ places a toon in its starting position """
        toon = self.getAvatar(avId)
        if self.numPlayers == 1:
            toon.setPos(0,0,self.TOON_Z)
            toon.setHpr(180,0,0)
        else:
            posIndex = self.avIdList.index(avId)
            toon.setPos(self.startPosHTable[posIndex][0])
            toon.setHpr(self.startPosHTable[posIndex][1],0,0)

    def setGameReady(self):
        if not self.hasLocalToon: return
        self.notify.debug("setGameReady")
        if DistributedMinigame.setGameReady(self):
            return
        
        # all of the remote toons have joined the game;
        # it's safe to show them now.

        # show the remote toons
        for avId in self.remoteAvIdList:
            toon = self.getAvatar(avId)
            if toon:
                toon.reparentTo(render)
                self.__placeToon(avId)
                toon.setAnimState('Happy', 1.0)
                # Start the smoothing task.
                toon.startSmooth()
                toon.startLookAround()

    def setGameStart(self, timestamp):
        if not self.hasLocalToon: return
        self.notify.debug("setGameStart")
        # base class will cause gameFSM to enter initial state
        DistributedMinigame.setGameStart(self, timestamp)
        # all players have finished reading the rules,
        # and are ready to start playing.
        if self.introTrack.isPlaying():
            self.introTrack.finish()

        # make the remote toons stop looking around
        for avId in self.remoteAvIdList:
            toon = self.getAvatar(avId)
            if toon:
                toon.stopLookAround()

        # transition to the appropriate state
        self.gameFSM.request("play")

    def handleDisabledAvatar(self, avId):
        # yikes, this avatar is about to disappear
        # if it's playing, finish up his fly interval before it's too late
        hitTrack = self.toonHitTracks[avId]
        if hitTrack.isPlaying():
            hitTrack.finish()

        # hand it off to the base class
        DistributedMinigame.handleDisabledAvatar(self, avId)

    # these are enter and exit functions for the game's
    # fsm (finite state machine)

    def enterOff(self):
        self.notify.debug("enterOff")

    def exitOff(self):
        pass

    def enterPlay(self):
        self.notify.debug("enterPlay")

        # Initialize the scoreboard
        for i in xrange(self.numPlayers):
            avId = self.avIdList[i]
            avName = self.getAvatarName(avId)
            scorePanel = \
                       MinigameAvatarScorePanel.MinigameAvatarScorePanel(avId,
                                                                         avName)
            scorePanel.setPos(1.12, 0.0, .5 - 0.28*i)
            self.scorePanels.append(scorePanel)

        self.goalBar.show()
        self.goalBar['value'] = 0.

        # We need the right edge of the screen for display of the
        # scoreboard, so we can't have any offscreen popups there.
        base.setCellsAvailable(base.rightCells, 0)

        self.__spawnUpdateSuitsTask()

        orthoDrive = OrthoDrive(
            self.TOON_SPEED,
            maxFrameMove=self.MAX_FRAME_MOVE,
            customCollisionCallback=self.__doMazeCollisions,
            priority = 1
            )
        self.orthoWalk = OrthoWalk(orthoDrive,
                                   broadcast=not self.isSinglePlayer())
        self.orthoWalk.start()

        # listen for collisions with the suits
        self.accept(MazeSuit.COLLISION_EVENT_NAME, self.__hitBySuit)

        # listen for treasure pickups
        self.accept(self.TREASURE_GRAB_EVENT_NAME, self.__treasureGrabbed)

        # Start counting down the game clock,
        # call timerExpired when it reaches 0
        self.timer = ToontownTimer.ToontownTimer()
        self.timer.posInTopRightCorner()
        self.timer.setTime(MazeGameGlobals.GAME_DURATION)
        self.timer.countdown(MazeGameGlobals.GAME_DURATION, self.timerExpired)

        # listen for resetClock messages so we can keep our clock in sync
        self.accept("resetClock", self.__resetClock)

        base.playMusic(self.music, looping = 0, volume = .8)

    def exitPlay(self):
        self.notify.debug("exitPlay")
        self.ignore("resetClock")

        self.ignore(MazeSuit.COLLISION_EVENT_NAME)
        self.ignore(self.TREASURE_GRAB_EVENT_NAME)

        self.orthoWalk.stop()
        self.orthoWalk.destroy()
        del self.orthoWalk

        self.__killUpdateSuitsTask()

        self.timer.stop()
        self.timer.destroy()
        del self.timer

        # keep the toons from walking in place
        for avId in self.avIdList:
            toon = self.getAvatar(avId)
            if toon:
                toon.loop('neutral')

    def __resetClock(self, tOffset):
        self.notify.debug("resetClock")
        self.gameStartTime += tOffset
        self.timer.countdown(self.timer.currentTime + tOffset,
                             self.timerExpired)

    def __treasureGrabbed(self, treasureNum):
        # local toon grabbed this treasure
        # another toon may actually get the credit,
        # but proceed as if we got it

        # make the treasure react
        self.treasures[treasureNum].showGrab()
        # play a sound
        self.grabSounds[self.grabSoundIndex].play()
        self.grabSoundIndex = (self.grabSoundIndex + 1) % len(self.grabSounds)
        # tell the AI we're claiming this treasure
        self.sendUpdate("claimTreasure", [treasureNum])

    def setTreasureGrabbed(self, avId, treasureNum):
        if not self.hasLocalToon: return
        #self.notify.debug("treasure %s grabbed by %s" % (treasureNum, avId))

        if avId != self.localAvId:
            # destroy the treasure
            self.treasures[treasureNum].showGrab()

        # update the toon's score
        i = self.avIdList.index(avId)
        self.scores[i] += 1
        self.scorePanels[i].setScore(self.scores[i])

        # update the total treasure percentage
        total = 0
        for score in self.scores:
            total += score
        self.goalBar['value'] = 100. * \
                                (float(total) / float(self.maze.numTreasures))

    def __hitBySuit(self, suitNum):
        # localtoon was hit by a suit
        self.notify.debug("hitBySuit")
        if __debug__:
            if self.cheat:
                return
        timestamp = globalClockDelta.localToNetworkTime(\
            globalClock.getFrameTime())
        self.sendUpdate("hitBySuit", [self.localAvId, timestamp])
        self.__showToonHitBySuit(self.localAvId, timestamp)

    def hitBySuit(self, avId, timestamp):
        if not self.hasLocalToon: return
        if self.gameFSM.getCurrentState().getName() not in [
            'play', 'showScores']:
            self.notify.warning('ignoring msg: av %s hit by suit' % avId)
            return
        self.notify.debug("avatar " + `avId` + " hit by a suit")
        if avId != self.localAvId:
            self.__showToonHitBySuit(avId, timestamp)
        
    def __showToonHitBySuit(self, avId, timestamp):
        toon = self.getAvatar(avId)
        if toon == None:
            return
        rng = self.toonRNGs[self.avIdList.index(avId)]

        # make sure this toon's old track is done
        curPos = toon.getPos(render)
        oldTrack = self.toonHitTracks[avId]
        if oldTrack.isPlaying():
            oldTrack.finish()
        # preserve the toon's current position, in case he gets hit
        # by two suits at a time
        toon.setPos(curPos)
        toon.setZ(self.TOON_Z)

        # put the toon under a new node
        assert (toon.getParent() == render)
        parentNode = render.attachNewNode('mazeFlyToonParent-'+`avId`)
        parentNode.setPos(toon.getPos())
        toon.reparentTo(parentNode)
        toon.setPos(0,0,0)

        # shoot the toon up into the air
        startPos = parentNode.getPos()

        # make a copy of the toon's dropshadow
        dropShadow = toon.dropShadow.copyTo(parentNode)
        dropShadow.setScale(toon.dropShadow.getScale(render))
        
        trajectory = Trajectory.Trajectory(0,
                                           Point3(0,0,0),
                                           Point3(0,0,50),
                                           gravMult=1.)
        flyDur = trajectory.calcTimeOfImpactOnPlane(0.)
        assert(flyDur > 0)

        # choose a random landing point
        while 1:
            endTile = [rng.randint(2,self.maze.width-1),
                       rng.randint(2,self.maze.height-1)]
            if self.maze.isWalkable(endTile[0],endTile[1]):
                break

        endWorldCoords = self.maze.tile2world(endTile[0],endTile[1])
        endPos = Point3(endWorldCoords[0], endWorldCoords[1], startPos[2])

        def flyFunc(t, trajectory, startPos=startPos, endPos=endPos,
                    dur=flyDur, moveNode=parentNode, flyNode=toon):
            u = (t/dur)
            moveNode.setX(startPos[0] + (u * (endPos[0]-startPos[0])))
            moveNode.setY(startPos[1] + (u * (endPos[1]-startPos[1])))
            # set the full position, since the toon might get banged
            # by telemetry
            flyNode.setPos(trajectory.getPos(t))

        flyTrack = Sequence(
            LerpFunctionInterval(flyFunc,
                                 fromData=0., toData=flyDur,
                                 duration=flyDur,
                                 extraArgs=[trajectory]),
            name=toon.uniqueName("hitBySuit-fly"))

        # if localtoon, move the camera to get a better view
        if avId != self.localAvId:
            cameraTrack = Sequence()
        else:
            # keep the camera parent node on the ground
            # with the toon parent node
            self.camParent.reparentTo(parentNode)
            startCamPos = camera.getPos()

            destCamPos = camera.getPos()
            # trajectory starts at Z==0, ends at Z==0
            zenith = trajectory.getPos(flyDur/2.)[2]
            # make the camera go up above the toon's zenith...
            destCamPos.setZ(zenith * 1.3)
            # and pull in fairly far towards the toon
            destCamPos.setY(destCamPos[1] * .3)

            # make sure the camera keeps looking at the toon
            def camTask(task, zenith=zenith,
                              flyNode=toon,
                              startCamPos=startCamPos,
                              camOffset=destCamPos-startCamPos):
                # move the camera proportional to the current height
                # of the toon wrt the height of its total trajectory
                u = flyNode.getZ() / zenith
                camera.setPos(startCamPos + (camOffset * u))
                camera.lookAt(toon)
                return Task.cont

            camTaskName = "mazeToonFlyCam-"+`avId`
            taskMgr.add(camTask, camTaskName, priority=20)

            def cleanupCamTask(self=self, toon=toon,
                               camTaskName=camTaskName,
                               startCamPos=startCamPos):
                taskMgr.remove(camTaskName)
                self.camParent.reparentTo(toon)
                camera.setPos(startCamPos)
                camera.lookAt(toon)

            cameraTrack = Sequence(
                Wait(flyDur),
                Func(cleanupCamTask),
                name="hitBySuit-cameraLerp")

        # make the toon spin in H and P
        # it seems like we need to put the rotations on two different
        # nodes in order to avoid interactions between the rotations
        geomNode = toon.getGeomNode()
        
        # apply the H rotation around the geomNode, since it's OK
        # to spin the toon in H at a node at his feet
        startHpr = geomNode.getHpr()
        destHpr = Point3(startHpr)
        # make the toon rotate in h 1..7 times
        hRot = rng.randrange(1,8)
        if rng.choice([0,1]):
            hRot = -hRot
        destHpr.setX(destHpr[0]+(hRot*360))
        spinHTrack = Sequence(
            LerpHprInterval(geomNode, flyDur, destHpr, startHpr=startHpr),
            Func(geomNode.setHpr, startHpr),
            name=toon.uniqueName("hitBySuit-spinH"))
        
        # put an extra node above the geomNode, so we can spin the
        # toon in P around his waist
        parent = geomNode.getParent()
        rotNode = parent.attachNewNode('rotNode')
        geomNode.reparentTo(rotNode)
        rotNode.setZ(toon.getHeight()/2.)
        oldGeomNodeZ = geomNode.getZ()
        geomNode.setZ(-toon.getHeight()/2.)

        # spin the toon in P around his waist
        startHpr = rotNode.getHpr()
        destHpr = Point3(startHpr)
        # make the toon rotate in P 1..2 times
        pRot = rng.randrange(1,3)
        if rng.choice([0,1]):
            pRot = -pRot
        destHpr.setY(destHpr[1]+(pRot*360))
        spinPTrack = Sequence(
            LerpHprInterval(rotNode, flyDur, destHpr, startHpr=startHpr),
            Func(rotNode.setHpr, startHpr),
            name=toon.uniqueName("hitBySuit-spinP"))

        # play some sounds
        i = self.avIdList.index(avId)
        soundTrack = Sequence(
            Func(base.playSfx, self.sndTable['hitBySuit'][i]),
            Wait(flyDur * (2./3.)),
            SoundInterval(self.sndTable['falling'][i],
                          duration=(flyDur*(1./3.))),
            name=toon.uniqueName("hitBySuit-soundTrack"))

        def preFunc(self=self, avId=avId, toon=toon, dropShadow=dropShadow):
            forwardSpeed = toon.forwardSpeed
            rotateSpeed = toon.rotateSpeed

            if avId == self.localAvId:
                # disable control of local toon
                self.orthoWalk.stop()
            else:
                toon.stopSmooth()
            
            # preserve old bug/feature where toon would be running in the air
            # if toon was moving, make him continue to run
            if forwardSpeed or rotateSpeed:
                toon.setSpeed(forwardSpeed, rotateSpeed)

            # set toon's speed to zero to stop any walk animations
            # leave it, it's funny to see toon running in mid-air
            #toon.setSpeed(0,0)

            # hide the toon's dropshadow
            toon.dropShadow.hide()

        def postFunc(self=self, avId=avId, oldGeomNodeZ=oldGeomNodeZ,
                     dropShadow=dropShadow, parentNode=parentNode):
            if avId == self.localAvId:
                base.localAvatar.setPos(endPos)
                # game may have ended by now, check
                if hasattr(self, 'orthoWalk'):
                    # re-enable control of local toon only if we're still in the play state.
                    if (self.gameFSM.getCurrentState().getName() == "play"):
                        self.orthoWalk.start()

            # get rid of the dropshadow
            dropShadow.removeNode()
            del dropShadow

            # show the toon's dropshadow
            toon.dropShadow.show()

            # get rid of the extra nodes
            geomNode = toon.getGeomNode()
            rotNode = geomNode.getParent()
            baseNode = rotNode.getParent()
            geomNode.reparentTo(baseNode)
            rotNode.removeNode()
            del rotNode
            geomNode.setZ(oldGeomNodeZ)

            toon.reparentTo(render)
            toon.setPos(endPos)
            parentNode.removeNode()
            del parentNode

            if avId != self.localAvId:
                toon.startSmooth()

        # call the preFunc _this_frame_ to ensure that the local toon
        # update task does not run this frame
        preFunc()

        hitTrack = Sequence(
            Parallel(flyTrack, cameraTrack,
                     spinHTrack, spinPTrack, soundTrack),
            Func(postFunc),
            name=toon.uniqueName("hitBySuit"))

        self.toonHitTracks[avId] = hitTrack
        hitTrack.start(globalClockDelta.localElapsedTime(timestamp))
        
    def allTreasuresTaken(self):
        if not self.hasLocalToon: return
        # all of the treasures are gone, move on
        self.notify.debug("all treasures taken")
        if not MazeGameGlobals.ENDLESS_GAME:
            self.gameFSM.request('showScores')

    def timerExpired(self):
        self.notify.debug("local timer expired")
        if not MazeGameGlobals.ENDLESS_GAME:
            self.gameFSM.request('showScores')

    def __doMazeCollisions(self, oldPos, newPos):
        # we will calculate an offset vector that
        # keeps the toon out of the walls
        offset = newPos - oldPos

        # toons can only get this close to walls
        WALL_OFFSET = 1.

        # make sure we're not in a wall already
        curX = oldPos[0]; curY = oldPos[1]
        curTX, curTY = self.maze.world2tile(curX, curY)
        assert(not self.maze.collisionTable[curTY][curTX])

        def calcFlushCoord(curTile, newTile, centerTile):
            # calculates resulting one-dimensional coordinate,
            # given that the object is moving from curTile to
            # newTile, where newTile is a wall
            EPSILON = 0.01
            if newTile > curTile:
                return ((newTile-centerTile)*self.CELL_WIDTH)\
                       -EPSILON-WALL_OFFSET
            else:
                return ((curTile-centerTile)*self.CELL_WIDTH)+WALL_OFFSET

        offsetX = offset[0]; offsetY = offset[1]

        WALL_OFFSET_X = WALL_OFFSET
        if offsetX < 0:
            WALL_OFFSET_X = -WALL_OFFSET_X
        WALL_OFFSET_Y = WALL_OFFSET
        if offsetY < 0:
            WALL_OFFSET_Y = -WALL_OFFSET_Y

        # check movement in X direction
        newX = curX + offsetX + WALL_OFFSET_X; newY = curY
        newTX, newTY = self.maze.world2tile(newX, newY)
        if newTX != curTX:
            # we've crossed a tile boundary
            if self.maze.collisionTable[newTY][newTX]:
                # there's a wall
                # adjust the X offset so that the toon
                # hits the wall exactly
                offset.setX(calcFlushCoord(curTX, newTX,
                                           self.maze.originTX)-curX)

        newX = curX; newY = curY + offsetY + WALL_OFFSET_Y
        newTX, newTY = self.maze.world2tile(newX, newY)
        if newTY != curTY:
            # we've crossed a tile boundary
            if self.maze.collisionTable[newTY][newTX]:
                # there's a wall
                # adjust the Y offset so that the toon
                # hits the wall exactly
                offset.setY(calcFlushCoord(curTY, newTY,
                                           self.maze.originTY)-curY)

        # at this point, if our new position is in a wall, we're
        # running right into a protruding corner:
        #
        #  \
        #   ###
        #   ###
        #   ###
        #
        offsetX = offset[0]; offsetY = offset[1]

        newX = curX + offsetX + WALL_OFFSET_X
        newY = curY + offsetY + WALL_OFFSET_Y
        newTX, newTY = self.maze.world2tile(newX, newY)
        if self.maze.collisionTable[newTY][newTX]:
            # collide in only one of the dimensions
            cX = calcFlushCoord(curTX, newTX, self.maze.originTX)
            cY = calcFlushCoord(curTY, newTY, self.maze.originTY)
            if (abs(cX - curX) < abs(cY - curY)):
                offset.setX(cX - curX)
            else:
                offset.setY(cY - curY)

        return oldPos + offset

    def __spawnCameraTask(self):
        self.notify.debug("spawnCameraTask")

        camera.lookAt(base.localAvatar)

        taskMgr.remove(self.CAMERA_TASK)
        # The camera control needs to run after the toon collision/movement processing
        taskMgr.add(self.__cameraTask, self.CAMERA_TASK, priority=45)

    def __killCameraTask(self):
        self.notify.debug("killCameraTask")
        taskMgr.remove(self.CAMERA_TASK)

    def __cameraTask(self, task):
        # simulate a compass node; always make sure the camera
        # parent node is rotated correctly, regardless of the
        # orientation of the parent (localtoon)
        self.camParent.setHpr(render, 0,0,0)
        return Task.cont

    ## SUITS
    def __loadSuits(self):
        self.notify.debug("loadSuits")
        self.suits = []
        self.numSuits = 4 * self.numPlayers

        safeZone = self.getSafezoneId()

        slowerTable = self.slowerSuitPeriods
        if self.SLOWER_SUIT_CURVE:
            slowerTable = self.slowerSuitPeriodsCurve
        slowerPeriods = slowerTable[safeZone][self.numSuits]

        fasterTable = self.fasterSuitPeriods
        if self.FASTER_SUIT_CURVE:
            fasterTable = self.fasterSuitPeriodsCurve
        fasterPeriods = fasterTable[safeZone][self.numSuits]

        suitPeriods = slowerPeriods + fasterPeriods
        self.notify.debug("suit periods: " + `suitPeriods`)

        self.randomNumGen.shuffle(suitPeriods)
        
        for i in xrange(self.numSuits):
            self.suits.append(MazeSuit(i, self.maze, self.randomNumGen,
                                       suitPeriods[i], self.getDifficulty()))

    def __unloadSuits(self):
        self.notify.debug("unloadSuits")
        for suit in self.suits:
            suit.destroy()
        del self.suits

    def __spawnUpdateSuitsTask(self):
        self.notify.debug("spawnUpdateSuitsTask")
        for suit in self.suits:
            suit.gameStart(self.gameStartTime)

        taskMgr.remove(self.UPDATE_SUITS_TASK)
        taskMgr.add(self.__updateSuitsTask, self.UPDATE_SUITS_TASK)

    def __killUpdateSuitsTask(self):
        self.notify.debug("killUpdateSuitsTask")
        taskMgr.remove(self.UPDATE_SUITS_TASK)

        for suit in self.suits:
            suit.gameEnd()

    def __updateSuitsTask(self, task):
        #print "__updateSuitsTask"
        
        curT = globalClock.getFrameTime() - self.gameStartTime
        curTic = int(curT * float(MazeGameGlobals.SUIT_TIC_FREQ))

        # this list will hold a sorted list of (tic, suit index) pairs
        # that represent the suit updates that must be executed
        # this frame
        suitUpdates = []

        # aggregate a list of all the suit update times
        for i in xrange(len(self.suits)):
            updateTics = self.suits[i].getThinkTimestampTics(curTic)
            suitUpdates.extend(zip(updateTics, [i]*len(updateTics)))
        # sort the list in-place
        suitUpdates.sort(lambda a,b: a[0]-b[0])

        if len(suitUpdates) > 0:
            # see below
            curTic = 0

            # run through the sorted update list, and execute the updates
            for i in xrange(len(suitUpdates)):
                update = suitUpdates[i]
                tic = update[0]
                suitIndex = update[1]
                suit = self.suits[suitIndex]

                # if multiple suits are scheduled to update at exactly the
                # same time, call prepareToMove() on them, to prevent
                # collisions between a suit and another suit's
                # old (about to be changed) position
                if tic > curTic:
                    curTic = tic
                    j = i + 1
                    while j < len(suitUpdates):
                        if suitUpdates[j][0] > tic:
                            break
                        self.suits[suitUpdates[j][1]].prepareToThink()
                        j += 1

                # make list of tiles where this suit may not walk
                # (because other suits are already there)
                unwalkables = []
                for si in xrange(suitIndex):
                    unwalkables.extend(self.suits[si].occupiedTiles)
                for si in xrange(suitIndex+1,len(self.suits)):
                    unwalkables.extend(self.suits[si].occupiedTiles)

                # do the actual update
                suit.think(curTic, curT, unwalkables)

        return Task.cont

    def enterShowScores(self):
        self.notify.debug("enterShowScores")

        # lerp up the goal bar, score panels
        lerpTrack = Parallel()
        lerpDur = .5
        # goal panel
        lerpTrack.append(Parallel(
            LerpPosInterval(self.goalBar, lerpDur, Point3(0,0,-.6),
                            blendType='easeInOut'),
            LerpScaleInterval(self.goalBar, lerpDur,
                              Vec3(self.goalBar.getScale())*2.,
                              blendType='easeInOut'),
            ))
        # score panels
        # top/bottom Y
        tY = .6; bY = -.05
        # left/center/right X
        lX = -.5; cX = 0; rX = .5
        scorePanelLocs = (
            ((cX,bY),),
            ((lX,bY),(rX,bY)),
            ((cX,tY),(lX,bY),(rX,bY)),
            ((lX,tY),(rX,tY),(lX,bY),(rX,bY)),
            )
        scorePanelLocs = scorePanelLocs[self.numPlayers-1]
        for i in xrange(self.numPlayers):
            panel = self.scorePanels[i]
            pos = scorePanelLocs[i]
            lerpTrack.append(Parallel(
                LerpPosInterval(panel, lerpDur, Point3(pos[0],0,pos[1]),
                                blendType='easeInOut'),
                LerpScaleInterval(panel, lerpDur,
                                  Vec3(panel.getScale())*2.,
                                  blendType='easeInOut'),
                ))

        self.showScoreTrack = Parallel(
            lerpTrack,
            Sequence(Wait(MazeGameGlobals.SHOWSCORES_DURATION),
                     Func(self.gameOver),
                     ),
            )

        self.showScoreTrack.start()

    def exitShowScores(self):
        # calling finish() here would cause problems if we're
        # exiting abnormally, because of the gameOver() call
        self.showScoreTrack.pause()
        del self.showScoreTrack

    def enterCleanup(self):
        self.notify.debug("enterCleanup")
        # play and showScores are currently self-contained enough
        # to clean themselves up in their exit() funcs

    def exitCleanup(self):
        pass

    def getIntroTrack(self):
        # show a close-up of the toon, and pull back to the final
        # camera position

        # pump the camera task to make sure the camera parent is
        # rotated correctly
        self.__cameraTask(None)

        # store the camera's original parent/pos/hpr
        origCamParent = camera.getParent()
        origCamPos = camera.getPos()
        origCamHpr = camera.getHpr()

        # put a node under the toon, and put the camera under that node
        iCamParent = base.localAvatar.attachNewNode('iCamParent')
        # In the final camera position that is used during the game,
        # a toon with a heading of 0 will be facing away from the camera.
        # We want to start out facing the toon, and smoothly transition to
        # the final camera pos. Therefore, we should give the camera parent
        # node a 180-degree rotation.
        iCamParent.setH(180)

        camera.reparentTo(iCamParent)
        toonHeight = base.localAvatar.getHeight()
        camera.setPos(0, -15, toonHeight * 3)
        camera.lookAt(0, 0, toonHeight/2.)

        # put the new parent node under the original parent node
        # so that all we have to do to make the new parent node
        # coincide with the old parent node is lerp the new parent's
        # pos/hpr to zero
        iCamParent.wrtReparentTo(origCamParent)

        waitDur = 5.
        lerpDur = 4.5

        lerpTrack = Parallel()
        # lerp the camera parent to where the old parent is
        # make sure that we don't lerp more than 180 degrees
        # we're lerping to H=0, so make sure -180 <= startH <= 180
        startHpr = iCamParent.getHpr()
        startHpr.setX(reduceAngle(startHpr[0]))
        lerpTrack.append(
            LerpPosHprInterval(iCamParent, lerpDur,
                               pos = Point3(0,0,0),
                               hpr = Point3(0,0,0),
                               startHpr = startHpr,
                               name=self.uniqueName('introLerpParent')))

        # lerp the camera to its old offset/orientation
        lerpTrack.append(
            LerpPosHprInterval(camera, lerpDur,
                               pos = origCamPos,
                               hpr = origCamHpr,
                               blendType = 'easeInOut',
                               name=self.uniqueName('introLerpCameraPos')))

        base.localAvatar.startLookAround()

        def cleanup(origCamParent=origCamParent,
                    origCamPos=origCamPos,
                    origCamHpr=origCamHpr,
                    iCamParent=iCamParent):
            camera.reparentTo(origCamParent)
            camera.setPos(origCamPos)
            camera.setHpr(origCamHpr)
            iCamParent.removeNode()
            del iCamParent
            base.localAvatar.stopLookAround()
            
        return Sequence(
            Wait(waitDur),
            lerpTrack,
            Func(cleanup),
            )
class DistributedMazeGame(DistributedMinigame):
    CAMERA_TASK = 'MazeGameCameraTask'
    UPDATE_SUITS_TASK = 'MazeGameUpdateSuitsTask'
    TREASURE_GRAB_EVENT_NAME = 'MazeTreasureGrabbed'

    def __init__(self, cr):
        DistributedMinigame.__init__(self, cr)
        self.gameFSM = FSM.FSM('DistributedMazeGame', [
            State.State('off', self.enterOff, self.exitOff, ['play']),
            State.State('play', self.enterPlay, self.exitPlay,
                        ['cleanup', 'showScores']),
            State.State('showScores', self.enterShowScores,
                        self.exitShowScores, ['cleanup']),
            State.State('cleanup', self.enterCleanup, self.exitCleanup, [])
        ], 'off', 'cleanup')
        self.addChildGameFSM(self.gameFSM)
        self.usesLookAround = 1

    def getTitle(self):
        return Localizer.MazeGameTitle

    def getInstructions(self):
        return Localizer.MazeGameInstructions

    def getMaxDuration(self):
        return MazeGameGlobals.GAME_DURATION

    def _DistributedMazeGame__defineConstants(self):
        self.TOON_SPEED = 8.0
        self.TOON_Z = 0
        self.MinSuitSpeedRange = [
            0.80000000000000004 * self.TOON_SPEED,
            0.59999999999999998 * self.TOON_SPEED
        ]
        self.MaxSuitSpeedRange = [
            1.1000000000000001 * self.TOON_SPEED, 2.0 * self.TOON_SPEED
        ]
        self.FASTER_SUIT_CURVE = 1
        self.SLOWER_SUIT_CURVE = self.getDifficulty() < 0.5
        self.slowerSuitPeriods = {
            2000: {
                4: [128, 76],
                8: [128, 99, 81, 68],
                12: [128, 108, 93, 82, 74, 67],
                16: [128, 112, 101, 91, 83, 76, 71, 66]
            },
            1000: {
                4: [110, 69],
                8: [110, 88, 73, 62],
                12: [110, 95, 83, 74, 67, 61],
                16: [110, 98, 89, 81, 75, 69, 64, 60]
            },
            5000: {
                4: [96, 63],
                8: [96, 79, 66, 57],
                12: [96, 84, 75, 67, 61, 56],
                16: [96, 87, 80, 73, 68, 63, 59, 55]
            },
            4000: {
                4: [86, 58],
                8: [86, 71, 61, 53],
                12: [86, 76, 68, 62, 56, 52],
                16: [86, 78, 72, 67, 62, 58, 54, 51]
            },
            3000: {
                4: [78, 54],
                8: [78, 65, 56, 49],
                12: [78, 69, 62, 57, 52, 48],
                16: [78, 71, 66, 61, 57, 54, 51, 48]
            },
            9000: {
                4: [71, 50],
                8: [71, 60, 52, 46],
                12: [71, 64, 58, 53, 49, 45],
                16: [71, 65, 61, 57, 53, 50, 47, 45]
            }
        }
        self.slowerSuitPeriodsCurve = {
            2000: {
                4: [128, 65],
                8: [128, 78, 66, 64],
                12: [128, 88, 73, 67, 64, 64],
                16: [128, 94, 79, 71, 67, 65, 64, 64]
            },
            1000: {
                4: [110, 59],
                8: [110, 70, 60, 58],
                12: [110, 78, 66, 61, 59, 58],
                16: [110, 84, 72, 65, 61, 59, 58, 58]
            },
            5000: {
                4: [96, 55],
                8: [96, 64, 56, 54],
                12: [96, 71, 61, 56, 54, 54],
                16: [96, 76, 65, 59, 56, 55, 54, 54]
            },
            4000: {
                4: [86, 51],
                8: [86, 59, 52, 50],
                12: [86, 65, 56, 52, 50, 50],
                16: [86, 69, 60, 55, 52, 51, 50, 50]
            },
            3000: {
                4: [78, 47],
                8: [78, 55, 48, 47],
                12: [78, 60, 52, 48, 47, 47],
                16: [78, 63, 55, 51, 49, 47, 47, 47]
            },
            9000: {
                4: [71, 44],
                8: [71, 51, 45, 44],
                12: [71, 55, 48, 45, 44, 44],
                16: [71, 58, 51, 48, 45, 44, 44, 44]
            }
        }
        self.fasterSuitPeriods = {
            2000: {
                4: [54, 42],
                8: [59, 52, 47, 42],
                12: [61, 56, 52, 48, 45, 42],
                16: [61, 58, 54, 51, 49, 46, 44, 42]
            },
            1000: {
                4: [50, 40],
                8: [55, 48, 44, 40],
                12: [56, 52, 48, 45, 42, 40],
                16: [56, 53, 50, 48, 45, 43, 41, 40]
            },
            5000: {
                4: [47, 37],
                8: [51, 45, 41, 37],
                12: [52, 48, 45, 42, 39, 37],
                16: [52, 49, 47, 44, 42, 40, 39, 37]
            },
            4000: {
                4: [44, 35],
                8: [47, 42, 38, 35],
                12: [48, 45, 42, 39, 37, 35],
                16: [49, 46, 44, 42, 40, 38, 37, 35]
            },
            3000: {
                4: [41, 33],
                8: [44, 40, 36, 33],
                12: [45, 42, 39, 37, 35, 33],
                16: [45, 43, 41, 39, 38, 36, 35, 33]
            },
            9000: {
                4: [39, 32],
                8: [41, 37, 34, 32],
                12: [42, 40, 37, 35, 33, 32],
                16: [43, 41, 39, 37, 35, 34, 33, 32]
            }
        }
        self.fasterSuitPeriodsCurve = {
            2000: {
                4: [62, 42],
                8: [63, 61, 54, 42],
                12: [63, 63, 61, 56, 50, 42],
                16: [63, 63, 62, 60, 57, 53, 48, 42]
            },
            1000: {
                4: [57, 40],
                8: [58, 56, 50, 40],
                12: [58, 58, 56, 52, 46, 40],
                16: [58, 58, 57, 56, 53, 49, 45, 40]
            },
            5000: {
                4: [53, 37],
                8: [54, 52, 46, 37],
                12: [54, 53, 52, 48, 43, 37],
                16: [54, 54, 53, 51, 49, 46, 42, 37]
            },
            4000: {
                4: [49, 35],
                8: [50, 48, 43, 35],
                12: [50, 49, 48, 45, 41, 35],
                16: [50, 50, 49, 48, 46, 43, 39, 35]
            },
            3000: {
                4: [46, 33],
                8: [47, 45, 41, 33],
                12: [47, 46, 45, 42, 38, 33],
                16: [47, 46, 46, 45, 43, 40, 37, 33]
            },
            9000: {
                4: [43, 32],
                8: [44, 42, 38, 32],
                12: [44, 43, 42, 40, 36, 32],
                16: [44, 44, 43, 42, 40, 38, 35, 32]
            }
        }
        self.CELL_WIDTH = MazeData.CELL_WIDTH
        self.MAX_FRAME_MOVE = self.CELL_WIDTH / 2
        startOffset = 3
        self.startPosHTable = [[Point3(0, startOffset, self.TOON_Z), 0],
                               [Point3(0, -startOffset, self.TOON_Z), 180],
                               [Point3(startOffset, 0, self.TOON_Z), 270],
                               [Point3(-startOffset, 0, self.TOON_Z), 90]]
        self.camOffset = Vec3(0, -19, 45)

    def load(self):
        self.notify.debug('load')
        DistributedMinigame.load(self)
        self._DistributedMazeGame__defineConstants()
        mazeName = MazeGameGlobals.getMazeName(self.doId, self.numPlayers,
                                               MazeData.mazeNames)
        self.maze = Maze.Maze(mazeName)
        model = loader.loadModel('phase_3.5/models/props/mickeySZ')
        self.treasureModel = model.find('**/mickeySZ')
        model.removeNode()
        self.treasureModel.setScale(1.6000000000000001)
        self.treasureModel.setP(-90)
        self.music = base.loadMusic('phase_4/audio/bgm/MG_toontag.mid')
        self.toonHitTracks = {}
        self.scorePanels = []

    def unload(self):
        self.notify.debug('unload')
        DistributedMinigame.unload(self)
        del self.toonHitTracks
        self.maze.destroy()
        del self.maze
        self.treasureModel.removeNode()
        del self.treasureModel
        del self.music
        self.removeChildGameFSM(self.gameFSM)
        del self.gameFSM

    def onstage(self):
        self.notify.debug('onstage')
        DistributedMinigame.onstage(self)
        self.maze.onstage()
        self.randomNumGen.shuffle(self.startPosHTable)
        lt = toonbase.localToon
        lt.reparentTo(render)
        lt.hideName()
        self._DistributedMazeGame__placeToon(self.localAvId)
        lt.setAnimState('Happy', 1.0)
        lt.setSpeed(0, 0)
        self.camParent = render.attachNewNode('mazeGameCamParent')
        self.camParent.reparentTo(toonbase.localToon)
        self.camParent.setPos(0, 0, 0)
        self.camParent.setHpr(render, 0, 0, 0)
        camera.reparentTo(self.camParent)
        camera.setPos(self.camOffset)
        self._DistributedMazeGame__spawnCameraTask()
        self.toonRNGs = []
        for i in xrange(self.numPlayers):
            self.toonRNGs.append(RandomNumGen.RandomNumGen(self.randomNumGen))

        self.treasures = []
        for i in xrange(self.maze.numTreasures):
            self.treasures.append(
                MazeTreasure.MazeTreasure(self.treasureModel,
                                          self.maze.treasurePosList[i], i,
                                          self.doId))

        self._DistributedMazeGame__loadSuits()
        for suit in self.suits:
            suit.onstage()

        self.sndTable = {
            'hitBySuit': [None] * self.numPlayers,
            'falling': [None] * self.numPlayers
        }
        for i in xrange(self.numPlayers):
            self.sndTable['hitBySuit'][i] = base.loadSfx(
                'phase_4/audio/sfx/MG_Tag_C.mp3')
            self.sndTable['falling'][i] = base.loadSfx(
                'phase_4/audio/sfx/MG_cannon_whizz.mp3')

        self.grabSounds = []
        for i in xrange(5):
            self.grabSounds.append(
                base.loadSfx('phase_4/audio/sfx/MG_maze_pickup.mp3'))

        self.grabSoundIndex = 0
        for avId in self.avIdList:
            self.toonHitTracks[avId] = Wait(0.10000000000000001)

        self.scores = [0] * self.numPlayers
        self.goalBar = DirectWaitBar(
            parent=render2d,
            relief=SUNKEN,
            frameSize=(-0.34999999999999998, 0.34999999999999998,
                       -0.14999999999999999, 0.14999999999999999),
            borderWidth=(0.02, 0.02),
            scale=0.41999999999999998,
            pos=(0.83999999999999997, 0,
                 (0.5 - 0.28000000000000003 * self.numPlayers) +
                 0.050000000000000003),
            barColor=(0, 0.69999999999999996, 0, 1))
        self.goalBar.hide()
        self.introTrack = self.getIntroTrack()
        self.introTrack.start()

    def offstage(self):
        self.notify.debug('offstage')
        if self.introTrack.isPlaying():
            self.introTrack.finish()

        del self.introTrack
        for avId in self.toonHitTracks.keys():
            track = self.toonHitTracks[avId]
            if track.isPlaying():
                track.finish()

        self._DistributedMazeGame__killCameraTask()
        camera.wrtReparentTo(render)
        self.camParent.removeNode()
        del self.camParent
        for panel in self.scorePanels:
            panel.cleanup()

        self.scorePanels = []
        self.goalBar.destroy()
        del self.goalBar
        toonbase.setCellsAvailable(toonbase.rightCells, 1)
        for suit in self.suits:
            suit.offstage()

        self._DistributedMazeGame__unloadSuits()
        for treasure in self.treasures:
            treasure.destroy()

        del self.treasures
        del self.sndTable
        del self.grabSounds
        del self.toonRNGs
        self.maze.offstage()
        toonbase.localToon.showName()
        DistributedMinigame.offstage(self)

    def _DistributedMazeGame__placeToon(self, avId):
        toon = self.getAvatar(avId)
        if self.numPlayers == 1:
            toon.setPos(0, 0, self.TOON_Z)
            toon.setHpr(180, 0, 0)
        else:
            posIndex = self.avIdList.index(avId)
            toon.setPos(self.startPosHTable[posIndex][0])
            toon.setHpr(self.startPosHTable[posIndex][1], 0, 0)

    def setGameReady(self):
        self.notify.debug('setGameReady')
        if DistributedMinigame.setGameReady(self):
            return None

        for avId in self.remoteAvIdList:
            toon = self.getAvatar(avId)
            if toon:
                toon.reparentTo(render)
                self._DistributedMazeGame__placeToon(avId)
                toon.setAnimState('Happy', 1.0)
                toon.startSmooth()
                toon.startLookAround()

    def setGameStart(self, timestamp):
        self.notify.debug('setGameStart')
        DistributedMinigame.setGameStart(self, timestamp)
        if self.introTrack.isPlaying():
            self.introTrack.finish()

        for avId in self.remoteAvIdList:
            toon = self.getAvatar(avId)
            if toon:
                toon.stopLookAround()

        self.gameFSM.request('play')

    def handleDisabledAvatar(self, avId):
        hitTrack = self.toonHitTracks[avId]
        if hitTrack.isPlaying():
            hitTrack.finish()

        DistributedMinigame.handleDisabledAvatar(self, avId)

    def enterOff(self):
        self.notify.debug('enterOff')

    def exitOff(self):
        pass

    def enterPlay(self):
        self.notify.debug('enterPlay')
        for i in xrange(self.numPlayers):
            avId = self.avIdList[i]
            avName = self.getAvatarName(avId)
            scorePanel = MinigameAvatarScorePanel.MinigameAvatarScorePanel(
                avId, avName)
            scorePanel.setPos(1.1200000000000001, 0.0,
                              0.5 - 0.28000000000000003 * i)
            self.scorePanels.append(scorePanel)

        self.goalBar.show()
        self.goalBar['value'] = 0.0
        toonbase.setCellsAvailable(toonbase.rightCells, 0)
        self._DistributedMazeGame__spawnUpdateSuitsTask()
        orthoDrive = OrthoDrive(self.TOON_SPEED,
                                maxFrameMove=self.MAX_FRAME_MOVE,
                                customCollisionCallback=self.
                                _DistributedMazeGame__doMazeCollisions)
        self.orthoWalk = OrthoWalk(orthoDrive,
                                   broadcast=not self.isSinglePlayer())
        self.orthoWalk.start()
        self.accept(MazeSuit.COLLISION_EVENT_NAME,
                    self._DistributedMazeGame__hitBySuit)
        self.accept(self.TREASURE_GRAB_EVENT_NAME,
                    self._DistributedMazeGame__treasureGrabbed)
        self.timer = ToontownTimer.ToontownTimer()
        self.timer.posInTopRightCorner()
        self.timer.setTime(MazeGameGlobals.GAME_DURATION)
        self.timer.countdown(MazeGameGlobals.GAME_DURATION, self.timerExpired)
        self.accept('resetClock', self._DistributedMazeGame__resetClock)
        base.playMusic(self.music, looping=0, volume=0.80000000000000004)

    def exitPlay(self):
        self.ignore('resetClock')
        self.ignore(MazeSuit.COLLISION_EVENT_NAME)
        self.ignore(self.TREASURE_GRAB_EVENT_NAME)
        self.orthoWalk.stop()
        self.orthoWalk.destroy()
        del self.orthoWalk
        self._DistributedMazeGame__killUpdateSuitsTask()
        self.timer.stop()
        self.timer.destroy()
        del self.timer
        for avId in self.avIdList:
            toon = self.getAvatar(avId)
            if toon:
                toon.loop('neutral')

    def _DistributedMazeGame__resetClock(self, tOffset):
        self.notify.debug('resetClock')
        self.gameStartTime += tOffset
        self.timer.countdown(self.timer.currentTime + tOffset,
                             self.timerExpired)

    def _DistributedMazeGame__treasureGrabbed(self, treasureNum):
        self.treasures[treasureNum].showGrab()
        self.grabSounds[self.grabSoundIndex].play()
        self.grabSoundIndex = (self.grabSoundIndex + 1) % len(self.grabSounds)
        self.sendUpdate('claimTreasure', [treasureNum])

    def setTreasureGrabbed(self, avId, treasureNum):
        if avId != self.localAvId:
            self.treasures[treasureNum].showGrab()

        i = self.avIdList.index(avId)
        self.scores[i] += 1
        self.scorePanels[i].setScore(self.scores[i])
        total = 0
        for score in self.scores:
            total += score

        self.goalBar['value'] = 100.0 * (float(total) /
                                         float(self.maze.numTreasures))

    def _DistributedMazeGame__hitBySuit(self, suitNum):
        self.notify.debug('hitBySuit')
        timestamp = globalClockDelta.localToNetworkTime(
            globalClock.getFrameTime())
        self.sendUpdate('hitBySuit', [self.localAvId, timestamp])
        self._DistributedMazeGame__showToonHitBySuit(self.localAvId, timestamp)

    def hitBySuit(self, avId, timestamp):
        if self.gameFSM.getCurrentState().getName() not in [
                'play', 'showScores'
        ]:
            self.notify.warning('ignoring msg: av %s hit by suit' % avId)
            return None

        self.notify.debug('avatar ' + ` avId ` + ' hit by a suit')
        if avId != self.localAvId:
            self._DistributedMazeGame__showToonHitBySuit(avId, timestamp)

    def _DistributedMazeGame__showToonHitBySuit(self, avId, timestamp):
        toon = self.getAvatar(avId)
        rng = self.toonRNGs[self.avIdList.index(avId)]
        curPos = toon.getPos(render)
        oldTrack = self.toonHitTracks[avId]
        if oldTrack.isPlaying():
            oldTrack.finish()

        toon.setPos(curPos)
        toon.setZ(self.TOON_Z)
        parentNode = render.attachNewNode('mazeFlyToonParent-' + ` avId `)
        parentNode.setPos(toon.getPos())
        toon.reparentTo(parentNode)
        toon.setPos(0, 0, 0)
        startPos = parentNode.getPos()
        dropShadow = toon.dropShadows[0].copyTo(parentNode)
        dropShadow.setScale(toon.dropShadows[0].getScale(render))
        trajectory = Trajectory.Trajectory(0,
                                           Point3(0, 0, 0),
                                           Point3(0, 0, 50),
                                           gravMult=1.0)
        flyDur = trajectory.calcTimeOfImpactOnPlane(0.0)
        while 1:
            endTile = [
                rng.randint(2, self.maze.width - 1),
                rng.randint(2, self.maze.height - 1)
            ]
            if self.maze.isWalkable(endTile[0], endTile[1]):
                break

        endWorldCoords = self.maze.tile2world(endTile[0], endTile[1])
        endPos = Point3(endWorldCoords[0], endWorldCoords[1], startPos[2])

        def flyFunc(t,
                    trajectory,
                    startPos=startPos,
                    endPos=endPos,
                    dur=flyDur,
                    moveNode=parentNode,
                    flyNode=toon):
            u = t / dur
            moveNode.setX(startPos[0] + u * (endPos[0] - startPos[0]))
            moveNode.setY(startPos[1] + u * (endPos[1] - startPos[1]))
            flyNode.setPos(trajectory.getPos(t))

        flyTrack = Sequence(LerpFunctionInterval(flyFunc,
                                                 fromData=0.0,
                                                 toData=flyDur,
                                                 duration=flyDur,
                                                 extraArgs=[trajectory]),
                            name=toon.uniqueName('hitBySuit-fly'))
        if avId != self.localAvId:
            cameraTrack = Sequence()
        else:
            self.camParent.reparentTo(parentNode)
            startCamPos = camera.getPos()
            destCamPos = camera.getPos()
            zenith = trajectory.getPos(flyDur / 2.0)[2]
            destCamPos.setZ(zenith * 1.3)
            destCamPos.setY(destCamPos[1] * 0.29999999999999999)

            def camTask(task,
                        zenith=zenith,
                        flyNode=toon,
                        startCamPos=startCamPos,
                        camOffset=destCamPos - startCamPos):
                u = flyNode.getZ() / zenith
                camera.setPos(startCamPos + camOffset * u)
                camera.lookAt(toon)
                return Task.cont

            camTaskName = 'mazeToonFlyCam-' + ` avId `
            taskMgr.add(camTask, camTaskName, priority=20)

            def cleanupCamTask(self=self,
                               toon=toon,
                               camTaskName=camTaskName,
                               startCamPos=startCamPos):
                taskMgr.remove(camTaskName)
                self.camParent.reparentTo(toon)
                camera.setPos(startCamPos)
                camera.lookAt(toon)

            cameraTrack = Sequence(Wait(flyDur),
                                   Func(cleanupCamTask),
                                   name='hitBySuit-cameraLerp')
        geomNode = toon.getGeomNode()
        startHpr = geomNode.getHpr()
        destHpr = Point3(startHpr)
        hRot = rng.randrange(1, 8)
        if rng.choice([0, 1]):
            hRot = -hRot

        destHpr.setX(destHpr[0] + hRot * 360)
        spinHTrack = Sequence(LerpHprInterval(geomNode,
                                              flyDur,
                                              destHpr,
                                              startHpr=startHpr),
                              Func(geomNode.setHpr, startHpr),
                              name=toon.uniqueName('hitBySuit-spinH'))
        parent = geomNode.getParent()
        rotNode = parent.attachNewNode('rotNode')
        geomNode.reparentTo(rotNode)
        rotNode.setZ(toon.getHeight() / 2.0)
        oldGeomNodeZ = geomNode.getZ()
        geomNode.setZ(-toon.getHeight() / 2.0)
        startHpr = rotNode.getHpr()
        destHpr = Point3(startHpr)
        pRot = rng.randrange(1, 3)
        if rng.choice([0, 1]):
            pRot = -pRot

        destHpr.setY(destHpr[1] + pRot * 360)
        spinPTrack = Sequence(LerpHprInterval(rotNode,
                                              flyDur,
                                              destHpr,
                                              startHpr=startHpr),
                              Func(rotNode.setHpr, startHpr),
                              name=toon.uniqueName('hitBySuit-spinP'))
        i = self.avIdList.index(avId)
        soundTrack = Sequence(Func(base.playSfx,
                                   self.sndTable['hitBySuit'][i]),
                              Wait(flyDur * (2.0 / 3.0)),
                              SoundInterval(self.sndTable['falling'][i],
                                            duration=flyDur * (1.0 / 3.0)),
                              name=toon.uniqueName('hitBySuit-soundTrack'))

        def preFunc(self=self, avId=avId, toon=toon, dropShadow=dropShadow):
            forwardSpeed = toon.forwardSpeed
            rotateSpeed = toon.rotateSpeed
            if avId == self.localAvId:
                self.orthoWalk.stop()
            else:
                toon.stopSmooth()
            if forwardSpeed or rotateSpeed:
                toon.setSpeed(forwardSpeed, rotateSpeed)

            for dropShadow in toon.dropShadows:
                dropShadow.hide()

        def postFunc(self=self,
                     avId=avId,
                     oldGeomNodeZ=oldGeomNodeZ,
                     dropShadow=dropShadow,
                     parentNode=parentNode):
            if avId == self.localAvId:
                toonbase.localToon.setPos(endPos)
                if hasattr(self, 'orthoWalk'):
                    self.orthoWalk.start()

            dropShadow.removeNode()
            del dropShadow
            for dropShadow in toon.dropShadows:
                dropShadow.show()

            geomNode = toon.getGeomNode()
            rotNode = geomNode.getParent()
            baseNode = rotNode.getParent()
            geomNode.reparentTo(baseNode)
            rotNode.removeNode()
            del rotNode
            geomNode.setZ(oldGeomNodeZ)
            toon.reparentTo(render)
            toon.setPos(endPos)
            parentNode.removeNode()
            del parentNode
            if avId != self.localAvId:
                toon.startSmooth()

        preFunc()
        hitTrack = Sequence(Parallel(flyTrack, cameraTrack, spinHTrack,
                                     spinPTrack, soundTrack),
                            Func(postFunc),
                            name=toon.uniqueName('hitBySuit'))
        self.toonHitTracks[avId] = hitTrack
        hitTrack.start(globalClockDelta.localElapsedTime(timestamp))

    def allTreasuresTaken(self):
        self.notify.debug('all treasures taken')
        if not (MazeGameGlobals.ENDLESS_GAME):
            self.gameFSM.request('showScores')

    def timerExpired(self):
        self.notify.debug('local timer expired')
        if not (MazeGameGlobals.ENDLESS_GAME):
            self.gameFSM.request('showScores')

    def _DistributedMazeGame__doMazeCollisions(self, oldPos, newPos):
        offset = newPos - oldPos
        WALL_OFFSET = 1.0
        curX = oldPos[0]
        curY = oldPos[1]
        (curTX, curTY) = self.maze.world2tile(curX, curY)

        def calcFlushCoord(curTile, newTile, centerTile):
            EPSILON = 0.01
            if newTile > curTile:
                return (newTile -
                        centerTile) * self.CELL_WIDTH - EPSILON - WALL_OFFSET
            else:
                return (curTile - centerTile) * self.CELL_WIDTH + WALL_OFFSET

        offsetX = offset[0]
        offsetY = offset[1]
        WALL_OFFSET_X = WALL_OFFSET
        if offsetX < 0:
            WALL_OFFSET_X = -WALL_OFFSET_X

        WALL_OFFSET_Y = WALL_OFFSET
        if offsetY < 0:
            WALL_OFFSET_Y = -WALL_OFFSET_Y

        newX = curX + offsetX + WALL_OFFSET_X
        newY = curY
        (newTX, newTY) = self.maze.world2tile(newX, newY)
        if newTX != curTX:
            if self.maze.collisionTable[newTY][newTX]:
                offset.setX(
                    calcFlushCoord(curTX, newTX, self.maze.originTX) - curX)

        newX = curX
        newY = curY + offsetY + WALL_OFFSET_Y
        (newTX, newTY) = self.maze.world2tile(newX, newY)
        if newTY != curTY:
            if self.maze.collisionTable[newTY][newTX]:
                offset.setY(
                    calcFlushCoord(curTY, newTY, self.maze.originTY) - curY)

        offsetX = offset[0]
        offsetY = offset[1]
        newX = curX + offsetX + WALL_OFFSET_X
        newY = curY + offsetY + WALL_OFFSET_Y
        (newTX, newTY) = self.maze.world2tile(newX, newY)
        if self.maze.collisionTable[newTY][newTX]:
            cX = calcFlushCoord(curTX, newTX, self.maze.originTX)
            cY = calcFlushCoord(curTY, newTY, self.maze.originTY)
            if abs(cX - curX) < abs(cY - curY):
                offset.setX(cX - curX)
            else:
                offset.setY(cY - curY)

        return oldPos + offset

    def _DistributedMazeGame__spawnCameraTask(self):
        self.notify.debug('spawnCameraTask')
        camera.lookAt(toonbase.localToon)
        taskMgr.remove(self.CAMERA_TASK)
        taskMgr.add(self._DistributedMazeGame__cameraTask,
                    self.CAMERA_TASK,
                    priority=10)

    def _DistributedMazeGame__killCameraTask(self):
        self.notify.debug('killCameraTask')
        taskMgr.remove(self.CAMERA_TASK)

    def _DistributedMazeGame__cameraTask(self, task):
        self.camParent.setHpr(render, 0, 0, 0)
        return Task.cont

    def _DistributedMazeGame__loadSuits(self):
        self.notify.debug('loadSuits')
        self.suits = []
        self.numSuits = 4 * self.numPlayers
        safeZone = self.getSafezoneId()
        slowerTable = self.slowerSuitPeriods
        if self.SLOWER_SUIT_CURVE:
            slowerTable = self.slowerSuitPeriodsCurve

        slowerPeriods = slowerTable[safeZone][self.numSuits]
        fasterTable = self.fasterSuitPeriods
        if self.FASTER_SUIT_CURVE:
            fasterTable = self.fasterSuitPeriodsCurve

        fasterPeriods = fasterTable[safeZone][self.numSuits]
        suitPeriods = slowerPeriods + fasterPeriods
        self.notify.debug('suit periods: ' + ` suitPeriods `)
        self.randomNumGen.shuffle(suitPeriods)
        for i in xrange(self.numSuits):
            self.suits.append(
                MazeSuit(i, self.maze, self.randomNumGen, suitPeriods[i],
                         self.getDifficulty()))

    def _DistributedMazeGame__unloadSuits(self):
        self.notify.debug('unloadSuits')
        for suit in self.suits:
            suit.destroy()

        del self.suits

    def _DistributedMazeGame__spawnUpdateSuitsTask(self):
        self.notify.debug('spawnUpdateSuitsTask')
        for suit in self.suits:
            suit.gameStart(self.gameStartTime)

        taskMgr.remove(self.UPDATE_SUITS_TASK)
        taskMgr.add(self._DistributedMazeGame__updateSuitsTask,
                    self.UPDATE_SUITS_TASK)

    def _DistributedMazeGame__killUpdateSuitsTask(self):
        self.notify.debug('killUpdateSuitsTask')
        taskMgr.remove(self.UPDATE_SUITS_TASK)
        for suit in self.suits:
            suit.gameEnd()

    def _DistributedMazeGame__updateSuitsTask(self, task):
        curT = globalClock.getFrameTime() - self.gameStartTime
        curTic = int(curT * float(MazeGameGlobals.SUIT_TIC_FREQ))
        suitUpdates = []
        for i in xrange(len(self.suits)):
            updateTics = self.suits[i].getThinkTimestampTics(curTic)
            suitUpdates.extend(zip(updateTics, [i] * len(updateTics)))

        suitUpdates.sort(lambda a, b: a[0] - b[0])
        if len(suitUpdates) > 0:
            curTic = 0
            for i in xrange(len(suitUpdates)):
                update = suitUpdates[i]
                tic = update[0]
                suitIndex = update[1]
                suit = self.suits[suitIndex]
                if tic > curTic:
                    curTic = tic
                    j = i + 1
                    while j < len(suitUpdates):
                        if suitUpdates[j][0] > tic:
                            break

                        self.suits[suitUpdates[j][1]].prepareToThink()
                        j += 1

                unwalkables = []
                for si in xrange(suitIndex):
                    unwalkables.extend(self.suits[si].occupiedTiles)

                for si in xrange(suitIndex + 1, len(self.suits)):
                    unwalkables.extend(self.suits[si].occupiedTiles)

                suit.think(curTic, curT, unwalkables)

        return Task.cont

    def enterShowScores(self):
        self.notify.debug('enterShowScores')
        lerpTrack = Parallel()
        lerpDur = 0.5
        lerpTrack.append(
            Parallel(
                LerpPosInterval(self.goalBar,
                                lerpDur,
                                Point3(0, 0, -0.59999999999999998),
                                blendType='easeInOut'),
                LerpScaleInterval(self.goalBar,
                                  lerpDur,
                                  Vec3(self.goalBar.getScale()) * 2.0,
                                  blendType='easeInOut')))
        tY = 0.59999999999999998
        bY = -0.050000000000000003
        lX = -0.5
        cX = 0
        rX = 0.5
        scorePanelLocs = (((cX, bY), ), ((lX, bY), (rX, bY)),
                          ((cX, tY), (lX, bY), (rX, bY)), ((lX, tY), (rX, tY),
                                                           (lX, bY), (rX, bY)))
        scorePanelLocs = scorePanelLocs[self.numPlayers - 1]
        for i in xrange(self.numPlayers):
            panel = self.scorePanels[i]
            pos = scorePanelLocs[i]
            lerpTrack.append(
                Parallel(
                    LerpPosInterval(panel,
                                    lerpDur,
                                    Point3(pos[0], 0, pos[1]),
                                    blendType='easeInOut'),
                    LerpScaleInterval(panel,
                                      lerpDur,
                                      Vec3(panel.getScale()) * 2.0,
                                      blendType='easeInOut')))

        self.showScoreTrack = Parallel(
            lerpTrack,
            Sequence(Wait(MazeGameGlobals.SHOWSCORES_DURATION),
                     Func(self.gameOver)))
        self.showScoreTrack.start()

    def exitShowScores(self):
        self.showScoreTrack.pause()
        del self.showScoreTrack

    def enterCleanup(self):
        self.notify.debug('enterCleanup')

    def exitCleanup(self):
        pass

    def getIntroTrack(self):
        self._DistributedMazeGame__cameraTask(None)
        origCamParent = camera.getParent()
        origCamPos = camera.getPos()
        origCamHpr = camera.getHpr()
        iCamParent = toonbase.localToon.attachNewNode('iCamParent')
        iCamParent.setH(180)
        camera.reparentTo(iCamParent)
        toonHeight = toonbase.localToon.getHeight()
        camera.setPos(0, -15, toonHeight * 3)
        camera.lookAt(0, 0, toonHeight / 2.0)
        iCamParent.wrtReparentTo(origCamParent)
        waitDur = 5.0
        lerpDur = 4.5
        lerpTrack = Parallel()
        startHpr = iCamParent.getHpr()
        startHpr.setX(reduceAngle(startHpr[0]))
        lerpTrack.append(
            LerpPosHprInterval(iCamParent,
                               lerpDur,
                               pos=Point3(0, 0, 0),
                               hpr=Point3(0, 0, 0),
                               startHpr=startHpr,
                               name=self.uniqueName('introLerpParent')))
        lerpTrack.append(
            LerpPosHprInterval(camera,
                               lerpDur,
                               pos=origCamPos,
                               hpr=origCamHpr,
                               blendType='easeInOut',
                               name=self.uniqueName('introLerpCameraPos')))
        toonbase.localToon.startLookAround()

        def cleanup(origCamParent=origCamParent,
                    origCamPos=origCamPos,
                    origCamHpr=origCamHpr,
                    iCamParent=iCamParent):
            camera.reparentTo(origCamParent)
            camera.setPos(origCamPos)
            camera.setHpr(origCamHpr)
            iCamParent.removeNode()
            del iCamParent
            toonbase.localToon.stopLookAround()

        return Sequence(Wait(waitDur), lerpTrack, Func(cleanup))