from direct.directnotify import DirectNotifyGlobal
from panda3d.core import *
from otp.nametag.NametagFloat3d import NametagFloat3d
from otp.nametag.Nametag import Nametag
from toontown.toonbase.ToonBaseGlobal import *
from DistributedMinigame import *
from direct.distributed.ClockDelta import *
from direct.interval.IntervalGlobal import *
from direct.fsm import ClassicFSM, State
from direct.fsm import State
from toontown.toonbase import ToontownGlobals
from toontown.toonbase import ToontownTimer
from direct.task.Task import Task
import Trajectory
import math
from toontown.toon import ToonHead
from toontown.effects import Splash
from toontown.effects import DustCloud
import CannonGameGlobals
from direct.gui.DirectGui import *
from toontown.toonbase import TTLocalizer
LAND_TIME = 2
WORLD_SCALE = 2.0
GROUND_SCALE = 1.4 * WORLD_SCALE
CANNON_SCALE = 1.0
FAR_PLANE_DIST = 600 * WORLD_SCALE
CANNON_Y = -int(CannonGameGlobals.TowerYRange / 2 * 1.3)
CANNON_X_SPACING = 12
CANNON_Z = 20
CANNON_MOVE_UPDATE_FREQ = 0.5
CAMERA_PULLBACK_MIN = 20
CAMERA_PULLBACK_MAX = 40
MAX_LOOKAT_OFFSET = 80
TOON_TOWER_THRESHOLD = 150
SHADOW_Z_OFFSET = 0.5
TOWER_HEIGHT = 43.85
TOWER_RADIUS = 10.5
BUCKET_HEIGHT = 36
TOWER_Y_RANGE = CannonGameGlobals.TowerYRange
TOWER_X_RANGE = int(TOWER_Y_RANGE / 2.0)
INITIAL_VELOCITY = 94.0
WHISTLE_SPEED = INITIAL_VELOCITY * 0.55

class DistributedCannonGame(DistributedMinigame):
    notify = DirectNotifyGlobal.directNotify.newCategory('DistributedMinigame')
    font = ToontownGlobals.getToonFont()
    LOCAL_CANNON_MOVE_TASK = 'localCannonMoveTask'
    REWARD_COUNTDOWN_TASK = 'cannonGameRewardCountdown'
    HIT_GROUND = 0
    HIT_TOWER = 1
    HIT_WATER = 2
    FIRE_KEY = 'control'
    UP_KEY = 'arrow_up'
    DOWN_KEY = 'arrow_down'
    LEFT_KEY = 'arrow_left'
    RIGHT_KEY = 'arrow_right'
    INTRO_TASK_NAME = 'CannonGameIntro'
    INTRO_TASK_NAME_CAMERA_LERP = 'CannonGameIntroCamera'

    def __init__(self, cr):
        DistributedMinigame.__init__(self, cr)
        self.gameFSM = ClassicFSM.ClassicFSM('DistributedCannonGame', [State.State('off', self.enterOff, self.exitOff, ['aim']),
         State.State('aim', self.enterAim, self.exitAim, ['shoot', 'waitForToonsToLand', 'cleanup']),
         State.State('shoot', self.enterShoot, self.exitShoot, ['aim', 'waitForToonsToLand', 'cleanup']),
         State.State('waitForToonsToLand', self.enterWaitForToonsToLand, self.exitWaitForToonsToLand, ['cleanup']),
         State.State('cleanup', self.enterCleanup, self.exitCleanup, [])], 'off', 'cleanup')
        self.addChildGameFSM(self.gameFSM)
        self.cannonLocationDict = {}
        self.cannonPositionDict = {}
        self.cannonDict = {}
        self.toonModelDict = {}
        self.dropShadowDict = {}
        self.toonHeadDict = {}
        self.toonScaleDict = {}
        self.toonIntervalDict = {}
        self.leftPressed = 0
        self.rightPressed = 0
        self.upPressed = 0
        self.downPressed = 0
        self.cannonMoving = 0
        self.modelCount = 14

    def getTitle(self):
        return TTLocalizer.CannonGameTitle

    def getInstructions(self):
        return TTLocalizer.CannonGameInstructions

    def getMaxDuration(self):
        return CannonGameGlobals.GameTime

    def load(self):
        self.notify.debug('load')
        DistributedMinigame.load(self)
        self.sky = loader.loadModel('phase_3.5/models/props/TT_sky')
        self.ground = loader.loadModel('phase_4/models/minigames/toon_cannon_gameground')
        self.tower = loader.loadModel('phase_4/models/minigames/toon_cannon_water_tower')
        self.cannon = loader.loadModel('phase_4/models/minigames/toon_cannon')
        self.dropShadow = loader.loadModel('phase_3/models/props/drop_shadow')
        self.hill = loader.loadModel('phase_4/models/minigames/cannon_hill')
        self.sky.setScale(WORLD_SCALE)
        self.ground.setScale(GROUND_SCALE)
        self.cannon.setScale(CANNON_SCALE)
        self.dropShadow.setColor(0, 0, 0, 0.5)
        self.ground.setColor(0.85, 0.85, 0.85, 1.0)
        self.hill.setScale(1, 1, CANNON_Z / 20.0)
        self.dropShadow.setBin('fixed', 0, 1)
        self.splash = Splash.Splash(render)
        self.dustCloud = DustCloud.DustCloud(render)
        purchaseModels = loader.loadModel('phase_4/models/gui/purchase_gui')
        self.jarImage = purchaseModels.find('**/Jar')
        self.jarImage.reparentTo(hidden)
        self.rewardPanel = DirectLabel(parent=hidden, relief=None, pos=(-0.173, 0.0, -0.55), scale=0.65, text='', text_scale=0.2, text_fg=(0.95, 0.95, 0, 1), text_pos=(0, -.13), text_font=ToontownGlobals.getSignFont(), image=self.jarImage)
        self.rewardPanelTitle = DirectLabel(parent=self.rewardPanel, relief=None, pos=(0, 0, 0.06), scale=0.08, text=TTLocalizer.CannonGameReward, text_fg=(0.95, 0.95, 0, 1), text_shadow=(0, 0, 0, 1))
        self.music = base.loadMusic('phase_4/audio/bgm/MG_cannon_game_tug.ogg')
        self.sndCannonMove = base.loadSfx('phase_4/audio/sfx/MG_cannon_adjust.ogg')
        self.sndCannonFire = base.loadSfx('phase_4/audio/sfx/MG_cannon_fire_alt.ogg')
        self.sndHitGround = base.loadSfx('phase_4/audio/sfx/MG_cannon_hit_dirt.ogg')
        self.sndHitTower = base.loadSfx('phase_4/audio/sfx/MG_cannon_hit_tower.ogg')
        self.sndHitWater = base.loadSfx('phase_4/audio/sfx/MG_cannon_splash.ogg')
        self.sndWhizz = base.loadSfx('phase_4/audio/sfx/MG_cannon_whizz.ogg')
        self.sndWin = base.loadSfx('phase_4/audio/sfx/MG_win.ogg')
        self.sndRewardTick = base.loadSfx('phase_3.5/audio/sfx/tick_counter.ogg')
        guiModel = 'phase_4/models/gui/cannon_game_gui'
        cannonGui = loader.loadModel(guiModel)
        self.aimPad = DirectFrame(image=cannonGui.find('**/CannonFire_PAD'), relief=None, pos=(0.7, 0, -0.553333), scale=0.8)
        cannonGui.removeNode()
        self.aimPad.hide()
        self.fireButton = DirectButton(parent=self.aimPad, image=((guiModel, '**/Fire_Btn_UP'), (guiModel, '**/Fire_Btn_DN'), (guiModel, '**/Fire_Btn_RLVR')), relief=None, pos=(0.0115741, 0, 0.00505051), scale=1.0, command=self.__firePressed)
        self.upButton = DirectButton(parent=self.aimPad, image=((guiModel, '**/Cannon_Arrow_UP'), (guiModel, '**/Cannon_Arrow_DN'), (guiModel, '**/Cannon_Arrow_RLVR')), relief=None, pos=(0.0115741, 0, 0.221717))
        self.downButton = DirectButton(parent=self.aimPad, image=((guiModel, '**/Cannon_Arrow_UP'), (guiModel, '**/Cannon_Arrow_DN'), (guiModel, '**/Cannon_Arrow_RLVR')), relief=None, pos=(0.0136112, 0, -0.210101), image_hpr=(0, 0, 180))
        self.leftButton = DirectButton(parent=self.aimPad, image=((guiModel, '**/Cannon_Arrow_UP'), (guiModel, '**/Cannon_Arrow_DN'), (guiModel, '**/Cannon_Arrow_RLVR')), relief=None, pos=(-0.199352, 0, -0.000505269), image_hpr=(0, 0, -90))
        self.rightButton = DirectButton(parent=self.aimPad, image=((guiModel, '**/Cannon_Arrow_UP'), (guiModel, '**/Cannon_Arrow_DN'), (guiModel, '**/Cannon_Arrow_RLVR')), relief=None, pos=(0.219167, 0, -0.00101024), image_hpr=(0, 0, 90))
        self.aimPad.setColor(1, 1, 1, 0.9)

        def bindButton(button, upHandler, downHandler):
            button.bind(DGG.B1PRESS, lambda x, handler = upHandler: handler())
            button.bind(DGG.B1RELEASE, lambda x, handler = downHandler: handler())

        bindButton(self.upButton, self.__upPressed, self.__upReleased)
        bindButton(self.downButton, self.__downPressed, self.__downReleased)
        bindButton(self.leftButton, self.__leftPressed, self.__leftReleased)
        bindButton(self.rightButton, self.__rightPressed, self.__rightReleased)
        self.timer = ToontownTimer.ToontownTimer()
        self.timer.posInTopRightCorner()
        self.timer.hide()
        self.DEBUG_TOWER_RANGE = 0
        self.DEBUG_CANNON_FAR_LEFT = 0
        self.DEBUG_TOWER_NEAR = 1
        self.DEBUG_TOWER_FAR_LEFT = 1
        return

    def unload(self):
        self.notify.debug('unload')
        DistributedMinigame.unload(self)
        self.sky.removeNode()
        del self.sky
        self.ground.removeNode()
        del self.ground
        self.tower.removeNode()
        del self.tower
        self.cannon.removeNode()
        del self.cannon
        del self.dropShadowDict
        self.dropShadow.removeNode()
        del self.dropShadow
        self.splash.destroy()
        del self.splash
        self.dustCloud.destroy()
        del self.dustCloud
        self.hill.removeNode()
        del self.hill
        self.rewardPanel.destroy()
        del self.rewardPanel
        self.jarImage.removeNode()
        del self.jarImage
        del self.music
        del self.sndCannonMove
        del self.sndCannonFire
        del self.sndHitGround
        del self.sndHitTower
        del self.sndHitWater
        del self.sndWhizz
        del self.sndWin
        del self.sndRewardTick
        self.aimPad.destroy()
        del self.aimPad
        del self.fireButton
        del self.upButton
        del self.downButton
        del self.leftButton
        del self.rightButton
        for avId in self.toonHeadDict.keys():
            head = self.toonHeadDict[avId]
            head.stopBlink()
            head.stopLookAroundNow()
            av = self.getAvatar(avId)
            if av:
                av.loop('neutral')
                av.setPlayRate(1.0, 'run')
                av.nametag.removeNametag(head.tag)
            head.delete()

        del self.toonHeadDict
        for model in self.toonModelDict.values():
            model.removeNode()

        del self.toonModelDict
        del self.toonScaleDict
        for interval in self.toonIntervalDict.values():
            interval.finish()

        del self.toonIntervalDict
        for avId in self.avIdList:
            self.cannonDict[avId][0].removeNode()
            del self.cannonDict[avId][0]

        del self.cannonDict
        self.timer.destroy()
        del self.timer
        del self.cannonLocationDict
        self.removeChildGameFSM(self.gameFSM)
        del self.gameFSM

    def onstage(self):
        self.notify.debug('onstage')
        DistributedMinigame.onstage(self)
        self.__createCannons()
        for avId in self.avIdList:
            self.cannonDict[avId][0].reparentTo(render)

        self.towerPos = self.getTowerPosition()
        self.tower.setPos(self.towerPos)
        self.tower.reparentTo(render)
        self.sky.reparentTo(render)
        self.ground.reparentTo(render)
        self.hill.setPosHpr(0, CANNON_Y + 2.33, 0, 0, 0, 0)
        self.hill.reparentTo(render)
        self.splash.reparentTo(render)
        self.dustCloud.reparentTo(render)
        self.__createToonModels(self.localAvId)
        camera.reparentTo(render)
        self.__oldCamFar = base.camLens.getFar()
        base.camLens.setFar(FAR_PLANE_DIST)
        self.__startIntro()
        base.transitions.irisIn(0.4)
        base.playMusic(self.music, looping=1, volume=0.8)

    def offstage(self):
        self.notify.debug('offstage')
        self.sky.reparentTo(hidden)
        self.ground.reparentTo(hidden)
        self.hill.reparentTo(hidden)
        self.tower.reparentTo(hidden)
        for avId in self.avIdList:
            self.cannonDict[avId][0].reparentTo(hidden)
            if avId in self.dropShadowDict:
                self.dropShadowDict[avId].reparentTo(hidden)
            av = self.getAvatar(avId)
            if av:
                av.dropShadow.show()
                av.resetLOD()

        self.splash.reparentTo(hidden)
        self.splash.stop()
        self.dustCloud.reparentTo(hidden)
        self.dustCloud.stop()
        self.__stopIntro()
        base.camLens.setFar(self.__oldCamFar)
        self.timer.reparentTo(hidden)
        self.rewardPanel.reparentTo(hidden)
        DistributedMinigame.offstage(self)

    def getTowerPosition(self):
        yRange = TOWER_Y_RANGE
        yMin = yRange * 0.3
        yMax = yRange
        if self.DEBUG_TOWER_RANGE:
            if self.DEBUG_TOWER_NEAR:
                y = yMin
            else:
                y = yMax
        else:
            y = self.randomNumGen.randint(yMin, yMax)
        xRange = TOWER_X_RANGE
        if self.DEBUG_TOWER_RANGE:
            if self.DEBUG_TOWER_FAR_LEFT:
                x = 0
            else:
                x = xRange
        else:
            x = self.randomNumGen.randint(0, xRange)
        x = x - int(xRange / 2.0)
        if base.wantMinigameDifficulty:
            diff = self.getDifficulty()
            scale = 0.5 + 0.5 * diff
            x *= scale
            yCenter = (yMin + yMax) / 2.0
            y = (y - yCenter) * scale + yCenter
        x = float(x) * (float(y) / float(yRange))
        y = y - int(yRange / 2.0)
        self.notify.debug('getTowerPosition: ' + str(x) + ', ' + str(y))
        return Point3(x, y, 0.0)

    def __createCannons(self):
        for avId in self.avIdList:
            cannon = self.cannon.copyTo(hidden)
            barrel = cannon.find('**/cannon')
            self.cannonDict[avId] = [cannon, barrel]

        numAvs = self.numPlayers
        for i in xrange(numAvs):
            avId = self.avIdList[i]
            self.cannonLocationDict[avId] = Point3(i * CANNON_X_SPACING - (numAvs - 1) * CANNON_X_SPACING / 2, CANNON_Y, CANNON_Z)
            if self.DEBUG_TOWER_RANGE:
                if self.DEBUG_CANNON_FAR_LEFT:
                    self.cannonLocationDict[avId] = Point3(0 * CANNON_X_SPACING - (4 - 1) * CANNON_X_SPACING / 2, CANNON_Y, CANNON_Z)
                else:
                    self.cannonLocationDict[avId] = Point3(3 * CANNON_X_SPACING - (4 - 1) * CANNON_X_SPACING / 2, CANNON_Y, CANNON_Z)
            self.cannonPositionDict[avId] = [0, CannonGameGlobals.CANNON_ANGLE_MIN]
            self.cannonDict[avId][0].setPos(self.cannonLocationDict[avId])
            self.__updateCannonPosition(avId)

    def setGameReady(self):
        if not self.hasLocalToon:
            return
        self.notify.debug('setGameReady')
        if DistributedMinigame.setGameReady(self):
            return
        for avId in self.avIdList:
            if avId != self.localAvId:
                self.__createToonModels(avId)

    def __createToonModels(self, avId):
        toon = self.getAvatar(avId)
        self.toonScaleDict[avId] = toon.getScale()
        toon.useLOD(1000)
        toonParent = render.attachNewNode('toonOriginChange')
        toon.reparentTo(toonParent)
        toon.setPosHpr(0, 0, -(toon.getHeight() / 2.0), 0, 0, 0)
        self.toonModelDict[avId] = toonParent
        head = ToonHead.ToonHead()
        head.setupHead(self.getAvatar(avId).style)
        head.reparentTo(hidden)
        self.toonHeadDict[avId] = head
        toon = self.getAvatar(avId)
        tag = NametagFloat3d()
        tag.setContents(Nametag.CSpeech | Nametag.CThought)
        tag.setBillboardOffset(0)
        tag.setAvatar(head)
        toon.nametag.addNametag(tag)
        tagPath = head.attachNewNode(tag)
        tagPath.setPos(0, 0, 1)
        head.tag = tag
        self.__loadToonInCannon(avId)
        self.getAvatar(avId).dropShadow.hide()
        self.dropShadowDict[avId] = self.dropShadow.copyTo(hidden)

    def setGameStart(self, timestamp):
        if not self.hasLocalToon:
            return
        self.notify.debug('setGameStart')
        DistributedMinigame.setGameStart(self, timestamp)
        self.__stopIntro()
        self.__putCameraBehindCannon()
        if not base.config.GetBool('endless-cannon-game', 0):
            self.timer.show()
            self.timer.countdown(CannonGameGlobals.GameTime, self.__gameTimerExpired)
        self.rewardPanel.reparentTo(base.a2dTopRight)
        self.scoreMult = MinigameGlobals.getScoreMult(self.cr.playGame.hood.id)
        self.__startRewardCountdown()
        self.airborneToons = 0
        self.clockStopTime = None
        self.gameFSM.request('aim')
        return

    def __gameTimerExpired(self):
        self.notify.debug('game timer expired')
        self.gameOver()

    def __playing(self):
        return self.gameFSM.getCurrentState() != self.gameFSM.getFinalState()

    def updateCannonPosition(self, avId, zRot, angle):
        if not self.hasLocalToon:
            return
        if not self.__playing():
            return
        if avId != self.localAvId:
            self.cannonPositionDict[avId] = [zRot, angle]
            self.__updateCannonPosition(avId)

    def setCannonWillFire(self, avId, fireTime, zRot, angle):
        if not self.hasLocalToon:
            return
        if not self.__playing():
            return
        self.notify.debug('setCannonWillFire: ' + str(avId) + ': zRot=' + str(zRot) + ', angle=' + str(angle) + ', time=' + str(fireTime))
        self.cannonPositionDict[avId][0] = zRot
        self.cannonPositionDict[avId][1] = angle
        self.__updateCannonPosition(avId)
        task = Task(self.__fireCannonTask)
        task.avId = avId
        task.fireTime = fireTime
        timeToWait = task.fireTime - self.getCurrentGameTime()
        if timeToWait > 0.0:
            fireTask = Task.sequence(Task.pause(timeToWait), task)
        else:
            fireTask = task
        fireTask = task
        taskMgr.add(fireTask, 'fireCannon' + str(avId))
        self.airborneToons += 1

    def announceToonWillLandInWater(self, avId, landTime):
        if not self.hasLocalToon:
            return
        self.notify.debug('announceToonWillLandInWater: ' + str(avId) + ': time=' + str(landTime))
        if self.clockStopTime == None:
            self.clockStopTime = landTime
        return

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

    def exitOff(self):
        pass

    def enterAim(self):
        self.notify.debug('enterAim')
        self.__enableAimInterface()
        self.__putCameraBehindCannon()

    def exitAim(self):
        self.__disableAimInterface()

    def enterShoot(self):
        self.notify.debug('enterShoot')
        self.__broadcastLocalCannonPosition()
        self.sendUpdate('setCannonLit', [self.cannonPositionDict[self.localAvId][0], self.cannonPositionDict[self.localAvId][1]])

    def exitShoot(self):
        pass

    def __somebodyWon(self, avId):
        if avId == self.localAvId:
            base.playSfx(self.sndWin)
        self.__killRewardCountdown()
        self.timer.stop()
        self.gameFSM.request('waitForToonsToLand')

    def enterWaitForToonsToLand(self):
        self.notify.debug('enterWaitForToonsToLand')
        if not self.airborneToons:
            self.gameOver()

    def exitWaitForToonsToLand(self):
        pass

    def enterCleanup(self):
        self.notify.debug('enterCleanup')
        self.music.stop()
        self.__killRewardCountdown()
        if hasattr(self, 'jarIval'):
            self.jarIval.finish()
            del self.jarIval
        for avId in self.avIdList:
            taskMgr.remove('fireCannon' + str(avId))
            taskMgr.remove('flyingToon' + str(avId))

    def exitCleanup(self):
        pass

    def __enableAimInterface(self):
        self.aimPad.show()
        self.accept(self.FIRE_KEY, self.__fireKeyPressed)
        self.accept(self.UP_KEY, self.__upKeyPressed)
        self.accept(self.DOWN_KEY, self.__downKeyPressed)
        self.accept(self.LEFT_KEY, self.__leftKeyPressed)
        self.accept(self.RIGHT_KEY, self.__rightKeyPressed)
        self.__spawnLocalCannonMoveTask()

    def __disableAimInterface(self):
        self.aimPad.hide()
        self.ignore(self.FIRE_KEY)
        self.ignore(self.UP_KEY)
        self.ignore(self.DOWN_KEY)
        self.ignore(self.LEFT_KEY)
        self.ignore(self.RIGHT_KEY)
        self.ignore(self.FIRE_KEY + '-up')
        self.ignore(self.UP_KEY + '-up')
        self.ignore(self.DOWN_KEY + '-up')
        self.ignore(self.LEFT_KEY + '-up')
        self.ignore(self.RIGHT_KEY + '-up')
        self.__killLocalCannonMoveTask()

    def __fireKeyPressed(self):
        self.ignore(self.FIRE_KEY)
        self.accept(self.FIRE_KEY + '-up', self.__fireKeyReleased)
        self.__firePressed()

    def __upKeyPressed(self):
        self.ignore(self.UP_KEY)
        self.accept(self.UP_KEY + '-up', self.__upKeyReleased)
        self.__upPressed()

    def __downKeyPressed(self):
        self.ignore(self.DOWN_KEY)
        self.accept(self.DOWN_KEY + '-up', self.__downKeyReleased)
        self.__downPressed()

    def __leftKeyPressed(self):
        self.ignore(self.LEFT_KEY)
        self.accept(self.LEFT_KEY + '-up', self.__leftKeyReleased)
        self.__leftPressed()

    def __rightKeyPressed(self):
        self.ignore(self.RIGHT_KEY)
        self.accept(self.RIGHT_KEY + '-up', self.__rightKeyReleased)
        self.__rightPressed()

    def __fireKeyReleased(self):
        self.ignore(self.FIRE_KEY + '-up')
        self.accept(self.FIRE_KEY, self.__fireKeyPressed)
        self.__fireReleased()

    def __leftKeyReleased(self):
        self.ignore(self.LEFT_KEY + '-up')
        self.accept(self.LEFT_KEY, self.__leftKeyPressed)
        self.__leftReleased()

    def __rightKeyReleased(self):
        self.ignore(self.RIGHT_KEY + '-up')
        self.accept(self.RIGHT_KEY, self.__rightKeyPressed)
        self.__rightReleased()

    def __upKeyReleased(self):
        self.ignore(self.UP_KEY + '-up')
        self.accept(self.UP_KEY, self.__upKeyPressed)
        self.__upReleased()

    def __downKeyReleased(self):
        self.ignore(self.DOWN_KEY + '-up')
        self.accept(self.DOWN_KEY, self.__downKeyPressed)
        self.__downReleased()

    def __firePressed(self):
        self.notify.debug('fire pressed')
        self.gameFSM.request('shoot')

    def __upPressed(self):
        self.notify.debug('up pressed')
        self.upPressed = self.__enterControlActive(self.upPressed)

    def __downPressed(self):
        self.notify.debug('down pressed')
        self.downPressed = self.__enterControlActive(self.downPressed)

    def __leftPressed(self):
        self.notify.debug('left pressed')
        self.leftPressed = self.__enterControlActive(self.leftPressed)

    def __rightPressed(self):
        self.notify.debug('right pressed')
        self.rightPressed = self.__enterControlActive(self.rightPressed)

    def __upReleased(self):
        self.notify.debug('up released')
        self.upPressed = self.__exitControlActive(self.upPressed)

    def __downReleased(self):
        self.notify.debug('down released')
        self.downPressed = self.__exitControlActive(self.downPressed)

    def __leftReleased(self):
        self.notify.debug('left released')
        self.leftPressed = self.__exitControlActive(self.leftPressed)

    def __rightReleased(self):
        self.notify.debug('right released')
        self.rightPressed = self.__exitControlActive(self.rightPressed)

    def __enterControlActive(self, control):
        return control + 1

    def __exitControlActive(self, control):
        return max(0, control - 1)

    def __spawnLocalCannonMoveTask(self):
        self.leftPressed = 0
        self.rightPressed = 0
        self.upPressed = 0
        self.downPressed = 0
        self.cannonMoving = 0
        task = Task(self.__localCannonMoveTask)
        task.lastPositionBroadcastTime = 0.0
        taskMgr.add(task, self.LOCAL_CANNON_MOVE_TASK)

    def __killLocalCannonMoveTask(self):
        taskMgr.remove(self.LOCAL_CANNON_MOVE_TASK)
        if self.cannonMoving:
            self.sndCannonMove.stop()

    def __localCannonMoveTask(self, task):
        pos = self.cannonPositionDict[self.localAvId]
        oldRot = pos[0]
        oldAng = pos[1]
        rotVel = 0
        if self.leftPressed:
            rotVel += CannonGameGlobals.CANNON_ROTATION_VEL
        if self.rightPressed:
            rotVel -= CannonGameGlobals.CANNON_ROTATION_VEL
        pos[0] += rotVel * globalClock.getDt()
        if pos[0] < CannonGameGlobals.CANNON_ROTATION_MIN:
            pos[0] = CannonGameGlobals.CANNON_ROTATION_MIN
        elif pos[0] > CannonGameGlobals.CANNON_ROTATION_MAX:
            pos[0] = CannonGameGlobals.CANNON_ROTATION_MAX
        angVel = 0
        if self.upPressed:
            angVel += CannonGameGlobals.CANNON_ANGLE_VEL
        if self.downPressed:
            angVel -= CannonGameGlobals.CANNON_ANGLE_VEL
        pos[1] += angVel * globalClock.getDt()
        if pos[1] < CannonGameGlobals.CANNON_ANGLE_MIN:
            pos[1] = CannonGameGlobals.CANNON_ANGLE_MIN
        elif pos[1] > CannonGameGlobals.CANNON_ANGLE_MAX:
            pos[1] = CannonGameGlobals.CANNON_ANGLE_MAX
        if oldRot != pos[0] or oldAng != pos[1]:
            if self.cannonMoving == 0:
                self.cannonMoving = 1
                base.playSfx(self.sndCannonMove, looping=1)
            self.__updateCannonPosition(self.localAvId)
            if task.time - task.lastPositionBroadcastTime > CANNON_MOVE_UPDATE_FREQ:
                task.lastPositionBroadcastTime = task.time
                self.__broadcastLocalCannonPosition()
        elif self.cannonMoving:
            self.cannonMoving = 0
            self.sndCannonMove.stop()
            self.__broadcastLocalCannonPosition()
        return Task.cont

    def __broadcastLocalCannonPosition(self):
        self.sendUpdate('setCannonPosition', [self.cannonPositionDict[self.localAvId][0], self.cannonPositionDict[self.localAvId][1]])

    def __updateCannonPosition(self, avId):
        self.cannonDict[avId][0].setHpr(self.cannonPositionDict[avId][0], 0.0, 0.0)
        self.cannonDict[avId][1].setHpr(0.0, self.cannonPositionDict[avId][1], 0.0)

    def __getCameraPositionBehindCannon(self):
        return Point3(self.cannonLocationDict[self.localAvId][0], CANNON_Y - 25.0, CANNON_Z + 7)

    def __putCameraBehindCannon(self):
        camera.setPos(self.__getCameraPositionBehindCannon())
        camera.setHpr(0, 0, 0)

    def __loadToonInCannon(self, avId):
        self.toonModelDict[avId].detachNode()
        head = self.toonHeadDict[avId]
        head.startBlink()
        head.startLookAround()
        head.reparentTo(self.cannonDict[avId][1])
        head.setPosHpr(0, 6, 0, 0, -45, 0)
        sc = self.toonScaleDict[avId]
        head.setScale(render, sc[0], sc[1], sc[2])

    def __toRadians(self, angle):
        return angle * 2.0 * math.pi / 360.0

    def __toDegrees(self, angle):
        return angle * 360.0 / (2.0 * math.pi)

    def __calcFlightResults(self, avId, launchTime):
        head = self.toonHeadDict[avId]
        startPos = head.getPos(render)
        startHpr = head.getHpr(render)
        hpr = self.cannonDict[avId][1].getHpr(render)
        towerPos = self.tower.getPos(render)
        rotation = self.__toRadians(hpr[0])
        angle = self.__toRadians(hpr[1])
        horizVel = INITIAL_VELOCITY * math.cos(angle)
        xVel = horizVel * -math.sin(rotation)
        yVel = horizVel * math.cos(rotation)
        zVel = INITIAL_VELOCITY * math.sin(angle)
        startVel = Vec3(xVel, yVel, zVel)
        trajectory = Trajectory.Trajectory(launchTime, startPos, startVel)
        towerList = [towerPos + Point3(0, 0, BUCKET_HEIGHT), TOWER_RADIUS, TOWER_HEIGHT - BUCKET_HEIGHT]
        self.notify.debug('calcFlightResults(%s): rotation(%s), angle(%s), horizVel(%s), xVel(%s), yVel(%s), zVel(%s), startVel(%s), trajectory(%s), towerList(%s)' % (avId,
         rotation,
         angle,
         horizVel,
         xVel,
         yVel,
         zVel,
         startVel,
         trajectory,
         towerList))
        timeOfImpact, hitWhat = self.__calcToonImpact(trajectory, towerList)
        return startPos, startHpr, startVel, trajectory, timeOfImpact, hitWhat

    def __fireCannonTask(self, task):
        launchTime = task.fireTime
        avId = task.avId
        self.notify.debug('FIRING CANNON FOR AVATAR ' + str(avId))
        startPos, startHpr, startVel, trajectory, timeOfImpact, hitWhat = self.__calcFlightResults(avId, launchTime)

        self.notify.debug('start position: ' + str(startPos))
        self.notify.debug('start velocity: ' + str(startVel))
        self.notify.debug('time of launch: ' + str(launchTime))
        self.notify.debug('time of impact: ' + str(timeOfImpact))
        self.notify.debug('location of impact: ' + str(trajectory.getPos(timeOfImpact)))
        if hitWhat == self.HIT_WATER:
            self.notify.debug('toon will land in the water')
        elif hitWhat == self.HIT_TOWER:
            self.notify.debug('toon will hit the tower')
        else:
            self.notify.debug('toon will hit the ground')
        head = self.toonHeadDict[avId]
        head.stopBlink()
        head.stopLookAroundNow()
        head.reparentTo(hidden)
        av = self.toonModelDict[avId]
        av.reparentTo(render)
        av.setPos(startPos)
        av.setHpr(startHpr)
        avatar = self.getAvatar(avId)
        avatar.loop('swim')
        avatar.setPosHpr(0, 0, -(avatar.getHeight() / 2.0), 0, 0, 0)
        shootTask = Task(self.__shootTask)
        flyTask = Task(self.__flyTask)
        seqDoneTask = Task(self.__flySequenceDoneTask)
        info = {}
        info['avId'] = avId
        info['trajectory'] = trajectory
        info['launchTime'] = launchTime
        info['timeOfImpact'] = timeOfImpact
        info['hitWhat'] = hitWhat
        info['toon'] = self.toonModelDict[avId]
        info['hRot'] = self.cannonPositionDict[avId][0]
        info['haveWhistled'] = 0
        info['maxCamPullback'] = CAMERA_PULLBACK_MIN
        info['timeEnterTowerXY'], info['timeExitTowerXY'] = trajectory.calcEnterAndLeaveCylinderXY(self.tower.getPos(render), TOWER_RADIUS)
        shootTask.info = info
        flyTask.info = info
        seqDoneTask.info = info
        seqTask = Task.sequence(shootTask, flyTask, Task.pause(LAND_TIME), seqDoneTask)
        taskMgr.add(seqTask, 'flyingToon' + str(avId))
        if avId == self.localAvId:
            if info['hitWhat'] == self.HIT_WATER:
                self.sendUpdate('setToonWillLandInWater', [info['timeOfImpact']])
        return Task.done

    def __calcToonImpact(self, trajectory, waterTower):
        self.notify.debug('trajectory: %s' % trajectory)
        self.notify.debug('waterTower: %s' % waterTower)
        waterDiscCenter = Point3(waterTower[0])
        waterDiscCenter.setZ(waterDiscCenter[2] + waterTower[2])
        t_waterImpact = trajectory.checkCollisionWithDisc(waterDiscCenter, waterTower[1])
        self.notify.debug('t_waterImpact: %s' % t_waterImpact)
        if t_waterImpact > 0:
            return (t_waterImpact, self.HIT_WATER)
        t_towerImpact = trajectory.checkCollisionWithCylinderSides(waterTower[0], waterTower[1], waterTower[2])
        self.notify.debug('t_towerImpact: %s' % t_towerImpact)
        if t_towerImpact > 0:
            return (t_towerImpact, self.HIT_TOWER)
        t_groundImpact = trajectory.checkCollisionWithGround()
        self.notify.debug('t_groundImpact: %s' % t_groundImpact)
        if t_groundImpact >= trajectory.getStartTime():
            return (t_groundImpact, self.HIT_GROUND)
        else:
            self.notify.error('__calcToonImpact: toon never impacts ground?')
            return (self.startTime, self.HIT_GROUND)

    def __shootTask(self, task):
        base.playSfx(self.sndCannonFire)
        self.dropShadowDict[task.info['avId']].reparentTo(render)
        return Task.done

    def __flyTask(self, task):
        curTime = task.time + task.info['launchTime']
        t = min(curTime, task.info['timeOfImpact'])
        pos = task.info['trajectory'].getPos(t)
        task.info['toon'].setPos(pos)
        shadowPos = Point3(pos)
        if t >= task.info['timeEnterTowerXY'] and t <= task.info['timeExitTowerXY'] and pos[2] >= self.tower.getPos(render)[2] + TOWER_HEIGHT:
            shadowPos.setZ(self.tower.getPos(render)[2] + TOWER_HEIGHT + SHADOW_Z_OFFSET)
        else:
            shadowPos.setZ(SHADOW_Z_OFFSET)
        self.dropShadowDict[task.info['avId']].setPos(shadowPos)
        vel = task.info['trajectory'].getVel(t)
        run = math.sqrt(vel[0] * vel[0] + vel[1] * vel[1])
        rise = vel[2]
        theta = self.__toDegrees(math.atan(rise / run))
        task.info['toon'].setHpr(task.info['hRot'], -90 + theta, 0)
        if task.info['avId'] == self.localAvId:
            lookAt = self.tower.getPos(render)
            lookAt.setZ(lookAt.getZ() - TOWER_HEIGHT / 2.0)
            towerPos = Point3(self.towerPos)
            towerPos.setZ(TOWER_HEIGHT)
            ttVec = Vec3(pos - towerPos)
            toonTowerDist = ttVec.length()
            multiplier = 0.0
            if toonTowerDist < TOON_TOWER_THRESHOLD:
                up = Vec3(0.0, 0.0, 1.0)
                perp = up.cross(vel)
                perp.normalize()
                if ttVec.dot(perp) > 0.0:
                    perp = Vec3(-perp[0], -perp[1], -perp[2])
                a = 1.0 - toonTowerDist / TOON_TOWER_THRESHOLD
                a_2 = a * a
                multiplier = -2.0 * a_2 * a + 3 * a_2
                lookAt = lookAt + perp * (multiplier * MAX_LOOKAT_OFFSET)
            foo = Vec3(pos - lookAt)
            foo.normalize()
            task.info['maxCamPullback'] = max(task.info['maxCamPullback'], CAMERA_PULLBACK_MIN + multiplier * (CAMERA_PULLBACK_MAX - CAMERA_PULLBACK_MIN))
            foo = foo * task.info['maxCamPullback']
            camPos = pos + Point3(foo)
            camera.setPos(camPos)
            camera.lookAt(pos)
        if task.info['haveWhistled'] == 0:
            if -vel[2] > WHISTLE_SPEED:
                if t < task.info['timeOfImpact'] - 0.5:
                    task.info['haveWhistled'] = 1
                    base.playSfx(self.sndWhizz)
        if t == task.info['timeOfImpact']:
            if task.info['haveWhistled']:
                self.sndWhizz.stop()
            self.dropShadowDict[task.info['avId']].reparentTo(hidden)
            avatar = self.getAvatar(task.info['avId'])
            if task.info['hitWhat'] == self.HIT_WATER:
                avatar.loop('neutral')
                self.splash.setPos(task.info['toon'].getPos())
                self.splash.setScale(2)
                self.splash.play()
                base.playSfx(self.sndHitWater)
                task.info['toon'].setHpr(task.info['hRot'], 0, 0)
                self.__somebodyWon(task.info['avId'])
            elif task.info['hitWhat'] == self.HIT_TOWER:
                toon = task.info['toon']
                pos = toon.getPos()
                ttVec = Vec3(pos - self.towerPos)
                ttVec.setZ(0)
                ttVec.normalize()
                h = rad2Deg(math.asin(ttVec[0]))
                toon.setHpr(h, 94, 0)
                deltaZ = TOWER_HEIGHT - BUCKET_HEIGHT
                sf = min(max(pos[2] - BUCKET_HEIGHT, 0), deltaZ) / deltaZ
                hitPos = pos + Point3(ttVec * (0.75 * sf))
                toon.setPos(hitPos)
                hitPos.setZ(hitPos[2] - 1.0)
                s = Sequence(Wait(0.5), toon.posInterval(duration=LAND_TIME - 0.5, pos=hitPos, blendType='easeIn'))
                self.toonIntervalDict[task.info['avId']] = s
                s.start()
                avatar.iPos()
                avatar.pose('slip-forward', 25)
                base.playSfx(self.sndHitTower)
            elif task.info['hitWhat'] == self.HIT_GROUND:
                task.info['toon'].setP(render, -150.0)
                self.dustCloud.setPos(task.info['toon'], 0, 0, -2.5)
                self.dustCloud.setScale(0.35)
                self.dustCloud.play()
                base.playSfx(self.sndHitGround)
                avatar.setPlayRate(2.0, 'run')
                avatar.loop('run')
            return Task.done
        return Task.cont

    def __flySequenceDoneTask(self, task):
        self.airborneToons -= 1
        if self.gameFSM.getCurrentState().getName() == 'waitForToonsToLand':
            if 0 == self.airborneToons:
                self.gameOver()
        else:
            self.__loadToonInCannon(task.info['avId'])
            if task.info['avId'] == self.localAvId:
                self.gameFSM.request('aim')
        return Task.done

    def __startRewardCountdown(self):
        taskMgr.remove(self.REWARD_COUNTDOWN_TASK)
        taskMgr.add(self.__updateRewardCountdown, self.REWARD_COUNTDOWN_TASK)

    def __killRewardCountdown(self):
        taskMgr.remove(self.REWARD_COUNTDOWN_TASK)

    def __updateRewardCountdown(self, task):
        if not hasattr(self, 'rewardPanel'):
            return Task.cont
        curTime = self.getCurrentGameTime()
        if self.clockStopTime is not None:
            if self.clockStopTime < curTime:
                self.__killRewardCountdown()
                curTime = self.clockStopTime
        score = int(self.scoreMult * CannonGameGlobals.calcScore(curTime) + 0.5)
        if not hasattr(task, 'curScore'):
            task.curScore = score
        self.rewardPanel['text'] = str(score)
        if task.curScore != score:
            if hasattr(self, 'jarIval'):
                self.jarIval.finish()
            s = self.rewardPanel.getScale()
            self.jarIval = Parallel(Sequence(self.rewardPanel.scaleInterval(0.15, s * 3.0 / 4.0, blendType='easeOut'), self.rewardPanel.scaleInterval(0.15, s, blendType='easeIn')), SoundInterval(self.sndRewardTick), name='cannonGameRewardJarThrob')
            self.jarIval.start()
        task.curScore = score
        return Task.cont

    def __startIntro(self):
        self.T_WATER = 1
        self.T_WATER2LONGVIEW = 1
        self.T_LONGVIEW = 1
        self.T_LONGVIEW2TOONHEAD = 2
        self.T_TOONHEAD = 2
        self.T_TOONHEAD2CANNONBACK = 2
        self.__introCameraInterval = None
        taskLookInWater = Task(self.__taskLookInWater)
        taskPullBackFromWater = Task(self.__taskPullBackFromWater)
        taskFlyUpToToon = Task(self.__flyUpToToon)
        taskFlyToBackOfCannon = Task(self.__flyToBackOfCannon)
        commonData = {}
        taskLookInWater.data = commonData
        taskPullBackFromWater.data = commonData
        taskFlyUpToToon.data = commonData
        taskFlyToBackOfCannon.data = commonData
        introTask = Task.sequence(taskLookInWater, Task.pause(self.T_WATER), taskPullBackFromWater, Task.pause(self.T_WATER2LONGVIEW + self.T_LONGVIEW), taskFlyUpToToon, Task.pause(self.T_LONGVIEW2TOONHEAD + self.T_TOONHEAD), taskFlyToBackOfCannon)
        taskMgr.add(introTask, self.INTRO_TASK_NAME)

    def __stopIntro(self):
        taskMgr.remove(self.INTRO_TASK_NAME)
        if self.__introCameraInterval:
            self.__introCameraInterval.finish()
        camera.wrtReparentTo(render)

    def __spawnCameraLookAtLerp(self, targetPos, targetLookAt, duration):
        oldPos = camera.getPos()
        oldHpr = camera.getHpr()
        camera.setPos(targetPos)
        camera.lookAt(targetLookAt)
        targetQuat = Quat()
        targetQuat.setHpr(camera.getHpr())
        camera.setPos(oldPos)
        camera.setHpr(oldHpr)
        if self.__introCameraInterval:
            self.__introCameraInterval.finish()
        self.__introCameraInterval = camera.posQuatInterval(duration, Point3(targetPos), targetQuat, blendType='easeInOut')
        self.__introCameraInterval.start()

    def __taskLookInWater(self, task):
        task.data['cannonCenter'] = Point3(0, CANNON_Y, CANNON_Z)
        task.data['towerWaterCenter'] = Point3(self.towerPos + Point3(0, 0, TOWER_HEIGHT))
        task.data['vecTowerToCannon'] = Point3(task.data['cannonCenter'] - task.data['towerWaterCenter'])
        vecAwayFromCannons = Vec3(Point3(0, 0, 0) - task.data['vecTowerToCannon'])
        vecAwayFromCannons.setZ(0.0)
        vecAwayFromCannons.normalize()
        camLoc = Point3(vecAwayFromCannons * 20) + Point3(0, 0, 20)
        camLoc = camLoc + task.data['towerWaterCenter']
        camera.setPos(camLoc)
        camera.lookAt(task.data['towerWaterCenter'])
        task.data['vecAwayFromCannons'] = vecAwayFromCannons
        return Task.done

    def __taskPullBackFromWater(self, task):
        camLoc = Point3(task.data['vecAwayFromCannons'] * 40) + Point3(0, 0, 20)
        camLoc = camLoc + task.data['towerWaterCenter']
        lookAt = task.data['cannonCenter']
        self.__spawnCameraLookAtLerp(camLoc, lookAt, self.T_WATER2LONGVIEW)
        return Task.done

    def __flyUpToToon(self, task):
        headPos = self.toonHeadDict[self.localAvId].getPos(render)
        camLoc = headPos + Point3(0, 5, 0)
        lookAt = Point3(headPos)
        self.__spawnCameraLookAtLerp(camLoc, lookAt, self.T_LONGVIEW2TOONHEAD)
        return Task.done

    def __flyToBackOfCannon(self, task):
        lerpNode = hidden.attachNewNode('CannonGameCameraLerpNode')
        lerpNode.reparentTo(render)
        lerpNode.setPos(self.cannonLocationDict[self.localAvId] + Point3(0, 1, 0))
        relCamPos = camera.getPos(lerpNode)
        relCamHpr = camera.getHpr(lerpNode)
        startRotation = lerpNode.getHpr()
        endRotation = Point3(-180, 0, 0)
        lerpNode.setHpr(endRotation)
        camera.setPos(self.__getCameraPositionBehindCannon())
        endPos = camera.getPos(lerpNode)
        lerpNode.setHpr(startRotation)
        camera.reparentTo(lerpNode)
        camera.setPos(relCamPos)
        camera.setHpr(relCamHpr)
        if self.__introCameraInterval:
            self.__introCameraInterval.finish()
        self.__introCameraInterval = Parallel(
            lerpNode.hprInterval(self.T_TOONHEAD2CANNONBACK, endRotation, blendType='easeInOut'),
            camera.posInterval(self.T_TOONHEAD2CANNONBACK, endPos, blendType='easeInOut'))
        self.__introCameraInterval.start()
        return Task.done