# Author: Shao Zhang, Phil Saltzman, and Eddie Caanan
# Last Updated: 4/20/2005
#
# This tutorial shows how to play animations on models aka "actors".
# It is based on the popular game of "Rock 'em Sock 'em Robots".

import direct.directbase.DirectStart
from pandac.PandaModules import *
from direct.gui.DirectGui import *
from direct.interval.IntervalGlobal import *
from direct.actor import Actor
from random import random
import sys

class World(DirectObject):
  #Macro-like function used to reduce the amount to code needed to create the
  #on screen instructions
  def genLabelText(self, text, i):
    return OnscreenText(text = text, pos = (-1.3, .95-.05*i), fg=(1,1,1,1),
                        align = TextNode.ALeft, scale = .05)

  def __init__(self):
    #This code puts the standard title and instruction text on screen
    self.title = OnscreenText(text="Panda3D: Tutorial - Actors",
                              style=1, fg=(0,0,0,1),
                              pos=(0.8,-0.95), scale = .07)
    self.escapeEventText = self.genLabelText("ESC: Quit", 0)
    self.akeyEventText = self.genLabelText("[A]: Robot 1 Left Punch", 1)
    self.skeyEventText = self.genLabelText("[S]: Robot 1 Right Punch", 2)
    self.kkeyEventText = self.genLabelText("[K]: Robot 2 Left Punch", 3)
    self.lkeyEventText = self.genLabelText("[L]: Robot 2 Right Punch", 4)
  
    #Set the camera in a fixed position
    base.disableMouse()
    camera.setPosHpr(14.5, -15.4, 14, 45, -14, 0)
    base.setBackgroundColor( 0, 0, 0 )

    #Add lighting so that the objects are not drawn flat
    self.setupLights()

    #Load the ring
    self.ring = loader.loadModel('models/ring')
    self.ring.reparentTo(render)

    #Models that use skeletal animation are known as Actors instead of models
    #Instead of just one file, the have one file for the main model, and an
    #additional file for each playable animation.
    #They are loaded using Actor.Actor instead of loader.LoadModel.
    #The constructor takes the location of the main object as with a normal model
    #and a dictionary (A fancy python structure that is like a lookup table)
    #that contains names for animations, and paths to the appropriate files
    self.robot1 = Actor.Actor('models/robot',
      {'leftPunch' : 'models/robot_left_punch', 
       'rightPunch' : 'models/robot_right_punch', 
       'headUp' : 'models/robot_head_up', 
       'headDown' : 'models/robot_head_down'})

    #Actors need to be positioned and parented like normal objects
    self.robot1.setPosHprScale(-1,-2.5,4,45,0,0,1.25,1.25,1.25)
    self.robot1.reparentTo(render)

    #We'll repeat the process for the second robot. The only thing that changes
    #here is the robot's color and position
    self.robot2 = Actor.Actor('models/robot',
      {'leftPunch' : 'models/robot_left_punch',
       'rightPunch' : 'models/robot_right_punch',
       'headUp' : 'models/robot_head_up',
       'headDown' : 'models/robot_head_down'})

    #Set the properties of this robot
    self.robot2.setPosHprScale(1,1.5,4,225,0,0,1.25,1.25,1.25)
    self.robot2.setColor(Vec4(.7,0,0,1))
    self.robot2.reparentTo(render)

    #Now we define how the animated models will move. Animations are played
    #through special intervals. In this case we use actor intervals in a
    #sequence to play the part of the punch animation where the arm extends,
    #call a function to check if the punch landed, and then play the part of the
    #animation where the arm retracts

    #Punch sequence for robot 1's left arm
    self.robot1.punchLeft = Sequence(
      #Interval for the outstreched animation
      self.robot1.actorInterval('leftPunch', startFrame = 1, endFrame = 10),
      Func(self.checkPunch, 2), #Function to check if the punch was successful
      #Interval for the retract animation
      self.robot1.actorInterval('leftPunch', startFrame = 11, endFrame = 32))
    
    #Punch sequence for robot 1's right arm
    self.robot1.punchRight = Sequence(
      self.robot1.actorInterval('rightPunch', startFrame = 1, endFrame = 10),
      Func(self.checkPunch, 2),
      self.robot1.actorInterval('rightPunch', startFrame = 11, endFrame = 32))

    #Punch sequence for robot 2's left arm
    self.robot2.punchLeft = Sequence(
      self.robot2.actorInterval('leftPunch', startFrame = 1, endFrame = 10),
      Func(self.checkPunch, 1),
      self.robot2.actorInterval('leftPunch', startFrame = 11, endFrame = 32))

    #Punch sequence for robot 2's right arm
    self.robot2.punchRight = Sequence(
      self.robot2.actorInterval('rightPunch', startFrame = 1, endFrame = 10),
      Func(self.checkPunch, 1),
      self.robot2.actorInterval('rightPunch', startFrame = 11, endFrame = 32))

    #We use the same techinique to create a sequence for when a robot is knocked
    #out where the head pops up, waits a while, and then resets
    
    #Head animation for robot 1
    self.robot1.resetHead = Sequence(
      #Interval for the head going up. Since no start or end frames were given,
      #the entire animation is played.
      self.robot1.actorInterval('headUp'), 
      Wait(3),
      #The head down animation was animated a little too quickly, so this will
      #play it at 75% of it's normal speed
      self.robot1.actorInterval('headDown', playRate = .75))
    
    #Head animation for robot 2
    self.robot2.resetHead = Sequence(
      self.robot2.actorInterval('headUp'),
      Wait(3),
      self.robot2.actorInterval('headDown', playRate = .75))
    
    #Now that we have defined the motion, we can define our key input.
    #Each fist is bound to a key. When a key is pressed, self.tryPunch checks to
    #make sure that the both robots have their heads down, and if they do it
    #plays the given interval
    self.accept('escape', sys.exit)
    self.accept('a', self.tryPunch, [self.robot1.punchLeft])
    self.accept('s', self.tryPunch, [self.robot1.punchRight])
    self.accept('k', self.tryPunch, [self.robot2.punchLeft])
    self.accept('l', self.tryPunch, [self.robot2.punchRight])
  
  #tryPunch will play the interval passed to it only if
  #-neither robot has 'resetHead' playing (a head is up) AND
  #-the punch interval passed to it is not already playing
  def tryPunch(self, interval):
    if (not self.robot1.resetHead.isPlaying() and
      not self.robot2.resetHead.isPlaying() and
      not interval.isPlaying()):
      interval.start()
    
  #checkPunch will determine if a successful punch has been thrown  
  def checkPunch(self, robot):
    if robot == 1:
    #punch is directed to robot 1
      #if robot 1 is playing'resetHead', do nothing
      if self.robot1.resetHead.isPlaying(): return 
      #if robot 1 is not punching...
      if (not self.robot1.punchLeft.isPlaying() and 
          not self.robot1.punchRight.isPlaying()):
        #...15% chance of successful hit
        if random() > .85: self.robot1.resetHead.start() 
      #Otherwise, only 5% chance of sucessful hit
      elif random() > .95: self.robot1.resetHead.start() 
    else:
      #punch is directed to robot 2, same as above
      if self.robot2.resetHead.isPlaying(): return
      if (not self.robot2.punchLeft.isPlaying() and
        not self.robot2.punchRight.isPlaying()):
        if random() > .85: self.robot2.resetHead.start()
      elif random() > .95: self.robot2.resetHead.start()

  #This function sets up the lighting
  def setupLights(self):
    lAttrib = LightAttrib.makeAllOff()
    ambientLight = AmbientLight( "ambientLight" )
    ambientLight.setColor( Vec4(.8, .8, .75, 1) )
    lAttrib = lAttrib.addLight( ambientLight )
    directionalLight = DirectionalLight( "directionalLight" )
    directionalLight.setDirection( Vec3( 0, 0, -2.5 ) )
    directionalLight.setColor( Vec4( 0.9, 0.8, 0.9, 1 ) )
    lAttrib = lAttrib.addLight( directionalLight )
    render.attachNewNode( directionalLight.upcastToPandaNode() ) 
    render.attachNewNode( ambientLight.upcastToPandaNode() ) 
    render.node().setAttrib( lAttrib )

w = World()
run()

