]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_keyhunt.qc
Merge branch 'master' into TimePath/deathtypes
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / gamemode_keyhunt.qc
1 #include "gamemode_keyhunt.qh"
2
3 #include "gamemode.qh"
4
5
6 // #define KH_PLAYER_USE_ATTACHMENT
7 // #define KH_PLAYER_USE_CARRIEDMODEL
8
9 #ifdef KH_PLAYER_USE_ATTACHMENT
10 const vector KH_PLAYER_ATTACHMENT_DIST_ROTATED = '0 -4 0';
11 const vector KH_PLAYER_ATTACHMENT_DIST = '4 0 0';
12 const vector KH_PLAYER_ATTACHMENT = '0 0 0';
13 const vector KH_PLAYER_ATTACHMENT_ANGLES = '0 0 0';
14 const string KH_PLAYER_ATTACHMENT_BONE = "";
15 #else
16 const float KH_KEY_ZSHIFT = 22;
17 const float KH_KEY_XYDIST = 24;
18 const float KH_KEY_XYSPEED = 45;
19 #endif
20 const float KH_KEY_WP_ZSHIFT = 20;
21
22 const vector KH_KEY_MIN = '-10 -10 -46';
23 const vector KH_KEY_MAX = '10 10 3';
24 const float KH_KEY_BRIGHTNESS = 2;
25
26 float kh_no_radar_circles;
27
28 // kh_state
29 //     bits  0- 4: team of key 1, or 0 for no such key, or 30 for dropped, or 31 for self
30 //     bits  5- 9: team of key 2, or 0 for no such key, or 30 for dropped, or 31 for self
31 //     bits 10-14: team of key 3, or 0 for no such key, or 30 for dropped, or 31 for self
32 //     bits 15-19: team of key 4, or 0 for no such key, or 30 for dropped, or 31 for self
33 .float kh_state;
34 .float siren_time;  //  time delay the siren
35 //.float stuff_time;  //  time delay to stuffcmd a cvar
36
37 float kh_keystatus[17];
38 //kh_keystatus[0] = status of dropped keys, kh_keystatus[1 - 16] = player #
39 //replace 17 with cvar("maxplayers") or similar !!!!!!!!!
40 //for(i = 0; i < maxplayers; ++i)
41 //      kh_keystatus[i] = "0";
42
43 float kh_Team_ByID(float t)
44 {
45         if(t == 0) return NUM_TEAM_1;
46         if(t == 1) return NUM_TEAM_2;
47         if(t == 2) return NUM_TEAM_3;
48         if(t == 3) return NUM_TEAM_4;
49         return 0;
50 }
51
52 //entity kh_worldkeylist;
53 .entity kh_worldkeynext;
54 entity kh_controller;
55 //float kh_tracking_enabled;
56 float kh_teams;
57 float kh_interferemsg_time, kh_interferemsg_team;
58 .entity kh_next, kh_prev; // linked list
59 .float kh_droptime;
60 .float kh_dropperteam;
61 .entity kh_previous_owner;
62 .float kh_previous_owner_playerid;
63 .float kh_cp_duration;
64
65 float kh_key_dropped, kh_key_carried;
66
67 const float ST_KH_CAPS = 1;
68 const float SP_KH_CAPS = 4;
69 const float SP_KH_PUSHES = 5;
70 const float SP_KH_DESTROYS = 6;
71 const float SP_KH_PICKUPS = 7;
72 const float SP_KH_KCKILLS = 8;
73 const float SP_KH_LOSSES = 9;
74 void kh_ScoreRules(float teams)
75 {
76         ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, SFL_SORT_PRIO_PRIMARY, true);
77         ScoreInfo_SetLabel_TeamScore(  ST_KH_CAPS,      "caps",      SFL_SORT_PRIO_SECONDARY);
78         ScoreInfo_SetLabel_PlayerScore(SP_KH_CAPS,      "caps",      SFL_SORT_PRIO_SECONDARY);
79         ScoreInfo_SetLabel_PlayerScore(SP_KH_PUSHES,    "pushes",    0);
80         ScoreInfo_SetLabel_PlayerScore(SP_KH_DESTROYS,  "destroyed", SFL_LOWER_IS_BETTER);
81         ScoreInfo_SetLabel_PlayerScore(SP_KH_PICKUPS,   "pickups",   0);
82         ScoreInfo_SetLabel_PlayerScore(SP_KH_KCKILLS,   "kckills",   0);
83         ScoreInfo_SetLabel_PlayerScore(SP_KH_LOSSES,    "losses",    SFL_LOWER_IS_BETTER);
84         ScoreRules_basics_end();
85 }
86
87 float kh_KeyCarrier_waypointsprite_visible_for_player(entity e)  // runs all the time
88 {SELFPARAM();
89         if(!IS_PLAYER(e) || self.team != e.team)
90                 if(!kh_tracking_enabled)
91                         return false;
92
93         return true;
94 }
95
96 float kh_Key_waypointsprite_visible_for_player(entity e) // ??
97 {SELFPARAM();
98         if(!kh_tracking_enabled)
99                 return false;
100         if(!self.owner)
101                 return true;
102         if(!self.owner.owner)
103                 return true;
104         return false;  // draw only when key is not owned
105 }
106
107 void kh_update_state()
108 {
109         entity player;
110         entity key;
111         float s;
112         float f;
113
114         s = 0;
115         FOR_EACH_KH_KEY(key)
116         {
117                 if(key.owner)
118                         f = key.team;
119                 else
120                         f = 30;
121                 s |= pow(32, key.count) * f;
122         }
123
124         FOR_EACH_CLIENT(player)
125         {
126                 player.kh_state = s;
127         }
128
129         FOR_EACH_KH_KEY(key)
130         {
131                 if(key.owner)
132                         key.owner.kh_state |= pow(32, key.count) * 31;
133         }
134         //print(ftos((nextent(world)).kh_state), "\n");
135 }
136
137
138
139
140 var kh_Think_t kh_Controller_Thinkfunc;
141 void kh_Controller_SetThink(float t, kh_Think_t func)  // runs occasionaly
142 {
143         kh_Controller_Thinkfunc = func;
144         kh_controller.cnt = ceil(t);
145         if(t == 0)
146                 kh_controller.nextthink = time; // force
147 }
148 void kh_WaitForPlayers();
149 void kh_Controller_Think()  // called a lot
150 {SELFPARAM();
151         if(intermission_running)
152                 return;
153         if(self.cnt > 0)
154         { if(self.think != kh_WaitForPlayers) { self.cnt -= 1; } }
155         else if(self.cnt == 0)
156         {
157                 self.cnt -= 1;
158                 kh_Controller_Thinkfunc();
159         }
160         self.nextthink = time + 1;
161 }
162
163 // frags f: take from cvar * f
164 // frags 0: no frags
165 void kh_Scores_Event(entity player, entity key, string what, float frags_player, float frags_owner)  // update the score when a key is captured
166 {
167         string s;
168         if(intermission_running)
169                 return;
170
171         if(frags_player)
172                 UpdateFrags(player, frags_player);
173
174         if(key && key.owner && frags_owner)
175                 UpdateFrags(key.owner, frags_owner);
176
177         if(!autocvar_sv_eventlog)  //output extra info to the console or text file
178                 return;
179
180         s = strcat(":keyhunt:", what, ":", ftos(player.playerid), ":", ftos(frags_player));
181
182         if(key && key.owner)
183                 s = strcat(s, ":", ftos(key.owner.playerid));
184         else
185                 s = strcat(s, ":0");
186
187         s = strcat(s, ":", ftos(frags_owner), ":");
188
189         if(key)
190                 s = strcat(s, key.netname);
191
192         GameLogEcho(s);
193 }
194
195 vector kh_AttachedOrigin(entity e)  // runs when a team captures the flag, it can run 2 or 3 times.
196 {
197         if(e.tag_entity)
198         {
199                 makevectors(e.tag_entity.angles);
200                 return e.tag_entity.origin + e.origin.x * v_forward - e.origin.y * v_right + e.origin.z * v_up;
201         }
202         else
203                 return e.origin;
204 }
205
206 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
207 {
208 #ifdef KH_PLAYER_USE_ATTACHMENT
209         entity first;
210         first = key.owner.kh_next;
211         if(key == first)
212         {
213                 setattachment(key, key.owner, KH_PLAYER_ATTACHMENT_BONE);
214                 if(key.kh_next)
215                 {
216                         setattachment(key.kh_next, key, "");
217                         setorigin(key, key.kh_next.origin - 0.5 * KH_PLAYER_ATTACHMENT_DIST);
218                         setorigin(key.kh_next, KH_PLAYER_ATTACHMENT_DIST_ROTATED);
219                         key.kh_next.angles = '0 0 0';
220                 }
221                 else
222                         setorigin(key, KH_PLAYER_ATTACHMENT);
223                 key.angles = KH_PLAYER_ATTACHMENT_ANGLES;
224         }
225         else
226         {
227                 setattachment(key, key.kh_prev, "");
228                 if(key.kh_next)
229                         setattachment(key.kh_next, key, "");
230                 setorigin(key, KH_PLAYER_ATTACHMENT_DIST_ROTATED);
231                 setorigin(first, first.origin - 0.5 * KH_PLAYER_ATTACHMENT_DIST);
232                 key.angles = '0 0 0';
233         }
234 #else
235         setattachment(key, key.owner, "");
236         setorigin(key, '0 0 1' * KH_KEY_ZSHIFT);  // fixing x, y in think
237         key.angles_y -= key.owner.angles.y;
238 #endif
239         key.flags = 0;
240         key.solid = SOLID_NOT;
241         key.movetype = MOVETYPE_NONE;
242         key.team = key.owner.team;
243         key.nextthink = time;
244         key.damageforcescale = 0;
245         key.takedamage = DAMAGE_NO;
246         key.modelindex = kh_key_carried;
247 }
248
249 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
250 {
251 #ifdef KH_PLAYER_USE_ATTACHMENT
252         entity first;
253         first = key.owner.kh_next;
254         if(key == first)
255         {
256                 if(key.kh_next)
257                 {
258                         setattachment(key.kh_next, key.owner, KH_PLAYER_ATTACHMENT_BONE);
259                         setorigin(key.kh_next, key.origin + 0.5 * KH_PLAYER_ATTACHMENT_DIST);
260                         key.kh_next.angles = KH_PLAYER_ATTACHMENT_ANGLES;
261                 }
262         }
263         else
264         {
265                 if(key.kh_next)
266                         setattachment(key.kh_next, key.kh_prev, "");
267                 setorigin(first, first.origin + 0.5 * KH_PLAYER_ATTACHMENT_DIST);
268         }
269         // in any case:
270         setattachment(key, world, "");
271         setorigin(key, key.owner.origin + '0 0 1' * (PL_MIN.z - KH_KEY_MIN_z));
272         key.angles = key.owner.angles;
273 #else
274         setorigin(key, key.owner.origin + key.origin.z * '0 0 1');
275         setattachment(key, world, "");
276         key.angles_y += key.owner.angles.y;
277 #endif
278         key.flags = FL_ITEM;
279         key.solid = SOLID_TRIGGER;
280         key.movetype = MOVETYPE_TOSS;
281         key.pain_finished = time + autocvar_g_balance_keyhunt_delay_return;
282         key.damageforcescale = autocvar_g_balance_keyhunt_damageforcescale;
283         key.takedamage = DAMAGE_YES;
284         // let key.team stay
285         key.modelindex = kh_key_dropped;
286         key.kh_previous_owner = key.owner;
287         key.kh_previous_owner_playerid = key.owner.playerid;
288 }
289
290 void kh_Key_AssignTo(entity key, entity player)  // runs every time a key is picked up or assigned. Runs prior to kh_key_attach
291 {
292         entity k;
293         float ownerteam0, ownerteam;
294         if(key.owner == player)
295                 return;
296
297         ownerteam0 = kh_Key_AllOwnedByWhichTeam();
298
299         if(key.owner)
300         {
301                 kh_Key_Detach(key);
302
303                 // remove from linked list
304                 if(key.kh_next)
305                         key.kh_next.kh_prev = key.kh_prev;
306                 key.kh_prev.kh_next = key.kh_next;
307                 key.kh_next = world;
308                 key.kh_prev = world;
309
310                 if(key.owner.kh_next == world)
311                 {
312                         // No longer a key carrier
313                         if(!kh_no_radar_circles)
314                                 WaypointSprite_Ping(key.owner.waypointsprite_attachedforcarrier);
315                         WaypointSprite_DetachCarrier(key.owner);
316                 }
317         }
318
319         key.owner = player;
320
321         if(player)
322         {
323                 // insert into linked list
324                 key.kh_next = player.kh_next;
325                 key.kh_prev = player;
326                 player.kh_next = key;
327                 if(key.kh_next)
328                         key.kh_next.kh_prev = key;
329
330                 float i;
331                 i = kh_keystatus[key.owner.playerid];
332                         if(key.netname == "^1red key")
333                                 i += 1;
334                         if(key.netname == "^4blue key")
335                                 i += 2;
336                         if(key.netname == "^3yellow key")
337                                 i += 4;
338                         if(key.netname == "^6pink key")
339                                 i += 8;
340                 kh_keystatus[key.owner.playerid] = i;
341
342                 kh_Key_Attach(key);
343
344                 if(key.kh_next == world)
345                 {
346                         // player is now a key carrier
347                         entity wp = WaypointSprite_AttachCarrier(WP_Null, player, RADARICON_FLAGCARRIER);
348                         wp.colormod = colormapPaletteColor(player.team - 1, 0);
349                         player.waypointsprite_attachedforcarrier.waypointsprite_visible_for_player = kh_KeyCarrier_waypointsprite_visible_for_player;
350                         WaypointSprite_UpdateRule(player.waypointsprite_attachedforcarrier, player.team, SPRITERULE_TEAMPLAY);
351                         if(player.team == NUM_TEAM_1)
352                                 WaypointSprite_UpdateSprites(player.waypointsprite_attachedforcarrier, WP_KeyCarrierRed, WP_KeyCarrierFriend, WP_KeyCarrierRed);
353                         else if(player.team == NUM_TEAM_2)
354                                 WaypointSprite_UpdateSprites(player.waypointsprite_attachedforcarrier, WP_KeyCarrierBlue, WP_KeyCarrierFriend, WP_KeyCarrierBlue);
355                         else if(player.team == NUM_TEAM_3)
356                                 WaypointSprite_UpdateSprites(player.waypointsprite_attachedforcarrier, WP_KeyCarrierYellow, WP_KeyCarrierFriend, WP_KeyCarrierYellow);
357                         else if(player.team == NUM_TEAM_4)
358                                 WaypointSprite_UpdateSprites(player.waypointsprite_attachedforcarrier, WP_KeyCarrierPink, WP_KeyCarrierFriend, WP_KeyCarrierPink);
359                         if(!kh_no_radar_circles)
360                                 WaypointSprite_Ping(player.waypointsprite_attachedforcarrier);
361                 }
362         }
363
364         // moved that here, also update if there's no player
365         kh_update_state();
366
367         key.pusher = world;
368
369         ownerteam = kh_Key_AllOwnedByWhichTeam();
370         if(ownerteam != ownerteam0)
371         {
372                 if(ownerteam != -1)
373                 {
374                         kh_interferemsg_time = time + 0.2;
375                         kh_interferemsg_team = player.team;
376
377                         // audit all key carrier sprites, update them to RUN HERE
378                         FOR_EACH_KH_KEY(k)
379                         {
380                                 if (!k.owner) continue;
381                                 entity first = WP_Null;
382                                 FOREACH(Waypoints, it.netname == k.owner.waypointsprite_attachedforcarrier.model1, LAMBDA(first = it; break));
383                                 entity third = WP_Null;
384                                 FOREACH(Waypoints, it.netname == k.owner.waypointsprite_attachedforcarrier.model3, LAMBDA(third = it; break));
385                                 WaypointSprite_UpdateSprites(k.owner.waypointsprite_attachedforcarrier, first, WP_KeyCarrierFinish, third);
386                         }
387                 }
388                 else
389                 {
390                         kh_interferemsg_time = 0;
391
392                         // audit all key carrier sprites, update them to RUN HERE
393                         FOR_EACH_KH_KEY(k)
394                         {
395                                 if (!k.owner) continue;
396                                 entity first = WP_Null;
397                                 FOREACH(Waypoints, it.netname == k.owner.waypointsprite_attachedforcarrier.model1, LAMBDA(first = it; break));
398                                 entity third = WP_Null;
399                                 FOREACH(Waypoints, it.netname == k.owner.waypointsprite_attachedforcarrier.model3, LAMBDA(third = it; break));
400                                 WaypointSprite_UpdateSprites(k.owner.waypointsprite_attachedforcarrier, first, WP_KeyCarrierFriend, third);
401                         }
402                 }
403         }
404 }
405
406 void kh_Key_Damage(entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
407 {SELFPARAM();
408         if(self.owner)
409                 return;
410         if(ITEM_DAMAGE_NEEDKILL(deathtype))
411         {
412                 // touching lava, or hurt trigger
413                 // what shall we do?
414                 // immediately return is bad
415                 // maybe start a shorter countdown?
416         }
417         if(vlen(force) <= 0)
418                 return;
419         if(time > self.pushltime)
420                 if(IS_PLAYER(attacker))
421                         self.team = attacker.team;
422 }
423
424 void kh_Key_Collect(entity key, entity player)  //a player picks up a dropped key
425 {
426         sound(player, CH_TRIGGER, SND_KH_COLLECT, VOL_BASE, ATTEN_NORM);
427
428         if(key.kh_dropperteam != player.team)
429         {
430                 kh_Scores_Event(player, key, "collect", autocvar_g_balance_keyhunt_score_collect, 0);
431                 PlayerScore_Add(player, SP_KH_PICKUPS, 1);
432         }
433         key.kh_dropperteam = 0;
434         Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(key, INFO_KEYHUNT_PICKUP_), player.netname);
435
436         kh_Key_AssignTo(key, player); // this also updates .kh_state
437 }
438
439 void kh_Key_Touch()  // runs many, many times when a key has been dropped and can be picked up
440 {SELFPARAM();
441         if(intermission_running)
442                 return;
443
444         if(self.owner) // already carried
445                 return;
446
447         if(ITEM_TOUCH_NEEDKILL())
448         {
449                 // touching sky, or nodrop
450                 // what shall we do?
451                 // immediately return is bad
452                 // maybe start a shorter countdown?
453         }
454
455         if (!IS_PLAYER(other))
456                 return;
457         if(other.deadflag != DEAD_NO)
458                 return;
459         if(other == self.enemy)
460                 if(time < self.kh_droptime + autocvar_g_balance_keyhunt_delay_collect)
461                         return;  // you just dropped it!
462         kh_Key_Collect(self, other);
463 }
464
465 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
466 {
467         entity o;
468         o = key.owner;
469         kh_Key_AssignTo(key, world);
470         if(o) // it was attached
471                 WaypointSprite_Kill(key.waypointsprite_attachedforcarrier);
472         else // it was dropped
473                 WaypointSprite_DetachCarrier(key);
474
475         // remove key from key list
476         if (kh_worldkeylist == key)
477                 kh_worldkeylist = kh_worldkeylist.kh_worldkeynext;
478         else
479         {
480                 o = kh_worldkeylist;
481                 while (o)
482                 {
483                         if (o.kh_worldkeynext == key)
484                         {
485                                 o.kh_worldkeynext = o.kh_worldkeynext.kh_worldkeynext;
486                                 break;
487                         }
488                         o = o.kh_worldkeynext;
489                 }
490         }
491
492         remove(key);
493
494         kh_update_state();
495 }
496
497 void kh_FinishRound()  // runs when a team captures the keys
498 {
499         // prepare next round
500         kh_interferemsg_time = 0;
501         entity key;
502
503         kh_no_radar_circles = true;
504         FOR_EACH_KH_KEY(key)
505                 kh_Key_Remove(key);
506         kh_no_radar_circles = false;
507
508         Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_KEYHUNT_ROUNDSTART, autocvar_g_balance_keyhunt_delay_round);
509         kh_Controller_SetThink(autocvar_g_balance_keyhunt_delay_round, kh_StartRound);
510 }
511
512 void kh_WinnerTeam(float teem)  // runs when a team wins // Samual: Teem?.... TEEM?!?! what the fuck is wrong with you people
513 {
514         // all key carriers get some points
515         vector firstorigin, lastorigin, midpoint;
516         float first;
517         entity key;
518         float score;
519         score = (kh_teams - 1) * autocvar_g_balance_keyhunt_score_capture;
520         DistributeEvenly_Init(score, kh_teams);
521         // twice the score for 3 team games, three times the score for 4 team games!
522         // note: for a win by destroying the key, this should NOT be applied
523         FOR_EACH_KH_KEY(key)
524         {
525                 float f;
526                 f = DistributeEvenly_Get(1);
527                 kh_Scores_Event(key.owner, key, "capture", f, 0);
528                 PlayerTeamScore_Add(key.owner, SP_KH_CAPS, ST_KH_CAPS, 1);
529                 nades_GiveBonus(key.owner, autocvar_g_nades_bonus_score_high);
530         }
531
532         first = true;
533         string keyowner = "";
534         FOR_EACH_KH_KEY(key)
535                 if(key.owner.kh_next == key)
536                 {
537                         if(!first)
538                                 keyowner = strcat(keyowner, ", ");
539                         keyowner = key.owner.netname;
540                         first = false;
541                 }
542
543         Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(teem, INFO_KEYHUNT_CAPTURE_), keyowner);
544
545         first = true;
546         midpoint = '0 0 0';
547         firstorigin = '0 0 0';
548         lastorigin = '0 0 0';
549         FOR_EACH_KH_KEY(key)
550         {
551                 vector thisorigin;
552
553                 thisorigin = kh_AttachedOrigin(key);
554                 //dprint("Key origin: ", vtos(thisorigin), "\n");
555                 midpoint += thisorigin;
556
557                 if(!first)
558                         te_lightning2(world, lastorigin, thisorigin);
559                 lastorigin = thisorigin;
560                 if(first)
561                         firstorigin = thisorigin;
562                 first = false;
563         }
564         if(kh_teams > 2)
565         {
566                 te_lightning2(world, lastorigin, firstorigin);
567         }
568         midpoint = midpoint * (1 / kh_teams);
569         te_customflash(midpoint, 1000, 1, Team_ColorRGB(teem) * 0.5 + '0.5 0.5 0.5');  // make the color >=0.5 in each component
570
571         play2all(SND(KH_CAPTURE));
572         kh_FinishRound();
573 }
574
575 void kh_LoserTeam(float teem, entity lostkey)  // runs when a player pushes a flag carrier off the map
576 {
577         entity player, key, attacker;
578         float players;
579         float keys;
580         float f;
581
582         attacker = world;
583         if(lostkey.pusher)
584                 if(lostkey.pusher.team != teem)
585                         if(IS_PLAYER(lostkey.pusher))
586                                 attacker = lostkey.pusher;
587
588         players = keys = 0;
589
590         if(attacker)
591         {
592                 if(lostkey.kh_previous_owner)
593                         kh_Scores_Event(lostkey.kh_previous_owner, world, "pushed", 0, -autocvar_g_balance_keyhunt_score_push);
594                         // don't actually GIVE him the -nn points, just log
595                 kh_Scores_Event(attacker, world, "push", autocvar_g_balance_keyhunt_score_push, 0);
596                 PlayerScore_Add(attacker, SP_KH_PUSHES, 1);
597                 //centerprint(attacker, "Your push is the best!"); // does this really need to exist?
598         }
599         else
600         {
601                 float of, fragsleft, i, j, thisteam;
602                 of = autocvar_g_balance_keyhunt_score_destroyed_ownfactor;
603
604                 FOR_EACH_PLAYER(player)
605                         if(player.team != teem)
606                                 ++players;
607
608                 FOR_EACH_KH_KEY(key)
609                         if(key.owner && key.team != teem)
610                                 ++keys;
611
612                 if(lostkey.kh_previous_owner)
613                         kh_Scores_Event(lostkey.kh_previous_owner, world, "destroyed", 0, -autocvar_g_balance_keyhunt_score_destroyed);
614                         // don't actually GIVE him the -nn points, just log
615
616                 if(lostkey.kh_previous_owner.playerid == lostkey.kh_previous_owner_playerid)
617                         PlayerScore_Add(lostkey.kh_previous_owner, SP_KH_DESTROYS, 1);
618
619                 DistributeEvenly_Init(autocvar_g_balance_keyhunt_score_destroyed, keys * of + players);
620
621                 FOR_EACH_KH_KEY(key)
622                         if(key.owner && key.team != teem)
623                         {
624                                 f = DistributeEvenly_Get(of);
625                                 kh_Scores_Event(key.owner, world, "destroyed_holdingkey", f, 0);
626                         }
627
628                 fragsleft = DistributeEvenly_Get(players);
629
630                 // Now distribute these among all other teams...
631                 j = kh_teams - 1;
632                 for(i = 0; i < kh_teams; ++i)
633                 {
634                         thisteam = kh_Team_ByID(i);
635                         if(thisteam == teem) // bad boy, no cookie - this WILL happen
636                                 continue;
637
638                         players = 0;
639                         FOR_EACH_PLAYER(player)
640                                 if(player.team == thisteam)
641                                         ++players;
642
643                         DistributeEvenly_Init(fragsleft, j);
644                         fragsleft = DistributeEvenly_Get(j - 1);
645                         DistributeEvenly_Init(DistributeEvenly_Get(1), players);
646
647                         FOR_EACH_PLAYER(player)
648                                 if(player.team == thisteam)
649                                 {
650                                         f = DistributeEvenly_Get(1);
651                                         kh_Scores_Event(player, world, "destroyed", f, 0);
652                                 }
653
654                         --j;
655                 }
656         }
657
658         Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(lostkey, INFO_KEYHUNT_LOST_), lostkey.kh_previous_owner.netname);
659
660         play2all(SND(KH_DESTROY));
661         te_tarexplosion(lostkey.origin);
662
663         kh_FinishRound();
664 }
665
666 void kh_Key_Think()  // runs all the time
667 {SELFPARAM();
668         entity head;
669         //entity player;  // needed by FOR_EACH_PLAYER
670
671         if(intermission_running)
672                 return;
673
674         if(self.owner)
675         {
676 #ifndef KH_PLAYER_USE_ATTACHMENT
677                 makevectors('0 1 0' * (self.cnt + (time % 360) * KH_KEY_XYSPEED));
678                 setorigin(self, v_forward * KH_KEY_XYDIST + '0 0 1' * self.origin.z);
679 #endif
680         }
681
682         // if in nodrop or time over, end the round
683         if(!self.owner)
684                 if(time > self.pain_finished)
685                         kh_LoserTeam(self.team, self);
686
687         if(self.owner)
688         if(kh_Key_AllOwnedByWhichTeam() != -1)
689         {
690                 if(self.siren_time < time)
691                 {
692                         sound(self.owner, CH_TRIGGER, SND_KH_ALARM, VOL_BASE, ATTEN_NORM);  // play a simple alarm
693                         self.siren_time = time + 2.5;  // repeat every 2.5 seconds
694                 }
695
696                 entity key;
697                 vector p;
698                 p = self.owner.origin;
699                 FOR_EACH_KH_KEY(key)
700                         if(vlen(key.owner.origin - p) > autocvar_g_balance_keyhunt_maxdist)
701                                 goto not_winning;
702                 kh_WinnerTeam(self.team);
703 :not_winning
704         }
705
706         if(kh_interferemsg_time && time > kh_interferemsg_time)
707         {
708                 kh_interferemsg_time = 0;
709                 FOR_EACH_PLAYER(head)
710                 {
711                         if(head.team == kh_interferemsg_team)
712                                 if(head.kh_next)
713                                         Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_KEYHUNT_MEET);
714                                 else
715                                         Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_KEYHUNT_HELP);
716                         else
717                                 Send_Notification(NOTIF_ONE, head, MSG_CENTER, APP_TEAM_NUM_4(kh_interferemsg_team, CENTER_KEYHUNT_INTERFERE_));
718                 }
719         }
720
721         self.nextthink = time + 0.05;
722 }
723
724 void key_reset()
725 {SELFPARAM();
726         kh_Key_AssignTo(self, world);
727         kh_Key_Remove(self);
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;
734         key = spawn();
735         key.count = i;
736         key.classname = STR_ITEM_KH_KEY;
737         key.touch = kh_Key_Touch;
738         key.think = kh_Key_Think;
739         key.nextthink = time;
740         key.items = IT_KEY1 | IT_KEY2;
741         key.cnt = angle;
742         key.angles = '0 360 0' * random();
743         key.event_damage = kh_Key_Damage;
744         key.takedamage = DAMAGE_YES;
745         key.modelindex = kh_key_dropped;
746         key.model = "key";
747         key.kh_dropperteam = 0;
748         key.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP;
749         setsize(key, KH_KEY_MIN, KH_KEY_MAX);
750         key.colormod = Team_ColorRGB(initial_owner.team) * KH_KEY_BRIGHTNESS;
751         key.reset = key_reset;
752
753         switch(initial_owner.team)
754         {
755                 case NUM_TEAM_1:
756                         key.netname = "^1red key";
757                         break;
758                 case NUM_TEAM_2:
759                         key.netname = "^4blue key";
760                         break;
761                 case NUM_TEAM_3:
762                         key.netname = "^3yellow key";
763                         break;
764                 case NUM_TEAM_4:
765                         key.netname = "^6pink key";
766                         break;
767                 default:
768                         key.netname = "NETGIER key";
769                         break;
770         }
771
772         // link into key list
773         key.kh_worldkeynext = kh_worldkeylist;
774         kh_worldkeylist = key;
775
776         Send_Notification(NOTIF_ONE, initial_owner, MSG_CENTER, APP_TEAM_NUM_4(initial_owner.team, CENTER_KEYHUNT_START_));
777
778         WaypointSprite_Spawn(WP_KeyDropped, 0, 0, key, '0 0 1' * KH_KEY_WP_ZSHIFT, world, key.team, key, waypointsprite_attachedforcarrier, false, RADARICON_FLAG);
779         key.waypointsprite_attachedforcarrier.waypointsprite_visible_for_player = kh_Key_waypointsprite_visible_for_player;
780
781         kh_Key_AssignTo(key, initial_owner);
782 }
783
784 // -1 when no team completely owns all keys yet
785 float kh_Key_AllOwnedByWhichTeam()  // constantly called. check to see if all the keys are owned by the same team
786 {
787         entity key;
788         float teem;
789         float keys;
790
791         teem = -1;
792         keys = kh_teams;
793         FOR_EACH_KH_KEY(key)
794         {
795                 if(!key.owner)
796                         return -1;
797                 if(teem == -1)
798                         teem = key.team;
799                 else if(teem != key.team)
800                         return -1;
801                 --keys;
802         }
803         if(keys != 0)
804                 return -1;
805         return teem;
806 }
807
808 void kh_Key_DropOne(entity key)
809 {
810         // prevent collecting this one for some time
811         entity player;
812         player = key.owner;
813
814         key.kh_droptime = time;
815         key.enemy = player;
816
817         kh_Scores_Event(player, key, "dropkey", 0, 0);
818         PlayerScore_Add(player, SP_KH_LOSSES, 1);
819         Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(key, INFO_KEYHUNT_DROP_), player.netname);
820
821         kh_Key_AssignTo(key, world);
822         makevectors(player.v_angle);
823         key.velocity = W_CalculateProjectileVelocity(player.velocity, autocvar_g_balance_keyhunt_throwvelocity * v_forward, false);
824         key.pusher = world;
825         key.pushltime = time + autocvar_g_balance_keyhunt_protecttime;
826         key.kh_dropperteam = key.team;
827
828         sound(player, CH_TRIGGER, SND_KH_DROP, VOL_BASE, ATTEN_NORM);
829 }
830
831 void kh_Key_DropAll(entity player, float suicide) // runs whenever a player dies
832 {
833         entity key;
834         entity mypusher;
835         if(player.kh_next)
836         {
837                 mypusher = world;
838                 if(player.pusher)
839                         if(time < player.pushltime)
840                                 mypusher = player.pusher;
841                 while((key = player.kh_next))
842                 {
843                         kh_Scores_Event(player, key, "losekey", 0, 0);
844                         PlayerScore_Add(player, SP_KH_LOSSES, 1);
845                         Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(key, INFO_KEYHUNT_LOST_), player.netname);
846                         kh_Key_AssignTo(key, world);
847                         makevectors('-1 0 0' * (45 + 45 * random()) + '0 360 0' * random());
848                         key.velocity = W_CalculateProjectileVelocity(player.velocity, autocvar_g_balance_keyhunt_dropvelocity * v_forward, false);
849                         key.pusher = mypusher;
850                         key.pushltime = time + autocvar_g_balance_keyhunt_protecttime;
851                         if(suicide)
852                                 key.kh_dropperteam = player.team;
853                 }
854                 sound(player, CH_TRIGGER, SND_KH_DROP, VOL_BASE, ATTEN_NORM);
855         }
856 }
857
858 float kh_CheckPlayers(float num)
859 {
860         if(num < kh_teams)
861         {
862                 float t_team = kh_Team_ByID(num);
863                 float players = 0;
864                 entity tmp_player;
865                 FOR_EACH_PLAYER(tmp_player)
866                         if(tmp_player.deadflag == DEAD_NO)
867                                 if(!tmp_player.BUTTON_CHAT)
868                                         if(tmp_player.team == t_team)
869                                                 ++players;
870
871                 if (!players) { return t_team; }
872         }
873         return 0;
874 }
875
876 #define KH_READY_TEAMS() (!p1 + !p2 + ((kh_teams >= 3) ? !p3 : p3) + ((kh_teams >= 4) ? !p4 : p4))
877 #define KH_READY_TEAMS_OK() (KH_READY_TEAMS() == kh_teams)
878 void kh_WaitForPlayers()  // delay start of the round until enough players are present
879 {
880         if(time < game_starttime)
881         {
882                 kh_Controller_SetThink(game_starttime - time + 0.1, kh_WaitForPlayers);
883                 return;
884         }
885
886         static float prev_missing_teams_mask;
887         float p1 = kh_CheckPlayers(0), p2 = kh_CheckPlayers(1), p3 = kh_CheckPlayers(2), p4 = kh_CheckPlayers(3);
888         if(KH_READY_TEAMS_OK())
889         {
890                 if(prev_missing_teams_mask > 0)
891                         Kill_Notification(NOTIF_ALL, world, MSG_CENTER_CPID, CPID_MISSING_TEAMS);
892                 prev_missing_teams_mask = -1;
893                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_KEYHUNT_ROUNDSTART, autocvar_g_balance_keyhunt_delay_round);
894                 kh_Controller_SetThink(autocvar_g_balance_keyhunt_delay_round, kh_StartRound);
895         }
896         else
897         {
898                 if(player_count == 0)
899                 {
900                         if(prev_missing_teams_mask > 0)
901                                 Kill_Notification(NOTIF_ALL, world, MSG_CENTER_CPID, CPID_MISSING_TEAMS);
902                         prev_missing_teams_mask = -1;
903                 }
904                 else
905                 {
906                         float missing_teams_mask = (!!p1) + (!!p2) * 2;
907                         if(kh_teams >= 3) missing_teams_mask += (!!p3) * 4;
908                         if(kh_teams >= 4) missing_teams_mask += (!!p4) * 8;
909                         if(prev_missing_teams_mask != missing_teams_mask)
910                         {
911                                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_MISSING_TEAMS, missing_teams_mask);
912                                 prev_missing_teams_mask = missing_teams_mask;
913                         }
914                 }
915                 kh_Controller_SetThink(1, kh_WaitForPlayers);
916         }
917 }
918
919 void kh_EnableTrackingDevice()  // runs after each round
920 {
921         Kill_Notification(NOTIF_ALL, world, MSG_CENTER_CPID, CPID_KEYHUNT);
922         Kill_Notification(NOTIF_ALL, world, MSG_CENTER_CPID, CPID_KEYHUNT_OTHER);
923
924         kh_tracking_enabled = true;
925 }
926
927 void kh_StartRound()  // runs at the start of each round
928 {
929         float i, players, teem;
930         entity player;
931
932         if(time < game_starttime)
933         {
934                 kh_Controller_SetThink(game_starttime - time + 0.1, kh_WaitForPlayers);
935                 return;
936         }
937
938         float p1 = kh_CheckPlayers(0), p2 = kh_CheckPlayers(1), p3 = kh_CheckPlayers(2), p4 = kh_CheckPlayers(3);
939         if(!KH_READY_TEAMS_OK())
940         {
941                 kh_Controller_SetThink(1, kh_WaitForPlayers);
942                 return;
943         }
944
945         Kill_Notification(NOTIF_ALL, world, MSG_CENTER_CPID, CPID_KEYHUNT);
946         Kill_Notification(NOTIF_ALL, world, MSG_CENTER_CPID, CPID_KEYHUNT_OTHER);
947
948         for(i = 0; i < kh_teams; ++i)
949         {
950                 teem = kh_Team_ByID(i);
951                 players = 0;
952                 entity my_player = world;
953                 FOR_EACH_PLAYER(player)
954                         if(player.deadflag == DEAD_NO)
955                                 if(!player.BUTTON_CHAT)
956                                         if(player.team == teem)
957                                         {
958                                                 ++players;
959                                                 if(random() * players <= 1)
960                                                         my_player = player;
961                                         }
962                 kh_Key_Spawn(my_player, 360 * i / kh_teams, i);
963         }
964
965         kh_tracking_enabled = false;
966         Send_Notification(NOTIF_ALL, world, 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 != world; 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         // make a KH entity for controlling the game
1006         kh_controller = spawn();
1007         kh_controller.think = kh_Controller_Think;
1008         kh_Controller_SetThink(0, kh_WaitForPlayers);
1009
1010         setmodel(kh_controller, MDL_KH_KEY);
1011         kh_key_dropped = kh_controller.modelindex;
1012         /*
1013         dprint(vtos(kh_controller.mins));
1014         dprint(vtos(kh_controller.maxs));
1015         dprint("\n");
1016         */
1017 #ifdef KH_PLAYER_USE_CARRIEDMODEL
1018         setmodel(kh_controller, MDL_KH_KEY_CARRIED);
1019         kh_key_carried = kh_controller.modelindex;
1020 #else
1021         kh_key_carried = kh_key_dropped;
1022 #endif
1023
1024         kh_controller.model = "";
1025         kh_controller.modelindex = 0;
1026
1027         addstat(STAT_KH_KEYS, AS_INT, kh_state);
1028
1029         kh_ScoreRules(kh_teams);
1030 }
1031
1032 void kh_finalize()
1033 {
1034         // to be called before intermission
1035         kh_FinishRound();
1036         remove(kh_controller);
1037         kh_controller = world;
1038 }
1039
1040 // register this as a mutator
1041
1042 MUTATOR_HOOKFUNCTION(kh_Key_DropAll)
1043 {SELFPARAM();
1044         kh_Key_DropAll(self, true);
1045         return 0;
1046 }
1047
1048 MUTATOR_HOOKFUNCTION(kh_PlayerDies)
1049 {SELFPARAM();
1050         if(self == other)
1051                 kh_Key_DropAll(self, true);
1052         else if(IS_PLAYER(other))
1053                 kh_Key_DropAll(self, false);
1054         else
1055                 kh_Key_DropAll(self, true);
1056         return 0;
1057 }
1058
1059 MUTATOR_HOOKFUNCTION(kh_GiveFragsForKill)
1060 {
1061         frag_score = kh_HandleFrags(frag_attacker, frag_target, frag_score);
1062         return 0;
1063 }
1064
1065 MUTATOR_HOOKFUNCTION(kh_finalize)
1066 {
1067         kh_finalize();
1068         return 0;
1069 }
1070
1071 MUTATOR_HOOKFUNCTION(kh_GetTeamCount)
1072 {
1073         ret_float = kh_teams;
1074         return 0;
1075 }
1076
1077 MUTATOR_HOOKFUNCTION(kh_SpectateCopy)
1078 {SELFPARAM();
1079         self.kh_state = other.kh_state;
1080         return 0;
1081 }
1082
1083 MUTATOR_HOOKFUNCTION(kh_PlayerUseKey)
1084 {SELFPARAM();
1085         if(MUTATOR_RETURNVALUE == 0)
1086         {
1087                 entity k;
1088                 k = self.kh_next;
1089                 if(k)
1090                 {
1091                         kh_Key_DropOne(k);
1092                         return 1;
1093                 }
1094         }
1095         return 0;
1096 }
1097
1098 MUTATOR_DEFINITION(gamemode_keyhunt)
1099 {
1100         MUTATOR_HOOK(MakePlayerObserver, kh_Key_DropAll, CBC_ORDER_ANY);
1101         MUTATOR_HOOK(ClientDisconnect, kh_Key_DropAll, CBC_ORDER_ANY);
1102         MUTATOR_HOOK(PlayerDies, kh_PlayerDies, CBC_ORDER_ANY);
1103         MUTATOR_HOOK(GiveFragsForKill, kh_GiveFragsForKill, CBC_ORDER_FIRST);
1104         MUTATOR_HOOK(MatchEnd, kh_finalize, CBC_ORDER_ANY);
1105         MUTATOR_HOOK(GetTeamCount, kh_GetTeamCount, CBC_ORDER_EXCLUSIVE);
1106         MUTATOR_HOOK(SpectateCopy, kh_SpectateCopy, CBC_ORDER_ANY);
1107         MUTATOR_HOOK(PlayerUseKey, kh_PlayerUseKey, CBC_ORDER_ANY);
1108
1109         MUTATOR_ONADD
1110         {
1111                 if(time > 1) // game loads at time 1
1112                         error("This is a game type and it cannot be added at runtime.");
1113                 kh_Initialize();
1114         }
1115
1116         MUTATOR_ONROLLBACK_OR_REMOVE
1117         {
1118                 // we actually cannot roll back kh_Initialize here
1119                 // BUT: we don't need to! If this gets called, adding always
1120                 // succeeds.
1121         }
1122
1123         MUTATOR_ONREMOVE
1124         {
1125                 LOG_INFO("This is a game type and it cannot be removed at runtime.");
1126                 return -1;
1127         }
1128
1129         return 0;
1130 }