]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_keyhunt.qc
71a65c6b596cd7935f287a5400037eabf9300339
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / gamemode_keyhunt.qc
1 #include "gamemode_keyhunt.qh"
2
3 #include "gamemode.qh"
4
5 int autocvar_g_keyhunt_point_leadlimit;
6 bool autocvar_g_keyhunt_team_spawns;
7 #define autocvar_g_keyhunt_point_limit cvar("g_keyhunt_point_limit")
8 int autocvar_g_keyhunt_teams;
9 int autocvar_g_keyhunt_teams_override;
10
11 // #define KH_PLAYER_USE_ATTACHMENT
12 // #define KH_PLAYER_USE_CARRIEDMODEL
13
14 #ifdef KH_PLAYER_USE_ATTACHMENT
15 const vector KH_PLAYER_ATTACHMENT_DIST_ROTATED = '0 -4 0';
16 const vector KH_PLAYER_ATTACHMENT_DIST = '4 0 0';
17 const vector KH_PLAYER_ATTACHMENT = '0 0 0';
18 const vector KH_PLAYER_ATTACHMENT_ANGLES = '0 0 0';
19 const string KH_PLAYER_ATTACHMENT_BONE = "";
20 #else
21 const float KH_KEY_ZSHIFT = 22;
22 const float KH_KEY_XYDIST = 24;
23 const float KH_KEY_XYSPEED = 45;
24 #endif
25 const float KH_KEY_WP_ZSHIFT = 20;
26
27 const vector KH_KEY_MIN = '-10 -10 -46';
28 const vector KH_KEY_MAX = '10 10 3';
29 const float KH_KEY_BRIGHTNESS = 2;
30
31 float kh_no_radar_circles;
32
33 // kh_state
34 //     bits  0- 4: team of key 1, or 0 for no such key, or 30 for dropped, or 31 for self
35 //     bits  5- 9: team of key 2, or 0 for no such key, or 30 for dropped, or 31 for self
36 //     bits 10-14: team of key 3, or 0 for no such key, or 30 for dropped, or 31 for self
37 //     bits 15-19: team of key 4, or 0 for no such key, or 30 for dropped, or 31 for self
38 .float kh_state;
39 .float siren_time;  //  time delay the siren
40 //.float stuff_time;  //  time delay to stuffcmd a cvar
41
42 float kh_keystatus[17];
43 //kh_keystatus[0] = status of dropped keys, kh_keystatus[1 - 16] = player #
44 //replace 17 with cvar("maxplayers") or similar !!!!!!!!!
45 //for(i = 0; i < maxplayers; ++i)
46 //      kh_keystatus[i] = "0";
47
48 float kh_Team_ByID(float t)
49 {
50         if(t == 0) return NUM_TEAM_1;
51         if(t == 1) return NUM_TEAM_2;
52         if(t == 2) return NUM_TEAM_3;
53         if(t == 3) return NUM_TEAM_4;
54         return 0;
55 }
56
57 //entity kh_worldkeylist;
58 .entity kh_worldkeynext;
59 entity kh_controller;
60 //float kh_tracking_enabled;
61 float kh_teams;
62 float kh_interferemsg_time, kh_interferemsg_team;
63 .entity kh_next, kh_prev; // linked list
64 .float kh_droptime;
65 .float kh_dropperteam;
66 .entity kh_previous_owner;
67 .float kh_previous_owner_playerid;
68 .float kh_cp_duration;
69
70 float kh_key_dropped, kh_key_carried;
71
72 const float ST_KH_CAPS = 1;
73 const float SP_KH_CAPS = 4;
74 const float SP_KH_PUSHES = 5;
75 const float SP_KH_DESTROYS = 6;
76 const float SP_KH_PICKUPS = 7;
77 const float SP_KH_KCKILLS = 8;
78 const float SP_KH_LOSSES = 9;
79 void kh_ScoreRules(float teams)
80 {
81         ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, SFL_SORT_PRIO_PRIMARY, true);
82         ScoreInfo_SetLabel_TeamScore(  ST_KH_CAPS,      "caps",      SFL_SORT_PRIO_SECONDARY);
83         ScoreInfo_SetLabel_PlayerScore(SP_KH_CAPS,      "caps",      SFL_SORT_PRIO_SECONDARY);
84         ScoreInfo_SetLabel_PlayerScore(SP_KH_PUSHES,    "pushes",    0);
85         ScoreInfo_SetLabel_PlayerScore(SP_KH_DESTROYS,  "destroyed", SFL_LOWER_IS_BETTER);
86         ScoreInfo_SetLabel_PlayerScore(SP_KH_PICKUPS,   "pickups",   0);
87         ScoreInfo_SetLabel_PlayerScore(SP_KH_KCKILLS,   "kckills",   0);
88         ScoreInfo_SetLabel_PlayerScore(SP_KH_LOSSES,    "losses",    SFL_LOWER_IS_BETTER);
89         ScoreRules_basics_end();
90 }
91
92 float kh_KeyCarrier_waypointsprite_visible_for_player(entity e)  // runs all the time
93 {SELFPARAM();
94         if(!IS_PLAYER(e) || self.team != e.team)
95                 if(!kh_tracking_enabled)
96                         return false;
97
98         return true;
99 }
100
101 float kh_Key_waypointsprite_visible_for_player(entity e) // ??
102 {SELFPARAM();
103         if(!kh_tracking_enabled)
104                 return false;
105         if(!self.owner)
106                 return true;
107         if(!self.owner.owner)
108                 return true;
109         return false;  // draw only when key is not owned
110 }
111
112 void kh_update_state()
113 {
114         entity player;
115         entity key;
116         float s;
117         float f;
118
119         s = 0;
120         FOR_EACH_KH_KEY(key)
121         {
122                 if(key.owner)
123                         f = key.team;
124                 else
125                         f = 30;
126                 s |= pow(32, key.count) * f;
127         }
128
129         FOR_EACH_CLIENT(player)
130         {
131                 player.kh_state = s;
132         }
133
134         FOR_EACH_KH_KEY(key)
135         {
136                 if(key.owner)
137                         key.owner.kh_state |= pow(32, key.count) * 31;
138         }
139         //print(ftos((nextent(world)).kh_state), "\n");
140 }
141
142
143
144
145 var kh_Think_t kh_Controller_Thinkfunc;
146 void kh_Controller_SetThink(float t, kh_Think_t func)  // runs occasionaly
147 {
148         kh_Controller_Thinkfunc = func;
149         kh_controller.cnt = ceil(t);
150         if(t == 0)
151                 kh_controller.nextthink = time; // force
152 }
153 void kh_WaitForPlayers();
154 void kh_Controller_Think()  // called a lot
155 {SELFPARAM();
156         if(intermission_running)
157                 return;
158         if(self.cnt > 0)
159         { if(self.think != kh_WaitForPlayers) { self.cnt -= 1; } }
160         else if(self.cnt == 0)
161         {
162                 self.cnt -= 1;
163                 kh_Controller_Thinkfunc();
164         }
165         self.nextthink = time + 1;
166 }
167
168 // frags f: take from cvar * f
169 // frags 0: no frags
170 void kh_Scores_Event(entity player, entity key, string what, float frags_player, float frags_owner)  // update the score when a key is captured
171 {
172         string s;
173         if(intermission_running)
174                 return;
175
176         if(frags_player)
177                 UpdateFrags(player, frags_player);
178
179         if(key && key.owner && frags_owner)
180                 UpdateFrags(key.owner, frags_owner);
181
182         if(!autocvar_sv_eventlog)  //output extra info to the console or text file
183                 return;
184
185         s = strcat(":keyhunt:", what, ":", ftos(player.playerid), ":", ftos(frags_player));
186
187         if(key && key.owner)
188                 s = strcat(s, ":", ftos(key.owner.playerid));
189         else
190                 s = strcat(s, ":0");
191
192         s = strcat(s, ":", ftos(frags_owner), ":");
193
194         if(key)
195                 s = strcat(s, key.netname);
196
197         GameLogEcho(s);
198 }
199
200 vector kh_AttachedOrigin(entity e)  // runs when a team captures the flag, it can run 2 or 3 times.
201 {
202         if(e.tag_entity)
203         {
204                 makevectors(e.tag_entity.angles);
205                 return e.tag_entity.origin + e.origin.x * v_forward - e.origin.y * v_right + e.origin.z * v_up;
206         }
207         else
208                 return e.origin;
209 }
210
211 void kh_Key_Attach(entity key)  // runs when a player picks up a key and several times when a key is assigned to a player at the start of a round
212 {
213 #ifdef KH_PLAYER_USE_ATTACHMENT
214         entity first;
215         first = key.owner.kh_next;
216         if(key == first)
217         {
218                 setattachment(key, key.owner, KH_PLAYER_ATTACHMENT_BONE);
219                 if(key.kh_next)
220                 {
221                         setattachment(key.kh_next, key, "");
222                         setorigin(key, key.kh_next.origin - 0.5 * KH_PLAYER_ATTACHMENT_DIST);
223                         setorigin(key.kh_next, KH_PLAYER_ATTACHMENT_DIST_ROTATED);
224                         key.kh_next.angles = '0 0 0';
225                 }
226                 else
227                         setorigin(key, KH_PLAYER_ATTACHMENT);
228                 key.angles = KH_PLAYER_ATTACHMENT_ANGLES;
229         }
230         else
231         {
232                 setattachment(key, key.kh_prev, "");
233                 if(key.kh_next)
234                         setattachment(key.kh_next, key, "");
235                 setorigin(key, KH_PLAYER_ATTACHMENT_DIST_ROTATED);
236                 setorigin(first, first.origin - 0.5 * KH_PLAYER_ATTACHMENT_DIST);
237                 key.angles = '0 0 0';
238         }
239 #else
240         setattachment(key, key.owner, "");
241         setorigin(key, '0 0 1' * KH_KEY_ZSHIFT);  // fixing x, y in think
242         key.angles_y -= key.owner.angles.y;
243 #endif
244         key.flags = 0;
245         key.solid = SOLID_NOT;
246         key.movetype = MOVETYPE_NONE;
247         key.team = key.owner.team;
248         key.nextthink = time;
249         key.damageforcescale = 0;
250         key.takedamage = DAMAGE_NO;
251         key.modelindex = kh_key_carried;
252 }
253
254 void kh_Key_Detach(entity key) // runs every time a key is dropped or lost. Runs several times times when all the keys are captured
255 {
256 #ifdef KH_PLAYER_USE_ATTACHMENT
257         entity first;
258         first = key.owner.kh_next;
259         if(key == first)
260         {
261                 if(key.kh_next)
262                 {
263                         setattachment(key.kh_next, key.owner, KH_PLAYER_ATTACHMENT_BONE);
264                         setorigin(key.kh_next, key.origin + 0.5 * KH_PLAYER_ATTACHMENT_DIST);
265                         key.kh_next.angles = KH_PLAYER_ATTACHMENT_ANGLES;
266                 }
267         }
268         else
269         {
270                 if(key.kh_next)
271                         setattachment(key.kh_next, key.kh_prev, "");
272                 setorigin(first, first.origin + 0.5 * KH_PLAYER_ATTACHMENT_DIST);
273         }
274         // in any case:
275         setattachment(key, world, "");
276         setorigin(key, key.owner.origin + '0 0 1' * (PL_MIN.z - KH_KEY_MIN_z));
277         key.angles = key.owner.angles;
278 #else
279         setorigin(key, key.owner.origin + key.origin.z * '0 0 1');
280         setattachment(key, world, "");
281         key.angles_y += key.owner.angles.y;
282 #endif
283         key.flags = FL_ITEM;
284         key.solid = SOLID_TRIGGER;
285         key.movetype = MOVETYPE_TOSS;
286         key.pain_finished = time + autocvar_g_balance_keyhunt_delay_return;
287         key.damageforcescale = autocvar_g_balance_keyhunt_damageforcescale;
288         key.takedamage = DAMAGE_YES;
289         // let key.team stay
290         key.modelindex = kh_key_dropped;
291         key.kh_previous_owner = key.owner;
292         key.kh_previous_owner_playerid = key.owner.playerid;
293 }
294
295 void kh_Key_AssignTo(entity key, entity player)  // runs every time a key is picked up or assigned. Runs prior to kh_key_attach
296 {
297         entity k;
298         float ownerteam0, ownerteam;
299         if(key.owner == player)
300                 return;
301
302         ownerteam0 = kh_Key_AllOwnedByWhichTeam();
303
304         if(key.owner)
305         {
306                 kh_Key_Detach(key);
307
308                 // remove from linked list
309                 if(key.kh_next)
310                         key.kh_next.kh_prev = key.kh_prev;
311                 key.kh_prev.kh_next = key.kh_next;
312                 key.kh_next = world;
313                 key.kh_prev = world;
314
315                 if(key.owner.kh_next == world)
316                 {
317                         // No longer a key carrier
318                         if(!kh_no_radar_circles)
319                                 WaypointSprite_Ping(key.owner.waypointsprite_attachedforcarrier);
320                         WaypointSprite_DetachCarrier(key.owner);
321                 }
322         }
323
324         key.owner = player;
325
326         if(player)
327         {
328                 // insert into linked list
329                 key.kh_next = player.kh_next;
330                 key.kh_prev = player;
331                 player.kh_next = key;
332                 if(key.kh_next)
333                         key.kh_next.kh_prev = key;
334
335                 float i;
336                 i = kh_keystatus[key.owner.playerid];
337                         if(key.netname == "^1red key")
338                                 i += 1;
339                         if(key.netname == "^4blue key")
340                                 i += 2;
341                         if(key.netname == "^3yellow key")
342                                 i += 4;
343                         if(key.netname == "^6pink key")
344                                 i += 8;
345                 kh_keystatus[key.owner.playerid] = i;
346
347                 kh_Key_Attach(key);
348
349                 if(key.kh_next == world)
350                 {
351                         // player is now a key carrier
352                         entity wp = WaypointSprite_AttachCarrier(WP_Null, player, RADARICON_FLAGCARRIER);
353                         wp.colormod = colormapPaletteColor(player.team - 1, 0);
354                         player.waypointsprite_attachedforcarrier.waypointsprite_visible_for_player = kh_KeyCarrier_waypointsprite_visible_for_player;
355                         WaypointSprite_UpdateRule(player.waypointsprite_attachedforcarrier, player.team, SPRITERULE_TEAMPLAY);
356                         if(player.team == NUM_TEAM_1)
357                                 WaypointSprite_UpdateSprites(player.waypointsprite_attachedforcarrier, WP_KeyCarrierRed, WP_KeyCarrierFriend, WP_KeyCarrierRed);
358                         else if(player.team == NUM_TEAM_2)
359                                 WaypointSprite_UpdateSprites(player.waypointsprite_attachedforcarrier, WP_KeyCarrierBlue, WP_KeyCarrierFriend, WP_KeyCarrierBlue);
360                         else if(player.team == NUM_TEAM_3)
361                                 WaypointSprite_UpdateSprites(player.waypointsprite_attachedforcarrier, WP_KeyCarrierYellow, WP_KeyCarrierFriend, WP_KeyCarrierYellow);
362                         else if(player.team == NUM_TEAM_4)
363                                 WaypointSprite_UpdateSprites(player.waypointsprite_attachedforcarrier, WP_KeyCarrierPink, WP_KeyCarrierFriend, WP_KeyCarrierPink);
364                         if(!kh_no_radar_circles)
365                                 WaypointSprite_Ping(player.waypointsprite_attachedforcarrier);
366                 }
367         }
368
369         // moved that here, also update if there's no player
370         kh_update_state();
371
372         key.pusher = world;
373
374         ownerteam = kh_Key_AllOwnedByWhichTeam();
375         if(ownerteam != ownerteam0)
376         {
377                 if(ownerteam != -1)
378                 {
379                         kh_interferemsg_time = time + 0.2;
380                         kh_interferemsg_team = player.team;
381
382                         // audit all key carrier sprites, update them to RUN HERE
383                         FOR_EACH_KH_KEY(k)
384                         {
385                                 if (!k.owner) continue;
386                                 entity first = WP_Null;
387                                 FOREACH(Waypoints, it.netname == k.owner.waypointsprite_attachedforcarrier.model1, LAMBDA(first = it; break));
388                                 entity third = WP_Null;
389                                 FOREACH(Waypoints, it.netname == k.owner.waypointsprite_attachedforcarrier.model3, LAMBDA(third = it; break));
390                                 WaypointSprite_UpdateSprites(k.owner.waypointsprite_attachedforcarrier, first, WP_KeyCarrierFinish, third);
391                         }
392                 }
393                 else
394                 {
395                         kh_interferemsg_time = 0;
396
397                         // audit all key carrier sprites, update them to RUN HERE
398                         FOR_EACH_KH_KEY(k)
399                         {
400                                 if (!k.owner) continue;
401                                 entity first = WP_Null;
402                                 FOREACH(Waypoints, it.netname == k.owner.waypointsprite_attachedforcarrier.model1, LAMBDA(first = it; break));
403                                 entity third = WP_Null;
404                                 FOREACH(Waypoints, it.netname == k.owner.waypointsprite_attachedforcarrier.model3, LAMBDA(third = it; break));
405                                 WaypointSprite_UpdateSprites(k.owner.waypointsprite_attachedforcarrier, first, WP_KeyCarrierFriend, third);
406                         }
407                 }
408         }
409 }
410
411 void kh_Key_Damage(entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
412 {SELFPARAM();
413         if(self.owner)
414                 return;
415         if(ITEM_DAMAGE_NEEDKILL(deathtype))
416         {
417                 // touching lava, or hurt trigger
418                 // what shall we do?
419                 // immediately return is bad
420                 // maybe start a shorter countdown?
421         }
422         if(vlen(force) <= 0)
423                 return;
424         if(time > self.pushltime)
425                 if(IS_PLAYER(attacker))
426                         self.team = attacker.team;
427 }
428
429 void kh_Key_Collect(entity key, entity player)  //a player picks up a dropped key
430 {
431         sound(player, CH_TRIGGER, SND_KH_COLLECT, VOL_BASE, ATTEN_NORM);
432
433         if(key.kh_dropperteam != player.team)
434         {
435                 kh_Scores_Event(player, key, "collect", autocvar_g_balance_keyhunt_score_collect, 0);
436                 PlayerScore_Add(player, SP_KH_PICKUPS, 1);
437         }
438         key.kh_dropperteam = 0;
439         Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(key, INFO_KEYHUNT_PICKUP_), player.netname);
440
441         kh_Key_AssignTo(key, player); // this also updates .kh_state
442 }
443
444 void kh_Key_Touch()  // runs many, many times when a key has been dropped and can be picked up
445 {SELFPARAM();
446         if(intermission_running)
447                 return;
448
449         if(self.owner) // already carried
450                 return;
451
452         if(ITEM_TOUCH_NEEDKILL())
453         {
454                 // touching sky, or nodrop
455                 // what shall we do?
456                 // immediately return is bad
457                 // maybe start a shorter countdown?
458         }
459
460         if (!IS_PLAYER(other))
461                 return;
462         if(other.deadflag != DEAD_NO)
463                 return;
464         if(other == self.enemy)
465                 if(time < self.kh_droptime + autocvar_g_balance_keyhunt_delay_collect)
466                         return;  // you just dropped it!
467         kh_Key_Collect(self, other);
468 }
469
470 void kh_Key_Remove(entity key)  // runs after when all the keys have been collected or when a key has been dropped for more than X seconds
471 {
472         entity o;
473         o = key.owner;
474         kh_Key_AssignTo(key, world);
475         if(o) // it was attached
476                 WaypointSprite_Kill(key.waypointsprite_attachedforcarrier);
477         else // it was dropped
478                 WaypointSprite_DetachCarrier(key);
479
480         // remove key from key list
481         if (kh_worldkeylist == key)
482                 kh_worldkeylist = kh_worldkeylist.kh_worldkeynext;
483         else
484         {
485                 o = kh_worldkeylist;
486                 while (o)
487                 {
488                         if (o.kh_worldkeynext == key)
489                         {
490                                 o.kh_worldkeynext = o.kh_worldkeynext.kh_worldkeynext;
491                                 break;
492                         }
493                         o = o.kh_worldkeynext;
494                 }
495         }
496
497         remove(key);
498
499         kh_update_state();
500 }
501
502 void kh_FinishRound()  // runs when a team captures the keys
503 {
504         // prepare next round
505         kh_interferemsg_time = 0;
506         entity key;
507
508         kh_no_radar_circles = true;
509         FOR_EACH_KH_KEY(key)
510                 kh_Key_Remove(key);
511         kh_no_radar_circles = false;
512
513         Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_KEYHUNT_ROUNDSTART, autocvar_g_balance_keyhunt_delay_round);
514         kh_Controller_SetThink(autocvar_g_balance_keyhunt_delay_round, kh_StartRound);
515 }
516
517 void kh_WinnerTeam(float teem)  // runs when a team wins // Samual: Teem?.... TEEM?!?! what the fuck is wrong with you people
518 {
519         // all key carriers get some points
520         vector firstorigin, lastorigin, midpoint;
521         float first;
522         entity key;
523         float score;
524         score = (kh_teams - 1) * autocvar_g_balance_keyhunt_score_capture;
525         DistributeEvenly_Init(score, kh_teams);
526         // twice the score for 3 team games, three times the score for 4 team games!
527         // note: for a win by destroying the key, this should NOT be applied
528         FOR_EACH_KH_KEY(key)
529         {
530                 float f;
531                 f = DistributeEvenly_Get(1);
532                 kh_Scores_Event(key.owner, key, "capture", f, 0);
533                 PlayerTeamScore_Add(key.owner, SP_KH_CAPS, ST_KH_CAPS, 1);
534                 nades_GiveBonus(key.owner, autocvar_g_nades_bonus_score_high);
535         }
536
537         first = true;
538         string keyowner = "";
539         FOR_EACH_KH_KEY(key)
540                 if(key.owner.kh_next == key)
541                 {
542                         if(!first)
543                                 keyowner = strcat(keyowner, ", ");
544                         keyowner = key.owner.netname;
545                         first = false;
546                 }
547
548         Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(teem, INFO_KEYHUNT_CAPTURE_), keyowner);
549
550         first = true;
551         midpoint = '0 0 0';
552         firstorigin = '0 0 0';
553         lastorigin = '0 0 0';
554         FOR_EACH_KH_KEY(key)
555         {
556                 vector thisorigin;
557
558                 thisorigin = kh_AttachedOrigin(key);
559                 //dprint("Key origin: ", vtos(thisorigin), "\n");
560                 midpoint += thisorigin;
561
562                 if(!first)
563                         te_lightning2(world, lastorigin, thisorigin);
564                 lastorigin = thisorigin;
565                 if(first)
566                         firstorigin = thisorigin;
567                 first = false;
568         }
569         if(kh_teams > 2)
570         {
571                 te_lightning2(world, lastorigin, firstorigin);
572         }
573         midpoint = midpoint * (1 / kh_teams);
574         te_customflash(midpoint, 1000, 1, Team_ColorRGB(teem) * 0.5 + '0.5 0.5 0.5');  // make the color >=0.5 in each component
575
576         play2all(SND(KH_CAPTURE));
577         kh_FinishRound();
578 }
579
580 void kh_LoserTeam(float teem, entity lostkey)  // runs when a player pushes a flag carrier off the map
581 {
582         entity player, key, attacker;
583         float players;
584         float keys;
585         float f;
586
587         attacker = world;
588         if(lostkey.pusher)
589                 if(lostkey.pusher.team != teem)
590                         if(IS_PLAYER(lostkey.pusher))
591                                 attacker = lostkey.pusher;
592
593         players = keys = 0;
594
595         if(attacker)
596         {
597                 if(lostkey.kh_previous_owner)
598                         kh_Scores_Event(lostkey.kh_previous_owner, world, "pushed", 0, -autocvar_g_balance_keyhunt_score_push);
599                         // don't actually GIVE him the -nn points, just log
600                 kh_Scores_Event(attacker, world, "push", autocvar_g_balance_keyhunt_score_push, 0);
601                 PlayerScore_Add(attacker, SP_KH_PUSHES, 1);
602                 //centerprint(attacker, "Your push is the best!"); // does this really need to exist?
603         }
604         else
605         {
606                 float of, fragsleft, i, j, thisteam;
607                 of = autocvar_g_balance_keyhunt_score_destroyed_ownfactor;
608
609                 FOR_EACH_PLAYER(player)
610                         if(player.team != teem)
611                                 ++players;
612
613                 FOR_EACH_KH_KEY(key)
614                         if(key.owner && key.team != teem)
615                                 ++keys;
616
617                 if(lostkey.kh_previous_owner)
618                         kh_Scores_Event(lostkey.kh_previous_owner, world, "destroyed", 0, -autocvar_g_balance_keyhunt_score_destroyed);
619                         // don't actually GIVE him the -nn points, just log
620
621                 if(lostkey.kh_previous_owner.playerid == lostkey.kh_previous_owner_playerid)
622                         PlayerScore_Add(lostkey.kh_previous_owner, SP_KH_DESTROYS, 1);
623
624                 DistributeEvenly_Init(autocvar_g_balance_keyhunt_score_destroyed, keys * of + players);
625
626                 FOR_EACH_KH_KEY(key)
627                         if(key.owner && key.team != teem)
628                         {
629                                 f = DistributeEvenly_Get(of);
630                                 kh_Scores_Event(key.owner, world, "destroyed_holdingkey", f, 0);
631                         }
632
633                 fragsleft = DistributeEvenly_Get(players);
634
635                 // Now distribute these among all other teams...
636                 j = kh_teams - 1;
637                 for(i = 0; i < kh_teams; ++i)
638                 {
639                         thisteam = kh_Team_ByID(i);
640                         if(thisteam == teem) // bad boy, no cookie - this WILL happen
641                                 continue;
642
643                         players = 0;
644                         FOR_EACH_PLAYER(player)
645                                 if(player.team == thisteam)
646                                         ++players;
647
648                         DistributeEvenly_Init(fragsleft, j);
649                         fragsleft = DistributeEvenly_Get(j - 1);
650                         DistributeEvenly_Init(DistributeEvenly_Get(1), players);
651
652                         FOR_EACH_PLAYER(player)
653                                 if(player.team == thisteam)
654                                 {
655                                         f = DistributeEvenly_Get(1);
656                                         kh_Scores_Event(player, world, "destroyed", f, 0);
657                                 }
658
659                         --j;
660                 }
661         }
662
663         Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(lostkey, INFO_KEYHUNT_LOST_), lostkey.kh_previous_owner.netname);
664
665         play2all(SND(KH_DESTROY));
666         te_tarexplosion(lostkey.origin);
667
668         kh_FinishRound();
669 }
670
671 void kh_Key_Think()  // runs all the time
672 {SELFPARAM();
673         entity head;
674         //entity player;  // needed by FOR_EACH_PLAYER
675
676         if(intermission_running)
677                 return;
678
679         if(self.owner)
680         {
681 #ifndef KH_PLAYER_USE_ATTACHMENT
682                 makevectors('0 1 0' * (self.cnt + (time % 360) * KH_KEY_XYSPEED));
683                 setorigin(self, v_forward * KH_KEY_XYDIST + '0 0 1' * self.origin.z);
684 #endif
685         }
686
687         // if in nodrop or time over, end the round
688         if(!self.owner)
689                 if(time > self.pain_finished)
690                         kh_LoserTeam(self.team, self);
691
692         if(self.owner)
693         if(kh_Key_AllOwnedByWhichTeam() != -1)
694         {
695                 if(self.siren_time < time)
696                 {
697                         sound(self.owner, CH_TRIGGER, SND_KH_ALARM, VOL_BASE, ATTEN_NORM);  // play a simple alarm
698                         self.siren_time = time + 2.5;  // repeat every 2.5 seconds
699                 }
700
701                 entity key;
702                 vector p;
703                 p = self.owner.origin;
704                 FOR_EACH_KH_KEY(key)
705                         if(vlen(key.owner.origin - p) > autocvar_g_balance_keyhunt_maxdist)
706                                 goto not_winning;
707                 kh_WinnerTeam(self.team);
708 :not_winning
709         }
710
711         if(kh_interferemsg_time && time > kh_interferemsg_time)
712         {
713                 kh_interferemsg_time = 0;
714                 FOR_EACH_PLAYER(head)
715                 {
716                         if(head.team == kh_interferemsg_team)
717                                 if(head.kh_next)
718                                         Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_KEYHUNT_MEET);
719                                 else
720                                         Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_KEYHUNT_HELP);
721                         else
722                                 Send_Notification(NOTIF_ONE, head, MSG_CENTER, APP_TEAM_NUM_4(kh_interferemsg_team, CENTER_KEYHUNT_INTERFERE_));
723                 }
724         }
725
726         self.nextthink = time + 0.05;
727 }
728
729 void key_reset()
730 {SELFPARAM();
731         kh_Key_AssignTo(self, world);
732         kh_Key_Remove(self);
733 }
734
735 const string STR_ITEM_KH_KEY = "item_kh_key";
736 void kh_Key_Spawn(entity initial_owner, float angle, float i)  // runs every time a new flag is created, ie after all the keys have been collected
737 {
738         entity key;
739         key = spawn();
740         key.count = i;
741         key.classname = STR_ITEM_KH_KEY;
742         key.touch = kh_Key_Touch;
743         key.think = kh_Key_Think;
744         key.nextthink = time;
745         key.items = IT_KEY1 | IT_KEY2;
746         key.cnt = angle;
747         key.angles = '0 360 0' * random();
748         key.event_damage = kh_Key_Damage;
749         key.takedamage = DAMAGE_YES;
750         key.modelindex = kh_key_dropped;
751         key.model = "key";
752         key.kh_dropperteam = 0;
753         key.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP;
754         setsize(key, KH_KEY_MIN, KH_KEY_MAX);
755         key.colormod = Team_ColorRGB(initial_owner.team) * KH_KEY_BRIGHTNESS;
756         key.reset = key_reset;
757
758         switch(initial_owner.team)
759         {
760                 case NUM_TEAM_1:
761                         key.netname = "^1red key";
762                         break;
763                 case NUM_TEAM_2:
764                         key.netname = "^4blue key";
765                         break;
766                 case NUM_TEAM_3:
767                         key.netname = "^3yellow key";
768                         break;
769                 case NUM_TEAM_4:
770                         key.netname = "^6pink key";
771                         break;
772                 default:
773                         key.netname = "NETGIER key";
774                         break;
775         }
776
777         // link into key list
778         key.kh_worldkeynext = kh_worldkeylist;
779         kh_worldkeylist = key;
780
781         Send_Notification(NOTIF_ONE, initial_owner, MSG_CENTER, APP_TEAM_NUM_4(initial_owner.team, CENTER_KEYHUNT_START_));
782
783         WaypointSprite_Spawn(WP_KeyDropped, 0, 0, key, '0 0 1' * KH_KEY_WP_ZSHIFT, world, key.team, key, waypointsprite_attachedforcarrier, false, RADARICON_FLAG);
784         key.waypointsprite_attachedforcarrier.waypointsprite_visible_for_player = kh_Key_waypointsprite_visible_for_player;
785
786         kh_Key_AssignTo(key, initial_owner);
787 }
788
789 // -1 when no team completely owns all keys yet
790 float kh_Key_AllOwnedByWhichTeam()  // constantly called. check to see if all the keys are owned by the same team
791 {
792         entity key;
793         float teem;
794         float keys;
795
796         teem = -1;
797         keys = kh_teams;
798         FOR_EACH_KH_KEY(key)
799         {
800                 if(!key.owner)
801                         return -1;
802                 if(teem == -1)
803                         teem = key.team;
804                 else if(teem != key.team)
805                         return -1;
806                 --keys;
807         }
808         if(keys != 0)
809                 return -1;
810         return teem;
811 }
812
813 void kh_Key_DropOne(entity key)
814 {
815         // prevent collecting this one for some time
816         entity player;
817         player = key.owner;
818
819         key.kh_droptime = time;
820         key.enemy = player;
821
822         kh_Scores_Event(player, key, "dropkey", 0, 0);
823         PlayerScore_Add(player, SP_KH_LOSSES, 1);
824         Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(key, INFO_KEYHUNT_DROP_), player.netname);
825
826         kh_Key_AssignTo(key, world);
827         makevectors(player.v_angle);
828         key.velocity = W_CalculateProjectileVelocity(player.velocity, autocvar_g_balance_keyhunt_throwvelocity * v_forward, false);
829         key.pusher = world;
830         key.pushltime = time + autocvar_g_balance_keyhunt_protecttime;
831         key.kh_dropperteam = key.team;
832
833         sound(player, CH_TRIGGER, SND_KH_DROP, VOL_BASE, ATTEN_NORM);
834 }
835
836 void kh_Key_DropAll(entity player, float suicide) // runs whenever a player dies
837 {
838         entity key;
839         entity mypusher;
840         if(player.kh_next)
841         {
842                 mypusher = world;
843                 if(player.pusher)
844                         if(time < player.pushltime)
845                                 mypusher = player.pusher;
846                 while((key = player.kh_next))
847                 {
848                         kh_Scores_Event(player, key, "losekey", 0, 0);
849                         PlayerScore_Add(player, SP_KH_LOSSES, 1);
850                         Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(key, INFO_KEYHUNT_LOST_), player.netname);
851                         kh_Key_AssignTo(key, world);
852                         makevectors('-1 0 0' * (45 + 45 * random()) + '0 360 0' * random());
853                         key.velocity = W_CalculateProjectileVelocity(player.velocity, autocvar_g_balance_keyhunt_dropvelocity * v_forward, false);
854                         key.pusher = mypusher;
855                         key.pushltime = time + autocvar_g_balance_keyhunt_protecttime;
856                         if(suicide)
857                                 key.kh_dropperteam = player.team;
858                 }
859                 sound(player, CH_TRIGGER, SND_KH_DROP, VOL_BASE, ATTEN_NORM);
860         }
861 }
862
863 float kh_CheckPlayers(float num)
864 {
865         if(num < kh_teams)
866         {
867                 float t_team = kh_Team_ByID(num);
868                 float players = 0;
869                 entity tmp_player;
870                 FOR_EACH_PLAYER(tmp_player)
871                         if(tmp_player.deadflag == DEAD_NO)
872                                 if(!tmp_player.BUTTON_CHAT)
873                                         if(tmp_player.team == t_team)
874                                                 ++players;
875
876                 if (!players) { return t_team; }
877         }
878         return 0;
879 }
880
881 #define KH_READY_TEAMS() (!p1 + !p2 + ((kh_teams >= 3) ? !p3 : p3) + ((kh_teams >= 4) ? !p4 : p4))
882 #define KH_READY_TEAMS_OK() (KH_READY_TEAMS() == kh_teams)
883 void kh_WaitForPlayers()  // delay start of the round until enough players are present
884 {
885         if(time < game_starttime)
886         {
887                 kh_Controller_SetThink(game_starttime - time + 0.1, kh_WaitForPlayers);
888                 return;
889         }
890
891         static float prev_missing_teams_mask;
892         float p1 = kh_CheckPlayers(0), p2 = kh_CheckPlayers(1), p3 = kh_CheckPlayers(2), p4 = kh_CheckPlayers(3);
893         if(KH_READY_TEAMS_OK())
894         {
895                 if(prev_missing_teams_mask > 0)
896                         Kill_Notification(NOTIF_ALL, world, MSG_CENTER_CPID, CPID_MISSING_TEAMS);
897                 prev_missing_teams_mask = -1;
898                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_KEYHUNT_ROUNDSTART, autocvar_g_balance_keyhunt_delay_round);
899                 kh_Controller_SetThink(autocvar_g_balance_keyhunt_delay_round, kh_StartRound);
900         }
901         else
902         {
903                 if(player_count == 0)
904                 {
905                         if(prev_missing_teams_mask > 0)
906                                 Kill_Notification(NOTIF_ALL, world, MSG_CENTER_CPID, CPID_MISSING_TEAMS);
907                         prev_missing_teams_mask = -1;
908                 }
909                 else
910                 {
911                         float missing_teams_mask = (!!p1) + (!!p2) * 2;
912                         if(kh_teams >= 3) missing_teams_mask += (!!p3) * 4;
913                         if(kh_teams >= 4) missing_teams_mask += (!!p4) * 8;
914                         if(prev_missing_teams_mask != missing_teams_mask)
915                         {
916                                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_MISSING_TEAMS, missing_teams_mask);
917                                 prev_missing_teams_mask = missing_teams_mask;
918                         }
919                 }
920                 kh_Controller_SetThink(1, kh_WaitForPlayers);
921         }
922 }
923
924 void kh_EnableTrackingDevice()  // runs after each round
925 {
926         Kill_Notification(NOTIF_ALL, world, MSG_CENTER_CPID, CPID_KEYHUNT);
927         Kill_Notification(NOTIF_ALL, world, MSG_CENTER_CPID, CPID_KEYHUNT_OTHER);
928
929         kh_tracking_enabled = true;
930 }
931
932 void kh_StartRound()  // runs at the start of each round
933 {
934         float i, players, teem;
935         entity player;
936
937         if(time < game_starttime)
938         {
939                 kh_Controller_SetThink(game_starttime - time + 0.1, kh_WaitForPlayers);
940                 return;
941         }
942
943         float p1 = kh_CheckPlayers(0), p2 = kh_CheckPlayers(1), p3 = kh_CheckPlayers(2), p4 = kh_CheckPlayers(3);
944         if(!KH_READY_TEAMS_OK())
945         {
946                 kh_Controller_SetThink(1, kh_WaitForPlayers);
947                 return;
948         }
949
950         Kill_Notification(NOTIF_ALL, world, MSG_CENTER_CPID, CPID_KEYHUNT);
951         Kill_Notification(NOTIF_ALL, world, MSG_CENTER_CPID, CPID_KEYHUNT_OTHER);
952
953         for(i = 0; i < kh_teams; ++i)
954         {
955                 teem = kh_Team_ByID(i);
956                 players = 0;
957                 entity my_player = world;
958                 FOR_EACH_PLAYER(player)
959                         if(player.deadflag == DEAD_NO)
960                                 if(!player.BUTTON_CHAT)
961                                         if(player.team == teem)
962                                         {
963                                                 ++players;
964                                                 if(random() * players <= 1)
965                                                         my_player = player;
966                                         }
967                 kh_Key_Spawn(my_player, 360 * i / kh_teams, i);
968         }
969
970         kh_tracking_enabled = false;
971         Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_KEYHUNT_SCAN, autocvar_g_balance_keyhunt_delay_tracking);
972         kh_Controller_SetThink(autocvar_g_balance_keyhunt_delay_tracking, kh_EnableTrackingDevice);
973 }
974
975 float kh_HandleFrags(entity attacker, entity targ, float f)  // adds to the player score
976 {
977         if(attacker == targ)
978                 return f;
979
980         if(targ.kh_next)
981         {
982                 if(attacker.team == targ.team)
983                 {
984                         entity k;
985                         float nk;
986                         nk = 0;
987                         for(k = targ.kh_next; k != world; k = k.kh_next)
988                                 ++nk;
989                         kh_Scores_Event(attacker, targ.kh_next, "carrierfrag", -nk * autocvar_g_balance_keyhunt_score_collect, 0);
990                 }
991                 else
992                 {
993                         kh_Scores_Event(attacker, targ.kh_next, "carrierfrag", autocvar_g_balance_keyhunt_score_carrierfrag-1, 0);
994                         PlayerScore_Add(attacker, SP_KH_KCKILLS, 1);
995                         // the frag gets added later
996                 }
997         }
998
999         return f;
1000 }
1001
1002 void kh_Initialize()  // sets up th KH environment
1003 {
1004         // setup variables
1005         kh_teams = autocvar_g_keyhunt_teams_override;
1006         if(kh_teams < 2)
1007                 kh_teams = autocvar_g_keyhunt_teams;
1008         kh_teams = bound(2, kh_teams, 4);
1009
1010         // make a KH entity for controlling the game
1011         kh_controller = spawn();
1012         kh_controller.think = kh_Controller_Think;
1013         kh_Controller_SetThink(0, kh_WaitForPlayers);
1014
1015         setmodel(kh_controller, MDL_KH_KEY);
1016         kh_key_dropped = kh_controller.modelindex;
1017         /*
1018         dprint(vtos(kh_controller.mins));
1019         dprint(vtos(kh_controller.maxs));
1020         dprint("\n");
1021         */
1022 #ifdef KH_PLAYER_USE_CARRIEDMODEL
1023         setmodel(kh_controller, MDL_KH_KEY_CARRIED);
1024         kh_key_carried = kh_controller.modelindex;
1025 #else
1026         kh_key_carried = kh_key_dropped;
1027 #endif
1028
1029         kh_controller.model = "";
1030         kh_controller.modelindex = 0;
1031
1032         addstat(STAT_KH_KEYS, AS_INT, kh_state);
1033
1034         kh_ScoreRules(kh_teams);
1035 }
1036
1037 void kh_finalize()
1038 {
1039         // to be called before intermission
1040         kh_FinishRound();
1041         remove(kh_controller);
1042         kh_controller = world;
1043 }
1044
1045 // legacy bot role
1046
1047 void() havocbot_role_kh_carrier;
1048 void() havocbot_role_kh_defense;
1049 void() havocbot_role_kh_offense;
1050 void() havocbot_role_kh_freelancer;
1051
1052
1053 void havocbot_goalrating_kh(float ratingscale_team, float ratingscale_dropped, float ratingscale_enemy)
1054 {SELFPARAM();
1055         entity head;
1056         for (head = kh_worldkeylist; head; head = head.kh_worldkeynext)
1057         {
1058                 if(head.owner == self)
1059                         continue;
1060                 if(!kh_tracking_enabled)
1061                 {
1062                         // if it's carried by our team we know about it
1063                         // otherwise we have to see it to know about it
1064                         if(!head.owner || head.team != self.team)
1065                         {
1066                                 traceline(self.origin + self.view_ofs, head.origin, MOVE_NOMONSTERS, self);
1067                                 if (trace_fraction < 1 && trace_ent != head)
1068                                         continue; // skip what I can't see
1069                         }
1070                 }
1071                 if(!head.owner)
1072                         navigation_routerating(head, ratingscale_dropped * BOT_PICKUP_RATING_HIGH, 100000);
1073                 else if(head.team == self.team)
1074                         navigation_routerating(head.owner, ratingscale_team * BOT_PICKUP_RATING_HIGH, 100000);
1075                 else
1076                         navigation_routerating(head.owner, ratingscale_enemy * BOT_PICKUP_RATING_HIGH, 100000);
1077         }
1078
1079         havocbot_goalrating_items(1, self.origin, 10000);
1080 }
1081
1082 void havocbot_role_kh_carrier()
1083 {SELFPARAM();
1084         if(self.deadflag != DEAD_NO)
1085                 return;
1086
1087         if (!(self.kh_next))
1088         {
1089                 LOG_TRACE("changing role to freelancer\n");
1090                 self.havocbot_role = havocbot_role_kh_freelancer;
1091                 self.havocbot_role_timeout = 0;
1092                 return;
1093         }
1094
1095         if (self.bot_strategytime < time)
1096         {
1097                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1098                 navigation_goalrating_start();
1099
1100                 if(kh_Key_AllOwnedByWhichTeam() == self.team)
1101                         havocbot_goalrating_kh(10, 0.1, 0.1); // bring home
1102                 else
1103                         havocbot_goalrating_kh(4, 4, 1); // play defensively
1104
1105                 navigation_goalrating_end();
1106         }
1107 }
1108
1109 void havocbot_role_kh_defense()
1110 {SELFPARAM();
1111         if(self.deadflag != DEAD_NO)
1112                 return;
1113
1114         if (self.kh_next)
1115         {
1116                 LOG_TRACE("changing role to carrier\n");
1117                 self.havocbot_role = havocbot_role_kh_carrier;
1118                 self.havocbot_role_timeout = 0;
1119                 return;
1120         }
1121
1122         if (!self.havocbot_role_timeout)
1123                 self.havocbot_role_timeout = time + random() * 10 + 20;
1124         if (time > self.havocbot_role_timeout)
1125         {
1126                 LOG_TRACE("changing role to freelancer\n");
1127                 self.havocbot_role = havocbot_role_kh_freelancer;
1128                 self.havocbot_role_timeout = 0;
1129                 return;
1130         }
1131
1132         if (self.bot_strategytime < time)
1133         {
1134                 float key_owner_team;
1135                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1136                 navigation_goalrating_start();
1137
1138                 key_owner_team = kh_Key_AllOwnedByWhichTeam();
1139                 if(key_owner_team == self.team)
1140                         havocbot_goalrating_kh(10, 0.1, 0.1); // defend key carriers
1141                 else if(key_owner_team == -1)
1142                         havocbot_goalrating_kh(4, 1, 0.1); // play defensively
1143                 else
1144                         havocbot_goalrating_kh(0.1, 0.1, 10); // ATTACK ANYWAY
1145
1146                 navigation_goalrating_end();
1147         }
1148 }
1149
1150 void havocbot_role_kh_offense()
1151 {SELFPARAM();
1152         if(self.deadflag != DEAD_NO)
1153                 return;
1154
1155         if (self.kh_next)
1156         {
1157                 LOG_TRACE("changing role to carrier\n");
1158                 self.havocbot_role = havocbot_role_kh_carrier;
1159                 self.havocbot_role_timeout = 0;
1160                 return;
1161         }
1162
1163         if (!self.havocbot_role_timeout)
1164                 self.havocbot_role_timeout = time + random() * 10 + 20;
1165         if (time > self.havocbot_role_timeout)
1166         {
1167                 LOG_TRACE("changing role to freelancer\n");
1168                 self.havocbot_role = havocbot_role_kh_freelancer;
1169                 self.havocbot_role_timeout = 0;
1170                 return;
1171         }
1172
1173         if (self.bot_strategytime < time)
1174         {
1175                 float key_owner_team;
1176
1177                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1178                 navigation_goalrating_start();
1179
1180                 key_owner_team = kh_Key_AllOwnedByWhichTeam();
1181                 if(key_owner_team == self.team)
1182                         havocbot_goalrating_kh(10, 0.1, 0.1); // defend anyway
1183                 else if(key_owner_team == -1)
1184                         havocbot_goalrating_kh(0.1, 1, 4); // play offensively
1185                 else
1186                         havocbot_goalrating_kh(0.1, 0.1, 10); // ATTACK! EMERGENCY!
1187
1188                 navigation_goalrating_end();
1189         }
1190 }
1191
1192 void havocbot_role_kh_freelancer()
1193 {SELFPARAM();
1194         if(self.deadflag != DEAD_NO)
1195                 return;
1196
1197         if (self.kh_next)
1198         {
1199                 LOG_TRACE("changing role to carrier\n");
1200                 self.havocbot_role = havocbot_role_kh_carrier;
1201                 self.havocbot_role_timeout = 0;
1202                 return;
1203         }
1204
1205         if (!self.havocbot_role_timeout)
1206                 self.havocbot_role_timeout = time + random() * 10 + 10;
1207         if (time > self.havocbot_role_timeout)
1208         {
1209                 if (random() < 0.5)
1210                 {
1211                         LOG_TRACE("changing role to offense\n");
1212                         self.havocbot_role = havocbot_role_kh_offense;
1213                 }
1214                 else
1215                 {
1216                         LOG_TRACE("changing role to defense\n");
1217                         self.havocbot_role = havocbot_role_kh_defense;
1218                 }
1219                 self.havocbot_role_timeout = 0;
1220                 return;
1221         }
1222
1223         if (self.bot_strategytime < time)
1224         {
1225                 float key_owner_team;
1226
1227                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1228                 navigation_goalrating_start();
1229
1230                 key_owner_team = kh_Key_AllOwnedByWhichTeam();
1231                 if(key_owner_team == self.team)
1232                         havocbot_goalrating_kh(10, 0.1, 0.1); // defend anyway
1233                 else if(key_owner_team == -1)
1234                         havocbot_goalrating_kh(1, 10, 4); // prefer dropped keys
1235                 else
1236                         havocbot_goalrating_kh(0.1, 0.1, 10); // ATTACK ANYWAY
1237
1238                 navigation_goalrating_end();
1239         }
1240 }
1241
1242
1243 // register this as a mutator
1244
1245 MUTATOR_HOOKFUNCTION(kh, ClientDisconnect)
1246 {SELFPARAM();
1247         kh_Key_DropAll(self, true);
1248         return 0;
1249 }
1250
1251 MUTATOR_HOOKFUNCTION(kh, MakePlayerObserver)
1252 {SELFPARAM();
1253         kh_Key_DropAll(self, true);
1254         return 0;
1255 }
1256
1257 MUTATOR_HOOKFUNCTION(kh, PlayerDies)
1258 {SELFPARAM();
1259         if(self == other)
1260                 kh_Key_DropAll(self, true);
1261         else if(IS_PLAYER(other))
1262                 kh_Key_DropAll(self, false);
1263         else
1264                 kh_Key_DropAll(self, true);
1265         return 0;
1266 }
1267
1268 MUTATOR_HOOKFUNCTION(kh, GiveFragsForKill, CBC_ORDER_FIRST)
1269 {
1270         frag_score = kh_HandleFrags(frag_attacker, frag_target, frag_score);
1271         return 0;
1272 }
1273
1274 MUTATOR_HOOKFUNCTION(kh, MatchEnd)
1275 {
1276         kh_finalize();
1277         return 0;
1278 }
1279
1280 MUTATOR_HOOKFUNCTION(kh, GetTeamCount, CBC_ORDER_EXCLUSIVE)
1281 {
1282         ret_float = kh_teams;
1283         return false;
1284 }
1285
1286 MUTATOR_HOOKFUNCTION(kh, SpectateCopy)
1287 {SELFPARAM();
1288         self.kh_state = other.kh_state;
1289         return 0;
1290 }
1291
1292 MUTATOR_HOOKFUNCTION(kh, PlayerUseKey)
1293 {SELFPARAM();
1294         if(MUTATOR_RETURNVALUE == 0)
1295         {
1296                 entity k;
1297                 k = self.kh_next;
1298                 if(k)
1299                 {
1300                         kh_Key_DropOne(k);
1301                         return 1;
1302                 }
1303         }
1304         return 0;
1305 }
1306
1307 MUTATOR_HOOKFUNCTION(kh, HavocBot_ChooseRole)
1308 {
1309         if(self.deadflag != DEAD_NO)
1310                 return true;
1311
1312         float r = random() * 3;
1313         if (r < 1)
1314                 self.havocbot_role = havocbot_role_kh_offense;
1315         else if (r < 2)
1316                 self.havocbot_role = havocbot_role_kh_defense;
1317         else
1318                 self.havocbot_role = havocbot_role_kh_freelancer;
1319
1320         return true;
1321 }
1322
1323 REGISTER_MUTATOR(kh, g_keyhunt)
1324 {
1325         ActivateTeamplay();
1326         SetLimits(autocvar_g_keyhunt_point_limit, autocvar_g_keyhunt_point_leadlimit, -1, -1);
1327         if(autocvar_g_keyhunt_team_spawns)
1328                 have_team_spawns = -1; // request team spawns
1329
1330         MUTATOR_ONADD
1331         {
1332                 if(time > 1) // game loads at time 1
1333                         error("This is a game type and it cannot be added at runtime.");
1334                 kh_Initialize();
1335         }
1336
1337         MUTATOR_ONROLLBACK_OR_REMOVE
1338         {
1339                 // we actually cannot roll back kh_Initialize here
1340                 // BUT: we don't need to! If this gets called, adding always
1341                 // succeeds.
1342         }
1343
1344         MUTATOR_ONREMOVE
1345         {
1346                 LOG_INFO("This is a game type and it cannot be removed at runtime.");
1347                 return -1;
1348         }
1349
1350         return 0;
1351 }