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