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