import direct.directbase.DirectStart    # starts the panda window
from pandac.PandaModules import *       # basic panda modules
from direct.actor import Actor          # Actor class
from direct.interval.IntervalGlobal import *    # Intervals (Parallel, Sequence, etc)
from direct.gui.DirectGui import *              # 2D gui elements
from direct.showbase.DirectObject import DirectObject   # for event handling
from direct.fsm import FSM
from direct.fsm import State
from random import *

class PoundMain(DirectObject):
    def __init__(self):
        #Initial Constants
        self.g = -32
        self.curDir = None; self.nextDir = None
        self.numBuildings = 0
        
        base.disableMouse()
        camera.setPosHpr(0,-86,150,0,-60,0)
        camera.reparentTo(render)
        self.setupLighting()

        self.loadModels()
        self.loadSounds()
        self.setupKeys()
        
        self.walkInt = Sequence(
            Parallel(
            self.pc.find('**/LeftLeg').hprInterval(.1, Point3(0, -30, 0)),
            self.pc.find('**/RightWing').hprInterval(.1, Point3(0, 30, 0))),
            Parallel(
            self.pc.find('**/LeftLeg').hprInterval(.2, Point3(0, 30, 0)),
            self.pc.find('**/RightLeg').hprInterval(.2, Point3(0, -30, 0)),
            self.pc.find('**/LeftWing').hprInterval(.2, Point3(0, -30, 0)),
            self.pc.find('**/RightWing').hprInterval(.2, Point3(0, 30, 0))),
            Parallel(
            self.pc.find('**/LeftLeg').hprInterval(.2, Point3(0, -30, 0)),
            self.pc.find('**/RightLeg').hprInterval(.2, Point3(0, 30, 0)),
            self.pc.find('**/LeftWing').hprInterval(.2, Point3(0, 30, 0)),
            self.pc.find('**/RightWing').hprInterval(.2, Point3(0, -30, 0))),
            Parallel(
            self.pc.find('**/LeftLeg').hprInterval(.2, Point3(0, 30, 0)),
            self.pc.find('**/RightLeg').hprInterval(.2, Point3(0, -30, 0)),
            self.pc.find('**/LeftWing').hprInterval(.2, Point3(0, -30, 0)),
            self.pc.find('**/RightWing').hprInterval(.2, Point3(0, 30, 0))),
            Parallel(
            self.pc.find('**/LeftLeg').hprInterval(.2, Point3(0, -30, 0)),
            self.pc.find('**/RightLeg').hprInterval(.2, Point3(0, 30, 0)),
            self.pc.find('**/LeftWing').hprInterval(.2, Point3(0, 30, 0)),
            self.pc.find('**/RightWing').hprInterval(.2, Point3(0, -30, 0))),
            Parallel(
            self.pc.find('**/LeftLeg').hprInterval(.1, Point3(0, 0, 0)),
            self.pc.find('**/RightLeg').hprInterval(.1, Point3(0, 0, 0)),
            self.pc.find('**/LeftWing').hprInterval(.1, Point3(0, 0, 0)),
            self.pc.find('**/RightWing').hprInterval(.1, Point3(0,0, 0))))

    def deinit(self):
        self.ignoreAll()
        render.removeChildren()
        self.npc.forceTransition('Off')
        self.npc.seq.finish()
        self.npc.world = None
        del self.npc
        self.npc = None
 
    def setupKeys(self):
        self.acceptOnce('arrow_up', lambda: self.ButtonDown('arrow_up'))
        self.acceptOnce('arrow_left', lambda: self.ButtonDown('arrow_left'))
        self.acceptOnce('arrow_down', lambda: self.ButtonDown('arrow_down'))
        self.acceptOnce('arrow_right', lambda: self.ButtonDown('arrow_right'))
        self.accept('arrow_up-up', lambda: self.ButtonUp('arrow_up'))
        self.accept('arrow_left-up', lambda: self.ButtonUp('arrow_left'))
        self.accept('arrow_down-up', lambda: self.ButtonUp('arrow_down'))
        self.accept('arrow_right-up', lambda: self.ButtonUp('arrow_right'))

    def loadModels(self):
        def loadPC():
            self.pc = loader.loadModel('Penguin')
            self.pc.reparentTo(render)
            self.pc.setPos(0, 0, 0)
            self.pc.cell = Vec2(0, 0)
            self.pc.bMoving = 0
            self.pc.setScale(8)

        def loadBuildings():
            self.grid = [[None]*5]
            for x in range(4):
                self.grid.append([None]*5)

            for y in range(5):
                for x in range(5):
                    if random() > .33:
                        if random() > .5:
                            self.grid[y][x] = loader.loadModelCopy('Skyscraper1')
                        else:
                            self.grid[y][x] = loader.loadModelCopy('Skyscraper2')
                        self.grid[y][x].setScale(.1)
                        self.grid[y][x].reparentTo(render)
                        self.grid[y][x].setPos(x * 30, y * 30, 0)
                        self.numBuildings += 1

            if self.grid[int(self.pc.cell.getX())][int(self.pc.cell.getY())] != None:
                self.removeBuilding(int(self.pc.cell.getX()), int(self.pc.cell.getY()))

        def loadWorld():
            self.terrain = loader.loadModel('Ground2')
            self.terrain.reparentTo(render)

        def loadFence():
            self.fence = []
            transform = [(0, (52, -30, 0)),
                         (90, (135, 60, 0)),
                         (180, (52, 150, 0)),
                         (270, (-30, 60, 0))]
            for angle, pos in transform:
                elt = loader.loadModelCopy('fence')
                elt.reparentTo(render)
                elt.setScale(13, 10, 20)
                elt.setPos(pos[0], pos[1], pos[2])
                elt.setH(angle)
                self.fence.append(elt)

        loadWorld()
        loadPC()
        loadBuildings()
        self.smoke = Smoke()
        loadFence()
        self.npc=Builder('npc', self)

    def loadSounds(self):
        self.bounce = base.loadSfx('bounce.wav')
        self.crushSFX = base.loadSfx('crush.wav')

    def setupLighting(self):
        # start with a blank light attribute
        self.lightAttribute=LightAttrib.makeAllOff()
        
        # and a dim (.25) ambient light
        ambient=AmbientLight('ambient')
        ambient.setColor(Vec4(.5, .5, .5, 1))
        ambientNP=render.attachNewNode(ambient.upcastToPandaNode()) # crashes without .upcastToPandaNode()
        self.lightAttribute=self.lightAttribute.addLight(ambient) # add this light
        
        # add a directional light point down and forward
        dir=DirectionalLight('dir')
        dir.setColor(Vec4(.80, .80, .80, 1))
        dirNP=render.attachNewNode(dir.upcastToPandaNode())
        dirNP.setHpr(-30, -60, 0)
        self.lightAttribute=self.lightAttribute.addLight(dir)
        
        # set the world to use this lighting
        render.node().setAttrib(self.lightAttribute)
        # end setupLighting

    #Handle Keypresses
    def ButtonUp(self, button):
        self.acceptOnce(button, lambda: self.ButtonDown(button))
        if button == self.curDir:
            self.curDir = None
        elif button == self.nextDir:
            self.nextDir = None
    
    def ButtonDown(self, button):
        dirs = {'arrow_up': (Vec2(1, 0), 0), 'arrow_left': (Vec2(0, -1), 90), 
                'arrow_down': (Vec2(-1, 0), 180), 'arrow_right': (Vec2(0, 1), 270)}
        if not dirs.has_key(button): return
        if (self.curDir == None and (not self.pc.bMoving)
            and self.moveOkay(button)):
            dest = dirs[button][0] + self.pc.cell
            self.curDir = button
            self.MovePC(dest, dirs[button][0], dirs[button][1])
        else:
            self.nextDir = button

    def moveOkay(self, button):
        dirs = {'arrow_up': (Vec2(1, 0), 0), 'arrow_left': (Vec2(0, -1), 90), 
                'arrow_down': (Vec2(-1, 0), 180), 'arrow_right': (Vec2(0, 1), 270)}
        dest = dirs[button][0] + self.pc.cell
        return (dest.getX() >= 0 and dest.getY() >= 0 and
                dest.getX() < 5 and dest.getY() < 5 and
                self.grid[int(dest.getX())][int(dest.getY())] != 'ip')

    def continueMoving(self):
        dirs = {'arrow_up': (Vec2(1, 0), 0), 'arrow_left': (Vec2(0, -1), 90), 
                'arrow_down': (Vec2(-1, 0), 180), 'arrow_right': (Vec2(0, 1), 270)}
        if self.curDir != None and self.moveOkay(self.curDir):
            dest = dirs[self.curDir][0] + self.pc.cell
            self.MovePC(dest, dirs[self.curDir][0], dirs[self.curDir][1])
        elif self.nextDir != None and self.moveOkay(self.nextDir):
            self.curDir = self.nextDir
            self.nextDir = None
            dest = dirs[self.curDir][0] + self.pc.cell
            self.MovePC(dest, dirs[self.curDir][0], dirs[self.curDir][1])

    def SetMovingFlag(self, val):
        self.pc.bMoving = val

    #Penguin movement: Main function
    def MovePC(self, dest, dir, spin):
        turnInt = Sequence(Func(lambda:self.SetMovingFlag(1)))
        #Spin
        if self.pc.getH() != spin:
            if self.pc.getH() - spin > 180: self.pc.setH(self.pc.getH()-360)
            elif spin - self.pc.getH() > 180: self.pc.setH(self.pc.getH()+360)
            time = abs((self.pc.getH() - spin) / (90 * 3))
            turnInt.append(self.pc.hprInterval(time, Point3(spin, 0, 0)))
        if self.grid[int(dest.getX())][int(dest.getY())] != None:
            #Crush building
            turnInt.append(
                Parallel(Func(lambda: self.StartJump(dir)),
                         SoundInterval(self.bounce),
                         camera.posInterval(1, Point3(camera.getX()+dir.getY()*30,
                                                      camera.getY()+dir.getX()*30,
                                                      camera.getZ())),
            
                         self.pc.find('**/LeftWing').hprInterval(.25, Point3(0, -30, 75)),
                         self.pc.find('**/RightWing').hprInterval(.25, Point3(0, -30, -75))))
        else:
            #Walk to cell
            self.pc.cell += Vec2(dir.getX(), dir.getY())
            turnInt.append(Parallel(
                self.pc.posInterval(1, Point3(self.pc.cell.getY()*30,
                                              self.pc.cell.getX()*30,
                                              0)),
                self.walkInt,
                camera.posInterval(1, Point3(camera.getX()+dir.getY()*30,
                                             camera.getY()+dir.getX()*30,
                                             camera.getZ()))))
            turnInt.append(Func(lambda:self.SetMovingFlag(0)))
            turnInt.append(Func(self.continueMoving))
        self.pcMove = turnInt
        turnInt.start()
                
    def StartJump(self, dir):
        #Setup and start the jump task
        self.jmpTask = taskMgr.add(self.JumpTask, 'jump')
        self.jmpTask.startPos = Point3(self.pc.cell.getY()*30, self.pc.cell.getX() *30, 0)
        self.pc.movV = Vec3(dir.getY()*36, dir.getX()*36, 32)
        self.pc.cell += Vec2(dir.getX(), dir.getY())
        

    def JumpTask(self, task):
        #Set the position
        newPos = task.startPos + self.pc.movV*task.time
        newPos.setZ(self.g*(task.time**2)+self.pc.movV.getZ()*task.time)
        self.pc.setPos(newPos.getX(), newPos.getY(), newPos.getZ()*6)

        #At the end of the jump, start other intervals
        if self.pc.getZ() <= 30 and task.time > .5:
            bldg = self.grid[int(self.pc.cell.getX())][int(self.pc.cell.getY())]
            crush = Sequence(
                Parallel(
                bldg.scaleInterval(.75, Point3(bldg.getScale().getX(), bldg.getScale().getY(), 0)),
                self.pc.posInterval(.75, Point3(self.pc.getPos().getX(), self.pc.getPos().getY(), 0)),
                LerpFunc(self.smoke.shake, duration=.75,
                         fromData=0, toData=40)),
                Parallel(
                self.pc.find('**/LeftWing').hprInterval(.1, Point3(0, 0, 0)),
                self.pc.find('**/RightWing').hprInterval(.1, Point3(0, 0, 0)),
                Func(self.smoke.hide),
                Func(lambda: self.removeBuilding(int(self.pc.cell.getX()), int(self.pc.cell.getY())))),
                Func(lambda: self.SetMovingFlag(0)),
                Func(self.continueMoving))
            self.smoke.show(self.pc.cell.getY(), self.pc.cell.getX())
            SoundInterval(self.crushSFX).start()
            crush.start()
            return Task.done
        else: return Task.cont

    def removeBuilding(self, x, y):
        self.grid[x][y].remove()
        self.grid[x][y] = None
        self.numBuildings -= 1
        if self.numBuildings == 0: game.request('Win')

class Builder(FSM.FSM):
    def __init__(self, name, world):
        FSM.FSM.__init__(self, name)
        self.world = world
        self.truck = loader.loadModel('CementTruck')
        self.truck.reparentTo(render)
        self.truck.setPos(105, 105, 10)
        self.truck.setScale(.4)
        self.cell = Vec2(4, 4)
        self.request('Move')
        self.smoke = Smoke()
        self.sound = base.loadSfx('build.wav')

    def turnSeq(self, hStart, hEnd, seq):
        if hStart == hEnd: return
        if hStart - hEnd > 180:   hStart -= 360
        elif hEnd - hStart > 180: hStart += 360
        time = abs((hStart - hEnd) / (90 * 3)) * 1.25
        seq.append(LerpFunc(self.truck.setH, fromData = hStart, toData = hEnd,
                            duration = time))
        
    def moveSeq(self, func, init, dist, seq):
        if abs(dist) < .5: return
        seq.append(LerpFunc(func, fromData = init, toData = init + dist*30,
                            duration = abs(dist*1.5)))

    def enterMove(self):
        if self.world == None: return

        free = []
        for x in range(5):
            for y in range(5):
                if self.world.grid[x][y] == None:
                    free.append(Vec2(x, y))
        dest = choice(free)

        #build interval
        initH = self.truck.getH()
        delta = dest - self.cell
        if int(delta.getY()) < 0: midH = 90
        elif int(delta.getY()) > 0: midH = 270
        else: midH = initH

        if int(delta.getX()) < 0: finH = 180
        elif int(delta.getX()) > 0: finH = 0
        else: finH = midH

        moveseq = Sequence()
        self.turnSeq(initH, midH, moveseq)
        self.moveSeq(self.truck.setX, self.truck.getX(), delta.getY(), moveseq)
        self.turnSeq(midH, finH, moveseq)
        self.moveSeq(self.truck.setY, self.truck.getY(), delta.getX(), moveseq)
        moveseq.append(Func(lambda: self.request('Build')))

        moveseq.start()
        self.seq = moveseq
        self.cell = dest

    def filterMove(self, request, *args):
        if request == 'Build': return request
        return None

    def enterBuild(self):
        if self.world == None: return
        self.bldg = None
        if self.world.pc.cell == self.cell:
            seq = Func(lambda: self.request('Move'))
            seq.start()
            return
        self.world.grid[int(self.cell.getX())][int(self.cell.getY())] = 'ip'
        if random() > .5:
            self.bldg = loader.loadModelCopy('Skyscraper1')
        else:
            self.bldg = loader.loadModelCopy('Skyscraper2')
        self.bldg.setScale(.1, .1, 0)
        self.bldg.reparentTo(render)
        self.bldg.setPos(self.cell.getY() * 30, self.cell.getX() * 30, 0)
        self.world.numBuildings += 1
        const = Sequence(
            Parallel(
            self.bldg.scaleInterval(2, Point3(self.bldg.getScale().getX(),
                                              self.bldg.getScale().getY(),
                                              .1)),
            LerpFunc(self.smoke.shake, duration=2, fromData=0, toData=50)),
            Func(lambda: self.request('Move')))
        self.smoke.show(self.cell.getY(), self.cell.getX())
        SoundInterval(self.sound).start()
        const.start()
        self.seq = const

    def filterBuild(self, request, *args):
        if request == 'Move': return request
        return None

    def exitBuild(self):
        if self.world == None: return
        self.world.grid[int(self.cell.getX())][int(self.cell.getY())] = self.bldg
        self.smoke.hide()
        
class Smoke:
    def __init__(self):
        self.s = loader.loadModelCopy('smoke')
        self.s.reparentTo(render)
        self.s.setScale(9)
        self.s.setP(-60)
        self.s.setTransparency(1)
        self.s.hide()

    def show(self, x, y):
        self.s.setPos(30*x, 30*y-12, 0)
        self.s.show()
        i = LerpFunc(self.s.setAlphaScale, duration = .25,
                     fromData = 0, toData = .75)
        i.start()

    def shake(self, val):
        startPos = self.s.getPos()
        self.s.setPos(startPos.getX()+(val%4-2)*.25,
                      startPos.getY(),
                      startPos.getZ()+(val%3-1.5)*.25)

    def hide(self):
        i = Sequence(LerpFunc(self.s.setAlphaScale, duration = .25,
                              fromData=.75, toData=0),
                     Func(self.s.hide))
        i.start()


class GameFlow(FSM.FSM):
    def __init__(self, name):
        FSM.FSM.__init__(self, name)
        self.request('Start')

    def enterStart(self):
        self.pic = OnscreenImage('splash.tif', scale = 1.35, pos = (0, 0, -.3))
        self.startButton = DirectButton(image='startButton.tif',
                                   command=lambda: self.request('Main'),
                                   scale = (.211, 0, .117),
                                   pos = (.7, 0, -.1))
    def filterStart(self, request, *args):
        if request == 'Main': return request
        return None
        
    def enterMain(self):
        render.removeChildren()
        self.pic.remove()
        self.startButton.remove()
        self.world = PoundMain()

    def filterMain(self, request, *args):
        if request == 'Win': return request
        return None

    def enterWin(self):
        self.world.deinit()
        self.world = None
        self.startButton = DirectButton(image='endScreen.tif',
                                        command=lambda: self.request('Main'),
                                        scale = (1.35, 0, 1.25))
    def filterWin(self, request, *args):
        if request == 'Main': return request


game = GameFlow('Game')
run()
