]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/mutator/gamemode_ctf.qc
Merge branch 'master' into Mario/intrusive_2
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / mutator / gamemode_ctf.qc
1 #include "gamemode_ctf.qh"
2
3 #ifndef CSQC
4 void ctf_Initialize();
5
6 REGISTER_MUTATOR(ctf, false)
7 {
8         MUTATOR_ONADD
9         {
10                 if (time > 1) // game loads at time 1
11                         error("This is a game type and it cannot be added at runtime.");
12                 ctf_Initialize();
13
14                 ActivateTeamplay();
15                 SetLimits(autocvar_capturelimit_override, autocvar_captureleadlimit_override, autocvar_timelimit_override, -1);
16                 have_team_spawns = -1; // request team spawns
17         }
18
19         MUTATOR_ONROLLBACK_OR_REMOVE
20         {
21                 // we actually cannot roll back ctf_Initialize here
22                 // BUT: we don't need to! If this gets called, adding always
23                 // succeeds.
24         }
25
26         MUTATOR_ONREMOVE
27         {
28                 LOG_INFO("This is a game type and it cannot be removed at runtime.");
29                 return -1;
30         }
31
32         return 0;
33 }
34 #endif
35
36 #ifdef SVQC
37 #include <common/vehicles/all.qh>
38 #include <server/teamplay.qh>
39 #endif
40
41 #include <lib/warpzone/common.qh>
42
43 bool autocvar_g_ctf_allow_vehicle_carry;
44 bool autocvar_g_ctf_allow_vehicle_touch;
45 bool autocvar_g_ctf_allow_monster_touch;
46 bool autocvar_g_ctf_throw;
47 float autocvar_g_ctf_throw_angle_max;
48 float autocvar_g_ctf_throw_angle_min;
49 int autocvar_g_ctf_throw_punish_count;
50 float autocvar_g_ctf_throw_punish_delay;
51 float autocvar_g_ctf_throw_punish_time;
52 float autocvar_g_ctf_throw_strengthmultiplier;
53 float autocvar_g_ctf_throw_velocity_forward;
54 float autocvar_g_ctf_throw_velocity_up;
55 float autocvar_g_ctf_drop_velocity_up;
56 float autocvar_g_ctf_drop_velocity_side;
57 bool autocvar_g_ctf_oneflag_reverse;
58 bool autocvar_g_ctf_portalteleport;
59 bool autocvar_g_ctf_pass;
60 float autocvar_g_ctf_pass_arc;
61 float autocvar_g_ctf_pass_arc_max;
62 float autocvar_g_ctf_pass_directional_max;
63 float autocvar_g_ctf_pass_directional_min;
64 float autocvar_g_ctf_pass_radius;
65 float autocvar_g_ctf_pass_wait;
66 bool autocvar_g_ctf_pass_request;
67 float autocvar_g_ctf_pass_turnrate;
68 float autocvar_g_ctf_pass_timelimit;
69 float autocvar_g_ctf_pass_velocity;
70 bool autocvar_g_ctf_dynamiclights;
71 float autocvar_g_ctf_flag_collect_delay;
72 float autocvar_g_ctf_flag_damageforcescale;
73 bool autocvar_g_ctf_flag_dropped_waypoint;
74 bool autocvar_g_ctf_flag_dropped_floatinwater;
75 bool autocvar_g_ctf_flag_glowtrails;
76 int autocvar_g_ctf_flag_health;
77 bool autocvar_g_ctf_flag_return;
78 bool autocvar_g_ctf_flag_return_carrying;
79 float autocvar_g_ctf_flag_return_carried_radius;
80 float autocvar_g_ctf_flag_return_time;
81 bool autocvar_g_ctf_flag_return_when_unreachable;
82 float autocvar_g_ctf_flag_return_damage;
83 float autocvar_g_ctf_flag_return_damage_delay;
84 float autocvar_g_ctf_flag_return_dropped;
85 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
86 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
87 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
88 float autocvar_g_ctf_flagcarrier_selfforcefactor;
89 float autocvar_g_ctf_flagcarrier_damagefactor;
90 float autocvar_g_ctf_flagcarrier_forcefactor;
91 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
92 bool autocvar_g_ctf_fullbrightflags;
93 bool autocvar_g_ctf_ignore_frags;
94 int autocvar_g_ctf_score_capture;
95 int autocvar_g_ctf_score_capture_assist;
96 int autocvar_g_ctf_score_kill;
97 int autocvar_g_ctf_score_penalty_drop;
98 int autocvar_g_ctf_score_penalty_returned;
99 int autocvar_g_ctf_score_pickup_base;
100 int autocvar_g_ctf_score_pickup_dropped_early;
101 int autocvar_g_ctf_score_pickup_dropped_late;
102 int autocvar_g_ctf_score_return;
103 float autocvar_g_ctf_shield_force;
104 float autocvar_g_ctf_shield_max_ratio;
105 int autocvar_g_ctf_shield_min_negscore;
106 bool autocvar_g_ctf_stalemate;
107 int autocvar_g_ctf_stalemate_endcondition;
108 float autocvar_g_ctf_stalemate_time;
109 bool autocvar_g_ctf_reverse;
110 float autocvar_g_ctf_dropped_capture_delay;
111 float autocvar_g_ctf_dropped_capture_radius;
112
113 void ctf_FakeTimeLimit(entity e, float t)
114 {
115         msg_entity = e;
116         WriteByte(MSG_ONE, 3); // svc_updatestat
117         WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
118         if(t < 0)
119                 WriteCoord(MSG_ONE, autocvar_timelimit);
120         else
121                 WriteCoord(MSG_ONE, (t + 1) / 60);
122 }
123
124 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
125 {
126         if(autocvar_sv_eventlog)
127                 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != NULL) ? ftos(actor.playerid) : "")));
128                 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
129 }
130
131 void ctf_CaptureRecord(entity flag, entity player)
132 {
133         float cap_record = ctf_captimerecord;
134         float cap_time = (time - flag.ctf_pickuptime);
135         string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
136
137         // notify about shit
138         if(ctf_oneflag) { Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname); }
139         else if(!ctf_captimerecord) { Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_ENT(flag, CHOICE_CTF_CAPTURE_TIME), player.netname, (cap_time * 100)); }
140         else if(cap_time < cap_record) { Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_ENT(flag, CHOICE_CTF_CAPTURE_BROKEN), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
141         else { Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_ENT(flag, CHOICE_CTF_CAPTURE_UNBROKEN), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
142
143         // write that shit in the database
144         if(!ctf_oneflag) // but not in 1-flag mode
145         if((!ctf_captimerecord) || (cap_time < cap_record))
146         {
147                 ctf_captimerecord = cap_time;
148                 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
149                 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
150                 write_recordmarker(player, (time - cap_time), cap_time);
151         }
152 }
153
154 bool ctf_Return_Customize(entity this, entity client)
155 {
156         // only to the carrier
157         return boolean(client == this.owner);
158 }
159
160 void ctf_FlagcarrierWaypoints(entity player)
161 {
162         WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
163         WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
164         WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
165         WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
166
167         if(player.flagcarried && CTF_SAMETEAM(player, player.flagcarried))
168         {
169                 if(!player.wps_enemyflagcarrier)
170                 {
171                         entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, 0, player, wps_enemyflagcarrier, true, RADARICON_FLAG);
172                         wp.colormod = WPCOLOR_ENEMYFC(player.team);
173                         setcefc(wp, ctf_Stalemate_Customize);
174
175                         if(IS_REAL_CLIENT(player) && !ctf_stalemate)
176                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_VISIBLE);
177                 }
178
179                 if(!player.wps_flagreturn)
180                 {
181                         entity owp = WaypointSprite_SpawnFixed(WP_FlagReturn, player.flagcarried.ctf_spawnorigin + FLAG_WAYPOINT_OFFSET, player, wps_flagreturn, RADARICON_FLAG);
182                         owp.colormod = '0 0.8 0.8';
183                         //WaypointSprite_UpdateTeamRadar(player.wps_flagreturn, RADARICON_FLAG, ((player.team) ? colormapPaletteColor(player.team - 1, false) : '1 1 1'));
184                         setcefc(owp, ctf_Return_Customize);
185                 }
186         }
187 }
188
189 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
190 {
191         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
192         float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
193         float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
194         //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
195
196         vector targpos;
197         if(current_height) // make sure we can actually do this arcing path
198         {
199                 targpos = (to + ('0 0 1' * current_height));
200                 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
201                 if(trace_fraction < 1)
202                 {
203                         //print("normal arc line failed, trying to find new pos...");
204                         WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
205                         targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
206                         WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
207                         if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
208                         /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
209                 }
210         }
211         else { targpos = to; }
212
213         //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
214
215         vector desired_direction = normalize(targpos - from);
216         if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
217         else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
218 }
219
220 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
221 {
222         if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
223         {
224                 // directional tracing only
225                 float spreadlimit;
226                 makevectors(passer_angle);
227
228                 // find the closest point on the enemy to the center of the attack
229                 float h; // hypotenuse, which is the distance between attacker to head
230                 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
231
232                 h = vlen(head_center - passer_center);
233                 a = h * (normalize(head_center - passer_center) * v_forward);
234
235                 vector nearest_on_line = (passer_center + a * v_forward);
236                 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
237
238                 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
239                 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
240
241                 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
242                         { return true; }
243                 else
244                         { return false; }
245         }
246         else { return true; }
247 }
248
249
250 // =======================
251 // CaptureShield Functions
252 // =======================
253
254 bool ctf_CaptureShield_CheckStatus(entity p)
255 {
256         int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
257         int players_worseeq, players_total;
258
259         if(ctf_captureshield_max_ratio <= 0)
260                 return false;
261
262         s  = PlayerScore_Add(p, SP_CTF_CAPS,    0);
263         s2 = PlayerScore_Add(p, SP_CTF_PICKUPS, 0);
264         s3 = PlayerScore_Add(p, SP_CTF_RETURNS, 0);
265         s4 = PlayerScore_Add(p, SP_CTF_FCKILLS, 0);
266
267         sr = ((s - s2) + (s3 + s4));
268
269         if(sr >= -ctf_captureshield_min_negscore)
270                 return false;
271
272         players_total = players_worseeq = 0;
273         FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
274                 if(DIFF_TEAM(it, p))
275                         continue;
276                 se  = PlayerScore_Add(it, SP_CTF_CAPS,    0);
277                 se2 = PlayerScore_Add(it, SP_CTF_PICKUPS, 0);
278                 se3 = PlayerScore_Add(it, SP_CTF_RETURNS, 0);
279                 se4 = PlayerScore_Add(it, SP_CTF_FCKILLS, 0);
280
281                 ser = ((se - se2) + (se3 + se4));
282
283                 if(ser <= sr)
284                         ++players_worseeq;
285                 ++players_total;
286         ));
287
288         // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
289         // use this rule here
290
291         if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
292                 return false;
293
294         return true;
295 }
296
297 void ctf_CaptureShield_Update(entity player, bool wanted_status)
298 {
299         bool updated_status = ctf_CaptureShield_CheckStatus(player);
300         if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
301         {
302                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
303                 player.ctf_captureshielded = updated_status;
304         }
305 }
306
307 bool ctf_CaptureShield_Customize(entity this, entity client)
308 {
309         if(!client.ctf_captureshielded) { return false; }
310         if(CTF_SAMETEAM(this, client)) { return false; }
311
312         return true;
313 }
314
315 void ctf_CaptureShield_Touch(entity this, entity toucher)
316 {
317         if(!toucher.ctf_captureshielded) { return; }
318         if(CTF_SAMETEAM(this, toucher)) { return; }
319
320         vector mymid = (this.absmin + this.absmax) * 0.5;
321         vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
322
323         Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
324         if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
325 }
326
327 void ctf_CaptureShield_Spawn(entity flag)
328 {
329         entity shield = new(ctf_captureshield);
330
331         shield.enemy = flag;
332         shield.team = flag.team;
333         settouch(shield, ctf_CaptureShield_Touch);
334         setcefc(shield, ctf_CaptureShield_Customize);
335         shield.effects = EF_ADDITIVE;
336         set_movetype(shield, MOVETYPE_NOCLIP);
337         shield.solid = SOLID_TRIGGER;
338         shield.avelocity = '7 0 11';
339         shield.scale = 0.5;
340
341         setorigin(shield, flag.origin);
342         setmodel(shield, MDL_CTF_SHIELD);
343         setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
344 }
345
346
347 // ====================
348 // Drop/Pass/Throw Code
349 // ====================
350
351 void ctf_Handle_Drop(entity flag, entity player, int droptype)
352 {
353         // declarations
354         player = (player ? player : flag.pass_sender);
355
356         // main
357         set_movetype(flag, MOVETYPE_TOSS);
358         flag.takedamage = DAMAGE_YES;
359         flag.angles = '0 0 0';
360         flag.health = flag.max_flag_health;
361         flag.ctf_droptime = time;
362         flag.ctf_dropper = player;
363         flag.ctf_status = FLAG_DROPPED;
364
365         // messages and sounds
366         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, ((flag.team) ? APP_TEAM_ENT(flag, INFO_CTF_LOST) : INFO_CTF_LOST_NEUTRAL), player.netname);
367         _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
368         ctf_EventLog("dropped", player.team, player);
369
370         // scoring
371         PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);
372         PlayerScore_Add(player, SP_CTF_DROPS, 1);
373
374         // waypoints
375         if(autocvar_g_ctf_flag_dropped_waypoint) {
376                 entity wp = WaypointSprite_Spawn(WP_FlagDropped, 0, 0, flag, FLAG_WAYPOINT_OFFSET, NULL, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, true, RADARICON_FLAG);
377                 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
378         }
379
380         if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
381         {
382                 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
383                 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
384         }
385
386         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
387
388         if(droptype == DROP_PASS)
389         {
390                 flag.pass_distance = 0;
391                 flag.pass_sender = NULL;
392                 flag.pass_target = NULL;
393         }
394 }
395
396 void ctf_Handle_Retrieve(entity flag, entity player)
397 {
398         entity sender = flag.pass_sender;
399
400         // transfer flag to player
401         flag.owner = player;
402         flag.owner.flagcarried = flag;
403
404         // reset flag
405         if(player.vehicle)
406         {
407                 setattachment(flag, player.vehicle, "");
408                 setorigin(flag, VEHICLE_FLAG_OFFSET);
409                 flag.scale = VEHICLE_FLAG_SCALE;
410         }
411         else
412         {
413                 setattachment(flag, player, "");
414                 setorigin(flag, FLAG_CARRY_OFFSET);
415         }
416         set_movetype(flag, MOVETYPE_NONE);
417         flag.takedamage = DAMAGE_NO;
418         flag.solid = SOLID_NOT;
419         flag.angles = '0 0 0';
420         flag.ctf_status = FLAG_CARRY;
421
422         // messages and sounds
423         _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
424         ctf_EventLog("receive", flag.team, player);
425
426         FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), LAMBDA(
427                 if(it == sender)
428                         Send_Notification(NOTIF_ONE, it, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT(flag, CENTER_CTF_PASS_SENT) : CENTER_CTF_PASS_SENT_NEUTRAL), player.netname);
429                 else if(it == player)
430                         Send_Notification(NOTIF_ONE, it, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT(flag, CENTER_CTF_PASS_RECEIVED) : CENTER_CTF_PASS_RECEIVED_NEUTRAL), sender.netname);
431                 else if(SAME_TEAM(it, sender))
432                         Send_Notification(NOTIF_ONE, it, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT(flag, CENTER_CTF_PASS_OTHER) : CENTER_CTF_PASS_OTHER_NEUTRAL), sender.netname, player.netname);
433         ));
434
435         // create new waypoint
436         ctf_FlagcarrierWaypoints(player);
437
438         sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
439         player.throw_antispam = sender.throw_antispam;
440
441         flag.pass_distance = 0;
442         flag.pass_sender = NULL;
443         flag.pass_target = NULL;
444 }
445
446 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
447 {
448         entity flag = player.flagcarried;
449         vector targ_origin, flag_velocity;
450
451         if(!flag) { return; }
452         if((droptype == DROP_PASS) && !receiver) { return; }
453
454         if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
455
456         // reset the flag
457         setattachment(flag, NULL, "");
458         setorigin(flag, player.origin + FLAG_DROP_OFFSET);
459         flag.owner.flagcarried = NULL;
460         flag.owner = NULL;
461         flag.solid = SOLID_TRIGGER;
462         flag.ctf_dropper = player;
463         flag.ctf_droptime = time;
464
465         flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
466
467         switch(droptype)
468         {
469                 case DROP_PASS:
470                 {
471                         // warpzone support:
472                         // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
473                         // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
474                         WarpZone_RefSys_Copy(flag, receiver);
475                         WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
476                         targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
477
478                         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
479                         ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
480
481                         // main
482                         set_movetype(flag, MOVETYPE_FLY);
483                         flag.takedamage = DAMAGE_NO;
484                         flag.pass_sender = player;
485                         flag.pass_target = receiver;
486                         flag.ctf_status = FLAG_PASSING;
487
488                         // other
489                         _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
490                         WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
491                         ctf_EventLog("pass", flag.team, player);
492                         break;
493                 }
494
495                 case DROP_THROW:
496                 {
497                         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'));
498
499                         flag_velocity = (('0 0 1' * autocvar_g_ctf_throw_velocity_up) + ((v_forward * autocvar_g_ctf_throw_velocity_forward) * ((player.items & ITEM_Strength.m_itemid) ? autocvar_g_ctf_throw_strengthmultiplier : 1)));
500                         flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
501                         ctf_Handle_Drop(flag, player, droptype);
502                         break;
503                 }
504
505                 case DROP_RESET:
506                 {
507                         flag.velocity = '0 0 0'; // do nothing
508                         break;
509                 }
510
511                 default:
512                 case DROP_NORMAL:
513                 {
514                         flag.velocity = W_CalculateProjectileVelocity(player, 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);
515                         ctf_Handle_Drop(flag, player, droptype);
516                         break;
517                 }
518         }
519
520         // kill old waypointsprite
521         WaypointSprite_Ping(player.wps_flagcarrier);
522         WaypointSprite_Kill(player.wps_flagcarrier);
523
524         if(player.wps_enemyflagcarrier)
525                 WaypointSprite_Kill(player.wps_enemyflagcarrier);
526
527         if(player.wps_flagreturn)
528                 WaypointSprite_Kill(player.wps_flagreturn);
529
530         // captureshield
531         ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
532 }
533
534 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
535 {
536         return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
537 }
538
539 // ==============
540 // Event Handlers
541 // ==============
542
543 void nades_GiveBonus(entity player, float score);
544
545 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
546 {
547         entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
548         entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
549         entity player_team_flag = NULL, tmp_entity;
550         float old_time, new_time;
551
552         if(!player) { return; } // without someone to give the reward to, we can't possibly cap
553         if(CTF_DIFFTEAM(player, flag)) { return; }
554
555         if(ctf_oneflag)
556         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
557         if(SAME_TEAM(tmp_entity, player))
558         {
559                 player_team_flag = tmp_entity;
560                 break;
561         }
562
563         nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
564
565         player.throw_prevtime = time;
566         player.throw_count = 0;
567
568         // messages and sounds
569         Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((enemy_flag.team) ? APP_TEAM_ENT(enemy_flag, CENTER_CTF_CAPTURE) : CENTER_CTF_CAPTURE_NEUTRAL));
570         ctf_CaptureRecord(enemy_flag, player);
571         _sound(player, CH_TRIGGER, ((ctf_oneflag) ? player_team_flag.snd_flag_capture : ((DIFF_TEAM(player, flag)) ? enemy_flag.snd_flag_capture : flag.snd_flag_capture)), VOL_BASE, ATTEN_NONE);
572
573         switch(capturetype)
574         {
575                 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
576                 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
577                 default: break;
578         }
579
580         // scoring
581         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
582         PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
583
584         old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
585         new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
586         if(!old_time || new_time < old_time)
587                 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
588
589         // effects
590         Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
591         //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
592
593         // other
594         if(capturetype == CAPTURE_NORMAL)
595         {
596                 WaypointSprite_Kill(player.wps_flagcarrier);
597                 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
598
599                 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
600                         { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
601         }
602
603         // reset the flag
604         player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
605         ctf_RespawnFlag(enemy_flag);
606 }
607
608 void ctf_Handle_Return(entity flag, entity player)
609 {
610         // messages and sounds
611         if(IS_MONSTER(player))
612         {
613                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_ENT(flag, INFO_CTF_RETURN_MONSTER), player.monster_name);
614         }
615         else if(flag.team)
616         {
617                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT(flag, CENTER_CTF_RETURN));
618                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_ENT(flag, INFO_CTF_RETURN), player.netname);
619         }
620         _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
621         ctf_EventLog("return", flag.team, player);
622
623         // scoring
624         if(IS_PLAYER(player))
625         {
626                 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
627                 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
628
629                 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
630         }
631
632         TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
633
634         if(flag.ctf_dropper)
635         {
636                 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
637                 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
638                 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
639         }
640
641         // other
642         if(player.flagcarried == flag)
643                 WaypointSprite_Kill(player.wps_flagcarrier);
644
645         // reset the flag
646         ctf_RespawnFlag(flag);
647 }
648
649 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
650 {
651         // declarations
652         float pickup_dropped_score; // used to calculate dropped pickup score
653
654         // attach the flag to the player
655         flag.owner = player;
656         player.flagcarried = flag;
657         if(player.vehicle)
658         {
659                 setattachment(flag, player.vehicle, "");
660                 setorigin(flag, VEHICLE_FLAG_OFFSET);
661                 flag.scale = VEHICLE_FLAG_SCALE;
662         }
663         else
664         {
665                 setattachment(flag, player, "");
666                 setorigin(flag, FLAG_CARRY_OFFSET);
667         }
668
669         // flag setup
670         set_movetype(flag, MOVETYPE_NONE);
671         flag.takedamage = DAMAGE_NO;
672         flag.solid = SOLID_NOT;
673         flag.angles = '0 0 0';
674         flag.ctf_status = FLAG_CARRY;
675
676         switch(pickuptype)
677         {
678                 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
679                 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
680                 default: break;
681         }
682
683         // messages and sounds
684         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, ((flag.team) ? APP_TEAM_ENT(flag, INFO_CTF_PICKUP) : INFO_CTF_PICKUP_NEUTRAL), player.netname);
685         if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
686         if(!flag.team) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL); }
687         else if(CTF_DIFFTEAM(player, flag)) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT(flag, CENTER_CTF_PICKUP)); }
688         else { Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_TEAM : CENTER_CTF_PICKUP_TEAM_ENEMY), Team_ColorCode(flag.team)); }
689
690         Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, ((flag.team) ? APP_TEAM_ENT(flag, CHOICE_CTF_PICKUP_TEAM) : CHOICE_CTF_PICKUP_TEAM_NEUTRAL), Team_ColorCode(player.team), player.netname);
691
692         if(!flag.team)
693                 FOREACH_CLIENT(IS_PLAYER(it) && it != player && DIFF_TEAM(it, player), LAMBDA(Send_Notification(NOTIF_ONE, it, MSG_CHOICE, CHOICE_CTF_PICKUP_ENEMY_NEUTRAL, Team_ColorCode(player.team), player.netname)));
694
695         if(flag.team)
696                 FOREACH_CLIENT(IS_PLAYER(it) && it != player, LAMBDA(
697                         if(CTF_SAMETEAM(flag, it))
698                         if(SAME_TEAM(player, it))
699                                 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_ENT(flag, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
700                         else
701                                 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, ((SAME_TEAM(flag, player)) ? CHOICE_CTF_PICKUP_ENEMY_TEAM : CHOICE_CTF_PICKUP_ENEMY), Team_ColorCode(player.team), player.netname);
702                 ));
703
704         _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
705
706         // scoring
707         PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
708         nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
709         switch(pickuptype)
710         {
711                 case PICKUP_BASE:
712                 {
713                         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
714                         ctf_EventLog("steal", flag.team, player);
715                         break;
716                 }
717
718                 case PICKUP_DROPPED:
719                 {
720                         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);
721                         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);
722                         LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
723                         PlayerTeamScore_AddScore(player, pickup_dropped_score);
724                         ctf_EventLog("pickup", flag.team, player);
725                         break;
726                 }
727
728                 default: break;
729         }
730
731         // speedrunning
732         if(pickuptype == PICKUP_BASE)
733         {
734                 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
735                 if((player.speedrunning) && (ctf_captimerecord))
736                         ctf_FakeTimeLimit(player, time + ctf_captimerecord);
737         }
738
739         // effects
740         Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
741
742         // waypoints
743         if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
744         ctf_FlagcarrierWaypoints(player);
745         WaypointSprite_Ping(player.wps_flagcarrier);
746 }
747
748
749 // ===================
750 // Main Flag Functions
751 // ===================
752
753 void ctf_CheckFlagReturn(entity flag, int returntype)
754 {
755         if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
756         {
757                 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
758
759                 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
760                 {
761                         switch(returntype)
762                         {
763                                 case RETURN_DROPPED: Send_Notification(NOTIF_ALL, NULL, MSG_INFO, ((flag.team) ? APP_TEAM_ENT(flag, INFO_CTF_FLAGRETURN_DROPPED) : INFO_CTF_FLAGRETURN_DROPPED_NEUTRAL)); break;
764                                 case RETURN_DAMAGE: Send_Notification(NOTIF_ALL, NULL, MSG_INFO, ((flag.team) ? APP_TEAM_ENT(flag, INFO_CTF_FLAGRETURN_DAMAGED) : INFO_CTF_FLAGRETURN_DAMAGED_NEUTRAL)); break;
765                                 case RETURN_SPEEDRUN: Send_Notification(NOTIF_ALL, NULL, MSG_INFO, ((flag.team) ? APP_TEAM_ENT(flag, INFO_CTF_FLAGRETURN_SPEEDRUN) : INFO_CTF_FLAGRETURN_SPEEDRUN_NEUTRAL), ctf_captimerecord); break;
766                                 case RETURN_NEEDKILL: Send_Notification(NOTIF_ALL, NULL, MSG_INFO, ((flag.team) ? APP_TEAM_ENT(flag, INFO_CTF_FLAGRETURN_NEEDKILL) : INFO_CTF_FLAGRETURN_NEEDKILL_NEUTRAL)); break;
767
768                                 default:
769                                 case RETURN_TIMEOUT:
770                                         { Send_Notification(NOTIF_ALL, NULL, MSG_INFO, ((flag.team) ? APP_TEAM_ENT(flag, INFO_CTF_FLAGRETURN_TIMEOUT) : INFO_CTF_FLAGRETURN_TIMEOUT_NEUTRAL)); break; }
771                         }
772                         _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
773                         ctf_EventLog("returned", flag.team, NULL);
774                         ctf_RespawnFlag(flag);
775                 }
776         }
777 }
778
779 bool ctf_Stalemate_Customize(entity this, entity client)
780 {
781         // make spectators see what the player would see
782         entity e = WaypointSprite_getviewentity(client);
783         entity wp_owner = this.owner;
784
785         // team waypoints
786         //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
787         if(SAME_TEAM(wp_owner, e)) { return false; }
788         if(!IS_PLAYER(e)) { return false; }
789
790         return true;
791 }
792
793 void ctf_CheckStalemate()
794 {
795         // declarations
796         int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
797         entity tmp_entity;
798
799         entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
800
801         // build list of stale flags
802         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
803         {
804                 if(autocvar_g_ctf_stalemate)
805                 if(tmp_entity.ctf_status != FLAG_BASE)
806                 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
807                 {
808                         tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
809                         ctf_staleflaglist = tmp_entity;
810
811                         switch(tmp_entity.team)
812                         {
813                                 case NUM_TEAM_1: ++stale_red_flags; break;
814                                 case NUM_TEAM_2: ++stale_blue_flags; break;
815                                 case NUM_TEAM_3: ++stale_yellow_flags; break;
816                                 case NUM_TEAM_4: ++stale_pink_flags; break;
817                                 default: ++stale_neutral_flags; break;
818                         }
819                 }
820         }
821
822         if(ctf_oneflag)
823                 stale_flags = (stale_neutral_flags >= 1);
824         else
825                 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
826
827         if(ctf_oneflag && stale_flags == 1)
828                 ctf_stalemate = true;
829         else if(stale_flags >= 2)
830                 ctf_stalemate = true;
831         else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
832                 { ctf_stalemate = false; wpforenemy_announced = false; }
833         else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
834                 { ctf_stalemate = false; wpforenemy_announced = false; }
835
836         // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
837         if(ctf_stalemate)
838         {
839                 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
840                 {
841                         if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
842                         {
843                                 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, tmp_entity.owner, FLAG_WAYPOINT_OFFSET, NULL, 0, tmp_entity.owner, wps_enemyflagcarrier, true, RADARICON_FLAG);
844                                 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
845                                 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
846                         }
847                 }
848
849                 if (!wpforenemy_announced)
850                 {
851                         FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), LAMBDA(Send_Notification(NOTIF_ONE, it, MSG_CENTER, ((it.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER))));
852
853                         wpforenemy_announced = true;
854                 }
855         }
856 }
857
858 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
859 {
860         if(ITEM_DAMAGE_NEEDKILL(deathtype))
861         {
862                 if(autocvar_g_ctf_flag_return_damage_delay)
863                 {
864                         this.ctf_flagdamaged = true;
865                 }
866                 else
867                 {
868                         this.health = 0;
869                         ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
870                 }
871                 return;
872         }
873         if(autocvar_g_ctf_flag_return_damage)
874         {
875                 // reduce health and check if it should be returned
876                 this.health = this.health - damage;
877                 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
878                 return;
879         }
880 }
881
882 void ctf_FlagThink(entity this)
883 {
884         // declarations
885         entity tmp_entity;
886
887         this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
888
889         // captureshield
890         if(this == ctf_worldflaglist) // only for the first flag
891                 FOREACH_CLIENT(true, LAMBDA(ctf_CaptureShield_Update(it, 1))); // release shield only
892
893         // sanity checks
894         if(this.mins != CTF_FLAG.m_mins || this.maxs != CTF_FLAG.m_maxs) { // reset the flag boundaries in case it got squished
895                 LOG_TRACE("wtf the flag got squashed?");
896                 tracebox(this.origin, CTF_FLAG.m_mins, CTF_FLAG.m_maxs, this.origin, MOVE_NOMONSTERS, this);
897                 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
898                         setsize(this, CTF_FLAG.m_mins, CTF_FLAG.m_maxs); }
899
900         switch(this.ctf_status) // reset flag angles in case warpzones adjust it
901         {
902                 case FLAG_DROPPED:
903                 {
904                         this.angles = '0 0 0';
905                         break;
906                 }
907
908                 default: break;
909         }
910
911         // main think method
912         switch(this.ctf_status)
913         {
914                 case FLAG_BASE:
915                 {
916                         if(autocvar_g_ctf_dropped_capture_radius)
917                         {
918                                 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
919                                         if(tmp_entity.ctf_status == FLAG_DROPPED)
920                                         if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
921                                         if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
922                                                 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
923                         }
924                         return;
925                 }
926
927                 case FLAG_DROPPED:
928                 {
929                         if(autocvar_g_ctf_flag_dropped_floatinwater)
930                         {
931                                 vector midpoint = ((this.absmin + this.absmax) * 0.5);
932                                 if(pointcontents(midpoint) == CONTENT_WATER)
933                                 {
934                                         this.velocity = this.velocity * 0.5;
935
936                                         if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
937                                                 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
938                                         else
939                                                 { set_movetype(this, MOVETYPE_FLY); }
940                                 }
941                                 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
942                         }
943                         if(autocvar_g_ctf_flag_return_dropped)
944                         {
945                                 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
946                                 {
947                                         this.health = 0;
948                                         ctf_CheckFlagReturn(this, RETURN_DROPPED);
949                                         return;
950                                 }
951                         }
952                         if(this.ctf_flagdamaged)
953                         {
954                                 this.health -= ((this.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
955                                 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
956                                 return;
957                         }
958                         else if(autocvar_g_ctf_flag_return_time)
959                         {
960                                 this.health -= ((this.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
961                                 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
962                                 return;
963                         }
964                         return;
965                 }
966
967                 case FLAG_CARRY:
968                 {
969                         if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
970                         {
971                                 this.health = 0;
972                                 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
973
974                                 this.owner.impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
975                                 ImpulseCommands(this.owner);
976                         }
977                         if(autocvar_g_ctf_stalemate)
978                         {
979                                 if(time >= wpforenemy_nextthink)
980                                 {
981                                         ctf_CheckStalemate();
982                                         wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
983                                 }
984                         }
985                         if(CTF_SAMETEAM(this, this.owner) && this.team)
986                         {
987                                 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
988                                         ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
989                                 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
990                                         ctf_Handle_Return(this, this.owner);
991                         }
992                         return;
993                 }
994
995                 case FLAG_PASSING:
996                 {
997                         vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
998                         targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
999                         WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1000
1001                         if((this.pass_target == NULL)
1002                                 || (IS_DEAD(this.pass_target))
1003                                 || (this.pass_target.flagcarried)
1004                                 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1005                                 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1006                                 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1007                         {
1008                                 // give up, pass failed
1009                                 ctf_Handle_Drop(this, NULL, DROP_PASS);
1010                         }
1011                         else
1012                         {
1013                                 // still a viable target, go for it
1014                                 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1015                         }
1016                         return;
1017                 }
1018
1019                 default: // this should never happen
1020                 {
1021                         LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1022                         return;
1023                 }
1024         }
1025 }
1026
1027 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1028 {
1029         return = false;
1030         if(gameover) { return; }
1031         if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1032
1033         bool is_not_monster = (!IS_MONSTER(toucher));
1034
1035         // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1036         if(ITEM_TOUCH_NEEDKILL())
1037         {
1038                 if(!autocvar_g_ctf_flag_return_damage_delay)
1039                 {
1040                         flag.health = 0;
1041                         ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1042                 }
1043                 if(!flag.ctf_flagdamaged) { return; }
1044         }
1045
1046         int num_perteam = 0;
1047         FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), LAMBDA(++num_perteam));
1048
1049         // special touch behaviors
1050         if(STAT(FROZEN, toucher)) { return; }
1051         else if(IS_VEHICLE(toucher))
1052         {
1053                 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1054                         toucher = toucher.owner; // the player is actually the vehicle owner, not other
1055                 else
1056                         return; // do nothing
1057         }
1058         else if(IS_MONSTER(toucher))
1059         {
1060                 if(!autocvar_g_ctf_allow_monster_touch)
1061                         return; // do nothing
1062         }
1063         else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1064         {
1065                 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1066                 {
1067                         Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1068                         _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1069                         flag.wait = time + FLAG_TOUCHRATE;
1070                 }
1071                 return;
1072         }
1073         else if(IS_DEAD(toucher)) { return; }
1074
1075         switch(flag.ctf_status)
1076         {
1077                 case FLAG_BASE:
1078                 {
1079                         if(ctf_oneflag)
1080                         {
1081                                 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1082                                         ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1083                                 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1084                                         ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1085                         }
1086                         else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1087                                 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1088                         else if(CTF_DIFFTEAM(toucher, flag) && (toucher.flagcarried) && CTF_SAMETEAM(toucher.flagcarried, toucher) && (!toucher.ctf_captureshielded) && autocvar_g_ctf_flag_return_carrying && (time > toucher.next_take_time) && is_not_monster)
1089                         {
1090                                 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1091                                 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1092                         }
1093                         else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1094                                 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1095                         break;
1096                 }
1097
1098                 case FLAG_DROPPED:
1099                 {
1100                         if(CTF_SAMETEAM(toucher, flag) && (autocvar_g_ctf_flag_return || num_perteam <= 1 || (autocvar_g_ctf_flag_return_carrying && toucher.flagcarried)) && flag.team) // automatically return if there's only 1 player on the team
1101                                 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1102                         else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1103                                 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1104                         break;
1105                 }
1106
1107                 case FLAG_CARRY:
1108                 {
1109                         LOG_TRACE("Someone touched a flag even though it was being carried?");
1110                         break;
1111                 }
1112
1113                 case FLAG_PASSING:
1114                 {
1115                         if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1116                         {
1117                                 if(DIFF_TEAM(toucher, flag.pass_sender))
1118                                         ctf_Handle_Return(flag, toucher);
1119                                 else
1120                                         ctf_Handle_Retrieve(flag, toucher);
1121                         }
1122                         break;
1123                 }
1124         }
1125 }
1126
1127 .float last_respawn;
1128 void ctf_RespawnFlag(entity flag)
1129 {
1130         // check for flag respawn being called twice in a row
1131         if(flag.last_respawn > time - 0.5)
1132                 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1133
1134         flag.last_respawn = time;
1135
1136         // reset the player (if there is one)
1137         if((flag.owner) && (flag.owner.flagcarried == flag))
1138         {
1139                 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1140                 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1141                 WaypointSprite_Kill(flag.wps_flagcarrier);
1142
1143                 flag.owner.flagcarried = NULL;
1144
1145                 if(flag.speedrunning)
1146                         ctf_FakeTimeLimit(flag.owner, -1);
1147         }
1148
1149         if((flag.owner) && (flag.owner.vehicle))
1150                 flag.scale = FLAG_SCALE;
1151
1152         if(flag.ctf_status == FLAG_DROPPED)
1153                 { WaypointSprite_Kill(flag.wps_flagdropped); }
1154
1155         // reset the flag
1156         setattachment(flag, NULL, "");
1157         setorigin(flag, flag.ctf_spawnorigin);
1158
1159         set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS));
1160         flag.takedamage = DAMAGE_NO;
1161         flag.health = flag.max_flag_health;
1162         flag.solid = SOLID_TRIGGER;
1163         flag.velocity = '0 0 0';
1164         flag.angles = flag.mangle;
1165         flag.flags = FL_ITEM | FL_NOTARGET;
1166
1167         flag.ctf_status = FLAG_BASE;
1168         flag.owner = NULL;
1169         flag.pass_distance = 0;
1170         flag.pass_sender = NULL;
1171         flag.pass_target = NULL;
1172         flag.ctf_dropper = NULL;
1173         flag.ctf_pickuptime = 0;
1174         flag.ctf_droptime = 0;
1175         flag.ctf_flagdamaged = 0;
1176
1177         ctf_CheckStalemate();
1178 }
1179
1180 void ctf_Reset(entity this)
1181 {
1182         if(this.owner && IS_PLAYER(this.owner))
1183         ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1184
1185         ctf_RespawnFlag(this);
1186 }
1187
1188 bool ctf_FlagBase_Customize(entity this, entity client)
1189 {
1190         if(client.flagcarried && CTF_SAMETEAM(client, client.flagcarried))
1191                 return false;
1192         return true;
1193 }
1194
1195 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1196 {
1197         // bot waypoints
1198         waypoint_spawnforitem_force(this, this.origin);
1199         this.nearestwaypointtimeout = 0; // activate waypointing again
1200         this.bot_basewaypoint = this.nearestwaypoint;
1201
1202         // waypointsprites
1203         entity basename;
1204         switch (this.team)
1205         {
1206                 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1207                 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1208                 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1209                 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1210                 default: basename = WP_FlagBaseNeutral; break;
1211         }
1212
1213         entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1214         wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1215         WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1216         setcefc(wp, ctf_FlagBase_Customize);
1217
1218         // captureshield setup
1219         ctf_CaptureShield_Spawn(this);
1220 }
1221
1222 .bool pushable;
1223
1224 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1225 {
1226         // main setup
1227         flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1228         ctf_worldflaglist = flag;
1229
1230         setattachment(flag, NULL, "");
1231
1232         flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1233         flag.team = teamnumber;
1234         flag.classname = "item_flag_team";
1235         flag.target = "###item###"; // wut?
1236         flag.flags = FL_ITEM | FL_NOTARGET;
1237         IL_PUSH(g_items, flag);
1238         flag.solid = SOLID_TRIGGER;
1239         flag.takedamage = DAMAGE_NO;
1240         flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1241         flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1242         flag.health = flag.max_flag_health;
1243         flag.event_damage = ctf_FlagDamage;
1244         flag.pushable = true;
1245         flag.teleportable = TELEPORT_NORMAL;
1246         flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1247         flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1248         flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1249         if(flag.damagedbycontents)
1250                 IL_PUSH(g_damagedbycontents, flag);
1251         flag.velocity = '0 0 0';
1252         flag.mangle = flag.angles;
1253         flag.reset = ctf_Reset;
1254         settouch(flag, ctf_FlagTouch);
1255         setthink(flag, ctf_FlagThink);
1256         flag.nextthink = time + FLAG_THINKRATE;
1257         flag.ctf_status = FLAG_BASE;
1258
1259         string teamname = Static_Team_ColorName_Lower(teamnumber);
1260         // appearence
1261         if(!flag.scale)                         { flag.scale = FLAG_SCALE; }
1262         if(flag.skin == 0)                      { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1263         if(flag.model == "")            { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1264         if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnumber).eent_eff_name; }
1265         if (flag.passeffect == "")      { flag.passeffect = EFFECT_PASS(teamnumber).eent_eff_name; }
1266         if (flag.capeffect == "")       { flag.capeffect = EFFECT_CAP(teamnumber).eent_eff_name; }
1267
1268         // sounds
1269 #define X(s,b) \
1270                 if(flag.s == "") flag.s = b; \
1271                 precache_sound(flag.s);
1272
1273         X(snd_flag_taken,               strzone(SND(CTF_TAKEN(teamnumber))))
1274         X(snd_flag_returned,    strzone(SND(CTF_RETURNED(teamnumber))))
1275         X(snd_flag_capture,     strzone(SND(CTF_CAPTURE(teamnumber))))
1276         X(snd_flag_dropped,     strzone(SND(CTF_DROPPED(teamnumber))))
1277         X(snd_flag_respawn,     strzone(SND(CTF_RESPAWN)))
1278         X(snd_flag_touch,               strzone(SND(CTF_TOUCH)))
1279         X(snd_flag_pass,                strzone(SND(CTF_PASS)))
1280 #undef X
1281
1282         // precache
1283         precache_model(flag.model);
1284
1285         // appearence
1286         _setmodel(flag, flag.model); // precision set below
1287         setsize(flag, CTF_FLAG.m_mins, CTF_FLAG.m_maxs);
1288         setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1289
1290         if(autocvar_g_ctf_flag_glowtrails)
1291         {
1292                 switch(teamnumber)
1293                 {
1294                         case NUM_TEAM_1: flag.glow_color = 251; break;
1295                         case NUM_TEAM_2: flag.glow_color = 210; break;
1296                         case NUM_TEAM_3: flag.glow_color = 110; break;
1297                         case NUM_TEAM_4: flag.glow_color = 145; break;
1298                         default:                 flag.glow_color = 254; break;
1299                 }
1300                 flag.glow_size = 25;
1301                 flag.glow_trail = 1;
1302         }
1303
1304         flag.effects |= EF_LOWPRECISION;
1305         if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1306         if(autocvar_g_ctf_dynamiclights)
1307         {
1308                 switch(teamnumber)
1309                 {
1310                         case NUM_TEAM_1: flag.effects |= EF_RED; break;
1311                         case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1312                         case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1313                         case NUM_TEAM_4: flag.effects |= EF_RED; break;
1314                         default:                 flag.effects |= EF_DIMLIGHT; break;
1315                 }
1316         }
1317
1318         // flag placement
1319         if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1320         {
1321                 flag.dropped_origin = flag.origin;
1322                 flag.noalign = true;
1323                 set_movetype(flag, MOVETYPE_NONE);
1324         }
1325         else // drop to floor, automatically find a platform and set that as spawn origin
1326         {
1327                 flag.noalign = false;
1328                 droptofloor(flag);
1329                 set_movetype(flag, MOVETYPE_NONE);
1330         }
1331
1332         InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1333 }
1334
1335
1336 // ================
1337 // Bot player logic
1338 // ================
1339
1340 // NOTE: LEGACY CODE, needs to be re-written!
1341
1342 void havocbot_calculate_middlepoint()
1343 {
1344         entity f;
1345         vector s = '0 0 0';
1346         vector fo = '0 0 0';
1347         float n = 0;
1348
1349         f = ctf_worldflaglist;
1350         while (f)
1351         {
1352                 fo = f.origin;
1353                 s = s + fo;
1354                 f = f.ctf_worldflagnext;
1355         }
1356         if(!n)
1357                 return;
1358         havocbot_ctf_middlepoint = s * (1.0 / n);
1359         havocbot_ctf_middlepoint_radius  = vlen(fo - havocbot_ctf_middlepoint);
1360 }
1361
1362
1363 entity havocbot_ctf_find_flag(entity bot)
1364 {
1365         entity f;
1366         f = ctf_worldflaglist;
1367         while (f)
1368         {
1369                 if (CTF_SAMETEAM(bot, f))
1370                         return f;
1371                 f = f.ctf_worldflagnext;
1372         }
1373         return NULL;
1374 }
1375
1376 entity havocbot_ctf_find_enemy_flag(entity bot)
1377 {
1378         entity f;
1379         f = ctf_worldflaglist;
1380         while (f)
1381         {
1382                 if(ctf_oneflag)
1383                 {
1384                         if(CTF_DIFFTEAM(bot, f))
1385                         {
1386                                 if(f.team)
1387                                 {
1388                                         if(bot.flagcarried)
1389                                                 return f;
1390                                 }
1391                                 else if(!bot.flagcarried)
1392                                         return f;
1393                         }
1394                 }
1395                 else if (CTF_DIFFTEAM(bot, f))
1396                         return f;
1397                 f = f.ctf_worldflagnext;
1398         }
1399         return NULL;
1400 }
1401
1402 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1403 {
1404         if (!teamplay)
1405                 return 0;
1406
1407         int c = 0;
1408
1409         FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
1410                 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1411                         continue;
1412
1413                 if(vdist(it.origin - org, <, tc_radius))
1414                         ++c;
1415         ));
1416
1417         return c;
1418 }
1419
1420 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1421 {
1422         entity head;
1423         head = ctf_worldflaglist;
1424         while (head)
1425         {
1426                 if (CTF_SAMETEAM(this, head))
1427                         break;
1428                 head = head.ctf_worldflagnext;
1429         }
1430         if (head)
1431                 navigation_routerating(this, head, ratingscale, 10000);
1432 }
1433
1434 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1435 {
1436         entity head;
1437         head = ctf_worldflaglist;
1438         while (head)
1439         {
1440                 if (CTF_SAMETEAM(this, head))
1441                         break;
1442                 head = head.ctf_worldflagnext;
1443         }
1444         if (!head)
1445                 return;
1446
1447         navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1448 }
1449
1450 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1451 {
1452         entity head;
1453         head = ctf_worldflaglist;
1454         while (head)
1455         {
1456                 if(ctf_oneflag)
1457                 {
1458                         if(CTF_DIFFTEAM(this, head))
1459                         {
1460                                 if(head.team)
1461                                 {
1462                                         if(this.flagcarried)
1463                                                 break;
1464                                 }
1465                                 else if(!this.flagcarried)
1466                                         break;
1467                         }
1468                 }
1469                 else if(CTF_DIFFTEAM(this, head))
1470                         break;
1471                 head = head.ctf_worldflagnext;
1472         }
1473         if (head)
1474                 navigation_routerating(this, head, ratingscale, 10000);
1475 }
1476
1477 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1478 {
1479         if (!bot_waypoints_for_items)
1480         {
1481                 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1482                 return;
1483         }
1484
1485         entity head;
1486
1487         head = havocbot_ctf_find_enemy_flag(this);
1488
1489         if (!head)
1490                 return;
1491
1492         navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1493 }
1494
1495 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1496 {
1497         entity mf;
1498
1499         mf = havocbot_ctf_find_flag(this);
1500
1501         if(mf.ctf_status == FLAG_BASE)
1502                 return;
1503
1504         if(mf.tag_entity)
1505                 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1506 }
1507
1508 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1509 {
1510         entity head;
1511         head = ctf_worldflaglist;
1512         while (head)
1513         {
1514                 // flag is out in the field
1515                 if(head.ctf_status != FLAG_BASE)
1516                 if(head.tag_entity==NULL)       // dropped
1517                 {
1518                         if(df_radius)
1519                         {
1520                                 if(vdist(org - head.origin, <, df_radius))
1521                                         navigation_routerating(this, head, ratingscale, 10000);
1522                         }
1523                         else
1524                                 navigation_routerating(this, head, ratingscale, 10000);
1525                 }
1526
1527                 head = head.ctf_worldflagnext;
1528         }
1529 }
1530
1531 void havocbot_goalrating_ctf_carrieritems(entity this, float ratingscale, vector org, float sradius)
1532 {
1533         IL_EACH(g_items, it.bot_pickup,
1534         {
1535                 // gather health and armor only
1536                 if (it.solid)
1537                 if (it.health || it.armorvalue)
1538                 if (vdist(it.origin - org, <, sradius))
1539                 {
1540                         // get the value of the item
1541                         float t = it.bot_pickupevalfunc(this, it) * 0.0001;
1542                         if (t > 0)
1543                                 navigation_routerating(this, it, t * ratingscale, 500);
1544                 }
1545         });
1546 }
1547
1548 void havocbot_ctf_reset_role(entity this)
1549 {
1550         float cdefense, cmiddle, coffense;
1551         entity mf, ef;
1552         float c;
1553
1554         if(IS_DEAD(this))
1555                 return;
1556
1557         if(havocbot_ctf_middlepoint == '0 0 0')
1558                 havocbot_calculate_middlepoint();
1559
1560         // Check ctf flags
1561         if (this.flagcarried)
1562         {
1563                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1564                 return;
1565         }
1566
1567         mf = havocbot_ctf_find_flag(this);
1568         ef = havocbot_ctf_find_enemy_flag(this);
1569
1570         // Retrieve stolen flag
1571         if(mf.ctf_status!=FLAG_BASE)
1572         {
1573                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1574                 return;
1575         }
1576
1577         // If enemy flag is taken go to the middle to intercept pursuers
1578         if(ef.ctf_status!=FLAG_BASE)
1579         {
1580                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1581                 return;
1582         }
1583
1584         // if there is only me on the team switch to offense
1585         c = 0;
1586         FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(it, this), LAMBDA(++c));
1587
1588         if(c==1)
1589         {
1590                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1591                 return;
1592         }
1593
1594         // Evaluate best position to take
1595         // Count mates on middle position
1596         cmiddle = havocbot_ctf_teamcount(this, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1597
1598         // Count mates on defense position
1599         cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1600
1601         // Count mates on offense position
1602         coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1603
1604         if(cdefense<=coffense)
1605                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1606         else if(coffense<=cmiddle)
1607                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1608         else
1609                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1610 }
1611
1612 void havocbot_role_ctf_carrier(entity this)
1613 {
1614         if(IS_DEAD(this))
1615         {
1616                 havocbot_ctf_reset_role(this);
1617                 return;
1618         }
1619
1620         if (this.flagcarried == NULL)
1621         {
1622                 havocbot_ctf_reset_role(this);
1623                 return;
1624         }
1625
1626         if (this.bot_strategytime < time)
1627         {
1628                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1629
1630                 navigation_goalrating_start(this);
1631                 if(ctf_oneflag)
1632                         havocbot_goalrating_ctf_enemybase(this, 50000);
1633                 else
1634                         havocbot_goalrating_ctf_ourbase(this, 50000);
1635
1636                 if(this.health<100)
1637                         havocbot_goalrating_ctf_carrieritems(this, 1000, this.origin, 1000);
1638
1639                 navigation_goalrating_end(this);
1640
1641                 if (this.navigation_hasgoals)
1642                         this.havocbot_cantfindflag = time + 10;
1643                 else if (time > this.havocbot_cantfindflag)
1644                 {
1645                         // Can't navigate to my own base, suicide!
1646                         // TODO: drop it and wander around
1647                         Damage(this, this, this, 100000, DEATH_KILL.m_id, this.origin, '0 0 0');
1648                         return;
1649                 }
1650         }
1651 }
1652
1653 void havocbot_role_ctf_escort(entity this)
1654 {
1655         entity mf, ef;
1656
1657         if(IS_DEAD(this))
1658         {
1659                 havocbot_ctf_reset_role(this);
1660                 return;
1661         }
1662
1663         if (this.flagcarried)
1664         {
1665                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1666                 return;
1667         }
1668
1669         // If enemy flag is back on the base switch to previous role
1670         ef = havocbot_ctf_find_enemy_flag(this);
1671         if(ef.ctf_status==FLAG_BASE)
1672         {
1673                 this.havocbot_role = this.havocbot_previous_role;
1674                 this.havocbot_role_timeout = 0;
1675                 return;
1676         }
1677
1678         // If the flag carrier reached the base switch to defense
1679         mf = havocbot_ctf_find_flag(this);
1680         if(mf.ctf_status!=FLAG_BASE)
1681         if(vdist(ef.origin - mf.dropped_origin, <, 300))
1682         {
1683                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1684                 return;
1685         }
1686
1687         // Set the role timeout if necessary
1688         if (!this.havocbot_role_timeout)
1689         {
1690                 this.havocbot_role_timeout = time + random() * 30 + 60;
1691         }
1692
1693         // If nothing happened just switch to previous role
1694         if (time > this.havocbot_role_timeout)
1695         {
1696                 this.havocbot_role = this.havocbot_previous_role;
1697                 this.havocbot_role_timeout = 0;
1698                 return;
1699         }
1700
1701         // Chase the flag carrier
1702         if (this.bot_strategytime < time)
1703         {
1704                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1705                 navigation_goalrating_start(this);
1706                 havocbot_goalrating_ctf_enemyflag(this, 30000);
1707                 havocbot_goalrating_ctf_ourstolenflag(this, 40000);
1708                 havocbot_goalrating_items(this, 10000, this.origin, 10000);
1709                 navigation_goalrating_end(this);
1710         }
1711 }
1712
1713 void havocbot_role_ctf_offense(entity this)
1714 {
1715         entity mf, ef;
1716         vector pos;
1717
1718         if(IS_DEAD(this))
1719         {
1720                 havocbot_ctf_reset_role(this);
1721                 return;
1722         }
1723
1724         if (this.flagcarried)
1725         {
1726                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1727                 return;
1728         }
1729
1730         // Check flags
1731         mf = havocbot_ctf_find_flag(this);
1732         ef = havocbot_ctf_find_enemy_flag(this);
1733
1734         // Own flag stolen
1735         if(mf.ctf_status!=FLAG_BASE)
1736         {
1737                 if(mf.tag_entity)
1738                         pos = mf.tag_entity.origin;
1739                 else
1740                         pos = mf.origin;
1741
1742                 // Try to get it if closer than the enemy base
1743                 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1744                 {
1745                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1746                         return;
1747                 }
1748         }
1749
1750         // Escort flag carrier
1751         if(ef.ctf_status!=FLAG_BASE)
1752         {
1753                 if(ef.tag_entity)
1754                         pos = ef.tag_entity.origin;
1755                 else
1756                         pos = ef.origin;
1757
1758                 if(vdist(pos - mf.dropped_origin, >, 700))
1759                 {
1760                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1761                         return;
1762                 }
1763         }
1764
1765         // About to fail, switch to middlefield
1766         if(this.health<50)
1767         {
1768                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1769                 return;
1770         }
1771
1772         // Set the role timeout if necessary
1773         if (!this.havocbot_role_timeout)
1774                 this.havocbot_role_timeout = time + 120;
1775
1776         if (time > this.havocbot_role_timeout)
1777         {
1778                 havocbot_ctf_reset_role(this);
1779                 return;
1780         }
1781
1782         if (this.bot_strategytime < time)
1783         {
1784                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1785                 navigation_goalrating_start(this);
1786                 havocbot_goalrating_ctf_ourstolenflag(this, 50000);
1787                 havocbot_goalrating_ctf_enemybase(this, 20000);
1788                 havocbot_goalrating_items(this, 5000, this.origin, 1000);
1789                 havocbot_goalrating_items(this, 1000, this.origin, 10000);
1790                 navigation_goalrating_end(this);
1791         }
1792 }
1793
1794 // Retriever (temporary role):
1795 void havocbot_role_ctf_retriever(entity this)
1796 {
1797         entity mf;
1798
1799         if(IS_DEAD(this))
1800         {
1801                 havocbot_ctf_reset_role(this);
1802                 return;
1803         }
1804
1805         if (this.flagcarried)
1806         {
1807                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1808                 return;
1809         }
1810
1811         // If flag is back on the base switch to previous role
1812         mf = havocbot_ctf_find_flag(this);
1813         if(mf.ctf_status==FLAG_BASE)
1814         {
1815                 havocbot_ctf_reset_role(this);
1816                 return;
1817         }
1818
1819         if (!this.havocbot_role_timeout)
1820                 this.havocbot_role_timeout = time + 20;
1821
1822         if (time > this.havocbot_role_timeout)
1823         {
1824                 havocbot_ctf_reset_role(this);
1825                 return;
1826         }
1827
1828         if (this.bot_strategytime < time)
1829         {
1830                 float rt_radius;
1831                 rt_radius = 10000;
1832
1833                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1834                 navigation_goalrating_start(this);
1835                 havocbot_goalrating_ctf_ourstolenflag(this, 50000);
1836                 havocbot_goalrating_ctf_droppedflags(this, 40000, this.origin, rt_radius);
1837                 havocbot_goalrating_ctf_enemybase(this, 30000);
1838                 havocbot_goalrating_items(this, 500, this.origin, rt_radius);
1839                 navigation_goalrating_end(this);
1840         }
1841 }
1842
1843 void havocbot_role_ctf_middle(entity this)
1844 {
1845         entity mf;
1846
1847         if(IS_DEAD(this))
1848         {
1849                 havocbot_ctf_reset_role(this);
1850                 return;
1851         }
1852
1853         if (this.flagcarried)
1854         {
1855                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1856                 return;
1857         }
1858
1859         mf = havocbot_ctf_find_flag(this);
1860         if(mf.ctf_status!=FLAG_BASE)
1861         {
1862                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1863                 return;
1864         }
1865
1866         if (!this.havocbot_role_timeout)
1867                 this.havocbot_role_timeout = time + 10;
1868
1869         if (time > this.havocbot_role_timeout)
1870         {
1871                 havocbot_ctf_reset_role(this);
1872                 return;
1873         }
1874
1875         if (this.bot_strategytime < time)
1876         {
1877                 vector org;
1878
1879                 org = havocbot_ctf_middlepoint;
1880                 org.z = this.origin.z;
1881
1882                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1883                 navigation_goalrating_start(this);
1884                 havocbot_goalrating_ctf_ourstolenflag(this, 50000);
1885                 havocbot_goalrating_ctf_droppedflags(this, 30000, this.origin, 10000);
1886                 havocbot_goalrating_enemyplayers(this, 10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1887                 havocbot_goalrating_items(this, 5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1888                 havocbot_goalrating_items(this, 2500, this.origin, 10000);
1889                 havocbot_goalrating_ctf_enemybase(this, 2500);
1890                 navigation_goalrating_end(this);
1891         }
1892 }
1893
1894 void havocbot_role_ctf_defense(entity this)
1895 {
1896         entity mf;
1897
1898         if(IS_DEAD(this))
1899         {
1900                 havocbot_ctf_reset_role(this);
1901                 return;
1902         }
1903
1904         if (this.flagcarried)
1905         {
1906                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1907                 return;
1908         }
1909
1910         // If own flag was captured
1911         mf = havocbot_ctf_find_flag(this);
1912         if(mf.ctf_status!=FLAG_BASE)
1913         {
1914                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1915                 return;
1916         }
1917
1918         if (!this.havocbot_role_timeout)
1919                 this.havocbot_role_timeout = time + 30;
1920
1921         if (time > this.havocbot_role_timeout)
1922         {
1923                 havocbot_ctf_reset_role(this);
1924                 return;
1925         }
1926         if (this.bot_strategytime < time)
1927         {
1928                 float mp_radius;
1929                 vector org;
1930
1931                 org = mf.dropped_origin;
1932                 mp_radius = havocbot_ctf_middlepoint_radius;
1933
1934                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1935                 navigation_goalrating_start(this);
1936
1937                 // if enemies are closer to our base, go there
1938                 entity closestplayer = NULL;
1939                 float distance, bestdistance = 10000;
1940                 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), LAMBDA(
1941                         distance = vlen(org - it.origin);
1942                         if(distance<bestdistance)
1943                         {
1944                                 closestplayer = it;
1945                                 bestdistance = distance;
1946                         }
1947                 ));
1948
1949                 if(closestplayer)
1950                 if(DIFF_TEAM(closestplayer, this))
1951                 if(vdist(org - this.origin, >, 1000))
1952                 if(checkpvs(this.origin,closestplayer)||random()<0.5)
1953                         havocbot_goalrating_ctf_ourbase(this, 30000);
1954
1955                 havocbot_goalrating_ctf_ourstolenflag(this, 20000);
1956                 havocbot_goalrating_ctf_droppedflags(this, 20000, org, mp_radius);
1957                 havocbot_goalrating_enemyplayers(this, 15000, org, mp_radius);
1958                 havocbot_goalrating_items(this, 10000, org, mp_radius);
1959                 havocbot_goalrating_items(this, 5000, this.origin, 10000);
1960                 navigation_goalrating_end(this);
1961         }
1962 }
1963
1964 void havocbot_role_ctf_setrole(entity bot, int role)
1965 {
1966         string s = "(null)";
1967         switch(role)
1968         {
1969                 case HAVOCBOT_CTF_ROLE_CARRIER:
1970                         s = "carrier";
1971                         bot.havocbot_role = havocbot_role_ctf_carrier;
1972                         bot.havocbot_role_timeout = 0;
1973                         bot.havocbot_cantfindflag = time + 10;
1974                         bot.bot_strategytime = 0;
1975                         break;
1976                 case HAVOCBOT_CTF_ROLE_DEFENSE:
1977                         s = "defense";
1978                         bot.havocbot_role = havocbot_role_ctf_defense;
1979                         bot.havocbot_role_timeout = 0;
1980                         break;
1981                 case HAVOCBOT_CTF_ROLE_MIDDLE:
1982                         s = "middle";
1983                         bot.havocbot_role = havocbot_role_ctf_middle;
1984                         bot.havocbot_role_timeout = 0;
1985                         break;
1986                 case HAVOCBOT_CTF_ROLE_OFFENSE:
1987                         s = "offense";
1988                         bot.havocbot_role = havocbot_role_ctf_offense;
1989                         bot.havocbot_role_timeout = 0;
1990                         break;
1991                 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1992                         s = "retriever";
1993                         bot.havocbot_previous_role = bot.havocbot_role;
1994                         bot.havocbot_role = havocbot_role_ctf_retriever;
1995                         bot.havocbot_role_timeout = time + 10;
1996                         bot.bot_strategytime = 0;
1997                         break;
1998                 case HAVOCBOT_CTF_ROLE_ESCORT:
1999                         s = "escort";
2000                         bot.havocbot_previous_role = bot.havocbot_role;
2001                         bot.havocbot_role = havocbot_role_ctf_escort;
2002                         bot.havocbot_role_timeout = time + 30;
2003                         bot.bot_strategytime = 0;
2004                         break;
2005         }
2006         LOG_TRACE(bot.netname, " switched to ", s);
2007 }
2008
2009
2010 // ==============
2011 // Hook Functions
2012 // ==============
2013
2014 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2015 {
2016         entity player = M_ARGV(0, entity);
2017
2018         int t = 0, t2 = 0, t3 = 0;
2019
2020         // initially clear items so they can be set as necessary later.
2021         player.ctf_flagstatus &= ~(CTF_RED_FLAG_CARRYING                | CTF_RED_FLAG_TAKEN            | CTF_RED_FLAG_LOST
2022                                                    | CTF_BLUE_FLAG_CARRYING             | CTF_BLUE_FLAG_TAKEN           | CTF_BLUE_FLAG_LOST
2023                                                    | CTF_YELLOW_FLAG_CARRYING   | CTF_YELLOW_FLAG_TAKEN         | CTF_YELLOW_FLAG_LOST
2024                                                    | CTF_PINK_FLAG_CARRYING     | CTF_PINK_FLAG_TAKEN           | CTF_PINK_FLAG_LOST
2025                                                    | CTF_NEUTRAL_FLAG_CARRYING  | CTF_NEUTRAL_FLAG_TAKEN        | CTF_NEUTRAL_FLAG_LOST
2026                                                    | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2027
2028         // scan through all the flags and notify the client about them
2029         for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2030         {
2031                 if(flag.team == NUM_TEAM_1) { t = CTF_RED_FLAG_CARRYING;                t2 = CTF_RED_FLAG_TAKEN;                t3 = CTF_RED_FLAG_LOST; }
2032                 if(flag.team == NUM_TEAM_2) { t = CTF_BLUE_FLAG_CARRYING;               t2 = CTF_BLUE_FLAG_TAKEN;               t3 = CTF_BLUE_FLAG_LOST; }
2033                 if(flag.team == NUM_TEAM_3) { t = CTF_YELLOW_FLAG_CARRYING;     t2 = CTF_YELLOW_FLAG_TAKEN;             t3 = CTF_YELLOW_FLAG_LOST; }
2034                 if(flag.team == NUM_TEAM_4) { t = CTF_PINK_FLAG_CARRYING;               t2 = CTF_PINK_FLAG_TAKEN;               t3 = CTF_PINK_FLAG_LOST; }
2035                 if(flag.team == 0)                      { t = CTF_NEUTRAL_FLAG_CARRYING;        t2 = CTF_NEUTRAL_FLAG_TAKEN;    t3 = CTF_NEUTRAL_FLAG_LOST; player.ctf_flagstatus |= CTF_FLAG_NEUTRAL; }
2036
2037                 switch(flag.ctf_status)
2038                 {
2039                         case FLAG_PASSING:
2040                         case FLAG_CARRY:
2041                         {
2042                                 if((flag.owner == player) || (flag.pass_sender == player))
2043                                         player.ctf_flagstatus |= t; // carrying: player is currently carrying the flag
2044                                 else
2045                                         player.ctf_flagstatus |= t2; // taken: someone else is carrying the flag
2046                                 break;
2047                         }
2048                         case FLAG_DROPPED:
2049                         {
2050                                 player.ctf_flagstatus |= t3; // lost: the flag is dropped somewhere on the map
2051                                 break;
2052                         }
2053                 }
2054         }
2055
2056         // item for stopping players from capturing the flag too often
2057         if(player.ctf_captureshielded)
2058                 player.ctf_flagstatus |= CTF_SHIELDED;
2059
2060         if(ctf_stalemate)
2061                 player.ctf_flagstatus |= CTF_STALEMATE;
2062
2063         // update the health of the flag carrier waypointsprite
2064         if(player.wps_flagcarrier)
2065                 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
2066 }
2067
2068 MUTATOR_HOOKFUNCTION(ctf, PlayerDamage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2069 {
2070         entity frag_attacker = M_ARGV(1, entity);
2071         entity frag_target = M_ARGV(2, entity);
2072         float frag_damage = M_ARGV(4, float);
2073         vector frag_force = M_ARGV(6, vector);
2074
2075         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2076         {
2077                 if(frag_target == frag_attacker) // damage done to yourself
2078                 {
2079                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2080                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2081                 }
2082                 else // damage done to everyone else
2083                 {
2084                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2085                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2086                 }
2087
2088                 M_ARGV(4, float) = frag_damage;
2089                 M_ARGV(6, vector) = frag_force;
2090         }
2091         else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2092         {
2093                 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.m_id)))
2094                 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2095                 {
2096                         frag_target.wps_helpme_time = time;
2097                         WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2098                 }
2099                 // todo: add notification for when flag carrier needs help?
2100         }
2101 }
2102
2103 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2104 {
2105         entity frag_attacker = M_ARGV(1, entity);
2106         entity frag_target = M_ARGV(2, entity);
2107
2108         if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2109         {
2110                 PlayerTeamScore_AddScore(frag_attacker, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2111                 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
2112         }
2113
2114         if(frag_target.flagcarried)
2115         {
2116                 entity tmp_entity = frag_target.flagcarried;
2117                 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2118                 tmp_entity.ctf_dropper = NULL;
2119         }
2120 }
2121
2122 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2123 {
2124         M_ARGV(2, float) = 0; // frag score
2125         return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2126 }
2127
2128 void ctf_RemovePlayer(entity player)
2129 {
2130         if(player.flagcarried)
2131                 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2132
2133         for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2134         {
2135                 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2136                 if(flag.pass_target == player) { flag.pass_target = NULL; }
2137                 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2138         }
2139 }
2140
2141 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2142 {
2143         entity player = M_ARGV(0, entity);
2144
2145         ctf_RemovePlayer(player);
2146 }
2147
2148 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2149 {
2150         entity player = M_ARGV(0, entity);
2151
2152         ctf_RemovePlayer(player);
2153 }
2154
2155 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2156 {
2157         entity player = M_ARGV(0, entity);
2158
2159         if(player.flagcarried)
2160         if(!autocvar_g_ctf_portalteleport)
2161                 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2162 }
2163
2164 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2165 {
2166         if(MUTATOR_RETURNVALUE || gameover) { return; }
2167
2168         entity player = M_ARGV(0, entity);
2169
2170         if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2171         {
2172                 // pass the flag to a team mate
2173                 if(autocvar_g_ctf_pass)
2174                 {
2175                         entity head, closest_target = NULL;
2176                         head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2177
2178                         while(head) // find the closest acceptable target to pass to
2179                         {
2180                                 if(IS_PLAYER(head) && !IS_DEAD(head))
2181                                 if(head != player && SAME_TEAM(head, player))
2182                                 if(!head.speedrunning && !head.vehicle)
2183                                 {
2184                                         // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2185                                         vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2186                                         vector passer_center = CENTER_OR_VIEWOFS(player);
2187
2188                                         if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2189                                         {
2190                                                 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2191                                                 {
2192                                                         if(IS_BOT_CLIENT(head))
2193                                                         {
2194                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2195                                                                 ctf_Handle_Throw(head, player, DROP_PASS);
2196                                                         }
2197                                                         else
2198                                                         {
2199                                                                 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2200                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2201                                                         }
2202                                                         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2203                                                         return true;
2204                                                 }
2205                                                 else if(player.flagcarried)
2206                                                 {
2207                                                         if(closest_target)
2208                                                         {
2209                                                                 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2210                                                                 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2211                                                                         { closest_target = head; }
2212                                                         }
2213                                                         else { closest_target = head; }
2214                                                 }
2215                                         }
2216                                 }
2217                                 head = head.chain;
2218                         }
2219
2220                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2221                 }
2222
2223                 // throw the flag in front of you
2224                 if(autocvar_g_ctf_throw && player.flagcarried)
2225                 {
2226                         if(player.throw_count == -1)
2227                         {
2228                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2229                                 {
2230                                         player.throw_prevtime = time;
2231                                         player.throw_count = 1;
2232                                         ctf_Handle_Throw(player, NULL, DROP_THROW);
2233                                         return true;
2234                                 }
2235                                 else
2236                                 {
2237                                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2238                                         return false;
2239                                 }
2240                         }
2241                         else
2242                         {
2243                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2244                                 else { player.throw_count += 1; }
2245                                 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2246
2247                                 player.throw_prevtime = time;
2248                                 ctf_Handle_Throw(player, NULL, DROP_THROW);
2249                                 return true;
2250                         }
2251                 }
2252         }
2253 }
2254
2255 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2256 {
2257         entity player = M_ARGV(0, entity);
2258
2259         if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2260         {
2261                 player.wps_helpme_time = time;
2262                 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2263         }
2264         else // create a normal help me waypointsprite
2265         {
2266                 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2267                 WaypointSprite_Ping(player.wps_helpme);
2268         }
2269
2270         return true;
2271 }
2272
2273 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2274 {
2275         entity player = M_ARGV(0, entity);
2276         entity veh = M_ARGV(1, entity);
2277
2278         if(player.flagcarried)
2279         {
2280                 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2281                 {
2282                         ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2283                 }
2284                 else
2285                 {
2286                         player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2287                         setattachment(player.flagcarried, veh, "");
2288                         setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2289                         player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2290                         //player.flagcarried.angles = '0 0 0';
2291                 }
2292                 return true;
2293         }
2294 }
2295
2296 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2297 {
2298         entity player = M_ARGV(0, entity);
2299
2300         if(player.flagcarried)
2301         {
2302                 setattachment(player.flagcarried, player, "");
2303                 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2304                 player.flagcarried.scale = FLAG_SCALE;
2305                 player.flagcarried.angles = '0 0 0';
2306                 player.flagcarried.nodrawtoclient = NULL;
2307                 return true;
2308         }
2309 }
2310
2311 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2312 {
2313         entity player = M_ARGV(0, entity);
2314
2315         if(player.flagcarried)
2316         {
2317                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, ((player.flagcarried.team) ? APP_TEAM_ENT(player.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN) : INFO_CTF_FLAGRETURN_ABORTRUN_NEUTRAL));
2318                 ctf_RespawnFlag(player.flagcarried);
2319                 return true;
2320         }
2321 }
2322
2323 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2324 {
2325         entity flag; // temporary entity for the search method
2326
2327         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2328         {
2329                 switch(flag.ctf_status)
2330                 {
2331                         case FLAG_DROPPED:
2332                         case FLAG_PASSING:
2333                         {
2334                                 // lock the flag, game is over
2335                                 set_movetype(flag, MOVETYPE_NONE);
2336                                 flag.takedamage = DAMAGE_NO;
2337                                 flag.solid = SOLID_NOT;
2338                                 flag.nextthink = false; // stop thinking
2339
2340                                 //dprint("stopping the ", flag.netname, " from moving.\n");
2341                                 break;
2342                         }
2343
2344                         default:
2345                         case FLAG_BASE:
2346                         case FLAG_CARRY:
2347                         {
2348                                 // do nothing for these flags
2349                                 break;
2350                         }
2351                 }
2352         }
2353 }
2354
2355 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2356 {
2357         entity bot = M_ARGV(0, entity);
2358
2359         havocbot_ctf_reset_role(bot);
2360         return true;
2361 }
2362
2363 MUTATOR_HOOKFUNCTION(ctf, GetTeamCount)
2364 {
2365         //M_ARGV(0, float) = ctf_teams;
2366         M_ARGV(1, string) = "ctf_team";
2367         return true;
2368 }
2369
2370 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2371 {
2372         entity spectatee = M_ARGV(0, entity);
2373         entity client = M_ARGV(1, entity);
2374
2375         client.ctf_flagstatus = spectatee.ctf_flagstatus;
2376 }
2377
2378 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2379 {
2380         int record_page = M_ARGV(0, int);
2381         string ret_string = M_ARGV(1, string);
2382
2383         for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2384         {
2385                 if (MapInfo_Get_ByID(i))
2386                 {
2387                         float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2388
2389                         if(!r)
2390                                 continue;
2391
2392                         // TODO: uid2name
2393                         string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2394                         ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2395                 }
2396         }
2397
2398         M_ARGV(1, string) = ret_string;
2399 }
2400
2401 bool superspec_Spectate(entity this, entity targ); // TODO
2402 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2403 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2404 {
2405         entity player = M_ARGV(0, entity);
2406         string cmd_name = M_ARGV(1, string);
2407         int cmd_argc = M_ARGV(2, int);
2408
2409         if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2410
2411         if(cmd_name == "followfc")
2412         {
2413                 if(!g_ctf)
2414                         return true;
2415
2416                 int _team = 0;
2417                 bool found = false;
2418
2419                 if(cmd_argc == 2)
2420                 {
2421                         switch(argv(1))
2422                         {
2423                                 case "red":    if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2424                                 case "blue":   if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2425                                 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2426                                 case "pink":   if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2427                         }
2428                 }
2429
2430                 FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
2431                         if(it.flagcarried && (it.team == _team || _team == 0))
2432                         {
2433                                 found = true;
2434                                 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2435                                         continue; // already spectating this fc, try another
2436                                 return superspec_Spectate(player, it);
2437                         }
2438                 ));
2439
2440                 if(!found)
2441                         superspec_msg("", "", player, "No active flag carrier\n", 1);
2442                 return true;
2443         }
2444 }
2445
2446 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2447 {
2448         entity frag_target = M_ARGV(0, entity);
2449
2450         if(frag_target.flagcarried)
2451                 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2452 }
2453
2454
2455 // ==========
2456 // Spawnfuncs
2457 // ==========
2458
2459 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2460 CTF flag for team one (Red).
2461 Keys:
2462 "angle" Angle the flag will point (minus 90 degrees)...
2463 "model" model to use, note this needs red and blue as skins 0 and 1...
2464 "noise" sound played when flag is picked up...
2465 "noise1" sound played when flag is returned by a teammate...
2466 "noise2" sound played when flag is captured...
2467 "noise3" sound played when flag is lost in the field and respawns itself...
2468 "noise4" sound played when flag is dropped by a player...
2469 "noise5" sound played when flag touches the ground... */
2470 spawnfunc(item_flag_team1)
2471 {
2472         if(!g_ctf) { delete(this); return; }
2473
2474         ctf_FlagSetup(NUM_TEAM_1, this);
2475 }
2476
2477 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2478 CTF flag for team two (Blue).
2479 Keys:
2480 "angle" Angle the flag will point (minus 90 degrees)...
2481 "model" model to use, note this needs red and blue as skins 0 and 1...
2482 "noise" sound played when flag is picked up...
2483 "noise1" sound played when flag is returned by a teammate...
2484 "noise2" sound played when flag is captured...
2485 "noise3" sound played when flag is lost in the field and respawns itself...
2486 "noise4" sound played when flag is dropped by a player...
2487 "noise5" sound played when flag touches the ground... */
2488 spawnfunc(item_flag_team2)
2489 {
2490         if(!g_ctf) { delete(this); return; }
2491
2492         ctf_FlagSetup(NUM_TEAM_2, this);
2493 }
2494
2495 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2496 CTF flag for team three (Yellow).
2497 Keys:
2498 "angle" Angle the flag will point (minus 90 degrees)...
2499 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2500 "noise" sound played when flag is picked up...
2501 "noise1" sound played when flag is returned by a teammate...
2502 "noise2" sound played when flag is captured...
2503 "noise3" sound played when flag is lost in the field and respawns itself...
2504 "noise4" sound played when flag is dropped by a player...
2505 "noise5" sound played when flag touches the ground... */
2506 spawnfunc(item_flag_team3)
2507 {
2508         if(!g_ctf) { delete(this); return; }
2509
2510         ctf_FlagSetup(NUM_TEAM_3, this);
2511 }
2512
2513 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2514 CTF flag for team four (Pink).
2515 Keys:
2516 "angle" Angle the flag will point (minus 90 degrees)...
2517 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2518 "noise" sound played when flag is picked up...
2519 "noise1" sound played when flag is returned by a teammate...
2520 "noise2" sound played when flag is captured...
2521 "noise3" sound played when flag is lost in the field and respawns itself...
2522 "noise4" sound played when flag is dropped by a player...
2523 "noise5" sound played when flag touches the ground... */
2524 spawnfunc(item_flag_team4)
2525 {
2526         if(!g_ctf) { delete(this); return; }
2527
2528         ctf_FlagSetup(NUM_TEAM_4, this);
2529 }
2530
2531 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2532 CTF flag (Neutral).
2533 Keys:
2534 "angle" Angle the flag will point (minus 90 degrees)...
2535 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2536 "noise" sound played when flag is picked up...
2537 "noise1" sound played when flag is returned by a teammate...
2538 "noise2" sound played when flag is captured...
2539 "noise3" sound played when flag is lost in the field and respawns itself...
2540 "noise4" sound played when flag is dropped by a player...
2541 "noise5" sound played when flag touches the ground... */
2542 spawnfunc(item_flag_neutral)
2543 {
2544         if(!g_ctf) { delete(this); return; }
2545         if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2546
2547         ctf_FlagSetup(0, this);
2548 }
2549
2550 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2551 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2552 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.
2553 Keys:
2554 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2555 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2556 spawnfunc(ctf_team)
2557 {
2558         if(!g_ctf) { delete(this); return; }
2559
2560         this.classname = "ctf_team";
2561         this.team = this.cnt + 1;
2562 }
2563
2564 // compatibility for quake maps
2565 spawnfunc(team_CTF_redflag)    { spawnfunc_item_flag_team1(this);    }
2566 spawnfunc(team_CTF_blueflag)   { spawnfunc_item_flag_team2(this);    }
2567 spawnfunc(info_player_team1);
2568 spawnfunc(team_CTF_redplayer)  { spawnfunc_info_player_team1(this);  }
2569 spawnfunc(team_CTF_redspawn)   { spawnfunc_info_player_team1(this);  }
2570 spawnfunc(info_player_team2);
2571 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this);  }
2572 spawnfunc(team_CTF_bluespawn)  { spawnfunc_info_player_team2(this);  }
2573
2574 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this);  }
2575 spawnfunc(team_neutralobelisk)  { spawnfunc_item_flag_neutral(this);  }
2576
2577
2578 // ==============
2579 // Initialization
2580 // ==============
2581
2582 // scoreboard setup
2583 void ctf_ScoreRules(int teams)
2584 {
2585         CheckAllowedTeams(NULL);
2586         ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
2587         ScoreInfo_SetLabel_TeamScore  (ST_CTF_CAPS,     "caps",      SFL_SORT_PRIO_PRIMARY);
2588         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS,    "caps",      SFL_SORT_PRIO_SECONDARY);
2589         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime",   SFL_LOWER_IS_BETTER | SFL_TIME);
2590         ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups",   0);
2591         ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills",   0);
2592         ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns",   0);
2593         ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS,   "drops",     SFL_LOWER_IS_BETTER);
2594         ScoreRules_basics_end();
2595 }
2596
2597 // code from here on is just to support maps that don't have flag and team entities
2598 void ctf_SpawnTeam (string teamname, int teamcolor)
2599 {
2600         entity this = new_pure(ctf_team);
2601         this.netname = teamname;
2602         this.cnt = teamcolor - 1;
2603         this.spawnfunc_checked = true;
2604         this.team = teamcolor;
2605 }
2606
2607 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2608 {
2609         ctf_teams = 0;
2610
2611         entity tmp_entity;
2612         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2613         {
2614                 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2615                 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2616
2617                 switch(tmp_entity.team)
2618                 {
2619                         case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2620                         case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2621                         case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2622                         case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2623                 }
2624                 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2625         }
2626
2627         if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2628         {
2629                 ctf_teams = 0; // so set the default red and blue teams
2630                 BITSET_ASSIGN(ctf_teams, BIT(0));
2631                 BITSET_ASSIGN(ctf_teams, BIT(1));
2632         }
2633
2634         //ctf_teams = bound(2, ctf_teams, 4);
2635
2636         // if no teams are found, spawn defaults
2637         if(find(NULL, classname, "ctf_team") == NULL)
2638         {
2639                 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2640                 if(ctf_teams & BIT(0))
2641                         ctf_SpawnTeam("Red", NUM_TEAM_1);
2642                 if(ctf_teams & BIT(1))
2643                         ctf_SpawnTeam("Blue", NUM_TEAM_2);
2644                 if(ctf_teams & BIT(2))
2645                         ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2646                 if(ctf_teams & BIT(3))
2647                         ctf_SpawnTeam("Pink", NUM_TEAM_4);
2648         }
2649
2650         ctf_ScoreRules(ctf_teams);
2651 }
2652
2653 void ctf_Initialize()
2654 {
2655         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2656
2657         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2658         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2659         ctf_captureshield_force = autocvar_g_ctf_shield_force;
2660
2661         InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);
2662 }