]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_ctf.qc
Clean up a little bit
[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 float ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
64 {
65         if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
66         {
67                 // directional tracing only
68                 float spreadlimit;
69                 makevectors(passer_angle);
70
71                 // find the closest point on the enemy to the center of the attack
72                 float ang; // angle between shotdir and h
73                 float h; // hypotenuse, which is the distance between attacker to head
74                 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
75                 
76                 h = vlen(head_center - passer_center);
77                 ang = acos(dotproduct(normalize(head_center - passer_center), v_forward));
78                 a = h * cos(ang);
79
80                 vector nearest_on_line = (passer_center + a * v_forward);
81                 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
82
83                 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
84                 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
85
86                 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
87                         { return TRUE; }
88                 else
89                         { return FALSE; }
90         }
91         else { return TRUE; }
92 }
93
94
95 // =======================
96 // CaptureShield Functions 
97 // =======================
98
99 float ctf_CaptureShield_CheckStatus(entity p) 
100 {
101         float s, se;
102         entity e;
103         float players_worseeq, players_total;
104
105         if(ctf_captureshield_max_ratio <= 0)
106                 return FALSE;
107
108         s = PlayerScore_Add(p, SP_SCORE, 0);
109         if(s >= -ctf_captureshield_min_negscore)
110                 return FALSE;
111
112         players_total = players_worseeq = 0;
113         FOR_EACH_PLAYER(e)
114         {
115                 if(IsDifferentTeam(e, p))
116                         continue;
117                 se = PlayerScore_Add(e, SP_SCORE, 0);
118                 if(se <= s)
119                         ++players_worseeq;
120                 ++players_total;
121         }
122
123         // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
124         // use this rule here
125         
126         if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
127                 return FALSE;
128
129         return TRUE;
130 }
131
132 void ctf_CaptureShield_Update(entity player, float wanted_status)
133 {
134         float updated_status = ctf_CaptureShield_CheckStatus(player);
135         if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
136         {
137                 if(updated_status) // TODO csqc notifier for this // Samual: How?
138                         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);
139                 else
140                         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);
141                         
142                 player.ctf_captureshielded = updated_status;
143         }
144 }
145
146 float ctf_CaptureShield_Customize()
147 {
148         if(!other.ctf_captureshielded) { return FALSE; }
149         if(!IsDifferentTeam(self, other)) { return FALSE; }
150         
151         return TRUE;
152 }
153
154 void ctf_CaptureShield_Touch()
155 {
156         if(!other.ctf_captureshielded) { return; }
157         if(!IsDifferentTeam(self, other)) { return; }
158         
159         vector mymid = (self.absmin + self.absmax) * 0.5;
160         vector othermid = (other.absmin + other.absmax) * 0.5;
161
162         Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
163         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);
164 }
165
166 void ctf_CaptureShield_Spawn(entity flag)
167 {
168         entity shield = spawn();
169         
170         shield.enemy = self;
171         shield.team = self.team;
172         shield.touch = ctf_CaptureShield_Touch;
173         shield.customizeentityforclient = ctf_CaptureShield_Customize;
174         shield.classname = "ctf_captureshield";
175         shield.effects = EF_ADDITIVE;
176         shield.movetype = MOVETYPE_NOCLIP;
177         shield.solid = SOLID_TRIGGER;
178         shield.avelocity = '7 0 11';
179         shield.scale = 0.5;
180         
181         setorigin(shield, self.origin);
182         setmodel(shield, "models/ctf/shield.md3");
183         setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
184 }
185
186
187 // ====================
188 // Drop/Pass/Throw Code
189 // ====================
190
191 void ctf_Handle_Drop(entity flag, entity player, float droptype)
192 {
193         // declarations
194         player = (player ? player : flag.pass_sender);
195
196         // main
197         flag.movetype = MOVETYPE_TOSS;
198         flag.takedamage = DAMAGE_YES;
199         flag.health = flag.max_flag_health;
200         flag.ctf_droptime = time;
201         flag.ctf_dropper = player;
202         flag.ctf_status = FLAG_DROPPED;
203         
204         // messages and sounds
205         Send_KillNotification(player.netname, flag.netname, "", INFO_LOSTFLAG, MSG_INFO);
206         sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTN_NONE);
207         ctf_EventLog("dropped", player.team, player);
208
209         // scoring
210         PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);   
211         PlayerScore_Add(player, SP_CTF_DROPS, 1);
212         
213         // waypoints
214         if(autocvar_g_ctf_flag_dropped_waypoint)
215                 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));
216
217         if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
218         {
219                 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
220                 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
221         }
222         
223         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
224         
225         if(droptype == DROP_PASS)
226         {
227                 flag.pass_sender = world;
228                 flag.pass_target = world;
229         }
230 }
231
232 void ctf_Handle_Retrieve(entity flag, entity player)
233 {
234         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
235         entity sender = flag.pass_sender;
236         
237         // transfer flag to player
238         flag.owner = player;
239         flag.owner.flagcarried = flag;
240         
241         // reset flag
242         setattachment(flag, player, "");
243         setorigin(flag, FLAG_CARRY_OFFSET);
244         flag.movetype = MOVETYPE_NONE;
245         flag.takedamage = DAMAGE_NO;
246         flag.solid = SOLID_NOT;
247         flag.ctf_status = FLAG_CARRY;
248
249         // messages and sounds
250         sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTN_NORM);
251         ctf_EventLog("receive", flag.team, player);
252         
253         FOR_EACH_REALPLAYER(tmp_player)
254         {
255                 if(tmp_player == sender)
256                         centerprint(tmp_player, strcat("You passed the ", flag.netname, " to ", player.netname));
257                 else if(tmp_player == player)
258                         centerprint(tmp_player, strcat("You received the ", flag.netname, " from ", sender.netname));
259                 else if(!IsDifferentTeam(tmp_player, sender))
260                         centerprint(tmp_player, strcat(sender.netname, " passed the ", flag.netname, " to ", player.netname));
261         }
262         
263         // create new waypoint
264         ctf_FlagcarrierWaypoints(player);
265         
266         sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
267         player.throw_antispam = sender.throw_antispam;
268
269         flag.pass_sender = world;
270         flag.pass_target = world;
271 }
272
273 void ctf_Handle_Throw(entity player, entity receiver, float droptype)
274 {
275         entity flag = player.flagcarried;
276         vector targ_origin, flag_velocity;
277         
278         if(!flag) { return; }
279         if((droptype == DROP_PASS) && !receiver) { return; }
280         
281         if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
282         
283         // reset the flag
284         setattachment(flag, world, "");
285         setorigin(flag, player.origin + FLAG_DROP_OFFSET);
286         flag.owner.flagcarried = world;
287         flag.owner = world;
288         flag.solid = SOLID_TRIGGER;
289         flag.ctf_dropper = player;
290         flag.ctf_droptime = time;
291         
292         flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
293         
294         switch(droptype)
295         {
296                 case DROP_PASS:
297                 {
298                         // warpzone support:
299                         // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
300                         // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
301                         WarpZone_RefSys_Copy(flag, receiver);
302                         WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
303                         targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
304                         flag.velocity = (normalize(targ_origin - player.origin) * autocvar_g_ctf_pass_velocity);
305
306                         // main
307                         flag.movetype = MOVETYPE_FLY;
308                         flag.takedamage = DAMAGE_NO;
309                         flag.pass_sender = player;
310                         flag.pass_target = receiver;
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), player.origin, targ_origin);
316                         ctf_EventLog("pass", flag.team, player);
317                         break;
318                 }
319                 
320                 case DROP_THROW:
321                 {
322                         makevectors((player.v_angle_y * '0 1 0') + (bound(autocvar_g_ctf_throw_angle_min, player.v_angle_x, autocvar_g_ctf_throw_angle_max) * '1 0 0'));
323                                 
324                         flag_velocity = (('0 0 1' * autocvar_g_ctf_throw_velocity_up) + ((v_forward * autocvar_g_ctf_throw_velocity_forward) * ((player.items & IT_STRENGTH) ? autocvar_g_ctf_throw_strengthmultiplier : 1)));
325                         flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, FALSE);
326                         ctf_Handle_Drop(flag, player, droptype);
327                         break;
328                 }
329                 
330                 case DROP_RESET:
331                 {
332                         flag.velocity = '0 0 0'; // do nothing
333                         break;
334                 }
335                 
336                 default:
337                 case DROP_NORMAL:
338                 {
339                         flag.velocity = W_CalculateProjectileVelocity(player.velocity, (('0 0 1' * autocvar_g_ctf_drop_velocity_up) + ((('0 1 0' * crandom()) + ('1 0 0' * crandom())) * autocvar_g_ctf_drop_velocity_side)), FALSE);
340                         ctf_Handle_Drop(flag, player, droptype);
341                         break;
342                 }
343         }
344
345         // kill old waypointsprite
346         WaypointSprite_Ping(player.wps_flagcarrier);
347         WaypointSprite_Kill(player.wps_flagcarrier);
348         
349         if(player.wps_enemyflagcarrier)
350                 WaypointSprite_Kill(player.wps_enemyflagcarrier);
351         
352         // captureshield
353         ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
354 }
355
356
357 // ==============
358 // Event Handlers
359 // ==============
360
361 void ctf_Handle_Capture(entity flag, entity toucher, float capturetype)
362 {
363         entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
364         entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
365         float old_time, new_time; 
366         
367         if not(player) { return; } // without someone to give the reward to, we can't possibly cap
368         
369         // messages and sounds
370         Send_KillNotification(player.netname, enemy_flag.netname, ctf_CaptureRecord(enemy_flag, player), INFO_CAPTUREFLAG, MSG_INFO);
371         sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE);
372         
373         switch(capturetype)
374         {
375                 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
376                 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
377                 default: break;
378         }
379         
380         // scoring
381         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
382         PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
383
384         old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
385         new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
386         if(!old_time || new_time < old_time)
387                 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
388
389         // effects
390         pointparticles(particleeffectnum(flag.capeffect), flag.origin, '0 0 0', 1);
391         //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
392
393         // other
394         if(capturetype == CAPTURE_NORMAL)
395         {
396                 WaypointSprite_Kill(player.wps_flagcarrier);
397                 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
398                 
399                 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
400                         { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
401         }
402         
403         // reset the flag
404         player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
405         ctf_RespawnFlag(enemy_flag);
406 }
407
408 void ctf_Handle_Return(entity flag, entity player)
409 {
410         // messages and sounds
411         //centerprint(player, strcat("You returned the ", flag.netname));
412         Send_KillNotification(player.netname, flag.netname, "", INFO_RETURNFLAG, MSG_INFO);
413         sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTN_NONE);
414         ctf_EventLog("return", flag.team, player);
415
416         // scoring
417         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
418         PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
419
420         TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
421         
422         if(flag.ctf_dropper) 
423         {
424                 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
425                 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag 
426                 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
427         }
428         
429         // reset the flag
430         ctf_RespawnFlag(flag);
431 }
432
433 void ctf_Handle_Pickup(entity flag, entity player, float pickuptype)
434 {
435         // declarations
436         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
437         string verbosename; // holds the name of the player OR no name at all for printing in the centerprints
438         float pickup_dropped_score; // used to calculate dropped pickup score
439         
440         // attach the flag to the player
441         flag.owner = player;
442         player.flagcarried = flag;
443         setattachment(flag, player, "");
444         setorigin(flag, FLAG_CARRY_OFFSET);
445         
446         // flag setup
447         flag.movetype = MOVETYPE_NONE;
448         flag.takedamage = DAMAGE_NO;
449         flag.solid = SOLID_NOT;
450         flag.angles = '0 0 0';
451         flag.ctf_status = FLAG_CARRY;
452         
453         switch(pickuptype)
454         {
455                 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
456                 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
457                 default: break;
458         }
459
460         // messages and sounds
461         Send_KillNotification (player.netname, flag.netname, "", INFO_GOTFLAG, MSG_INFO);
462         sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE);
463         verbosename = ((autocvar_g_ctf_flag_pickup_verbosename) ? strcat(Team_ColorCode(player.team), "(^7", player.netname, Team_ColorCode(player.team), ") ") : "");
464         
465         FOR_EACH_REALPLAYER(tmp_player)
466         {
467                 if(tmp_player == player)
468                         centerprint(tmp_player, strcat("You got the ", flag.netname, "!"));
469                 else if(!IsDifferentTeam(tmp_player, player))
470                         centerprint(tmp_player, strcat("Your ", Team_ColorCode(player.team), "team mate ", verbosename, "^7got the flag! Protect them!"));
471                 else if(!IsDifferentTeam(tmp_player, flag))
472                         centerprint(tmp_player, strcat("The ", Team_ColorCode(player.team), "enemy ", verbosename, "^7got your flag! Retrieve it!"));
473         }
474         
475         // scoring
476         PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
477         switch(pickuptype)
478         {               
479                 case PICKUP_BASE:
480                 {
481                         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
482                         ctf_EventLog("steal", flag.team, player);
483                         break;
484                 }
485                 
486                 case PICKUP_DROPPED:
487                 {
488                         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);
489                         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);
490                         dprint("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
491                         PlayerTeamScore_AddScore(player, pickup_dropped_score);
492                         ctf_EventLog("pickup", flag.team, player);
493                         break;
494                 }
495                 
496                 default: break;
497         }
498         
499         // speedrunning
500         if(pickuptype == PICKUP_BASE)
501         {
502                 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
503                 if((player.speedrunning) && (ctf_captimerecord))
504                         ctf_FakeTimeLimit(player, time + ctf_captimerecord);
505         }
506                 
507         // effects
508         pointparticles(particleeffectnum(flag.toucheffect), player.origin, '0 0 0', 1);
509         
510         // waypoints 
511         if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
512         ctf_FlagcarrierWaypoints(player);
513         WaypointSprite_Ping(player.wps_flagcarrier);
514 }
515
516
517 // ===================
518 // Main Flag Functions
519 // ===================
520
521 void ctf_CheckFlagReturn(entity flag, float returntype)
522 {
523         if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
524         {
525                 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
526                 
527                 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
528                 {
529                         switch(returntype)
530                         {
531                                 case RETURN_DROPPED: bprint("The ", flag.netname, " was dropped in the base and returned itself\n"); break;
532                                 case RETURN_DAMAGE: bprint("The ", flag.netname, " was destroyed and returned to base\n"); break;
533                                 case RETURN_SPEEDRUN: bprint("The ", flag.netname, " became impatient after ", ftos_decimals(ctf_captimerecord, 2), " seconds and returned itself\n"); break;
534                                 case RETURN_NEEDKILL: bprint("The ", flag.netname, " fell somewhere it couldn't be reached and returned to base\n"); break;
535                                 
536                                 default:
537                                 case RETURN_TIMEOUT:
538                                         { bprint("The ", flag.netname, " has returned to base\n"); break; }
539                         }
540                         sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTN_NONE);
541                         ctf_EventLog("returned", flag.team, world);
542                         ctf_RespawnFlag(flag);
543                 }
544         }
545 }
546
547 void ctf_CheckStalemate(void)
548 {
549         // declarations
550         float stale_red_flags, stale_blue_flags;
551         entity tmp_entity;
552
553         entity ctf_staleflaglist; // reset the list, we need to build the list each time this function runs
554
555         // build list of stale flags
556         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
557         {
558                 if(autocvar_g_ctf_flagcarrier_waypointforenemy_stalemate)
559                 if(tmp_entity.ctf_status != FLAG_BASE)
560                 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_flagcarrier_waypointforenemy_stalemate)
561                 {
562                         tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
563                         ctf_staleflaglist = tmp_entity;
564                         
565                         switch(tmp_entity.team)
566                         {
567                                 case COLOR_TEAM1: ++stale_red_flags; break;
568                                 case COLOR_TEAM2: ++stale_blue_flags; break;
569                         }
570                 }
571         }
572
573         if(stale_red_flags && stale_blue_flags)
574                 ctf_stalemate = TRUE;
575         else if(!stale_red_flags && !stale_blue_flags)
576                 ctf_stalemate = FALSE;
577         
578         // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
579         if(ctf_stalemate)
580         {
581                 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
582                 {
583                         if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
584                                 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));
585                 }
586                 
587                 if not(wpforenemy_announced)
588                 {
589                         FOR_EACH_REALPLAYER(tmp_entity)
590                                 if(tmp_entity.flagcarried)
591                                         centerprint(tmp_entity, "Stalemate! Enemies can now see you on radar!");
592                                 else
593                                         centerprint(tmp_entity, "Stalemate! Flag carriers can now be seen by enemies on radar!");
594                         
595                         wpforenemy_announced = TRUE;
596                 }
597         }
598 }
599
600 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
601 {
602         if(ITEM_DAMAGE_NEEDKILL(deathtype))
603         {
604                 // automatically kill the flag and return it
605                 self.health = 0;
606                 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
607                 return;
608         }
609         if(autocvar_g_ctf_flag_return_damage) 
610         {
611                 // reduce health and check if it should be returned
612                 self.health = self.health - damage;
613                 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
614                 return;
615         }
616 }
617
618 void ctf_FlagThink()
619 {
620         // declarations
621         entity tmp_entity;
622
623         self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
624
625         // captureshield
626         if(self == ctf_worldflaglist) // only for the first flag
627                 FOR_EACH_CLIENT(tmp_entity)
628                         ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
629
630         // sanity checks
631         if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
632                 dprint("wtf the flag got squashed?\n");
633                 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
634                 if(!trace_startsolid) // can we resize it without getting stuck?
635                         setsize(self, FLAG_MIN, FLAG_MAX); }
636                         
637         switch(self.ctf_status) // reset flag angles in case warpzones adjust it
638         {
639                 case FLAG_DROPPED:
640                 case FLAG_PASSING:
641                 {
642                         self.angles = '0 0 0';
643                         break;
644                 }
645                 
646                 default: break;
647         }
648
649         // main think method
650         switch(self.ctf_status)
651         {       
652                 case FLAG_BASE:
653                 {
654                         if(autocvar_g_ctf_dropped_capture_radius)
655                         {
656                                 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
657                                         if(tmp_entity.ctf_status == FLAG_DROPPED)
658                                                 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
659                                                         ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
660                         }
661                         return;
662                 }
663                 
664                 case FLAG_DROPPED:
665                 {
666                         if(autocvar_g_ctf_flag_dropped_floatinwater)
667                         {
668                                 vector midpoint = ((self.absmin + self.absmax) * 0.5);
669                                 if(pointcontents(midpoint) == CONTENT_WATER)
670                                 {
671                                         self.velocity = self.velocity * 0.5;
672                                         
673                                         if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
674                                                 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
675                                         else
676                                                 { self.movetype = MOVETYPE_FLY; }
677                                 }
678                                 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
679                         }
680                         if(autocvar_g_ctf_flag_return_dropped)
681                         {
682                                 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
683                                 {
684                                         self.health = 0;
685                                         ctf_CheckFlagReturn(self, RETURN_DROPPED);
686                                         return;
687                                 }
688                         }
689                         if(autocvar_g_ctf_flag_return_time)
690                         {
691                                 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
692                                 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
693                                 return;
694                         } 
695                         return;
696                 }
697                         
698                 case FLAG_CARRY:
699                 {
700                         if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord)) 
701                         {
702                                 self.health = 0;
703                                 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
704
705                                 tmp_entity = self;
706                                 self = self.owner;
707                                 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
708                                 ImpulseCommands();
709                                 self = tmp_entity;
710                         }
711                         if(autocvar_g_ctf_flagcarrier_waypointforenemy_stalemate)
712                         {
713                                 if(time >= wpforenemy_nextthink)
714                                 {
715                                         ctf_CheckStalemate();
716                                         wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
717                                 }
718                         }
719                         return;
720                 }
721                 
722                 case FLAG_PASSING:
723                 {
724                         vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
725                         targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
726                         WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
727                         
728                         if((self.pass_target == world)
729                                 || (self.pass_target.deadflag != DEAD_NO)
730                                 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
731                                 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
732                                 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
733                         {
734                                 ctf_Handle_Drop(self, world, DROP_PASS);
735                         }
736                         else // still a viable target, go for it
737                         {
738                                 vector desired_direction = normalize(targ_origin - self.origin);
739                                 vector current_direction = normalize(self.velocity);
740                                 self.velocity = (normalize(current_direction + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); 
741                         }
742                         return;
743                 }
744
745                 default: // this should never happen
746                 {
747                         dprint("ctf_FlagThink(): Flag exists with no status?\n");
748                         return;
749                 }
750         }
751 }
752
753 void ctf_FlagTouch()
754 {
755         if(gameover) { return; }
756         
757         entity toucher = other;
758         
759         // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
760         if(ITEM_TOUCH_NEEDKILL())
761         {
762                 self.health = 0;
763                 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
764                 return;
765         }
766         
767         // special touch behaviors
768         if(toucher.vehicle_flags & VHF_ISVEHICLE)
769         {
770                 if(autocvar_g_ctf_allow_vehicle_touch)
771                         toucher = toucher.owner; // the player is actually the vehicle owner, not other
772                 else
773                         return; // do nothing
774         }
775         else if(toucher.classname != "player") // The flag just touched an object, most likely the world
776         {
777                 if(time > self.wait) // if we haven't in a while, play a sound/effect
778                 {
779                         pointparticles(particleeffectnum(self.toucheffect), self.origin, '0 0 0', 1);
780                         sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTN_NORM);
781                         self.wait = time + FLAG_TOUCHRATE;
782                 }
783                 return;
784         }
785         else if(toucher.deadflag != DEAD_NO) { return; }
786
787         switch(self.ctf_status) 
788         {       
789                 case FLAG_BASE:
790                 {
791                         if(!IsDifferentTeam(toucher, self) && (toucher.flagcarried) && IsDifferentTeam(toucher.flagcarried, self))
792                                 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
793                         else if(IsDifferentTeam(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time))
794                                 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
795                         break;
796                 }
797                 
798                 case FLAG_DROPPED:
799                 {
800                         if(!IsDifferentTeam(toucher, self))
801                                 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
802                         else if((!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
803                                 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
804                         break;
805                 }
806                         
807                 case FLAG_CARRY:
808                 {
809                         dprint("Someone touched a flag even though it was being carried?\n");
810                         break;
811                 }
812                 
813                 case FLAG_PASSING:
814                 {
815                         if((toucher.classname == "player") && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
816                         {
817                                 if(IsDifferentTeam(toucher, self.pass_sender))
818                                         ctf_Handle_Return(self, toucher);
819                                 else
820                                         ctf_Handle_Retrieve(self, toucher);
821                         }
822                         break;
823                 }
824         }
825 }
826
827 .float last_respawn;
828 void ctf_RespawnFlag(entity flag)
829 {
830         // check for flag respawn being called twice in a row
831         if(flag.last_respawn > time - 0.5)
832                 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
833
834         flag.last_respawn = time;
835         
836         // reset the player (if there is one)
837         if((flag.owner) && (flag.owner.flagcarried == flag))
838         {
839                 if(flag.owner.wps_enemyflagcarrier)
840                         WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
841                         
842                 WaypointSprite_Kill(flag.wps_flagcarrier);
843                 
844                 flag.owner.flagcarried = world;
845
846                 if(flag.speedrunning)
847                         ctf_FakeTimeLimit(flag.owner, -1);
848         }
849
850         if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
851                 { WaypointSprite_Kill(flag.wps_flagdropped); }
852
853         // reset the flag
854         setattachment(flag, world, "");
855         setorigin(flag, flag.ctf_spawnorigin);
856         
857         flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
858         flag.takedamage = DAMAGE_NO;
859         flag.health = flag.max_flag_health;
860         flag.solid = SOLID_TRIGGER;
861         flag.velocity = '0 0 0';
862         flag.angles = flag.mangle;
863         flag.flags = FL_ITEM | FL_NOTARGET;
864         
865         flag.ctf_status = FLAG_BASE;
866         flag.owner = world;
867         flag.pass_sender = world;
868         flag.pass_target = world;
869         flag.ctf_dropper = world;
870         flag.ctf_pickuptime = 0;
871         flag.ctf_droptime = 0;
872
873         wpforenemy_announced = FALSE;
874 }
875
876 void ctf_Reset()
877 {
878         if(self.owner)
879                 if(self.owner.classname == "player")
880                         ctf_Handle_Throw(self.owner, world, DROP_RESET);
881                         
882         ctf_RespawnFlag(self);
883 }
884
885 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
886 {
887         // bot waypoints
888         waypoint_spawnforitem_force(self, self.origin);
889         self.nearestwaypointtimeout = 0; // activate waypointing again
890         self.bot_basewaypoint = self.nearestwaypoint;
891
892         // waypointsprites
893         WaypointSprite_SpawnFixed(((self.team == COLOR_TEAM1) ? "redbase" : "bluebase"), self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
894         WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
895
896         // captureshield setup
897         ctf_CaptureShield_Spawn(self);
898 }
899
900 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc 
901 {       
902         // declarations
903         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. 
904         self = flag; // for later usage with droptofloor()
905         
906         // main setup
907         flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
908         ctf_worldflaglist = flag;
909
910         setattachment(flag, world, ""); 
911
912         flag.netname = ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag");
913         flag.team = ((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2); // COLOR_TEAM1: color 4 team (red) - COLOR_TEAM2: color 13 team (blue)
914         flag.items = ((teamnumber) ? IT_KEY2 : IT_KEY1); // IT_KEY2: gold key (redish enough) - IT_KEY1: silver key (bluish enough)
915         flag.classname = "item_flag_team";
916         flag.target = "###item###"; // wut?
917         flag.flags = FL_ITEM | FL_NOTARGET;
918         flag.solid = SOLID_TRIGGER;
919         flag.takedamage = DAMAGE_NO;
920         flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;   
921         flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
922         flag.health = flag.max_flag_health;
923         flag.event_damage = ctf_FlagDamage;
924         flag.pushable = TRUE;
925         flag.teleportable = TELEPORT_NORMAL;
926         flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
927         flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
928         flag.velocity = '0 0 0';
929         flag.mangle = flag.angles;
930         flag.reset = ctf_Reset;
931         flag.touch = ctf_FlagTouch;
932         flag.think = ctf_FlagThink;
933         flag.nextthink = time + FLAG_THINKRATE;
934         flag.ctf_status = FLAG_BASE;
935         
936         if(!flag.model) { flag.model = ((teamnumber) ? autocvar_g_ctf_flag_red_model : autocvar_g_ctf_flag_blue_model); }
937         if(!flag.scale) { flag.scale = FLAG_SCALE; }
938         if(!flag.skin) { flag.skin = ((teamnumber) ? autocvar_g_ctf_flag_red_skin : autocvar_g_ctf_flag_blue_skin); }
939         if(!flag.toucheffect) { flag.toucheffect = ((teamnumber) ? "redflag_touch" : "blueflag_touch"); }
940         if(!flag.passeffect) { flag.passeffect = ((teamnumber) ? "red_pass" : "blue_pass"); }
941         if(!flag.capeffect) { flag.capeffect = ((teamnumber) ? "red_cap" : "blue_cap"); }
942         
943         // sound 
944         if(!flag.snd_flag_taken) { flag.snd_flag_taken  = ((teamnumber) ? "ctf/red_taken.wav" : "ctf/blue_taken.wav"); }
945         if(!flag.snd_flag_returned) { flag.snd_flag_returned = ((teamnumber) ? "ctf/red_returned.wav" : "ctf/blue_returned.wav"); }
946         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
947         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.
948         if(!flag.snd_flag_dropped) { flag.snd_flag_dropped = ((teamnumber) ? "ctf/red_dropped.wav" : "ctf/blue_dropped.wav"); }
949         if(!flag.snd_flag_touch) { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
950         if(!flag.snd_flag_pass) { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
951         
952         // precache
953         precache_sound(flag.snd_flag_taken);
954         precache_sound(flag.snd_flag_returned);
955         precache_sound(flag.snd_flag_capture);
956         precache_sound(flag.snd_flag_respawn);
957         precache_sound(flag.snd_flag_dropped);
958         precache_sound(flag.snd_flag_touch);
959         precache_sound(flag.snd_flag_pass);
960         precache_model(flag.model);
961         precache_model("models/ctf/shield.md3");
962         precache_model("models/ctf/shockwavetransring.md3");
963
964         // appearence
965         setmodel(flag, flag.model); // precision set below
966         setsize(flag, FLAG_MIN, FLAG_MAX);
967         setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
968         
969         if(autocvar_g_ctf_flag_glowtrails)
970         {
971                 flag.glow_color = ((teamnumber) ? 251 : 210); // 251: red - 210: blue
972                 flag.glow_size = 25;
973                 flag.glow_trail = 1;
974         }
975         
976         flag.effects |= EF_LOWPRECISION;
977         if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
978         if(autocvar_g_ctf_dynamiclights)   { flag.effects |= ((teamnumber) ? EF_RED : EF_BLUE); }
979         
980         // flag placement
981         if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
982         {       
983                 flag.dropped_origin = flag.origin; 
984                 flag.noalign = TRUE;
985                 flag.movetype = MOVETYPE_NONE;
986         }
987         else // drop to floor, automatically find a platform and set that as spawn origin
988         { 
989                 flag.noalign = FALSE;
990                 self = flag;
991                 droptofloor();
992                 flag.movetype = MOVETYPE_TOSS; 
993         }       
994         
995         InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
996 }
997
998
999 // ================
1000 // Bot player logic
1001 // ================
1002
1003 // NOTE: LEGACY CODE, needs to be re-written!
1004
1005 void havocbot_calculate_middlepoint()
1006 {
1007         entity f;
1008         vector s = '0 0 0';
1009         vector fo = '0 0 0';
1010         float n = 0;
1011
1012         f = ctf_worldflaglist;
1013         while (f)
1014         {
1015                 fo = f.origin;
1016                 s = s + fo;
1017                 f = f.ctf_worldflagnext;
1018         }
1019         if(!n)
1020                 return;
1021         havocbot_ctf_middlepoint = s * (1.0 / n);
1022         havocbot_ctf_middlepoint_radius  = vlen(fo - havocbot_ctf_middlepoint);
1023 }
1024
1025
1026 entity havocbot_ctf_find_flag(entity bot)
1027 {
1028         entity f;
1029         f = ctf_worldflaglist;
1030         while (f)
1031         {
1032                 if (bot.team == f.team)
1033                         return f;
1034                 f = f.ctf_worldflagnext;
1035         }
1036         return world;
1037 }
1038
1039 entity havocbot_ctf_find_enemy_flag(entity bot)
1040 {
1041         entity f;
1042         f = ctf_worldflaglist;
1043         while (f)
1044         {
1045                 if (bot.team != f.team)
1046                         return f;
1047                 f = f.ctf_worldflagnext;
1048         }
1049         return world;
1050 }
1051
1052 float havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1053 {
1054         if not(teamplay)
1055                 return 0;
1056
1057         float c = 0;
1058         entity head;
1059
1060         FOR_EACH_PLAYER(head)
1061         {
1062                 if(head.team!=bot.team || head.deadflag != DEAD_NO || head == bot)
1063                         continue;
1064
1065                 if(vlen(head.origin - org) < tc_radius)
1066                         ++c;
1067         }
1068
1069         return c;
1070 }
1071
1072 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1073 {
1074         entity head;
1075         head = ctf_worldflaglist;
1076         while (head)
1077         {
1078                 if (self.team == head.team)
1079                         break;
1080                 head = head.ctf_worldflagnext;
1081         }
1082         if (head)
1083                 navigation_routerating(head, ratingscale, 10000);
1084 }
1085
1086 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1087 {
1088         entity head;
1089         head = ctf_worldflaglist;
1090         while (head)
1091         {
1092                 if (self.team == head.team)
1093                         break;
1094                 head = head.ctf_worldflagnext;
1095         }
1096         if not(head)
1097                 return;
1098
1099         navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1100 }
1101
1102 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1103 {
1104         entity head;
1105         head = ctf_worldflaglist;
1106         while (head)
1107         {
1108                 if (self.team != head.team)
1109                         break;
1110                 head = head.ctf_worldflagnext;
1111         }
1112         if (head)
1113                 navigation_routerating(head, ratingscale, 10000);
1114 }
1115
1116 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1117 {
1118         if not(bot_waypoints_for_items)
1119         {
1120                 havocbot_goalrating_ctf_enemyflag(ratingscale);
1121                 return;
1122         }
1123
1124         entity head;
1125
1126         head = havocbot_ctf_find_enemy_flag(self);
1127
1128         if not(head)
1129                 return;
1130
1131         navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1132 }
1133
1134 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1135 {
1136         entity mf;
1137
1138         mf = havocbot_ctf_find_flag(self);
1139
1140         if(mf.ctf_status == FLAG_BASE)
1141                 return;
1142
1143         if(mf.tag_entity)
1144                 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1145 }
1146
1147 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1148 {
1149         entity head;
1150         head = ctf_worldflaglist;
1151         while (head)
1152         {
1153                 // flag is out in the field
1154                 if(head.ctf_status != FLAG_BASE)
1155                 if(head.tag_entity==world)      // dropped
1156                 {
1157                         if(df_radius)
1158                         {
1159                                 if(vlen(org-head.origin)<df_radius)
1160                                         navigation_routerating(head, ratingscale, 10000);
1161                         }
1162                         else
1163                                 navigation_routerating(head, ratingscale, 10000);
1164                 }
1165
1166                 head = head.ctf_worldflagnext;
1167         }
1168 }
1169
1170 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1171 {
1172         entity head;
1173         float t;
1174         head = findchainfloat(bot_pickup, TRUE);
1175         while (head)
1176         {
1177                 // gather health and armor only
1178                 if (head.solid)
1179                 if (head.health || head.armorvalue)
1180                 if (vlen(head.origin - org) < sradius)
1181                 {
1182                         // get the value of the item
1183                         t = head.bot_pickupevalfunc(self, head) * 0.0001;
1184                         if (t > 0)
1185                                 navigation_routerating(head, t * ratingscale, 500);
1186                 }
1187                 head = head.chain;
1188         }
1189 }
1190
1191 void havocbot_ctf_reset_role(entity bot)
1192 {
1193         float cdefense, cmiddle, coffense;
1194         entity mf, ef, head;
1195         float c;
1196
1197         if(bot.deadflag != DEAD_NO)
1198                 return;
1199
1200         if(vlen(havocbot_ctf_middlepoint)==0)
1201                 havocbot_calculate_middlepoint();
1202
1203         // Check ctf flags
1204         if (bot.flagcarried)
1205         {
1206                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1207                 return;
1208         }
1209
1210         mf = havocbot_ctf_find_flag(bot);
1211         ef = havocbot_ctf_find_enemy_flag(bot);
1212
1213         // Retrieve stolen flag
1214         if(mf.ctf_status!=FLAG_BASE)
1215         {
1216                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1217                 return;
1218         }
1219
1220         // If enemy flag is taken go to the middle to intercept pursuers
1221         if(ef.ctf_status!=FLAG_BASE)
1222         {
1223                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1224                 return;
1225         }
1226
1227         // if there is only me on the team switch to offense
1228         c = 0;
1229         FOR_EACH_PLAYER(head)
1230         if(head.team==bot.team)
1231                 ++c;
1232
1233         if(c==1)
1234         {
1235                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1236                 return;
1237         }
1238
1239         // Evaluate best position to take
1240         // Count mates on middle position
1241         cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1242
1243         // Count mates on defense position
1244         cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1245
1246         // Count mates on offense position
1247         coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1248
1249         if(cdefense<=coffense)
1250                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1251         else if(coffense<=cmiddle)
1252                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1253         else
1254                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1255 }
1256
1257 void havocbot_role_ctf_carrier()
1258 {
1259         if(self.deadflag != DEAD_NO)
1260         {
1261                 havocbot_ctf_reset_role(self);
1262                 return;
1263         }
1264
1265         if (self.flagcarried == world)
1266         {
1267                 havocbot_ctf_reset_role(self);
1268                 return;
1269         }
1270
1271         if (self.bot_strategytime < time)
1272         {
1273                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1274
1275                 navigation_goalrating_start();
1276                 havocbot_goalrating_ctf_ourbase(50000);
1277
1278                 if(self.health<100)
1279                         havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1280
1281                 navigation_goalrating_end();
1282
1283                 if (self.navigation_hasgoals)
1284                         self.havocbot_cantfindflag = time + 10;
1285                 else if (time > self.havocbot_cantfindflag)
1286                 {
1287                         // Can't navigate to my own base, suicide!
1288                         // TODO: drop it and wander around
1289                         Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1290                         return;
1291                 }
1292         }
1293 }
1294
1295 void havocbot_role_ctf_escort()
1296 {
1297         entity mf, ef;
1298
1299         if(self.deadflag != DEAD_NO)
1300         {
1301                 havocbot_ctf_reset_role(self);
1302                 return;
1303         }
1304
1305         if (self.flagcarried)
1306         {
1307                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1308                 return;
1309         }
1310
1311         // If enemy flag is back on the base switch to previous role
1312         ef = havocbot_ctf_find_enemy_flag(self);
1313         if(ef.ctf_status==FLAG_BASE)
1314         {
1315                 self.havocbot_role = self.havocbot_previous_role;
1316                 self.havocbot_role_timeout = 0;
1317                 return;
1318         }
1319
1320         // If the flag carrier reached the base switch to defense
1321         mf = havocbot_ctf_find_flag(self);
1322         if(mf.ctf_status!=FLAG_BASE)
1323         if(vlen(ef.origin - mf.dropped_origin) < 300)
1324         {
1325                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1326                 return;
1327         }
1328
1329         // Set the role timeout if necessary
1330         if (!self.havocbot_role_timeout)
1331         {
1332                 self.havocbot_role_timeout = time + random() * 30 + 60;
1333         }
1334
1335         // If nothing happened just switch to previous role
1336         if (time > self.havocbot_role_timeout)
1337         {
1338                 self.havocbot_role = self.havocbot_previous_role;
1339                 self.havocbot_role_timeout = 0;
1340                 return;
1341         }
1342
1343         // Chase the flag carrier
1344         if (self.bot_strategytime < time)
1345         {
1346                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1347                 navigation_goalrating_start();
1348                 havocbot_goalrating_ctf_enemyflag(30000);
1349                 havocbot_goalrating_ctf_ourstolenflag(40000);
1350                 havocbot_goalrating_items(10000, self.origin, 10000);
1351                 navigation_goalrating_end();
1352         }
1353 }
1354
1355 void havocbot_role_ctf_offense()
1356 {
1357         entity mf, ef;
1358         vector pos;
1359
1360         if(self.deadflag != DEAD_NO)
1361         {
1362                 havocbot_ctf_reset_role(self);
1363                 return;
1364         }
1365
1366         if (self.flagcarried)
1367         {
1368                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1369                 return;
1370         }
1371
1372         // Check flags
1373         mf = havocbot_ctf_find_flag(self);
1374         ef = havocbot_ctf_find_enemy_flag(self);
1375
1376         // Own flag stolen
1377         if(mf.ctf_status!=FLAG_BASE)
1378         {
1379                 if(mf.tag_entity)
1380                         pos = mf.tag_entity.origin;
1381                 else
1382                         pos = mf.origin;
1383
1384                 // Try to get it if closer than the enemy base
1385                 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1386                 {
1387                         havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1388                         return;
1389                 }
1390         }
1391
1392         // Escort flag carrier
1393         if(ef.ctf_status!=FLAG_BASE)
1394         {
1395                 if(ef.tag_entity)
1396                         pos = ef.tag_entity.origin;
1397                 else
1398                         pos = ef.origin;
1399
1400                 if(vlen(pos-mf.dropped_origin)>700)
1401                 {
1402                         havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1403                         return;
1404                 }
1405         }
1406
1407         // About to fail, switch to middlefield
1408         if(self.health<50)
1409         {
1410                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1411                 return;
1412         }
1413
1414         // Set the role timeout if necessary
1415         if (!self.havocbot_role_timeout)
1416                 self.havocbot_role_timeout = time + 120;
1417
1418         if (time > self.havocbot_role_timeout)
1419         {
1420                 havocbot_ctf_reset_role(self);
1421                 return;
1422         }
1423
1424         if (self.bot_strategytime < time)
1425         {
1426                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1427                 navigation_goalrating_start();
1428                 havocbot_goalrating_ctf_ourstolenflag(50000);
1429                 havocbot_goalrating_ctf_enemybase(20000);
1430                 havocbot_goalrating_items(5000, self.origin, 1000);
1431                 havocbot_goalrating_items(1000, self.origin, 10000);
1432                 navigation_goalrating_end();
1433         }
1434 }
1435
1436 // Retriever (temporary role):
1437 void havocbot_role_ctf_retriever()
1438 {
1439         entity mf;
1440
1441         if(self.deadflag != DEAD_NO)
1442         {
1443                 havocbot_ctf_reset_role(self);
1444                 return;
1445         }
1446
1447         if (self.flagcarried)
1448         {
1449                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1450                 return;
1451         }
1452
1453         // If flag is back on the base switch to previous role
1454         mf = havocbot_ctf_find_flag(self);
1455         if(mf.ctf_status==FLAG_BASE)
1456         {
1457                 havocbot_ctf_reset_role(self);
1458                 return;
1459         }
1460
1461         if (!self.havocbot_role_timeout)
1462                 self.havocbot_role_timeout = time + 20;
1463
1464         if (time > self.havocbot_role_timeout)
1465         {
1466                 havocbot_ctf_reset_role(self);
1467                 return;
1468         }
1469
1470         if (self.bot_strategytime < time)
1471         {
1472                 float rt_radius;
1473                 rt_radius = 10000;
1474
1475                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1476                 navigation_goalrating_start();
1477                 havocbot_goalrating_ctf_ourstolenflag(50000);
1478                 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1479                 havocbot_goalrating_ctf_enemybase(30000);
1480                 havocbot_goalrating_items(500, self.origin, rt_radius);
1481                 navigation_goalrating_end();
1482         }
1483 }
1484
1485 void havocbot_role_ctf_middle()
1486 {
1487         entity mf;
1488
1489         if(self.deadflag != DEAD_NO)
1490         {
1491                 havocbot_ctf_reset_role(self);
1492                 return;
1493         }
1494
1495         if (self.flagcarried)
1496         {
1497                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1498                 return;
1499         }
1500
1501         mf = havocbot_ctf_find_flag(self);
1502         if(mf.ctf_status!=FLAG_BASE)
1503         {
1504                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1505                 return;
1506         }
1507
1508         if (!self.havocbot_role_timeout)
1509                 self.havocbot_role_timeout = time + 10;
1510
1511         if (time > self.havocbot_role_timeout)
1512         {
1513                 havocbot_ctf_reset_role(self);
1514                 return;
1515         }
1516
1517         if (self.bot_strategytime < time)
1518         {
1519                 vector org;
1520
1521                 org = havocbot_ctf_middlepoint;
1522                 org_z = self.origin_z;
1523
1524                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1525                 navigation_goalrating_start();
1526                 havocbot_goalrating_ctf_ourstolenflag(50000);
1527                 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1528                 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1529                 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1530                 havocbot_goalrating_items(2500, self.origin, 10000);
1531                 havocbot_goalrating_ctf_enemybase(2500);
1532                 navigation_goalrating_end();
1533         }
1534 }
1535
1536 void havocbot_role_ctf_defense()
1537 {
1538         entity mf;
1539
1540         if(self.deadflag != DEAD_NO)
1541         {
1542                 havocbot_ctf_reset_role(self);
1543                 return;
1544         }
1545
1546         if (self.flagcarried)
1547         {
1548                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1549                 return;
1550         }
1551
1552         // If own flag was captured
1553         mf = havocbot_ctf_find_flag(self);
1554         if(mf.ctf_status!=FLAG_BASE)
1555         {
1556                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1557                 return;
1558         }
1559
1560         if (!self.havocbot_role_timeout)
1561                 self.havocbot_role_timeout = time + 30;
1562
1563         if (time > self.havocbot_role_timeout)
1564         {
1565                 havocbot_ctf_reset_role(self);
1566                 return;
1567         }
1568         if (self.bot_strategytime < time)
1569         {
1570                 float mp_radius;
1571                 vector org;
1572
1573                 org = mf.dropped_origin;
1574                 mp_radius = havocbot_ctf_middlepoint_radius;
1575
1576                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1577                 navigation_goalrating_start();
1578
1579                 // if enemies are closer to our base, go there
1580                 entity head, closestplayer = world;
1581                 float distance, bestdistance = 10000;
1582                 FOR_EACH_PLAYER(head)
1583                 {
1584                         if(head.deadflag!=DEAD_NO)
1585                                 continue;
1586
1587                         distance = vlen(org - head.origin);
1588                         if(distance<bestdistance)
1589                         {
1590                                 closestplayer = head;
1591                                 bestdistance = distance;
1592                         }
1593                 }
1594
1595                 if(closestplayer)
1596                 if(closestplayer.team!=self.team)
1597                 if(vlen(org - self.origin)>1000)
1598                 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1599                         havocbot_goalrating_ctf_ourbase(30000);
1600
1601                 havocbot_goalrating_ctf_ourstolenflag(20000);
1602                 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1603                 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1604                 havocbot_goalrating_items(10000, org, mp_radius);
1605                 havocbot_goalrating_items(5000, self.origin, 10000);
1606                 navigation_goalrating_end();
1607         }
1608 }
1609
1610 void havocbot_role_ctf_setrole(entity bot, float role)
1611 {
1612         dprint(strcat(bot.netname," switched to "));
1613         switch(role)
1614         {
1615                 case HAVOCBOT_CTF_ROLE_CARRIER:
1616                         dprint("carrier");
1617                         bot.havocbot_role = havocbot_role_ctf_carrier;
1618                         bot.havocbot_role_timeout = 0;
1619                         bot.havocbot_cantfindflag = time + 10;
1620                         bot.bot_strategytime = 0;
1621                         break;
1622                 case HAVOCBOT_CTF_ROLE_DEFENSE:
1623                         dprint("defense");
1624                         bot.havocbot_role = havocbot_role_ctf_defense;
1625                         bot.havocbot_role_timeout = 0;
1626                         break;
1627                 case HAVOCBOT_CTF_ROLE_MIDDLE:
1628                         dprint("middle");
1629                         bot.havocbot_role = havocbot_role_ctf_middle;
1630                         bot.havocbot_role_timeout = 0;
1631                         break;
1632                 case HAVOCBOT_CTF_ROLE_OFFENSE:
1633                         dprint("offense");
1634                         bot.havocbot_role = havocbot_role_ctf_offense;
1635                         bot.havocbot_role_timeout = 0;
1636                         break;
1637                 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1638                         dprint("retriever");
1639                         bot.havocbot_previous_role = bot.havocbot_role;
1640                         bot.havocbot_role = havocbot_role_ctf_retriever;
1641                         bot.havocbot_role_timeout = time + 10;
1642                         bot.bot_strategytime = 0;
1643                         break;
1644                 case HAVOCBOT_CTF_ROLE_ESCORT:
1645                         dprint("escort");
1646                         bot.havocbot_previous_role = bot.havocbot_role;
1647                         bot.havocbot_role = havocbot_role_ctf_escort;
1648                         bot.havocbot_role_timeout = time + 30;
1649                         bot.bot_strategytime = 0;
1650                         break;
1651         }
1652         dprint("\n");
1653 }
1654
1655
1656 // ==============
1657 // Hook Functions
1658 // ==============
1659
1660 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1661 {
1662         entity flag;
1663         
1664         // initially clear items so they can be set as necessary later.
1665         self.items &~= (IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST 
1666                 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST | IT_CTF_SHIELDED);
1667
1668         // scan through all the flags and notify the client about them 
1669         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1670         {
1671                 switch(flag.ctf_status)
1672                 {
1673                         case FLAG_PASSING:
1674                         case FLAG_CARRY:
1675                         {
1676                                 if((flag.owner == self) || (flag.pass_sender == self))
1677                                         self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
1678                                 else 
1679                                         self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
1680                                 break;
1681                         }
1682                         case FLAG_DROPPED:
1683                         {
1684                                 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
1685                                 break;
1686                         }
1687                 }
1688         }
1689         
1690         // item for stopping players from capturing the flag too often
1691         if(self.ctf_captureshielded)
1692                 self.items |= IT_CTF_SHIELDED;
1693         
1694         // update the health of the flag carrier waypointsprite
1695         if(self.wps_flagcarrier) 
1696                 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent));
1697         
1698         return FALSE;
1699 }
1700
1701 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1702 {
1703         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1704         {
1705                 if(frag_target == frag_attacker) // damage done to yourself
1706                 {
1707                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1708                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1709                 }
1710                 else // damage done to everyone else
1711                 {
1712                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1713                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1714                 }
1715         }
1716         else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && IsDifferentTeam(frag_target, frag_attacker)) // if the target is a flagcarrier
1717         {
1718                 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)))
1719                         WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1720         }
1721         return FALSE;
1722 }
1723
1724 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1725 {
1726         if((frag_attacker != frag_target) && (frag_attacker.classname == "player") && (frag_target.flagcarried))
1727         {
1728                 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1729                 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1730         }
1731                                 
1732         if(frag_target.flagcarried)
1733                 { ctf_Handle_Throw(frag_target, world, DROP_NORMAL); }
1734                 
1735         return FALSE;
1736 }
1737
1738 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1739 {
1740         frag_score = 0;
1741         return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1742 }
1743
1744 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1745 {
1746         entity flag; // temporary entity for the search method
1747         
1748         if(self.flagcarried)
1749                 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1750         
1751         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1752         {
1753                 if(flag.pass_sender == self) { flag.pass_sender = world; }
1754                 if(flag.pass_target == self) { flag.pass_target = world; }
1755                 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
1756         }
1757                 
1758         return FALSE;
1759 }
1760
1761 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
1762 {
1763         if(self.flagcarried) 
1764         if(!autocvar_g_ctf_portalteleport)
1765                 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1766
1767         return FALSE;
1768 }
1769
1770 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
1771 {
1772         if(MUTATOR_RETURNVALUE || gameover) { return FALSE; }
1773         
1774         entity player = self;
1775
1776         if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1777         {
1778                 // pass the flag to a team mate
1779                 if(autocvar_g_ctf_pass)
1780                 {
1781                         entity head, closest_target;
1782                         head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, TRUE);
1783                         
1784                         while(head) // find the closest acceptable target to pass to
1785                         {
1786                                 if(head.classname == "player" && head.deadflag == DEAD_NO)
1787                                 if(head != player && !IsDifferentTeam(head, player))
1788                                 if(!head.speedrunning && !head.vehicle)
1789                                 {
1790                                         // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc) 
1791                                         vector head_center = WarpZone_UnTransformOrigin(head, PLAYER_CENTER(head));
1792                                         vector passer_center = PLAYER_CENTER(player);
1793                                         
1794                                         if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
1795                                         {
1796                                                 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried) 
1797                                                 { 
1798                                                         if(clienttype(head) == CLIENTTYPE_BOT)
1799                                                         {
1800                                                                 centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname)); 
1801                                                                 ctf_Handle_Throw(head, player, DROP_PASS);
1802                                                         }
1803                                                         else
1804                                                         {
1805                                                                 centerprint(head, strcat(player.netname, " requests you to pass the ", head.flagcarried.netname)); 
1806                                                                 centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname)); 
1807                                                         }
1808                                                         player.throw_antispam = time + autocvar_g_ctf_pass_wait; 
1809                                                         return TRUE; 
1810                                                 }
1811                                                 else if(player.flagcarried)
1812                                                 {
1813                                                         if(closest_target)
1814                                                         {
1815                                                                 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, PLAYER_CENTER(closest_target));
1816                                                                 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
1817                                                                         { closest_target = head; }
1818                                                         }
1819                                                         else { closest_target = head; }
1820                                                 }
1821                                         }
1822                                 }
1823                                 head = head.chain;
1824                         }
1825                         
1826                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return TRUE; }
1827                 }
1828                 
1829                 // throw the flag in front of you
1830                 if(autocvar_g_ctf_throw && player.flagcarried)
1831                         { ctf_Handle_Throw(player, world, DROP_THROW); return TRUE; }
1832         }
1833                 
1834         return FALSE;
1835 }
1836
1837 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
1838 {
1839         if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
1840         {
1841                 WaypointSprite_HelpMePing(self.wps_flagcarrier);
1842         } 
1843         else // create a normal help me waypointsprite
1844         {
1845                 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');
1846                 WaypointSprite_Ping(self.wps_helpme);
1847         }
1848
1849         return TRUE;
1850 }
1851
1852 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
1853 {
1854         if(vh_player.flagcarried)
1855         {
1856                 if(!autocvar_g_ctf_allow_vehicle_carry)
1857                 {
1858                         ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
1859                 }
1860                 else
1861                 {            
1862                         setattachment(vh_player.flagcarried, vh_vehicle, ""); 
1863                         setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
1864                         vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
1865                         //vh_player.flagcarried.angles = '0 0 0';       
1866                 }
1867                 return TRUE;
1868         }
1869                 
1870         return FALSE;
1871 }
1872
1873 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
1874 {
1875         if(vh_player.flagcarried)
1876         {
1877                 setattachment(vh_player.flagcarried, vh_player, ""); 
1878                 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
1879                 vh_player.flagcarried.scale = FLAG_SCALE;
1880                 vh_player.flagcarried.angles = '0 0 0';
1881                 return TRUE;
1882         }
1883
1884         return FALSE;
1885 }
1886
1887 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
1888 {
1889         if(self.flagcarried)
1890         {
1891                 bprint("The ", self.flagcarried.netname, " was returned to base by its carrier\n");
1892                 ctf_RespawnFlag(self);
1893                 return TRUE;
1894         }
1895         
1896         return FALSE;
1897 }
1898
1899 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
1900 {
1901         entity flag; // temporary entity for the search method
1902         
1903         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1904         {
1905                 switch(flag.ctf_status)
1906                 {
1907                         case FLAG_DROPPED:
1908                         case FLAG_PASSING:
1909                         {
1910                                 // lock the flag, game is over
1911                                 flag.movetype = MOVETYPE_NONE;
1912                                 flag.takedamage = DAMAGE_NO;
1913                                 flag.solid = SOLID_NOT;
1914                                 flag.nextthink = FALSE; // stop thinking
1915                                 
1916                                 print("stopping the ", flag.netname, " from moving.\n");
1917                                 break;
1918                         }
1919                         
1920                         default:
1921                         case FLAG_BASE:
1922                         case FLAG_CARRY:
1923                         {
1924                                 // do nothing for these flags
1925                                 break;
1926                         }
1927                 }
1928         }
1929         
1930         return FALSE;
1931 }
1932
1933 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
1934 {
1935         havocbot_ctf_reset_role(self);
1936         return TRUE;
1937 }
1938
1939
1940 // ==========
1941 // Spawnfuncs
1942 // ==========
1943
1944 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
1945 CTF Starting point for a player in team one (Red).
1946 Keys: "angle" viewing angle when spawning. */
1947 void spawnfunc_info_player_team1()
1948 {
1949         if(g_assault) { remove(self); return; }
1950         
1951         self.team = COLOR_TEAM1; // red
1952         spawnfunc_info_player_deathmatch();
1953 }
1954
1955
1956 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
1957 CTF Starting point for a player in team two (Blue).
1958 Keys: "angle" viewing angle when spawning. */
1959 void spawnfunc_info_player_team2()
1960 {
1961         if(g_assault) { remove(self); return; }
1962         
1963         self.team = COLOR_TEAM2; // blue
1964         spawnfunc_info_player_deathmatch();
1965 }
1966
1967 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
1968 CTF Starting point for a player in team three (Yellow).
1969 Keys: "angle" viewing angle when spawning. */
1970 void spawnfunc_info_player_team3()
1971 {
1972         if(g_assault) { remove(self); return; }
1973         
1974         self.team = COLOR_TEAM3; // yellow
1975         spawnfunc_info_player_deathmatch();
1976 }
1977
1978
1979 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
1980 CTF Starting point for a player in team four (Purple).
1981 Keys: "angle" viewing angle when spawning. */
1982 void spawnfunc_info_player_team4()
1983 {
1984         if(g_assault) { remove(self); return; }
1985         
1986         self.team = COLOR_TEAM4; // purple
1987         spawnfunc_info_player_deathmatch();
1988 }
1989
1990 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
1991 CTF flag for team one (Red).
1992 Keys: 
1993 "angle" Angle the flag will point (minus 90 degrees)... 
1994 "model" model to use, note this needs red and blue as skins 0 and 1...
1995 "noise" sound played when flag is picked up...
1996 "noise1" sound played when flag is returned by a teammate...
1997 "noise2" sound played when flag is captured...
1998 "noise3" sound played when flag is lost in the field and respawns itself... 
1999 "noise4" sound played when flag is dropped by a player...
2000 "noise5" sound played when flag touches the ground... */
2001 void spawnfunc_item_flag_team1()
2002 {
2003         if(!g_ctf) { remove(self); return; }
2004
2005         ctf_FlagSetup(1, self); // 1 = red
2006 }
2007
2008 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2009 CTF flag for team two (Blue).
2010 Keys: 
2011 "angle" Angle the flag will point (minus 90 degrees)... 
2012 "model" model to use, note this needs red and blue as skins 0 and 1...
2013 "noise" sound played when flag is picked up...
2014 "noise1" sound played when flag is returned by a teammate...
2015 "noise2" sound played when flag is captured...
2016 "noise3" sound played when flag is lost in the field and respawns itself... 
2017 "noise4" sound played when flag is dropped by a player...
2018 "noise5" sound played when flag touches the ground... */
2019 void spawnfunc_item_flag_team2()
2020 {
2021         if(!g_ctf) { remove(self); return; }
2022
2023         ctf_FlagSetup(0, self); // the 0 is misleading, but -- 0 = blue.
2024 }
2025
2026 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2027 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2028 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.
2029 Keys:
2030 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2031 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2032 void spawnfunc_ctf_team()
2033 {
2034         if(!g_ctf) { remove(self); return; }
2035         
2036         self.classname = "ctf_team";
2037         self.team = self.cnt + 1;
2038 }
2039
2040 // compatibility for quake maps
2041 void spawnfunc_team_CTF_redflag()    { spawnfunc_item_flag_team1();    }
2042 void spawnfunc_team_CTF_blueflag()   { spawnfunc_item_flag_team2();    }
2043 void spawnfunc_team_CTF_redplayer()  { spawnfunc_info_player_team1();  }
2044 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2();  }
2045 void spawnfunc_team_CTF_redspawn()   { spawnfunc_info_player_team1();  }
2046 void spawnfunc_team_CTF_bluespawn()  { spawnfunc_info_player_team2();  }
2047
2048
2049 // ==============
2050 // Initialization
2051 // ==============
2052
2053 // scoreboard setup
2054 void ctf_ScoreRules()
2055 {
2056         ScoreRules_basics(2, SFL_SORT_PRIO_PRIMARY, 0, TRUE);
2057         ScoreInfo_SetLabel_TeamScore  (ST_CTF_CAPS,     "caps",      SFL_SORT_PRIO_PRIMARY);
2058         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS,     "caps",      SFL_SORT_PRIO_SECONDARY);
2059         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME,  "captime",   SFL_LOWER_IS_BETTER | SFL_TIME);
2060         ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS,  "pickups",   0);
2061         ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS,  "fckills",   0);
2062         ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS,  "returns",   0);
2063         ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS,    "drops",     SFL_LOWER_IS_BETTER);
2064         ScoreRules_basics_end();
2065 }
2066
2067 // code from here on is just to support maps that don't have flag and team entities
2068 void ctf_SpawnTeam (string teamname, float teamcolor)
2069 {
2070         entity oldself;
2071         oldself = self;
2072         self = spawn();
2073         self.classname = "ctf_team";
2074         self.netname = teamname;
2075         self.cnt = teamcolor;
2076
2077         spawnfunc_ctf_team();
2078
2079         self = oldself;
2080 }
2081
2082 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2083 {
2084         // if no teams are found, spawn defaults
2085         if(find(world, classname, "ctf_team") == world)
2086         {
2087                 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2088                 ctf_SpawnTeam("Red", COLOR_TEAM1 - 1);
2089                 ctf_SpawnTeam("Blue", COLOR_TEAM2 - 1);
2090         }
2091         
2092         ctf_ScoreRules();
2093 }
2094
2095 void ctf_Initialize()
2096 {
2097         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2098
2099         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2100         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2101         ctf_captureshield_force = autocvar_g_ctf_shield_force;
2102         
2103         InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2104 }
2105
2106
2107 MUTATOR_DEFINITION(gamemode_ctf)
2108 {
2109         MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2110         MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2111         MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2112         MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2113         MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2114         MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2115         MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2116         MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2117         MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2118         MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2119         MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2120         MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2121         MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2122         MUTATOR_HOOK(HavocBot_ChooseRule, ctf_BotRoles, CBC_ORDER_ANY);
2123         
2124         MUTATOR_ONADD
2125         {
2126                 if(time > 1) // game loads at time 1
2127                         error("This is a game type and it cannot be added at runtime.");
2128                 g_ctf = 1;
2129                 ctf_Initialize();
2130         }
2131
2132         MUTATOR_ONREMOVE
2133         {
2134                 g_ctf = 0;
2135                 error("This is a game type and it cannot be removed at runtime.");
2136         }
2137
2138         return 0;
2139 }