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