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