// This file may be redistributed and modified only under the terms of
// the GNU General Public License (See COPYING for details).
// Copyright (C) 2000,2004 Alistair Riddoch
// Modified 2006 Philip Boulain

#ifdef WIN32
#include <Windows.h>
#define M_PI 3.14159265f
#endif

#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <stdbool.h>
#include <limits.h>
#include <assert.h>

#include <GL/gl.h>
#include <GL/glu.h>
#include <SDL.h>
#include <SDL_audio.h>

#include <libmd3/structure.h>
#include <libmd3/loader.h>
#include <libmd3/convert.h>
#include <libmd3/mesh.h>

#include "font.h"
#include "texture.h"

// Constants

#define GAME_TITLE "Magneactor" // Magnet reactor?!

// Make this true to cover things in green hair
#define USE_MODEL_NORMALS 0 /* They make strange things happen with lighting */
#define RENDER_NORMALS 0

static const int screen_width  = 800; // 640;
static const int screen_height = 600; // 480;

static const double far_clip = 25.0;

// Number of squares in the grid. The number of points is this number +1.
#define GRIDWIDTH  12
#define GRIDHEIGHT 12
//#define OUTERWALLLENGTH 12 /* Needed for array initialisation in draw_outer_walls */
#define MAXOBJECTS (GRIDWIDTH * GRIDHEIGHT)
static const int grid_width  = GRIDWIDTH;
static const int grid_height = GRIDHEIGHT;
//static const int max_objects = MAXOBJECTS;
static const int outerwall_height = GRIDWIDTH;

// Pick an invalidly high proximity value (two objects inside the grid cannot have a proximity of the entire grid)
static const int invalid_proximity = (GRIDWIDTH > GRIDHEIGHT ? GRIDWIDTH : GRIDHEIGHT);

// Number of milliseconds between steps in the game model.
static const int step_time = 500;

// Polarity representation (magnetism)
typedef enum { p_none = 0, p_red, p_blue } polarity_t; /* North and south, effectively */
typedef enum { d_right = 0, d_up, d_left, d_down } direction_t;

static inline void direction_turn(direction_t dir) { glRotatef(((float) dir) * 90, 0.0, 0.0, 1.0); }

// Structure to hold the properties of a single square on the grid.
// If you want to add more information to the grid, add new members here.
typedef struct block_properties {
	enum { ft_clear = 0, ft_radslime, ft_magtrack, ft_chute, ft_exit } floor;
	enum { bt_clear = 0, bt_wall, bt_magnet } block;
	direction_t direction; // Only applicable to magtrack
	polarity_t polarity;
} BlockProperties;

typedef struct object_properties {
	enum { ot_robot, ot_coolant } type; // Robot doesn't actually check type; anything else shouldn't be of type robot!
	direction_t direction; // Only applicable to robot
	polarity_t polarity; // Ignored for robot
	int x;
	int y;
	bool sinking;
	GLfloat sunk; // 0--1
	bool kill; // If true, will be deleted (avoids deletes mid-loop)
} ObjectProperties;

void object_init(ObjectProperties* obj) {
	obj->type = ot_coolant;
	obj->direction = d_right;
	obj->polarity = p_none;
	obj->x = obj->y = 0;
	obj->kill = false;
	obj->sinking = false;
	obj->sunk = 0.0;
}

/* typedef struct level_properties {
	BlockProperties  initial_blocks[GRIDWIDTH][GRIDHEIGHT];
	ObjectProperties initial_objects[MAXOBJECTS];
	int robot_start_x;
	int robot_start_y;
	int initial_object_count;
} LevelProperties; */

// Current blocks on the grid, stored as true if there is a block at the
// given localtion.
static BlockProperties properties[GRIDWIDTH][GRIDHEIGHT];
// Non-robot objects
static ObjectProperties objects[MAXOBJECTS];
static int object_count;
#undef GRIDWIDTH
#undef GRIDHEIGHT
#undef MAXOBJECTS
// Robot
static ObjectProperties robot;
static bool robotmoving;
static bool robotmustmove; // Should move at least once (short press)
static direction_t robotmovingdir;
// Level data
static char thislevel[64]; /* Filename of current level */
static char nextlevel[64]; /* Filename to load on win */
static char levelauthor[256];
static char leveladvice[512];
static int leveltime;
static const int SHOW_ADVICE_FOR = 10;
static const int LEVEL_TIME_STOPCARING = 10;

// Number of magnets remaining
static unsigned int magnets_left_red  = 0;
static unsigned int magnets_left_blue = 0;

// Temperature/victory
static const unsigned char HEAT_PANIC_LEVEL = 85; // Percentage
static unsigned int reactorheat_max = 120;
static unsigned int reactorheat;
static unsigned int barrel_cooling = 15; // How much cooling power barrels have
typedef enum { g_play = 0, g_victory, g_meltdown, g_crushed, g_slimed } gamestate_t;
static gamestate_t gamestate;

// GL lights availible/used
static GLint lights_availible; // Will be inited by glGet
static GLint lights_used = 0; // LIGHT0 reserved for robot

// Flag used to inform the main loop if the program should now terminate.
// Set this to true if its done.
static bool program_finished = false;

// Calculated frames per second to display. Very useful feedback when
// debugging graphics performance problems.
static int average_frames_per_second;

// Texture handles for the texture used to handle printing text on the
// screen.
static GLuint textTexture;
static GLuint textBase;

// Additional textures
static texture* tex_flr_normal;
static texture* tex_flr_radslime;
//static texture* tex_flr_magtrack;
static texture* tex_flr_chute;
static texture* tex_flr_exit;
static texture* tex_block_wall;
static texture* tex_block_magnet_red;
static texture* tex_block_magnet_blue;
//static texture* tex_object_robot;
//static texture* tex_object_coolant;
static texture* tex_outer_wall;
static texture* tex_dust;

static libmd3_file* mdl_barrel;
static libmd3_file* mdl_robot;
static libmd3_file* mdl_buttonout;
static libmd3_file* mdl_buttonin;

enum { cm_default, cm_orbitcube, cm_faraway } camera_mode;

typedef struct {
	Uint8   *sound;    /* Pointer to wave data */
	Uint32   soundlen; /* Length of wave data */
} Wave;

Wave wav_music;
Wave wav_place;
Wave wav_chute;
Wave wav_victory;
Wave wav_meltdown;
Wave wav_crushed;
Wave wav_slimed;

Uint32 musicplaypos;
Uint32 eventplaypos;
Wave* currentsoundevent;

static const float musicvolume = 0.5;

static bool paused;

static double endfx; // Starts at zero and rises by 1.0 every second after level end to maximum
static const double endfx_max = 4.0;

static GLUquadric* quadric; // Used for rendering dust sphere

static unsigned int score;
static unsigned int score_forbarrel;
static unsigned int score_forheatleft;
static unsigned int score_forexit;

bool gamecomplete;


libmd3_file* load_model(const char* model) {
	libmd3_file* mdl = libmd3_file_load(model);
	if(!mdl || !mdl->header->mesh_count) {
		fprintf(stderr, "Failed to get any meshes for model '%s'!\n", model);
		abort();
	}
	for(int i = 0; i < mdl->header->mesh_count; ++i) {
		// Need to do this *even if USE_MODEL_NORMALS is off*!
		//libmd3_strip_env_texcoords(&mdl->meshes[i]); // Old version
		libmd3_unpack_normals(&mdl->meshes[i]);
	}
	fprintf(stderr, "Loaded  model  %s\n", model);
	return mdl;
}

// Prototypes needed for initialisation rendering
void gl_print(const char * str);
void draw_model(libmd3_file* model);

// Initialise the graphics subsystem. This is pretty much boiler plate
// code with very little to worry about.
bool init_graphics() {
	GLint one = 1;
	
	// Initialise SDL
	if (SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO|SDL_INIT_TIMER) != 0) {
		fprintf(stderr, "Failed to intiialise SDL: %s\n", SDL_GetError());
		return false;
	}

	SDL_GL_SetAttribute(SDL_GL_RED_SIZE,     8);
	SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE,   8);
	SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE,    8);
	SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE,  16);
	SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

	SDL_Surface * screen;

	// Create the window
	screen = SDL_SetVideoMode(screen_width, screen_height, 0, SDL_OPENGL);
	if (screen == NULL) {
		fprintf(stderr, "Failed to set video mode: %s\n", SDL_GetError());
		SDL_Quit();
		return false;
	}

	SDL_WM_SetCaption(GAME_TITLE, GAME_TITLE);

	// Misc GL init
	glFrontFace(GL_CCW); /* triangle creation direction */
	glEnable(GL_CULL_FACE); /* backface culling (will be toggled by draw_model) */
	glCullFace(GL_BACK);
	glEnable(GL_BLEND); /* alpha transparency support */
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // WARNING: modified in rendering
	glEnable(GL_TEXTURE_2D); /* texturing */
	glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
	glEnable(GL_COLOR_MATERIAL);
	glEnable(GL_LINE_SMOOTH); /* Antialiasing of lines (NOT polygons) */
	glLineWidth(2.0); /* thicker gridlines */
	glEnable(GL_NORMALIZE); // Scaling may completely mung normals something evil
	glEnable(GL_LIGHTING); /* lighting */
	glShadeModel(GL_SMOOTH);
	//glShadeModel(GL_FLAT); // Shows up dodgy normals something grand
	glLightModeliv(GL_LIGHT_MODEL_LOCAL_VIEWER, &one);
	glGetIntegerv(GL_MAX_LIGHTS, &lights_availible);
	glEnable(GL_LIGHT0); /* 0 == robot's light */
	GLfloat lparms[] = { 0.0, 0.0, 0.0, 0.0 };
	glLightfv(GL_LIGHT0, GL_AMBIENT,  lparms);
	lparms[0] = 0.5; lparms[1] = 0.5; lparms[2] = 0.5;
	glLightfv(GL_LIGHT0, GL_DIFFUSE,  lparms);
	lparms[0] = 0.9; lparms[1] = 0.9; lparms[2] = 0.9;
	glLightfv(GL_LIGHT0, GL_SPECULAR, lparms);
	glLightf( GL_LIGHT0, GL_CONSTANT_ATTENUATION,  0.0);
	glLightf( GL_LIGHT0, GL_LINEAR_ATTENUATION,    0.0);
	glLightf( GL_LIGHT0, GL_QUADRATIC_ATTENUATION, 0.01);
	glLightf( GL_LIGHT0, GL_SPOT_EXPONENT,         0.0);
	glLightf( GL_LIGHT0, GL_SPOT_CUTOFF,          45.0);
	/* Configure other lights in advance */
	for(int i = 1; i < lights_availible; i++) {
		glLightf( GL_LIGHT0+i, GL_CONSTANT_ATTENUATION,  0.0);
		glLightf( GL_LIGHT0+i, GL_LINEAR_ATTENUATION,    0.0);
		glLightf( GL_LIGHT0+i, GL_QUADRATIC_ATTENUATION, 0.05);
	}

	// Setup the viewport transform
	glViewport(0, 0, screen_width, screen_height);

	// Enable vertex arrays
	glEnableClientState(GL_VERTEX_ARRAY);
	// Texture coordinate arrays well need to be enabled _ONLY_ when using
	// texture coordinates from an array, and disabled afterwards.
	// glEnableClientState(GL_TEXTURE_COORD_ARRAY);

	// Set the colour the screen will be when cleared
	glClearColor(0.125, 0.125, 0.125, 0.0);

	// Set up fog---used later as a meltdown effect
	glFogi(GL_FOG_MODE, GL_LINEAR);
	glFogf(GL_FOG_START, 0.0);
	glDisable(GL_FOG); // Not yet!

	// Initialise the texture used for rendering text
	glGenTextures(1, &textTexture);
	glBindTexture(GL_TEXTURE_2D, textTexture);
	glEnableClientState(GL_TEXTURE_COORD_ARRAY);
	glTexImage2D(GL_TEXTURE_2D, 0, texture_font_internalFormat,
				 texture_font_width, texture_font_height, 0,
				 texture_font_format, GL_UNSIGNED_BYTE, texture_font_pixels);
	if (glGetError() != 0) {
		return false;
	}
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	textBase = glGenLists(256);
	float vertices[] = { 0, 0, 16, 0, 16, 16, 0, 16 };
	glVertexPointer(2, GL_FLOAT, 0, vertices);
	for(int loop=0; loop<256; loop++) {
		float cx=(float)(loop%16)/16.0f;	  // X Position Of Current Character
		float cy=(float)(loop/16)/16.0f;	  // Y Position Of Current Character

		float texcoords[] = { cx, 1-cy-0.0625f,
		                      cx+0.0625f, 1-cy-0.0625f,
		                      cx+0.0625f, 1-cy,
		                      cx, 1-cy };

		glNewList(textBase+loop,GL_COMPILE); // Start Building A List

		glTexCoordPointer(2, GL_FLOAT, 0, texcoords);
		glDrawArrays(GL_QUADS, 0, 4);

		glTranslated(10,0,0); // Move To The Right Of The Character
		glEndList(); // Done Building The Display List
	}
	glDisableClientState(GL_TEXTURE_COORD_ARRAY);

	// Disable the depth test, as its not useful when rendering text
	glDisable(GL_DEPTH_TEST);
	glDisable(GL_LIGHTING); // And lighting

	// Show loading message
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glOrtho(0, screen_width, 0, screen_height, -800.0f, 800.0f);
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	glPushMatrix();
	glTranslatef((screen_width / 2.0) - 48.0, (screen_height * 0.5) - 8, 0.0);
	glClear(GL_COLOR_BUFFER_BIT);
	gl_print("Loading...");
	SDL_GL_SwapBuffers();
	glClear(GL_COLOR_BUFFER_BIT); /* Putting message in both buffers helps */
	gl_print("Loading...");
	SDL_GL_SwapBuffers();
	glPopMatrix();

	// Load other textures
	tex_flr_normal        = texture_new("textures/metalfloor.png");
	tex_flr_radslime      = texture_new("textures/radslimefloor.png");
//	tex_flr_magtrack      = texture_new("textures/lava01.jpg"); // TODO
	tex_flr_chute         = texture_new("textures/chute.png");
	tex_flr_exit          = texture_new("textures/dirtystripes.png");
	tex_block_wall        = texture_new("textures/metalwall.jpg");
	tex_block_magnet_red  = texture_new("textures/magred.jpg");
	tex_block_magnet_blue = texture_new("textures/magblue.jpg");
//	tex_object_robot      = texture_new("textures/robot.jpg");
//	tex_object_coolant    = texture_new("textures/coolant.jpg");
	tex_outer_wall        = texture_new("textures/metalwall.jpg"); // TODO Better texture?
	tex_dust              = texture_new("textures/dust.png");

	// Load models
	mdl_barrel    = load_model("models/barrelx.md3");
	mdl_robot     = load_model("models/robotx.md3");
	mdl_buttonout = load_model("models/andbutton1x.md3");
	mdl_buttonin  = load_model("models/andbutton2x.md3");

	// Force models to load textures by drawing them offscreen
	glTranslatef(screen_width, 0.0, 0.0);
	draw_model(mdl_barrel);
	draw_model(mdl_robot);
	draw_model(mdl_buttonout);
	draw_model(mdl_buttonin);

	// Set up quadric
	quadric = gluNewQuadric();
	gluQuadricNormals(quadric, GLU_SMOOTH);
	gluQuadricTexture(quadric, GL_TRUE);

	return true;
}

void load_wav(char* wavfile, Wave* into, SDL_AudioSpec* spec) {
	SDL_AudioSpec filespec;
	SDL_AudioCVT  conversion;
	
	if(!SDL_LoadWAV(wavfile, &filespec, &into->sound, &into->soundlen)) {
		fprintf(stderr, "Failed to load wavfile '%s': %s\n", wavfile, SDL_GetError());
		exit(1);
	}
	fprintf(stderr, "Loaded wavfile %s, %i bytes, %i channel(s), %iHz.\n", wavfile, into->soundlen, filespec.channels, filespec.freq);

	/* Only convert if not the same */
	if((filespec.format   != spec->format)   ||
	   (filespec.channels != spec->channels) ||
	   (filespec.freq     != spec->freq)) {
		if(0 == SDL_BuildAudioCVT(&conversion,
			filespec.format, filespec.channels, filespec.freq,
			   spec->format,    spec->channels,    spec->freq))
			{ fprintf(stderr, "Failed to build conversion table for %s (%s)!\n", wavfile, SDL_GetError()); return; }
		conversion.len = into->soundlen;
		conversion.buf = malloc(conversion.len * conversion.len_mult);
		memcpy(conversion.buf, into->sound, conversion.len);
		free(into->sound);
		into->sound = conversion.buf;
		into->soundlen = conversion.len * conversion.len_ratio;
		if(SDL_ConvertAudio(&conversion))
			{ fprintf(stderr, "Failed to convert wavfile %s!\n", wavfile); return; }
//		fprintf(stderr, "Converted size is  %i bytes.\n", into->soundlen);
	}
}

void fill_callback(void* pdata, Uint8* stream, int len) {
	Uint8* waveptr;
	int    waveleft;

	Uint8* ostream = stream;
	int    olen    = len;

	/* Music */
	waveptr  = wav_music.sound    + musicplaypos;
	waveleft = wav_music.soundlen - musicplaypos;
	while(waveleft <= len) {
		SDL_MixAudio(stream, waveptr, waveleft, SDL_MIX_MAXVOLUME * musicvolume);
		stream += waveleft;
		len -= waveleft;
		waveptr = wav_music.sound;
		waveleft = wav_music.soundlen;
		musicplaypos = 0;
	}
	if(len) {
		SDL_MixAudio(stream, waveptr, len, SDL_MIX_MAXVOLUME * musicvolume);
		musicplaypos += len;
	}

	/* Event sound (if any) */
	if(currentsoundevent) {
		waveptr  = currentsoundevent->sound    + eventplaypos;
		waveleft = currentsoundevent->soundlen - eventplaypos;
		len = (olen < waveleft) ? olen : waveleft;
		SDL_MixAudio(ostream, waveptr, len, SDL_MIX_MAXVOLUME * (1 - musicvolume));
		eventplaypos += len;
	}
	//waveptr = ostream; waveleft = len; while(waveleft--) { *waveptr++ = sin(waveleft); } // DEBUG!
}

void init_audio() {
	char adname[32];
	SDL_AudioSpec spec = {
		.freq     = 22050,
		.format   = AUDIO_S16,
		.channels = 2, /* Stereo */
		.samples  = 512, /* Low latency */
		.callback = fill_callback,
		.userdata = NULL,
	};
	
	if(SDL_OpenAudio(&spec, NULL) == -1) { fprintf(stderr, "WARNING: SDL_OpenAudio failed!\n"); }
	
	load_wav("audio/ies13musx22trim.wav", &wav_music,    &spec);
	load_wav("audio/place.wav",           &wav_place,    &spec);
	load_wav("audio/chute.wav",           &wav_chute,    &spec);
	load_wav("audio/victory.wav",         &wav_victory,  &spec);
	load_wav("audio/meltdown.wav",        &wav_meltdown, &spec);
	load_wav("audio/crushed.wav",         &wav_crushed,  &spec);
	load_wav("audio/slimed.wav",          &wav_slimed,   &spec);
	
	musicplaypos = 0;
	eventplaypos = 0;
	currentsoundevent = 0;

	SDL_PauseAudio(0); // Go, callback, go!

	fprintf(stderr, "Using audio driver: %s\n", SDL_AudioDriverName(adname, sizeof(adname)));
//	if(SDL_GetAudioStatus() == SDL_AUDIO_PLAYING) { fprintf(stderr, "Playback in progress\n"); }
}

void playeventsound(Wave* event) {
	eventplaypos = 0;
	currentsoundevent = event;
}

// Clear the grid state.
void clear()
{
	for(int i = 0; i < grid_width; ++i) {
		for(int j = 0; j < grid_height; ++j) {
			properties[i][j].floor = ft_clear;
			properties[i][j].block = bt_clear;
			properties[i][j].polarity = p_none;
		}
	}
	object_count = 0;
}

// Print a text string on the screen at the current position.
void gl_print(const char * str)
{
	glPushMatrix();
//	glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
	glBindTexture(GL_TEXTURE_2D, textTexture);
//	glEnable(GL_TEXTURE_2D);
//	glEnable(GL_BLEND);
	glListBase(textBase-32);
	glCallLists(strlen(str),GL_BYTE,str);
//	glDisable(GL_BLEND);
//	glDisable(GL_TEXTURE_2D);
//	glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
	glPopMatrix();
	glBindTexture(GL_TEXTURE_2D, 0);
}

void changegamestate(gamestate_t state) {
	if(state) {
		switch(state) {
			case g_play: break; // Keep GCC happy
			case g_victory:
				camera_mode = cm_faraway;
				playeventsound(&wav_victory);
				break;
			case g_meltdown:
				camera_mode = cm_default;
				playeventsound(&wav_meltdown);
				break;
			case g_crushed:
				//camera_mode = cm_orbitcube; // Any, really
				playeventsound(&wav_crushed);
				break;
			case g_slimed:
				camera_mode = cm_orbitcube;
				playeventsound(&wav_slimed);
				break;
		}
	} else {
		camera_mode = cm_default;
		glEnable(GL_LIGHT0); // May have been disabled if crushed
	}
	gamestate = state;
}

void load_level(const char* levelname) {
	char buf[256];
	FILE* levelfile;
	int items;
	int inh;
	int barx, bary; char barp;
	double lastheatpct = reactorheat_max ? (((double) reactorheat * 100.0) / reactorheat_max) : 0;
	
	// Init. to defaults
	clear(); // Sets properties[] and object_count
	changegamestate(g_play);
	if(thislevel != levelname) // Don't copy if reloading level (same memory!)
		{ strncpy(thislevel, levelname, 64); }
	strncpy(nextlevel, levelname, 64);
	strncpy(levelauthor, "(unknown)", 256);
	strncpy(leveladvice, "", 256);
	leveltime = 0; // Time in level---ALWAYS init to zero!
	endfx = 0.0; // Likewise
	reactorheat_max = 120;
	barrel_cooling = 15;
	magnets_left_red = magnets_left_blue = 0;
	robot.x = robot.y = 0;
	robot.direction = d_right;
	score_forbarrel   = 100; // Effectively worth barrel_cooling * heatleft, too
	score_forheatleft =   5; // -> About 490 per level if you're good..
	score_forexit     = 500; // Probably plus some barrels

	// Attempt to open level file
	snprintf(buf, 255, "levels/%s", levelname);
	fprintf(stderr, "Loading level  %s\n", buf);
	levelfile = fopen(buf, "r");
	if(!levelfile) { fprintf(stderr, "\tFailed to open file!\n"); abort(); }
	fscanf(levelfile, " AUTHOR %255s", levelauthor);
	items = fscanf(levelfile, " ADVICE %s", leveladvice);
	if(items) { // Got some advice
		items = strlen(leveladvice); // Gratuitous overloading
		while(!feof(levelfile) &&
		      (buf[0] = fgetc(levelfile)) &&
		      (buf[0] != '\n') &&
		      (items < 512)) {
			leveladvice[items] = buf[0];
			items++;
		}
	}
	leveladvice[items] = '\0';
	items = fscanf(levelfile, " NEXT %255s", nextlevel);
	if(!items) { fprintf(stderr, "No next level --- will loop!\n"); }
	fscanf(levelfile, " LIMIT %u %u", &magnets_left_red, &magnets_left_blue);
	fscanf(levelfile, " TIME %u", &reactorheat_max);
	reactorheat_max *= (1000 / step_time); // Convert to seconds
	fscanf(levelfile, " COOLEFFECT %u", &barrel_cooling);
	barrel_cooling *= (1000 / step_time); // Convert to seconds
	items = fscanf(levelfile, " INHERIT %d", &inh);
	if(items) { // Got inherit limit
//		fprintf(stderr, "\tLevel has inherit limit of %d%%; previous heat %d%%;", inh, (int) lastheatpct);
		lastheatpct = (lastheatpct > inh) ? inh : lastheatpct;
		reactorheat = (lastheatpct / 100.0) * reactorheat_max;
//		fprintf(stderr, " inherited %d%% (%ds)\n", (int) lastheatpct, reactorheat);
	}
	fscanf(levelfile, " SCOREBARREL %u", &score_forbarrel);
	fscanf(levelfile, " SCOREHEAT %u", &score_forheatleft);
	fscanf(levelfile, " SCOREEXIT %u", &score_forexit);
//	fprintf(stderr, "Scoring: bar=%u, heat=%u, exit=%u\n", score_forbarrel, score_forheatleft, score_forexit);
	fscanf(levelfile, " ROBOT %d %d %c", &robot.x, &robot.y, buf);
	switch(buf[0]) {
		case '>': robot.direction = d_right; break;
		case '^': robot.direction = d_up;    break;
		case '<': robot.direction = d_left;  break;
		case 'v': robot.direction = d_down;  break;
		default:  fprintf(stderr, "Bad robot direction '%c'\n", buf[0]); break; // Nonfatal
	}
	robot.y = grid_height - ++robot.y;
	while((items = fscanf(levelfile, " BARREL %d %d %c", &barx, &bary, &barp))) {
		object_init(&objects[object_count]);
		objects[object_count].x = barx;
		objects[object_count].y = grid_height - ++bary;
		objects[object_count].polarity = (barp == 'R' ? p_red : p_blue);
		object_count++;
	}
	fscanf(levelfile, " DATA ");

	// Read data section
	for(int y = grid_height - 1; y >= 0; y--) {
		for(int x = 0; x < grid_width; x++) {
			switch((buf[0] = fgetc(levelfile))) {
				case '.': // empty
					break;
				case '#': // wall
					properties[x][y].block = bt_wall;
					break;
				case '~': // slime
					properties[x][y].floor = ft_radslime;
					break;
				case 'O': // chute
					properties[x][y].floor = ft_chute;
					break;
				case 'x': // exit
					properties[x][y].floor = ft_exit;
					break;
				case 'R': // existing red mag
					properties[x][y].block = bt_magnet;
					properties[x][y].polarity = p_red;
					break;
				case 'B': // existing blue mag
					properties[x][y].block = bt_magnet;
					properties[x][y].polarity = p_blue;
					break;
				default : // ohnoes
					fprintf(stderr, "Unrecognized level character '%c'\n", buf[0]);
					exit(1);
			}
		}
		fscanf(levelfile, " ");
	}
	
	// Close level file
	fclose(levelfile);
//	fprintf(stderr, "Level loaded OK: %s - %s\n", levelauthor, leveladvice);
}

void setup()
{
	// Init the robot
	robot.type = ot_robot;
	robot.polarity = p_none;
	
	// Init reactor heat, score, and game completion
	reactorheat = 0;
	score = 0;
	gamecomplete = false;
	
	// Init level
	load_level("start");
	paused = true;
}

void cleanup() {
	fprintf(stderr, "Unloading...");

	// Better shut down audio before we free the samples...
	SDL_PauseAudio(1);
	
	// Free heap used for texture handles
	texture_destroy(tex_flr_normal);
	texture_destroy(tex_flr_radslime);
//	texture_destroy(tex_flr_magtrack);
	texture_destroy(tex_flr_chute);
	texture_destroy(tex_flr_exit);
	texture_destroy(tex_block_wall);
	texture_destroy(tex_block_magnet_red);
	texture_destroy(tex_block_magnet_blue);
	texture_destroy(tex_outer_wall);
	texture_destroy(tex_dust);

	// Free mesh textures (libmd3 has no mechanism to free meshes themselves =/ )
	texture_destroy(mdl_barrel->user.p);
	texture_destroy(mdl_robot->user.p);
	texture_destroy(mdl_buttonout->user.p);
	texture_destroy(mdl_buttonin->user.p);

	// Should probably SDL_reeWAV here too
	SDL_FreeWAV(wav_music.sound);
	SDL_FreeWAV(wav_place.sound);
	SDL_FreeWAV(wav_chute.sound);
	SDL_FreeWAV(wav_victory.sound);
	SDL_FreeWAV(wav_meltdown.sound);
	SDL_FreeWAV(wav_crushed.sound);
	SDL_FreeWAV(wav_slimed.sound);
	
	// Free quadric
	gluDeleteQuadric(quadric);

	fprintf(stderr, "bye!\n");
}

// FIXME These seem to get lit a bit odd, but I can't see any normal problems...
void draw_floor() {
	glEnableClientState(GL_TEXTURE_COORD_ARRAY);

	static const float texcoords[] = {
		0.5, 0.5,
		0.0, 0.0,
		1.0, 0.0,
		1.0, 1.0,
		0.0, 1.0,
		0.0, 0.0,
	};
	
	static const float vertices[] = {
		0.5, 0.5, 0.0,
		0.0, 0.0, 0.0,
		1.0, 0.0, 0.0,
		1.0, 1.0, 0.0,
		0.0, 1.0, 0.0,
		0.0, 0.0, 0.0,
	};
	
	glVertexPointer(  3, GL_FLOAT, 0, vertices);
	glTexCoordPointer(2, GL_FLOAT, 0, texcoords);
	glNormal3f( 0.0,  0.0,  1.0);
	glDrawArrays(GL_TRIANGLE_FAN, 0, 6);

	glDisableClientState(GL_TEXTURE_COORD_ARRAY);
}

void draw_outer_walls() { // Assumes square grid!
	glEnableClientState(GL_TEXTURE_COORD_ARRAY);
	glPushMatrix();

	glTranslatef(grid_width / 2.0, grid_height / 2.0, 0.0);
	for(int wall = 0; wall < 4; wall++) {
		glPushMatrix();
		glTranslatef(grid_width / 2.0, -grid_height / 2.0, 0.0);
		glRotatef(-90, 0.0, 1.0, 0.0); // Rotate to vertical
		for(int along = 0; along < grid_width; along++) {
			for(int up = 0; up < outerwall_height; up++) {
				draw_floor();
				glTranslatef(1.0, 0.0, 0.0);
			}
			glTranslatef(-outerwall_height, 0.0, 0.0); // Avoids matrix stackwork
			glTranslatef(0.0, 1.0, 0.0);
		}
		glPopMatrix();
		glRotatef(90, 0.0, 0.0, 1.0); // Rotate for next wall
	}

	glPopMatrix();
	glDisableClientState(GL_TEXTURE_COORD_ARRAY);
}

void draw_unit_cube()
{
	glEnableClientState(GL_TEXTURE_COORD_ARRAY);

	static const float texcoords[] = {
		0.f, 0.f,
		0.f, 1.f,
		1.f, 1.f,
		1.f, 0.f,
	};
	
	static const float front_vertices[] = {
		0.f, 0.f, 1.f,
		1.f, 0.f, 1.f,
		1.f, 1.f, 1.f,
		0.f, 1.f, 1.f,
	};
	glVertexPointer(  3, GL_FLOAT,             0, front_vertices);
//	glTexCoordPointer(2, GL_FLOAT, sizeof(float), front_vertices);
	glTexCoordPointer(2, GL_FLOAT, 0, texcoords);
	glNormal3f( 0.0,  0.0,  1.0);
	glDrawArrays(GL_QUADS, 0, 4);

	static const float left_vertices[] = {
		0.f, 0.f, 0.f,
		0.f, 0.f, 1.f,
		0.f, 1.f, 1.f,
		0.f, 1.f, 0.f,
	};
	glVertexPointer(  3, GL_FLOAT,             0, left_vertices);
//	glTexCoordPointer(2, GL_FLOAT, sizeof(float), left_vertices + 1);
	glTexCoordPointer(2, GL_FLOAT, 0, texcoords);
	glNormal3f(-1.0,  0.0,  0.0);
	glDrawArrays(GL_QUADS, 0, 4);

	static const float right_vertices[] = {
		1.f, 0.f, 1.f,
		1.f, 0.f, 0.f,
		1.f, 1.f, 0.f,
		1.f, 1.f, 1.f,
	};
	glVertexPointer(  3, GL_FLOAT,             0, right_vertices);
	glTexCoordPointer(2, GL_FLOAT, 0, texcoords);
	glNormal3f( 1.0,  0.0,  0.0);
	glDrawArrays(GL_QUADS, 0, 4);

	static const float top_vertices[] = {
		0.f, 1.f, 1.f,
		1.f, 1.f, 1.f,
		1.f, 1.f, 0.f,
		0.f, 1.f, 0.f,
	};
	glVertexPointer(  3, GL_FLOAT, 0, top_vertices);
	glTexCoordPointer(2, GL_FLOAT, 0, texcoords);
	glNormal3f( 0.0,  1.0,  0.0);
	glDrawArrays(GL_QUADS, 0, 4);

	static const float bottom_vertices[] = {
		0.f, 0.f, 0.f,
		1.f, 0.f, 0.f,
		1.f, 0.f, 1.f,
		0.f, 0.f, 1.f,
	};
	glVertexPointer(  3, GL_FLOAT, 0, bottom_vertices);
	glTexCoordPointer(2, GL_FLOAT, 0, texcoords);
	glNormal3f( 0.0, -1.0,  0.0);
	glDrawArrays(GL_QUADS, 0, 4);
	
	glDisableClientState(GL_TEXTURE_COORD_ARRAY);
}

void draw_model(libmd3_file* model) {
	libmd3_mesh* mesh;
	char* c;
	char* cc;
	char texnam[256];
	char texraw[256];
	mesh = model->meshes;
	glPushMatrix();
// #define DOWNSCALE (1.0 / 8192.0)
#define DOWNSCALE (1.0 / 10000.0)
	glScalef(DOWNSCALE, DOWNSCALE, DOWNSCALE);
#undef  DOWNSCALE
	// FIXME This should cache textures properly: identifty an already-loaded name and re-use the ID number (i.e. don't load again)
	for(int i = 0; i < model->header->mesh_count; i++, mesh++) {
		if (mesh->mesh_header->skin_count != 0) {
			if (mesh->user.p == 0) {
				if(i && !strcmp((char*)(mesh-1)->skins[0].name, (char*)mesh->skins[0].name))
				{ mesh->user.p = model->meshes->user.p; } else { // FIXME This is a hack to fake texture caching for this limited case
				// Fixup texture name to include no path component
				strncpy(texraw, (char*)mesh->skins[0].name, sizeof(texraw)); // Don't modify original
				c = cc = texraw;
				while(*c){c++;}c--; // Find end
					// Find last '\' or '/'
				while((c > cc) && (*c != '\\') && (*c != '/')) {c--;}
				c++;
					// Copy texture name back to start
				if(c != cc) {
//					fprintf(stderr, "Mapping texture name '%s' to 'textures/%s'\n", cc, c);
					while(*c) {
						*cc = *c;
						c++;
						cc++;
					}
					*cc = '\0';
				}
				// Load texture
				snprintf(texnam, 255, "textures/%s", texraw);
				mesh->user.p = texture_new(texnam);
				} // FIXME Part of above hack
			}
		}
		if(mesh->user.p) {
			glEnableClientState(GL_TEXTURE_COORD_ARRAY);
		}
		texture_apply(mesh->user.p); // Might be NULL, which is correct (texturing off)
		glVertexPointer(3, GL_SHORT, 0, mesh->vertices);
		glTexCoordPointer(2, GL_FLOAT, 0, mesh->texcoords);
		if(USE_MODEL_NORMALS && mesh->normals)
			{ glNormalPointer(GL_FLOAT, 0, mesh->normals); }
		else
			{ glNormal3f(0.0, 0.0, 1.0); /* Fake it as best we can */ }
		glDisable(GL_CULL_FACE); // Yeah, you WISH the polies were wound right... ;P
		glDrawElements(GL_TRIANGLES, mesh->mesh_header->triangle_count * 3,
			GL_UNSIGNED_INT, mesh->triangles);
		glEnable(GL_CULL_FACE); // Put this back on for all the _sane_ geometry
		if(mesh->user.p) {
			texture_apply(0);
			glDisableClientState(GL_TEXTURE_COORD_ARRAY);
		}
		/* Hairy models for debugging */
#if RENDER_NORMALS
		if(USE_MODEL_NORMALS && mesh->normals) {
			glDisable(GL_LIGHTING);
			glColor3f(0.0, 1.0, 0.0);
			for(int i = 0; i < mesh->mesh_header->vertex_count; i++) {
				glPushMatrix();
				glTranslatef(mesh->vertices[i*3 + 0],
				             mesh->vertices[i*3 + 1],
				             mesh->vertices[i*3 + 2]);
				glScalef(2000.0, 2000.0, 2000.0);
				glBegin(GL_LINES);
				glVertex3f(0.0, 0.0, 0.0);
				glVertex3f(mesh->normals[i*3 + 0],
				           mesh->normals[i*3 + 1],
				           mesh->normals[i*3 + 2]);
				glEnd();
				glPopMatrix();
			}
			glColor3f(1.0, 1.0, 1.0);
			glEnable(GL_LIGHTING);
		}
#endif
	}
	glPopMatrix();
}

void draw_grid(void)
{
	// Move to the origin of the grid
	glPushMatrix();
	glTranslatef(-(float)grid_width/2.0f, -(float)grid_height/2.0f, 0.0f);

	glColor4f(1.0, 1.0, 1.0, 1.0);

	if(gamestate == g_meltdown) { // Use fog as a simple heat effect
		       if(endfx < 1.0) {
			GLfloat fogcolour[] = {endfx * 4.0, endfx * 2.0, endfx, 1.0};
			glFogfv(GL_FOG_COLOR, fogcolour);
			glFogf(GL_FOG_END, far_clip - (endfx * far_clip));
		// Could skip rendering the middle section in these next two cases
		} else if(endfx < 2.0) {
			GLfloat fogcolour[] = {1.0, 1.0, 1.0, 1.0};
			glFogfv(GL_FOG_COLOR, fogcolour);
			glFogf(GL_FOG_END, 0.0);
		} else {
			register GLfloat fade = 3.0 - endfx; // GL will clip
			GLfloat fogcolour[] = {fade, fade, fade, 1.0};
			glFogfv(GL_FOG_COLOR, fogcolour);
		}
		glEnable(GL_FOG);
	}

	// Set up lighting
	lights_used = 1; // Reserve first for robot
		// Robot
	glPushMatrix();
	if(gamecomplete) {
		register GLfloat efx3 = endfx < 3.0 ? endfx : 3.0;
		glTranslatef(efx3 + 7.5, 6.5, 0.0);
	} else {
		glTranslatef(robot.x + 0.5, robot.y + 0.5, 0.f);
		direction_turn(robot.direction);
	}
	{ GLfloat here[] = { 0.0, 0.0, 0.425, 1.0 }; // Just about right for self-illumination
	  if(gamestate == g_victory && !gamecomplete) { // Raise light with robot if exiting
		register double rise = endfx / 2.0;
		if(rise > 2.0) { rise = 2.0; }
		here[2] += rise;
	  }
	  glLightfv(GL_LIGHT0, GL_POSITION, here);
	  here[0] = 1.0; here[1] = 0.0; here[2] = 0.0;
	  glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, here); }
	glPopMatrix();
		// Blocks
	glPushMatrix();
	{ GLfloat here[] = { 0.5, 0.5, 1.2, 1.0 }; // Block off-centre compensation
	  GLfloat colour[] = { 0.0, 0.0, 0.0, 1.0 };
#define LIGHTHERE(R,G,B) \
	if(lights_used < lights_availible) { \
	glLightfv(GL_LIGHT0 + lights_used, GL_POSITION, here); \
	colour[0] = (R); colour[1] = (G); colour[2] = (B); \
	glLightfv(GL_LIGHT0 + lights_used, GL_DIFFUSE, colour); \
	glEnable(GL_LIGHT0 + lights_used++); }
	for(int i = 0; i < grid_width; ++i) {
		for(int j = 0; j < grid_height; ++j) {
			if ((properties[i][j].block)) {
				// Draw block
				switch(properties[i][j].block) {
					case bt_magnet:
						if(!properties[i][j].polarity) {
							fprintf(stderr, "Non-magnetic magnet!\n");
							abort();
						}
						if(properties[i][j].polarity == p_red) {
							LIGHTHERE(0.5, 0.05, 0.05);
						} else {
							LIGHTHERE(0.05, 0.05, 0.5);
						}
						break;
					default: break; // Shut gcc up
				}
			} else {
				// Draw floor
				switch(properties[i][j].floor) {
					case ft_radslime:
						LIGHTHERE(0.02, 0.2, 0.02);
						break;
					case ft_exit:
						if(gamecomplete) {
							LIGHTHERE(0.05, 0.05, 0.005);
						} else {
							LIGHTHERE(0.5, 0.5, 0.05);
						}
						break;
					default: break; // Shut gcc up
				}
			}
			glTranslatef(0.0f, 1.0f, 0.0f);
		}
		glTranslatef(1.0f, -grid_height, 0.0f);
	}
	glPopMatrix();
		// Objects
	for(int i = 0; i < object_count; i++) {
		double fade = 1.0 - objects[i].sunk;
		switch(objects[i].type) {
			case ot_coolant:
				glPushMatrix();
				glTranslatef(objects[i].x + 0.5, objects[i].y + 0.5, 1.2);
				if(objects[i].polarity == p_red) {
					LIGHTHERE(0.20*fade, 0.01*fade, 0.01*fade);
				} else {
					LIGHTHERE(0.01*fade, 0.01*fade, 0.20*fade);
				}
				glPopMatrix();
				break;
			default: break; // Shut gcc up
		}
	}
		// 'Avert nuclear disaster' button
	here[0] = here[1] = here[2] = 0.0; // FROM NOW ON, here == *RIGHT HERE*
	if(gamecomplete) {
		register GLfloat red, gb;
		if(endfx < 3.0) {
			red = cos(endfx * M_PI); red *= red;
			gb  = 0.01;
		} else {
			register GLfloat e = endfx - 3.0;
			register GLfloat f = 1.0 - e;
			red = f*1.0  + e*0.15;
			gb  = f*0.01 + e*0.20;
		}
		glPushMatrix();
		glTranslatef(10.75, 6.5, 1.5);
		LIGHTHERE(red, gb, gb);
		glPopMatrix();
	}
#undef LIGHTHERE
	}

	// Draw outer walls
	texture_apply(tex_outer_wall);
	draw_outer_walls();
	
	// Set material properties for blocks
/*	{ GLfloat mat_specular[] = { 0.05, 0.05, 0.05, 1.0 };
	  glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular); } */

	// Draw static grid items
	glPushMatrix();
	for(int i = 0; i < grid_width; ++i) {
		for(int j = 0; j < grid_height; ++j) {
			if ((properties[i][j].block)) {
				// Draw block
				switch(properties[i][j].block) {
					case bt_wall:
						texture_apply(tex_block_wall);
						//glMaterialf(GL_FRONT, GL_SHININESS, 64.0);
						break;
					case bt_magnet:
						if(properties[i][j].polarity == p_red) {
							texture_apply(tex_block_magnet_red);
						} else {
							texture_apply(tex_block_magnet_blue);
						}
						//glMaterialf(GL_FRONT, GL_SHININESS, 128.0);
						break;
					default:
						fprintf(stderr, "Invalid block type\n");
						abort();
				}
				draw_unit_cube();
			} else {
				// Draw floor
				switch(properties[i][j].floor) {
					case ft_clear:
						texture_apply(tex_flr_normal);
						break;
					case ft_radslime:
						texture_apply(tex_flr_radslime);
						break;
//					case ft_magtrack:
//						texture_apply(tex_flr_magtrack);
//						break;
					case ft_chute:
						texture_apply(tex_flr_chute);
						break;
					case ft_exit:
						texture_apply(tex_flr_exit);
						break;
					default:
						fprintf(stderr, "Invalid floor type\n");
						abort();
				}
				draw_floor();
			}
			glTranslatef(0.0f, 1.0f, 0.0f);
		}
		glTranslatef(1.0f, -grid_height, 0.0f);
	}
	glPopMatrix();

	// Draw non-robot objects
	for(int i = 0; i < object_count; i++) {
		glPushMatrix();
		glTranslatef(objects[i].x, objects[i].y, 0.0);
		switch(objects[i].type) {
			case ot_robot:
				fprintf(stderr, "Robotic object\n");
				abort();
			case ot_coolant:
//				texture_apply(tex_object_coolant);
				if(objects[i].polarity == p_red) {
					glColor4f(1.0, 0.1, 0.1, 1.0);
				} else {
					glColor4f(0.1, 0.1, 1.0, 1.0);
				}
				//draw_unit_cube();
				glPushMatrix();
				glTranslatef(0.5, 0.5, 0.5 - objects[i].sunk);
				draw_model(mdl_barrel);
				glPopMatrix();
				glColor4f(1.0, 1.0, 1.0, 1.0);
				break;
			default:
				fprintf(stderr, "Unknown object type\n");
				abort();
		}
		glPopMatrix();
	}
	
	if(gamecomplete) {
		// Draw the "avert nuclear disaster" button
		glPushMatrix();
		glTranslatef(11.5, 6.5, 0.0);
		draw_model(endfx < 3.0 ? mdl_buttonout : mdl_buttonin);
		glPopMatrix();
	}
	
	// Draw robot (unless crushed)
	if(gamestate != g_crushed) {
		// Don't need to push matrix -- last thing
		if(gamecomplete) {
			register GLfloat efx3 = endfx < 3.0 ? endfx : 3.0;
			glTranslatef(efx3 + 7.5, 6.5, 0.0);
			//glRotatef(endfx * 90, 0.0, 0.0, 1.0); // Debugging spin ;)
		} else {
			glTranslatef(robot.x + 0.5, robot.y + 0.5, 0.0);
			direction_turn(robot.direction);
			if(gamestate == g_victory) { // Exit lift
				register double rise = endfx / 2.0;
				if(rise > 2.0) { rise = 2.0; }
				glTranslatef(0.0, 0.0, rise);
				// Lift itself
				texture_apply(tex_flr_exit);
				glPushMatrix();
				glTranslatef(-0.5, -0.5, -1.0); draw_unit_cube();
				glTranslatef( 0.0,  0.0, -1.0); draw_unit_cube();
				glPopMatrix();
			} else if(gamestate == g_slimed) { // Melt
				register double efx = endfx > 1.0 ? 1.0 : endfx;
				register double spread = (efx * 0.2) + 1.0;
				glScalef(spread, spread, 1.0 - (efx * 0.9));
			}
		}
		draw_model(mdl_robot);
	} else {
		// Draw dust cloud
		if(endfx <= 4.0) {
			register GLdouble radius = (sqrt(endfx * 0.25) * 2.0) + 0.25;
			glTranslatef(robot.x + 0.5, robot.y + 0.5, 0.25);
			direction_turn(robot.direction);
			glTranslatef(0.5, 0.0, 0.0);
			glColor4f(1.0, 1.0, 1.0, 1.0 - (endfx / 4.0));
			texture_apply(tex_dust);
			/* Draw from inside out, or will fail depth test */
			gluSphere(quadric, radius * 0.5,   8,  8);
			gluSphere(quadric, radius * 0.75, 12, 12);
			gluSphere(quadric, radius,        16, 16);
			glColor4f(1.0, 1.0, 1.0, 1.0);
		}
	}

	// Set things back to normality
	if(gamestate == g_meltdown) { glDisable(GL_FOG); }
	texture_apply(0);
	glPopMatrix();

	while(lights_used > 0)
		glDisable(GL_LIGHT0 + lights_used--);
}

float camera_rotation = 0.0f;

void camera_pos()
{
	// Set up the modelview
	glMatrixMode(GL_MODELVIEW);
	// Reset the camera
	glLoadIdentity();
	if(gamecomplete) {
		register GLfloat g = -(grid_width/2.0);
		gluLookAt(        8.0+g, (endfx * 0.25) + 2.0+g, 2.0,
		          endfx + 6.5+g,                  6.0+g, 1.25,
		                  0.0,                    0.0,   1.0);
	} else { switch(camera_mode) {
		case cm_default:
			// Move the camera 15 units from the objects (prev. 20)
			glTranslatef(0.0f, 0.0f, -15.0f);
			// Add a little camera movement (was x=10)
			glRotatef(1.0, sin(camera_rotation), cos(camera_rotation), 0.0f);
			break;
		case cm_orbitcube: // Remember: fun an exciting reverse order
			// And look down (was 30 degrees)
			glRotatef((-10*sin(camera_rotation))-30, 1.0, 0.0, 0.0);
			// Move out a little to cause orbit
			glTranslatef(0.0, 2.0, 0.0);
			// Twist (note use of 180, not 360, for half-speed during conversion)
			glRotatef(camera_rotation * (180 / M_PI), 0.0, 0.0, 1.0);
			// Move to an orbit location
//			glTranslatef(sin(camera_rotation)/2.0, cos(camera_rotation)/2.0, 0.0);
			// Move to a little above the cube (was z=-5)
			glTranslatef(-(robot.x + 0.5), -(robot.y + 0.5), sin(camera_rotation)-3.5);
			// Move to the origin of the grid
			glTranslatef((float)grid_width/2.0f, (float)grid_height/2.0f, 0.0f);
			break;
		case cm_faraway:
			// Move to look from a corner, using gluLookAt (NOT inverted!)
			// (was z=1.5)
			gluLookAt(-(grid_width/2.0 + 1), -(grid_height/2.0 + 1), 10.0,
			          robot.x + 0.5 - (grid_width/2.0), robot.y + 0.5 - (grid_width/2.0), 0.0,
			          0.0, 0.0, 1.0);
			break;
	}}
}

void render_scene()
{
	// Clear the screen
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	// Enable the depth test
	glEnable(GL_DEPTH_TEST);

	// Set the projection transform
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(45, (float)screen_width/screen_height, 1.f, far_clip);

	// Set the camera position
	camera_pos();

	// Draw the scene
	draw_grid();
}

// Draw any text output and other screen oriented user interface
// If you want any kind of text or other information overlayed on top
// of the 3d view, put it here.
void render_interface()
{
	char buf[256];
	unsigned char heatpct;

	// Set the projection to a transform that allows us to use pixel
	// coordinates.
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glOrtho(0, screen_width, 0, screen_height, -800.0f, 800.0f);

	// Set up the modelview
	glMatrixMode(GL_MODELVIEW);
	// Reset the camera
	glLoadIdentity();

	// Disable the depth test, as its not useful when rendering text
	glDisable(GL_DEPTH_TEST);
	glDisable(GL_LIGHTING); // And lighting

	// Print the number of frames per second. This is essential performance
	// information when developing 3D graphics.

	// Use glTranslatef to go to the screen coordinates where we want the
	// text. The origin is the bottom left by default in OpenGL.
	glTranslatef(5.f, 5.f, 0);
	glColor4f(1.f, 1.f, 1.f, 0.5);
	sprintf(buf, "FPS: %4d", average_frames_per_second);
	gl_print(buf);
	glColor4f(1.f, 1.f, 1.f, 1.0);

	if(gamecomplete) {
		glPushMatrix();
		glTranslatef(
			(screen_width  / 2.0) - 96.0, // (640 / 2) - 96 = 224
			(screen_height / 2.0), // (480 / 2) - 16(fps) + 16 = 240
			0.0);
		sprintf(buf, "FINAL SCORE %6u", score); gl_print(buf);
		glTranslatef(-40.0, 16.0, 0.0);
		glScalef(1.5, 1.5, 0.0);
		gl_print("Meltdown averted!");
		glPopMatrix();
	} else {
		// Now show the reactor temperature (as a percentage)
		glTranslatef(0.0, 16.0, 0.0);
		sprintf(buf, "Heat:"); gl_print(buf);
		glPushMatrix();
		glTranslatef(64.0, 0.0, 0.0);
		heatpct = (reactorheat * 100) / reactorheat_max;
		sprintf(buf, "%03d%%", heatpct);
		if(heatpct > HEAT_PANIC_LEVEL) {
			glColor3f(1.0, 0.5, 0.25);
			gl_print(buf);
			glColor4f(1.0, 0.01, 0.01, 0.5);
			glTranslatef(-1.0, -1.0, 0.0); gl_print(buf);
			glTranslatef( 2.0,  0.0, 0.0); gl_print(buf);
			glTranslatef( 0.0,  2.0, 0.0); gl_print(buf);
		glTranslatef(-2.0,  0.0, 0.0); gl_print(buf);
		glColor4f(1.0, 1.0, 1.0, 1.0);
		} else {
			gl_print(buf);
		}
		glPopMatrix();

		// And the number of remaining magnets
		glTranslatef(0.0, 16.0, 0.0);
		gl_print("Mags:");
		glPushMatrix();
		glTranslatef(64.0, 0.0, 0.0); /* "Remaining:" == 104.0 */
		glColor3f(1.0, 0.2, 0.2); sprintf(buf, "%02d", magnets_left_red ); gl_print(buf);
		glTranslatef(32.0, 0.0, 0.0);
		glColor3f(0.2, 0.2, 1.0); sprintf(buf, "%02d", magnets_left_blue); gl_print(buf);
		glPopMatrix();
		glColor3f(1.0, 1.0, 1.0);

		// And the score
		glTranslatef(0.0, 16.0, 0.0);
		sprintf(buf, "Score%6u", score); gl_print(buf);

		// Show level information
		if(leveltime < SHOW_ADVICE_FOR) {
			glPushMatrix();
			glTranslatef(0.0, screen_height - 80.0, 0.0); // 480 - 80 = 400
			gl_print(leveladvice);
			glColor3f(0.1, 1.0, 0.1);
			glTranslatef(0.0,-16.0, 0.0);
			gl_print(levelauthor);
			glColor3f(1.0, 1.0, 1.0);
			glPopMatrix();
		}
	
		// Paused?
		if(paused) {
			glPushMatrix();
			glTranslatef(
				(screen_width / 2.0) - 48.0, // (640 / 2) - 48 = 272
				(screen_height * 0.5) - 56.0, // hud+font = 12-(16*4) = -52; was 256
				0.0);
			glScalef(1.5, 1.5, 0.0);
			gl_print("Paused");
			glPopMatrix();
		}

		// Do we have a gamestate to display?
		if(gamestate) {
			glTranslatef(0.0, 16.0, 0.0);
			sprintf(buf, "Press SPACE");
			gl_print(buf);
			glScalef(2.0, 2.0, 0.0);
			glTranslatef(0.0, 16.0, 0.0);
			switch(gamestate) {
				case g_play: break; // Keep GCC quiet but still warn if this doesn't cover all (other) cases
				case g_victory:  sprintf(buf, "Victory!"); break;
				case g_meltdown: sprintf(buf, "MELTDOWN!"); break;
				case g_crushed:  sprintf(buf, "Crushed!"); break;
				case g_slimed:   sprintf(buf, "Slimed!"); break;
			}
			gl_print(buf);
		}
	}
	
	glEnable(GL_LIGHTING); // Re-enable
}

// Handle a mouse click. Call this function with the screen coordinates where
// the mouse was clicked, in OpenGL format with the origin in the bottom left.
// This function exactly mimics rendering a scene, and then detects which
// grid location of the scene were under the mouse pointer when the click
// occured.
void mouse_click(unsigned int x, unsigned int y, bool rightmouse)
{
	GLuint selectBuf[512];
	GLfloat square_vertices[] = { 0.f, 0.f, 0.f, 1.f, 0.f, 0.f,
	                              1.f, 1.f, 0.f, 0.f, 1.f, 0.f };

	// Stop right here if we're not in the normal play gamestate
	if(gamestate) { return; }

	// Put OpenGL into Selection mode rather than Render mode. This disables
	// rendering to the window.
	glSelectBuffer(512,selectBuf);
	glRenderMode(GL_SELECT);

	
	// This is just like setting up the projection for normal rendering
	// except we use gluPickMatrix to specify the portion of the screen
	// we are interested in - the 1 pixel square under the mouse pointer.
	{
		// Set the projection transform
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
		GLint viewport[4];
		glGetIntegerv(GL_VIEWPORT,viewport);
		gluPickMatrix(x, y, 1, 1, viewport);
		gluPerspective(45, (float)screen_width/screen_height, 1.f, 100.f);

		// Set the camera position
		camera_pos();
	}

	// Each thing we render will have a numerical name that we must later use
	// to identify which thing we rendered was under the mouse pointer.
	// The names are pushed onto a stack, so a thing can have more than
	// one. We will just have one name per thing, so we start by pushing a
	// token name onto the stack.
	glInitNames();
	glPushName(0);

	// Here we will draw the grid exactly as it was drawn when rendering to
	// the screen, except we draw a quad on each square rather than just
	// drawing lines. If you want to detect clicking on other things, you
	// will need to modify the code here.
	glTranslatef(-(float)grid_width/2.0f, -(float)grid_height/2.0f, 0.0f);

	glVertexPointer(3, GL_FLOAT, 0, square_vertices);

	for(int i = 0; i < grid_width; ++i) {
		for(int j = 0; j < grid_height; ++j) {
			// Before we render each grid square, we specify the name by
			// loading it into the top of the name stack. The name
			// is derived from the coordinates of the square, so that later
			// when we get a name back, we can tell which square it corresponds
			// to.
			glLoadName(i + j * grid_width);
			glDrawArrays(GL_QUADS, 0, 4);
			glTranslatef(0.0f, 1.0f, 0.0f);
		}
		glTranslatef(1.0f, -grid_height, 0.0f);
	}

	// We have now finished with the name stack, so we empty it.
	glPopName();

	// Put OpenGL back into rendering mode. This has the side effect of
	// returning how many hits we got in the selection code above.
	int hits = glRenderMode(GL_RENDER);

	// std::cout << "Got " << hits << " hits" << std::endl << std::flush;

	// If we got no hits, the user clicked on empty space.
	if (hits == 0) {
		return;
	}

	// This code searches through the results buffer to find what hits we
	// got.

	// ptr points to the beginning of the results
	GLuint * ptr = &selectBuf[0];
	GLuint minDepth = UINT_MAX, noNames = 0;
	GLuint * namePtr = 0;
	for (int i = 0; i < hits; i++) {
		int names = *(ptr++);
		// std::cout << "{" << *ptr << "}";
		// Check if this hit is closer to the viewer than the last one we
		// processed. If this test passes, then this is the closest hit
		// so far, so we record the necessary details.
		if (*ptr < minDepth) {
			noNames = names;
			minDepth = *ptr;
			namePtr = ptr + 2;
		}
		ptr += (names + 2);
	}
	// Once this loop is complete, we have the details of the nearest hit
	// stored, which is the thing we rendered that was closest to the viewer
	// under the mouse pointer. This should be the thing the player was aiming
	// to click on when they clicked.

	// noNames = Number of names our hit has
	// namePtr = Pointer to list of names

	// Extract the X and Y coords of the clicked square from the hit name
	GLuint hitName = *namePtr;
	int hit_x = hitName % grid_width;
	int hit_y = hitName / grid_width;

	// Place a magnet on the square the user clicked, /if/...
	if (hit_x < grid_width && hit_y < grid_height // inside grid
		&& properties[hit_x][hit_y].block == bt_clear // block clear
		&& properties[hit_x][hit_y].floor == ft_clear // floor clear
		&& !(hit_x == robot.x && hit_y == robot.y) // not where robot is
		&& ((rightmouse ? magnets_left_blue : magnets_left_red) > 0) ) { // got some left
		// But is there an object there?
		bool objclear = true;
		for(int i = 0; i < object_count; i++) {
			if(objects[i].x == hit_x
			&& objects[i].y == hit_y)
				{ objclear = false; i = object_count; }
		}
		if(objclear) {
			properties[hit_x][hit_y].block = bt_magnet;
			properties[hit_x][hit_y].polarity = rightmouse ? p_blue : p_red;
			rightmouse ? magnets_left_blue-- : magnets_left_red--;
			playeventsound(&wav_place);
		}
	}
}

// Find dominant magnetic force in one direction (return strong == strong, else weak)
bool magnetic_influence_onedir(int x, int y, direction_t direction, polarity_t* polarity, int* proximity) {
	int i = x;
	int j = y;
	bool keeplooking = true;
	bool strong = false;

	// Start with no magnitism found (weak invalid_proximity)
	*proximity = invalid_proximity;

	// TODO Magtrack here? (Prox 0 strong => unbeatable)

	while(keeplooking) { // Fake for-ish loop (a little more do/while, really, but meh)
		int potprox = 0; // Potential proximity (pointless init for GCC's stupid sake)
		switch(direction) {
			case d_right: potprox = i - x; break;
			case d_up   : potprox = y - j; break;
			case d_left : potprox = x - i; break;
			case d_down : potprox = j - y; break;
		}

		// What is the block type here?
		switch(properties[i][j].block) {
			case bt_clear: break; /* Carry on... */
			case bt_wall: /* Stop looking in this direction */
				keeplooking = false;
				break;
			case bt_magnet: /* Ooh, influence! */
				keeplooking = false; /* Strongest possible */
				strong = true;
				*proximity = potprox;
				*polarity  = properties[i][j].polarity;
				break;
		}

		// Is there an object here?
		if( keeplooking
		&& (potprox != 0)    // Do not include ourself!
		&& (potprox <= 1)) { // Limit object magnetism range to 1
			for(int obji = 0; obji < object_count; obji++) {
				if(objects[obji].x == i && objects[obji].y == j
				&& objects[obji].polarity) {
					/* Keep looking to allow for box */
					*proximity = potprox;
					*polarity  = objects[obji].polarity;
				}
			}
		}
		
		// Neeexxxt!
		switch(direction) {
			case d_right: i++; keeplooking &= (i < grid_width ); break;
			case d_up   : j--; keeplooking &= (j >= 0         ); break;
			case d_left : i--; keeplooking &= (i >= 0         ); break;
			case d_down : j++; keeplooking &= (j < grid_height); break;
		}
	}

	return strong;
}

// True == horizontal dominant; false == vertical dominant
// horiz and vert will be (-1, 0, 1)
bool magnetic_influence(int x, int y, polarity_t upon, int* horiz, int* vert) {
	bool strongr, strongu, strongl, strongd; // Strong
	polarity_t polr, polu, poll, pold; // Polarity
	int proxr, proxu, proxl, proxd; // Proximity
	bool dominance; // Return value

	strongr = magnetic_influence_onedir(x, y, d_right, &polr, &proxr);
	strongu = magnetic_influence_onedir(x, y, d_up   , &polu, &proxu);
	strongl = magnetic_influence_onedir(x, y, d_left , &poll, &proxl);
	strongd = magnetic_influence_onedir(x, y, d_down , &pold, &proxd);
//	fprintf(stderr, "Pull r%i u%i l%i d%i\n", polr, polu, poll, pold);

	// Boil down the right/left effects to a sign and strength in right
	if((strongr && strongl) || (!strongr && !strongl)) { // Just proximity
		if((proxl == invalid_proximity)
		&& (proxr == invalid_proximity)) {
			// No magnetism
			proxr  = invalid_proximity;
			polr   = p_none;
			*horiz = 0;
		} else {
			proxr  = (proxl < proxr) ? proxl : proxr;
			polr   = (proxl < proxr) ? poll  : polr ;
			*horiz = (proxl < proxr) ?    -1 :     1;
		}
	} else { // One is strong, one isn't
			proxr  = strongl         ? proxl : proxr;
			polr   = strongl         ? poll  : polr ;
			*horiz = strongl         ?    -1 :     1;
	}
	strongr = (strongr || strongl); // Strong in this axis?
	// And now vertical
	if((strongu && strongd) || (!strongu && !strongd)) {
		if((proxd == invalid_proximity)
		&& (proxu == invalid_proximity)) {
			proxu  = invalid_proximity;
			polu   = p_none;
			*vert  = 0;
		} else {
			proxu  = (proxd < proxu) ? proxd : proxu;
			polu   = (proxd < proxu) ? pold  : polu ;
			*vert  = (proxd < proxu) ?     1 :    -1;
		}
	} else {
			proxu  = strongd         ? proxd : proxu;
			polu   = strongd         ? pold  : polu ;
			*vert  = strongd         ?     1 :    -1;
	}
	strongu = (strongu || strongd);
	// Now determine which axis is dominant
	if((strongr && strongu) || (!strongr && !strongu)) {
		if(proxr == proxu) { // It's a tie!
			// Repulsion trumps attraction; horizontal trumps vertical
			// upon (obj.) | polr | polu | dominance (horiz true)
			// ------------+------+------+-----------------------
			//       R     | - R  | - R  | horizontal (hz dom)
			//       R     | - R  | + B  | horizontal (rp dom)
			//       R     | + B  | - R  | vertical   (rp dom)
			//       R     | + B  | + B  | horizontal (hz dom)
			//       B     | + R  | + R  | horizontal (hz dom)
			//       B     | + R  | - B  | vertical   (rp dom)
			//       B     | - B  | + R  | horizontal (rp dom)
			//       B     | - B  | - B  | horizontal (hz dom)
			// Vertical if upon != polr && upon == polu. Negate.
			// Horizontal if hz repels, or vert attracts.
			dominance = (upon == polr || upon != polu);
		} else {
			dominance = (proxr < proxu);
		}
	} else {
		dominance = strongr;
	}
	// And blat the pull in the other one
	if(dominance) { *vert = 0; } else { *horiz = 0; }
	// Toggle this if the polarities are the same (repulsion)
	if(upon == (dominance ? polr : polu)) {
		*horiz *= -1;
		*vert  *= -1;
	}
	// Done!
//	fprintf(stderr, "Influence is %i %i\n", *horiz, *vert);
	return dominance;
}

// Returns TRUE if the object should be removed (remove_object...)
bool affect_by_floor(ObjectProperties* object) {
	switch(properties[object->x][object->y].floor) {
		case ft_radslime: // 'Splode!
			if(object == &robot) { changegamestate(g_slimed); }
				else { object->sinking = true; /* return true; */ }
			break;
		case ft_chute:
			if(object->type == ot_coolant) {
				reactorheat = (reactorheat < barrel_cooling) ? 0 : (reactorheat - barrel_cooling);
				playeventsound(&wav_chute);
				object->sinking = true; /* return true; */
				score += score_forbarrel;
			}
			break;
		case ft_exit:
			if(object == &robot) {
				changegamestate(g_victory);
				// Add score here, rather than in changegamestate,
				// so that cheaters don't prosper. ;)
				// (Plus, it keeps it together with the barrel score.)
				score += score_forexit;
				score += score_forheatleft *
					(100 - ((reactorheat * 100) / reactorheat_max));
			}
			break;
		default: // inc. ft_clear, ft_magtrack (caught by magnetism checks)
			break; // Shut up, GCC
	}
	return false;
}

// Returns FALSE if blocked; either dx or dy should be zero (no diagonals!)
// *WILL* move if it can (needed for effective recursion...)
bool try_to_move(ObjectProperties* object, int dx, int dy) {
	if(!((dx == 0 || dy == 0) && ((dx*dx) + (dy*dy) == 1))) {
		fprintf(stderr, "Invalid try_to_move offsets (%i, %i)\n", dx, dy);
		abort();
	}
	int obji;
	bool canmove = true;

	// Blocked by non-clear cell?
	if(properties[object->x + dx][object->y + dy].block != bt_clear) { return false; }

	// Blocked by grid edge?
	if((object->x + dx <  0)
	|| (object->x + dx >= grid_width)
	|| (object->y + dy <  0)
	|| (object->y + dy >= grid_height)) {
		return false;
	}

	// Push another object?
	for(obji = 0; obji < object_count; obji++) {
		if((object->x + dx == objects[obji].x)
		&& (object->y + dy == objects[obji].y)) {
			// This one's in the way
			if(object == &robot) {
				// Robot cannot push
				canmove = false;
			} else if(!try_to_move(&objects[obji], dx, dy)) {
				// It didn't get out of the way
				canmove = false;
			}
		}
	}

	// Push the robot?
	if((object->x + dx == robot.x)
	&& (object->y + dy == robot.y)) {
		if(!try_to_move(&robot, dx, dy)) {
			// Robot crushed!
			changegamestate(g_crushed);
			// Save direction it was crushed in
			robot.direction = dx ?
				(dx > 0 ? d_right : d_left) :
				(dy > 0 ? d_up    : d_down);
			// Turn off its light
			glDisable(GL_LIGHT0);
		}
	}
	
	// Actually move
	if(canmove) {
		object->x += dx;
		object->y += dy;
	}
	
	// Aaand be affected by the floor
	object->kill &= affect_by_floor(object);
	
	return canmove;
}

void remove_object(int objectid) {
	assert((objectid < object_count) && (objectid >= 0));
	object_count--;
	if(object_count) { objects[objectid] = objects[object_count]; }
}

// This function is called every step_time milliseconds. In many games you
// will want to use this function to update the game state. For example in
// a Tetris game, this would be the place where the falling blocks were
// moved down the grid.
void step(void)
{
	// Pause doesn't even time out the level advice; likewise gamecomplete does nowt
	if(paused || gamecomplete) { return; }
	
	if(leveltime < LEVEL_TIME_STOPCARING) { leveltime++; }
	
	// Do nothing if demonstrating loss/victory (the behaviour is handled elsewhere)
	if(gamestate) { return; }

	// Warm up the reactor
	reactorheat++;
	if(reactorheat > reactorheat_max) { changegamestate(g_meltdown); }

	// Move the robot
	if(robotmoving || robotmustmove) {
		switch(robotmovingdir) {
			case d_right: try_to_move(&robot,  1,  0); break;
			case d_up   : try_to_move(&robot,  0,  1); break;
			case d_left : try_to_move(&robot, -1,  0); break;
			case d_down : try_to_move(&robot,  0, -1); break;
		}
		robot.direction = robotmovingdir;
		robotmustmove = false;
	}
	
	// Move magnetic objects
	for(int obji = 0; obji < object_count; obji++) {
		if(objects[obji].polarity) {
			int dx, dy;
			magnetic_influence(objects[obji].x, objects[obji].y, objects[obji].polarity, &dx, &dy);
			if(dx || dy)
				{ try_to_move(&objects[obji], dx, dy); }
		}

		if(objects[obji].kill) {
			remove_object(obji);
			obji--; // Must do this to catch all objects after removal reodering
		}
	}
}

// The main program loop function. This does not return until the program
// has finished.
void loop()
{
	SDL_Event event;
	int elapsed_time = SDL_GetTicks();
	int last_step = elapsed_time;
	int frame_count = 0;

	// This is the main program loop. It will run until something sets
	// the flag to indicate we are done.
	while (!program_finished) {
		// Check for events
		while (SDL_PollEvent(&event)) {
			switch (event.type) {
				case SDL_QUIT:
					// The user closed the window
					program_finished = true;
					break;
				case SDL_KEYDOWN:
					// We have a keypress
					if ( event.key.keysym.sym == SDLK_ESCAPE ) { // quit
						program_finished = true;
					} else if ( event.key.keysym.sym == SDLK_PAUSE ) {
						paused = !paused;
					} else if ( event.key.keysym.sym == SDLK_r ) {
						reactorheat = reactorheat_max;
					} else if ( event.key.keysym.sym == SDLK_F12 ) {
						changegamestate(g_victory);
					} else if ( event.key.keysym.sym == SDLK_SPACE ) {
						if(gamecomplete) { // Should this do something?
							setup();
						} else if(gamestate) {
							if(gamestate == g_victory) {
								if(strncmp(nextlevel, "END", 4) == 0) {
									gamecomplete = true;
									load_level(nextlevel);
									gamestate = g_victory; // Put this back without replaying sound or meddling with camera
								} else {
									// Next level
									load_level(nextlevel);
								}
							} else {
								// Restart level
								//setup();
								load_level(thislevel);
								reactorheat = 0;
								score = 0;
							}
							paused = true;
						}
					}
#define MOVEBOT(D) \
	paused = false; \
	robotmoving = robotmustmove = true; \
	robotmovingdir = (D);
					else if ( event.key.keysym.sym == SDLK_UP ) {
						MOVEBOT(d_up)
					} else if ( event.key.keysym.sym == SDLK_DOWN ) {
						MOVEBOT(d_down)
					} else if ( event.key.keysym.sym == SDLK_LEFT ) {
						MOVEBOT(d_left)
					} else if ( event.key.keysym.sym == SDLK_RIGHT ) {
						MOVEBOT(d_right)
					}
#undef MOVEBOT
					else if ( event.key.keysym.sym == SDLK_RETURN ) {
						if(camera_mode == cm_faraway) {
							camera_mode = cm_default;
						} else {
							camera_mode++;
						}
					}
					break;
				case SDL_KEYUP:
					       if ( event.key.keysym.sym == SDLK_UP    ) {
						if(robotmovingdir == d_up)
							{ robotmoving = false; }
					} else if ( event.key.keysym.sym == SDLK_DOWN  ) {
						if(robotmovingdir == d_down)
							{ robotmoving = false; }
					} else if ( event.key.keysym.sym == SDLK_LEFT  ) {
						if(robotmovingdir == d_left)
							{ robotmoving = false; }
					} else if ( event.key.keysym.sym == SDLK_RIGHT ) {
						if(robotmovingdir == d_right)
							{ robotmoving = false; }
					}
					break;
				case SDL_MOUSEBUTTONDOWN:
					paused = false;
					mouse_click(event.button.x,
						screen_height - event.button.y,
						event.button.button != SDL_BUTTON_LEFT);
					break;
				default:
					break;
			}
		}

		++frame_count;

		// Get the time and check if a complete time step has passed.
		// For step based games like Tetris, this is used to update the
		// the game state
		const int ticks = SDL_GetTicks();
		if ((ticks - last_step) > step_time) {
			last_step = ticks;
			average_frames_per_second = frame_count * (1000.0 / step_time);
			frame_count = 0;
			step();
		}

		// Calculate the time in seconds since the last frame
		// For a real time program this would be used to update the game state
		float delta = (ticks - elapsed_time) / 1000.0f;
		elapsed_time = ticks;

		// Update the rotation on the camera
		camera_rotation += delta;
		if (camera_rotation > (4 * M_PI)) { // Two full circles, as orbiting mode is halfspeed
			camera_rotation -= (4 * M_PI);
		}

		// Update endfx
		if(gamestate && (endfx < endfx_max)) {
			endfx += delta;
			if(endfx > endfx_max) { endfx = endfx_max; }
		}

		// Sink objects
		for(int i = 0; i < object_count; i++) {
			if(objects[i].sinking) {
				if((objects[i].sunk += delta * 2.0) > 1.0) {
					//objects[i].kill = true; // No; do so immediately
					remove_object(i--); // Note POSTdecrement
				}
			}
		}

		// Render the screen
		render_scene();
		render_interface();

		SDL_GL_SwapBuffers();
	}
}

int main()
{
	// Initialise the graphics
	if (!init_graphics()) {
		return 1;
	}

	// Initialise the audio
	init_audio();

	// Initialise the game state
	setup();

	// Run the game
	loop();

	// Clean up and exit
	cleanup();
	return 0;
}

#ifdef WIN32

int WinMain(
	HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPSTR lpCmdLine,
	int nCmdShow
)
{
	main();
}

#endif
