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