1 #include "gamemode_ctf.qh"
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, autocvar_timelimit_override, -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.");
38 #include <common/vehicles/all.qh>
39 #include <server/teamplay.qh>
42 #include <lib/warpzone/common.qh>
44 bool autocvar_g_ctf_allow_vehicle_carry;
45 bool autocvar_g_ctf_allow_vehicle_touch;
46 bool autocvar_g_ctf_allow_monster_touch;
47 bool autocvar_g_ctf_throw;
48 float autocvar_g_ctf_throw_angle_max;
49 float autocvar_g_ctf_throw_angle_min;
50 int autocvar_g_ctf_throw_punish_count;
51 float autocvar_g_ctf_throw_punish_delay;
52 float autocvar_g_ctf_throw_punish_time;
53 float autocvar_g_ctf_throw_strengthmultiplier;
54 float autocvar_g_ctf_throw_velocity_forward;
55 float autocvar_g_ctf_throw_velocity_up;
56 float autocvar_g_ctf_drop_velocity_up;
57 float autocvar_g_ctf_drop_velocity_side;
58 bool autocvar_g_ctf_oneflag_reverse;
59 bool autocvar_g_ctf_portalteleport;
60 bool autocvar_g_ctf_pass;
61 float autocvar_g_ctf_pass_arc;
62 float autocvar_g_ctf_pass_arc_max;
63 float autocvar_g_ctf_pass_directional_max;
64 float autocvar_g_ctf_pass_directional_min;
65 float autocvar_g_ctf_pass_radius;
66 float autocvar_g_ctf_pass_wait;
67 bool autocvar_g_ctf_pass_request;
68 float autocvar_g_ctf_pass_turnrate;
69 float autocvar_g_ctf_pass_timelimit;
70 float autocvar_g_ctf_pass_velocity;
71 bool autocvar_g_ctf_dynamiclights;
72 float autocvar_g_ctf_flag_collect_delay;
73 float autocvar_g_ctf_flag_damageforcescale;
74 bool autocvar_g_ctf_flag_dropped_waypoint;
75 bool autocvar_g_ctf_flag_dropped_floatinwater;
76 bool autocvar_g_ctf_flag_glowtrails;
77 int autocvar_g_ctf_flag_health;
78 bool autocvar_g_ctf_flag_return;
79 bool autocvar_g_ctf_flag_return_carrying;
80 float autocvar_g_ctf_flag_return_carried_radius;
81 float autocvar_g_ctf_flag_return_time;
82 bool autocvar_g_ctf_flag_return_when_unreachable;
83 float autocvar_g_ctf_flag_return_damage;
84 float autocvar_g_ctf_flag_return_damage_delay;
85 float autocvar_g_ctf_flag_return_dropped;
86 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
87 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
88 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
89 float autocvar_g_ctf_flagcarrier_selfforcefactor;
90 float autocvar_g_ctf_flagcarrier_damagefactor;
91 float autocvar_g_ctf_flagcarrier_forcefactor;
92 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
93 bool autocvar_g_ctf_fullbrightflags;
94 bool autocvar_g_ctf_ignore_frags;
95 int autocvar_g_ctf_score_capture;
96 int autocvar_g_ctf_score_capture_assist;
97 int autocvar_g_ctf_score_kill;
98 int autocvar_g_ctf_score_penalty_drop;
99 int autocvar_g_ctf_score_penalty_returned;
100 int autocvar_g_ctf_score_pickup_base;
101 int autocvar_g_ctf_score_pickup_dropped_early;
102 int autocvar_g_ctf_score_pickup_dropped_late;
103 int autocvar_g_ctf_score_return;
104 float autocvar_g_ctf_shield_force;
105 float autocvar_g_ctf_shield_max_ratio;
106 int autocvar_g_ctf_shield_min_negscore;
107 bool autocvar_g_ctf_stalemate;
108 int autocvar_g_ctf_stalemate_endcondition;
109 float autocvar_g_ctf_stalemate_time;
110 bool autocvar_g_ctf_reverse;
111 float autocvar_g_ctf_dropped_capture_delay;
112 float autocvar_g_ctf_dropped_capture_radius;
114 void ctf_FakeTimeLimit(entity e, float t)
117 WriteByte(MSG_ONE, 3); // svc_updatestat
118 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
120 WriteCoord(MSG_ONE, autocvar_timelimit);
122 WriteCoord(MSG_ONE, (t + 1) / 60);
125 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
127 if(autocvar_sv_eventlog)
128 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != world) ? ftos(actor.playerid) : "")));
129 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
132 void ctf_CaptureRecord(entity flag, entity player)
134 float cap_record = ctf_captimerecord;
135 float cap_time = (time - flag.ctf_pickuptime);
136 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
139 if(ctf_oneflag) { Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname); }
140 else if(!ctf_captimerecord) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT(flag, CHOICE_CTF_CAPTURE_TIME), player.netname, (cap_time * 100)); }
141 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)); }
142 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)); }
144 // write that shit in the database
145 if(!ctf_oneflag) // but not in 1-flag mode
146 if((!ctf_captimerecord) || (cap_time < cap_record))
148 ctf_captimerecord = cap_time;
149 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
150 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
151 write_recordmarker(player, (time - cap_time), cap_time);
155 void ctf_FlagcarrierWaypoints(entity player)
157 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
158 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
159 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
160 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
163 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
165 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
166 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
167 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
168 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
171 if(current_height) // make sure we can actually do this arcing path
173 targpos = (to + ('0 0 1' * current_height));
174 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
175 if(trace_fraction < 1)
177 //print("normal arc line failed, trying to find new pos...");
178 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
179 targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
180 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
181 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
182 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
185 else { targpos = to; }
187 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
189 vector desired_direction = normalize(targpos - from);
190 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
191 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
194 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
196 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
198 // directional tracing only
200 makevectors(passer_angle);
202 // find the closest point on the enemy to the center of the attack
203 float h; // hypotenuse, which is the distance between attacker to head
204 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
206 h = vlen(head_center - passer_center);
207 a = h * (normalize(head_center - passer_center) * v_forward);
209 vector nearest_on_line = (passer_center + a * v_forward);
210 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
212 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
213 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
215 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
220 else { return true; }
224 // =======================
225 // CaptureShield Functions
226 // =======================
228 bool ctf_CaptureShield_CheckStatus(entity p)
230 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
231 int players_worseeq, players_total;
233 if(ctf_captureshield_max_ratio <= 0)
236 s = PlayerScore_Add(p, SP_CTF_CAPS, 0);
237 s2 = PlayerScore_Add(p, SP_CTF_PICKUPS, 0);
238 s3 = PlayerScore_Add(p, SP_CTF_RETURNS, 0);
239 s4 = PlayerScore_Add(p, SP_CTF_FCKILLS, 0);
241 sr = ((s - s2) + (s3 + s4));
243 if(sr >= -ctf_captureshield_min_negscore)
246 players_total = players_worseeq = 0;
247 FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
250 se = PlayerScore_Add(it, SP_CTF_CAPS, 0);
251 se2 = PlayerScore_Add(it, SP_CTF_PICKUPS, 0);
252 se3 = PlayerScore_Add(it, SP_CTF_RETURNS, 0);
253 se4 = PlayerScore_Add(it, SP_CTF_FCKILLS, 0);
255 ser = ((se - se2) + (se3 + se4));
262 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
263 // use this rule here
265 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
271 void ctf_CaptureShield_Update(entity player, bool wanted_status)
273 bool updated_status = ctf_CaptureShield_CheckStatus(player);
274 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
276 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
277 player.ctf_captureshielded = updated_status;
281 bool ctf_CaptureShield_Customize()
283 if(!other.ctf_captureshielded) { return false; }
284 if(CTF_SAMETEAM(self, other)) { return false; }
289 void ctf_CaptureShield_Touch()
291 if(!other.ctf_captureshielded) { return; }
292 if(CTF_SAMETEAM(self, other)) { return; }
294 vector mymid = (self.absmin + self.absmax) * 0.5;
295 vector othermid = (other.absmin + other.absmax) * 0.5;
297 Damage(other, self, self, 0, DEATH_HURTTRIGGER.m_id, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
298 if(IS_REAL_CLIENT(other)) { Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
301 void ctf_CaptureShield_Spawn(entity flag)
303 entity shield = new(ctf_captureshield);
306 shield.team = self.team;
307 shield.touch = ctf_CaptureShield_Touch;
308 shield.customizeentityforclient = ctf_CaptureShield_Customize;
309 shield.effects = EF_ADDITIVE;
310 shield.movetype = MOVETYPE_NOCLIP;
311 shield.solid = SOLID_TRIGGER;
312 shield.avelocity = '7 0 11';
315 setorigin(shield, self.origin);
316 setmodel(shield, MDL_CTF_SHIELD);
317 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
321 // ====================
322 // Drop/Pass/Throw Code
323 // ====================
325 void ctf_Handle_Drop(entity flag, entity player, int droptype)
328 player = (player ? player : flag.pass_sender);
331 flag.movetype = MOVETYPE_TOSS;
332 flag.takedamage = DAMAGE_YES;
333 flag.angles = '0 0 0';
334 flag.health = flag.max_flag_health;
335 flag.ctf_droptime = time;
336 flag.ctf_dropper = player;
337 flag.ctf_status = FLAG_DROPPED;
339 // messages and sounds
340 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT(flag, INFO_CTF_LOST) : INFO_CTF_LOST_NEUTRAL), player.netname);
341 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
342 ctf_EventLog("dropped", player.team, player);
345 PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);
346 PlayerScore_Add(player, SP_CTF_DROPS, 1);
349 if(autocvar_g_ctf_flag_dropped_waypoint) {
350 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);
351 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
354 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
356 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
357 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
360 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
362 if(droptype == DROP_PASS)
364 flag.pass_distance = 0;
365 flag.pass_sender = world;
366 flag.pass_target = world;
370 void ctf_Handle_Retrieve(entity flag, entity player)
372 entity sender = flag.pass_sender;
374 // transfer flag to player
376 flag.owner.flagcarried = flag;
381 setattachment(flag, player.vehicle, "");
382 setorigin(flag, VEHICLE_FLAG_OFFSET);
383 flag.scale = VEHICLE_FLAG_SCALE;
387 setattachment(flag, player, "");
388 setorigin(flag, FLAG_CARRY_OFFSET);
390 flag.movetype = MOVETYPE_NONE;
391 flag.takedamage = DAMAGE_NO;
392 flag.solid = SOLID_NOT;
393 flag.angles = '0 0 0';
394 flag.ctf_status = FLAG_CARRY;
396 // messages and sounds
397 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
398 ctf_EventLog("receive", flag.team, player);
400 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), LAMBDA(
402 Send_Notification(NOTIF_ONE, it, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT(flag, CENTER_CTF_PASS_SENT) : CENTER_CTF_PASS_SENT_NEUTRAL), player.netname);
403 else if(it == player)
404 Send_Notification(NOTIF_ONE, it, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT(flag, CENTER_CTF_PASS_RECEIVED) : CENTER_CTF_PASS_RECEIVED_NEUTRAL), sender.netname);
405 else if(SAME_TEAM(it, sender))
406 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);
409 // create new waypoint
410 ctf_FlagcarrierWaypoints(player);
412 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
413 player.throw_antispam = sender.throw_antispam;
415 flag.pass_distance = 0;
416 flag.pass_sender = world;
417 flag.pass_target = world;
420 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
422 entity flag = player.flagcarried;
423 vector targ_origin, flag_velocity;
425 if(!flag) { return; }
426 if((droptype == DROP_PASS) && !receiver) { return; }
428 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
431 setattachment(flag, world, "");
432 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
433 flag.owner.flagcarried = world;
435 flag.solid = SOLID_TRIGGER;
436 flag.ctf_dropper = player;
437 flag.ctf_droptime = time;
439 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
446 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
447 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
448 WarpZone_RefSys_Copy(flag, receiver);
449 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
450 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
452 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
453 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
456 flag.movetype = MOVETYPE_FLY;
457 flag.takedamage = DAMAGE_NO;
458 flag.pass_sender = player;
459 flag.pass_target = receiver;
460 flag.ctf_status = FLAG_PASSING;
463 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
464 WarpZone_TrailParticles(world, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
465 ctf_EventLog("pass", flag.team, player);
471 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'));
473 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)));
474 flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, false);
475 ctf_Handle_Drop(flag, player, droptype);
481 flag.velocity = '0 0 0'; // do nothing
488 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);
489 ctf_Handle_Drop(flag, player, droptype);
494 // kill old waypointsprite
495 WaypointSprite_Ping(player.wps_flagcarrier);
496 WaypointSprite_Kill(player.wps_flagcarrier);
498 if(player.wps_enemyflagcarrier)
499 WaypointSprite_Kill(player.wps_enemyflagcarrier);
502 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
505 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
507 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
514 void nades_GiveBonus(entity player, float score);
516 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
518 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
519 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
520 entity player_team_flag = world, tmp_entity;
521 float old_time, new_time;
523 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
524 if(CTF_DIFFTEAM(player, flag)) { return; }
527 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
528 if(SAME_TEAM(tmp_entity, player))
530 player_team_flag = tmp_entity;
534 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
536 player.throw_prevtime = time;
537 player.throw_count = 0;
539 // messages and sounds
540 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((enemy_flag.team) ? APP_TEAM_ENT(enemy_flag, CENTER_CTF_CAPTURE) : CENTER_CTF_CAPTURE_NEUTRAL));
541 ctf_CaptureRecord(enemy_flag, player);
542 _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);
546 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
547 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
552 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
553 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
555 old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
556 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
557 if(!old_time || new_time < old_time)
558 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
561 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
562 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
565 if(capturetype == CAPTURE_NORMAL)
567 WaypointSprite_Kill(player.wps_flagcarrier);
568 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
570 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
571 { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
575 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
576 ctf_RespawnFlag(enemy_flag);
579 void ctf_Handle_Return(entity flag, entity player)
581 // messages and sounds
582 if(IS_MONSTER(player))
584 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT(flag, INFO_CTF_RETURN_MONSTER), player.monster_name);
588 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT(flag, CENTER_CTF_RETURN));
589 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT(flag, INFO_CTF_RETURN), player.netname);
591 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
592 ctf_EventLog("return", flag.team, player);
595 if(IS_PLAYER(player))
597 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
598 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
600 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
603 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
607 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
608 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
609 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
613 if(player.flagcarried == flag)
614 WaypointSprite_Kill(player.wps_flagcarrier);
617 ctf_RespawnFlag(flag);
620 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
623 float pickup_dropped_score; // used to calculate dropped pickup score
625 // attach the flag to the player
627 player.flagcarried = flag;
630 setattachment(flag, player.vehicle, "");
631 setorigin(flag, VEHICLE_FLAG_OFFSET);
632 flag.scale = VEHICLE_FLAG_SCALE;
636 setattachment(flag, player, "");
637 setorigin(flag, FLAG_CARRY_OFFSET);
641 flag.movetype = MOVETYPE_NONE;
642 flag.takedamage = DAMAGE_NO;
643 flag.solid = SOLID_NOT;
644 flag.angles = '0 0 0';
645 flag.ctf_status = FLAG_CARRY;
649 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
650 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
654 // messages and sounds
655 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT(flag, INFO_CTF_PICKUP) : INFO_CTF_PICKUP_NEUTRAL), player.netname);
656 if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
657 if(!flag.team) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL); }
658 else if(CTF_DIFFTEAM(player, flag)) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT(flag, CENTER_CTF_PICKUP)); }
659 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)); }
661 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);
664 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)));
667 FOREACH_CLIENT(IS_PLAYER(it) && it != player, LAMBDA(
668 if(CTF_SAMETEAM(flag, it))
669 if(SAME_TEAM(player, it))
670 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_ENT(flag, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
672 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);
675 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
678 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
679 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
684 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
685 ctf_EventLog("steal", flag.team, player);
691 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);
692 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);
693 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
694 PlayerTeamScore_AddScore(player, pickup_dropped_score);
695 ctf_EventLog("pickup", flag.team, player);
703 if(pickuptype == PICKUP_BASE)
705 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
706 if((player.speedrunning) && (ctf_captimerecord))
707 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
711 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
714 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
715 ctf_FlagcarrierWaypoints(player);
716 WaypointSprite_Ping(player.wps_flagcarrier);
720 // ===================
721 // Main Flag Functions
722 // ===================
724 void ctf_CheckFlagReturn(entity flag, int returntype)
726 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
728 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
730 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
734 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;
735 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;
736 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;
737 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;
741 { Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT(flag, INFO_CTF_FLAGRETURN_TIMEOUT) : INFO_CTF_FLAGRETURN_TIMEOUT_NEUTRAL)); break; }
743 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
744 ctf_EventLog("returned", flag.team, world);
745 ctf_RespawnFlag(flag);
750 bool ctf_Stalemate_Customize()
752 // make spectators see what the player would see
754 e = WaypointSprite_getviewentity(other);
755 wp_owner = self.owner;
758 if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
759 if(SAME_TEAM(wp_owner, e)) { return false; }
760 if(!IS_PLAYER(e)) { return false; }
765 void ctf_CheckStalemate()
768 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
771 entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
773 // build list of stale flags
774 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
776 if(autocvar_g_ctf_stalemate)
777 if(tmp_entity.ctf_status != FLAG_BASE)
778 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
780 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
781 ctf_staleflaglist = tmp_entity;
783 switch(tmp_entity.team)
785 case NUM_TEAM_1: ++stale_red_flags; break;
786 case NUM_TEAM_2: ++stale_blue_flags; break;
787 case NUM_TEAM_3: ++stale_yellow_flags; break;
788 case NUM_TEAM_4: ++stale_pink_flags; break;
789 default: ++stale_neutral_flags; break;
795 stale_flags = (stale_neutral_flags >= 1);
797 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
799 if(ctf_oneflag && stale_flags == 1)
800 ctf_stalemate = true;
801 else if(stale_flags >= 2)
802 ctf_stalemate = true;
803 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
804 { ctf_stalemate = false; wpforenemy_announced = false; }
805 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
806 { ctf_stalemate = false; wpforenemy_announced = false; }
808 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
811 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
813 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
815 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);
816 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
817 tmp_entity.owner.wps_enemyflagcarrier.customizeentityforclient = ctf_Stalemate_Customize;
821 if (!wpforenemy_announced)
823 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))));
825 wpforenemy_announced = true;
830 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
832 if(ITEM_DAMAGE_NEEDKILL(deathtype))
834 if(autocvar_g_ctf_flag_return_damage_delay)
836 this.ctf_flagdamaged = true;
841 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
845 if(autocvar_g_ctf_flag_return_damage)
847 // reduce health and check if it should be returned
848 this.health = this.health - damage;
849 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
859 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
862 if(self == ctf_worldflaglist) // only for the first flag
863 FOREACH_CLIENT(true, LAMBDA(ctf_CaptureShield_Update(it, 1))); // release shield only
866 if(self.mins != CTF_FLAG.m_mins || self.maxs != CTF_FLAG.m_maxs) { // reset the flag boundaries in case it got squished
867 LOG_TRACE("wtf the flag got squashed?\n");
868 tracebox(self.origin, CTF_FLAG.m_mins, CTF_FLAG.m_maxs, self.origin, MOVE_NOMONSTERS, self);
869 if(!trace_startsolid || self.noalign) // can we resize it without getting stuck?
870 setsize(self, CTF_FLAG.m_mins, CTF_FLAG.m_maxs); }
872 switch(self.ctf_status) // reset flag angles in case warpzones adjust it
876 self.angles = '0 0 0';
884 switch(self.ctf_status)
888 if(autocvar_g_ctf_dropped_capture_radius)
890 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
891 if(tmp_entity.ctf_status == FLAG_DROPPED)
892 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
893 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
894 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
901 if(autocvar_g_ctf_flag_dropped_floatinwater)
903 vector midpoint = ((self.absmin + self.absmax) * 0.5);
904 if(pointcontents(midpoint) == CONTENT_WATER)
906 self.velocity = self.velocity * 0.5;
908 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
909 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
911 { self.movetype = MOVETYPE_FLY; }
913 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
915 if(autocvar_g_ctf_flag_return_dropped)
917 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
920 ctf_CheckFlagReturn(self, RETURN_DROPPED);
924 if(self.ctf_flagdamaged)
926 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
927 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
930 else if(autocvar_g_ctf_flag_return_time)
932 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
933 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
941 if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
944 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
947 self.impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
948 ImpulseCommands(self);
951 if(autocvar_g_ctf_stalemate)
953 if(time >= wpforenemy_nextthink)
955 ctf_CheckStalemate();
956 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
959 if(CTF_SAMETEAM(self, self.owner) && self.team)
961 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
962 ctf_Handle_Throw(self.owner, world, DROP_THROW);
963 else if(vlen(self.owner.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_carried_radius)
964 ctf_Handle_Return(self, self.owner);
971 vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
972 targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
973 WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
975 if((self.pass_target == world)
976 || (IS_DEAD(self.pass_target))
977 || (self.pass_target.flagcarried)
978 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
979 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
980 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
982 // give up, pass failed
983 ctf_Handle_Drop(self, world, DROP_PASS);
987 // still a viable target, go for it
988 ctf_CalculatePassVelocity(self, targ_origin, self.origin, true);
993 default: // this should never happen
995 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?\n");
1001 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1004 if(gameover) { return; }
1005 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1007 bool is_not_monster = (!IS_MONSTER(toucher));
1009 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1010 if(ITEM_TOUCH_NEEDKILL())
1012 if(!autocvar_g_ctf_flag_return_damage_delay)
1015 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1017 if(!flag.ctf_flagdamaged) { return; }
1020 int num_perteam = 0;
1021 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), LAMBDA(++num_perteam));
1023 // special touch behaviors
1024 if(STAT(FROZEN, toucher)) { return; }
1025 else if(IS_VEHICLE(toucher))
1027 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1028 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1030 return; // do nothing
1032 else if(IS_MONSTER(toucher))
1034 if(!autocvar_g_ctf_allow_monster_touch)
1035 return; // do nothing
1037 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1039 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1041 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1042 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1043 flag.wait = time + FLAG_TOUCHRATE;
1047 else if(IS_DEAD(toucher)) { return; }
1049 switch(flag.ctf_status)
1055 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1056 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1057 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1058 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1060 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1061 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1062 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)
1064 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1065 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1067 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1068 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1074 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
1075 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1076 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1077 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1083 LOG_TRACE("Someone touched a flag even though it was being carried?\n");
1089 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1091 if(DIFF_TEAM(toucher, flag.pass_sender))
1092 ctf_Handle_Return(flag, toucher);
1094 ctf_Handle_Retrieve(flag, toucher);
1101 .float last_respawn;
1102 void ctf_RespawnFlag(entity flag)
1104 // check for flag respawn being called twice in a row
1105 if(flag.last_respawn > time - 0.5)
1106 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1108 flag.last_respawn = time;
1110 // reset the player (if there is one)
1111 if((flag.owner) && (flag.owner.flagcarried == flag))
1113 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1114 WaypointSprite_Kill(flag.wps_flagcarrier);
1116 flag.owner.flagcarried = world;
1118 if(flag.speedrunning)
1119 ctf_FakeTimeLimit(flag.owner, -1);
1122 if((flag.owner) && (flag.owner.vehicle))
1123 flag.scale = FLAG_SCALE;
1125 if(flag.ctf_status == FLAG_DROPPED)
1126 { WaypointSprite_Kill(flag.wps_flagdropped); }
1129 setattachment(flag, world, "");
1130 setorigin(flag, flag.ctf_spawnorigin);
1132 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
1133 flag.takedamage = DAMAGE_NO;
1134 flag.health = flag.max_flag_health;
1135 flag.solid = SOLID_TRIGGER;
1136 flag.velocity = '0 0 0';
1137 flag.angles = flag.mangle;
1138 flag.flags = FL_ITEM | FL_NOTARGET;
1140 flag.ctf_status = FLAG_BASE;
1142 flag.pass_distance = 0;
1143 flag.pass_sender = world;
1144 flag.pass_target = world;
1145 flag.ctf_dropper = world;
1146 flag.ctf_pickuptime = 0;
1147 flag.ctf_droptime = 0;
1148 flag.ctf_flagdamaged = 0;
1150 ctf_CheckStalemate();
1153 void ctf_Reset(entity this)
1155 if(this.owner && IS_PLAYER(this.owner))
1156 ctf_Handle_Throw(this.owner, world, DROP_RESET);
1158 ctf_RespawnFlag(this);
1161 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1164 waypoint_spawnforitem_force(self, self.origin);
1165 self.nearestwaypointtimeout = 0; // activate waypointing again
1166 self.bot_basewaypoint = self.nearestwaypoint;
1172 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1173 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1174 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1175 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1176 default: basename = WP_FlagBaseNeutral; break;
1179 entity wp = WaypointSprite_SpawnFixed(basename, self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG);
1180 wp.colormod = ((self.team) ? Team_ColorRGB(self.team) : '1 1 1');
1181 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, ((self.team) ? colormapPaletteColor(self.team - 1, false) : '1 1 1'));
1183 // captureshield setup
1184 ctf_CaptureShield_Spawn(self);
1189 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1192 setself(flag); // for later usage with droptofloor()
1195 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1196 ctf_worldflaglist = flag;
1198 setattachment(flag, world, "");
1200 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1201 flag.team = teamnumber;
1202 flag.classname = "item_flag_team";
1203 flag.target = "###item###"; // wut?
1204 flag.flags = FL_ITEM | FL_NOTARGET;
1205 flag.solid = SOLID_TRIGGER;
1206 flag.takedamage = DAMAGE_NO;
1207 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1208 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1209 flag.health = flag.max_flag_health;
1210 flag.event_damage = ctf_FlagDamage;
1211 flag.pushable = true;
1212 flag.teleportable = TELEPORT_NORMAL;
1213 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1214 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1215 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1216 flag.velocity = '0 0 0';
1217 flag.mangle = flag.angles;
1218 flag.reset = ctf_Reset;
1219 flag.touch = ctf_FlagTouch;
1220 flag.think = ctf_FlagThink;
1221 flag.nextthink = time + FLAG_THINKRATE;
1222 flag.ctf_status = FLAG_BASE;
1224 string teamname = Static_Team_ColorName_Lower(teamnumber);
1226 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1227 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1228 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1229 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnumber).eent_eff_name; }
1230 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnumber).eent_eff_name; }
1231 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnumber).eent_eff_name; }
1235 if(flag.s == "") flag.s = b; \
1236 precache_sound(flag.s);
1238 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnumber))))
1239 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnumber))))
1240 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnumber))))
1241 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnumber))))
1242 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1243 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1244 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1248 precache_model(flag.model);
1251 _setmodel(flag, flag.model); // precision set below
1252 setsize(flag, CTF_FLAG.m_mins, CTF_FLAG.m_maxs);
1253 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1255 if(autocvar_g_ctf_flag_glowtrails)
1259 case NUM_TEAM_1: flag.glow_color = 251; break;
1260 case NUM_TEAM_2: flag.glow_color = 210; break;
1261 case NUM_TEAM_3: flag.glow_color = 110; break;
1262 case NUM_TEAM_4: flag.glow_color = 145; break;
1263 default: flag.glow_color = 254; break;
1265 flag.glow_size = 25;
1266 flag.glow_trail = 1;
1269 flag.effects |= EF_LOWPRECISION;
1270 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1271 if(autocvar_g_ctf_dynamiclights)
1275 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1276 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1277 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1278 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1279 default: flag.effects |= EF_DIMLIGHT; break;
1284 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1286 flag.dropped_origin = flag.origin;
1287 flag.noalign = true;
1288 flag.movetype = MOVETYPE_NONE;
1290 else // drop to floor, automatically find a platform and set that as spawn origin
1292 flag.noalign = false;
1295 flag.movetype = MOVETYPE_TOSS;
1298 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1306 // NOTE: LEGACY CODE, needs to be re-written!
1308 void havocbot_calculate_middlepoint()
1312 vector fo = '0 0 0';
1315 f = ctf_worldflaglist;
1320 f = f.ctf_worldflagnext;
1324 havocbot_ctf_middlepoint = s * (1.0 / n);
1325 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1329 entity havocbot_ctf_find_flag(entity bot)
1332 f = ctf_worldflaglist;
1335 if (CTF_SAMETEAM(bot, f))
1337 f = f.ctf_worldflagnext;
1342 entity havocbot_ctf_find_enemy_flag(entity bot)
1345 f = ctf_worldflaglist;
1350 if(CTF_DIFFTEAM(bot, f))
1357 else if(!bot.flagcarried)
1361 else if (CTF_DIFFTEAM(bot, f))
1363 f = f.ctf_worldflagnext;
1368 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1375 FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
1376 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1379 if(vlen(it.origin - org) < tc_radius)
1386 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1389 head = ctf_worldflaglist;
1392 if (CTF_SAMETEAM(this, head))
1394 head = head.ctf_worldflagnext;
1397 navigation_routerating(this, head, ratingscale, 10000);
1400 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1403 head = ctf_worldflaglist;
1406 if (CTF_SAMETEAM(this, head))
1408 head = head.ctf_worldflagnext;
1413 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1416 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1419 head = ctf_worldflaglist;
1424 if(CTF_DIFFTEAM(this, head))
1428 if(this.flagcarried)
1431 else if(!this.flagcarried)
1435 else if(CTF_DIFFTEAM(this, head))
1437 head = head.ctf_worldflagnext;
1440 navigation_routerating(this, head, ratingscale, 10000);
1443 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1445 if (!bot_waypoints_for_items)
1447 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1453 head = havocbot_ctf_find_enemy_flag(this);
1458 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1461 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1465 mf = havocbot_ctf_find_flag(this);
1467 if(mf.ctf_status == FLAG_BASE)
1471 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1474 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1477 head = ctf_worldflaglist;
1480 // flag is out in the field
1481 if(head.ctf_status != FLAG_BASE)
1482 if(head.tag_entity==world) // dropped
1486 if(vlen(org-head.origin)<df_radius)
1487 navigation_routerating(self, head, ratingscale, 10000);
1490 navigation_routerating(self, head, ratingscale, 10000);
1493 head = head.ctf_worldflagnext;
1497 void havocbot_goalrating_ctf_carrieritems(entity this, float ratingscale, vector org, float sradius)
1501 head = findchainfloat(bot_pickup, true);
1504 // gather health and armor only
1506 if (head.health || head.armorvalue)
1507 if (vlen(head.origin - org) < sradius)
1509 // get the value of the item
1510 t = head.bot_pickupevalfunc(this, head) * 0.0001;
1512 navigation_routerating(this, head, t * ratingscale, 500);
1518 void havocbot_ctf_reset_role(entity this)
1520 float cdefense, cmiddle, coffense;
1527 if(havocbot_ctf_middlepoint == '0 0 0')
1528 havocbot_calculate_middlepoint();
1531 if (this.flagcarried)
1533 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1537 mf = havocbot_ctf_find_flag(this);
1538 ef = havocbot_ctf_find_enemy_flag(this);
1540 // Retrieve stolen flag
1541 if(mf.ctf_status!=FLAG_BASE)
1543 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1547 // If enemy flag is taken go to the middle to intercept pursuers
1548 if(ef.ctf_status!=FLAG_BASE)
1550 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1554 // if there is only me on the team switch to offense
1556 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(it, this), LAMBDA(++c));
1560 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1564 // Evaluate best position to take
1565 // Count mates on middle position
1566 cmiddle = havocbot_ctf_teamcount(this, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1568 // Count mates on defense position
1569 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1571 // Count mates on offense position
1572 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1574 if(cdefense<=coffense)
1575 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1576 else if(coffense<=cmiddle)
1577 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1579 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1582 void havocbot_role_ctf_carrier(entity this)
1586 havocbot_ctf_reset_role(this);
1590 if (this.flagcarried == world)
1592 havocbot_ctf_reset_role(this);
1596 if (this.bot_strategytime < time)
1598 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1600 navigation_goalrating_start(this);
1602 havocbot_goalrating_ctf_enemybase(this, 50000);
1604 havocbot_goalrating_ctf_ourbase(this, 50000);
1607 havocbot_goalrating_ctf_carrieritems(this, 1000, this.origin, 1000);
1609 navigation_goalrating_end(this);
1611 if (this.navigation_hasgoals)
1612 this.havocbot_cantfindflag = time + 10;
1613 else if (time > this.havocbot_cantfindflag)
1615 // Can't navigate to my own base, suicide!
1616 // TODO: drop it and wander around
1617 Damage(this, this, this, 100000, DEATH_KILL.m_id, this.origin, '0 0 0');
1623 void havocbot_role_ctf_escort(entity this)
1629 havocbot_ctf_reset_role(this);
1633 if (this.flagcarried)
1635 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1639 // If enemy flag is back on the base switch to previous role
1640 ef = havocbot_ctf_find_enemy_flag(this);
1641 if(ef.ctf_status==FLAG_BASE)
1643 this.havocbot_role = this.havocbot_previous_role;
1644 this.havocbot_role_timeout = 0;
1648 // If the flag carrier reached the base switch to defense
1649 mf = havocbot_ctf_find_flag(this);
1650 if(mf.ctf_status!=FLAG_BASE)
1651 if(vlen(ef.origin - mf.dropped_origin) < 300)
1653 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1657 // Set the role timeout if necessary
1658 if (!this.havocbot_role_timeout)
1660 this.havocbot_role_timeout = time + random() * 30 + 60;
1663 // If nothing happened just switch to previous role
1664 if (time > this.havocbot_role_timeout)
1666 this.havocbot_role = this.havocbot_previous_role;
1667 this.havocbot_role_timeout = 0;
1671 // Chase the flag carrier
1672 if (this.bot_strategytime < time)
1674 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1675 navigation_goalrating_start(this);
1676 havocbot_goalrating_ctf_enemyflag(this, 30000);
1677 havocbot_goalrating_ctf_ourstolenflag(this, 40000);
1678 havocbot_goalrating_items(this, 10000, this.origin, 10000);
1679 navigation_goalrating_end(this);
1683 void havocbot_role_ctf_offense(entity this)
1690 havocbot_ctf_reset_role(this);
1694 if (this.flagcarried)
1696 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1701 mf = havocbot_ctf_find_flag(this);
1702 ef = havocbot_ctf_find_enemy_flag(this);
1705 if(mf.ctf_status!=FLAG_BASE)
1708 pos = mf.tag_entity.origin;
1712 // Try to get it if closer than the enemy base
1713 if(vlen(this.origin-ef.dropped_origin)>vlen(this.origin-pos))
1715 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1720 // Escort flag carrier
1721 if(ef.ctf_status!=FLAG_BASE)
1724 pos = ef.tag_entity.origin;
1728 if(vlen(pos-mf.dropped_origin)>700)
1730 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1735 // About to fail, switch to middlefield
1738 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1742 // Set the role timeout if necessary
1743 if (!this.havocbot_role_timeout)
1744 this.havocbot_role_timeout = time + 120;
1746 if (time > this.havocbot_role_timeout)
1748 havocbot_ctf_reset_role(this);
1752 if (this.bot_strategytime < time)
1754 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1755 navigation_goalrating_start(this);
1756 havocbot_goalrating_ctf_ourstolenflag(this, 50000);
1757 havocbot_goalrating_ctf_enemybase(this, 20000);
1758 havocbot_goalrating_items(this, 5000, this.origin, 1000);
1759 havocbot_goalrating_items(this, 1000, this.origin, 10000);
1760 navigation_goalrating_end(this);
1764 // Retriever (temporary role):
1765 void havocbot_role_ctf_retriever(entity this)
1771 havocbot_ctf_reset_role(this);
1775 if (this.flagcarried)
1777 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1781 // If flag is back on the base switch to previous role
1782 mf = havocbot_ctf_find_flag(this);
1783 if(mf.ctf_status==FLAG_BASE)
1785 havocbot_ctf_reset_role(this);
1789 if (!this.havocbot_role_timeout)
1790 this.havocbot_role_timeout = time + 20;
1792 if (time > this.havocbot_role_timeout)
1794 havocbot_ctf_reset_role(this);
1798 if (this.bot_strategytime < time)
1803 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1804 navigation_goalrating_start(this);
1805 havocbot_goalrating_ctf_ourstolenflag(this, 50000);
1806 havocbot_goalrating_ctf_droppedflags(this, 40000, this.origin, rt_radius);
1807 havocbot_goalrating_ctf_enemybase(this, 30000);
1808 havocbot_goalrating_items(this, 500, this.origin, rt_radius);
1809 navigation_goalrating_end(this);
1813 void havocbot_role_ctf_middle(entity this)
1819 havocbot_ctf_reset_role(this);
1823 if (this.flagcarried)
1825 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1829 mf = havocbot_ctf_find_flag(this);
1830 if(mf.ctf_status!=FLAG_BASE)
1832 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1836 if (!this.havocbot_role_timeout)
1837 this.havocbot_role_timeout = time + 10;
1839 if (time > this.havocbot_role_timeout)
1841 havocbot_ctf_reset_role(this);
1845 if (this.bot_strategytime < time)
1849 org = havocbot_ctf_middlepoint;
1850 org.z = this.origin.z;
1852 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1853 navigation_goalrating_start(this);
1854 havocbot_goalrating_ctf_ourstolenflag(this, 50000);
1855 havocbot_goalrating_ctf_droppedflags(this, 30000, this.origin, 10000);
1856 havocbot_goalrating_enemyplayers(this, 10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1857 havocbot_goalrating_items(this, 5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1858 havocbot_goalrating_items(this, 2500, this.origin, 10000);
1859 havocbot_goalrating_ctf_enemybase(this, 2500);
1860 navigation_goalrating_end(this);
1864 void havocbot_role_ctf_defense(entity this)
1870 havocbot_ctf_reset_role(this);
1874 if (this.flagcarried)
1876 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1880 // If own flag was captured
1881 mf = havocbot_ctf_find_flag(this);
1882 if(mf.ctf_status!=FLAG_BASE)
1884 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1888 if (!this.havocbot_role_timeout)
1889 this.havocbot_role_timeout = time + 30;
1891 if (time > this.havocbot_role_timeout)
1893 havocbot_ctf_reset_role(this);
1896 if (this.bot_strategytime < time)
1901 org = mf.dropped_origin;
1902 mp_radius = havocbot_ctf_middlepoint_radius;
1904 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1905 navigation_goalrating_start(this);
1907 // if enemies are closer to our base, go there
1908 entity closestplayer = world;
1909 float distance, bestdistance = 10000;
1910 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), LAMBDA(
1911 distance = vlen(org - it.origin);
1912 if(distance<bestdistance)
1915 bestdistance = distance;
1920 if(DIFF_TEAM(closestplayer, this))
1921 if(vlen(org - this.origin)>1000)
1922 if(checkpvs(this.origin,closestplayer)||random()<0.5)
1923 havocbot_goalrating_ctf_ourbase(this, 30000);
1925 havocbot_goalrating_ctf_ourstolenflag(this, 20000);
1926 havocbot_goalrating_ctf_droppedflags(this, 20000, org, mp_radius);
1927 havocbot_goalrating_enemyplayers(this, 15000, org, mp_radius);
1928 havocbot_goalrating_items(this, 10000, org, mp_radius);
1929 havocbot_goalrating_items(this, 5000, this.origin, 10000);
1930 navigation_goalrating_end(this);
1934 void havocbot_role_ctf_setrole(entity bot, int role)
1936 LOG_TRACE(strcat(bot.netname," switched to "));
1939 case HAVOCBOT_CTF_ROLE_CARRIER:
1940 LOG_TRACE("carrier");
1941 bot.havocbot_role = havocbot_role_ctf_carrier;
1942 bot.havocbot_role_timeout = 0;
1943 bot.havocbot_cantfindflag = time + 10;
1944 bot.bot_strategytime = 0;
1946 case HAVOCBOT_CTF_ROLE_DEFENSE:
1947 LOG_TRACE("defense");
1948 bot.havocbot_role = havocbot_role_ctf_defense;
1949 bot.havocbot_role_timeout = 0;
1951 case HAVOCBOT_CTF_ROLE_MIDDLE:
1952 LOG_TRACE("middle");
1953 bot.havocbot_role = havocbot_role_ctf_middle;
1954 bot.havocbot_role_timeout = 0;
1956 case HAVOCBOT_CTF_ROLE_OFFENSE:
1957 LOG_TRACE("offense");
1958 bot.havocbot_role = havocbot_role_ctf_offense;
1959 bot.havocbot_role_timeout = 0;
1961 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1962 LOG_TRACE("retriever");
1963 bot.havocbot_previous_role = bot.havocbot_role;
1964 bot.havocbot_role = havocbot_role_ctf_retriever;
1965 bot.havocbot_role_timeout = time + 10;
1966 bot.bot_strategytime = 0;
1968 case HAVOCBOT_CTF_ROLE_ESCORT:
1969 LOG_TRACE("escort");
1970 bot.havocbot_previous_role = bot.havocbot_role;
1971 bot.havocbot_role = havocbot_role_ctf_escort;
1972 bot.havocbot_role_timeout = time + 30;
1973 bot.bot_strategytime = 0;
1984 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
1987 int t = 0, t2 = 0, t3 = 0;
1989 // initially clear items so they can be set as necessary later.
1990 self.ctf_flagstatus &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
1991 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
1992 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
1993 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
1994 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
1995 | CTF_FLAG_NEUTRAL | CTF_SHIELDED);
1997 // scan through all the flags and notify the client about them
1998 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2000 if(flag.team == NUM_TEAM_1) { t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2001 if(flag.team == NUM_TEAM_2) { t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2002 if(flag.team == NUM_TEAM_3) { t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2003 if(flag.team == NUM_TEAM_4) { t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2004 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; }
2006 switch(flag.ctf_status)
2011 if((flag.owner == self) || (flag.pass_sender == self))
2012 self.ctf_flagstatus |= t; // carrying: self is currently carrying the flag
2014 self.ctf_flagstatus |= t2; // taken: someone else is carrying the flag
2019 self.ctf_flagstatus |= t3; // lost: the flag is dropped somewhere on the map
2025 // item for stopping players from capturing the flag too often
2026 if(self.ctf_captureshielded)
2027 self.ctf_flagstatus |= CTF_SHIELDED;
2029 // update the health of the flag carrier waypointsprite
2030 if(self.wps_flagcarrier)
2031 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
2036 MUTATOR_HOOKFUNCTION(ctf, PlayerDamage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2038 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2040 if(frag_target == frag_attacker) // damage done to yourself
2042 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2043 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2045 else // damage done to everyone else
2047 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2048 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2051 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2053 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)))
2054 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2056 frag_target.wps_helpme_time = time;
2057 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2059 // todo: add notification for when flag carrier needs help?
2064 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2066 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2068 PlayerTeamScore_AddScore(frag_attacker, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2069 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
2072 if(frag_target.flagcarried)
2074 entity tmp_entity = frag_target.flagcarried;
2075 ctf_Handle_Throw(frag_target, world, DROP_NORMAL);
2076 tmp_entity.ctf_dropper = world;
2082 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2085 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2088 void ctf_RemovePlayer(entity player)
2090 if(player.flagcarried)
2091 { ctf_Handle_Throw(player, world, DROP_NORMAL); }
2093 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2095 if(flag.pass_sender == player) { flag.pass_sender = world; }
2096 if(flag.pass_target == player) { flag.pass_target = world; }
2097 if(flag.ctf_dropper == player) { flag.ctf_dropper = world; }
2101 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2103 ctf_RemovePlayer(self);
2107 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2109 ctf_RemovePlayer(self);
2113 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2115 if(self.flagcarried)
2116 if(!autocvar_g_ctf_portalteleport)
2117 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2122 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2124 if(MUTATOR_RETURNVALUE || gameover) { return false; }
2126 entity player = self;
2128 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2130 // pass the flag to a team mate
2131 if(autocvar_g_ctf_pass)
2133 entity head, closest_target = world;
2134 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2136 while(head) // find the closest acceptable target to pass to
2138 if(IS_PLAYER(head) && !IS_DEAD(head))
2139 if(head != player && SAME_TEAM(head, player))
2140 if(!head.speedrunning && !head.vehicle)
2142 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2143 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2144 vector passer_center = CENTER_OR_VIEWOFS(player);
2146 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2148 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2150 if(IS_BOT_CLIENT(head))
2152 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2153 ctf_Handle_Throw(head, player, DROP_PASS);
2157 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2158 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2160 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2163 else if(player.flagcarried)
2167 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2168 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
2169 { closest_target = head; }
2171 else { closest_target = head; }
2178 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2181 // throw the flag in front of you
2182 if(autocvar_g_ctf_throw && player.flagcarried)
2184 if(player.throw_count == -1)
2186 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2188 player.throw_prevtime = time;
2189 player.throw_count = 1;
2190 ctf_Handle_Throw(player, world, DROP_THROW);
2195 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2201 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2202 else { player.throw_count += 1; }
2203 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2205 player.throw_prevtime = time;
2206 ctf_Handle_Throw(player, world, DROP_THROW);
2215 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2217 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2219 self.wps_helpme_time = time;
2220 WaypointSprite_HelpMePing(self.wps_flagcarrier);
2222 else // create a normal help me waypointsprite
2224 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, self, FLAG_WAYPOINT_OFFSET, world, self.team, self, wps_helpme, false, RADARICON_HELPME);
2225 WaypointSprite_Ping(self.wps_helpme);
2231 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2233 if(vh_player.flagcarried)
2235 vh_player.flagcarried.nodrawtoclient = vh_player; // hide the flag from the driver
2237 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2239 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
2243 setattachment(vh_player.flagcarried, vh_vehicle, "");
2244 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
2245 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2246 //vh_player.flagcarried.angles = '0 0 0';
2254 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2256 if(vh_player.flagcarried)
2258 setattachment(vh_player.flagcarried, vh_player, "");
2259 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2260 vh_player.flagcarried.scale = FLAG_SCALE;
2261 vh_player.flagcarried.angles = '0 0 0';
2262 vh_player.flagcarried.nodrawtoclient = world;
2269 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2271 if(self.flagcarried)
2273 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((self.flagcarried.team) ? APP_TEAM_ENT(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN) : INFO_CTF_FLAGRETURN_ABORTRUN_NEUTRAL));
2274 ctf_RespawnFlag(self.flagcarried);
2281 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2283 entity flag; // temporary entity for the search method
2285 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2287 switch(flag.ctf_status)
2292 // lock the flag, game is over
2293 flag.movetype = MOVETYPE_NONE;
2294 flag.takedamage = DAMAGE_NO;
2295 flag.solid = SOLID_NOT;
2296 flag.nextthink = false; // stop thinking
2298 //dprint("stopping the ", flag.netname, " from moving.\n");
2306 // do nothing for these flags
2315 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2317 havocbot_ctf_reset_role(self);
2321 MUTATOR_HOOKFUNCTION(ctf, GetTeamCount)
2323 //ret_float = ctf_teams;
2324 ret_string = "ctf_team";
2328 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2330 self.ctf_flagstatus = other.ctf_flagstatus;
2334 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2336 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2338 if (MapInfo_Get_ByID(i))
2340 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2346 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2347 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2354 bool superspec_Spectate(entity _player); // TODO
2355 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2356 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2359 if(IS_PLAYER(self) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2361 if(cmd_name == "followfc")
2373 case "red": _team = NUM_TEAM_1; break;
2374 case "blue": _team = NUM_TEAM_2; break;
2375 case "yellow": if(ctf_teams >= 3) _team = NUM_TEAM_3; break;
2376 case "pink": if(ctf_teams >= 4) _team = NUM_TEAM_4; break;
2380 FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
2381 if(it.flagcarried && (it.team == _team || _team == 0))
2384 if(_team == 0 && IS_SPEC(self) && self.enemy == it)
2385 continue; // already spectating this fc, try another
2386 return superspec_Spectate(it);
2391 superspec_msg("", "", self, "No active flag carrier\n", 1);
2398 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2400 if(frag_target.flagcarried)
2401 ctf_Handle_Throw(frag_target, world, DROP_THROW);
2411 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2412 CTF flag for team one (Red).
2414 "angle" Angle the flag will point (minus 90 degrees)...
2415 "model" model to use, note this needs red and blue as skins 0 and 1...
2416 "noise" sound played when flag is picked up...
2417 "noise1" sound played when flag is returned by a teammate...
2418 "noise2" sound played when flag is captured...
2419 "noise3" sound played when flag is lost in the field and respawns itself...
2420 "noise4" sound played when flag is dropped by a player...
2421 "noise5" sound played when flag touches the ground... */
2422 spawnfunc(item_flag_team1)
2424 if(!g_ctf) { remove(self); return; }
2426 ctf_FlagSetup(NUM_TEAM_1, self);
2429 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2430 CTF flag for team two (Blue).
2432 "angle" Angle the flag will point (minus 90 degrees)...
2433 "model" model to use, note this needs red and blue as skins 0 and 1...
2434 "noise" sound played when flag is picked up...
2435 "noise1" sound played when flag is returned by a teammate...
2436 "noise2" sound played when flag is captured...
2437 "noise3" sound played when flag is lost in the field and respawns itself...
2438 "noise4" sound played when flag is dropped by a player...
2439 "noise5" sound played when flag touches the ground... */
2440 spawnfunc(item_flag_team2)
2442 if(!g_ctf) { remove(self); return; }
2444 ctf_FlagSetup(NUM_TEAM_2, self);
2447 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2448 CTF flag for team three (Yellow).
2450 "angle" Angle the flag will point (minus 90 degrees)...
2451 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2452 "noise" sound played when flag is picked up...
2453 "noise1" sound played when flag is returned by a teammate...
2454 "noise2" sound played when flag is captured...
2455 "noise3" sound played when flag is lost in the field and respawns itself...
2456 "noise4" sound played when flag is dropped by a player...
2457 "noise5" sound played when flag touches the ground... */
2458 spawnfunc(item_flag_team3)
2460 if(!g_ctf) { remove(self); return; }
2462 ctf_FlagSetup(NUM_TEAM_3, self);
2465 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2466 CTF flag for team four (Pink).
2468 "angle" Angle the flag will point (minus 90 degrees)...
2469 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2470 "noise" sound played when flag is picked up...
2471 "noise1" sound played when flag is returned by a teammate...
2472 "noise2" sound played when flag is captured...
2473 "noise3" sound played when flag is lost in the field and respawns itself...
2474 "noise4" sound played when flag is dropped by a player...
2475 "noise5" sound played when flag touches the ground... */
2476 spawnfunc(item_flag_team4)
2478 if(!g_ctf) { remove(self); return; }
2480 ctf_FlagSetup(NUM_TEAM_4, self);
2483 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2486 "angle" Angle the flag will point (minus 90 degrees)...
2487 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2488 "noise" sound played when flag is picked up...
2489 "noise1" sound played when flag is returned by a teammate...
2490 "noise2" sound played when flag is captured...
2491 "noise3" sound played when flag is lost in the field and respawns itself...
2492 "noise4" sound played when flag is dropped by a player...
2493 "noise5" sound played when flag touches the ground... */
2494 spawnfunc(item_flag_neutral)
2496 if(!g_ctf) { remove(self); return; }
2497 if(!cvar("g_ctf_oneflag")) { remove(self); return; }
2499 ctf_FlagSetup(0, self);
2502 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2503 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2504 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.
2506 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2507 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2510 if(!g_ctf) { remove(self); return; }
2512 self.classname = "ctf_team";
2513 self.team = self.cnt + 1;
2516 // compatibility for quake maps
2517 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2518 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2519 spawnfunc(info_player_team1);
2520 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2521 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2522 spawnfunc(info_player_team2);
2523 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2524 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2526 void team_CTF_neutralflag() { SELFPARAM(); spawnfunc_item_flag_neutral(self); }
2527 void team_neutralobelisk() { SELFPARAM(); spawnfunc_item_flag_neutral(self); }
2535 void ctf_ScoreRules(int teams)
2537 CheckAllowedTeams(world);
2538 ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
2539 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2540 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2541 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2542 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2543 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2544 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2545 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2546 ScoreRules_basics_end();
2549 // code from here on is just to support maps that don't have flag and team entities
2550 void ctf_SpawnTeam (string teamname, int teamcolor)
2552 entity this = new_pure(ctf_team);
2553 this.netname = teamname;
2554 this.cnt = teamcolor;
2555 this.spawnfunc_checked = true;
2556 WITHSELF(this, spawnfunc_ctf_team(this));
2559 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2564 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2566 if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2567 if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2568 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2571 ctf_teams = bound(2, ctf_teams, 4);
2573 // if no teams are found, spawn defaults
2574 if(find(world, classname, "ctf_team") == world)
2576 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.\n");
2577 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2578 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2580 ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2582 ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2585 ctf_ScoreRules(ctf_teams);
2588 void ctf_Initialize()
2590 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2592 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2593 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2594 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2596 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);