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