# NHSGA Steampunk Rollerball game
# Phase 1 - 2008
# Program, sound edit, flat texture: Sean Kelly - spkelly@alumni.cmu.edu
# 3D Art, model texture: Jen Gilbert, Tony Yruegas

import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject
from pandac.PandaModules import *
from direct.task import Task
from direct.fsm import FSM
from direct.interval.IntervalGlobal import *
from math import *
from random import *

from Wiimote import Wiimote
from Ball import Ball
from Flat import Flat
from constants import *

import sys


# _____  y
# |   |  |
# | + |  |
# | O |  ---x
# |...|
# |   |
# | o |
# | o |
# |___|
#
#

CAM_MODES=3 # fixed angle, dynamic angle, fixed position
CAM_TRACK_RATE = 4.0

class main(DirectObject,FSM.FSM):
    def __init__(self):
        self.wm = Wiimote()
        #self.wm.initUDP() # comment out unless a wiimote is actually hooked up
        
        # moved to enterGameplay
        self.accept("newWiimoteData",self.onNewWiimoteData)
        
        taskMgr.add(self.wm.receiveTask,"wiimote UDP receiver")
        print "receive task added"
        
        # ball respawn point and heading
        self.ballSpawn = BALL_START
        self.ballSpawnH = 0
        
        # interval for the 'attract mode' cammera spinnery
        # gets set up in setupPreviewCam, called at start of Title
        self.camPreviewInt = Wait(2)
        self.previewSeg = 0
        
        # sets up its own geometry, 
        # needs to be present for initCollision and initShader
        self.ball = Ball()
        
        self.initGeometry()
        self.initShader()
        self.initCollisions()
        self.initSound()
        self.initFSM()
        
        # how many gems have we picked up recently?
        self.chainCounter = 0
        # how many total?
        self.gemCount = 0
        # score
        self.score=0
        
        # last camera task call time
        self.oldCamTime = 0
        # how quickly does the camera move to the
        # ideal position?
        # 1.0 = 100% per second
        self.camTrackRate = CAM_TRACK_RATE
        
        base.disableMouse()
        
        # moved to enterGameplay
        ##taskMgr.add(self.camUpdateTask,"camera control task")
        
        # 0 = global space above & behind
        # 1 = vel vector space above & behind
        # 2 = fixed
        self.camMode = 0
        self.camIncButton = False
        self.camDecButton = False
        # pull these directly from the NC
        self.camLookX = 0
        self.camLookY = 0
        
        self.accept("escape",self.finish)
        
        #debug
        self.ballUpKey = False
        # moved to enterGameplay
        ## self.accept("arrow_left",self.debugBallLeft)
        ## self.accept("arrow_left-up",self.debugBallStopLeft)
        ## self.accept("arrow_right",self.debugBallRight)
        ## self.accept("arrow_right-up",self.debugBallStopRight)
        ## self.accept("arrow_up",self.debugBallGo)
        ## self.accept("arrow_up-up",self.debugBallStopGo)
        ## self.accept("arrow_down",self.debugBallBrake)
        ## self.accept("arrow_down-up",self.debugBallStopBrake)
        ## taskMgr.add(self.debugBallGoTask,"debug ball motion")
        ## self.accept("r",self.debugResetBall)
        ## self.accept("+",self.incCamMode)
        ## self.accept("-",self.decCamMode)
        
    def initGeometry(self):
        # the shader seems to do specular hotspots from point lights incorrectly
        # and besides, the shader only does 1 light and I want the whole scene
        # lit nicely.
        #self.light1 = PointLight('pointLight1')
        self.light1 = DirectionalLight('dirLight1')
        self.light1.setColor(Vec4(1,1,.9,1))
        self.light1NP = render.attachNewNode(self.light1)
        #self.light1NP.setPos(0,0,100)
        self.light1NP.setHpr(0,-70,0)
        
        render.setLight(self.light1NP)
        
        ## # prototype segments
        ## self.trackNames = ["Models/Test_FullPipe.egg","Models/Test_HalfPipe.egg", "Models/Test_Plane.egg", "Models/Test_HalfPipe.egg", "Models/Test_SCurve.egg"]
        ## self.colliderNames = ["Models/Test_Collider_FullPipe.egg", "Models/Test_Collider_HalfPipe.egg", "Models/Test_Collider_Plane.egg", "Models/Test_Collider_HalfPipe.egg", "Models/Test_Collider_SCurve.egg"]
        
        ## self.testTracks = []
        
        ## offset = 0
        ## for name in self.trackNames:
            ## print offset
            ## self.testTracks.append(loader.loadModel(name))
            ## self.testTracks[-1].reparentTo(render)
            ## self.testTracks[-1].setScale(MODEL_SCALE)
            ## self.testTracks[-1].setH(90)
            ## self.testTracks[-1].setPos(0,20*offset,0)
            ## self.testTracks[-1].setTransparency(TransparencyAttrib.MAlpha)
            ## offset+=1
        ## self.testTracks[0].setZ(5)
        
        # final segments
        segmentNames = ["capend","short","long","lcurve","rcurve","mount","loop","spiral","capend2"]
        visibleDir = "TrackEggs"
        collisionDir = "TrackEggs"
        collisionPrefix = ""
        collisionSuffix = "Collision"
        
        # debug texture - something should ultimately be built into the eggs
        tracktex = loader.loadTexture("Textures/testtexture.jpg")
        
        # once we have a list of parts, all we need to define
        # a track as a list of segment indeces
        # this could/should be loaded from file at some point
        trackSequence = [0,1,2,3,4,5,6,7,1,8]
        
        # load the actual segments
        trackSegments = []
        for name in segmentNames:
            seg = loader.loadModel(visibleDir+"/"+name+".egg")
            seg.setTransparency(TransparencyAttrib.MAlpha)
            seg.setTexture(tracktex)
            col = loader.loadModel(collisionDir+"/"+collisionPrefix+name+collisionSuffix+".egg")
            col.reparentTo(seg)
            seg.setScale(MODEL_SCALE)
            # Explicitly name the collision node so we can identify it later.
            # Kinda a hack, but it works for the simple collision models
            # we're using.
            col.getChildren()[0].getChildren()[0].setName("track")
            trackSegments.append(seg)
            
        # parse the track list and assemble the track
        self.currentTrack = []
        self.waypoints = []
        nextPos = Point3(0,0,0)
        nextHpr = Point3(90,0,0)
        for i in trackSequence:
            newPiece = trackSegments[i].copyTo(render)
            newPiece.setHpr(nextHpr)
            # debug until the artists get the locators right
            if i==5:
                nextPos=nextPos-Vec3(0,0,2)
            newPiece.setPos(nextPos)
            loc = newPiece.getChildren()[0].find("locator") # the 'out' point of this segment
            #loc.hide()
            nextPos = loc.getPos(render)
            # debug until the artists get the locators right
            nextPos = nextPos+Vec3(0,0,2)
            nextHpr = loc.getHpr(render)
            self.currentTrack.append(newPiece)
            
        # construct the waypoint frames list
        wpframes = []
        for i in range(9):
            wpframes.append("flats/waypoint"+str(i)+".png")
        
        # set up waypoints at the end of every piece but the start
        # and one before the end
        wpPieces = range(1,len(self.currentTrack)-2)#includes 1st index but not last?
        wpPieces.append(len(self.currentTrack)-1)
        for i in wpPieces:
            waypoint = NodePath("waypoint "+str(i))
            loc = self.currentTrack[i].getChildren()[0].find("locator")
            wp1 = Flat(wpframes)
            wp1.flat.reparentTo(waypoint)
            wp2 = Flat(wpframes)
            wp2.flat.reparentTo(waypoint)
            wp2.flat.setHpr(180,0,0)
            wp1.loopForwards(9)
            wp2.loopBackwards(9)
            waypoint.setScale(20)
            dummy = render.attachNewNode("dummy")
            dummy.setH(-90)
            waypoint.reparentTo(dummy)
            waypoint.setHpr(loc.getHpr(render))
            waypoint.wrtReparentTo(render)
            waypoint.setPos(loc.getPos(render)+Vec3(0,0,2)) #offset is debug until the artists get the locators right
            dummy.removeNode()
            self.waypoints.append(waypoint)
        # tint the finish locator
        self.waypoints[-1].setColor(1,.5,.5)
        
        # actual gem list will be filled in in initGems
        # called at start of Title
        self.gems = []
            
        # init sparkle frame list
        self.sparkleFrames = []
        for i in range(8):
            self.sparkleFrames.append("flats/sparkle"+str(i)+".png")
            
        # init waypoint ripple frame list
        self.wayrippleFrames = []
        for i in range(7):
            self.wayrippleFrames.append("flats/wayripple"+str(i)+".png")
            
        # glowy light arrows for spawn effect
        self.beamArrow = NodePath("beam arrow")
        face1=Flat(["flats/beamarrow.png"])
        face2=Flat(["flats/beamarrow.png"])
        face2.flat.setH(180)
        face1.flat.reparentTo(self.beamArrow)
        face2.flat.reparentTo(self.beamArrow)
        self.arrowRingRoot = NodePath("arrow ring")
        self.beamArrow.reparentTo(self.arrowRingRoot)
        self.beamArrow.setPos(0,2,0)
        self.arrowRingRoot.reparentTo(render)
        self.arrowRingRoot.hide()
            
        # init number frame list
        self.numeralFrames = []
        for i in range(10):
            self.numeralFrames.append("flats/num"+str(i)+".png")
        
        # score display
        self.scoreFlats = []
        for i in range(6):
            self.scoreFlats.append(Flat(self.numeralFrames))
            self.scoreFlats[-1].flat.reparentTo(aspect2d)
            self.scoreFlats[-1].flat.setScale(.15)
            self.scoreFlats[-1].flat.setPos(-1.2+0.12*i,0,.9)
            
        self.gemIcon = Flat(["flats/gem.png"])
        self.gemIcon.flat.reparentTo(aspect2d)
        self.gemIcon.flat.setScale(.2)
        self.gemIcon.flat.setPos(-1.2,0,-.9)
        self.gemIcon.flat.setColor(0.1,0.1,0.1)
        # gem collection display
        self.gemCountFlats = []
        for i in range(3):
            self.gemCountFlats.append(Flat(self.numeralFrames))
            self.gemCountFlats[-1].flat.reparentTo(aspect2d)
            self.gemCountFlats[-1].flat.setScale(.2)
            self.gemCountFlats[-1].flat.setPos(-1.0+0.18*i,0,-.9)
            
        self.camModeFlat=Flat(["flats/camera0.png","flats/camera1.png","flats/camera2.png"])
        self.camModeFlat.flat.reparentTo(aspect2d)
        self.camModeFlat.flat.setScale(.2)
        self.camModeFlat.flat.setPos(1.2,0,.85)
        
        self.titleFlat = Flat(["flats/title.png"])
        self.titleFlat.flat.reparentTo(aspect2d)
        self.titleFlat.flat.setScale(8.0/3.0,1,2.0)
            
        # programmer-art skybox
        self.skybox = NodePath("skybox")
        front = Flat(["flats/skybox_side.png"])
        front.flat.reparentTo(self.skybox)
        front.flat.setPos(0,.5,0)
        right = Flat(["flats/skybox_side.png"])
        right.flat.reparentTo(self.skybox)
        right.flat.setPos(.5,0,0)
        right.flat.setH(-90)
        back = Flat(["flats/skybox_side.png"])
        back.flat.reparentTo(self.skybox)
        back.flat.setPos(0,-.5,0)
        back.flat.setH(180)
        left = Flat(["flats/skybox_side.png"])
        left.flat.reparentTo(self.skybox)
        left.flat.setPos(-.5,0,0)
        left.flat.setH(90)
        top = Flat(["flats/skybox_top.png"])
        top.flat.reparentTo(self.skybox)
        top.flat.setPos(0,0,.5)
        top.flat.setP(90)
        base.setBackgroundColor(0,0,0)
        
        self.skybox.reparentTo(render)
        self.skybox.setLightOff()
        self.skybox.setClipPlaneOff()
        self.skybox.setBin("background",10)
        self.skybox.setDepthTest(False)
        self.skybox.setDepthWrite(False)
        self.skybox.setScale(50)
        taskMgr.add(self.skyboxMoveTask,"skybox motion")
        
    def initGems(self):
        # add gems to collect
        # ultimately, I'd like to scan each track segment for gem locators "gem0"-"gemNNN"
        
        for g in self.gems:
            g.destroy()
        
        self.gems = []
        
        for g in range(20):
            gem = Flat(["flats/gem.png"],base.cam)
            self.gems.append(gem)
            self.gems[-1].flat.reparentTo(render)
            self.gems[-1].flat.setPos(0,1+(g+1)*2,1)
            
        for g in self.gems:
            cs = CollisionSphere(0,0,0,.5)
            cnp = g.flat.attachNewNode(CollisionNode('gem'))
            cnp.node().addSolid(cs)
            #cnp.show()
            cnp.setPos(0,0,0)
        
        
    def initShader(self):
        mat1 = Material()
        mat1.setSpecular(Vec4(1,1,1,1))
        mat1.setShininess(80)
        self.ball.root.setMaterial(mat1)
        self.ball.shell.setColor(.7,.6,0)
        self.ball.core.setColor(0,.6,.1)
        
        for w in self.waypoints:
            w.setMaterial(mat1) #oops- I setLightOff on all flats, so this won't do much while the waypoints are flats
        
        render.setShaderAuto()
        
    def initCollisions(self):
        #ground- if you touch this, you're dead
        self.groundCS = CollisionPlane(Plane(Vec3(0, 0, 1), Point3(0, 0, 0)))
        self.groundCNP = render.attachNewNode(CollisionNode('ground'))
        self.groundCNP.node().addSolid(self.groundCS)
        #self.groundCNP.show()
        self.groundCNP.setPos(0,10,-40)
        
        ## #track
        ## self.testTrackColliders=[]
        ## self.collidersToTracks = {}
        ## index = 0
        ## for name in self.colliderNames:
            ## self.testTrackColliders.append(loader.loadModel(name))
            ## self.testTrackColliders[-1].reparentTo(self.testTracks[index])
            ## #self.testTrackColliders[-1].setName("track collider %d" % index)
            ## self.collidersToTracks[self.testTrackColliders[-1]]=self.testTracks[index]
            ## index+=1
        ## print self.collidersToTracks
        
        #ball - the player.  Runs into things.
        self.ballCS = CollisionSphere(0, 0, 0, self.ball.radius)
        self.ballCNP = self.ball.root.attachNewNode(CollisionNode('ball'))
        self.ballCNP.node().addSolid(self.ballCS)
        #self.ballCNP.show()
        self.ballCNP.setPos(0,0,0)
        
        # camera (for hiding walls between cam and ball)
        self.camCS = CollisionSegment(0,0,0,0,1,0)
        self.camCNP = base.cam.attachNewNode(CollisionNode('cam'))
        self.camCNP.node().addSolid(self.camCS)
        #self.camCNP.show()
        self.camCNP.setPos(0,0,0)
        #marker = loader.loadModel("Models/Test_Inner_Ball.egg")
        #marker.setScale(.01)
        #marker.reparentTo(self.camCNP)
        #marker.setPos(0,1,0)
        
        # gems are now set up in initGems
            
        # waypoints- log the player's progress for respawn
        for w in self.waypoints:
            wpquad = CollisionPolygon(Point3(-.5,0,-.5),Point3(-.5,0,.5),Point3(.5,0,.5),Point3(.5,0,-.5))
            cnp = w.attachNewNode(CollisionNode('waypoint'))
            if w==self.waypoints[-1]:
                cnp.setName("finish")
            cnp.node().addSolid(wpquad)
            #cnp.show()
            cnp.setPos(0,0,0)
        
        base.cTrav = CollisionTraverser()
        #base.cTrav.showCollisions(render)
        self.handler = CollisionHandlerEvent()
        base.cTrav.addCollider(self.ballCNP, self.handler)
        base.cTrav.addCollider(self.camCNP, self.handler)
        
        
        self.handler.addInPattern('%fn in %in')
        self.handler.addAgainPattern('%fn again %in')
        self.handler.addOutPattern('%fn out %in')
        
        # moved to enterGameplay
        ## self.accept('ball in',self.handler_bounce)
        ## self.accept('ball again',self.handler_bounce)
        ## self.accept('ball out',self.handler_fall)
        
        ## self.accept('cam in',self.handler_hideGeom)
        ## self.accept('cam out',self.handler_showGeom)
        
    def initSound(self):
        self.sfx_crashes = []
        for i in range(6):
            self.sfx_crashes.append(loader.loadSfx("sounds/crash"+str(i)+".wav"))
        self.sfx_pickup = loader.loadSfx("sounds/pickup.wav")
        self.sfx_pickup.setPlayRate(1.0)
        self.sfx_waypoint = loader.loadSfx("sounds/waypoint.wav")
        self.sfx_arrow = loader.loadSfx("sounds/arrowswish.wav")
        
    def initFSM(self):
        FSM.FSM.__init__(self,'gameFSM')
        self.defaultTransitions = {
            'Title' : ['Spawn'],
            'Spawn' : ['Gameplay'],
            'Gameplay' : ['Die','Finish'],
            'Die' : ['Spawn'],
            'Finish' : ['Title']
            }
        self.request('Title')
        
    def enterSpawn(self):
        self.resetBall()
        ballH = self.ball.root.getH()
        xDist = 20*sin(radians(ballH))
        yDist = -20*cos(radians(ballH))
        base.cam.setPos(self.ball.root.getPos() + Vec3(xDist,yDist,3))
        base.cam.lookAt(self.ball.root)
        shellFadeInt = self.ball.shell.colorInterval(duration=2.0,color=Vec4(.7,.6,0,1),startColor=Vec4(.7,.6,0,0))
        coreFadeInt = self.ball.core.colorInterval(duration=1.75,color=Vec4(0,.6,.1,1),startColor=Vec4(0,.6,.1,0))
        
        self.arrowRingRoot.show()
        self.arrowRingRoot.setPos(self.ball.root.getPos()-Vec3(0,0,3))
        
        arrowUpInt = self.beamArrow.posInterval(duration=2.0/24.0, pos=Point3(0,2,4), startPos=Point3(0,2,-1))
        arrowNarrowInt = self.beamArrow.scaleInterval(duration=2.0/24.0, scale=Point3(3,1,4), startScale=Point3(3,1,2))
        arrowMoveInt = Parallel(arrowUpInt,arrowNarrowInt)
        
        beamArrowInt = Wait(0)
        for i in range(24):
            hprFunc = Func(self.arrowRingRoot.setH,i*45)
            playRateFunc = Func(self.sfx_arrow.setPlayRate,i/10.0)
            soundPlayFunc = Func(self.sfx_arrow.play)
            beamArrowInt = Sequence(beamArrowInt,playRateFunc,soundPlayFunc,hprFunc,arrowMoveInt)
        
        hideArrowInt = Func(self.arrowRingRoot.hide)
        toGameplayInt = Func(self.request,'Gameplay')
        combinedInt = Sequence(Parallel(shellFadeInt,coreFadeInt,beamArrowInt),hideArrowInt,toGameplayInt)
        combinedInt.start()
    
    def enterGameplay(self):
        self.oldCamTime = 0
        
        self.ball.enable()
        self.accept('ball in track',self.handler_bounce)
        self.accept('ball again track',self.handler_bounce)
        self.accept('ball out track',self.handler_fall)
        
        self.accept('ball in gem',self.handler_collect)
        self.accept('ball in waypoint',self.handler_checkin)
        self.accept('ball in finish',self.handler_finish)
        self.accept('ball in ground',self.handler_die)
        
        self.accept('cam in track',self.handler_hideGeom)
        self.accept('cam out track',self.handler_showGeom)
        
        self.accept("newWiimoteData",self.onNewWiimoteData)
        if not taskMgr.hasTaskNamed("camera control task"):
            taskMgr.add(self.camUpdateTask,"camera control task")
        
        # debug
        self.ballUpKey = False
        self.accept("arrow_left",self.debugBallLeft)
        self.accept("arrow_left-up",self.debugBallStopLeft)
        self.accept("arrow_right",self.debugBallRight)
        self.accept("arrow_right-up",self.debugBallStopRight)
        self.accept("arrow_up",self.debugBallGo)
        self.accept("arrow_up-up",self.debugBallStopGo)
        self.accept("arrow_down",self.debugBallBrake)
        self.accept("arrow_down-up",self.debugBallStopBrake)
        self.accept("space",self.debugBallTurbo)
        self.accept("space-up",self.debugBallStopTurbo)
        if not taskMgr.hasTaskNamed("debug ball motion"):
            taskMgr.add(self.debugBallGoTask,"debug ball motion")
        self.accept("r",self.resetBall)
        self.accept("+",self.incCamMode)
        self.accept("-",self.decCamMode)
        
    def exitGameplay(self):
        self.ball.disable()
        self.ignore('ball in track')
        self.ignore('ball again track')
        self.ignore('ball out track')
        
        self.ignore('ball in gem')
        self.ignore('ball in waypoint')
        self.ignore('ball in ground')
        
        self.ignore('cam in track')
        self.ignore('cam out track')
        
        self.ignore("newWiimoteData")
        taskMgr.remove("camera control task")
        
        # debug
        self.ignore("arrow_left")
        self.ignore("arrow_left-up")
        self.ignore("arrow_right")
        self.ignore("arrow_right-up")
        self.ignore("arrow_up")
        self.ignore("arrow_up-up")
        self.ignore("arrow_down")
        self.ignore("arrow_down-up")
        self.ignore("space")
        self.ignore("space-up")
        taskMgr.remove("debug ball motion")
        self.ignore("r")
        self.ignore("+")
        self.ignore("-")
        
    def enterDie(self):
        shellFadeInt = self.ball.shell.colorInterval(duration=0.25,color=Vec4(.7,.6,0,0),startColor=Vec4(.7,.6,0,1))
        coreFadeInt = self.ball.core.colorInterval(duration=0.25,color=Vec4(0,.6,.1,0),startColor=Vec4(0,.6,.1,1))
        toSpawnInt = Func(self.request,'Spawn')
        combinedInt = Sequence(Parallel(shellFadeInt,coreFadeInt),toSpawnInt)
        combinedInt.start()
        
    def enterFinish(self):
        print "FINISH!"
        shellFadeInt = self.ball.shell.colorInterval(duration=2.0,color=Vec4(.7,.6,0,0),startColor=Vec4(.7,.6,0,1))
        coreFadeInt = self.ball.core.colorInterval(duration=1.75,color=Vec4(0,.6,.1,0),startColor=Vec4(0,.6,.1,1))
        risingInt = self.ball.root.posInterval(duration=2.0,pos=self.ball.root.getPos()+Vec3(0,0,2))
        
        self.arrowRingRoot.show()
        self.arrowRingRoot.setPos(self.ball.root.getPos()-Vec3(0,0,1))
        
        arrowUpInt = self.beamArrow.posInterval(duration=2.0/24.0, pos=Point3(0,2,4), startPos=Point3(0,2,-1))
        arrowNarrowInt = self.beamArrow.scaleInterval(duration=2.0/24.0, scale=Point3(3,1,4), startScale=Point3(3,1,2))
        arrowMoveInt = Parallel(arrowUpInt,arrowNarrowInt)
        
        beamArrowInt = Wait(0)
        for i in range(24):
            hprFunc = Func(self.arrowRingRoot.setH,i*45)
            playRateFunc = Func(self.sfx_arrow.setPlayRate,i/10.0)
            volumeFunc = Func(self.sfx_arrow.setVolume,(24-i)/24.0)
            soundPlayFunc = Func(self.sfx_arrow.play)
            beamArrowInt = Sequence(beamArrowInt,playRateFunc,soundPlayFunc,volumeFunc,hprFunc,arrowMoveInt)
        
        hideArrowInt = Func(self.arrowRingRoot.hide)
        toTitleInt = Func(self.request,'Title')
        volumeFunc = Func(self.sfx_arrow.setVolume,1.0)
        combinedInt = Sequence(Parallel(shellFadeInt,coreFadeInt,beamArrowInt,risingInt),hideArrowInt,toTitleInt,volumeFunc)
        combinedInt.start()
        
    def enterTitle(self):
        self.titleFlat.flat.show()
        
        self.ballSpawn = BALL_START
        self.ballSpawnH = 0
        
        self.initGems()
        
        self.previewSeg=0
        self.setupPreviewCam()
        
        self.accept('space',self.request,['Spawn'])
        
    def exitTitle(self):
        self.chainCounter = 0
        self.gemCount = 0
        self.score=0
        self.updateGemCountFlats()
        self.updateScoreFlats()
        
        self.camPreviewInt.finish()
        taskMgr.remove("preview cam")
        self.ignore('space')
        self.titleFlat.flat.hide()
        
    def setupPreviewCam(self):
        dummy = NodePath("camera preview dummy")
        dummy.reparentTo(render)
        segindex = self.previewSeg
        nextseg = segindex
        while nextseg == segindex:
            nextseg = randint(0,len(self.currentTrack)-1)
        self.previewSeg = nextseg
        trackseg = self.currentTrack[segindex]
        center = (trackseg.getChildren()[0].find("locator").getPos(render) + trackseg.getPos(render))*.5
        dummy.setPos(center)
        base.cam.reparentTo(dummy)
        base.cam.setPos(0,-50,30)
        base.cam.lookAt(dummy)
        detachCamInt = Func(base.cam.wrtReparentTo,render)
        killDummyInt = Func(dummy.removeNode)
        q1 = dummy.hprInterval(duration=3.0,hpr=Vec3(90,0,0),startHpr=Vec3(0,0,0))
        q2 = dummy.hprInterval(duration=3.0,hpr=Vec3(180,0,0),startHpr=Vec3(90,0,0))
        q3 = dummy.hprInterval(duration=3.0,hpr=Vec3(270,0,0),startHpr=Vec3(180,0,0))
        q4 = dummy.hprInterval(duration=3.0,hpr=Vec3(360,0,0),startHpr=Vec3(270,0,0))
        self.camPreviewInt.finish()
        self.camPreviewInt = Sequence(q1,q2,q3,q4,detachCamInt,killDummyInt)
        self.camPreviewInt.start()
        taskMgr.doMethodLater(12.5,self.setupPreviewCam,"preview cam",extraArgs=[])
        
    # handles ball-hitting-gem
    def handler_collect(self,entry):
        #print entry.getIntoNodePath().getParent()
        p = entry.getIntoNodePath().getPos(render)
        entry.getIntoNodePath().getParent().detachNode()
        sparkle = Flat(self.sparkleFrames,base.cam)
        sparkle.flat.reparentTo(render)
        sparkle.flat.setPos(p)
        sparkle.playForwards(15,7)
        taskMgr.doMethodLater(1,sparkle.destroy,name="kill sparkle",extraArgs=[])
        self.sfx_pickup.setPlayRate(1.0+self.chainCounter/10.0)
        self.sfx_pickup.play()
        if self.chainCounter<10:
            self.chainCounter+=1
        if taskMgr.hasTaskNamed("reset chain counter"):
            taskMgr.remove("reset chain counter")
        taskMgr.doMethodLater(1.0,self.resetChainCounter,name="reset chain counter",extraArgs=[])
        self.gemCount += 1
        self.score += 10*self.chainCounter
        self.updateGemCountFlats()
        self.updateScoreFlats()
        return
    
    # handles ball-hitting-groundplane
    def handler_die(self,entry):
        self.request("Die")
        return

    def handler_checkin(self,entry):
        print "waypoint!"
        self.ballSpawn = entry.getIntoNodePath().getPos(render)+Point3(0,0,3)
        self.ballSpawnH = entry.getIntoNodePath().getH()
        
        p = entry.getSurfacePoint(render)
        n = entry.getSurfaceNormal(render)
        ripple = Flat(self.wayrippleFrames)
        ripple.flat.reparentTo(render)
        ripple.flat.setPos(p)
        ripple.flat.lookAt(p+n)
        ripple.flat.setScale(10)
        ripple2 = Flat(self.wayrippleFrames)
        ripple2.flat.reparentTo(render)
        ripple2.flat.setPos(p)
        ripple2.flat.lookAt(p-n)
        ripple2.flat.setScale(10)
        ripple.playForwards(30,6)
        ripple2.playForwards(30,6)
        taskMgr.doMethodLater(.5,ripple.destroy,name="kill ripple",extraArgs=[])
        taskMgr.doMethodLater(.5,ripple2.destroy,name="kill ripple2",extraArgs=[])
        
        self.sfx_waypoint.play()
    
    def handler_finish(self,entry):
        self.request("Finish")
    
    # handles ball-hitting-track.
    # additionally handles ball-hitting-anything-else :P
    def handler_bounce(self,entry):
        point = entry.getSurfacePoint(render)
        normal = entry.getSurfaceNormal(render)
        normal.normalize()
        #print (point + normal*self.ball.radius)-self.ball.root.getPos(render),normal
        
        # you'd think this would work, but when there's any lag between the
        # collision and its handling, e.g. enough that the ball has already
        # moved away on its own, the ball gets jumped backwards, leading to
        # jitter, and in some cases effective velocity reduction.
        #self.ball.root.setPos(point + normal*self.ball.radius) 
        
        penetrationDist = (entry.getInteriorPoint(render)-point).length()
        self.ball.root.setPos(self.ball.root.getPos() + normal*penetrationDist)
        
        self.ball.lastNormal = normal
        
        velocity = self.ball.velocity
        
        # actually the length of the normal.  The direction comes from normal itself.
        # note to self: this points into the surface as such
        normComponent = velocity.dot(normal) 
        #print velocity, normal, normComponent
        onSurface = (velocity-normal*normComponent) # cut the normal component
        
        #print velocity.length(), normComponent, onSurface.length()
        
        if abs(normComponent)>.5*(onSurface.length()) and abs(normComponent)>.5:
            reflectAmount = 1.0+self.ball.elasticity
            reflected = (velocity-normal*reflectAmount*normComponent) # reverse the normal component
            #print velocity, reflected
            self.ball.velocity = reflected
            self.ball.grounded = False # should be already, but...
            #print abs(normComponent)
            if abs(normComponent)>.75:
                crash = randint(0,len(self.sfx_crashes)-1)
                self.sfx_crashes[crash].setVolume(min(6.0,abs(normComponent)/6.0))
                self.sfx_crashes[crash].play()
            #print "bounce..."
        else:
            self.ball.velocity = onSurface
            taskMgr.remove("unground ball")
            self.ball.grounded = True
            #print "land."
    
    # handles ball leaving track
    def handler_fall(self,entry):
        #self.ball.grounded=False
        if taskMgr.hasTaskNamed("unground ball"):
            taskMgr.remove("unground ball")
        taskMgr.doMethodLater(0.1,self.ball.unground,"unground ball",extraArgs=[])
        
    def handler_hideGeom(self,entry):
        if entry.getIntoNode().getName()=="ball" or entry.getIntoNode().getName()=="gem":
            print "discarding cam-ball or cam-gem collision"
            return
        print "clipping ", entry.getIntoNode().getName()
        ## if entry.getIntoNodePath().getParent().getParent() in self.collidersToTracks:
            ## geom = self.collidersToTracks[entry.getIntoNodePath().getParent().getParent()]
            ## #geom.hide()
            ## color=geom.getColor()
            ## geom.setColor(color[0],color[1],color[2],.5)
        if entry.getIntoNodePath().getParent().getParent().getParent() in self.currentTrack:
            geom = entry.getIntoNodePath().getParent().getParent().getParent()
            #geom.hide()
            color=geom.getColor()
            geom.setColor(color[0],color[1],color[2],.5)
        pass
    
    def handler_showGeom(self,entry):
        if entry.getIntoNode().getName()=="ball" or entry.getIntoNode().getName()=="gem":
            print "discarding cam-ball or cam-gem collision"
            return
        print "no more clip"
        ## if entry.getIntoNodePath().getParent().getParent() in self.collidersToTracks:
            ## geom = self.collidersToTracks[entry.getIntoNodePath().getParent().getParent()]
            ## #geom.show()
            ## color=geom.getColor()
            ## geom.setColor(color[0],color[1],color[2],1)
        if entry.getIntoNodePath().getParent().getParent().getParent() in self.currentTrack:
            geom = entry.getIntoNodePath().getParent().getParent().getParent()
            #geom.show()
            color=geom.getColor()
            geom.setColor(color[0],color[1],color[2],1)
        pass
        
    def camUpdateTask(self,task):
        dt = task.time - self.oldCamTime
        self.oldCamTime = task.time
        pullbackDist = 20
        minVelToTrack = 3
            
        if self.camMode == 0 or (self.camMode == 1 and self.ball.spinrate<0):# or not self.ball.grounded:
            ballH = self.ball.root.getH()
            xDist = pullbackDist*sin(radians(ballH))
            yDist = -pullbackDist*cos(radians(ballH))
            idealPos = self.ball.root.getPos() + Vec3(xDist,yDist,3)
            dist = idealPos - base.cam.getPos()
            # if cam is not in snap/look mode, move it gradually
            # otherwise, snap it and adjust for look
            if self.camTrackRate>0:
                base.cam.setPos(base.cam.getPos() + dist*self.camTrackRate*dt)
                base.cam.lookAt(self.ball.root)
            else:
                base.cam.setPos(idealPos)
                base.cam.lookAt(self.ball.root)
                dummy = NodePath("dummy")
                dummy.reparentTo(render)
                dummy.setPos(self.ball.root.getPos())
                dummy.setH(self.ball.root.getH(render))
                base.cam.wrtReparentTo(dummy)
                dummy.setH(self.ball.root.getH(render)+180*self.camLookX)
                dummy.setP(180*self.camLookY)
                base.cam.wrtReparentTo(render)
                dummy.removeNode()
        elif self.camMode == 1:
            ballRight = Vec3(cos(radians(self.ball.root.getH())),sin(radians(self.ball.root.getH())),0) # ball's right-vector
            ballRight.normalize()
            if self.ball.velocity.length()>=minVelToTrack:
                ballForward = self.ball.velocity / self.ball.velocity.length()
                #ballForward.normalize() # somehow, unless we divide in the assignment, we only make a new reference and the actual velocity is normalized...?
            else:
                ballForward = Vec3(-sin(radians(self.ball.root.getH())),cos(radians(self.ball.root.getH())),0)
        
            ballUp = ballRight.cross(ballForward)
            #print ballRight,ballForward,ballUp
            idealPos = self.ball.root.getPos() - ballForward*pullbackDist + ballUp*3
            dist = idealPos - base.cam.getPos()
            # if cam is not in snap/look mode, move it gradually
            # otherwise, snap it and adjust for look
            if self.camTrackRate>0:
                base.cam.setPos(base.cam.getPos() + dist*self.camTrackRate*dt)
                base.cam.lookAt(self.ball.root)
            else:
                base.cam.setPos(idealPos)
                base.cam.lookAt(self.ball.root)
                dummy = NodePath("dummy")
                dummy.reparentTo(render)
                dummy.setPos(self.ball.root.getPos(render))
                dummy.setH(self.ball.root.getH(render))
                base.cam.wrtReparentTo(dummy)
                dummy.setH(self.ball.root.getH(render)+180*self.camLookX)
                dummy.setP(180*self.camLookY)
                base.cam.wrtReparentTo(render)
                dummy.removeNode()
            
            ## base.cam.setPos(idealPos)
            
            ## camH = degrees(atan2(base.cam.getX()-self.ball.root.getX(),self.ball.root.getY()-base.cam.getY()))
            ## velXYcomp = sqrt(ballForward.getX()*ballForward.getX()+ballForward.getY()*ballForward.getY())
            ## camP = degrees(atan2(ballForward.getZ(),velXYcomp))# - 20
            ## camR=0
            ## print camH, camP
            ## if camP>180:
                ## camP=camP-360
            ## if camP>90:
                ## camP=180-camP
                ## camR=180
            ## if camP<-90:
                ## camP=-180-camP
                ## camR=180
                
            ## base.cam.setHpr(camH,camP,0)
            
            base.cam.lookAt(self.ball.root.getPos(),ballUp)
        else:
            base.cam.lookAt(self.ball.root)
            
            
        
        self.camCNP.setScale(1,base.cam.getDistance(self.ball.root),1)
        return Task.cont
    
    # for proper effect, we should never get any nearer to or further from
    # the skybox.
    def skyboxMoveTask(self,task):
        camX = base.cam.getX(render)
        camY = base.cam.getY(render)
        camZ = base.cam.getZ(render)
        self.skybox.setPos(camX,camY,camZ+10)
        return Task.cont
    
    def incCamMode(self):
        self.camMode = (self.camMode+1)%CAM_MODES
        self.camModeFlat.setFrame(self.camMode)
    def decCamMode(self):
        self.camMode = (self.camMode-1)%CAM_MODES
        self.camModeFlat.setFrame(self.camMode)
        
    def gemDrainTask(self,task):
        if self.gemCount>0:
            self.gemCount-=1
            self.updateGemCountFlats()
            darkInt = self.gemIcon.flat.colorInterval(duration=0.25, color=Vec4(0.5,0.5,0.5,1), startColor=Vec4(1,1,1,1))
            if self.gemCount>0:
                lightInt = self.gemIcon.flat.colorInterval(duration=0.25, color=Vec4(1,1,1,1), startColor=Vec4(0.5,0.5,0.5,1))
            else:
                lightInt = self.gemIcon.flat.colorInterval(duration=0.25, color=Vec4(0.1,0.1,0.1,1), startColor=Vec4(0.5,0.5,0.5,1))
            blinkInt = Sequence(darkInt,lightInt)
            blinkInt.start()
            return Task.again
        else:
            self.ball.turbo = False
        
    def resetChainCounter(self):
        self.chainCounter=0
        
    def updateScoreFlats(self):
        remainingScore = self.score
        powsOfTen = []
        for i in range(len(self.scoreFlats)):
            powsOfTen.append(int(pow(10,len(self.scoreFlats)-1-i)))
        
        digits = []
        for d in powsOfTen:
            digits.append(remainingScore - remainingScore%d)
            remainingScore-=digits[-1]
            digits[-1] = digits[-1]/d
            
        if digits[0]>10:
            digits[0]=digits[0]%10
        #print self.score, digits
        for i in range(len(self.scoreFlats)):
            self.scoreFlats[i].setFrame(digits[i])
        if self.gemCount>0:
            self.gemIcon.flat.setColor(1,1,1)
            
    def updateGemCountFlats(self):
        remainingGC = self.gemCount
        powsOfTen = []
        for i in range(len(self.gemCountFlats)):
            powsOfTen.append(int(pow(10,len(self.gemCountFlats)-1-i)))
        
        digits = []
        for d in powsOfTen:
            digits.append(remainingGC - remainingGC%d)
            remainingGC-=digits[-1]
            digits[-1] = digits[-1]/d
            
        if digits[0]>10:
            digits[0]=digits[0]%10
        #print self.score, digits
        for i in range(len(self.gemCountFlats)):
            self.gemCountFlats[i].setFrame(digits[i])
        
    def onNewWiimoteData(self):
        #print "entered newWiimoteData"
        # shaking the wiimote & NC in alternation produces movement
        #accel = abs(self.wm.NormAccelX)+abs(self.wm.NormAccelY)+abs(self.wm.NormAccelZ)
        #accel = abs(self.wm.NCNormAccelX)+abs(self.wm.NCNormAccelY)+abs(self.wm.NCNormAccelZ)
        # old UDP monitor returns signed vals in the single digits
        ## acX = self.wm.NormAccelX
        ## acY = self.wm.NormAccelY
        ## acZ = self.wm.NormAccelZ
        ## ncacX = self.wm.NCNormAccelX
        ## ncacY = self.wm.NCNormAccelY
        ## ncacZ = self.wm.NCNormAccelZ
        
        # new UDP monitor returns unsigned vals 0-300 (150 neutral)
        acX = (self.wm.NormAccelX-150)/50.0
        acY = (self.wm.NormAccelY-150)/50.0
        acZ = (self.wm.NormAccelZ-150)/50.0
        ncacX = (self.wm.NCNormAccelX-150)/50.0
        ncacY = (self.wm.NCNormAccelY-150)/50.0
        ncacZ = (self.wm.NCNormAccelZ-150)/50.0
        
        
        accel = abs(ncacX)+abs(ncacY)+abs(ncacZ) + abs(acX)+abs(acY)+abs(acZ)
        #accel = accel/2.0 
         
        WMsign = False
        NCsign = False
        print acZ
        if acZ>0:
            WMsign=True
        if ncacZ>0:
            NCsign=True
        if accel>1 and WMsign!=NCsign:
            self.ball.spinrate=self.ball.spinrate+2*accel
        #print "%f %f %f\n" % (self.wm.NormAccelX, self.wm.NormAccelY, self.wm.NormAccelZ)
        #self.maxAccel=max(self.maxAccel,max(abs(self.wm.NormAccelX),max(abs(self.wm.NormAccelY),abs(self.wm.NormAccelZ))))
        #print self.maxAccel
        
        # L/Z turn
        # L+Z brakes
        self.ball.turnLeft = self.wm.NCButtons[1]
        self.ball.turnRight = self.wm.Buttons[1]
        self.ball.setBrake(self.wm.NCButtons[1] and self.wm.Buttons[1])
        # A activates turbo
        if not self.ball.turbo and self.wm.Buttons[0] and self.gemCount>0:
            self.ball.turbo=True
            if not taskMgr.hasTaskNamed("turbo expend gems"):
                taskMgr.doMethodLater(1.0,self.gemDrainTask,"turbo expend gems")
        elif self.ball.turbo and not self.wm.Buttons[0]:
            self.ball.turbo = False
            taskMgr.remove("turbo expend gems")
        
        # NC C snaps cam to ideal view for mode
        if self.wm.NCButtons[0]:
            self.camTrackRate=-1
        else:
            self.camTrackRate = CAM_TRACK_RATE
            
        # d-pad up/down increase & decrease cam mode
        if self.wm.Buttons[2]:
            self.camIncButton = True
        else:
            if self.camIncButton:
                self.incCamMode()
            self.camIncButton = False
            
        if self.wm.Buttons[4]:
            self.camDecButton = True
        else:
            if self.camDecButton:
                self.decCamMode()
            self.camDecButton = False
            
        self.camLookX = self.wm.NCJoyX
        self.camLookY = self.wm.NCJoyY
        #print "new data",self.wm.Buttons
        
    def debugBallLeft(self):
        self.ball.turnLeft=True
    def debugBallStopLeft(self):
        self.ball.turnLeft=False
    def debugBallRight(self):
        self.ball.turnRight=True
    def debugBallStopRight(self):
        self.ball.turnRight=False
    def debugBallGo(self):
        self.ballUpKey = True
    def debugBallStopGo(self):
        self.ballUpKey = False
    def debugBallBrake(self):
        self.ball.setBrake(True)
    def debugBallStopBrake(self):
        self.ball.setBrake(False)
    def debugBallTurbo(self):
        self.ball.turbo=True
        if not taskMgr.hasTaskNamed("turbo expend gems"):
            taskMgr.doMethodLater(1.0,self.gemDrainTask,"turbo expend gems")
    def debugBallStopTurbo(self):
        self.ball.turbo=False
        taskMgr.remove("turbo expend gems")
    def debugBallGoTask(self,task):
        if self.ballUpKey:
            self.ball.spinrate=self.ball.spinrate+50.0
        return Task.cont
    def resetBall(self):
        self.ball.root.setPos(self.ballSpawn)
        self.ball.root.setHpr(0,0,0)
        self.ball.root.setH(self.ballSpawnH)
        self.ball.velocity=Vec3(0,0,0)
        self.ball.grounded=False
        self.ball.spinrate=0
        
    def finish(self):
        sys.exit()

m = main()
run()