]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/keyhunt.qc
keyhunt: dlight effect for end of round (capture, lost key)
[divverent/nexuiz.git] / data / qcsrc / server / keyhunt.qc
1 string STR_ITEM_KH_KEY = "item_kh_key";
2 #define FOR_EACH_KH_KEY(v) for(v = world; (v = find(v, classname, STR_ITEM_KH_KEY)); )
3
4 // #define KH_PLAYER_USE_ATTACHMENT
5 // #define KH_PLAYER_USE_CARRIEDMODEL
6 // #define KH_KEY_ATTACHMENT_DEBUG
7
8 #ifdef KH_PLAYER_USE_ATTACHMENT
9 vector KH_PLAYER_ATTACHMENT_DIST_ROTATED = '0 -4 0';
10 vector KH_PLAYER_ATTACHMENT_DIST = '4 0 0';
11 vector KH_PLAYER_ATTACHMENT = '0 0 0';
12 vector KH_PLAYER_ATTACHMENT_ANGLES = '0 0 0';
13 string KH_PLAYER_ATTACHMENT_BONE = "";
14 #else
15 float KH_KEY_ZSHIFT = 22;
16 float KH_KEY_XYDIST = 24;
17 float KH_KEY_XYSPEED = 45;
18 #endif
19 float KH_KEY_WP_ZSHIFT = 20;
20
21 vector KH_KEY_MIN = '-10 -10 -46';
22 vector KH_KEY_MAX = '10 10 3';
23 float KH_KEY_BRIGHTNESS = 0.015625;
24
25 typedef void(void) kh_Think_t;
26 var kh_Think_t kh_Controller_Thinkfunc;
27 string kh_Controller_Waitmsg;
28
29 float kh_Team_ByID(float t)
30 {
31         if(t == 0) return COLOR_TEAM1;
32         if(t == 1) return COLOR_TEAM2;
33         if(t == 2) return COLOR_TEAM3;
34         if(t == 3) return COLOR_TEAM4;
35         return 0;
36 }
37
38 vector kh_average_teamcolor;
39 entity kh_controller;
40 float kh_tracking_enabled;
41 float kh_teams;
42 float kh_interferemsg_time, kh_interferemsg_team;
43 .entity kh_next, kh_prev; // linked list
44 .float kh_droptime;
45
46 string kh_sound_capture = "sound/ctf/capture.wav";
47 string kh_sound_destroy = "sound/ctf/return.wav";
48 string kh_sound_drop = "sound/misc/mouseclick.wav";
49 string kh_sound_collect = "sound/ctf/take.wav";
50
51 float kh_sprite_dropped, kh_sprite_finish, kh_sprite_red, kh_sprite_blue, kh_sprite_pink, kh_sprite_yellow, kh_sprite_friend;
52 float kh_key_dropped, kh_key_carried;
53
54 float kh_GetCarrierSprite(float t, float e)
55 {
56         if(t == e)           return kh_sprite_friend;
57         if(t == COLOR_TEAM1) return kh_sprite_red;
58         if(t == COLOR_TEAM2) return kh_sprite_blue;
59         if(t == COLOR_TEAM3) return kh_sprite_yellow;
60         if(t == COLOR_TEAM4) return kh_sprite_pink;
61         return 0;
62 }
63
64 void kh_Controller_SetThink(float t, string msg, kh_Think_t func)
65 {
66         kh_Controller_Thinkfunc = func;
67         kh_controller.cnt = t;
68         if(kh_Controller_Waitmsg != "")
69                 strunzone(kh_Controller_Waitmsg);
70         if(msg == "")
71                 kh_Controller_Waitmsg = "";
72         else
73                 kh_Controller_Waitmsg = strzone(msg);
74         if(t == 0)
75                 kh_controller.nextthink = time; // force
76 }
77
78 void kh_Controller_Think()
79 {
80         entity e;
81         if(intermission_running)
82                 return;
83         if(self.cnt > 0)
84         {
85                 if(kh_Controller_Waitmsg != "")
86                 {
87                         string s;
88                         if(substring(kh_Controller_Waitmsg, strlen(kh_Controller_Waitmsg)-1, 1) == " ")
89                                 s = strcat(kh_Controller_Waitmsg, ftos(self.cnt));
90                         else
91                                 s = kh_Controller_Waitmsg;
92
93                         //dprint(s, "\n");
94
95                         FOR_EACH_PLAYER(e)
96                                 if(clienttype(e) == CLIENTTYPE_REAL)
97                                         centerprint_atprio(e, CENTERPRIO_SPAM, s);
98                 }
99                 self.cnt -= 1;
100         }
101         else if(self.cnt == 0)
102         {
103                 self.cnt -= 1;
104                 kh_Controller_Thinkfunc();
105         }
106         self.nextthink = time + 1;
107 }
108
109 // frags f: take from cvar * f
110 // frags 0: no frags
111 void kh_Scores_Event(entity player, entity key, string what, float frags_player, float frags_owner)
112 {
113         string s;
114         if(intermission_running)
115                 return;
116         if(frags_player)
117                 player.frags = player.frags + floor(0.5 + frags_player);
118         if(frags_owner)
119                 key.owner.frags = key.owner.frags + floor(0.5 + frags_owner);
120         if(!cvar("sv_eventlog"))
121                 return;
122         s = strcat(":keyhunt:", what, ":", ftos(player.playerid));
123         s = strcat(s, ":", ftos(frags_player));
124         if(key && key.owner)
125                 s = strcat(s, ":", ftos(key.owner.playerid));
126         else
127                 s = strcat(s, ":0");
128         s = strcat(s, ":", ftos(frags_owner), ":");
129         if(key)
130                 s = strcat(s, key.netname);
131         GameLogEcho(s, FALSE);
132 }
133
134 vector kh_AttachedOrigin(entity e)
135 {
136         if(e.tag_entity)
137         {
138                 makevectors(e.tag_entity.angles);
139                 return e.tag_entity.origin + e.origin_x * v_forward - e.origin_y * v_right + e.origin_z * v_up;
140         }
141         else
142                 return e.origin;
143 }
144
145 void kh_Key_Attach(entity key)
146 {
147 #ifdef KH_PLAYER_USE_ATTACHMENT
148         entity first;
149         first = key.owner.kh_next;
150         if(key == first)
151         {
152                 setattachment(key, key.owner, KH_PLAYER_ATTACHMENT_BONE);
153                 if(key.kh_next)
154                 {
155                         setattachment(key.kh_next, key, "");
156                         setorigin(key, key.kh_next.origin - 0.5 * KH_PLAYER_ATTACHMENT_DIST);
157                         setorigin(key.kh_next, KH_PLAYER_ATTACHMENT_DIST_ROTATED);
158                         key.kh_next.angles = '0 0 0';
159                 }
160                 else
161                         setorigin(key, KH_PLAYER_ATTACHMENT);
162                 key.angles = KH_PLAYER_ATTACHMENT_ANGLES;
163         }
164         else
165         {
166                 setattachment(key, key.kh_prev, "");
167                 if(key.kh_next)
168                         setattachment(key.kh_next, key, "");
169                 setorigin(key, KH_PLAYER_ATTACHMENT_DIST_ROTATED);
170                 setorigin(first, first.origin - 0.5 * KH_PLAYER_ATTACHMENT_DIST);
171                 key.angles = '0 0 0';
172         }
173 #else
174         setattachment(key, key.owner, "");
175         setorigin(key, '0 0 1' * KH_KEY_ZSHIFT); // fixing x, y in think
176         key.angles_y -= key.owner.angles_y;
177 #endif
178         key.flags = 0;
179         key.solid = SOLID_NOT;
180         key.movetype = MOVETYPE_NONE;
181         key.team = key.owner.team;
182         key.nextthink = time;
183         key.damageforcescale = 0;
184         key.modelindex = kh_key_carried;
185 }
186
187 void kh_Key_Detach(entity key)
188 {
189 #ifdef KH_PLAYER_USE_ATTACHMENT
190         entity first;
191         first = key.owner.kh_next;
192         if(key == first)
193         {
194                 if(key.kh_next)
195                 {
196                         setattachment(key.kh_next, key.owner, KH_PLAYER_ATTACHMENT_BONE);
197                         setorigin(key.kh_next, key.origin + 0.5 * KH_PLAYER_ATTACHMENT_DIST);
198                         key.kh_next.angles = KH_PLAYER_ATTACHMENT_ANGLES;
199                 }
200         }
201         else
202         {
203                 if(key.kh_next)
204                         setattachment(key.kh_next, key.kh_prev, "");
205                 setorigin(first, first.origin + 0.5 * KH_PLAYER_ATTACHMENT_DIST);
206         }
207         // in any case:
208         setattachment(key, world, "");
209         setorigin(key, key.owner.origin + '0 0 1' * (PL_MIN_z - KH_KEY_MIN_z));
210         key.angles = key.owner.angles;
211 #else
212         setorigin(key, key.owner.origin + key.origin_z * '0 0 1');
213         setattachment(key, world, "");
214         key.angles_y += key.owner.angles_y;
215 #endif
216         key.aiment = world;
217         key.flags = FL_ITEM;
218         key.solid = SOLID_TRIGGER;
219         key.movetype = MOVETYPE_TOSS;
220         key.pain_finished = time + cvar("g_balance_keyhunt_delay_return");
221         key.damageforcescale = cvar("g_balance_keyhunt_damageforcescale");
222         // let key.team stay
223         key.modelindex = kh_key_dropped;
224 }
225
226 void kh_Key_AssignTo(entity key, entity player)
227 {
228         if(key.owner == player)
229                 return;
230
231         if(key.owner)
232         {
233                 kh_Key_Detach(key);
234
235                 // remove from linked list
236                 if(key.kh_next)
237                         key.kh_next.kh_prev = key.kh_prev;
238                 key.kh_prev.kh_next = key.kh_next;
239                 key.kh_next = world;
240                 key.kh_prev = world;
241
242                 if(key.owner.kh_next == world)
243                 {
244                         // No longer a key carrier
245                         WaypointSprite_DetachCarrier(key.owner);
246                 }
247         }
248
249         key.owner = player;
250
251         if(player)
252         {
253                 // insert into linked list
254                 key.kh_next = player.kh_next;
255                 key.kh_prev = player;
256                 player.kh_next = key;
257                 if(key.kh_next)
258                         key.kh_next.kh_prev = key;
259
260                 kh_Key_Attach(key);
261
262                 if(key.kh_next == world)
263                 {
264                         // player is now a key carrier
265                         WaypointSprite_AttachCarrier("", player);
266                         player.waypointsprite_attachedforcarrier.waypointsprite_for_player = kh_KeyCarrier_waypointsprite_for_player;
267                         player.waypointsprite_attachedforcarrier.team = player.team;
268                 }
269         }
270
271         key.pusher = world;
272 }
273
274 void kh_Key_Damage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
275 {
276         if(self.owner)
277                 return;
278         if(vlen(force) <= 0)
279                 return;
280         if(time > self.pushltime)
281                 if(attacker.classname == "player")
282                         self.team = attacker.team;
283 }
284
285 void kh_Key_Spawn(entity initial_owner, float angle)
286 {
287         entity key;
288         key = spawn();
289         key.classname = STR_ITEM_KH_KEY;
290         key.touch = kh_Key_Touch;
291         key.think = kh_Key_Think;
292         key.nextthink = time;
293         key.items = IT_KEY1 | IT_KEY2;
294         key.cnt = angle;
295         key.angles = '0 360 0' * random();
296         key.event_damage = kh_Key_Damage;
297         key.modelindex = kh_key_dropped;
298         key.model = "key";
299         setsize(key, KH_KEY_MIN, KH_KEY_MAX);
300
301         switch(initial_owner.team)
302         {
303                 case COLOR_TEAM1:
304                         key.netname = "^1red key";
305                         key.colormod = '103 0 0' * KH_KEY_BRIGHTNESS;
306                         kh_average_teamcolor += key.colormod;
307                         break;
308                 case COLOR_TEAM2:
309                         key.netname = "^4blue key";
310                         key.colormod = '35 35 191' * KH_KEY_BRIGHTNESS;
311                         kh_average_teamcolor += key.colormod;
312                         break;
313                 case COLOR_TEAM3:
314                         key.netname = "^3yellow key";
315                         key.colormod = '187 167 15' * KH_KEY_BRIGHTNESS;
316                         kh_average_teamcolor += key.colormod;
317                         break;
318                 case COLOR_TEAM4:
319                         key.netname = "^6pink key";
320                         key.colormod = '139 79 107' * KH_KEY_BRIGHTNESS;
321                         kh_average_teamcolor += key.colormod;
322                         break;
323                 default:
324                         key.netname = "NETGIER key";
325                         key.colormod = '1 1 1';
326                         break;
327         }
328
329         sprint(initial_owner, strcat("You got the ^2", key.netname, "\n"));
330
331         WaypointSprite_Spawn("", 0, 0, key, '0 0 1' * KH_KEY_WP_ZSHIFT, world, key.team, key, waypointsprite_attachedforcarrier, FALSE);
332         key.waypointsprite_attachedforcarrier.waypointsprite_for_player = kh_Key_waypointsprite_for_player;
333
334         kh_Key_AssignTo(key, initial_owner);
335 }
336
337 void kh_Key_Remove(entity key)
338 {
339         entity o;
340         o = key.owner;
341         kh_Key_AssignTo(key, world);
342         if(o) // it was attached
343                 WaypointSprite_Kill(key.waypointsprite_attachedforcarrier);
344         else // it was dropped
345                 WaypointSprite_DetachCarrier(key);
346
347         remove(key);
348 }
349
350 // -1 when no team completely owns all keys yet
351 float kh_Key_AllOwnedByWhichTeam()
352 {
353         entity key;
354         float teem;
355
356         teem = -1;
357         FOR_EACH_KH_KEY(key)
358         {
359                 if(!key.owner)
360                         return -1;
361                 if(teem == -1)
362                         teem = key.team;
363                 else if(teem != key.team)
364                         return -1;
365         }
366         return teem;
367 }
368
369 void kh_Key_Collect(entity key, entity player)
370 {
371         sound(player, CHAN_AUTO, kh_sound_collect, 1, ATTN_NORM);
372
373         kh_Scores_Event(player, key, "collect", cvar("g_balance_keyhunt_score_collect"), 0);
374         bprint(player.netname, "^7 collected the ", key.netname, "\n");
375         kh_Key_AssignTo(key, player);
376
377         if(kh_Key_AllOwnedByWhichTeam() != -1)
378         {
379                 kh_interferemsg_time = time + 0.2;
380                 kh_interferemsg_team = player.team;
381         }
382 }
383
384 void kh_Key_DropAll(entity player)
385 {
386         entity key;
387         entity mypusher;
388         if(player.kh_next)
389         {
390                 mypusher = world;
391                 if(player.pusher)
392                         if(time < player.pushltime)
393                                 mypusher = player.pusher;
394                 while((key = player.kh_next))
395                 {
396                         kh_Scores_Event(player, key, "losekey", 0, 0);
397                         bprint(player.netname, "^7 lost the ", key.netname, "\n");
398                         kh_Key_AssignTo(key, world);
399                         makevectors('-1 0 0' * (45 + 45 * random()) + '0 360 0' * random());
400                         key.velocity = W_CalculateProjectileVelocity(player.velocity, cvar("g_balance_keyhunt_dropvelocity") * v_forward);
401                         key.pusher = mypusher;
402                         key.pushltime = time + cvar("g_balance_keyhunt_protecttime");
403                 }
404                 sound(player, CHAN_AUTO, kh_sound_drop, 1, ATTN_NORM);
405         }
406 }
407
408 void kh_Key_Touch()
409 {
410         if(intermission_running)
411                 return;
412
413         if(self.owner) // already carried
414                 return;
415         if(other.classname != "player")
416                 return;
417         if(other.deadflag != DEAD_NO)
418                 return;
419         if(other == self.enemy)
420                 if(time < self.kh_droptime + cvar("g_balance_keyhunt_delay_collect"))
421                         return; // you just dropped it!
422         kh_Key_Collect(self, other);
423 }
424
425 void kh_Key_Think()
426 {
427         entity head;
428
429         if(intermission_running)
430                 return;
431
432 #ifdef KH_KEY_ATTACHMENT_DEBUG
433         if(self.kh_prev == self.owner)
434         {
435                 if(cvar_string("_angles") != "")
436                 {
437                         self.angles = stov(cvar_string("_angles"));
438                         self.origin = stov(cvar_string("_origin"));
439                 }
440         }
441 #endif
442
443         if(self.owner)
444         {
445 #ifndef KH_PLAYER_USE_ATTACHMENT
446                 makevectors('0 1 0' * (self.cnt + math_mod(time, 360) * KH_KEY_XYSPEED));
447                 setorigin(self, v_forward * KH_KEY_XYDIST + '0 0 1' * self.origin_z);
448 #endif
449
450                 if(self.owner.buttonuse)
451                 if(time >= self.owner.kh_droptime + cvar("g_balance_keyhunt_delay_drop"))
452                 {
453                         self.owner.kh_droptime = time;
454                         self.kh_droptime = time; // prevent collecting this one for some time
455                         self.enemy = self.owner;
456                         self.pusher = world;
457                         kh_Scores_Event(self.owner, self, "dropkey", 0, 0);
458                         bprint(self.owner.netname, "^7 dropped the ", self.netname, "\n");
459                         sound(self.owner, CHAN_AUTO, kh_sound_drop, 1, ATTN_NORM);
460                         makevectors(self.owner.v_angle);
461                         self.velocity = W_CalculateProjectileVelocity(self.owner.velocity, cvar("g_balance_keyhunt_throwvelocity") * v_forward);
462                         kh_Key_AssignTo(self, world);
463                         self.pushltime = time + cvar("g_balance_keyhunt_protecttime");
464                 }
465         }
466
467         // if in nodrop or time over, end the round
468         if(!self.owner)
469                 if(time > self.pain_finished)
470                         kh_LoserTeam(self.team, self);
471         
472         if(self.owner)
473         if(kh_Key_AllOwnedByWhichTeam() != -1)
474         {
475                 entity key;
476                 vector p;
477                 p = self.owner.origin;
478                 FOR_EACH_KH_KEY(key)
479                         if(vlen(key.owner.origin - p) > cvar("g_balance_keyhunt_maxdist"))
480                                 goto not_winning;
481                 kh_WinnerTeam(self.team);
482 :not_winning
483         }
484
485         if(kh_interferemsg_time && time > kh_interferemsg_time)
486         {
487                 kh_interferemsg_time = 0;
488                 FOR_EACH_PLAYER(head)
489                 {
490                         if(head.team == kh_interferemsg_team)
491                                 if(head.kh_next)
492                                         centerprint(head, "All keys are in your team's hands!\n\nMeet the other key carriers ^1NOW^7!\n");
493                                 else
494                                         centerprint(head, "All keys are in your team's hands!\n\nHelp the key carriers to meet!\n");
495                         else
496                                 centerprint(head, strcat("All keys are in the ", ColoredTeamName(kh_interferemsg_team), "^7's hands!\n\nInterfere ^1NOW^7!\n"));
497                 }
498         }
499
500         self.nextthink = time + 0.05;
501 }
502
503 void kh_WinnerTeam(float teem)
504 {
505         // all key carriers get some points
506         vector firstorigin, lastorigin, midpoint;
507         float first;
508         entity key;
509         float score;
510         score = (kh_teams - 1) * cvar("g_balance_keyhunt_score_capture");
511         DistributeEvenly_Init(score, kh_teams);
512         // twice the score for 3 team games, three times the score for 4 team games!
513         // note: for a win by destroying the key, this should NOT be applied
514         FOR_EACH_KH_KEY(key)
515                 kh_Scores_Event(key.owner, key, "capture", DistributeEvenly_Get(1), 0);
516
517         first = TRUE;
518         FOR_EACH_KH_KEY(key)
519                 if(key.owner.kh_next == key)
520                 {
521                         if(!first)
522                                 bprint("^7, ");
523                         bprint(key.owner.netname);
524                         first = FALSE;
525                 }
526         bprint("^7 captured the keys for the ", ColoredTeamName(teem), "\n");
527
528         first = TRUE;
529         midpoint = '0 0 0';
530         FOR_EACH_KH_KEY(key)
531         {
532                 vector thisorigin;
533
534                 thisorigin = kh_AttachedOrigin(key);
535                 midpoint += thisorigin;
536
537                 if(!first)
538                         te_lightning2(world, lastorigin, thisorigin);
539                 lastorigin = thisorigin;
540                 if(first)
541                         firstorigin = thisorigin;
542                 first = FALSE;
543         }
544         if(kh_teams > 2)
545         {
546                 te_lightning2(world, lastorigin, firstorigin);
547         }
548         midpoint *= 1 / kh_teams;
549         te_customflash(midpoint, 1000, 1, kh_average_teamcolor);
550
551         sound(world, CHAN_AUTO, kh_sound_capture, 1, ATTN_NONE);
552         kh_FinishRound();
553 }
554
555 void kh_LoserTeam(float teem, entity lostkey)
556 {
557         entity player, key, attacker;
558         float players;
559         float keys;
560
561         attacker = world;
562         if(lostkey.pusher)
563                 if(lostkey.pusher.team != teem)
564                         if(lostkey.pusher.classname == "player")
565                                 attacker = lostkey.pusher;
566
567         players = keys = 0;
568
569         if(attacker)
570         {
571                 kh_Scores_Event(attacker, world, "push", cvar("g_balance_keyhunt_score_push"), 0);
572                 centerprint(attacker, "Your push is the best!\n\n\n");
573                 bprint("The ", ColoredTeamName(teem), "^7 could not take care of the ", lostkey.netname, "^7 when ", attacker.netname, "^7 came\n");
574         }
575         else
576         {
577                 float of, fragsleft, i, j, thisteam;
578                 of = cvar("g_balance_keyhunt_score_destroyed_ownfactor");
579
580                 FOR_EACH_PLAYER(player)
581                         if(player.team != teem)
582                                 ++players;
583                 
584                 FOR_EACH_KH_KEY(key)
585                         if(key.owner && key.team != teem)
586                                 ++keys;
587
588                 DistributeEvenly_Init(cvar("g_balance_keyhunt_score_destroyed"), keys * of + players);
589
590                 FOR_EACH_KH_KEY(key)
591                         if(key.owner && key.team != teem)
592                                 kh_Scores_Event(key.owner, world, "destroyed_holdingkey", DistributeEvenly_Get(of), 0);
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                                         kh_Scores_Event(player, world, "destroyed", DistributeEvenly_Get(1), 0);
616
617                         --j;
618                 }
619
620                 bprint("The ", ColoredTeamName(teem), "^7 could not take care of the ", lostkey.netname, "\n");
621         }
622         sound(world, CHAN_AUTO, kh_sound_destroy, 1, ATTN_NONE);
623         te_tarexplosion(lostkey.origin);
624
625         kh_FinishRound();
626 }
627
628 void kh_FinishRound()
629 {
630         // prepare next round
631         kh_interferemsg_time = 0;
632         entity key;
633         FOR_EACH_KH_KEY(key)
634                 kh_Key_Remove(key);
635
636         kh_Controller_SetThink(cvar("g_balance_keyhunt_delay_round"), "Round starts in ", kh_StartRound);
637 }
638
639 string kh_CheckEnoughPlayers()
640 {
641         float i, players, teem;
642         entity player;
643         string result;
644         result = "";
645         
646         // find a random player per team
647         for(i = 0; i < kh_teams; ++i)
648         {
649                 teem = kh_Team_ByID(i);
650                 players = 0;
651                 FOR_EACH_PLAYER(player)
652                         if(player.deadflag == DEAD_NO)
653                                 if(!player.buttonchat)
654                                         if(player.team == teem)
655                                                 ++players;
656                 if(players == 0)
657                 {
658                         if(result != "")
659                                 result = strcat(result, ", ");
660                         result = strcat(result, ColoredTeamName(teem));
661                 }
662         }
663         return result;
664 }
665
666 void kh_WaitForPlayers()
667 {
668         string teams_missing;
669         teams_missing = kh_CheckEnoughPlayers();
670         if(teams_missing == "")
671                 kh_Controller_SetThink(cvar("g_balance_keyhunt_delay_round"), "Round starts in ", kh_StartRound);
672         else
673                 kh_Controller_SetThink(1, strcat("Waiting for players to join...\n\nNeed active players for: ", teams_missing), kh_WaitForPlayers);
674 }
675
676 void kh_StartRound()
677 {
678         string teams_missing;
679         float i, players, teem;
680         entity player;
681
682         teams_missing = kh_CheckEnoughPlayers();
683         if(teams_missing != "")
684         {
685                 kh_Controller_SetThink(1, strcat("Waiting for players to join...\n\nNeed active players for: ", teams_missing), kh_WaitForPlayers);
686                 return;
687         }
688
689         FOR_EACH_PLAYER(player)
690                 if(clienttype(player) == CLIENTTYPE_REAL)
691                         centerprint_expire(player, CENTERPRIO_SPAM);
692
693         kh_average_teamcolor = '0 0 0';
694         for(i = 0; i < kh_teams; ++i)
695         {
696                 teem = kh_Team_ByID(i);
697                 players = 0;
698                 entity my_player;
699                 FOR_EACH_PLAYER(player)
700                         if(player.deadflag == DEAD_NO)
701                                 if(!player.buttonchat)
702                                         if(player.team == teem)
703                                         {
704                                                 ++players;
705                                                 if(random() * players <= 1)
706                                                         my_player = player;
707                                         }
708                 kh_Key_Spawn(my_player, 360 * i / kh_teams);
709                 //kh_Key_Spawn(my_player, 360 * i / kh_teams);
710                 //kh_Key_Spawn(my_player, 360 * i / kh_teams);
711         }
712         kh_average_teamcolor *= 1 / kh_teams;
713
714         kh_tracking_enabled = FALSE;
715         kh_Controller_SetThink(cvar("g_balance_keyhunt_delay_tracking"), "Scanning frequency range...", kh_EnableTrackingDevice);
716 }
717
718 void kh_setstatus()
719 {
720         if(kh_teams)
721         {
722                 float kh_KEY;
723                 kh_KEY = (IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST); // the one impossible combination
724                 if(self.kh_next)
725                         self.items = self.items | kh_KEY;
726                 else
727                         self.items = self.items - (self.items & kh_KEY);
728         }
729 }
730
731 void kh_EnableTrackingDevice()
732 {
733         entity player;
734
735         FOR_EACH_PLAYER(player)
736                 if(clienttype(player) == CLIENTTYPE_REAL)
737                         centerprint_expire(player, CENTERPRIO_SPAM);
738
739         kh_tracking_enabled = TRUE;
740 }
741
742 float kh_Key_waypointsprite_for_player(entity e)
743 {
744         if(!kh_tracking_enabled)
745                 return 0;
746         if(!self.owner)
747                 return kh_sprite_dropped;
748         if(!self.owner.owner)
749                 return kh_sprite_dropped;
750         return 0; // draw only when key is not owned
751 }
752
753 float kh_KeyCarrier_waypointsprite_for_player(entity e)
754 {
755         if(e.classname != "player" || self.team != e.team)
756                 if(!kh_tracking_enabled)
757                         return 0;
758
759         // e is spectator? That's no team mate...
760         if(e.classname != "player")
761                 return kh_GetCarrierSprite(self.team, -1);
762         
763         // e is no key carrier: simple case...
764         if(!e.kh_next)
765                 return kh_GetCarrierSprite(self.team, e.team);
766         
767         // e is a key carrier: if any key is dropped or owned by another team, show
768         // the carrier sprite; otherwise show run here
769         if(kh_Key_AllOwnedByWhichTeam() == e.team)
770                 return kh_sprite_finish;
771
772         return kh_GetCarrierSprite(self.team, e.team);
773 }
774
775 float kh_HandleFrags(entity attacker, entity targ, float f)
776 {
777         if(f <= 0)
778                 return f;
779         if(attacker == targ)
780                 return f;
781
782         if(targ.kh_next)
783                 kh_Scores_Event(attacker, targ.kh_next, "carrierfrag", cvar("g_balance_keyhunt_score_carrierfrag")-1, 0);
784
785         return f;
786 }
787
788 void kh_init()
789 {
790         precache_sound(kh_sound_capture);
791         precache_sound(kh_sound_destroy);
792         precache_sound(kh_sound_drop);
793         precache_sound(kh_sound_collect);
794
795         precache_model("models/sprites/key-dropped.sp2");
796         precache_model("models/sprites/keycarrier-finish.sp2");
797         precache_model("models/sprites/keycarrier-friend.sp2");
798         precache_model("models/sprites/keycarrier-red.sp2");
799         precache_model("models/sprites/keycarrier-blue.sp2");
800         precache_model("models/sprites/keycarrier-yellow.sp2");
801         precache_model("models/sprites/keycarrier-pink.sp2");
802 #ifdef KH_PLAYER_USE_CARRIEDMODEL
803         precache_model("models/keyhunt/key-carried.md3");
804 #endif
805         precache_model("models/keyhunt/key.md3");
806
807         // setup variables
808         kh_teams = cvar("g_keyhunt_teams_override");
809         if(kh_teams < 2)
810                 kh_teams = cvar("g_keyhunt_teams");
811         kh_teams = bound(2, kh_teams, 4);
812
813         // make a KH entity for controlling the game
814         kh_controller = spawn();
815         kh_controller.think = kh_Controller_Think;
816         kh_Controller_SetThink(0, "", kh_WaitForPlayers);
817
818         setmodel(kh_controller, "models/keyhunt/key.md3");
819         kh_key_dropped = kh_controller.modelindex;
820         /*
821         dprint(vtos(kh_controller.mins)); 
822         dprint(vtos(kh_controller.maxs)); 
823         dprint("\n");
824         */
825 #ifdef KH_PLAYER_USE_CARRIEDMODEL
826         setmodel(kh_controller, "models/keyhunt/key-carried.md3");
827         kh_key_carried = kh_controller.modelindex;
828 #else
829         kh_key_carried = kh_key_dropped;
830 #endif
831         setmodel(kh_controller, "models/sprites/key-dropped.sp2");
832         kh_sprite_dropped = kh_controller.modelindex;
833         setmodel(kh_controller, "models/sprites/keycarrier-finish.sp2");
834         kh_sprite_finish = kh_controller.modelindex;
835         setmodel(kh_controller, "models/sprites/keycarrier-friend.sp2");
836         kh_sprite_friend = kh_controller.modelindex;
837         setmodel(kh_controller, "models/sprites/keycarrier-red.sp2");
838         kh_sprite_red = kh_controller.modelindex;
839         setmodel(kh_controller, "models/sprites/keycarrier-blue.sp2");
840         kh_sprite_blue = kh_controller.modelindex;
841         setmodel(kh_controller, "models/sprites/keycarrier-pink.sp2");
842         kh_sprite_pink = kh_controller.modelindex;
843         setmodel(kh_controller, "models/sprites/keycarrier-yellow.sp2");
844         kh_sprite_yellow = kh_controller.modelindex;
845         setmodel(kh_controller, "");
846 }
847
848 void kh_finalize()
849 {
850         // to be called before intermission
851         kh_FinishRound();
852         remove(kh_controller);
853         kh_controller = world;
854 }