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