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