]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_ctf.qc
Finish implementing passing and fix a few other things
[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") 
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         else if(self.wait > time) { return; }
524
525         switch(self.ctf_status) 
526         {       
527                 case FLAG_BASE:
528                         if((other.team == self.team) && (other.flagcarried) && (other.flagcarried.team != self.team))
529                                 ctf_Handle_Capture(self, other); // other just captured the enemies flag to his base
530                         else if((other.team != self.team) && (!other.flagcarried) && (!other.ctf_captureshielded))
531                                 ctf_Handle_Pickup_Base(self, other); // other just stole the enemies flag
532                         break;
533                 
534                 case FLAG_DROPPED:
535                         if(other.team == self.team)
536                                 ctf_Handle_Return(self, other); // other just returned his own flag
537                         else if((!other.flagcarried) && ((other.playerid != self.ctf_dropperid) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
538                                 ctf_Handle_Pickup_Dropped(self, other); // other just picked up a dropped enemy flag
539                         break;
540                                 
541                 case FLAG_CARRY:
542                         dprint("Someone touched a flag even though it was being carried?\n");
543                         break;
544
545                 default: // this should never happen
546                         dprint("Touch: Flag exists with no status?\n");
547                         break;
548         }
549 }
550
551 void ctf_RespawnFlag(entity flag)
552 {
553         // reset the player (if there is one)
554         if((flag.owner) && (flag.owner.flagcarried == flag))
555         {
556                 WaypointSprite_Kill(flag.wps_flagcarrier);
557                 flag.owner.flagcarried = world;
558
559                 if(flag.speedrunning)
560                         ctf_FakeTimeLimit(flag.owner, -1);
561         }
562
563         if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
564                 { WaypointSprite_Kill(flag.wps_flagdropped); }
565
566         // reset the flag
567         setattachment(flag, world, "");
568         setorigin(flag, flag.ctf_spawnorigin); // replace with flag.ctf_spawnorigin
569         flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
570         flag.takedamage = DAMAGE_NO;
571         flag.health = flag.max_flag_health;
572         flag.solid = SOLID_TRIGGER;
573         flag.velocity = '0 0 0';
574         flag.angles = flag.mangle;
575         flag.ctf_status = FLAG_BASE;
576         flag.flags = FL_ITEM | FL_NOTARGET;
577         flag.owner = world;
578 }
579
580 void ctf_Reset()
581 {
582         if(self.owner)
583                 if(self.owner.classname == "player")
584                         ctf_Handle_Drop(self.owner, DROPTYPE_NORMAL);
585                         
586         ctf_RespawnFlag(self);
587 }
588
589 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
590 {
591         // declarations
592         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. 
593
594         // bot waypoints
595         waypoint_spawnforitem_force(self, self.origin);
596         self.nearestwaypointtimeout = 0; // activate waypointing again
597         self.bot_basewaypoint = self.nearestwaypoint;
598
599         // waypointsprites
600         WaypointSprite_SpawnFixed(((teamnumber) ? "redbase" : "bluebase"), self.origin + '0 0 64', self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2) - 1, FALSE));
601         WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2) - 1, FALSE));
602
603         // captureshield setup
604         ctf_CaptureShield_Spawn(self);
605 }
606
607 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc 
608 {       
609         // declarations
610         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. 
611         self = flag; // for later usage with droptofloor()
612         
613         // main setup
614         flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
615         ctf_worldflaglist = flag;
616
617         setattachment(flag, world, ""); 
618
619         flag.netname = ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag");
620         flag.team = ((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2); // COLOR_TEAM1: color 4 team (red) - COLOR_TEAM2: color 13 team (blue)
621         flag.items = ((teamnumber) ? IT_KEY2 : IT_KEY1); // IT_KEY2: gold key (redish enough) - IT_KEY1: silver key (bluish enough)
622         flag.classname = "item_flag_team";
623         flag.target = "###item###"; // wut?
624         flag.flags = FL_ITEM | FL_NOTARGET;
625         flag.solid = SOLID_TRIGGER;
626         flag.takedamage = DAMAGE_NO;
627         flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;   
628         flag.max_flag_health = ((autocvar_g_ctf_flag_take_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
629         flag.health = flag.max_flag_health;
630         flag.event_damage = ctf_FlagDamage;
631         flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
632         flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
633         flag.velocity = '0 0 0';
634         flag.mangle = flag.angles;
635         flag.reset = ctf_Reset;
636         flag.touch = ctf_FlagTouch;
637         flag.think = ctf_FlagThink;
638         flag.nextthink = time + 0.2;
639         flag.ctf_status = FLAG_BASE;
640
641         // appearence
642         if(!flag.model) { flag.model = ((teamnumber) ? autocvar_g_ctf_flag_red_model : autocvar_g_ctf_flag_blue_model); }
643         setmodel(flag, flag.model); // precision set below
644         setsize(flag, FLAG_MIN, FLAG_MAX);
645         setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
646         if(!flag.scale) { flag.scale = FLAG_SCALE; }
647         
648         flag.skin = ((teamnumber) ? autocvar_g_ctf_flag_red_skin : autocvar_g_ctf_flag_blue_skin);
649         
650         if(autocvar_g_ctf_flag_glowtrails)
651         {
652                 flag.glow_color = ((teamnumber) ? 251 : 210); // 251: red - 210: blue
653                 flag.glow_size = 25;
654                 flag.glow_trail = 1;
655         }
656         
657         flag.effects |= EF_LOWPRECISION;
658         if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
659         if(autocvar_g_ctf_dynamiclights)   { flag.effects |= ((teamnumber) ? EF_RED : EF_BLUE); }
660         
661         // sound 
662         if(!flag.snd_flag_taken) { flag.snd_flag_taken  = ((teamnumber) ? "ctf/red_taken.wav" : "ctf/blue_taken.wav"); }
663         if(!flag.snd_flag_returned) { flag.snd_flag_returned = ((teamnumber) ? "ctf/red_returned.wav" : "ctf/blue_returned.wav"); }
664         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
665         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.
666         if(!flag.snd_flag_dropped) { flag.snd_flag_dropped = ((teamnumber) ? "ctf/red_dropped.wav" : "ctf/blue_dropped.wav"); }
667         if(!flag.snd_flag_touch) { flag.snd_flag_touch = "keepaway/touch.wav"; } // again has no team-based sound // FIXME
668         
669         // precache
670         precache_sound(flag.snd_flag_taken);
671         precache_sound(flag.snd_flag_returned);
672         precache_sound(flag.snd_flag_capture);
673         precache_sound(flag.snd_flag_respawn);
674         precache_sound(flag.snd_flag_dropped);
675         precache_sound(flag.snd_flag_touch);
676         precache_model(flag.model);
677         precache_model("models/ctf/shield.md3");
678         precache_model("models/ctf/shockwavetransring.md3");
679         
680         // flag placement
681         if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
682         {       
683                 flag.dropped_origin = flag.origin; 
684                 flag.noalign = TRUE;
685                 flag.movetype = MOVETYPE_NONE;
686         }
687         else // drop to floor, automatically find a platform and set that as spawn origin
688         { 
689                 flag.noalign = FALSE;
690                 self = flag;
691                 droptofloor();
692                 flag.movetype = MOVETYPE_TOSS; 
693         }       
694         
695         InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
696 }
697
698
699 // ==============
700 // Hook Functions
701 // ==============
702
703 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
704 {
705         if(self.flagcarried) { ctf_Handle_Drop(self, DROPTYPE_NORMAL); }
706         return 0;
707 }
708
709 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
710 {
711         entity flag;
712         
713         // initially clear items so they can be set as necessary later.
714         self.items &~= (IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST 
715                 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST | IT_CTF_SHIELDED);
716
717         // item for stopping players from capturing the flag too often
718         if(self.ctf_captureshielded)
719                 self.items |= IT_CTF_SHIELDED;
720
721         // scan through all the flags and notify the client about them 
722         for (flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
723         {
724                 if(flag.ctf_status == FLAG_CARRY)
725                         if(flag.owner == self)
726                                 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
727                         else 
728                                 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
729                 else if(flag.ctf_status == FLAG_DROPPED) 
730                         self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
731         }
732         
733         if(self.wps_flagcarrier) { WaypointSprite_UpdateHealth(self.wps_flagcarrier, self.health); }
734         
735         return 0;
736 }
737
738 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
739 {       /*
740         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
741         {
742                 if(frag_target == frag_attacker) // damage done to yourself
743                 {
744                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
745                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
746                 }
747                 else // damage done to noncarriers
748                 {
749                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
750                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
751                 }
752         }*/
753         return 0;
754 }
755
756 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
757 {
758         frag_score = 0; // no frags counted in ctf
759         return (g_ctf_ignore_frags); // you deceptive little bugger ;3 This needs to be true in order for this function to even count. 
760 }
761
762 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
763 {
764         entity player = self;
765
766         if((player.flagcarried) && !(player.speedrunning))
767         {
768                 if(autocvar_g_ctf_allow_pass)
769                 {
770                         entity head;
771                         head = findradius(player.origin, autocvar_g_ctf_pass_radius);
772                         
773                         while(head)
774                         {
775                                 if(head.classname == "player" && head.deadflag == DEAD_NO)
776                                 if(head != player && !IsDifferentTeam(head, player))
777                                 {
778                                         ctf_Handle_Pass(player, head);
779                                         return 0;
780                                 }
781                                 head = head.chain;
782                         }
783                 }
784                 
785                 if(autocvar_g_ctf_allow_drop) { ctf_Handle_Drop(player, DROPTYPE_THROWN); }
786         }
787                 
788         return 0;
789 }
790
791 // ==========
792 // Spawnfuncs
793 // ==========
794
795 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
796 CTF Starting point for a player in team one (Red).
797 Keys: "angle" viewing angle when spawning. */
798 void spawnfunc_info_player_team1()
799 {
800         if(g_assault) { remove(self); return; }
801         
802         self.team = COLOR_TEAM1; // red
803         spawnfunc_info_player_deathmatch();
804 }
805
806
807 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
808 CTF Starting point for a player in team two (Blue).
809 Keys: "angle" viewing angle when spawning. */
810 void spawnfunc_info_player_team2()
811 {
812         if(g_assault) { remove(self); return; }
813         
814         self.team = COLOR_TEAM2; // blue
815         spawnfunc_info_player_deathmatch();
816 }
817
818 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
819 CTF Starting point for a player in team three (Yellow).
820 Keys: "angle" viewing angle when spawning. */
821 void spawnfunc_info_player_team3()
822 {
823         if(g_assault) { remove(self); return; }
824         
825         self.team = COLOR_TEAM3; // yellow
826         spawnfunc_info_player_deathmatch();
827 }
828
829
830 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
831 CTF Starting point for a player in team four (Purple).
832 Keys: "angle" viewing angle when spawning. */
833 void spawnfunc_info_player_team4()
834 {
835         if(g_assault) { remove(self); return; }
836         
837         self.team = COLOR_TEAM4; // purple
838         spawnfunc_info_player_deathmatch();
839 }
840
841 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
842 CTF flag for team one (Red).
843 Keys: 
844 "angle" Angle the flag will point (minus 90 degrees)... 
845 "model" model to use, note this needs red and blue as skins 0 and 1...
846 "noise" sound played when flag is picked up...
847 "noise1" sound played when flag is returned by a teammate...
848 "noise2" sound played when flag is captured...
849 "noise3" sound played when flag is lost in the field and respawns itself... 
850 "noise4" sound played when flag is dropped by a player...
851 "noise5" sound played when flag touches the ground... */
852 void spawnfunc_item_flag_team1()
853 {
854         if(!g_ctf) { remove(self); return; }
855
856         ctf_FlagSetup(1, self); // 1 = red
857 }
858
859 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
860 CTF flag for team two (Blue).
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_team2()
871 {
872         if(!g_ctf) { remove(self); return; }
873
874         ctf_FlagSetup(0, self); // the 0 is misleading, but -- 0 = blue.
875 }
876
877 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
878 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
879 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.
880 Keys:
881 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
882 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
883 void spawnfunc_ctf_team()
884 {
885         if(!g_ctf) { remove(self); return; }
886         
887         self.classname = "ctf_team";
888         self.team = self.cnt + 1;
889 }
890
891
892 // ==============
893 // Initialization
894 // ==============
895
896 // code from here on is just to support maps that don't have flag and team entities
897 void ctf_SpawnTeam (string teamname, float teamcolor)
898 {
899         entity oldself;
900         oldself = self;
901         self = spawn();
902         self.classname = "ctf_team";
903         self.netname = teamname;
904         self.cnt = teamcolor;
905
906         spawnfunc_ctf_team();
907
908         self = oldself;
909 }
910
911 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
912 {
913         // if no teams are found, spawn defaults
914         if(find(world, classname, "ctf_team") == world)
915         {
916                 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
917                 ctf_SpawnTeam("Red", COLOR_TEAM1 - 1);
918                 ctf_SpawnTeam("Blue", COLOR_TEAM2 - 1);
919         }
920         
921         ScoreRules_ctf();
922 }
923
924 void ctf_Initialize()
925 {
926         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
927
928         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
929         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
930         ctf_captureshield_force = autocvar_g_ctf_shield_force;
931
932         //g_ctf_win_mode = cvar("g_ctf_win_mode");
933         
934         InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
935 }
936
937
938 MUTATOR_DEFINITION(gamemode_ctf)
939 {
940         MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
941         MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
942         MUTATOR_HOOK(PlayerDies, ctf_RemovePlayer, CBC_ORDER_ANY);
943         MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
944         MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
945         MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
946         MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
947         //MUTATOR_HOOK(PlayerPowerups, ctf_PlayerPowerups, CBC_ORDER_ANY);
948
949         MUTATOR_ONADD
950         {
951                 if(time > 1) // game loads at time 1
952                         error("This is a game type and it cannot be added at runtime.");
953                 g_ctf = 1;
954                 ctf_Initialize();
955         }
956
957         MUTATOR_ONREMOVE
958         {
959                 g_ctf = 0;
960                 error("This is a game type and it cannot be removed at runtime.");
961         }
962
963         return 0;
964 }