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