about summary refs log tree commit diff
path: root/src/game/client/gc_client.cpp
diff options
context:
space:
mode:
authorMagnus Auvinen <magnus.auvinen@gmail.com>2007-12-15 10:24:49 +0000
committerMagnus Auvinen <magnus.auvinen@gmail.com>2007-12-15 10:24:49 +0000
commita2566b3ebd93e0bbc55a920a7be08054a9377f11 (patch)
tree44a4612805d894168fe4b3b4c065fccc1a1686e9 /src/game/client/gc_client.cpp
parentac9873056aa1fe529b098f19ff31e9ffa0e016a2 (diff)
downloadzcatch-a2566b3ebd93e0bbc55a920a7be08054a9377f11.tar.gz
zcatch-a2566b3ebd93e0bbc55a920a7be08054a9377f11.zip
cleaned up code structure a bit
Diffstat (limited to 'src/game/client/gc_client.cpp')
-rw-r--r--src/game/client/gc_client.cpp3103
1 files changed, 3103 insertions, 0 deletions
diff --git a/src/game/client/gc_client.cpp b/src/game/client/gc_client.cpp
new file mode 100644
index 00000000..98275188
--- /dev/null
+++ b/src/game/client/gc_client.cpp
@@ -0,0 +1,3103 @@
+/* copyright (c) 2007 magnus auvinen, see licence.txt for more info */
+#include <game/g_math.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+extern "C" {
+	#include <engine/client/ec_ui.h>
+	#include <engine/e_config.h>
+};
+
+#include "../g_game.h"
+#include "../g_version.h"
+#include "../g_mapres.h"
+#include "gc_mapres_image.h"
+#include "gc_mapres_tilemap.h"
+#include "../generated/gc_data.h"
+#include "gc_menu.h"
+#include "gc_skin.h"
+#include "gc_render.h"
+
+// sound channels
+enum
+{
+	CHN_GUI=0,
+	CHN_MUSIC,
+	CHN_WORLD,
+	CHN_GLOBAL,
+};
+
+data_container *data = 0x0;
+
+int gametype = GAMETYPE_DM;
+
+extern void modmenu_render();
+
+enum
+{
+	CHATMODE_NONE=0,
+	CHATMODE_ALL,
+	CHATMODE_TEAM,
+	CHATMODE_CONSOLE,
+	CHATMODE_REMOTECONSOLE,
+};
+
+typedef struct 
+{
+    float x, y, w, h;
+} RECT;
+RECT *ui2_screen();
+
+static int chat_mode = CHATMODE_NONE;
+bool menu_active = false;
+bool menu_game_active = false;
+static bool emoticon_selector_active = false;
+
+static vec2 mouse_pos;
+static vec2 local_character_pos;
+static vec2 local_target_pos;
+static const obj_player_character *local_character = 0;
+static const obj_player_character *local_prev_character = 0;
+const obj_player_info *local_info = 0;
+static const obj_flag *flags[2] = {0,0};
+static const obj_game *gameobj = 0;
+
+static int picked_up_weapon = 0;
+
+static struct client_data
+{
+	char name[64];
+	char skin_name[64];
+	int skin_id;
+	int skin_color;
+	int team;
+	int emoticon;
+	int emoticon_start;
+	player_core predicted;
+	
+	tee_render_info skin_info;
+	
+} client_datas[MAX_CLIENTS];
+
+class client_effects
+{
+public:
+	float zoom;
+	float currentzoom;
+	float stage;
+	int lastzoomin;
+	int lastincrease;
+
+	client_effects()
+	{
+		currentzoom = zoom = 3.0f;
+		stage = 0.0f;
+	}
+
+	float getorgzoom() { return zoom; }
+
+	float getzoom(int tick, float intratick, const obj_player_character *player)
+	{
+		float currentstage = ((float)player->weaponstage) * 0.1f;
+		if (currentstage < stage)
+		{
+			if ((tick - lastincrease) > (client_tickspeed() / 2))
+				stage = currentstage;
+		}
+		else
+		{
+			lastincrease = tick;
+			stage = currentstage;
+		}
+
+		float targetzoom = 3.0f + stage;
+		currentzoom = LERP(currentzoom, targetzoom, 0.1);
+		return currentzoom;
+	}
+};
+
+client_effects cl_effects;
+
+inline float frandom() { return rand()/(float)(RAND_MAX); }
+
+void snd_play_random(int chn, int setid, float vol, vec2 pos)
+{
+	soundset *set = &data->sounds[setid];
+
+	if(!set->num_sounds)
+		return;
+
+	if(set->num_sounds == 1)
+	{
+		snd_play_at(chn, set->sounds[0].id, 0, pos.x, pos.y);
+		return;
+	}
+
+	// play a random one
+	int id;
+	do {
+		id = rand() % set->num_sounds;
+	} while(id == set->last);
+	snd_play_at(chn, set->sounds[id].id, 0, pos.x, pos.y);
+	set->last = id;
+}
+
+// sound volume tweak
+static const float stereo_separation = 0.001f;
+static const float stereo_separation_deadzone = 200.0f;
+static const float volume_distance_falloff = 200.0f;
+static const float volume_distance_deadzone = 320.0f;
+static const float volume_gun = 0.5f;
+static const float volume_tee = 0.5f;
+static const float volume_hit = 0.5f;
+static const float volume_music = 0.8f;
+
+void sound_vol_pan(const vec2& p, float *vol, float *pan)
+{
+	vec2 player_to_ev = p - local_character_pos;
+	*pan = 0.0f;
+	*vol = 1.0f;
+
+	if(fabs(player_to_ev.x) > stereo_separation_deadzone)
+	{
+		*pan = stereo_separation * (player_to_ev.x - sign(player_to_ev.x)*stereo_separation_deadzone);
+		if(*pan < -1.0f) *pan = -1.0f;
+		if(*pan > 1.0f) *pan = 1.0f;
+	}
+
+	float len = length(player_to_ev);
+	if(len > volume_distance_deadzone)
+	{
+		*vol = volume_distance_falloff / (len - volume_distance_deadzone);
+
+		if(*vol < 0.0f) *vol = 0.0f;
+		if(*vol > 1.0f) *vol = 1.0f;
+	}
+}
+
+enum
+{
+	SPRITE_FLAG_FLIP_Y=1,
+	SPRITE_FLAG_FLIP_X=2,
+};
+
+static float sprite_w_scale;
+static float sprite_h_scale;
+
+static void select_sprite(sprite *spr, int flags=0, int sx=0, int sy=0)
+{
+	int x = spr->x+sx;
+	int y = spr->y+sy;
+	int w = spr->w;
+	int h = spr->h;
+	int cx = spr->set->gridx;
+	int cy = spr->set->gridy;
+
+	float f = sqrtf(h*h + w*w);
+	sprite_w_scale = w/f;
+	sprite_h_scale = h/f;
+	
+	float x1 = x/(float)cx;
+	float x2 = (x+w)/(float)cx;
+	float y1 = y/(float)cy;
+	float y2 = (y+h)/(float)cy;
+	float temp = 0;
+
+	if(flags&SPRITE_FLAG_FLIP_Y)
+	{
+		temp = y1;
+		y1 = y2;
+		y2 = temp;
+	}
+
+	if(flags&SPRITE_FLAG_FLIP_X)
+	{
+		temp = x1;
+		x1 = x2;
+		x2 = temp;
+	}
+	
+	gfx_quads_setsubset(x1, y1, x2, y2);
+}
+
+void select_sprite(int id, int flags=0, int sx=0, int sy=0)
+{
+	if(id < 0 || id > data->num_sprites)
+		return;
+	select_sprite(&data->sprites[id], flags, sx, sy);
+}
+
+void draw_sprite(float x, float y, float size)
+{
+	gfx_quads_draw(x, y, size*sprite_w_scale, size*sprite_h_scale);
+}
+
+class damage_indicators
+{
+public:
+	int64 lastupdate;
+	struct item
+	{
+		vec2 pos;
+		vec2 dir;
+		float life;
+		float startangle;
+	};
+
+	enum
+	{
+		MAX_ITEMS=64,
+	};
+
+	damage_indicators()
+	{
+		lastupdate = 0;
+		num_items = 0;
+	}
+
+	item items[MAX_ITEMS];
+	int num_items;
+
+	item *create_i()
+	{
+		if (num_items < MAX_ITEMS)
+		{
+			item *p = &items[num_items];
+			num_items++;
+			return p;
+		}
+		return 0;
+	}
+
+	void destroy_i(item *i)
+	{
+		num_items--;
+		*i = items[num_items];
+	}
+
+	void create(vec2 pos, vec2 dir)
+	{
+		item *i = create_i();
+		if (i)
+		{
+			i->pos = pos;
+			i->life = 0.75f;
+			i->dir = dir*-1;
+			i->startangle = (( (float)rand()/(float)RAND_MAX) - 1.0f) * 2.0f * pi;
+		}
+	}
+
+	void render()
+	{
+		gfx_texture_set(data->images[IMAGE_GAME].id);
+		gfx_quads_begin();
+		for(int i = 0; i < num_items;)
+		{
+			vec2 pos = mix(items[i].pos+items[i].dir*75.0f, items[i].pos, clamp((items[i].life-0.60f)/0.15f, 0.0f, 1.0f));
+
+			items[i].life -= client_frametime();
+			if(items[i].life < 0.0f)
+				destroy_i(&items[i]);
+			else
+			{
+				gfx_setcolor(1.0f,1.0f,1.0f, items[i].life/0.1f);
+				gfx_quads_setrotation(items[i].startangle + items[i].life * 2.0f);
+				select_sprite(SPRITE_STAR1);
+				draw_sprite(pos.x, pos.y, 48.0f);
+				i++;
+			}
+		}
+		gfx_quads_end();
+	}
+
+};
+
+static damage_indicators damageind;
+
+class particle_system
+{
+public:
+	struct particle
+	{
+		vec2 pos;
+		vec2 vel;
+		float life;
+		float max_life;
+		float size;
+
+		float rot;
+		float rotspeed;
+
+		float gravity;
+		float friction;
+		int iparticle;
+
+		vec4 color;
+	};
+
+	enum
+	{
+		MAX_PARTICLES=1024,
+	};
+
+	particle particles[MAX_PARTICLES];
+	int num_particles;
+
+	particle_system()
+	{
+		num_particles = 0;
+	}
+
+	void new_particle(vec2 pos, vec2 vel, float life, float size, float gravity, float friction, vec4 color=vec4(1,1,1,1))
+	{
+		if (num_particles >= MAX_PARTICLES)
+			return;
+
+		particles[num_particles].iparticle = rand() % data->num_particles;
+		particles[num_particles].pos = pos;
+		particles[num_particles].vel = vel;
+		particles[num_particles].life = life - (data->particles[particles[num_particles].iparticle].lifemod/100.0f) * life;
+		particles[num_particles].size = size;
+		particles[num_particles].max_life = life;
+		particles[num_particles].gravity = gravity;
+		particles[num_particles].friction = friction;
+		particles[num_particles].rot = frandom()*pi*2;
+		particles[num_particles].rotspeed = frandom() * 10.0f;
+		particles[num_particles].color = color;
+		num_particles++;
+	}
+
+	void update(float time_passed)
+	{
+		for(int i = 0; i < num_particles; i++)
+		{
+			particles[i].vel.y += particles[i].gravity*time_passed;
+			particles[i].vel *= particles[i].friction;
+			vec2 vel = particles[i].vel*time_passed;
+			move_point(&particles[i].pos, &vel, 0.1f+0.9f*frandom(), NULL);
+			particles[i].vel = vel* (1.0f/time_passed);
+			particles[i].life += time_passed;
+			particles[i].rot += time_passed * particles[i].rotspeed;
+
+			// check particle death
+			if(particles[i].life > particles[i].max_life)
+			{
+				num_particles--;
+				particles[i] = particles[num_particles];
+				i--;
+			}
+		}
+	}
+
+	void render()
+	{
+		gfx_blend_additive();
+		gfx_texture_set(data->images[IMAGE_GAME].id);
+		gfx_quads_begin();
+
+		for(int i = 0; i < num_particles; i++)
+		{
+			int type = particles[i].iparticle;
+			select_sprite(data->particles[type].spr);
+			float a = 1 - particles[i].life / particles[i].max_life;
+			vec2 p = particles[i].pos;
+
+			gfx_quads_setrotation(particles[i].rot);
+
+			gfx_setcolor(
+				data->particles[type].color_r * particles[i].color.r,
+				data->particles[type].color_g * particles[i].color.g,
+				data->particles[type].color_b * particles[i].color.b,
+				pow(a, 0.75f) * particles[i].color.a);
+
+			gfx_quads_draw(p.x, p.y,particles[i].size,particles[i].size);
+		}
+		gfx_quads_end();
+		gfx_blend_normal();
+	}
+};
+
+static particle_system temp_system;
+
+class projectile_particles
+{
+public:
+	enum
+	{
+		LISTSIZE = 1000,
+	};
+	// meh, just use size %
+	int lastadd[LISTSIZE];
+	projectile_particles()
+	{
+		reset();
+	}
+
+	void reset()
+	{
+		for (int i = 0; i < LISTSIZE; i++)
+			lastadd[i] = -1000;
+	}
+
+	void addparticle(int projectiletype, int projectileid, vec2 pos, vec2 vel)
+	{
+		int particlespersecond = data->projectileinfo[projectiletype].particlespersecond;
+		int lastaddtick = lastadd[projectileid % LISTSIZE];
+
+		if(!particlespersecond)
+			return;
+
+		if ((client_tick() - lastaddtick) > (client_tickspeed() / particlespersecond))
+		{
+			lastadd[projectileid % LISTSIZE] = client_tick();
+			float life = data->projectileinfo[projectiletype].particlelife;
+			float size = data->projectileinfo[projectiletype].particlesize;
+			vec2 v = vel * 0.2f + normalize(vec2(frandom()-0.5f, -frandom()))*(32.0f+frandom()*32.0f);
+
+			// add the particle (from projectiletype later on, but meh...)
+			temp_system.new_particle(pos, v, life, size, 0, 0.95f);
+		}
+	}
+};
+static projectile_particles proj_particles;
+
+static char chat_input[512];
+static unsigned chat_input_len;
+static const int chat_max_lines = 10;
+
+struct chatline
+{
+	int tick;
+	int client_id;
+	int team;
+	char text[512+64];
+};
+
+chatline chat_lines[chat_max_lines];
+static int chat_current_line = 0;
+
+void chat_reset()
+{
+	for(int i = 0; i < chat_max_lines; i++)
+		chat_lines[i].tick = -1000000;
+	chat_current_line = 0;
+}
+
+void chat_add_line(int client_id, int team, const char *line)
+{
+	chat_current_line = (chat_current_line+1)%chat_max_lines;
+	chat_lines[chat_current_line].tick = client_tick();
+	chat_lines[chat_current_line].client_id = client_id;
+	chat_lines[chat_current_line].team = team;
+
+	if(client_id == -1) // server message
+		sprintf(chat_lines[chat_current_line].text, "*** %s", line);
+	else
+	{
+		sprintf(chat_lines[chat_current_line].text, "%s: %s", client_datas[client_id].name, line);
+	}
+}
+
+struct killmsg
+{
+	int weapon;
+	int victim;
+	int killer;
+	int mode_special; // for CTF, if the guy is carrying a flag for example
+	int tick;
+};
+
+static const int killmsg_max = 5;
+killmsg killmsgs[killmsg_max];
+static int killmsg_current = 0;
+
+extern unsigned char internal_data[];
+
+void create_air_jump_effect(vec2 pos)
+{
+	const int count = 12;
+	for(int i = 0; i <= count; i++)
+	{
+		float a = i/(float)count;
+		vec2 v = vec2((a-0.5f)*512.0f, 0);
+		temp_system.new_particle(pos+vec2(0,28), v, 0.4f, 16.0f, 0, 0.985f, vec4(0.25f,0.4f,1,1));
+	}
+}
+
+
+extern void draw_round_rect(float x, float y, float w, float h, float r);
+extern int render_popup(const char *caption, const char *text, const char *button_text);
+
+static void render_loading(float percent)
+{
+	gfx_clear(0.65f,0.78f,0.9f);
+    RECT screen = *ui2_screen();
+	gfx_mapscreen(screen.x, screen.y, screen.w, screen.h);
+
+	float tw;
+
+	float w = 700;
+	float h = 200;
+	float x = screen.w/2-w/2;
+	float y = screen.h/2-h/2;
+
+	gfx_blend_normal();
+
+	gfx_texture_set(-1);
+	gfx_quads_begin();
+	gfx_setcolor(0,0,0,0.50f);
+	draw_round_rect(x, y, w, h, 40.0f);
+	gfx_quads_end();
+
+	const char *caption = "Loading";
+
+	tw = gfx_pretty_text_width(48.0f, caption, -1);
+	ui_do_label(x+w/2-tw/2, y+20, caption, 48.0f);
+
+	gfx_texture_set(-1);
+	gfx_quads_begin();
+	gfx_setcolor(1,1,1,1.0f);
+	draw_round_rect(x+40, y+h-75, (w-80)*percent, 25, 5.0f);
+	gfx_quads_end();
+
+	gfx_swap();
+}
+
+extern "C" void modc_init()
+{
+	// setup sound channels
+	snd_set_channel(CHN_GUI, 1.0f, 0.0f);
+	snd_set_channel(CHN_MUSIC, 1.0f, 0.0f);
+	snd_set_channel(CHN_WORLD, 0.9f, 1.0f);
+	snd_set_channel(CHN_GLOBAL, 1.0f, 0.0f);
+
+	// load the data container
+	data = load_data_from_memory(internal_data);
+
+	// TODO: should be removed
+	snd_set_listener_pos(0.0f, 0.0f);
+
+	float total = data->num_sounds+data->num_images;
+	float current = 0;
+
+	// load sounds
+	for(int s = 0; s < data->num_sounds; s++)
+	{
+		render_loading(current/total);
+		for(int i = 0; i < data->sounds[s].num_sounds; i++)
+		{
+			int id;
+			//if (strcmp(data->sounds[s].sounds[i].filename + strlen(data->sounds[s].sounds[i].filename) - 3, ".wv") == 0)
+			id = snd_load_wv(data->sounds[s].sounds[i].filename);
+			//else
+			//	id = snd_load_wav(data->sounds[s].sounds[i].filename);
+
+			data->sounds[s].sounds[i].id = id;
+		}
+
+		current++;
+	}
+
+	// load textures
+	for(int i = 0; i < data->num_images; i++)
+	{
+		render_loading(current/total);
+		data->images[i].id = gfx_load_texture(data->images[i].filename);
+		current++;
+	}
+	
+	skin_init();
+}
+
+extern "C" void modc_entergame()
+{
+}
+
+extern "C" void modc_shutdown()
+{
+	// shutdown the menu
+}
+
+static void process_events(int s)
+{
+	int num = snap_num_items(s);
+	for(int index = 0; index < num; index++)
+	{
+		SNAP_ITEM item;
+		const void *data = snap_get_item(s, index, &item);
+
+		if(item.type == EVENT_DAMAGEINDICATION)
+		{
+			ev_damageind *ev = (ev_damageind *)data;
+			damageind.create(vec2(ev->x, ev->y), get_direction(ev->angle));
+		}
+		else if(item.type == EVENT_AIR_JUMP)
+		{
+			ev_common *ev = (ev_common *)data;
+			create_air_jump_effect(vec2(ev->x, ev->y));
+		}
+		else if(item.type == EVENT_EXPLOSION)
+		{
+			ev_explosion *ev = (ev_explosion *)data;
+			vec2 p(ev->x, ev->y);
+
+			// center explosion
+			temp_system.new_particle(p, vec2(0,0), 0.3f, 96.0f, 0, 0.95f);
+			temp_system.new_particle(p, vec2(0,0), 0.3f, 64.0f, 0, 0.95f);
+			temp_system.new_particle(p, vec2(0,0), 0.3f, 32.0f, 0, 0.95f);
+			temp_system.new_particle(p, vec2(0,0), 0.3f, 16.0f, 0, 0.95f);
+
+			for(int i = 0; i < 16; i++)
+			{
+				vec2 v = normalize(vec2(frandom()-0.5f, frandom()-0.5f))*(128.0f+frandom()*128.0f);
+				temp_system.new_particle(p, v, 0.2f+0.25f*frandom(), 16.0f, 0, 0.985f);
+			}
+
+			for(int i = 0; i < 16; i++)
+			{
+				vec2 v = normalize(vec2(frandom()-0.5f, frandom()-0.5f))*(256.0f+frandom()*512.0f);
+				temp_system.new_particle(p, v, 0.2f+0.25f*frandom(), 16.0f, 128.0f, 0.985f);
+			}
+
+			for(int i = 0; i < 64; i++)
+			{
+				vec2 v = normalize(vec2(frandom()-0.5f, frandom()-0.5f))*(frandom()*256.0f);
+				temp_system.new_particle(p, v, 0.2f+0.25f*frandom(), 24.0f, 128.0f, 0.985f);
+			}
+		}
+		else if(item.type == EVENT_SMOKE)
+		{
+			ev_explosion *ev = (ev_explosion *)data;
+			vec2 p(ev->x, ev->y);
+
+			// center explosion
+			vec2 v = normalize(vec2(frandom()-0.5f, -frandom()))*(32.0f+frandom()*32.0f);
+			temp_system.new_particle(p, v, 1.2f, 64.0f, 0, 0.95f);
+			v = normalize(vec2(frandom()-0.5f, -frandom()))*(128.0f+frandom()*128.0f);
+			temp_system.new_particle(p, v, 1.2f, 32.0f, 0, 0.95f);
+			v = normalize(vec2(frandom()-0.5f, -frandom()))*(128.0f+frandom()*128.0f);
+			temp_system.new_particle(p, v, 1.2f, 16.0f, 0, 0.95f);
+
+			for(int i = 0; i < 8; i++)
+			{
+				vec2 v = normalize(vec2(frandom()-0.5f, frandom()-0.5f))*(64.0f+frandom()*64.0f);
+				temp_system.new_particle(p, v, 0.5f+0.5f*frandom(), 16.0f, 0, 0.985f);
+			}
+
+			for(int i = 0; i < 8; i++)
+			{
+				vec2 v = normalize(vec2(frandom()-0.5f, frandom()-0.5f))*(128.0f+frandom()*256.0f);
+				temp_system.new_particle(p, v, 0.5f+0.5f*frandom(), 16.0f, 128.0f, 0.985f);
+			}
+		}
+		else if(item.type == EVENT_SPAWN)
+		{
+			ev_explosion *ev = (ev_explosion *)data;
+			vec2 p(ev->x, ev->y);
+
+			// center explosion
+			vec2 v = normalize(vec2(frandom()-0.5f, -frandom()))*(32.0f+frandom()*32.0f);
+			temp_system.new_particle(p, v, 1.2f, 64.0f, 0, 0.95f);
+			v = normalize(vec2(frandom()-0.5f, -frandom()))*(128.0f+frandom()*128.0f);
+			temp_system.new_particle(p, v, 1.2f, 32.0f, 0, 0.95f);
+			v = normalize(vec2(frandom()-0.5f, -frandom()))*(128.0f+frandom()*128.0f);
+			temp_system.new_particle(p, v, 1.2f, 16.0f, 0, 0.95f);
+
+			for(int i = 0; i < 8; i++)
+			{
+				vec2 v = normalize(vec2(frandom()-0.5f, frandom()-0.5f))*(64.0f+frandom()*64.0f);
+				temp_system.new_particle(p, v, 0.5f+0.5f*frandom(), 16.0f, 0, 0.985f);
+			}
+
+			for(int i = 0; i < 8; i++)
+			{
+				vec2 v = normalize(vec2(frandom()-0.5f, frandom()-0.5f))*(128.0f+frandom()*256.0f);
+				temp_system.new_particle(p, v, 0.5f+0.5f*frandom(), 16.0f, 128.0f, 0.985f);
+			}
+		}
+		else if(item.type == EVENT_DEATH)
+		{
+			ev_explosion *ev = (ev_explosion *)data;
+			vec2 p(ev->x, ev->y);
+			vec4 c(0.5f, 0.1f, 0.1f, 1.0f);
+
+			// center explosion
+			vec2 v = normalize(vec2(frandom()-0.5f, -frandom()))*(32.0f+frandom()*32.0f);
+			temp_system.new_particle(p, v, 1.2f, 64.0f, 0, 0.95f, c);
+			v = normalize(vec2(frandom()-0.5f, -frandom()))*(128.0f+frandom()*128.0f);
+			temp_system.new_particle(p, v, 1.2f, 32.0f, 0, 0.95f, c);
+			v = normalize(vec2(frandom()-0.5f, -frandom()))*(128.0f+frandom()*128.0f);
+			temp_system.new_particle(p, v, 1.2f, 16.0f, 0, 0.95f, c);
+
+			for(int i = 0; i < 8; i++)
+			{
+				vec2 v = normalize(vec2(frandom()-0.5f, frandom()-0.5f))*(64.0f+frandom()*64.0f);
+				temp_system.new_particle(p, v, 0.5f+0.5f*frandom(), 16.0f, 0, 0.985f, c);
+			}
+
+			for(int i = 0; i < 8; i++)
+			{
+				vec2 v = normalize(vec2(frandom()-0.5f, frandom()-0.5f))*(128.0f+frandom()*256.0f);
+				temp_system.new_particle(p, v, 0.5f+0.5f*frandom(), 16.0f, 128.0f, 0.985f, c);
+			}
+		}
+		else if(item.type == EVENT_SOUND_WORLD)
+		{
+			ev_sound *ev = (ev_sound *)data;
+			if(ev->sound >= 0 && ev->sound < NUM_SOUNDS)
+				snd_play_random(CHN_WORLD, ev->sound, 1.0f, vec2(ev->x, ev->y));
+		}
+	}
+}
+
+static player_core predicted_prev_player;
+static player_core predicted_player;
+static int predicted_tick = 0;
+static int last_new_predicted_tick = -1;
+
+extern "C" void modc_predict()
+{
+	player_core before_prev_player = predicted_prev_player;
+	player_core before_player = predicted_player;
+
+
+	// repredict player
+	world_core world;
+	int local_cid = -1;
+
+	// search for players
+	for(int i = 0; i < snap_num_items(SNAP_CURRENT); i++)
+	{
+		SNAP_ITEM item;
+		const void *data = snap_get_item(SNAP_CURRENT, i, &item);
+		int client_id = item.id;
+
+		if(item.type == OBJTYPE_PLAYER_CHARACTER)
+		{
+			const obj_player_character *character = (const obj_player_character *)data;
+			client_datas[client_id].predicted.world = &world;
+			world.players[client_id] = &client_datas[client_id].predicted;
+
+			client_datas[client_id].predicted.read(character);
+		}
+		else if(item.type == OBJTYPE_PLAYER_INFO)
+		{
+			const obj_player_info *info = (const obj_player_info *)data;
+			if(info->local)
+				local_cid = client_id;
+		}
+	}
+
+	// predict
+	for(int tick = client_tick()+1; tick <= client_predtick(); tick++)
+	{
+		// fetch the local
+		if(tick == client_predtick() && world.players[local_cid])
+			predicted_prev_player = *world.players[local_cid];
+		
+		// first calculate where everyone should move
+		for(int c = 0; c < MAX_CLIENTS; c++)
+		{
+			if(!world.players[c])
+				continue;
+
+			mem_zero(&world.players[c]->input, sizeof(world.players[c]->input));
+			if(local_cid == c)
+			{
+				// apply player input
+				int *input = client_get_input(tick);
+				if(input)
+					world.players[c]->input = *((player_input*)input);
+			}
+
+			world.players[c]->tick();
+		}
+
+		// move all players and quantize their data
+		for(int c = 0; c < MAX_CLIENTS; c++)
+		{
+			if(!world.players[c])
+				continue;
+
+			world.players[c]->move();
+			world.players[c]->quantize();
+		}
+		
+		if(tick > last_new_predicted_tick)
+		{
+			last_new_predicted_tick = tick;
+			
+			if(local_cid != -1 && world.players[local_cid])
+			{
+				vec2 pos = world.players[local_cid]->pos;
+				int events = world.players[local_cid]->triggered_events;
+				if(events&COREEVENT_GROUND_JUMP) snd_play_random(CHN_WORLD, SOUND_PLAYER_JUMP, 1.0f, pos);
+				if(events&COREEVENT_AIR_JUMP)
+				{
+					create_air_jump_effect(pos);
+					snd_play_random(CHN_WORLD, SOUND_PLAYER_JUMP, 1.0f, pos);
+				}
+				//if(events&COREEVENT_HOOK_LAUNCH) snd_play_random(CHN_WORLD, SOUND_HOOK_LOOP, 1.0f, pos);
+				if(events&COREEVENT_HOOK_ATTACH_PLAYER) snd_play_random(CHN_WORLD, SOUND_HOOK_ATTACH_PLAYER, 1.0f, pos);
+				if(events&COREEVENT_HOOK_ATTACH_GROUND) snd_play_random(CHN_WORLD, SOUND_HOOK_ATTACH_GROUND, 1.0f, pos);
+				//if(events&COREEVENT_HOOK_RETRACT) snd_play_random(CHN_WORLD, SOUND_PLAYER_JUMP, 1.0f, pos);
+			}
+
+
+			/*
+			dbg_msg("predict", "%d %d %d", tick,
+				(int)world.players[c]->pos.x, (int)world.players[c]->pos.y,
+				(int)world.players[c]->vel.x, (int)world.players[c]->vel.y);*/
+		}
+		
+		if(tick == client_predtick() && world.players[local_cid])
+			predicted_player = *world.players[local_cid];
+	}
+	
+	if(config.debug && predicted_tick == client_predtick())
+	{
+		if(predicted_player.pos.x != before_player.pos.x ||
+			predicted_player.pos.y != before_player.pos.y)
+		{
+			dbg_msg("client", "prediction error, (%d %d) (%d %d)", 
+				(int)before_player.pos.x, (int)before_player.pos.y,
+				(int)predicted_player.pos.x, (int)predicted_player.pos.y);
+		}
+
+		if(predicted_prev_player.pos.x != before_prev_player.pos.x ||
+			predicted_prev_player.pos.y != before_prev_player.pos.y)
+		{
+			dbg_msg("client", "prediction error, prev (%d %d) (%d %d)", 
+				(int)before_prev_player.pos.x, (int)before_prev_player.pos.y,
+				(int)predicted_prev_player.pos.x, (int)predicted_prev_player.pos.y);
+		}
+	}
+	
+	predicted_tick = client_predtick();
+}
+
+static void clear_object_pointers()
+{
+	// clear out the invalid pointers
+	local_character = 0;
+	local_prev_character = 0;
+	local_info = 0;
+	flags[0] = 0;
+	flags[1] = 0;
+	gameobj = 0;
+}
+
+extern "C" void modc_newsnapshot()
+{
+	process_events(SNAP_CURRENT);
+
+	if(config.dbg_stress)
+	{
+		if((client_tick()%250) == 0)
+		{
+			msg_pack_start(MSG_SAY, MSGFLAG_VITAL);
+			msg_pack_int(-1);
+			msg_pack_string("galenskap!!!!", 512);
+			msg_pack_end();
+			client_send_msg();
+		}
+	}
+
+	clear_object_pointers();
+
+	// setup world view
+	{
+		// 1. fetch local player
+		// 2. set him to the center
+		int num = snap_num_items(SNAP_CURRENT);
+		for(int i = 0; i < num; i++)
+		{
+			SNAP_ITEM item;
+			const void *data = snap_get_item(SNAP_CURRENT, i, &item);
+
+			if(item.type == OBJTYPE_PLAYER_INFO)
+			{
+				const obj_player_info *info = (const obj_player_info *)data;
+				if(info->local)
+				{
+					local_info = info;
+					const void *data = snap_find_item(SNAP_CURRENT, OBJTYPE_PLAYER_CHARACTER, item.id);
+					if(data)
+					{
+						local_character = (const obj_player_character *)data;
+						local_character_pos = vec2(local_character->x, local_character->y);
+
+						const void *p = snap_find_item(SNAP_PREV, OBJTYPE_PLAYER_CHARACTER, item.id);
+						if(p)
+							local_prev_character = (obj_player_character *)p;
+					}
+				}
+			}
+			else if(item.type == OBJTYPE_GAME)
+				gameobj = (obj_game *)data;
+			else if(item.type == OBJTYPE_FLAG)
+			{
+				flags[item.id%2] = (const obj_flag *)data;
+			}
+		}
+	}
+}
+
+void send_info(bool start)
+{
+	if(start)
+		msg_pack_start(MSG_STARTINFO, MSGFLAG_VITAL);
+	else
+		msg_pack_start(MSG_CHANGEINFO, MSGFLAG_VITAL);
+	msg_pack_string(config.player_name, 64);
+	msg_pack_string(config.player_skin, 64);
+	msg_pack_int(config.player_use_custom_color);
+	msg_pack_int(config.player_color_body);
+	msg_pack_int(config.player_color_feet);
+	msg_pack_end();
+	client_send_msg();
+}
+
+void send_emoticon(int emoticon)
+{
+	msg_pack_start(MSG_EMOTICON, MSGFLAG_VITAL);
+	msg_pack_int(emoticon);
+	msg_pack_end();
+	client_send_msg();
+}
+
+static void render_projectile(const obj_projectile *prev, const obj_projectile *current, int itemid)
+{
+	gfx_texture_set(data->images[IMAGE_GAME].id);
+	gfx_quads_begin();
+
+	// get positions
+	float gravity = -400;
+	if(current->type != WEAPON_ROCKET)
+		gravity = -100;
+
+	float ct = (client_tick()-current->start_tick)/(float)SERVER_TICK_SPEED + client_intratick()*1/(float)SERVER_TICK_SPEED;
+	vec2 startpos(current->x, current->y);
+	vec2 startvel(current->vx, current->vy);
+	vec2 pos = calc_pos(startpos, startvel, gravity, ct);
+	vec2 prevpos = calc_pos(startpos, startvel, gravity, ct-0.001f);
+
+	select_sprite(data->weapons[current->type%data->num_weapons].sprite_proj);
+	vec2 vel = pos-prevpos;
+	//vec2 pos = mix(vec2(prev->x, prev->y), vec2(current->x, current->y), client_intratick());
+
+	// add particle for this projectile
+	proj_particles.addparticle(current->type, itemid, pos, vel);
+
+	if(length(vel) > 0.00001f)
+		gfx_quads_setrotation(get_angle(vel));
+	else
+		gfx_quads_setrotation(0);
+
+	// TODO: do this, but nice
+	//temp_system.new_particle(pos, vec2(0,0), 0.3f, 14.0f, 0, 0.95f);
+
+	gfx_quads_draw(pos.x, pos.y, 32, 32);
+	gfx_quads_setrotation(0);
+	gfx_quads_end();
+}
+
+static void render_powerup(const obj_powerup *prev, const obj_powerup *current)
+{
+	gfx_texture_set(data->images[IMAGE_GAME].id);
+	gfx_quads_begin();
+	vec2 pos = mix(vec2(prev->x, prev->y), vec2(current->x, current->y), client_intratick());
+	float angle = 0.0f;
+	float size = 64.0f;
+	if (current->type == POWERUP_WEAPON)
+	{
+		angle = 0; //-pi/6;//-0.25f * pi * 2.0f;
+		select_sprite(data->weapons[current->subtype%data->num_weapons].sprite_body);
+		size = data->weapons[current->subtype%data->num_weapons].visual_size;
+	}
+	else
+	{
+		const int c[] = {
+			SPRITE_POWERUP_HEALTH,
+			SPRITE_POWERUP_ARMOR,
+			SPRITE_POWERUP_WEAPON,
+			SPRITE_POWERUP_NINJA,
+			SPRITE_POWERUP_TIMEFIELD
+			};
+		select_sprite(c[current->type]);
+
+		if(c[current->type] == SPRITE_POWERUP_NINJA)
+		{
+			proj_particles.addparticle(0, 0,
+				pos+vec2((frandom()-0.5f)*80.0f, (frandom()-0.5f)*20.0f),
+				vec2((frandom()-0.5f)*10.0f, (frandom()-0.5f)*10.0f));
+			size *= 2.0f;
+			pos.x += 10.0f;
+		}
+	}
+
+	gfx_quads_setrotation(angle);
+
+	float offset = pos.y/32.0f + pos.x/32.0f;
+	pos.x += cosf(client_localtime()*2.0f+offset)*2.5f;
+	pos.y += sinf(client_localtime()*2.0f+offset)*2.5f;
+	draw_sprite(pos.x, pos.y, size);
+	gfx_quads_end();
+}
+
+static void render_flag(const obj_flag *prev, const obj_flag *current)
+{
+	float angle = 0.0f;
+	float size = 42.0f;
+
+    gfx_blend_normal();
+    gfx_texture_set(data->images[IMAGE_GAME].id);
+    gfx_quads_begin();
+
+	if(current->team == 0) // red team
+		select_sprite(SPRITE_FLAG_RED);
+	else
+		select_sprite(SPRITE_FLAG_BLUE);
+
+	gfx_quads_setrotation(angle);
+
+	vec2 pos = mix(vec2(prev->x, prev->y), vec2(current->x, current->y), client_intratick());
+
+	if(local_info && current->carried_by == local_info->clientid)
+		pos = local_character_pos;
+
+    gfx_setcolor(current->team ? 0 : 1,0,current->team ? 1 : 0,1);
+    //draw_sprite(pos.x, pos.y, size);
+    gfx_quads_draw(pos.x, pos.y-size*0.75f, size, size*2);
+    gfx_quads_end();
+}
+
+void anim_seq_eval(sequence *seq, float time, keyframe *frame)
+{
+	if(seq->num_frames == 0)
+	{
+		frame->time = 0;
+		frame->x = 0;
+		frame->y = 0;
+		frame->angle = 0;
+	}
+	else if(seq->num_frames == 1)
+	{
+		*frame = seq->frames[0];
+	}
+	else
+	{
+		//time = max(0.0f, min(1.0f, time / duration)); // TODO: use clamp
+		keyframe *frame1 = 0;
+		keyframe *frame2 = 0;
+		float blend = 0.0f;
+
+		// TODO: make this smarter.. binary search
+		for (int i = 1; i < seq->num_frames; i++)
+		{
+			if (seq->frames[i-1].time <= time && seq->frames[i].time >= time)
+			{
+				frame1 = &seq->frames[i-1];
+				frame2 = &seq->frames[i];
+				blend = (time - frame1->time) / (frame2->time - frame1->time);
+				break;
+			}
+		}
+
+		if (frame1 && frame2)
+		{
+			frame->time = time;
+			frame->x = mix(frame1->x, frame2->x, blend);
+			frame->y = mix(frame1->y, frame2->y, blend);
+			frame->angle = mix(frame1->angle, frame2->angle, blend);
+		}
+	}
+}
+
+void anim_eval(animation *anim, float time, animstate *state)
+{
+	anim_seq_eval(&anim->body, time, &state->body);
+	anim_seq_eval(&anim->back_foot, time, &state->back_foot);
+	anim_seq_eval(&anim->front_foot, time, &state->front_foot);
+	anim_seq_eval(&anim->attach, time, &state->attach);
+}
+
+void anim_add_keyframe(keyframe *seq, keyframe *added, float amount)
+{
+	seq->x += added->x*amount;
+	seq->y += added->y*amount;
+	seq->angle += added->angle*amount;
+}
+
+void anim_add(animstate *state, animstate *added, float amount)
+{
+	anim_add_keyframe(&state->body, &added->body, amount);
+	anim_add_keyframe(&state->back_foot, &added->back_foot, amount);
+	anim_add_keyframe(&state->front_foot, &added->front_foot, amount);
+	anim_add_keyframe(&state->attach, &added->attach, amount);
+}
+
+void anim_eval_add(animstate *state, animation *anim, float time, float amount)
+{
+	animstate add;
+	anim_eval(anim, time, &add);
+	anim_add(state, &add, amount);
+}
+
+static void render_hand(int skin_id, vec2 center_pos, vec2 dir, float angle_offset, vec2 post_rot_offset)
+{
+	// for drawing hand
+	const skin *s = skin_get(skin_id);
+	
+	float basesize = 10.0f;
+	//dir = normalize(hook_pos-pos);
+
+	vec2 hand_pos = center_pos + dir;
+	float angle = get_angle(dir);
+	if (dir.x < 0)
+		angle -= angle_offset;
+	else
+		angle += angle_offset;
+
+	vec2 dirx = dir;
+	vec2 diry(-dir.y,dir.x);
+
+	if (dir.x < 0)
+		diry = -diry;
+
+	hand_pos += dirx * post_rot_offset.x;
+	hand_pos += diry * post_rot_offset.y;
+
+	//gfx_texture_set(data->images[IMAGE_CHAR_DEFAULT].id);
+	gfx_texture_set(s->color_texture);
+	gfx_quads_begin();
+
+	// two passes
+	for (int i = 0; i < 2; i++)
+	{
+		bool outline = i == 0;
+
+		select_sprite(outline?SPRITE_TEE_HAND_OUTLINE:SPRITE_TEE_HAND, 0, 0, 0);
+		gfx_quads_setrotation(angle);
+		gfx_quads_draw(hand_pos.x, hand_pos.y, 2*basesize, 2*basesize);
+	}
+
+	gfx_quads_setrotation(0);
+	gfx_quads_end();
+}
+
+void render_tee(animstate *anim, tee_render_info *info, int emote, vec2 dir, vec2 pos)
+{
+	vec2 direction = dir;
+	vec2 position = pos;
+
+	//gfx_texture_set(data->images[IMAGE_CHAR_DEFAULT].id);
+	gfx_texture_set(info->texture);
+	gfx_quads_begin();
+	//gfx_quads_draw(pos.x, pos.y-128, 128, 128);
+
+	// first pass we draw the outline
+	// second pass we draw the filling
+	for(int p = 0; p < 2; p++)
+	{
+		int outline = p==0 ? 1 : 0;
+
+		for(int f = 0; f < 2; f++)
+		{
+			float animscale = info->size * 1.0f/64.0f;
+			float basesize = info->size;
+			if(f == 1)
+			{
+				gfx_quads_setrotation(anim->body.angle*pi*2);
+
+				// draw body
+				gfx_setcolor(info->color_body.r, info->color_body.g, info->color_body.b, info->color_body.a);
+				vec2 body_pos = position + vec2(anim->body.x, anim->body.y)*animscale;
+				select_sprite(outline?SPRITE_TEE_BODY_OUTLINE:SPRITE_TEE_BODY, 0, 0, 0);
+				gfx_quads_draw(body_pos.x, body_pos.y, basesize, basesize);
+
+				// draw eyes
+				if(p == 1)
+				{
+					switch (emote)
+					{
+						case EMOTE_PAIN:
+							select_sprite(SPRITE_TEE_EYE_PAIN, 0, 0, 0);
+							break;
+						case EMOTE_HAPPY:
+							select_sprite(SPRITE_TEE_EYE_HAPPY, 0, 0, 0);
+							break;
+						case EMOTE_SURPRISE:
+							select_sprite(SPRITE_TEE_EYE_SURPRISE, 0, 0, 0);
+							break;
+						case EMOTE_ANGRY:
+							select_sprite(SPRITE_TEE_EYE_ANGRY, 0, 0, 0);
+							break;
+						default:
+							select_sprite(SPRITE_TEE_EYE_NORMAL, 0, 0, 0);
+							break;
+					}
+					
+					float eyescale = basesize*0.40f;
+					float h = emote == EMOTE_BLINK ? basesize*0.15f : eyescale;
+					float eyeseparation = (0.075f - 0.010f*fabs(direction.x))*basesize;
+					vec2 offset = vec2(direction.x*0.125f, -0.05f+direction.y*0.10f)*basesize;
+					gfx_quads_draw(body_pos.x-eyeseparation+offset.x, body_pos.y+offset.y, eyescale, h);
+					gfx_quads_draw(body_pos.x+eyeseparation+offset.x, body_pos.y+offset.y, -eyescale, h);
+				}
+			}
+
+			// draw feet
+			gfx_setcolor(info->color_feet.r, info->color_feet.g, info->color_feet.b, info->color_feet.a);
+			select_sprite(outline?SPRITE_TEE_FOOT_OUTLINE:SPRITE_TEE_FOOT, 0, 0, 0);
+
+			keyframe *foot = f ? &anim->front_foot : &anim->back_foot;
+
+			float w = basesize;
+			float h = basesize/2;
+
+			gfx_quads_setrotation(foot->angle*pi*2);
+			gfx_quads_draw(position.x+foot->x*animscale, position.y+foot->y*animscale, w, h);
+		}
+	}
+
+	gfx_quads_end();
+}
+
+void draw_circle(float x, float y, float r, int segments)
+{
+	float f_segments = (float)segments;
+	for(int i = 0; i < segments; i+=2)
+	{
+		float a1 = i/f_segments * 2*pi;
+		float a2 = (i+1)/f_segments * 2*pi;
+		float a3 = (i+2)/f_segments * 2*pi;
+		float ca1 = cosf(a1);
+		float ca2 = cosf(a2);
+		float ca3 = cosf(a3);
+		float sa1 = sinf(a1);
+		float sa2 = sinf(a2);
+		float sa3 = sinf(a3);
+
+		gfx_quads_draw_freeform(
+			x, y,
+			x+ca1*r, y+sa1*r,
+			x+ca3*r, y+sa3*r,
+			x+ca2*r, y+sa2*r);
+	}
+}
+
+void draw_round_rect_ext(float x, float y, float w, float h, float r, int corners)
+{
+	int num = 8;
+	for(int i = 0; i < num; i+=2)
+	{
+		float a1 = i/(float)num * pi/2;
+		float a2 = (i+1)/(float)num * pi/2;
+		float a3 = (i+2)/(float)num * pi/2;
+		float ca1 = cosf(a1);
+		float ca2 = cosf(a2);
+		float ca3 = cosf(a3);
+		float sa1 = sinf(a1);
+		float sa2 = sinf(a2);
+		float sa3 = sinf(a3);
+
+		if(corners&1) // TL
+		gfx_quads_draw_freeform(
+			x+r, y+r,
+			x+(1-ca1)*r, y+(1-sa1)*r,
+			x+(1-ca3)*r, y+(1-sa3)*r,
+			x+(1-ca2)*r, y+(1-sa2)*r);
+
+		if(corners&2) // TR
+		gfx_quads_draw_freeform(
+			x+w-r, y+r,
+			x+w-r+ca1*r, y+(1-sa1)*r,
+			x+w-r+ca3*r, y+(1-sa3)*r,
+			x+w-r+ca2*r, y+(1-sa2)*r);
+
+		if(corners&4) // BL
+		gfx_quads_draw_freeform(
+			x+r, y+h-r,
+			x+(1-ca1)*r, y+h-r+sa1*r,
+			x+(1-ca3)*r, y+h-r+sa3*r,
+			x+(1-ca2)*r, y+h-r+sa2*r);
+
+		if(corners&8) // BR
+		gfx_quads_draw_freeform(
+			x+w-r, y+h-r,
+			x+w-r+ca1*r, y+h-r+sa1*r,
+			x+w-r+ca3*r, y+h-r+sa3*r,
+			x+w-r+ca2*r, y+h-r+sa2*r);
+	}
+
+	gfx_quads_drawTL(x+r, y+r, w-r*2, h-r*2); // center
+	gfx_quads_drawTL(x+r, y, w-r*2, r); // top
+	gfx_quads_drawTL(x+r, y+h-r, w-r*2, r); // bottom
+	gfx_quads_drawTL(x, y+r, r, h-r*2); // left
+	gfx_quads_drawTL(x+w-r, y+r, r, h-r*2); // right
+	
+	if(!(corners&1)) gfx_quads_drawTL(x, y, r, r); // TL
+	if(!(corners&2)) gfx_quads_drawTL(x+w, y, -r, r); // TR
+	if(!(corners&4)) gfx_quads_drawTL(x, y+h, r, -r); // BL
+	if(!(corners&8)) gfx_quads_drawTL(x+w, y+h, -r, -r); // BR
+}
+
+void draw_round_rect(float x, float y, float w, float h, float r)
+{
+	draw_round_rect_ext(x,y,w,h,r,0xf);
+}
+
+
+static void render_player(
+	const obj_player_character *prev_char,
+	const obj_player_character *player_char,
+	const obj_player_info *prev_info,
+	const obj_player_info *player_info
+	)
+{
+	obj_player_character prev;
+	obj_player_character player;
+	prev = *prev_char;
+	player = *player_char;
+
+	obj_player_info info = *player_info;
+
+	float intratick = client_intratick();
+
+	if(player.health < 0) // dont render dead players
+		return;
+
+	if(info.local && config.cl_predict)
+	{
+		if(!local_character || (local_character->health < 0) || (gameobj && gameobj->game_over))
+		{
+		}
+		else
+		{
+			// apply predicted results
+			predicted_player.write(&player);
+			predicted_prev_player.write(&prev);
+			intratick = client_intrapredtick();
+		}
+	}
+
+	// TODO: proper skin selection
+	int skin_id = client_datas[info.clientid].skin_id; //charids[info.clientid];
+	//if(gametype != GAMETYPE_DM)
+		//skin_id = info.team*9; // 0 or 9
+
+	vec2 direction = get_direction(player.angle);
+	float angle = player.angle/256.0f;
+	vec2 position = mix(vec2(prev.x, prev.y), vec2(player.x, player.y), intratick);
+
+	if(prev.health < 0) // Don't flicker from previous position
+		position = vec2(player.x, player.y);
+
+	bool stationary = player.vx < 1 && player.vx > -1;
+	bool inair = col_check_point(player.x, player.y+16) == 0;
+
+	// evaluate animation
+	float walk_time = fmod(position.x, 100.0f)/100.0f;
+	animstate state;
+	anim_eval(&data->animations[ANIM_BASE], 0, &state);
+
+	if(inair)
+		anim_eval_add(&state, &data->animations[ANIM_INAIR], 0, 1.0f); // TODO: some sort of time here
+	else if(stationary)
+		anim_eval_add(&state, &data->animations[ANIM_IDLE], 0, 1.0f); // TODO: some sort of time here
+	else
+		anim_eval_add(&state, &data->animations[ANIM_WALK], walk_time, 1.0f);
+
+	if (player.weapon == WEAPON_HAMMER)
+	{
+		float a = clamp((client_tick()-player.attacktick+intratick)/10.0f, 0.0f, 1.0f);
+		anim_eval_add(&state, &data->animations[ANIM_HAMMER_SWING], a, 1.0f);
+	}
+	if (player.weapon == WEAPON_NINJA)
+	{
+		float a = clamp((client_tick()-player.attacktick+intratick)/40.0f, 0.0f, 1.0f);
+		anim_eval_add(&state, &data->animations[ANIM_NINJA_SWING], a, 1.0f);
+	}
+
+	// draw hook
+	if (prev.hook_state>0 && player.hook_state>0)
+	{
+		gfx_texture_set(data->images[IMAGE_GAME].id);
+		gfx_quads_begin();
+		//gfx_quads_begin();
+
+		vec2 pos = position;
+		vec2 hook_pos = mix(vec2(prev.hook_x, prev.hook_y), vec2(player.hook_x, player.hook_y), intratick);
+
+		float d = distance(pos, hook_pos);
+		vec2 dir = normalize(pos-hook_pos);
+
+		gfx_quads_setrotation(get_angle(dir)+pi);
+
+		// render head
+		select_sprite(SPRITE_HOOK_HEAD);
+		gfx_quads_draw(hook_pos.x, hook_pos.y, 24,16);
+
+		// render chain
+		select_sprite(SPRITE_HOOK_CHAIN);
+		for(float f = 24; f < d; f += 24)
+		{
+			vec2 p = hook_pos + dir*f;
+			gfx_quads_draw(p.x, p.y,24,16);
+		}
+
+		gfx_quads_setrotation(0);
+		gfx_quads_end();
+
+		render_hand(skin_id, position, normalize(hook_pos-pos), -pi/2, vec2(20, 0));
+	}
+
+	// draw gun
+	{
+		gfx_texture_set(data->images[IMAGE_GAME].id);
+		gfx_quads_begin();
+		gfx_quads_setrotation(state.attach.angle*pi*2+angle);
+
+		// normal weapons
+		int iw = clamp(player.weapon, 0, NUM_WEAPONS-1);
+		select_sprite(data->weapons[iw].sprite_body, direction.x < 0 ? SPRITE_FLAG_FLIP_Y : 0);
+
+		vec2 dir = direction;
+		float recoil = 0.0f;
+		vec2 p;
+		if (player.weapon == WEAPON_HAMMER)
+		{
+			// Static position for hammer
+			p = position;
+			p.y += data->weapons[iw].offsety;
+			// if attack is under way, bash stuffs
+			if(direction.x < 0)
+			{
+				gfx_quads_setrotation(-pi/2-state.attach.angle*pi*2);
+				p.x -= data->weapons[iw].offsetx;
+			}
+			else
+			{
+				gfx_quads_setrotation(-pi/2+state.attach.angle*pi*2);
+			}
+			draw_sprite(p.x, p.y, data->weapons[iw].visual_size);
+		}
+		else if (player.weapon == WEAPON_NINJA)
+		{
+			p = position;
+			p.y += data->weapons[iw].offsety;
+
+			if(direction.x < 0)
+			{
+				gfx_quads_setrotation(-pi/2-state.attach.angle*pi*2);
+				p.x -= data->weapons[iw].offsetx;
+			}
+			else
+			{
+				gfx_quads_setrotation(-pi/2+state.attach.angle*pi*2);
+			}
+			draw_sprite(p.x, p.y, data->weapons[iw].visual_size);
+
+			// HADOKEN
+			if ((client_tick()-player.attacktick) <= (SERVER_TICK_SPEED / 6) && data->weapons[iw].nummuzzlesprites)
+			{
+				int itex = rand() % data->weapons[iw].nummuzzlesprites;
+				float alpha = 1.0f;
+				if (alpha > 0.0f && data->weapons[iw].sprite_muzzle[itex].psprite)
+				{
+					vec2 dir = vec2(player_char->x,player_char->y) - vec2(prev_char->x, prev_char->y);
+					dir = normalize(dir);
+					float hadokenangle = get_angle(dir);
+					gfx_quads_setrotation(hadokenangle);
+					//float offsety = -data->weapons[iw].muzzleoffsety;
+					select_sprite(data->weapons[iw].sprite_muzzle[itex].psprite, 0);
+					vec2 diry(-dir.y,dir.x);
+					p = position;
+					float offsetx = data->weapons[iw].muzzleoffsetx;
+					p -= dir * offsetx;
+					draw_sprite(p.x, p.y, 160.0f);
+				}
+			}
+		}
+		else
+		{
+			// TODO: should be an animation
+			recoil = 0;
+			float a = (client_tick()-player.attacktick+intratick)/5.0f;
+			if(a < 1)
+				recoil = sinf(a*pi);
+			p = position + dir * data->weapons[iw].offsetx - dir*recoil*10.0f;
+			p.y += data->weapons[iw].offsety;
+			draw_sprite(p.x, p.y, data->weapons[iw].visual_size);
+		}
+
+		if (player.weapon == WEAPON_GUN || player.weapon == WEAPON_SHOTGUN)
+		{
+			// check if we're firing stuff
+			if (true)//prev.attackticks)
+			{
+				float alpha = 0.0f;
+				int phase1tick = (client_tick() - player.attacktick);
+				if (phase1tick < (data->weapons[iw].muzzleduration + 3))
+				{
+					float t = ((((float)phase1tick) + intratick)/(float)data->weapons[iw].muzzleduration);
+					alpha = LERP(2.0, 0.0f, min(1.0f,max(0.0f,t)));
+				}
+
+				int itex = rand() % data->weapons[iw].nummuzzlesprites;
+				if (alpha > 0.0f && data->weapons[iw].sprite_muzzle[itex].psprite)
+				{
+					float offsety = -data->weapons[iw].muzzleoffsety;
+					select_sprite(data->weapons[iw].sprite_muzzle[itex].psprite, direction.x < 0 ? SPRITE_FLAG_FLIP_Y : 0);
+					if(direction.x < 0)
+						offsety = -offsety;
+
+					vec2 diry(-dir.y,dir.x);
+					vec2 muzzlepos = p + dir * data->weapons[iw].muzzleoffsetx + diry * offsety;
+
+					draw_sprite(muzzlepos.x, muzzlepos.y, data->weapons[iw].visual_size);
+					/*gfx_setcolor(1.0f,1.0f,1.0f,alpha);
+					vec2 diry(-dir.y,dir.x);
+					p += dir * muzzleparams[player.weapon].offsetx + diry * offsety;
+					gfx_quads_draw(p.x,p.y,muzzleparams[player.weapon].sizex, muzzleparams[player.weapon].sizey);*/
+				}
+			}
+		}
+		gfx_quads_end();
+
+		switch (player.weapon)
+		{
+			case WEAPON_GUN: render_hand(skin_id, p, direction, -3*pi/4, vec2(-15, 4)); break;
+			case WEAPON_SHOTGUN: render_hand(skin_id, p, direction, -pi/2, vec2(-5, 4)); break;
+			case WEAPON_ROCKET: render_hand(skin_id, p, direction, -pi/2, vec2(-4, 7)); break;
+		}
+
+	}
+
+	// render the "shadow" tee
+	if(info.local && config.debug)
+	{
+		vec2 ghost_position = mix(vec2(prev_char->x, prev_char->y), vec2(player_char->x, player_char->y), client_intratick());
+		tee_render_info ghost = client_datas[info.clientid].skin_info;
+		ghost.color_body.a = 0.5f;
+		ghost.color_feet.a = 0.5f;
+		render_tee(&state, &ghost, player.emote, direction, ghost_position); // render ghost
+	}
+
+	// render the tee
+	render_tee(&state, &client_datas[info.clientid].skin_info, player.emote, direction, position);
+
+	if(player.state == STATE_CHATTING)
+	{
+		gfx_texture_set(data->images[IMAGE_EMOTICONS].id);
+		gfx_quads_begin();
+		select_sprite(SPRITE_DOTDOT);
+		gfx_quads_draw(position.x + 24, position.y - 40, 64,64);
+		gfx_quads_end();
+	}
+
+	if (client_datas[info.clientid].emoticon_start != -1 && client_datas[info.clientid].emoticon_start + 2 * client_tickspeed() > client_tick())
+	{
+		gfx_texture_set(data->images[IMAGE_EMOTICONS].id);
+		gfx_quads_begin();
+
+		int since_start = client_tick() - client_datas[info.clientid].emoticon_start;
+		int from_end = client_datas[info.clientid].emoticon_start + 2 * client_tickspeed() - client_tick();
+
+		float a = 1;
+
+		if (from_end < client_tickspeed() / 5)
+			a = from_end / (client_tickspeed() / 5.0);
+
+		float h = 1;
+		if (since_start < client_tickspeed() / 10)
+			h = since_start / (client_tickspeed() / 10.0);
+
+		float wiggle = 0;
+		if (since_start < client_tickspeed() / 5)
+			wiggle = since_start / (client_tickspeed() / 5.0);
+
+		float wiggle_angle = sin(5*wiggle);
+
+		gfx_quads_setrotation(pi/6*wiggle_angle);
+
+		gfx_setcolor(1.0f,1.0f,1.0f,a);
+		// client_datas::emoticon is an offset from the first emoticon
+		select_sprite(SPRITE_OOP + client_datas[info.clientid].emoticon);
+		gfx_quads_draw(position.x, position.y - 23 - 32*h, 64, 64*h);
+		gfx_quads_end();
+	}
+	
+	// render name plate
+	if(!info.local && config.cl_nameplates)
+	{
+		//gfx_pretty_text_color
+		float a = 1;
+		if(config.cl_nameplates_always == 0)
+			a = clamp(1-powf(distance(local_target_pos, position)/200.0f,16.0f), 0.0f, 1.0f);
+			
+		const char *name = client_datas[info.clientid].name;
+		float tw = gfx_pretty_text_width(28.0f, name, -1);
+		gfx_pretty_text_color(1,1,1,a);
+		gfx_pretty_text(position.x-tw/2.0f, position.y-60, 28.0f, name, -1);
+		gfx_pretty_text_color(1,1,1,1);
+	}
+}
+
+void render_moon(float x, float y)
+{
+	gfx_texture_set(data->images[IMAGE_MOON].id);
+	gfx_quads_begin();
+	gfx_quads_draw(x, y, 512, 512);
+	gfx_quads_end();
+}
+
+void render_stars()
+{
+	struct star
+	{
+		vec2 p;
+		float tingle;
+		float rot;
+		float size;
+	};
+	
+	static const int NUM_STARS = 100;
+	static star stars[NUM_STARS];
+	static bool init = true;
+	if(init)
+	{
+		for(int i = 0; i < NUM_STARS; i++)
+		{
+			stars[i].p.x = (frandom()-0.5f)*800*3;
+			stars[i].p.y = (frandom()-0.5f)*600*3;
+			stars[i].tingle = frandom();
+			stars[i].rot = frandom()*pi;
+			stars[i].size = 10.0f+frandom()*10.0f;
+		}
+		
+		init = false;
+	}
+
+	int64 now = time_get();
+	int64 freq = time_freq()*5;
+	float t = (now%freq)/(float)freq;
+	gfx_texture_set(data->images[IMAGE_STARS].id);
+	gfx_quads_begin();
+	gfx_quads_setsubset(0,0,0.5f,1);
+	for(int i = 0; i < NUM_STARS; i++)
+	{
+		float a = fmod(t+stars[i].tingle, 1.0f);
+		gfx_quads_setrotation(stars[i].rot);
+		gfx_setcolor(1,1,1,0.25f+sinf(a*pi)*0.75f);
+		gfx_quads_draw(stars[i].p.x, stars[i].p.y, stars[i].size, stars[i].size);
+	}
+	
+	gfx_quads_end();
+}
+
+void render_snow()
+{
+	vec2 tl, br;
+	gfx_getscreen(&tl.x, &tl.y, &br.x, &br.y);
+	tl.x += 1000; // this is here to fix positions below 0,0
+	tl.y += 1000;
+	br.x += 1000;
+	br.y += 1000;
+	
+	struct flake
+	{
+		vec2 p;
+		float tingle;
+		float rot;
+		float size;
+		float yspeed;
+		float xspeed;
+	};
+	
+	static const int NUM_FLAKES = 100;
+	static flake flakes[NUM_FLAKES];
+	static bool init = true;
+	
+	float w = br.x-tl.x;
+	float h = br.y-tl.y;
+	
+	if(init)
+	{
+		for(int i = 0; i < NUM_FLAKES; i++)
+		{
+			flakes[i].p.x = frandom()*w;
+			flakes[i].p.y = frandom()*h;
+			flakes[i].tingle = frandom();
+			flakes[i].rot = frandom()*pi;
+			flakes[i].size = 50.0f+frandom()*10.0f;
+			flakes[i].yspeed = 50+frandom()*20.0f;
+			flakes[i].xspeed = flakes[i].yspeed * (0.4+frandom()*0.4f);
+		}
+		
+		init = false;
+	}
+	
+	int basex = (int)(tl.x/w);
+	float splitx = tl.x-basex*w;
+	int basey = (int)(tl.y/h);
+	float splity = tl.y-basey*h;
+	
+	float f = client_frametime();
+	gfx_texture_set(data->images[IMAGE_SNOW].id);
+	gfx_quads_begin();
+	for(int i = 0; i < NUM_FLAKES; i++)
+	{
+		flakes[i].p.x -= f*flakes[i].xspeed;
+		flakes[i].p.y += f*flakes[i].yspeed;
+		if(flakes[i].p.x < 0)
+			flakes[i].p.x += w;
+		if(flakes[i].p.y > h)
+			flakes[i].p.y -= h;
+			
+		float x = flakes[i].p.x + basex*w;
+		float y = flakes[i].p.y + basey*h;
+		
+		if(flakes[i].p.x < splitx)
+			x += w;
+		if(flakes[i].p.y < splity)
+			y += h;
+			
+		x -= 1000;
+		y -= 1000;
+			
+		gfx_quads_setrotation(flakes[i].rot);
+		gfx_quads_draw(x, y, flakes[i].size, flakes[i].size);
+	}
+		
+	gfx_quads_end();
+}
+
+void render_sun(float x, float y)
+{
+	vec2 pos(x, y);
+
+	gfx_texture_set(-1);
+	gfx_blend_additive();
+	gfx_quads_begin();
+	const int rays = 10;
+	gfx_setcolor(1.0f,1.0f,1.0f,0.025f);
+	for(int r = 0; r < rays; r++)
+	{
+		float a = r/(float)rays + client_localtime()*0.025f;
+		float size = (1.0f/(float)rays)*0.25f;
+		vec2 dir0(sinf((a-size)*pi*2.0f), cosf((a-size)*pi*2.0f));
+		vec2 dir1(sinf((a+size)*pi*2.0f), cosf((a+size)*pi*2.0f));
+
+		gfx_setcolorvertex(0, 1.0f,1.0f,1.0f,0.025f);
+		gfx_setcolorvertex(1, 1.0f,1.0f,1.0f,0.025f);
+		gfx_setcolorvertex(2, 1.0f,1.0f,1.0f,0.0f);
+		gfx_setcolorvertex(3, 1.0f,1.0f,1.0f,0.0f);
+		const float range = 1000.0f;
+		gfx_quads_draw_freeform(
+			pos.x+dir0.x, pos.y+dir0.y,
+			pos.x+dir1.x, pos.y+dir1.y,
+			pos.x+dir0.x*range, pos.y+dir0.y*range,
+			pos.x+dir1.x*range, pos.y+dir1.y*range);
+	}
+	gfx_quads_end();
+	gfx_blend_normal();
+
+	gfx_texture_set(data->images[IMAGE_SUN].id);
+	gfx_quads_begin();
+	gfx_quads_draw(pos.x, pos.y, 256, 256);
+	gfx_quads_end();
+}
+
+static vec2 emoticon_selector_mouse;
+
+void emoticon_selector_reset()
+{
+	emoticon_selector_mouse = vec2(0, 0);
+}
+
+int emoticon_selector_render()
+{
+	int x, y;
+	inp_mouse_relative(&x, &y);
+
+	emoticon_selector_mouse.x += x;
+	emoticon_selector_mouse.y += y;
+
+	if (length(emoticon_selector_mouse) > 140)
+		emoticon_selector_mouse = normalize(emoticon_selector_mouse) * 140;
+
+	float selected_angle = get_angle(emoticon_selector_mouse) + 2*pi/24;
+	if (selected_angle < 0)
+		selected_angle += 2*pi;
+
+	bool return_now = false;
+	int selected_emoticon = -1;
+
+	if (length(emoticon_selector_mouse) > 100)
+		selected_emoticon = (int)(selected_angle / (2*pi) * 12.0f);
+
+	if(!inp_key_pressed(config.key_emoticon))
+	{
+		return_now = true;
+		emoticon_selector_active = false;
+	}
+
+    RECT screen = *ui2_screen();
+
+	gfx_mapscreen(screen.x, screen.y, screen.w, screen.h);
+
+	gfx_blend_normal();
+
+	gfx_texture_set(-1);
+	gfx_quads_begin();
+	gfx_setcolor(0,0,0,0.3f);
+	draw_circle(screen.w/2, screen.h/2, 160, 64);
+	gfx_quads_end();
+
+	gfx_texture_set(data->images[IMAGE_EMOTICONS].id);
+	gfx_quads_begin();
+
+	for (int i = 0; i < 12; i++)
+	{
+		float angle = 2*pi*i/12.0;
+		if (angle > pi)
+			angle -= 2*pi;
+
+		bool selected = selected_emoticon == i;
+
+		float size = selected ? 96 : 64;
+
+		float nudge_x = 120 * cos(angle);
+		float nudge_y = 120 * sin(angle);
+		select_sprite(SPRITE_OOP + i);
+		gfx_quads_draw(screen.w/2 + nudge_x, screen.h/2 + nudge_y, size, size);
+	}
+
+	gfx_quads_end();
+
+    gfx_texture_set(data->images[IMAGE_CURSOR].id);
+    gfx_quads_begin();
+    gfx_setcolor(1,1,1,1);
+    gfx_quads_drawTL(emoticon_selector_mouse.x+screen.w/2,emoticon_selector_mouse.y+screen.h/2,24,24);
+    gfx_quads_end();
+
+	return return_now ? selected_emoticon : -1;
+}
+
+void render_goals(float x, float y, float w)
+{
+	float h = 50.0f;
+
+	gfx_blend_normal();
+	gfx_texture_set(-1);
+	gfx_quads_begin();
+	gfx_setcolor(0,0,0,0.5f);
+	draw_round_rect(x-10.f, y-10.f, w, h, 10.0f);
+	gfx_quads_end();
+
+	// render goals
+	//y = ystart+h-54;
+	if(gameobj && gameobj->time_limit)
+	{
+		char buf[64];
+		sprintf(buf, "Time Limit: %d min", gameobj->time_limit);
+		gfx_pretty_text(x+w/2, y, 32, buf, -1);
+	}
+	if(gameobj && gameobj->score_limit)
+	{
+		char buf[64];
+		sprintf(buf, "Score Limit: %d", gameobj->score_limit);
+		gfx_pretty_text(x+40, y, 32, buf, -1);
+	}
+
+}
+
+void render_scoreboard(float x, float y, float w, int team, const char *title)
+{
+	//float w = 550.0f;
+	//float x = width/2-w/2;
+	//;
+	//float y = ystart;
+	//float w = 550.0f;
+
+	animstate idlestate;
+	anim_eval(&data->animations[ANIM_BASE], 0, &idlestate);
+	anim_eval_add(&idlestate, &data->animations[ANIM_IDLE], 0, 1.0f);
+
+	//float ystart = y;
+	float h = 600.0f;
+
+	gfx_blend_normal();
+	gfx_texture_set(-1);
+	gfx_quads_begin();
+	gfx_setcolor(0,0,0,0.5f);
+	draw_round_rect(x-10.f, y-10.f, w, h, 40.0f);
+	gfx_quads_end();
+
+	// render title
+	if(!title)
+	{
+		if(gameobj->game_over)
+			title = "Game Over";
+		else
+			title = "Score Board";
+	}
+
+	float tw = gfx_pretty_text_width( 64, title, -1);
+
+	if(team == -1)
+	{
+		gfx_pretty_text(x+w/2-tw/2, y, 64, title, -1);
+	}
+	else
+	{
+		gfx_pretty_text(x+10, y, 64, title, -1);
+
+		if(gameobj)
+		{
+			char buf[128];
+			sprintf(buf, "%d", gameobj->teamscore[team&1]);
+			tw = gfx_pretty_text_width(64, buf, -1);
+			gfx_pretty_text(x+w-tw-40, y, 64, buf, -1);
+		}
+	}
+
+	y += 64.0f;
+
+	/*
+	if(team)
+	{
+		char buf[128];
+		sprintf(buf, "%4d", gameobj->teamscore[team&1]);
+		gfx_pretty_text(x+w/2-tw/2, y, 32, buf, -1);
+	}*/
+
+
+	// find players
+	const obj_player_info *players[MAX_CLIENTS] = {0};
+	int num_players = 0;
+	for(int i = 0; i < snap_num_items(SNAP_CURRENT); i++)
+	{
+		SNAP_ITEM item;
+		const void *data = snap_get_item(SNAP_CURRENT, i, &item);
+
+		if(item.type == OBJTYPE_PLAYER_INFO)
+		{
+			players[num_players] = (const obj_player_info *)data;
+			num_players++;
+		}
+	}
+
+	// sort players
+	for(int k = 0; k < num_players; k++) // ffs, bubblesort
+	{
+		for(int i = 0; i < num_players-k-1; i++)
+		{
+			if(players[i]->score < players[i+1]->score)
+			{
+				const obj_player_info *tmp = players[i];
+				players[i] = players[i+1];
+				players[i+1] = tmp;
+			}
+		}
+	}
+
+	// render headlines
+	gfx_pretty_text(x+10, y, 32, "Score", -1);
+	gfx_pretty_text(x+125, y, 32, "Name", -1);
+	gfx_pretty_text(x+w-70, y, 32, "Ping", -1);
+	y += 38.0f;
+
+	// render player scores
+	for(int i = 0; i < num_players; i++)
+	{
+		const obj_player_info *info = players[i];
+
+		// make sure that we render the correct team
+		if(team != -1 && info->team != team)
+			continue;
+
+		char buf[128];
+		float font_size = 46.0f;
+		if(info->local)
+		{
+			// background so it's easy to find the local player
+			gfx_texture_set(-1);
+			gfx_quads_begin();
+			gfx_setcolor(1,1,1,0.25f);
+			draw_round_rect(x, y, w-20, 48, 20.0f);
+			gfx_quads_end();
+		}
+
+		sprintf(buf, "%4d", info->score);
+		gfx_pretty_text(x+60-gfx_pretty_text_width(font_size,buf,-1), y, font_size, buf, -1);
+		gfx_pretty_text(x+128, y, font_size, client_datas[info->clientid].name, -1);
+
+		sprintf(buf, "%4d", info->latency);
+		float tw = gfx_pretty_text_width(font_size, buf, -1);
+		gfx_pretty_text(x+w-tw-35, y, font_size, buf, -1);
+
+		// render avatar
+		render_tee(&idlestate, &client_datas[info->clientid].skin_info, EMOTE_NORMAL, vec2(1,0), vec2(x+90, y+28));
+		y += 50.0f;
+	}
+}
+
+void mapscreen_to_world(float center_x, float center_y, float zoom)
+{
+    RECT screen = *ui2_screen();
+
+	const float default_zoom = 1.5f;
+	float width = screen.w*default_zoom*zoom;
+	float height = screen.h*default_zoom*zoom;
+	gfx_mapscreen(center_x-width/2, center_y-height/2, center_x+width/2, center_y+height/2);
+}
+
+// renders the complete game world
+void render_world(float center_x, float center_y, float zoom)
+{
+	mapscreen_to_world(center_x, center_y, zoom);
+	//gfx_mapscreen(center_x-width/2, center_y-height/2, center_x+width/2, center_y+height/2);
+
+	// render background environment
+	int theme_id = 0;
+	mapres_theme *t = (mapres_theme *)map_find_item(MAPRES_TEMP_THEME, 0);
+	if(t)
+		theme_id = t->id;
+	
+	if(config.gfx_high_detail)
+	{
+		if(theme_id == 1)
+		{
+			// Winter night
+			gfx_mapscreen(0,0,1,1);
+			gfx_texture_set(-1);
+			gfx_quads_begin();
+				vec4 top(0x11/(float)0xff, 0x1a/(float)0xff, 0x21/(float)0xff, 1.0f);
+				vec4 bottom(0x2a/(float)0xff, 0x40/(float)0xff, 0x52/(float)0xff, 1.0f);
+				gfx_setcolorvertex(0, top.r, top.g, top.b, top.a);
+				gfx_setcolorvertex(1, top.r, top.g, top.b, top.a);
+				gfx_setcolorvertex(2, bottom.r, bottom.g, bottom.b, bottom.a);
+				gfx_setcolorvertex(3, bottom.r, bottom.g, bottom.b, bottom.a);
+				gfx_quads_drawTL(0, 0, 1, 1);
+			gfx_quads_end();
+
+			mapscreen_to_world(center_x*0.1f, center_y*0.1f, zoom);
+			render_stars();
+			
+			mapscreen_to_world(center_x, center_y, zoom);
+			
+			render_moon(center_x*0.8f, center_y*0.8f);
+			
+			mapscreen_to_world(center_x, center_y, zoom);
+		}
+		else
+		{
+			// Summer day
+			render_sun(20+center_x*0.6f, 20+center_y*0.6f);
+
+			// draw clouds
+			static vec2 cloud_pos[6] = {vec2(-500,0),vec2(-500,200),vec2(-500,400)};
+			static float cloud_speed[6] = {30, 20, 10};
+			static int cloud_sprites[6] = {SPRITE_CLOUD1, SPRITE_CLOUD2, SPRITE_CLOUD3};
+
+			gfx_texture_set(data->images[IMAGE_CLOUDS].id);
+			gfx_quads_begin();
+			for(int i = 0; i < 3; i++)
+			{
+				float parallax_amount = 0.55f;
+				select_sprite(cloud_sprites[i]);
+				draw_sprite((cloud_pos[i].x+fmod(client_localtime()*cloud_speed[i]+i*100.0f, 3000.0f))+center_x*parallax_amount,
+					cloud_pos[i].y+center_y*parallax_amount, 300);
+			}
+			gfx_quads_end();
+
+			// draw backdrop
+			gfx_texture_set(data->images[IMAGE_BACKDROP].id);
+			gfx_quads_begin();
+			float parallax_amount = 0.25f;
+			for(int x = -1; x < 3; x++)
+				gfx_quads_drawTL(1024*x+center_x*parallax_amount, (center_y)*parallax_amount+150+512, 1024, 512);
+			gfx_quads_end();
+		}
+	}
+	
+	// render background tilemaps
+	tilemap_render(32.0f, 0);
+
+	// render items
+	{
+		int num = snap_num_items(SNAP_CURRENT);
+		for(int i = 0; i < num; i++)
+		{
+			SNAP_ITEM item;
+			const void *data = snap_get_item(SNAP_CURRENT, i, &item);
+
+			if(item.type == OBJTYPE_PROJECTILE)
+			{
+				const void *prev = snap_find_item(SNAP_PREV, item.type, item.id);
+				if(prev)
+					render_projectile((const obj_projectile *)prev, (const obj_projectile *)data, item.id);
+			}
+			else if(item.type == OBJTYPE_POWERUP)
+			{
+				const void *prev = snap_find_item(SNAP_PREV, item.type, item.id);
+				if(prev)
+					render_powerup((const obj_powerup *)prev, (const obj_powerup *)data);
+			}
+			else if(item.type == OBJTYPE_FLAG)
+			{
+				const void *prev = snap_find_item(SNAP_PREV, item.type, item.id);
+				if (prev)
+					render_flag((const obj_flag *)prev, (const obj_flag *)data);
+			}
+		}
+	}
+
+	// render players above all
+	{
+		int num = snap_num_items(SNAP_CURRENT);
+		for(int i = 0; i < num; i++)
+		{
+			SNAP_ITEM item;
+			const void *data = snap_get_item(SNAP_CURRENT, i, &item);
+
+			if(item.type == OBJTYPE_PLAYER_CHARACTER)
+			{
+				const void *prev = snap_find_item(SNAP_PREV, item.type, item.id);
+				const void *prev_info = snap_find_item(SNAP_PREV, OBJTYPE_PLAYER_INFO, item.id);
+				const void *info = snap_find_item(SNAP_CURRENT, OBJTYPE_PLAYER_INFO, item.id);
+				if(prev && prev_info && info)
+				{
+					client_datas[((const obj_player_info *)info)->clientid].team = ((const obj_player_info *)info)->team;
+					render_player(
+							(const obj_player_character *)prev,
+							(const obj_player_character *)data,
+							(const obj_player_info *)prev_info,
+							(const obj_player_info *)info
+						);
+				}
+			}
+		}
+	}
+
+	// render particles
+	temp_system.update(client_frametime());
+	temp_system.render();
+
+	// render foreground tilemaps
+	tilemap_render(32.0f, 1);
+	
+	// render front environment effects
+	if(config.gfx_high_detail)
+	{
+		if(theme_id == 1)
+		{
+			//mapscreen_to_world(center_x, center_y, zoom);
+			render_snow();
+		}
+	}
+
+	// render damage indications
+	damageind.render();
+}
+
+static void do_input(int *v, int key)
+{
+	*v += inp_key_presses(key) + inp_key_releases(key);
+	if((*v&1) != inp_key_state(key))
+		(*v)++;
+	*v &= INPUT_STATE_MASK;
+}
+
+void render_game()
+{
+	float width = 400*3.0f;
+	float height = 300*3.0f;
+
+	bool spectate = false;
+
+	if(config.cl_predict)
+	{
+		if(!local_character || (local_character->health < 0) || (gameobj && gameobj->game_over))
+		{
+			// don't use predicted
+		}
+		else
+			local_character_pos = mix(predicted_prev_player.pos, predicted_player.pos, client_intrapredtick());
+	}
+	else if(local_character && local_prev_character)
+	{
+		local_character_pos = mix(
+			vec2(local_prev_character->x, local_prev_character->y),
+			vec2(local_character->x, local_character->y), client_intratick());
+	}
+	
+	if(local_info && local_info->team == -1)
+		spectate = true;
+
+	animstate idlestate;
+	anim_eval(&data->animations[ANIM_BASE], 0, &idlestate);
+	anim_eval_add(&idlestate, &data->animations[ANIM_IDLE], 0, 1.0f);
+
+	if (inp_key_down(KEY_ESC))
+	{
+		if (chat_mode)
+			chat_mode = CHATMODE_NONE;
+		else
+		{
+			menu_active = !menu_active;
+			if(menu_active)
+				menu_game_active = true;
+		}
+	}
+
+	// make sure to send our info again if the menu closes	
+	static bool menu_was_active = false;
+	if(menu_active)
+		menu_was_active = true;
+	else if(menu_was_active)
+	{
+		send_info(false);
+		menu_was_active = false;
+	}
+
+	// handle chat input
+	if (!menu_active)
+	{
+		if(chat_mode != CHATMODE_NONE)
+		{
+			if(inp_key_down(KEY_ENTER))
+			{
+				// send message
+				if(chat_input_len)
+				{
+					if(chat_mode == CHATMODE_CONSOLE)
+						config_set(chat_input);
+					else if(chat_mode == CHATMODE_REMOTECONSOLE)
+						client_rcon(chat_input);
+					else
+					{
+						// send chat message
+						msg_pack_start(MSG_SAY, MSGFLAG_VITAL);
+						if(chat_mode == CHATMODE_ALL)
+							msg_pack_int(0);
+						else
+							msg_pack_int(1);
+						msg_pack_string(chat_input, 512);
+						msg_pack_end();
+						client_send_msg();
+					}
+				}
+
+				chat_mode = CHATMODE_NONE;
+			}
+
+			int c = inp_last_char();
+			int k = inp_last_key();
+
+			if (!(c >= 0 && c < 32))
+			{
+				if (chat_input_len < sizeof(chat_input) - 1)
+				{
+					chat_input[chat_input_len] = c;
+					chat_input[chat_input_len+1] = 0;
+					chat_input_len++;
+				}
+			}
+
+			if(k == KEY_BACKSPACE)
+			{
+				if(chat_input_len > 0)
+				{
+					chat_input[chat_input_len-1] = 0;
+					chat_input_len--;
+				}
+			}
+
+		}
+		else
+		{
+			if(chat_mode == CHATMODE_NONE)
+			{
+				if(inp_key_down(config.key_chat))
+					chat_mode = CHATMODE_ALL;
+
+				if(inp_key_down(config.key_teamchat))
+					chat_mode = CHATMODE_TEAM;
+
+				if(inp_key_down(config.key_console))
+					chat_mode = CHATMODE_CONSOLE;
+				
+				if(inp_key_down(config.key_remoteconsole))
+					chat_mode = CHATMODE_REMOTECONSOLE;
+
+				if(chat_mode != CHATMODE_NONE)
+				{
+					mem_zero(chat_input, sizeof(chat_input));
+					chat_input_len = 0;
+				}
+			}
+		}
+	}
+
+	if (!menu_active)
+		inp_clear();
+
+	// fetch new input
+	if(!menu_active && !emoticon_selector_active)
+	{
+		int x, y;
+		inp_mouse_relative(&x, &y);
+		mouse_pos += vec2(x, y);
+		if(!spectate)
+		{
+			float l = length(mouse_pos);
+			if(l > 600.0f)
+				mouse_pos = normalize(mouse_pos)*600.0f;
+		}
+	}
+
+	// set listner pos
+	if(spectate)
+	{
+		local_target_pos = mouse_pos;
+		snd_set_listener_pos(mouse_pos.x, mouse_pos.y);
+	}
+	else
+	{
+		local_target_pos = local_character_pos + mouse_pos;
+		snd_set_listener_pos(local_character_pos.x, local_character_pos.y);
+	}
+
+	// snap input
+	{
+		static player_input input = {0};
+
+		input.target_x = (int)mouse_pos.x;
+		input.target_y = (int)mouse_pos.y;
+
+		if(chat_mode != CHATMODE_NONE)
+			input.state = STATE_CHATTING;
+		else if(menu_active)
+			input.state = STATE_IN_MENU;
+		else
+		{
+			input.state = STATE_PLAYING;
+			input.left = inp_key_state(config.key_move_left);
+			input.right = inp_key_state(config.key_move_right);
+			input.hook = inp_key_state(config.key_hook);
+			input.jump  = inp_key_state(config.key_jump);
+
+			if(!emoticon_selector_active)
+				do_input(&input.fire, config.key_fire);
+
+			// weapon selection
+			do_input(&input.next_weapon, config.key_next_weapon);
+			do_input(&input.prev_weapon, config.key_prev_weapon);
+
+			if(inp_key_presses(config.key_next_weapon) || inp_key_presses(config.key_prev_weapon))
+				input.wanted_weapon = 0;
+			else if (config.cl_autoswitch_weapons && picked_up_weapon)
+            {
+                input.wanted_weapon = picked_up_weapon;
+            }
+            else
+			{
+				if(inp_key_presses(config.key_weapon1)) input.wanted_weapon = 1;
+				if(inp_key_presses(config.key_weapon2)) input.wanted_weapon = 2;
+				if(inp_key_presses(config.key_weapon3)) input.wanted_weapon = 3;
+				if(inp_key_presses(config.key_weapon4)) input.wanted_weapon = 4;
+				if(inp_key_presses(config.key_weapon5)) input.wanted_weapon = 5;
+				if(inp_key_presses(config.key_weapon6)) input.wanted_weapon = 6;
+			}
+
+            picked_up_weapon = 0;
+		}
+
+		// stress testing
+		if(config.dbg_stress)
+		{
+			float t = client_localtime();
+			mem_zero(&input, sizeof(input));
+
+			input.left = ((int)t/2)&1;
+			input.right = ((int)t/2+1)&1;
+			input.jump = ((int)t);
+			input.fire = ((int)(t*10));
+			input.hook = ((int)(t*2))&1;
+			input.wanted_weapon = ((int)t)%NUM_WEAPONS;
+			input.target_x = (int)(sinf(t*3)*100.0f);
+			input.target_y = (int)(cosf(t*3)*100.0f);
+		}
+
+		snap_input(&input, sizeof(input));
+	}
+
+	// center at char but can be moved when mouse is far away
+	float offx = 0, offy = 0;
+	if (config.cl_dynamic_camera)
+	{
+		int deadzone = 300;
+		if(mouse_pos.x > deadzone) offx = mouse_pos.x-deadzone;
+		if(mouse_pos.x <-deadzone) offx = mouse_pos.x+deadzone;
+		if(mouse_pos.y > deadzone) offy = mouse_pos.y-deadzone;
+		if(mouse_pos.y <-deadzone) offy = mouse_pos.y+deadzone;
+		offx = offx*2/3;
+		offy = offy*2/3;
+	}
+
+	// render the world
+	gfx_clear(0.65f,0.78f,0.9f);
+	if(spectate)
+		render_world(mouse_pos.x, mouse_pos.y, 1.0f);
+	else
+	{
+		render_world(local_character_pos.x+offx, local_character_pos.y+offy, 1.0f);
+
+		// draw screen box
+		if(0)
+		{
+			gfx_texture_set(-1);
+			gfx_blend_normal();
+			gfx_lines_begin();
+				float cx = local_character_pos.x+offx;
+				float cy = local_character_pos.y+offy;
+				float w = 400*3/2;
+				float h = 300*3/2;
+				gfx_lines_draw(cx-w,cy-h,cx+w,cy-h);
+				gfx_lines_draw(cx+w,cy-h,cx+w,cy+h);
+				gfx_lines_draw(cx+w,cy+h,cx-w,cy+h);
+				gfx_lines_draw(cx-w,cy+h,cx-w,cy-h);
+			gfx_lines_end();
+		}
+	}
+
+
+	// pseudo format
+	// ZOOM ZOOM
+	float zoom = 3.0;
+	if(local_character)
+		cl_effects.getzoom(client_tick(), client_intratick(), local_character);
+
+	// DEBUG TESTING
+	if(zoom > 3.01f)
+	{
+		gfx_clear_mask(0);
+
+		gfx_texture_set(-1);
+		gfx_blend_normal();
+
+		gfx_mask_op(MASK_NONE, 1);
+
+		gfx_quads_begin();
+		gfx_setcolor(0.65f,0.78f,0.9f,1.0f);
+
+		float fov;
+		if (zoom > 3.01f)
+			fov = pi * (zoom - 3.0f) / 6.0f;
+		else
+			fov = pi / 6.0f;
+
+		float fade = 0.7f;
+
+
+		float a = get_angle(normalize(vec2(mouse_pos.x, mouse_pos.y)));
+		vec2 d = get_dir(a);
+		vec2 d0 = get_dir(a-fov/2.0f);
+		vec2 d1 = get_dir(a+fov/2.0f);
+
+		vec2 cd0 = get_dir(a-(fov*fade)/2.0f); // center direction
+		vec2 cd1 = get_dir(a+(fov*fade)/2.0f);
+
+		vec2 p0n = local_character_pos + d0*32.0f;
+		vec2 p1n = local_character_pos + d1*32.0f;
+		vec2 p0f = local_character_pos + d0*1000.0f;
+		vec2 p1f = local_character_pos + d1*1000.0f;
+
+		vec2 cn = local_character_pos + d*32.0f;
+		vec2 cf = local_character_pos + d*1000.0f;
+
+		vec2 cp0n = local_character_pos + cd0*32.0f;
+		vec2 cp0f = local_character_pos + cd0*1000.0f;
+		vec2 cp1n = local_character_pos + cd1*32.0f;
+		vec2 cp1f = local_character_pos + cd1*1000.0f;
+
+		gfx_quads_draw_freeform(
+			p0n.x,p0n.y,
+			p1n.x,p1n.y,
+			p0f.x,p0f.y,
+			p1f.x,p1f.y);
+		gfx_quads_end();
+
+		gfx_mask_op(MASK_SET, 0);
+
+		render_world(local_character_pos.x+offx, local_character_pos.y+offy, 2.0f);
+
+		gfx_mask_op(MASK_NONE, 0);
+
+		mapscreen_to_world(local_character_pos.x+offx, local_character_pos.y+offy, 1.0f);
+
+		gfx_texture_set(-1);
+		gfx_blend_normal();
+		gfx_quads_begin();
+		gfx_setcolor(0.5f,0.9f,0.5f,0.25f);
+		float r=0.5f, g=1.0f, b=0.5f;
+		float r2=r*0.25f, g2=g*0.25f, b2=b*0.25f;
+
+		gfx_setcolor(r,g,b,0.2f);
+		gfx_quads_draw_freeform(
+			cn.x,cn.y,
+			cn.x,cn.y,
+			cp0f.x,cp0f.y,
+			cp1f.x,cp1f.y);
+
+		gfx_setcolorvertex(0, r, g, b, 0.2f);
+		gfx_setcolorvertex(1, r2, g2, b2, 0.9f);
+		gfx_setcolorvertex(2, r, g, b, 0.2f);
+		gfx_setcolorvertex(3, r2, g2, b2, 0.9f);
+		gfx_quads_draw_freeform(
+			cn.x,cn.y,
+			p0n.x,p0n.y,
+			cp0f.x,cp0f.y,
+			p0f.x,p0f.y);
+
+		gfx_quads_draw_freeform(
+			cn.x,cn.y,
+			p1n.x,p1n.y,
+			cp1f.x,cp1f.y,
+			p1f.x,p1f.y);
+
+		gfx_quads_end();
+	}
+
+	if(local_character && !spectate && !(gameobj && gameobj->game_over))
+	{
+		gfx_texture_set(data->images[IMAGE_GAME].id);
+		gfx_quads_begin();
+
+		// render cursor
+		if (!menu_active && !emoticon_selector_active)
+		{
+			select_sprite(data->weapons[local_character->weapon%data->num_weapons].sprite_cursor);
+			float cursorsize = 64;
+			draw_sprite(local_target_pos.x, local_target_pos.y, cursorsize);
+		}
+		
+		float x = 5;
+		float y = 5;
+
+		// render ammo count
+		// render gui stuff
+		gfx_quads_end();
+		gfx_quads_begin();
+		gfx_mapscreen(0,0,400,300);
+		// if weaponstage is active, put a "glow" around the stage ammo
+		select_sprite(SPRITE_TEE_BODY);
+		for (int i = 0; i < local_character->weaponstage; i++)
+			gfx_quads_drawTL(x+local_character->ammocount * 12 -i*12, y+22, 11, 11);
+		select_sprite(data->weapons[local_character->weapon%data->num_weapons].sprite_proj);
+		for (int i = 0; i < local_character->ammocount; i++)
+			gfx_quads_drawTL(x+i*12,y+24,10,10);
+
+		gfx_quads_end();
+
+		gfx_texture_set(data->images[IMAGE_GAME].id);
+		gfx_quads_begin();
+		int h = 0;
+
+		// render health
+		select_sprite(SPRITE_HEALTH_FULL);
+		for(; h < local_character->health; h++)
+			gfx_quads_drawTL(x+h*12,y,10,10);
+
+		select_sprite(SPRITE_HEALTH_EMPTY);
+		for(; h < 10; h++)
+			gfx_quads_drawTL(x+h*12,y,10,10);
+
+		// render armor meter
+		h = 0;
+		select_sprite(SPRITE_ARMOR_FULL);
+		for(; h < local_character->armor; h++)
+			gfx_quads_drawTL(x+h*12,y+12,10,10);
+
+		select_sprite(SPRITE_ARMOR_EMPTY);
+		for(; h < 10; h++)
+			gfx_quads_drawTL(x+h*12,y+12,10,10);
+		gfx_quads_end();
+	}
+
+	// render kill messages
+	{
+		gfx_mapscreen(0, 0, width*1.5f, height*1.5f);
+		float startx = width*1.5f-10.0f;
+		float y = 20.0f;
+
+		for(int i = 0; i < killmsg_max; i++)
+		{
+
+			int r = (killmsg_current+i+1)%killmsg_max;
+			if(client_tick() > killmsgs[r].tick+50*10)
+				continue;
+
+			float font_size = 48.0f;
+			float killername_w = gfx_pretty_text_width(font_size, client_datas[killmsgs[r].killer].name, -1);
+			float victimname_w = gfx_pretty_text_width(font_size, client_datas[killmsgs[r].victim].name, -1);
+
+			float x = startx;
+
+			// render victim name
+			x -= victimname_w;
+			gfx_pretty_text(x, y, font_size, client_datas[killmsgs[r].victim].name, -1);
+
+			// render victim tee
+			x -= 24.0f;
+			
+			if(gameobj && gameobj->gametype == GAMETYPE_CTF)
+			{
+				if(killmsgs[r].mode_special&1)
+				{
+					gfx_blend_normal();
+					gfx_texture_set(data->images[IMAGE_GAME].id);
+					gfx_quads_begin();
+
+					if(client_datas[killmsgs[r].victim].team == 0) select_sprite(SPRITE_FLAG_BLUE);
+					else select_sprite(SPRITE_FLAG_RED);
+					
+					float size = 56.0f;
+					gfx_quads_drawTL(x, y-16, size/2, size);
+					gfx_quads_end();					
+				}
+			}
+			
+			render_tee(&idlestate, &client_datas[killmsgs[r].victim].skin_info, EMOTE_PAIN, vec2(-1,0), vec2(x, y+28));
+			x -= 32.0f;
+			
+			// render weapon
+			x -= 44.0f;
+			if (killmsgs[r].weapon >= 0)
+			{
+				gfx_texture_set(data->images[IMAGE_GAME].id);
+				gfx_quads_begin();
+				select_sprite(data->weapons[killmsgs[r].weapon].sprite_body);
+				draw_sprite(x, y+28, 96);
+				gfx_quads_end();
+			}
+			x -= 52.0f;
+
+			if(killmsgs[r].victim != killmsgs[r].killer)
+			{
+				if(gameobj && gameobj->gametype == GAMETYPE_CTF)
+				{
+					if(killmsgs[r].mode_special&2)
+					{
+						gfx_blend_normal();
+						gfx_texture_set(data->images[IMAGE_GAME].id);
+						gfx_quads_begin();
+
+						if(client_datas[killmsgs[r].killer].team == 0) select_sprite(SPRITE_FLAG_BLUE, SPRITE_FLAG_FLIP_X);
+						else select_sprite(SPRITE_FLAG_RED, SPRITE_FLAG_FLIP_X);
+						
+						float size = 56.0f;
+						gfx_quads_drawTL(x-56, y-16, size/2, size);
+						gfx_quads_end();				
+					}
+				}				
+				
+				// render killer tee
+				x -= 24.0f;
+				render_tee(&idlestate, &client_datas[killmsgs[r].killer].skin_info, EMOTE_ANGRY, vec2(1,0), vec2(x, y+28));
+				x -= 32.0f;
+
+				// render killer name
+				x -= killername_w;
+				gfx_pretty_text(x, y, font_size, client_datas[killmsgs[r].killer].name, -1);
+			}
+
+			y += 44;
+		}
+	}
+
+	// render chat
+	{
+		gfx_mapscreen(0,0,400,300);
+		float x = 10.0f;
+		float y = 300.0f-50.0f;
+		float starty = -1;
+		if(chat_mode != CHATMODE_NONE)
+		{
+			// render chat input
+			char buf[sizeof(chat_input)+16];
+			if(chat_mode == CHATMODE_ALL)
+				sprintf(buf, "All: %s_", chat_input);
+			else if(chat_mode == CHATMODE_TEAM)
+				sprintf(buf, "Team: %s_", chat_input);
+			else if(chat_mode == CHATMODE_CONSOLE)
+				sprintf(buf, "Console: %s_", chat_input);
+			else if(chat_mode == CHATMODE_REMOTECONSOLE)
+				sprintf(buf, "Rcon: %s_", chat_input);
+			else
+				sprintf(buf, "Chat: %s_", chat_input);
+			gfx_pretty_text(x, y, 10.0f, buf, 380);
+			starty = y;
+		}
+
+		y -= 10;
+
+		int i;
+		for(i = 0; i < chat_max_lines; i++)
+		{
+			int r = ((chat_current_line-i)+chat_max_lines)%chat_max_lines;
+			if(client_tick() > chat_lines[r].tick+50*15)
+				break;
+
+			int lines = int(gfx_pretty_text_width(10, chat_lines[r].text, -1)) / 380 + 1;
+
+			gfx_pretty_text_color(1,1,1,1);
+			if(chat_lines[r].client_id == -1)
+				gfx_pretty_text_color(1,1,0.5f,1); // system
+			else if(chat_lines[r].team)
+				gfx_pretty_text_color(0.5f,1,0.5f,1); // team message
+
+			gfx_pretty_text(x, y - 8 * (lines - 1), 10, chat_lines[r].text, 380);
+			y -= 8 * lines;
+		}
+
+		gfx_pretty_text_color(1,1,1,1);
+	}
+
+	// render goals
+	if(gameobj)
+	{
+		gametype = gameobj->gametype;
+		gfx_mapscreen(0,0,400,300);
+		if(!gameobj->sudden_death)
+		{
+			char buf[32];
+			int time = 0;
+			if(gameobj->time_limit)
+			{
+				time = gameobj->time_limit*60 - ((client_tick()-gameobj->round_start_tick)/client_tickspeed());
+
+				if(gameobj->game_over)
+					time  = 0;
+			}
+			else
+				time = (client_tick()-gameobj->round_start_tick)/client_tickspeed();
+
+			sprintf(buf, "%d:%02d", time /60, time %60);
+			float w = gfx_pretty_text_width(16, buf, -1);
+			gfx_pretty_text(200-w/2, 2, 16, buf, -1);
+		}
+
+		if(gameobj->sudden_death)
+		{
+			const char *text = "Sudden Death";
+			float w = gfx_pretty_text_width(16, text, -1);
+			gfx_pretty_text(200-w/2, 2, 16, text, -1);
+		}
+
+		// render small score hud
+		if(!(gameobj && gameobj->game_over) && (gametype == GAMETYPE_TDM || gametype == GAMETYPE_CTF))
+		{
+			for(int t = 0; t < 2; t++)
+			{
+				gfx_blend_normal();
+				gfx_texture_set(-1);
+				gfx_quads_begin();
+				if(t == 0)
+					gfx_setcolor(1,0,0,0.25f);
+				else
+					gfx_setcolor(0,0,1,0.25f);
+				draw_round_rect(400-40, 300-40-15+t*20, 50, 18, 5.0f);
+				gfx_quads_end();
+
+				char buf[32];
+				sprintf(buf, "%d", gameobj->teamscore[t]);
+				float w = gfx_pretty_text_width(14, buf, -1);
+				
+				if(gametype == GAMETYPE_CTF)
+				{
+					gfx_pretty_text(400-20-w/2+5, 300-40-15+t*20+2, 14, buf, -1);
+					if(flags[t])
+					{
+ 						if(flags[t]->carried_by == -2 || (flags[t]->carried_by == -1 && ((client_tick()/10)&1)))
+ 						{
+							gfx_blend_normal();
+							gfx_texture_set(data->images[IMAGE_GAME].id);
+							gfx_quads_begin();
+
+							if(t == 0) select_sprite(SPRITE_FLAG_RED);
+							else select_sprite(SPRITE_FLAG_BLUE);
+							
+							float size = 16;					
+							gfx_quads_drawTL(400-40+5, 300-40-15+t*20+1, size/2, size);
+							gfx_quads_end();
+						}
+						else if(flags[t]->carried_by >= 0)
+						{
+							int id = flags[t]->carried_by%MAX_CLIENTS;
+							const char *name = client_datas[id].name;
+							float w = gfx_pretty_text_width(10, name, -1);
+							gfx_pretty_text(400-40-5-w, 300-40-15+t*20+2, 10, name, -1);
+							tee_render_info info = client_datas[id].skin_info;
+							info.size = 18.0f;
+							
+							render_tee(&idlestate, &info, EMOTE_NORMAL, vec2(1,0),
+								vec2(400-40+10, 300-40-15+9+t*20+1));
+						}
+					}
+				}
+				else
+					gfx_pretty_text(400-20-w/2, 300-40-15+t*20+2, 14, buf, -1);
+			}
+		}
+
+		// render warmup timer
+		if(gameobj->warmup)
+		{
+			char buf[256];
+			float w = gfx_pretty_text_width(24, "Warmup", -1);
+			gfx_pretty_text(200+-w/2, 50, 24, "Warmup", -1);
+
+			int seconds = gameobj->warmup/SERVER_TICK_SPEED;
+			if(seconds < 5)
+				sprintf(buf, "%d.%d", seconds, (gameobj->warmup*10/SERVER_TICK_SPEED)%10);
+			else
+				sprintf(buf, "%d", seconds);
+			w = gfx_pretty_text_width(24, buf, -1);
+			gfx_pretty_text(200+-w/2, 75, 24, buf, -1);
+		}
+	}
+
+	if (menu_active)
+	{
+		modmenu_render();
+		return;
+	}
+
+	if(chat_mode == CHATMODE_NONE && !menu_active && !spectate)
+	{
+		if(!emoticon_selector_active && inp_key_pressed(config.key_emoticon))
+		{
+			emoticon_selector_active = true;
+			emoticon_selector_reset();
+		}
+	}
+	else
+		emoticon_selector_active = false;
+
+	if(emoticon_selector_active)
+	{
+		int emoticon = emoticon_selector_render();
+		if (emoticon != -1)
+		{
+			send_emoticon(emoticon);
+			emoticon_selector_active = false;
+		}
+	}
+	
+	if(client_connection_problems())
+	{
+		gfx_mapscreen(0, 0, 400, 300);
+		const char *text = "Connection Problems...";
+		float w = gfx_pretty_text_width(24, text, -1);
+		gfx_pretty_text(200-w/2, 50, 24, text, -1);
+	}
+
+	// render score board
+	if(inp_key_pressed(KEY_TAB) || // user requested
+		(!spectate && (!local_character || local_character->health < 0)) || // not spectating and is dead
+		(gameobj && gameobj->game_over) // game over
+		)
+	{
+		gfx_mapscreen(0, 0, width, height);
+
+		float w = 550.0f;
+
+		if (gameobj && gameobj->gametype == GAMETYPE_DM)
+		{
+			render_scoreboard(width/2-w/2, 150.0f, w, -1, 0);
+			//render_scoreboard(gameobj, 0, 0, -1, 0);
+		}
+		else
+		{
+				
+			if(gameobj && gameobj->game_over)
+			{
+				const char *text = "DRAW!";
+				if(gameobj->teamscore[0] > gameobj->teamscore[1])
+					text = "Red Team Wins!";
+				else if(gameobj->teamscore[1] > gameobj->teamscore[0])
+					text = "Blue Team Wins!";
+					
+				float w = gfx_pretty_text_width(92.0f, text, -1);
+				gfx_pretty_text(width/2-w/2, 45, 92.0f, text, -1);
+			}
+			
+			render_scoreboard(width/2-w-20, 150.0f, w, 0, "Red Team");
+			render_scoreboard(width/2 + 20, 150.0f, w, 1, "Blue Team");
+		}
+
+		render_goals(width/2-w/2, 150+600+25, w);
+
+	}
+}
+
+extern "C" void modc_render()
+{
+	// this should be moved around abit
+	if(client_state() == CLIENTSTATE_ONLINE)
+	{
+		render_game();
+
+		// handle team switching
+		// TODO: FUGLY!!!
+		if(config.cl_team != -10)
+		{
+			msg_pack_start(MSG_SETTEAM, MSGFLAG_VITAL);
+			msg_pack_int(config.cl_team);
+			msg_pack_end();
+			client_send_msg();
+		}
+	}
+	else // if (client_state() != CLIENTSTATE_CONNECTING && client_state() != CLIENTSTATE_LOADING)
+	{
+		modmenu_render();
+	}
+
+	//
+	config.cl_team = -10;
+}
+
+
+void menu_do_disconnected();
+void menu_do_connecting();
+void menu_do_connected();
+
+extern "C" void modc_statechange(int state, int old)
+{
+	clear_object_pointers();
+	
+	if(state == CLIENTSTATE_OFFLINE)
+	{
+	 	menu_do_disconnected();
+	 	menu_game_active = false;
+	}
+	else if(state == CLIENTSTATE_CONNECTING)
+		menu_do_connecting();
+	else if (state == CLIENTSTATE_ONLINE)
+	{
+		menu_active = false;
+	 	menu_game_active = true;
+		menu_do_connected();
+	}
+}
+
+extern "C" void modc_message(int msg)
+{
+	if(msg == MSG_CHAT)
+	{
+		int cid = msg_unpack_int();
+		int team = msg_unpack_int();
+		const char *message = msg_unpack_string();
+		dbg_msg("message", "chat cid=%d team=%d msg='%s'", cid, team, message);
+		chat_add_line(cid, team, message);
+
+		if(cid >= 0)
+			snd_play(CHN_GUI, data->sounds[SOUND_CHAT_CLIENT].sounds[0].id, 0);
+		else
+			snd_play(CHN_GUI, data->sounds[SOUND_CHAT_SERVER].sounds[0].id, 0);
+	}
+	else if(msg == MSG_SETINFO)
+	{
+		int cid = msg_unpack_int();
+		const char *name = msg_unpack_string();
+		const char *skinname = msg_unpack_string();
+		
+		strncpy(client_datas[cid].name, name, 64);
+		strncpy(client_datas[cid].skin_name, skinname, 64);
+		
+		int use_custom_color = msg_unpack_int();
+		client_datas[cid].skin_info.color_body = skin_get_color(msg_unpack_int());
+		client_datas[cid].skin_info.color_feet = skin_get_color(msg_unpack_int());
+		client_datas[cid].skin_info.size = 64;
+		
+		// find new skin
+		client_datas[cid].skin_id = skin_find(client_datas[cid].skin_name);
+		if(client_datas[cid].skin_id < 0)
+			client_datas[cid].skin_id = 0;
+		
+		if(use_custom_color)
+			client_datas[cid].skin_info.texture = skin_get(client_datas[cid].skin_id)->color_texture;
+		else
+		{
+			client_datas[cid].skin_info.texture = skin_get(client_datas[cid].skin_id)->org_texture;
+			client_datas[cid].skin_info.color_body = vec4(1,1,1,1);
+			client_datas[cid].skin_info.color_feet = vec4(1,1,1,1);
+		}
+	}
+    else if(msg == MSG_WEAPON_PICKUP)
+    {
+        int weapon = msg_unpack_int();
+
+        picked_up_weapon = weapon+1;
+    }
+	else if(msg == MSG_READY_TO_ENTER)
+	{
+		client_entergame();
+	}
+	else if(msg == MSG_KILLMSG)
+	{
+		killmsg_current = (killmsg_current+1)%killmsg_max;
+		killmsgs[killmsg_current].killer = msg_unpack_int();
+		killmsgs[killmsg_current].victim = msg_unpack_int();
+		killmsgs[killmsg_current].weapon = msg_unpack_int();
+		killmsgs[killmsg_current].mode_special = msg_unpack_int();
+		killmsgs[killmsg_current].tick = client_tick();
+	}
+	else if (msg == MSG_EMOTICON)
+	{
+		int cid = msg_unpack_int();
+		int emoticon = msg_unpack_int();
+		client_datas[cid].emoticon = emoticon;
+		client_datas[cid].emoticon_start = client_tick();
+	}
+	else if(msg == MSG_SOUND_GLOBAL)
+	{
+		int soundid = msg_unpack_int();
+		snd_play_random(CHN_GLOBAL, soundid, 1.0f, vec2(0,0));
+	}
+}
+
+extern "C" void modc_connected()
+{
+	// init some stuff
+	col_init(32);
+	img_init();
+	tilemap_init();
+	chat_reset();
+
+	proj_particles.reset();
+	
+	clear_object_pointers();
+	last_new_predicted_tick = -1;
+
+	for(int i = 0; i < MAX_CLIENTS; i++)
+	{
+		client_datas[i].name[0] = 0;
+		client_datas[i].team = 0;
+		client_datas[i].emoticon = 0;
+		client_datas[i].emoticon_start = -1;
+	}
+
+	for(int i = 0; i < killmsg_max; i++)
+		killmsgs[i].tick = -100000;
+		
+	send_info(true);
+}
+
+extern "C" const char *modc_net_version() { return TEEWARS_NETVERSION; }