#----------------------------------------------------------------------
# Copyright (c) 2012, Guy Carver
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
#     * Redistributions of source code must retain the above copyright notice,
#       this list of conditions and the following disclaimer.
#
#     * Redistributions in binary form must reproduce the above copyright notice,
#       this list of conditions and the following disclaimer in the documentation
#       and/or other materials provided with the distribution.
#
#     * The name of Guy Carver may not be used to endorse or promote products # derived#
#       from # this software without specific prior written permission.#
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# FILE    ripoffv3.py
# BY      Guy Carver
# DATE    11/19/2012 06:16 PM
#----------------------------------------------------------------------

from time import clock
from scene import *
from sound import *
from threading import Thread, Event
from math import sin, cos, pi, sqrt, acos, hypot, modf
from random import random, randint, shuffle, uniform, choice

pi2 = pi * 2 #360 deg rotation in radians.
hpi = pi / 2 #90 deg rotation in radians.
g_scale = 16 #global scale value for mobs.
numcrates = 9 #Number of crates to start with.
cratespacing = 45 #Space between crates in rows and columns.
numbullets = 5 #Number of bullets players may shoot at a time.
aibulletspeed = 500 #Speed of AI bullets in pixels/second.
aibulletlife = .5 #Life time of AI bullets in seconds.
bulletspeed = 1000 #Player bullet speed in pixles/second.
movesense = 2
movescale = 2
turnscale = .05 #convert linear movement to angular radians/second.
uadj = pi / 16 #maximum random amount to adjust angular velocity by when unstable (on a crate)
expv = 32 #Maximum explosion velocity in pixles/second.
expav = pi #Maximum explosion angular velocity in radians/second.
expdur = .75 #Amount to reduce explosion alpha by per second.
blastdur = 2.5 #Amound to reduce blast alph by per second.
blastexp = 64 #Blast expansion rate in pixels/second.
deadtime = 5 #Seconds player stays dead.
screenrad = 100 #Distance from center of screen to a corner (calculated in Scene.setup())
tetherlen = 32 #Length of tether from robber to crate.
maxkillers = 4 #Maximum number of killer AI mobs.
firedelay = .5 #Delay between shots by killers when zeroed in.
robbercount = 6 #Number of robbers.
killerinterval = 4 #Waves between killer addition.
killerdowntime = 5 #Seconds killer remains dead before re-spawning.
killervel = 1.25 #Velocity scalar.
velbase = 100.0 #Base velocity for all AI.
velscale = 1.0 / 100.0 #Velocity increase per wave for robbers and killers.
mt = False #True = multi-thread AI update.  Actually slows things down a bit.
debug = False #Set to true to render waypoints and such.

crateverts = [Point(-.5, 0), Point(0, .75), Point(.5, 0), Point(0, -.75)]
cratesegs = [(0,1), (1,2), (2,3), (3,0), (0,2)]
cratemesh = (crateverts, cratesegs)

playerverts = [Point(0, .75), Point(1, 0), Point(.75, -.5), Point(.5, -.25),
               Point(-.5,-.25), Point(-.75,-.5), Point(-1, 0)]
playersegs = [(0,1),(1,2),(2,3),(3,4),(4,5),(5,6),(6,0)]
playermesh = (playerverts, playersegs)

robberverts = [Point(0, 1), Point(.5, .25), Point(.5, -.25), Point(0, -.75),
               Point(-.5, -.25), Point(-.5, .25)]
robbersegs = [(0,1), (1,2), (2,3), (3,4), (4,5), (5,0), (0, 3)] #(1,5), (2,4)]
robbermesh = (robberverts, robbersegs)

killerverts = [Point(0,1), Point(.35,0), Point(.5, -.75), Point(0,0),
               Point(-.5,-.75), Point(-.35,0)]
killersegs = [(0,1), (1,2), (2,3), (3,4), (4,5), (5,0)]
killermesh = (killerverts, killersegs)

sounds = ['Laser_6', 'Laser_3', 'Hit_3', 'Explosion_5','Clank','Ding_2']

#Waypoint list for robbers.
#tuple of distance from screen center, angle relative to current position in radians, min,max % of velbase.
pathrange = [(.85, pi, Point(.8, .6)), (.5, hpi, Point(.6, .5)), (.25, hpi / 2, Point(.5, .3)), (0, 0, Point(.3, .02)), (1.5, pi, Point(.6, .3))]
exitwp = len(pathrange) - 1 #Index for the exit waypoint.
pickupwp = exitwp - 1 #Index of the crate pickup waypoint.
wpskipchance = 0.1 #% chance of skipping an approach waypoint.
wpskipfactor = 0.01 #Amount to add to wpskipchance per wave.

playerfilter = 1 #Player bullet collision ID.
aifilter = 2 #AI bullet collision ID.

###-1 if < 0 otherwise 1
def sgn(val): return 1 if val >= 0 else -1
###squared length of given vector.
def lensq(vec): return (vec.x * vec.x + vec.y * vec.y)
###Dot product of p1, p2.
def dot(p1, p2): return p1.x * p2.x + p1.y * p2.y
###Center of segment p1-p2.
def center(p1, p2): return Point(p1.x + p2.x / 2, p1.y + p2.y / 2)
###Draw line between 2 points.
def drawline(p1, p2): line(p1.x, p1.y, p2.x, p2.y)

def normalize(pnt):
###Normalize given Point in place and return the length.
	len = hypot(pnt.x, pnt.y)
	pnt.x /= len
	pnt.y /= len
	return len

def segvcircle(p1, p2, circle):
	###Check intersection of segment p1-p2 with circle.
	###circle is a Vector3, z = radius.
	segv = Point(p2.x - p1.x, p2.y - p1.y)
	#Get vector from p1 to circle.
	cp1v = Point(circle.x - p1.x, circle.y - p1.y)

	segvn = Point(*segv) #Make copy of vector from p1 to p2.
	l = normalize(segvn) # and normalize.

	sl = dot(cp1v, segvn) #Get distance from between line and circle.
	c = Point(*p1) #Start and segment point 1.
	#If off the end of the segment use the end point of the segment.
	if sl >= l: c = Point(*p2)
	elif sl > 0: #Move point on segment until segment of this point to circle
							 # is perpendicular to p1-p2 segment.
		c.x += segvn.x * sl
		c.y += segvn.y * sl
	#Return true if distnce from this point to circle is < radius of circle.
	return (c.distance(circle) < circle.z)

def addangle(a1, a2):
	###return a1 + a2 making sure the result is within 0-2pi.
	a1 += a2
	while a1 < 0: a1 += pi2
	while a1 > pi2: a1 -= pi2
	return a1

def deltaangle(a0, a1):
	###Return shortest angle in radians between a1 and a0.
	### Result will -pi < delta < pi.
	a = a1 - a0
	b = abs(a)
	c = pi2 - b #Get rotation in oppisite direction.
	if b < c: return a #if 1st rotation is shorter then return it.
	else: return c * -sgn(a) #Otherwise return 2nd rotation setting the sign to opposite of 1st rotation.

def anglefromvector(vect):
	#Return Tuple (radian rotation, vect length) represented by given vector.
	len = hypot(vect.x, vect.y) #sqrt(lensq(vect))
	if len > 0: #If not 0 length.
		a = acos(vect.y / len) #Get angle from cos.
		#If x negative then angle is between pi-2pi
		if vect.x < 0: a = pi2 - a
	else: a = 0
	return (a, len)

def dampen(val, d):
	###Return given velocity dampened by given amount not letting velocity change signs.
	s = sgn(val)
	val -= s * d
	if sgn(val) != s: val = 0 #If sign changed clamp at 0.
	return val

def rotpoint(a, p):
	###Rotate point in place by angle.
	c = cos(a)
	s = sin(a)
	x = p.x * c + p.y * s
	p.y = p.y * c - p.x * s
	p.x = x

def clippoint(pnt, bound):
	###Clip pnt in place to given bound.
	l = bound.left()
	r = bound.right()
	t = bound.top()
	b = bound.bottom()
	pnt.x = max(l, min(r, pnt.x))
	pnt.y = max(b, min(t, pnt.y))

class mob(object):
	###Base class representing a transformable vector graphics image.
	def __init__(self, pos, scn, mesh):
		object.__init__(self)
		self.scene = scn #Scene to which mob belogs.
		self.pos = Point(*pos)
		self.filter = 0 #Bullet ID.
		self.scale = g_scale #Scale of the mesh.
		self.color = Color(.4, .8, 1) #Color of the mesh.
		self.mesh = mesh
		self.points = [Point(*p) for p in mesh[0]] #Make copy of points for transform.
		self.angle = 0
		self.dotrans = True #If true transform the points on update.
		self.on = False #Start turned off.

	def reset(self):
		self.angle = 0 #Reset the heading.
		self.on = True #Turn on.

	def boundcheck(self, bound):
		###Check circle representing mob bound against given bound.
		if not self.on: return False
		x = bound.x - self.pos.x
		y = bound.y - self.pos.y
		dsq = x * x + y * y
		r = self.scale + bound.z #Radius of mob is scale * 1.
		return dsq <= r * r  #Collide if distance squared < square of radius sum.

	def offscreen(self):
		###Check if mob is off screen.
		s = self.scale
		s2 = 2 * s
		#Make rectangle around mob.
		r = Rect(self.pos.x - s, self.pos.y - s, s2, s2)
		return not self.scene.bounds.intersects(r)

	def transformpoints(self):
		###Transform points
		c = cos(self.angle)
		s = sin(self.angle)

		#Local function to transform point.
		def trans(p, d):
			d.x = (p.x * c + p.y * s) * self.scale + self.pos.x
			d.y = (p.y * c - p.x * s) * self.scale + self.pos.y

		#Loop through points and transform into destination of self.points
		for i, p in enumerate(self.mesh[0]):
			trans(p, self.points[i])

	def draw(self):
		###If on draw the mob.
		if self.on:
			stroke(*self.color)
			stroke_weight(1)
			if self.dotrans: self.transformpoints()
			for p0, p1 in self.mesh[1]:	#Draw each segment.
				drawline(self.points[p0], self.points[p1])

	###Turn off.
	def kill(self): self.on = False

class explosion(object):
	###Object to represent an explosion consisting of the exploded
	### mesh segments and a blast circle.
	def __init__(self, mob):
		object.__init__(self)
		c = mob.color
		self.pos = Point(*mob.pos)
		self.alpha = 1
		self.color = Color(c.r, c.g, c.b, 1)
		self.angle = mob.angle
		self.blast = 4 #Stroke weight of blast circle.
		numsegs = len(mob.mesh[1])

		###Make and explosion segment from the given segment index.
		###Returns a Tuple of Vector3 for point and angle, vector of segment, Vector3 of velocities).
		def make(index):
			xv = uniform(-expv, expv) #Set random x,y velocities.
			yv = uniform(-expv, expv) #random() * expv * 2 - expv
			av = uniform(-expav, expav) #Set random angular velocity.
			i0, i1 = mob.mesh[1][index] #Get point indices for this segment.
			p0 = mob.points[i0] #Get point 0.
			p1 = Point(*mob.points[i1]) #Get copy of point 1.
			p1.x -= p0.x #Convert point 1 into vector to point1 from point0.
			p1.y -= p0.y
			return (Vector3(p0.x,p0.y,0), Point(p1.x, p1.y), Vector3(xv, yv, av))

		#Make list of segments representing exploded mesh.
		self.segs = [make(i) for i in xrange(numsegs)]
		set_volume(0.5)
		play_effect(sounds[3]) #Play explosion mesh.

	def update(self, dt):
		###Update explosion and return true when off.
		on = self.color.a > 0 #On as long as alpha isn't 0.
		if on:
			self.color.a = max(0, self.color.a - expdur * dt)
			self.alpha = max(0, self.alpha - blastdur * dt)
			self.blast += blastexp * dt
			for p0, _, v in self.segs:
				p0.x += v.x * dt
				p0.y += v.y * dt
				p0.z = addangle(p0.z, v.z * dt)
		return not on

	def draw(self):
		###Draw the explosion.
		fill(0,0,0,0) #No fill color.
		if self.alpha:
			width = self.alpha * 32 #The stroke width is % of 32 pixels.
			asq = self.alpha * self.alpha
			#Red alwasy 1, green = 1.5 * alpha, blue = 1 * alpha squared.
			#This will make the color animate from white to yellow to orange.
			stroke(1,1.5 * self.alpha,1 * asq, self.alpha)
			stroke_weight(width)
			x = self.pos.x - self.blast #Get lower left corner of circle.
			y = self.pos.y - self.blast
			wh = self.blast * 2 #Diameter.
			ellipse(x, y, wh, wh) #Draw the explosion circle.

		stroke(*self.color)
		stroke_weight(1)
		#Now draw the segments.
		for p0, p1d, v in self.segs:
			p1 = Point(p1d.x, p1d.y)
			rotpoint(p0.z, p1) #Rotate the direction by the angle.
			p1.x += p0.x #p1 = p0 + direction.
			p1.y += p0.y
			drawline(p0, p1)

class crate(mob):
	###Mob rebresenting a crate the robbers will attempt to steal.
	def __init__(self, pos, scn):
		mob.__init__(self, pos, scn, cratemesh)
		self.color = Color(0.80, 0.80, 0.20)
		self.reset()

	def reset(self):
		mob.reset(self)
		self.targeted = 0 #No robbers are targeting.
		self.tethered = None #No robber is tethered to the crate.
		self.dotrans = False #Don't transform the points each frame.
		self.transformpoints() #Transform the points 1 time.

	def kill(self):
		###Kill the crate.
		mob.kill(self)
		self.scene.killcrate(self) #Remove the create from the scene.
		set_volume(0.5)
		play_effect(sounds[4])

class bullet(object):
	###Object representing a shot bullet.
	def __init__(self, owner):
		object.__init__(self)
		self.owner = owner #Owner of this bullet.
		self.pos = Point(0,0)
		self.vel = Point(0,0)
		self.color = Color(1, 0.7, 0.7)
		self.life = 0 #Current life of the bullet.
		self.speed = bulletspeed
		self.lifespan = 1 #Maximum life span of bullet in seconds.

	###Return filter ID of the owner of this bullet.
	def getfilter(self): return self.owner.filter

	def turnon(self, pos, vel):
		###Turn the bullet on.
		self.life = self.lifespan #Reset the life span.
		self.pos = pos
		self.vel.x = vel.x * self.speed
		self.vel.y = vel.y * self.speed

	def update(self, scn, dt):
		###Update bullet and return true when turned off.
		if self.life: #If any life left.
			self.life = (max(0, self.life - dt)) #Reduce life.
			if self.life: #If still on.
				prev = Point(*self.pos)
				self.pos.x += self.vel.x * dt
				self.pos.y += self.vel.y * dt
				#Check segment representing bullet traversal against mobs in scene.
				if scn.checkbullet(prev, self.pos, self.owner):
					self.life = 0 #Hit something so turn off.
			if not self.life:
				self.owner.shotcount -= 1 #reduce shot count on owner if bullet turned off.
				return True
		return False

	def draw(self):
		###Draw bullet if on.
		if self.life:
			stroke(*self.color)
			stroke_weight(4)
			x1 = self.pos.x
			x2 = x1 + 2
			y1 = self.pos.y
			y2 = y1 + 2
			line(x1, y1, x2, y2)

class machine(mob):
	###Base class for player and AI machines.  You will note some
	### features that are not used by all machines.  They are here
	### just in case.  For instance making robbers shoot.
	def __init__(self, pos, filter, scn, mesh, bulletcount):
		mob.__init__(self, pos, scn, mesh)
		self.filter = filter #Set bullet ID.
		self.brk = 200 #Break amount in pixels/second.
		self.brka = pi * 2 #Angular break amount int rads/second.
		self.shotcount = 0 #Number of shots.
		#Create bullets.
		self.bullets = [bullet(self) for i in xrange(bulletcount)]
		self.shoot = sounds[0] #Set shooting sound.
		self.shootv = 0.6 #Shot sound volume.

	###Reset the mob.
	def reset(self):
		mob.reset(self)
		self.avel = 0
		self.vel = Point(0, 0)
		self.wrap = True  #Wrap mob around on screen as opposed to cliping on edges.

	###Apply breaks to liner velocity.
	def slowdown(self, dt): self.vel.y = dampen(self.vel.y, self.brk * dt)

	###Get screen position and shot direction.
	def shotpos(self):
		p = Point(*self.points[0]) #Shot comes out of point 0.
		v = Point((p.x - self.pos.x) / self.scale, (p.y - self.pos.y) / self.scale)
		return (p, v)

	###Shoot a bullet.
	def fire(self):
		if self.on and self.shotcount < len(self.bullets):
			for b in self.bullets:
				if not b.life: #Find unused bullet.
					b.turnon(*self.shotpos())
					self.shotcount += 1
					set_volume(self.shootv)
					play_effect(self.shoot)
					self.scene.activebullets.append(b) #Add bullet to scene.
					return

	###Update machine.
	def update(self, dt):
		if self.on:
			v = Point(*self.vel)
			rotpoint(self.angle, v)
			self.pos.x += v.x * dt #Add velocity to position.
			self.pos.y += v.y * dt
			if self.wrap: #If wraping on screen then do so.
				sz = self.scene.size

				if self.pos.x > sz.w:
					self.pos.x -= sz.w
				elif self.pos.x < 0:
					self.pos.x += sz.w

				if self.pos.y > sz.h:
					self.pos.y -= sz.h
				elif self.pos.y < 0:
					self.pos.y += sz.h
			#Adjust direction by angular velocity.
			self.angle = addangle(self.angle, self.avel * dt)

class aimachine(machine):
	###Base class for the AI controlled machines.
	def __init__(self, scn, mesh, numbullets):
		machine.__init__(self, Point(0,0), aifilter, scn, mesh, numbullets)
		self.on = False #Start turned off.
		self.brka = 0 #No angular velocity break.

	def reset(self, pos, angle):
		machine.reset(self)
		self.wrap = False #Don't wrap AI mobs on screen.
		self.pos = pos
		self.angle = angle
		self.minvel = 0
		self.maxvel = 0
		self.wp = Point(0,0)
		self.wpn = Point(0,0)
		self.nextwaypoint() #Set 1st waypoint.

	###Return base velocity adjust by the wave count.
	def basevel(self): return velbase + (velbase * float(self.scene.wave) * velscale)

	def updatevels(self, dt):
	###Update the linear and angular velocities.
		vect = Point(*self.wp)
		vect.x -= self.pos.x
		vect.y -= self.pos.y
		a, l = anglefromvector(vect) #Get angle, distance to target.
		self.avel = deltaangle(self.angle, a) #Get delta angle to target.
		self.vel = Point(0, max(self.minvel, min(self.maxvel, l))) #Set velocity based on target distance.

	def checkdest(self):
		###Check to see if we have reached target waypoint.
		delta = Point(self.pos.x - self.wp.x, self.pos.y - self.wp.y)
		l = lensq(delta)
		#If distance < minimum velocity.
		if l < (self.minvel * self.minvel):
			d = dot(delta, self.wpn) #See if we are on other side of waypoint normal.
			return d <= 0
		return False

	def update(self, dt):
		###Update the AI machine if on. return 1 if updated else 0.
		if self.on:
			self.state(dt) #Call the state function.
			machine.update(self, dt) #Call base update method.
			return 1
		return 0

class robber(aimachine):
	###Robber AI machine.
	def __init__(self, scn):
		aimachine.__init__(self, scn, robbermesh, 0)

	def reset(self, pos, angle, tgt):
		self.state = self.approachstate #Start with target approach state.
		self.wpindex = -1 #Start with waypoint index -1 so 1st index will be 0 when NextWaypoint is called.
		self.approacha = 0 #Approach angle of 0.
		self.target = tgt #Target crate.
		if tgt: tgt.targeted += 1
		aimachine.reset(self, pos, angle)

	def setexit(self)	:
		###Set exit state and tether to target crate.
		self.state = self.exitstate
		self.wrap = False
		tgt = self.target
		if not tgt.tethered: #If target not tethered then tether to it.
			b = Vector3(self.pos.x, self.pos.y, self.scale)
			if tgt.boundcheck(b): #Make sure in range of target before tethering.
				tgt.tethered = self
				tgt.dotrans = True #start crate updating transform as we are going to move it.
				return
		#if didn't tether then follow crate (Another robber is tethered to it).
		self.wp = tgt.pos #Reference target position and follow that.
											# If target moves so will our way point.
		self.state = self.followstate

	def done(self):
		###The robber has exited the screen so turn off.
		self.on = False
		#If tethered to a crate the kill the crate.
		if self.target and self.target.tethered is self:
			self.target.kill()

	def kill(self):
		###Kill the robber.
		mob.kill(self)
		tgt = self.target
		if tgt: #If targeting a create stop targeting.
			self.target = None
			tgt.targeted -= 1
			if tgt.tethered is self: #If pulling a crate then stop pulling.
				tgt.tethered = None
				tgt.dotrans = False #Make crate not update it's transforms as it is no longer moving.

	def pullcrate(self, dt):
		###Pull the crate.
		tgt = self.target
		if tgt and tgt.tethered is self:
			if tgt.offscreen(): #If target off screen then kill robber/crate.
				self.done()
			else: #Move crate.
				p = tgt.pos
				d = Point(self.pos.x - p.x, self.pos.y - p.y)
				dm = Point(abs(d.x) - tetherlen, abs(d.y) - tetherlen)
				if dm.x > 0:
					p.x += dm.x * sgn(d.x)
				if dm.y > 0:
					p.y += dm.y * sgn(d.y)
		elif self.offscreen(): #If robber off screen then stop it.
			self.done()

	#Get screen location of tether point on mesh.
	def tetherpos(self): return self.points[3] #Point 3 is tether point.

	def nextwaypoint(self):
		###Set next waypoint.
		self.wpindex += 1
		#Run chance of skipping waypoint but only up to pickup waypoint.
		while self.wpindex < pickupwp and (random() < self.scene.wpskipchance):
			self.wpindex += 1
		self.setwaypoint() #Set the waypoint position and normal.
		if self.wpindex == exitwp: #If we are at the exit waypoint change states.
			self.setexit()

	def setwaypoint(self):
		###Set the waypoint.
		if self.target:
			rad, da, vels = pathrange[self.wpindex]
			v = self.basevel()
			self.maxvel = vels.x * v #Set maximum/minimum velcoties as % of base velocity.
			self.minvel = vels.y * v
			#Adjust current approach angle by delta angle from next waypoint.
			self.approacha = addangle(self.approacha, uniform(-da, da))
			self.wp = Point(0, rad * screenrad)
			rotpoint(self.approacha, self.wp)
			self.wp.x += self.target.pos.x
			self.wp.y += self.target.pos.y
			if self.wpindex < exitwp: #don't clip exit waypoint as it takes us off screen.
				clippoint(self.wp, self.scene.bounds) #Clip point to screen.
			#Waypoint normal from vector of pos-wp.
			self.wpn = Point(self.pos.x - self.wp.x, self.pos.y - self.wp.y)
			normalize(self.wpn)

	def followstate(self, dt):
		###Watch target and if no longer tethered then tether to it.
		if not self.target.tethered: #If not tethered.
			self.wpindex = pickupwp #Re-run pickup waypoint.
			self.state = self.approachstate #Switch back to approach state.
			self.setwaypoint() #Set pickup waypoint.
		else:
			self.exitstate(dt) #Run exit state.

	def approachstate(self, dt):
		###Approach target through series of waypoins.
		if self.checkdest(): #Check if reached waypoint.
			self.nextwaypoint() #Next waypoint.
		self.updatevels(dt) #Update the velocities to get to waypoint.
		machine.update(self, dt)

	def exitstate(self, dt):
		self.updatevels(dt) #Update velocities to get to waypoint.
		self.pullcrate(dt) #Pull the crate if tethered.  NOTE: We could make a
											 # exitpull state and not have to check if tethered in pullcrate().

	def draw(self):
		###Draw the robber if on.
		if self.on:
			mob.draw(self)
			tgt = self.target
			#If tethered then draw tether line.
			if tgt and tgt.tethered is self:
				stroke(1,1,1,.5)
				stroke_weight(1)
				tp = self.tetherpos()
				line(tp.x, tp.y, tgt.pos.x, tgt.pos.y)

			if debug: #If debug then draw the current waypoint.
				v = Point(self.wpn.x * 16, self.wpn.y * 16)
				v.x += self.wp.x
				v.y += self.wp.y
				stroke(1,0,0,1)
				stroke_weight(2)
				line(self.wp.x, self.wp.y, v.x, v.y)
				stroke(1,1,1,1)
				stroke_weight(1)
				line(self.wp.x, self.wp.y, self.wp.x + 1, self.wp.y + 1)
				tint(1,1,0,1)

class killer(aimachine):
	###Hunter Killer AI machine.
	def __init__(self, scn):
		aimachine.__init__(self, scn, killermesh, 2)
		self.color = Color(1.00, 0.00, 1.00)
		self.shoot = sounds[2]
		self.downtime = killerdowntime
		self.state = self.down

	def reset(self, pos, angle):
		aimachine.reset(self, pos, angle)
		v = killervel * self.basevel()
		self.minvel = v
		self.maxvel = v
		self.firedelay = 0 #Reset the fire delay timer.
		self.state = self.hunt #Start hunding.
		#Set bullet speed and lifespan again just in case they have changed.
		for b in self.bullets:
			b.speed = aibulletspeed
			b.lifespan = aibulletlife

	def nextwaypoint(self):
		###Randomly create a new waypoint on screen.
		self.wp = Point(randint(0, self.scene.size.w), randint(0, self.scene.size.h))
		self.wpn = Point(self.pos.x - self.wp.x, self.pos.y - self.wp.y)
		normalize(self.wpn)

	def kill(self):
		self.downtime = killerdowntime #Reset the down time countdown timer.
		aimachine.kill(self)
		self.state = self.down

	def down(self, dt):
		###Down state, remain so until downtime is up.
		self.downtime -= dt
		if self.downtime <= 0:
			p, a = self.scene.startpos()
			self.reset(p, a)

	def checkfire(self, dt):
		#Check to see if we should fire.
		if self.firedelay > 0: #Wait until delay timer is up.
			self.firedelay = max(0, self.firedelay - dt)
			return

		for p in self.scene.pl: #Check each player to see if in front of killer.
			v = Point(p.pos.x - self.pos.x, p.pos.y - self.pos.y)
			a1, _ = anglefromvector(v)
			da = deltaangle(self.angle, a1)
			if abs(da) < 0.07: #If delta angle is within range shoot.
				self.firedelay = firedelay
				self.fire()

	def hunt(self, dt):
		###State to keep going to random waypoints and checking for fire opportunities.
		if self.checkdest():
			self.nextwaypoint()
		self.updatevels(dt)
		self.checkfire(dt)
		machine.update(self, dt)

	def update(self, dt): self.state(dt)

###Machine to represent a player.
class player(machine):

	def __init__(self, pos, scn, dir):
		machine.__init__(self, pos, playerfilter, scn, playermesh, numbullets)
		self.dir = dir #+ or - 1.
		self.startpos = Point(*pos)
		self.control = 0
		self.unstable = False
		self.reset()

	###Reset the player to original position.
	def reset(self):
		machine.reset(self)
		self.dead = 0
		self.pos.x = self.startpos.x
		self.pos.y = self.startpos.y
		self.vel.y = self.scale * 8 #Start out with a velocity to move onto screen.
		self.angle = -hpi * self.dir
		self.destangle = self.angle

	###Kill the player.
	def kill(self):
		mob.kill(self)
		self.dead = deadtime #Start dead timer.

	###Move the player.
	def move(self, dt):
		if self.control:
			da = deltaangle(self.angle, self.destangle)
			if da: #If delta angle adjust angular velocity.
				av = self.avel + da
				s = sgn(av)
				self.avel = min(pi, abs(av)) * s

	def update(self, dt):
		if self.on: #If on then update.
			self.move(dt)
			self.unstable = False
			b = Vector3(self.pos.x, self.pos.y, self.scale * .75)
			#Check if colliding with crate, if so add instability to turning.
			for m in self.scene.crates:
				if m.boundcheck(b):
					self.unstable = True
					break
			#Dampen angular velocity.
			self.avel = dampen(self.avel, self.brka * dt)
			#If not controlling linear velocity dampen that as well.
			if not self.control:
				self.slowdown(dt)

			machine.update(self, dt)
		else: #Death of player is different from killer which uses state functions
					# simply to show another way of doing it.  I prefer states.
			self.dead -= dt
			if self.dead <= 0: #As soon as dead time up reset.
				self.reset()

	###Calculate direction/velocity from movement button touch.
	def movetouch(self, touch):
		deltax = (touch.location.x - self.movepos.x)
		deltay = (touch.location.y - self.movepos.y)
		a, l = anglefromvector(Point(deltax, deltay))
		self.destangle = a
		if l > movesense:
			self.vel.y = l * movescale

	###Check left/right button pressed, return true if handled.
	def touch_began(self, touch):
		if self.on:
			tl = touch.location
			if tl in self.moverect: #If movement rectangle touched.
				self.movetouch(touch)
				self.control += 1
				return True
			if tl in self.shootrect: #If fire rectangle touched.
				self.fire()
				return True
		return False

	def touch_moved(self, touch):
		if self.on:
			tl = touch.location
			if tl in self.moverect: #If movement rectangle touched.
				self.movetouch(touch)
				return True

			tpl = touch.prev_location
			if tpl in self.moverect: #If previous location was in movement rectangle then stop movement.
				self.control = max(0, self.control - 1)
				return True

			if tl in self.shootrect: #If fire rectangle touched calculate anguler velocity.
			#NOTE: We don't check if previous location was in button, we just assume so.
				delta = touch.location.y - touch.prev_location.y
				av = delta * turnscale * self.dir
				if self.unstable and av: #If unstable (on crate) add random angular velocity.
					r = uniform(-uadj, uadj)
					self.angle = addangle(self.angle, r)
				self.avel += av
				return True
		return False

	###Touch ends.
	def touch_ended(self, touch):
		if self.on:
			tl = touch.location
			if tl in self.moverect: #If no longer touching movement button decrement control count.
				self.control = max(0, self.control - 1)
				return True
			return tl in self.shootrect #Return handled if intersects fire rectangle.
		return False

class MyScene(Scene):
	###Main scene.
	def setup(self):
		global screenrad
		# This will be called before the first frame is drawn.
		pos = self.bounds.center()
		cp = Point(*pos)
		pos.x = 0
		w = self.size.w
		h = self.size.h
		w3 = w / 5 #1/5th Screen width.
		w6 = w3 / 2 #1/10th Screen width.
		screenrad = hypot(w, h) * .5 #Set screen radius.
		self.pl = []
		plr = player(pos, self, 1.0) #Create player 1.
		self.pl.append(plr)
		plr.color = Color(1.00, 0.50, 0.00)
		plr.moverect = Rect(w - w3, 0, w3, w3) #Set movement button rectangle.
		plr.movepos = plr.moverect.center()
		plr.shootrect = Rect(w - w6, w3 * 2.25, w6, w3) #Set fire button rectangle.
		pos.x = w
		plr = player(pos, self, -1.0) #Create player 2.
		self.pl.append(plr)
		plr.color = Color(0.40, 1.00, 0.40)
		plr.shoot = sounds[1] #Change fire sound for player 2.
		plr.shootv = 0.3 #Set lower volume.
		plr.moverect = Rect(0, h - w3, w3, w3) #Set movement button rectangle.
		plr.movepos = plr.moverect.center()
		plr.shootrect = Rect(0, h - w3 * 2.25 - w3, w6, w3) #Set fire button rectangle.
		self.activebullets = [] #Array of active bullets.
		self.explosions = [] #Array of active explosions.
		self.controlalpha = 0.45 #Control rectangle alpha.
		self.wave = 0 #Wave counter.
		self.wpskipchance = wpskipchance
		self.killerinterval = killerinterval
		self.state = self.run
		self.gameovertxt = render_text('You\'ve been Ripped Off!', 'Copperplate', 32)
		self.pausetxt = render_text('Pause', 'Copperplate', 28)
		c = self.bounds.center()
		hh = self.pausetxt[1].w / 2
		self.pauserect = Rect(c.x - hh, 0, hh * 2, self.pausetxt[1].h)
		if mt:
			self.udstart = Event()
			self.uddone = Event()
			self.udthread = Thread(target=self.udthread)
			self.udthread.start()

		for s in sounds: load_effect(s) #pre-load sound effects.

		def makecrate(i):
		###Local function to create a crate mob.
			x = (int(i / 3) - 1) * cratespacing
			y = ((i % 3) - 1) * cratespacing
			cr = crate(Point(cp.x + x, cp.y + y), self)
			return cr

		self.crates = [makecrate(i) for i in xrange(numcrates)]
		self.robbers = [robber(self) for i in xrange(robbercount)]
		self.killers = [] #Start with 0 killers.
		self.numrobbers = 0

	###Remove crate from the crates array.
	def killcrate(self, cr): self.crates.remove(cr)

	def adjustdifficulty(self):
		###Adjust difficulty level based on # of waves.
		self.wpskipchance = wpskipchance + (self.wave * wpskipfactor)
		nk = min(self.wave / self.killerinterval, maxkillers)
		nk -= len(self.killers)
		while nk > 0: #Add killers.
			self.killerinterval += 2 #Adjust wave interval for next killer.
			nk -= 1
			k = killer(self)
			self.killers.append(k)

	def checkwave(self, dt):
		###Check to see if wave is complete.
		if not self.numrobbers: #If no more live robbers.
			if len(self.crates): #If still some live crates.
				self.wave += 1 #New wave.
				self.scoretxt = render_text('wave: ' + str(self.wave), 'Copperplate', 28)
				self.adjustdifficulty()
				self.startrobbers() #Restart all of the robbers.
			else:
				self.state = self.gameover

	def checkbullet(self, p1, p2, owner):
		###Check bullet collision, return true if hit something.
		hit = False
		def checkcol(amob):
			###Local function to
			if amob.on:
				circle = Vector3(amob.pos.x, amob.pos.y, amob.scale)
				if segvcircle(p1, p2, circle): #If circle hit then create an explosion.
					self.explosions.append(explosion(amob))
					amob.kill() #Kill the hit object.
					return True
			return False

		#If not a player ID then check player collisions.
		if owner.filter != playerfilter:
			for p in self.pl:
				hit |= checkcol(p)
		elif owner.filter != aifilter: #If not AI ID.
			for k in self.killers: #Check killers 1st.
				if checkcol(k):
					return True #If killer took hit exit.
			for r in self.robbers: #Check robbers, 1 bullet can hit many.
				hit |= checkcol(r)

		return hit

	def startpos(self):
		###Get a random start position just off screen.
		a = uniform(0, pi2) #Get angle from 0 to 2pi.
		p = Point(0, screenrad)
		rotpoint(a, p)
		c = self.bounds.center()
		p.x += c.x
		p.y += c.y
		return p, a

	def startrobbers(self):
		###Start all robbers.
		for r in self.robbers:
			if not r.on:
				c = choice(self.crates) #pick a target.
				p, a = self.startpos()
				r.reset(p, a, c)

	def udrobbers(self):
		###Update robbers and set # of live robbers.
		self.numrobbers = 0
		for r in self.robbers:
			self.numrobbers += r.update(self.dt)

	def udthread(self):
		###Mutli-threaded update.
		while True:
			self.udstart.wait() #Wait for main thread to signal update.
			self.udstart.clear() #Clear signal.
			self.udrobbers() #Update robbers.
			self.uddone.set() #Signal update done.

	def update(self, dt):
		###Update the scene.
		if debug:
			self.t0 = clock()
		if mt: self.udstart.set() #If multi-threaded signal update start.
		#if no screen touches reset control counters for safety (they can get out of sync).
		if len(self.touches) == 0:
			for p in self.pl:
				p.control = 0
		for p in self.pl: #Update players.
			p.update(dt)
		for k in self.killers: #Update killers.
			k.update(dt)
		#If not multi-threaded update robbers here.
		if not mt: self.udrobbers()
		#Update active bullets.
		for b in self.activebullets:
			if b.update(self, self.dt):
				self.activebullets.remove(b)
		#Update active explosions.
		for e in self.explosions:
			if e.update(dt):
				self.explosions.remove(e)
		if debug:
			self.t1 = clock()

	###Pause state does nothing.
	def paused(self): pass

	def gameover(self):
		###Game over state.
		tint(0.00, 1.00, 0.00)
		c = self.bounds.center()
		s = self.gameovertxt[1]
		image(self.gameovertxt[0], c.x - (s.w / 2), c.y, *s)
		self.update(self.dt / 4)

	def run(self):
		###Main state.
		dt = min(0.1, self.dt)
		self.checkwave(dt)
		self.update(dt)

	def drawcontrols(self):
		###Draw control boxes.
		if self.controlalpha:
			fill(0,0,0,0)
			stroke(0, 0, .5, self.controlalpha)
			stroke_weight(2)
			for p in self.pl:
				rect(*p.moverect)
			stroke(.5, 0, .5, self.controlalpha)
			for p in self.pl:
				rect(*p.shootrect)

	def drawscore(self):
		###Draw the score and pause text.
		tint(1,0,0)
		c = self.bounds.center()
		s = self.scoretxt[1]
		image(self.scoretxt[0], c.x - (s.w / 2), self.size.h - s.h, *s)
		clr = Color(1,1,0) if self.state == self.paused else Color(0.80, 0.40, 1)
		if debug: #In debug draw timing text.
			tmg = int((self.t1 - self.t0) * 1000.0)
			text(str(tmg), x=20, y=20, alignment=9)
		tint(*clr)
		s = self.pausetxt[1]
		image(self.pausetxt[0], c.x - (s.w / 2), 0, *s)

	def draw(self):
		###Draw all objects in the scene.
		background(0, 0, 0)
		self.state() #Update state.
		self.drawscore()
		self.drawcontrols()
		
		if mt and self.state == self.run: #Wait for other thread to finish.
			self.uddone.wait()
			self.uddone.clear()

		for cr in self.crates: cr.draw()
		for p in self.pl: p.draw()

		for k in self.killers: k.draw()
		for b in self.activebullets: b.draw()

		for r in self.robbers: r.draw()
		for e in self.explosions: e.draw()

	def checkpause(self, loc):
		###Check to see if pause button pressed.
		if loc in self.pauserect:
			play_effect(sounds[5])
			if self.state == self.paused:
				self.state = self.prevstate
			else:
				self.prevstate = self.state
				self.state = self.paused

	def touch_began(self, touch):
		###Handle touch events.
		for p in self.pl:
			if p.touch_began(touch):
				return

	def touch_moved(self, touch):
		###Handle touch move events.
		for p in self.pl:
			if p.touch_moved(touch):
				return

	def touch_ended(self, touch):
		###Handle touch end events.
		for p in self.pl:
			if p.touch_ended(touch):
				return
		self.checkpause(touch.location) #Check for pause button press.

run(MyScene(), LANDSCAPE)