r2454 - in branches/nexuiz-2.0/data: . models models/keyhunt models/sprites qcsrc/server

DONOTREPLY at icculus.org DONOTREPLY at icculus.org
Wed May 2 14:09:26 EDT 2007


Author: div0
Date: 2007-05-02 14:09:25 -0400 (Wed, 02 May 2007)
New Revision: 2454

Added:
   branches/nexuiz-2.0/data/models/keyhunt/
   branches/nexuiz-2.0/data/models/sprites/key-dropped.sp2
   branches/nexuiz-2.0/data/models/sprites/key-dropped.tga
   branches/nexuiz-2.0/data/models/sprites/keycarrier-blue.sp2
   branches/nexuiz-2.0/data/models/sprites/keycarrier-blue.tga
   branches/nexuiz-2.0/data/models/sprites/keycarrier-finish.sp2
   branches/nexuiz-2.0/data/models/sprites/keycarrier-finish.tga
   branches/nexuiz-2.0/data/models/sprites/keycarrier-friend.sp2
   branches/nexuiz-2.0/data/models/sprites/keycarrier-friend.tga
   branches/nexuiz-2.0/data/models/sprites/keycarrier-pink.sp2
   branches/nexuiz-2.0/data/models/sprites/keycarrier-pink.tga
   branches/nexuiz-2.0/data/models/sprites/keycarrier-red.sp2
   branches/nexuiz-2.0/data/models/sprites/keycarrier-red.tga
   branches/nexuiz-2.0/data/models/sprites/keycarrier-yellow.sp2
   branches/nexuiz-2.0/data/models/sprites/keycarrier-yellow.tga
   branches/nexuiz-2.0/data/qcsrc/server/keyhunt.qc
   branches/nexuiz-2.0/data/qcsrc/server/keyhunt.qh
Modified:
   branches/nexuiz-2.0/data/default.cfg
   branches/nexuiz-2.0/data/game_reset.cfg
   branches/nexuiz-2.0/data/models/keyhunt/key.md3
   branches/nexuiz-2.0/data/qcsrc/server/bots.qc
   branches/nexuiz-2.0/data/qcsrc/server/builtins.qh
   branches/nexuiz-2.0/data/qcsrc/server/cl_client.qc
   branches/nexuiz-2.0/data/qcsrc/server/cl_impulse.qc
   branches/nexuiz-2.0/data/qcsrc/server/cl_player.qc
   branches/nexuiz-2.0/data/qcsrc/server/cl_weaponsystem.qc
   branches/nexuiz-2.0/data/qcsrc/server/clientcommands.qc
   branches/nexuiz-2.0/data/qcsrc/server/constants.qh
   branches/nexuiz-2.0/data/qcsrc/server/ctf.qc
   branches/nexuiz-2.0/data/qcsrc/server/defs.qh
   branches/nexuiz-2.0/data/qcsrc/server/domination.qc
   branches/nexuiz-2.0/data/qcsrc/server/g_damage.qc
   branches/nexuiz-2.0/data/qcsrc/server/g_world.qc
   branches/nexuiz-2.0/data/qcsrc/server/havocbot.qc
   branches/nexuiz-2.0/data/qcsrc/server/havocbot_roles.qc
   branches/nexuiz-2.0/data/qcsrc/server/miscfunctions.qc
   branches/nexuiz-2.0/data/qcsrc/server/progs.src
   branches/nexuiz-2.0/data/qcsrc/server/t_items.qc
   branches/nexuiz-2.0/data/qcsrc/server/teamplay.qc
   branches/nexuiz-2.0/data/qcsrc/server/w_nex.qc
   branches/nexuiz-2.0/data/qcsrc/server/waypointsprites.qc
Log:
- New game mode: Key Hunt
- switched yellow and pink team
- play2 function
- Weapon throwing uses Newton
- minplayers/bot_number no longer get changed by QC code (prevents suddenly appearing bot_number)
- info_player_deathmatch now does all the work, including setting classname
- fix a division by zero message in havocbots (annoying when developer 1, but harmless)



Modified: branches/nexuiz-2.0/data/default.cfg
===================================================================
--- branches/nexuiz-2.0/data/default.cfg	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/default.cfg	2007-05-02 18:09:25 UTC (rev 2454)
@@ -236,6 +236,7 @@
 seta g_ctf_capture_limit -1
 seta g_domination_point_limit -1
 seta g_runematch_point_limit -1
+set g_keyhunt_point_limit -1
 
 // respawn delay
 set g_respawn_delay 2
@@ -713,3 +714,25 @@
 alias g_maplist_remove     "qc_cmd rpn /g_maplist g_maplist /$1 difference def"
 alias g_maplist_putfirst   "qc_cmd rpn /maps/$1.mapcfg fexists_assert /g_maplist /$1 g_maplist union def"
 alias g_maplist_shufflenow "qc_cmd rpn /g_maplist g_maplist shuffle def"
+
+// key hunt
+set g_keyhunt 0
+set g_balance_keyhunt_delay_return 30
+set g_balance_keyhunt_delay_round 5
+set g_balance_keyhunt_delay_tracking 10
+set g_balance_keyhunt_delay_fadeout 2
+set g_balance_keyhunt_delay_collect 1.5
+set g_balance_keyhunt_delay_drop 0.4
+set g_balance_keyhunt_maxdist 150
+set g_balance_keyhunt_score_collect 3
+set g_balance_keyhunt_score_carrierfrag 2
+set g_balance_keyhunt_score_capture 100
+set g_balance_keyhunt_score_push 60
+set g_balance_keyhunt_score_destroyed 50
+set g_balance_keyhunt_score_destroyed_ownfactor 1
+set g_balance_keyhunt_dropvelocity 300
+set g_balance_keyhunt_throwvelocity 400
+set g_balance_keyhunt_protecttime 0.8
+set g_balance_keyhunt_damageforcescale 1
+set g_keyhunt_teams_override 0
+set g_keyhunt_teams 0 // mapcfgs NEED to specify this!

Modified: branches/nexuiz-2.0/data/game_reset.cfg
===================================================================
--- branches/nexuiz-2.0/data/game_reset.cfg	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/game_reset.cfg	2007-05-02 18:09:25 UTC (rev 2454)
@@ -14,6 +14,7 @@
 set g_lms 0
 set g_arena 0
 set g_campaign 0
+set g_keyhunt 0
 set teamplay 0
 set gamecfg 0
 

Copied: branches/nexuiz-2.0/data/models/keyhunt (from rev 2414, trunk/data/models/keyhunt)

Modified: branches/nexuiz-2.0/data/models/keyhunt/key.md3
===================================================================
(Binary files differ)

Copied: branches/nexuiz-2.0/data/models/sprites/key-dropped.sp2 (from rev 2414, trunk/data/models/sprites/key-dropped.sp2)
===================================================================
(Binary files differ)

Copied: branches/nexuiz-2.0/data/models/sprites/key-dropped.tga (from rev 2414, trunk/data/models/sprites/key-dropped.tga)
===================================================================
(Binary files differ)

Copied: branches/nexuiz-2.0/data/models/sprites/keycarrier-blue.sp2 (from rev 2414, trunk/data/models/sprites/keycarrier-blue.sp2)
===================================================================
(Binary files differ)

Copied: branches/nexuiz-2.0/data/models/sprites/keycarrier-blue.tga (from rev 2414, trunk/data/models/sprites/keycarrier-blue.tga)
===================================================================
(Binary files differ)

Copied: branches/nexuiz-2.0/data/models/sprites/keycarrier-finish.sp2 (from rev 2414, trunk/data/models/sprites/keycarrier-finish.sp2)
===================================================================
(Binary files differ)

Copied: branches/nexuiz-2.0/data/models/sprites/keycarrier-finish.tga (from rev 2414, trunk/data/models/sprites/keycarrier-finish.tga)
===================================================================
(Binary files differ)

Copied: branches/nexuiz-2.0/data/models/sprites/keycarrier-friend.sp2 (from rev 2414, trunk/data/models/sprites/keycarrier-friend.sp2)
===================================================================
(Binary files differ)

Copied: branches/nexuiz-2.0/data/models/sprites/keycarrier-friend.tga (from rev 2414, trunk/data/models/sprites/keycarrier-friend.tga)
===================================================================
(Binary files differ)

Copied: branches/nexuiz-2.0/data/models/sprites/keycarrier-pink.sp2 (from rev 2414, trunk/data/models/sprites/keycarrier-pink.sp2)
===================================================================
(Binary files differ)

Copied: branches/nexuiz-2.0/data/models/sprites/keycarrier-pink.tga (from rev 2414, trunk/data/models/sprites/keycarrier-pink.tga)
===================================================================
(Binary files differ)

Copied: branches/nexuiz-2.0/data/models/sprites/keycarrier-red.sp2 (from rev 2414, trunk/data/models/sprites/keycarrier-red.sp2)
===================================================================
(Binary files differ)

Copied: branches/nexuiz-2.0/data/models/sprites/keycarrier-red.tga (from rev 2414, trunk/data/models/sprites/keycarrier-red.tga)
===================================================================
(Binary files differ)

Copied: branches/nexuiz-2.0/data/models/sprites/keycarrier-yellow.sp2 (from rev 2414, trunk/data/models/sprites/keycarrier-yellow.sp2)
===================================================================
(Binary files differ)

Copied: branches/nexuiz-2.0/data/models/sprites/keycarrier-yellow.tga (from rev 2414, trunk/data/models/sprites/keycarrier-yellow.tga)
===================================================================
(Binary files differ)

Modified: branches/nexuiz-2.0/data/qcsrc/server/bots.qc
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/bots.qc	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/bots.qc	2007-05-02 18:09:25 UTC (rev 2454)
@@ -2018,6 +2018,7 @@
 
 
 float botframe_spawnedwaypoints;
+float botframe_nextthink;
 void() bot_serverframe =
 {
 	float realplayers, bots;
@@ -2038,17 +2039,13 @@
 	{
 		float realminplayers, minplayers;
 		realminplayers = cvar("minplayers");
-		minplayers = bound(0, floor(realminplayers), maxclients - 1);
-		if (realminplayers != minplayers)
-			cvar_set("minplayers", ftos(minplayers));
+		minplayers = max(0, floor(realminplayers));
 
 		float realminbots, minbots;
 		realminbots = cvar("bot_number");
-		minbots = bound(0, floor(realminbots), maxclients - 1);
-		if (realminbots != minbots)
-			cvar_set("bot_number", ftos(minbots));
+		minbots = max(0, floor(realminbots));
 
-		bots = bound(minbots, minplayers - realplayers, maxclients);
+		bots = min(max(minbots, minplayers - realplayers), maxclients - realplayers);
 	}
 	else
 	{
@@ -2059,18 +2056,21 @@
 	bot_ignore_bots = cvar("bot_ignore_bots");
 
 	// only add one bot per frame to avoid utter chaos
-	while (currentbots < bots)
+	if(time > botframe_nextthink)
 	{
-		if (bot_spawn() == world)
+		//dprint(ftos(bots), " ? ", ftos(currentbots), "\n");
+		while (currentbots < bots)
 		{
-			bprint("Can not add bot, server full.\n");
-			cvar_set("bot_number", ftos(currentbots));
-			bots = currentbots;
-			break;
+			if (bot_spawn() == world)
+			{
+				bprint("Can not add bot, server full.\n");
+				botframe_nextthink = time + 10;
+				break;
+			}
 		}
+		while (currentbots > bots)
+			bot_removenewest();
 	}
-	while (currentbots > bots)
-		bot_removenewest();
 
 	if(botframe_spawnedwaypoints)
 	{
@@ -2078,7 +2078,7 @@
 			localcmd("quit\n");
 	}
 
-	if (bots > 0 || cvar("g_waypointeditor"))
+	if (currentbots > 0 || cvar("g_waypointeditor"))
 	if (!botframe_spawnedwaypoints)
 	{
 		botframe_spawnedwaypoints = TRUE;

Modified: branches/nexuiz-2.0/data/qcsrc/server/builtins.qh
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/builtins.qh	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/builtins.qh	2007-05-02 18:09:25 UTC (rev 2454)
@@ -17,7 +17,7 @@
 void	traceline (vector v1, vector v2, float nomonst, entity forent)			= #16;
 entity	checkclient (void)								= #17;
 entity	find (entity start, .string fld, string match)					= #18;
-string	precache_sound (string s)							= #19;
+//string	precache_sound (string s)							= #19;
 string	precache_model (string s)							= #20;
 //void(entity client, string s)stuffcmd = #21;
 entity	findradius (vector org, float rad)						= #22;

Modified: branches/nexuiz-2.0/data/qcsrc/server/cl_client.qc
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/cl_client.qc	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/cl_client.qc	2007-05-02 18:09:25 UTC (rev 2454)
@@ -1,11 +1,11 @@
 void info_player_start (void)
 {
-	self.classname = "info_player_deathmatch";
-	relocate_spawnpoint();
+	info_player_deathmatch();
 }
 
 void info_player_deathmatch (void)
 {
+	self.classname = "info_player_deathmatch";
 	relocate_spawnpoint();
 }
 
@@ -287,13 +287,14 @@
 		WriteEntity(MSG_ONE, self);
 	}
 
-	WaypointSprite_PlayerDead();
-
 	DropAllRunes(self);
+	kh_Key_DropAll(self);
 
 	if(self.flagcarried)
 		DropFlag(self.flagcarried);
 
+	WaypointSprite_PlayerDead();
+
 	DistributeFragsAmongTeam(self, self.team, 1);
 
 	if(self.frags <= 0 && self.frags > -666 && cvar("g_lms") && self.killcount != -666)
@@ -831,6 +832,7 @@
 	WaypointSprite_PlayerGone();
 
 	DropAllRunes(self);
+	kh_Key_DropAll(self);
 
 	if(self.flagcarried)
 		DropFlag(self.flagcarried);
@@ -1239,7 +1241,7 @@
 		{
 			self.respawn_countdown = number - 1;
 			if(ceil(self.death_time - (time + 0.5)) == number) // only say it if it is the same number even in 0.5s; to prevent overlapping sounds
-				stuffcmd(self, strcat("play2 announcer/robotic/", ftos(number), ".ogg\n"));
+				play2(self, strcat("announcer/robotic/", ftos(number), ".ogg"));
 		}
 	}
 }
@@ -1508,6 +1510,7 @@
 			minstagib_ammocheck();
 
 		ctf_setstatus();
+		kh_setstatus();
 
 		//self.angles_y=self.v_angle_y + 90;   // temp
 

Modified: branches/nexuiz-2.0/data/qcsrc/server/cl_impulse.qc
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/cl_impulse.qc	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/cl_impulse.qc	2007-05-02 18:09:25 UTC (rev 2454)
@@ -118,7 +118,7 @@
 			if (self.weapon != WEP_LASER
 				&& !cvar("g_minstagib") && !cvar("g_instagib")
 				&& !cvar("g_rocketarena") && !cvar("g_lms") && cvar("g_pickup_items") && !cvar("g_nixnex"))
-				W_ThrowWeapon(self.velocity * 0.5 + v_forward * 750, '0 0 0', TRUE);
+				W_ThrowWeapon(W_CalculateProjectileVelocity(self.velocity, v_forward * 750), '0 0 0', TRUE);
 		}
 	}
 	// deploy waypoints

Modified: branches/nexuiz-2.0/data/qcsrc/server/cl_player.qc
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/cl_player.qc	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/cl_player.qc	2007-05-02 18:09:25 UTC (rev 2454)
@@ -285,8 +285,6 @@
 	{
 		self.deaths += 1;
 
-		// clear waypoints
-		WaypointSprite_PlayerDead();
 		// become fully visible
 		self.alpha = 1;
 		// clear selected player display
@@ -296,8 +294,11 @@
 		// print an obituary message
 		Obituary (attacker, self, deathtype);
 		DropAllRunes(self);
+		kh_Key_DropAll(self);
 		if(self.flagcarried)
 			DropFlag(self.flagcarried);
+		// clear waypoints
+		WaypointSprite_PlayerDead();
 		// make the corpse upright (not tilted)
 		self.angles_x = 0;
 		self.angles_z = 0;

Modified: branches/nexuiz-2.0/data/qcsrc/server/cl_weaponsystem.qc
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/cl_weaponsystem.qc	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/cl_weaponsystem.qc	2007-05-02 18:09:25 UTC (rev 2454)
@@ -404,20 +404,14 @@
 	// VorteX: haste can be added here
 };
 
-void(entity missile) W_SetupProjectileVelocity =
+vector(vector pvelocity, vector mvelocity) W_CalculateProjectileVelocity =
 {
-	vector pvelocity;
 	vector mdirection;
 	float mspeed;
 	float outspeed;
 	float nstyle;
-	vector mvelocity;
 	vector outvelocity;
 
-	if(missile.owner == world)
-		error("Unowned missile");
-	pvelocity = missile.owner.velocity;
-	mvelocity = missile.velocity;
 	mdirection = normalize(mvelocity);
 	mspeed = vlen(mvelocity);
 
@@ -478,7 +472,13 @@
 	else
 		error("g_projectiles_newton_style must be 0 (absolute), 1 (Newtonian), 2 (Newtonian + aimfix), 3 (pseudo Newtonian) or 4 (tZorkian)!");
 
-	//dprint("Adjusted from ", vtos(missile.velocity));
-	missile.velocity = outvelocity;
-	//dprint(" to ", vtos(missile.velocity), "\n");
+	return outvelocity;
 }
+
+void(entity missile) W_SetupProjectileVelocity =
+{
+	if(missile.owner == world)
+		error("Unowned missile");
+
+	missile.velocity = W_CalculateProjectileVelocity(missile.owner.velocity, missile.velocity);
+}

Modified: branches/nexuiz-2.0/data/qcsrc/server/clientcommands.qc
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/clientcommands.qc	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/clientcommands.qc	2007-05-02 18:09:25 UTC (rev 2454)
@@ -319,6 +319,8 @@
 		if(self.classname == "player" && cvar("sv_spectate") == 1) {
 			if(self.flagcarried)
 				DropFlag(self.flagcarried);
+			kh_Key_DropAll(self);
+			WaypointSprite_PlayerDead();
 			DistributeFragsAmongTeam(self, self.team, 1.0);
 			self.classname = "observer";
 			PutClientInServer();
@@ -343,15 +345,15 @@
 			SV_ChangeTeam( COLOR_TEAM1 - 1 );
 		} else if( argv(1) == "blue" ) {
 			SV_ChangeTeam( COLOR_TEAM2 - 1 );
+		} else if( argv(1) == "yellow" ) {
+			SV_ChangeTeam( COLOR_TEAM3 - 1 );
 		} else if( argv(1) == "pink" ) {
-			SV_ChangeTeam( COLOR_TEAM3 - 1 );
-		} else if( argv(1) == "yellow" ) {
 			SV_ChangeTeam( COLOR_TEAM4 - 1 );
 		} else if( argv(1) == "auto" ) {
 			self.team = -1;
 			JoinBestTeam( self, 0 );
 		} else {
-			sprint( self, strcat( "selectteam none/red/blue/pink/yellow/auto - \"", argv(1), "\" not recognised\n" ) );
+			sprint( self, strcat( "selectteam none/red/blue/yellow/pink/auto - \"", argv(1), "\" not recognised\n" ) );
 		}
 	} else if(argv(0) == "ready") {
 		if(cvar("sv_ready_restart"))

Modified: branches/nexuiz-2.0/data/qcsrc/server/constants.qh
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/constants.qh	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/constants.qh	2007-05-02 18:09:25 UTC (rev 2454)
@@ -220,7 +220,7 @@
 
 float COLOR_TEAM1	= 5;  // red
 float COLOR_TEAM2	= 14; // blue
-float COLOR_TEAM3	= 10; // pink
-float COLOR_TEAM4	= 13; // yellow
+float COLOR_TEAM3	= 13; // yellow
+float COLOR_TEAM4	= 10; // pink
 
 float NUM_PLAYERSKINS_TEAMPLAY = 3;

Modified: branches/nexuiz-2.0/data/qcsrc/server/ctf.qc
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/ctf.qc	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/ctf.qc	2007-05-02 18:09:25 UTC (rev 2454)
@@ -234,7 +234,7 @@
 		other.next_take_time = time + 1;
 	}
 	if (self.cnt == FLAG_BASE)
-	if (other.team == 5 || other.team == 14) // only red and blue team can steal flags
+	if (other.team == COLOR_TEAM1 || other.team == COLOR_TEAM2) // only red and blue team can steal flags
 	if (other.team != self.team)
 	if (!other.flagcarried)
 	{
@@ -268,11 +268,11 @@
 	if (self.cnt == FLAG_DROPPED)
 	{
 		self.flags = FL_ITEM; // clear FL_ONGROUND and any other junk
-		if (other.team == self.team || (other.team != 5 && other.team != 14))
+		if (other.team == self.team || (other.team != COLOR_TEAM1 && other.team != COLOR_TEAM2))
 		{
 			// return flag
 			bprint(other.netname, "^7 returned the ", self.netname, "\n");
-			if (other.team == 5 || other.team == 14)
+			if (other.team == COLOR_TEAM1 || other.team == COLOR_TEAM2)
 				UpdateFrags(other, cvar("g_ctf_flagscore_return"));
 			else
 				UpdateFrags(other, cvar("g_ctf_flagscore_return_rogue"));
@@ -315,9 +315,8 @@
 */
 void() info_player_team1 =
 {
-	self.classname = "info_player_deathmatch";
-	self.team = 5; // red
-	relocate_spawnpoint();
+	self.team = COLOR_TEAM1; // red
+	info_player_deathmatch();
 };
 //self.team = 4;self.classname = "info_player_start";info_player_start();};
 
@@ -331,9 +330,8 @@
 */
 void() info_player_team2 =
 {
-	self.classname = "info_player_deathmatch";
-	self.team = 14; // blue
-	relocate_spawnpoint();
+	self.team = COLOR_TEAM2; // blue
+	info_player_deathmatch();
 };
 //self.team = 13;self.classname = "info_player_start";info_player_start();};
 
@@ -347,9 +345,8 @@
 */
 void() info_player_team3 =
 {
-	self.classname = "info_player_deathmatch";
-	self.team = 10; // purple
-	relocate_spawnpoint();
+	self.team = COLOR_TEAM3; // purple
+	info_player_deathmatch();
 };
 
 
@@ -363,9 +360,8 @@
 */
 void() info_player_team4 =
 {
-	self.classname = "info_player_deathmatch";
-	self.team = 13; // yellow
-	relocate_spawnpoint();
+	self.team = COLOR_TEAM4; // yellow
+	info_player_deathmatch();
 };
 
 
@@ -404,7 +400,7 @@
 	//	cvar_set("teamplay", "3");
 
 	self.classname = "item_flag_team1";
-	self.team = 5; // color 4 team (red)
+	self.team = COLOR_TEAM1; // color 4 team (red)
 	self.items = IT_KEY2; // gold key (redish enough)
 	self.netname = "^1RED^7 flag";
 	self.target = "###item###";
@@ -460,7 +456,7 @@
 	//	cvar_set("teamplay", "3");
 
 	self.classname = "item_flag_team2";
-	self.team = 14; // color 13 team (blue)
+	self.team = COLOR_TEAM2; // color 13 team (blue)
 	self.items = IT_KEY1; // silver key (bluish enough)
 	self.netname = "^4BLUE^7 flag";
 	self.target = "###item###";
@@ -553,8 +549,8 @@
 
 	numteams = 2;//cvar("g_ctf_default_teams");
 
-	ctf_spawnteam("Red", 4);
-	ctf_spawnteam("Blue", 13);
+	ctf_spawnteam("Red", COLOR_TEAM1 - 1);
+	ctf_spawnteam("Blue", COLOR_TEAM2 - 1);
 };
 
 void() ctf_delayedinit =
@@ -578,8 +574,8 @@
 {
 	if (flag) {
 		local float shift;
-		if (flag.team == 5) shift = IT_RED_FLAG_TAKEN;
-		else if (flag.team == 14) shift = IT_BLUE_FLAG_TAKEN;
+		if (flag.team == COLOR_TEAM1) shift = IT_RED_FLAG_TAKEN;
+		else if (flag.team == COLOR_TEAM2) shift = IT_BLUE_FLAG_TAKEN;
 		else shift = 0;
 
 		local float status;

Modified: branches/nexuiz-2.0/data/qcsrc/server/defs.qh
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/defs.qh	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/defs.qh	2007-05-02 18:09:25 UTC (rev 2454)
@@ -152,7 +152,6 @@
 float WEP_FIRST				= 1;
 float WEP_LAST				= 9;
 
-void(entity e, float chan, string samp, float vol, float atten) sound = #8;
 void(entity client, string s)	stuffcmd = #21;
 void(entity client, string s)	sprint = #24;
 vector(entity e, float sped)	aim = #44;

Modified: branches/nexuiz-2.0/data/qcsrc/server/domination.qc
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/domination.qc	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/domination.qc	2007-05-02 18:09:25 UTC (rev 2454)
@@ -544,12 +544,12 @@
 
 	numteams = cvar("g_domination_default_teams");
 	// LordHavoc: edit this if you want to change defaults
-	dom_spawnteam("Red", 4, "models/domination/dom_red.md3", 0, "domination/claim.wav", "", "Red team has captured a control point");
-	dom_spawnteam("Blue", 13, "models/domination/dom_blue.md3", 0, "domination/claim.wav", "", "Blue team has captured a control point");
+	dom_spawnteam("Red", COLOR_TEAM1-1, "models/domination/dom_red.md3", 0, "domination/claim.wav", "", "Red team has captured a control point");
+	dom_spawnteam("Blue", COLOR_TEAM2-1, "models/domination/dom_blue.md3", 0, "domination/claim.wav", "", "Blue team has captured a control point");
 	if(numteams > 2)
-		dom_spawnteam("Pink", 9, "models/domination/dom_pink.md3", 0, "domination/claim.wav", "", "Pink team has captured a control point");
+		dom_spawnteam("Yellow", COLOR_TEAM3-1, "models/domination/dom_yellow.md3", 0, "domination/claim.wav", "", "Yellow team has captured a control point");
 	if(numteams > 3)
-		dom_spawnteam("Yellow", 12, "models/domination/dom_yellow.md3", 0, "domination/claim.wav", "", "Yellow team has captured a control point");
+		dom_spawnteam("Pink", COLOR_TEAM4-1, "models/domination/dom_pink.md3", 0, "domination/claim.wav", "", "Pink team has captured a control point");
 	dom_spawnteam("", 0, "models/domination/dom_unclaimed.md3", 0, "", "", "");
 };
 
@@ -593,8 +593,8 @@
 	// so we don't even know yet if this map is set up for domination...
 	precache_model("models/domination/dom_red.md3");
 	precache_model("models/domination/dom_blue.md3");
+	precache_model("models/domination/dom_yellow.md3");
 	precache_model("models/domination/dom_pink.md3");
-	precache_model("models/domination/dom_yellow.md3");
 	precache_model("models/domination/dom_unclaimed.md3");
 	precache_sound("domination/claim.wav");
 	e = spawn();

Modified: branches/nexuiz-2.0/data/qcsrc/server/g_damage.qc
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/g_damage.qc	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/g_damage.qc	2007-05-02 18:09:25 UTC (rev 2454)
@@ -26,6 +26,11 @@
 		if(f > 0)
 			f = RunematchHandleFrags(attacker, targ, f);
 	}
+	else if(cvar("g_keyhunt"))
+	{
+		if(f > 0)
+			f = kh_HandleFrags(attacker, targ, f);
+	}
 	else if(cvar("g_lms"))
 	{
 		// count remaining lives, not frags in lms
@@ -91,7 +96,7 @@
 
 void Obituary (entity attacker, entity targ, float deathtype)
 {
-	string	s, a, m;
+	string	s, a;
 
 	if (targ.classname == "player" || targ.classname == "corpse")
 	{
@@ -105,29 +110,11 @@
 		{
 			if (deathtype == DEATH_TEAMCHANGE)
 			{
-				m = "You are now on: ";
-				if (targ.team == 5)
-					m = strcat(m, "^1Red Team");
-				else if (targ.team == 14)
-					m = strcat(m, "^4Blue Team");
-				else if (targ.team == 10)
-					m = strcat(m, "^6Pink Team");
-				else if (targ.team == 13)
-					m = strcat(m, "^3Yellow Team");
-				centerprint(targ, m);
+				centerprint(targ, strcat("You are now on: ", ColoredTeamName(targ.team)));
 			}
 			else if (deathtype == DEATH_AUTOTEAMCHANGE)
 			{
-				m = "You have been moved into a different team to improve team balance\nYou are now on: ";
-				if (targ.team == 5)
-					m = strcat(m, "^1Red Team");
-				else if (targ.team == 14)
-					m = strcat(m, "^4Blue Team");
-				else if (targ.team == 10)
-					m = strcat(m, "^6Pink Team");
-				else if (targ.team == 13)
-					m = strcat(m, "^3Yellow Team");
-				centerprint(targ, m);
+				centerprint(targ, strcat("You have been moved into a different team to improve team balance\nYou are now on: ", ColoredTeamName(targ.team)));
 				return;
 			}
 			else if (deathtype == DEATH_CAMP)
@@ -249,37 +236,37 @@
 				if (attacker.killcount == 3)
 				{
 					bprint (a,"^7 made a ^1TRIPLE FRAG\n");
-					stuffcmd(attacker, "play2 announcer/male/03kills.ogg\n");
+					play2(attacker, "announcer/male/03kills.ogg");
 				}
 				else if (attacker.killcount == 5)
 				{
 					bprint (a,"^7 made a ^1FIVE FRAG COMBO\n");
-					stuffcmd(attacker, "play2 announcer/male/05kills.ogg\n");
+					play2(attacker, "announcer/male/05kills.ogg");
 				}
 				else if (attacker.killcount == 10)
 				{
 					bprint (a,"^7 is on a ^1RAGE\n");
-					stuffcmd(attacker, "play2 announcer/male/10kills.ogg\n");
+					play2(attacker, "announcer/male/10kills.ogg");
 				}
 				else if (attacker.killcount == 15)
 				{
 					bprint (a,"^7 has done a ^1MASSACRE!\n");
-					stuffcmd(attacker, "play2 announcer/male/15kills.ogg\n");
+					play2(attacker, "announcer/male/15kills.ogg");
 				}
 				else if (attacker.killcount == 20)
 				{
 					bprint (a,"^7 is ^1UNHUMAN!\n");
-					stuffcmd(attacker, "play2 announcer/male/20kills.ogg\n");
+					play2(attacker, "announcer/male/20kills.ogg");
 				}
 				else if (attacker.killcount == 25)
 				{
 					bprint (a,"^7 is a ^1DEATH INCARNATION!\n");
-					stuffcmd(attacker, "play2 announcer/male/25kills.ogg\n");
+					play2(attacker, "announcer/male/25kills.ogg");
 				}
 				else if (attacker.killcount == 30)
 				{
 					bprint (a,"^7 is maybe a ^1AIMBOTTER?!\n");
-					stuffcmd(attacker, "play2 announcer/male/30kills.ogg\n");
+					play2(attacker, "announcer/male/30kills.ogg");
 				}
 			}
 		}
@@ -304,7 +291,7 @@
 				bprint ("^1",s, "^1 died\n");
 			GiveFrags(targ, targ, -1);
 			if(targ.frags == -5) {
-				stuffcmd(targ, "play2 announcer/male/botlike.ogg\n");
+				play2(targ, "announcer/male/botlike.ogg");
 			}
 
 			if (targ.killcount > 2)
@@ -414,7 +401,7 @@
 		}
 		else if (deathtype == IT_NEX && targ.items & IT_STRENGTH)
 		{
-			if(clienttype(attacker) == CLIENTTYPE_REAL) stuffcmd(attacker, "play2 announcer/male/yoda.ogg\n");
+			if(clienttype(attacker) == CLIENTTYPE_REAL) play2(attacker, "announcer/male/yoda.ogg");
 		}
 		if (deathtype == IT_LASER)
 		{
@@ -433,7 +420,7 @@
 	} else {
 		if (deathtype == IT_NEX && !targ.deadflag && !(attacker.flags & FL_ONGROUND) && !(targ.flags & FL_ONGROUND) && attacker.waterlevel < 2 && targ.waterlevel < 2 && attacker.killcount != 3 && attacker.killcount != 5 && attacker.killcount != 10 && attacker.killcount != 15 && attacker.killcount != 20 && attacker.killcount != 25 && attacker.killcount != 30)
 		{
-			if(clienttype(attacker) == CLIENTTYPE_REAL)  stuffcmd(attacker, "play2 announcer/male/yoda.ogg\n");
+			if(clienttype(attacker) == CLIENTTYPE_REAL)  play2(attacker, "announcer/male/yoda.ogg");
 		}
 	}
 

Modified: branches/nexuiz-2.0/data/qcsrc/server/g_world.qc
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/g_world.qc	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/g_world.qc	2007-05-02 18:09:25 UTC (rev 2454)
@@ -76,6 +76,16 @@
 	if(world_already_spawned)
 		error("world already spawned - you may have EXACTLY ONE worldspawn!");
 	world_already_spawned = TRUE;
+
+	/*
+	TODO sound pack system
+	// initialize sound pack system
+	soundpack = cvar_string("g_soundpack");
+	if(soundpack != "")
+		soundpack = strcat(soundpack, "/");
+	soundpack = strzone(soundpack);
+	*/
+
 	// Precache all player models
 	// Workaround for "invisible players"
 	precache_model("models/player/carni.zym");
@@ -455,6 +465,8 @@
 		return "rune";
 	else if (game == GAME_LMS)
 		return "lms";
+	else if (game == GAME_KEYHUNT)
+		return "kh";
 	return "dm";
 }
 
@@ -1438,8 +1450,8 @@
 	{
 		PrintScoreboardFor(e, "Red", "^1", COLOR_TEAM1);
 		PrintScoreboardFor(e, "Blue", "^4", COLOR_TEAM2);
-		PrintScoreboardFor(e, "Pink", "^6", COLOR_TEAM3);
-		PrintScoreboardFor(e, "Yellow", "^3", COLOR_TEAM4);
+		PrintScoreboardFor(e, "Yellow", "^3", COLOR_TEAM3);
+		PrintScoreboardFor(e, "Pink", "^6", COLOR_TEAM4);
 	}
 	else
 	{
@@ -1549,7 +1561,7 @@
 	{
 		if(teams_matter)
 		{
-			if(cvar("g_tdm") || cvar("g_runematch") || cvar("g_ctf") || cvar("g_domination"))
+			if(cvar("g_tdm") || cvar("g_runematch") || cvar("g_ctf") || cvar("g_domination") || cvar("g_keyhunt"))
 				status = WinningCondition_MaxTeamSum(fraglimit);
 			//else if()
 			//	status = WinningCondition_MaxTeamMax(fraglimit);
@@ -1845,7 +1857,7 @@
 
 		// notify about keep-two
 		if(keeptwo != 0 && mapvote_keeptwotime == 0)
-			stuffcmd(other, "\nplay2 misc/invshot.wav\n");
+			play2(other, "misc/invshot.wav");
 
 		// clear possibly invalid votes
 		if(mapvote_maps[other.mapvote - 1] == "")
@@ -1932,12 +1944,12 @@
 		if(self.classname == "spectator")
 		{
 			if(self.enemy.hitsound)
-				stuffcmd(self, "play2 misc/hit.wav\n");
+				play2(self, "misc/hit.wav");
 		}
 		else
 		{
 			if(self.hitsound)
-				stuffcmd(self, "play2 misc/hit.wav\n");
+				play2(self, "misc/hit.wav");
 		}
 	}
 	FOR_EACH_CLIENT(self)

Modified: branches/nexuiz-2.0/data/qcsrc/server/havocbot.qc
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/havocbot.qc	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/havocbot.qc	2007-05-02 18:09:25 UTC (rev 2454)
@@ -331,13 +331,13 @@
 		shotgun = (cvar("g_balance_shotgun_primary_damage")*cvar("g_balance_shotgun_primary_bullets")/cvar("g_balance_shotgun_primary_refire")*1.0)
 			* bound(0,1/cvar("g_balance_shotgun_primary_spread")/distance*spreadpenalty,1);
 	if (client_hasweapon(self, WEP_LASER, FALSE, FALSE))
-		laser = (cvar("g_balance_laser_damage")/cvar("g_balance_laser_refire")*1.0)
-			* bound(0,cvar("g_balance_laser_speed")/distance*0.2*maxdelaytime,1);
+		laser = (cvar("g_balance_laser_primary_damage")/cvar("g_balance_laser_primary_refire")*1.0)
+			* bound(0,cvar("g_balance_laser_primary_speed")/distance*0.2*maxdelaytime,1);
 	if((self.enemy.flags & FL_ONGROUND)==FALSE){
 		rocket = rocket   * (1-bound(0, distancefromfloor/cvar("g_balance_rocketlauncher_radius"         ),0.9)); //slight bigger change
 		grenade = grenade * (1-bound(0,distancefromfloor/cvar("g_balance_grenadelauncher_primary_radius"),0.95));
 		electro = electro * (1-bound(0,distancefromfloor/cvar("g_balance_electro_primary_radius"        ),0.95));
-		laser = laser     * (1-bound(0,distancefromfloor/cvar("g_balance_laser_radius"                  ),0.95));
+		laser = laser     * (1-bound(0,distancefromfloor/cvar("g_balance_laser_primary_radius"                  ),0.95));
 	}
 /*
 	dprint("Floor distance: ",ftos(distancefromfloor),"\n");

Modified: branches/nexuiz-2.0/data/qcsrc/server/havocbot_roles.qc
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/havocbot_roles.qc	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/havocbot_roles.qc	2007-05-02 18:09:25 UTC (rev 2454)
@@ -126,7 +126,7 @@
 void(float ratingscale) havocbot_goalrating_ctf_ourflag =
 {
 	local entity head;
-	if (self.team == 5) // red
+	if (self.team == COLOR_TEAM1) // red
 		head = find(world, classname, "item_flag_team1"); // red flag
 	else // blue
 		head = find(world, classname, "item_flag_team2"); // blue flag
@@ -136,7 +136,7 @@
 void(float ratingscale) havocbot_goalrating_ctf_enemyflag =
 {
 	local entity head;
-	if (self.team == 5) // red
+	if (self.team == COLOR_TEAM1) // red
 		head = find(world, classname, "item_flag_team2"); // blue flag
 	else // blue
 		head = find(world, classname, "item_flag_team1"); // red flag
@@ -151,7 +151,7 @@
 void(float ratingscale) havocbot_goalrating_ctf_ourstolenflag =
 {
 	local entity head;
-	if (self.team == 5) // red
+	if (self.team == COLOR_TEAM1) // red
 		head = find(world, classname, "item_flag_team1"); // red flag
 	else // blue
 		head = find(world, classname, "item_flag_team2"); // blue flag
@@ -234,7 +234,7 @@
 		return;
 	}
 	// check our flag
-	if (self.team == 5) // red
+	if (self.team == COLOR_TEAM1) // red
 		f = find(world, classname, "item_flag_team1");
 	else // blue
 		f = find(world, classname, "item_flag_team2");
@@ -284,7 +284,7 @@
 		return;
 	}
 	// check our flag
-	if (self.team == 5) // red
+	if (self.team == COLOR_TEAM1) // red
 		f = find(world, classname, "item_flag_team1");
 	else // blue
 		f = find(world, classname, "item_flag_team2");
@@ -325,7 +325,7 @@
 		return;
 	}
 	// check our flag
-	if (self.team == 5) // red
+	if (self.team == COLOR_TEAM1) // red
 		f = find(world, classname, "item_flag_team1");
 	else // blue
 		f = find(world, classname, "item_flag_team2");
@@ -385,7 +385,7 @@
 		return;
 	}
 	// check our flag
-	if (self.team == 5) // red
+	if (self.team == COLOR_TEAM1) // red
 		f = find(world, classname, "item_flag_team1");
 	else // blue
 		f = find(world, classname, "item_flag_team2");
@@ -453,7 +453,7 @@
 {
 	local float r;
 	dprint("choose CTF role...\n");
-	if (self.team == 13)
+	if (self.team == COLOR_TEAM3 || self.team == COLOR_TEAM4)
 		self.havocbot_role = havocbot_role_ctf_rogue;
 	else
 	{
@@ -508,6 +508,202 @@
 	self.havocbot_role = havocbot_role_dom;
 };
 
+
+
+
+
+
+void(float ratingscale_team, float ratingscale_dropped, float ratingscale_enemy) havocbot_goalrating_kh =
+{
+	local entity head;
+	for(head = world; (head = find(head, classname, "item_kh_key")); )
+	{
+		if(head.owner == self)
+			continue;
+		if(!kh_tracking_enabled)
+			if(!head.owner || head.team == self.team)
+				continue; // skip what I can't see
+		if(!head.owner)
+			navigation_routerating(head, ratingscale_dropped);
+		else if(head.team == self.team)
+			navigation_routerating(head, ratingscale_team);
+		else
+			navigation_routerating(head, ratingscale_enemy);
+	}
+};
+
+void() havocbot_role_kh_carrier;
+void() havocbot_role_kh_defense;
+void() havocbot_role_kh_offense;
+void() havocbot_role_kh_freelancer;
+void() havocbot_role_kh_carrier =
+{
+	if (!(self.items & IT_KEY1))
+	{
+		dprint("changing role to freelancer\n");
+		self.havocbot_role = havocbot_role_kh_freelancer;
+		self.havocbot_role_timeout = 0;
+		return;
+	}
+
+	if (self.bot_strategytime < time)
+	{
+		self.bot_strategytime = time + cvar("bot_ai_strategyinterval");
+		navigation_goalrating_start();
+
+		if(kh_Key_AllOwnedByWhichTeam() == self.team)
+			havocbot_goalrating_kh(10000, 1, 1); // bring home
+		else
+			havocbot_goalrating_kh(4000, 4000, 100); // play defensively
+
+		havocbot_goalrating_items(10000, self.origin, 10000);
+		navigation_goalrating_end();
+	}
+}
+
+void() havocbot_role_kh_defense =
+{
+	if (self.items & IT_KEY1)
+	{
+		dprint("changing role to carrier\n");
+		self.havocbot_role = havocbot_role_kh_carrier;
+		self.havocbot_role_timeout = 0;
+		return;
+	}
+
+	if (!self.havocbot_role_timeout)
+		self.havocbot_role_timeout = time + random() * 10 + 20;
+	if (time > self.havocbot_role_timeout)
+	{
+		dprint("changing role to freelancer\n");
+		self.havocbot_role = havocbot_role_kh_freelancer;
+		self.havocbot_role_timeout = 0;
+		return;
+	}
+
+	if (self.bot_strategytime < time)
+	{
+		float key_owner_team;
+		self.bot_strategytime = time + cvar("bot_ai_strategyinterval");
+		navigation_goalrating_start();
+
+		key_owner_team = kh_Key_AllOwnedByWhichTeam();
+		if(key_owner_team == self.team)
+			havocbot_goalrating_kh(10000, 1, 1); // defend key carriers
+		else if(key_owner_team == -1)
+			havocbot_goalrating_kh(4000, 1000, 1); // play defensively
+		else
+			havocbot_goalrating_kh(1, 1, 10000); // ATTACK ANYWAY
+
+		havocbot_goalrating_items(10000, self.origin, 10000);
+		navigation_goalrating_end();
+	}
+};
+
+void() havocbot_role_kh_offense =
+{
+	if (self.items & IT_KEY1)
+	{
+		dprint("changing role to carrier\n");
+		self.havocbot_role = havocbot_role_kh_carrier;
+		self.havocbot_role_timeout = 0;
+		return;
+	}
+
+	if (!self.havocbot_role_timeout)
+		self.havocbot_role_timeout = time + random() * 10 + 20;
+	if (time > self.havocbot_role_timeout)
+	{
+		dprint("changing role to freelancer\n");
+		self.havocbot_role = havocbot_role_kh_freelancer;
+		self.havocbot_role_timeout = 0;
+		return;
+	}
+
+	if (self.bot_strategytime < time)
+	{
+		float key_owner_team;
+
+		self.bot_strategytime = time + cvar("bot_ai_strategyinterval");
+		navigation_goalrating_start();
+
+		key_owner_team = kh_Key_AllOwnedByWhichTeam();
+		if(key_owner_team == self.team)
+			havocbot_goalrating_kh(10000, 1, 1); // defend anyway
+		else if(key_owner_team == -1)
+			havocbot_goalrating_kh(1, 1000, 4000); // play offensively
+		else
+			havocbot_goalrating_kh(1, 1, 10000); // ATTACK! EMERGENCY!
+
+		havocbot_goalrating_items(10000, self.origin, 10000);
+		navigation_goalrating_end();
+	}
+};
+
+void() havocbot_role_kh_freelancer =
+{
+	if (self.items & IT_KEY1)
+	{
+		dprint("changing role to carrier\n");
+		self.havocbot_role = havocbot_role_kh_carrier;
+		self.havocbot_role_timeout = 0;
+		return;
+	}
+
+	if (!self.havocbot_role_timeout)
+		self.havocbot_role_timeout = time + random() * 10 + 10;
+	if (time > self.havocbot_role_timeout)
+	{
+		if (random() < 0.5)
+		{
+			dprint("changing role to offense\n");
+			self.havocbot_role = havocbot_role_kh_offense;
+		}
+		else
+		{
+			dprint("changing role to defense\n");
+			self.havocbot_role = havocbot_role_kh_defense;
+		}
+		self.havocbot_role_timeout = 0;
+		return;
+	}
+
+	if (self.bot_strategytime < time)
+	{
+		float key_owner_team;
+
+		self.bot_strategytime = time + cvar("bot_ai_strategyinterval");
+		navigation_goalrating_start();
+
+		key_owner_team = kh_Key_AllOwnedByWhichTeam();
+		if(key_owner_team == self.team)
+			havocbot_goalrating_kh(10000, 1, 1); // defend anyway
+		else if(key_owner_team == -1)
+			havocbot_goalrating_kh(1000, 4000, 1000); // prefer dropped keys
+		else
+			havocbot_goalrating_kh(1, 1, 10000); // ATTACK ANYWAY
+
+		havocbot_goalrating_items(10000, self.origin, 10000);
+		navigation_goalrating_end();
+	}
+};
+
+
+
+
+
+void() havocbot_chooserole_kh =
+{
+	local float r;
+	r = random() * 3;
+	if (r < 1)
+		self.havocbot_role = havocbot_role_kh_offense;
+	else if (r < 2)
+		self.havocbot_role = havocbot_role_kh_defense;
+	else
+		self.havocbot_role = havocbot_role_kh_freelancer;
+};
+
 void() havocbot_chooserole =
 {
 	dprint("choose a role...\n");
@@ -517,6 +713,8 @@
 		havocbot_chooserole_ctf();
 	else if (cvar("g_domination"))
 		havocbot_chooserole_dom();
+	else if (cvar("g_keyhunt"))
+		havocbot_chooserole_kh();
 	else // assume anything else is deathmatch
 		havocbot_chooserole_dm();
 };

Copied: branches/nexuiz-2.0/data/qcsrc/server/keyhunt.qc (from rev 2414, trunk/data/qcsrc/server/keyhunt.qc)
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/keyhunt.qc	                        (rev 0)
+++ branches/nexuiz-2.0/data/qcsrc/server/keyhunt.qc	2007-05-02 18:09:25 UTC (rev 2454)
@@ -0,0 +1,711 @@
+string STR_ITEM_KH_KEY = "item_kh_key";
+#define FOR_EACH_KH_KEY(v) for(v = world; (v = find(v, classname, STR_ITEM_KH_KEY)); )
+
+typedef void(void) kh_Think_t;
+var kh_Think_t kh_Controller_Thinkfunc;
+string kh_Controller_Waitmsg;
+
+float kh_Team_ByID(float t)
+{
+	if(t == 0) return COLOR_TEAM1;
+	if(t == 1) return COLOR_TEAM2;
+	if(t == 2) return COLOR_TEAM3;
+	if(t == 3) return COLOR_TEAM4;
+	return 0;
+}
+
+entity kh_controller;
+float kh_tracking_enabled;
+float kh_teams;
+float kh_interferemsg_time, kh_interferemsg_team;
+.entity kh_next, kh_prev; // linked list
+.float kh_droptime;
+
+string kh_sound_capture = "sound/ctf/capture.wav";
+string kh_sound_destroy = "sound/ctf/return.wav";
+string kh_sound_drop = "sound/misc/mouseclick.wav";
+string kh_sound_collect = "sound/ctf/take.wav";
+
+float kh_sprite_dropped, kh_sprite_finish, kh_sprite_red, kh_sprite_blue, kh_sprite_pink, kh_sprite_yellow, kh_sprite_friend;
+
+float kh_GetCarrierSprite(float t, float e)
+{
+	if(t == e)           return kh_sprite_friend;
+	if(t == COLOR_TEAM1) return kh_sprite_red;
+	if(t == COLOR_TEAM2) return kh_sprite_blue;
+	if(t == COLOR_TEAM3) return kh_sprite_yellow;
+	if(t == COLOR_TEAM4) return kh_sprite_pink;
+	return 0;
+}
+
+void kh_Controller_SetThink(float t, string msg, kh_Think_t func)
+{
+	kh_Controller_Thinkfunc = func;
+	kh_controller.cnt = t;
+	if(kh_Controller_Waitmsg != "")
+		strunzone(kh_Controller_Waitmsg);
+	if(msg == "")
+		kh_Controller_Waitmsg = "";
+	else
+		kh_Controller_Waitmsg = strzone(msg);
+	if(t == 0)
+		kh_controller.nextthink = time; // force
+}
+
+void kh_Controller_Think()
+{
+	entity e;
+	if(intermission_running)
+		return;
+	if(self.cnt > 0)
+	{
+		if(kh_Controller_Waitmsg != "")
+		{
+			string s;
+			if(substring(kh_Controller_Waitmsg, strlen(kh_Controller_Waitmsg)-1, 1) == " ")
+				s = strcat(kh_Controller_Waitmsg, ftos(self.cnt));
+			else
+				s = kh_Controller_Waitmsg;
+
+			dprint(s, "\n");
+
+			FOR_EACH_PLAYER(e)
+				if(clienttype(e) == CLIENTTYPE_REAL)
+					centerprint_atprio(e, CENTERPRIO_SPAM, s);
+		}
+		self.cnt -= 1;
+	}
+	else if(self.cnt == 0)
+	{
+		self.cnt -= 1;
+		kh_Controller_Thinkfunc();
+	}
+	self.nextthink = time + 1;
+}
+
+// frags f: take from cvar * f
+// frags 0: no frags
+void kh_Scores_Event(entity player, entity key, string what, float frags_player, float frags_owner)
+{
+	string s;
+	if(frags_player)
+		player.frags = player.frags + floor(0.5 + frags_player);
+	if(frags_owner)
+		key.owner.frags = key.owner.frags + floor(0.5 + frags_owner);
+	if(!cvar("sv_eventlog"))
+		return;
+	s = strcat(":keyhunt:", what, ":", ftos(player.playerid));
+	s = strcat(s, ":", ftos(frags_player));
+	if(key && key.owner)
+		s = strcat(s, ":", ftos(key.owner.playerid));
+	else
+		s = strcat(s, ":0");
+	s = strcat(s, ":", ftos(frags_owner), ":");
+	if(key)
+		s = strcat(s, key.netname);
+	GameLogEcho(s, FALSE);
+}
+
+void kh_Key_Attach(entity key)
+{
+	setattachment(key, key.owner, "");
+	setorigin(key, '0 0 -15'); // x/y fixed later in think, z has to be -15 to make the key fit even in the crouchbox
+	key.angles_y -= key.owner.angles_y;
+	key.flags = 0;
+	key.solid = SOLID_NOT;
+	key.movetype = MOVETYPE_NONE;
+	key.team = key.owner.team;
+	key.nextthink = time;
+	key.damageforcescale = 0;
+}
+
+vector kh_AttachedOrigin(entity e)
+{
+	if(e.tag_entity)
+	{
+		makevectors(e.tag_entity.angles);
+		return e.tag_entity.origin + e.origin_x * v_forward - e.origin_y * v_right + e.origin_z * v_up;
+	}
+	else
+		return e.origin;
+}
+
+void kh_Key_Detach(entity key)
+{
+	setorigin(key, key.owner.origin + key.origin_z * '0 0 1');
+	setattachment(key, world, "");
+	key.angles_y += key.owner.angles_y;
+	key.aiment = world;
+	key.flags = FL_ITEM;
+	key.solid = SOLID_TRIGGER;
+	key.movetype = MOVETYPE_TOSS;
+	key.pain_finished = time + cvar("g_balance_keyhunt_delay_return");
+	key.damageforcescale = cvar("g_balance_keyhunt_damageforcescale");
+	// let key.team stay
+}
+
+void kh_Key_AssignTo(entity key, entity player)
+{
+	if(key.owner == player)
+		return;
+
+	if(key.owner)
+	{
+		kh_Key_Detach(key);
+
+		// remove from linked list
+		if(key.kh_next)
+			key.kh_next.kh_prev = key.kh_prev;
+		key.kh_prev.kh_next = key.kh_next;
+		key.kh_next = world;
+		key.kh_prev = world;
+
+		if(key.owner.kh_next == world)
+		{
+			// No longer a key carrier
+			WaypointSprite_DetachCarrier(key.owner);
+		}
+	}
+
+	key.owner = player;
+
+	if(player)
+	{
+		// insert into linked list
+		key.kh_next = player.kh_next;
+		key.kh_prev = player;
+		player.kh_next = key;
+		if(key.kh_next)
+			key.kh_next.kh_prev = key;
+
+		kh_Key_Attach(key);
+
+		if(key.kh_next == world)
+		{
+			// player is now a key carrier
+			WaypointSprite_AttachCarrier("", player);
+			player.waypointsprite_attachedforcarrier.waypointsprite_for_player = kh_KeyCarrier_waypointsprite_for_player;
+			player.waypointsprite_attachedforcarrier.team = player.team;
+		}
+	}
+
+	key.pusher = world;
+}
+
+void kh_Key_Damage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
+{
+	if(self.owner)
+		return;
+	if(vlen(force) <= 0)
+		return;
+	if(time > self.pushltime)
+		if(attacker.classname == "player")
+			self.team = attacker.team;
+}
+
+void kh_Key_Spawn(entity initial_owner, float angle)
+{
+	entity key;
+	key = spawn();
+	key.classname = STR_ITEM_KH_KEY;
+	key.touch = kh_Key_Touch;
+	key.think = kh_Key_Think;
+	key.nextthink = time;
+	key.items = IT_KEY1 | IT_KEY2;
+	key.cnt = angle;
+	key.angles = '0 360 0' * random();
+	key.event_damage = kh_Key_Damage;
+	setmodel(key, "models/keyhunt/key.md3");
+	setsize(key, '-8 -8 -8', '8 8 40');
+
+	switch(initial_owner.team)
+	{
+		case COLOR_TEAM1:
+			key.netname = "^1red key";
+			key.colormod = '103 0 0' * (1/96);
+			break;
+		case COLOR_TEAM2:
+			key.netname = "^4blue key";
+			key.colormod = '35 35 191' * (1/96);
+			break;
+		case COLOR_TEAM3:
+			key.netname = "^3yellow key";
+			key.colormod = '187 167 15' * (1/96);
+			break;
+		case COLOR_TEAM4:
+			key.netname = "^6pink key";
+			key.colormod = '139 79 107' * (1/96);
+			break;
+		default:
+			key.netname = "NETGIER key";
+			key.colormod = '1 1 1';
+			break;
+	}
+
+	sprint(initial_owner, strcat("You got the ^2", key.netname, "\n"));
+
+	WaypointSprite_AttachCarrier("", key);
+	key.waypointsprite_attachedforcarrier.waypointsprite_for_player = kh_Key_waypointsprite_for_player;
+
+	kh_Key_AssignTo(key, initial_owner);
+}
+
+void kh_Key_Remove(entity key)
+{
+	entity o;
+	o = key.owner;
+	kh_Key_AssignTo(key, world);
+	if(o) // it was attached
+		WaypointSprite_Kill(key.waypointsprite_attachedforcarrier);
+	else // it was dropped
+		WaypointSprite_DetachCarrier(key);
+
+	remove(key);
+}
+
+// -1 when no team completely owns all keys yet
+float kh_Key_AllOwnedByWhichTeam()
+{
+	entity key;
+	float teem;
+
+	teem = -1;
+	FOR_EACH_KH_KEY(key)
+	{
+		if(!key.owner)
+			return -1;
+		if(teem == -1)
+			teem = key.team;
+		else if(teem != key.team)
+			return -1;
+	}
+	return teem;
+}
+
+void kh_Key_Collect(entity key, entity player)
+{
+	sound(key, CHAN_AUTO, kh_sound_collect, 1, ATTN_NORM);
+
+	kh_Scores_Event(player, key, "collect", cvar("g_balance_keyhunt_score_collect"), 0);
+	bprint(player.netname, "^7 collected the ", key.netname, "\n");
+	kh_Key_AssignTo(key, player);
+
+	if(kh_Key_AllOwnedByWhichTeam() != -1)
+	{
+		kh_interferemsg_time = time + 0.2;
+		kh_interferemsg_team = player.team;
+	}
+}
+
+void kh_Key_DropAll(entity player)
+{
+	entity key;
+	entity mypusher;
+	if(player.kh_next)
+	{
+		mypusher = world;
+		if(player.pusher)
+			if(time < player.pushltime)
+				mypusher = player.pusher;
+		while((key = player.kh_next))
+		{
+			kh_Scores_Event(player, key, "losekey", 0, 0);
+			bprint(player.netname, "^7 lost the ", key.netname, "\n");
+			kh_Key_AssignTo(key, world);
+			makevectors('-1 0 0' * (45 + 45 * random()) + '0 360 0' * random());
+			key.velocity = W_CalculateProjectileVelocity(player.velocity, cvar("g_balance_keyhunt_dropvelocity") * v_forward);
+			key.pusher = mypusher;
+			key.pushltime = time + cvar("g_balance_keyhunt_protecttime");
+		}
+		sound(player, CHAN_AUTO, kh_sound_drop, 1, ATTN_NORM);
+	}
+}
+
+void kh_Key_Touch()
+{
+	if(self.owner) // already carried
+		return;
+	if(other.classname != "player")
+		return;
+	if(other.deadflag != DEAD_NO)
+		return;
+	if(other == self.enemy)
+		if(time < self.kh_droptime + cvar("g_balance_keyhunt_delay_collect"))
+			return; // you just dropped it!
+	kh_Key_Collect(self, other);
+}
+
+void kh_Key_Think()
+{
+	entity head;
+
+	//self.angles_y = math_mod(self.angles_y + 0.05 * 135, 360);
+	// model is EF_ROTATING now
+
+	if(self.owner)
+	{
+		makevectors('0 1 0' * (self.cnt + math_mod(time, 360) * 45));
+		setorigin(self, v_forward * 24 + '0 0 1' * self.origin_z);
+
+		if(self.owner.buttonuse)
+		if(time >= self.owner.kh_droptime + cvar("g_balance_keyhunt_delay_drop"))
+		{
+			self.owner.kh_droptime = time;
+			self.kh_droptime = time; // prevent collecting this one for some time
+			self.enemy = self.owner;
+			self.pusher = world;
+			kh_Scores_Event(self.owner, self, "dropkey", 0, 0);
+			bprint(self.owner.netname, "^7 dropped the ", self.netname, "\n");
+			sound(self.owner, CHAN_AUTO, kh_sound_drop, 1, ATTN_NORM);
+			makevectors(self.owner.v_angle);
+			self.velocity = W_CalculateProjectileVelocity(self.owner.velocity, cvar("g_balance_keyhunt_throwvelocity") * v_forward);
+			kh_Key_AssignTo(self, world);
+			self.pushltime = time + cvar("g_balance_keyhunt_protecttime");
+		}
+	}
+
+	// if in nodrop or time over, end the round
+	if(!self.owner)
+		if(time > self.pain_finished)
+			kh_LoserTeam(self.team, self);
+	
+	if(self.owner)
+	if(kh_Key_AllOwnedByWhichTeam() != -1)
+	{
+		entity key;
+		vector p;
+		p = self.owner.origin;
+		FOR_EACH_KH_KEY(key)
+			if(vlen(key.owner.origin - p) > cvar("g_balance_keyhunt_maxdist"))
+				goto not_winning;
+		kh_WinnerTeam(self.team);
+:not_winning
+	}
+
+	if(kh_interferemsg_time && time > kh_interferemsg_time)
+	{
+		kh_interferemsg_time = 0;
+		FOR_EACH_PLAYER(head)
+		{
+			if(head.team == kh_interferemsg_team)
+				if(head.kh_next)
+					centerprint(head, "All keys are in your team's hands!\n\nMeet the other key carriers ^1NOW^7!\n");
+				else
+					centerprint(head, "All keys are in your team's hands!\n\nHelp the key carriers to meet!\n");
+			else
+				centerprint(head, "All keys are in the enemy's hands!\n\nInterfere ^1NOW^7!\n");
+		}
+	}
+
+	self.nextthink = time + 0.05;
+}
+
+void kh_WinnerTeam(float teem)
+{
+	// all key carriers get some points
+	vector firstorigin, lastorigin;
+	float first;
+	entity key;
+	float score;
+	score = (kh_teams - 1) * cvar("g_balance_keyhunt_score_capture");
+	DistributeEvenly_Init(score, kh_teams);
+	// twice the score for 3 team games, three times the score for 4 team games!
+	// note: for a win by destroying the key, this should NOT be applied
+	FOR_EACH_KH_KEY(key)
+		kh_Scores_Event(key.owner, key, "capture", DistributeEvenly_Get(1), 0);
+
+	first = TRUE;
+	FOR_EACH_KH_KEY(key)
+		if(key.owner.kh_next == key)
+		{
+			if(!first)
+				bprint("^7, ");
+			bprint(key.owner.netname);
+			first = FALSE;
+		}
+	bprint("^7 captured the keys for the ", ColoredTeamName(teem), "\n");
+
+	first = TRUE;
+	FOR_EACH_KH_KEY(key)
+	{
+		vector thisorigin;
+
+		thisorigin = kh_AttachedOrigin(key);
+
+		if(!first)
+			te_lightning2(world, lastorigin, thisorigin);
+		lastorigin = thisorigin;
+		if(first)
+			firstorigin = thisorigin;
+		first = FALSE;
+	}
+	if(kh_teams > 2)
+	{
+		te_lightning2(world, lastorigin, firstorigin);
+	}
+
+	sound(world, CHAN_AUTO, kh_sound_capture, 1, ATTN_NONE);
+	kh_FinishRound();
+}
+
+void kh_LoserTeam(float teem, entity lostkey)
+{
+	entity player, key, attacker;
+	float players;
+	float keys;
+
+	attacker = world;
+	if(lostkey.pusher)
+		if(lostkey.pusher.team != teem)
+			if(lostkey.pusher.classname == "player")
+				attacker = lostkey.pusher;
+
+	players = keys = 0;
+
+	if(attacker)
+	{
+		kh_Scores_Event(attacker, world, "push", cvar("g_balance_keyhunt_score_push"), 0);
+		centerprint(attacker, "Your push is the best!\n\n\n");
+		bprint("The ", ColoredTeamName(teem), "^7 could not take care of the ", lostkey.netname, "\n");
+	}
+	else
+	{
+		float of, fragsleft, i, thisteam;
+		of = cvar("g_balance_keyhunt_score_destroyed_ownfactor");
+
+		FOR_EACH_PLAYER(player)
+			if(player.team != teem)
+				++players;
+		
+		FOR_EACH_KH_KEY(key)
+			if(key.owner && key.team != teem)
+				++keys;
+
+		DistributeEvenly_Init(cvar("g_balance_keyhunt_score_destroyed"), keys * of + players);
+
+		FOR_EACH_KH_KEY(key)
+			if(key.owner && key.team != teem)
+				kh_Scores_Event(key.owner, world, "destroyed_holdingkey", DistributeEvenly_Get(of), 0);
+
+		fragsleft = DistributeEvenly_Get(players);
+
+		// Now distribute these among all other teams...
+		for(i = 0; i < kh_teams; ++i)
+		{
+			thisteam = kh_Team_ByID(i);
+			players = 0;
+			FOR_EACH_PLAYER(player)
+				if(player.team == thisteam)
+					++players;
+
+			DistributeEvenly_Init(fragsleft, kh_teams - i);
+			fragsleft = DistributeEvenly_Get(kh_teams - i - 1);
+			DistributeEvenly_Init(DistributeEvenly_Get(1), players);
+
+			FOR_EACH_PLAYER(player)
+				if(player.team == thisteam)
+					kh_Scores_Event(player, world, "destroyed", DistributeEvenly_Get(1), 0);
+		}
+
+		bprint("The ", ColoredTeamName(teem), "^7 could not take care of the ", lostkey.netname, "\n");
+	}
+	sound(world, CHAN_AUTO, kh_sound_destroy, 1, ATTN_NONE);
+
+	kh_FinishRound();
+}
+
+void kh_FinishRound()
+{
+	// prepare next round
+	kh_interferemsg_time = 0;
+	entity key;
+	FOR_EACH_KH_KEY(key)
+		kh_Key_Remove(key);
+
+	kh_Controller_SetThink(cvar("g_balance_keyhunt_delay_round"), "Round starts in ", kh_StartRound);
+}
+
+float kh_EnoughPlayers()
+{
+	float i, players, teem;
+	entity player;
+	
+	// find a random player per team
+	for(i = 0; i < kh_teams; ++i)
+	{
+		teem = kh_Team_ByID(i);
+		players = 0;
+		FOR_EACH_PLAYER(player)
+			if(player.deadflag == DEAD_NO)
+				if(player.team == teem)
+					++players;
+		if(players == 0)
+			return FALSE;
+	}
+	return TRUE;
+}
+
+void kh_WaitForPlayers()
+{
+	if(kh_EnoughPlayers())
+		kh_Controller_SetThink(cvar("g_balance_keyhunt_delay_round"), "Round starts in ", kh_StartRound);
+	else
+		kh_Controller_SetThink(1, "Waiting for players to join...", kh_WaitForPlayers);
+}
+
+void kh_StartRound()
+{
+	float i, players, teem;
+	entity player;
+
+	if(!kh_EnoughPlayers())
+	{
+		kh_Controller_SetThink(1, "Waiting for players to join...", kh_WaitForPlayers);
+		return;
+	}
+
+	FOR_EACH_PLAYER(player)
+		if(clienttype(player) == CLIENTTYPE_REAL)
+			centerprint_expire(player, CENTERPRIO_SPAM);
+
+	for(i = 0; i < kh_teams; ++i)
+	{
+		teem = kh_Team_ByID(i);
+		players = 0;
+		entity my_player;
+		FOR_EACH_PLAYER(player)
+			if(player.deadflag == DEAD_NO)
+				if(player.team == teem)
+				{
+					++players;
+					if(random() * players <= 1)
+						my_player = player;
+				}
+		kh_Key_Spawn(my_player, 360 * i / kh_teams);
+	}
+
+	kh_tracking_enabled = FALSE;
+	kh_Controller_SetThink(cvar("g_balance_keyhunt_delay_tracking"), "Scanning frequency range...", kh_EnableTrackingDevice);
+}
+
+void kh_setstatus()
+{
+	if(kh_teams)
+	{
+		float kh_KEY;
+		kh_KEY = (IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST); // the one impossible combination
+		if(self.kh_next)
+			self.items = self.items | kh_KEY;
+		else
+			self.items = self.items - (self.items & kh_KEY);
+	}
+}
+
+void kh_EnableTrackingDevice()
+{
+	entity player;
+
+	FOR_EACH_PLAYER(player)
+		if(clienttype(player) == CLIENTTYPE_REAL)
+			centerprint_expire(player, CENTERPRIO_SPAM);
+
+	kh_tracking_enabled = TRUE;
+}
+
+float kh_Key_waypointsprite_for_player(entity e)
+{
+	if(!kh_tracking_enabled)
+		return 0;
+	if(!self.owner)
+		return kh_sprite_dropped;
+	if(!self.owner.owner)
+		return kh_sprite_dropped;
+	return 0; // draw only when key is not owned
+}
+
+float kh_KeyCarrier_waypointsprite_for_player(entity e)
+{
+	if(e.classname != "player" || self.team != e.team)
+		if(!kh_tracking_enabled)
+			return 0;
+
+	// e is spectator? That's no team mate...
+	if(e.classname != "player")
+		return kh_GetCarrierSprite(self.team, -1);
+	
+	// e is no key carrier: simple case...
+	if(!e.kh_next)
+		return kh_GetCarrierSprite(self.team, e.team);
+	
+	// e is a key carrier: if any key is dropped or owned by another team, show
+	// the carrier sprite; otherwise show run here
+	if(kh_Key_AllOwnedByWhichTeam() == e.team)
+		return kh_sprite_finish;
+
+	return kh_GetCarrierSprite(self.team, e.team);
+}
+
+float kh_HandleFrags(entity attacker, entity targ, float f)
+{
+	if(f <= 0)
+		return f;
+	if(attacker == targ)
+		return f;
+
+	if(targ.kh_next)
+		kh_Scores_Event(attacker, targ.kh_next, "carrierfrag", cvar("g_balance_keyhunt_score_carrierfrag")-1, 0);
+
+	return f;
+}
+
+void kh_init()
+{
+	precache_sound(kh_sound_capture);
+	precache_sound(kh_sound_destroy);
+	precache_sound(kh_sound_drop);
+	precache_sound(kh_sound_collect);
+
+	precache_model("models/sprites/key-dropped.sp2");
+	precache_model("models/sprites/keycarrier-finish.sp2");
+	precache_model("models/sprites/keycarrier-friend.sp2");
+	precache_model("models/sprites/keycarrier-red.sp2");
+	precache_model("models/sprites/keycarrier-blue.sp2");
+	precache_model("models/sprites/keycarrier-yellow.sp2");
+	precache_model("models/sprites/keycarrier-pink.sp2");
+	precache_model("models/keyhunt/key.md3");
+
+	// setup variables
+	kh_teams = cvar("g_keyhunt_teams_override");
+	if(kh_teams < 2)
+		kh_teams = cvar("g_keyhunt_teams");
+	kh_teams = bound(2, kh_teams, 4);
+
+	// make a KH entity for controlling the game
+	kh_controller = spawn();
+	kh_controller.think = kh_Controller_Think;
+	kh_Controller_SetThink(0, "", kh_WaitForPlayers);
+
+	setmodel(kh_controller, "models/sprites/key-dropped.sp2");
+	kh_sprite_dropped = kh_controller.modelindex;
+	setmodel(kh_controller, "models/sprites/keycarrier-finish.sp2");
+	kh_sprite_finish = kh_controller.modelindex;
+	setmodel(kh_controller, "models/sprites/keycarrier-friend.sp2");
+	kh_sprite_friend = kh_controller.modelindex;
+	setmodel(kh_controller, "models/sprites/keycarrier-red.sp2");
+	kh_sprite_red = kh_controller.modelindex;
+	setmodel(kh_controller, "models/sprites/keycarrier-blue.sp2");
+	kh_sprite_blue = kh_controller.modelindex;
+	setmodel(kh_controller, "models/sprites/keycarrier-pink.sp2");
+	kh_sprite_pink = kh_controller.modelindex;
+	setmodel(kh_controller, "models/sprites/keycarrier-yellow.sp2");
+	kh_sprite_yellow = kh_controller.modelindex;
+	setmodel(kh_controller, "");
+}
+
+void kh_finalize()
+{
+	// to be called before intermission
+	kh_FinishRound();
+	remove(kh_controller);
+	kh_controller = world;
+}

Copied: branches/nexuiz-2.0/data/qcsrc/server/keyhunt.qh (from rev 2414, trunk/data/qcsrc/server/keyhunt.qh)
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/keyhunt.qh	                        (rev 0)
+++ branches/nexuiz-2.0/data/qcsrc/server/keyhunt.qh	2007-05-02 18:09:25 UTC (rev 2454)
@@ -0,0 +1,25 @@
+float kh_teams;
+float kh_tracking_enabled;
+
+void kh_Scores_Event(entity player, entity key, string what, float frags_player, float frags_owner);
+void kh_Key_Attach(entity key);
+void kh_Key_Detach(entity key);
+void kh_Key_AssignTo(entity key, entity player);
+void kh_Key_Spawn(entity initial_owner, float angle);
+void kh_Key_Remove(entity key);
+void kh_Key_Collect(entity key, entity player);
+void kh_Key_DropAll(entity player);
+void kh_Key_Touch();
+void kh_Key_Think();
+void kh_WinnerTeam(float teem);
+void kh_LoserTeam(float teem, entity lostkey);
+void kh_FinishRound();
+void kh_StartRound();
+void kh_EnableTrackingDevice();
+void kh_init();
+void kh_finalize();
+float kh_KeyCarrier_waypointsprite_for_player(entity e);
+float kh_Key_waypointsprite_for_player(entity e);
+void kh_setstatus();
+float kh_HandleFrags(entity attacker, entity targ, float f);
+float kh_Key_AllOwnedByWhichTeam();

Modified: branches/nexuiz-2.0/data/qcsrc/server/miscfunctions.qc
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/miscfunctions.qc	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/miscfunctions.qc	2007-05-02 18:09:25 UTC (rev 2454)
@@ -1,3 +1,29 @@
+void() info_player_deathmatch; // needed for the other spawnpoints
+string ColoredTeamName(float t);
+
+float DistributeEvenly_amount;
+float DistributeEvenly_totalweight;
+void DistributeEvenly_Init(float amount, float totalweight)
+{
+	if(DistributeEvenly_amount)
+	{
+		dprint("DistributeEvenly_Init: UNFINISHED DISTRIBUTION (", ftos(DistributeEvenly_amount), " for ");
+		dprint(ftos(DistributeEvenly_totalweight), "left!\n");
+	}
+	DistributeEvenly_amount = amount;
+	DistributeEvenly_totalweight = totalweight;
+}
+float DistributeEvenly_Get(float weight)
+{
+	float f;
+	if(weight <= 0)
+		return 0;
+	f = floor(0.5 + DistributeEvenly_amount * weight / DistributeEvenly_totalweight);
+	DistributeEvenly_totalweight -= weight;
+	DistributeEvenly_amount -= f;
+	return f;
+}
+
 void move_out_of_solid_expand(entity e, vector by)
 {
 	float eps = 0.0625;
@@ -492,10 +518,9 @@
 
 void DistributeFragsAmongTeam(entity p, float targetteam, float factor)
 {
-	float f;
-	float d;
 	float nTeam;
 	entity head;
+	float f;
 
 	if(!teams_matter)
 		return;
@@ -521,20 +546,12 @@
 	if(nTeam == 0)
 		return;
 
+	DistributeEvenly_Init(f, nTeam);
+
 	FOR_EACH_PLAYER(head)
 		if(head != p)
 			if(head.team == targetteam)
-			{
-				d = floor(f / nTeam);
-				head.frags = head.frags + d;
-				f = f - d;
-				nTeam = nTeam - 1;
-			}
-
-	if(nTeam != 0)
-		error("nPlayers in team changed!");
-	if(f != 0)
-		error(strcat("There were ", ftos(f), " frags left. BAD!"));
+				head.frags = head.frags + DistributeEvenly_Get(1);
 }
 
 string Team_ColorCode(float teamid)
@@ -544,9 +561,9 @@
 	else if(teamid == COLOR_TEAM2)
 		return "^4";
 	else if(teamid == COLOR_TEAM3)
+		return "^3";
+	else if(teamid == COLOR_TEAM4)
 		return "^6";
-	else if(teamid == COLOR_TEAM4)
-		return "^3";
 	else
 		return "^7";
 }
@@ -608,6 +625,7 @@
 */
 
 #define CENTERPRIO_POINT 1
+#define CENTERPRIO_SPAM 2
 #define CENTERPRIO_REBALANCE 2
 #define CENTERPRIO_VOTE 4
 #define CENTERPRIO_NORMAL 5
@@ -703,3 +721,30 @@
 	g_pickup_healthmega                = cvar("g_pickup_healthmega");
 	g_pickup_healthmega_max            = cvar("g_pickup_healthmega_max");
 }
+
+/*
+// TODO sound pack system
+string soundpack;
+
+string precache_sound_builtin (string s) = #19;
+void(entity e, float chan, string samp, float vol, float atten) sound_builtin = #8;
+string precache_sound(string s)
+{
+	return precache_sound_builtin(strcat(soundpack, s));
+}
+void play2(entity e, string filename)
+{
+	stuffcmd(e, strcat("play2 ", soundpack, filename, "\n"));
+}
+void sound(entity e, float chan, string samp, float vol, float atten)
+{
+	sound_builtin(e, chan, strcat(soundpack, samp), vol, atten);
+}
+*/
+
+string precache_sound (string s) = #19;
+void(entity e, float chan, string samp, float vol, float atten) sound = #8;
+void play2(entity e, string filename)
+{
+	stuffcmd(e, strcat("play2 ", filename, "\n"));
+}

Modified: branches/nexuiz-2.0/data/qcsrc/server/progs.src
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/progs.src	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/progs.src	2007-05-02 18:09:25 UTC (rev 2454)
@@ -12,6 +12,8 @@
 ../common/util.qh
 ../common/util.qc
 
+keyhunt.qh
+
 miscfunctions.qc
 
 waypointsprites.qc
@@ -84,3 +86,5 @@
 
 ../common/gamecommand.qc
 gamecommand.qc
+
+keyhunt.qc

Modified: branches/nexuiz-2.0/data/qcsrc/server/t_items.qc
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/t_items.qc	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/t_items.qc	2007-05-02 18:09:25 UTC (rev 2454)
@@ -54,12 +54,12 @@
 			// play some cool sounds ;)
 			centerprint(other, "\n");
 			if(other.health <= 5)
-				stuffcmd(other, "play2 announcer/robotic/lastsecond.ogg\n");
+				play2(other, "announcer/robotic/lastsecond.ogg");
 			else if(other.health < 50)
-				stuffcmd(other, "play2 announcer/robotic/narrowly.ogg\n");
+				play2(other, "announcer/robotic/narrowly.ogg");
 			// sound not available
 			// else if(self.items == IT_CELLS)
-			//	stuffcmd(other, "play2 announce/robotic/ammo.ogg\n");
+			//	play2(other, "announce/robotic/ammo.ogg");
 
 			if (self.items & IT_NEX)
 				W_GiveWeapon (other, IT_NEX, "Nex");
@@ -73,7 +73,7 @@
 		{
 			pickedup = TRUE;
 			// sound not available
-			// stuffcmd(other, "play2 announce/robotic/extra.ogg\nplay2 announce/robotic/_lives.ogg\n");
+			// play2(other, "announce/robotic/extra.ogg\nplay2 announce/robotic/_lives.ogg");
 			other.armorvalue = other.armorvalue + cvar("g_minstagib_extralives");
 			sprint(other, "^3You picked up some extra lives\n");
 		}
@@ -83,7 +83,7 @@
 		{
 			pickedup = TRUE;
 			// sound not available
-			// stuffcmd(other, "play2 announce/robotic/invisible.ogg\n");
+			// play2(other, "announce/robotic/invisible.ogg");
 			other.strength_finished = max(other.strength_finished, time) + cvar("g_balance_powerup_strength_time");
 		}
 
@@ -92,7 +92,7 @@
 		{
 			pickedup = TRUE;
 			// sound not available
-			// stuffcmd(other, "play2 announce/robotic/speed.ogg\n");
+			// play2(other, "announce/robotic/speed.ogg");
 			other.invincible_finished = max(other.invincible_finished, time) + cvar("g_balance_powerup_strength_time");
 		}
 	}
@@ -376,7 +376,10 @@
 	self.touch = Item_Touch;
 	setmodel (self, self.mdl); // precision set below
 	self.effects |= EF_LOWPRECISION;
-	setsize (self, '-16 -16 0', '16 16 32');
+	if((itemflags & FL_POWERUP) || self.health || self.armorvalue)
+		setsize (self, '-16 -16 0', '16 16 48');
+	else
+		setsize (self, '-16 -16 0', '16 16 32');
 	if (itemflags & FL_WEAPON)
 	{
 		// neutral team color for pickup weapons

Modified: branches/nexuiz-2.0/data/qcsrc/server/teamplay.qc
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/teamplay.qc	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/teamplay.qc	2007-05-02 18:09:25 UTC (rev 2454)
@@ -5,13 +5,14 @@
 float GAME_RUNEMATCH		= 5;
 float GAME_LMS			= 6;
 float GAME_ARENA		= 7;
+float GAME_KEYHUNT		= 8;
 
 // client counts for each team
 float c1, c2, c3, c4;
 // # of bots on those teams
 float cb1, cb2, cb3, cb4;
 
-float g_domination, g_ctf, g_tdm;
+float g_domination, g_ctf, g_tdm, g_keyhunt;
 
 float audit_teams_time;
 
@@ -44,9 +45,9 @@
 	if(t == COLOR_TEAM2)
 		return "Blue Team";
 	if(t == COLOR_TEAM3)
+		return "Yellow Team";
+	if(t == COLOR_TEAM4)
 		return "Pink Team";
-	if(t == COLOR_TEAM4)
-		return "Yellow Team";
 	return "Neutral Team";
 }
 string ColoredTeamName(float t)
@@ -57,9 +58,9 @@
 	if(t == COLOR_TEAM2)
 		return "^4Blue Team^7";
 	if(t == COLOR_TEAM3)
+		return "^3Yellow Team^7";
+	if(t == COLOR_TEAM4)
 		return "^6Pink Team^7";
-	if(t == COLOR_TEAM4)
-		return "^3Yellow Team^7";
 	return "Neutral Team";
 }
 string TeamNoName(float t)
@@ -70,9 +71,9 @@
 	if(t == 2)
 		return "Blue Team";
 	if(t == 3)
+		return "Yellow Team";
+	if(t == 4)
 		return "Pink Team";
-	if(t == 4)
-		return "Yellow Team";
 	return "Neutral Team";
 }
 
@@ -101,6 +102,7 @@
 	cvar_set("g_runematch", "0");
 	cvar_set("g_lms", "0");
 	cvar_set("g_arena", "0");
+	cvar_set("g_keyhunt", "0");
 	cvar_set("teamplay", "0");
 }
 
@@ -224,6 +226,16 @@
 		gamemode_name = "Arena";
 		teams_matter = 0;
 	}
+	else if(game == GAME_KEYHUNT || cvar("g_keyhunt"))
+	{
+		ResetGameCvars();
+		game = GAME_KEYHUNT;
+		cvar_set("g_keyhunt", "1");
+		fraglimit_override = cvar("g_keyhunt_point_limit");
+		ActivateTeamplay();
+		gamemode_name = "Key Hunt";
+		teams_matter = 1;
+	}
 	else
 	{
 		// we can only assume...
@@ -260,6 +272,8 @@
 		runematch_init();
 	else if (game == GAME_TEAM_DEATHMATCH)//cvar("g_runematch"))
 		tdm_init();
+	else if (game == GAME_KEYHUNT)//cvar("g_keyhunt"))
+		kh_init();
 
 	// those mutators rule each other out
 	if(cvar("g_minstagib"))
@@ -286,6 +300,7 @@
 	g_domination = cvar("g_domination");
 	g_ctf = cvar("g_ctf");
 	g_tdm = cvar("g_tdm");
+	g_keyhunt = cvar("g_keyhunt");
 }
 
 string GetClientVersionMessage(float v) {
@@ -503,7 +518,10 @@
 	else
 	{
 		// cover anything else by treating it like tdm with no teams spawned
-		dm = cvar("g_tdm_teams");
+		if(g_keyhunt)
+			dm = kh_teams;
+		else
+			dm = cvar("g_tdm_teams");
 		if(dm < 2)
 			error("g_tdm_teams < 2, not enough teams to play team deathmatch\n");
 
@@ -634,6 +652,8 @@
 			error("Too few teams available for domination\n");
 		else if(g_ctf)
 			error("Too few teams available for ctf\n");
+		else if(g_keyhunt)
+			error("Too few teams available for key hunt\n");
 		else
 			error("Too few teams available for team deathmatch\n");
 	}
@@ -719,7 +739,7 @@
 	float smallest, selectedteam;
 
 	// don't join a team if we're not playing a team game
-	if(!cvar("teamplay") && !g_domination && !g_ctf)
+	if(!cvar("teamplay") && !g_domination && !g_ctf && !g_keyhunt)
 		return 0;
 
 	// find out what teams are available
@@ -948,7 +968,6 @@
 	float smallestteam, smallestteam_count, steam;
 	float lowest_bot_score, lowest_player_score;
 	entity head, lowest_bot, lowest_player, selected;
-	string m;
 
 	smallestteam = 0;
 	smallestteam_count = 999999999;
@@ -1081,17 +1100,8 @@
 	SetPlayerTeam(selected, smallestteam, source_team, FALSE);
 
 	if(selected.deadflag == DEAD_NO)
-			Damage(selected, selected, selected, 100000, DEATH_AUTOTEAMCHANGE, selected.origin, '0 0 0');
-	m = "You have been moved into a different team to improve team balance\nYou are now on: ";
-	if (selected.team == 5)
-		m = strcat(m, "^1Red Team");
-	else if (selected.team == 14)
-		m = strcat(m, "^4Blue Team");
-	else if (selected.team == 10)
-		m = strcat(m, "^6Pink Team");
-	else if (selected.team == 13)
-		m = strcat(m, "^3Yellow Team");
-	centerprint(selected, m);
+		Damage(selected, selected, selected, 100000, DEATH_AUTOTEAMCHANGE, selected.origin, '0 0 0');
+	centerprint(selected, strcat("You have been moved into a different team to improve team balance\nYou are now on: ", ColoredTeamName(selected.team)));
 }
 
 float lastRebalanceInfo;
@@ -1242,8 +1252,8 @@
 
 	numteams = cvar("g_tdm_teams");
 
-	tdm_spawnteam("Red", 4);
-	tdm_spawnteam("Blue", 13);
+	tdm_spawnteam("Red", COLOR_TEAM1-1);
+	tdm_spawnteam("Blue", COLOR_TEAM2-1);
 };
 
 void() tdm_delayedinit =

Modified: branches/nexuiz-2.0/data/qcsrc/server/w_nex.qc
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/w_nex.qc	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/w_nex.qc	2007-05-02 18:09:25 UTC (rev 2454)
@@ -69,67 +69,67 @@
 		{
 			centerprint(self, "you're dead now...\n");
 			Damage(self, self, self, 5, DEATH_NOAMMO, self.origin, '0 0 0');
-			stuffcmd(self, "play2 announcer/robotic/terminated.ogg\n");
+			play2(self, "announcer/robotic/terminated.ogg");
 		}
 		if (self.health == 10)
 		{
 			centerprint(self, "^11^7 second left to find some ammo\n");
 			Damage(self, self, self, 5, DEATH_NOAMMO, self.origin, '0 0 0');
-			stuffcmd(self, "play2 announcer/robotic/1.ogg\n");
+			play2(self, "announcer/robotic/1.ogg");
 		}
 		if (self.health == 20)
 		{
 			centerprint(self, "^12^7 seconds left to find some ammo\n");
 			Damage(self, self, self, 10, DEATH_NOAMMO, self.origin, '0 0 0');
-			stuffcmd(self, "play2 announcer/robotic/2.ogg\n");
+			play2(self, "announcer/robotic/2.ogg");
 		}
 		if (self.health == 30)
 		{
 			centerprint(self, "^13^7 seconds left to find some ammo\n");
 			Damage(self, self, self, 10, DEATH_NOAMMO, self.origin, '0 0 0');
-			stuffcmd(self, "play2 announcer/robotic/3.ogg\n");
+			play2(self, "announcer/robotic/3.ogg");
 		}
 		if (self.health == 40)
 		{
 			centerprint(self, "^14^7 seconds left to find some ammo\n");
 			Damage(self, self, self, 10, DEATH_NOAMMO, self.origin, '0 0 0');
-			stuffcmd(self, "play2 announcer/robotic/4.ogg\n");
+			play2(self, "announcer/robotic/4.ogg");
 		}
 		if (self.health == 50)
 		{
 			centerprint(self, "^15^7 seconds left to find some ammo\n");
 			Damage(self, self, self, 10, DEATH_NOAMMO, self.origin, '0 0 0');
-			stuffcmd(self, "play2 announcer/robotic/5.ogg\n");
+			play2(self, "announcer/robotic/5.ogg");
 		}
 		if (self.health == 60)
 		{
 			centerprint(self, "^36^7 seconds left to find some ammo\n");
 			Damage(self, self, self, 10, DEATH_NOAMMO, self.origin, '0 0 0');
-			stuffcmd(self, "play2 announcer/robotic/6.ogg\n");
+			play2(self, "announcer/robotic/6.ogg");
 		}
 		if (self.health == 70)
 		{
 			centerprint(self, "^37^7 seconds left to find some ammo\n");
 			Damage(self, self, self, 10, DEATH_NOAMMO, self.origin, '0 0 0');
-			stuffcmd(self, "play2 announcer/robotic/7.ogg\n");
+			play2(self, "announcer/robotic/7.ogg");
 		}
 		if (self.health == 80)
 		{
 			centerprint(self, "^38^7 seconds left to find some ammo\n");
 			Damage(self, self, self, 10, DEATH_NOAMMO, self.origin, '0 0 0');
-			stuffcmd(self, "play2 announcer/robotic/8.ogg\n");
+			play2(self, "announcer/robotic/8.ogg");
 		}
 		if (self.health == 90)
 		{
 			centerprint(self, "^39^7 seconds left to find some ammo\n");
 			Damage(self, self, self, 10, DEATH_NOAMMO, self.origin, '0 0 0');
-			stuffcmd(self, "play2 announcer/robotic/9.ogg\n");
+			play2(self, "announcer/robotic/9.ogg");
 		}
 		if (self.health == 100)
 		{
 			centerprint(self, "get some ammo or\nyou'll be dead in ^310^7 seconds...");
 			Damage(self, self, self, 10, DEATH_NOAMMO, self.origin, '0 0 0');
-			stuffcmd(self, "play2 announcer/robotic/10.ogg\n");
+			play2(self, "announcer/robotic/10.ogg");
 		}
 	}
 	self.minstagib_nextthink = time + 1;

Modified: branches/nexuiz-2.0/data/qcsrc/server/waypointsprites.qc
===================================================================
--- branches/nexuiz-2.0/data/qcsrc/server/waypointsprites.qc	2007-05-02 17:12:05 UTC (rev 2453)
+++ branches/nexuiz-2.0/data/qcsrc/server/waypointsprites.qc	2007-05-02 18:09:25 UTC (rev 2454)
@@ -9,7 +9,32 @@
 float waypointsprite_limitedrange;
 
 ..entity owned_by_field;
+.float(entity) waypointsprite_for_player; // returns a model index or 0 for hide
+float waypointsprite_for_player_default(entity e)
+{
+	// personal waypoints
+	if(self.enemy)
+		if(self.enemy != other)
+			return FALSE;
 
+	// team waypoints
+	if(self.team)
+	{
+		if(self.team != other.team)
+			return FALSE;
+		if(other.classname != "player")
+			return FALSE;
+	}
+
+	// fixed waypoints
+	if(self.currentammo) // hidable?
+		if(other.cvar_cl_hidewaypoints) // wants to hide;
+			return FALSE;
+
+	// otherwise, accept the model
+	return self.modelindex;
+}
+
 void WaypointSprite_Init()
 {
 	waypointsprite_fadedistance = vlen(world.maxs - world.mins);
@@ -104,6 +129,7 @@
 	vector realorigin, porigin;
 	float distancealpha, timealpha;
 	float distance;
+	float newmodel;
 
 	if(self.health)
 	{
@@ -115,21 +141,12 @@
 	else
 		timealpha = 1;
 
-	if(self.enemy)
-		if(self.enemy != other)
-			return FALSE;
-	if(self.team)
-	{
-		if(self.team != other.team)
-			return FALSE;
-		if(other.classname != "player")
-			return FALSE;
-	}
+	// customize WP
+	newmodel = self.waypointsprite_for_player(other);
+	if(newmodel == 0)
+		return FALSE;
+	self.modelindex = newmodel;
 
-	if(self.currentammo) // hidable?
-		if(other.cvar_cl_hidewaypoints) // wants to hide;
-			return FALSE;
-
 	porigin = other.origin + other.view_ofs_z * '0 0 1';
 
 #ifdef ATTACHMENT_WORKS_WITH_EF_NODEPTHTEST
@@ -228,7 +245,12 @@
 	wp.think = WaypointSprite_Think;
 	wp.nextthink = time;
 	wp.effects = EF_NODEPTHTEST | EF_LOWPRECISION;
-	setmodel(wp, strcat("models/sprites/", spr, ".sp2")); // precision set above
+	if(spr != "")
+		setmodel(wp, strcat("models/sprites/", spr, ".sp2")); // precision set above
+	else
+		wp.model = "waypoint";
+	setsize(wp, '0 0 0', '0 0 0');
+	wp.waypointsprite_for_player = waypointsprite_for_player_default;
 	return wp;
 }
 




More information about the nexuiz-commits mailing list