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