1 #include "gamemode_ctf.qh"
8 REGISTER_MUTATOR(ctf, false)
12 if (time > 1) // game loads at time 1
13 error("This is a game type and it cannot be added at runtime.");
17 SetLimits(autocvar_capturelimit_override, autocvar_captureleadlimit_override, -1, -1);
18 have_team_spawns = -1; // request team spawns
21 MUTATOR_ONROLLBACK_OR_REMOVE
23 // we actually cannot roll back ctf_Initialize here
24 // BUT: we don't need to! If this gets called, adding always
30 LOG_INFO("This is a game type and it cannot be removed at runtime.");
40 void ctf_RespawnFlag(entity flag);
42 // score rule declarations
43 const int ST_CTF_CAPS = 1;
44 const int SP_CTF_CAPS = 4;
45 const int SP_CTF_CAPTIME = 5;
46 const int SP_CTF_PICKUPS = 6;
47 const int SP_CTF_DROPS = 7;
48 const int SP_CTF_FCKILLS = 8;
49 const int SP_CTF_RETURNS = 9;
52 ATTRIB(Flag, m_mins, vector, PL_MIN_CONST + '0 0 -13')
53 ATTRIB(Flag, m_maxs, vector, PL_MAX_CONST + '0 0 -13')
55 Flag CTF_FLAG; STATIC_INIT(Flag) { CTF_FLAG = NEW(Flag); }
56 void ctf_FlagTouch() { SELFPARAM(); ITEM_HANDLE(Pickup, CTF_FLAG, this, other); }
58 // flag constants // for most of these, there is just one question to be asked: WHYYYYY?
60 const float FLAG_SCALE = 0.6;
62 const float FLAG_THINKRATE = 0.2;
63 const float FLAG_TOUCHRATE = 0.5;
64 const float WPFE_THINKRATE = 0.5;
66 const vector FLAG_DROP_OFFSET = ('0 0 32');
67 const vector FLAG_CARRY_OFFSET = ('-16 0 8');
68 #define FLAG_SPAWN_OFFSET ('0 0 1' * (PL_MAX_CONST.z - 13))
69 const vector FLAG_WAYPOINT_OFFSET = ('0 0 64');
70 const vector FLAG_FLOAT_OFFSET = ('0 0 32');
71 const vector FLAG_PASS_ARC_OFFSET = ('0 0 -10');
73 const vector VEHICLE_FLAG_OFFSET = ('0 0 96');
74 const float VEHICLE_FLAG_SCALE = 1.0;
77 #define WPCOLOR_ENEMYFC(t) ((t) ? colormapPaletteColor(t - 1, false) * 0.75 : '1 1 1')
78 #define WPCOLOR_FLAGCARRIER(t) (WP_FlagCarrier.m_color)
79 #define WPCOLOR_DROPPEDFLAG(t) ((t) ? ('0.25 0.25 0.25' + colormapPaletteColor(t - 1, false)) * 0.5 : '1 1 1')
82 #define snd_flag_taken noise
83 #define snd_flag_returned noise1
84 #define snd_flag_capture noise2
85 #define snd_flag_respawn noise3
86 .string snd_flag_dropped;
87 .string snd_flag_touch;
88 .string snd_flag_pass;
95 // list of flags on the map
96 entity ctf_worldflaglist;
97 .entity ctf_worldflagnext;
98 .entity ctf_staleflagnext;
101 .entity bot_basewaypoint; // flag waypointsprite
103 .entity wps_flagbase;
104 .entity wps_flagcarrier;
105 .entity wps_flagdropped;
106 .entity wps_enemyflagcarrier;
107 .float wps_helpme_time;
108 bool wpforenemy_announced;
109 float wpforenemy_nextthink;
112 const int FLAG_BASE = 1;
113 const int FLAG_DROPPED = 2;
114 const int FLAG_CARRY = 3;
115 const int FLAG_PASSING = 4;
117 const int DROP_NORMAL = 1;
118 const int DROP_THROW = 2;
119 const int DROP_PASS = 3;
120 const int DROP_RESET = 4;
122 const int PICKUP_BASE = 1;
123 const int PICKUP_DROPPED = 2;
125 const int CAPTURE_NORMAL = 1;
126 const int CAPTURE_DROPPED = 2;
128 const int RETURN_TIMEOUT = 1;
129 const int RETURN_DROPPED = 2;
130 const int RETURN_DAMAGE = 3;
131 const int RETURN_SPEEDRUN = 4;
132 const int RETURN_NEEDKILL = 5;
134 void ctf_Handle_Throw(entity player, entity receiver, float droptype);
137 #define ctf_spawnorigin dropped_origin
138 bool ctf_stalemate; // indicates that a stalemate is active
139 float ctf_captimerecord; // record time for capturing the flag
140 .float ctf_pickuptime;
142 .int ctf_status; // status of the flag (FLAG_BASE, FLAG_DROPPED, FLAG_CARRY declared globally)
143 .entity ctf_dropper; // don't allow spam of dropping the flag
144 .int max_flag_health;
145 .float next_take_time;
146 .bool ctf_flagdamaged;
149 // passing/throwing properties
150 .float pass_distance;
153 .float throw_antispam;
154 .float throw_prevtime;
157 // CaptureShield: If the player is too bad to be allowed to capture, shield them from taking the flag.
158 .bool ctf_captureshielded; // set to 1 if the player is too bad to be allowed to capture
159 float ctf_captureshield_min_negscore; // punish at -20 points
160 float ctf_captureshield_max_ratio; // punish at most 30% of each team
161 float ctf_captureshield_force; // push force of the shield
164 bool ctf_oneflag; // indicates whether or not a neutral flag has been found
167 const int HAVOCBOT_CTF_ROLE_NONE = 0;
168 const int HAVOCBOT_CTF_ROLE_DEFENSE = 2;
169 const int HAVOCBOT_CTF_ROLE_MIDDLE = 4;
170 const int HAVOCBOT_CTF_ROLE_OFFENSE = 8;
171 const int HAVOCBOT_CTF_ROLE_CARRIER = 16;
172 const int HAVOCBOT_CTF_ROLE_RETRIEVER = 32;
173 const int HAVOCBOT_CTF_ROLE_ESCORT = 64;
175 .bool havocbot_cantfindflag;
177 vector havocbot_ctf_middlepoint;
178 float havocbot_ctf_middlepoint_radius;
180 void havocbot_role_ctf_setrole(entity bot, int role);
183 #define CTF_SAMETEAM(a,b) ((autocvar_g_ctf_reverse || (ctf_oneflag && autocvar_g_ctf_oneflag_reverse)) ? DIFF_TEAM(a,b) : SAME_TEAM(a,b))
184 #define CTF_DIFFTEAM(a,b) ((autocvar_g_ctf_reverse || (ctf_oneflag && autocvar_g_ctf_oneflag_reverse)) ? SAME_TEAM(a,b) : DIFF_TEAM(a,b))
186 // networked flag statuses
187 .int ctf_flagstatus = _STAT(CTF_FLAGSTATUS);
190 const int CTF_RED_FLAG_TAKEN = 1;
191 const int CTF_RED_FLAG_LOST = 2;
192 const int CTF_RED_FLAG_CARRYING = 3;
193 const int CTF_BLUE_FLAG_TAKEN = 4;
194 const int CTF_BLUE_FLAG_LOST = 8;
195 const int CTF_BLUE_FLAG_CARRYING = 12;
196 const int CTF_YELLOW_FLAG_TAKEN = 16;
197 const int CTF_YELLOW_FLAG_LOST = 32;
198 const int CTF_YELLOW_FLAG_CARRYING = 48;
199 const int CTF_PINK_FLAG_TAKEN = 64;
200 const int CTF_PINK_FLAG_LOST = 128;
201 const int CTF_PINK_FLAG_CARRYING = 192;
202 const int CTF_NEUTRAL_FLAG_TAKEN = 256;
203 const int CTF_NEUTRAL_FLAG_LOST = 512;
204 const int CTF_NEUTRAL_FLAG_CARRYING = 768;
205 const int CTF_FLAG_NEUTRAL = 2048;
206 const int CTF_SHIELDED = 4096;
209 #ifdef IMPLEMENTATION
212 #include <common/vehicles/all.qh>
213 #include <server/teamplay.qh>
216 #include <lib/warpzone/common.qh>
218 bool autocvar_g_ctf_allow_vehicle_carry;
219 bool autocvar_g_ctf_allow_vehicle_touch;
220 bool autocvar_g_ctf_allow_monster_touch;
221 bool autocvar_g_ctf_throw;
222 float autocvar_g_ctf_throw_angle_max;
223 float autocvar_g_ctf_throw_angle_min;
224 int autocvar_g_ctf_throw_punish_count;
225 float autocvar_g_ctf_throw_punish_delay;
226 float autocvar_g_ctf_throw_punish_time;
227 float autocvar_g_ctf_throw_strengthmultiplier;
228 float autocvar_g_ctf_throw_velocity_forward;
229 float autocvar_g_ctf_throw_velocity_up;
230 float autocvar_g_ctf_drop_velocity_up;
231 float autocvar_g_ctf_drop_velocity_side;
232 bool autocvar_g_ctf_oneflag_reverse;
233 bool autocvar_g_ctf_portalteleport;
234 bool autocvar_g_ctf_pass;
235 float autocvar_g_ctf_pass_arc;
236 float autocvar_g_ctf_pass_arc_max;
237 float autocvar_g_ctf_pass_directional_max;
238 float autocvar_g_ctf_pass_directional_min;
239 float autocvar_g_ctf_pass_radius;
240 float autocvar_g_ctf_pass_wait;
241 bool autocvar_g_ctf_pass_request;
242 float autocvar_g_ctf_pass_turnrate;
243 float autocvar_g_ctf_pass_timelimit;
244 float autocvar_g_ctf_pass_velocity;
245 bool autocvar_g_ctf_dynamiclights;
246 float autocvar_g_ctf_flag_collect_delay;
247 float autocvar_g_ctf_flag_damageforcescale;
248 bool autocvar_g_ctf_flag_dropped_waypoint;
249 bool autocvar_g_ctf_flag_dropped_floatinwater;
250 bool autocvar_g_ctf_flag_glowtrails;
251 int autocvar_g_ctf_flag_health;
252 bool autocvar_g_ctf_flag_return;
253 bool autocvar_g_ctf_flag_return_carrying;
254 float autocvar_g_ctf_flag_return_carried_radius;
255 float autocvar_g_ctf_flag_return_time;
256 bool autocvar_g_ctf_flag_return_when_unreachable;
257 float autocvar_g_ctf_flag_return_damage;
258 float autocvar_g_ctf_flag_return_damage_delay;
259 float autocvar_g_ctf_flag_return_dropped;
260 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
261 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
262 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
263 float autocvar_g_ctf_flagcarrier_selfforcefactor;
264 float autocvar_g_ctf_flagcarrier_damagefactor;
265 float autocvar_g_ctf_flagcarrier_forcefactor;
266 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
267 bool autocvar_g_ctf_fullbrightflags;
268 bool autocvar_g_ctf_ignore_frags;
269 int autocvar_g_ctf_score_capture;
270 int autocvar_g_ctf_score_capture_assist;
271 int autocvar_g_ctf_score_kill;
272 int autocvar_g_ctf_score_penalty_drop;
273 int autocvar_g_ctf_score_penalty_returned;
274 int autocvar_g_ctf_score_pickup_base;
275 int autocvar_g_ctf_score_pickup_dropped_early;
276 int autocvar_g_ctf_score_pickup_dropped_late;
277 int autocvar_g_ctf_score_return;
278 float autocvar_g_ctf_shield_force;
279 float autocvar_g_ctf_shield_max_ratio;
280 int autocvar_g_ctf_shield_min_negscore;
281 bool autocvar_g_ctf_stalemate;
282 int autocvar_g_ctf_stalemate_endcondition;
283 float autocvar_g_ctf_stalemate_time;
284 bool autocvar_g_ctf_reverse;
285 float autocvar_g_ctf_dropped_capture_delay;
286 float autocvar_g_ctf_dropped_capture_radius;
288 void ctf_FakeTimeLimit(entity e, float t)
291 WriteByte(MSG_ONE, 3); // svc_updatestat
292 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
294 WriteCoord(MSG_ONE, autocvar_timelimit);
296 WriteCoord(MSG_ONE, (t + 1) / 60);
299 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
301 if(autocvar_sv_eventlog)
302 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != world) ? ftos(actor.playerid) : "")));
303 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
306 void ctf_CaptureRecord(entity flag, entity player)
308 float cap_record = ctf_captimerecord;
309 float cap_time = (time - flag.ctf_pickuptime);
310 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
313 if(ctf_oneflag) { Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname); }
314 else if(!ctf_captimerecord) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT(flag, CHOICE_CTF_CAPTURE_TIME), player.netname, (cap_time * 100)); }
315 else if(cap_time < cap_record) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT(flag, CHOICE_CTF_CAPTURE_BROKEN), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
316 else { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT(flag, CHOICE_CTF_CAPTURE_UNBROKEN), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
318 // write that shit in the database
319 if(!ctf_oneflag) // but not in 1-flag mode
320 if((!ctf_captimerecord) || (cap_time < cap_record))
322 ctf_captimerecord = cap_time;
323 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
324 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
325 write_recordmarker(player, (time - cap_time), cap_time);
329 void ctf_FlagcarrierWaypoints(entity player)
331 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
332 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
333 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
334 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
337 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
339 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
340 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
341 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
342 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
345 if(current_height) // make sure we can actually do this arcing path
347 targpos = (to + ('0 0 1' * current_height));
348 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
349 if(trace_fraction < 1)
351 //print("normal arc line failed, trying to find new pos...");
352 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
353 targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
354 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
355 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
356 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
359 else { targpos = to; }
361 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
363 vector desired_direction = normalize(targpos - from);
364 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
365 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
368 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
370 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
372 // directional tracing only
374 makevectors(passer_angle);
376 // find the closest point on the enemy to the center of the attack
377 float h; // hypotenuse, which is the distance between attacker to head
378 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
380 h = vlen(head_center - passer_center);
381 a = h * (normalize(head_center - passer_center) * v_forward);
383 vector nearest_on_line = (passer_center + a * v_forward);
384 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
386 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
387 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
389 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
394 else { return true; }
398 // =======================
399 // CaptureShield Functions
400 // =======================
402 bool ctf_CaptureShield_CheckStatus(entity p)
404 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
405 int players_worseeq, players_total;
407 if(ctf_captureshield_max_ratio <= 0)
410 s = PlayerScore_Add(p, SP_CTF_CAPS, 0);
411 s2 = PlayerScore_Add(p, SP_CTF_PICKUPS, 0);
412 s3 = PlayerScore_Add(p, SP_CTF_RETURNS, 0);
413 s4 = PlayerScore_Add(p, SP_CTF_FCKILLS, 0);
415 sr = ((s - s2) + (s3 + s4));
417 if(sr >= -ctf_captureshield_min_negscore)
420 players_total = players_worseeq = 0;
421 FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
424 se = PlayerScore_Add(it, SP_CTF_CAPS, 0);
425 se2 = PlayerScore_Add(it, SP_CTF_PICKUPS, 0);
426 se3 = PlayerScore_Add(it, SP_CTF_RETURNS, 0);
427 se4 = PlayerScore_Add(it, SP_CTF_FCKILLS, 0);
429 ser = ((se - se2) + (se3 + se4));
436 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
437 // use this rule here
439 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
445 void ctf_CaptureShield_Update(entity player, bool wanted_status)
447 bool updated_status = ctf_CaptureShield_CheckStatus(player);
448 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
450 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
451 player.ctf_captureshielded = updated_status;
455 bool ctf_CaptureShield_Customize()
457 if(!other.ctf_captureshielded) { return false; }
458 if(CTF_SAMETEAM(self, other)) { return false; }
463 void ctf_CaptureShield_Touch()
465 if(!other.ctf_captureshielded) { return; }
466 if(CTF_SAMETEAM(self, other)) { return; }
468 vector mymid = (self.absmin + self.absmax) * 0.5;
469 vector othermid = (other.absmin + other.absmax) * 0.5;
471 Damage(other, self, self, 0, DEATH_HURTTRIGGER.m_id, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
472 if(IS_REAL_CLIENT(other)) { Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
475 void ctf_CaptureShield_Spawn(entity flag)
477 entity shield = new(ctf_captureshield);
480 shield.team = self.team;
481 shield.touch = ctf_CaptureShield_Touch;
482 shield.customizeentityforclient = ctf_CaptureShield_Customize;
483 shield.effects = EF_ADDITIVE;
484 shield.movetype = MOVETYPE_NOCLIP;
485 shield.solid = SOLID_TRIGGER;
486 shield.avelocity = '7 0 11';
489 setorigin(shield, self.origin);
490 setmodel(shield, MDL_CTF_SHIELD);
491 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
495 // ====================
496 // Drop/Pass/Throw Code
497 // ====================
499 void ctf_Handle_Drop(entity flag, entity player, int droptype)
502 player = (player ? player : flag.pass_sender);
505 flag.movetype = MOVETYPE_TOSS;
506 flag.takedamage = DAMAGE_YES;
507 flag.angles = '0 0 0';
508 flag.health = flag.max_flag_health;
509 flag.ctf_droptime = time;
510 flag.ctf_dropper = player;
511 flag.ctf_status = FLAG_DROPPED;
513 // messages and sounds
514 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT(flag, INFO_CTF_LOST) : INFO_CTF_LOST_NEUTRAL), player.netname);
515 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
516 ctf_EventLog("dropped", player.team, player);
519 PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);
520 PlayerScore_Add(player, SP_CTF_DROPS, 1);
523 if(autocvar_g_ctf_flag_dropped_waypoint) {
524 entity wp = WaypointSprite_Spawn(WP_FlagDropped, 0, 0, flag, FLAG_WAYPOINT_OFFSET, world, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, true, RADARICON_FLAG);
525 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
528 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
530 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
531 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
534 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
536 if(droptype == DROP_PASS)
538 flag.pass_distance = 0;
539 flag.pass_sender = world;
540 flag.pass_target = world;
544 void ctf_Handle_Retrieve(entity flag, entity player)
546 entity sender = flag.pass_sender;
548 // transfer flag to player
550 flag.owner.flagcarried = flag;
555 setattachment(flag, player.vehicle, "");
556 setorigin(flag, VEHICLE_FLAG_OFFSET);
557 flag.scale = VEHICLE_FLAG_SCALE;
561 setattachment(flag, player, "");
562 setorigin(flag, FLAG_CARRY_OFFSET);
564 flag.movetype = MOVETYPE_NONE;
565 flag.takedamage = DAMAGE_NO;
566 flag.solid = SOLID_NOT;
567 flag.angles = '0 0 0';
568 flag.ctf_status = FLAG_CARRY;
570 // messages and sounds
571 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
572 ctf_EventLog("receive", flag.team, player);
574 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), LAMBDA(
576 Send_Notification(NOTIF_ONE, it, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT(flag, CENTER_CTF_PASS_SENT) : CENTER_CTF_PASS_SENT_NEUTRAL), player.netname);
577 else if(it == player)
578 Send_Notification(NOTIF_ONE, it, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT(flag, CENTER_CTF_PASS_RECEIVED) : CENTER_CTF_PASS_RECEIVED_NEUTRAL), sender.netname);
579 else if(SAME_TEAM(it, sender))
580 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);
583 // create new waypoint
584 ctf_FlagcarrierWaypoints(player);
586 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
587 player.throw_antispam = sender.throw_antispam;
589 flag.pass_distance = 0;
590 flag.pass_sender = world;
591 flag.pass_target = world;
594 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
596 entity flag = player.flagcarried;
597 vector targ_origin, flag_velocity;
599 if(!flag) { return; }
600 if((droptype == DROP_PASS) && !receiver) { return; }
602 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
605 setattachment(flag, world, "");
606 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
607 flag.owner.flagcarried = world;
609 flag.solid = SOLID_TRIGGER;
610 flag.ctf_dropper = player;
611 flag.ctf_droptime = time;
613 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
620 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
621 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
622 WarpZone_RefSys_Copy(flag, receiver);
623 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
624 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
626 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
627 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
630 flag.movetype = MOVETYPE_FLY;
631 flag.takedamage = DAMAGE_NO;
632 flag.pass_sender = player;
633 flag.pass_target = receiver;
634 flag.ctf_status = FLAG_PASSING;
637 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
638 WarpZone_TrailParticles(world, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
639 ctf_EventLog("pass", flag.team, player);
645 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'));
647 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)));
648 flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, false);
649 ctf_Handle_Drop(flag, player, droptype);
655 flag.velocity = '0 0 0'; // do nothing
662 flag.velocity = W_CalculateProjectileVelocity(player.velocity, (('0 0 1' * autocvar_g_ctf_drop_velocity_up) + ((('0 1 0' * crandom()) + ('1 0 0' * crandom())) * autocvar_g_ctf_drop_velocity_side)), false);
663 ctf_Handle_Drop(flag, player, droptype);
668 // kill old waypointsprite
669 WaypointSprite_Ping(player.wps_flagcarrier);
670 WaypointSprite_Kill(player.wps_flagcarrier);
672 if(player.wps_enemyflagcarrier)
673 WaypointSprite_Kill(player.wps_enemyflagcarrier);
676 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
679 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
681 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
688 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
690 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
691 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
692 entity player_team_flag = world, tmp_entity;
693 float old_time, new_time;
695 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
696 if(CTF_DIFFTEAM(player, flag)) { return; }
699 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
700 if(SAME_TEAM(tmp_entity, player))
702 player_team_flag = tmp_entity;
706 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
708 player.throw_prevtime = time;
709 player.throw_count = 0;
711 // messages and sounds
712 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((enemy_flag.team) ? APP_TEAM_ENT(enemy_flag, CENTER_CTF_CAPTURE) : CENTER_CTF_CAPTURE_NEUTRAL));
713 ctf_CaptureRecord(enemy_flag, player);
714 _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);
718 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
719 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
724 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
725 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
727 old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
728 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
729 if(!old_time || new_time < old_time)
730 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
733 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
734 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
737 if(capturetype == CAPTURE_NORMAL)
739 WaypointSprite_Kill(player.wps_flagcarrier);
740 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
742 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
743 { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
747 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
748 ctf_RespawnFlag(enemy_flag);
751 void ctf_Handle_Return(entity flag, entity player)
753 // messages and sounds
754 if(IS_MONSTER(player))
756 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT(flag, INFO_CTF_RETURN_MONSTER), player.monster_name);
760 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT(flag, CENTER_CTF_RETURN));
761 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT(flag, INFO_CTF_RETURN), player.netname);
763 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
764 ctf_EventLog("return", flag.team, player);
767 if(IS_PLAYER(player))
769 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
770 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
772 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
775 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
779 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
780 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
781 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
785 if(player.flagcarried == flag)
786 WaypointSprite_Kill(player.wps_flagcarrier);
789 ctf_RespawnFlag(flag);
792 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
795 float pickup_dropped_score; // used to calculate dropped pickup score
797 // attach the flag to the player
799 player.flagcarried = flag;
802 setattachment(flag, player.vehicle, "");
803 setorigin(flag, VEHICLE_FLAG_OFFSET);
804 flag.scale = VEHICLE_FLAG_SCALE;
808 setattachment(flag, player, "");
809 setorigin(flag, FLAG_CARRY_OFFSET);
813 flag.movetype = MOVETYPE_NONE;
814 flag.takedamage = DAMAGE_NO;
815 flag.solid = SOLID_NOT;
816 flag.angles = '0 0 0';
817 flag.ctf_status = FLAG_CARRY;
821 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
822 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
826 // messages and sounds
827 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT(flag, INFO_CTF_PICKUP) : INFO_CTF_PICKUP_NEUTRAL), player.netname);
828 if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
829 if(!flag.team) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL); }
830 else if(CTF_DIFFTEAM(player, flag)) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT(flag, CENTER_CTF_PICKUP)); }
831 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)); }
833 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);
836 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)));
839 FOREACH_CLIENT(IS_PLAYER(it) && it != player, LAMBDA(
840 if(CTF_SAMETEAM(flag, it))
841 if(SAME_TEAM(player, it))
842 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_ENT(flag, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
844 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);
847 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
850 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
851 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
856 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
857 ctf_EventLog("steal", flag.team, player);
863 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);
864 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);
865 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
866 PlayerTeamScore_AddScore(player, pickup_dropped_score);
867 ctf_EventLog("pickup", flag.team, player);
875 if(pickuptype == PICKUP_BASE)
877 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
878 if((player.speedrunning) && (ctf_captimerecord))
879 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
883 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
886 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
887 ctf_FlagcarrierWaypoints(player);
888 WaypointSprite_Ping(player.wps_flagcarrier);
892 // ===================
893 // Main Flag Functions
894 // ===================
896 void ctf_CheckFlagReturn(entity flag, int returntype)
898 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
900 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
902 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
906 case RETURN_DROPPED: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT(flag, INFO_CTF_FLAGRETURN_DROPPED) : INFO_CTF_FLAGRETURN_DROPPED_NEUTRAL)); break;
907 case RETURN_DAMAGE: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT(flag, INFO_CTF_FLAGRETURN_DAMAGED) : INFO_CTF_FLAGRETURN_DAMAGED_NEUTRAL)); break;
908 case RETURN_SPEEDRUN: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT(flag, INFO_CTF_FLAGRETURN_SPEEDRUN) : INFO_CTF_FLAGRETURN_SPEEDRUN_NEUTRAL), ctf_captimerecord); break;
909 case RETURN_NEEDKILL: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT(flag, INFO_CTF_FLAGRETURN_NEEDKILL) : INFO_CTF_FLAGRETURN_NEEDKILL_NEUTRAL)); break;
913 { Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT(flag, INFO_CTF_FLAGRETURN_TIMEOUT) : INFO_CTF_FLAGRETURN_TIMEOUT_NEUTRAL)); break; }
915 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
916 ctf_EventLog("returned", flag.team, world);
917 ctf_RespawnFlag(flag);
922 bool ctf_Stalemate_Customize()
924 // make spectators see what the player would see
926 e = WaypointSprite_getviewentity(other);
927 wp_owner = self.owner;
930 if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
931 if(SAME_TEAM(wp_owner, e)) { return false; }
932 if(!IS_PLAYER(e)) { return false; }
937 void ctf_CheckStalemate()
940 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
943 entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
945 // build list of stale flags
946 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
948 if(autocvar_g_ctf_stalemate)
949 if(tmp_entity.ctf_status != FLAG_BASE)
950 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
952 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
953 ctf_staleflaglist = tmp_entity;
955 switch(tmp_entity.team)
957 case NUM_TEAM_1: ++stale_red_flags; break;
958 case NUM_TEAM_2: ++stale_blue_flags; break;
959 case NUM_TEAM_3: ++stale_yellow_flags; break;
960 case NUM_TEAM_4: ++stale_pink_flags; break;
961 default: ++stale_neutral_flags; break;
967 stale_flags = (stale_neutral_flags >= 1);
969 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
971 if(ctf_oneflag && stale_flags == 1)
972 ctf_stalemate = true;
973 else if(stale_flags >= 2)
974 ctf_stalemate = true;
975 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
976 { ctf_stalemate = false; wpforenemy_announced = false; }
977 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
978 { ctf_stalemate = false; wpforenemy_announced = false; }
980 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
983 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
985 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
987 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, tmp_entity.owner, FLAG_WAYPOINT_OFFSET, world, 0, tmp_entity.owner, wps_enemyflagcarrier, true, RADARICON_FLAG);
988 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
989 tmp_entity.owner.wps_enemyflagcarrier.customizeentityforclient = ctf_Stalemate_Customize;
993 if (!wpforenemy_announced)
995 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))));
997 wpforenemy_announced = true;
1002 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
1004 if(ITEM_DAMAGE_NEEDKILL(deathtype))
1006 if(autocvar_g_ctf_flag_return_damage_delay)
1008 this.ctf_flagdamaged = true;
1013 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
1017 if(autocvar_g_ctf_flag_return_damage)
1019 // reduce health and check if it should be returned
1020 this.health = this.health - damage;
1021 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
1026 void ctf_FlagThink()
1031 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
1034 if(self == ctf_worldflaglist) // only for the first flag
1035 FOREACH_CLIENT(true, LAMBDA(ctf_CaptureShield_Update(it, 1))); // release shield only
1038 if(self.mins != CTF_FLAG.m_mins || self.maxs != CTF_FLAG.m_maxs) { // reset the flag boundaries in case it got squished
1039 LOG_TRACE("wtf the flag got squashed?\n");
1040 tracebox(self.origin, CTF_FLAG.m_mins, CTF_FLAG.m_maxs, self.origin, MOVE_NOMONSTERS, self);
1041 if(!trace_startsolid || self.noalign) // can we resize it without getting stuck?
1042 setsize(self, CTF_FLAG.m_mins, CTF_FLAG.m_maxs); }
1044 switch(self.ctf_status) // reset flag angles in case warpzones adjust it
1048 self.angles = '0 0 0';
1055 // main think method
1056 switch(self.ctf_status)
1060 if(autocvar_g_ctf_dropped_capture_radius)
1062 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
1063 if(tmp_entity.ctf_status == FLAG_DROPPED)
1064 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
1065 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
1066 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
1073 if(autocvar_g_ctf_flag_dropped_floatinwater)
1075 vector midpoint = ((self.absmin + self.absmax) * 0.5);
1076 if(pointcontents(midpoint) == CONTENT_WATER)
1078 self.velocity = self.velocity * 0.5;
1080 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
1081 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
1083 { self.movetype = MOVETYPE_FLY; }
1085 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
1087 if(autocvar_g_ctf_flag_return_dropped)
1089 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
1092 ctf_CheckFlagReturn(self, RETURN_DROPPED);
1096 if(self.ctf_flagdamaged)
1098 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
1099 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
1102 else if(autocvar_g_ctf_flag_return_time)
1104 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
1105 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
1113 if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
1116 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
1118 setself(self.owner);
1119 self.impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
1120 ImpulseCommands(self);
1123 if(autocvar_g_ctf_stalemate)
1125 if(time >= wpforenemy_nextthink)
1127 ctf_CheckStalemate();
1128 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1131 if(CTF_SAMETEAM(self, self.owner) && self.team)
1133 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1134 ctf_Handle_Throw(self.owner, world, DROP_THROW);
1135 else if(vlen(self.owner.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_carried_radius)
1136 ctf_Handle_Return(self, self.owner);
1143 vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
1144 targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
1145 WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
1147 if((self.pass_target == world)
1148 || (IS_DEAD(self.pass_target))
1149 || (self.pass_target.flagcarried)
1150 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
1151 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
1152 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1154 // give up, pass failed
1155 ctf_Handle_Drop(self, world, DROP_PASS);
1159 // still a viable target, go for it
1160 ctf_CalculatePassVelocity(self, targ_origin, self.origin, true);
1165 default: // this should never happen
1167 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?\n");
1173 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1176 if(gameover) { return; }
1177 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1179 bool is_not_monster = (!IS_MONSTER(toucher));
1181 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1182 if(ITEM_TOUCH_NEEDKILL())
1184 if(!autocvar_g_ctf_flag_return_damage_delay)
1187 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1189 if(!flag.ctf_flagdamaged) { return; }
1192 int num_perteam = 0;
1193 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), LAMBDA(++num_perteam));
1195 // special touch behaviors
1196 if(STAT(FROZEN, toucher)) { return; }
1197 else if(IS_VEHICLE(toucher))
1199 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1200 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1202 return; // do nothing
1204 else if(IS_MONSTER(toucher))
1206 if(!autocvar_g_ctf_allow_monster_touch)
1207 return; // do nothing
1209 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1211 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1213 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1214 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1215 flag.wait = time + FLAG_TOUCHRATE;
1219 else if(IS_DEAD(toucher)) { return; }
1221 switch(flag.ctf_status)
1227 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1228 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1229 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1230 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1232 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1233 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1234 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)
1236 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1237 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1239 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1240 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1246 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
1247 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1248 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1249 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1255 LOG_TRACE("Someone touched a flag even though it was being carried?\n");
1261 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1263 if(DIFF_TEAM(toucher, flag.pass_sender))
1264 ctf_Handle_Return(flag, toucher);
1266 ctf_Handle_Retrieve(flag, toucher);
1273 .float last_respawn;
1274 void ctf_RespawnFlag(entity flag)
1276 // check for flag respawn being called twice in a row
1277 if(flag.last_respawn > time - 0.5)
1278 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1280 flag.last_respawn = time;
1282 // reset the player (if there is one)
1283 if((flag.owner) && (flag.owner.flagcarried == flag))
1285 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1286 WaypointSprite_Kill(flag.wps_flagcarrier);
1288 flag.owner.flagcarried = world;
1290 if(flag.speedrunning)
1291 ctf_FakeTimeLimit(flag.owner, -1);
1294 if((flag.owner) && (flag.owner.vehicle))
1295 flag.scale = FLAG_SCALE;
1297 if(flag.ctf_status == FLAG_DROPPED)
1298 { WaypointSprite_Kill(flag.wps_flagdropped); }
1301 setattachment(flag, world, "");
1302 setorigin(flag, flag.ctf_spawnorigin);
1304 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
1305 flag.takedamage = DAMAGE_NO;
1306 flag.health = flag.max_flag_health;
1307 flag.solid = SOLID_TRIGGER;
1308 flag.velocity = '0 0 0';
1309 flag.angles = flag.mangle;
1310 flag.flags = FL_ITEM | FL_NOTARGET;
1312 flag.ctf_status = FLAG_BASE;
1314 flag.pass_distance = 0;
1315 flag.pass_sender = world;
1316 flag.pass_target = world;
1317 flag.ctf_dropper = world;
1318 flag.ctf_pickuptime = 0;
1319 flag.ctf_droptime = 0;
1320 flag.ctf_flagdamaged = 0;
1322 ctf_CheckStalemate();
1325 void ctf_Reset(entity this)
1327 if(this.owner && IS_PLAYER(this.owner))
1328 ctf_Handle_Throw(this.owner, world, DROP_RESET);
1330 ctf_RespawnFlag(this);
1333 void ctf_DelayedFlagSetup() // called after a flag is placed on a map by ctf_FlagSetup()
1336 waypoint_spawnforitem_force(self, self.origin);
1337 self.nearestwaypointtimeout = 0; // activate waypointing again
1338 self.bot_basewaypoint = self.nearestwaypoint;
1344 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1345 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1346 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1347 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1348 default: basename = WP_FlagBaseNeutral; break;
1351 entity wp = WaypointSprite_SpawnFixed(basename, self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG);
1352 wp.colormod = ((self.team) ? Team_ColorRGB(self.team) : '1 1 1');
1353 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, ((self.team) ? colormapPaletteColor(self.team - 1, false) : '1 1 1'));
1355 // captureshield setup
1356 ctf_CaptureShield_Spawn(self);
1359 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1362 setself(flag); // for later usage with droptofloor()
1365 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1366 ctf_worldflaglist = flag;
1368 setattachment(flag, world, "");
1370 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1371 flag.team = teamnumber;
1372 flag.classname = "item_flag_team";
1373 flag.target = "###item###"; // wut?
1374 flag.flags = FL_ITEM | FL_NOTARGET;
1375 flag.solid = SOLID_TRIGGER;
1376 flag.takedamage = DAMAGE_NO;
1377 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1378 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1379 flag.health = flag.max_flag_health;
1380 flag.event_damage = ctf_FlagDamage;
1381 flag.pushable = true;
1382 flag.teleportable = TELEPORT_NORMAL;
1383 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1384 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1385 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1386 flag.velocity = '0 0 0';
1387 flag.mangle = flag.angles;
1388 flag.reset = ctf_Reset;
1389 flag.touch = ctf_FlagTouch;
1390 flag.think = ctf_FlagThink;
1391 flag.nextthink = time + FLAG_THINKRATE;
1392 flag.ctf_status = FLAG_BASE;
1394 string teamname = Static_Team_ColorName_Lower(teamnumber);
1396 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1397 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1398 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1399 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnumber).eent_eff_name; }
1400 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnumber).eent_eff_name; }
1401 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnumber).eent_eff_name; }
1404 flag.snd_flag_taken = strzone(SND(CTF_TAKEN(teamnumber)));
1405 flag.snd_flag_returned = strzone(SND(CTF_RETURNED(teamnumber)));
1406 flag.snd_flag_capture = strzone(SND(CTF_CAPTURE(teamnumber)));
1407 flag.snd_flag_dropped = strzone(SND(CTF_DROPPED(teamnumber)));
1408 if (flag.snd_flag_respawn == "") flag.snd_flag_respawn = strzone(SND(CTF_RESPAWN)); // if there is ever a team-based sound for this, update the code to match.
1409 precache_sound(flag.snd_flag_respawn);
1410 if (flag.snd_flag_touch == "") flag.snd_flag_touch = strzone(SND(CTF_TOUCH)); // again has no team-based sound
1411 precache_sound(flag.snd_flag_touch);
1412 if (flag.snd_flag_pass == "") flag.snd_flag_pass = strzone(SND(CTF_PASS)); // same story here
1413 precache_sound(flag.snd_flag_pass);
1416 precache_model(flag.model);
1419 _setmodel(flag, flag.model); // precision set below
1420 setsize(flag, CTF_FLAG.m_mins, CTF_FLAG.m_maxs);
1421 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1423 if(autocvar_g_ctf_flag_glowtrails)
1427 case NUM_TEAM_1: flag.glow_color = 251; break;
1428 case NUM_TEAM_2: flag.glow_color = 210; break;
1429 case NUM_TEAM_3: flag.glow_color = 110; break;
1430 case NUM_TEAM_4: flag.glow_color = 145; break;
1431 default: flag.glow_color = 254; break;
1433 flag.glow_size = 25;
1434 flag.glow_trail = 1;
1437 flag.effects |= EF_LOWPRECISION;
1438 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1439 if(autocvar_g_ctf_dynamiclights)
1443 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1444 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1445 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1446 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1447 default: flag.effects |= EF_DIMLIGHT; break;
1452 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1454 flag.dropped_origin = flag.origin;
1455 flag.noalign = true;
1456 flag.movetype = MOVETYPE_NONE;
1458 else // drop to floor, automatically find a platform and set that as spawn origin
1460 flag.noalign = false;
1463 flag.movetype = MOVETYPE_TOSS;
1466 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1474 // NOTE: LEGACY CODE, needs to be re-written!
1476 void havocbot_calculate_middlepoint()
1480 vector fo = '0 0 0';
1483 f = ctf_worldflaglist;
1488 f = f.ctf_worldflagnext;
1492 havocbot_ctf_middlepoint = s * (1.0 / n);
1493 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1497 entity havocbot_ctf_find_flag(entity bot)
1500 f = ctf_worldflaglist;
1503 if (CTF_SAMETEAM(bot, f))
1505 f = f.ctf_worldflagnext;
1510 entity havocbot_ctf_find_enemy_flag(entity bot)
1513 f = ctf_worldflaglist;
1518 if(CTF_DIFFTEAM(bot, f))
1525 else if(!bot.flagcarried)
1529 else if (CTF_DIFFTEAM(bot, f))
1531 f = f.ctf_worldflagnext;
1536 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1543 FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
1544 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1547 if(vlen(it.origin - org) < tc_radius)
1554 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1557 head = ctf_worldflaglist;
1560 if (CTF_SAMETEAM(self, head))
1562 head = head.ctf_worldflagnext;
1565 navigation_routerating(head, ratingscale, 10000);
1568 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1571 head = ctf_worldflaglist;
1574 if (CTF_SAMETEAM(self, head))
1576 head = head.ctf_worldflagnext;
1581 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1584 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1587 head = ctf_worldflaglist;
1592 if(CTF_DIFFTEAM(self, head))
1596 if(self.flagcarried)
1599 else if(!self.flagcarried)
1603 else if(CTF_DIFFTEAM(self, head))
1605 head = head.ctf_worldflagnext;
1608 navigation_routerating(head, ratingscale, 10000);
1611 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1613 if (!bot_waypoints_for_items)
1615 havocbot_goalrating_ctf_enemyflag(ratingscale);
1621 head = havocbot_ctf_find_enemy_flag(self);
1626 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1629 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1633 mf = havocbot_ctf_find_flag(self);
1635 if(mf.ctf_status == FLAG_BASE)
1639 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1642 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1645 head = ctf_worldflaglist;
1648 // flag is out in the field
1649 if(head.ctf_status != FLAG_BASE)
1650 if(head.tag_entity==world) // dropped
1654 if(vlen(org-head.origin)<df_radius)
1655 navigation_routerating(head, ratingscale, 10000);
1658 navigation_routerating(head, ratingscale, 10000);
1661 head = head.ctf_worldflagnext;
1665 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1669 head = findchainfloat(bot_pickup, true);
1672 // gather health and armor only
1674 if (head.health || head.armorvalue)
1675 if (vlen(head.origin - org) < sradius)
1677 // get the value of the item
1678 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1680 navigation_routerating(head, t * ratingscale, 500);
1686 void havocbot_ctf_reset_role(entity bot)
1688 float cdefense, cmiddle, coffense;
1695 if(vlen(havocbot_ctf_middlepoint)==0)
1696 havocbot_calculate_middlepoint();
1699 if (bot.flagcarried)
1701 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1705 mf = havocbot_ctf_find_flag(bot);
1706 ef = havocbot_ctf_find_enemy_flag(bot);
1708 // Retrieve stolen flag
1709 if(mf.ctf_status!=FLAG_BASE)
1711 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1715 // If enemy flag is taken go to the middle to intercept pursuers
1716 if(ef.ctf_status!=FLAG_BASE)
1718 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1722 // if there is only me on the team switch to offense
1724 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(it, bot), LAMBDA(++c));
1728 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1732 // Evaluate best position to take
1733 // Count mates on middle position
1734 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1736 // Count mates on defense position
1737 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1739 // Count mates on offense position
1740 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1742 if(cdefense<=coffense)
1743 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1744 else if(coffense<=cmiddle)
1745 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1747 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1750 void havocbot_role_ctf_carrier()
1754 havocbot_ctf_reset_role(self);
1758 if (self.flagcarried == world)
1760 havocbot_ctf_reset_role(self);
1764 if (self.bot_strategytime < time)
1766 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1768 navigation_goalrating_start();
1770 havocbot_goalrating_ctf_enemybase(50000);
1772 havocbot_goalrating_ctf_ourbase(50000);
1775 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1777 navigation_goalrating_end();
1779 if (self.navigation_hasgoals)
1780 self.havocbot_cantfindflag = time + 10;
1781 else if (time > self.havocbot_cantfindflag)
1783 // Can't navigate to my own base, suicide!
1784 // TODO: drop it and wander around
1785 Damage(self, self, self, 100000, DEATH_KILL.m_id, self.origin, '0 0 0');
1791 void havocbot_role_ctf_escort()
1797 havocbot_ctf_reset_role(self);
1801 if (self.flagcarried)
1803 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1807 // If enemy flag is back on the base switch to previous role
1808 ef = havocbot_ctf_find_enemy_flag(self);
1809 if(ef.ctf_status==FLAG_BASE)
1811 self.havocbot_role = self.havocbot_previous_role;
1812 self.havocbot_role_timeout = 0;
1816 // If the flag carrier reached the base switch to defense
1817 mf = havocbot_ctf_find_flag(self);
1818 if(mf.ctf_status!=FLAG_BASE)
1819 if(vlen(ef.origin - mf.dropped_origin) < 300)
1821 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1825 // Set the role timeout if necessary
1826 if (!self.havocbot_role_timeout)
1828 self.havocbot_role_timeout = time + random() * 30 + 60;
1831 // If nothing happened just switch to previous role
1832 if (time > self.havocbot_role_timeout)
1834 self.havocbot_role = self.havocbot_previous_role;
1835 self.havocbot_role_timeout = 0;
1839 // Chase the flag carrier
1840 if (self.bot_strategytime < time)
1842 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1843 navigation_goalrating_start();
1844 havocbot_goalrating_ctf_enemyflag(30000);
1845 havocbot_goalrating_ctf_ourstolenflag(40000);
1846 havocbot_goalrating_items(10000, self.origin, 10000);
1847 navigation_goalrating_end();
1851 void havocbot_role_ctf_offense()
1858 havocbot_ctf_reset_role(self);
1862 if (self.flagcarried)
1864 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1869 mf = havocbot_ctf_find_flag(self);
1870 ef = havocbot_ctf_find_enemy_flag(self);
1873 if(mf.ctf_status!=FLAG_BASE)
1876 pos = mf.tag_entity.origin;
1880 // Try to get it if closer than the enemy base
1881 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1883 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1888 // Escort flag carrier
1889 if(ef.ctf_status!=FLAG_BASE)
1892 pos = ef.tag_entity.origin;
1896 if(vlen(pos-mf.dropped_origin)>700)
1898 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1903 // About to fail, switch to middlefield
1906 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1910 // Set the role timeout if necessary
1911 if (!self.havocbot_role_timeout)
1912 self.havocbot_role_timeout = time + 120;
1914 if (time > self.havocbot_role_timeout)
1916 havocbot_ctf_reset_role(self);
1920 if (self.bot_strategytime < time)
1922 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1923 navigation_goalrating_start();
1924 havocbot_goalrating_ctf_ourstolenflag(50000);
1925 havocbot_goalrating_ctf_enemybase(20000);
1926 havocbot_goalrating_items(5000, self.origin, 1000);
1927 havocbot_goalrating_items(1000, self.origin, 10000);
1928 navigation_goalrating_end();
1932 // Retriever (temporary role):
1933 void havocbot_role_ctf_retriever()
1939 havocbot_ctf_reset_role(self);
1943 if (self.flagcarried)
1945 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1949 // If flag is back on the base switch to previous role
1950 mf = havocbot_ctf_find_flag(self);
1951 if(mf.ctf_status==FLAG_BASE)
1953 havocbot_ctf_reset_role(self);
1957 if (!self.havocbot_role_timeout)
1958 self.havocbot_role_timeout = time + 20;
1960 if (time > self.havocbot_role_timeout)
1962 havocbot_ctf_reset_role(self);
1966 if (self.bot_strategytime < time)
1971 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1972 navigation_goalrating_start();
1973 havocbot_goalrating_ctf_ourstolenflag(50000);
1974 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1975 havocbot_goalrating_ctf_enemybase(30000);
1976 havocbot_goalrating_items(500, self.origin, rt_radius);
1977 navigation_goalrating_end();
1981 void havocbot_role_ctf_middle()
1987 havocbot_ctf_reset_role(self);
1991 if (self.flagcarried)
1993 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1997 mf = havocbot_ctf_find_flag(self);
1998 if(mf.ctf_status!=FLAG_BASE)
2000 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
2004 if (!self.havocbot_role_timeout)
2005 self.havocbot_role_timeout = time + 10;
2007 if (time > self.havocbot_role_timeout)
2009 havocbot_ctf_reset_role(self);
2013 if (self.bot_strategytime < time)
2017 org = havocbot_ctf_middlepoint;
2018 org.z = self.origin.z;
2020 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
2021 navigation_goalrating_start();
2022 havocbot_goalrating_ctf_ourstolenflag(50000);
2023 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
2024 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
2025 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
2026 havocbot_goalrating_items(2500, self.origin, 10000);
2027 havocbot_goalrating_ctf_enemybase(2500);
2028 navigation_goalrating_end();
2032 void havocbot_role_ctf_defense()
2038 havocbot_ctf_reset_role(self);
2042 if (self.flagcarried)
2044 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
2048 // If own flag was captured
2049 mf = havocbot_ctf_find_flag(self);
2050 if(mf.ctf_status!=FLAG_BASE)
2052 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
2056 if (!self.havocbot_role_timeout)
2057 self.havocbot_role_timeout = time + 30;
2059 if (time > self.havocbot_role_timeout)
2061 havocbot_ctf_reset_role(self);
2064 if (self.bot_strategytime < time)
2069 org = mf.dropped_origin;
2070 mp_radius = havocbot_ctf_middlepoint_radius;
2072 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
2073 navigation_goalrating_start();
2075 // if enemies are closer to our base, go there
2076 entity closestplayer = world;
2077 float distance, bestdistance = 10000;
2078 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), LAMBDA(
2079 distance = vlen(org - it.origin);
2080 if(distance<bestdistance)
2083 bestdistance = distance;
2088 if(DIFF_TEAM(closestplayer, self))
2089 if(vlen(org - self.origin)>1000)
2090 if(checkpvs(self.origin,closestplayer)||random()<0.5)
2091 havocbot_goalrating_ctf_ourbase(30000);
2093 havocbot_goalrating_ctf_ourstolenflag(20000);
2094 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
2095 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
2096 havocbot_goalrating_items(10000, org, mp_radius);
2097 havocbot_goalrating_items(5000, self.origin, 10000);
2098 navigation_goalrating_end();
2102 void havocbot_role_ctf_setrole(entity bot, int role)
2104 LOG_TRACE(strcat(bot.netname," switched to "));
2107 case HAVOCBOT_CTF_ROLE_CARRIER:
2108 LOG_TRACE("carrier");
2109 bot.havocbot_role = havocbot_role_ctf_carrier;
2110 bot.havocbot_role_timeout = 0;
2111 bot.havocbot_cantfindflag = time + 10;
2112 bot.bot_strategytime = 0;
2114 case HAVOCBOT_CTF_ROLE_DEFENSE:
2115 LOG_TRACE("defense");
2116 bot.havocbot_role = havocbot_role_ctf_defense;
2117 bot.havocbot_role_timeout = 0;
2119 case HAVOCBOT_CTF_ROLE_MIDDLE:
2120 LOG_TRACE("middle");
2121 bot.havocbot_role = havocbot_role_ctf_middle;
2122 bot.havocbot_role_timeout = 0;
2124 case HAVOCBOT_CTF_ROLE_OFFENSE:
2125 LOG_TRACE("offense");
2126 bot.havocbot_role = havocbot_role_ctf_offense;
2127 bot.havocbot_role_timeout = 0;
2129 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2130 LOG_TRACE("retriever");
2131 bot.havocbot_previous_role = bot.havocbot_role;
2132 bot.havocbot_role = havocbot_role_ctf_retriever;
2133 bot.havocbot_role_timeout = time + 10;
2134 bot.bot_strategytime = 0;
2136 case HAVOCBOT_CTF_ROLE_ESCORT:
2137 LOG_TRACE("escort");
2138 bot.havocbot_previous_role = bot.havocbot_role;
2139 bot.havocbot_role = havocbot_role_ctf_escort;
2140 bot.havocbot_role_timeout = time + 30;
2141 bot.bot_strategytime = 0;
2152 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2155 int t = 0, t2 = 0, t3 = 0;
2157 // initially clear items so they can be set as necessary later.
2158 self.ctf_flagstatus &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2159 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2160 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2161 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2162 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2163 | CTF_FLAG_NEUTRAL | CTF_SHIELDED);
2165 // scan through all the flags and notify the client about them
2166 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2168 if(flag.team == NUM_TEAM_1) { t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2169 if(flag.team == NUM_TEAM_2) { t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2170 if(flag.team == NUM_TEAM_3) { t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2171 if(flag.team == NUM_TEAM_4) { t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2172 if(flag.team == 0) { t = CTF_NEUTRAL_FLAG_CARRYING; t2 = CTF_NEUTRAL_FLAG_TAKEN; t3 = CTF_NEUTRAL_FLAG_LOST; self.ctf_flagstatus |= CTF_FLAG_NEUTRAL; }
2174 switch(flag.ctf_status)
2179 if((flag.owner == self) || (flag.pass_sender == self))
2180 self.ctf_flagstatus |= t; // carrying: self is currently carrying the flag
2182 self.ctf_flagstatus |= t2; // taken: someone else is carrying the flag
2187 self.ctf_flagstatus |= t3; // lost: the flag is dropped somewhere on the map
2193 // item for stopping players from capturing the flag too often
2194 if(self.ctf_captureshielded)
2195 self.ctf_flagstatus |= CTF_SHIELDED;
2197 // update the health of the flag carrier waypointsprite
2198 if(self.wps_flagcarrier)
2199 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
2204 MUTATOR_HOOKFUNCTION(ctf, PlayerDamage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2206 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2208 if(frag_target == frag_attacker) // damage done to yourself
2210 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2211 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2213 else // damage done to everyone else
2215 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2216 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2219 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2221 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)))
2222 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2224 frag_target.wps_helpme_time = time;
2225 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2227 // todo: add notification for when flag carrier needs help?
2232 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2234 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2236 PlayerTeamScore_AddScore(frag_attacker, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2237 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
2240 if(frag_target.flagcarried)
2242 entity tmp_entity = frag_target.flagcarried;
2243 ctf_Handle_Throw(frag_target, world, DROP_NORMAL);
2244 tmp_entity.ctf_dropper = world;
2250 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2253 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2256 void ctf_RemovePlayer(entity player)
2258 if(player.flagcarried)
2259 { ctf_Handle_Throw(player, world, DROP_NORMAL); }
2261 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2263 if(flag.pass_sender == player) { flag.pass_sender = world; }
2264 if(flag.pass_target == player) { flag.pass_target = world; }
2265 if(flag.ctf_dropper == player) { flag.ctf_dropper = world; }
2269 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2271 ctf_RemovePlayer(self);
2275 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2277 ctf_RemovePlayer(self);
2281 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2283 if(self.flagcarried)
2284 if(!autocvar_g_ctf_portalteleport)
2285 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2290 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2292 if(MUTATOR_RETURNVALUE || gameover) { return false; }
2294 entity player = self;
2296 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2298 // pass the flag to a team mate
2299 if(autocvar_g_ctf_pass)
2301 entity head, closest_target = world;
2302 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2304 while(head) // find the closest acceptable target to pass to
2306 if(IS_PLAYER(head) && !IS_DEAD(head))
2307 if(head != player && SAME_TEAM(head, player))
2308 if(!head.speedrunning && !head.vehicle)
2310 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2311 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2312 vector passer_center = CENTER_OR_VIEWOFS(player);
2314 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2316 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2318 if(IS_BOT_CLIENT(head))
2320 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2321 ctf_Handle_Throw(head, player, DROP_PASS);
2325 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2326 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2328 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2331 else if(player.flagcarried)
2335 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2336 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
2337 { closest_target = head; }
2339 else { closest_target = head; }
2346 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2349 // throw the flag in front of you
2350 if(autocvar_g_ctf_throw && player.flagcarried)
2352 if(player.throw_count == -1)
2354 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2356 player.throw_prevtime = time;
2357 player.throw_count = 1;
2358 ctf_Handle_Throw(player, world, DROP_THROW);
2363 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2369 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2370 else { player.throw_count += 1; }
2371 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2373 player.throw_prevtime = time;
2374 ctf_Handle_Throw(player, world, DROP_THROW);
2383 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2385 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2387 self.wps_helpme_time = time;
2388 WaypointSprite_HelpMePing(self.wps_flagcarrier);
2390 else // create a normal help me waypointsprite
2392 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, self, FLAG_WAYPOINT_OFFSET, world, self.team, self, wps_helpme, false, RADARICON_HELPME);
2393 WaypointSprite_Ping(self.wps_helpme);
2399 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2401 if(vh_player.flagcarried)
2403 vh_player.flagcarried.nodrawtoclient = vh_player; // hide the flag from the driver
2405 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2407 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
2411 setattachment(vh_player.flagcarried, vh_vehicle, "");
2412 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
2413 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2414 //vh_player.flagcarried.angles = '0 0 0';
2422 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2424 if(vh_player.flagcarried)
2426 setattachment(vh_player.flagcarried, vh_player, "");
2427 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2428 vh_player.flagcarried.scale = FLAG_SCALE;
2429 vh_player.flagcarried.angles = '0 0 0';
2430 vh_player.flagcarried.nodrawtoclient = world;
2437 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2439 if(self.flagcarried)
2441 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((self.flagcarried.team) ? APP_TEAM_ENT(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN) : INFO_CTF_FLAGRETURN_ABORTRUN_NEUTRAL));
2442 ctf_RespawnFlag(self.flagcarried);
2449 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2451 entity flag; // temporary entity for the search method
2453 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2455 switch(flag.ctf_status)
2460 // lock the flag, game is over
2461 flag.movetype = MOVETYPE_NONE;
2462 flag.takedamage = DAMAGE_NO;
2463 flag.solid = SOLID_NOT;
2464 flag.nextthink = false; // stop thinking
2466 //dprint("stopping the ", flag.netname, " from moving.\n");
2474 // do nothing for these flags
2483 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2485 havocbot_ctf_reset_role(self);
2489 MUTATOR_HOOKFUNCTION(ctf, GetTeamCount)
2491 //ret_float = ctf_teams;
2492 ret_string = "ctf_team";
2496 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2498 self.ctf_flagstatus = other.ctf_flagstatus;
2502 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2504 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2506 if (MapInfo_Get_ByID(i))
2508 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2514 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2515 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2522 bool superspec_Spectate(entity _player); // TODO
2523 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2524 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2526 if(IS_PLAYER(self) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2528 if(cmd_name == "followfc")
2540 case "red": _team = NUM_TEAM_1; break;
2541 case "blue": _team = NUM_TEAM_2; break;
2542 case "yellow": if(ctf_teams >= 3) _team = NUM_TEAM_3; break;
2543 case "pink": if(ctf_teams >= 4) _team = NUM_TEAM_4; break;
2547 FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
2548 if(it.flagcarried && (it.team == _team || _team == 0))
2551 if(_team == 0 && IS_SPEC(self) && self.enemy == it)
2552 continue; // already spectating this fc, try another
2553 return superspec_Spectate(it);
2558 superspec_msg("", "", self, "No active flag carrier\n", 1);
2565 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2567 if(frag_target.flagcarried)
2568 ctf_Handle_Throw(frag_target, world, DROP_THROW);
2578 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2579 CTF flag for team one (Red).
2581 "angle" Angle the flag will point (minus 90 degrees)...
2582 "model" model to use, note this needs red and blue as skins 0 and 1...
2583 "noise" sound played when flag is picked up...
2584 "noise1" sound played when flag is returned by a teammate...
2585 "noise2" sound played when flag is captured...
2586 "noise3" sound played when flag is lost in the field and respawns itself...
2587 "noise4" sound played when flag is dropped by a player...
2588 "noise5" sound played when flag touches the ground... */
2589 spawnfunc(item_flag_team1)
2591 if(!g_ctf) { remove(self); return; }
2593 ctf_FlagSetup(NUM_TEAM_1, self);
2596 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2597 CTF flag for team two (Blue).
2599 "angle" Angle the flag will point (minus 90 degrees)...
2600 "model" model to use, note this needs red and blue as skins 0 and 1...
2601 "noise" sound played when flag is picked up...
2602 "noise1" sound played when flag is returned by a teammate...
2603 "noise2" sound played when flag is captured...
2604 "noise3" sound played when flag is lost in the field and respawns itself...
2605 "noise4" sound played when flag is dropped by a player...
2606 "noise5" sound played when flag touches the ground... */
2607 spawnfunc(item_flag_team2)
2609 if(!g_ctf) { remove(self); return; }
2611 ctf_FlagSetup(NUM_TEAM_2, self);
2614 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2615 CTF flag for team three (Yellow).
2617 "angle" Angle the flag will point (minus 90 degrees)...
2618 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2619 "noise" sound played when flag is picked up...
2620 "noise1" sound played when flag is returned by a teammate...
2621 "noise2" sound played when flag is captured...
2622 "noise3" sound played when flag is lost in the field and respawns itself...
2623 "noise4" sound played when flag is dropped by a player...
2624 "noise5" sound played when flag touches the ground... */
2625 spawnfunc(item_flag_team3)
2627 if(!g_ctf) { remove(self); return; }
2629 ctf_FlagSetup(NUM_TEAM_3, self);
2632 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2633 CTF flag for team four (Pink).
2635 "angle" Angle the flag will point (minus 90 degrees)...
2636 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2637 "noise" sound played when flag is picked up...
2638 "noise1" sound played when flag is returned by a teammate...
2639 "noise2" sound played when flag is captured...
2640 "noise3" sound played when flag is lost in the field and respawns itself...
2641 "noise4" sound played when flag is dropped by a player...
2642 "noise5" sound played when flag touches the ground... */
2643 spawnfunc(item_flag_team4)
2645 if(!g_ctf) { remove(self); return; }
2647 ctf_FlagSetup(NUM_TEAM_4, self);
2650 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2653 "angle" Angle the flag will point (minus 90 degrees)...
2654 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2655 "noise" sound played when flag is picked up...
2656 "noise1" sound played when flag is returned by a teammate...
2657 "noise2" sound played when flag is captured...
2658 "noise3" sound played when flag is lost in the field and respawns itself...
2659 "noise4" sound played when flag is dropped by a player...
2660 "noise5" sound played when flag touches the ground... */
2661 spawnfunc(item_flag_neutral)
2663 if(!g_ctf) { remove(self); return; }
2664 if(!cvar("g_ctf_oneflag")) { remove(self); return; }
2666 ctf_FlagSetup(0, self);
2669 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2670 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2671 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.
2673 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2674 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2677 if(!g_ctf) { remove(self); return; }
2679 self.classname = "ctf_team";
2680 self.team = self.cnt + 1;
2683 // compatibility for quake maps
2684 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2685 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2686 spawnfunc(info_player_team1);
2687 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2688 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2689 spawnfunc(info_player_team2);
2690 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2691 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2693 void team_CTF_neutralflag() { SELFPARAM(); spawnfunc_item_flag_neutral(self); }
2694 void team_neutralobelisk() { SELFPARAM(); spawnfunc_item_flag_neutral(self); }
2702 void ctf_ScoreRules(int teams)
2704 CheckAllowedTeams(world);
2705 ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
2706 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2707 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2708 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2709 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2710 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2711 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2712 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2713 ScoreRules_basics_end();
2716 // code from here on is just to support maps that don't have flag and team entities
2717 void ctf_SpawnTeam (string teamname, int teamcolor)
2719 entity this = new_pure(ctf_team);
2720 this.netname = teamname;
2721 this.cnt = teamcolor;
2722 this.spawnfunc_checked = true;
2723 WITH(entity, self, this, spawnfunc_ctf_team(this));
2726 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2731 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2733 if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2734 if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2735 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2738 ctf_teams = bound(2, ctf_teams, 4);
2740 // if no teams are found, spawn defaults
2741 if(find(world, classname, "ctf_team") == world)
2743 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.\n");
2744 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2745 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2747 ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2749 ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2752 ctf_ScoreRules(ctf_teams);
2755 void ctf_Initialize()
2757 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2759 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2760 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2761 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2763 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);