about summary refs log tree commit diff
diff options
context:
space:
mode:
authorMagnus Auvinen <magnus.auvinen@gmail.com>2008-01-29 21:39:41 +0000
committerMagnus Auvinen <magnus.auvinen@gmail.com>2008-01-29 21:39:41 +0000
commit7bc733dc10f3d01985021b7b5d6ae140dd5af6f1 (patch)
treec9b0fcd8d128ec9abd40c10dfe4fcf245650a870
parent0dab7db963e2706182ea120c98f746f5e265c14c (diff)
downloadzcatch-7bc733dc10f3d01985021b7b5d6ae140dd5af6f1.tar.gz
zcatch-7bc733dc10f3d01985021b7b5d6ae140dd5af6f1.zip
large update. cleaned up some code. added new effects for almost everything
-rw-r--r--data/particles.pngbin0 -> 31096 bytes
-rw-r--r--datasrc/client.dts18
-rw-r--r--datasrc/teewars.ds143
-rw-r--r--src/engine/client/ec_gfx.c15
-rw-r--r--src/engine/e_system.h3
-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
-rw-r--r--src/game/editor/ed_editor.hpp5
-rw-r--r--src/game/editor/ed_layer_tiles.cpp2
-rw-r--r--src/game/g_collision.cpp6
-rw-r--r--src/game/g_game.cpp6
-rw-r--r--src/game/g_layers.cpp25
-rw-r--r--src/game/g_layers.h3
-rw-r--r--src/game/g_protocol.h11
-rw-r--r--src/game/g_variables.h7
-rw-r--r--src/game/server/gs_common.h2
-rw-r--r--src/game/server/gs_server.cpp64
27 files changed, 715 insertions, 552 deletions
diff --git a/data/particles.png b/data/particles.png
new file mode 100644
index 00000000..94d0d166
--- /dev/null
+++ b/data/particles.png
Binary files differdiff --git a/datasrc/client.dts b/datasrc/client.dts
index eec61fa4..edded536 100644
--- a/datasrc/client.dts
+++ b/datasrc/client.dts
@@ -27,25 +27,10 @@ struct soundset {
 	array:sound sounds = *
 }
 
-struct particleinfo {
-	ptr:sprite spr = sprite@1
-	float color_r = color@1
-	float color_g = color@2
-	float color_b = color@3
-	float color_a = color@4
-	int lifemod = life@1
-}
-
 struct spriteptr {
 	ptr:sprite psprite = @0
 }
 
-struct projectileparticles {
-	int particlespersecond = particlespersecond@1
-	float particlesize = particlesize@1
-	float particlelife = particlelife@1
-}
-
 struct weapon {
 	ptr:sprite sprite_body = sprite_body@1
 	ptr:sprite sprite_cursor = sprite_cursor@1
@@ -85,12 +70,9 @@ struct data_container {
 	array:spriteset spritesets = sprites.*
 	array:sprite sprites = sprites.*.*
 	array:weapon weapons = weapons.*
-	array:particleinfo particles = particles.*
-	array:projectileparticles projectileinfo = projectileparticles.*
 
 	array:soundset sounds = sounds.*
 	array:animation animations = animations.*
-
 }
 
 const array:int weapon = weapons.*
diff --git a/datasrc/teewars.ds b/datasrc/teewars.ds
index 1265dde0..d9e250a9 100644
--- a/datasrc/teewars.ds
+++ b/datasrc/teewars.ds
@@ -223,6 +223,10 @@ images {
 		filename "data/game.png"
 	}
 
+	particles {
+		filename "data/particles.png"
+	}
+
 	cursor {
 		filename "data/gui_cursor.png"
 	}
@@ -240,62 +244,6 @@ images {
 	}
 }
 
-particles {
-	part1 {
-		sprite sprites.game.part1
-		color 0.7 0.7 0.7 1.0
-		life 50
-	}
-
-	part2 {
-		sprite sprites.game.part2
-		color 1.0 1.0 1.0 1.0
-		life 50
-	}
-
-	part3 {
-		sprite sprites.game.part3
-		color 0.8 0.8 0.8 1.0
-		life 50
-	}
-
-	part4 {
-		sprite sprites.game.part4
-		color 0.98 0.1 0.16 1.0
-		life 70
-	}
-
-	part5 {
-		sprite sprites.game.part5
-		color 1.0 1.0 1.0 1.0
-		life 70
-	}
-
-	part6 {
-		sprite sprites.game.part6
-		color 0.6 0.6 0.6 1.0
-		life 100
-	}
-
-	part7 {
-		sprite sprites.game.part7
-		color 1.0 1.0 1.0 1.0
-		life 100
-	}
-	
-	part8 {
-		sprite sprites.game.part8
-		color 0.7 0.7 0.7 1.0
-		life 150
-	}
-	
-	part9 {
-		sprite sprites.game.part9
-		color 1.0 1.0 1.0 1.0
-		life 40
-	}
-}
-
 powerups {
 	health {
 		amount 1
@@ -319,44 +267,6 @@ powerups {
 	}
 }
 
-projectileparticles {
-	gun {
-		particlespersecond 200
-		particlesize 16.0
-		particlelife 0.5
-	}
-	rocket {
-		particlespersecond 100
-		particlesize 22.0
-		particlelife 1.3
-	}
-	shotgun {
-		particlespersecond 100
-		particlesize 16.0
-		particlelife 0.8
-	}
-	empty1 {
-		particlespersecond 300
-		particlesize 8.0
-		particlelife 0.8
-	}
-	empty2 {
-		particlespersecond 300
-		particlesize 8.0
-		particlelife 0.8
-	}
-	empty3 {
-		particlespersecond 300
-		particlesize 8.0
-		particlelife 0.8
-	}
-	sniper {
-		particlespersecond 300
-		particlesize 8.0
-		particlelife 0.8
-	}
-}
-
 weapons {
 	hammer {
 		sprite_body sprites.game.weapon_hammer_body
@@ -507,37 +417,6 @@ weapons {
 		overchargetime 10.0
 	}
 
-	bomb {
-		sprite_body sprites.game.weapon_rocket_body
-		sprite_cursor sprites.game.weapon_rocket_cursor
-		sprite_proj sprites.game.weapon_rocket_proj
-		sprite_muzzles {
-			sprites.game.weapon_shotgun_muzzle1
-			sprites.game.weapon_shotgun_muzzle2
-			sprites.game.weapon_shotgun_muzzle3
-		}
-		
-		nummuzzlesprites 3
-		muzzleoffsetx 0.0
-		muzzleoffsety 0.0
-		maxammo 10
-		costammo 1
-		recoil 10
-		firedelay 800
-		muzzleduration 0
-		visual_size 128
-		offsetx 24.0
-		offsety -2.0
-		meleedamage 0
-		meleereach 0
-		ammoregentime 0
-		duration -1
-		movetime 0
-		velocity 0
-		chargetime 1.0
-		overchargetime 10.0
-	}
-
 	ninja {
 		sprite_body sprites.game.weapon_ninja_body
 		sprite_cursor sprites.game.weapon_ninja_cursor
@@ -572,6 +451,20 @@ weapons {
 }
 	
 sprites {
+
+	particles images.particles 8 8 {
+		part_slice 0 0 1 1
+		part_ball 1 0 1 1
+		part_splat01 2 0 1 1
+		part_splat02 3 0 1 1
+		part_splat03 4 0 1 1
+
+		part_smoke 0 1 1 1
+		part_shell 0 2 2 2
+		part_expl01 0 4 4 4
+		part_airjump 2 2 2 2
+	}
+
 	game images.game 32 16 {
 	
 		health_full 21 0 2 2
diff --git a/src/engine/client/ec_gfx.c b/src/engine/client/ec_gfx.c
index 101b0279..1e4378d2 100644
--- a/src/engine/client/ec_gfx.c
+++ b/src/engine/client/ec_gfx.c
@@ -163,7 +163,7 @@ int gfx_init()
 	/* open window */	
 	if(config.gfx_fullscreen)
 	{
-		int result = glfwOpenWindow(screen_width, screen_height, 8, 8, 8, 0, 24, 0, GLFW_FULLSCREEN);
+		int result = glfwOpenWindow(screen_width, screen_height, 8, 8, 8, 8, 24, 8, GLFW_FULLSCREEN);
 		if(result != GL_TRUE)
 		{
 			dbg_msg("game", "failed to create gl context");
@@ -172,19 +172,22 @@ int gfx_init()
 	}
 	else
 	{
-		int result = glfwOpenWindow(screen_width, screen_height, 0, 0, 0, 0, 24, 0, GLFW_WINDOW);
+		int result = glfwOpenWindow(screen_width, screen_height, 0, 0, 0, 8, 24, 8, GLFW_WINDOW);
 		if(result != GL_TRUE)
 		{
 			dbg_msg("game", "failed to create gl context");
 			return 0;
 		}
 	}
-
 	
 	glfwSetWindowSizeCallback(screen_resize);
 	
+	glGetIntegerv(GL_ALPHA_BITS, &i);
+	dbg_msg("gfx", "alphabits = %d", i);
 	glGetIntegerv(GL_DEPTH_BITS, &i);
 	dbg_msg("gfx", "depthbits = %d", i);
+	glGetIntegerv(GL_STENCIL_BITS, &i);
+	dbg_msg("gfx", "stencilbits = %d", i);
 	
 	glfwSetWindowTitle("Teewars");
 	
@@ -212,6 +215,10 @@ int gfx_init()
 	glDisable(GL_DEPTH_TEST);
 	glMatrixMode(GL_MODELVIEW);
 	glLoadIdentity();
+/*	glAlphaFunc(GL_GREATER, 0);
+	glEnable(GL_ALPHA_TEST);*/
+
+
 	gfx_mask_op(MASK_NONE, 0);
 	
 	/* Set all z to -5.0f */
@@ -325,6 +332,8 @@ void gfx_set_vsync(int val)
 
 int gfx_unload_texture(int index)
 {
+	if(index < 0)
+		return 0;
 	glDeleteTextures(1, &textures[index].tex);
 	textures[index].next = first_free_texture;
 	memory_usage -= textures[index].memsize;
diff --git a/src/engine/e_system.h b/src/engine/e_system.h
index 4802111f..72a4ff7b 100644
--- a/src/engine/e_system.h
+++ b/src/engine/e_system.h
@@ -524,6 +524,9 @@ void mem_debug_dump();
 int mem_allocated();
 
 void swap_endian(void *data, unsigned elem_size, unsigned num);
+
+/* #define cache_prefetch(addr) __builtin_prefetch(addr) */
+
 #ifdef __cplusplus
 }
 #endif
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)
 	{
diff --git a/src/game/editor/ed_editor.hpp b/src/game/editor/ed_editor.hpp
index df3a1fc1..80ca42d5 100644
--- a/src/game/editor/ed_editor.hpp
+++ b/src/game/editor/ed_editor.hpp
@@ -221,6 +221,11 @@ public:
 		format = 0;
 	}
 	
+	~IMAGE()
+	{
+		gfx_unload_texture(tex_id);
+	}
+	
 	int tex_id;
 	int external;
 	char name[128];
diff --git a/src/game/editor/ed_layer_tiles.cpp b/src/game/editor/ed_layer_tiles.cpp
index d0f04cbc..f6a5dfe9 100644
--- a/src/game/editor/ed_layer_tiles.cpp
+++ b/src/game/editor/ed_layer_tiles.cpp
@@ -34,7 +34,7 @@ void LAYER_TILES::render()
 	if(image >= 0 && image < editor.map.images.len())
 		tex_id = editor.map.images[image]->tex_id;
 	gfx_texture_set(tex_id);
-	render_tilemap(tiles, width, height, 32.0f, 0);
+	render_tilemap(tiles, width, height, 32.0f, vec4(1,1,1,1), 0);
 }
 
 int LAYER_TILES::convert_x(float x) const { return (int)(x/32.0f); }
diff --git a/src/game/g_collision.cpp b/src/game/g_collision.cpp
index b15af913..6c50064b 100644
--- a/src/game/g_collision.cpp
+++ b/src/game/g_collision.cpp
@@ -16,9 +16,9 @@ int col_height() { return height; }
 
 int col_init()
 {
-	width = layers_game()->width;
-	height = layers_game()->height;
-	tiles = (TILE *)map_get_data(layers_game()->data);
+	width = layers_game_layer()->width;
+	height = layers_game_layer()->height;
+	tiles = (TILE *)map_get_data(layers_game_layer()->data);
 	return 1;
 }
 
diff --git a/src/game/g_game.cpp b/src/game/g_game.cpp
index 66da6295..98ae8c47 100644
--- a/src/game/g_game.cpp
+++ b/src/game/g_game.cpp
@@ -284,7 +284,7 @@ void player_core::tick()
 				hookvel.x *= 0.75f;
 			
 			vec2 new_vel = vel+hookvel;
-			
+
 			// check if we are under the legal limit for the hook
 			if(length(new_vel) < hook_drag_speed || length(new_vel) < length(vel))
 				vel = new_vel; // no problem. apply
@@ -348,8 +348,8 @@ void player_core::tick()
 	}	
 
 	// clamp the velocity to something sane
-	if(length(vel) > 100.0f)
-		vel = normalize(vel) * 100.0f;
+	if(length(vel) > terminal_velocity)
+		vel = normalize(vel) * terminal_velocity;
 }
 
 void player_core::move()
diff --git a/src/game/g_layers.cpp b/src/game/g_layers.cpp
index 1e46168c..1595e266 100644
--- a/src/game/g_layers.cpp
+++ b/src/game/g_layers.cpp
@@ -1,7 +1,8 @@
 #include <engine/e_common_interface.h>
 #include "g_layers.h"
 
-static MAPITEM_LAYER_TILEMAP *game_layer;
+static MAPITEM_LAYER_TILEMAP *game_layer = 0;
+static MAPITEM_GROUP *game_group = 0;
 
 static int groups_start = 0;
 static int groups_num = 0;
@@ -13,6 +14,7 @@ void layers_init()
 	map_get_type(MAPITEMTYPE_GROUP, &groups_start, &groups_num);
 	
 	{
+		int p = 0;
 		map_get_type(MAPITEMTYPE_LAYER, &layers_start, &layers_num);
 			
 		for(int i = 0; i < layers_num; i++)
@@ -21,11 +23,23 @@ void layers_init()
 			if(layer->type == LAYERTYPE_TILES)
 			{
 				MAPITEM_LAYER_TILEMAP *tilemap = (MAPITEM_LAYER_TILEMAP *)layer;
+				
+				if(p)
+				{
+					p--;
+					if(p == 0)
+						tilemap->flags |= 2;
+				}
+				
 				if(tilemap->flags&1)
+				{
+					dbg_msg("layers", "game");
 					game_layer = tilemap;
+					p = 2;
+				}
 			}
+			dbg_msg("layers", "%d %d", i, layer->type);
 		}
-		
 	}
 }
 
@@ -40,8 +54,13 @@ MAPITEM_LAYER *layers_get_layer(int index)
 	return (MAPITEM_LAYER *)map_get_item(layers_start+index, 0, 0);
 }
 
-MAPITEM_LAYER_TILEMAP *layers_game()
+MAPITEM_LAYER_TILEMAP *layers_game_layer()
 {
 	return game_layer;
 }
 
+MAPITEM_GROUP *layers_game_group()
+{
+	return game_group;
+}
+
diff --git a/src/game/g_layers.h b/src/game/g_layers.h
index 455b74a8..3ece897f 100644
--- a/src/game/g_layers.h
+++ b/src/game/g_layers.h
@@ -2,7 +2,8 @@
 
 void layers_init();
 
-MAPITEM_LAYER_TILEMAP *layers_game();
+MAPITEM_LAYER_TILEMAP *layers_game_layer();
+MAPITEM_GROUP *layers_game_group();
 
 int layers_num_groups();
 MAPITEM_GROUP *layers_get_group(int index);
diff --git a/src/game/g_protocol.h b/src/game/g_protocol.h
index 9ae7b170..32ddb971 100644
--- a/src/game/g_protocol.h
+++ b/src/game/g_protocol.h
@@ -5,12 +5,13 @@
 #define GAME_PROTOCOL_H
 
 // --------- PHYSICS TWEAK! --------
-const float ground_control_speed = 7.0f;
-const float ground_control_accel = 2.0f;
+const float ticks_per_second = 50.0f;
+const float ground_control_speed = 350.0f / ticks_per_second;
+const float ground_control_accel = 100.0f / ticks_per_second;
 const float ground_friction = 0.5f;
 const float ground_jump_speed = 12.6f;
 const float ground_air_speed = 11.5f;
-const float air_control_speed = 5.0f;
+const float air_control_speed = 250.0f / ticks_per_second;
 const float air_control_accel = 1.5f;
 const float air_friction = 0.95f;
 const float hook_length = 34*10.0f;
@@ -18,6 +19,8 @@ const float hook_fire_speed = 45.0f;
 const float hook_drag_accel = 3.0f;
 const float hook_drag_speed = 15.0f;
 const float gravity = 0.5f;
+const float terminal_velocity = 20.0f;
+
 const float wall_friction = 0.80f;
 const float wall_jump_speed_up = ground_jump_speed*0.8f;
 const float wall_jump_speed_out = ground_jump_speed*0.8f;
@@ -36,7 +39,7 @@ enum
 	EVENT_DAMAGEINDICATION,
 	EVENT_SOUND_WORLD,
 	EVENT_SMOKE,
-	EVENT_SPAWN,
+	EVENT_PLAYERSPAWN,
 	EVENT_DEATH,
 	EVENT_AIR_JUMP,
 	
diff --git a/src/game/g_variables.h b/src/game/g_variables.h
index 08abe313..9e4d1c7c 100644
--- a/src/game/g_variables.h
+++ b/src/game/g_variables.h
@@ -25,16 +25,17 @@ MACRO_CONFIG_INT(key_remoteconsole, 256+3, 32, 512)
 
 MACRO_CONFIG_INT(key_toggleconsole, 256+4, 32, 512)
 
-MACRO_CONFIG_INT(dbg_bots, 0, 0, 11)
 
 MACRO_CONFIG_INT(cl_predict, 1, 0, 1)
 MACRO_CONFIG_INT(cl_nameplates, 0, 0, 1)
 MACRO_CONFIG_INT(cl_nameplates_always, 0, 0, 1)
 MACRO_CONFIG_INT(cl_dynamic_camera, 1, 0, 1)
-MACRO_CONFIG_INT(cl_team, -10, -1, 0)
 MACRO_CONFIG_INT(cl_autoswitch_weapons, 0, 0, 1)
 MACRO_CONFIG_INT(cl_show_player_ids, 0, 0, 1)
 
+
+MACRO_CONFIG_INT(cl_flow, 0, 0, 1)
+
 MACRO_CONFIG_INT(cl_show_welcome, 1, 0, 1)
 
 MACRO_CONFIG_INT(player_use_custom_color, 0, 0, 1)
@@ -42,7 +43,9 @@ MACRO_CONFIG_INT(player_color_body, 65408, 0, 0)
 MACRO_CONFIG_INT(player_color_feet, 65408, 0, 0)
 MACRO_CONFIG_STR(player_skin, 64, "default")
 
+MACRO_CONFIG_INT(dbg_bots, 0, 0, 11)
 MACRO_CONFIG_INT(dbg_firedelay, 0, 0, 1)
+MACRO_CONFIG_INT(dbg_flow, 0, 0, 1)
 
 MACRO_CONFIG_INT(ui_page, 1, 0, 5)
 MACRO_CONFIG_STR(ui_server_address, 128, "localhost:8303")
diff --git a/src/game/server/gs_common.h b/src/game/server/gs_common.h
index b91e777e..2c7b7920 100644
--- a/src/game/server/gs_common.h
+++ b/src/game/server/gs_common.h
@@ -248,8 +248,6 @@ public:
 	int attack_tick;
 	
 	int sniper_chargetick;
-
-	int bomb_firetick;
 	
 	int damage_taken;
 
diff --git a/src/game/server/gs_server.cpp b/src/game/server/gs_server.cpp
index 914fd8e1..8e123043 100644
--- a/src/game/server/gs_server.cpp
+++ b/src/game/server/gs_server.cpp
@@ -18,7 +18,7 @@ class player* get_player(int index);
 void create_damageind(vec2 p, float angle_mod, int amount);
 void create_explosion(vec2 p, int owner, int weapon, bool bnodamage);
 void create_smoke(vec2 p);
-void create_spawn(vec2 p);
+void create_playerspawn(vec2 p);
 void create_death(vec2 p);
 void create_sound(vec2 pos, int sound, int mask=-1);
 class player *intersect_player(vec2 pos0, vec2 pos1, vec2 &new_pos, class entity *notthis = 0);
@@ -396,8 +396,6 @@ void projectile::tick()
 	float gravity = -400;
 	if(type != WEAPON_ROCKET)
 		gravity = -100;
-	if(type == WEAPON_BOMB)
-		gravity = 0;
 	
 	float pt = (server_tick()-start_tick-1)/(float)server_tickspeed();
 	float ct = (server_tick()-start_tick)/(float)server_tickspeed();
@@ -410,17 +408,10 @@ void projectile::tick()
 	
 	vec2 new_pos;
 	entity *targetplayer = (entity*)intersect_player(prevpos, curpos, new_pos, powner);
-	player *p = (player*) powner;
 	
-	if(targetplayer || collide || lifespan < 0 || (type == WEAPON_BOMB && count_input(p->previnput.fire, p->input.fire).releases))
+	if(targetplayer || collide || lifespan < 0)
 	{
-		if(type == WEAPON_BOMB)
-		{
-			p->bomb_firetick = -1;
-			p->reload_timer = data->weapons[WEAPON_BOMB].firedelay * server_tickspeed() / 1000;
-		}
-
-		if (lifespan >= 0 || weapon == WEAPON_ROCKET || weapon == WEAPON_BOMB)
+		if (lifespan >= 0 || weapon == WEAPON_ROCKET)
 			create_sound(pos, sound_impact);
 
 		if (flags & PROJECTILE_FLAGS_EXPLODE)
@@ -504,7 +495,6 @@ void player::reset()
 	numobjectshit = 0;
 	ninja_activationtick = 0;
 	sniper_chargetick = -1;
-	bomb_firetick = -1;
 	currentmovetime = 0;
 	
 	active_weapon = WEAPON_GUN;
@@ -718,8 +708,6 @@ void player::try_respawn()
 
 	//weapons[WEAPON_SNIPER].got = true;
 	//weapons[WEAPON_SNIPER].ammo = data->weapons[WEAPON_SNIPER].maxammo;
-	weapons[WEAPON_BOMB].got = true;
-	weapons[WEAPON_BOMB].ammo = data->weapons[WEAPON_BOMB].maxammo;
 	active_weapon = WEAPON_GUN;
 	last_weapon = WEAPON_HAMMER;
 	wanted_weapon = WEAPON_GUN;
@@ -728,7 +716,7 @@ void player::try_respawn()
 
 	// Create sound and spawn effects
 	create_sound(pos, SOUND_PLAYER_SPAWN);
-	create_spawn(pos);
+	create_playerspawn(pos);
 
 	gameobj->on_player_spawn(this);
 }
@@ -941,32 +929,6 @@ int player::handle_sniper()
 	return 0;
 }
 
-int player::handle_bomb()
-{
-	struct input_count button = count_input(previnput.fire, input.fire);
-
-	if(button.releases)
-	{
-	}
-	else if(input.fire & 1 && bomb_firetick == -1 && reload_timer == 0)
-	{
-		vec2 direction = normalize(vec2(input.target_x, input.target_y));
-		new projectile(WEAPON_BOMB,
-				client_id,
-				pos+vec2(0,0),
-				direction*7.0f,
-				100,
-				this,
-				1, projectile::PROJECTILE_FLAGS_EXPLODE, 0, SOUND_ROCKET_EXPLODE, WEAPON_ROCKET);
-		create_sound(pos, SOUND_ROCKET_FIRE);
-		bomb_firetick = server_tick();
-		attack_tick = server_tick();
-		weapons[active_weapon].ammo--;
-	}
-
-	return 0;
-}
-
 int player::handle_weapons()
 {
 	vec2 direction = normalize(vec2(latest_input.target_x, latest_input.target_y));
@@ -1043,8 +1005,6 @@ int player::handle_weapons()
 	if (active_weapon == WEAPON_SNIPER)
 		return handle_sniper();
 	*/
-	if (active_weapon == WEAPON_BOMB)
-		return handle_bomb();
 
 	if(reload_timer == 0)
 	{
@@ -1073,7 +1033,7 @@ int player::handle_weapons()
 							client_id,
 							pos+vec2(0,0),
 							direction*30.0f,
-							100,
+							server_tickspeed(),
 							this,
 							1, 0, 0, -1, WEAPON_GUN);
 						create_sound(pos, SOUND_GUN_FIRE);
@@ -1105,7 +1065,7 @@ int player::handle_weapons()
 								pos+vec2(0,0),
 								vec2(cosf(a), sinf(a))*(28.0f + 12.0f*v),
 								//vec2(cosf(a), sinf(a))*20.0f,
-								(int)(server_tickspeed()*0.3f),
+								(int)(server_tickspeed()*0.25f),
 								this,
 								1, 0, 0, -1, WEAPON_SHOTGUN);
 						}
@@ -1723,12 +1683,6 @@ void create_explosion(vec2 p, int owner, int weapon, bool bnodamage)
 		float radius = 128.0f;
 		float innerradius = 42.0f;
 
-		if(weapon == WEAPON_BOMB)
-		{
-			radius = 256.0f;
-			innerradius = 64.0f;
-		}
-
 		int num = world->find_entities(p, radius, ents, 64);
 		for(int i = 0; i < num; i++)
 		{
@@ -1756,10 +1710,10 @@ void create_smoke(vec2 p)
 	}
 }
 
-void create_spawn(vec2 p)
+void create_playerspawn(vec2 p)
 {
 	// create the event
-	ev_spawn *ev = (ev_spawn *)events.create(EVENT_SPAWN, sizeof(ev_spawn));
+	ev_spawn *ev = (ev_spawn *)events.create(EVENT_PLAYERSPAWN, sizeof(ev_spawn));
 	if(ev)
 	{
 		ev->x = (int)p.x;
@@ -2062,7 +2016,7 @@ void mods_init()
 		players[i].core.world = &world->core;
 
 	// create all entities from the game layer
-	MAPITEM_LAYER_TILEMAP *tmap = layers_game();
+	MAPITEM_LAYER_TILEMAP *tmap = layers_game_layer();
 	TILE *tiles = (TILE *)map_get_data(tmap->data);
 	
 	num_spawn_points[0] = 0;