from pandac.PandaModules import *
from toontown.toonbase.ToonBaseGlobal import *
from direct.interval.IntervalGlobal import *
from DistributedMinigame import *
from direct.distributed.ClockDelta import *
from direct.fsm import ClassicFSM, State
from direct.fsm import State
from direct.task import Task
import ArrowKeys
import Ring
import RingTrack
import RingGameGlobals
import RingGroup
import RingTrackGroups
from toontown.toonbase import ToontownGlobals
from toontown.toonbase import TTLocalizer

class DistributedRingGame(DistributedMinigame):
    UPDATE_ENVIRON_TASK = 'RingGameUpdateEnvironTask'
    UPDATE_LOCALTOON_TASK = 'RingGameUpdateLocalToonTask'
    UPDATE_RINGS_TASK = 'RingGameUpdateRingsTask'
    UPDATE_SHADOWS_TASK = 'RingGameUpdateShadowsTask'
    COLLISION_DETECTION_TASK = 'RingGameCollisionDetectionTask'
    END_GAME_WAIT_TASK = 'RingGameCollisionDetectionTask'
    COLLISION_DETECTION_PRIORITY = 5
    UPDATE_SHADOWS_PRIORITY = 47
    RT_UNKNOWN = 0
    RT_SUCCESS = 1
    RT_GROUPSUCCESS = 2
    RT_FAILURE = 3

    def __init__(self, cr):
        DistributedMinigame.__init__(self, cr)
        self.gameFSM = ClassicFSM.ClassicFSM('DistributedRingGame', [State.State('off', self.enterOff, self.exitOff, ['swim']), State.State('swim', self.enterSwim, self.exitSwim, ['cleanup']), State.State('cleanup', self.enterCleanup, self.exitCleanup, [])], 'off', 'cleanup')
        self.addChildGameFSM(self.gameFSM)

    def getTitle(self):
        return TTLocalizer.RingGameTitle

    def getInstructions(self):
        p = self.avIdList.index(self.localAvId)
        if self.isSinglePlayer():
            text = TTLocalizer.RingGameInstructionsSinglePlayer
        else:
            text = TTLocalizer.RingGameInstructionsMultiPlayer
        return text % RingGameGlobals.ringColors[self.colorIndices[p]][0]

    def getMaxDuration(self):
        return RingGameGlobals.NUM_RING_GROUPS * self.ringGroupArrivalPeriod + self.T_FIRST_RING_GROUP_ARRIVES + self.GAME_END_DELAY

    def defineConstants(self):
        self.CAMERA_Y = -15
        self.TOON_Y = 0
        self.FAR_PLANE_DIST = 150
        tScreenCenterToEdge = 1.0
        self.TOONXZ_SPEED = RingGameGlobals.MAX_TOONXZ / tScreenCenterToEdge
        self.WATER_DEPTH = 35.0
        self.ENVIRON_LENGTH = 150.0
        self.ENVIRON_START_OFFSET = 20.0
        self.TOON_INITIAL_SPACING = 4.0
        waterZOffset = 3.0
        self.SEA_FLOOR_Z = -self.WATER_DEPTH / 2.0 + waterZOffset
        farPlaneDist = self.CAMERA_Y + self.FAR_PLANE_DIST - self.TOON_Y
        self.ringGroupArrivalPeriod = 3.0
        self.RING_GROUP_SPACING = farPlaneDist / 2.0
        self.TOON_SWIM_VEL = self.RING_GROUP_SPACING / self.ringGroupArrivalPeriod
        self.T_FIRST_RING_GROUP_ARRIVES = farPlaneDist / self.TOON_SWIM_VEL
        self.WATER_COLOR = Vec4(0, 0, 0.6, 1)
        self.SHADOW_Z_OFFSET = 0.1
        self.Y_VIS_MAX = self.FAR_PLANE_DIST
        self.Y_VIS_MIN = self.CAMERA_Y
        ringRadius = RingGameGlobals.RING_RADIUS * 1.025
        self.RING_RADIUS_SQRD = ringRadius * ringRadius
        self.GAME_END_DELAY = 1.0
        self.RING_RESPONSE_DELAY = 0.1
        self.TOON_LOD = 1000
        self.NumRingGroups = 16

    def load(self):
        self.notify.debug('load')
        DistributedMinigame.load(self)
        self.defineConstants()
        self.music = base.loadMusic('phase_4/audio/bgm/MG_toontag.ogg')
        self.sndAmbience = base.loadSfx('phase_4/audio/sfx/AV_ambient_water.ogg')
        self.sndPerfect = base.loadSfx('phase_4/audio/sfx/ring_perfect.ogg')
        loadBase = 'phase_4/models/minigames/'
        self.environModel = loader.loadModel(loadBase + 'swimming_game.bam')
        self.environModel.setPos(0, self.ENVIRON_LENGTH / 2.0, self.SEA_FLOOR_Z)
        self.environModel.flattenMedium()
        self.ringModel = loader.loadModel(loadBase + 'swimming_game_ring.bam')
        self.ringModel.setTransparency(1)
        modelRadius = 4.0
        self.ringModel.setScale(RingGameGlobals.RING_RADIUS / modelRadius)
        self.ringModel.flattenMedium()
        self.dropShadowModel = loader.loadModel('phase_3/models/props/drop_shadow')
        self.dropShadowModel.setColor(0, 0, 0, 0.5)
        self.dropShadowModel.flattenMedium()
        self.toonDropShadows = []
        self.ringDropShadows = []
        self.__textGen = TextNode('ringGame')
        self.__textGen.setFont(ToontownGlobals.getSignFont())
        self.__textGen.setAlign(TextNode.ACenter)

    def unload(self):
        self.notify.debug('unload')
        DistributedMinigame.unload(self)
        del self.__textGen
        del self.toonDropShadows
        del self.ringDropShadows
        self.dropShadowModel.removeNode()
        del self.dropShadowModel
        self.environModel.removeNode()
        del self.environModel
        self.ringModel.removeNode()
        del self.ringModel
        del self.music
        del self.sndAmbience
        del self.sndPerfect
        self.removeChildGameFSM(self.gameFSM)
        del self.gameFSM

    def onstage(self):
        self.notify.debug('onstage')
        DistributedMinigame.onstage(self)
        self.arrowKeys = ArrowKeys.ArrowKeys()
        toon = base.localAvatar
        toon.reparentTo(render)
        toon.setAnimState('swim', 1.0)
        toon.stopBobSwimTask()
        toon.useLOD(self.TOON_LOD)
        self.__placeToon(self.localAvId)
        toon.dropShadow.hide()
        camera.reparentTo(render)
        camera.reparentTo(base.localAvatar)
        camera.setPosHpr(0, self.CAMERA_Y + self.TOON_Y, 0, 0, 0, 0)
        base.camLens.setMinFov(80/(4./3.))
        base.camLens.setFar(self.FAR_PLANE_DIST)
        base.setBackgroundColor(self.WATER_COLOR)
        self.__fog = Fog('ringGameFog')
        if base.wantFog:
            self.__fog.setColor(self.WATER_COLOR)
            self.__fog.setLinearRange(0.1, self.FAR_PLANE_DIST - 1.0)
            render.setFog(self.__fog)
        self.environNode = render.attachNewNode('environNode')
        self.environBlocks = []
        for i in xrange(0, 2):
            instance = self.environModel.instanceUnderNode(self.environNode, 'model')
            y = self.ENVIRON_LENGTH * i
            instance.setY(y)
            self.environBlocks.append(instance)
            for j in xrange(0, 2):
                instance = self.environModel.instanceUnderNode(self.environNode, 'blocks')
                x = self.ENVIRON_LENGTH * (j + 1)
                instance.setY(y)
                instance.setX(-x)
                self.environBlocks.append(instance)

            for j in xrange(0, 2):
                instance = self.environModel.instanceUnderNode(self.environNode, 'blocks')
                x = self.ENVIRON_LENGTH * (j + 1)
                instance.setY(y)
                instance.setX(x)
                self.environBlocks.append(instance)

        self.ringNode = render.attachNewNode('ringNode')
        self.sndTable = {'gotRing': [None] * self.numPlayers,
         'missedRing': [None] * self.numPlayers}
        for i in xrange(0, self.numPlayers):
            self.sndTable['gotRing'][i] = base.loadSfx('phase_4/audio/sfx/ring_get.ogg')
            self.sndTable['missedRing'][i] = base.loadSfx('phase_4/audio/sfx/ring_miss.ogg')

        self.__addToonDropShadow(self.getAvatar(self.localAvId))
        self.__spawnUpdateEnvironTask()
        self.__spawnUpdateShadowsTask()
        self.__spawnUpdateLocalToonTask()
        if None != self.sndAmbience:
            base.playSfx(self.sndAmbience, looping=1, volume=0.8)
        return

    def offstage(self):
        self.notify.debug('offstage')
        DistributedMinigame.offstage(self)
        self.music.stop()
        if None != self.sndAmbience:
            self.sndAmbience.stop()
        self.__killUpdateLocalToonTask()
        self.__killUpdateShadowsTask()
        self.__killUpdateEnvironTask()
        del self.sndTable
        self.__removeAllToonDropShadows()
        render.clearFog()
        base.camLens.setFar(ToontownGlobals.DefaultCameraFar)
        base.camLens.setMinFov(ToontownGlobals.DefaultCameraFov/(4./3.))
        base.setBackgroundColor(ToontownGlobals.DefaultBackgroundColor)
        self.arrowKeys.destroy()
        del self.arrowKeys
        for block in self.environBlocks:
            del block

        self.environNode.removeNode()
        del self.environNode
        self.ringNode.removeNode()
        del self.ringNode
        for avId in self.avIdList:
            av = self.getAvatar(avId)
            if av:
                av.dropShadow.show()
                av.resetLOD()
                av.setAnimState('neutral', 1.0)

        return

    def handleDisabledAvatar(self, avId):
        self.notify.debug('handleDisabledAvatar')
        self.notify.debug('avatar ' + str(avId) + ' disabled')
        self.__removeToonDropShadow(self.remoteToons[avId])
        DistributedMinigame.handleDisabledAvatar(self, avId)

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

    def __placeToon(self, avId):
        toon = self.getAvatar(avId)
        i = self.avIdList.index(avId)
        numToons = float(self.numPlayers)
        x = i * self.TOON_INITIAL_SPACING
        x -= self.TOON_INITIAL_SPACING * (numToons - 1) / 2.0
        toon.setPosHpr(x, self.TOON_Y, 0, 0, 0, 0)

    def setGameReady(self):
        if not self.hasLocalToon:
            return
        self.notify.debug('setGameReady')
        if DistributedMinigame.setGameReady(self):
            return
        if not self.isSinglePlayer():
            base.localAvatar.collisionsOff()
            cSphere = CollisionSphere(0.0, 0.0, 0.0, RingGameGlobals.CollisionRadius)
            cSphereNode = CollisionNode('RingGameSphere-%s' % self.localAvId)
            cSphereNode.addSolid(cSphere)
            cSphereNode.setFromCollideMask(RingGameGlobals.CollideMask)
            cSphereNode.setIntoCollideMask(BitMask32.allOff())
            self.cSphereNodePath = base.localAvatar.attachNewNode(cSphereNode)
            self.pusher = CollisionHandlerPusher()
            self.pusher.addCollider(self.cSphereNodePath, base.localAvatar)
            self.pusher.setHorizontal(0)
            self.cTrav = CollisionTraverser('DistributedRingGame')
            self.cTrav.addCollider(self.cSphereNodePath, self.pusher)
            self.remoteToonCollNPs = {}
            for avId in self.remoteAvIdList:
                toon = self.getAvatar(avId)
                if toon:
                    cSphere = CollisionSphere(0.0, 0.0, 0.0, RingGameGlobals.CollisionRadius)
                    cSphereNode = CollisionNode('RingGameSphere-%s' % avId)
                    cSphereNode.addSolid(cSphere)
                    cSphereNode.setCollideMask(RingGameGlobals.CollideMask)
                    cSphereNP = toon.attachNewNode(cSphereNode)
                    self.remoteToonCollNPs[avId] = cSphereNP

        for avId in self.remoteAvIdList:
            toon = self.getAvatar(avId)
            if toon:
                toon.reparentTo(render)
                self.__placeToon(avId)
                toon.setAnimState('swim', 1.0)
                toon.stopBobSwimTask()
                toon.useLOD(self.TOON_LOD)
                toon.dropShadow.hide()
                self.__addToonDropShadow(toon)
                toon.startSmooth()

        self.remoteToons = {}
        for avId in self.remoteAvIdList:
            toon = self.getAvatar(avId)
            self.remoteToons[avId] = toon

        self.__nextRingGroupResultIndex = 0

    def setGameStart(self, timestamp):
        if not self.hasLocalToon:
            return
        self.notify.debug('setGameStart')
        DistributedMinigame.setGameStart(self, timestamp)
        self.gameFSM.request('swim')
        base.playMusic(self.music, looping=0, volume=0.8)

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

    def exitOff(self):
        pass

    def enterSwim(self):
        self.notify.debug('enterSwim')
        self.__ringTimeBase = self.gameStartTime
        self.__numRingGroups = RingGameGlobals.NUM_RING_GROUPS
        self.__ringGroupsPassed = 0
        self.__generateRings()
        self.__spawnUpdateRingsTask()
        self.__spawnCollisionDetectionTask()
        self.__ringTracks = []
        self.colorRing = self.ringModel.copyTo(hidden)
        self.colorRing.reparentTo(base.a2dBottomRight)
        self.colorRing.setTwoSided(0)
        self.colorRing.setPos(-0.143, 10, 0.14)
        self.colorRing.setScale(0.04)
        p = self.avIdList.index(self.localAvId)
        self.colorRing.setColor(RingGameGlobals.ringColors[self.colorIndices[p]][1])
        self.resultTable = [self.RT_UNKNOWN] * self.__numRingGroups
        self.__initTallyDisplay()

    def __initTallyDisplay(self):
        self.__tallyTextNode = TextNode('tally')
        self.__tallyTextNode.setFont(ToontownGlobals.getSignFont())
        self.__tallyTextNode.setAlign(TextNode.ACenter)
        self.tallyMarkers = [None] * self.__numRingGroups
        for i in xrange(0, self.__numRingGroups):
            self.__createTallyMarker(i, self.RT_UNKNOWN)

        return

    def __destroyTallyDisplay(self):
        for i in xrange(0, self.__numRingGroups):
            self.__deleteTallyMarker(i)

        del self.tallyMarkers
        del self.__tallyTextNode

    def __createTallyMarker(self, index, result):
        chars = '-OOX'
        colors = (Point4(0.8, 0.8, 0.8, 1),
         Point4(0, 1, 0, 1),
         Point4(1, 1, 0, 1),
         Point4(1, 0, 0, 1))
        self.__deleteTallyMarker(index)
        self.__tallyTextNode.setText(chars[result])
        node = self.__tallyTextNode.generate()
        tallyText = base.a2dBottomLeft.attachNewNode(node)
        tallyText.setColor(colors[result])
        tallyText.setScale(0.1)
        zOffset = 0
        if result == self.RT_UNKNOWN:
            zOffset = 0.015
        xSpacing = 0.085
        tallyText.setPos(0.333 + xSpacing * index, 0, 0.07 + zOffset)
        self.tallyMarkers[index] = tallyText

    def __deleteTallyMarker(self, index):
        marker = self.tallyMarkers[index]
        if None != marker:
            marker.removeNode()
            self.tallyMarkers[index] = None
        return

    def __updateTallyDisplay(self, index):
        self.__createTallyMarker(index, self.resultTable[index])

    def __generateRings(self):
        self.ringGroups = []
        difficultyDistributions = {ToontownGlobals.ToontownCentral: [14, 2, 0],
         ToontownGlobals.DonaldsDock: [10, 6, 0],
         ToontownGlobals.DaisyGardens: [4, 12, 0],
         ToontownGlobals.MinniesMelodyland: [4, 8, 4],
         ToontownGlobals.TheBrrrgh: [4, 6, 6],
         ToontownGlobals.DonaldsDreamland: [2, 6, 8]}
        for distr in difficultyDistributions.values():
            sum = reduce(lambda x, y: x + y, distr)

        difficultyPatterns = {ToontownGlobals.ToontownCentral: [[0] * 14 + [1] * 2 + [2] * 0, [0,
                                            0,
                                            0,
                                            0,
                                            0,
                                            0,
                                            0,
                                            1] * 2, [0,
                                            0,
                                            0,
                                            0,
                                            0,
                                            0,
                                            0,
                                            0,
                                            0,
                                            0,
                                            0,
                                            1,
                                            0,
                                            0,
                                            0,
                                            1]],
         ToontownGlobals.DonaldsDock: [[0] * 10 + [1] * 6 + [2] * 0, [0,
                                        0,
                                        0,
                                        0,
                                        0,
                                        1,
                                        1,
                                        1] * 2, [0,
                                        0,
                                        0,
                                        1,
                                        0,
                                        0,
                                        1,
                                        1] * 2],
         ToontownGlobals.DaisyGardens: [[0] * 4 + [1] * 12 + [2] * 0, [0,
                                         0,
                                         1,
                                         1,
                                         1,
                                         1,
                                         1,
                                         1] * 2, [0,
                                         1,
                                         1,
                                         1,
                                         0,
                                         1,
                                         1,
                                         1] * 2],
         ToontownGlobals.MinniesMelodyland: [[0] * 4 + [1] * 8 + [2] * 4,
                                             [0,
                                              0,
                                              1,
                                              1,
                                              1,
                                              1,
                                              2,
                                              2] * 2,
                                             [0,
                                              1,
                                              1,
                                              1,
                                              1,
                                              0,
                                              2,
                                              2] * 2,
                                             [0,
                                              1,
                                              1,
                                              2,
                                              0,
                                              1,
                                              1,
                                              2] * 2,
                                             [0,
                                              1,
                                              2,
                                              1,
                                              0,
                                              1,
                                              2,
                                              1] * 2],
         ToontownGlobals.TheBrrrgh: [[0] * 4 + [1] * 6 + [2] * 6,
                                     [0,
                                      0,
                                      1,
                                      1,
                                      1,
                                      2,
                                      2,
                                      2] * 2,
                                     [0,
                                      1,
                                      1,
                                      1,
                                      0,
                                      2,
                                      2,
                                      2] * 2,
                                     [0,
                                      1,
                                      1,
                                      2,
                                      0,
                                      1,
                                      2,
                                      2] * 2,
                                     [0,
                                      1,
                                      2,
                                      1,
                                      0,
                                      1,
                                      2,
                                      2] * 2],
         ToontownGlobals.DonaldsDreamland: [[0] * 2 + [1] * 6 + [2] * 8,
                                            [0,
                                             1,
                                             1,
                                             1,
                                             2,
                                             2,
                                             2,
                                             2] * 2,
                                            [0,
                                             1,
                                             1,
                                             2,
                                             2,
                                             1,
                                             2,
                                             2] * 2,
                                            [0,
                                             1,
                                             2,
                                             1,
                                             2,
                                             1,
                                             2,
                                             2] * 2]}
        safezone = self.getSafezoneId()
        numGroupsPerDifficulty = difficultyDistributions[safezone]

        def patternsAreValid(difficultyPatterns = difficultyPatterns, difficultyDistributions = difficultyDistributions):
            for sz in difficultyPatterns.keys():
                for pattern in difficultyPatterns[sz]:
                    for difficulty in [0, 1, 2]:
                        numGroupsPerDifficulty = difficultyDistributions[sz]
                        if numGroupsPerDifficulty[difficulty] != pattern.count(difficulty):
                            print 'safezone:', sz
                            print 'pattern:', pattern
                            print 'difficulty:', difficulty
                            print 'expected %s %ss, found %s' % (numGroupsPerDifficulty[difficulty], difficulty, pattern.count(difficulty))
                            return 0

            return 1

        pattern = self.randomNumGen.choice(difficultyPatterns[self.getSafezoneId()])
        for i in xrange(0, self.__numRingGroups):
            numRings = self.numPlayers
            trackGroup = RingTrackGroups.getRandomRingTrackGroup(pattern[i], numRings, self.randomNumGen)
            ringGroup = RingGroup.RingGroup(trackGroup, self.ringModel, RingGameGlobals.MAX_TOONXZ, self.colorIndices)
            for r in xrange(numRings):
                self.__addRingDropShadow(ringGroup.getRing(r))

            self.ringGroups.append(ringGroup)
            ringGroup.reparentTo(self.ringNode)
            firstGroupOffset = self.TOON_Y + self.T_FIRST_RING_GROUP_ARRIVES * self.TOON_SWIM_VEL
            ringGroup.setY(i * self.RING_GROUP_SPACING + firstGroupOffset)

    def __destroyRings(self):
        for group in self.ringGroups:
            group.delete()
            group.removeNode()

        self.__removeAllRingDropShadows()
        del self.ringGroups

    def __spawnUpdateLocalToonTask(self):
        self.__initPosBroadcast()
        taskMgr.remove(self.UPDATE_LOCALTOON_TASK)
        taskMgr.add(self.__updateLocalToonTask, self.UPDATE_LOCALTOON_TASK)

    def __killUpdateLocalToonTask(self):
        taskMgr.remove(self.UPDATE_LOCALTOON_TASK)

    def __initPosBroadcast(self):
        self.__posBroadcastPeriod = 0.2
        self.__timeSinceLastPosBroadcast = 0.0
        self.__lastPosBroadcast = self.getAvatar(self.localAvId).getPos()
        self.__storeStop = 0
        lt = self.getAvatar(self.localAvId)
        lt.d_clearSmoothing()
        lt.sendCurrentPosition()

    def __posBroadcast(self, dt):
        self.__timeSinceLastPosBroadcast += dt
        if self.__timeSinceLastPosBroadcast > self.__posBroadcastPeriod:
            self.__timeSinceLastPosBroadcast -= self.__posBroadcastPeriod
            self.getAvatar(self.localAvId).cnode.broadcastPosHprFull()

    def __updateLocalToonTask(self, task):
        dt = globalClock.getDt()
        toonPos = self.getAvatar(self.localAvId).getPos()
        pos = [toonPos[0], 0, toonPos[2]]
        xVel = 0.0
        if self.arrowKeys.leftPressed():
            xVel -= self.TOONXZ_SPEED
        if self.arrowKeys.rightPressed():
            xVel += self.TOONXZ_SPEED
        pos[0] += xVel * dt
        if pos[0] < -RingGameGlobals.MAX_TOONXZ:
            pos[0] = -RingGameGlobals.MAX_TOONXZ
        if pos[0] > RingGameGlobals.MAX_TOONXZ:
            pos[0] = RingGameGlobals.MAX_TOONXZ
        zVel = 0.0
        if self.arrowKeys.upPressed():
            zVel += self.TOONXZ_SPEED
        if self.arrowKeys.downPressed():
            zVel -= self.TOONXZ_SPEED
        pos[2] += zVel * dt
        if pos[2] < -RingGameGlobals.MAX_TOONXZ:
            pos[2] = -RingGameGlobals.MAX_TOONXZ
        if pos[2] > RingGameGlobals.MAX_TOONXZ:
            pos[2] = RingGameGlobals.MAX_TOONXZ
        self.getAvatar(self.localAvId).setPos(pos[0], self.TOON_Y, pos[2])
        if hasattr(self, 'cTrav'):
            self.cTrav.traverse(render)
        self.__posBroadcast(dt)
        return Task.cont

    def exitSwim(self):
        for track in self.__ringTracks:
            track.finish()

        del self.__ringTracks
        self.colorRing.removeNode()
        del self.colorRing
        self.__destroyTallyDisplay()
        del self.resultTable
        taskMgr.remove(self.END_GAME_WAIT_TASK)
        self.__killUpdateRingsTask()
        self.__killCollisionDetectionTask()
        self.__destroyRings()

    def enterCleanup(self):
        self.notify.debug('enterCleanup')
        if not self.isSinglePlayer():
            for np in self.remoteToonCollNPs.values():
                np.removeNode()

            del self.remoteToonCollNPs
            self.cSphereNodePath.removeNode()
            del self.cSphereNodePath
            del self.pusher
            del self.cTrav
            base.localAvatar.collisionsOn()

    def exitCleanup(self):
        pass

    def __addDropShadow_INTERNAL(self, object, scale_x, scale_y, scale_z, list):
        shadow = self.dropShadowModel.copyTo(render)
        shadow.setPos(0, self.CAMERA_Y, -100)
        shadow.setScale(scale_x, scale_y, scale_z)
        list.append([shadow, object])

    def __removeDropShadow_INTERNAL(self, object, list):
        for i in xrange(len(list)):
            entry = list[i]
            if entry[1] == object:
                entry[0].removeNode()
                list.pop(i)
                return

        self.notify.warning('parent object ' + str(object) + ' not found in drop shadow list!')

    def __addToonDropShadow(self, object):
        self.__addDropShadow_INTERNAL(object, 0.5, 0.5, 0.5, self.toonDropShadows)

    def __removeToonDropShadow(self, object):
        self.__removeDropShadow_INTERNAL(object, self.toonDropShadows)

    def __addRingDropShadow(self, object):
        self.__addDropShadow_INTERNAL(object, 1.2, 0.31, 1.0, self.ringDropShadows)

    def __removeRingDropShadow(self, object):
        self.__removeDropShadow_INTERNAL(object, self.ringDropShadows)

    def __removeAllToonDropShadows(self):
        for entry in self.toonDropShadows:
            entry[0].removeNode()

        self.toonDropShadows = []

    def __removeAllRingDropShadows(self):
        for entry in self.ringDropShadows:
            entry[0].removeNode()

        self.ringDropShadows = []

    def __spawnUpdateShadowsTask(self):
        taskMgr.remove(self.UPDATE_SHADOWS_TASK)
        taskMgr.add(self.__updateShadowsTask, self.UPDATE_SHADOWS_TASK, priority=self.UPDATE_SHADOWS_PRIORITY)

    def __killUpdateShadowsTask(self):
        taskMgr.remove(self.UPDATE_SHADOWS_TASK)

    def __updateShadowsTask(self, task):
        list = self.toonDropShadows + self.ringDropShadows
        for entry in list:
            object = entry[1]
            y = object.getY(render)
            if y > self.Y_VIS_MAX:
                continue
            x = object.getX(render)
            z = self.SEA_FLOOR_Z + self.SHADOW_Z_OFFSET
            shadow = entry[0]
            shadow.setPos(x, y, z)

        return Task.cont

    def __spawnUpdateEnvironTask(self):
        taskMgr.remove(self.UPDATE_ENVIRON_TASK)
        taskMgr.add(self.__updateEnvironTask, self.UPDATE_ENVIRON_TASK)

    def __killUpdateEnvironTask(self):
        taskMgr.remove(self.UPDATE_ENVIRON_TASK)

    def __updateEnvironTask(self, task):
        t = globalClock.getFrameTime() - self.__timeBase
        distance = t * self.TOON_SWIM_VEL
        distance %= self.ENVIRON_LENGTH
        distance += self.ENVIRON_START_OFFSET
        self.environNode.setY(-distance)
        return Task.cont

    def __spawnUpdateRingsTask(self):
        taskMgr.remove(self.UPDATE_RINGS_TASK)
        taskMgr.add(self.__updateRingsTask, self.UPDATE_RINGS_TASK)

    def __killUpdateRingsTask(self):
        taskMgr.remove(self.UPDATE_RINGS_TASK)

    def __updateRingsTask(self, task):
        t = globalClock.getFrameTime() - self.__ringTimeBase
        distance = t * self.TOON_SWIM_VEL
        self.ringNode.setY(-distance)
        for ringGroup in self.ringGroups:
            groupY = ringGroup.getY(render)
            if groupY <= self.Y_VIS_MAX and groupY >= self.Y_VIS_MIN:
                ringGroup.setT(t)

        return Task.cont

    def __spawnCollisionDetectionTask(self):
        self.__ringGroupsPassed = 0
        taskMgr.remove(self.COLLISION_DETECTION_TASK)
        taskMgr.add(self.__collisionDetectionTask, self.COLLISION_DETECTION_TASK, priority=self.COLLISION_DETECTION_PRIORITY)

    def __killCollisionDetectionTask(self):
        taskMgr.remove(self.COLLISION_DETECTION_TASK)

    def __makeRingSuccessFadeTrack(self, ring, duration, endScale, ringIndex):
        targetScale = Point3(endScale, endScale, endScale)
        dFade = 0.5 * duration
        dColorChange = duration - dFade
        origColor = ring.getColor()
        targetColor = Point4(1.0 - origColor[0], 1.0 - origColor[1], 1.0 - origColor[2], 1)

        def colorChangeFunc(t, ring = ring, targetColor = targetColor, origColor = origColor):
            newColor = targetColor * t + origColor * (1.0 - t)
            ring.setColor(newColor)

        def fadeFunc(t, ring = ring):
            ring.setColorScale(1, 1, 1, 1.0 - t)

        fadeAwayTrack = Parallel(Sequence(LerpFunctionInterval(colorChangeFunc, fromData=0.0, toData=1.0, duration=dColorChange), LerpFunctionInterval(fadeFunc, fromData=0.0, toData=1.0, duration=dFade)), LerpScaleInterval(ring, duration, targetScale))
        successTrack = Sequence(Wait(self.RING_RESPONSE_DELAY), Parallel(SoundInterval(self.sndTable['gotRing'][ringIndex]), Sequence(Func(ring.wrtReparentTo, render), fadeAwayTrack, Func(self.__removeRingDropShadow, ring), Func(ring.reparentTo, hidden))))
        return successTrack

    def __makeRingFailureFadeTrack(self, ring, duration, ringIndex):
        ts = 0.01
        targetScale = Point3(ts, ts, ts)
        missedTextNode = self.__genText(TTLocalizer.RingGameMissed)
        missedText = hidden.attachNewNode(missedTextNode)
        ringColor = RingGameGlobals.ringColors[self.colorIndices[ringIndex]][1]

        def addMissedText(ring = ring, ringColor = ringColor, missedText = missedText):
            missedText.reparentTo(render)
            missedText.setPos(ring.getPos(render) + Point3(0, -1, 0))
            missedText.setColor(ringColor)

        def removeMissedText(missedText = missedText):
            missedText.removeNode()
            missedText = None
            return

        failureTrack = Sequence(Wait(self.RING_RESPONSE_DELAY), Parallel(SoundInterval(self.sndTable['missedRing'][ringIndex]), Sequence(Func(ring.wrtReparentTo, render), Func(addMissedText), LerpScaleInterval(ring, duration, targetScale, blendType='easeIn'), Func(removeMissedText), Func(self.__removeRingDropShadow, ring), Func(ring.reparentTo, hidden))))
        return failureTrack

    def __makeRingFadeAway(self, ring, success, ringIndex):
        if success:
            track = self.__makeRingSuccessFadeTrack(ring, 0.5, 2.0, ringIndex)
        else:
            track = self.__makeRingFailureFadeTrack(ring, 0.5, ringIndex)
        self.__ringTracks.append(track)
        track.start()

    def __collisionDetectionTask(self, task):
        nextRingGroupIndex = self.__ringGroupsPassed
        nextRingGroup = self.ringGroups[nextRingGroupIndex]
        if nextRingGroup.getY(render) < 0:
            groupIndex = nextRingGroupIndex
            gotRing = 0
            ourRing = nextRingGroup.getRing(self.avIdList.index(self.localAvId))
            ringPos = ourRing.getPos(render)
            localToonPos = base.localAvatar.getPos(render)
            distX = localToonPos[0] - ringPos[0]
            distZ = localToonPos[2] - ringPos[2]
            distSqrd = distX * distX + distZ * distZ
            if distSqrd <= self.RING_RADIUS_SQRD:
                gotRing = 1
            self.__makeRingFadeAway(ourRing, gotRing, self.avIdList.index(self.localAvId))
            if gotRing:
                self.resultTable[groupIndex] = self.RT_SUCCESS
            else:
                self.resultTable[groupIndex] = self.RT_FAILURE
            self.__updateTallyDisplay(groupIndex)
            self.sendUpdate('setToonGotRing', [gotRing])
            if self.isSinglePlayer():
                self.__processRingGroupResults([gotRing])
            self.__ringGroupsPassed += 1
            if self.__ringGroupsPassed >= self.__numRingGroups:
                self.__killCollisionDetectionTask()
        return Task.cont

    def __endGameDolater(self, task):
        self.gameOver()
        return Task.done

    def setTimeBase(self, timestamp):
        if not self.hasLocalToon:
            return
        self.__timeBase = globalClockDelta.networkToLocalTime(timestamp)

    def setColorIndices(self, a, b, c, d):
        if not self.hasLocalToon:
            return
        self.colorIndices = [a,
         b,
         c,
         d]

    def __getSuccessTrack(self, groupIndex):

        def makeSuccessTrack(text, holdDuration, fadeDuration = 0.5, perfect = 0, self = self):
            successText = hidden.attachNewNode(self.__genText(text))
            successText.setScale(0.25)
            successText.setColor(1, 1, 0.5, 1)

            def fadeFunc(t, text):
                text.setColorScale(1, 1, 1, 1.0 - t)

            def destroyText(text):
                text.removeNode()
                text = None
                return

            track = Sequence(Func(successText.reparentTo, aspect2d), Wait(holdDuration), LerpFunctionInterval(fadeFunc, extraArgs=[successText], fromData=0.0, toData=1.0, duration=fadeDuration, blendType='easeIn'), Func(destroyText, successText))
            if perfect:
                track = Parallel(track, SoundInterval(self.sndPerfect))
            return track

        def isPerfect(list, goodValues):
            for value in list:
                if value not in goodValues:
                    return 0

            return 1

        if groupIndex >= self.__numRingGroups - 1:
            if not self.isSinglePlayer():
                if isPerfect(self.resultTable, [self.RT_GROUPSUCCESS]):
                    return makeSuccessTrack(TTLocalizer.RingGameGroupPerfect, 1.5, perfect=1)
            if isPerfect(self.resultTable, [self.RT_SUCCESS, self.RT_GROUPSUCCESS]):
                return makeSuccessTrack(TTLocalizer.RingGamePerfect, 1.5, perfect=1)
            return Wait(1.0)
        if not self.isSinglePlayer():
            if self.resultTable[groupIndex] == self.RT_GROUPSUCCESS:
                return makeSuccessTrack(TTLocalizer.RingGameGroupBonus, 0.0, fadeDuration=0.4)
        return None

    def __processRingGroupResults(self, results):
        groupIndex = self.__nextRingGroupResultIndex
        ringGroup = self.ringGroups[self.__nextRingGroupResultIndex]
        self.__nextRingGroupResultIndex += 1
        for i in xrange(0, self.numPlayers):
            if self.avIdList[i] != self.localAvId:
                ring = ringGroup.getRing(i)
                self.__makeRingFadeAway(ring, results[i], i)

        if not self.isSinglePlayer():
            if 0 not in results:
                self.notify.debug('Everyone got their rings!!')
                self.resultTable[groupIndex] = self.RT_GROUPSUCCESS
                self.__updateTallyDisplay(groupIndex)
        successTrack = self.__getSuccessTrack(groupIndex)
        endGameTrack = None
        if groupIndex >= self.__numRingGroups - 1:

            def endTheGame(self = self):
                if not RingGameGlobals.ENDLESS_GAME:
                    taskMgr.doMethodLater(self.GAME_END_DELAY, self.__endGameDolater, self.END_GAME_WAIT_TASK)

            endGameTrack = Func(endTheGame)
        if None != successTrack or None != endGameTrack:
            track = Sequence()
            if None != successTrack:
                track.append(Wait(self.RING_RESPONSE_DELAY))
                track.append(successTrack)
            if None != endGameTrack:
                track.append(endGameTrack)
            self.__ringTracks.append(track)
            track.start()
        return

    def setRingGroupResults(self, bitfield):
        if not self.hasLocalToon:
            return
        if self.gameFSM.getCurrentState().getName() != 'swim':
            return
        results = []
        mask = 1
        for avId in self.avIdList:
            results.append(not bitfield & mask)
            mask <<= 1

        self.__processRingGroupResults(results)