from pandac.PandaModules import *
from toontown.toonbase.ToonBaseGlobal import *
from .DistributedMinigame import *
from direct.interval.IntervalGlobal import *
from .OrthoWalk import *
from direct.showbase.PythonUtil import Functor, bound, lineupPos, lerp
from direct.fsm import ClassicFSM, State
from direct.fsm import State
from toontown.toonbase import TTLocalizer
from . import CatchGameGlobals
from direct.task.Task import Task
from toontown.toon import Toon
from toontown.suit import Suit
from . import MinigameAvatarScorePanel
from toontown.toonbase import ToontownTimer
from toontown.toonbase import ToontownGlobals
from . import CatchGameToonSD
from . import Trajectory
import math
from direct.distributed import DistributedSmoothNode
from direct.showbase.RandomNumGen import RandomNumGen
from . import MinigameGlobals
from toontown.toon import ToonDNA
from toontown.suit import SuitDNA
from .CatchGameGlobals import DropObjectTypes
from .CatchGameGlobals import Name2DropObjectType
from .DropPlacer import *
from .DropScheduler import *
from functools import reduce

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.loader.loadMusic('phase_4/audio/bgm/MG_toontag.mid')
        self.sndGoodCatch = base.loader.loadSfx('phase_4/audio/sfx/SZ_DD_treasure.mp3')
        self.sndOof = base.loader.loadSfx('phase_4/audio/sfx/MG_cannon_hit_dirt.mp3')
        self.sndAnvilLand = base.loader.loadSfx('phase_4/audio/sfx/AA_drop_anvil_miss.mp3')
        self.sndPerfect = base.loader.loadSfx('phase_4/audio/sfx/ring_perfect.mp3')
        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 list(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 list(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, list(typeProbs.values()))
        for key in list(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 list(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 range(self.numPlayers):
            avId = self.avIdList[i]
            avName = self.getAvatarName(avId)
            scorePanel = MinigameAvatarScorePanel.MinigameAvatarScorePanel(avId, avName)
            scorePanel.setScale(0.9)
            scorePanel.setPos(0.75 - spacing * (self.numPlayers - 1 - i), 0.0, 0.85)
            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 list(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