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