about summary refs log tree commit diff
path: root/src/game/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/game/client')
-rw-r--r--src/game/client/gc_client.cpp284
-rw-r--r--src/game/client/gc_client.h77
-rw-r--r--src/game/client/gc_console.cpp9
-rw-r--r--src/game/client/gc_effects.cpp162
-rw-r--r--src/game/client/gc_flow.cpp87
-rw-r--r--src/game/client/gc_hooks.cpp14
-rw-r--r--src/game/client/gc_menu.cpp9
-rw-r--r--src/game/client/gc_particles.cpp146
-rw-r--r--src/game/client/gc_render.cpp117
-rw-r--r--src/game/client/gc_render.h13
-rw-r--r--src/game/client/gc_render_map.cpp3
-rw-r--r--src/game/client/gc_render_obj.cpp36
12 files changed, 625 insertions, 332 deletions
diff --git a/src/game/client/gc_client.cpp b/src/game/client/gc_client.cpp
index 8df0e698..4cc8ba29 100644
--- a/src/game/client/gc_client.cpp
+++ b/src/game/client/gc_client.cpp
@@ -13,6 +13,7 @@ extern "C" {
 #include "../g_game.h"
 #include "../g_version.h"
 #include "../g_layers.h"
+#include "../g_math.h"
 #include "gc_map_image.h"
 #include "../generated/gc_data.h"
 #include "gc_menu.h"
@@ -23,6 +24,8 @@ extern "C" {
 #include "gc_anim.h"
 #include "gc_console.h"
 
+#include <GL/gl.h>
+
 struct data_container *data = 0;
 static int64 debug_firedelay = 0;
 
@@ -87,6 +90,15 @@ void snd_play_random(int chn, int setid, float vol, vec2 pos)
 	set->last = id;
 }
 
+
+void send_switch_team(int team)
+{
+	msg_pack_start(MSG_SETTEAM, MSGFLAG_VITAL);
+	msg_pack_int(team);
+	msg_pack_end();
+	client_send_msg();	
+}
+
 class damage_indicators
 {
 public:
@@ -179,159 +191,6 @@ void render_damage_indicators()
 	dmgind.render();
 }
 
-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;
-
-void reset_projectile_particles() // TODO: remove
-{
-	proj_particles.reset();
-}
-
 static char chat_input[512];
 static unsigned chat_input_len;
 static const int chat_max_lines = 10;
@@ -391,16 +250,7 @@ void chat_add_line(int client_id, int team, const char *line)
 killmsg killmsgs[killmsg_max];
 int killmsg_current = 0;
 
-void effect_air_jump(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));
-	}
-}
+//bool add_trail = false;
 
 extern int render_popup(const char *caption, const char *text, const char *button_text);
 
@@ -425,107 +275,22 @@ void process_events(int snaptype)
 		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);
-			}
+			effect_explosion(vec2(ev->x, ev->y));
 		}
 		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)
+		else if(item.type == EVENT_PLAYERSPAWN)
 		{
 			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);
-			}
+			effect_playerspawn(vec2(ev->x, ev->y));
 		}
 		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);
-			}
+			effect_playerdeath(vec2(ev->x, ev->y));
 		}
 		else if(item.type == EVENT_SOUND_WORLD)
 		{
@@ -964,6 +729,11 @@ static int do_input(int *v, int key)
 
 void render_game()
 {
+	// update the effects
+	effects_update();
+	particle_update(client_frametime());
+
+	
 	float width = 400*3.0f*gfx_screenaspect();
 	float height = 400*3.0f;
 
@@ -1195,12 +965,16 @@ void render_game()
 	}
 
 	// render the world
+	float zoom = 1.0f;
+	if(inp_key_pressed('E'))
+		zoom = 0.5f;
+	
 	gfx_clear(0.65f,0.78f,0.9f);
 	if(spectate)
-		render_world(mouse_pos.x, mouse_pos.y, 1.0f);
+		render_world(mouse_pos.x, mouse_pos.y, zoom);
 	else
 	{
-		render_world(local_character_pos.x+offx, local_character_pos.y+offy, 1.0f);
+		render_world(local_character_pos.x+offx, local_character_pos.y+offy, zoom);
 
 		// draw screen box
 		if(0)
@@ -1683,7 +1457,7 @@ void render_game()
 			vec2(local_character->x, local_character->y));
 		
 		char buf[512];
-		sprintf(buf, "%.2f", speed);
+		sprintf(buf, "%.2f", speed/2);
 		gfx_text(0, 150, 50, 12, buf, -1);
 	}
 
diff --git a/src/game/client/gc_client.h b/src/game/client/gc_client.h
index f1ff02b1..def3cad8 100644
--- a/src/game/client/gc_client.h
+++ b/src/game/client/gc_client.h
@@ -83,12 +83,89 @@ const int killmsg_max = 5;
 extern killmsg killmsgs[killmsg_max];
 extern int killmsg_current;
 
+//
+void send_switch_team(int team);
+
 // various helpers
 void snd_play_random(int chn, int setid, float vol, vec2 pos);
 void process_events(int snaptype);
 void clear_object_pointers();
 void reset_projectile_particles();
 void send_info(bool start);
+inline vec2 random_dir() { return normalize(vec2(frandom()-0.5f, frandom()-0.5f)); }
+
+
+// effects
+void effects_update();
 
+void effect_bullettrail(vec2 pos);
+void effect_smoketrail(vec2 pos, vec2 vel);
+void effect_explosion(vec2 pos);
 void effect_air_jump(vec2 pos);
 void effect_damage_indicator(vec2 pos, vec2 dir);
+void effect_playerspawn(vec2 pos);
+void effect_playerdeath(vec2 pos);
+
+// particles
+struct particle
+{
+	void set_default()
+	{
+		vel = vec2(0,0);
+		life_span = 0;
+		start_size = 32;
+		end_size = 32;
+		rot = 0;
+		rotspeed = 0;
+		gravity = 0;
+		friction = 0;
+		flow_affected = 1.0f;
+		color = vec4(1,1,1,1);
+	}
+	
+	vec2 pos;
+	vec2 vel;
+
+	int spr;
+
+	float flow_affected;
+
+	float life_span;
+	
+	float start_size;
+	float end_size;
+
+	float rot;
+	float rotspeed;
+
+	float gravity;
+	float friction;
+
+	vec4 color;
+	
+	// set by the particle system
+	float life;
+	int prev_part;
+	int next_part;
+};
+
+enum
+{
+	PARTGROUP_PROJECTILE_TRAIL=0,
+	PARTGROUP_EXPLOSIONS,
+	PARTGROUP_GENERAL,
+	NUM_PARTGROUPS
+};
+
+void particle_add(int group, particle *part);
+void particle_render(int group);
+void particle_update(float time_passed);
+void particle_reset();
+
+// flow grid
+vec2 flow_get(vec2 pos);
+void flow_add(vec2 pos, vec2 vel, float size);
+void flow_dbg_render();
+void flow_init();
+void flow_update();
+
diff --git a/src/game/client/gc_console.cpp b/src/game/client/gc_console.cpp
index 9cf417e2..0a4bb3d6 100644
--- a/src/game/client/gc_console.cpp
+++ b/src/game/client/gc_console.cpp
@@ -12,6 +12,7 @@ extern "C" {
 #include <cstdio>
 
 #include "gc_ui.h"
+#include "gc_client.h"
 
 static unsigned int console_input_len = 0;
 static char console_input[256] = {0};
@@ -57,12 +58,20 @@ static void quit_command(struct lexer_result *result, void *user_data)
 	client_quit();
 }
 
+static void con_team(struct lexer_result *result, void *user_data)
+{
+	int new_team;
+	extract_result_int(result, 1, &new_team);
+	send_switch_team(new_team);
+}
+
 void client_console_init()
 {
 	console_register_print_callback(client_console_print);
 	MACRO_REGISTER_COMMAND("quit", "", quit_command, 0x0);
 	MACRO_REGISTER_COMMAND("connect", "s", connect_command, 0x0);
 	MACRO_REGISTER_COMMAND("disconnect", "", disconnect_command, 0x0);
+	MACRO_REGISTER_COMMAND("team", "i", con_team, 0x0);
 }
 
 void console_handle_input()
diff --git a/src/game/client/gc_effects.cpp b/src/game/client/gc_effects.cpp
new file mode 100644
index 00000000..6394075e
--- /dev/null
+++ b/src/game/client/gc_effects.cpp
@@ -0,0 +1,162 @@
+#include <engine/e_client_interface.h>
+#include "gc_client.h"
+#include "../generated/gc_data.h"
+
+static bool add_trail = false;
+
+void effect_air_jump(vec2 pos)
+{
+	particle p;
+	p.set_default();
+	p.spr = SPRITE_PART_AIRJUMP;
+	p.pos = pos + vec2(-6.0f, 16.0f);
+	p.vel = vec2(0, -200);
+	p.life_span = 0.5f;
+	p.start_size = 48.0f;
+	p.end_size = 0;
+	p.rot = frandom()*pi*2;
+	p.rotspeed = pi*2;
+	p.gravity = 500;
+	p.friction = 0.7f;
+	p.flow_affected = 0.0f;
+	particle_add(PARTGROUP_GENERAL, &p);
+
+	p.pos = pos + vec2(6.0f, 16.0f);
+	particle_add(PARTGROUP_GENERAL, &p);
+}
+
+void effect_smoketrail(vec2 pos, vec2 vel)
+{
+	if(!add_trail)
+		return;
+		
+	particle p;
+	p.set_default();
+	p.spr = SPRITE_PART_SMOKE;
+	p.pos = pos;
+	p.vel = vel + random_dir()*50.0f;
+	p.life_span = 0.5f + frandom()*0.5f;
+	p.start_size = 12.0f + frandom()*8;
+	p.end_size = 0;
+	p.friction = 0.7;
+	p.gravity = frandom()*-500.0f;
+	particle_add(PARTGROUP_PROJECTILE_TRAIL, &p);
+}
+
+
+void effect_bullettrail(vec2 pos)
+{
+	if(!add_trail)
+		return;
+		
+	particle p;
+	p.set_default();
+	p.spr = SPRITE_PART_BALL;
+	p.pos = pos;
+	p.life_span = 0.25f + frandom()*0.25f;
+	p.start_size = 8.0f;
+	p.end_size = 0;
+	p.friction = 0.7;
+	particle_add(PARTGROUP_PROJECTILE_TRAIL, &p);
+}
+
+void effect_playerspawn(vec2 pos)
+{
+	for(int i = 0; i < 32; i++)
+	{
+		particle p;
+		p.set_default();
+		p.spr = SPRITE_PART_SHELL;
+		p.pos = pos;
+		p.vel = random_dir() * (pow(frandom(), 3)*600.0f);
+		p.life_span = 0.3f + frandom()*0.3f;
+		p.start_size = 64.0f + frandom()*32;
+		p.end_size = 0;
+		p.rot = frandom()*pi*2;
+		p.rotspeed = frandom();
+		p.gravity = frandom()*-400.0f;
+		p.friction = 0.7f;
+		p.color = vec4(0xb5/255.0f, 0x50/255.0f, 0xcb/255.0f, 1.0f);
+		particle_add(PARTGROUP_GENERAL, &p);
+		
+	}
+}
+
+void effect_playerdeath(vec2 pos)
+{
+	for(int i = 0; i < 64; i++)
+	{
+		particle p;
+		p.set_default();
+		p.spr = SPRITE_PART_SPLAT01 + (rand()%3);
+		p.pos = pos;
+		p.vel = random_dir() * ((frandom()+0.1f)*900.0f);
+		p.life_span = 0.3f + frandom()*0.3f;
+		p.start_size = 24.0f + frandom()*16;
+		p.end_size = 0;
+		p.rot = frandom()*pi*2;
+		p.rotspeed = (frandom()-0.5f) * pi;
+		p.gravity = 800.0f;
+		p.friction = 0.8f;
+		p.color = mix(vec4(0.75f,0.2f,0.2f,0.75f), vec4(0.5f,0.1f,0.1f,0.75f), frandom());
+		particle_add(PARTGROUP_GENERAL, &p);
+		
+	}
+}
+
+
+void effect_explosion(vec2 pos)
+{
+	// add to flow
+	for(int y = -8; y <= 8; y++)
+		for(int x = -8; x <= 8; x++)
+		{
+			if(x == 0 && y == 0)
+				continue;
+			
+			float a = 1 - (length(vec2(x,y)) / length(vec2(8,8)));
+			flow_add(pos+vec2(x,y)*16, normalize(vec2(x,y))*5000.0f*a, 10.0f);
+		}
+		
+	// add the explosion
+	particle p;
+	p.set_default();
+	p.spr = SPRITE_PART_EXPL01;
+	p.pos = pos;
+	p.life_span = 0.4f;
+	p.start_size = 150.0f;
+	p.end_size = 0;
+	p.rot = frandom()*pi*2;
+	particle_add(PARTGROUP_EXPLOSIONS, &p);
+	
+	// add the smoke
+	for(int i = 0; i < 24; i++)
+	{
+		particle p;
+		p.set_default();
+		p.spr = SPRITE_PART_SMOKE;
+		p.pos = pos;
+		p.vel = random_dir() * ((1.0f + frandom()*0.2f) * 1000.0f);
+		p.life_span = 0.5f + frandom()*0.4f;
+		p.start_size = 32.0f + frandom()*8;
+		p.end_size = 0;
+		p.gravity = frandom()*-800.0f;
+		p.friction = 0.4f;
+		p.color = mix(vec4(0.75f,0.75f,0.75f,1.0f), vec4(0.5f,0.5f,0.5f,1.0f), frandom());
+		particle_add(PARTGROUP_GENERAL, &p);
+	}
+}
+
+void effects_update()
+{
+	static float last_update = 0;
+	if(client_localtime()-last_update > 0.02f)
+	{
+		add_trail = true;
+		last_update = client_localtime();
+		flow_update();
+	}
+	else
+		add_trail = false;
+		
+}
diff --git a/src/game/client/gc_flow.cpp b/src/game/client/gc_flow.cpp
new file mode 100644
index 00000000..aac35058
--- /dev/null
+++ b/src/game/client/gc_flow.cpp
@@ -0,0 +1,87 @@
+#include <engine/e_client_interface.h>
+#include <engine/e_config.h>
+#include "gc_client.h"
+#include "../g_layers.h"
+
+struct FLOWCELL
+{
+	vec2 vel;
+};
+
+static FLOWCELL *cells = 0;
+static int height = 0;
+static int width = 0;
+static int spacing = 16;
+
+void flow_init()
+{
+	if(cells)
+	{
+		mem_free(cells);
+		cells = 0;
+	}
+	
+	MAPITEM_LAYER_TILEMAP *tilemap = layers_game_layer();
+	width = tilemap->width*32/spacing;
+	height = tilemap->height*32/spacing;
+
+	// allocate and clear	
+	cells = (FLOWCELL *)mem_alloc(sizeof(FLOWCELL)*width*height, 1);
+	for(int y = 0; y < height; y++)
+		for(int x = 0; x < width; x++)
+			cells[y*width+x].vel = vec2(0.0f, 0.0f);
+}
+
+void flow_update()
+{
+	if(!config.cl_flow)
+		return;
+		
+	for(int y = 0; y < height; y++)
+		for(int x = 0; x < width; x++)
+			cells[y*width+x].vel *= 0.85f;
+}
+
+void flow_dbg_render()
+{
+	if(!config.cl_flow)
+		return;
+
+	gfx_texture_set(-1);
+	gfx_lines_begin();
+	for(int y = 0; y < height; y++)
+		for(int x = 0; x < width; x++)
+		{
+			vec2 pos(x*spacing, y*spacing);
+			vec2 vel = cells[y*width+x].vel * 0.01f;
+			gfx_lines_draw(pos.x, pos.y, pos.x+vel.x, pos.y+vel.y);
+		}
+		
+	gfx_lines_end();
+}
+
+void flow_add(vec2 pos, vec2 vel, float size)
+{
+	if(!config.cl_flow)
+		return;
+		
+	int x = (int)(pos.x / spacing);
+	int y = (int)(pos.y / spacing);
+	if(x < 0 || y < 0 || x >= width || y >= height)
+		return;
+	
+	cells[y*width+x].vel += vel;
+}
+
+vec2 flow_get(vec2 pos)
+{
+	if(!config.cl_flow)
+		return vec2(0,0);
+	
+	int x = (int)(pos.x / spacing);
+	int y = (int)(pos.y / spacing);
+	if(x < 0 || y < 0 || x >= width || y >= height)
+		return vec2(0,0);
+	
+	return cells[y*width+x].vel;
+}
diff --git a/src/game/client/gc_hooks.cpp b/src/game/client/gc_hooks.cpp
index ca425059..c16923b8 100644
--- a/src/game/client/gc_hooks.cpp
+++ b/src/game/client/gc_hooks.cpp
@@ -40,6 +40,7 @@ extern "C" void modc_init()
 	
 	gfx_text_set_default_font(&default_font);
 
+	particle_reset();
 	menu_init();
 	
 	// setup sound channels
@@ -300,13 +301,11 @@ extern "C" void modc_render()
 
 		// 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)
 	{
@@ -317,7 +316,7 @@ extern "C" void modc_render()
 	}
 
 	//
-	config.cl_team = -10;
+	//config.cl_team = -10;
 }
 
 
@@ -492,11 +491,12 @@ extern "C" void modc_connected()
 	layers_init();
 	col_init();
 	img_init();
+	flow_init();
 	
 	//tilemap_init();
 	chat_reset();
 
-	reset_projectile_particles();
+	particle_reset();
 	
 	clear_object_pointers();
 	last_new_predicted_tick = -1;
diff --git a/src/game/client/gc_menu.cpp b/src/game/client/gc_menu.cpp
index 9b72d4b5..6b982c37 100644
--- a/src/game/client/gc_menu.cpp
+++ b/src/game/client/gc_menu.cpp
@@ -22,6 +22,7 @@ extern "C" {
 #include "gc_anim.h"
 #include "gc_skin.h"
 #include "gc_ui.h"
+#include "gc_client.h"
 #include <mastersrv/mastersrv.h>
 
 extern data_container *data;
@@ -1623,7 +1624,7 @@ static void menu2_render_game(RECT main_view)
 			static int spectate_button = 0;
 			if(ui_do_button(&spectate_button, "Spectate", 0, &button, ui_draw_menu_button, 0))
 			{
-				config.cl_team = -1;
+				send_switch_team(-1);
 				menu_active = false;
 			}
 		}
@@ -1637,7 +1638,7 @@ static void menu2_render_game(RECT main_view)
 				static int spectate_button = 0;
 				if(ui_do_button(&spectate_button, "Join Game", 0, &button, ui_draw_menu_button, 0))
 				{
-					config.cl_team = 0;
+					send_switch_team(0);
 					menu_active = false;
 				}
 			}						
@@ -1651,7 +1652,7 @@ static void menu2_render_game(RECT main_view)
 				static int spectate_button = 0;
 				if(ui_do_button(&spectate_button, "Join Red", 0, &button, ui_draw_menu_button, 0))
 				{
-					config.cl_team = 0;
+					send_switch_team(0);
 					menu_active = false;
 				}
 			}
@@ -1663,7 +1664,7 @@ static void menu2_render_game(RECT main_view)
 				static int spectate_button = 0;
 				if(ui_do_button(&spectate_button, "Join Blue", 0, &button, ui_draw_menu_button, 0))
 				{
-					config.cl_team = 1;
+					send_switch_team(1);
 					menu_active = false;
 				}
 			}
diff --git a/src/game/client/gc_particles.cpp b/src/game/client/gc_particles.cpp
new file mode 100644
index 00000000..bdeb26fd
--- /dev/null
+++ b/src/game/client/gc_particles.cpp
@@ -0,0 +1,146 @@
+#include <engine/e_client_interface.h>
+#include "gc_client.h"
+#include "../generated/gc_data.h"
+
+// NOTE: the way the particle system works isn't very cache friendly
+
+enum
+{
+	MAX_PARTICLES=1024*8,
+};
+
+static particle particles[MAX_PARTICLES];
+static int first_free = -1;
+static int first_part[NUM_PARTGROUPS] = {-1};
+
+void particle_reset()
+{
+	// reset particles
+	for(int i = 0; i < MAX_PARTICLES; i++)
+	{
+		particles[i].prev_part = i-1;
+		particles[i].next_part = i+1;
+	}
+	
+	particles[0].prev_part = 0;
+	particles[MAX_PARTICLES-1].next_part = -1;
+	first_free = 0;
+
+	for(int i = 0; i < NUM_PARTGROUPS; i++)
+		first_part[i] = -1;
+}
+
+
+void particle_add(int group, particle *part)
+{
+	if (first_free == -1)
+		return;
+		
+	// remove from the free list
+	int id = first_free;
+	first_free = particles[id].next_part;
+	particles[first_free].prev_part = -1;
+	
+	// copy data
+	particles[id] = *part;
+	
+	// insert to the group list
+	particles[id].prev_part = -1;
+	particles[id].next_part = first_part[group];
+	if(first_part[group] != -1)
+		particles[first_part[group]].prev_part = id;
+	first_part[group] = id;
+	
+	// set some parameters
+	particles[id].life = 0;
+}
+
+void particle_update(float time_passed)
+{
+	static float friction_fraction = 0;
+	friction_fraction += time_passed;
+
+	if(friction_fraction > 2.0f) // safty messure
+		friction_fraction = 0;
+	
+	int friction_count = 0;
+	while(friction_fraction > 0.05f)
+	{
+		friction_count++;
+		friction_fraction -= 0.05f;
+	}
+	
+	for(int g = 0; g < NUM_PARTGROUPS; g++)
+	{
+		int i = first_part[g];
+		while(i != -1)
+		{
+			int next = particles[i].next_part;
+			particles[i].vel += flow_get(particles[i].pos)*time_passed * particles[i].flow_affected;
+			particles[i].vel.y += particles[i].gravity*time_passed;
+			
+			for(int f = 0; f < friction_count; f++) // apply friction
+				particles[i].vel *= particles[i].friction;
+			
+			// move the point
+			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].life_span)
+			{
+				// remove it from the group list
+				if(particles[i].prev_part != -1)
+					particles[particles[i].prev_part].next_part = particles[i].next_part;
+				else
+					first_part[g] = particles[i].next_part;
+					
+				if(particles[i].next_part != -1)
+					particles[particles[i].next_part].prev_part = particles[i].prev_part;
+					
+				// insert to the free list
+				if(first_free != -1)
+					particles[first_free].prev_part = i;
+				particles[i].prev_part = -1;
+				particles[i].next_part = first_free;
+			}
+			
+			i = next;
+		}
+	}
+}
+
+void particle_render(int group)
+{
+	gfx_blend_normal();
+	//gfx_blend_additive();
+	gfx_texture_set(data->images[IMAGE_PARTICLES].id);
+	gfx_quads_begin();
+
+	int i = first_part[group];
+	while(i != -1)
+	{
+		select_sprite(particles[i].spr);
+		float a = particles[i].life / particles[i].life_span;
+		vec2 p = particles[i].pos;
+		float size = mix(particles[i].start_size, particles[i].end_size, a);
+
+		gfx_quads_setrotation(particles[i].rot);
+
+		gfx_setcolor(
+			particles[i].color.r,
+			particles[i].color.g,
+			particles[i].color.b,
+			particles[i].color.a); // pow(a, 0.75f) * 
+
+		gfx_quads_draw(p.x, p.y, size, size);
+		
+		i = particles[i].next_part;
+	}
+	gfx_quads_end();
+	gfx_blend_normal();
+}
diff --git a/src/game/client/gc_render.cpp b/src/game/client/gc_render.cpp
index 91ae5662..6297fc9c 100644
--- a/src/game/client/gc_render.cpp
+++ b/src/game/client/gc_render.cpp
@@ -197,7 +197,7 @@ void render_tee(animstate *anim, tee_render_info *info, int emote, vec2 dir, vec
 
 			// 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);
+			select_sprite((outline||!info->got_airjump)?SPRITE_TEE_FOOT_OUTLINE:SPRITE_TEE_FOOT, 0, 0, 0);
 
 			keyframe *foot = f ? &anim->front_foot : &anim->back_foot;
 
@@ -278,7 +278,7 @@ void render_layers(float center_x, float center_y, int pass)
 			bool render = false;
 			bool is_game_layer = false;
 			
-			if(layer == (MAPITEM_LAYER*)layers_game())
+			if(layer == (MAPITEM_LAYER*)layers_game_layer())
 			{
 				is_game_layer = true;
 				passed_gamelayer = 1;
@@ -306,7 +306,7 @@ void render_layers(float center_x, float center_y, int pass)
 					else
 						gfx_texture_set(img_get(tmap->image));
 					TILE *tiles = (TILE *)map_get_data(tmap->data);
-					render_tilemap(tiles, tmap->width, tmap->height, 32.0f, 1);
+					render_tilemap(tiles, tmap->width, tmap->height, 32.0f, vec4(1,1,1,1), 1);
 				}
 				else if(layer->type == LAYERTYPE_QUADS)
 				{
@@ -323,71 +323,82 @@ void render_layers(float center_x, float center_y, int pass)
 	}
 }
 
-// renders the complete game world
-void render_world(float center_x, float center_y, float zoom)
+static void render_items()
 {
-	// render background layers
-	render_layers(center_x, center_y, 0);
-
-	// render items
+	int num = snap_num_items(SNAP_CURRENT);
+	for(int i = 0; i < num; i++)
 	{
-		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);
+		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 *)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);
-			}
+		if(item.type == OBJTYPE_PROJECTILE)
+		{
+			render_projectile((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
+
+static void render_players()
+{
+	int num = snap_num_items(SNAP_CURRENT);
+	for(int i = 0; i < num; i++)
 	{
-		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)
 		{
-			SNAP_ITEM item;
-			const void *data = snap_get_item(SNAP_CURRENT, i, &item);
+			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(item.type == OBJTYPE_PLAYER_CHARACTER)
+			if(prev && prev_info && info)
 			{
-				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)
-				{
-					render_player(
-							(const obj_player_character *)prev,
-							(const obj_player_character *)data,
-							(const obj_player_info *)prev_info,
-							(const obj_player_info *)info
-						);
-				}
+				render_player(
+						(const obj_player_character *)prev,
+						(const obj_player_character *)data,
+						(const obj_player_info *)prev_info,
+						(const obj_player_info *)info
+					);
 			}
 		}
 	}
+}
+
+// renders the complete game world
+void render_world(float center_x, float center_y, float zoom)
+{
+	// render background layers
+	render_layers(center_x, center_y, 0);
+
+	// render trails
+	particle_render(PARTGROUP_PROJECTILE_TRAIL);
+
+	// render items
+	render_items();
+
+	// render players above all
+	render_players();
 
 	// render particles
-	//temp_system.update(client_frametime());
-	//temp_system.render();
+	particle_render(PARTGROUP_EXPLOSIONS);
+	particle_render(PARTGROUP_GENERAL);
+	
+	if(config.dbg_flow)
+		flow_dbg_render();
 
 	// render foreground layers
 	render_layers(center_x, center_y, 1);
diff --git a/src/game/client/gc_render.h b/src/game/client/gc_render.h
index 5294b89e..d7adeada 100644
--- a/src/game/client/gc_render.h
+++ b/src/game/client/gc_render.h
@@ -8,10 +8,20 @@
 
 struct tee_render_info
 {
+	tee_render_info()
+	{
+		texture = -1;
+		color_body = vec4(1,1,1,1);
+		color_feet = vec4(1,1,1,1);
+		size = 1.0f;
+		got_airjump = 1;
+	};
+	
 	int texture;
 	vec4 color_body;
 	vec4 color_feet;
 	float size;
+	int got_airjump;
 };
 
 // sprite renderings
@@ -40,6 +50,7 @@ void render_world(float center_x, float center_y, float zoom);
 void render_loading(float percent);
 
 void render_damage_indicators();
+void render_particles();
 
 // object render methods (gc_render_obj.cpp)
 void render_tee(class animstate *anim, tee_render_info *info, int emote, vec2 dir, vec2 pos);
@@ -53,7 +64,7 @@ void render_player(
 // map render methods (gc_render_map.cpp)
 void render_eval_envelope(ENVPOINT *points, int num_points, int channels, float time, float *result);
 void render_quads(QUAD *quads, int num_quads, void (*eval)(float time_offset, int env, float *channels));
-void render_tilemap(TILE *tiles, int w, int h, float scale, int flags);
+void render_tilemap(TILE *tiles, int w, int h, float scale, vec4 color, int flags);
 
 // helpers
 void mapscreen_to_world(float center_x, float center_y, float parallax_x, float parallax_y,
diff --git a/src/game/client/gc_render_map.cpp b/src/game/client/gc_render_map.cpp
index ec96f583..4728f771 100644
--- a/src/game/client/gc_render_map.cpp
+++ b/src/game/client/gc_render_map.cpp
@@ -148,7 +148,7 @@ void render_quads(QUAD *quads, int num_quads, void (*eval)(float time_offset, in
 }
 
 
-void render_tilemap(TILE *tiles, int w, int h, float scale, int flags)
+void render_tilemap(TILE *tiles, int w, int h, float scale, vec4 color, int flags)
 {
 			//gfx_texture_set(img_get(tmap->image));
 	float screen_x0, screen_y0, screen_x1, screen_y1;
@@ -160,6 +160,7 @@ void render_tilemap(TILE *tiles, int w, int h, float scale, int flags)
 	float final_tilesize_scale = final_tilesize/tile_pixelsize;
 	
 	gfx_quads_begin();
+	gfx_setcolor(color.r, color.g, color.b, color.a);
 	
 	int starty = (int)(screen_y0/scale)-1;
 	int startx = (int)(screen_x0/scale)-1;
diff --git a/src/game/client/gc_render_obj.cpp b/src/game/client/gc_render_obj.cpp
index bdb4cfac..3ee1704d 100644
--- a/src/game/client/gc_render_obj.cpp
+++ b/src/game/client/gc_render_obj.cpp
@@ -1,5 +1,6 @@
 /* copyright (c) 2007 magnus auvinen, see licence.txt for more info */
 #include <math.h>
+#include <stdio.h>
 #include <engine/e_client_interface.h>
 #include <engine/e_config.h>
 #include "../generated/gc_data.h"
@@ -27,8 +28,6 @@ void render_projectile(const obj_projectile *current, int itemid)
 	float gravity = -400;
 	if(current->type != WEAPON_ROCKET)
 		gravity = -100;
-	if(current->type == WEAPON_BOMB)
-		gravity = 0;
 
 	float ct = (client_tick()-current->start_tick)/(float)SERVER_TICK_SPEED + client_ticktime()*1/(float)SERVER_TICK_SPEED;
 	vec2 startpos(current->x, current->y);
@@ -39,17 +38,26 @@ void render_projectile(const obj_projectile *current, int itemid)
 	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));
+	if(current->type == WEAPON_ROCKET)
+	{
+		effect_smoketrail(pos, vel*-1);
+		flow_add(pos, vel*1000*client_frametime(), 10.0f);
+		gfx_quads_setrotation(client_localtime()*pi*2*2 + itemid);
+	}
 	else
-		gfx_quads_setrotation(0);
+	{
+		effect_bullettrail(pos);
+		flow_add(pos, vel*1000*client_frametime(), 10.0f);
 
-	// TODO: do this, but nice
-	//temp_system.new_particle(pos, vec2(0,0), 0.3f, 14.0f, 0, 0.95f);
+		if(length(vel) > 0.00001f)
+			gfx_quads_setrotation(get_angle(vel));
+		else
+			gfx_quads_setrotation(0);
+
+	}
 
 	gfx_quads_draw(pos.x, pos.y, 32, 32);
 	gfx_quads_setrotation(0);
@@ -185,6 +193,7 @@ void render_player(
 	player = *player_char;
 
 	obj_player_info info = *player_info;
+	tee_render_info render_info = client_datas[info.clientid].render_info;
 
 	float intratick = client_intratick();
 	float ticktime = client_ticktime();
@@ -209,6 +218,11 @@ void render_player(
 	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);
+	vec2 vel = vec2(player.x, player.y)-vec2(prev.x, prev.y);
+	
+	flow_add(position, vel*100.0f, 10.0f);
+	
+	render_info.got_airjump = player.jumped&2?0:1;
 
 	if(prev.health < 0) // Don't flicker from previous position
 		position = vec2(player.x, player.y);
@@ -411,14 +425,14 @@ void render_player(
 	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].render_info;
+		tee_render_info ghost = render_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].render_info, player.emote, direction, position);
+	render_tee(&state, &render_info, player.emote, direction, position);
 
 	if(player.state == STATE_CHATTING)
 	{