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.reparentTo(base.a2dTopRight)
            scorePanel.setPos(-0.213, 0.0, -0.5 - 0.28 * i)
            self.scorePanels.append(scorePanel)

        self.goalBar.show()
        self.goalBar['value'] = 0.0
        base.setCellsActive(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()
        self.accept(MazeSuit.COLLISION_EVENT_NAME, self.__hitBySuit)
        self.accept(self.TREASURE_GRAB_EVENT_NAME, self.__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.__resetClock)
        base.playMusic(self.music, looping=0, volume=0.8)
    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.reparentTo(base.a2dTopRight)
            scorePanel.setPos(-0.213, 0.0, -0.5 - 0.28 * i)
            self.scorePanels.append(scorePanel)

        self.goalBar.show()
        self.goalBar['value'] = 0.0
        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()
        self.accept(MazeSuit.COLLISION_EVENT_NAME, self.__hitBySuit)
        self.accept(self.TREASURE_GRAB_EVENT_NAME, self.__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.__resetClock)
        base.playMusic(self.music, looping=0, volume=0.8)
 def enterPlay(self):
     self.notify.debug('enterPlay')
     base.playMusic(self.music, looping = 1, volume = 0.90000000000000002)
     orthoDrive = OrthoDrive(self.TOON_SPEED, maxFrameMove = self.MAX_FRAME_MOVE, customCollisionCallback = self._DistributedPairingGame__doPairingGameCollisions)
     self.orthoWalk = OrthoWalk(orthoDrive, broadcast = not self.isSinglePlayer())
     self.orthoWalk.start()
     self.accept('insert', self._DistributedPairingGame__flipKeyPressed)
     self.accept('delete', self._DistributedPairingGame__flipKeyPressed)
     self.accept('time-control', self._DistributedPairingGame__beginSignal)
     self.accept('time-control-up', self._DistributedPairingGame__endSignal)
     self.bonusGlowIndex = 0
     self.bonusGlowCard = self.bonusTraversal[self.bonusGlowIndex]
     self.startBonusTask()
     self.timer = ToontownTimer.ToontownTimer()
     self.timer.posInTopRightCorner()
     self.timer.setTime(self.gameDuration)
     self.timer.countdown(self.gameDuration, self.timerExpired)
     if base.localAvatar.laffMeter:
         base.localAvatar.laffMeter.stop()
Beispiel #4
0
    def enterPlay(self):
        self.notify.debug("enterPlay")

        # Start music
        base.playMusic(self.music, looping=1, volume=0.9)

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

        # listen for key presses
        # We used to use the insert key for tossing pies.
        # Nowadays we use the delete key instead, for better
        # consistency with Macs (which lack an insert key).
        self.accept('insert', self.__flipKeyPressed)
        self.accept('delete', self.__flipKeyPressed)

        self.accept('time-control', self.__beginSignal)
        self.accept('time-control-up', self.__endSignal)

        self.bonusGlowIndex = 0
        self.bonusGlowCard = self.bonusTraversal[self.bonusGlowIndex]
        self.startBonusTask()

        self.timer = ToontownTimer.ToontownTimer()
        self.timer.posInTopRightCorner()
        self.timer.setTime(self.gameDuration)
        self.timer.countdown(self.gameDuration, self.timerExpired)

        if base.localAvatar.laffMeter:
            base.localAvatar.laffMeter.stop()
 def enterPlay(self):
     self.notify.debug('enterPlay')
     base.playMusic(self.music, looping=1, volume=0.9)
     orthoDrive = OrthoDrive(self.TOON_SPEED, maxFrameMove=self.MAX_FRAME_MOVE, customCollisionCallback=self.__doPairingGameCollisions)
     self.orthoWalk = OrthoWalk(orthoDrive, broadcast=not self.isSinglePlayer())
     self.orthoWalk.start()
     self.accept('insert', self.__flipKeyPressed)
     self.accept('delete', self.__flipKeyPressed)
     self.accept('time-control', self.__beginSignal)
     self.accept('time-control-up', self.__endSignal)
     self.bonusGlowIndex = 0
     self.bonusGlowCard = self.bonusTraversal[self.bonusGlowIndex]
     self.startBonusTask()
     self.timer = ToontownTimer.ToontownTimer()
     self.timer.posInTopRightCorner()
     self.timer.setTime(self.gameDuration)
     self.timer.countdown(self.gameDuration, self.timerExpired)
     if base.localAvatar.laffMeter:
         base.localAvatar.laffMeter.stop()
Beispiel #6
0
class DistributedPairingGame(DistributedMinigame):

    # define constants that you won't want to tweak here
    TOON_SPEED = 11  #8
    MAX_FRAME_MOVE = 1  # maximum movement in one frame
    MAX_FACE_UP_CARDS = 2

    notify = directNotify.newCategory("DistributedPairingGame")
    bonusGlowTime = 0.5  # how many seconds does bonus stay on a card

    EndGameTaskName = 'endPairingGame'

    xCardInc = 4

    cardsPerRow = 8
    cardsPerCol = 5

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

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

        # it's important for the final state to do cleanup;
        # on disconnect, the ClassicFSM will be forced into the
        # final state. All states (except 'off') should
        # be prepared to transition to 'cleanup' at any time.

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

        self.cameraTopView = (17.6, 6.18756, 43.9956, 0, -89, 0)
        #self.cameraThreeQuarterView = (13.8234, -8.93352, 33.4497, 0, -62.89, 0)
        self.cameraThreeQuarterView = (14.0, -8.93352, 33.4497, 0, -62.89, 0)

        self.deckSeed = 0
        self.faceUpList = []  # which cards are face up
        self.localFaceUpList = []  # which cards did this local toon turn up

        self.inList = []  # which cards is the local toon in
        self.inactiveList = []  # which cards are out of play

        self.points = 0
        self.flips = 0
        self.matches = 0

        self.yCardInc = 4

        self.startingPositions = [
            (0, 0, 0, -45),
            ((self.cardsPerRow - 1) * self.xCardInc,
             (self.cardsPerCol - 1) * self.yCardInc, 0, 135),
            ((self.cardsPerRow - 1) * self.xCardInc, 0, 0, 45),
            (0, (self.cardsPerCol - 1) * self.yCardInc, 0, -135),
        ]

        self.stageMin = Point2(0, 0)
        self.stageMax = Point2((self.cardsPerRow - 1) * self.xCardInc,
                               (self.cardsPerCol - 1) * self.yCardInc)
        self.gameDuration = PairingGameGlobals.EasiestGameDuration

    def moveCameraToTop(self):
        camera.reparentTo(render)
        #p = self.cameraTopView
        p = self.cameraThreeQuarterView
        camera.setPosHpr(p[0], p[1], p[2], p[3], p[4], p[5])

    def getTitle(self):
        return TTLocalizer.PairingGameTitle

    def getInstructions(self):
        if self.numPlayers > 1:
            return TTLocalizer.PairingGameInstructionsMulti
        else:
            return TTLocalizer.PairingGameInstructions

    def getMaxDuration(self):
        # how many seconds can this minigame possibly last (within reason)?
        # this is for debugging only
        return 0

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

        self.gameDuration = PairingGameGlobals.calcGameDuration(
            self.getDifficulty())

        #self.gameBoard = loader.loadModel("phase_4/models/minigames/toon_cannon_gameground")
        self.gameBoard = loader.loadModel(
            "phase_4/models/minigames/memory_room")
        self.gameBoard.setPosHpr(0.5, 0, 0, 0, 0, 0)
        self.gameBoard.setScale(1.0)
        #self.gameBoard.find('**/tree_ring').removeNode()

        #self.debugAxis = loader.loadModel('models/misc/xyzAxis')
        #self.debugAxis.reparentTo(self.gameBoard)
        self.deck = PairingGameGlobals.createDeck(self.deckSeed,
                                                  self.numPlayers)
        self.notify.debug('%s' % self.deck.cards)

        testCard = self.getDeckOrderIndex(self.cardsPerCol - 1, 0)
        if not testCard > -1:
            self.yCardInc *= 1.25

        self.cards = []

        for index in xrange(len(self.deck.cards)):
            cardValue = self.deck.cards[index]
            oneCard = PairingGameCard.PairingGameCard(cardValue)
            oneCard.load()
            xPos, yPos = self.getCardPos(index)
            oneCard.setPos(xPos, yPos, 0)
            oneCard.reparentTo(render)
            self.notify.debug('%s' % oneCard.getPos())
            self.notify.debug('suit %s rank %s value %s' %
                              (oneCard.suit, oneCard.rank, oneCard.value))
            self.accept('entercardCollision-%d' % oneCard.value,
                        self.enterCard)
            self.accept('exitcardCollision-%d' % oneCard.value, self.exitCard)
            oneCard.turnDown(doInterval=False)
            self.cards.append(oneCard)

        self.bonusTraversal = range(len(self.cards))

        self.bonusGlow = render.attachNewNode('bonusGlow')

        sign = loader.loadModel("phase_4/models/minigames/garden_sign_memory")
        # remove the bits we don't want
        sign.find('**/sign1').removeNode()
        sign.find('**/sign2').removeNode()
        sign.find('**/collision').removeNode()
        #sign.setTransparency(1)
        #sign.setColorScale(1,1,0,0.5)
        sign.setPos(0, 0, 0.05)
        sign.reparentTo(self.bonusGlow)

        self.bonusGlow.setScale(2.5)

        self.pointsFrame = DirectFrame(
            #parent = self.gui,
            relief=None,
            geom=DGG.getDefaultDialogGeom(),
            geom_color=GlobalDialogColor,
            geom_scale=(4, 1, 1),
            pos=(-0.33, 0, 0.9),
            scale=0.1,
            text=TTLocalizer.PairingGamePoints,
            text_align=TextNode.ALeft,
            text_scale=TTLocalizer.DPGpointsFrame,
            text_pos=(-1.94, -0.1, 0.0))
        self.pointsLabel = DirectLabel(
            parent=self.pointsFrame,
            relief=None,
            text='0',
            text_fg=VBase4(0, 0.5, 0, 1),
            text_align=TextNode.ARight,
            text_scale=0.7,
            pos=(1.82, 0, -0.15),
        )

        self.flipsFrame = DirectFrame(
            #parent = self.gui,
            relief=None,
            geom=DGG.getDefaultDialogGeom(),
            geom_color=GlobalDialogColor,
            geom_scale=(4, 1, 1),
            pos=(0.33, 0, 0.9),
            scale=0.1,
            text=TTLocalizer.PairingGameFlips,
            text_align=TextNode.ALeft,
            text_scale=TTLocalizer.DPGflipsFrame,
            text_pos=(-1.94, -0.1, 0.0))
        self.flipsLabel = DirectLabel(
            parent=self.flipsFrame,
            relief=None,
            text='0',
            text_fg=VBase4(0, 1.0, 0, 1),
            text_align=TextNode.ARight,
            text_scale=0.7,
            pos=(1.82, 0, -0.15),
        )

        # this will be used to generate textnodes
        self.__textGen = TextNode("ringGame")
        self.__textGen.setFont(ToontownGlobals.getSignFont())
        self.__textGen.setAlign(TextNode.ACenter)

        self.sndPerfect = base.loadSfx(
            "phase_4/audio/sfx/MG_pairing_all_matched.mp3")

        self.calcBonusTraversal()

        self.music = base.loadMusic("phase_4/audio/bgm/MG_Pairing.mid")
        self.matchSfx = base.loadSfx("phase_4/audio/sfx/MG_pairing_match.mp3")
        self.matchWithBonusSfx = base.loadSfx(
            "phase_4/audio/sfx/MG_pairing_match_bonus_both.mp3")
        self.signalSfx = []
        for i in range(4):
            self.signalSfx.append(
                base.loadSfx(
                    "phase_4/audio/sfx/MG_pairing_jumping_signal.mp3"))
        self.bonusMovesSfx = base.loadSfx(
            "phase_4/audio/sfx/MG_pairing_bonus_moves.mp3")

        # WARNING DEBUG only, remove or else it will leak
        # base.minigame = self

    def unload(self):
        self.notify.debug("unload")
        DistributedMinigame.unload(self)
        # unload resources and delete objects from load() here
        # remove our game ClassicFSM from the framework ClassicFSM
        self.removeChildGameFSM(self.gameFSM)
        del self.gameFSM

        self.gameBoard.removeNode()
        del self.gameBoard

        for card in self.cards:
            card.unload()
            del card
        self.cards = []

        self.pointsFrame.removeNode()
        del self.pointsFrame
        self.flipsFrame.removeNode()
        del self.flipsFrame

        del self.__textGen
        del self.sndPerfect

        self.bonusGlow.removeNode()
        del self.bonusGlow

        del self.music
        del self.matchSfx
        del self.matchWithBonusSfx
        for i in range(4):
            del self.signalSfx[0]
        self.signalSfx = []
        del self.bonusMovesSfx

    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.gameBoard.reparentTo(render)

        for card in self.cards:
            card.reparentTo(render)

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

        self.moveCameraToTop()

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

        self.gameBoard.hide()

        for card in self.cards:
            card.hide()

        # the base class parents the toons to hidden, so consider
        # calling it last
        DistributedMinigame.offstage(self)

    def handleDisabledAvatar(self, avId):
        """This will be called if an avatar exits unexpectedly"""
        self.notify.debug("handleDisabledAvatar")
        self.notify.debug("avatar " + str(avId) + " disabled")
        # clean up any references to the disabled avatar before he disappears

        # then call the base class
        DistributedMinigame.handleDisabledAvatar(self, avId)

    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.
        for index in xrange(self.numPlayers):
            avId = self.avIdList[index]
            # Find the actual avatar in the cr
            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.

        # 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")

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

    def isInPlayState(self):
        """Return true if we are in the play state."""
        if not self.gameFSM.getCurrentState():
            return False
        if not self.gameFSM.getCurrentState().getName() == 'play':
            return False
        return True

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

    def exitOff(self):
        pass

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

        # Start music
        base.playMusic(self.music, looping=1, volume=0.9)

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

        # listen for key presses
        # We used to use the insert key for tossing pies.
        # Nowadays we use the delete key instead, for better
        # consistency with Macs (which lack an insert key).
        self.accept('insert', self.__flipKeyPressed)
        self.accept('delete', self.__flipKeyPressed)

        self.accept('time-control', self.__beginSignal)
        self.accept('time-control-up', self.__endSignal)

        self.bonusGlowIndex = 0
        self.bonusGlowCard = self.bonusTraversal[self.bonusGlowIndex]
        self.startBonusTask()

        self.timer = ToontownTimer.ToontownTimer()
        self.timer.posInTopRightCorner()
        self.timer.setTime(self.gameDuration)
        self.timer.countdown(self.gameDuration, self.timerExpired)

        if base.localAvatar.laffMeter:
            base.localAvatar.laffMeter.stop()

        # when the game is done, call gameOver()
        # self.gameOver()

    def exitPlay(self):
        # Stop music
        self.music.stop()

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

        self.stopBonusTask()

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

        self.ignoreAll()

        if base.localAvatar.laffMeter:
            base.localAvatar.laffMeter.start()

        if hasattr(self, 'perfectIval'):
            self.perfectIval.pause()
            del self.perfectIval

        taskMgr.remove(self.EndGameTaskName)
        taskMgr.remove('pairGameContinueSignal')

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

    def exitCleanup(self):
        pass

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

    def __doPairingGameCollisions(self, oldPos, newPos):
        x = bound(newPos[0], self.stageMin[0], self.stageMax[0])
        y = bound(newPos[1], self.stageMin[1], self.stageMax[1])
        newPos.setX(x)
        newPos.setY(y)

        if self.inList:
            newPos.setZ(0.15)
        else:
            newPos.setZ(0.0)

        #if we're moving also cancel out the signaling task
        if not oldPos == newPos:
            taskMgr.remove('pairGameContinueSignal')
        return newPos

    def getDeckOrderFromValue(self, value):
        for index in xrange(len(self.cards)):
            if self.cards[index].value == value:
                return index
        return -1

    def getDeckOrderFromPairingGameCard(self, into):
        try:
            index = self.cards.index(into)
        except ValueError:
            index = -1
        return index

    def enterCard(self, colEntry):
        intoName = colEntry.getIntoNodePath().getName()
        parts = intoName.split('-')
        value = int(parts[1])

        self.notify.debug('entered cardValue %d' % value)
        deckOrder = self.getDeckOrderFromValue(value)

        if not deckOrder in self.inList:
            self.inList.append(deckOrder)

    def exitCard(self, colEntry):
        intoName = colEntry.getIntoNodePath().getName()
        parts = intoName.split('-')
        value = int(parts[1])
        self.notify.debug('exited cardValue %d' % value)
        deckOrder = self.getDeckOrderFromValue(value)
        if deckOrder in self.inList:
            self.inList.remove(deckOrder)

    def handleMatch(self, cardA, cardB, withBonus):
        self.notify.debug('we got a match %d %d' % (cardA, cardB))
        self.matches += 1

        if cardA in self.faceUpList:
            self.faceUpList.remove(cardA)
        if cardB in self.faceUpList:
            self.faceUpList.remove(cardB)

        self.inactiveList.append(cardA)
        self.inactiveList.append(cardB)

        matchIval = Parallel()
        for card in [cardA, cardB]:
            self.cards[card].setTransparency(1)
            cardSeq = Sequence(
                LerpColorScaleInterval(self.cards[card],
                                       duration=1,
                                       colorScale=Vec4(1.0, 1.0, 1.0, 0.0)),
                Func(self.cards[card].hide))
            matchIval.append(cardSeq)

        if withBonus:
            matchIval.append(
                SoundInterval(self.matchWithBonusSfx,
                              node=self.cards[card],
                              listenerNode=base.localAvatar,
                              cutOff=240))
        else:
            matchIval.append(
                SoundInterval(self.matchSfx,
                              node=self.cards[card],
                              listenerNode=base.localAvatar,
                              cutOff=240))
        matchIval.start()

        # report we're done if all cards are matched
        if len(self.inactiveList) == len(self.cards):
            self.sendUpdate('reportDone')

    def turnUpCard(self, deckOrder):
        self.cards[deckOrder].turnUp()
        self.faceUpList.append(deckOrder)

    def turnDownCard(self, deckOrder):
        self.cards[deckOrder].turnDown()
        if deckOrder in self.faceUpList:
            self.faceUpList.remove(deckOrder)

    def __flipKeyPressed(self):
        if self.inList:
            shortestDistance = 10000
            cardToFlip = -1
            for deckOrder in self.inList:
                dist = base.localAvatar.getDistance(self.cards[deckOrder])
                if dist < shortestDistance:
                    shortestDistance = dist
                    cardToFlip = deckOrder
            deckOrderIndex = cardToFlip  # self.inList[-1]
            card = self.cards[deckOrderIndex]
            if card.isFaceDown() and not deckOrderIndex in self.inactiveList:
                #self.turnUpCard(deckOrderIndex)
                self.sendUpdate('openCardRequest',
                                [deckOrderIndex, self.bonusGlowCard])
            elif card.isFaceUp() and deckOrderIndex in self.faceUpList:
                pass
                #make sure this is the latest card
                #self.faceUpList.remove(deckOrderIndex)
                #self.faceUpList.append(deckOrderIndex)

    def moveBonusGlowTask(self, task):
        """
        move the bonus glow based on the game time to keep 2 clients in sync
        """
        if len(self.cards) == 0:
            return Task.done

        curT = self.getCurrentGameTime()
        intTime = int(curT / self.bonusGlowTime)
        newIndex = intTime % len(self.cards)
        if not newIndex == self.bonusGlowIndex:
            self.bonusGlowIndex = newIndex
            self.bonusGlowCard = self.bonusTraversal[self.bonusGlowIndex]
            card = self.cards[self.bonusGlowCard]
            self.bonusGlow.setPos(card.getPos())
            base.playSfx(self.bonusMovesSfx, node=card, volume=0.25)
        return Task.cont

    def timerExpired(self):
        self.sendUpdate('reportDone')
        pass

    def setDeckSeed(self, deckSeed):
        if not self.hasLocalToon: return
        self.deckSeed = deckSeed

    def updateFlipText(self):
        self.flipsLabel['text'] = str(self.flips)
        lowFlipModifier = PairingGameGlobals.calcLowFlipModifier(
            self.matches, self.flips)
        red = 1.0 - lowFlipModifier
        green = lowFlipModifier
        self.flipsLabel['text_fg'] = Vec4(red, green, 0, 1.0)

    def openCardResult(self, cardToTurnUp, avId, matchingCard, points,
                       cardsToTurnDown):
        if not self.hasLocalToon: return
        assert self.notify.debugStateCall()

        if not self.isInPlayState():
            return

        if avId == base.localAvatar.doId:
            self.localFaceUpList.append(cardToTurnUp)

        self.turnUpCard(cardToTurnUp)

        gotBonus = False
        if points - self.points > 1:
            gotBonus = True

        if matchingCard > -1:
            self.handleMatch(cardToTurnUp, matchingCard, gotBonus)

        self.flips += 1
        self.updateFlipText()

        self.points = points
        self.pointsLabel['text'] = str(self.points)

        for card in cardsToTurnDown:
            self.turnDownCard(card)

    def startBonusTask(self):
        taskMgr.add(self.moveBonusGlowTask, self.taskName("moveBonusGlowTask"))

    def stopBonusTask(self):
        taskMgr.remove(self.taskName("moveBonusGlowTask"))

    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 PairingGameGlobals.EndlessGame:
                self.gameOver()
            return Task.done

        # hide the timer
        self.timer.hide()

        # hide the bonus
        self.bonusGlow.hide()

        # if it was a perfect game, let the players know
        if len(self.inactiveList) == len(self.cards):
            self.notify.debug("perfect game!")

            perfectTextSubnode = hidden.attachNewNode(
                self.__genText(TTLocalizer.PairingGamePerfect))
            perfectText = hidden.attachNewNode('perfectText')
            perfectTextSubnode.reparentTo(perfectText)
            # offset the subnode so that the text is centered on both axes
            # we need the parent node so that the text will scale correctly
            frame = self.__textGen.getCardActual()
            offsetY = -abs(frame[2] + frame[3]) / 2.
            perfectTextSubnode.setPos(0, 0, offsetY)

            perfectText.setColor(1, .1, .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=.5,
                                      scale=.3,
                                      startScale=0.),
                    LerpFunctionInterval(
                        fadeFunc,
                        fromData=0.,
                        toData=1.,
                        duration=.5,
                    )),
                Wait(2.),
                Parallel(
                    LerpScaleInterval(perfectText, duration=.5, scale=1.),
                    LerpFunctionInterval(fadeFunc,
                                         fromData=1.,
                                         toData=0.,
                                         duration=.5,
                                         blendType="easeIn"),
                ),
                Func(destroyText),
                WaitInterval(.5),
                Func(endGame, None),
            )

            soundTrack = SoundInterval(self.sndPerfect)

            self.perfectIval = Parallel(textTrack, soundTrack)
            self.perfectIval.start()
        else:
            taskMgr.doMethodLater(1, endGame, self.EndGameTaskName)

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

    def b_setSignaling(self, avId):
        self.setSignaling(avId)
        self.sendUpdate('setSignaling', [self.localAvId])

    def setSignaling(self, avId):
        if not self.hasLocalToon: return
        avIndex = self.avIdList.index(avId)
        av = base.cr.doId2do.get(avId)
        if av and (avIndex >= 0) and hasattr(self,
                                             'signalSfx') and self.signalSfx:
            base.playSfx(self.signalSfx[avIndex], node=av)

    def __beginSignal(self, mouseParam):
        self.notify.debug('beginSignal')
        base.localAvatar.b_setEmoteState(1, 1.0)
        self.b_setSignaling(self.localAvId)

        # we get the time of the jump animation  from base.localAvatar.animPanel()
        taskMgr.doMethodLater(1.67, self.__continueSignal,
                              'pairGameContinueSignal')

    def __endSignal(self, mouseParam):
        self.notify.debug('endSignal')
        base.localAvatar.b_setEmoteState(-1, 1.0)
        taskMgr.remove('pairGameContinueSignal')

    def __continueSignal(self, task):
        base.localAvatar.b_setEmoteState(1, 1.0)
        self.b_setSignaling(self.localAvId)
        taskMgr.doMethodLater(1.67, self.__continueSignal,
                              'pairGameContinueSignal')

    def getCardPos(self, deckOrderIndex):
        col = deckOrderIndex % self.cardsPerRow
        row = deckOrderIndex / self.cardsPerRow
        x = col * self.xCardInc
        y = row * self.yCardInc
        return x, y

    def getDeckOrderIndex(self, row, col):
        """
        returns the card at a given row, and column, or -1 if a card is not there
        """
        retval = row * self.cardsPerRow
        retval += col
        if retval >= len(self.deck.cards):
            retval = -1
        return retval

    def calcBonusTraversal(self):
        self.bonusTraversal = []
        halfRow = self.cardsPerRow / 2
        if self.cardsPerRow % 2:
            halfRow += 1
        for i in xrange(halfRow):
            for j in xrange(2):
                col = i + j * halfRow
                for row in xrange(self.cardsPerCol):
                    card = self.getDeckOrderIndex(row, col)
                    if card > -1:
                        self.bonusTraversal.append(card)
class DistributedPairingGame(DistributedMinigame):
    TOON_SPEED = 11
    MAX_FRAME_MOVE = 1
    MAX_FACE_UP_CARDS = 2
    notify = directNotify.newCategory('DistributedPairingGame')
    bonusGlowTime = 0.5
    EndGameTaskName = 'endPairingGame'
    xCardInc = 4
    cardsPerRow = 8
    cardsPerCol = 5

    def __init__(self, cr):
        DistributedMinigame.__init__(self, cr)
        self.gameFSM = ClassicFSM.ClassicFSM('DistributedPairingGame', [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.cameraTopView = (17.6, 6.18756, 43.9956, 0, -89, 0)
        self.cameraThreeQuarterView = (14.0, -8.93352, 33.4497, 0, -62.89, 0)
        self.deckSeed = 0
        self.faceUpList = []
        self.localFaceUpList = []
        self.inList = []
        self.inactiveList = []
        self.points = 0
        self.flips = 0
        self.matches = 0
        self.yCardInc = 4
        self.startingPositions = [(0, 0, 0, -45),
         ((self.cardsPerRow - 1) * self.xCardInc,
          (self.cardsPerCol - 1) * self.yCardInc,
          0,
          135),
         ((self.cardsPerRow - 1) * self.xCardInc,
          0,
          0,
          45),
         (0,
          (self.cardsPerCol - 1) * self.yCardInc,
          0,
          -135)]
        self.stageMin = Point2(0, 0)
        self.stageMax = Point2((self.cardsPerRow - 1) * self.xCardInc, (self.cardsPerCol - 1) * self.yCardInc)
        self.gameDuration = PairingGameGlobals.EasiestGameDuration

    def moveCameraToTop(self):
        camera.reparentTo(render)
        p = self.cameraThreeQuarterView
        camera.setPosHpr(p[0], p[1], p[2], p[3], p[4], p[5])

    def getTitle(self):
        return TTLocalizer.PairingGameTitle

    def getInstructions(self):
        if self.numPlayers > 1:
            return TTLocalizer.PairingGameInstructionsMulti
        else:
            return TTLocalizer.PairingGameInstructions

    def getMaxDuration(self):
        return 0

    def load(self):
        self.notify.debug('load')
        DistributedMinigame.load(self)
        self.gameDuration = PairingGameGlobals.calcGameDuration(self.getDifficulty())
        self.gameBoard = loader.loadModel('phase_4/models/minigames/memory_room')
        self.gameBoard.setPosHpr(0.5, 0, 0, 0, 0, 0)
        self.gameBoard.setScale(1.0)
        self.deck = PairingGameGlobals.createDeck(self.deckSeed, self.numPlayers)
        self.notify.debug('%s' % self.deck.cards)
        testCard = self.getDeckOrderIndex(self.cardsPerCol - 1, 0)
        if not testCard > -1:
            self.yCardInc *= 1.25
        self.cards = []
        for index in xrange(len(self.deck.cards)):
            cardValue = self.deck.cards[index]
            oneCard = PairingGameCard.PairingGameCard(cardValue)
            oneCard.load()
            xPos, yPos = self.getCardPos(index)
            oneCard.setPos(xPos, yPos, 0)
            oneCard.reparentTo(render)
            self.notify.debug('%s' % oneCard.getPos())
            self.notify.debug('suit %s rank %s value %s' % (oneCard.suit, oneCard.rank, oneCard.value))
            self.accept('entercardCollision-%d' % oneCard.value, self.enterCard)
            self.accept('exitcardCollision-%d' % oneCard.value, self.exitCard)
            oneCard.turnDown(doInterval=False)
            self.cards.append(oneCard)

        self.bonusTraversal = range(len(self.cards))
        self.bonusGlow = render.attachNewNode('bonusGlow')
        sign = loader.loadModel('phase_4/models/minigames/garden_sign_memory')
        sign.find('**/sign1').removeNode()
        sign.find('**/sign2').removeNode()
        sign.find('**/collision').removeNode()
        sign.setPos(0, 0, 0.05)
        sign.reparentTo(self.bonusGlow)
        self.bonusGlow.setScale(2.5)
        self.pointsFrame = DirectFrame(relief=None, geom=DGG.getDefaultDialogGeom(), geom_color=GlobalDialogColor, geom_scale=(4, 1, 1), pos=(-0.33, 0, 0.9), scale=0.1, text=TTLocalizer.PairingGamePoints, text_align=TextNode.ALeft, text_scale=TTLocalizer.DPGpointsFrame, text_pos=(-1.94, -0.1, 0.0))
        self.pointsLabel = DirectLabel(parent=self.pointsFrame, relief=None, text='0', text_fg=VBase4(0, 0.5, 0, 1), text_align=TextNode.ARight, text_scale=0.7, pos=(1.82, 0, -0.15))
        self.flipsFrame = DirectFrame(relief=None, geom=DGG.getDefaultDialogGeom(), geom_color=GlobalDialogColor, geom_scale=(4, 1, 1), pos=(0.33, 0, 0.9), scale=0.1, text=TTLocalizer.PairingGameFlips, text_align=TextNode.ALeft, text_scale=TTLocalizer.DPGflipsFrame, text_pos=(-1.94, -0.1, 0.0))
        self.flipsLabel = DirectLabel(parent=self.flipsFrame, relief=None, text='0', text_fg=VBase4(0, 1.0, 0, 1), text_align=TextNode.ARight, text_scale=0.7, pos=(1.82, 0, -0.15))
        self.__textGen = TextNode('ringGame')
        self.__textGen.setFont(ToontownGlobals.getSignFont())
        self.__textGen.setAlign(TextNode.ACenter)
        self.sndPerfect = base.loadSfx('phase_4/audio/sfx/MG_pairing_all_matched.ogg')
        self.calcBonusTraversal()
        self.music = base.loadMusic('phase_4/audio/bgm/MG_Pairing.ogg')
        self.matchSfx = base.loadSfx('phase_4/audio/sfx/MG_pairing_match.ogg')
        self.matchWithBonusSfx = base.loadSfx('phase_4/audio/sfx/MG_pairing_match_bonus_both.ogg')
        self.signalSfx = []
        for i in range(4):
            self.signalSfx.append(base.loadSfx('phase_4/audio/sfx/MG_pairing_jumping_signal.ogg'))

        self.bonusMovesSfx = base.loadSfx('phase_4/audio/sfx/MG_pairing_bonus_moves.ogg')
        return

    def unload(self):
        self.notify.debug('unload')
        DistributedMinigame.unload(self)
        self.removeChildGameFSM(self.gameFSM)
        del self.gameFSM
        self.gameBoard.removeNode()
        del self.gameBoard
        for card in self.cards:
            card.unload()
            del card

        self.cards = []
        self.pointsFrame.removeNode()
        del self.pointsFrame
        self.flipsFrame.removeNode()
        del self.flipsFrame
        del self.__textGen
        del self.sndPerfect
        self.bonusGlow.removeNode()
        del self.bonusGlow
        del self.music
        del self.matchSfx
        del self.matchWithBonusSfx
        for i in range(4):
            del self.signalSfx[0]

        self.signalSfx = []
        del self.bonusMovesSfx

    def onstage(self):
        self.notify.debug('onstage')
        DistributedMinigame.onstage(self)
        self.gameBoard.reparentTo(render)
        for card in self.cards:
            card.reparentTo(render)

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

    def offstage(self):
        self.notify.debug('offstage')
        self.gameBoard.hide()
        for card in self.cards:
            card.hide()

        DistributedMinigame.offstage(self)

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

    def setGameReady(self):
        if not self.hasLocalToon:
            return
        self.notify.debug('setGameReady')
        if DistributedMinigame.setGameReady(self):
            return
        for index in xrange(self.numPlayers):
            avId = self.avIdList[index]
            toon = self.getAvatar(avId)
            if toon:
                toon.reparentTo(render)
                self.__placeToon(avId)
                toon.setAnimState('Happy', 1.0)
                toon.startSmooth()
                toon.startLookAround()

    def setGameStart(self, timestamp):
        if not self.hasLocalToon:
            return
        self.notify.debug('setGameStart')
        DistributedMinigame.setGameStart(self, timestamp)
        for avId in self.remoteAvIdList:
            toon = self.getAvatar(avId)
            if toon:
                toon.stopLookAround()

        self.gameFSM.request('play')

    def isInPlayState(self):
        if not self.gameFSM.getCurrentState():
            return False
        if not self.gameFSM.getCurrentState().getName() == 'play':
            return False
        return True

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

    def exitOff(self):
        pass

    def enterPlay(self):
        self.notify.debug('enterPlay')
        base.playMusic(self.music, looping=1, volume=0.9)
        orthoDrive = OrthoDrive(self.TOON_SPEED, maxFrameMove=self.MAX_FRAME_MOVE, customCollisionCallback=self.__doPairingGameCollisions)
        self.orthoWalk = OrthoWalk(orthoDrive, broadcast=not self.isSinglePlayer())
        self.orthoWalk.start()
        self.accept('insert', self.__flipKeyPressed)
        self.accept('delete', self.__flipKeyPressed)
        self.accept('time-control', self.__beginSignal)
        self.accept('time-control-up', self.__endSignal)
        self.bonusGlowIndex = 0
        self.bonusGlowCard = self.bonusTraversal[self.bonusGlowIndex]
        self.startBonusTask()
        self.timer = ToontownTimer.ToontownTimer()
        self.timer.posInTopRightCorner()
        self.timer.setTime(self.gameDuration)
        self.timer.countdown(self.gameDuration, self.timerExpired)
        if base.localAvatar.laffMeter:
            base.localAvatar.laffMeter.stop()

    def exitPlay(self):
        self.music.stop()
        self.orthoWalk.stop()
        self.orthoWalk.destroy()
        del self.orthoWalk
        self.bonusGlow.hide()
        self.stopBonusTask()
        self.timer.stop()
        self.timer.destroy()
        del self.timer
        self.ignoreAll()
        if base.localAvatar.laffMeter:
            base.localAvatar.laffMeter.start()
        if hasattr(self, 'perfectIval'):
            self.perfectIval.pause()
            del self.perfectIval
        taskMgr.remove(self.EndGameTaskName)
        taskMgr.remove('pairGameContinueSignal')

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

    def exitCleanup(self):
        pass

    def __placeToon(self, avId):
        toon = self.getAvatar(avId)
        if self.numPlayers == 1:
            toon.setPos(0, 0, 0)
            toon.setHpr(0, 0, 0)
        else:
            posIndex = self.avIdList.index(avId)
            pos = self.startingPositions[posIndex]
            toon.setPos(pos[0], pos[1], pos[2])
            toon.setHpr(pos[3], 0, 0)

    def __doPairingGameCollisions(self, oldPos, newPos):
        x = bound(newPos[0], self.stageMin[0], self.stageMax[0])
        y = bound(newPos[1], self.stageMin[1], self.stageMax[1])
        newPos.setX(x)
        newPos.setY(y)
        if self.inList:
            newPos.setZ(0.15)
        else:
            newPos.setZ(0.0)
        if not oldPos == newPos:
            taskMgr.remove('pairGameContinueSignal')
        return newPos

    def getDeckOrderFromValue(self, value):
        for index in xrange(len(self.cards)):
            if self.cards[index].value == value:
                return index

        return -1

    def getDeckOrderFromPairingGameCard(self, into):
        try:
            index = self.cards.index(into)
        except ValueError:
            index = -1

        return index

    def enterCard(self, colEntry):
        intoName = colEntry.getIntoNodePath().getName()
        parts = intoName.split('-')
        value = int(parts[1])
        self.notify.debug('entered cardValue %d' % value)
        deckOrder = self.getDeckOrderFromValue(value)
        if deckOrder not in self.inList:
            self.inList.append(deckOrder)

    def exitCard(self, colEntry):
        intoName = colEntry.getIntoNodePath().getName()
        parts = intoName.split('-')
        value = int(parts[1])
        self.notify.debug('exited cardValue %d' % value)
        deckOrder = self.getDeckOrderFromValue(value)
        if deckOrder in self.inList:
            self.inList.remove(deckOrder)

    def handleMatch(self, cardA, cardB, withBonus):
        self.notify.debug('we got a match %d %d' % (cardA, cardB))
        self.matches += 1
        if cardA in self.faceUpList:
            self.faceUpList.remove(cardA)
        if cardB in self.faceUpList:
            self.faceUpList.remove(cardB)
        self.inactiveList.append(cardA)
        self.inactiveList.append(cardB)
        matchIval = Parallel()
        for card in [cardA, cardB]:
            self.cards[card].setTransparency(1)
            cardSeq = Sequence(LerpColorScaleInterval(self.cards[card], duration=1, colorScale=Vec4(1.0, 1.0, 1.0, 0.0)), Func(self.cards[card].hide))
            matchIval.append(cardSeq)

        if withBonus:
            matchIval.append(SoundInterval(self.matchWithBonusSfx, node=self.cards[card], listenerNode=base.localAvatar, cutOff=240))
        else:
            matchIval.append(SoundInterval(self.matchSfx, node=self.cards[card], listenerNode=base.localAvatar, cutOff=240))
        matchIval.start()
        if len(self.inactiveList) == len(self.cards):
            self.sendUpdate('reportDone')

    def turnUpCard(self, deckOrder):
        self.cards[deckOrder].turnUp()
        self.faceUpList.append(deckOrder)

    def turnDownCard(self, deckOrder):
        self.cards[deckOrder].turnDown()
        if deckOrder in self.faceUpList:
            self.faceUpList.remove(deckOrder)

    def __flipKeyPressed(self):
        if self.inList:
            shortestDistance = 10000
            cardToFlip = -1
            for deckOrder in self.inList:
                dist = base.localAvatar.getDistance(self.cards[deckOrder])
                if dist < shortestDistance:
                    shortestDistance = dist
                    cardToFlip = deckOrder

            deckOrderIndex = cardToFlip
            card = self.cards[deckOrderIndex]
            if card.isFaceDown() and deckOrderIndex not in self.inactiveList:
                self.sendUpdate('openCardRequest', [deckOrderIndex, self.bonusGlowCard])
            elif card.isFaceUp() and deckOrderIndex in self.faceUpList:
                pass

    def moveBonusGlowTask(self, task):
        if len(self.cards) == 0:
            return Task.done
        curT = self.getCurrentGameTime()
        intTime = int(curT / self.bonusGlowTime)
        newIndex = intTime % len(self.cards)
        if not newIndex == self.bonusGlowIndex:
            self.bonusGlowIndex = newIndex
            self.bonusGlowCard = self.bonusTraversal[self.bonusGlowIndex]
            card = self.cards[self.bonusGlowCard]
            self.bonusGlow.setPos(card.getPos())
            base.playSfx(self.bonusMovesSfx, node=card, volume=0.25)
        return Task.cont

    def timerExpired(self):
        self.sendUpdate('reportDone')

    def setDeckSeed(self, deckSeed):
        if not self.hasLocalToon:
            return
        self.deckSeed = deckSeed

    def updateFlipText(self):
        self.flipsLabel['text'] = str(self.flips)
        lowFlipModifier = PairingGameGlobals.calcLowFlipModifier(self.matches, self.flips)
        red = 1.0 - lowFlipModifier
        green = lowFlipModifier
        self.flipsLabel['text_fg'] = Vec4(red, green, 0, 1.0)

    def openCardResult(self, cardToTurnUp, avId, matchingCard, points, cardsToTurnDown):
        if not self.hasLocalToon:
            return
        if not self.isInPlayState():
            return
        if avId == base.localAvatar.doId:
            self.localFaceUpList.append(cardToTurnUp)
        self.turnUpCard(cardToTurnUp)
        gotBonus = False
        if points - self.points > 1:
            gotBonus = True
        if matchingCard > -1:
            self.handleMatch(cardToTurnUp, matchingCard, gotBonus)
        self.flips += 1
        self.updateFlipText()
        self.points = points
        self.pointsLabel['text'] = str(self.points)
        for card in cardsToTurnDown:
            self.turnDownCard(card)

    def startBonusTask(self):
        taskMgr.add(self.moveBonusGlowTask, self.taskName('moveBonusGlowTask'))

    def stopBonusTask(self):
        taskMgr.remove(self.taskName('moveBonusGlowTask'))

    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 PairingGameGlobals.EndlessGame:
                self.gameOver()
            return Task.done

        self.timer.hide()
        self.bonusGlow.hide()
        if len(self.inactiveList) == len(self.cards):
            self.notify.debug('perfect game!')
            perfectTextSubnode = hidden.attachNewNode(self.__genText(TTLocalizer.PairingGamePerfect))
            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 __genText(self, text):
        self.__textGen.setText(text)
        return self.__textGen.generate()

    def b_setSignaling(self, avId):
        self.setSignaling(avId)
        self.sendUpdate('setSignaling', [self.localAvId])

    def setSignaling(self, avId):
        if not self.hasLocalToon:
            return
        avIndex = self.avIdList.index(avId)
        av = base.cr.doId2do.get(avId)
        if av and avIndex >= 0 and hasattr(self, 'signalSfx') and self.signalSfx:
            base.playSfx(self.signalSfx[avIndex], node=av)

    def __beginSignal(self, mouseParam):
        self.notify.debug('beginSignal')
        base.localAvatar.b_setEmoteState(1, 1.0)
        self.b_setSignaling(self.localAvId)
        taskMgr.doMethodLater(1.67, self.__continueSignal, 'pairGameContinueSignal')

    def __endSignal(self, mouseParam):
        self.notify.debug('endSignal')
        base.localAvatar.b_setEmoteState(-1, 1.0)
        taskMgr.remove('pairGameContinueSignal')

    def __continueSignal(self, task):
        base.localAvatar.b_setEmoteState(1, 1.0)
        self.b_setSignaling(self.localAvId)
        taskMgr.doMethodLater(1.67, self.__continueSignal, 'pairGameContinueSignal')

    def getCardPos(self, deckOrderIndex):
        col = deckOrderIndex % self.cardsPerRow
        row = deckOrderIndex / self.cardsPerRow
        x = col * self.xCardInc
        y = row * self.yCardInc
        return (x, y)

    def getDeckOrderIndex(self, row, col):
        retval = row * self.cardsPerRow
        retval += col
        if retval >= len(self.deck.cards):
            retval = -1
        return retval

    def calcBonusTraversal(self):
        self.bonusTraversal = []
        halfRow = self.cardsPerRow / 2
        if self.cardsPerRow % 2:
            halfRow += 1
        for i in xrange(halfRow):
            for j in xrange(2):
                col = i + j * halfRow
                for row in xrange(self.cardsPerCol):
                    card = self.getDeckOrderIndex(row, col)
                    if card > -1:
                        self.bonusTraversal.append(card)
class DistributedMazeGame(DistributedMinigame):
    notify = directNotify.newCategory('DistributedMazeGame')
    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, [])], 'off', 'cleanup')
        self.addChildGameFSM(self.gameFSM)
        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.0
        self.TOON_Z = 0
        self.MinSuitSpeedRange = [0.8 * self.TOON_SPEED, 0.6 * self.TOON_SPEED]
        self.MaxSuitSpeedRange = [1.1 * 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.__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(-90)
        self.music = base.loader.loadMusic('phase_4/audio/bgm/MG_toontag.ogg')
        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 = 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()
        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.__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.loader.loadSfx('phase_4/audio/sfx/MG_Tag_C.ogg')
            self.sndTable['falling'][i] = base.loader.loadSfx('phase_4/audio/sfx/MG_cannon_whizz.ogg')

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

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

        self.scores = [0] * self.numPlayers
        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=(0.84, 0, 0.5 - 0.28 * self.numPlayers + 0.05), barColor=(0, 0.7, 0, 1))
        self.goalBar.setBin('unsorted', 0)
        self.goalBar.hide()
        self.introTrack = self.getIntroTrack()
        self.introTrack.start()
        return

    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.__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
        base.setCellsActive(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()
        DistributedMinigame.offstage(self)

    def __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):
        if not self.hasLocalToon:
            return
        self.notify.debug('setGameReady')
        if DistributedMinigame.setGameReady(self):
            return
        for avId in self.remoteAvIdList:
            toon = self.getAvatar(avId)
            if toon:
                toon.reparentTo(render)
                self.__placeToon(avId)
                toon.setAnimState('Happy', 1.0)
                toon.startSmooth()
                toon.startLookAround()

    def setGameStart(self, timestamp):
        if not self.hasLocalToon:
            return
        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.reparentTo(base.a2dTopRight)
            scorePanel.setPos(-0.213, 0.0, -0.5 - 0.28 * i)
            self.scorePanels.append(scorePanel)

        self.goalBar.show()
        self.goalBar['value'] = 0.0
        base.setCellsActive(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()
        self.accept(MazeSuit.COLLISION_EVENT_NAME, self.__hitBySuit)
        self.accept(self.TREASURE_GRAB_EVENT_NAME, self.__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.__resetClock)
        base.playMusic(self.music, looping=0, volume=0.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
        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):
        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 not self.hasLocalToon:
            return
        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 __hitBySuit(self, suitNum):
        self.notify.debug('hitBySuit')
        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)]
        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.dropShadow.copyTo(parentNode)
        dropShadow.setScale(toon.dropShadow.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.3)
            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)
            toon.dropShadow.hide()

        def postFunc(self = self, avId = avId, oldGeomNodeZ = oldGeomNodeZ, dropShadow = dropShadow, parentNode = parentNode):
            if avId == self.localAvId:
                base.localAvatar.setPos(endPos)
                if hasattr(self, 'orthoWalk'):
                    if self.gameFSM.getCurrentState().getName() == 'play':
                        self.orthoWalk.start()
            dropShadow.removeNode()
            del dropShadow
            toon.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):
        if not self.hasLocalToon:
            return
        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):
        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 __spawnCameraTask(self):
        self.notify.debug('spawnCameraTask')
        camera.lookAt(base.localAvatar)
        taskMgr.remove(self.CAMERA_TASK)
        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):
        self.camParent.setHpr(render, 0, 0, 0)
        return Task.cont

    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()

        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):
        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, -.6), blendType='easeInOut'), LerpScaleInterval(self.goalBar, lerpDur, Vec3(self.goalBar.getScale()) * 2.0, blendType='easeInOut')))
        tY = 0.6
        bY = -.05
        lX = -.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]
            panel.wrtReparentTo(aspect2d)
            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()

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

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

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

    def exitCleanup(self):
        pass

    def getIntroTrack(self):
        self.__cameraTask(None)
        origCamParent = camera.getParent()
        origCamPos = camera.getPos()
        origCamHpr = camera.getHpr()
        iCamParent = base.localAvatar.attachNewNode('iCamParent')
        iCamParent.setH(180)
        camera.reparentTo(iCamParent)
        toonHeight = base.localAvatar.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(PythonUtil.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')))
        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))
Beispiel #9
0
class DistributedPairingGame(DistributedMinigame):
    TOON_SPEED = 11
    MAX_FRAME_MOVE = 1
    MAX_FACE_UP_CARDS = 2
    notify = directNotify.newCategory('DistributedPairingGame')
    bonusGlowTime = 0.5
    EndGameTaskName = 'endPairingGame'
    xCardInc = 4
    cardsPerRow = 8
    cardsPerCol = 5

    def __init__(self, cr):
        DistributedMinigame.__init__(self, cr)
        self.gameFSM = ClassicFSM.ClassicFSM('DistributedPairingGame', [
            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.cameraTopView = (17.6, 6.18756, 43.9956, 0, -89, 0)
        self.cameraThreeQuarterView = (14.0, -8.93352, 33.4497, 0, -62.89, 0)
        self.deckSeed = 0
        self.faceUpList = []
        self.localFaceUpList = []
        self.inList = []
        self.inactiveList = []
        self.points = 0
        self.flips = 0
        self.matches = 0
        self.yCardInc = 4
        self.startingPositions = [
            (0, 0, 0, -45),
            ((self.cardsPerRow - 1) * self.xCardInc,
             (self.cardsPerCol - 1) * self.yCardInc, 0, 135),
            ((self.cardsPerRow - 1) * self.xCardInc, 0, 0, 45),
            (0, (self.cardsPerCol - 1) * self.yCardInc, 0, -135)
        ]
        self.stageMin = Point2(0, 0)
        self.stageMax = Point2((self.cardsPerRow - 1) * self.xCardInc,
                               (self.cardsPerCol - 1) * self.yCardInc)
        self.gameDuration = PairingGameGlobals.EasiestGameDuration

    def moveCameraToTop(self):
        camera.reparentTo(render)
        p = self.cameraThreeQuarterView
        camera.setPosHpr(p[0], p[1], p[2], p[3], p[4], p[5])

    def getTitle(self):
        return TTLocalizer.PairingGameTitle

    def getInstructions(self):
        if self.numPlayers > 1:
            return TTLocalizer.PairingGameInstructionsMulti
        else:
            return TTLocalizer.PairingGameInstructions

    def getMaxDuration(self):
        return 0

    def load(self):
        self.notify.debug('load')
        DistributedMinigame.load(self)
        self.gameDuration = PairingGameGlobals.calcGameDuration(
            self.getDifficulty())
        self.gameBoard = loader.loadModel(
            'phase_4/models/minigames/memory_room')
        self.gameBoard.setPosHpr(0.5, 0, 0, 0, 0, 0)
        self.gameBoard.setScale(1.0)
        self.deck = PairingGameGlobals.createDeck(self.deckSeed,
                                                  self.numPlayers)
        self.notify.debug('%s' % self.deck.cards)
        testCard = self.getDeckOrderIndex(self.cardsPerCol - 1, 0)
        if not testCard > -1:
            self.yCardInc *= 1.25
        self.cards = []
        for index in xrange(len(self.deck.cards)):
            cardValue = self.deck.cards[index]
            oneCard = PairingGameCard.PairingGameCard(cardValue)
            oneCard.load()
            xPos, yPos = self.getCardPos(index)
            oneCard.setPos(xPos, yPos, 0)
            oneCard.reparentTo(render)
            self.notify.debug('%s' % oneCard.getPos())
            self.notify.debug('suit %s rank %s value %s' %
                              (oneCard.suit, oneCard.rank, oneCard.value))
            self.accept('entercardCollision-%d' % oneCard.value,
                        self.enterCard)
            self.accept('exitcardCollision-%d' % oneCard.value, self.exitCard)
            oneCard.turnDown(doInterval=False)
            self.cards.append(oneCard)

        self.bonusTraversal = range(len(self.cards))
        self.bonusGlow = render.attachNewNode('bonusGlow')
        sign = loader.loadModel('phase_4/models/minigames/garden_sign_memory')
        sign.find('**/sign1').removeNode()
        sign.find('**/sign2').removeNode()
        sign.find('**/collision').removeNode()
        sign.setPos(0, 0, 0.05)
        sign.reparentTo(self.bonusGlow)
        self.bonusGlow.setScale(2.5)
        self.pointsFrame = DirectFrame(relief=None,
                                       geom=DGG.getDefaultDialogGeom(),
                                       geom_color=GlobalDialogColor,
                                       geom_scale=(4, 1, 1),
                                       pos=(-0.33, 0, 0.9),
                                       scale=0.1,
                                       text=TTLocalizer.PairingGamePoints,
                                       text_align=TextNode.ALeft,
                                       text_scale=TTLocalizer.DPGpointsFrame,
                                       text_pos=(-1.94, -0.1, 0.0))
        self.pointsLabel = DirectLabel(parent=self.pointsFrame,
                                       relief=None,
                                       text='0',
                                       text_fg=VBase4(0, 0.5, 0, 1),
                                       text_align=TextNode.ARight,
                                       text_scale=0.7,
                                       pos=(1.82, 0, -0.15))
        self.flipsFrame = DirectFrame(relief=None,
                                      geom=DGG.getDefaultDialogGeom(),
                                      geom_color=GlobalDialogColor,
                                      geom_scale=(4, 1, 1),
                                      pos=(0.33, 0, 0.9),
                                      scale=0.1,
                                      text=TTLocalizer.PairingGameFlips,
                                      text_align=TextNode.ALeft,
                                      text_scale=TTLocalizer.DPGflipsFrame,
                                      text_pos=(-1.94, -0.1, 0.0))
        self.flipsLabel = DirectLabel(parent=self.flipsFrame,
                                      relief=None,
                                      text='0',
                                      text_fg=VBase4(0, 1.0, 0, 1),
                                      text_align=TextNode.ARight,
                                      text_scale=0.7,
                                      pos=(1.82, 0, -0.15))
        self.__textGen = TextNode('ringGame')
        self.__textGen.setFont(ToontownGlobals.getSignFont())
        self.__textGen.setAlign(TextNode.ACenter)
        self.sndPerfect = base.loadSfx(
            'phase_4/audio/sfx/MG_pairing_all_matched.ogg')
        self.calcBonusTraversal()
        self.music = base.loadMusic('phase_4/audio/bgm/MG_Pairing.ogg')
        self.matchSfx = base.loadSfx('phase_4/audio/sfx/MG_pairing_match.ogg')
        self.matchWithBonusSfx = base.loadSfx(
            'phase_4/audio/sfx/MG_pairing_match_bonus_both.ogg')
        self.signalSfx = []
        for i in range(4):
            self.signalSfx.append(
                base.loadSfx(
                    'phase_4/audio/sfx/MG_pairing_jumping_signal.ogg'))

        self.bonusMovesSfx = base.loadSfx(
            'phase_4/audio/sfx/MG_pairing_bonus_moves.ogg')
        return

    def unload(self):
        self.notify.debug('unload')
        DistributedMinigame.unload(self)
        self.removeChildGameFSM(self.gameFSM)
        del self.gameFSM
        self.gameBoard.removeNode()
        del self.gameBoard
        for card in self.cards:
            card.unload()
            del card

        self.cards = []
        self.pointsFrame.removeNode()
        del self.pointsFrame
        self.flipsFrame.removeNode()
        del self.flipsFrame
        del self.__textGen
        del self.sndPerfect
        self.bonusGlow.removeNode()
        del self.bonusGlow
        del self.music
        del self.matchSfx
        del self.matchWithBonusSfx
        for i in range(4):
            del self.signalSfx[0]

        self.signalSfx = []
        del self.bonusMovesSfx

    def onstage(self):
        self.notify.debug('onstage')
        DistributedMinigame.onstage(self)
        self.gameBoard.reparentTo(render)
        for card in self.cards:
            card.reparentTo(render)

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

    def offstage(self):
        self.notify.debug('offstage')
        self.gameBoard.hide()
        for card in self.cards:
            card.hide()

        DistributedMinigame.offstage(self)

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

    def setGameReady(self):
        if not self.hasLocalToon:
            return
        self.notify.debug('setGameReady')
        if DistributedMinigame.setGameReady(self):
            return
        for index in xrange(self.numPlayers):
            avId = self.avIdList[index]
            toon = self.getAvatar(avId)
            if toon:
                toon.reparentTo(render)
                self.__placeToon(avId)
                toon.setAnimState('Happy', 1.0)
                toon.startSmooth()
                toon.startLookAround()

    def setGameStart(self, timestamp):
        if not self.hasLocalToon:
            return
        self.notify.debug('setGameStart')
        DistributedMinigame.setGameStart(self, timestamp)
        for avId in self.remoteAvIdList:
            toon = self.getAvatar(avId)
            if toon:
                toon.stopLookAround()

        self.gameFSM.request('play')

    def isInPlayState(self):
        if not self.gameFSM.getCurrentState():
            return False
        if not self.gameFSM.getCurrentState().getName() == 'play':
            return False
        return True

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

    def exitOff(self):
        pass

    def enterPlay(self):
        self.notify.debug('enterPlay')
        base.playMusic(self.music, looping=1, volume=0.9)
        orthoDrive = OrthoDrive(
            self.TOON_SPEED,
            maxFrameMove=self.MAX_FRAME_MOVE,
            customCollisionCallback=self.__doPairingGameCollisions)
        self.orthoWalk = OrthoWalk(orthoDrive,
                                   broadcast=not self.isSinglePlayer())
        self.orthoWalk.start()
        self.accept('insert', self.__flipKeyPressed)
        self.accept('delete', self.__flipKeyPressed)
        self.accept('time-control', self.__beginSignal)
        self.accept('time-control-up', self.__endSignal)
        self.bonusGlowIndex = 0
        self.bonusGlowCard = self.bonusTraversal[self.bonusGlowIndex]
        self.startBonusTask()
        self.timer = ToontownTimer.ToontownTimer()
        self.timer.posInTopRightCorner()
        self.timer.setTime(self.gameDuration)
        self.timer.countdown(self.gameDuration, self.timerExpired)
        if base.localAvatar.laffMeter:
            base.localAvatar.laffMeter.stop()

    def exitPlay(self):
        self.music.stop()
        self.orthoWalk.stop()
        self.orthoWalk.destroy()
        del self.orthoWalk
        self.bonusGlow.hide()
        self.stopBonusTask()
        self.timer.stop()
        self.timer.destroy()
        del self.timer
        self.ignoreAll()
        if base.localAvatar.laffMeter:
            base.localAvatar.laffMeter.start()
        if hasattr(self, 'perfectIval'):
            self.perfectIval.pause()
            del self.perfectIval
        taskMgr.remove(self.EndGameTaskName)
        taskMgr.remove('pairGameContinueSignal')

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

    def exitCleanup(self):
        pass

    def __placeToon(self, avId):
        toon = self.getAvatar(avId)
        if self.numPlayers == 1:
            toon.setPos(0, 0, 0)
            toon.setHpr(0, 0, 0)
        else:
            posIndex = self.avIdList.index(avId)
            pos = self.startingPositions[posIndex]
            toon.setPos(pos[0], pos[1], pos[2])
            toon.setHpr(pos[3], 0, 0)

    def __doPairingGameCollisions(self, oldPos, newPos):
        x = bound(newPos[0], self.stageMin[0], self.stageMax[0])
        y = bound(newPos[1], self.stageMin[1], self.stageMax[1])
        newPos.setX(x)
        newPos.setY(y)
        if self.inList:
            newPos.setZ(0.15)
        else:
            newPos.setZ(0.0)
        if not oldPos == newPos:
            taskMgr.remove('pairGameContinueSignal')
        return newPos

    def getDeckOrderFromValue(self, value):
        for index in xrange(len(self.cards)):
            if self.cards[index].value == value:
                return index

        return -1

    def getDeckOrderFromPairingGameCard(self, into):
        try:
            index = self.cards.index(into)
        except ValueError:
            index = -1

        return index

    def enterCard(self, colEntry):
        intoName = colEntry.getIntoNodePath().getName()
        parts = intoName.split('-')
        value = int(parts[1])
        self.notify.debug('entered cardValue %d' % value)
        deckOrder = self.getDeckOrderFromValue(value)
        if deckOrder not in self.inList:
            self.inList.append(deckOrder)

    def exitCard(self, colEntry):
        intoName = colEntry.getIntoNodePath().getName()
        parts = intoName.split('-')
        value = int(parts[1])
        self.notify.debug('exited cardValue %d' % value)
        deckOrder = self.getDeckOrderFromValue(value)
        if deckOrder in self.inList:
            self.inList.remove(deckOrder)

    def handleMatch(self, cardA, cardB, withBonus):
        self.notify.debug('we got a match %d %d' % (cardA, cardB))
        self.matches += 1
        if cardA in self.faceUpList:
            self.faceUpList.remove(cardA)
        if cardB in self.faceUpList:
            self.faceUpList.remove(cardB)
        self.inactiveList.append(cardA)
        self.inactiveList.append(cardB)
        matchIval = Parallel()
        for card in [cardA, cardB]:
            self.cards[card].setTransparency(1)
            cardSeq = Sequence(
                LerpColorScaleInterval(self.cards[card],
                                       duration=1,
                                       colorScale=Vec4(1.0, 1.0, 1.0, 0.0)),
                Func(self.cards[card].hide))
            matchIval.append(cardSeq)

        if withBonus:
            matchIval.append(
                SoundInterval(self.matchWithBonusSfx,
                              node=self.cards[card],
                              listenerNode=base.localAvatar,
                              cutOff=240))
        else:
            matchIval.append(
                SoundInterval(self.matchSfx,
                              node=self.cards[card],
                              listenerNode=base.localAvatar,
                              cutOff=240))
        matchIval.start()
        if len(self.inactiveList) == len(self.cards):
            self.sendUpdate('reportDone')

    def turnUpCard(self, deckOrder):
        self.cards[deckOrder].turnUp()
        self.faceUpList.append(deckOrder)

    def turnDownCard(self, deckOrder):
        self.cards[deckOrder].turnDown()
        if deckOrder in self.faceUpList:
            self.faceUpList.remove(deckOrder)

    def __flipKeyPressed(self):
        if self.inList:
            shortestDistance = 10000
            cardToFlip = -1
            for deckOrder in self.inList:
                dist = base.localAvatar.getDistance(self.cards[deckOrder])
                if dist < shortestDistance:
                    shortestDistance = dist
                    cardToFlip = deckOrder

            deckOrderIndex = cardToFlip
            card = self.cards[deckOrderIndex]
            if card.isFaceDown() and deckOrderIndex not in self.inactiveList:
                self.sendUpdate('openCardRequest',
                                [deckOrderIndex, self.bonusGlowCard])
            elif card.isFaceUp() and deckOrderIndex in self.faceUpList:
                pass

    def moveBonusGlowTask(self, task):
        if len(self.cards) == 0:
            return Task.done
        curT = self.getCurrentGameTime()
        intTime = int(curT / self.bonusGlowTime)
        newIndex = intTime % len(self.cards)
        if not newIndex == self.bonusGlowIndex:
            self.bonusGlowIndex = newIndex
            self.bonusGlowCard = self.bonusTraversal[self.bonusGlowIndex]
            card = self.cards[self.bonusGlowCard]
            self.bonusGlow.setPos(card.getPos())
            base.playSfx(self.bonusMovesSfx, node=card, volume=0.25)
        return Task.cont

    def timerExpired(self):
        self.sendUpdate('reportDone')

    def setDeckSeed(self, deckSeed):
        if not self.hasLocalToon:
            return
        self.deckSeed = deckSeed

    def updateFlipText(self):
        self.flipsLabel['text'] = str(self.flips)
        lowFlipModifier = PairingGameGlobals.calcLowFlipModifier(
            self.matches, self.flips)
        red = 1.0 - lowFlipModifier
        green = lowFlipModifier
        self.flipsLabel['text_fg'] = Vec4(red, green, 0, 1.0)

    def openCardResult(self, cardToTurnUp, avId, matchingCard, points,
                       cardsToTurnDown):
        if not self.hasLocalToon:
            return
        if not self.isInPlayState():
            return
        if avId == base.localAvatar.doId:
            self.localFaceUpList.append(cardToTurnUp)
        self.turnUpCard(cardToTurnUp)
        gotBonus = False
        if points - self.points > 1:
            gotBonus = True
        if matchingCard > -1:
            self.handleMatch(cardToTurnUp, matchingCard, gotBonus)
        self.flips += 1
        self.updateFlipText()
        self.points = points
        self.pointsLabel['text'] = str(self.points)
        for card in cardsToTurnDown:
            self.turnDownCard(card)

    def startBonusTask(self):
        taskMgr.add(self.moveBonusGlowTask, self.taskName('moveBonusGlowTask'))

    def stopBonusTask(self):
        taskMgr.remove(self.taskName('moveBonusGlowTask'))

    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 PairingGameGlobals.EndlessGame:
                self.gameOver()
            return Task.done

        self.timer.hide()
        self.bonusGlow.hide()
        if len(self.inactiveList) == len(self.cards):
            self.notify.debug('perfect game!')
            perfectTextSubnode = hidden.attachNewNode(
                self.__genText(TTLocalizer.PairingGamePerfect))
            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 __genText(self, text):
        self.__textGen.setText(text)
        return self.__textGen.generate()

    def b_setSignaling(self, avId):
        self.setSignaling(avId)
        self.sendUpdate('setSignaling', [self.localAvId])

    def setSignaling(self, avId):
        if not self.hasLocalToon:
            return
        avIndex = self.avIdList.index(avId)
        av = base.cr.doId2do.get(avId)
        if av and avIndex >= 0 and hasattr(self,
                                           'signalSfx') and self.signalSfx:
            base.playSfx(self.signalSfx[avIndex], node=av)

    def __beginSignal(self, mouseParam):
        self.notify.debug('beginSignal')
        base.localAvatar.b_setEmoteState(1, 1.0)
        self.b_setSignaling(self.localAvId)
        taskMgr.doMethodLater(1.67, self.__continueSignal,
                              'pairGameContinueSignal')

    def __endSignal(self, mouseParam):
        self.notify.debug('endSignal')
        base.localAvatar.b_setEmoteState(-1, 1.0)
        taskMgr.remove('pairGameContinueSignal')

    def __continueSignal(self, task):
        base.localAvatar.b_setEmoteState(1, 1.0)
        self.b_setSignaling(self.localAvId)
        taskMgr.doMethodLater(1.67, self.__continueSignal,
                              'pairGameContinueSignal')

    def getCardPos(self, deckOrderIndex):
        col = deckOrderIndex % self.cardsPerRow
        row = deckOrderIndex / self.cardsPerRow
        x = col * self.xCardInc
        y = row * self.yCardInc
        return (x, y)

    def getDeckOrderIndex(self, row, col):
        retval = row * self.cardsPerRow
        retval += col
        if retval >= len(self.deck.cards):
            retval = -1
        return retval

    def calcBonusTraversal(self):
        self.bonusTraversal = []
        halfRow = self.cardsPerRow / 2
        if self.cardsPerRow % 2:
            halfRow += 1
        for i in xrange(halfRow):
            for j in xrange(2):
                col = i + j * halfRow
                for row in xrange(self.cardsPerCol):
                    card = self.getDeckOrderIndex(row, col)
                    if card > -1:
                        self.bonusTraversal.append(card)
class DistributedMazeGame(DistributedMinigame):
    notify = directNotify.newCategory('DistributedMazeGame')
    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, [])], 'off', 'cleanup')
        self.addChildGameFSM(self.gameFSM)
        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.0
        self.TOON_Z = 0
        self.MinSuitSpeedRange = [0.8 * self.TOON_SPEED, 0.6 * self.TOON_SPEED]
        self.MaxSuitSpeedRange = [1.1 * 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.__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(-90)
        self.music = base.loadMusic('phase_4/audio/bgm/MG_toontag.ogg')
        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 = 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()
        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.__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.ogg')
            self.sndTable['falling'][i] = base.loadSfx('phase_4/audio/sfx/MG_cannon_whizz.ogg')

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

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

        self.scores = [0] * self.numPlayers
        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=(0.84, 0, 0.5 - 0.28 * self.numPlayers + 0.05), barColor=(0, 0.7, 0, 1))
        self.goalBar.setBin('unsorted', 0)
        self.goalBar.hide()
        self.introTrack = self.getIntroTrack()
        self.introTrack.start()
        return

    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.__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
        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()
        DistributedMinigame.offstage(self)

    def __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):
        if not self.hasLocalToon:
            return
        self.notify.debug('setGameReady')
        if DistributedMinigame.setGameReady(self):
            return
        for avId in self.remoteAvIdList:
            toon = self.getAvatar(avId)
            if toon:
                toon.reparentTo(render)
                self.__placeToon(avId)
                toon.setAnimState('Happy', 1.0)
                toon.startSmooth()
                toon.startLookAround()

    def setGameStart(self, timestamp):
        if not self.hasLocalToon:
            return
        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.reparentTo(base.a2dTopRight)
            scorePanel.setPos(-0.213, 0.0, -0.5 - 0.28 * i)
            self.scorePanels.append(scorePanel)

        self.goalBar.show()
        self.goalBar['value'] = 0.0
        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()
        self.accept(MazeSuit.COLLISION_EVENT_NAME, self.__hitBySuit)
        self.accept(self.TREASURE_GRAB_EVENT_NAME, self.__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.__resetClock)
        base.playMusic(self.music, looping=0, volume=0.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
        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):
        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 not self.hasLocalToon:
            return
        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 __hitBySuit(self, suitNum):
        self.notify.debug('hitBySuit')
        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)]
        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.dropShadow.copyTo(parentNode)
        dropShadow.setScale(toon.dropShadow.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.3)
            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)
            toon.dropShadow.hide()

        def postFunc(self = self, avId = avId, oldGeomNodeZ = oldGeomNodeZ, dropShadow = dropShadow, parentNode = parentNode):
            if avId == self.localAvId:
                base.localAvatar.setPos(endPos)
                if hasattr(self, 'orthoWalk'):
                    if self.gameFSM.getCurrentState().getName() == 'play':
                        self.orthoWalk.start()
            dropShadow.removeNode()
            del dropShadow
            toon.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):
        if not self.hasLocalToon:
            return
        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):
        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 __spawnCameraTask(self):
        self.notify.debug('spawnCameraTask')
        camera.lookAt(base.localAvatar)
        taskMgr.remove(self.CAMERA_TASK)
        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):
        self.camParent.setHpr(render, 0, 0, 0)
        return Task.cont

    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()

        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):
        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, -.6), blendType='easeInOut'), LerpScaleInterval(self.goalBar, lerpDur, Vec3(self.goalBar.getScale()) * 2.0, blendType='easeInOut')))
        tY = 0.6
        bY = -.05
        lX = -.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]
            panel.wrtReparentTo(aspect2d)
            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()

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

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

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

    def exitCleanup(self):
        pass

    def getIntroTrack(self):
        self.__cameraTask(None)
        origCamParent = camera.getParent()
        origCamPos = camera.getPos()
        origCamHpr = camera.getHpr()
        iCamParent = base.localAvatar.attachNewNode('iCamParent')
        iCamParent.setH(180)
        camera.reparentTo(iCamParent)
        toonHeight = base.localAvatar.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(PythonUtil.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')))
        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))