about summary refs log tree commit diff
path: root/src/game/server/entities
diff options
context:
space:
mode:
Diffstat (limited to 'src/game/server/entities')
-rw-r--r--src/game/server/entities/character.cpp1131
-rw-r--r--src/game/server/entities/character.hpp122
-rw-r--r--src/game/server/entities/laser.cpp112
-rw-r--r--src/game/server/entities/laser.hpp31
-rw-r--r--src/game/server/entities/pickup.cpp142
-rw-r--r--src/game/server/entities/pickup.hpp24
-rw-r--r--src/game/server/entities/projectile.cpp108
-rw-r--r--src/game/server/entities/projectile.hpp38
8 files changed, 1708 insertions, 0 deletions
diff --git a/src/game/server/entities/character.cpp b/src/game/server/entities/character.cpp
new file mode 100644
index 00000000..160e080a
--- /dev/null
+++ b/src/game/server/entities/character.cpp
@@ -0,0 +1,1131 @@
+#include <new>
+#include <engine/e_server_interface.h>
+#include <engine/e_config.h>
+#include <game/server/gs_common.hpp>
+
+#include "character.hpp"
+#include "laser.hpp"
+#include "projectile.hpp"
+
+struct INPUT_COUNT
+{
+	int presses;
+	int releases;
+};
+
+static INPUT_COUNT count_input(int prev, int cur)
+{
+	INPUT_COUNT c = {0,0};
+	prev &= INPUT_STATE_MASK;
+	cur &= INPUT_STATE_MASK;
+	int i = prev;
+	while(i != cur)
+	{
+		i = (i+1)&INPUT_STATE_MASK;
+		if(i&1)
+			c.presses++;
+		else
+			c.releases++;
+	}
+
+	return c;
+}
+
+// player
+CHARACTER::CHARACTER()
+: ENTITY(NETOBJTYPE_CHARACTER)
+{}
+
+void CHARACTER::reset()
+{
+	
+}
+
+bool CHARACTER::spawn(PLAYER *player, vec2 pos, int team)
+{
+	player_state = PLAYERSTATE_UNKNOWN;
+	emote_stop = -1;
+	last_action = -1;
+	active_weapon = WEAPON_GUN;
+	last_weapon = WEAPON_HAMMER;
+	queued_weapon = -1;
+	
+	//clear();
+	this->player = player;
+	this->pos = pos;
+	this->team = team;
+	
+	core.reset();
+	core.world = &game.world.core;
+	core.pos = pos;
+	game.world.core.characters[player->client_id] = &core;
+	
+	game.world.insert_entity(this);
+	alive = true;
+	return true;
+}
+
+void CHARACTER::destroy()
+{
+	game.world.core.characters[player->client_id] = 0;
+	alive = false;
+}
+
+void CHARACTER::set_weapon(int w)
+{
+	if(w == active_weapon)
+		return;
+		
+	last_weapon = active_weapon;
+	queued_weapon = -1;
+	active_weapon = w;
+	if(active_weapon < 0 || active_weapon >= NUM_WEAPONS)
+		active_weapon = 0;
+	
+	game.create_sound(pos, SOUND_WEAPON_SWITCH);
+}
+
+bool CHARACTER::is_grounded()
+{
+	if(col_check_point((int)(pos.x+phys_size/2), (int)(pos.y+phys_size/2+5)))
+		return true;
+	if(col_check_point((int)(pos.x-phys_size/2), (int)(pos.y+phys_size/2+5)))
+		return true;
+	return false;
+}
+
+
+int CHARACTER::handle_ninja()
+{
+	vec2 direction = normalize(vec2(latest_input.target_x, latest_input.target_y));
+
+	if ((server_tick() - ninja.activationtick) > (data->weapons.ninja.duration * server_tickspeed() / 1000))
+	{
+		// time's up, return
+		weapons[WEAPON_NINJA].got = false;
+		active_weapon = last_weapon;
+		if(active_weapon == WEAPON_NINJA)
+			active_weapon = WEAPON_GUN;
+		set_weapon(active_weapon);
+		return 0;
+	}
+	
+	// force ninja weapon
+	set_weapon(WEAPON_NINJA);
+
+	// Check if it should activate
+	if (count_input(latest_previnput.fire, latest_input.fire).presses && (server_tick() > ninja.currentcooldown))
+	{
+		// ok then, activate ninja
+		attack_tick = server_tick();
+		ninja.activationdir = direction;
+		ninja.currentmovetime = data->weapons.ninja.movetime * server_tickspeed() / 1000;
+		ninja.currentcooldown = data->weapons.ninja.base->firedelay * server_tickspeed() / 1000 + server_tick();
+		
+		// reset hit objects
+		numobjectshit = 0;
+
+		game.create_sound(pos, SOUND_NINJA_FIRE);
+
+		// release all hooks when ninja is activated
+		//release_hooked();
+		//release_hooks();
+	}
+
+	ninja.currentmovetime--;
+
+	if (ninja.currentmovetime == 0)
+	{
+		// reset player velocity
+		core.vel *= 0.2f;
+		//return MODIFIER_RETURNFLAGS_OVERRIDEWEAPON;
+	}
+
+	if (ninja.currentmovetime > 0)
+	{
+		// Set player velocity
+		core.vel = ninja.activationdir * data->weapons.ninja.velocity;
+		vec2 oldpos = pos;
+		move_box(&core.pos, &core.vel, vec2(phys_size, phys_size), 0.0f);
+		// reset velocity so the client doesn't predict stuff
+		core.vel = vec2(0.0f,0.0f);
+		if ((ninja.currentmovetime % 2) == 0)
+		{
+			//create_smoke(pos);
+		}
+
+		// check if we hit anything along the way
+		{
+			CHARACTER *ents[64];
+			vec2 dir = pos - oldpos;
+			float radius = phys_size * 2.0f; //length(dir * 0.5f);
+			vec2 center = oldpos + dir * 0.5f;
+			int num = game.world.find_entities(center, radius, (ENTITY**)ents, 64, NETOBJTYPE_CHARACTER);
+
+			for (int i = 0; i < num; i++)
+			{
+				// Check if entity is a player
+				if (ents[i] == this)
+					continue;
+				// make sure we haven't hit this object before
+				bool balreadyhit = false;
+				for (int j = 0; j < numobjectshit; j++)
+				{
+					if (hitobjects[j] == ents[i])
+						balreadyhit = true;
+				}
+				if (balreadyhit)
+					continue;
+
+				// check so we are sufficiently close
+				if (distance(ents[i]->pos, pos) > (phys_size * 2.0f))
+					continue;
+
+				// hit a player, give him damage and stuffs...
+				game.create_sound(ents[i]->pos, SOUND_NINJA_HIT);
+				// set his velocity to fast upward (for now)
+				if(numobjectshit < 10)
+					hitobjects[numobjectshit++] = ents[i];
+					
+				ents[i]->take_damage(vec2(0,10.0f), data->weapons.ninja.base->damage, player->client_id,WEAPON_NINJA);
+			}
+		}
+		return 0;
+	}
+
+	return 0;
+}
+
+
+void CHARACTER::do_weaponswitch()
+{
+	if(reload_timer != 0) // make sure we have reloaded
+		return;
+		
+	if(queued_weapon == -1) // check for a queued weapon
+		return;
+
+	if(weapons[WEAPON_NINJA].got) // if we have ninja, no weapon selection is possible
+		return;
+
+	// switch weapon
+	set_weapon(queued_weapon);
+}
+
+void CHARACTER::handle_weaponswitch()
+{
+	int wanted_weapon = active_weapon;
+	if(queued_weapon != -1)
+		wanted_weapon = queued_weapon;
+	
+	// select weapon
+	int next = count_input(latest_previnput.next_weapon, latest_input.next_weapon).presses;
+	int prev = count_input(latest_previnput.prev_weapon, latest_input.prev_weapon).presses;
+
+	if(next < 128) // make sure we only try sane stuff
+	{
+		while(next) // next weapon selection
+		{
+			wanted_weapon = (wanted_weapon+1)%NUM_WEAPONS;
+			if(weapons[wanted_weapon].got)
+				next--;
+		}
+	}
+
+	if(prev < 128) // make sure we only try sane stuff
+	{
+		while(prev) // prev weapon selection
+		{
+			wanted_weapon = (wanted_weapon-1)<0?NUM_WEAPONS-1:wanted_weapon-1;
+			if(weapons[wanted_weapon].got)
+				prev--;
+		}
+	}
+
+	// direct weapon selection
+	if(latest_input.wanted_weapon)
+		wanted_weapon = input.wanted_weapon-1;
+
+	// check for insane values
+	if(wanted_weapon >= 0 && wanted_weapon < NUM_WEAPONS && wanted_weapon != active_weapon && weapons[wanted_weapon].got)
+		queued_weapon = wanted_weapon;
+	
+	do_weaponswitch();
+}
+
+void CHARACTER::fire_weapon()
+{
+	if(reload_timer != 0 || active_weapon == WEAPON_NINJA)
+		return;
+		
+	do_weaponswitch();
+	
+	vec2 direction = normalize(vec2(latest_input.target_x, latest_input.target_y));
+	
+	bool fullauto = false;
+	if(active_weapon == WEAPON_GRENADE || active_weapon == WEAPON_SHOTGUN || active_weapon == WEAPON_RIFLE)
+		fullauto = true;
+
+
+	// check if we gonna fire
+	bool will_fire = false;
+	if(count_input(latest_previnput.fire, latest_input.fire).presses) will_fire = true;
+	if(fullauto && (latest_input.fire&1) && weapons[active_weapon].ammo) will_fire = true;
+	if(!will_fire)
+		return;
+		
+	// check for ammo
+	if(!weapons[active_weapon].ammo)
+	{
+		game.create_sound(pos, SOUND_WEAPON_NOAMMO);
+		return;
+	}
+	
+	vec2 projectile_startpos = pos+direction*phys_size*0.75f;
+	
+	switch(active_weapon)
+	{
+		case WEAPON_HAMMER:
+		{
+			// reset objects hit
+			numobjectshit = 0;
+			game.create_sound(pos, SOUND_HAMMER_FIRE);
+			
+			CHARACTER *ents[64];
+			int num = game.world.find_entities(pos+direction*phys_size*0.75f, phys_size*0.5f, (ENTITY**)ents, 64, NETOBJTYPE_CHARACTER);
+
+			for (int i = 0; i < num; i++)
+			{
+				CHARACTER *target = ents[i];
+				if (target == this)
+					continue;
+					
+				// hit a player, give him damage and stuffs...
+				vec2 fdir = normalize(ents[i]->pos - pos);
+
+				// set his velocity to fast upward (for now)
+				game.create_sound(pos, SOUND_HAMMER_HIT);
+				ents[i]->take_damage(vec2(0,-1.0f), data->weapons.hammer.base->damage, player->client_id, active_weapon);
+				vec2 dir;
+				if (length(target->pos - pos) > 0.0f)
+					dir = normalize(target->pos - pos);
+				else
+					dir = vec2(0,-1);
+					
+				target->core.vel += normalize(dir + vec2(0,-1.1f)) * 10.0f;
+			}
+			
+		} break;
+
+		case WEAPON_GUN:
+		{
+			PROJECTILE *proj = new PROJECTILE(WEAPON_GUN,
+				player->client_id,
+				projectile_startpos,
+				direction,
+				(int)(server_tickspeed()*tuning.gun_lifetime),
+				this,
+				1, 0, 0, -1, WEAPON_GUN);
+				
+			// pack the projectile and send it to the client directly
+			NETOBJ_PROJECTILE p;
+			proj->fill_info(&p);
+			
+			msg_pack_start(NETMSGTYPE_SV_EXTRAPROJECTILE, 0);
+			msg_pack_int(1);
+			for(unsigned i = 0; i < sizeof(NETOBJ_PROJECTILE)/sizeof(int); i++)
+				msg_pack_int(((int *)&p)[i]);
+			msg_pack_end();
+			server_send_msg(player->client_id);
+							
+			game.create_sound(pos, SOUND_GUN_FIRE);
+		} break;
+		
+		case WEAPON_SHOTGUN:
+		{
+			int shotspread = 2;
+
+			msg_pack_start(NETMSGTYPE_SV_EXTRAPROJECTILE, 0);
+			msg_pack_int(shotspread*2+1);
+			
+			for(int i = -shotspread; i <= shotspread; i++)
+			{
+				float spreading[] = {-0.185f, -0.070f, 0, 0.070f, 0.185f};
+				float a = get_angle(direction);
+				a += spreading[i+2];
+				float v = 1-(abs(i)/(float)shotspread);
+				float speed = mix((float)tuning.shotgun_speeddiff, 1.0f, v);
+				PROJECTILE *proj = new PROJECTILE(WEAPON_SHOTGUN,
+					player->client_id,
+					projectile_startpos,
+					vec2(cosf(a), sinf(a))*speed,
+					(int)(server_tickspeed()*tuning.shotgun_lifetime),
+					this,
+					1, 0, 0, -1, WEAPON_SHOTGUN);
+					
+				// pack the projectile and send it to the client directly
+				NETOBJ_PROJECTILE p;
+				proj->fill_info(&p);
+				
+				for(unsigned i = 0; i < sizeof(NETOBJ_PROJECTILE)/sizeof(int); i++)
+					msg_pack_int(((int *)&p)[i]);
+			}
+
+			msg_pack_end();
+			server_send_msg(player->client_id);					
+			
+			game.create_sound(pos, SOUND_SHOTGUN_FIRE);
+		} break;
+
+		case WEAPON_GRENADE:
+		{
+			PROJECTILE *proj = new PROJECTILE(WEAPON_GRENADE,
+				player->client_id,
+				projectile_startpos,
+				direction,
+				(int)(server_tickspeed()*tuning.grenade_lifetime),
+				this,
+				1, PROJECTILE::PROJECTILE_FLAGS_EXPLODE, 0, SOUND_GRENADE_EXPLODE, WEAPON_GRENADE);
+
+			// pack the projectile and send it to the client directly
+			NETOBJ_PROJECTILE p;
+			proj->fill_info(&p);
+			
+			msg_pack_start(NETMSGTYPE_SV_EXTRAPROJECTILE, 0);
+			msg_pack_int(1);
+			for(unsigned i = 0; i < sizeof(NETOBJ_PROJECTILE)/sizeof(int); i++)
+				msg_pack_int(((int *)&p)[i]);
+			msg_pack_end();
+			server_send_msg(player->client_id);
+
+			game.create_sound(pos, SOUND_GRENADE_FIRE);
+		} break;
+		
+		case WEAPON_RIFLE:
+		{
+			new LASER(pos, direction, tuning.laser_reach, this);
+			game.create_sound(pos, SOUND_RIFLE_FIRE);
+		} break;
+		
+	}
+
+	if(weapons[active_weapon].ammo > 0) // -1 == unlimited
+		weapons[active_weapon].ammo--;
+	attack_tick = server_tick();
+	reload_timer = data->weapons.id[active_weapon].firedelay * server_tickspeed() / 1000;
+}
+
+int CHARACTER::handle_weapons()
+{
+	vec2 direction = normalize(vec2(latest_input.target_x, latest_input.target_y));
+
+	/*
+	if(config.dbg_stress)
+	{
+		for(int i = 0; i < NUM_WEAPONS; i++)
+		{
+			weapons[i].got = true;
+			weapons[i].ammo = 10;
+		}
+
+		if(reload_timer) // twice as fast reload
+			reload_timer--;
+	} */
+
+	// check reload timer
+	if(reload_timer)
+	{
+		reload_timer--;
+		return 0;
+	}
+	
+	if (active_weapon == WEAPON_NINJA)
+	{
+		// don't update other weapons while ninja is active
+		return handle_ninja();
+	}
+
+	// fire weapon, if wanted
+	fire_weapon();
+
+	// ammo regen
+	int ammoregentime = data->weapons.id[active_weapon].ammoregentime;
+	if(ammoregentime)
+	{
+		// If equipped and not active, regen ammo?
+		if (reload_timer <= 0)
+		{
+			if (weapons[active_weapon].ammoregenstart < 0)
+				weapons[active_weapon].ammoregenstart = server_tick();
+
+			if ((server_tick() - weapons[active_weapon].ammoregenstart) >= ammoregentime * server_tickspeed() / 1000)
+			{
+				// Add some ammo
+				weapons[active_weapon].ammo = min(weapons[active_weapon].ammo + 1, 10);
+				weapons[active_weapon].ammoregenstart = -1;
+			}
+		}
+		else
+		{
+			weapons[active_weapon].ammoregenstart = -1;
+		}
+	}
+	
+	return 0;
+}
+
+void CHARACTER::on_predicted_input(NETOBJ_PLAYER_INPUT *new_input)
+{
+	// check for changes
+	if(mem_comp(&input, new_input, sizeof(NETOBJ_PLAYER_INPUT)) != 0)
+		last_action = server_tick();
+		
+	// copy new input
+	mem_copy(&input, new_input, sizeof(input));
+	num_inputs++;
+	
+	// or are not allowed to aim in the center
+	if(input.target_x == 0 && input.target_y == 0)
+		input.target_y = -1;	
+}
+
+void CHARACTER::on_direct_input(NETOBJ_PLAYER_INPUT *new_input)
+{
+	mem_copy(&latest_previnput, &latest_input, sizeof(latest_input));
+	mem_copy(&latest_input, new_input, sizeof(latest_input));
+	
+	if(num_inputs > 2 && team != -1)
+	{
+		handle_weaponswitch();
+		fire_weapon();
+	}
+}
+
+void CHARACTER::tick()
+{
+	//input = latest_input;
+	
+	// grab latest input
+	/*
+	{
+		int size = 0;
+		int *input = server_latestinput(client_id, &size);
+		if(input)
+		{
+			mem_copy(&latest_previnput, &latest_input, sizeof(latest_input));
+			mem_copy(&latest_input, input, sizeof(latest_input));
+		}
+	}*/
+	
+	// check if we have enough input
+	// this is to prevent initial weird clicks
+	/*
+	if(num_inputs < 2)
+	{
+		latest_previnput = latest_input;
+		previnput = input;
+	}*/
+	
+	//game.world.core.players[player->client_id] = &core;
+
+	// enable / disable physics
+	/*
+	if(team == -1 || dead)
+	{
+		game.world.core.players[client_id] = 0;
+		//game.world.remove_entity(this);
+	}
+	else
+	{
+		game.world.core.players[client_id] = &core;
+		//game.world._entity(this);
+	}
+
+	// spectator
+	if(team == -1)
+		return;
+
+	if(spawning)
+		try_respawn();
+
+	// TODO: rework the input to be more robust
+	if(dead)
+	{
+		if(server_tick()-die_tick >= server_tickspeed()/2 && count_input(latest_previnput.fire, latest_input.fire).presses)
+			die_tick = -1;
+		if(server_tick()-die_tick >= server_tickspeed()*5) // auto respawn after 3 sec
+			respawn();
+		//if((input.fire&1) && server_tick()-die_tick >= server_tickspeed()/2) // auto respawn after 0.5 sec
+			//respawn();
+		return;
+	}
+	* */
+
+	//player_core core;
+	//core.pos = pos;
+	//core.jumped = jumped;
+	core.input = input;
+	core.tick();
+
+	// handle weapons
+	handle_weapons();
+
+	player_state = input.player_state;
+
+	// Previnput
+	previnput = input;
+	return;
+}
+
+void CHARACTER::tick_defered()
+{
+	/*if(!dead)
+	{*/
+		vec2 start_pos = core.pos;
+		vec2 start_vel = core.vel;
+		bool stuck_before = test_box(core.pos, vec2(28.0f, 28.0f));
+		
+		core.move();
+		bool stuck_after_move = test_box(core.pos, vec2(28.0f, 28.0f));
+		core.quantize();
+		bool stuck_after_quant = test_box(core.pos, vec2(28.0f, 28.0f));
+		pos = core.pos;
+		
+		if(!stuck_before && (stuck_after_move || stuck_after_quant))
+		{
+			dbg_msg("player", "STUCK!!! %d %d %d %f %f %f %f %x %x %x %x", 
+				stuck_before,
+				stuck_after_move,
+				stuck_after_quant,
+				start_pos.x, start_pos.y,
+				start_vel.x, start_vel.y,
+				*((unsigned *)&start_pos.x), *((unsigned *)&start_pos.y),
+				*((unsigned *)&start_vel.x), *((unsigned *)&start_vel.y));
+		}
+
+		int events = core.triggered_events;
+		int mask = cmask_all_except_one(player->client_id);
+		
+		if(events&COREEVENT_GROUND_JUMP) game.create_sound(pos, SOUND_PLAYER_JUMP, mask);
+		if(events&COREEVENT_AIR_JUMP)
+		{
+			game.create_sound(pos, SOUND_PLAYER_AIRJUMP, mask);
+			NETEVENT_COMMON *c = (NETEVENT_COMMON *)game.events.create(NETEVENTTYPE_AIRJUMP, sizeof(NETEVENT_COMMON), mask);
+			if(c)
+			{
+				c->x = (int)pos.x;
+				c->y = (int)pos.y;
+			}
+		}
+		
+		//if(events&COREEVENT_HOOK_LAUNCH) snd_play_random(CHN_WORLD, SOUND_HOOK_LOOP, 1.0f, pos);
+		if(events&COREEVENT_HOOK_ATTACH_PLAYER) game.create_sound(pos, SOUND_HOOK_ATTACH_PLAYER, cmask_all());
+		if(events&COREEVENT_HOOK_ATTACH_GROUND) game.create_sound(pos, SOUND_HOOK_ATTACH_GROUND, mask);
+		//if(events&COREEVENT_HOOK_RETRACT) snd_play_random(CHN_WORLD, SOUND_PLAYER_JUMP, 1.0f, pos);
+	//}
+	
+	if(team == -1)
+	{
+		pos.x = input.target_x;
+		pos.y = input.target_y;
+	}
+}
+
+bool CHARACTER::increase_health(int amount)
+{
+	if(health >= 10)
+		return false;
+	health = clamp(health+amount, 0, 10);
+	return true;
+}
+
+bool CHARACTER::increase_armor(int amount)
+{
+	if(armor >= 10)
+		return false;
+	armor = clamp(armor+amount, 0, 10);
+	return true;
+}
+
+void CHARACTER::die(int killer, int weapon)
+{
+	/*if (dead || team == -1)
+		return;*/
+	int mode_special = game.controller->on_character_death(this, &game.players[killer], weapon);
+
+	dbg_msg("game", "kill killer='%d:%s' victim='%d:%s' weapon=%d special=%d",
+		killer, server_clientname(killer),
+		player->client_id, server_clientname(player->client_id), weapon, mode_special);
+
+	// send the kill message
+	NETMSG_SV_KILLMSG msg;
+	msg.killer = killer;
+	msg.victim = player->client_id;
+	msg.weapon = weapon;
+	msg.mode_special = mode_special;
+	msg.pack(MSGFLAG_VITAL);
+	server_send_msg(-1);
+
+	// a nice sound
+	game.create_sound(pos, SOUND_PLAYER_DIE);
+
+	// set dead state
+	// TODO: do stuff here
+	/*
+	die_pos = pos;
+	dead = true;
+	die_tick = server_tick();
+	*/
+	alive = false;
+	game.world.remove_entity(this);
+	game.create_death(pos, player->client_id);
+}
+
+bool CHARACTER::take_damage(vec2 force, int dmg, int from, int weapon)
+{
+	core.vel += force;
+	
+	if(game.controller->is_friendly_fire(player->client_id, from) && !config.sv_teamdamage)
+		return false;
+
+	// player only inflicts half damage on self
+	if(from == player->client_id)
+		dmg = max(1, dmg/2);
+
+	// CTF and TDM (TODO: check for FF)
+	//if (gameobj->gametype != GAMETYPE_DM && from >= 0 && players[from].team == team)
+		//return false;
+
+	damage_taken++;
+
+	// create healthmod indicator
+	if(server_tick() < damage_taken_tick+25)
+	{
+		// make sure that the damage indicators doesn't group together
+		game.create_damageind(pos, damage_taken*0.25f, dmg);
+	}
+	else
+	{
+		damage_taken = 0;
+		game.create_damageind(pos, 0, dmg);
+	}
+
+	if(dmg)
+	{
+		if(armor)
+		{
+			if(dmg > 1)
+			{
+				health--;
+				dmg--;
+			}
+			
+			if(dmg > armor)
+			{
+				dmg -= armor;
+				armor = 0;
+			}
+			else
+			{
+				armor -= dmg;
+				dmg = 0;
+			}
+		}
+		
+		health -= dmg;
+	}
+
+	damage_taken_tick = server_tick();
+
+	// do damage hit sound
+	if(from >= 0 && from != player->client_id)
+		game.create_sound(game.players[from].view_pos, SOUND_HIT, cmask_one(from));
+
+	// check for death
+	if(health <= 0)
+	{
+		die(from, weapon);
+
+		// set attacker's face to happy (taunt!)
+		if (from >= 0 && from != player->client_id)
+		{
+			CHARACTER *chr = &game.players[from].character;
+			chr->emote_type = EMOTE_HAPPY;
+			chr->emote_stop = server_tick() + server_tickspeed();
+		}
+
+		return false;
+	}
+
+	if (dmg > 2)
+		game.create_sound(pos, SOUND_PLAYER_PAIN_LONG);
+	else
+		game.create_sound(pos, SOUND_PLAYER_PAIN_SHORT);
+
+	emote_type = EMOTE_PAIN;
+	emote_stop = server_tick() + 500 * server_tickspeed() / 1000;
+
+	// spawn blood?
+	return true;
+}
+
+void CHARACTER::snap(int snaping_client)
+{
+	if(distance(game.players[snaping_client].view_pos, pos) > 1000.0f)
+		return;
+	
+	NETOBJ_CHARACTER *character = (NETOBJ_CHARACTER *)snap_new_item(NETOBJTYPE_CHARACTER, player->client_id, sizeof(NETOBJ_CHARACTER));
+
+	core.write(character);
+
+	// this is to make sure that players that are just standing still
+	// isn't sent. this is because the physics keep bouncing between
+	// 0-128 when just standing.
+	// TODO: fix the physics so this isn't needed
+	if(snaping_client != player->client_id && abs(character->vy) < 256.0f)
+		character->vy = 0;
+
+	if (emote_stop < server_tick())
+	{
+		emote_type = EMOTE_NORMAL;
+		emote_stop = -1;
+	}
+
+	character->emote = emote_type;
+
+	character->ammocount = 0;
+	character->health = 0;
+	character->armor = 0;
+	
+	character->weapon = active_weapon;
+	character->attacktick = attack_tick;
+
+	character->wanted_direction = input.direction;
+	/*
+	if(input.left && !input.right)
+		character->wanted_direction = -1;
+	else if(!input.left && input.right)
+		character->wanted_direction = 1;*/
+
+
+	if(player->client_id == snaping_client)
+	{
+		character->health = health;
+		character->armor = armor;
+		if(weapons[active_weapon].ammo > 0)
+			character->ammocount = weapons[active_weapon].ammo;
+	}
+
+	if (character->emote == EMOTE_NORMAL)
+	{
+		if(250 - ((server_tick() - last_action)%(250)) < 5)
+			character->emote = EMOTE_BLINK;
+	}
+
+	character->player_state = player_state;
+}
+
+PLAYER::PLAYER()
+{
+}
+
+void PLAYER::init(int client_id)
+{
+	// clear everything
+	mem_zero(this, sizeof(*this));
+	new(this) PLAYER();
+	this->client_id = client_id;
+}
+
+void PLAYER::tick()
+{
+	server_setclientscore(client_id, score);
+
+	// do latency stuff
+	{
+		CLIENT_INFO info;
+		if(server_getclientinfo(client_id, &info))
+		{
+			latency.accum += info.latency;
+			latency.accum_max = max(latency.accum_max, info.latency);
+			latency.accum_min = min(latency.accum_min, info.latency);
+		}
+
+		if(server_tick()%server_tickspeed() == 0)
+		{
+			latency.avg = latency.accum/server_tickspeed();
+			latency.max = latency.accum_max;
+			latency.min = latency.accum_min;
+			latency.accum = 0;
+			latency.accum_min = 1000;
+			latency.accum_max = 0;
+		}
+	}
+	
+	if(spawning && !get_character())
+		try_respawn();
+		
+	if(get_character())
+		view_pos = get_character()->pos;
+}
+
+void PLAYER::snap(int snaping_client)
+{
+	NETOBJ_PLAYER_INFO *info = (NETOBJ_PLAYER_INFO *)snap_new_item(NETOBJTYPE_PLAYER_INFO, client_id, sizeof(NETOBJ_PLAYER_INFO));
+
+	info->latency = latency.min;
+	info->latency_flux = latency.max-latency.min;
+	info->local = 0;
+	info->cid = client_id;
+	info->score = score;
+	info->team = team;
+
+	if(client_id == snaping_client)
+		info->local = 1;	
+}
+
+void PLAYER::on_disconnect()
+{
+	kill_character();
+	
+	//game.controller->on_player_death(&game.players[client_id], 0, -1);
+		
+	char buf[512];
+	str_format(buf, sizeof(buf),  "%s has left the game", server_clientname(client_id));
+	game.send_chat(-1, CHAT_ALL, buf);
+
+	dbg_msg("game", "leave player='%d:%s'", client_id, server_clientname(client_id));
+
+	// clear this whole structure
+	init(-1);
+
+	/*game.world.remove_entity(&game.players[client_id]);
+	game.world.core.players[client_id] = 0x0;
+	game.players[client_id].client_id = -1;	*/
+}
+
+void PLAYER::on_predicted_input(NETOBJ_PLAYER_INPUT *new_input)
+{
+	CHARACTER *chr = get_character();
+	if(chr)
+		chr->on_predicted_input(new_input);
+}
+
+void PLAYER::on_direct_input(NETOBJ_PLAYER_INPUT *new_input)
+{
+	CHARACTER *chr = get_character();
+	if(chr)
+		chr->on_direct_input(new_input);
+
+	if(!chr && team >= 0 && (new_input->fire&1))
+	{
+		spawning = true;
+		dbg_msg("", "I wanna spawn");
+	}
+}
+
+CHARACTER *PLAYER::get_character()
+{
+	if(character.alive)
+		return &character;
+	return 0;
+}
+
+void PLAYER::kill_character()
+{
+	CHARACTER *chr = get_character();
+	if(chr)
+		chr->die(-1, -1);
+}
+
+void PLAYER::respawn()
+{
+	spawning = true;
+}
+
+void PLAYER::set_team(int new_team)
+{
+	// clamp the team
+	new_team = game.controller->clampteam(new_team);
+	if(team == new_team)
+		return;
+		
+	char buf[512];
+	str_format(buf, sizeof(buf), "%s joined the %s", server_clientname(client_id), game.controller->get_team_name(new_team));
+	game.send_chat(-1, CHAT_ALL, buf); 
+	
+	kill_character();
+	team = new_team;
+	score = 0;
+	dbg_msg("game", "team_join player='%d:%s' team=%d", client_id, server_clientname(client_id), team);
+	
+	game.controller->on_player_info_change(&game.players[client_id]);
+
+	// send all info to this client
+	for(int i = 0; i < MAX_CLIENTS; i++)
+	{
+		if(game.players[i].client_id != -1)
+			game.send_info(i, -1);
+	}
+}
+
+vec2 spawn_points[3][64];
+int num_spawn_points[3] = {0};
+
+struct SPAWNEVAL
+{
+	SPAWNEVAL()
+	{
+		got = false;
+		friendly_team = -1;
+//		die_pos = vec2(0,0);
+		pos = vec2(100,100);
+	}
+		
+	vec2 pos;
+	bool got;
+	int friendly_team;
+	float score;
+//	vec2 die_pos;
+};
+
+static float evaluate_spawn(SPAWNEVAL *eval, vec2 pos)
+{
+	float score = 0.0f;
+	CHARACTER *c = (CHARACTER *)game.world.find_first(NETOBJTYPE_CHARACTER);
+	for(; c; c = (CHARACTER *)c->typenext())
+	{
+		// team mates are not as dangerous as enemies
+		float scoremod = 1.0f;
+		if(eval->friendly_team != -1 && c->team == eval->friendly_team)
+			scoremod = 0.5f;
+			
+		float d = distance(pos, c->pos);
+		if(d == 0)
+			score += 1000000000.0f;
+		else
+			score += 1.0f/d;
+	}
+	
+	// weight in the die posititon
+	/*
+	float d = distance(pos, eval->die_pos);
+	if(d == 0)
+		score += 1000000000.0f;
+	else
+		score += 1.0f/d;*/
+	
+	return score;
+}
+
+static void evaluate_spawn_type(SPAWNEVAL *eval, int t)
+{
+	// get spawn point
+	/*
+	int start, num;
+	map_get_type(t, &start, &num);
+	if(!num)
+		return;
+	*/
+	for(int i  = 0; i < num_spawn_points[t]; i++)
+	{
+		//num_spawn_points[t]
+		//mapres_spawnpoint *sp = (mapres_spawnpoint*)map_get_item(start + i, NULL, NULL);
+		vec2 p = spawn_points[t][i];// vec2((float)sp->x, (float)sp->y);
+		float s = evaluate_spawn(eval, p);
+		if(!eval->got || eval->score > s)
+		{
+			eval->got = true;
+			eval->score = s;
+			eval->pos = p;
+		}
+	}
+}
+
+void PLAYER::try_respawn()
+{
+	vec2 spawnpos = vec2(100.0f, -60.0f);
+	
+	// get spawn point
+	SPAWNEVAL eval;
+	//eval.die_pos = die_pos;
+	
+	eval.pos = vec2(100, 100);
+	
+	if(game.controller->gametype == GAMETYPE_CTF)
+	{
+		eval.friendly_team = team;
+		
+		// try first try own team spawn, then normal spawn and then enemy
+		evaluate_spawn_type(&eval, 1+(team&1));
+		if(!eval.got)
+		{
+			evaluate_spawn_type(&eval, 0);
+			if(!eval.got)
+				evaluate_spawn_type(&eval, 1+((team+1)&1));
+		}
+	}
+	else
+	{
+		if(game.controller->gametype == GAMETYPE_TDM)
+			eval.friendly_team = team;
+			
+		evaluate_spawn_type(&eval, 0);
+		evaluate_spawn_type(&eval, 1);
+		evaluate_spawn_type(&eval, 2);
+	}
+	
+	spawnpos = eval.pos;
+
+	// check if the position is occupado
+	ENTITY *ents[2] = {0};
+	int num_ents = game.world.find_entities(spawnpos, 64, ents, 2, NETOBJTYPE_CHARACTER);
+	
+	if(num_ents == 0)
+	{
+		spawning = false;
+		character.spawn(this, spawnpos, team);
+	}
+	
+	/*
+	pos = spawnpos;
+
+	core.pos = pos;
+	core.vel = vec2(0,0);
+	core.hooked_player = -1;
+
+	health = 10;
+	armor = 0;
+	jumped = 0;
+	
+	mem_zero(&ninja, sizeof(ninja));
+	
+	dead = false;
+	player_state = PLAYERSTATE_PLAYING;
+	
+	game.world.insert_entity(this);
+
+	core.hook_state = HOOK_IDLE;
+
+	mem_zero(&input, sizeof(input));
+
+	// init weapons
+	mem_zero(&weapons, sizeof(weapons));
+	weapons[WEAPON_HAMMER].got = true;
+	weapons[WEAPON_HAMMER].ammo = -1;
+	weapons[WEAPON_GUN].got = true;
+	weapons[WEAPON_GUN].ammo = 10;
+
+	active_weapon = WEAPON_GUN;
+	last_weapon = WEAPON_HAMMER;
+	queued_weapon = 0;
+
+	reload_timer = 0;
+
+	// Create sound and spawn effects
+	game.create_sound(pos, SOUND_PLAYER_SPAWN);
+	game.create_playerspawn(pos);
+
+	game.controller->on_player_spawn(player);
+	*/
+}
diff --git a/src/game/server/entities/character.hpp b/src/game/server/entities/character.hpp
new file mode 100644
index 00000000..6ceb987c
--- /dev/null
+++ b/src/game/server/entities/character.hpp
@@ -0,0 +1,122 @@
+/* copyright (c) 2007 magnus auvinen, see licence.txt for more info */
+
+#ifndef GAME_SERVER_ENTITY_CHARACTER_H
+#define GAME_SERVER_ENTITY_CHARACTER_H
+
+#include <game/server/entity.hpp>
+#include <game/generated/gs_data.hpp>
+#include <game/generated/g_protocol.hpp>
+
+#include <game/g_game.hpp>
+
+class CHARACTER : public ENTITY
+{
+public:
+	// player controlling this character
+	class PLAYER *player;
+	
+	bool alive;
+
+	// weapon info
+	ENTITY *hitobjects[10];
+	int numobjectshit;
+	struct WEAPONSTAT
+	{
+		int ammoregenstart;
+		int ammo;
+		int ammocost;
+		bool got;
+	} weapons[NUM_WEAPONS];
+	
+	int active_weapon;
+	int last_weapon;
+	int queued_weapon;
+	
+	int reload_timer;
+	int attack_tick;
+	
+	int damage_taken;
+
+	int emote_type;
+	int emote_stop;
+
+	// TODO: clean this up
+	char skin_name[64];
+	int use_custom_color;
+	int color_body;
+	int color_feet;
+	
+	int last_action; // last tick that the player took any action ie some input
+
+	// these are non-heldback inputs
+	NETOBJ_PLAYER_INPUT latest_previnput;
+	NETOBJ_PLAYER_INPUT latest_input;
+
+	// input	
+	NETOBJ_PLAYER_INPUT previnput;
+	NETOBJ_PLAYER_INPUT input;
+	int num_inputs;
+	int jumped;
+	
+	int damage_taken_tick;
+
+	int health;
+	int armor;
+
+	// ninja
+	struct
+	{
+		vec2 activationdir;
+		int activationtick;
+		int currentcooldown;
+		int currentmovetime;
+	} ninja;
+
+	//
+	//int score;
+	int team;
+	int player_state; // if the client is chatting, accessing a menu or so
+
+	// the player core for the physics	
+	CHARACTER_CORE core;
+
+	//
+	CHARACTER();
+	
+	virtual void reset();
+	virtual void destroy();
+		
+	bool is_grounded();
+	
+	void set_weapon(int w);
+	
+	void handle_weaponswitch();
+	void do_weaponswitch();
+	
+	int handle_weapons();
+	int handle_ninja();
+
+	void on_predicted_input(NETOBJ_PLAYER_INPUT *new_input);
+	void on_direct_input(NETOBJ_PLAYER_INPUT *new_input);
+	void fire_weapon();
+
+	void die(int killer, int weapon);
+
+	bool take_damage(vec2 force, int dmg, int from, int weapon);	
+
+	
+	bool spawn(PLAYER *player, vec2 pos, int team);
+	//bool init_tryspawn(int team);
+	bool remove();
+
+	static const int phys_size = 28;
+
+	virtual void tick();
+	virtual void tick_defered();
+	virtual void snap(int snaping_client);
+	
+	bool increase_health(int amount);
+	bool increase_armor(int amount);
+};
+
+#endif
diff --git a/src/game/server/entities/laser.cpp b/src/game/server/entities/laser.cpp
new file mode 100644
index 00000000..cedf7850
--- /dev/null
+++ b/src/game/server/entities/laser.cpp
@@ -0,0 +1,112 @@
+/* copyright (c) 2007 magnus auvinen, see licence.txt for more info */
+#include <engine/e_server_interface.h>
+#include <game/generated/g_protocol.hpp>
+#include <game/server/gs_common.hpp>
+#include "laser.hpp"
+
+//////////////////////////////////////////////////
+// laser
+//////////////////////////////////////////////////
+LASER::LASER(vec2 pos, vec2 direction, float start_energy, CHARACTER *owner)
+: ENTITY(NETOBJTYPE_LASER)
+{
+	this->pos = pos;
+	this->owner = owner;
+	energy = start_energy;
+	dir = direction;
+	bounces = 0;
+	do_bounce();
+	
+	game.world.insert_entity(this);
+}
+
+
+bool LASER::hit_character(vec2 from, vec2 to)
+{
+	vec2 at;
+	CHARACTER *hit = game.world.intersect_character(pos, to, 0.0f, at, owner);
+	if(!hit)
+		return false;
+
+	this->from = from;
+	pos = at;
+	energy = -1;		
+	hit->take_damage(vec2(0,0), tuning.laser_damage, owner->player->client_id, WEAPON_RIFLE);
+	return true;
+}
+
+void LASER::do_bounce()
+{
+	eval_tick = server_tick();
+	
+	if(energy < 0)
+	{
+		//dbg_msg("laser", "%d removed", server_tick());
+		game.world.destroy_entity(this);
+		return;
+	}
+	
+	vec2 to = pos + dir*energy;
+	
+	if(col_intersect_line(pos, to, &to))
+	{
+		if(!hit_character(pos, to))
+		{
+			// intersected
+			from = pos;
+			pos = to - dir*2;
+			vec2 temp_pos = pos;
+			vec2 temp_dir = dir*4.0f;
+			
+			move_point(&temp_pos, &temp_dir, 1.0f, 0);
+			pos = temp_pos;
+			dir = normalize(temp_dir);
+			
+			energy -= distance(from, pos) + tuning.laser_bounce_cost;
+			bounces++;
+			
+			if(bounces > tuning.laser_bounce_num)
+				energy = -1;
+				
+			game.create_sound(pos, SOUND_RIFLE_BOUNCE);
+		}
+	}
+	else
+	{
+		if(!hit_character(pos, to))
+		{
+			from = pos;
+			pos = to;
+			energy = -1;
+		}
+	}
+		
+	//dbg_msg("laser", "%d done %f %f %f %f", server_tick(), from.x, from.y, pos.x, pos.y);
+}
+	
+void LASER::reset()
+{
+	game.world.destroy_entity(this);
+}
+
+void LASER::tick()
+{
+	if(server_tick() > eval_tick+(server_tickspeed()*tuning.laser_bounce_delay)/1000.0f)
+	{
+		do_bounce();
+	}
+
+}
+
+void LASER::snap(int snapping_client)
+{
+	if(distance(game.players[snapping_client].view_pos, pos) > 1000.0f)
+		return;
+
+	NETOBJ_LASER *obj = (NETOBJ_LASER *)snap_new_item(NETOBJTYPE_LASER, id, sizeof(NETOBJ_LASER));
+	obj->x = (int)pos.x;
+	obj->y = (int)pos.y;
+	obj->from_x = (int)from.x;
+	obj->from_y = (int)from.y;
+	obj->start_tick = eval_tick;
+}
diff --git a/src/game/server/entities/laser.hpp b/src/game/server/entities/laser.hpp
new file mode 100644
index 00000000..4842b3f8
--- /dev/null
+++ b/src/game/server/entities/laser.hpp
@@ -0,0 +1,31 @@
+/* copyright (c) 2007 magnus auvinen, see licence.txt for more info */
+
+#ifndef GAME_SERVER_ENTITY_LASER_H
+#define GAME_SERVER_ENTITY_LASER_H
+
+#include <game/server/entity.hpp>
+
+class CHARACTER;
+
+class LASER : public ENTITY
+{
+	vec2 from;
+	vec2 dir;
+	float energy;
+	int bounces;
+	int eval_tick;
+	CHARACTER *owner;
+	
+	bool hit_character(vec2 from, vec2 to);
+	void do_bounce();
+	
+public:
+	
+	LASER(vec2 pos, vec2 direction, float start_energy, CHARACTER *owner);
+	
+	virtual void reset();
+	virtual void tick();
+	virtual void snap(int snapping_client);
+};
+
+#endif
diff --git a/src/game/server/entities/pickup.cpp b/src/game/server/entities/pickup.cpp
new file mode 100644
index 00000000..a4a3a2c9
--- /dev/null
+++ b/src/game/server/entities/pickup.cpp
@@ -0,0 +1,142 @@
+#include <engine/e_server_interface.h>
+#include <game/generated/g_protocol.hpp>
+#include <game/server/gs_common.hpp>
+#include "pickup.hpp"
+
+//////////////////////////////////////////////////
+// pickup
+//////////////////////////////////////////////////
+PICKUP::PICKUP(int _type, int _subtype)
+: ENTITY(NETOBJTYPE_PICKUP)
+{
+	type = _type;
+	subtype = _subtype;
+	proximity_radius = phys_size;
+
+	reset();
+
+	// TODO: should this be done here?
+	game.world.insert_entity(this);
+}
+
+void PICKUP::reset()
+{
+	if (data->pickups[type].spawndelay > 0)
+		spawntick = server_tick() + server_tickspeed() * data->pickups[type].spawndelay;
+	else
+		spawntick = -1;
+}
+
+void PICKUP::tick()
+{
+	// wait for respawn
+	if(spawntick > 0)
+	{
+		if(server_tick() > spawntick)
+		{
+			// respawn
+			spawntick = -1;
+
+			if(type == POWERUP_WEAPON)
+				game.create_sound(pos, SOUND_WEAPON_SPAWN);
+		}
+		else
+			return;
+	}
+	// Check if a player intersected us
+	CHARACTER *chr = game.world.closest_character(pos, 20.0f, 0);
+	if(chr)
+	{
+		// player picked us up, is someone was hooking us, let them go
+		int respawntime = -1;
+		switch (type)
+		{
+		case POWERUP_HEALTH:
+			if(chr->increase_health(1))
+			{
+				game.create_sound(pos, SOUND_PICKUP_HEALTH);
+				respawntime = data->pickups[type].respawntime;
+			}
+			break;
+		case POWERUP_ARMOR:
+			if(chr->increase_armor(1))
+			{
+				game.create_sound(pos, SOUND_PICKUP_ARMOR);
+				respawntime = data->pickups[type].respawntime;
+			}
+			break;
+
+		case POWERUP_WEAPON:
+			if(subtype >= 0 && subtype < NUM_WEAPONS)
+			{
+				if(chr->weapons[subtype].ammo < data->weapons.id[subtype].maxammo || !chr->weapons[subtype].got)
+				{
+					chr->weapons[subtype].got = true;
+					chr->weapons[subtype].ammo = min(data->weapons.id[subtype].maxammo, chr->weapons[subtype].ammo + 10);
+					respawntime = data->pickups[type].respawntime;
+
+					// TODO: data compiler should take care of stuff like this
+					if(subtype == WEAPON_GRENADE)
+						game.create_sound(pos, SOUND_PICKUP_GRENADE);
+					else if(subtype == WEAPON_SHOTGUN)
+						game.create_sound(pos, SOUND_PICKUP_SHOTGUN);
+					else if(subtype == WEAPON_RIFLE)
+						game.create_sound(pos, SOUND_PICKUP_SHOTGUN);
+
+					if(chr->player)
+                    	game.send_weapon_pickup(chr->player->client_id, subtype);
+				}
+			}
+			break;
+		case POWERUP_NINJA:
+			{
+				// activate ninja on target player
+				chr->ninja.activationtick = server_tick();
+				chr->weapons[WEAPON_NINJA].got = true;
+				chr->last_weapon = chr->active_weapon;
+				chr->active_weapon = WEAPON_NINJA;
+				respawntime = data->pickups[type].respawntime;
+				game.create_sound(pos, SOUND_PICKUP_NINJA);
+
+				// loop through all players, setting their emotes
+				ENTITY *ents[64];
+				int num = game.world.find_entities(vec2(0, 0), 1000000, ents, 64, NETOBJTYPE_CHARACTER);
+				for (int i = 0; i < num; i++)
+				{
+					CHARACTER *c = (CHARACTER *)ents[i];
+					if (c != chr)
+					{
+						c->emote_type = EMOTE_SURPRISE;
+						c->emote_stop = server_tick() + server_tickspeed();
+					}
+				}
+
+				chr->emote_type = EMOTE_ANGRY;
+				chr->emote_stop = server_tick() + 1200 * server_tickspeed() / 1000;
+				
+				break;
+			}
+		default:
+			break;
+		};
+
+		if(respawntime >= 0)
+		{
+			dbg_msg("game", "pickup player='%d:%s' item=%d/%d",
+				chr->player->client_id, server_clientname(chr->player->client_id), type, subtype);
+			spawntick = server_tick() + server_tickspeed() * respawntime;
+		}
+	}
+}
+
+void PICKUP::snap(int snapping_client)
+{
+	if(spawntick != -1)
+		return;
+
+	NETOBJ_PICKUP *up = (NETOBJ_PICKUP *)snap_new_item(NETOBJTYPE_PICKUP, id, sizeof(NETOBJ_PICKUP));
+	up->x = (int)pos.x;
+	up->y = (int)pos.y;
+	up->type = type; // TODO: two diffrent types? what gives?
+	up->subtype = subtype;
+}
diff --git a/src/game/server/entities/pickup.hpp b/src/game/server/entities/pickup.hpp
new file mode 100644
index 00000000..cd480d92
--- /dev/null
+++ b/src/game/server/entities/pickup.hpp
@@ -0,0 +1,24 @@
+/* copyright (c) 2007 magnus auvinen, see licence.txt for more info */
+
+#ifndef GAME_SERVER_ENTITY_PICKUP_H
+#define GAME_SERVER_ENTITY_PICKUP_H
+
+#include <game/server/entity.hpp>
+
+// TODO: move to seperate file
+class PICKUP : public ENTITY
+{
+public:
+	static const int phys_size = 14;
+	
+	int type;
+	int subtype; // weapon type for instance?
+	int spawntick;
+	PICKUP(int _type, int _subtype = 0);
+	
+	virtual void reset();
+	virtual void tick();
+	virtual void snap(int snapping_client);
+};
+
+#endif
diff --git a/src/game/server/entities/projectile.cpp b/src/game/server/entities/projectile.cpp
new file mode 100644
index 00000000..d258e69e
--- /dev/null
+++ b/src/game/server/entities/projectile.cpp
@@ -0,0 +1,108 @@
+#include <engine/e_server_interface.h>
+#include <game/generated/g_protocol.hpp>
+#include <game/server/gs_common.hpp>
+#include "projectile.hpp"
+
+
+//////////////////////////////////////////////////
+// projectile
+//////////////////////////////////////////////////
+PROJECTILE::PROJECTILE(int type, int owner, vec2 pos, vec2 dir, int span, ENTITY* powner,
+	int damage, int flags, float force, int sound_impact, int weapon)
+: ENTITY(NETOBJTYPE_PROJECTILE)
+{
+	this->type = type;
+	this->pos = pos;
+	this->direction = dir;
+	this->lifespan = span;
+	this->owner = owner;
+	this->powner = powner;
+	this->flags = flags;
+	this->force = force;
+	this->damage = damage;
+	this->sound_impact = sound_impact;
+	this->weapon = weapon;
+	this->bounce = 0;
+	this->start_tick = server_tick();
+	game.world.insert_entity(this);
+}
+
+void PROJECTILE::reset()
+{
+	game.world.destroy_entity(this);
+}
+
+vec2 PROJECTILE::get_pos(float time)
+{
+	float curvature = 0;
+	float speed = 0;
+	if(type == WEAPON_GRENADE)
+	{
+		curvature = tuning.grenade_curvature;
+		speed = tuning.grenade_speed;
+	}
+	else if(type == WEAPON_SHOTGUN)
+	{
+		curvature = tuning.shotgun_curvature;
+		speed = tuning.shotgun_speed;
+	}
+	else if(type == WEAPON_GUN)
+	{
+		curvature = tuning.gun_curvature;
+		speed = tuning.gun_speed;
+	}
+	
+	return calc_pos(pos, direction, curvature, speed, time);
+}
+
+
+void PROJECTILE::tick()
+{
+	
+	float pt = (server_tick()-start_tick-1)/(float)server_tickspeed();
+	float ct = (server_tick()-start_tick)/(float)server_tickspeed();
+	vec2 prevpos = get_pos(pt);
+	vec2 curpos = get_pos(ct);
+
+	lifespan--;
+	
+	int collide = col_intersect_line(prevpos, curpos, &curpos);
+	//int collide = col_check_point((int)curpos.x, (int)curpos.y);
+	
+	CHARACTER *targetchr = game.world.intersect_character(prevpos, curpos, 6.0f, curpos, powner);
+	if(targetchr || collide || lifespan < 0)
+	{
+		if(lifespan >= 0 || weapon == WEAPON_GRENADE)
+			game.create_sound(curpos, sound_impact);
+
+		if(flags & PROJECTILE_FLAGS_EXPLODE)
+			game.create_explosion(curpos, owner, weapon, false);
+		else if(targetchr)
+		{
+			targetchr->take_damage(direction * max(0.001f, force), damage, owner, weapon);
+		}
+
+		game.world.destroy_entity(this);
+	}
+}
+
+void PROJECTILE::fill_info(NETOBJ_PROJECTILE *proj)
+{
+	proj->x = (int)pos.x;
+	proj->y = (int)pos.y;
+	proj->vx = (int)(direction.x*100.0f);
+	proj->vy = (int)(direction.y*100.0f);
+	proj->start_tick = start_tick;
+	proj->type = type;
+}
+
+void PROJECTILE::snap(int snapping_client)
+{
+	float ct = (server_tick()-start_tick)/(float)server_tickspeed();
+	
+	if(distance(game.players[snapping_client].view_pos, get_pos(ct)) > 1000.0f)
+		return;
+
+	NETOBJ_PROJECTILE *proj = (NETOBJ_PROJECTILE *)snap_new_item(NETOBJTYPE_PROJECTILE, id, sizeof(NETOBJ_PROJECTILE));
+	fill_info(proj);
+}
diff --git a/src/game/server/entities/projectile.hpp b/src/game/server/entities/projectile.hpp
new file mode 100644
index 00000000..c1370af1
--- /dev/null
+++ b/src/game/server/entities/projectile.hpp
@@ -0,0 +1,38 @@
+/* copyright (c) 2007 magnus auvinen, see licence.txt for more info */
+
+#ifndef GAME_SERVER_ENTITY_PROJECTILE_H
+#define GAME_SERVER_ENTITY_PROJECTILE_H
+
+class PROJECTILE : public ENTITY
+{
+public:
+	enum
+	{
+		PROJECTILE_FLAGS_EXPLODE = 1 << 0,
+	};
+	
+	vec2 direction;
+	ENTITY *powner; // this is nasty, could be removed when client quits
+	int lifespan;
+	int owner;
+	int type;
+	int flags;
+	int damage;
+	int sound_impact;
+	int weapon;
+	int bounce;
+	float force;
+	int start_tick;
+	
+	PROJECTILE(int type, int owner, vec2 pos, vec2 vel, int span, ENTITY* powner,
+		int damage, int flags, float force, int sound_impact, int weapon);
+
+	vec2 get_pos(float time);
+	void fill_info(NETOBJ_PROJECTILE *proj);
+
+	virtual void reset();
+	virtual void tick();
+	virtual void snap(int snapping_client);
+};
+
+#endif