]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_ctf.qc
Implement "FLAG_TOUCHRATE" to limit the sounds/effects used when a flag is touched...
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / gamemode_ctf.qc
1 // ================================================================
2 //  Official capture the flag game mode coding, reworked by Samual
3 //  Last updated: March 30th, 2012
4 // ================================================================
5
6 float ctf_ReadScore(string parameter) // make this obsolete
7 {
8         //if(g_ctf_win_mode != 2)
9                 return cvar(strcat("g_ctf_personal", parameter));
10         //else
11         //      return cvar(strcat("g_ctf_flag", parameter));
12 }
13
14 void ctf_FakeTimeLimit(entity e, float t)
15 {
16         msg_entity = e;
17         WriteByte(MSG_ONE, 3); // svc_updatestat
18         WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
19         if(t < 0)
20                 WriteCoord(MSG_ONE, autocvar_timelimit);
21         else
22                 WriteCoord(MSG_ONE, (t + 1) / 60);
23 }
24
25 void ctf_EventLog(string mode, float flagteam, entity actor) // use an alias for easy changing and quick editing later
26 {
27         if(autocvar_sv_eventlog)
28                 GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
29 }
30
31
32 // =======================
33 // CaptureShield Functions 
34 // =======================
35
36 float ctf_CaptureShield_CheckStatus(entity p) 
37 {
38         float s, se;
39         entity e;
40         float players_worseeq, players_total;
41
42         if(ctf_captureshield_max_ratio <= 0)
43                 return FALSE;
44
45         s = PlayerScore_Add(p, SP_SCORE, 0);
46         if(s >= -ctf_captureshield_min_negscore)
47                 return FALSE;
48
49         players_total = players_worseeq = 0;
50         FOR_EACH_PLAYER(e)
51         {
52                 if(e.team != p.team)
53                         continue;
54                 se = PlayerScore_Add(e, SP_SCORE, 0);
55                 if(se <= s)
56                         ++players_worseeq;
57                 ++players_total;
58         }
59
60         // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
61         // use this rule here
62         
63         if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
64                 return FALSE;
65
66         return TRUE;
67 }
68
69 void ctf_CaptureShield_Update(entity player, float wanted_status)
70 {
71         float updated_status = ctf_CaptureShield_CheckStatus(player);
72         if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
73         {
74                 if(updated_status) // TODO csqc notifier for this // Samual: How?
75                         Send_CSQC_Centerprint_Generic(player, CPID_CTF_CAPTURESHIELD, "^3You are now ^4shielded^3 from the flag\n^3for ^1too many unsuccessful attempts^3 to capture.\n\n^3Make some defensive scores before trying again.", 5, 0);
76                 else
77                         Send_CSQC_Centerprint_Generic(player, CPID_CTF_CAPTURESHIELD, "^3You are now free.\n\n^3Feel free to ^1try to capture^3 the flag again\n^3if you think you will succeed.", 5, 0);
78                         
79                 player.ctf_captureshielded = updated_status;
80         }
81 }
82
83 float ctf_CaptureShield_Customize()
84 {
85         if not(other.ctf_captureshielded) { return FALSE; }
86         if(self.team == other.team) { return FALSE; }
87         
88         return TRUE;
89 }
90
91 void ctf_CaptureShield_Touch()
92 {
93         if not(other.ctf_captureshielded) { return; }
94         if(self.team == other.team) { return; }
95         
96         vector mymid = (self.absmin + self.absmax) * 0.5;
97         vector othermid = (other.absmin + other.absmax) * 0.5;
98
99         Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
100         Send_CSQC_Centerprint_Generic(other, CPID_CTF_CAPTURESHIELD, "^3You are ^4shielded^3 from the flag\n^3for ^1too many unsuccessful attempts^3 to capture.\n\n^3Get some defensive scores before trying again.", 5, 0);
101 }
102
103 void ctf_CaptureShield_Spawn(entity flag)
104 {
105         entity shield = spawn();
106         
107         shield.enemy = self;
108         shield.team = self.team;
109         shield.touch = ctf_CaptureShield_Touch;
110         shield.customizeentityforclient = ctf_CaptureShield_Customize;
111         shield.classname = "ctf_captureshield";
112         shield.effects = EF_ADDITIVE;
113         shield.movetype = MOVETYPE_NOCLIP;
114         shield.solid = SOLID_TRIGGER;
115         shield.avelocity = '7 0 11';
116         shield.scale = 0.5;
117         
118         setorigin(shield, self.origin);
119         setmodel(shield, "models/ctf/shield.md3");
120         setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
121 }
122
123
124 // ==============
125 // Event Handlers
126 // ==============
127
128 void ctf_Handle_Pass(entity player, entity reciever)
129 {
130         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
131         entity flag = player.flagcarried;
132         if(!flag) { return; }
133         
134         // reset player
135         player.flagcarried = world;
136         WaypointSprite_Ping(player.wps_flagcarrier);
137         WaypointSprite_Kill(player.wps_flagcarrier);
138         
139         // transfer flag to reciever
140         flag.owner = reciever;
141         flag.owner.flagcarried = flag;
142         flag.ctf_pickupid = reciever.playerid;
143         setattachment(flag, reciever, "");
144         setorigin(flag, FLAG_CARRY_OFFSET);
145
146         // messages and sounds
147         sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTN_NORM);
148         ctf_EventLog("pass", flag.team, player);
149         ctf_EventLog("recieve", flag.team, reciever);
150         FOR_EACH_PLAYER(tmp_player)
151                 if(tmp_player == player)
152                         centerprint(tmp_player, strcat("You passed the ", flag.netname, " to ", reciever.netname));
153                 else if(tmp_player == reciever)
154                         centerprint(tmp_player, strcat("You recieved the ", flag.netname, " from ", player.netname));
155                 else if(tmp_player.team == player.team)
156                         centerprint(tmp_player, strcat(player.netname, " passed the ", flag.netname, " to ", reciever.netname));
157         
158         // create new waypoint
159         WaypointSprite_Spawn("flagcarrier", 0, 0, reciever, '0 0 64', world, reciever.team, reciever, wps_flagcarrier, FALSE, RADARICON_FLAG, '1 1 0');
160         WaypointSprite_UpdateMaxHealth(reciever.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
161         WaypointSprite_UpdateHealth(reciever.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(reciever.health, reciever.armorvalue, autocvar_g_balance_armor_blockpercent));
162         WaypointSprite_UpdateTeamRadar(reciever.wps_flagcarrier, RADARICON_FLAGCARRIER, '1 1 0');
163 }
164
165 void ctf_Handle_Drop(entity player, float droptype)
166 {
167         entity flag = player.flagcarried;
168         if(!flag) { return; }
169         
170         if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
171         
172         // reset the flag
173         setattachment(flag, world, "");
174         setorigin(flag, player.origin + FLAG_DROP_OFFSET);
175         flag.owner.flagcarried = world;
176         flag.owner = world;
177         flag.movetype = MOVETYPE_TOSS;
178         flag.solid = SOLID_TRIGGER;
179         flag.takedamage = DAMAGE_YES;
180         flag.health = flag.max_flag_health;
181         
182         switch(droptype)
183         {
184                 case DROPTYPE_THROWN:
185                 {
186                         makevectors((player.v_angle_y * '0 1 0') + (player.v_angle_x * '0.5 0 0'));
187                         flag.velocity = W_CalculateProjectileVelocity(player.velocity, ('0 0 200' + (v_forward * autocvar_g_ctf_throw_velocity)), FALSE);
188                         break;
189                 }
190                 
191                 default:
192                 case DROPTYPE_NORMAL:
193                 {
194                         flag.velocity = ('0 0 200' + ('0 100 0' * crandom()) + ('100 0 0' * crandom()));
195                         break;
196                 }
197         }
198         
199         flag.ctf_droptime = time;
200         flag.ctf_dropperid = player.playerid;
201         flag.ctf_status = FLAG_DROPPED;
202
203         // messages and sounds
204         Send_KillNotification(player.netname, flag.netname, "", INFO_LOSTFLAG, MSG_INFO);
205         sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTN_NONE);
206         ctf_EventLog("dropped", player.team, player);
207         
208         // scoring
209         PlayerTeamScore_AddScore(player, -ctf_ReadScore("penalty_drop"));       
210         PlayerScore_Add(player, SP_CTF_DROPS, 1);
211
212         // waypoints
213         if(autocvar_g_ctf_flag_dropped_waypoint)
214                 WaypointSprite_Spawn("flagdropped", 0, 0, flag, '0 0 64', world, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, FALSE, RADARICON_FLAG, '0 0.5 0' + ((flag.team == COLOR_TEAM1) ? '0.75 0 0' : '0 0 0.75')); // (COLOR_TEAM1 + COLOR_TEAM2 - flag.team)
215         
216         WaypointSprite_Ping(player.wps_flagcarrier);
217         WaypointSprite_Kill(player.wps_flagcarrier);
218
219         if(autocvar_g_ctf_flag_returntime || (autocvar_g_ctf_flag_take_damage && autocvar_g_ctf_flag_health))
220         {
221                 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
222                 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
223         }
224
225         // captureshield
226         ctf_CaptureShield_Update(player, 0); // shield only
227
228         // check if the flag will fall off the map
229         trace_startsolid = FALSE;
230         tracebox(flag.origin, flag.mins, flag.maxs, flag.origin, TRUE, flag);
231         if(trace_startsolid)
232                 dprint("FLAG FALLTHROUGH will happen SOON\n");
233 }
234
235 void ctf_Handle_Capture(entity flag, entity player)
236 {
237         // declarations
238         float cap_time, cap_record, success;
239         string cap_message, refername;
240
241         // records
242         if((autocvar_g_ctf_captimerecord_always) || (player_count - currentbots)) {
243                 cap_record = ctf_captimerecord;
244                 cap_time = (time - player.flagcarried.ctf_pickuptime);
245
246                 refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
247                 refername = ((refername == player.netname) ? "their" : strcat(refername, "^7's"));
248
249                 if(!ctf_captimerecord) 
250                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds"); success = TRUE; }
251                 else if(cap_time < cap_record) 
252                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, breaking ", refername, " previous record of ", ftos_decimals(cap_record, 2), " seconds"); success = TRUE; }
253                 else
254                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, failing to break ", refername, " record of ", ftos_decimals(cap_record, 2), " seconds"); success = FALSE; }
255
256                 if(success) {
257                         ctf_captimerecord = cap_time;
258                         db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
259                         db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
260                         write_recordmarker(player, (time - cap_time), cap_time); } }
261         
262         // messages and sounds
263         Send_KillNotification(player.netname, player.flagcarried.netname, cap_message, INFO_CAPTUREFLAG, MSG_INFO);
264         sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE); // "ctf/*_capture.wav"
265         ctf_EventLog("capture", player.flagcarried.team, player);
266         
267         // scoring
268         PlayerTeamScore_AddScore(player, ctf_ReadScore("score_capture"));
269         PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
270
271         // effects
272         if (autocvar_g_ctf_flag_capture_effects) 
273         {
274                 pointparticles(particleeffectnum((player.team == COLOR_TEAM1) ? "red_ground_quake" : "blue_ground_quake"), flag.origin, '0 0 0', 1);
275                 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
276         }
277
278         // waypointsprites
279         WaypointSprite_Kill(player.wps_flagcarrier);
280
281         // reset the flag
282         if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
283         
284         ctf_RespawnFlag(player.flagcarried);
285 }
286
287 void ctf_Handle_Return(entity flag, entity player)
288 {
289         // messages and sounds
290         //centerprint(player, strcat("You returned ", flag.netname));
291         Send_KillNotification (player.netname, flag.netname, "", INFO_RETURNFLAG, MSG_INFO);
292         sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTN_NONE);
293         ctf_EventLog("return", flag.team, player);
294
295         // scoring
296         PlayerTeamScore_AddScore(player, ctf_ReadScore(strcat("score_return", ((player.playerid == flag.playerid) ? "_by_killer" : "")))); // reward for return
297         PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
298
299         TeamScore_AddToTeam(((flag.team == COLOR_TEAM1) ? COLOR_TEAM2 : COLOR_TEAM1), ST_SCORE, -ctf_ReadScore("penalty_returned")); // punish the team who was last carrying it
300         FOR_EACH_PLAYER(player) if(player.playerid == flag.ctf_dropperid) // punish the player who dropped the flag
301         {
302                 PlayerScore_Add(player, SP_SCORE, -ctf_ReadScore("penalty_returned"));
303                 ctf_CaptureShield_Update(player, 0); // shield only
304         }
305         
306         // reset the flag
307         ctf_RespawnFlag(flag);
308 }
309
310 void ctf_Handle_Pickup_Base(entity flag, entity player)
311 {
312         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
313         string verbosename; // holds the name of the player OR no name at all for printing in the centerprints
314
315         // attach the flag to the player
316         flag.owner = player;
317         player.flagcarried = flag;
318         setattachment(flag, player, "");
319         setorigin(flag, FLAG_CARRY_OFFSET);
320         
321         // set up the flag
322         flag.movetype = MOVETYPE_NONE;
323         flag.takedamage = DAMAGE_NO;
324         flag.solid = SOLID_NOT;
325         flag.angles = '0 0 0';
326         flag.ctf_pickuptime = time; // used for timing runs
327         flag.ctf_pickupid = player.playerid;
328         flag.ctf_status = FLAG_CARRY;
329         
330         // messages and sounds
331         Send_KillNotification (player.netname, flag.netname, "", INFO_GOTFLAG, MSG_INFO);
332         sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE);
333         ctf_EventLog("steal", flag.team, player);
334         verbosename = ((autocvar_g_ctf_flag_pickup_verbosename) ? strcat(Team_ColorCode(player.team), "(^7", player.netname, Team_ColorCode(player.team), ") ") : "");
335         FOR_EACH_PLAYER(tmp_player)
336                 if(tmp_player == player)
337                         centerprint(tmp_player, strcat("You got the ", flag.netname, "!"));
338                 else if(tmp_player.team == player.team)
339                         centerprint(tmp_player, strcat("Your ", Team_ColorCode(player.team), "team mate ", verbosename, "^7got the flag! Protect them!"));
340                 else if(tmp_player.team == flag.team)
341                         centerprint(tmp_player, strcat("The ", Team_ColorCode(player.team), "enemy ", verbosename, "^7got your flag! Retrieve it!"));
342         
343         // scoring
344         PlayerTeamScore_AddScore(player, ctf_ReadScore("score_pickup_base"));
345         PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
346         
347         // speedrunning
348         flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
349         if((player.speedrunning) && (ctf_captimerecord))
350                 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
351                 
352         // effects
353         if (autocvar_g_ctf_flag_pickup_effects)
354         {
355                 pointparticles(particleeffectnum("smoke_ring"), 0.5 * (flag.absmin + flag.absmax), '0 0 0', 1);
356         }
357         
358         // waypoints 
359         WaypointSprite_Spawn("flagcarrier", 0, 0, player, '0 0 64', world, player.team, player, wps_flagcarrier, FALSE, RADARICON_FLAG, '1 1 0'); // (COLOR_TEAM1 + COLOR_TEAM2 - flag.team)
360         WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
361         WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
362         WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, '1 1 0');
363         WaypointSprite_Ping(player.wps_flagcarrier);
364 }
365  
366 void ctf_Handle_Pickup_Dropped(entity flag, entity player)
367 {
368         // declarations
369         float returnscore = bound(0, (flag.pain_finished - time) / autocvar_g_ctf_flag_returntime, 1); // can this be division by zero? FIXME
370         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
371         string verbosename; // holds the name of the player OR no name at all for printing in the centerprints
372
373         // attach the flag to the player
374         flag.owner = player;
375         player.flagcarried = flag;
376         setattachment(flag, player, "");
377         setorigin(flag, FLAG_CARRY_OFFSET);
378         
379         // set up the flag
380         flag.movetype = MOVETYPE_NONE;
381         flag.takedamage = DAMAGE_NO;
382         flag.health = flag.max_flag_health;
383         flag.solid = SOLID_NOT;
384         flag.angles = '0 0 0';
385         //flag.ctf_pickuptime = time; // don't update pickuptime since this isn't a real steal. 
386         flag.ctf_pickupid = player.playerid;
387         flag.ctf_status = FLAG_CARRY;
388
389         // messages and sounds
390         Send_KillNotification (player.netname, flag.netname, "", INFO_PICKUPFLAG, MSG_INFO);
391         sound (player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE);
392         ctf_EventLog("pickup", flag.team, player);
393         verbosename = ((autocvar_g_ctf_flag_pickup_verbosename) ? strcat(Team_ColorCode(player.team), "(^7", player.netname, Team_ColorCode(player.team), ") ") : "");
394         FOR_EACH_PLAYER(tmp_player)
395                 if(tmp_player == player)
396                         centerprint(tmp_player, strcat("You got the ", flag.netname, "!"));
397                 else if(tmp_player.team == player.team)
398                         centerprint(tmp_player, strcat("Your ", Team_ColorCode(player.team), "team mate ", verbosename, "^7got the flag! Protect them!"));
399                 else if(tmp_player.team == flag.team)
400                         centerprint(tmp_player, strcat("The ", Team_ColorCode(player.team), "enemy ", verbosename, "^7got your flag! Retrieve it!"));
401                         
402         // scoring
403         returnscore = floor((ctf_ReadScore("score_pickup_dropped_late") * (1-returnscore) + ctf_ReadScore("score_pickup_dropped_early") * returnscore) + 0.5);
404         print("score is ", ftos(returnscore), "\n");
405         PlayerTeamScore_AddScore(player, returnscore);
406         PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
407
408         // effects
409         if (autocvar_g_ctf_flag_pickup_effects) // field pickup effect
410         {
411                 pointparticles(particleeffectnum("smoke_ring"), 0.5 * (flag.absmin + flag.absmax), '0 0 0', 1); 
412         }
413
414         // waypoints
415         WaypointSprite_Kill(flag.wps_flagdropped);
416         WaypointSprite_Spawn("flagcarrier", 0, 0, player, '0 0 64', world, player.team, player, wps_flagcarrier, FALSE, RADARICON_FLAG, '1 1 0'); // (COLOR_TEAM1 + COLOR_TEAM2 - flag.team)
417         WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
418         WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
419         WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, '1 1 0');
420         WaypointSprite_Ping(player.wps_flagcarrier);
421 }
422
423
424 // ===================
425 // Main Flag Functions
426 // ===================
427
428 void ctf_CheckFlagReturn(entity flag)
429 {
430         if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
431         
432         if((flag.health <= 0) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_returntime))
433         {
434                 bprint("The ", flag.netname, " has returned to base\n");
435                 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTN_NONE);
436                 ctf_EventLog("returned", flag.team, world);
437                 ctf_RespawnFlag(flag);
438         }
439 }
440
441 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
442 {
443         if(deathtype == DEATH_HURTTRIGGER || deathtype == DEATH_SLIME || deathtype == DEATH_LAVA)
444         {
445                 // automatically kill the flag and return it
446                 self.health = 0;
447                 ctf_CheckFlagReturn(self);
448         }
449         
450         if(autocvar_g_ctf_flag_take_damage) 
451         {
452                 self.health = self.health - damage;
453                 ctf_CheckFlagReturn(self);
454         }
455 }
456
457 void ctf_FlagThink()
458 {
459         // declarations
460         entity tmp_entity;
461
462         self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
463
464         // captureshield
465         if(self == ctf_worldflaglist) // only for the first flag
466                 FOR_EACH_CLIENT(tmp_entity)
467                         ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
468
469         // sanity checks
470         if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
471                 dprint("wtf the flag got squished?\n");
472                 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
473                 if(!trace_startsolid) // can we resize it without getting stuck?
474                         setsize(self, FLAG_MIN, FLAG_MAX); }
475
476         // main think method
477         switch(self.ctf_status)
478         {       
479                 case FLAG_BASE: // nothing to do here
480                         return;
481                 
482                 case FLAG_DROPPED:
483                         if(autocvar_g_ctf_flag_returntime)
484                         {
485                                 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_returntime) * FLAG_THINKRATE);
486                                 ctf_CheckFlagReturn(self);
487                         } 
488                         return;
489                                 
490                 case FLAG_CARRY:
491                         if((self.owner) && (self.speedrunning) && (ctf_captimerecord) && (time >= self.ctf_pickuptime + ctf_captimerecord)) 
492                         {
493                                 bprint("The ", self.netname, " became impatient after ", ftos_decimals(ctf_captimerecord, 2), " seconds and returned itself\n");
494                                 sound(self, CH_TRIGGER, self.snd_flag_respawn, VOL_BASE, ATTN_NONE);
495                                 ctf_EventLog("returned", self.team, world);
496                                 ctf_RespawnFlag(tmp_entity);
497
498                                 tmp_entity = self;
499                                 self = self.owner;
500                                 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
501                                 ImpulseCommands();
502                                 self = tmp_entity;
503                         }
504                         return;
505
506                 default: // this should never happen
507                         dprint("Think: Flag exists with no status?\n");
508                         return;
509         }
510 }
511
512 void ctf_FlagTouch()
513 {
514         if(gameover) { return; }
515         if(!self) { return; }
516         if(other.deadflag != DEAD_NO) { return; }
517         if((other.classname != "player") && (self.wait > time))
518         {  // The flag just touched an object, most likely the world
519                 pointparticles(particleeffectnum("kaball_sparks"), self.origin, '0 0 0', 1);
520                 sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTN_NORM);
521                 return; 
522         }
523
524         switch(self.ctf_status) 
525         {       
526                 case FLAG_BASE:
527                         if((other.team == self.team) && (other.flagcarried) && (other.flagcarried.team != self.team))
528                                 ctf_Handle_Capture(self, other); // other just captured the enemies flag to his base
529                         else if((other.team != self.team) && (!other.flagcarried) && (!other.ctf_captureshielded))
530                                 ctf_Handle_Pickup_Base(self, other); // other just stole the enemies flag
531                         break;
532                 
533                 case FLAG_DROPPED:
534                         if(other.team == self.team)
535                                 ctf_Handle_Return(self, other); // other just returned his own flag
536                         else if((!other.flagcarried) && ((other.playerid != self.ctf_dropperid) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
537                                 ctf_Handle_Pickup_Dropped(self, other); // other just picked up a dropped enemy flag
538                         break;
539                                 
540                 case FLAG_CARRY:
541                         dprint("Someone touched a flag even though it was being carried?\n");
542                         break;
543
544                 default: // this should never happen
545                         dprint("Touch: Flag exists with no status?\n");
546                         break;
547         }
548         
549         self.wait = time + FLAG_TOUCHRATE;
550 }
551
552 void ctf_RespawnFlag(entity flag)
553 {
554         // reset the player (if there is one)
555         if((flag.owner) && (flag.owner.flagcarried == flag))
556         {
557                 WaypointSprite_Kill(flag.wps_flagcarrier);
558                 flag.owner.flagcarried = world;
559
560                 if(flag.speedrunning)
561                         ctf_FakeTimeLimit(flag.owner, -1);
562         }
563
564         if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
565                 { WaypointSprite_Kill(flag.wps_flagdropped); }
566
567         // reset the flag
568         setattachment(flag, world, "");
569         setorigin(flag, flag.ctf_spawnorigin); // replace with flag.ctf_spawnorigin
570         flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
571         flag.takedamage = DAMAGE_NO;
572         flag.health = flag.max_flag_health;
573         flag.solid = SOLID_TRIGGER;
574         flag.velocity = '0 0 0';
575         flag.angles = flag.mangle;
576         flag.ctf_status = FLAG_BASE;
577         flag.flags = FL_ITEM | FL_NOTARGET;
578         flag.owner = world;
579 }
580
581 void ctf_Reset()
582 {
583         if(self.owner)
584                 if(self.owner.classname == "player")
585                         ctf_Handle_Drop(self.owner, DROPTYPE_NORMAL);
586                         
587         ctf_RespawnFlag(self);
588 }
589
590 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
591 {
592         // declarations
593         float teamnumber = ((self.team == COLOR_TEAM1) ? TRUE : FALSE); // if we were originally 1, this will become 0. If we were originally 0, this will become 1. 
594
595         // bot waypoints
596         waypoint_spawnforitem_force(self, self.origin);
597         self.nearestwaypointtimeout = 0; // activate waypointing again
598         self.bot_basewaypoint = self.nearestwaypoint;
599
600         // waypointsprites
601         WaypointSprite_SpawnFixed(((teamnumber) ? "redbase" : "bluebase"), self.origin + '0 0 64', self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2) - 1, FALSE));
602         WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2) - 1, FALSE));
603
604         // captureshield setup
605         ctf_CaptureShield_Spawn(self);
606 }
607
608 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc 
609 {       
610         // declarations
611         teamnumber = fabs(teamnumber - bound(0, g_ctf_reverse, 1)); // if we were originally 1, this will become 0. If we were originally 0, this will become 1. 
612         self = flag; // for later usage with droptofloor()
613         
614         // main setup
615         flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
616         ctf_worldflaglist = flag;
617
618         setattachment(flag, world, ""); 
619
620         flag.netname = ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag");
621         flag.team = ((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2); // COLOR_TEAM1: color 4 team (red) - COLOR_TEAM2: color 13 team (blue)
622         flag.items = ((teamnumber) ? IT_KEY2 : IT_KEY1); // IT_KEY2: gold key (redish enough) - IT_KEY1: silver key (bluish enough)
623         flag.classname = "item_flag_team";
624         flag.target = "###item###"; // wut?
625         flag.flags = FL_ITEM | FL_NOTARGET;
626         flag.solid = SOLID_TRIGGER;
627         flag.takedamage = DAMAGE_NO;
628         flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;   
629         flag.max_flag_health = ((autocvar_g_ctf_flag_take_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
630         flag.health = flag.max_flag_health;
631         flag.event_damage = ctf_FlagDamage;
632         flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
633         flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
634         flag.velocity = '0 0 0';
635         flag.mangle = flag.angles;
636         flag.reset = ctf_Reset;
637         flag.touch = ctf_FlagTouch;
638         flag.think = ctf_FlagThink;
639         flag.nextthink = time + 0.2;
640         flag.ctf_status = FLAG_BASE;
641
642         // appearence
643         if(!flag.model) { flag.model = ((teamnumber) ? autocvar_g_ctf_flag_red_model : autocvar_g_ctf_flag_blue_model); }
644         setmodel(flag, flag.model); // precision set below
645         setsize(flag, FLAG_MIN, FLAG_MAX);
646         setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
647         if(!flag.scale) { flag.scale = FLAG_SCALE; }
648         
649         flag.skin = ((teamnumber) ? autocvar_g_ctf_flag_red_skin : autocvar_g_ctf_flag_blue_skin);
650         
651         if(autocvar_g_ctf_flag_glowtrails)
652         {
653                 flag.glow_color = ((teamnumber) ? 251 : 210); // 251: red - 210: blue
654                 flag.glow_size = 25;
655                 flag.glow_trail = 1;
656         }
657         
658         flag.effects |= EF_LOWPRECISION;
659         if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
660         if(autocvar_g_ctf_dynamiclights)   { flag.effects |= ((teamnumber) ? EF_RED : EF_BLUE); }
661         
662         // sound 
663         if(!flag.snd_flag_taken) { flag.snd_flag_taken  = ((teamnumber) ? "ctf/red_taken.wav" : "ctf/blue_taken.wav"); }
664         if(!flag.snd_flag_returned) { flag.snd_flag_returned = ((teamnumber) ? "ctf/red_returned.wav" : "ctf/blue_returned.wav"); }
665         if(!flag.snd_flag_capture) { flag.snd_flag_capture = ((teamnumber) ? "ctf/red_capture.wav" : "ctf/blue_capture.wav"); } // blue team scores by capturing the red flag
666         if(!flag.snd_flag_respawn) { flag.snd_flag_respawn = "ctf/flag_respawn.wav"; } // if there is ever a team-based sound for this, update the code to match.
667         if(!flag.snd_flag_dropped) { flag.snd_flag_dropped = ((teamnumber) ? "ctf/red_dropped.wav" : "ctf/blue_dropped.wav"); }
668         if(!flag.snd_flag_touch) { flag.snd_flag_touch = "keepaway/touch.wav"; } // again has no team-based sound // FIXME
669         
670         // precache
671         precache_sound(flag.snd_flag_taken);
672         precache_sound(flag.snd_flag_returned);
673         precache_sound(flag.snd_flag_capture);
674         precache_sound(flag.snd_flag_respawn);
675         precache_sound(flag.snd_flag_dropped);
676         precache_sound(flag.snd_flag_touch);
677         precache_model(flag.model);
678         precache_model("models/ctf/shield.md3");
679         precache_model("models/ctf/shockwavetransring.md3");
680         
681         // flag placement
682         if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
683         {       
684                 flag.dropped_origin = flag.origin; 
685                 flag.noalign = TRUE;
686                 flag.movetype = MOVETYPE_NONE;
687         }
688         else // drop to floor, automatically find a platform and set that as spawn origin
689         { 
690                 flag.noalign = FALSE;
691                 self = flag;
692                 droptofloor();
693                 flag.movetype = MOVETYPE_TOSS; 
694         }       
695         
696         InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
697 }
698
699
700 // ==============
701 // Hook Functions
702 // ==============
703
704 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
705 {
706         if(self.flagcarried) { ctf_Handle_Drop(self, DROPTYPE_NORMAL); }
707         return 0;
708 }
709
710 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
711 {
712         entity flag;
713         
714         // initially clear items so they can be set as necessary later.
715         self.items &~= (IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST 
716                 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST | IT_CTF_SHIELDED);
717
718         // item for stopping players from capturing the flag too often
719         if(self.ctf_captureshielded)
720                 self.items |= IT_CTF_SHIELDED;
721
722         // scan through all the flags and notify the client about them 
723         for (flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
724         {
725                 if(flag.ctf_status == FLAG_CARRY)
726                         if(flag.owner == self)
727                                 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
728                         else 
729                                 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
730                 else if(flag.ctf_status == FLAG_DROPPED) 
731                         self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
732         }
733         
734         if(self.wps_flagcarrier) { WaypointSprite_UpdateHealth(self.wps_flagcarrier, self.health); }
735         
736         return 0;
737 }
738
739 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
740 {       /*
741         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
742         {
743                 if(frag_target == frag_attacker) // damage done to yourself
744                 {
745                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
746                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
747                 }
748                 else // damage done to noncarriers
749                 {
750                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
751                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
752                 }
753         }*/
754         return 0;
755 }
756
757 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
758 {
759         frag_score = 0; // no frags counted in ctf
760         return (g_ctf_ignore_frags); // you deceptive little bugger ;3 This needs to be true in order for this function to even count. 
761 }
762
763 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
764 {
765         entity player = self;
766
767         if((player.flagcarried) && !(player.speedrunning))
768         {
769                 if(autocvar_g_ctf_allow_pass)
770                 {
771                         entity head;
772                         head = findradius(player.origin, autocvar_g_ctf_pass_radius);
773                         
774                         while(head)
775                         {
776                                 if(head.classname == "player" && head.deadflag == DEAD_NO)
777                                 if(head != player && !IsDifferentTeam(head, player))
778                                 {
779                                         ctf_Handle_Pass(player, head);
780                                         return 0;
781                                 }
782                                 head = head.chain;
783                         }
784                 }
785                 
786                 if(autocvar_g_ctf_allow_drop) { ctf_Handle_Drop(player, DROPTYPE_THROWN); }
787         }
788                 
789         return 0;
790 }
791
792 // ==========
793 // Spawnfuncs
794 // ==========
795
796 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
797 CTF Starting point for a player in team one (Red).
798 Keys: "angle" viewing angle when spawning. */
799 void spawnfunc_info_player_team1()
800 {
801         if(g_assault) { remove(self); return; }
802         
803         self.team = COLOR_TEAM1; // red
804         spawnfunc_info_player_deathmatch();
805 }
806
807
808 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
809 CTF Starting point for a player in team two (Blue).
810 Keys: "angle" viewing angle when spawning. */
811 void spawnfunc_info_player_team2()
812 {
813         if(g_assault) { remove(self); return; }
814         
815         self.team = COLOR_TEAM2; // blue
816         spawnfunc_info_player_deathmatch();
817 }
818
819 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
820 CTF Starting point for a player in team three (Yellow).
821 Keys: "angle" viewing angle when spawning. */
822 void spawnfunc_info_player_team3()
823 {
824         if(g_assault) { remove(self); return; }
825         
826         self.team = COLOR_TEAM3; // yellow
827         spawnfunc_info_player_deathmatch();
828 }
829
830
831 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
832 CTF Starting point for a player in team four (Purple).
833 Keys: "angle" viewing angle when spawning. */
834 void spawnfunc_info_player_team4()
835 {
836         if(g_assault) { remove(self); return; }
837         
838         self.team = COLOR_TEAM4; // purple
839         spawnfunc_info_player_deathmatch();
840 }
841
842 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
843 CTF flag for team one (Red).
844 Keys: 
845 "angle" Angle the flag will point (minus 90 degrees)... 
846 "model" model to use, note this needs red and blue as skins 0 and 1...
847 "noise" sound played when flag is picked up...
848 "noise1" sound played when flag is returned by a teammate...
849 "noise2" sound played when flag is captured...
850 "noise3" sound played when flag is lost in the field and respawns itself... 
851 "noise4" sound played when flag is dropped by a player...
852 "noise5" sound played when flag touches the ground... */
853 void spawnfunc_item_flag_team1()
854 {
855         if(!g_ctf) { remove(self); return; }
856
857         ctf_FlagSetup(1, self); // 1 = red
858 }
859
860 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
861 CTF flag for team two (Blue).
862 Keys: 
863 "angle" Angle the flag will point (minus 90 degrees)... 
864 "model" model to use, note this needs red and blue as skins 0 and 1...
865 "noise" sound played when flag is picked up...
866 "noise1" sound played when flag is returned by a teammate...
867 "noise2" sound played when flag is captured...
868 "noise3" sound played when flag is lost in the field and respawns itself... 
869 "noise4" sound played when flag is dropped by a player...
870 "noise5" sound played when flag touches the ground... */
871 void spawnfunc_item_flag_team2()
872 {
873         if(!g_ctf) { remove(self); return; }
874
875         ctf_FlagSetup(0, self); // the 0 is misleading, but -- 0 = blue.
876 }
877
878 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
879 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
880 Note: If you use spawnfunc_ctf_team entities you must define at least 2!  However, unlike domination, you don't need to make a blank one too.
881 Keys:
882 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
883 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
884 void spawnfunc_ctf_team()
885 {
886         if(!g_ctf) { remove(self); return; }
887         
888         self.classname = "ctf_team";
889         self.team = self.cnt + 1;
890 }
891
892
893 // ==============
894 // Initialization
895 // ==============
896
897 // code from here on is just to support maps that don't have flag and team entities
898 void ctf_SpawnTeam (string teamname, float teamcolor)
899 {
900         entity oldself;
901         oldself = self;
902         self = spawn();
903         self.classname = "ctf_team";
904         self.netname = teamname;
905         self.cnt = teamcolor;
906
907         spawnfunc_ctf_team();
908
909         self = oldself;
910 }
911
912 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
913 {
914         // if no teams are found, spawn defaults
915         if(find(world, classname, "ctf_team") == world)
916         {
917                 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
918                 ctf_SpawnTeam("Red", COLOR_TEAM1 - 1);
919                 ctf_SpawnTeam("Blue", COLOR_TEAM2 - 1);
920         }
921         
922         ScoreRules_ctf();
923 }
924
925 void ctf_Initialize()
926 {
927         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
928
929         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
930         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
931         ctf_captureshield_force = autocvar_g_ctf_shield_force;
932
933         //g_ctf_win_mode = cvar("g_ctf_win_mode");
934         
935         InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
936 }
937
938
939 MUTATOR_DEFINITION(gamemode_ctf)
940 {
941         MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
942         MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
943         MUTATOR_HOOK(PlayerDies, ctf_RemovePlayer, CBC_ORDER_ANY);
944         MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
945         MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
946         MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
947         MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
948         //MUTATOR_HOOK(PlayerPowerups, ctf_PlayerPowerups, CBC_ORDER_ANY);
949
950         MUTATOR_ONADD
951         {
952                 if(time > 1) // game loads at time 1
953                         error("This is a game type and it cannot be added at runtime.");
954                 g_ctf = 1;
955                 ctf_Initialize();
956         }
957
958         MUTATOR_ONREMOVE
959         {
960                 g_ctf = 0;
961                 error("This is a game type and it cannot be removed at runtime.");
962         }
963
964         return 0;
965 }