7 REGISTER_MUTATOR(ctf, false)
11 if (time > 1) // game loads at time 1
12 error("This is a game type and it cannot be added at runtime.");
16 SetLimits(autocvar_capturelimit_override, autocvar_captureleadlimit_override, -1, -1);
17 have_team_spawns = -1; // request team spawns
20 MUTATOR_ONROLLBACK_OR_REMOVE
22 // we actually cannot roll back ctf_Initialize here
23 // BUT: we don't need to! If this gets called, adding always
29 LOG_INFO("This is a game type and it cannot be removed at runtime.");
39 void ctf_RespawnFlag(entity flag);
41 // score rule declarations
42 const int ST_CTF_CAPS = 1;
43 const int SP_CTF_CAPS = 4;
44 const int SP_CTF_CAPTIME = 5;
45 const int SP_CTF_PICKUPS = 6;
46 const int SP_CTF_DROPS = 7;
47 const int SP_CTF_FCKILLS = 8;
48 const int SP_CTF_RETURNS = 9;
51 ATTRIB(Flag, m_mins, vector, PL_MIN_CONST + '0 0 -13')
52 ATTRIB(Flag, m_maxs, vector, PL_MAX_CONST + '0 0 -13')
54 Flag CTF_FLAG; STATIC_INIT(Flag) { CTF_FLAG = NEW(Flag); }
55 void ctf_FlagTouch() { SELFPARAM(); ITEM_HANDLE(Pickup, CTF_FLAG, this, other); }
57 // flag constants // for most of these, there is just one question to be asked: WHYYYYY?
59 const float FLAG_SCALE = 0.6;
61 const float FLAG_THINKRATE = 0.2;
62 const float FLAG_TOUCHRATE = 0.5;
63 const float WPFE_THINKRATE = 0.5;
65 const vector FLAG_DROP_OFFSET = ('0 0 32');
66 const vector FLAG_CARRY_OFFSET = ('-16 0 8');
67 #define FLAG_SPAWN_OFFSET ('0 0 1' * (PL_MAX_CONST.z - 13))
68 const vector FLAG_WAYPOINT_OFFSET = ('0 0 64');
69 const vector FLAG_FLOAT_OFFSET = ('0 0 32');
70 const vector FLAG_PASS_ARC_OFFSET = ('0 0 -10');
72 const vector VEHICLE_FLAG_OFFSET = ('0 0 96');
73 const float VEHICLE_FLAG_SCALE = 1.0;
76 #define WPCOLOR_ENEMYFC(t) ((t) ? colormapPaletteColor(t - 1, false) * 0.75 : '1 1 1')
77 #define WPCOLOR_FLAGCARRIER(t) (WP_FlagCarrier.m_color)
78 #define WPCOLOR_DROPPEDFLAG(t) ((t) ? ('0.25 0.25 0.25' + colormapPaletteColor(t - 1, false)) * 0.5 : '1 1 1')
81 #define snd_flag_taken noise
82 #define snd_flag_returned noise1
83 #define snd_flag_capture noise2
84 #define snd_flag_respawn noise3
85 .string snd_flag_dropped;
86 .string snd_flag_touch;
87 .string snd_flag_pass;
94 // list of flags on the map
95 entity ctf_worldflaglist;
96 .entity ctf_worldflagnext;
97 .entity ctf_staleflagnext;
100 .entity bot_basewaypoint; // flag waypointsprite
102 .entity wps_flagbase;
103 .entity wps_flagcarrier;
104 .entity wps_flagdropped;
105 .entity wps_enemyflagcarrier;
106 .float wps_helpme_time;
107 bool wpforenemy_announced;
108 float wpforenemy_nextthink;
111 const int FLAG_BASE = 1;
112 const int FLAG_DROPPED = 2;
113 const int FLAG_CARRY = 3;
114 const int FLAG_PASSING = 4;
116 const int DROP_NORMAL = 1;
117 const int DROP_THROW = 2;
118 const int DROP_PASS = 3;
119 const int DROP_RESET = 4;
121 const int PICKUP_BASE = 1;
122 const int PICKUP_DROPPED = 2;
124 const int CAPTURE_NORMAL = 1;
125 const int CAPTURE_DROPPED = 2;
127 const int RETURN_TIMEOUT = 1;
128 const int RETURN_DROPPED = 2;
129 const int RETURN_DAMAGE = 3;
130 const int RETURN_SPEEDRUN = 4;
131 const int RETURN_NEEDKILL = 5;
133 void ctf_Handle_Throw(entity player, entity receiver, float droptype);
136 #define ctf_spawnorigin dropped_origin
137 bool ctf_stalemate; // indicates that a stalemate is active
138 float ctf_captimerecord; // record time for capturing the flag
139 .float ctf_pickuptime;
141 .int ctf_status; // status of the flag (FLAG_BASE, FLAG_DROPPED, FLAG_CARRY declared globally)
142 .entity ctf_dropper; // don't allow spam of dropping the flag
143 .int max_flag_health;
144 .float next_take_time;
145 .bool ctf_flagdamaged;
148 // passing/throwing properties
149 .float pass_distance;
152 .float throw_antispam;
153 .float throw_prevtime;
156 // CaptureShield: If the player is too bad to be allowed to capture, shield them from taking the flag.
157 .bool ctf_captureshielded; // set to 1 if the player is too bad to be allowed to capture
158 float ctf_captureshield_min_negscore; // punish at -20 points
159 float ctf_captureshield_max_ratio; // punish at most 30% of each team
160 float ctf_captureshield_force; // push force of the shield
163 bool ctf_oneflag; // indicates whether or not a neutral flag has been found
166 const int HAVOCBOT_CTF_ROLE_NONE = 0;
167 const int HAVOCBOT_CTF_ROLE_DEFENSE = 2;
168 const int HAVOCBOT_CTF_ROLE_MIDDLE = 4;
169 const int HAVOCBOT_CTF_ROLE_OFFENSE = 8;
170 const int HAVOCBOT_CTF_ROLE_CARRIER = 16;
171 const int HAVOCBOT_CTF_ROLE_RETRIEVER = 32;
172 const int HAVOCBOT_CTF_ROLE_ESCORT = 64;
174 .bool havocbot_cantfindflag;
176 vector havocbot_ctf_middlepoint;
177 float havocbot_ctf_middlepoint_radius;
179 void havocbot_role_ctf_setrole(entity bot, int role);
182 #define CTF_SAMETEAM(a,b) ((autocvar_g_ctf_reverse || (ctf_oneflag && autocvar_g_ctf_oneflag_reverse)) ? DIFF_TEAM(a,b) : SAME_TEAM(a,b))
183 #define CTF_DIFFTEAM(a,b) ((autocvar_g_ctf_reverse || (ctf_oneflag && autocvar_g_ctf_oneflag_reverse)) ? SAME_TEAM(a,b) : DIFF_TEAM(a,b))
185 // networked flag statuses
189 const int CTF_RED_FLAG_TAKEN = 1;
190 const int CTF_RED_FLAG_LOST = 2;
191 const int CTF_RED_FLAG_CARRYING = 3;
192 const int CTF_BLUE_FLAG_TAKEN = 4;
193 const int CTF_BLUE_FLAG_LOST = 8;
194 const int CTF_BLUE_FLAG_CARRYING = 12;
195 const int CTF_YELLOW_FLAG_TAKEN = 16;
196 const int CTF_YELLOW_FLAG_LOST = 32;
197 const int CTF_YELLOW_FLAG_CARRYING = 48;
198 const int CTF_PINK_FLAG_TAKEN = 64;
199 const int CTF_PINK_FLAG_LOST = 128;
200 const int CTF_PINK_FLAG_CARRYING = 192;
201 const int CTF_NEUTRAL_FLAG_TAKEN = 256;
202 const int CTF_NEUTRAL_FLAG_LOST = 512;
203 const int CTF_NEUTRAL_FLAG_CARRYING = 768;
204 const int CTF_FLAG_NEUTRAL = 2048;
205 const int CTF_SHIELDED = 4096;
208 #ifdef IMPLEMENTATION
211 #include "../../../common/vehicles/all.qh"
212 #include "../../teamplay.qh"
215 #include "../../../lib/warpzone/common.qh"
217 bool autocvar_g_ctf_allow_vehicle_carry;
218 bool autocvar_g_ctf_allow_vehicle_touch;
219 bool autocvar_g_ctf_allow_monster_touch;
220 bool autocvar_g_ctf_throw;
221 float autocvar_g_ctf_throw_angle_max;
222 float autocvar_g_ctf_throw_angle_min;
223 int autocvar_g_ctf_throw_punish_count;
224 float autocvar_g_ctf_throw_punish_delay;
225 float autocvar_g_ctf_throw_punish_time;
226 float autocvar_g_ctf_throw_strengthmultiplier;
227 float autocvar_g_ctf_throw_velocity_forward;
228 float autocvar_g_ctf_throw_velocity_up;
229 float autocvar_g_ctf_drop_velocity_up;
230 float autocvar_g_ctf_drop_velocity_side;
231 bool autocvar_g_ctf_oneflag_reverse;
232 bool autocvar_g_ctf_portalteleport;
233 bool autocvar_g_ctf_pass;
234 float autocvar_g_ctf_pass_arc;
235 float autocvar_g_ctf_pass_arc_max;
236 float autocvar_g_ctf_pass_directional_max;
237 float autocvar_g_ctf_pass_directional_min;
238 float autocvar_g_ctf_pass_radius;
239 float autocvar_g_ctf_pass_wait;
240 bool autocvar_g_ctf_pass_request;
241 float autocvar_g_ctf_pass_turnrate;
242 float autocvar_g_ctf_pass_timelimit;
243 float autocvar_g_ctf_pass_velocity;
244 bool autocvar_g_ctf_dynamiclights;
245 float autocvar_g_ctf_flag_collect_delay;
246 float autocvar_g_ctf_flag_damageforcescale;
247 bool autocvar_g_ctf_flag_dropped_waypoint;
248 bool autocvar_g_ctf_flag_dropped_floatinwater;
249 bool autocvar_g_ctf_flag_glowtrails;
250 int autocvar_g_ctf_flag_health;
251 bool autocvar_g_ctf_flag_return;
252 float autocvar_g_ctf_flag_return_carried_radius;
253 float autocvar_g_ctf_flag_return_time;
254 bool autocvar_g_ctf_flag_return_when_unreachable;
255 float autocvar_g_ctf_flag_return_damage;
256 float autocvar_g_ctf_flag_return_damage_delay;
257 float autocvar_g_ctf_flag_return_dropped;
258 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
259 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
260 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
261 float autocvar_g_ctf_flagcarrier_selfforcefactor;
262 float autocvar_g_ctf_flagcarrier_damagefactor;
263 float autocvar_g_ctf_flagcarrier_forcefactor;
264 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
265 bool autocvar_g_ctf_fullbrightflags;
266 bool autocvar_g_ctf_ignore_frags;
267 int autocvar_g_ctf_score_capture;
268 int autocvar_g_ctf_score_capture_assist;
269 int autocvar_g_ctf_score_kill;
270 int autocvar_g_ctf_score_penalty_drop;
271 int autocvar_g_ctf_score_penalty_returned;
272 int autocvar_g_ctf_score_pickup_base;
273 int autocvar_g_ctf_score_pickup_dropped_early;
274 int autocvar_g_ctf_score_pickup_dropped_late;
275 int autocvar_g_ctf_score_return;
276 float autocvar_g_ctf_shield_force;
277 float autocvar_g_ctf_shield_max_ratio;
278 int autocvar_g_ctf_shield_min_negscore;
279 bool autocvar_g_ctf_stalemate;
280 int autocvar_g_ctf_stalemate_endcondition;
281 float autocvar_g_ctf_stalemate_time;
282 bool autocvar_g_ctf_reverse;
283 float autocvar_g_ctf_dropped_capture_delay;
284 float autocvar_g_ctf_dropped_capture_radius;
286 void ctf_FakeTimeLimit(entity e, float t)
289 WriteByte(MSG_ONE, 3); // svc_updatestat
290 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
292 WriteCoord(MSG_ONE, autocvar_timelimit);
294 WriteCoord(MSG_ONE, (t + 1) / 60);
297 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
299 if(autocvar_sv_eventlog)
300 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != world) ? ftos(actor.playerid) : "")));
301 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
304 void ctf_CaptureRecord(entity flag, entity player)
306 float cap_record = ctf_captimerecord;
307 float cap_time = (time - flag.ctf_pickuptime);
308 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
311 if(ctf_oneflag) { Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname); }
312 else if(!ctf_captimerecord) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_TIME_), player.netname, (cap_time * 100)); }
313 else if(cap_time < cap_record) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_BROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
314 else { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_UNBROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
316 // write that shit in the database
317 if(!ctf_oneflag) // but not in 1-flag mode
318 if((!ctf_captimerecord) || (cap_time < cap_record))
320 ctf_captimerecord = cap_time;
321 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
322 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
323 write_recordmarker(player, (time - cap_time), cap_time);
327 void ctf_FlagcarrierWaypoints(entity player)
329 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
330 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
331 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
332 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
335 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
337 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
338 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
339 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
340 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
343 if(current_height) // make sure we can actually do this arcing path
345 targpos = (to + ('0 0 1' * current_height));
346 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
347 if(trace_fraction < 1)
349 //print("normal arc line failed, trying to find new pos...");
350 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
351 targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
352 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
353 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
354 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
357 else { targpos = to; }
359 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
361 vector desired_direction = normalize(targpos - from);
362 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
363 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
366 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
368 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
370 // directional tracing only
372 makevectors(passer_angle);
374 // find the closest point on the enemy to the center of the attack
375 float h; // hypotenuse, which is the distance between attacker to head
376 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
378 h = vlen(head_center - passer_center);
379 a = h * (normalize(head_center - passer_center) * v_forward);
381 vector nearest_on_line = (passer_center + a * v_forward);
382 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
384 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
385 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
387 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
392 else { return true; }
396 // =======================
397 // CaptureShield Functions
398 // =======================
400 bool ctf_CaptureShield_CheckStatus(entity p)
402 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
404 int players_worseeq, players_total;
406 if(ctf_captureshield_max_ratio <= 0)
409 s = PlayerScore_Add(p, SP_CTF_CAPS, 0);
410 s2 = PlayerScore_Add(p, SP_CTF_PICKUPS, 0);
411 s3 = PlayerScore_Add(p, SP_CTF_RETURNS, 0);
412 s4 = PlayerScore_Add(p, SP_CTF_FCKILLS, 0);
414 sr = ((s - s2) + (s3 + s4));
416 if(sr >= -ctf_captureshield_min_negscore)
419 players_total = players_worseeq = 0;
424 se = PlayerScore_Add(e, SP_CTF_CAPS, 0);
425 se2 = PlayerScore_Add(e, SP_CTF_PICKUPS, 0);
426 se3 = PlayerScore_Add(e, SP_CTF_RETURNS, 0);
427 se4 = PlayerScore_Add(e, 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_4(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 tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
547 entity sender = flag.pass_sender;
549 // transfer flag to player
551 flag.owner.flagcarried = flag;
556 setattachment(flag, player.vehicle, "");
557 setorigin(flag, VEHICLE_FLAG_OFFSET);
558 flag.scale = VEHICLE_FLAG_SCALE;
562 setattachment(flag, player, "");
563 setorigin(flag, FLAG_CARRY_OFFSET);
565 flag.movetype = MOVETYPE_NONE;
566 flag.takedamage = DAMAGE_NO;
567 flag.solid = SOLID_NOT;
568 flag.angles = '0 0 0';
569 flag.ctf_status = FLAG_CARRY;
571 // messages and sounds
572 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
573 ctf_EventLog("receive", flag.team, player);
575 FOR_EACH_REALPLAYER(tmp_player)
577 if(tmp_player == sender)
578 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_SENT_) : CENTER_CTF_PASS_SENT_NEUTRAL), player.netname);
579 else if(tmp_player == player)
580 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_RECEIVED_) : CENTER_CTF_PASS_RECEIVED_NEUTRAL), sender.netname);
581 else if(SAME_TEAM(tmp_player, sender))
582 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_OTHER_) : CENTER_CTF_PASS_OTHER_NEUTRAL), sender.netname, player.netname);
585 // create new waypoint
586 ctf_FlagcarrierWaypoints(player);
588 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
589 player.throw_antispam = sender.throw_antispam;
591 flag.pass_distance = 0;
592 flag.pass_sender = world;
593 flag.pass_target = world;
596 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
598 entity flag = player.flagcarried;
599 vector targ_origin, flag_velocity;
601 if(!flag) { return; }
602 if((droptype == DROP_PASS) && !receiver) { return; }
604 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
607 setattachment(flag, world, "");
608 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
609 flag.owner.flagcarried = world;
611 flag.solid = SOLID_TRIGGER;
612 flag.ctf_dropper = player;
613 flag.ctf_droptime = time;
615 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
622 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
623 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
624 WarpZone_RefSys_Copy(flag, receiver);
625 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
626 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
628 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
629 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
632 flag.movetype = MOVETYPE_FLY;
633 flag.takedamage = DAMAGE_NO;
634 flag.pass_sender = player;
635 flag.pass_target = receiver;
636 flag.ctf_status = FLAG_PASSING;
639 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
640 WarpZone_TrailParticles(world, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
641 ctf_EventLog("pass", flag.team, player);
647 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'));
649 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)));
650 flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, false);
651 ctf_Handle_Drop(flag, player, droptype);
657 flag.velocity = '0 0 0'; // do nothing
664 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);
665 ctf_Handle_Drop(flag, player, droptype);
670 // kill old waypointsprite
671 WaypointSprite_Ping(player.wps_flagcarrier);
672 WaypointSprite_Kill(player.wps_flagcarrier);
674 if(player.wps_enemyflagcarrier)
675 WaypointSprite_Kill(player.wps_enemyflagcarrier);
678 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
681 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
683 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
690 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
692 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
693 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
694 entity player_team_flag = world, tmp_entity;
695 float old_time, new_time;
697 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
698 if(CTF_DIFFTEAM(player, flag)) { return; }
701 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
702 if(SAME_TEAM(tmp_entity, player))
704 player_team_flag = tmp_entity;
708 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
710 player.throw_prevtime = time;
711 player.throw_count = 0;
713 // messages and sounds
714 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((enemy_flag.team) ? APP_TEAM_ENT_4(enemy_flag, CENTER_CTF_CAPTURE_) : CENTER_CTF_CAPTURE_NEUTRAL));
715 ctf_CaptureRecord(enemy_flag, player);
716 _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);
720 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
721 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
726 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
727 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
729 old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
730 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
731 if(!old_time || new_time < old_time)
732 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
735 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
736 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
739 if(capturetype == CAPTURE_NORMAL)
741 WaypointSprite_Kill(player.wps_flagcarrier);
742 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
744 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
745 { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
749 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
750 ctf_RespawnFlag(enemy_flag);
753 void ctf_Handle_Return(entity flag, entity player)
755 // messages and sounds
756 if(IS_MONSTER(player))
758 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_MONSTER_), player.monster_name);
762 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_RETURN_));
763 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_), player.netname);
765 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
766 ctf_EventLog("return", flag.team, player);
769 if(IS_PLAYER(player))
771 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
772 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
774 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
777 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
781 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
782 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
783 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
787 if(player.flagcarried == flag)
788 WaypointSprite_Kill(player.wps_flagcarrier);
791 ctf_RespawnFlag(flag);
794 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
797 float pickup_dropped_score; // used to calculate dropped pickup score
798 entity tmp_entity; // temporary entity
800 // attach the flag to the player
802 player.flagcarried = flag;
805 setattachment(flag, player.vehicle, "");
806 setorigin(flag, VEHICLE_FLAG_OFFSET);
807 flag.scale = VEHICLE_FLAG_SCALE;
811 setattachment(flag, player, "");
812 setorigin(flag, FLAG_CARRY_OFFSET);
816 flag.movetype = MOVETYPE_NONE;
817 flag.takedamage = DAMAGE_NO;
818 flag.solid = SOLID_NOT;
819 flag.angles = '0 0 0';
820 flag.ctf_status = FLAG_CARRY;
824 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
825 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
829 // messages and sounds
830 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_PICKUP_) : INFO_CTF_PICKUP_NEUTRAL), player.netname);
831 if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
832 if(!flag.team) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL); }
833 else if(CTF_DIFFTEAM(player, flag)) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PICKUP_)); }
834 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)); }
836 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, ((flag.team) ? APP_TEAM_ENT_4(flag, CHOICE_CTF_PICKUP_TEAM_) : CHOICE_CTF_PICKUP_TEAM_NEUTRAL), Team_ColorCode(player.team), player.netname);
839 FOR_EACH_PLAYER(tmp_entity)
840 if(tmp_entity != player)
841 if(DIFF_TEAM(player, tmp_entity))
842 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, CHOICE_CTF_PICKUP_ENEMY_NEUTRAL, Team_ColorCode(player.team), player.netname);
845 FOR_EACH_PLAYER(tmp_entity)
846 if(tmp_entity != player)
847 if(CTF_SAMETEAM(flag, tmp_entity))
848 if(SAME_TEAM(player, tmp_entity))
849 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_PICKUP_TEAM_), Team_ColorCode(player.team), player.netname);
851 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, ((SAME_TEAM(flag, player)) ? CHOICE_CTF_PICKUP_ENEMY_TEAM : CHOICE_CTF_PICKUP_ENEMY), Team_ColorCode(player.team), player.netname);
853 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
856 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
857 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
862 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
863 ctf_EventLog("steal", flag.team, player);
869 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);
870 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);
871 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
872 PlayerTeamScore_AddScore(player, pickup_dropped_score);
873 ctf_EventLog("pickup", flag.team, player);
881 if(pickuptype == PICKUP_BASE)
883 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
884 if((player.speedrunning) && (ctf_captimerecord))
885 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
889 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
892 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
893 ctf_FlagcarrierWaypoints(player);
894 WaypointSprite_Ping(player.wps_flagcarrier);
898 // ===================
899 // Main Flag Functions
900 // ===================
902 void ctf_CheckFlagReturn(entity flag, int returntype)
904 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
906 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
908 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
912 case RETURN_DROPPED: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DROPPED_) : INFO_CTF_FLAGRETURN_DROPPED_NEUTRAL)); break;
913 case RETURN_DAMAGE: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DAMAGED_) : INFO_CTF_FLAGRETURN_DAMAGED_NEUTRAL)); break;
914 case RETURN_SPEEDRUN: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_SPEEDRUN_) : INFO_CTF_FLAGRETURN_SPEEDRUN_NEUTRAL), ctf_captimerecord); break;
915 case RETURN_NEEDKILL: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_NEEDKILL_) : INFO_CTF_FLAGRETURN_NEEDKILL_NEUTRAL)); break;
919 { Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_TIMEOUT_) : INFO_CTF_FLAGRETURN_TIMEOUT_NEUTRAL)); break; }
921 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
922 ctf_EventLog("returned", flag.team, world);
923 ctf_RespawnFlag(flag);
928 bool ctf_Stalemate_Customize()
930 // make spectators see what the player would see
932 e = WaypointSprite_getviewentity(other);
933 wp_owner = self.owner;
936 if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
937 if(SAME_TEAM(wp_owner, e)) { return false; }
938 if(!IS_PLAYER(e)) { return false; }
943 void ctf_CheckStalemate()
946 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
949 entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
951 // build list of stale flags
952 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
954 if(autocvar_g_ctf_stalemate)
955 if(tmp_entity.ctf_status != FLAG_BASE)
956 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
958 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
959 ctf_staleflaglist = tmp_entity;
961 switch(tmp_entity.team)
963 case NUM_TEAM_1: ++stale_red_flags; break;
964 case NUM_TEAM_2: ++stale_blue_flags; break;
965 case NUM_TEAM_3: ++stale_yellow_flags; break;
966 case NUM_TEAM_4: ++stale_pink_flags; break;
967 default: ++stale_neutral_flags; break;
973 stale_flags = (stale_neutral_flags >= 1);
975 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
977 if(ctf_oneflag && stale_flags == 1)
978 ctf_stalemate = true;
979 else if(stale_flags >= 2)
980 ctf_stalemate = true;
981 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
982 { ctf_stalemate = false; wpforenemy_announced = false; }
983 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
984 { ctf_stalemate = false; wpforenemy_announced = false; }
986 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
989 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
991 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
993 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);
994 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
995 tmp_entity.owner.wps_enemyflagcarrier.customizeentityforclient = ctf_Stalemate_Customize;
999 if (!wpforenemy_announced)
1001 FOR_EACH_REALPLAYER(tmp_entity)
1002 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CENTER, ((tmp_entity.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER));
1004 wpforenemy_announced = true;
1009 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
1011 if(ITEM_DAMAGE_NEEDKILL(deathtype))
1013 if(autocvar_g_ctf_flag_return_damage_delay)
1015 self.ctf_flagdamaged = true;
1020 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
1024 if(autocvar_g_ctf_flag_return_damage)
1026 // reduce health and check if it should be returned
1027 self.health = self.health - damage;
1028 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
1033 void ctf_FlagThink()
1038 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
1041 if(self == ctf_worldflaglist) // only for the first flag
1042 FOR_EACH_CLIENT(tmp_entity)
1043 ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
1046 if(self.mins != CTF_FLAG.m_mins || self.maxs != CTF_FLAG.m_maxs) { // reset the flag boundaries in case it got squished
1047 LOG_TRACE("wtf the flag got squashed?\n");
1048 tracebox(self.origin, CTF_FLAG.m_mins, CTF_FLAG.m_maxs, self.origin, MOVE_NOMONSTERS, self);
1049 if(!trace_startsolid || self.noalign) // can we resize it without getting stuck?
1050 setsize(self, CTF_FLAG.m_mins, CTF_FLAG.m_maxs); }
1052 switch(self.ctf_status) // reset flag angles in case warpzones adjust it
1056 self.angles = '0 0 0';
1063 // main think method
1064 switch(self.ctf_status)
1068 if(autocvar_g_ctf_dropped_capture_radius)
1070 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
1071 if(tmp_entity.ctf_status == FLAG_DROPPED)
1072 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
1073 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
1074 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
1081 if(autocvar_g_ctf_flag_dropped_floatinwater)
1083 vector midpoint = ((self.absmin + self.absmax) * 0.5);
1084 if(pointcontents(midpoint) == CONTENT_WATER)
1086 self.velocity = self.velocity * 0.5;
1088 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
1089 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
1091 { self.movetype = MOVETYPE_FLY; }
1093 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
1095 if(autocvar_g_ctf_flag_return_dropped)
1097 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
1100 ctf_CheckFlagReturn(self, RETURN_DROPPED);
1104 if(self.ctf_flagdamaged)
1106 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
1107 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
1110 else if(autocvar_g_ctf_flag_return_time)
1112 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
1113 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
1121 if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
1124 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
1126 setself(self.owner);
1127 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
1131 if(autocvar_g_ctf_stalemate)
1133 if(time >= wpforenemy_nextthink)
1135 ctf_CheckStalemate();
1136 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1139 if(CTF_SAMETEAM(self, self.owner) && self.team)
1141 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1142 ctf_Handle_Throw(self.owner, world, DROP_THROW);
1143 else if(vlen(self.owner.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_carried_radius)
1144 ctf_Handle_Return(self, self.owner);
1151 vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
1152 targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
1153 WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
1155 if((self.pass_target == world)
1156 || (self.pass_target.deadflag != DEAD_NO)
1157 || (self.pass_target.flagcarried)
1158 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
1159 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
1160 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1162 // give up, pass failed
1163 ctf_Handle_Drop(self, world, DROP_PASS);
1167 // still a viable target, go for it
1168 ctf_CalculatePassVelocity(self, targ_origin, self.origin, true);
1173 default: // this should never happen
1175 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?\n");
1181 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1184 if(gameover) { return; }
1185 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1187 bool is_not_monster = (!IS_MONSTER(toucher));
1189 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1190 if(ITEM_TOUCH_NEEDKILL())
1192 if(!autocvar_g_ctf_flag_return_damage_delay)
1195 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1197 if(!flag.ctf_flagdamaged) { return; }
1200 int num_perteam = 0;
1201 entity tmp_entity; FOR_EACH_PLAYER(tmp_entity) if(SAME_TEAM(toucher, tmp_entity)) { ++num_perteam; }
1203 // special touch behaviors
1204 if(toucher.frozen) { return; }
1205 else if(IS_VEHICLE(toucher))
1207 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1208 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1210 return; // do nothing
1212 else if(IS_MONSTER(toucher))
1214 if(!autocvar_g_ctf_allow_monster_touch)
1215 return; // do nothing
1217 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1219 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1221 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1222 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1223 flag.wait = time + FLAG_TOUCHRATE;
1227 else if(toucher.deadflag != DEAD_NO) { return; }
1229 switch(flag.ctf_status)
1235 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1236 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1237 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1238 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1240 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1241 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1242 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1243 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1249 if(CTF_SAMETEAM(toucher, flag) && (autocvar_g_ctf_flag_return || num_perteam <= 1) && flag.team) // automatically return if there's only 1 player on the team
1250 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1251 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1252 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1258 LOG_TRACE("Someone touched a flag even though it was being carried?\n");
1264 if((IS_PLAYER(toucher)) && (toucher.deadflag == DEAD_NO) && (toucher != flag.pass_sender))
1266 if(DIFF_TEAM(toucher, flag.pass_sender))
1267 ctf_Handle_Return(flag, toucher);
1269 ctf_Handle_Retrieve(flag, toucher);
1276 .float last_respawn;
1277 void ctf_RespawnFlag(entity flag)
1279 // check for flag respawn being called twice in a row
1280 if(flag.last_respawn > time - 0.5)
1281 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1283 flag.last_respawn = time;
1285 // reset the player (if there is one)
1286 if((flag.owner) && (flag.owner.flagcarried == flag))
1288 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1289 WaypointSprite_Kill(flag.wps_flagcarrier);
1291 flag.owner.flagcarried = world;
1293 if(flag.speedrunning)
1294 ctf_FakeTimeLimit(flag.owner, -1);
1297 if((flag.owner) && (flag.owner.vehicle))
1298 flag.scale = FLAG_SCALE;
1300 if(flag.ctf_status == FLAG_DROPPED)
1301 { WaypointSprite_Kill(flag.wps_flagdropped); }
1304 setattachment(flag, world, "");
1305 setorigin(flag, flag.ctf_spawnorigin);
1307 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
1308 flag.takedamage = DAMAGE_NO;
1309 flag.health = flag.max_flag_health;
1310 flag.solid = SOLID_TRIGGER;
1311 flag.velocity = '0 0 0';
1312 flag.angles = flag.mangle;
1313 flag.flags = FL_ITEM | FL_NOTARGET;
1315 flag.ctf_status = FLAG_BASE;
1317 flag.pass_distance = 0;
1318 flag.pass_sender = world;
1319 flag.pass_target = world;
1320 flag.ctf_dropper = world;
1321 flag.ctf_pickuptime = 0;
1322 flag.ctf_droptime = 0;
1323 flag.ctf_flagdamaged = 0;
1325 ctf_CheckStalemate();
1328 void ctf_Reset(entity this)
1330 if(this.owner && IS_PLAYER(this.owner))
1331 ctf_Handle_Throw(this.owner, world, DROP_RESET);
1333 ctf_RespawnFlag(this);
1336 void ctf_DelayedFlagSetup() // called after a flag is placed on a map by ctf_FlagSetup()
1339 waypoint_spawnforitem_force(self, self.origin);
1340 self.nearestwaypointtimeout = 0; // activate waypointing again
1341 self.bot_basewaypoint = self.nearestwaypoint;
1347 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1348 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1349 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1350 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1351 default: basename = WP_FlagBaseNeutral; break;
1354 entity wp = WaypointSprite_SpawnFixed(basename, self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG);
1355 wp.colormod = ((self.team) ? Team_ColorRGB(self.team) : '1 1 1');
1356 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, ((self.team) ? colormapPaletteColor(self.team - 1, false) : '1 1 1'));
1358 // captureshield setup
1359 ctf_CaptureShield_Spawn(self);
1362 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1365 setself(flag); // for later usage with droptofloor()
1368 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1369 ctf_worldflaglist = flag;
1371 setattachment(flag, world, "");
1373 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1374 flag.team = teamnumber;
1375 flag.classname = "item_flag_team";
1376 flag.target = "###item###"; // wut?
1377 flag.flags = FL_ITEM | FL_NOTARGET;
1378 flag.solid = SOLID_TRIGGER;
1379 flag.takedamage = DAMAGE_NO;
1380 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1381 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1382 flag.health = flag.max_flag_health;
1383 flag.event_damage = ctf_FlagDamage;
1384 flag.pushable = true;
1385 flag.teleportable = TELEPORT_NORMAL;
1386 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP;
1387 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1388 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1389 flag.velocity = '0 0 0';
1390 flag.mangle = flag.angles;
1391 flag.reset = ctf_Reset;
1392 flag.touch = ctf_FlagTouch;
1393 flag.think = ctf_FlagThink;
1394 flag.nextthink = time + FLAG_THINKRATE;
1395 flag.ctf_status = FLAG_BASE;
1397 string teamname = Static_Team_ColorName_Lower(teamnumber);
1399 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1400 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1401 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1402 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnumber).eent_eff_name; }
1403 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnumber).eent_eff_name; }
1404 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnumber).eent_eff_name; }
1407 flag.snd_flag_taken = strzone(SND(CTF_TAKEN(teamnumber)));
1408 flag.snd_flag_returned = strzone(SND(CTF_RETURNED(teamnumber)));
1409 flag.snd_flag_capture = strzone(SND(CTF_CAPTURE(teamnumber)));
1410 flag.snd_flag_dropped = strzone(SND(CTF_DROPPED(teamnumber)));
1411 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.
1412 precache_sound(flag.snd_flag_respawn);
1413 if (flag.snd_flag_touch == "") flag.snd_flag_touch = strzone(SND(CTF_TOUCH)); // again has no team-based sound
1414 precache_sound(flag.snd_flag_touch);
1415 if (flag.snd_flag_pass == "") flag.snd_flag_pass = strzone(SND(CTF_PASS)); // same story here
1416 precache_sound(flag.snd_flag_pass);
1419 precache_model(flag.model);
1422 _setmodel(flag, flag.model); // precision set below
1423 setsize(flag, CTF_FLAG.m_mins, CTF_FLAG.m_maxs);
1424 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1426 if(autocvar_g_ctf_flag_glowtrails)
1430 case NUM_TEAM_1: flag.glow_color = 251; break;
1431 case NUM_TEAM_2: flag.glow_color = 210; break;
1432 case NUM_TEAM_3: flag.glow_color = 110; break;
1433 case NUM_TEAM_4: flag.glow_color = 145; break;
1434 default: flag.glow_color = 254; break;
1436 flag.glow_size = 25;
1437 flag.glow_trail = 1;
1440 flag.effects |= EF_LOWPRECISION;
1441 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1442 if(autocvar_g_ctf_dynamiclights)
1446 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1447 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1448 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1449 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1450 default: flag.effects |= EF_DIMLIGHT; break;
1455 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1457 flag.dropped_origin = flag.origin;
1458 flag.noalign = true;
1459 flag.movetype = MOVETYPE_NONE;
1461 else // drop to floor, automatically find a platform and set that as spawn origin
1463 flag.noalign = false;
1466 flag.movetype = MOVETYPE_TOSS;
1469 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1477 // NOTE: LEGACY CODE, needs to be re-written!
1479 void havocbot_calculate_middlepoint()
1483 vector fo = '0 0 0';
1486 f = ctf_worldflaglist;
1491 f = f.ctf_worldflagnext;
1495 havocbot_ctf_middlepoint = s * (1.0 / n);
1496 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1500 entity havocbot_ctf_find_flag(entity bot)
1503 f = ctf_worldflaglist;
1506 if (CTF_SAMETEAM(bot, f))
1508 f = f.ctf_worldflagnext;
1513 entity havocbot_ctf_find_enemy_flag(entity bot)
1516 f = ctf_worldflaglist;
1521 if(CTF_DIFFTEAM(bot, f))
1528 else if(!bot.flagcarried)
1532 else if (CTF_DIFFTEAM(bot, f))
1534 f = f.ctf_worldflagnext;
1539 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1547 FOR_EACH_PLAYER(head)
1549 if(DIFF_TEAM(head, bot) || head.deadflag != DEAD_NO || head == bot)
1552 if(vlen(head.origin - org) < tc_radius)
1559 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1562 head = ctf_worldflaglist;
1565 if (CTF_SAMETEAM(self, head))
1567 head = head.ctf_worldflagnext;
1570 navigation_routerating(head, ratingscale, 10000);
1573 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1576 head = ctf_worldflaglist;
1579 if (CTF_SAMETEAM(self, head))
1581 head = head.ctf_worldflagnext;
1586 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1589 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1592 head = ctf_worldflaglist;
1597 if(CTF_DIFFTEAM(self, head))
1601 if(self.flagcarried)
1604 else if(!self.flagcarried)
1608 else if(CTF_DIFFTEAM(self, head))
1610 head = head.ctf_worldflagnext;
1613 navigation_routerating(head, ratingscale, 10000);
1616 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1618 if (!bot_waypoints_for_items)
1620 havocbot_goalrating_ctf_enemyflag(ratingscale);
1626 head = havocbot_ctf_find_enemy_flag(self);
1631 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1634 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1638 mf = havocbot_ctf_find_flag(self);
1640 if(mf.ctf_status == FLAG_BASE)
1644 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1647 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1650 head = ctf_worldflaglist;
1653 // flag is out in the field
1654 if(head.ctf_status != FLAG_BASE)
1655 if(head.tag_entity==world) // dropped
1659 if(vlen(org-head.origin)<df_radius)
1660 navigation_routerating(head, ratingscale, 10000);
1663 navigation_routerating(head, ratingscale, 10000);
1666 head = head.ctf_worldflagnext;
1670 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1674 head = findchainfloat(bot_pickup, true);
1677 // gather health and armor only
1679 if (head.health || head.armorvalue)
1680 if (vlen(head.origin - org) < sradius)
1682 // get the value of the item
1683 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1685 navigation_routerating(head, t * ratingscale, 500);
1691 void havocbot_ctf_reset_role(entity bot)
1693 float cdefense, cmiddle, coffense;
1694 entity mf, ef, head;
1697 if(bot.deadflag != DEAD_NO)
1700 if(vlen(havocbot_ctf_middlepoint)==0)
1701 havocbot_calculate_middlepoint();
1704 if (bot.flagcarried)
1706 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1710 mf = havocbot_ctf_find_flag(bot);
1711 ef = havocbot_ctf_find_enemy_flag(bot);
1713 // Retrieve stolen flag
1714 if(mf.ctf_status!=FLAG_BASE)
1716 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1720 // If enemy flag is taken go to the middle to intercept pursuers
1721 if(ef.ctf_status!=FLAG_BASE)
1723 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1727 // if there is only me on the team switch to offense
1729 FOR_EACH_PLAYER(head)
1730 if(SAME_TEAM(head, bot))
1735 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1739 // Evaluate best position to take
1740 // Count mates on middle position
1741 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1743 // Count mates on defense position
1744 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1746 // Count mates on offense position
1747 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1749 if(cdefense<=coffense)
1750 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1751 else if(coffense<=cmiddle)
1752 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1754 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1757 void havocbot_role_ctf_carrier()
1759 if(self.deadflag != DEAD_NO)
1761 havocbot_ctf_reset_role(self);
1765 if (self.flagcarried == world)
1767 havocbot_ctf_reset_role(self);
1771 if (self.bot_strategytime < time)
1773 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1775 navigation_goalrating_start();
1777 havocbot_goalrating_ctf_enemybase(50000);
1779 havocbot_goalrating_ctf_ourbase(50000);
1782 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1784 navigation_goalrating_end();
1786 if (self.navigation_hasgoals)
1787 self.havocbot_cantfindflag = time + 10;
1788 else if (time > self.havocbot_cantfindflag)
1790 // Can't navigate to my own base, suicide!
1791 // TODO: drop it and wander around
1792 Damage(self, self, self, 100000, DEATH_KILL.m_id, self.origin, '0 0 0');
1798 void havocbot_role_ctf_escort()
1802 if(self.deadflag != DEAD_NO)
1804 havocbot_ctf_reset_role(self);
1808 if (self.flagcarried)
1810 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1814 // If enemy flag is back on the base switch to previous role
1815 ef = havocbot_ctf_find_enemy_flag(self);
1816 if(ef.ctf_status==FLAG_BASE)
1818 self.havocbot_role = self.havocbot_previous_role;
1819 self.havocbot_role_timeout = 0;
1823 // If the flag carrier reached the base switch to defense
1824 mf = havocbot_ctf_find_flag(self);
1825 if(mf.ctf_status!=FLAG_BASE)
1826 if(vlen(ef.origin - mf.dropped_origin) < 300)
1828 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1832 // Set the role timeout if necessary
1833 if (!self.havocbot_role_timeout)
1835 self.havocbot_role_timeout = time + random() * 30 + 60;
1838 // If nothing happened just switch to previous role
1839 if (time > self.havocbot_role_timeout)
1841 self.havocbot_role = self.havocbot_previous_role;
1842 self.havocbot_role_timeout = 0;
1846 // Chase the flag carrier
1847 if (self.bot_strategytime < time)
1849 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1850 navigation_goalrating_start();
1851 havocbot_goalrating_ctf_enemyflag(30000);
1852 havocbot_goalrating_ctf_ourstolenflag(40000);
1853 havocbot_goalrating_items(10000, self.origin, 10000);
1854 navigation_goalrating_end();
1858 void havocbot_role_ctf_offense()
1863 if(self.deadflag != DEAD_NO)
1865 havocbot_ctf_reset_role(self);
1869 if (self.flagcarried)
1871 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1876 mf = havocbot_ctf_find_flag(self);
1877 ef = havocbot_ctf_find_enemy_flag(self);
1880 if(mf.ctf_status!=FLAG_BASE)
1883 pos = mf.tag_entity.origin;
1887 // Try to get it if closer than the enemy base
1888 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1890 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1895 // Escort flag carrier
1896 if(ef.ctf_status!=FLAG_BASE)
1899 pos = ef.tag_entity.origin;
1903 if(vlen(pos-mf.dropped_origin)>700)
1905 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1910 // About to fail, switch to middlefield
1913 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1917 // Set the role timeout if necessary
1918 if (!self.havocbot_role_timeout)
1919 self.havocbot_role_timeout = time + 120;
1921 if (time > self.havocbot_role_timeout)
1923 havocbot_ctf_reset_role(self);
1927 if (self.bot_strategytime < time)
1929 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1930 navigation_goalrating_start();
1931 havocbot_goalrating_ctf_ourstolenflag(50000);
1932 havocbot_goalrating_ctf_enemybase(20000);
1933 havocbot_goalrating_items(5000, self.origin, 1000);
1934 havocbot_goalrating_items(1000, self.origin, 10000);
1935 navigation_goalrating_end();
1939 // Retriever (temporary role):
1940 void havocbot_role_ctf_retriever()
1944 if(self.deadflag != DEAD_NO)
1946 havocbot_ctf_reset_role(self);
1950 if (self.flagcarried)
1952 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1956 // If flag is back on the base switch to previous role
1957 mf = havocbot_ctf_find_flag(self);
1958 if(mf.ctf_status==FLAG_BASE)
1960 havocbot_ctf_reset_role(self);
1964 if (!self.havocbot_role_timeout)
1965 self.havocbot_role_timeout = time + 20;
1967 if (time > self.havocbot_role_timeout)
1969 havocbot_ctf_reset_role(self);
1973 if (self.bot_strategytime < time)
1978 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1979 navigation_goalrating_start();
1980 havocbot_goalrating_ctf_ourstolenflag(50000);
1981 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1982 havocbot_goalrating_ctf_enemybase(30000);
1983 havocbot_goalrating_items(500, self.origin, rt_radius);
1984 navigation_goalrating_end();
1988 void havocbot_role_ctf_middle()
1992 if(self.deadflag != DEAD_NO)
1994 havocbot_ctf_reset_role(self);
1998 if (self.flagcarried)
2000 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
2004 mf = havocbot_ctf_find_flag(self);
2005 if(mf.ctf_status!=FLAG_BASE)
2007 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
2011 if (!self.havocbot_role_timeout)
2012 self.havocbot_role_timeout = time + 10;
2014 if (time > self.havocbot_role_timeout)
2016 havocbot_ctf_reset_role(self);
2020 if (self.bot_strategytime < time)
2024 org = havocbot_ctf_middlepoint;
2025 org.z = self.origin.z;
2027 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
2028 navigation_goalrating_start();
2029 havocbot_goalrating_ctf_ourstolenflag(50000);
2030 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
2031 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
2032 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
2033 havocbot_goalrating_items(2500, self.origin, 10000);
2034 havocbot_goalrating_ctf_enemybase(2500);
2035 navigation_goalrating_end();
2039 void havocbot_role_ctf_defense()
2043 if(self.deadflag != DEAD_NO)
2045 havocbot_ctf_reset_role(self);
2049 if (self.flagcarried)
2051 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
2055 // If own flag was captured
2056 mf = havocbot_ctf_find_flag(self);
2057 if(mf.ctf_status!=FLAG_BASE)
2059 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
2063 if (!self.havocbot_role_timeout)
2064 self.havocbot_role_timeout = time + 30;
2066 if (time > self.havocbot_role_timeout)
2068 havocbot_ctf_reset_role(self);
2071 if (self.bot_strategytime < time)
2076 org = mf.dropped_origin;
2077 mp_radius = havocbot_ctf_middlepoint_radius;
2079 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
2080 navigation_goalrating_start();
2082 // if enemies are closer to our base, go there
2083 entity head, closestplayer = world;
2084 float distance, bestdistance = 10000;
2085 FOR_EACH_PLAYER(head)
2087 if(head.deadflag!=DEAD_NO)
2090 distance = vlen(org - head.origin);
2091 if(distance<bestdistance)
2093 closestplayer = head;
2094 bestdistance = distance;
2099 if(DIFF_TEAM(closestplayer, self))
2100 if(vlen(org - self.origin)>1000)
2101 if(checkpvs(self.origin,closestplayer)||random()<0.5)
2102 havocbot_goalrating_ctf_ourbase(30000);
2104 havocbot_goalrating_ctf_ourstolenflag(20000);
2105 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
2106 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
2107 havocbot_goalrating_items(10000, org, mp_radius);
2108 havocbot_goalrating_items(5000, self.origin, 10000);
2109 navigation_goalrating_end();
2113 void havocbot_role_ctf_setrole(entity bot, int role)
2115 LOG_TRACE(strcat(bot.netname," switched to "));
2118 case HAVOCBOT_CTF_ROLE_CARRIER:
2119 LOG_TRACE("carrier");
2120 bot.havocbot_role = havocbot_role_ctf_carrier;
2121 bot.havocbot_role_timeout = 0;
2122 bot.havocbot_cantfindflag = time + 10;
2123 bot.bot_strategytime = 0;
2125 case HAVOCBOT_CTF_ROLE_DEFENSE:
2126 LOG_TRACE("defense");
2127 bot.havocbot_role = havocbot_role_ctf_defense;
2128 bot.havocbot_role_timeout = 0;
2130 case HAVOCBOT_CTF_ROLE_MIDDLE:
2131 LOG_TRACE("middle");
2132 bot.havocbot_role = havocbot_role_ctf_middle;
2133 bot.havocbot_role_timeout = 0;
2135 case HAVOCBOT_CTF_ROLE_OFFENSE:
2136 LOG_TRACE("offense");
2137 bot.havocbot_role = havocbot_role_ctf_offense;
2138 bot.havocbot_role_timeout = 0;
2140 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2141 LOG_TRACE("retriever");
2142 bot.havocbot_previous_role = bot.havocbot_role;
2143 bot.havocbot_role = havocbot_role_ctf_retriever;
2144 bot.havocbot_role_timeout = time + 10;
2145 bot.bot_strategytime = 0;
2147 case HAVOCBOT_CTF_ROLE_ESCORT:
2148 LOG_TRACE("escort");
2149 bot.havocbot_previous_role = bot.havocbot_role;
2150 bot.havocbot_role = havocbot_role_ctf_escort;
2151 bot.havocbot_role_timeout = time + 30;
2152 bot.bot_strategytime = 0;
2163 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2166 int t = 0, t2 = 0, t3 = 0;
2168 // initially clear items so they can be set as necessary later.
2169 self.ctf_flagstatus &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2170 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2171 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2172 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2173 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2174 | CTF_FLAG_NEUTRAL | CTF_SHIELDED);
2176 // scan through all the flags and notify the client about them
2177 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2179 if(flag.team == NUM_TEAM_1) { t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2180 if(flag.team == NUM_TEAM_2) { t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2181 if(flag.team == NUM_TEAM_3) { t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2182 if(flag.team == NUM_TEAM_4) { t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2183 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; }
2185 switch(flag.ctf_status)
2190 if((flag.owner == self) || (flag.pass_sender == self))
2191 self.ctf_flagstatus |= t; // carrying: self is currently carrying the flag
2193 self.ctf_flagstatus |= t2; // taken: someone else is carrying the flag
2198 self.ctf_flagstatus |= t3; // lost: the flag is dropped somewhere on the map
2204 // item for stopping players from capturing the flag too often
2205 if(self.ctf_captureshielded)
2206 self.ctf_flagstatus |= CTF_SHIELDED;
2208 // update the health of the flag carrier waypointsprite
2209 if(self.wps_flagcarrier)
2210 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
2215 MUTATOR_HOOKFUNCTION(ctf, PlayerDamage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2217 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2219 if(frag_target == frag_attacker) // damage done to yourself
2221 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2222 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2224 else // damage done to everyone else
2226 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2227 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2230 else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2232 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)))
2233 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2235 frag_target.wps_helpme_time = time;
2236 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2238 // todo: add notification for when flag carrier needs help?
2243 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2245 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2247 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
2248 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
2251 if(frag_target.flagcarried)
2253 entity tmp_entity = frag_target.flagcarried;
2254 ctf_Handle_Throw(frag_target, world, DROP_NORMAL);
2255 tmp_entity.ctf_dropper = world;
2261 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2264 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2267 void ctf_RemovePlayer(entity player)
2269 if(player.flagcarried)
2270 { ctf_Handle_Throw(player, world, DROP_NORMAL); }
2272 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2274 if(flag.pass_sender == player) { flag.pass_sender = world; }
2275 if(flag.pass_target == player) { flag.pass_target = world; }
2276 if(flag.ctf_dropper == player) { flag.ctf_dropper = world; }
2280 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2282 ctf_RemovePlayer(self);
2286 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2288 ctf_RemovePlayer(self);
2292 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2294 if(self.flagcarried)
2295 if(!autocvar_g_ctf_portalteleport)
2296 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2301 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2303 if(MUTATOR_RETURNVALUE || gameover) { return false; }
2305 entity player = self;
2307 if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2309 // pass the flag to a team mate
2310 if(autocvar_g_ctf_pass)
2312 entity head, closest_target = world;
2313 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2315 while(head) // find the closest acceptable target to pass to
2317 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
2318 if(head != player && SAME_TEAM(head, player))
2319 if(!head.speedrunning && !head.vehicle)
2321 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2322 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2323 vector passer_center = CENTER_OR_VIEWOFS(player);
2325 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2327 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2329 if(IS_BOT_CLIENT(head))
2331 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2332 ctf_Handle_Throw(head, player, DROP_PASS);
2336 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2337 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2339 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2342 else if(player.flagcarried)
2346 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2347 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
2348 { closest_target = head; }
2350 else { closest_target = head; }
2357 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2360 // throw the flag in front of you
2361 if(autocvar_g_ctf_throw && player.flagcarried)
2363 if(player.throw_count == -1)
2365 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2367 player.throw_prevtime = time;
2368 player.throw_count = 1;
2369 ctf_Handle_Throw(player, world, DROP_THROW);
2374 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2380 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2381 else { player.throw_count += 1; }
2382 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2384 player.throw_prevtime = time;
2385 ctf_Handle_Throw(player, world, DROP_THROW);
2394 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2396 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2398 self.wps_helpme_time = time;
2399 WaypointSprite_HelpMePing(self.wps_flagcarrier);
2401 else // create a normal help me waypointsprite
2403 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, self, FLAG_WAYPOINT_OFFSET, world, self.team, self, wps_helpme, false, RADARICON_HELPME);
2404 WaypointSprite_Ping(self.wps_helpme);
2410 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2412 if(vh_player.flagcarried)
2414 vh_player.flagcarried.nodrawtoclient = vh_player; // hide the flag from the driver
2416 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2418 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
2422 setattachment(vh_player.flagcarried, vh_vehicle, "");
2423 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
2424 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2425 //vh_player.flagcarried.angles = '0 0 0';
2433 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2435 if(vh_player.flagcarried)
2437 setattachment(vh_player.flagcarried, vh_player, "");
2438 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2439 vh_player.flagcarried.scale = FLAG_SCALE;
2440 vh_player.flagcarried.angles = '0 0 0';
2441 vh_player.flagcarried.nodrawtoclient = world;
2448 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2450 if(self.flagcarried)
2452 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((self.flagcarried.team) ? APP_TEAM_ENT_4(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN_) : INFO_CTF_FLAGRETURN_ABORTRUN_NEUTRAL));
2453 ctf_RespawnFlag(self.flagcarried);
2460 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2462 entity flag; // temporary entity for the search method
2464 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2466 switch(flag.ctf_status)
2471 // lock the flag, game is over
2472 flag.movetype = MOVETYPE_NONE;
2473 flag.takedamage = DAMAGE_NO;
2474 flag.solid = SOLID_NOT;
2475 flag.nextthink = false; // stop thinking
2477 //dprint("stopping the ", flag.netname, " from moving.\n");
2485 // do nothing for these flags
2494 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2496 havocbot_ctf_reset_role(self);
2500 MUTATOR_HOOKFUNCTION(ctf, GetTeamCount)
2502 //ret_float = ctf_teams;
2503 ret_string = "ctf_team";
2507 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2509 self.ctf_flagstatus = other.ctf_flagstatus;
2513 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2515 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2517 if (MapInfo_Get_ByID(i))
2519 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2525 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2526 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2533 bool superspec_Spectate(entity _player); // TODO
2534 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2535 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2537 if(IS_PLAYER(self) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2539 if(cmd_name == "followfc")
2552 case "red": _team = NUM_TEAM_1; break;
2553 case "blue": _team = NUM_TEAM_2; break;
2554 case "yellow": if(ctf_teams >= 3) _team = NUM_TEAM_3; break;
2555 case "pink": if(ctf_teams >= 4) _team = NUM_TEAM_4; break;
2559 FOR_EACH_PLAYER(_player)
2561 if(_player.flagcarried && (_player.team == _team || _team == 0))
2564 if(_team == 0 && IS_SPEC(self) && self.enemy == _player)
2565 continue; // already spectating a fc, try to find the other fc
2566 return superspec_Spectate(_player);
2571 superspec_msg("", "", self, "No active flag carrier\n", 1);
2578 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2580 if(frag_target.flagcarried)
2581 ctf_Handle_Throw(frag_target, world, DROP_THROW);
2591 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2592 CTF flag for team one (Red).
2594 "angle" Angle the flag will point (minus 90 degrees)...
2595 "model" model to use, note this needs red and blue as skins 0 and 1...
2596 "noise" sound played when flag is picked up...
2597 "noise1" sound played when flag is returned by a teammate...
2598 "noise2" sound played when flag is captured...
2599 "noise3" sound played when flag is lost in the field and respawns itself...
2600 "noise4" sound played when flag is dropped by a player...
2601 "noise5" sound played when flag touches the ground... */
2602 spawnfunc(item_flag_team1)
2604 if(!g_ctf) { remove(self); return; }
2606 ctf_FlagSetup(NUM_TEAM_1, self);
2609 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2610 CTF flag for team two (Blue).
2612 "angle" Angle the flag will point (minus 90 degrees)...
2613 "model" model to use, note this needs red and blue as skins 0 and 1...
2614 "noise" sound played when flag is picked up...
2615 "noise1" sound played when flag is returned by a teammate...
2616 "noise2" sound played when flag is captured...
2617 "noise3" sound played when flag is lost in the field and respawns itself...
2618 "noise4" sound played when flag is dropped by a player...
2619 "noise5" sound played when flag touches the ground... */
2620 spawnfunc(item_flag_team2)
2622 if(!g_ctf) { remove(self); return; }
2624 ctf_FlagSetup(NUM_TEAM_2, self);
2627 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2628 CTF flag for team three (Yellow).
2630 "angle" Angle the flag will point (minus 90 degrees)...
2631 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2632 "noise" sound played when flag is picked up...
2633 "noise1" sound played when flag is returned by a teammate...
2634 "noise2" sound played when flag is captured...
2635 "noise3" sound played when flag is lost in the field and respawns itself...
2636 "noise4" sound played when flag is dropped by a player...
2637 "noise5" sound played when flag touches the ground... */
2638 spawnfunc(item_flag_team3)
2640 if(!g_ctf) { remove(self); return; }
2642 ctf_FlagSetup(NUM_TEAM_3, self);
2645 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2646 CTF flag for team four (Pink).
2648 "angle" Angle the flag will point (minus 90 degrees)...
2649 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2650 "noise" sound played when flag is picked up...
2651 "noise1" sound played when flag is returned by a teammate...
2652 "noise2" sound played when flag is captured...
2653 "noise3" sound played when flag is lost in the field and respawns itself...
2654 "noise4" sound played when flag is dropped by a player...
2655 "noise5" sound played when flag touches the ground... */
2656 spawnfunc(item_flag_team4)
2658 if(!g_ctf) { remove(self); return; }
2660 ctf_FlagSetup(NUM_TEAM_4, self);
2663 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2666 "angle" Angle the flag will point (minus 90 degrees)...
2667 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2668 "noise" sound played when flag is picked up...
2669 "noise1" sound played when flag is returned by a teammate...
2670 "noise2" sound played when flag is captured...
2671 "noise3" sound played when flag is lost in the field and respawns itself...
2672 "noise4" sound played when flag is dropped by a player...
2673 "noise5" sound played when flag touches the ground... */
2674 spawnfunc(item_flag_neutral)
2676 if(!g_ctf) { remove(self); return; }
2677 if(!cvar("g_ctf_oneflag")) { remove(self); return; }
2679 ctf_FlagSetup(0, self);
2682 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2683 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2684 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.
2686 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2687 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2690 if(!g_ctf) { remove(self); return; }
2692 self.classname = "ctf_team";
2693 self.team = self.cnt + 1;
2696 // compatibility for quake maps
2697 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2698 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2699 spawnfunc(info_player_team1);
2700 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2701 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2702 spawnfunc(info_player_team2);
2703 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2704 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2706 void team_CTF_neutralflag() { SELFPARAM(); spawnfunc_item_flag_neutral(self); }
2707 void team_neutralobelisk() { SELFPARAM(); spawnfunc_item_flag_neutral(self); }
2715 void ctf_ScoreRules(int teams)
2717 CheckAllowedTeams(world);
2718 ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
2719 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2720 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2721 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2722 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2723 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2724 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2725 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2726 ScoreRules_basics_end();
2729 // code from here on is just to support maps that don't have flag and team entities
2730 void ctf_SpawnTeam (string teamname, int teamcolor)
2732 entity this = new(ctf_team);
2733 this.netname = teamname;
2734 this.cnt = teamcolor;
2735 this.spawnfunc_checked = true;
2736 WITH(entity, self, this, spawnfunc_ctf_team(this));
2739 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2744 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2746 if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2747 if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2748 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2751 ctf_teams = bound(2, ctf_teams, 4);
2753 // if no teams are found, spawn defaults
2754 if(find(world, classname, "ctf_team") == world)
2756 LOG_INFO("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2757 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2758 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2760 ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2762 ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2765 ctf_ScoreRules(ctf_teams);
2768 void ctf_Initialize()
2770 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2772 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2773 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2774 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2776 addstat(STAT_CTF_FLAGSTATUS, AS_INT, ctf_flagstatus);
2778 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);