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