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