]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_ctf.qc
better calculation/settings for strength flag throwing
[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: September, 2012
4 // ================================================================
5
6 void ctf_FakeTimeLimit(entity e, float t)
7 {
8         msg_entity = e;
9         WriteByte(MSG_ONE, 3); // svc_updatestat
10         WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
11         if(t < 0)
12                 WriteCoord(MSG_ONE, autocvar_timelimit);
13         else
14                 WriteCoord(MSG_ONE, (t + 1) / 60);
15 }
16
17 void ctf_EventLog(string mode, float flagteam, entity actor) // use an alias for easy changing and quick editing later
18 {
19         if(autocvar_sv_eventlog)
20                 GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
21 }
22
23 string ctf_CaptureRecord(entity flag, entity player)
24 {
25         float cap_time, cap_record, success;
26         string cap_message, refername;
27         
28         if((autocvar_g_ctf_captimerecord_always) || (player_count - currentbots)) 
29         {
30                 cap_record = ctf_captimerecord;
31                 cap_time = (time - flag.ctf_pickuptime);
32
33                 refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
34                 refername = ((refername == player.netname) ? "their" : strcat(refername, "^7's"));
35
36                 if(!ctf_captimerecord) 
37                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds"); success = TRUE; }
38                 else if(cap_time < cap_record) 
39                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, breaking ", refername, " previous record of ", ftos_decimals(cap_record, 2), " seconds"); success = TRUE; }
40                 else
41                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, failing to break ", refername, " record of ", ftos_decimals(cap_record, 2), " seconds"); success = FALSE; }
42
43                 if(success) 
44                 {
45                         ctf_captimerecord = cap_time;
46                         db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
47                         db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
48                         write_recordmarker(player, (time - cap_time), cap_time); 
49                 } 
50         }
51         
52         return cap_message;
53 }
54
55 void ctf_FlagcarrierWaypoints(entity player)
56 {
57         WaypointSprite_Spawn("flagcarrier", 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, TRUE, RADARICON_FLAG, WPCOLOR_FLAGCARRIER(player.team));
58         WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
59         WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
60         WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
61 }
62
63
64 // =======================
65 // CaptureShield Functions 
66 // =======================
67
68 float ctf_CaptureShield_CheckStatus(entity p) 
69 {
70         float s, se;
71         entity e;
72         float players_worseeq, players_total;
73
74         if(ctf_captureshield_max_ratio <= 0)
75                 return FALSE;
76
77         s = PlayerScore_Add(p, SP_SCORE, 0);
78         if(s >= -ctf_captureshield_min_negscore)
79                 return FALSE;
80
81         players_total = players_worseeq = 0;
82         FOR_EACH_PLAYER(e)
83         {
84                 if(IsDifferentTeam(e, p))
85                         continue;
86                 se = PlayerScore_Add(e, SP_SCORE, 0);
87                 if(se <= s)
88                         ++players_worseeq;
89                 ++players_total;
90         }
91
92         // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
93         // use this rule here
94         
95         if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
96                 return FALSE;
97
98         return TRUE;
99 }
100
101 void ctf_CaptureShield_Update(entity player, float wanted_status)
102 {
103         float updated_status = ctf_CaptureShield_CheckStatus(player);
104         if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
105         {
106                 if(updated_status) // TODO csqc notifier for this // Samual: How?
107                         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);
108                 else
109                         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);
110                         
111                 player.ctf_captureshielded = updated_status;
112         }
113 }
114
115 float ctf_CaptureShield_Customize()
116 {
117         if(!other.ctf_captureshielded) { return FALSE; }
118         if(!IsDifferentTeam(self, other)) { return FALSE; }
119         
120         return TRUE;
121 }
122
123 void ctf_CaptureShield_Touch()
124 {
125         if(!other.ctf_captureshielded) { return; }
126         if(!IsDifferentTeam(self, other)) { return; }
127         
128         vector mymid = (self.absmin + self.absmax) * 0.5;
129         vector othermid = (other.absmin + other.absmax) * 0.5;
130
131         Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
132         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);
133 }
134
135 void ctf_CaptureShield_Spawn(entity flag)
136 {
137         entity shield = spawn();
138         
139         shield.enemy = self;
140         shield.team = self.team;
141         shield.touch = ctf_CaptureShield_Touch;
142         shield.customizeentityforclient = ctf_CaptureShield_Customize;
143         shield.classname = "ctf_captureshield";
144         shield.effects = EF_ADDITIVE;
145         shield.movetype = MOVETYPE_NOCLIP;
146         shield.solid = SOLID_TRIGGER;
147         shield.avelocity = '7 0 11';
148         shield.scale = 0.5;
149         
150         setorigin(shield, self.origin);
151         setmodel(shield, "models/ctf/shield.md3");
152         setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
153 }
154
155
156 // ====================
157 // Drop/Pass/Throw Code
158 // ====================
159
160 void ctf_Handle_Drop(entity flag, entity player, float droptype)
161 {
162         // declarations
163         player = (player ? player : flag.pass_sender);
164
165         // main
166         flag.movetype = MOVETYPE_TOSS;
167         flag.takedamage = DAMAGE_YES;
168         flag.health = flag.max_flag_health;
169         flag.ctf_droptime = time;
170         flag.ctf_dropper = player;
171         flag.ctf_status = FLAG_DROPPED;
172         
173         // messages and sounds
174         Send_KillNotification(player.netname, flag.netname, "", INFO_LOSTFLAG, MSG_INFO);
175         sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTN_NONE);
176         ctf_EventLog("dropped", player.team, player);
177
178         // scoring
179         PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);   
180         PlayerScore_Add(player, SP_CTF_DROPS, 1);
181         
182         // waypoints
183         if(autocvar_g_ctf_flag_dropped_waypoint)
184                 WaypointSprite_Spawn("flagdropped", 0, 0, flag, FLAG_WAYPOINT_OFFSET, world, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, TRUE, RADARICON_FLAG, WPCOLOR_DROPPEDFLAG(flag.team));
185
186         if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
187         {
188                 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
189                 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
190         }
191         
192         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
193         
194         if(droptype == DROP_PASS)
195         {
196                 flag.pass_sender = world;
197                 flag.pass_target = world;
198         }
199 }
200
201 void ctf_Handle_Retrieve(entity flag, entity player)
202 {
203         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
204         entity sender = flag.pass_sender;
205         
206         // transfer flag to player
207         flag.owner = player;
208         flag.owner.flagcarried = flag;
209         
210         // reset flag
211         setattachment(flag, player, "");
212         setorigin(flag, FLAG_CARRY_OFFSET);
213         flag.movetype = MOVETYPE_NONE;
214         flag.takedamage = DAMAGE_NO;
215         flag.solid = SOLID_NOT;
216         flag.ctf_status = FLAG_CARRY;
217
218         // messages and sounds
219         sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTN_NORM);
220         ctf_EventLog("recieve", flag.team, player);
221         
222         FOR_EACH_REALPLAYER(tmp_player)
223         {
224                 if(tmp_player == sender)
225                         centerprint(tmp_player, strcat("You passed the ", flag.netname, " to ", player.netname));
226                 else if(tmp_player == player)
227                         centerprint(tmp_player, strcat("You recieved the ", flag.netname, " from ", sender.netname));
228                 else if(!IsDifferentTeam(tmp_player, sender))
229                         centerprint(tmp_player, strcat(sender.netname, " passed the ", flag.netname, " to ", player.netname));
230         }
231         
232         // create new waypoint
233         ctf_FlagcarrierWaypoints(player);
234         
235         sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
236         player.throw_antispam = sender.throw_antispam;
237
238         flag.pass_sender = world;
239         flag.pass_target = world;
240 }
241
242 void ctf_Handle_Throw(entity player, entity receiver, float droptype)
243 {
244         entity flag = player.flagcarried;
245         vector targ_origin, flag_velocity;
246         
247         if(!flag) { return; }
248         if((droptype == DROP_PASS) && !receiver) { return; }
249         
250         if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
251         
252         // reset the flag
253         setattachment(flag, world, "");
254         setorigin(flag, player.origin + FLAG_DROP_OFFSET);
255         flag.owner.flagcarried = world;
256         flag.owner = world;
257         flag.solid = SOLID_TRIGGER;
258         flag.ctf_dropper = player;
259         flag.ctf_droptime = time;
260         
261         flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
262         
263         switch(droptype)
264         {
265                 case DROP_PASS:
266                 {
267                         WarpZone_RefSys_Copy(flag, receiver);
268                         targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax)));
269                         flag.velocity = (normalize(targ_origin - player.origin) * autocvar_g_ctf_pass_velocity);
270                         break;
271                 }
272                 
273                 case DROP_THROW:
274                 {
275                         makevectors((player.v_angle_y * '0 1 0') + (player.v_angle_x * '0.5 0 0'));
276                         flag_velocity = ('0 0 200' + ((v_forward * autocvar_g_ctf_drop_velocity) * ((player.items & IT_STRENGTH) ? autocvar_g_ctf_drop_strengthmultiplier : 1)));
277                         flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, FALSE);
278                         break;
279                 }
280                 
281                 case DROP_RESET:
282                 {
283                         flag.velocity = '0 0 0'; // do nothing
284                         break;
285                 }
286                 
287                 default:
288                 case DROP_NORMAL:
289                 {
290                         flag.velocity = W_CalculateProjectileVelocity(player.velocity, ('0 0 200' + ('0 100 0' * crandom()) + ('100 0 0' * crandom())), FALSE);
291                         break;
292                 }
293         }
294         
295         switch(droptype)
296         {
297                 case DROP_PASS:
298                 {
299                         // main
300                         flag.movetype = MOVETYPE_FLY;
301                         flag.takedamage = DAMAGE_NO;
302                         flag.pass_sender = player;
303                         flag.pass_target = receiver;
304                         flag.ctf_status = FLAG_PASSING;
305                         
306                         // other
307                         sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTN_NORM);
308                         WarpZone_TrailParticles(world, particleeffectnum(flag.passeffect), targ_origin, player.origin);
309                         ctf_EventLog("pass", flag.team, player);
310                         break;
311                 }
312
313                 case DROP_RESET: 
314                 {
315                         // do nothing
316                         break;
317                 }
318                 
319                 default:
320                 case DROP_THROW:
321                 case DROP_NORMAL:
322                 {
323                         ctf_Handle_Drop(flag, player, droptype);
324                         break;
325                 }
326         }
327
328         // kill old waypointsprite
329         WaypointSprite_Ping(player.wps_flagcarrier);
330         WaypointSprite_Kill(player.wps_flagcarrier);
331         
332         if(player.wps_enemyflagcarrier)
333                 WaypointSprite_Kill(player.wps_enemyflagcarrier);
334         
335         // captureshield
336         ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
337 }
338
339
340 // ==============
341 // Event Handlers
342 // ==============
343
344 void ctf_Handle_Capture(entity flag, entity toucher, float capturetype)
345 {
346         entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
347         entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
348         float old_time, new_time; 
349         
350         if not(player) { return; } // without someone to give the reward to, we can't possibly cap
351         
352         // messages and sounds
353         Send_KillNotification(player.netname, enemy_flag.netname, ctf_CaptureRecord(enemy_flag, player), INFO_CAPTUREFLAG, MSG_INFO);
354         sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE);
355         
356         switch(capturetype)
357         {
358                 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
359                 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
360                 default: break;
361         }
362         
363         // scoring
364         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
365         PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
366
367         old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
368         new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
369         if(!old_time || new_time < old_time)
370                 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
371
372         // effects
373         if(autocvar_g_ctf_flag_capture_effects) 
374         {
375                 pointparticles(particleeffectnum((player.team == COLOR_TEAM1) ? "red_ground_quake" : "blue_ground_quake"), flag.origin, '0 0 0', 1);
376                 shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
377         }
378
379         // other
380         if(capturetype == CAPTURE_NORMAL)
381         {
382                 WaypointSprite_Kill(player.wps_flagcarrier);
383                 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
384                 
385                 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
386                         { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
387         }
388         
389         // reset the flag
390         player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
391         ctf_RespawnFlag(enemy_flag);
392 }
393
394 void ctf_Handle_Return(entity flag, entity player)
395 {
396         // messages and sounds
397         //centerprint(player, strcat("You returned the ", flag.netname));
398         Send_KillNotification(player.netname, flag.netname, "", INFO_RETURNFLAG, MSG_INFO);
399         sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTN_NONE);
400         ctf_EventLog("return", flag.team, player);
401
402         // scoring
403         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
404         PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
405
406         TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
407         
408         if(flag.ctf_dropper) 
409         {
410                 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
411                 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag 
412                 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
413         }
414         
415         // reset the flag
416         ctf_RespawnFlag(flag);
417 }
418
419 void ctf_Handle_Pickup(entity flag, entity player, float pickuptype)
420 {
421         // declarations
422         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
423         string verbosename; // holds the name of the player OR no name at all for printing in the centerprints
424         float pickup_dropped_score; // used to calculate dropped pickup score
425         
426         // attach the flag to the player
427         flag.owner = player;
428         player.flagcarried = flag;
429         setattachment(flag, player, "");
430         setorigin(flag, FLAG_CARRY_OFFSET);
431         
432         // flag setup
433         flag.movetype = MOVETYPE_NONE;
434         flag.takedamage = DAMAGE_NO;
435         flag.solid = SOLID_NOT;
436         flag.angles = '0 0 0';
437         flag.ctf_status = FLAG_CARRY;
438         
439         switch(pickuptype)
440         {
441                 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
442                 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
443                 default: break;
444         }
445
446         // messages and sounds
447         Send_KillNotification (player.netname, flag.netname, "", INFO_GOTFLAG, MSG_INFO);
448         sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE);
449         verbosename = ((autocvar_g_ctf_flag_pickup_verbosename) ? strcat(Team_ColorCode(player.team), "(^7", player.netname, Team_ColorCode(player.team), ") ") : "");
450         
451         FOR_EACH_REALPLAYER(tmp_player)
452         {
453                 if(tmp_player == player)
454                         centerprint(tmp_player, strcat("You got the ", flag.netname, "!"));
455                 else if(!IsDifferentTeam(tmp_player, player))
456                         centerprint(tmp_player, strcat("Your ", Team_ColorCode(player.team), "team mate ", verbosename, "^7got the flag! Protect them!"));
457                 else if(!IsDifferentTeam(tmp_player, flag))
458                         centerprint(tmp_player, strcat("The ", Team_ColorCode(player.team), "enemy ", verbosename, "^7got your flag! Retrieve it!"));
459         }
460                 
461         switch(pickuptype)
462         {
463                 case PICKUP_BASE: ctf_EventLog("steal", flag.team, player); break;
464                 case PICKUP_DROPPED: ctf_EventLog("pickup", flag.team, player); break;
465                 default: break;
466         }
467         
468         // scoring
469         PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
470         switch(pickuptype)
471         {               
472                 case PICKUP_BASE:
473                 {
474                         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
475                         break;
476                 }
477                 
478                 case PICKUP_DROPPED:
479                 {
480                         pickup_dropped_score = (autocvar_g_ctf_flag_return_time ? bound(0, ((flag.ctf_droptime + autocvar_g_ctf_flag_return_time) - time) / autocvar_g_ctf_flag_return_time, 1) : 1);
481                         pickup_dropped_score = floor((autocvar_g_ctf_score_pickup_dropped_late * (1 - pickup_dropped_score) + autocvar_g_ctf_score_pickup_dropped_early * pickup_dropped_score) + 0.5);
482                         print("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
483                         PlayerTeamScore_AddScore(player, pickup_dropped_score);
484                         break;
485                 }
486                 
487                 default: break;
488         }
489         
490         // speedrunning
491         if(pickuptype == PICKUP_BASE)
492         {
493                 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
494                 if((player.speedrunning) && (ctf_captimerecord))
495                         ctf_FakeTimeLimit(player, time + ctf_captimerecord);
496         }
497                 
498         // effects
499         if(autocvar_g_ctf_flag_pickup_effects)
500                 pointparticles(particleeffectnum("smoke_ring"), 0.5 * (flag.absmin + flag.absmax), '0 0 0', 1);
501         
502         // waypoints 
503         if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
504         ctf_FlagcarrierWaypoints(player);
505         WaypointSprite_Ping(player.wps_flagcarrier);
506 }
507
508
509 // ===================
510 // Main Flag Functions
511 // ===================
512
513 void ctf_CheckFlagReturn(entity flag, float returntype)
514 {
515         if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
516         
517         if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
518         {
519                 switch(returntype)
520                 {
521                         case RETURN_DROPPED: bprint("The ", flag.netname, " was dropped in the base and returned itself\n"); break;
522                         case RETURN_DAMAGE: bprint("The ", flag.netname, " was destroyed and returned to base\n"); break;
523                         case RETURN_SPEEDRUN: bprint("The ", flag.netname, " became impatient after ", ftos_decimals(ctf_captimerecord, 2), " seconds and returned itself\n"); break;
524                         case RETURN_NEEDKILL: bprint("The ", flag.netname, " fell somewhere it couldn't be reached and returned to base\n"); break;
525                         
526                         default:
527                         case RETURN_TIMEOUT:
528                                 { bprint("The ", flag.netname, " has returned to base\n"); break; }
529                 }
530                 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTN_NONE);
531                 ctf_EventLog("returned", flag.team, world);
532                 ctf_RespawnFlag(flag);
533         }
534 }
535
536 void ctf_CheckStalemate(void)
537 {
538         // declarations
539         float stale_red_flags, stale_blue_flags;
540         entity tmp_entity;
541
542         entity ctf_staleflaglist; // reset the list, we need to build the list each time this function runs
543
544         // build list of stale flags
545         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
546         {
547                 if(autocvar_g_ctf_flagcarrier_waypointforenemy_stalemate)
548                 if(tmp_entity.ctf_status != FLAG_BASE)
549                 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_flagcarrier_waypointforenemy_stalemate)
550                 {
551                         tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
552                         ctf_staleflaglist = tmp_entity;
553                         
554                         switch(tmp_entity.team)
555                         {
556                                 case COLOR_TEAM1: ++stale_red_flags; break;
557                                 case COLOR_TEAM2: ++stale_blue_flags; break;
558                         }
559                 }
560         }
561
562         if(stale_red_flags && stale_blue_flags)
563                 ctf_stalemate = TRUE;
564         else if(!stale_red_flags && !stale_blue_flags)
565                 ctf_stalemate = FALSE;
566         
567         // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
568         if(ctf_stalemate)
569         {
570                 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
571                 {
572                         if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
573                                 WaypointSprite_Spawn("enemyflagcarrier", 0, 0, tmp_entity.owner, FLAG_WAYPOINT_OFFSET, world, tmp_entity.team, tmp_entity.owner, wps_enemyflagcarrier, TRUE, RADARICON_FLAG, WPCOLOR_ENEMYFC(tmp_entity.owner.team));
574                 }
575                 
576                 if not(wpforenemy_announced)
577                 {
578                         FOR_EACH_REALPLAYER(tmp_entity)
579                                 if(tmp_entity.flagcarried)
580                                         centerprint(tmp_entity, "Stalemate! Enemies can now see you on radar!");
581                                 else
582                                         centerprint(tmp_entity, "Stalemate! Flag carriers can now be seen by enemies on radar!");
583                         
584                         wpforenemy_announced = TRUE;
585                 }
586         }
587 }
588
589 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
590 {
591         if(ITEM_DAMAGE_NEEDKILL(deathtype))
592         {
593                 // automatically kill the flag and return it
594                 self.health = 0;
595                 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
596                 return;
597         }
598         if(autocvar_g_ctf_flag_return_damage) 
599         {
600                 // reduce health and check if it should be returned
601                 self.health = self.health - damage;
602                 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
603                 return;
604         }
605 }
606
607 void ctf_FlagThink()
608 {
609         // declarations
610         entity tmp_entity;
611
612         self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
613
614         // captureshield
615         if(self == ctf_worldflaglist) // only for the first flag
616                 FOR_EACH_CLIENT(tmp_entity)
617                         ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
618
619         // sanity checks
620         if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
621                 dprint("wtf the flag got squashed?\n");
622                 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
623                 if(!trace_startsolid) // can we resize it without getting stuck?
624                         setsize(self, FLAG_MIN, FLAG_MAX); }
625                         
626         switch(self.ctf_status) // reset flag angles in case warpzones adjust it
627         {
628                 case FLAG_DROPPED:
629                 case FLAG_PASSING:
630                 {
631                         self.angles = '0 0 0';
632                         break;
633                 }
634                 
635                 default: break;
636         }
637
638         // main think method
639         switch(self.ctf_status)
640         {       
641                 case FLAG_BASE:
642                 {
643                         if(autocvar_g_ctf_dropped_capture_radius)
644                         {
645                                 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
646                                         if(tmp_entity.ctf_status == FLAG_DROPPED)
647                                                 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
648                                                         ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
649                         }
650                         return;
651                 }
652                 
653                 case FLAG_DROPPED:
654                 {
655                         if(autocvar_g_ctf_flag_dropped_floatinwater)
656                         {
657                                 vector midpoint = ((self.absmin + self.absmax) * 0.5);
658                                 if(pointcontents(midpoint) == CONTENT_WATER)
659                                 {
660                                         self.velocity = self.velocity * 0.5;
661                                         
662                                         if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
663                                                 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
664                                         else
665                                                 { self.movetype = MOVETYPE_FLY; }
666                                 }
667                                 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
668                         }
669                         if(autocvar_g_ctf_flag_return_dropped)
670                         {
671                                 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
672                                 {
673                                         self.health = 0;
674                                         ctf_CheckFlagReturn(self, RETURN_DROPPED);
675                                         return;
676                                 }
677                         }
678                         if(autocvar_g_ctf_flag_return_time)
679                         {
680                                 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
681                                 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
682                                 return;
683                         } 
684                         return;
685                 }
686                         
687                 case FLAG_CARRY:
688                 {
689                         if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord)) 
690                         {
691                                 self.health = 0;
692                                 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
693
694                                 tmp_entity = self;
695                                 self = self.owner;
696                                 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
697                                 ImpulseCommands();
698                                 self = tmp_entity;
699                         }
700                         if(autocvar_g_ctf_flagcarrier_waypointforenemy_stalemate)
701                         {
702                                 if(time >= wpforenemy_nextthink)
703                                 {
704                                         ctf_CheckStalemate();
705                                         wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
706                                 }
707                         }
708                         return;
709                 }
710                 
711                 case FLAG_PASSING: // todo make work with warpzones
712                 {
713                         vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
714                         vector old_targ_origin = targ_origin;
715                         targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin);
716                         WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
717
718                         print(strcat("self: ", vtos(self.origin), ", old: ", vtos(old_targ_origin), " (", ftos(vlen(self.origin - old_targ_origin)), "qu)"), ", transformed: ", vtos(targ_origin), " (", ftos(vlen(self.origin - targ_origin)), "qu)", ".\n");
719                         
720                         if((self.pass_target.deadflag != DEAD_NO)
721                                 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
722                                 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
723                                 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
724                         {
725                                 ctf_Handle_Drop(self, world, DROP_PASS);
726                         }
727                         else // still a viable target, go for it
728                         {
729                                 vector desired_direction = normalize(targ_origin - self.origin);
730                                 vector current_direction = normalize(self.velocity);
731
732                                 self.velocity = (normalize(current_direction + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); 
733                         }
734                         return;
735                 }
736
737                 default: // this should never happen
738                 {
739                         dprint("ctf_FlagThink(): Flag exists with no status?\n");
740                         return;
741                 }
742         }
743 }
744
745 void ctf_FlagTouch()
746 {
747         if(gameover) { return; }
748         
749         entity toucher = other;
750         
751         // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
752         if(ITEM_TOUCH_NEEDKILL())
753         {
754                 self.health = 0;
755                 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
756                 return;
757         }
758         
759         // special touch behaviors
760         if(toucher.vehicle_flags & VHF_ISVEHICLE)
761         {
762                 if(autocvar_g_ctf_allow_vehicle_touch)
763                         toucher = toucher.owner; // the player is actually the vehicle owner, not other
764                 else
765                         return; // do nothing
766         }
767         else if(toucher.classname != "player") // The flag just touched an object, most likely the world
768         {
769                 if(time > self.wait) // if we haven't in a while, play a sound/effect
770                 {
771                         pointparticles(particleeffectnum(self.toucheffect), self.origin, '0 0 0', 1);
772                         sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTN_NORM);
773                         self.wait = time + FLAG_TOUCHRATE;
774                 }
775                 return;
776         }
777         else if(toucher.deadflag != DEAD_NO) { return; }
778
779         switch(self.ctf_status) 
780         {       
781                 case FLAG_BASE:
782                 {
783                         if(!IsDifferentTeam(toucher, self) && (toucher.flagcarried) && IsDifferentTeam(toucher.flagcarried, self))
784                                 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
785                         else if(IsDifferentTeam(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time))
786                                 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
787                         break;
788                 }
789                 
790                 case FLAG_DROPPED:
791                 {
792                         if(!IsDifferentTeam(toucher, self))
793                                 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
794                         else if((!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
795                                 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
796                         break;
797                 }
798                         
799                 case FLAG_CARRY:
800                 {
801                         dprint("Someone touched a flag even though it was being carried?\n");
802                         break;
803                 }
804                 
805                 case FLAG_PASSING:
806                 {
807                         if((toucher.classname == "player") && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
808                         {
809                                 if(IsDifferentTeam(toucher, self.pass_sender))
810                                         ctf_Handle_Return(self, toucher);
811                                 else
812                                         ctf_Handle_Retrieve(self, toucher);
813                         }
814                         break;
815                 }
816         }
817 }
818
819 void ctf_RespawnFlag(entity flag)
820 {
821         // reset the player (if there is one)
822         if((flag.owner) && (flag.owner.flagcarried == flag))
823         {
824                 if(flag.owner.wps_enemyflagcarrier)
825                         WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
826                         
827                 WaypointSprite_Kill(flag.wps_flagcarrier);
828                 
829                 flag.owner.flagcarried = world;
830
831                 if(flag.speedrunning)
832                         ctf_FakeTimeLimit(flag.owner, -1);
833         }
834
835         if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
836                 { WaypointSprite_Kill(flag.wps_flagdropped); }
837
838         // reset the flag
839         setattachment(flag, world, "");
840         setorigin(flag, flag.ctf_spawnorigin);
841         
842         flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
843         flag.takedamage = DAMAGE_NO;
844         flag.health = flag.max_flag_health;
845         flag.solid = SOLID_TRIGGER;
846         flag.velocity = '0 0 0';
847         flag.angles = flag.mangle;
848         flag.flags = FL_ITEM | FL_NOTARGET;
849         
850         flag.ctf_status = FLAG_BASE;
851         flag.owner = world;
852         flag.pass_sender = world;
853         flag.pass_target = world;
854         flag.ctf_dropper = world;
855         flag.ctf_pickuptime = 0;
856         flag.ctf_droptime = 0;
857
858         wpforenemy_announced = FALSE;
859 }
860
861 void ctf_Reset()
862 {
863         if(self.owner)
864                 if(self.owner.classname == "player")
865                         ctf_Handle_Throw(self.owner, world, DROP_RESET);
866                         
867         ctf_RespawnFlag(self);
868 }
869
870 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
871 {
872         // bot waypoints
873         waypoint_spawnforitem_force(self, self.origin);
874         self.nearestwaypointtimeout = 0; // activate waypointing again
875         self.bot_basewaypoint = self.nearestwaypoint;
876
877         // waypointsprites
878         WaypointSprite_SpawnFixed(((self.team == COLOR_TEAM1) ? "redbase" : "bluebase"), self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
879         WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
880
881         // captureshield setup
882         ctf_CaptureShield_Spawn(self);
883 }
884
885 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc 
886 {       
887         // declarations
888         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. 
889         self = flag; // for later usage with droptofloor()
890         
891         // main setup
892         flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
893         ctf_worldflaglist = flag;
894
895         setattachment(flag, world, ""); 
896
897         flag.netname = ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag");
898         flag.team = ((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2); // COLOR_TEAM1: color 4 team (red) - COLOR_TEAM2: color 13 team (blue)
899         flag.items = ((teamnumber) ? IT_KEY2 : IT_KEY1); // IT_KEY2: gold key (redish enough) - IT_KEY1: silver key (bluish enough)
900         flag.classname = "item_flag_team";
901         flag.target = "###item###"; // wut?
902         flag.flags = FL_ITEM | FL_NOTARGET;
903         flag.solid = SOLID_TRIGGER;
904         flag.takedamage = DAMAGE_NO;
905         flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;   
906         flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
907         flag.health = flag.max_flag_health;
908         flag.event_damage = ctf_FlagDamage;
909         flag.pushable = TRUE;
910         flag.teleportable = TELEPORT_NORMAL;
911         flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
912         flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
913         flag.velocity = '0 0 0';
914         flag.mangle = flag.angles;
915         flag.reset = ctf_Reset;
916         flag.touch = ctf_FlagTouch;
917         flag.think = ctf_FlagThink;
918         flag.nextthink = time + FLAG_THINKRATE;
919         flag.ctf_status = FLAG_BASE;
920         
921         if(!flag.model) { flag.model = ((teamnumber) ? autocvar_g_ctf_flag_red_model : autocvar_g_ctf_flag_blue_model); }
922         if(!flag.scale) { flag.scale = FLAG_SCALE; }
923         if(!flag.skin) { flag.skin = ((teamnumber) ? autocvar_g_ctf_flag_red_skin : autocvar_g_ctf_flag_blue_skin); }
924         if(!flag.toucheffect) { flag.toucheffect = ((teamnumber) ? "redflag_touch" : "blueflag_touch"); }
925         if(!flag.passeffect) { flag.passeffect = ((!teamnumber) ? "red_pass" : "blue_pass"); } // invert the team number of the flag to pass as enemy team color
926         
927         // sound 
928         if(!flag.snd_flag_taken) { flag.snd_flag_taken  = ((teamnumber) ? "ctf/red_taken.wav" : "ctf/blue_taken.wav"); }
929         if(!flag.snd_flag_returned) { flag.snd_flag_returned = ((teamnumber) ? "ctf/red_returned.wav" : "ctf/blue_returned.wav"); }
930         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
931         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.
932         if(!flag.snd_flag_dropped) { flag.snd_flag_dropped = ((teamnumber) ? "ctf/red_dropped.wav" : "ctf/blue_dropped.wav"); }
933         if(!flag.snd_flag_touch) { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
934         if(!flag.snd_flag_pass) { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
935         
936         // precache
937         precache_sound(flag.snd_flag_taken);
938         precache_sound(flag.snd_flag_returned);
939         precache_sound(flag.snd_flag_capture);
940         precache_sound(flag.snd_flag_respawn);
941         precache_sound(flag.snd_flag_dropped);
942         precache_sound(flag.snd_flag_touch);
943         precache_sound(flag.snd_flag_pass);
944         precache_model(flag.model);
945         precache_model("models/ctf/shield.md3");
946         precache_model("models/ctf/shockwavetransring.md3");
947
948         // appearence
949         setmodel(flag, flag.model); // precision set below
950         setsize(flag, FLAG_MIN, FLAG_MAX);
951         setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
952         
953         if(autocvar_g_ctf_flag_glowtrails)
954         {
955                 flag.glow_color = ((teamnumber) ? 251 : 210); // 251: red - 210: blue
956                 flag.glow_size = 25;
957                 flag.glow_trail = 1;
958         }
959         
960         flag.effects |= EF_LOWPRECISION;
961         if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
962         if(autocvar_g_ctf_dynamiclights)   { flag.effects |= ((teamnumber) ? EF_RED : EF_BLUE); }
963         
964         // flag placement
965         if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
966         {       
967                 flag.dropped_origin = flag.origin; 
968                 flag.noalign = TRUE;
969                 flag.movetype = MOVETYPE_NONE;
970         }
971         else // drop to floor, automatically find a platform and set that as spawn origin
972         { 
973                 flag.noalign = FALSE;
974                 self = flag;
975                 droptofloor();
976                 flag.movetype = MOVETYPE_TOSS; 
977         }       
978         
979         InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
980 }
981
982
983 // ==============
984 // Hook Functions
985 // ==============
986
987 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
988 {
989         entity flag;
990         
991         // initially clear items so they can be set as necessary later.
992         self.items &~= (IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST 
993                 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST | IT_CTF_SHIELDED);
994
995         // scan through all the flags and notify the client about them 
996         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
997         {
998                 switch(flag.ctf_status)
999                 {
1000                         case FLAG_PASSING:
1001                         case FLAG_CARRY:
1002                         {
1003                                 if((flag.owner == self) || (flag.pass_sender == self))
1004                                         self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
1005                                 else 
1006                                         self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
1007                                 break;
1008                         }
1009                         case FLAG_DROPPED:
1010                         {
1011                                 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
1012                                 break;
1013                         }
1014                 }
1015         }
1016         
1017         // item for stopping players from capturing the flag too often
1018         if(self.ctf_captureshielded)
1019                 self.items |= IT_CTF_SHIELDED;
1020         
1021         // update the health of the flag carrier waypointsprite
1022         if(self.wps_flagcarrier) 
1023                 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent));
1024         
1025         return 0;
1026 }
1027
1028 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1029 {
1030         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1031         {
1032                 if(frag_target == frag_attacker) // damage done to yourself
1033                 {
1034                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1035                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1036                 }
1037                 else // damage done to everyone else
1038                 {
1039                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1040                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1041                 }
1042         }
1043         else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && IsDifferentTeam(frag_target, frag_attacker)) // if the target is a flagcarrier
1044         {
1045                 if(autocvar_g_ctf_flagcarrier_auto_helpme_when_damaged > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent)))
1046                         WaypointSprite_HelpMePing(frag_target.wps_flagcarrier); // TODO: only do this if there is a significant loss of health?
1047         }
1048         return 0;
1049 }
1050
1051 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1052 {
1053         if((frag_attacker != frag_target) && (frag_attacker.classname == "player") && (frag_target.flagcarried))
1054         {
1055                 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1056                 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1057         }
1058                                 
1059         if(frag_target.flagcarried)
1060                 { ctf_Handle_Throw(frag_target, world, DROP_NORMAL); }
1061                 
1062         return 0;
1063 }
1064
1065 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1066 {
1067         frag_score = 0;
1068         return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1069 }
1070
1071 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1072 {
1073         if(self.flagcarried)
1074                 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1075                 
1076         return 0;
1077 }
1078
1079 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
1080 {
1081         if(self.flagcarried) 
1082         if(!autocvar_g_ctf_portalteleport)
1083                 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1084
1085         return 0;
1086 }
1087
1088 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
1089 {
1090         entity player = self;
1091
1092         if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1093         {
1094                 // pass the flag to a team mate
1095                 if(autocvar_g_ctf_pass)
1096                 {
1097                         entity head, closest_target;
1098                         head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, TRUE);
1099                         
1100                         while(head) // find the closest acceptable target to pass to
1101                         {
1102                                 if(head.classname == "player" && head.deadflag == DEAD_NO)
1103                                 if(head != player && !IsDifferentTeam(head, player))
1104                                 if(!head.speedrunning && (!head.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1105                                 {
1106                                         if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried) 
1107                                         { 
1108                                                 if(clienttype(head) == CLIENTTYPE_BOT)
1109                                                 {
1110                                                         centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname)); 
1111                                                         ctf_Handle_Throw(head, player, DROP_PASS);
1112                                                 }
1113                                                 else
1114                                                 {
1115                                                         centerprint(head, strcat(player.netname, " requests you to pass the ", head.flagcarried.netname)); 
1116                                                         centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname)); 
1117                                                 }
1118                                                 player.throw_antispam = time + autocvar_g_ctf_pass_wait; 
1119                                                 return 0; 
1120                                         }
1121                                         else if(player.flagcarried)
1122                                         {
1123                                                 if(closest_target)
1124                                                 {
1125                                                         if(vlen(player.origin - WarpZone_UnTransformOrigin(head, head.origin)) < vlen(player.origin - WarpZone_UnTransformOrigin(closest_target, closest_target.origin)))
1126                                                                 { closest_target = head; }
1127                                                 }
1128                                                 else { closest_target = head; }
1129                                         }
1130                                 }
1131                                 head = head.chain;
1132                         }
1133                         
1134                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return 0; }
1135                 }
1136                 
1137                 // throw the flag in front of you
1138                 if(autocvar_g_ctf_drop && player.flagcarried)
1139                         { ctf_Handle_Throw(player, world, DROP_THROW); }
1140         }
1141                 
1142         return 0;
1143 }
1144
1145 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
1146 {
1147         if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
1148         {
1149                 WaypointSprite_HelpMePing(self.wps_flagcarrier);
1150         } 
1151         else // create a normal help me waypointsprite
1152         {
1153                 WaypointSprite_Spawn("helpme", waypointsprite_deployed_lifetime, waypointsprite_limitedrange, self, FLAG_WAYPOINT_OFFSET, world, self.team, self, wps_helpme, FALSE, RADARICON_HELPME, '1 0.5 0');
1154                 WaypointSprite_Ping(self.wps_helpme);
1155         }
1156
1157         return 1;
1158 }
1159
1160 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
1161 {
1162         if(vh_player.flagcarried)
1163         {
1164                 if(!autocvar_g_ctf_flagcarrier_allow_vehicle_carry)
1165                 {
1166                         ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
1167                 }
1168                 else
1169                 {            
1170                         setattachment(vh_player.flagcarried, vh_vehicle, ""); 
1171                         setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
1172                         vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
1173                         //vh_player.flagcarried.angles = '0 0 0';       
1174                 }
1175         }
1176                 
1177         return 0;
1178 }
1179
1180 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
1181 {
1182         if(vh_player.flagcarried)
1183         {
1184                 setattachment(vh_player.flagcarried, vh_player, ""); 
1185                 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
1186                 vh_player.flagcarried.scale = FLAG_SCALE;
1187                 vh_player.flagcarried.angles = '0 0 0'; 
1188         }
1189
1190         return 0;
1191 }
1192
1193 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
1194 {
1195         if(self.flagcarried)
1196         {
1197                 bprint("The ", self.flagcarried.netname, " was returned to base by its carrier\n");
1198                 ctf_RespawnFlag(self);
1199         }
1200         
1201         return 0;
1202 }
1203
1204 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
1205 {
1206         entity flag; // temporary entity for the search method
1207         
1208         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1209         {
1210                 switch(flag.ctf_status)
1211                 {
1212                         case FLAG_DROPPED:
1213                         case FLAG_PASSING:
1214                         {
1215                                 // lock the flag, game is over
1216                                 flag.movetype = MOVETYPE_NONE;
1217                                 flag.takedamage = DAMAGE_NO;
1218                                 flag.solid = SOLID_NOT;
1219                                 flag.nextthink = 0; // stop thinking
1220                                 
1221                                 print("stopping the ", flag.netname, " from moving.\n");
1222                                 break;
1223                         }
1224                         
1225                         default:
1226                         case FLAG_BASE:
1227                         case FLAG_CARRY:
1228                         {
1229                                 // do nothing for these flags
1230                                 break;
1231                         }
1232                 }
1233         }
1234         
1235         return 0;
1236 }
1237
1238
1239 // ==========
1240 // Spawnfuncs
1241 // ==========
1242
1243 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
1244 CTF Starting point for a player in team one (Red).
1245 Keys: "angle" viewing angle when spawning. */
1246 void spawnfunc_info_player_team1()
1247 {
1248         if(g_assault) { remove(self); return; }
1249         
1250         self.team = COLOR_TEAM1; // red
1251         spawnfunc_info_player_deathmatch();
1252 }
1253
1254
1255 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
1256 CTF Starting point for a player in team two (Blue).
1257 Keys: "angle" viewing angle when spawning. */
1258 void spawnfunc_info_player_team2()
1259 {
1260         if(g_assault) { remove(self); return; }
1261         
1262         self.team = COLOR_TEAM2; // blue
1263         spawnfunc_info_player_deathmatch();
1264 }
1265
1266 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
1267 CTF Starting point for a player in team three (Yellow).
1268 Keys: "angle" viewing angle when spawning. */
1269 void spawnfunc_info_player_team3()
1270 {
1271         if(g_assault) { remove(self); return; }
1272         
1273         self.team = COLOR_TEAM3; // yellow
1274         spawnfunc_info_player_deathmatch();
1275 }
1276
1277
1278 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
1279 CTF Starting point for a player in team four (Purple).
1280 Keys: "angle" viewing angle when spawning. */
1281 void spawnfunc_info_player_team4()
1282 {
1283         if(g_assault) { remove(self); return; }
1284         
1285         self.team = COLOR_TEAM4; // purple
1286         spawnfunc_info_player_deathmatch();
1287 }
1288
1289 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
1290 CTF flag for team one (Red).
1291 Keys: 
1292 "angle" Angle the flag will point (minus 90 degrees)... 
1293 "model" model to use, note this needs red and blue as skins 0 and 1...
1294 "noise" sound played when flag is picked up...
1295 "noise1" sound played when flag is returned by a teammate...
1296 "noise2" sound played when flag is captured...
1297 "noise3" sound played when flag is lost in the field and respawns itself... 
1298 "noise4" sound played when flag is dropped by a player...
1299 "noise5" sound played when flag touches the ground... */
1300 void spawnfunc_item_flag_team1()
1301 {
1302         if(!g_ctf) { remove(self); return; }
1303
1304         ctf_FlagSetup(1, self); // 1 = red
1305 }
1306
1307 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
1308 CTF flag for team two (Blue).
1309 Keys: 
1310 "angle" Angle the flag will point (minus 90 degrees)... 
1311 "model" model to use, note this needs red and blue as skins 0 and 1...
1312 "noise" sound played when flag is picked up...
1313 "noise1" sound played when flag is returned by a teammate...
1314 "noise2" sound played when flag is captured...
1315 "noise3" sound played when flag is lost in the field and respawns itself... 
1316 "noise4" sound played when flag is dropped by a player...
1317 "noise5" sound played when flag touches the ground... */
1318 void spawnfunc_item_flag_team2()
1319 {
1320         if(!g_ctf) { remove(self); return; }
1321
1322         ctf_FlagSetup(0, self); // the 0 is misleading, but -- 0 = blue.
1323 }
1324
1325 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
1326 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
1327 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.
1328 Keys:
1329 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
1330 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
1331 void spawnfunc_ctf_team()
1332 {
1333         if(!g_ctf) { remove(self); return; }
1334         
1335         self.classname = "ctf_team";
1336         self.team = self.cnt + 1;
1337 }
1338
1339
1340 // ==============
1341 // Initialization
1342 // ==============
1343
1344 // code from here on is just to support maps that don't have flag and team entities
1345 void ctf_SpawnTeam (string teamname, float teamcolor)
1346 {
1347         entity oldself;
1348         oldself = self;
1349         self = spawn();
1350         self.classname = "ctf_team";
1351         self.netname = teamname;
1352         self.cnt = teamcolor;
1353
1354         spawnfunc_ctf_team();
1355
1356         self = oldself;
1357 }
1358
1359 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
1360 {
1361         // if no teams are found, spawn defaults
1362         if(find(world, classname, "ctf_team") == world)
1363         {
1364                 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
1365                 ctf_SpawnTeam("Red", COLOR_TEAM1 - 1);
1366                 ctf_SpawnTeam("Blue", COLOR_TEAM2 - 1);
1367         }
1368         
1369         ScoreRules_ctf();
1370 }
1371
1372 void ctf_Initialize()
1373 {
1374         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
1375
1376         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
1377         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
1378         ctf_captureshield_force = autocvar_g_ctf_shield_force;
1379         
1380         InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
1381 }
1382
1383
1384 MUTATOR_DEFINITION(gamemode_ctf)
1385 {
1386         MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
1387         MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
1388         MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
1389         MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
1390         MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
1391         MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
1392         MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
1393         MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
1394         MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
1395         MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
1396         MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
1397         MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
1398         MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
1399         
1400         MUTATOR_ONADD
1401         {
1402                 if(time > 1) // game loads at time 1
1403                         error("This is a game type and it cannot be added at runtime.");
1404                 g_ctf = 1;
1405                 ctf_Initialize();
1406         }
1407
1408         MUTATOR_ONREMOVE
1409         {
1410                 g_ctf = 0;
1411                 error("This is a game type and it cannot be removed at runtime.");
1412         }
1413
1414         return 0;
1415 }