# --- Nowhere To Run ---
#      by: Jure Vrscaj (jure@dijaski.net)
#
# Based on:
#  pyODE example 3: Collision detection, by Matthias Baas
#
# This was coded in one day, and is more a pyODE/pyOpenGL demo than a game. Anyhow, it's somewhat "playable" :) 
#
# HOW TO PLAY:
# - move the cube with arrow keys
# - when things get troublesome, press space (to release the POWER)
# - use mouse scroll to zoom

import sys, os, random
from math import *

import pygame 

from pygame.locals import *
from pygame.constants import *
from cgkit.cgtypes import *

from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *

import ode


# Player variables
move_force = 120000
turbo = False
explode_force = 6000000

# Enemy variables
enemy_move_force = 150000

# World variables
air_brake = 1000

# View variables
fps = 30
zoom = 1
fullscreen = True

# Default create_box variables
default_type = 'dummy'
default_dim = [0.5, 0.5, 0.5]
default_dens = 5000
default_pos = [-1, 1, 0]


class ai_class:
	loose_mode = True
	predict_mode = False
	
	def tactics(self):
		global bodies
		
		if random.randint(0, 100) == 0:
			self.loose_mode = not self.loose_mode
		if random.randint(0, 200) == 0:
			self.predict_mode = not self.predict_mode
		
		for b in bodies:
			if b.shape == 'enemy':
				dvect = vec3(player.getPosition()) - vec3(b.getPosition())

				if self.predict_mode:
					if hasattr(b, 'predict_ratio'):
						dvect += vec3(player.getLinearVel())*b.predict_ratio
					else:
						b.predict_ratio = random.randint(0, 4)
						
				if self.loose_mode:
					x, y, z = 0, 0, 0
					if hasattr(b, 'tac_pos'):
						x, y, z = b.tac_pos
					else:
						b.tac_pos = vec3(cos(random.randint(0, 100))*10, 0, sin(random.randint(0, 100))*10)
					dvect = dvect + vec3(x, y, z)
	
				push([b], dvect.normalize() * enemy_move_force, [0, 0, 0])
		
def signum(n):
	if n >= 0:
		return 1
	else:
		return -1
	
def prepare_GL():
	"""Prepare drawing."""

	global player, bodies, zoom
	
	# Viewport
	glViewport(0,0,1024,768)

	# Initialize
	glClearColor(0.8,0.8,0.9,0)
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glEnable(GL_DEPTH_TEST)
	glEnable(GL_LIGHTING)
	glEnable(GL_NORMALIZE)
	#glShadeModel(GL_FLAT)
	glEnable(GL_COLOR_MATERIAL)

	# Projection
	glMatrixMode(GL_PROJECTION)
	glLoadIdentity()
	P = mat4(1).perspective(100,1.3333,0.2,200)
	glMultMatrixd(P.toList())

	# Initialize ModelView matrix
	glMatrixMode(GL_MODELVIEW)
	glLoadIdentity()

	# Light source
	glLightfv(GL_LIGHT0,GL_POSITION,[0.8,0.8,0.8,1])
	glLightfv(GL_LIGHT0,GL_DIFFUSE,[0.8,0.8,0.8,1])
	glLightfv(GL_LIGHT0,GL_SPECULAR,[0.8,0.8,0.8,1])
	glEnable(GL_LIGHT0)

	x, y, z = player.getPosition()
	y = 0	
	# View transformation
	#V = mat4(1).lookAt(1.2*vec3(2,3,4),(0.5,0.5,0), up=(0,1,0))
	V = mat4(1).lookAt(vec3(x, y+12*(2-zoom), z+5),vec3(x,y,z), vec3(0,1,0))
	V.rotate(pi,vec3(0,1,0))  
	V = V.inverse()
	glMultMatrixd(V.toList())

#draw ground
def draw_ground():
	global world, player
	ground = ode.Body(world)
	ground.setGravityMode(False)
	ground.shape = 'ground'
	
	a = 10
	
	ground.boxsize = (a, 0.01, a)

	gmap = [
		[ (0.0, 0.3, 0.6), vec3(0, 0, 0) ],
		[ (0.0, 0.6, 0.3), vec3(a, 0, 0) ],
		[ (0.0, 0.3, 0.6), vec3(a, 0, a) ],
		[ (0.0, 0.6, 0.3), vec3(0, 0, a) ],
	]
	
	x, y, z = player.getPosition()
	
	x = int(x/(a*2))
	z = int(z/(a*2))
	for row in range(z-4, z+4):
		for col in range(x-4, x+4):
			for v in gmap:
				ground.color = v[0]
				ground.setPosition( v[1] + vec3(col*a*2 + a/2, 0, row*a*2 + a/2) )
				draw_body(ground)

	
	

# draw_body
def draw_body(body):
	"""Draw a body."""

	global turbo
	
	x, y, z = body.getPosition()
	R = body.getRotation()
	T = mat4()
	T[0,0] = R[0]
	T[0,1] = R[1]
	T[0,2] = R[2]
	T[1,0] = R[3]
	T[1,1] = R[4]
	T[1,2] = R[5]
	T[2,0] = R[6]
	T[2,1] = R[7]
	T[2,2] = R[8]
	T[3] = vec4(x, y, z, 1.0)
	
	glPushMatrix()
	glMultMatrixd(T.toList())

	if body.__class__.__dict__.has_key('color'):
		glColor3fv(body.color)
	
	if body.shape == "dummy":
		sx, sy, sz = body.boxsize
		glScale(sx, sy, sz)
		glutSolidCube(1)
	
	if body.shape == "player":
		sx, sy, sz = body.boxsize
		glScale(sx, sy, sz)
		glColor3fv((1, 1, 0))
		if turbo:
			glColor3fv((0, 1, 0))
		glutSolidCube(1)
	
	if body.shape == "ground":
		sx, sy, sz = body.boxsize
		glScale(sx, sy, sz)
		glColor3fv(body.color)
		glutSolidCube(1)

	if body.shape == "enemy":
		sx, sy, sz = body.boxsize
		glScale(sx, sy, sz)
		glColor3fv((1, 0.8, 0.4))
		glutSolidCube(1)
	
	glPopMatrix()

# create_box
def create_box(type=default_type, dim=default_dim, dens=default_dens, pos=default_pos):
	"""Create a box body and its corresponding geom.
	
	type: 'dummy' || 'player' || 'enemy'
	dim: [lx, ly, lz]
	dens: density
	pos: [x, y, z]
	
	"""

	global world, space, bodies
	
	# Create body
	lx, ly, lz = dim
	body = ode.Body(world)
	M = ode.Mass()
	M.setBox(dens, lx, ly, lz)
	body.setMass(M)

	# Set parameters for drawing the body
	body.shape = type
	body.boxsize = (lx, ly, lz)

	# Create a box geom for collision detection
	geom = ode.GeomBox(space, lengths=body.boxsize)
	geom.setBody(body)

	# Set position
	body.setPosition(pos)

	# Append to global bodies
	bodies.append(body)
	
	return body

# Apply force vector(vect) to bodies
def push(bodies, vect, relpos=[0, 0, 0]):
	for b in bodies:
		#b.addForce(vect)
		b.addForceAtRelPos(vect, relpos)

# Push away objects around body with force f 
def explode(body, force):
	"""Simulate an explosion.

	Every object is pushed away from the body.
	The force is dependent on the objects distance from the body.
	"""
	
	global bodies

	for b in bodies:
		if b == body: continue
		
		p = vec3(b.getPosition()) - vec3(body.getPosition())
		d = p.length()
		a = max(0, force*(1.0-0.2*d*d))
		p = vec3(p.x, p.y*2.0, p.z)
		b.addForce(a*p.normalize())
		
# Collision callback
def near_callback(args, geom1, geom2):
	"""Callback function for the collide() method.

	This function checks if the given geoms do collide and
	creates contact joints if they do.
	"""

	# Check if the objects do collide
	contacts = ode.collide(geom1, geom2)

	# Create contact joints
	world, contactgroup = args
	for c in contacts:
		c.setBounce(0.002)
		c.setMu(500000)
		j = ode.ContactJoint(world, contactgroup, c)
		j.attach(geom1.getBody(), geom2.getBody())


# Initialize pygame
passed, failed = pygame.init()

# Open a window
if fullscreen:
	srf = pygame.display.set_mode((1024,768), FULLSCREEN | OPENGL | DOUBLEBUF)
else:
	srf = pygame.display.set_mode((800,600), OPENGL | DOUBLEBUF)


# Create a world object
world = ode.World()
world.setGravity( (0,-9.81,0) )
world.setERP(0.8)
world.setCFM(1E-5)

# Create a space object
space = ode.Space()

# Create a plane geom which prevent the objects from falling forever
floor = ode.GeomPlane(space, (0,1,0), 0)

# A list with ODE bodies
bodies = []

# A joint group for the contact joints that are generated whenever
# two bodies collide
contactgroup = ode.JointGroup()

# Some more variables
dt = 1.0/fps
clk = pygame.time.Clock()
f = vec3(0, 0, 0) # force which moves the player 
ai = ai_class()

running = True


# Create some objects

create_box(pos=[0, 0.5, 2])
create_box(pos=[-2, 0.5, 2], dim=[1, 1, 1])
create_box(pos=[3, 1, 2], dim=[2, 2, 2])
create_box(dim=[0.6*2, 0.8*2, 0.2*2], pos=[0, 2, -2])

#enemy = create_box(type='enemy', dim=[3.0, 8.0, 0.6], dens=5000, pos=[-20, 4, 0])
player = create_box(type='player', dim=[1, 1, 1], dens=5000, pos=[0, 0.5, 0])

for i in range(19):
	x = cos(2*pi/20 * i) * 150  
	z = sin(2*pi/20 * i) * 150  
	body = create_box(type='enemy', dim=[1.0, 1.0, 1.0], dens=5000, pos=[x, 0.5, z])

while running:

	# Apply force to player
	x, y, z = 0, 0, 0
	if f.length() != 0:
		x, y, z = f.normalize()
	if turbo:
		x, y, z = x*10, y, z*10
	push([player], vec3(x*move_force, y*move_force, z*move_force))
	
	ai.tactics()

	for b in bodies:
		# Max velocity safety switch
		lv = vec3(b.getLinearVel())
		if lv.length() > 1000:
			b.setLinearVel(lv.normalize() * 900)
		
		# Air brake force
		vx, vy, vz = b.getLinearVel()
		fx = - pow(vx, 2) * signum(vx)
		fy = - pow(vy, 2) * signum(vy)
		fz = - pow(vz, 2) * signum(vz)
		air_f = vec3(fx, fy, fz) * air_brake
		push([b], air_f)
	

	# Keyboard and mouse events
	events = pygame.event.get()
	for e in events:

		if e.type==QUIT:
			running=False

		elif e.type == KEYDOWN or e.type == KEYUP:
			if e.type == KEYDOWN: foo = 1
			else: foo = -1
			
			f.y = 0
			if e.key == K_UP:
				f.z -= foo
			if e.key == K_DOWN:
				f.z += foo
			if e.key == K_LEFT:
				f.x -= foo
			if e.key == K_RIGHT:
				f.x += foo
			if e.key == K_SPACE:
				if foo == 1:
					explode(player, explode_force)
			if e.key == K_RETURN and e.type == KEYDOWN:
				#print player.vectorToWorld(vec3(0, 0, -1))
				print vec3(player.getLinearVel()).length()
			if e.key == K_PLUS:
				fps *= 2
			if e.key == K_MINUS:
				fps /= 2
			if e.key == K_ESCAPE:
				running=False
				
		elif e.type==MOUSEBUTTONDOWN:
			print e.dict
			if e.dict['button'] == 4:
				zoom += 0.2
			if e.dict['button'] == 5:
				zoom -= 0.2

		elif e.type==MOUSEMOTION:
			pass

			
	prepare_GL()

	# Draw ground
	draw_ground()
	
	# Draw bodies
	glColor3f(0.7, 0.7, 0.7)
	for b in bodies:
		draw_body(b)
	
	pygame.display.flip()

	# Simulate
	n = 2

	for i in range(n):
		# Detect collisions and create contact joints
		space.collide((world,contactgroup), near_callback)

		# Simulation step
		world.step(dt/n)

		# Remove all contact joints
		contactgroup.empty()
  
	clk.tick(fps)