#!/usr/pkg/bin/python2.7

# ACCELERATOR v0.1.0
# Copyright (c) Philip Allison, 2005
#
# http://sourceforge.net/projects/accelerator3d
# http://mangobrain.co.uk/
#
# Originally created for the first PyWeek Game Contest
#
# Released under the Artistic License - please read LICENSE or License.txt.
# If you did not receive a copy of the license with this file, please contact
# the author via one of the above URLs, stating where you obtained your package.

import sys

# this line helps us find OpenGL after py2exe packaging, since
# it is easier to add a stripped-down PyOpenGL to the lib folder
# than attempt to package it directly.
sys.path.append("lib")

import os
import math
import random
import pygame
import ode
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GL.EXT.separate_specular_color import *
from OpenGL.GLU import gluLookAt, gluBuild2DMipmaps, gluPerspective, gluNewQuadric, gluSphere
import OpenGL
import gc

gc.disable()

random.seed()

# reference to active Texture object
activeTexture = None

# current active OpenGL mode
glMode = None

# "safe" wrappers around glBegin and glEnd:
# they won't begin an already begun mode, won't re-begin if already in the given mode,
# transparently end if switching modes, and won't end if not begun.
# also, they keep the glMode global updated with the currently begun rendering mode.
def glEnd():
	global glMode
	if glMode != None:
		OpenGL.GL.glEnd()
		glMode = None
def glBegin(mode):
	global glMode
	if glMode != mode:
		glEnd()
		OpenGL.GL.glBegin(mode)
		glMode = mode

# display/internal resolutions
dxres = 800
dyres = 600
ixres = 800
iyres = 600

znear = 0.1
zfar = 250.0

# OpenGL Texture class
class Texture:
	# pass in a filename, and it'll load the image, upload the data to OpenGL (inc. transparency), and set some
	# default parameters on it (min/mag filters, wrapping).
	def __init__(self,texfile,mipmap = False):
		global activeTexture
		filename = os.path.join('/usr/pkg/share/accelerator3d',texfile)
		image = pygame.image.load(filename)
		data = pygame.image.tostring(image,'RGBX')
		self.__texref = glGenTextures(1)
		self.__width = image.get_width()
		self.__height = image.get_height()
		glBindTexture(GL_TEXTURE_2D,self.__texref)
		# bilinear magnification filter, clamp textures at edges (instead of repeating)
		# mipmapping if wanted
		if not mipmap:
			# (first 0 = mipmap level; second 0 = border)
			glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,self.__width,self.__height,0,GL_RGBA,GL_UNSIGNED_BYTE,data)
			glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST)
		else:
			OpenGL.GLU.gluBuild2DMipmaps(GL_TEXTURE_2D,GL_RGBA,self.__width,self.__height,GL_RGBA,GL_UNSIGNED_BYTE,data)
			glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_LINEAR)
		glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR)
		#glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,0x812F) # GL_CLAMP_TO_EDGE
		#glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,0x812F) # GL_CLAMP_TO_EDGE
		# indicate that this is now the bound texture
		activeTexture = self
		
	# destructor - release texture memory when associated Texture object is deleted
	def __del__(self):
		global activeTexture
		glDeleteTextures(self.__texref)
		if activeTexture == self.__texref:
			activeTexture = None
	
	# return a tuple of width/height
	def getDimensions(self):
		return self.__width, self.__height
	
	# make this the active texture object
	# (does not work inside glBegin/glEnd, but fixes this transparently)
	def bind(self):
		global activeTexture, glMode
		if activeTexture != self:
			oldglmode = None
			if glMode != None:
				oldglmode = glMode
				glEnd()
			glBindTexture(GL_TEXTURE_2D,self.__texref)
			activeTexture = self
			if oldglmode:
				glBegin(oldglmode)

# Bitmap font class
# Holds a list of strings for which texture coordinates have been cached;
# this list can be added to, cleared, and rendered from.
class Font:
	# pass in a filename (which will get passed directly to Texture's constructor), the number of glyphs
	# per row, the number of rows, and a string containing the bitmap font's glyphs in order (used to
	# test for glyph existance and calculate the position of a glyph on the bitmap).
	# also pass in X and Y padding values: glyphs should start from (0,0), but the overall bitmap may
	# have been padded at the right and bottom edges in order to ensure power of two dimensions.
	def __init__(self,fontfile,glyphs_per_row,rows,glyphs,x_padding,y_padding):
		self.__glyphs = glyphs
		self.__glyphs_per_row = glyphs_per_row
		self.__rows = rows
		self.__fontTexture = Texture(fontfile)
		tx, ty = self.__fontTexture.getDimensions()
		self.__glyphWidth = float(tx - x_padding) / glyphs_per_row
		self.__glyphHeight = float(ty - y_padding) / rows
		self.__stringList = {}
	
	# calculate the texture coordinates for the given string,
	# and return a list index suitable for passing to the rendering function.
	# (returns False if a non-renderable character is contained in the input string)
	# also give the optional stringname argument to alter an existing string rather than create a new one.
	# (note: you'll still access it via the old index, i.e. the original, un-edited string)
	def addString(self,string,stringname=None):
		# create a list of texture coordinates: index by finding the
		# characters in the given string in the internal glyph list,
		# then calculate coordinates using the index and known properties
		# of the font texture.
		if not stringname:
			stringname = string
		coords = []
		for i in xrange(len(string)):
			j = self.__glyphs.find(string[i])
			# check we can render the current glyph
			if (j == -1) and (string[i] != ' '):
				# uh oh. we can't!
				raise ValueError, 'String "'+string+'" contains non-renderable glyphs'
			else:
				# if we've been given a character, calculate texture coordinates and add them to the list.
				# otherwise, we've just been given a space, so add an indicator for a "dummy" character.
				if string[i] != ' ':
					row = math.floor(float(j)/self.__glyphs_per_row)
					column = j-(row*self.__glyphs_per_row)
					left = self.__glyphWidth * column
					right = left + self.__glyphWidth
					bottom = self.__glyphHeight * row
					top = bottom + self.__glyphHeight
					tw, th = self.__fontTexture.getDimensions()
					left = float(left) / tw
					right = float(right) / tw
					top = float(top) / th
					bottom = float(bottom)/th
					coords.append([[left,bottom],[right,bottom],[right,top],[left,top]])
				else:
					coords.append(-1)
		self.__stringList[stringname]=coords
		#print self.__stringList.keys()
		return stringname
	
	# draw a cached string.
	# pass in the index returned from addString.
	# also pass in x and y coordinates (for the bottom left corner).
	def draw(self,index,x,y):
		global activeTexture
		if activeTexture != self.__fontTexture:
			self.__fontTexture.bind()
		glBegin(GL_QUADS)
		# KeyError will be raised if string not found
		mystring = self.__stringList[index]
		for i in xrange(len(mystring)):
			# do we have a character, or just a space?
			if mystring[i] != -1:
				array = mystring[i][0]
				glTexCoord2f(array[0],array[1])
				glVertex2f(x,y + self.__glyphHeight)
				array = mystring[i][1]
				glTexCoord2f(array[0],array[1])
				glVertex2f(x + self.__glyphWidth,y + self.__glyphHeight)
				array = mystring[i][2]
				glTexCoord2f(array[0],array[1])
				glVertex2f(x + self.__glyphWidth,y)
				array = mystring[i][3]
				glTexCoord2f(array[0],array[1])
				glVertex2f(x,y)
			x += self.__glyphWidth
	
	# remove a cached string
	def removeString(self,string):
		del self.__stringList[string]
	
	# remove all strings
	def emptyStrings(self):
		self.__stringList.clear()
	
	# return list of currently cached strings
	def getStrings(self):
		return self.__stringList.keys()
	
	# return the width of the given string
	# (because strings can be edited, the length is not necessarily the length of the index string!)
	def getStringWidth(self,index):
		
		return float(self.__glyphWidth*len(self.__stringList[index]))
	
	# same for length
	def getStringLen(self,index):
		return len(self.__stringList[index])

# initialise OpenGL, and return a tuple of (success, message)
def DisplayInit():
	global znear, zfar
	global dxres, dyres, ixres, iyres
	# initialise pygame's display module
	try:
		pygame.display.init()
	except:
		return False, 'Could not initialise pygame display module'
	# try to open a window
	try:
		pygame.display.set_mode((dxres,dyres),OPENGL|DOUBLEBUF)#|FULLSCREEN)
		# retrieve ACTUAL display resolution
		dxres,dyres=pygame.display.get_surface().get_size()
	except:
		return False, 'Could not set screen mode'
	glViewport(0,0,dxres,dyres)
	glMatrixMode(GL_MODELVIEW)
	glLoadIdentity()
	# enable transparency without modifying source colours
	glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA)
	glEnable(GL_BLEND)
	glClearColor(0,0,0,0)
	glEnable(GL_TEXTURE_2D)
	glShadeModel(GL_SMOOTH)
	glDepthFunc(GL_LEQUAL)
	glClearDepth(zfar)
	glFogi(GL_FOG_MODE,GL_LINEAR)
	glFogf(GL_FOG_START,(zfar-znear)/3.0)
	glFogf(GL_FOG_END,zfar)
	glFogfv(GL_FOG_COLOR,(0,0,0))
	glEnable(GL_LIGHT0)
	glLightfv(GL_LIGHT0,GL_AMBIENT,(0,0,0,1))
	glLightfv(GL_LIGHT0,GL_DIFFUSE,(0.6,0.6,0.6,1))
	glLightfv(GL_LIGHT0,GL_POSITION,(0.5,0.5,1,0))

	for i in (GL_LIGHT1, GL_LIGHT2, GL_LIGHT3, GL_LIGHT4, GL_LIGHT5, GL_LIGHT6, GL_LIGHT7):
		glLightfv(i,GL_AMBIENT,(0,0,0,1))
		glLightfv(i,GL_DIFFUSE,(2,2,2,1))
		glLightfv(i,GL_SPECULAR,(15,9,0,1))
		glLightfv(i,GL_LINEAR_ATTENUATION,0.5)

	glEnable(GL_COLOR_MATERIAL)
	glColorMaterial(GL_FRONT,GL_AMBIENT_AND_DIFFUSE)
	glMaterialfv(GL_FRONT,GL_SHININESS,128)
	glMaterialfv(GL_FRONT,GL_SPECULAR,(0.5,0.5,0.5,1))
	glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL_EXT,GL_SEPARATE_SPECULAR_COLOR_EXT) # GL_LIGHT_MODEL_COLOR_CONTROL, GL_SEPARATE_SPECULAR_COLOR
	glEnable(GL_LINE_SMOOTH)
	glEnable(GL_NORMALIZE)
	#glPolygonMode(GL_FRONT,GL_LINE)
	return True,''

# Set projection matrix to 2D, orthographic projection, and generally get things ready for HUD/menu drawing.
def Set2D():
	# initialise 2D orthographic projection
	glMatrixMode(GL_PROJECTION)
	glLoadIdentity()
	# set this to whatever "internal" resolution we want to use
	# OGL will handle scaling it up to the viewport for us!
	glOrtho(0,ixres,0,iyres,0,0.1)
	glMatrixMode(GL_MODELVIEW)
	glLoadIdentity()
	# disable lighting, culling, depth-testing, and backface culling
	glDisable(GL_LIGHTING)
	glDisable(GL_CULL_FACE)
	glDisable(GL_DEPTH_TEST)
	glDisable(GL_FOG)
	glColor3f(1.0,1.0,1.0)

# Set projection matrix to 3D, and enable all the fanciness we want when drawing the level itself.
def Set3D():
	global znear, zfar
	glMatrixMode(GL_PROJECTION)
	glLoadIdentity()
	gluPerspective(45.0,float(ixres)/iyres,znear,zfar)
	glMatrixMode(GL_MODELVIEW)
	glLoadIdentity()
	glEnable(GL_CULL_FACE)
	glFrontFace(GL_CCW)
	glEnable(GL_DEPTH_TEST)
	glEnable(GL_FOG)
	glColor3f(1.0,1.0,1.0)

# Generate random, inside-tunnel coordinates
def genCoords(radius):
	global tunnel_radius
	a = random.uniform(0,359)
	r = random.uniform(0,tunnel_radius-radius)
	x = math.sin(math.radians(a))*r
	y = math.cos(math.radians(a))*r
	return x, y

# distance between tunnel "rings"; hence, also the distance to travel before looping the geometry
z_increment = 5

tunnel_radius = 8

# compile display list 1 for rendering the main particle accelerator interior
def createTunnelDisplayList():
	glNewList(1,GL_COMPILE)
	OpenGL.GL.glBegin(GL_QUADS)
	# draw tunnel as a series of progressively further away rings, rendered using quads
	z = 0
	global zfar, z_increment, tunnel_radius
	angle_increment = 10
	txi = float(angle_increment)/20.0
	tyi = float(z_increment)/5.0
	tx, ty = 0.0, 0.0
	for i in xrange(int(zfar/z_increment)+1):
		angle = 0
		while (angle < 360):
			x = math.sin(math.radians(angle))
			y = math.cos(math.radians(angle))
			angle += angle_increment
			x2 = math.sin(math.radians(angle))
			y2 = math.cos(math.radians(angle))
			# TODO: Generate normals here as well, so that lighting will work correctly
			# update: seems to be unnecessary for simple ambient/diffuse with purely distance-based attenuation
			glTexCoord2f(tx,ty)
			glNormal3f(-x,-y,0)
			glVertex3f(x*tunnel_radius,y*tunnel_radius,z)
			glTexCoord2f(tx,ty+tyi)
			glVertex3f(x*tunnel_radius,y*tunnel_radius,z-z_increment)
			glTexCoord2f(tx+txi,ty+tyi)
			glNormal3f(-x2,-y2,0)
			glVertex3f(x2*tunnel_radius,y2*tunnel_radius,z-z_increment)
			glTexCoord2f(tx+txi,ty)
			glVertex3f(x2*tunnel_radius,y2*tunnel_radius,z)
			tx += txi
			tx = math.fmod(tx,1.0)
		z -= z_increment
		ty += tyi
		ty = math.fmod(ty,1.0)
	z_increment = z_increment/tyi
	OpenGL.GL.glEnd()
	glEndList()

# compile a display list for drawing a full-screen quad
def createFullQuadDisplayList():
	glNewList(2,GL_COMPILE)
	glMatrixMode(GL_MODELVIEW)
	glPushMatrix()
	glLoadIdentity()
	glMatrixMode(GL_PROJECTION)
	glPushMatrix()
	glLoadIdentity()
	OpenGL.GL.glBegin(GL_QUADS)
	glVertex3i(-1, -1, -1)
	glVertex3i(1, -1, -1)
	glVertex3i(1, 1, -1)
	glVertex3i(-1, 1, -1)
	OpenGL.GL.glEnd()
	glPopMatrix()
	glMatrixMode(GL_MODELVIEW)
	glPopMatrix()
	glEndList()

# compile display list 3 for rendering a detailed sphere
def createSphereDisplayList():
	quad = gluNewQuadric()
	glNewList(3,GL_COMPILE)
	gluSphere(quad, 1, 16, 16)
	glEndList()
	# compile display list 4 for rendering a less detailed sphere
	quad = gluNewQuadric()
	glNewList(4,GL_COMPILE)
	gluSphere(quad, 1, 8, 8)
	glEndList()
	# compile display list 5 for rendering a really bad sphere
	quad = gluNewQuadric()
	glNewList(5,GL_COMPILE)
	gluSphere(quad, 1, 4, 4)
	glEndList()

# small function for drawing a textured rectangle (for HUD, menu, etc.)
def glRectangle(left,bottom,right,top,tleft=0.0,tbottom=1.0,tright=1.0,ttop=0.0):
	glBegin(GL_QUADS)
	glTexCoord2f(tleft,tbottom)
	glVertex2f(left,bottom)
	glTexCoord2f(tright,tbottom)
	glVertex2f(right,bottom)
	glTexCoord2f(tright,ttop)
	glVertex2f(right,top)
	glTexCoord2f(tleft,ttop)
	glVertex2f(left,top)

# ODE debug function - draw the AABB associated with the given collision geometry
def drawAABB(geom):
	box = geom.getAABB()
	glBegin(GL_LINE_LOOP)
	glVertex3f(box[0],box[2],box[4])
	glVertex3f(box[1],box[2],box[4])
	glVertex3f(box[1],box[3],box[4])
	glVertex3f(box[0],box[3],box[4])
	glEnd()
	glBegin(GL_LINE_LOOP)
	glVertex3f(box[0],box[2],box[5])
	glVertex3f(box[1],box[2],box[5])
	glVertex3f(box[1],box[3],box[5])
	glVertex3f(box[0],box[3],box[5])
	glEnd()
	glBegin(GL_LINE_LOOP)
	glVertex3f(box[0],box[2],box[4])
	glVertex3f(box[1],box[2],box[4])
	glVertex3f(box[1],box[2],box[5])
	glVertex3f(box[0],box[2],box[5])
	glEnd()
	glBegin(GL_LINE_LOOP)
	glVertex3f(box[0],box[3],box[4])
	glVertex3f(box[1],box[3],box[4])
	glVertex3f(box[1],box[3],box[5])
	glVertex3f(box[0],box[3],box[5])
	glEnd()
	glBegin(GL_LINE_LOOP)
	glVertex3f(box[0],box[2],box[4])
	glVertex3f(box[0],box[3],box[4])
	glVertex3f(box[0],box[3],box[5])
	glVertex3f(box[0],box[2],box[5])
	glEnd()
	glBegin(GL_LINE_LOOP)
	glVertex3f(box[1],box[2],box[4])
	glVertex3f(box[1],box[3],box[4])
	glVertex3f(box[1],box[3],box[5])
	glVertex3f(box[1],box[2],box[5])
	glEnd()

#
# Main game implementation begins here!
#

#
# Base main loop class.
#
# Override this and implement doLoop, init, etc. to render whatever we want;
# being an object allows us to save state between frames without globals,
# and to have a destructor for safe cleanups.
#
# A baseLoop should exit by setting nextLoop to the next loop class, or None
# to quit the game.
class baseLoop:
	def doLoop(self):
		pass

#
# Menu funcs
#

def startgame():
	global nextLoop
	nextLoop = gameLoop()
def quitgame():
	global nextLoop
	nextLoop = None
def backtomenu():
	global nextLoop, pausedLoop
	pausedLoop = None
	makeMainMenu()
	nextLoop = menuLoop(mainMenu)
def unpause():
	global nextLoop, pausedLoop, font
	nextLoop = pausedLoop
	nextLoop.unpause()

#
# Options menu callbacks
#

def dosensitivity(left):
	global msensitivity, mainMenu, font
	if left and msensitivity>0.06:
		msensitivity -= 0.05
	elif (not left) and msensitivity<0.99:
		msensitivity += 0.05
	font.addString('sensitivity: '+str(msensitivity), mainMenu[1][0][0])

def doxinvert(left):
	global xinvert, mainMenu, font
	xinvert *= -1
	if (xinvert == -1):
		xistring = 'invert x axis: no'
	else:
		xistring = 'invert x axis: yes'
	font.addString(xistring, mainMenu[1][0][1])

def doyinvert(left):
	global yinvert, mainMenu, font
	yinvert *= -1
	if (yinvert == -1):
		yistring = 'invert y axis: no'
	else:
		yistring = 'invert y axis: yes'
	font.addString(yistring, mainMenu[1][0][2])

# default options
# mouse: invert X and Y axes (-1: no, 1: yes); sensitivity
xinvert = -1
yinvert = -1
msensitivity = 0.2
# sound
soundon = True

# the main menu.
# demonstrates required datastructure for menus:
# list of 'pages', each containing a list of menu entries (strings), a second list
# detailing which page to go to/function to call when the matching menu entry is chosen,
# a third list giving either None or a function for when left or right is pressed on an entry
# (the latter one is useful for implementing options screens).

# this needs to be a func, not a constant, so that
# the options page be filled out dynamically each time
def makeMainMenu():
	global mainMenu
	if (xinvert == -1):
		xistring = 'invert x axis: no'
	else:
		xistring = 'invert x axis: yes'
	if (yinvert == -1):
		yistring = 'invert y axis: no'
	else:
		yistring = 'invert y axis: yes'
	msstring = 'sensitivity: '+str(msensitivity)
	mainMenu = [
		[
			['new game', 'options', 'quit'],
			[startgame, 1, quitgame],
			[None, None, None]
		],
		[
			[msstring, xistring, yistring, 'back'],
			[1, 1, 1, 0],
			[dosensitivity, doxinvert, doyinvert, None]
		]
	]

makeMainMenu()

# the pause menu
# no dynamic text here, constant structure is fine
pauseMenu = [
	[
		['resume', 'exit to main menu'],
		[unpause, 1],
		[None, None]
	],
	[
		['no, keep playing!','yes, i\'ve had enough'],
		[0, backtomenu],
		[None, None]
	]
]

# main loop class for menus
class menuLoop(baseLoop):
	__frate = 1.5
	# pass in the menu structure we want to work with
	def __init__(self,menu):
		global font
		self.__menu = menu
		self.__state = [0,0,-1]
		self.__fadealpha = 0.5
		self.__faderate = self.__frate
		self.__zpos = 0.0
		self.__angle = 0.0
		self.__anglerate = 5.0
		pygame.event.set_allowed(None)
		pygame.event.set_allowed([KEYDOWN,QUIT])
		font.emptyStrings()
		for j in xrange(len(self.__menu)):
			for i in xrange(len(self.__menu[j][0])):
				font.addString(self.__menu[j][0][i])
		if (soundon):
			self.__browsesound = pygame.mixer.Sound(os.path.join('/usr/pkg/share/accelerator3d','menu_browse.wav'))
			self.__selectsound = pygame.mixer.Sound(os.path.join('/usr/pkg/share/accelerator3d','menu_select.wav'))

	def doLoop(self):
		global elapsed, z_increment, font
		glEnable(GL_LIGHTING)
		glEnable(GL_TEXTURE_2D)
		glLightfv(GL_LIGHT0, GL_AMBIENT, (0.6, 0.6, 0.6, 1))
		# draw tunnel in background
		Set3D()
		circuit.bind()
		glTranslatef(0,0,self.__zpos)
		glRotatef(self.__angle,0.0,0.0,1.0)
		self.__zpos += z_increment*elapsed
		self.__zpos = math.fmod(self.__zpos,z_increment)
		self.__angle += self.__anglerate*elapsed
		self.__angle = math.fmod(self.__angle,360.0)
		glCallList(1)
		# draw logo
		Set2D()
		glDisable(GL_LIGHTING)
		logo.bind()
		glRectangle(144,471,656,599)
		# set up strings if we just switched from a different state
		if self.__state[2] != self.__state[0]:
			self.__state[2] = self.__state[0]
			self.__state[1] = 0
			self.__fadealpha = 0.5
			self.__faderate = self.__frate
		# render menu based on current state
		# render current strings
		# screen height over two, minus logo height, plus half of the menu height,
		# minus half the height of one menu item (rearranged to only do one division)
		ystart = (((iyres-128)+(len(self.__menu[self.__state[0]][0])*60))-60)/2
		for i in xrange(len(self.__menu[self.__state[0]][0])):
			string = self.__menu[self.__state[0]][0][i]
			if self.__state[1] == i:
				glColor4f(1.0,1.0,1.0,self.__fadealpha)
			font.draw(string,(ixres-font.getStringWidth(string))/2,ystart-(i*60))
			if self.__state[1] == i:
				glColor4ub(255,255,255,255)
		self.__fadealpha += self.__faderate*elapsed
		if self.__fadealpha <= 0.0:
			self.__faderate = self.__frate
			self.__fadealpha = 0.0
		elif self.__fadealpha >= 1.0:
			self.__faderate = -self.__frate
			self.__fadealpha = 1.0
		glEnd()
		# if a key isn't still being held down, parse keypresses
		for key in pygame.event.get(KEYDOWN):
			# move to next menu page (or execute function call) if return is pressed
			# trigger last item in menu when escape is pressed
			if key.key == K_RETURN or key.key == K_ESCAPE:
				if soundon:
					self.__selectsound.play()
				if key.key == K_ESCAPE:
					numitems = len(self.__menu[self.__state[0]][0])-1
					goto = self.__menu[self.__state[0]][1][numitems]
				else:
					goto = self.__menu[self.__state[0]][1][self.__state[1]]
				if isinstance(goto,int):
					self.__state[0] = goto
				else:
					goto()
			# well if it wasn't return or escape, play the browse sound.
			else:
				if soundon:
					self.__browsesound.play()
				# move up and down menu
				if key.key == K_DOWN:
					self.__state[1] += 1
					self.__fadealpha = 0.5
					self.__faderate = self.__frate
				elif key.key == K_UP:
					self.__state[1] -= 1
					self.__fadealpha = 0.5
					self.__faderate = self.__frate
				# move left and right on an option
				elif key.key == K_LEFT:
					func = self.__menu[self.__state[0]][2][self.__state[1]]
					if func != None:
						func(True)
				elif key.key == K_RIGHT:
					func = self.__menu[self.__state[0]][2][self.__state[1]]
					if func != None:
						func(False)
				# wrap top and bottom of menu when selecting items
				self.__state[1] %= len(self.__menu[self.__state[0]][0])

#
# Loop object for the game proper - woohoo!
#

ship_radius = 1.0

class gameLoop(baseLoop):
	def __init__(self):
		global ship_radius
		font.emptyStrings()
		
		# resources
		self.__tMeters = Texture('meters.png')
		self.__tPower = Texture('power.png')
		self.__tShield = Texture('shield.png')
		self.__tCrosshair = Texture('crosshair.png')
		self.__tPhlogiston = Texture('phlogiston.png',True)
		self.__tBullet = Texture('bullet.png',True)
		
		# for collision detection
		self.__space = ode.Space()
		self.__world = ode.World()
		self.__world.setGravity((0,0,0))
		self.__body = ode.Body(self.__world)
		self.__geom = ode.GeomSphere(self.__space, ship_radius)
		self.__geom.setBody(self.__body)
		self.__geom.setPosition((0,0,0))
		# set ship category bit
		self.__geom.setCategoryBits(1);
		self.__geom.setCollideBits(0);
		
		# ship stats
		self.__lives = 3
		self.__speed = 8
		self.__score = 0
		self.__oldscore = 0
		self.__power = 100.0
		self.__shield = 100.0
		font.addString(str(self.__score),'score')
		font.addString(str(self.__lives),'lives')
		# X and Y view angle
		self.__ax = 0.0
		self.__ay = 0.0
		# tunnel z-loop value
		self.__zpos = 0.0
		# ship position
		self.__px = 0.0
		self.__py = 0.0
		
		self.__time = 0.0
		self.__totaltime = 0.0
		self.__distance = 50.0
		self.__totaldistance = 0.0
		
		# lightning shoots from one part of the tunnel wall to another, and zaps lots of power
		self.__lightning = {}
		self.__lightningDrainRate = 12
		# bogons cause power (and minor shield) loss when hit, and score gain/power loss when shot at
		self.__bogons = {}
		self.__bogon_power_hit = -4
		self.__bogon_shield_hit = -8
		self.__bogon_score_shot = -100
		# cluons cause power gain and minor shield loss when hit, and score/power gain when shot at
		self.__cluons = {}
		self.__cluon_shield_hit = -2
		self.__cluon_power_hit = 2
		self.__cluon_score_shot = 200
		self.__cluon_power_shot = 4
		# phlogiston causes power gain when passed through, nothing for shooting
		self.__phlogiston = {}
		self.__phlogiston_powerup = 15
		# fat electrons cannot be destroyed, and stop you dead
		self.__fat_electrons = {}
		self.__bullets = {}
		
		self.__wall_scrape_damage = 30
		self.__wall_scrape_slowdown = 6
		
		# boilerplate
		pygame.event.set_allowed(None)
		pygame.event.set_allowed([KEYDOWN,MOUSEBUTTONDOWN,QUIT])
		pygame.event.set_grab(True)
		# grab relative mouse movement once before loop proper, to ensure first useful call will return near (0,0)
		pygame.mouse.set_pos((dxres/2,dyres/2))
		pygame.mouse.get_rel()
		
		# buffer for bullet lights - bullet linked to this light, GL light number, light age
		self.__lights = [[None, GL_LIGHT1, 0.0], [None, GL_LIGHT2, 0.0], [None, GL_LIGHT3, 0.0],
			[None, GL_LIGHT4, 0.0], [None, GL_LIGHT5, 0.0], [None, GL_LIGHT6, 0.0], [None, GL_LIGHT7, 0.0]]
		
		# pulsing tunnel lights
		self.__pulse_r, self.__pulse_g, self.__pulse_b = 0, 0, 0
		# pulse rate
		self.__rate_r = 0.1
		self.__rate_g = 0.04
		self.__rate_b = 0.17
		
		# full-screen colour fades
		self.__fadetimer = 0.0
		self.__fadecolour = [0.0, 0.0, 0.0]

		# sound!
		if soundon:
			self.__shootsound = pygame.mixer.Sound(os.path.join('/usr/pkg/share/accelerator3d','shoot.wav'))
			self.__cluonsound = pygame.mixer.Sound(os.path.join('/usr/pkg/share/accelerator3d','cluon.wav'))
			self.__bogonsound = pygame.mixer.Sound(os.path.join('/usr/pkg/share/accelerator3d','bogon.wav'))
			self.__shipdeadsound = pygame.mixer.Sound(os.path.join('/usr/pkg/share/accelerator3d','ship_explode.wav'))
			self.__shocksound = pygame.mixer.Sound(os.path.join('/usr/pkg/share/accelerator3d','electricshock.wav'))
			self.__thudsound = pygame.mixer.Sound(os.path.join('/usr/pkg/share/accelerator3d','thud.wav'))
			self.__enginesound = pygame.mixer.Sound(os.path.join('/usr/pkg/share/accelerator3d','engine.wav'))
			self.__chargesound = pygame.mixer.Sound(os.path.join('/usr/pkg/share/accelerator3d','neon.wav'))
			self.__chargeplaying = False
			self.__scrapesound = pygame.mixer.Sound(os.path.join('/usr/pkg/share/accelerator3d','scrape.wav'))
			self.__scrapeplaying = False
			self.__slamsound = pygame.mixer.Sound(os.path.join('/usr/pkg/share/accelerator3d','slam.wav'))
			self.__phlogsound = pygame.mixer.Sound(os.path.join('/usr/pkg/share/accelerator3d','phlogiston.wav'))

			# reserve channel 0 for bullet sounds
			pygame.mixer.set_reserved(2)
			self.__chanzero = pygame.mixer.Channel(0)
			# start looping engine sound
			pygame.mixer.Channel(1).play(self.__enginesound, -1)

	def __del__(self):
		pygame.event.set_grab(False)
		for i in xrange(7):
			glDisable(self.__lights[i][1])
		if soundon:
			# stop looped sounds
			self.__enginesound.stop()
			self.__scrapesound.stop()
			self.__chargesound.stop()
			# unreserve channel 0
			pygame.mixer.set_reserved(0)
		self.__space.remove(self.__geom)

	def unpause(self):
		# re-enable input grabbing (gets disabled on pause for user-friendliness)
		pygame.event.set_grab(True)
		pygame.event.set_allowed([MOUSEBUTTONDOWN])
		# grab relative mouse movement once before loop proper, to ensure first useful call will return near (0,0)
		pygame.mouse.get_rel()
		# reset strings
		font.emptyStrings()
		font.addString(str(self.__score),'score')
		font.addString(str(self.__lives),'lives')
		# restart engine sound
		if soundon:
			pygame.mixer.Channel(1).play(self.__enginesound, -1)
			if self.__chargeplaying:
				self.__chargesound.play(-1)
			if self.__scrapeplaying:
				self.__scrapesound.play(-1)

	# collision detection callback
	def nearCallback(self, arg, o1, o2):
		c = ode.collide(o1, o2)
		if c:
			# determine if it's the ship or the bullet we've hit
			# and which object is the ship/bullet
			hitShip = False
			hitObj = None
			hitBullet = None
			if o1.getCategoryBits() == 1:
				hitShip = True
				hitObj = o2
			elif o2.getCategoryBits() == 1:
				hitShip = True
				hitObj = o1
			elif o1.getCategoryBits() == 2:
				hitObj = o2
				hitBullet = o1
			else:
				hitObj = o1
				hitBullet = o2
			# take the correct action depending on what's hit what
			if hitObj.getCategoryBits() == 4 and hitShip:
				#print "lightning"
				self.__power -= self.__lightningDrainRate
				if soundon:
					self.__shocksound.play()
				self.__fadetimer = 0.5
				self.__fadecolour = [0.5, 1, 1]
				del self.__lightning[hitObj.parent]
			elif hitObj.getCategoryBits() == 8 and hitShip:
				#print "phlogiston"
				self.__power += self.__phlogiston_powerup
				if soundon:
					self.__phlogsound.play()
				self.__fadetimer = 0.28
				self.__fadecolour = [1, 1, 0]
				del self.__phlogiston[hitObj.parent]
			elif hitObj.getCategoryBits() == 16 and hitShip:
				#print "fat electron"
				self.__shield = 0.0
				self.__speed = 0.0
				del self.__fat_electrons[hitObj.parent]
			elif hitObj.getCategoryBits() == 32:
				#print "bogon"
				if hitShip:
					self.__shield += self.__bogon_shield_hit
					self.__power += self.__bogon_power_hit
					if soundon:
						self.__thudsound.play()
					self.__fadetimer = 0.28
					self.__fadecolour = [1, 0, 0]
				else:
					if soundon:
						doStereoSound(self.__px, hitObj.parent, self.__bogonsound)
					self.__score += self.__bogon_score_shot
				del self.__bogons[hitObj.parent]
			elif hitObj.getCategoryBits() == 64:
				#print "cluon"
				if hitShip:
					self.__shield += self.__cluon_shield_hit
					self.__power += self.__cluon_power_hit
					if soundon:
						self.__thudsound.play()
					self.__fadetimer = 0.28
					self.__fadecolour = [0, 1, 0]
				else:
					if soundon:
						doStereoSound(self.__px, hitObj.parent, self.__cluonsound)
					self.__score += self.__cluon_score_shot
					self.__power += self.__cluon_power_shot
				del self.__cluons[hitObj.parent]
			hitObj.parent = None
			if (hitBullet):
				#print "bullet"
				del self.__bullets[hitBullet.parent]
				for i in xrange(7):
					if self.__lights[i][0] == hitBullet.parent:
						self.__lights[i][0] = None
						break
				hitBullet.parent = None
			if self.__power <= 0.0 or self.__shield <= 0.0:
				# reset space
				self.__space.disable()

	def doLoop(self):
		global pausedLoop, nextLoop, z_increment, elapsed
		global xinvert, yinvert, msensitivity
		global tunnel_radius, ship_radius, font, circuit
		
		if self.__power > 0.0 and self.__shield > 0.0:
			# process keypresses
			for key in pygame.event.get(KEYDOWN):
				# P or Esc to pause		
				if key.key == K_p or key.key == K_ESCAPE:
					pausedLoop = self
					pygame.event.set_grab(False)
					nextLoop = menuLoop(pauseMenu)
					# stop engine sound
					if soundon:
						self.__enginesound.stop()
						self.__chargesound.stop()
						self.__scrapesound.stop()
					return
			# calculate view & movement vectors
			mx, my = pygame.mouse.get_rel()
			self.__ax -= mx*msensitivity*0.3
			self.__ay -= my*msensitivity*0.3
			if self.__ax < -30.0:
				self.__ax = -30.0
			elif self.__ax > 30.0:
				self.__ax = 30.0
			if self.__ay < -30.0:
				self.__ay = -30.0
			elif self.__ay > 30.0:
				self.__ay = 30.0
			# we want to rotate the point (0,0,1) by the viewing angle to calculate where we're heading
			# (these are heavily reduced versions of the classic trig. formulae for rotX, rotY and rotZ)
			cx = math.cos(math.radians(self.__ax))
			cy = math.cos(math.radians(self.__ay))
			sx = math.sin(math.radians(self.__ax))
			ly = -yinvert*math.sin(math.radians(self.__ay))
			lx = xinvert*cy*sx
			lz = cy*cx
			
			for key in pygame.event.get(MOUSEBUTTONDOWN):
				if key.button == 1:
					bullet = Bullet(self.__world, self.__space, self.__px, self.__py, lx*(self.__speed+80), ly*(self.__speed+80), -lz*80, self.__tBullet)
					self.__bullets[bullet] = bullet
					# store reference to bullet in light buffer - find free light, if any; otherwise, overwite oldest
					maxage = 0.0
					which = 0
					done = False
					for i in xrange(7):
						if self.__lights[i][0] == None:
							self.__lights[i][0] = bullet
							self.__lights[i][2] = 0.0
							done = True
							break
						elif self.__lights[i][2] > maxage:
							which = i
							maxage = self.__lights[i][2]
					if not done:
						self.__lights[which][0] = bullet
						self.__lights[which][2] = 0.0
					# play bullet sound
					if soundon:
						self.__chanzero.play(self.__shootsound)

			# move player at framerate-independent velocity
			speed = self.__speed*elapsed
			self.__px += lx*speed
			self.__py += ly*speed
			slz = lz*speed
	
			if pygame.mouse.get_pressed()[2]:
				if self.__shield<100.0:
					self.__power -= elapsed*10
					self.__shield += elapsed*10
					if soundon and not self.__chargeplaying:
						self.__chargeplaying = True
						self.__chargesound.play(-1)
				elif soundon and self.__chargeplaying:
					self.__chargesound.stop()
					self.__chargeplaying = False
			elif soundon and self.__chargeplaying:
				self.__chargesound.stop()
				self.__chargeplaying = False
			
			# collision detection
			# collide ship against tunnel
			if math.sqrt(pow(self.__px,2)+pow(self.__py,2)) >= tunnel_radius - ship_radius:
				# work out angle from tunnel cross section centre
				angle = math.atan2(self.__py, self.__px)
				# now knowing this angle and how long the hypotenuse should be (tunnel radius minus ship radius),
				# recompute px and py to be just within tunnel boundaries
				self.__px = math.cos(angle)*(tunnel_radius-ship_radius)
				self.__py = math.sin(angle)*(tunnel_radius-ship_radius)
				self.__shield -= self.__wall_scrape_damage * elapsed
				self.__speed -= self.__wall_scrape_slowdown * elapsed
				if self.__speed < 1.0:
					self.__speed = 1.0
				self.__wobblex = random.uniform(-0.005, 0.005)
				self.__wobbley = random.uniform(-0.005, 0.005)
				self.__fadecolour = [1, 0, 0]
				self.__fadetimer = 0.3
				if soundon and not self.__scrapeplaying:
					self.__slamsound.play()
					self.__scrapeplaying = True
					self.__scrapesound.play(-1)
			else:
				self.__wobblex = 0;
				self.__wobbley = 0;
				if soundon and self.__scrapeplaying:
					self.__scrapesound.stop()
					self.__scrapeplaying = False
			# set ship geom position ready for other collision tests
			self.__geom.setPosition((self.__px,self.__py,0))
			
			# the player's z is actually fixed at zero, and we instead move the world towards us
			# calculate tunnel z-looping offset
			self.__zpos += slz
			self.__zpos = math.fmod(self.__zpos,z_increment)
			# now move the world forwards by slz
			# set up camera
			Set3D()
			gluLookAt(self.__px + self.__wobblex, self.__py + self.__wobbley, 0, \
				self.__px + lx, self.__py + ly, -lz, \
				0,1,0)
			# draw tunnel in background
			circuit.bind()
			glPushMatrix()
			glTranslatef(0,0,self.__zpos)
			glEnable(GL_LIGHTING)
			glLightfv(GL_LIGHT0,GL_AMBIENT,(self.__pulse_r,self.__pulse_g,self.__pulse_b,1))
			self.__pulse_r += self.__rate_r*elapsed
			self.__pulse_g += self.__rate_g*elapsed
			self.__pulse_b += self.__rate_b*elapsed
			if self.__pulse_r >= 1.0 or self.__pulse_r <= 0.0:
				self.__rate_r = -self.__rate_r
			if self.__pulse_g >= 0.5 or self.__pulse_g <= 0.0:
				self.__rate_g = -self.__rate_g
			if self.__pulse_b >= 1.0 or self.__pulse_b <= 0.0:
				self.__rate_b = -self.__rate_b
			glEnable(GL_TEXTURE_2D)
			glCallList(1)
			glDisable(GL_TEXTURE_2D)
			glLightfv(GL_LIGHT0,GL_AMBIENT,(0,0,0,1))
			glPopMatrix()
			# draw world
			self.__world.quickStep(elapsed)
			# enable dynamic lighting
			for i in xrange(7):
				if self.__lights[i][0] != None:
					glEnable(self.__lights[i][1])
					glLightfv(self.__lights[i][1],GL_POSITION,(self.__lights[i][0].x,self.__lights[i][0].y,self.__lights[i][0].z,1))
					self.__lights[i][2] += elapsed
				else:
					glDisable(self.__lights[i][1])

			# fat electrons
			for (k, v) in self.__fat_electrons.items():
				distance = v.draw(self.__speed)
				if distance > 0.0:
					del self.__fat_electrons[k]
					k.getGeom().parent = None
			# bogons
			for (k, v) in self.__bogons.items():
				distance = v.draw(self.__speed)
				if distance > 0.0:
					del self.__bogons[k]
					k.getGeom().parent = None
			# cluons
			for (k, v) in self.__cluons.items():
				distance = v.draw(self.__speed)
				if distance > 0.0:
					del self.__cluons[k]
					k.getGeom().parent = None

			glDisable(GL_LIGHTING)
			# lightning
			for (k, v) in self.__lightning.items():
				distance = v.draw(self.__speed)
				if distance > 0.0:
					# destroy lightning bolt if it has gone behind ship
					del self.__lightning[k]
					k.getGeom().parent = None

			# phlogiston & bullets must be drawn last due to transparency (so as not to cause depth buffering issues)
			glDepthMask(GL_FALSE)
			glEnable(GL_TEXTURE_2D)
			glColor3ub(255,255,255)
			for (k, v) in self.__phlogiston.items():
				distance = v.draw(self.__speed)
				if distance > 0.0:
					del self.__phlogiston[k]
					k.getGeom().parent = None
			
			for (k, v) in self.__bullets.items():
				distance = v.draw(self.__speed)
				if distance < -zfar:
					for i in xrange(7):
						if self.__lights[i][0] == k:
							self.__lights[i][0] = None
							break
					del self.__bullets[k]
					k.getGeom().parent = None
				else:
					# bullets versus the tunnel wall
					if math.sqrt(pow(v.x,2)+pow(v.y,2)) >= tunnel_radius:
							for i in xrange(7):
								if self.__lights[i][0] == k:
									self.__lights[i][0] = None
									break
							del self.__bullets[k]
							k.getGeom().parent = None
			k = None
			v = None
			glDepthMask(GL_TRUE)

			# do collision detection
			self.__space.collide(0, self.nearCallback)

			# draw HUD
			Set2D()
			self.__score = int(math.floor(self.__score))
			if self.__score < 0:
				self.__score = 0
			if self.__score != self.__oldscore:
				font.addString(str(self.__score),'score')
				self.__oldscore = self.__score

			font.draw('score',0,iyres-40)
			font.draw('lives',128,0)
			self.__tPower.bind()
			if self.__power > 100.0:
				self.__power = 100.0
			amount = 14.0+((228.0/100.0)*self.__power)
			glRectangle(0,0,128,amount,0,1,1,1.0-(amount/256.0))
			self.__tShield.bind()
			if self.__shield > 100.0:
				self.__shield = 100.0
			amount = 14.0+((228.0/100.0)*self.__shield)
			glRectangle(0,0,128,amount,0,1,1,1.0-(amount/256.0))
			self.__tMeters.bind()
			glRectangle(0,0,128,256)
			self.__tCrosshair.bind()
			glRectangle((ixres/2)-16,(iyres/2)-16,(ixres/2)+16,(iyres/2)+16)
			glEnd()
			glDisable(GL_TEXTURE_2D)
			if self.__fadetimer > 0.0:
				glColor4f(self.__fadecolour[0],self.__fadecolour[1],self.__fadecolour[2],self.__fadetimer)
				glCallList(2)
				self.__fadetimer -= elapsed
				glColor4ub(255,255,255,255)
			
			# ACCELERATE!
			self.__time += elapsed
			self.__totaltime += elapsed
			while self.__time >= 1.0:
				self.__time -= 1.0
				self.__speed += 1
				self.__power -= 0.5
				self.__score += self.__speed
			# populate world with next round of obstacles
			self.__distance += slz
			self.__totaldistance += slz
			while self.__distance >= 50.0:
				self.__distance -= 50.0
				if random.randint(0, 2) == 0:
					# create some lightning. but.. burst or single?
					if random.randint(0, 4) == 0:
						# burst
						for i in xrange(random.randint(3,6)):
							lightning = Lightning(self.__world,self.__space,random.uniform(0,50))
							self.__lightning[lightning] = lightning
					else:
						# single
						lightning = Lightning(self.__world,self.__space,random.uniform(0,50))
						self.__lightning[lightning] = lightning
				if random.randint(0,5) == 0:
					# phlogiston!
					if random.randint(0, 5) == 0:
						# burst
						for i in xrange(random.randint(3,8)):
							phlogiston = Phlogiston(self.__world,self.__space,random.uniform(0,50),self.__tPhlogiston)
							self.__phlogiston[phlogiston] = phlogiston
					else:
						# single
						phlogiston = Phlogiston(self.__world,self.__space,random.uniform(0,50),self.__tPhlogiston)
						self.__phlogiston[phlogiston] = phlogiston
				if random.randint(0,5) == 0:
					# fat electron
					fatelectron = FatElectron(self.__world,self.__space,random.uniform(0,50))
					self.__fat_electrons[fatelectron] = fatelectron
				# bogon or cluon
				if random.randint(0,2)==0:
					#storm
					for i in xrange(random.randint(3,8)):
						if random.randint(0,1) == 0:
							bogon = BogonOrCluon(self.__world,self.__space,random.uniform(0,50),True)
							self.__bogons[bogon] = bogon
						else:
							cluon = BogonOrCluon(self.__world,self.__space,random.uniform(0,50),False)
							self.__cluons[cluon] = cluon
				else:
					#single
					if random.randint(0,1) == 0:
						bogon = BogonOrCluon(self.__world,self.__space,random.uniform(0,50),True)
						self.__bogons[bogon] = bogon
					else:
						cluon = BogonOrCluon(self.__world,self.__space,random.uniform(0,50),False)
						self.__cluons[cluon] = cluon
	
		else:
			# phew! if we got all the way down here, it means either the shield or the power got completely drained.
			if soundon:
				self.__shipdeadsound.play()
			self.__lives -= 1
			font.addString(str(self.__lives),'lives')
			if self.__lives == -1:
				nextLoop = menuLoop(mainMenu)
			else:
				# full-screen fade
				glDisable(GL_LIGHTING)
				if self.__shield <= 0.0:
					self.__fadecolour[0] = 1.0
					self.__fadecolour[1] = 0.6
					self.__fadecolour[2] = 1.0
				else:
					self.__fadecolour[0] = 0.6
					self.__fadecolour[1] = 0.8
					self.__fadecolour[2] = 1.0
				glColor4f(self.__fadecolour[0],self.__fadecolour[1],self.__fadecolour[2],1.0)
				self.__fadetimer = 1.0
				glCallList(2)
				glColor4ub(255,255,255,255)

				# delete all world objects (includes removing references to them)
				for i in xrange(7):
					self.__lights[i][0] = None
				for obj in self.__lightning.values():
					obj.getGeom().parent = None
				for obj in self.__bogons.values():
					obj.getGeom().parent = None
				for obj in self.__cluons.values():
					obj.getGeom().parent = None
				for obj in self.__phlogiston.values():
					obj.getGeom().parent = None
				for obj in self.__fat_electrons.values():
					obj.getGeom().parent = None
				for obj in self.__bullets.values():
					obj.getGeom().parent = None
				obj = None
				self.__lightning = {}
				self.__bogons = {}
				self.__cluons = {}
				self.__phlogiston = {}
				self.__fat_electrons = {}
				self.__bullets = {}
				self.__space.enable()
				gc.collect()
				if self.__space.getNumGeoms() != 1:
					print "Warning! " + str(self.__space.getNumGeoms()) + " left after death."

				# reset ship stats, position, etc.
				self.__speed = 8
				self.__power = 100.0
				self.__shield = 100.0
				# X and Y view angle
				self.__ax = 0.0
				self.__ay = 0.0
				# tunnel z-loop value
				self.__zpos = 0.0
				# ship position, which lags behind...
				self.__px = 0.0
				self.__py = 0.0
				
				# reset lighting
				# pulsing tunnel lights
				self.__pulse_r, self.__pulse_g, self.__pulse_b = 0, 0, 0
				# pulse rate
				self.__rate_r = 0.1
				self.__rate_g = 0.04
				self.__rate_b = 0.17
				

#
# World objects
#

class worldObject:
	# constructor - pass in ODE "world" and "space" to allow us to set up some collision geometry.
	# also, how far back from zfar should we start.
	def __init__(self,world,space,zstart):
		global zfar
		self.space = space
		self.z = -zfar-zstart
		self.doInit(world)
	# move forwards in z, passing in speed slz, then render. return new z distance (can be used as quick rejection test for collisions,
	# and to know when the object has gone behind the player and should be deleted outright)
	def draw(self,slz):
		pass
	def getGeom(self):
		return self.geom
	def __del__(self):
		#print "removing geom from space"
		self.space.remove(self.geom)

class Lightning(worldObject):
	__radius = 0.5
	def doInit(self,world):
		global tunnel_radius
		# pick two random start points on the tunnel walls, at least 80 degrees apart
		a = random.uniform(1,360)
		b = random.uniform(a+80,a+280)
		self.__ax = math.sin(math.radians(a))*tunnel_radius
		self.__ay = math.cos(math.radians(a))*tunnel_radius
		self.__bx = math.sin(math.radians(b))*tunnel_radius
		self.__by = math.cos(math.radians(b))*tunnel_radius
		self.__dx = self.__bx - self.__ax
		self.__dy = self.__by - self.__ay
		self.__t = 1.0
		# create capsule representing our geometry and add it to the ODE collision space
		# must have an associated body if it is to be placeable
		self.__body = ode.Body(world)
		self.__len = math.sqrt(pow(self.__dx,2)+pow(self.__dy,2))
		self.geom = ode.GeomCCylinder(self.space, self.__radius,  self.__len)
		self.geom.setBody(self.__body)
		# set collide bits - collide with ship-category objects
		self.geom.setCollideBits(1);
		self.geom.setCategoryBits(4);
		self.geom.parent = self;
		
		# position: half way between the two points; rotation: about Z only
		self.__cx = (self.__ax+self.__bx)/2
		self.__cy = (self.__ay+self.__by)/2
		self.geom.setPosition((self.__cx,self.__cy,self.z))

		# Rotate it 90 about X to stop it lying forwards along the Z axis,
		# then A about the Y axis to orient it along the lightning.
		# Add 90 degrees for luck, since it seemed to always be 90 out.
		a = math.atan2(self.__dy, self.__dx)+(math.pi/2)
		ca = math.cos(a)
		sa = math.sin(a)
		
		self.geom.setRotation((ca, 0, sa, sa, 0, -ca, 0, 1, 0))
		
	def draw(self,slz):
		global elapsed
		self.__x, self.__y, self.z = self.__body.getPosition()
		self.__body.setLinearVel((0,0,slz))
		# recalculate new lightning pattern twenty times per second
		# stops excessive flicker
		if self.__t >= 0.05:
			self.__t = math.fmod(self.__t, 0.05)
			self.__vertices = []
			self.__segments = []
			for i in xrange(4):
				segments = random.randint(2,10)
				self.__segments.append(segments-1)
				dx = self.__dx/segments
				dy = self.__dy/segments
				glBegin(GL_LINE_STRIP)
				glColor3ub(255,255,255)
				glVertex3f(self.__ax, self.__ay, self.z)
				switch = True
				for j in xrange(segments-1):
					ix = random.uniform(-self.__radius,self.__radius) + self.__ax + (dx*(j+1))
					iy = random.uniform(-self.__radius,self.__radius) + self.__ay + (dy*(j+1))
					iz = random.uniform(-self.__radius,self.__radius)
					if switch:
						glColor3ub(0,128,255)
						switch = False
					else:
						glColor3ub(255,255,255)
						switch = True
					glVertex3f(ix,iy,iz+self.z)
					self.__vertices.append([ix, iy, iz])
				glColor3ub(255,255,255)
				glVertex3f(self.__bx, self.__by, self.z)
				glEnd()
		else:
			c = 0
			for i in xrange(4):
				glBegin(GL_LINE_STRIP)
				glColor3ub(255,255,255)
				glVertex3f(self.__ax, self.__ay, self.z)
				switch = True
				for j in xrange(self.__segments[i]):
					if switch:
						glColor3ub(0,128,255)
						switch = False
					else:
						glColor3ub(255,255,255)
						switch = True
					glVertex3f(self.__vertices[c][0],self.__vertices[c][1],-self.__vertices[c][2]+self.z)
					c += 1
				glColor3ub(255,255,255)
				glVertex3f(self.__bx, self.__by, self.z)
				glEnd()
		#drawAABB(self.geom)
		self.__t += elapsed
		return self.z

class Phlogiston(worldObject):
	__radius = 1.0
	def __init__(self,world,space,zstart,texture):
		self.__texture = texture
		worldObject.__init__(self, world,space,zstart)
	
	def doInit(self,world):
		# create sphere representing our geometry and add it to the ODE collision space
		# must have an associated body if it is to be placeable
		self.__body = ode.Body(world)
		self.geom = ode.GeomSphere(self.space, self.__radius)
		self.geom.setBody(self.__body)
		self.__x, self.__y = genCoords(self.__radius)
		self.geom.setPosition((self.__x, self.__y, self.z))
		# set collide bits - collide with ship-category objects
		self.geom.setCollideBits(1);
		self.geom.setCategoryBits(8);
		self.geom.parent = self;
	
	def draw(self,slz):
		self.__x, self.__y, self.z = self.__body.getPosition()
		self.__body.setLinearVel((0,0,slz))
		
		# fake a quick spherical billboard
		glPushMatrix()
		glTranslatef(self.__x,self.__y,self.z)
		mvm = glGetFloatv(GL_MODELVIEW_MATRIX)
		for i in xrange(3):
			for j in xrange(3):
				if i != j:
					mvm[i][j] = 0.0
				else:
					mvm[i][j] = 1.0
		glLoadMatrixf(mvm)
		# read from depth buffer, but don't write to it
		self.__texture.bind()
		glRectangle(-self.__radius,-self.__radius,self.__radius,self.__radius)
		glEnd()	
		glPopMatrix()
		#drawAABB(self.geom)
		return self.z

class Bullet(worldObject):
	__radius = 0.25
	def __init__(self,world,space,px,py,dpx,dpy,dpz,texture):
		self.__texture = texture
		self.x, self.y = px, py
		self.__dx, self.__dy, self.__dz = dpx, dpy, dpz
		worldObject.__init__(self, world,space,0)
	
	def doInit(self,world):
		# create sphere representing our geometry and add it to the ODE collision space
		# must have an associated body if it is to be placeable
		self.__body = ode.Body(world)
		self.geom = ode.GeomSphere(self.space, self.__radius)
		self.geom.setBody(self.__body)
		# set bullet category bit
		self.geom.setCategoryBits(2);
		self.geom.setCollideBits(0);
		self.geom.parent = self;

		self.__body.setLinearVel((self.__dx, self.__dy, self.__dz))

		# give small initial offset so as not to obscure view when shooting
		#self.x += self.__dx*0.01
		#self.y += self.__dy*0.01
		#self.z = self.__dz*0.01
		self.z = 0

		self.geom.setPosition((self.x, self.y, self.z))
	
	def draw(self,slz):
		global elapsed
		self.x, self.y, self.z = self.__body.getPosition()
		
		# fake a quick spherical billboard
		glPushMatrix()
		glTranslatef(self.x,self.y,self.z)
		mvm = glGetFloatv(GL_MODELVIEW_MATRIX)
		for i in xrange(3):
			for j in xrange(3):
				if i != j:
					mvm[i][j] = 0.0
				else:
					mvm[i][j] = 1.0
		glLoadMatrixf(mvm)
		# read from depth buffer, but don't write to it
		self.__texture.bind()
		glRectangle(-self.__radius,-self.__radius,self.__radius,self.__radius)
		glEnd()	
		glPopMatrix()
		#drawAABB(self.geom)
		return self.z

class FatElectron(worldObject):
	__radius = 2.4
	def doInit(self,world):
		# create sphere representing our geometry and add it to the ODE collision space
		# must have an associated body if it is to be placeable
		self.__body = ode.Body(world)
		self.geom = ode.GeomSphere(self.space, self.__radius)
		self.geom.setBody(self.__body)
		self.__x, self.__y = genCoords(self.__radius)
		self.geom.setPosition((self.__x, self.__y, self.z))
		# set collide bits - collide with ship-category & bullet-category objects
		self.geom.setCollideBits(3);
		self.geom.setCategoryBits(16);
		self.geom.parent = self;
	
	def draw(self,slz):
		self.__x, self.__y, self.z = self.__body.getPosition()
		self.__body.setLinearVel((0,0,slz))
		glPushMatrix()
		glTranslatef(self.__x,self.__y,self.z)
		glScalef(self.__radius,self.__radius,self.__radius)
		glColor3f(1.5, 1.1, 0.2)
		glCallList(3)
		glPopMatrix()
		return self.z

# bogons and cluons render identically except for colour
class BogonOrCluon(worldObject):
	__radius = 0.8
	__orbit = 1.1
	__satellite_radius = 0.10
	def __init__(self,world,space,zstart,i_am_a_bogon):
		self.__i_am_a_bogon = i_am_a_bogon
		worldObject.__init__(self, world, space, zstart)
	
	def doInit(self, world):
		# create sphere representing our geometry and add it to the ODE collision space
		# must have an associated body if it is to be placeable
		self.__body = ode.Body(world)
		self.geom = ode.GeomSphere(self.space, self.__radius)
		self.geom.setBody(self.__body)
		self.x, self.__y = genCoords(self.__radius)
		self.__body.setPosition((self.x, self.__y, self.z))
		# set collide bits - collide with ship-category & bullet-category objects
		self.geom.setCollideBits(3)
		if self.__i_am_a_bogon:
			self.geom.setCategoryBits(32)
		else:
			self.geom.setCategoryBits(64)
		self.geom.parent = self;
		
		# create satellites - these orbit starting from random angles, with random angular rates.
		self.__satellites = []
		for i in xrange(4):
			self.__satellites.append([random.uniform(0,359),random.uniform(0,359),random.uniform(0,359),\
				random.choice([1,-1]),random.choice([1,-1]),random.choice([1,-1])])
	
	def draw(self,slz):
		global elapsed, zfar
		self.x, self.__y, self.z = self.__body.getPosition()
		self.__body.setLinearVel((0,0,slz))
		glPushMatrix() #1
		glTranslatef(self.x,self.__y,self.z)
		glPushMatrix() #2
		glScalef(self.__radius,self.__radius,self.__radius)
		if self.__i_am_a_bogon:
			glColor3ub(255, 0, 0)
		else:
			glColor3ub(0, 255, 0)
		if self.z < -zfar/2:
			glCallList(4)
		else:
			glCallList(3)
		glPopMatrix() #1
		if self.__i_am_a_bogon:
			glColor3ub(204, 0, 204)
		else:
			glColor3ub(102, 204, 102)			
		for satellite in self.__satellites:
			glPushMatrix() #2
			glRotatef(satellite[0],1,0,0)
			glRotatef(satellite[1],0,1,0)
			glRotatef(satellite[2],0,0,1)
			glTranslatef(self.__orbit,0,0)
			glScalef(self.__satellite_radius,self.__satellite_radius,self.__satellite_radius)
			satellite[0] += 120*elapsed*satellite[3]
			satellite[1] += 120*elapsed*satellite[4]
			satellite[2] += 120*elapsed*satellite[5]
			glCallList(5)
			glPopMatrix() #1
		glPopMatrix() #0
		#drawAABB(self.geom)
		return self.z

#
# Sound funcs
#

def doStereoSound(xpos, srcpos, sound):
	global zfar, tunnel_radius
	# set volume & panning according to source's z distance & x position
	vol = 1.0-(-srcpos.z/zfar)
	pos = (srcpos.x - xpos)/(tunnel_radius*2.0)
	right = 0.5+pos
	left =  0.5-pos
	sound.play().set_volume(left*vol,right*vol)

#
# Entry point!
#

# grab home dir, for saving options/high scores (unimplemented)
homedir = os.path.expanduser('~')

# initialise display - exit if we can't
r, e = DisplayInit()
if not r:
	sys.exit(e)
if not glInitSeparateSpecularColorEXT():
	sys.exit('Does not support required extension')
pygame.display.set_caption('accelerator 0.1.1')
pygame.mouse.set_visible(False)

# initialise pygame mixer
# hmm. initialising with anything other than defaults seems to mess up volume control.
pygame.mixer.pre_init(44100, -16, 32, 2048)
try:
	pygame.mixer.init()
except Exception, e:
	print 'Could not initialise mixer: ', e
	print 'Disabling sound support'
	soundon = False

# initialise screen, load in fonts & textures
Set2D()
font = Font('font.png',6,8,'abcdefghijklmnopqrstuvwxyz()-0123456789.:,\'"?!$%',20,0)
logo = Texture('logo.png')
circuit = Texture('circuit.png',True)
# create display lists for commonly used graphical elements
createTunnelDisplayList()
createSphereDisplayList()
createFullQuadDisplayList()

# allow only QUIT events
pygame.event.set_allowed(None)
pygame.event.set_allowed(QUIT)

# start by setting up the main menu!
nextLoop = menuLoop(mainMenu)

# This wraps up clearing the colour/depth buffers, grabbing elapsed time,
# grabbing mouse button and keyboard states, running the main loop,
# checking for quit, and flipping the screen buffers.
# If a QUIT event is found on the queue, or a main loop exits without setting
# a nextLoop, then the game is over (similarly for exceptions, which this catches).
try:
	clock = pygame.time.Clock()
	#seconds = 0.0
	while nextLoop:
		glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
		elapsed = float(clock.tick())/1000
		#seconds += elapsed
		nextLoop.doLoop()
		# don't do a global "get" here; any non-QUIT events will be wanted by the next loop iteration.
		if pygame.event.peek(QUIT):
			break
		pygame.display.flip()
		pygame.time.wait(1)
		#if seconds >= 1.0:
			#pygame.display.set_caption(str(clock.get_fps())+" fps")
			#seconds = 0.0
# exceptions are not "caught" in a finally, they are re-raised,
# so we can quit cleanly here and still get debugging info.
finally:
	del font, logo, circuit
	pygame.quit()
