]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_ctf.qc
Vector should be float
[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 string ctf_CaptureRecord(entity flag, entity player)
32 {
33         float cap_time, cap_record, success;
34         string cap_message, refername;
35         
36         if((autocvar_g_ctf_captimerecord_always) || (player_count - currentbots)) 
37         {
38                 cap_record = ctf_captimerecord;
39                 cap_time = (time - flag.ctf_pickuptime);
40
41                 refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
42                 refername = ((refername == player.netname) ? "their" : strcat(refername, "^7's"));
43
44                 if(!ctf_captimerecord) 
45                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds"); success = TRUE; }
46                 else if(cap_time < cap_record) 
47                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, breaking ", refername, " previous record of ", ftos_decimals(cap_record, 2), " seconds"); success = TRUE; }
48                 else
49                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, failing to break ", refername, " record of ", ftos_decimals(cap_record, 2), " seconds"); success = FALSE; }
50
51                 if(success) 
52                 {
53                         ctf_captimerecord = cap_time;
54                         db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
55                         db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
56                         write_recordmarker(player, (time - cap_time), cap_time); 
57                 } 
58         }
59         
60         return cap_message;
61 }
62
63 void ctf_FlagcarrierWaypoints(entity player)
64 {
65         WaypointSprite_Spawn("flagcarrier", 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, TRUE, RADARICON_FLAG, WPCOLOR_FLAGCARRIER(player.team));
66         WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
67         WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
68         WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
69 }
70
71
72 // =======================
73 // CaptureShield Functions 
74 // =======================
75
76 float ctf_CaptureShield_CheckStatus(entity p) 
77 {
78         float s, se;
79         entity e;
80         float players_worseeq, players_total;
81
82         if(ctf_captureshield_max_ratio <= 0)
83                 return FALSE;
84
85         s = PlayerScore_Add(p, SP_SCORE, 0);
86         if(s >= -ctf_captureshield_min_negscore)
87                 return FALSE;
88
89         players_total = players_worseeq = 0;
90         FOR_EACH_PLAYER(e)
91         {
92                 if(IsDifferentTeam(e, p))
93                         continue;
94                 se = PlayerScore_Add(e, SP_SCORE, 0);
95                 if(se <= s)
96                         ++players_worseeq;
97                 ++players_total;
98         }
99
100         // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
101         // use this rule here
102         
103         if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
104                 return FALSE;
105
106         return TRUE;
107 }
108
109 void ctf_CaptureShield_Update(entity player, float wanted_status)
110 {
111         float updated_status = ctf_CaptureShield_CheckStatus(player);
112         if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
113         {
114                 if(updated_status) // TODO csqc notifier for this // Samual: How?
115                         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);
116                 else
117                         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);
118                         
119                 player.ctf_captureshielded = updated_status;
120         }
121 }
122
123 float ctf_CaptureShield_Customize()
124 {
125         if(!other.ctf_captureshielded) { return FALSE; }
126         if(!IsDifferentTeam(self, other)) { return FALSE; }
127         
128         return TRUE;
129 }
130
131 void ctf_CaptureShield_Touch()
132 {
133         if(!other.ctf_captureshielded) { return; }
134         if(!IsDifferentTeam(self, other)) { return; }
135         
136         vector mymid = (self.absmin + self.absmax) * 0.5;
137         vector othermid = (other.absmin + other.absmax) * 0.5;
138
139         Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
140         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);
141 }
142
143 void ctf_CaptureShield_Spawn(entity flag)
144 {
145         entity shield = spawn();
146         
147         shield.enemy = self;
148         shield.team = self.team;
149         shield.touch = ctf_CaptureShield_Touch;
150         shield.customizeentityforclient = ctf_CaptureShield_Customize;
151         shield.classname = "ctf_captureshield";
152         shield.effects = EF_ADDITIVE;
153         shield.movetype = MOVETYPE_NOCLIP;
154         shield.solid = SOLID_TRIGGER;
155         shield.avelocity = '7 0 11';
156         shield.scale = 0.5;
157         
158         setorigin(shield, self.origin);
159         setmodel(shield, "models/ctf/shield.md3");
160         setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
161 }
162
163
164 // ====================
165 // Drop/Pass/Throw Code
166 // ====================
167
168 void ctf_Handle_Drop(entity flag, entity player, float droptype)
169 {
170         // declarations
171         player = (player ? player : flag.pass_sender);
172
173         // main
174         flag.movetype = MOVETYPE_TOSS;
175         flag.takedamage = DAMAGE_YES;
176         flag.health = flag.max_flag_health;
177         flag.ctf_droptime = time;
178         flag.ctf_dropper = player;
179         flag.ctf_status = FLAG_DROPPED;
180         
181         // messages and sounds
182         Send_KillNotification(player.netname, flag.netname, "", INFO_LOSTFLAG, MSG_INFO);
183         sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTN_NONE);
184         ctf_EventLog("dropped", player.team, player);
185
186         // scoring
187         PlayerTeamScore_AddScore(player, -ctf_ReadScore("penalty_drop"));       
188         PlayerScore_Add(player, SP_CTF_DROPS, 1);
189         
190         // waypoints
191         if(autocvar_g_ctf_flag_dropped_waypoint)
192                 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));
193
194         if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
195         {
196                 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
197                 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
198         }
199         
200         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
201         
202         if(droptype == DROP_PASS)
203         {
204                 flag.pass_sender = world;
205                 flag.pass_target = world;
206         }
207 }
208
209 void ctf_Handle_Retrieve(entity flag, entity player)
210 {
211         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
212         entity sender = flag.pass_sender;
213         
214         // transfer flag to player
215         flag.ctf_carrier = player;
216         flag.owner = player;
217         flag.owner.flagcarried = flag;
218         
219         // reset flag
220         setattachment(flag, player, "");
221         setorigin(flag, FLAG_CARRY_OFFSET);
222         flag.movetype = MOVETYPE_NONE;
223         flag.takedamage = DAMAGE_NO;
224         flag.solid = SOLID_NOT;
225         flag.ctf_carrier = player;
226         flag.ctf_status = FLAG_CARRY;
227
228         // messages and sounds
229         sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTN_NORM);
230         ctf_EventLog("recieve", flag.team, player);
231         
232         FOR_EACH_REALPLAYER(tmp_player)
233         {
234                 if(tmp_player == sender)
235                         centerprint(tmp_player, strcat("You passed the ", flag.netname, " to ", player.netname));
236                 else if(tmp_player == player)
237                         centerprint(tmp_player, strcat("You recieved the ", flag.netname, " from ", sender.netname));
238                 else if(!IsDifferentTeam(tmp_player, sender))
239                         centerprint(tmp_player, strcat(sender.netname, " passed the ", flag.netname, " to ", player.netname));
240         }
241         
242         // create new waypoint
243         ctf_FlagcarrierWaypoints(player);
244         
245         sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
246         player.throw_antispam = sender.throw_antispam;
247         
248         flag.pass_sender = world;
249         flag.pass_target = world;
250 }
251
252 void ctf_Handle_Throw(entity player, entity reciever, float droptype)
253 {
254         entity flag = player.flagcarried;
255         vector targ_origin;
256         
257         if(!flag) { return; }
258         if((droptype == DROP_PASS) && !reciever) { return; }
259         
260         if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
261         
262         // reset the flag
263         setattachment(flag, world, "");
264         setorigin(flag, player.origin + FLAG_DROP_OFFSET);
265         flag.owner.flagcarried = world;
266         flag.owner = world;
267         flag.solid = SOLID_TRIGGER;
268         flag.ctf_droptime = time;
269         
270         flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
271         
272         switch(droptype)
273         {
274                 case DROP_PASS:
275                 {
276                         targ_origin = WarpZone_UnTransformOrigin(reciever, (0.5 * (reciever.absmin + reciever.absmax)));
277                         flag.velocity = (normalize(targ_origin - player.origin) * autocvar_g_ctf_pass_velocity);
278                         break;
279                 }
280                 
281                 case DROP_THROW:
282                 {
283                         makevectors((player.v_angle_y * '0 1 0') + (player.v_angle_x * '0.5 0 0'));
284                         flag.velocity = W_CalculateProjectileVelocity(player.velocity, ('0 0 200' + (v_forward * autocvar_g_ctf_drop_velocity)), FALSE);
285                         break;
286                 }
287                 
288                 case DROP_RESET:
289                 {
290                         flag.velocity = '0 0 0'; // do nothing
291                         break;
292                 }
293                 
294                 default:
295                 case DROP_NORMAL:
296                 {
297                         flag.velocity = W_CalculateProjectileVelocity(player.velocity, ('0 0 200' + ('0 100 0' * crandom()) + ('100 0 0' * crandom())), FALSE);
298                         break;
299                 }
300         }
301         
302         switch(droptype)
303         {
304                 case DROP_PASS:
305                 {
306                         // main
307                         flag.movetype = MOVETYPE_FLY;
308                         flag.takedamage = DAMAGE_NO;
309                         flag.pass_sender = player;
310                         flag.pass_target = reciever;
311                         flag.ctf_status = FLAG_PASSING;
312                         
313                         // other
314                         sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTN_NORM);
315                         WarpZone_TrailParticles(world, particleeffectnum(flag.passeffect), targ_origin, player.origin);
316                         ctf_EventLog("pass", flag.team, player);
317                         break;
318                 }
319
320                 case DROP_RESET: 
321                 {
322                         // do nothing
323                         break;
324                 }
325                 
326                 default:
327                 case DROP_THROW:
328                 case DROP_NORMAL:
329                 {
330                         ctf_Handle_Drop(flag, player, droptype);
331                         break;
332                 }
333         }
334
335         // kill old waypointsprite
336         WaypointSprite_Ping(player.wps_flagcarrier);
337         WaypointSprite_Kill(player.wps_flagcarrier);
338         
339         if(player.wps_enemyflagcarrier)
340                 WaypointSprite_Kill(player.wps_enemyflagcarrier);
341         
342         // captureshield
343         ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
344 }
345
346
347 // ==============
348 // Event Handlers
349 // ==============
350
351 void ctf_Handle_Capture(entity flag, entity toucher, float capturetype)
352 {
353         entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
354         entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
355         
356         if not(player) { return; } // without someone to give the reward to, we can't possibly cap
357         
358         // messages and sounds
359         Send_KillNotification(player.netname, enemy_flag.netname, ctf_CaptureRecord(enemy_flag, player), INFO_CAPTUREFLAG, MSG_INFO);
360         sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE);
361         
362         switch(capturetype)
363         {
364                 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
365                 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
366                 default: break;
367         }
368         
369         // scoring
370         PlayerTeamScore_AddScore(player, ctf_ReadScore("score_capture"));
371         PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
372
373         // effects
374         if(autocvar_g_ctf_flag_capture_effects) 
375         {
376                 pointparticles(particleeffectnum((player.team == COLOR_TEAM1) ? "red_ground_quake" : "blue_ground_quake"), flag.origin, '0 0 0', 1);
377                 shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
378         }
379
380         // other
381         if(capturetype == CAPTURE_NORMAL)
382         {
383                 WaypointSprite_Kill(player.wps_flagcarrier);
384                 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
385         }
386         
387         // reset the flag
388         player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
389         ctf_RespawnFlag(enemy_flag);
390 }
391
392 void ctf_Handle_Return(entity flag, entity player)
393 {
394         // messages and sounds
395         //centerprint(player, strcat("You returned the ", flag.netname));
396         Send_KillNotification(player.netname, flag.netname, "", INFO_RETURNFLAG, MSG_INFO);
397         sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTN_NONE);
398         ctf_EventLog("return", flag.team, player);
399
400         // scoring
401         PlayerTeamScore_AddScore(player, ctf_ReadScore("score_return")); // reward for return
402         PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
403
404         TeamScore_AddToTeam(flag.team, ST_SCORE, -ctf_ReadScore("penalty_returned")); // punish the team who was last carrying it
405         
406         if(flag.ctf_dropper) 
407         {
408                 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -ctf_ReadScore("penalty_returned")); // punish the player who dropped the flag
409                 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag 
410                 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
411         }
412         
413         // reset the flag
414         ctf_RespawnFlag(flag);
415 }
416
417 void ctf_Handle_Pickup(entity flag, entity player, float pickuptype)
418 {
419         // declarations
420         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
421         string verbosename; // holds the name of the player OR no name at all for printing in the centerprints
422         float pickup_dropped_score; // used to calculate dropped pickup score
423         
424         // attach the flag to the player
425         flag.owner = player;
426         player.flagcarried = flag;
427         setattachment(flag, player, "");
428         setorigin(flag, FLAG_CARRY_OFFSET);
429         
430         // flag setup
431         flag.movetype = MOVETYPE_NONE;
432         flag.takedamage = DAMAGE_NO;
433         flag.solid = SOLID_NOT;
434         flag.angles = '0 0 0';
435         flag.ctf_carrier = player;
436         flag.ctf_status = FLAG_CARRY;
437         
438         switch(pickuptype)
439         {
440                 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
441                 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
442                 default: break;
443         }
444
445         // messages and sounds
446         Send_KillNotification (player.netname, flag.netname, "", INFO_GOTFLAG, MSG_INFO);
447         sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE);
448         verbosename = ((autocvar_g_ctf_flag_pickup_verbosename) ? strcat(Team_ColorCode(player.team), "(^7", player.netname, Team_ColorCode(player.team), ") ") : "");
449         
450         FOR_EACH_REALPLAYER(tmp_player)
451         {
452                 if(tmp_player == player)
453                         centerprint(tmp_player, strcat("You got the ", flag.netname, "!"));
454                 else if(!IsDifferentTeam(tmp_player, player))
455                         centerprint(tmp_player, strcat("Your ", Team_ColorCode(player.team), "team mate ", verbosename, "^7got the flag! Protect them!"));
456                 else if(!IsDifferentTeam(tmp_player, flag))
457                         centerprint(tmp_player, strcat("The ", Team_ColorCode(player.team), "enemy ", verbosename, "^7got your flag! Retrieve it!"));
458         }
459                 
460         switch(pickuptype)
461         {
462                 case PICKUP_BASE: ctf_EventLog("steal", flag.team, player); break;
463                 case PICKUP_DROPPED: ctf_EventLog("pickup", flag.team, player); break;
464                 default: break;
465         }
466         
467         // scoring
468         PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
469         switch(pickuptype)
470         {               
471                 case PICKUP_BASE:
472                 {
473                         PlayerTeamScore_AddScore(player, ctf_ReadScore("score_pickup_base"));
474                         break;
475                 }
476                 
477                 case PICKUP_DROPPED:
478                 {
479                         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);
480                         pickup_dropped_score = floor((ctf_ReadScore("score_pickup_dropped_late") * (1 - pickup_dropped_score) + ctf_ReadScore("score_pickup_dropped_early") * pickup_dropped_score) + 0.5);
481                         print("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
482                         PlayerTeamScore_AddScore(player, pickup_dropped_score);
483                         break;
484                 }
485                 
486                 default: break;
487         }
488         
489         // speedrunning
490         if(pickuptype == PICKUP_BASE)
491         {
492                 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
493                 if((player.speedrunning) && (ctf_captimerecord))
494                         ctf_FakeTimeLimit(player, time + ctf_captimerecord);
495         }
496                 
497         // effects
498         if(autocvar_g_ctf_flag_pickup_effects)
499                 pointparticles(particleeffectnum("smoke_ring"), 0.5 * (flag.absmin + flag.absmax), '0 0 0', 1);
500         
501         // waypoints 
502         if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
503         ctf_FlagcarrierWaypoints(player);
504         WaypointSprite_Ping(player.wps_flagcarrier);
505 }
506
507
508 // ===================
509 // Main Flag Functions
510 // ===================
511
512 void ctf_CheckFlagReturn(entity flag, float returntype)
513 {
514         if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
515         
516         if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
517         {
518                 switch(returntype)
519                 {
520                         case RETURN_DROPPED: bprint("The ", flag.netname, " was dropped in the base and returned itself\n"); break;
521                         case RETURN_DAMAGE: bprint("The ", flag.netname, " was destroyed and returned to base\n"); break;
522                         case RETURN_SPEEDRUN: bprint("The ", flag.netname, " became impatient after ", ftos_decimals(ctf_captimerecord, 2), " seconds and returned itself\n"); break;
523                         case RETURN_NEEDKILL: bprint("The ", flag.netname, " fell somewhere it couldn't be reached and returned to base\n"); break;
524                         
525                         default:
526                         case RETURN_TIMEOUT:
527                                 { bprint("The ", flag.netname, " has returned to base\n"); break; }
528                 }
529                 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTN_NONE);
530                 ctf_EventLog("returned", flag.team, world);
531                 ctf_RespawnFlag(flag);
532         }
533 }
534
535 void ctf_CheckStalemate(void)
536 {
537         // declarations
538         float stale_red_flags, stale_blue_flags;
539         entity tmp_entity;
540
541         entity ctf_staleflaglist; // reset the list, we need to build the list each time this function runs
542
543         // build list of stale flags
544         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
545         {
546                 if(autocvar_g_ctf_flagcarrier_waypointforenemy_stalemate)
547                 if(tmp_entity.ctf_status != FLAG_BASE)
548                 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_flagcarrier_waypointforenemy_stalemate)
549                 {
550                         tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
551                         ctf_staleflaglist = tmp_entity;
552                         
553                         switch(tmp_entity.team)
554                         {
555                                 case COLOR_TEAM1: ++stale_red_flags; break;
556                                 case COLOR_TEAM2: ++stale_blue_flags; break;
557                         }
558                 }
559         }
560
561         if(stale_red_flags && stale_blue_flags)
562                 ctf_stalemate = TRUE;
563         else if(!stale_red_flags && !stale_blue_flags)
564                 ctf_stalemate = FALSE;
565         
566         // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
567         if(ctf_stalemate)
568         {
569                 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
570                 {
571                         if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
572                                 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));
573                 }
574                 
575                 if not(wpforenemy_announced)
576                 {
577                         FOR_EACH_REALPLAYER(tmp_entity)
578                                 if(tmp_entity.flagcarried)
579                                         centerprint(tmp_entity, "Stalemate! Enemies can now see you on radar!");
580                                 else
581                                         centerprint(tmp_entity, "Stalemate! Flag carriers can now be seen by enemies on radar!");
582                         
583                         wpforenemy_announced = TRUE;
584                 }
585         }
586 }
587
588 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
589 {
590         if(ITEM_DAMAGE_NEEDKILL(deathtype))
591         {
592                 // automatically kill the flag and return it
593                 self.health = 0;
594                 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
595                 return;
596         }
597         if(autocvar_g_ctf_flag_return_damage) 
598         {
599                 // reduce health and check if it should be returned
600                 self.health = self.health - damage;
601                 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
602                 return;
603         }
604 }
605
606 void ctf_FlagThink()
607 {
608         // declarations
609         entity tmp_entity;
610
611         self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
612
613         // captureshield
614         if(self == ctf_worldflaglist) // only for the first flag
615                 FOR_EACH_CLIENT(tmp_entity)
616                         ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
617
618         // sanity checks
619         if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
620                 dprint("wtf the flag got squashed?\n");
621                 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
622                 if(!trace_startsolid) // can we resize it without getting stuck?
623                         setsize(self, FLAG_MIN, FLAG_MAX); }
624                         
625         switch(self.ctf_status) // reset flag angles in case warpzones adjust it
626         {
627                 case FLAG_DROPPED:
628                 case FLAG_PASSING:
629                 {
630                         self.angles = '0 0 0';
631                         break;
632                 }
633                 
634                 default: break;
635         }
636
637         // main think method
638         switch(self.ctf_status)
639         {       
640                 case FLAG_BASE:
641                 {
642                         if(autocvar_g_ctf_dropped_capture_radius)
643                         {
644                                 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
645                                         if(tmp_entity.ctf_status == FLAG_DROPPED)
646                                                 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
647                                                         ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
648                         }
649                         return;
650                 }
651                 
652                 case FLAG_DROPPED:
653                 {
654                         if(autocvar_g_ctf_flag_dropped_floatinwater)
655                         {
656                                 vector midpoint = ((self.absmin + self.absmax) * 0.5);
657                                 if(pointcontents(midpoint) == CONTENT_WATER)
658                                 {
659                                         self.velocity = self.velocity * 0.5;
660                                         
661                                         if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
662                                                 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
663                                         else
664                                                 { self.movetype = MOVETYPE_FLY; }
665                                 }
666                                 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
667                         }
668                         if(autocvar_g_ctf_flag_return_dropped)
669                         {
670                                 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
671                                 {
672                                         self.health = 0;
673                                         ctf_CheckFlagReturn(self, RETURN_DROPPED);
674                                         return;
675                                 }
676                         }
677                         if(autocvar_g_ctf_flag_return_time)
678                         {
679                                 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
680                                 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
681                                 return;
682                         } 
683                         return;
684                 }
685                         
686                 case FLAG_CARRY:
687                 {
688                         if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord)) 
689                         {
690                                 self.health = 0;
691                                 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
692
693                                 tmp_entity = self;
694                                 self = self.owner;
695                                 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
696                                 ImpulseCommands();
697                                 self = tmp_entity;
698                         }
699                         if(autocvar_g_ctf_flagcarrier_waypointforenemy_stalemate)
700                         {
701                                 if(time >= wpforenemy_nextthink)
702                                 {
703                                         ctf_CheckStalemate();
704                                         wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
705                                 }
706                         }
707                         return;
708                 }
709                 
710                 case FLAG_PASSING: // todo make work with warpzones
711                 {
712                         vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
713                         vector old_targ_origin = targ_origin;
714                         targ_origin = WarpZone_UnTransformOrigin(self.pass_target, targ_origin);
715                         WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
716
717                         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");
718                         
719                         if((self.pass_target.deadflag != DEAD_NO)
720                                 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
721                                 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
722                                 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
723                         {
724                                 ctf_Handle_Drop(self, world, DROP_PASS);
725                         }
726                         else // still a viable target, go for it
727                         {
728                                 vector desired_direction = normalize(targ_origin - self.origin);
729                                 vector current_direction = normalize(self.velocity);
730
731                                 self.velocity = (normalize(current_direction + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); 
732                         }
733                         return;
734                 }
735
736                 default: // this should never happen
737                 {
738                         dprint("ctf_FlagThink(): Flag exists with no status?\n");
739                         return;
740                 }
741         }
742 }
743
744 void ctf_FlagTouch()
745 {
746         if(gameover) { return; }
747         
748         entity toucher = other;
749         
750         // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
751         if(ITEM_TOUCH_NEEDKILL())
752         {
753                 self.health = 0;
754                 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
755                 return;
756         }
757         
758         // special touch behaviors
759         if(toucher.vehicle_flags & VHF_ISVEHICLE)
760         {
761                 if(autocvar_g_ctf_allow_vehicle_touch)
762                         toucher = toucher.owner; // the player is actually the vehicle owner, not other
763                 else
764                         return; // do nothing
765         }
766         else if(toucher.classname != "player") // The flag just touched an object, most likely the world
767         {
768                 if(time > self.wait) // if we haven't in a while, play a sound/effect
769                 {
770                         pointparticles(particleeffectnum(self.toucheffect), self.origin, '0 0 0', 1);
771                         sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTN_NORM);
772                         self.wait = time + FLAG_TOUCHRATE;
773                 }
774                 return;
775         }
776         else if(toucher.deadflag != DEAD_NO) { return; }
777
778         switch(self.ctf_status) 
779         {       
780                 case FLAG_BASE:
781                 {
782                         if(!IsDifferentTeam(toucher, self) && (toucher.flagcarried) && IsDifferentTeam(toucher.flagcarried, self))
783                                 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
784                         else if(IsDifferentTeam(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time))
785                                 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
786                         break;
787                 }
788                 
789                 case FLAG_DROPPED:
790                 {
791                         if(!IsDifferentTeam(toucher, self))
792                                 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
793                         else if((!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
794                                 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
795                         break;
796                 }
797                         
798                 case FLAG_CARRY:
799                 {
800                         dprint("Someone touched a flag even though it was being carried?\n");
801                         break;
802                 }
803                 
804                 case FLAG_PASSING:
805                 {
806                         if((toucher.classname == "player") && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
807                         {
808                                 if(IsDifferentTeam(toucher, self.pass_sender))
809                                         ctf_Handle_Return(self, toucher);
810                                 else
811                                         ctf_Handle_Retrieve(self, toucher);
812                         }
813                         break;
814                 }
815         }
816 }
817
818 void ctf_RespawnFlag(entity flag)
819 {
820         // reset the player (if there is one)
821         if((flag.owner) && (flag.owner.flagcarried == flag))
822         {
823                 if(flag.owner.wps_enemyflagcarrier)
824                         WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
825                         
826                 WaypointSprite_Kill(flag.wps_flagcarrier);
827                 
828                 flag.owner.flagcarried = world;
829
830                 if(flag.speedrunning)
831                         ctf_FakeTimeLimit(flag.owner, -1);
832         }
833
834         if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
835                 { WaypointSprite_Kill(flag.wps_flagdropped); }
836
837         // reset the flag
838         setattachment(flag, world, "");
839         setorigin(flag, flag.ctf_spawnorigin);
840         
841         flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
842         flag.takedamage = DAMAGE_NO;
843         flag.health = flag.max_flag_health;
844         flag.solid = SOLID_TRIGGER;
845         flag.velocity = '0 0 0';
846         flag.angles = flag.mangle;
847         flag.flags = FL_ITEM | FL_NOTARGET;
848         
849         flag.ctf_status = FLAG_BASE;
850         flag.owner = world;
851         flag.pass_sender = world;
852         flag.pass_target = world;
853         flag.ctf_carrier = 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_CARRY:
1001                         {
1002                                 if(flag.owner == self)
1003                                         self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
1004                                 else 
1005                                         self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
1006                                 break;
1007                         }
1008                         case FLAG_DROPPED:
1009                         {
1010                                 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
1011                                 break;
1012                         }
1013                 }
1014         }
1015         
1016         // item for stopping players from capturing the flag too often
1017         if(self.ctf_captureshielded)
1018                 self.items |= IT_CTF_SHIELDED;
1019         
1020         // update the health of the flag carrier waypointsprite
1021         if(self.wps_flagcarrier) 
1022                 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent));
1023         
1024         return 0;
1025 }
1026
1027 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1028 {
1029         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1030         {
1031                 if(frag_target == frag_attacker) // damage done to yourself
1032                 {
1033                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1034                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1035                 }
1036                 else // damage done to everyone else
1037                 {
1038                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1039                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1040                 }
1041         }
1042         else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && IsDifferentTeam(frag_target, frag_attacker)) // if the target is a flagcarrier
1043         {
1044                 // healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent)
1045                 if(autocvar_g_ctf_flagcarrier_auto_helpme_when_damaged > frag_target.health)
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, ctf_ReadScore("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 }