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
186 .int ctf_flagstatus = _STAT(CTF_FLAGSTATUS);
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 <server/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 bool autocvar_g_ctf_flag_return_carrying;
253 float autocvar_g_ctf_flag_return_carried_radius;
254 float autocvar_g_ctf_flag_return_time;
255 bool autocvar_g_ctf_flag_return_when_unreachable;
256 float autocvar_g_ctf_flag_return_damage;
257 float autocvar_g_ctf_flag_return_damage_delay;
258 float autocvar_g_ctf_flag_return_dropped;
259 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
260 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
261 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
262 float autocvar_g_ctf_flagcarrier_selfforcefactor;
263 float autocvar_g_ctf_flagcarrier_damagefactor;
264 float autocvar_g_ctf_flagcarrier_forcefactor;
265 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
266 bool autocvar_g_ctf_fullbrightflags;
267 bool autocvar_g_ctf_ignore_frags;
268 int autocvar_g_ctf_score_capture;
269 int autocvar_g_ctf_score_capture_assist;
270 int autocvar_g_ctf_score_kill;
271 int autocvar_g_ctf_score_penalty_drop;
272 int autocvar_g_ctf_score_penalty_returned;
273 int autocvar_g_ctf_score_pickup_base;
274 int autocvar_g_ctf_score_pickup_dropped_early;
275 int autocvar_g_ctf_score_pickup_dropped_late;
276 int autocvar_g_ctf_score_return;
277 float autocvar_g_ctf_shield_force;
278 float autocvar_g_ctf_shield_max_ratio;
279 int autocvar_g_ctf_shield_min_negscore;
280 bool autocvar_g_ctf_stalemate;
281 int autocvar_g_ctf_stalemate_endcondition;
282 float autocvar_g_ctf_stalemate_time;
283 bool autocvar_g_ctf_reverse;
284 float autocvar_g_ctf_dropped_capture_delay;
285 float autocvar_g_ctf_dropped_capture_radius;
287 void ctf_FakeTimeLimit(entity e, float t)
290 WriteByte(MSG_ONE, 3); // svc_updatestat
291 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
293 WriteCoord(MSG_ONE, autocvar_timelimit);
295 WriteCoord(MSG_ONE, (t + 1) / 60);
298 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
300 if(autocvar_sv_eventlog)
301 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != world) ? ftos(actor.playerid) : "")));
302 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
305 void ctf_CaptureRecord(entity flag, entity player)
307 float cap_record = ctf_captimerecord;
308 float cap_time = (time - flag.ctf_pickuptime);
309 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
312 if(ctf_oneflag) { Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname); }
313 else if(!ctf_captimerecord) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT(flag, CHOICE_CTF_CAPTURE_TIME), player.netname, (cap_time * 100)); }
314 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)); }
315 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)); }
317 // write that shit in the database
318 if(!ctf_oneflag) // but not in 1-flag mode
319 if((!ctf_captimerecord) || (cap_time < cap_record))
321 ctf_captimerecord = cap_time;
322 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
323 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
324 write_recordmarker(player, (time - cap_time), cap_time);
328 void ctf_FlagcarrierWaypoints(entity player)
330 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
331 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
332 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
333 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
336 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
338 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
339 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
340 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
341 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
344 if(current_height) // make sure we can actually do this arcing path
346 targpos = (to + ('0 0 1' * current_height));
347 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
348 if(trace_fraction < 1)
350 //print("normal arc line failed, trying to find new pos...");
351 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
352 targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
353 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
354 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
355 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
358 else { targpos = to; }
360 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
362 vector desired_direction = normalize(targpos - from);
363 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
364 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
367 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
369 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
371 // directional tracing only
373 makevectors(passer_angle);
375 // find the closest point on the enemy to the center of the attack
376 float h; // hypotenuse, which is the distance between attacker to head
377 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
379 h = vlen(head_center - passer_center);
380 a = h * (normalize(head_center - passer_center) * v_forward);
382 vector nearest_on_line = (passer_center + a * v_forward);
383 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
385 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
386 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
388 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
393 else { return true; }
397 // =======================
398 // CaptureShield Functions
399 // =======================
401 bool ctf_CaptureShield_CheckStatus(entity p)
403 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;
420 FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
423 se = PlayerScore_Add(it, SP_CTF_CAPS, 0);
424 se2 = PlayerScore_Add(it, SP_CTF_PICKUPS, 0);
425 se3 = PlayerScore_Add(it, SP_CTF_RETURNS, 0);
426 se4 = PlayerScore_Add(it, SP_CTF_FCKILLS, 0);
428 ser = ((se - se2) + (se3 + se4));
435 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
436 // use this rule here
438 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
444 void ctf_CaptureShield_Update(entity player, bool wanted_status)
446 bool updated_status = ctf_CaptureShield_CheckStatus(player);
447 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
449 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
450 player.ctf_captureshielded = updated_status;
454 bool ctf_CaptureShield_Customize()
456 if(!other.ctf_captureshielded) { return false; }
457 if(CTF_SAMETEAM(self, other)) { return false; }
462 void ctf_CaptureShield_Touch()
464 if(!other.ctf_captureshielded) { return; }
465 if(CTF_SAMETEAM(self, other)) { return; }
467 vector mymid = (self.absmin + self.absmax) * 0.5;
468 vector othermid = (other.absmin + other.absmax) * 0.5;
470 Damage(other, self, self, 0, DEATH_HURTTRIGGER.m_id, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
471 if(IS_REAL_CLIENT(other)) { Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
474 void ctf_CaptureShield_Spawn(entity flag)
476 entity shield = new(ctf_captureshield);
479 shield.team = self.team;
480 shield.touch = ctf_CaptureShield_Touch;
481 shield.customizeentityforclient = ctf_CaptureShield_Customize;
482 shield.effects = EF_ADDITIVE;
483 shield.movetype = MOVETYPE_NOCLIP;
484 shield.solid = SOLID_TRIGGER;
485 shield.avelocity = '7 0 11';
488 setorigin(shield, self.origin);
489 setmodel(shield, MDL_CTF_SHIELD);
490 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
494 // ====================
495 // Drop/Pass/Throw Code
496 // ====================
498 void ctf_Handle_Drop(entity flag, entity player, int droptype)
501 player = (player ? player : flag.pass_sender);
504 flag.movetype = MOVETYPE_TOSS;
505 flag.takedamage = DAMAGE_YES;
506 flag.angles = '0 0 0';
507 flag.health = flag.max_flag_health;
508 flag.ctf_droptime = time;
509 flag.ctf_dropper = player;
510 flag.ctf_status = FLAG_DROPPED;
512 // messages and sounds
513 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT(flag, INFO_CTF_LOST) : INFO_CTF_LOST_NEUTRAL), player.netname);
514 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
515 ctf_EventLog("dropped", player.team, player);
518 PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);
519 PlayerScore_Add(player, SP_CTF_DROPS, 1);
522 if(autocvar_g_ctf_flag_dropped_waypoint) {
523 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);
524 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
527 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
529 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
530 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
533 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
535 if(droptype == DROP_PASS)
537 flag.pass_distance = 0;
538 flag.pass_sender = world;
539 flag.pass_target = world;
543 void ctf_Handle_Retrieve(entity flag, entity player)
545 entity sender = flag.pass_sender;
547 // transfer flag to player
549 flag.owner.flagcarried = flag;
554 setattachment(flag, player.vehicle, "");
555 setorigin(flag, VEHICLE_FLAG_OFFSET);
556 flag.scale = VEHICLE_FLAG_SCALE;
560 setattachment(flag, player, "");
561 setorigin(flag, FLAG_CARRY_OFFSET);
563 flag.movetype = MOVETYPE_NONE;
564 flag.takedamage = DAMAGE_NO;
565 flag.solid = SOLID_NOT;
566 flag.angles = '0 0 0';
567 flag.ctf_status = FLAG_CARRY;
569 // messages and sounds
570 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
571 ctf_EventLog("receive", flag.team, player);
573 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), LAMBDA(
575 Send_Notification(NOTIF_ONE, it, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT(flag, CENTER_CTF_PASS_SENT) : CENTER_CTF_PASS_SENT_NEUTRAL), player.netname);
576 else if(it == player)
577 Send_Notification(NOTIF_ONE, it, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT(flag, CENTER_CTF_PASS_RECEIVED) : CENTER_CTF_PASS_RECEIVED_NEUTRAL), sender.netname);
578 else if(SAME_TEAM(it, sender))
579 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);
582 // create new waypoint
583 ctf_FlagcarrierWaypoints(player);
585 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
586 player.throw_antispam = sender.throw_antispam;
588 flag.pass_distance = 0;
589 flag.pass_sender = world;
590 flag.pass_target = world;
593 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
595 entity flag = player.flagcarried;
596 vector targ_origin, flag_velocity;
598 if(!flag) { return; }
599 if((droptype == DROP_PASS) && !receiver) { return; }
601 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
604 setattachment(flag, world, "");
605 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
606 flag.owner.flagcarried = world;
608 flag.solid = SOLID_TRIGGER;
609 flag.ctf_dropper = player;
610 flag.ctf_droptime = time;
612 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
619 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
620 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
621 WarpZone_RefSys_Copy(flag, receiver);
622 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
623 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
625 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
626 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
629 flag.movetype = MOVETYPE_FLY;
630 flag.takedamage = DAMAGE_NO;
631 flag.pass_sender = player;
632 flag.pass_target = receiver;
633 flag.ctf_status = FLAG_PASSING;
636 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
637 WarpZone_TrailParticles(world, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
638 ctf_EventLog("pass", flag.team, player);
644 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'));
646 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)));
647 flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, false);
648 ctf_Handle_Drop(flag, player, droptype);
654 flag.velocity = '0 0 0'; // do nothing
661 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);
662 ctf_Handle_Drop(flag, player, droptype);
667 // kill old waypointsprite
668 WaypointSprite_Ping(player.wps_flagcarrier);
669 WaypointSprite_Kill(player.wps_flagcarrier);
671 if(player.wps_enemyflagcarrier)
672 WaypointSprite_Kill(player.wps_enemyflagcarrier);
675 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
678 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
680 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
687 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
689 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
690 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
691 entity player_team_flag = world, tmp_entity;
692 float old_time, new_time;
694 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
695 if(CTF_DIFFTEAM(player, flag)) { return; }
698 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
699 if(SAME_TEAM(tmp_entity, player))
701 player_team_flag = tmp_entity;
705 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
707 player.throw_prevtime = time;
708 player.throw_count = 0;
710 // messages and sounds
711 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((enemy_flag.team) ? APP_TEAM_ENT(enemy_flag, CENTER_CTF_CAPTURE) : CENTER_CTF_CAPTURE_NEUTRAL));
712 ctf_CaptureRecord(enemy_flag, player);
713 _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);
717 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
718 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
723 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
724 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
726 old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
727 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
728 if(!old_time || new_time < old_time)
729 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
732 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
733 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
736 if(capturetype == CAPTURE_NORMAL)
738 WaypointSprite_Kill(player.wps_flagcarrier);
739 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
741 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
742 { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
746 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
747 ctf_RespawnFlag(enemy_flag);
750 void ctf_Handle_Return(entity flag, entity player)
752 // messages and sounds
753 if(IS_MONSTER(player))
755 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT(flag, INFO_CTF_RETURN_MONSTER), player.monster_name);
759 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT(flag, CENTER_CTF_RETURN));
760 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT(flag, INFO_CTF_RETURN), player.netname);
762 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
763 ctf_EventLog("return", flag.team, player);
766 if(IS_PLAYER(player))
768 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
769 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
771 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
774 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
778 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
779 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
780 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
784 if(player.flagcarried == flag)
785 WaypointSprite_Kill(player.wps_flagcarrier);
788 ctf_RespawnFlag(flag);
791 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
794 float pickup_dropped_score; // used to calculate dropped pickup score
796 // attach the flag to the player
798 player.flagcarried = flag;
801 setattachment(flag, player.vehicle, "");
802 setorigin(flag, VEHICLE_FLAG_OFFSET);
803 flag.scale = VEHICLE_FLAG_SCALE;
807 setattachment(flag, player, "");
808 setorigin(flag, FLAG_CARRY_OFFSET);
812 flag.movetype = MOVETYPE_NONE;
813 flag.takedamage = DAMAGE_NO;
814 flag.solid = SOLID_NOT;
815 flag.angles = '0 0 0';
816 flag.ctf_status = FLAG_CARRY;
820 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
821 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
825 // messages and sounds
826 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT(flag, INFO_CTF_PICKUP) : INFO_CTF_PICKUP_NEUTRAL), player.netname);
827 if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
828 if(!flag.team) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL); }
829 else if(CTF_DIFFTEAM(player, flag)) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT(flag, CENTER_CTF_PICKUP)); }
830 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)); }
832 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);
835 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)));
838 FOREACH_CLIENT(IS_PLAYER(it) && it != player, LAMBDA(
839 if(CTF_SAMETEAM(flag, it))
840 if(SAME_TEAM(player, it))
841 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_ENT(flag, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
843 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);
846 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
849 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
850 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
855 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
856 ctf_EventLog("steal", flag.team, player);
862 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);
863 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);
864 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
865 PlayerTeamScore_AddScore(player, pickup_dropped_score);
866 ctf_EventLog("pickup", flag.team, player);
874 if(pickuptype == PICKUP_BASE)
876 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
877 if((player.speedrunning) && (ctf_captimerecord))
878 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
882 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
885 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
886 ctf_FlagcarrierWaypoints(player);
887 WaypointSprite_Ping(player.wps_flagcarrier);
891 // ===================
892 // Main Flag Functions
893 // ===================
895 void ctf_CheckFlagReturn(entity flag, int returntype)
897 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
899 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
901 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
905 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;
906 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;
907 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;
908 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;
912 { Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT(flag, INFO_CTF_FLAGRETURN_TIMEOUT) : INFO_CTF_FLAGRETURN_TIMEOUT_NEUTRAL)); break; }
914 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
915 ctf_EventLog("returned", flag.team, world);
916 ctf_RespawnFlag(flag);
921 bool ctf_Stalemate_Customize()
923 // make spectators see what the player would see
925 e = WaypointSprite_getviewentity(other);
926 wp_owner = self.owner;
929 if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
930 if(SAME_TEAM(wp_owner, e)) { return false; }
931 if(!IS_PLAYER(e)) { return false; }
936 void ctf_CheckStalemate()
939 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
942 entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
944 // build list of stale flags
945 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
947 if(autocvar_g_ctf_stalemate)
948 if(tmp_entity.ctf_status != FLAG_BASE)
949 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
951 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
952 ctf_staleflaglist = tmp_entity;
954 switch(tmp_entity.team)
956 case NUM_TEAM_1: ++stale_red_flags; break;
957 case NUM_TEAM_2: ++stale_blue_flags; break;
958 case NUM_TEAM_3: ++stale_yellow_flags; break;
959 case NUM_TEAM_4: ++stale_pink_flags; break;
960 default: ++stale_neutral_flags; break;
966 stale_flags = (stale_neutral_flags >= 1);
968 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
970 if(ctf_oneflag && stale_flags == 1)
971 ctf_stalemate = true;
972 else if(stale_flags >= 2)
973 ctf_stalemate = true;
974 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
975 { ctf_stalemate = false; wpforenemy_announced = false; }
976 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
977 { ctf_stalemate = false; wpforenemy_announced = false; }
979 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
982 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
984 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
986 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);
987 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
988 tmp_entity.owner.wps_enemyflagcarrier.customizeentityforclient = ctf_Stalemate_Customize;
992 if (!wpforenemy_announced)
994 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))));
996 wpforenemy_announced = true;
1001 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
1003 if(ITEM_DAMAGE_NEEDKILL(deathtype))
1005 if(autocvar_g_ctf_flag_return_damage_delay)
1007 this.ctf_flagdamaged = true;
1012 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
1016 if(autocvar_g_ctf_flag_return_damage)
1018 // reduce health and check if it should be returned
1019 this.health = this.health - damage;
1020 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
1025 void ctf_FlagThink()
1030 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
1033 if(self == ctf_worldflaglist) // only for the first flag
1034 FOREACH_CLIENT(true, LAMBDA(ctf_CaptureShield_Update(it, 1))); // release shield only
1037 if(self.mins != CTF_FLAG.m_mins || self.maxs != CTF_FLAG.m_maxs) { // reset the flag boundaries in case it got squished
1038 LOG_TRACE("wtf the flag got squashed?\n");
1039 tracebox(self.origin, CTF_FLAG.m_mins, CTF_FLAG.m_maxs, self.origin, MOVE_NOMONSTERS, self);
1040 if(!trace_startsolid || self.noalign) // can we resize it without getting stuck?
1041 setsize(self, CTF_FLAG.m_mins, CTF_FLAG.m_maxs); }
1043 switch(self.ctf_status) // reset flag angles in case warpzones adjust it
1047 self.angles = '0 0 0';
1054 // main think method
1055 switch(self.ctf_status)
1059 if(autocvar_g_ctf_dropped_capture_radius)
1061 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
1062 if(tmp_entity.ctf_status == FLAG_DROPPED)
1063 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
1064 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
1065 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
1072 if(autocvar_g_ctf_flag_dropped_floatinwater)
1074 vector midpoint = ((self.absmin + self.absmax) * 0.5);
1075 if(pointcontents(midpoint) == CONTENT_WATER)
1077 self.velocity = self.velocity * 0.5;
1079 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
1080 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
1082 { self.movetype = MOVETYPE_FLY; }
1084 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
1086 if(autocvar_g_ctf_flag_return_dropped)
1088 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
1091 ctf_CheckFlagReturn(self, RETURN_DROPPED);
1095 if(self.ctf_flagdamaged)
1097 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
1098 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
1101 else if(autocvar_g_ctf_flag_return_time)
1103 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
1104 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
1112 if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
1115 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
1117 setself(self.owner);
1118 self.impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
1119 ImpulseCommands(self);
1122 if(autocvar_g_ctf_stalemate)
1124 if(time >= wpforenemy_nextthink)
1126 ctf_CheckStalemate();
1127 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1130 if(CTF_SAMETEAM(self, self.owner) && self.team)
1132 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1133 ctf_Handle_Throw(self.owner, world, DROP_THROW);
1134 else if(vlen(self.owner.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_carried_radius)
1135 ctf_Handle_Return(self, self.owner);
1142 vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
1143 targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
1144 WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
1146 if((self.pass_target == world)
1147 || (IS_DEAD(self.pass_target))
1148 || (self.pass_target.flagcarried)
1149 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
1150 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
1151 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1153 // give up, pass failed
1154 ctf_Handle_Drop(self, world, DROP_PASS);
1158 // still a viable target, go for it
1159 ctf_CalculatePassVelocity(self, targ_origin, self.origin, true);
1164 default: // this should never happen
1166 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?\n");
1172 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1175 if(gameover) { return; }
1176 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1178 bool is_not_monster = (!IS_MONSTER(toucher));
1180 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1181 if(ITEM_TOUCH_NEEDKILL())
1183 if(!autocvar_g_ctf_flag_return_damage_delay)
1186 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1188 if(!flag.ctf_flagdamaged) { return; }
1191 int num_perteam = 0;
1192 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), LAMBDA(++num_perteam));
1194 // special touch behaviors
1195 if(STAT(FROZEN, toucher)) { return; }
1196 else if(IS_VEHICLE(toucher))
1198 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1199 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1201 return; // do nothing
1203 else if(IS_MONSTER(toucher))
1205 if(!autocvar_g_ctf_allow_monster_touch)
1206 return; // do nothing
1208 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1210 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1212 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1213 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1214 flag.wait = time + FLAG_TOUCHRATE;
1218 else if(IS_DEAD(toucher)) { return; }
1220 switch(flag.ctf_status)
1226 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1227 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1228 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1229 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1231 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1232 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1233 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)
1235 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1236 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1238 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1239 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1245 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
1246 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1247 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1248 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1254 LOG_TRACE("Someone touched a flag even though it was being carried?\n");
1260 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1262 if(DIFF_TEAM(toucher, flag.pass_sender))
1263 ctf_Handle_Return(flag, toucher);
1265 ctf_Handle_Retrieve(flag, toucher);
1272 .float last_respawn;
1273 void ctf_RespawnFlag(entity flag)
1275 // check for flag respawn being called twice in a row
1276 if(flag.last_respawn > time - 0.5)
1277 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1279 flag.last_respawn = time;
1281 // reset the player (if there is one)
1282 if((flag.owner) && (flag.owner.flagcarried == flag))
1284 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1285 WaypointSprite_Kill(flag.wps_flagcarrier);
1287 flag.owner.flagcarried = world;
1289 if(flag.speedrunning)
1290 ctf_FakeTimeLimit(flag.owner, -1);
1293 if((flag.owner) && (flag.owner.vehicle))
1294 flag.scale = FLAG_SCALE;
1296 if(flag.ctf_status == FLAG_DROPPED)
1297 { WaypointSprite_Kill(flag.wps_flagdropped); }
1300 setattachment(flag, world, "");
1301 setorigin(flag, flag.ctf_spawnorigin);
1303 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
1304 flag.takedamage = DAMAGE_NO;
1305 flag.health = flag.max_flag_health;
1306 flag.solid = SOLID_TRIGGER;
1307 flag.velocity = '0 0 0';
1308 flag.angles = flag.mangle;
1309 flag.flags = FL_ITEM | FL_NOTARGET;
1311 flag.ctf_status = FLAG_BASE;
1313 flag.pass_distance = 0;
1314 flag.pass_sender = world;
1315 flag.pass_target = world;
1316 flag.ctf_dropper = world;
1317 flag.ctf_pickuptime = 0;
1318 flag.ctf_droptime = 0;
1319 flag.ctf_flagdamaged = 0;
1321 ctf_CheckStalemate();
1324 void ctf_Reset(entity this)
1326 if(this.owner && IS_PLAYER(this.owner))
1327 ctf_Handle_Throw(this.owner, world, DROP_RESET);
1329 ctf_RespawnFlag(this);
1332 void ctf_DelayedFlagSetup() // called after a flag is placed on a map by ctf_FlagSetup()
1335 waypoint_spawnforitem_force(self, self.origin);
1336 self.nearestwaypointtimeout = 0; // activate waypointing again
1337 self.bot_basewaypoint = self.nearestwaypoint;
1343 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1344 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1345 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1346 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1347 default: basename = WP_FlagBaseNeutral; break;
1350 entity wp = WaypointSprite_SpawnFixed(basename, self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG);
1351 wp.colormod = ((self.team) ? Team_ColorRGB(self.team) : '1 1 1');
1352 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, ((self.team) ? colormapPaletteColor(self.team - 1, false) : '1 1 1'));
1354 // captureshield setup
1355 ctf_CaptureShield_Spawn(self);
1358 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1361 setself(flag); // for later usage with droptofloor()
1364 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1365 ctf_worldflaglist = flag;
1367 setattachment(flag, world, "");
1369 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1370 flag.team = teamnumber;
1371 flag.classname = "item_flag_team";
1372 flag.target = "###item###"; // wut?
1373 flag.flags = FL_ITEM | FL_NOTARGET;
1374 flag.solid = SOLID_TRIGGER;
1375 flag.takedamage = DAMAGE_NO;
1376 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1377 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1378 flag.health = flag.max_flag_health;
1379 flag.event_damage = ctf_FlagDamage;
1380 flag.pushable = true;
1381 flag.teleportable = TELEPORT_NORMAL;
1382 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1383 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1384 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1385 flag.velocity = '0 0 0';
1386 flag.mangle = flag.angles;
1387 flag.reset = ctf_Reset;
1388 flag.touch = ctf_FlagTouch;
1389 flag.think = ctf_FlagThink;
1390 flag.nextthink = time + FLAG_THINKRATE;
1391 flag.ctf_status = FLAG_BASE;
1393 string teamname = Static_Team_ColorName_Lower(teamnumber);
1395 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1396 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1397 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1398 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnumber).eent_eff_name; }
1399 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnumber).eent_eff_name; }
1400 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnumber).eent_eff_name; }
1403 flag.snd_flag_taken = strzone(SND(CTF_TAKEN(teamnumber)));
1404 flag.snd_flag_returned = strzone(SND(CTF_RETURNED(teamnumber)));
1405 flag.snd_flag_capture = strzone(SND(CTF_CAPTURE(teamnumber)));
1406 flag.snd_flag_dropped = strzone(SND(CTF_DROPPED(teamnumber)));
1407 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.
1408 precache_sound(flag.snd_flag_respawn);
1409 if (flag.snd_flag_touch == "") flag.snd_flag_touch = strzone(SND(CTF_TOUCH)); // again has no team-based sound
1410 precache_sound(flag.snd_flag_touch);
1411 if (flag.snd_flag_pass == "") flag.snd_flag_pass = strzone(SND(CTF_PASS)); // same story here
1412 precache_sound(flag.snd_flag_pass);
1415 precache_model(flag.model);
1418 _setmodel(flag, flag.model); // precision set below
1419 setsize(flag, CTF_FLAG.m_mins, CTF_FLAG.m_maxs);
1420 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1422 if(autocvar_g_ctf_flag_glowtrails)
1426 case NUM_TEAM_1: flag.glow_color = 251; break;
1427 case NUM_TEAM_2: flag.glow_color = 210; break;
1428 case NUM_TEAM_3: flag.glow_color = 110; break;
1429 case NUM_TEAM_4: flag.glow_color = 145; break;
1430 default: flag.glow_color = 254; break;
1432 flag.glow_size = 25;
1433 flag.glow_trail = 1;
1436 flag.effects |= EF_LOWPRECISION;
1437 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1438 if(autocvar_g_ctf_dynamiclights)
1442 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1443 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1444 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1445 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1446 default: flag.effects |= EF_DIMLIGHT; break;
1451 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1453 flag.dropped_origin = flag.origin;
1454 flag.noalign = true;
1455 flag.movetype = MOVETYPE_NONE;
1457 else // drop to floor, automatically find a platform and set that as spawn origin
1459 flag.noalign = false;
1462 flag.movetype = MOVETYPE_TOSS;
1465 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1473 // NOTE: LEGACY CODE, needs to be re-written!
1475 void havocbot_calculate_middlepoint()
1479 vector fo = '0 0 0';
1482 f = ctf_worldflaglist;
1487 f = f.ctf_worldflagnext;
1491 havocbot_ctf_middlepoint = s * (1.0 / n);
1492 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1496 entity havocbot_ctf_find_flag(entity bot)
1499 f = ctf_worldflaglist;
1502 if (CTF_SAMETEAM(bot, f))
1504 f = f.ctf_worldflagnext;
1509 entity havocbot_ctf_find_enemy_flag(entity bot)
1512 f = ctf_worldflaglist;
1517 if(CTF_DIFFTEAM(bot, f))
1524 else if(!bot.flagcarried)
1528 else if (CTF_DIFFTEAM(bot, f))
1530 f = f.ctf_worldflagnext;
1535 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1542 FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
1543 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1546 if(vlen(it.origin - org) < tc_radius)
1553 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1556 head = ctf_worldflaglist;
1559 if (CTF_SAMETEAM(self, head))
1561 head = head.ctf_worldflagnext;
1564 navigation_routerating(head, ratingscale, 10000);
1567 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1570 head = ctf_worldflaglist;
1573 if (CTF_SAMETEAM(self, head))
1575 head = head.ctf_worldflagnext;
1580 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1583 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1586 head = ctf_worldflaglist;
1591 if(CTF_DIFFTEAM(self, head))
1595 if(self.flagcarried)
1598 else if(!self.flagcarried)
1602 else if(CTF_DIFFTEAM(self, head))
1604 head = head.ctf_worldflagnext;
1607 navigation_routerating(head, ratingscale, 10000);
1610 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1612 if (!bot_waypoints_for_items)
1614 havocbot_goalrating_ctf_enemyflag(ratingscale);
1620 head = havocbot_ctf_find_enemy_flag(self);
1625 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1628 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1632 mf = havocbot_ctf_find_flag(self);
1634 if(mf.ctf_status == FLAG_BASE)
1638 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1641 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1644 head = ctf_worldflaglist;
1647 // flag is out in the field
1648 if(head.ctf_status != FLAG_BASE)
1649 if(head.tag_entity==world) // dropped
1653 if(vlen(org-head.origin)<df_radius)
1654 navigation_routerating(head, ratingscale, 10000);
1657 navigation_routerating(head, ratingscale, 10000);
1660 head = head.ctf_worldflagnext;
1664 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1668 head = findchainfloat(bot_pickup, true);
1671 // gather health and armor only
1673 if (head.health || head.armorvalue)
1674 if (vlen(head.origin - org) < sradius)
1676 // get the value of the item
1677 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1679 navigation_routerating(head, t * ratingscale, 500);
1685 void havocbot_ctf_reset_role(entity bot)
1687 float cdefense, cmiddle, coffense;
1694 if(vlen(havocbot_ctf_middlepoint)==0)
1695 havocbot_calculate_middlepoint();
1698 if (bot.flagcarried)
1700 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1704 mf = havocbot_ctf_find_flag(bot);
1705 ef = havocbot_ctf_find_enemy_flag(bot);
1707 // Retrieve stolen flag
1708 if(mf.ctf_status!=FLAG_BASE)
1710 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1714 // If enemy flag is taken go to the middle to intercept pursuers
1715 if(ef.ctf_status!=FLAG_BASE)
1717 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1721 // if there is only me on the team switch to offense
1723 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(it, bot), LAMBDA(++c));
1727 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1731 // Evaluate best position to take
1732 // Count mates on middle position
1733 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1735 // Count mates on defense position
1736 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1738 // Count mates on offense position
1739 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1741 if(cdefense<=coffense)
1742 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1743 else if(coffense<=cmiddle)
1744 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1746 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1749 void havocbot_role_ctf_carrier()
1753 havocbot_ctf_reset_role(self);
1757 if (self.flagcarried == world)
1759 havocbot_ctf_reset_role(self);
1763 if (self.bot_strategytime < time)
1765 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1767 navigation_goalrating_start();
1769 havocbot_goalrating_ctf_enemybase(50000);
1771 havocbot_goalrating_ctf_ourbase(50000);
1774 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1776 navigation_goalrating_end();
1778 if (self.navigation_hasgoals)
1779 self.havocbot_cantfindflag = time + 10;
1780 else if (time > self.havocbot_cantfindflag)
1782 // Can't navigate to my own base, suicide!
1783 // TODO: drop it and wander around
1784 Damage(self, self, self, 100000, DEATH_KILL.m_id, self.origin, '0 0 0');
1790 void havocbot_role_ctf_escort()
1796 havocbot_ctf_reset_role(self);
1800 if (self.flagcarried)
1802 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1806 // If enemy flag is back on the base switch to previous role
1807 ef = havocbot_ctf_find_enemy_flag(self);
1808 if(ef.ctf_status==FLAG_BASE)
1810 self.havocbot_role = self.havocbot_previous_role;
1811 self.havocbot_role_timeout = 0;
1815 // If the flag carrier reached the base switch to defense
1816 mf = havocbot_ctf_find_flag(self);
1817 if(mf.ctf_status!=FLAG_BASE)
1818 if(vlen(ef.origin - mf.dropped_origin) < 300)
1820 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1824 // Set the role timeout if necessary
1825 if (!self.havocbot_role_timeout)
1827 self.havocbot_role_timeout = time + random() * 30 + 60;
1830 // If nothing happened just switch to previous role
1831 if (time > self.havocbot_role_timeout)
1833 self.havocbot_role = self.havocbot_previous_role;
1834 self.havocbot_role_timeout = 0;
1838 // Chase the flag carrier
1839 if (self.bot_strategytime < time)
1841 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1842 navigation_goalrating_start();
1843 havocbot_goalrating_ctf_enemyflag(30000);
1844 havocbot_goalrating_ctf_ourstolenflag(40000);
1845 havocbot_goalrating_items(10000, self.origin, 10000);
1846 navigation_goalrating_end();
1850 void havocbot_role_ctf_offense()
1857 havocbot_ctf_reset_role(self);
1861 if (self.flagcarried)
1863 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1868 mf = havocbot_ctf_find_flag(self);
1869 ef = havocbot_ctf_find_enemy_flag(self);
1872 if(mf.ctf_status!=FLAG_BASE)
1875 pos = mf.tag_entity.origin;
1879 // Try to get it if closer than the enemy base
1880 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1882 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1887 // Escort flag carrier
1888 if(ef.ctf_status!=FLAG_BASE)
1891 pos = ef.tag_entity.origin;
1895 if(vlen(pos-mf.dropped_origin)>700)
1897 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1902 // About to fail, switch to middlefield
1905 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1909 // Set the role timeout if necessary
1910 if (!self.havocbot_role_timeout)
1911 self.havocbot_role_timeout = time + 120;
1913 if (time > self.havocbot_role_timeout)
1915 havocbot_ctf_reset_role(self);
1919 if (self.bot_strategytime < time)
1921 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1922 navigation_goalrating_start();
1923 havocbot_goalrating_ctf_ourstolenflag(50000);
1924 havocbot_goalrating_ctf_enemybase(20000);
1925 havocbot_goalrating_items(5000, self.origin, 1000);
1926 havocbot_goalrating_items(1000, self.origin, 10000);
1927 navigation_goalrating_end();
1931 // Retriever (temporary role):
1932 void havocbot_role_ctf_retriever()
1938 havocbot_ctf_reset_role(self);
1942 if (self.flagcarried)
1944 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1948 // If flag is back on the base switch to previous role
1949 mf = havocbot_ctf_find_flag(self);
1950 if(mf.ctf_status==FLAG_BASE)
1952 havocbot_ctf_reset_role(self);
1956 if (!self.havocbot_role_timeout)
1957 self.havocbot_role_timeout = time + 20;
1959 if (time > self.havocbot_role_timeout)
1961 havocbot_ctf_reset_role(self);
1965 if (self.bot_strategytime < time)
1970 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1971 navigation_goalrating_start();
1972 havocbot_goalrating_ctf_ourstolenflag(50000);
1973 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1974 havocbot_goalrating_ctf_enemybase(30000);
1975 havocbot_goalrating_items(500, self.origin, rt_radius);
1976 navigation_goalrating_end();
1980 void havocbot_role_ctf_middle()
1986 havocbot_ctf_reset_role(self);
1990 if (self.flagcarried)
1992 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1996 mf = havocbot_ctf_find_flag(self);
1997 if(mf.ctf_status!=FLAG_BASE)
1999 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
2003 if (!self.havocbot_role_timeout)
2004 self.havocbot_role_timeout = time + 10;
2006 if (time > self.havocbot_role_timeout)
2008 havocbot_ctf_reset_role(self);
2012 if (self.bot_strategytime < time)
2016 org = havocbot_ctf_middlepoint;
2017 org.z = self.origin.z;
2019 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
2020 navigation_goalrating_start();
2021 havocbot_goalrating_ctf_ourstolenflag(50000);
2022 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
2023 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
2024 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
2025 havocbot_goalrating_items(2500, self.origin, 10000);
2026 havocbot_goalrating_ctf_enemybase(2500);
2027 navigation_goalrating_end();
2031 void havocbot_role_ctf_defense()
2037 havocbot_ctf_reset_role(self);
2041 if (self.flagcarried)
2043 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
2047 // If own flag was captured
2048 mf = havocbot_ctf_find_flag(self);
2049 if(mf.ctf_status!=FLAG_BASE)
2051 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
2055 if (!self.havocbot_role_timeout)
2056 self.havocbot_role_timeout = time + 30;
2058 if (time > self.havocbot_role_timeout)
2060 havocbot_ctf_reset_role(self);
2063 if (self.bot_strategytime < time)
2068 org = mf.dropped_origin;
2069 mp_radius = havocbot_ctf_middlepoint_radius;
2071 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
2072 navigation_goalrating_start();
2074 // if enemies are closer to our base, go there
2075 entity closestplayer = world;
2076 float distance, bestdistance = 10000;
2077 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), LAMBDA(
2078 distance = vlen(org - it.origin);
2079 if(distance<bestdistance)
2082 bestdistance = distance;
2087 if(DIFF_TEAM(closestplayer, self))
2088 if(vlen(org - self.origin)>1000)
2089 if(checkpvs(self.origin,closestplayer)||random()<0.5)
2090 havocbot_goalrating_ctf_ourbase(30000);
2092 havocbot_goalrating_ctf_ourstolenflag(20000);
2093 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
2094 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
2095 havocbot_goalrating_items(10000, org, mp_radius);
2096 havocbot_goalrating_items(5000, self.origin, 10000);
2097 navigation_goalrating_end();
2101 void havocbot_role_ctf_setrole(entity bot, int role)
2103 LOG_TRACE(strcat(bot.netname," switched to "));
2106 case HAVOCBOT_CTF_ROLE_CARRIER:
2107 LOG_TRACE("carrier");
2108 bot.havocbot_role = havocbot_role_ctf_carrier;
2109 bot.havocbot_role_timeout = 0;
2110 bot.havocbot_cantfindflag = time + 10;
2111 bot.bot_strategytime = 0;
2113 case HAVOCBOT_CTF_ROLE_DEFENSE:
2114 LOG_TRACE("defense");
2115 bot.havocbot_role = havocbot_role_ctf_defense;
2116 bot.havocbot_role_timeout = 0;
2118 case HAVOCBOT_CTF_ROLE_MIDDLE:
2119 LOG_TRACE("middle");
2120 bot.havocbot_role = havocbot_role_ctf_middle;
2121 bot.havocbot_role_timeout = 0;
2123 case HAVOCBOT_CTF_ROLE_OFFENSE:
2124 LOG_TRACE("offense");
2125 bot.havocbot_role = havocbot_role_ctf_offense;
2126 bot.havocbot_role_timeout = 0;
2128 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2129 LOG_TRACE("retriever");
2130 bot.havocbot_previous_role = bot.havocbot_role;
2131 bot.havocbot_role = havocbot_role_ctf_retriever;
2132 bot.havocbot_role_timeout = time + 10;
2133 bot.bot_strategytime = 0;
2135 case HAVOCBOT_CTF_ROLE_ESCORT:
2136 LOG_TRACE("escort");
2137 bot.havocbot_previous_role = bot.havocbot_role;
2138 bot.havocbot_role = havocbot_role_ctf_escort;
2139 bot.havocbot_role_timeout = time + 30;
2140 bot.bot_strategytime = 0;
2151 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2154 int t = 0, t2 = 0, t3 = 0;
2156 // initially clear items so they can be set as necessary later.
2157 self.ctf_flagstatus &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2158 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2159 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2160 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2161 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2162 | CTF_FLAG_NEUTRAL | CTF_SHIELDED);
2164 // scan through all the flags and notify the client about them
2165 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2167 if(flag.team == NUM_TEAM_1) { t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2168 if(flag.team == NUM_TEAM_2) { t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2169 if(flag.team == NUM_TEAM_3) { t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2170 if(flag.team == NUM_TEAM_4) { t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2171 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; }
2173 switch(flag.ctf_status)
2178 if((flag.owner == self) || (flag.pass_sender == self))
2179 self.ctf_flagstatus |= t; // carrying: self is currently carrying the flag
2181 self.ctf_flagstatus |= t2; // taken: someone else is carrying the flag
2186 self.ctf_flagstatus |= t3; // lost: the flag is dropped somewhere on the map
2192 // item for stopping players from capturing the flag too often
2193 if(self.ctf_captureshielded)
2194 self.ctf_flagstatus |= CTF_SHIELDED;
2196 // update the health of the flag carrier waypointsprite
2197 if(self.wps_flagcarrier)
2198 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
2203 MUTATOR_HOOKFUNCTION(ctf, PlayerDamage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2205 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2207 if(frag_target == frag_attacker) // damage done to yourself
2209 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2210 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2212 else // damage done to everyone else
2214 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2215 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2218 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2220 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)))
2221 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2223 frag_target.wps_helpme_time = time;
2224 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2226 // todo: add notification for when flag carrier needs help?
2231 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2233 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2235 PlayerTeamScore_AddScore(frag_attacker, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2236 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
2239 if(frag_target.flagcarried)
2241 entity tmp_entity = frag_target.flagcarried;
2242 ctf_Handle_Throw(frag_target, world, DROP_NORMAL);
2243 tmp_entity.ctf_dropper = world;
2249 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2252 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2255 void ctf_RemovePlayer(entity player)
2257 if(player.flagcarried)
2258 { ctf_Handle_Throw(player, world, DROP_NORMAL); }
2260 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2262 if(flag.pass_sender == player) { flag.pass_sender = world; }
2263 if(flag.pass_target == player) { flag.pass_target = world; }
2264 if(flag.ctf_dropper == player) { flag.ctf_dropper = world; }
2268 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2270 ctf_RemovePlayer(self);
2274 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2276 ctf_RemovePlayer(self);
2280 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2282 if(self.flagcarried)
2283 if(!autocvar_g_ctf_portalteleport)
2284 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2289 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2291 if(MUTATOR_RETURNVALUE || gameover) { return false; }
2293 entity player = self;
2295 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2297 // pass the flag to a team mate
2298 if(autocvar_g_ctf_pass)
2300 entity head, closest_target = world;
2301 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2303 while(head) // find the closest acceptable target to pass to
2305 if(IS_PLAYER(head) && !IS_DEAD(head))
2306 if(head != player && SAME_TEAM(head, player))
2307 if(!head.speedrunning && !head.vehicle)
2309 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2310 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2311 vector passer_center = CENTER_OR_VIEWOFS(player);
2313 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2315 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2317 if(IS_BOT_CLIENT(head))
2319 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2320 ctf_Handle_Throw(head, player, DROP_PASS);
2324 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2325 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2327 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2330 else if(player.flagcarried)
2334 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2335 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
2336 { closest_target = head; }
2338 else { closest_target = head; }
2345 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2348 // throw the flag in front of you
2349 if(autocvar_g_ctf_throw && player.flagcarried)
2351 if(player.throw_count == -1)
2353 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2355 player.throw_prevtime = time;
2356 player.throw_count = 1;
2357 ctf_Handle_Throw(player, world, DROP_THROW);
2362 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2368 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2369 else { player.throw_count += 1; }
2370 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2372 player.throw_prevtime = time;
2373 ctf_Handle_Throw(player, world, DROP_THROW);
2382 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2384 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2386 self.wps_helpme_time = time;
2387 WaypointSprite_HelpMePing(self.wps_flagcarrier);
2389 else // create a normal help me waypointsprite
2391 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, self, FLAG_WAYPOINT_OFFSET, world, self.team, self, wps_helpme, false, RADARICON_HELPME);
2392 WaypointSprite_Ping(self.wps_helpme);
2398 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2400 if(vh_player.flagcarried)
2402 vh_player.flagcarried.nodrawtoclient = vh_player; // hide the flag from the driver
2404 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2406 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
2410 setattachment(vh_player.flagcarried, vh_vehicle, "");
2411 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
2412 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2413 //vh_player.flagcarried.angles = '0 0 0';
2421 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2423 if(vh_player.flagcarried)
2425 setattachment(vh_player.flagcarried, vh_player, "");
2426 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2427 vh_player.flagcarried.scale = FLAG_SCALE;
2428 vh_player.flagcarried.angles = '0 0 0';
2429 vh_player.flagcarried.nodrawtoclient = world;
2436 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2438 if(self.flagcarried)
2440 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((self.flagcarried.team) ? APP_TEAM_ENT(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN) : INFO_CTF_FLAGRETURN_ABORTRUN_NEUTRAL));
2441 ctf_RespawnFlag(self.flagcarried);
2448 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2450 entity flag; // temporary entity for the search method
2452 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2454 switch(flag.ctf_status)
2459 // lock the flag, game is over
2460 flag.movetype = MOVETYPE_NONE;
2461 flag.takedamage = DAMAGE_NO;
2462 flag.solid = SOLID_NOT;
2463 flag.nextthink = false; // stop thinking
2465 //dprint("stopping the ", flag.netname, " from moving.\n");
2473 // do nothing for these flags
2482 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2484 havocbot_ctf_reset_role(self);
2488 MUTATOR_HOOKFUNCTION(ctf, GetTeamCount)
2490 //ret_float = ctf_teams;
2491 ret_string = "ctf_team";
2495 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2497 self.ctf_flagstatus = other.ctf_flagstatus;
2501 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2503 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2505 if (MapInfo_Get_ByID(i))
2507 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2513 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2514 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2521 bool superspec_Spectate(entity _player); // TODO
2522 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2523 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2525 if(IS_PLAYER(self) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2527 if(cmd_name == "followfc")
2539 case "red": _team = NUM_TEAM_1; break;
2540 case "blue": _team = NUM_TEAM_2; break;
2541 case "yellow": if(ctf_teams >= 3) _team = NUM_TEAM_3; break;
2542 case "pink": if(ctf_teams >= 4) _team = NUM_TEAM_4; break;
2546 FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
2547 if(it.flagcarried && (it.team == _team || _team == 0))
2550 if(_team == 0 && IS_SPEC(self) && self.enemy == it)
2551 continue; // already spectating this fc, try another
2552 return superspec_Spectate(it);
2557 superspec_msg("", "", self, "No active flag carrier\n", 1);
2564 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2566 if(frag_target.flagcarried)
2567 ctf_Handle_Throw(frag_target, world, DROP_THROW);
2577 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2578 CTF flag for team one (Red).
2580 "angle" Angle the flag will point (minus 90 degrees)...
2581 "model" model to use, note this needs red and blue as skins 0 and 1...
2582 "noise" sound played when flag is picked up...
2583 "noise1" sound played when flag is returned by a teammate...
2584 "noise2" sound played when flag is captured...
2585 "noise3" sound played when flag is lost in the field and respawns itself...
2586 "noise4" sound played when flag is dropped by a player...
2587 "noise5" sound played when flag touches the ground... */
2588 spawnfunc(item_flag_team1)
2590 if(!g_ctf) { remove(self); return; }
2592 ctf_FlagSetup(NUM_TEAM_1, self);
2595 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2596 CTF flag for team two (Blue).
2598 "angle" Angle the flag will point (minus 90 degrees)...
2599 "model" model to use, note this needs red and blue as skins 0 and 1...
2600 "noise" sound played when flag is picked up...
2601 "noise1" sound played when flag is returned by a teammate...
2602 "noise2" sound played when flag is captured...
2603 "noise3" sound played when flag is lost in the field and respawns itself...
2604 "noise4" sound played when flag is dropped by a player...
2605 "noise5" sound played when flag touches the ground... */
2606 spawnfunc(item_flag_team2)
2608 if(!g_ctf) { remove(self); return; }
2610 ctf_FlagSetup(NUM_TEAM_2, self);
2613 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2614 CTF flag for team three (Yellow).
2616 "angle" Angle the flag will point (minus 90 degrees)...
2617 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2618 "noise" sound played when flag is picked up...
2619 "noise1" sound played when flag is returned by a teammate...
2620 "noise2" sound played when flag is captured...
2621 "noise3" sound played when flag is lost in the field and respawns itself...
2622 "noise4" sound played when flag is dropped by a player...
2623 "noise5" sound played when flag touches the ground... */
2624 spawnfunc(item_flag_team3)
2626 if(!g_ctf) { remove(self); return; }
2628 ctf_FlagSetup(NUM_TEAM_3, self);
2631 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2632 CTF flag for team four (Pink).
2634 "angle" Angle the flag will point (minus 90 degrees)...
2635 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2636 "noise" sound played when flag is picked up...
2637 "noise1" sound played when flag is returned by a teammate...
2638 "noise2" sound played when flag is captured...
2639 "noise3" sound played when flag is lost in the field and respawns itself...
2640 "noise4" sound played when flag is dropped by a player...
2641 "noise5" sound played when flag touches the ground... */
2642 spawnfunc(item_flag_team4)
2644 if(!g_ctf) { remove(self); return; }
2646 ctf_FlagSetup(NUM_TEAM_4, self);
2649 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2652 "angle" Angle the flag will point (minus 90 degrees)...
2653 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2654 "noise" sound played when flag is picked up...
2655 "noise1" sound played when flag is returned by a teammate...
2656 "noise2" sound played when flag is captured...
2657 "noise3" sound played when flag is lost in the field and respawns itself...
2658 "noise4" sound played when flag is dropped by a player...
2659 "noise5" sound played when flag touches the ground... */
2660 spawnfunc(item_flag_neutral)
2662 if(!g_ctf) { remove(self); return; }
2663 if(!cvar("g_ctf_oneflag")) { remove(self); return; }
2665 ctf_FlagSetup(0, self);
2668 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2669 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2670 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.
2672 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2673 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2676 if(!g_ctf) { remove(self); return; }
2678 self.classname = "ctf_team";
2679 self.team = self.cnt + 1;
2682 // compatibility for quake maps
2683 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2684 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2685 spawnfunc(info_player_team1);
2686 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2687 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2688 spawnfunc(info_player_team2);
2689 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2690 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2692 void team_CTF_neutralflag() { SELFPARAM(); spawnfunc_item_flag_neutral(self); }
2693 void team_neutralobelisk() { SELFPARAM(); spawnfunc_item_flag_neutral(self); }
2701 void ctf_ScoreRules(int teams)
2703 CheckAllowedTeams(world);
2704 ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
2705 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2706 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2707 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2708 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2709 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2710 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2711 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2712 ScoreRules_basics_end();
2715 // code from here on is just to support maps that don't have flag and team entities
2716 void ctf_SpawnTeam (string teamname, int teamcolor)
2718 entity this = new(ctf_team);
2719 this.netname = teamname;
2720 this.cnt = teamcolor;
2721 this.spawnfunc_checked = true;
2722 WITH(entity, self, this, spawnfunc_ctf_team(this));
2725 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2730 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2732 if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2733 if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2734 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2737 ctf_teams = bound(2, ctf_teams, 4);
2739 // if no teams are found, spawn defaults
2740 if(find(world, classname, "ctf_team") == world)
2742 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.\n");
2743 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2744 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2746 ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2748 ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2751 ctf_ScoreRules(ctf_teams);
2754 void ctf_Initialize()
2756 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2758 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2759 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2760 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2762 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);