1 // ================================================================
2 // Official capture the flag game mode coding, reworked by Samual
3 // Last updated: September, 2012
4 // ================================================================
6 void ctf_FakeTimeLimit(entity e, float t)
9 WriteByte(MSG_ONE, 3); // svc_updatestat
10 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
12 WriteCoord(MSG_ONE, autocvar_timelimit);
14 WriteCoord(MSG_ONE, (t + 1) / 60);
17 void ctf_EventLog(string mode, float flagteam, entity actor) // use an alias for easy changing and quick editing later
19 if(autocvar_sv_eventlog)
20 GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
23 void ctf_CaptureRecord(entity flag, entity player)
26 float cap_record = ctf_captimerecord;
27 float cap_time = (time - flag.ctf_pickuptime);
28 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
31 FOR_EACH_REALCLIENT(tmp_entity)
33 if(tmp_entity.CAPTURE_VERBOSE)
35 if(!ctf_captimerecord) { Send_Notification(NOTIF_ONE_ONLY, tmp_entity, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_CAPTURE_TIME_), player.netname, (cap_time * 100)); }
36 else if(cap_time < cap_record) { Send_Notification(NOTIF_ONE_ONLY, tmp_entity, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_CAPTURE_BROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
37 else { Send_Notification(NOTIF_ONE_ONLY, tmp_entity, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_CAPTURE_UNBROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
39 else { Send_Notification(NOTIF_ONE_ONLY, tmp_entity, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_CAPTURE_), player.netname); }
42 // the previous notification broadcast is only sent to real clients, this will notify server log too
43 if(server_is_dedicated)
45 if(autocvar_notification_ctf_capture_verbose)
47 if(!ctf_captimerecord) { Local_Notification(MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_CAPTURE_TIME_), player.netname, (cap_time * 100)); }
48 else if(cap_time < cap_record) { Local_Notification(MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_CAPTURE_BROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
49 else { Local_Notification(MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_CAPTURE_UNBROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
51 else { Local_Notification(MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_CAPTURE_), player.netname); }
54 // write that shit in the database
55 if((!ctf_captimerecord) || (cap_time < cap_record))
57 ctf_captimerecord = cap_time;
58 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
59 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
60 write_recordmarker(player, (time - cap_time), cap_time);
64 void ctf_FlagcarrierWaypoints(entity player)
66 WaypointSprite_Spawn("flagcarrier", 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, TRUE, RADARICON_FLAG, WPCOLOR_FLAGCARRIER(player.team));
67 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
68 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
69 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
72 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
74 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
75 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
76 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
77 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
80 if(current_height) // make sure we can actually do this arcing path
82 targpos = (to + ('0 0 1' * current_height));
83 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
84 if(trace_fraction < 1)
86 //print("normal arc line failed, trying to find new pos...");
87 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
88 targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
89 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
90 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
91 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
94 else { targpos = to; }
96 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
98 vector desired_direction = normalize(targpos - from);
99 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
100 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
103 float ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
105 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
107 // directional tracing only
109 makevectors(passer_angle);
111 // find the closest point on the enemy to the center of the attack
112 float ang; // angle between shotdir and h
113 float h; // hypotenuse, which is the distance between attacker to head
114 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
116 h = vlen(head_center - passer_center);
117 ang = acos(dotproduct(normalize(head_center - passer_center), v_forward));
120 vector nearest_on_line = (passer_center + a * v_forward);
121 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
123 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
124 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
126 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
131 else { return TRUE; }
134 float ctf_IsDifferentTeam(entity a, entity b)
136 float f = IsDifferentTeam(a, b);
137 return (autocvar_g_ctf_reverse) ? !f : f;
140 float ctf_Stalemate_waypointsprite_visible_for_player(entity e)
143 if(ctf_IsDifferentTeam(self.owner.flagcarried, self.owner))
144 if(ctf_IsDifferentTeam(self.owner.flagcarried, e))
145 if(!IsDifferentTeam(self.owner, e))
154 // =======================
155 // CaptureShield Functions
156 // =======================
158 float ctf_CaptureShield_CheckStatus(entity p)
160 float s, s2, s3, s4, se, se2, se3, se4, sr, ser;
162 float players_worseeq, players_total;
164 if(ctf_captureshield_max_ratio <= 0)
167 s = PlayerScore_Add(p, SP_CTF_CAPS, 0);
168 s2 = PlayerScore_Add(p, SP_CTF_PICKUPS, 0);
169 s3 = PlayerScore_Add(p, SP_CTF_RETURNS, 0);
170 s4 = PlayerScore_Add(p, SP_CTF_FCKILLS, 0);
172 sr = ((s - s2) + (s3 + s4));
174 if(sr >= -ctf_captureshield_min_negscore)
177 players_total = players_worseeq = 0;
180 if(IsDifferentTeam(e, p))
182 se = PlayerScore_Add(e, SP_CTF_CAPS, 0);
183 se2 = PlayerScore_Add(e, SP_CTF_PICKUPS, 0);
184 se3 = PlayerScore_Add(e, SP_CTF_RETURNS, 0);
185 se4 = PlayerScore_Add(e, SP_CTF_FCKILLS, 0);
187 ser = ((se - se2) + (se3 + se4));
194 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
195 // use this rule here
197 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
203 void ctf_CaptureShield_Update(entity player, float wanted_status)
205 float updated_status = ctf_CaptureShield_CheckStatus(player);
206 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
208 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
209 player.ctf_captureshielded = updated_status;
213 float ctf_CaptureShield_Customize()
215 if(!other.ctf_captureshielded) { return FALSE; }
216 if(!ctf_IsDifferentTeam(self, other)) { return FALSE; }
221 void ctf_CaptureShield_Touch()
223 if(!other.ctf_captureshielded) { return; }
224 if(!ctf_IsDifferentTeam(self, other)) { return; }
226 vector mymid = (self.absmin + self.absmax) * 0.5;
227 vector othermid = (other.absmin + other.absmax) * 0.5;
229 Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
230 Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED);
233 void ctf_CaptureShield_Spawn(entity flag)
235 entity shield = spawn();
238 shield.team = self.team;
239 shield.touch = ctf_CaptureShield_Touch;
240 shield.customizeentityforclient = ctf_CaptureShield_Customize;
241 shield.classname = "ctf_captureshield";
242 shield.effects = EF_ADDITIVE;
243 shield.movetype = MOVETYPE_NOCLIP;
244 shield.solid = SOLID_TRIGGER;
245 shield.avelocity = '7 0 11';
248 setorigin(shield, self.origin);
249 setmodel(shield, "models/ctf/shield.md3");
250 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
254 // ====================
255 // Drop/Pass/Throw Code
256 // ====================
258 void ctf_Handle_Drop(entity flag, entity player, float droptype)
261 player = (player ? player : flag.pass_sender);
264 flag.movetype = MOVETYPE_TOSS;
265 flag.takedamage = DAMAGE_YES;
266 flag.angles = '0 0 0';
267 flag.health = flag.max_flag_health;
268 flag.ctf_droptime = time;
269 flag.ctf_dropper = player;
270 flag.ctf_status = FLAG_DROPPED;
272 // messages and sounds
273 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_LOST_), player.netname);
274 sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTN_NONE);
275 ctf_EventLog("dropped", player.team, player);
278 PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);
279 PlayerScore_Add(player, SP_CTF_DROPS, 1);
282 if(autocvar_g_ctf_flag_dropped_waypoint)
283 WaypointSprite_Spawn("flagdropped", 0, 0, flag, FLAG_WAYPOINT_OFFSET, world, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, TRUE, RADARICON_FLAG, WPCOLOR_DROPPEDFLAG(flag.team));
285 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
287 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
288 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
291 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
293 if(droptype == DROP_PASS)
295 flag.pass_distance = 0;
296 flag.pass_sender = world;
297 flag.pass_target = world;
301 void ctf_Handle_Retrieve(entity flag, entity player)
303 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
304 entity sender = flag.pass_sender;
306 // transfer flag to player
308 flag.owner.flagcarried = flag;
311 setattachment(flag, player, "");
312 setorigin(flag, FLAG_CARRY_OFFSET);
313 flag.movetype = MOVETYPE_NONE;
314 flag.takedamage = DAMAGE_NO;
315 flag.solid = SOLID_NOT;
316 flag.angles = '0 0 0';
317 flag.ctf_status = FLAG_CARRY;
319 // messages and sounds
320 sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTN_NORM);
321 ctf_EventLog("receive", flag.team, player);
323 FOR_EACH_REALPLAYER(tmp_player)
325 if(tmp_player == sender)
326 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_SENT_), player.netname);
327 else if(tmp_player == player)
328 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_RECEIVED_), sender.netname);
329 else if(!IsDifferentTeam(tmp_player, sender))
330 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_OTHER_), sender.netname, player.netname);
333 // create new waypoint
334 ctf_FlagcarrierWaypoints(player);
336 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
337 player.throw_antispam = sender.throw_antispam;
339 flag.pass_distance = 0;
340 flag.pass_sender = world;
341 flag.pass_target = world;
344 void ctf_Handle_Throw(entity player, entity receiver, float droptype)
346 entity flag = player.flagcarried;
347 vector targ_origin, flag_velocity;
349 if(!flag) { return; }
350 if((droptype == DROP_PASS) && !receiver) { return; }
352 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
355 setattachment(flag, world, "");
356 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
357 flag.owner.flagcarried = world;
359 flag.solid = SOLID_TRIGGER;
360 flag.ctf_dropper = player;
361 flag.ctf_droptime = time;
363 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
370 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
371 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
372 WarpZone_RefSys_Copy(flag, receiver);
373 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
374 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
376 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
377 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, FALSE);
380 flag.movetype = MOVETYPE_FLY;
381 flag.takedamage = DAMAGE_NO;
382 flag.pass_sender = player;
383 flag.pass_target = receiver;
384 flag.ctf_status = FLAG_PASSING;
387 sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTN_NORM);
388 WarpZone_TrailParticles(world, particleeffectnum(flag.passeffect), player.origin, targ_origin);
389 ctf_EventLog("pass", flag.team, player);
395 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'));
397 flag_velocity = (('0 0 1' * autocvar_g_ctf_throw_velocity_up) + ((v_forward * autocvar_g_ctf_throw_velocity_forward) * ((player.items & IT_STRENGTH) ? autocvar_g_ctf_throw_strengthmultiplier : 1)));
398 flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, FALSE);
399 ctf_Handle_Drop(flag, player, droptype);
405 flag.velocity = '0 0 0'; // do nothing
412 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);
413 ctf_Handle_Drop(flag, player, droptype);
418 // kill old waypointsprite
419 WaypointSprite_Ping(player.wps_flagcarrier);
420 WaypointSprite_Kill(player.wps_flagcarrier);
422 if(player.wps_enemyflagcarrier)
423 WaypointSprite_Kill(player.wps_enemyflagcarrier);
426 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
434 void ctf_Handle_Capture(entity flag, entity toucher, float capturetype)
436 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
437 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
438 float old_time, new_time;
440 if not(player) { return; } // without someone to give the reward to, we can't possibly cap
441 if(ctf_IsDifferentTeam(player, flag)) { return; }
443 // messages and sounds
444 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(enemy_flag, CENTER_CTF_CAPTURE_));
445 ctf_CaptureRecord(enemy_flag, player);
446 sound(player, CH_TRIGGER, ((IsDifferentTeam(player, flag)) ? enemy_flag.snd_flag_capture : flag.snd_flag_capture), VOL_BASE, ATTN_NONE);
450 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
451 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
456 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
457 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
459 old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
460 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
461 if(!old_time || new_time < old_time)
462 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
465 pointparticles(particleeffectnum(flag.capeffect), flag.origin, '0 0 0', 1);
466 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
469 if(capturetype == CAPTURE_NORMAL)
471 WaypointSprite_Kill(player.wps_flagcarrier);
472 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
474 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
475 { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
479 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
480 ctf_RespawnFlag(enemy_flag);
483 void ctf_Handle_Return(entity flag, entity player)
485 // messages and sounds
486 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_RETURN_));
487 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_), player.netname);
488 sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTN_NONE);
489 ctf_EventLog("return", flag.team, player);
492 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
493 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
495 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
499 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
500 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
501 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
505 ctf_RespawnFlag(flag);
508 void ctf_Handle_Pickup(entity flag, entity player, float pickuptype)
511 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
512 float pickup_dropped_score; // used to calculate dropped pickup score
514 // attach the flag to the player
516 player.flagcarried = flag;
517 setattachment(flag, player, "");
518 setorigin(flag, FLAG_CARRY_OFFSET);
521 flag.movetype = MOVETYPE_NONE;
522 flag.takedamage = DAMAGE_NO;
523 flag.solid = SOLID_NOT;
524 flag.angles = '0 0 0';
525 flag.ctf_status = FLAG_CARRY;
529 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
530 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
534 // messages and sounds
535 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_PICKUP_), player.netname);
536 sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE);
538 FOR_EACH_REALPLAYER(tmp_player)
540 if(tmp_player == player)
542 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PICKUP_));
543 if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
545 else if(!IsDifferentTeam(tmp_player, player) && tmp_player != player)
547 if(tmp_player.PICKUP_TEAM_VERBOSE)
548 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PICKUP_TEAM_VERBOSE_), Team_ColorCode(player.team), player.netname);
550 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PICKUP_TEAM_), Team_ColorCode(player.team));
552 else if(IsDifferentTeam(tmp_player, player) && !ctf_IsDifferentTeam(flag, tmp_player))
554 if(tmp_player.PICKUP_ENEMY_VERBOSE)
555 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, CENTER_CTF_PICKUP_ENEMY_VERBOSE, Team_ColorCode(player.team), player.netname);
557 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, CENTER_CTF_PICKUP_ENEMY, Team_ColorCode(player.team));
562 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
567 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
568 ctf_EventLog("steal", flag.team, player);
574 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);
575 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);
576 dprint("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
577 PlayerTeamScore_AddScore(player, pickup_dropped_score);
578 ctf_EventLog("pickup", flag.team, player);
586 if(pickuptype == PICKUP_BASE)
588 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
589 if((player.speedrunning) && (ctf_captimerecord))
590 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
594 pointparticles(particleeffectnum(flag.toucheffect), player.origin, '0 0 0', 1);
597 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
598 ctf_FlagcarrierWaypoints(player);
599 WaypointSprite_Ping(player.wps_flagcarrier);
603 // ===================
604 // Main Flag Functions
605 // ===================
607 void ctf_CheckFlagReturn(entity flag, float returntype)
609 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
611 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
613 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
617 case RETURN_DROPPED: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DROPPED_)); break;
618 case RETURN_DAMAGE: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DAMAGED_)); break;
619 case RETURN_SPEEDRUN: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_SPEEDRUN_), ctf_captimerecord); break;
620 case RETURN_NEEDKILL: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_NEEDKILL_)); break;
624 { Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_TIMEOUT_)); break; }
626 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTN_NONE);
627 ctf_EventLog("returned", flag.team, world);
628 ctf_RespawnFlag(flag);
633 void ctf_CheckStalemate(void)
636 float stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0;
639 entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
641 // build list of stale flags
642 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
644 if(autocvar_g_ctf_stalemate)
645 if(tmp_entity.ctf_status != FLAG_BASE)
646 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time)
648 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
649 ctf_staleflaglist = tmp_entity;
651 if(tmp_entity.team == NUM_TEAM_1) { ++stale_red_flags; }
652 if(tmp_entity.team == NUM_TEAM_2) { ++stale_blue_flags; }
653 if(tmp_entity.team == NUM_TEAM_3) { ++stale_yellow_flags; }
654 if(tmp_entity.team == NUM_TEAM_4) { ++stale_pink_flags; }
658 stale_flags = (stale_red_flags > 0) + (stale_blue_flags > 0) + (stale_yellow_flags > 0) + (stale_pink_flags > 0);
660 if(stale_flags >= ctf_teams)
661 ctf_stalemate = TRUE;
662 else if(stale_flags < ctf_teams && autocvar_g_ctf_stalemate_endcondition == 2)
663 { ctf_stalemate = FALSE; wpforenemy_announced = FALSE; }
664 else if(stale_flags > 0 && autocvar_g_ctf_stalemate_endcondition == 1)
665 { ctf_stalemate = FALSE; wpforenemy_announced = FALSE; }
667 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
670 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
672 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
674 WaypointSprite_Spawn("enemyflagcarrier", 0, 0, tmp_entity.owner, FLAG_WAYPOINT_OFFSET, world, 0, tmp_entity.owner, wps_enemyflagcarrier, TRUE, RADARICON_FLAG, WPCOLOR_ENEMYFC(tmp_entity.owner.team));
675 tmp_entity.owner.wps_enemyflagcarrier.waypointsprite_visible_for_player = ctf_Stalemate_waypointsprite_visible_for_player;
679 if not(wpforenemy_announced)
681 FOR_EACH_REALPLAYER(tmp_entity)
682 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CENTER, ((tmp_entity.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER));
684 wpforenemy_announced = TRUE;
689 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
691 if(ITEM_DAMAGE_NEEDKILL(deathtype))
693 // automatically kill the flag and return it
695 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
698 if(autocvar_g_ctf_flag_return_damage)
700 // reduce health and check if it should be returned
701 self.health = self.health - damage;
702 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
712 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
715 if(self == ctf_worldflaglist) // only for the first flag
716 FOR_EACH_CLIENT(tmp_entity)
717 ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
720 if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
721 dprint("wtf the flag got squashed?\n");
722 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
723 if(!trace_startsolid) // can we resize it without getting stuck?
724 setsize(self, FLAG_MIN, FLAG_MAX); }
726 switch(self.ctf_status) // reset flag angles in case warpzones adjust it
730 self.angles = '0 0 0';
738 switch(self.ctf_status)
742 if(autocvar_g_ctf_dropped_capture_radius)
744 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
745 if(tmp_entity.ctf_status == FLAG_DROPPED)
746 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
747 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
748 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
755 if(autocvar_g_ctf_flag_dropped_floatinwater)
757 vector midpoint = ((self.absmin + self.absmax) * 0.5);
758 if(pointcontents(midpoint) == CONTENT_WATER)
760 self.velocity = self.velocity * 0.5;
762 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
763 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
765 { self.movetype = MOVETYPE_FLY; }
767 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
769 if(autocvar_g_ctf_flag_return_dropped)
771 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
774 ctf_CheckFlagReturn(self, RETURN_DROPPED);
778 if(autocvar_g_ctf_flag_return_time)
780 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
781 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
789 if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
792 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
796 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
800 if(autocvar_g_ctf_stalemate)
802 if(time >= wpforenemy_nextthink)
804 ctf_CheckStalemate();
805 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
813 vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
814 targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
815 WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
817 if((self.pass_target == world)
818 || (self.pass_target.deadflag != DEAD_NO)
819 || (self.pass_target.flagcarried)
820 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
821 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
822 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
824 // give up, pass failed
825 ctf_Handle_Drop(self, world, DROP_PASS);
829 // still a viable target, go for it
830 ctf_CalculatePassVelocity(self, targ_origin, self.origin, TRUE);
835 default: // this should never happen
837 dprint("ctf_FlagThink(): Flag exists with no status?\n");
845 if(gameover) { return; }
847 entity toucher = other;
849 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
850 if(ITEM_TOUCH_NEEDKILL())
853 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
857 // special touch behaviors
858 if(toucher.vehicle_flags & VHF_ISVEHICLE)
860 if(autocvar_g_ctf_allow_vehicle_touch)
861 toucher = toucher.owner; // the player is actually the vehicle owner, not other
863 return; // do nothing
865 else if not(IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
867 if(time > self.wait) // if we haven't in a while, play a sound/effect
869 pointparticles(particleeffectnum(self.toucheffect), self.origin, '0 0 0', 1);
870 sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTN_NORM);
871 self.wait = time + FLAG_TOUCHRATE;
875 else if(toucher.deadflag != DEAD_NO) { return; }
877 switch(self.ctf_status)
881 if(!ctf_IsDifferentTeam(toucher, self) && (toucher.flagcarried) && IsDifferentTeam(toucher.flagcarried, self))
882 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
883 else if(ctf_IsDifferentTeam(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time))
884 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
890 if(!ctf_IsDifferentTeam(toucher, self))
891 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
892 else if((!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
893 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
899 dprint("Someone touched a flag even though it was being carried?\n");
905 if((IS_PLAYER(toucher)) && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
907 if(IsDifferentTeam(toucher, self.pass_sender))
908 ctf_Handle_Return(self, toucher);
910 ctf_Handle_Retrieve(self, toucher);
918 void ctf_RespawnFlag(entity flag)
920 // check for flag respawn being called twice in a row
921 if(flag.last_respawn > time - 0.5)
922 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
924 flag.last_respawn = time;
926 // reset the player (if there is one)
927 if((flag.owner) && (flag.owner.flagcarried == flag))
929 if(flag.owner.wps_enemyflagcarrier)
930 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
932 WaypointSprite_Kill(flag.wps_flagcarrier);
934 flag.owner.flagcarried = world;
936 if(flag.speedrunning)
937 ctf_FakeTimeLimit(flag.owner, -1);
940 if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
941 { WaypointSprite_Kill(flag.wps_flagdropped); }
944 setattachment(flag, world, "");
945 setorigin(flag, flag.ctf_spawnorigin);
947 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
948 flag.takedamage = DAMAGE_NO;
949 flag.health = flag.max_flag_health;
950 flag.solid = SOLID_TRIGGER;
951 flag.velocity = '0 0 0';
952 flag.angles = flag.mangle;
953 flag.flags = FL_ITEM | FL_NOTARGET;
955 flag.ctf_status = FLAG_BASE;
957 flag.pass_distance = 0;
958 flag.pass_sender = world;
959 flag.pass_target = world;
960 flag.ctf_dropper = world;
961 flag.ctf_pickuptime = 0;
962 flag.ctf_droptime = 0;
968 if(IS_PLAYER(self.owner))
969 ctf_Handle_Throw(self.owner, world, DROP_RESET);
971 ctf_RespawnFlag(self);
974 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
977 waypoint_spawnforitem_force(self, self.origin);
978 self.nearestwaypointtimeout = 0; // activate waypointing again
979 self.bot_basewaypoint = self.nearestwaypoint;
982 string basename = "base";
986 case NUM_TEAM_1: basename = "redbase"; break;
987 case NUM_TEAM_2: basename = "bluebase"; break;
988 case NUM_TEAM_3: basename = "yellowbase"; break;
989 case NUM_TEAM_4: basename = "pinkbase"; break;
992 WaypointSprite_SpawnFixed(basename, self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
993 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
995 // captureshield setup
996 ctf_CaptureShield_Spawn(self);
999 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1002 self = flag; // for later usage with droptofloor()
1005 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1006 ctf_worldflaglist = flag;
1008 setattachment(flag, world, "");
1010 flag.netname = strcat(Static_Team_ColorName_Upper(teamnumber), "^7 flag");
1011 flag.team = teamnumber;
1012 flag.classname = "item_flag_team";
1013 flag.target = "###item###"; // wut?
1014 flag.flags = FL_ITEM | FL_NOTARGET;
1015 flag.solid = SOLID_TRIGGER;
1016 flag.takedamage = DAMAGE_NO;
1017 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1018 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1019 flag.health = flag.max_flag_health;
1020 flag.event_damage = ctf_FlagDamage;
1021 flag.pushable = TRUE;
1022 flag.teleportable = TELEPORT_NORMAL;
1023 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1024 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1025 flag.velocity = '0 0 0';
1026 flag.mangle = flag.angles;
1027 flag.reset = ctf_Reset;
1028 flag.touch = ctf_FlagTouch;
1029 flag.think = ctf_FlagThink;
1030 flag.nextthink = time + FLAG_THINKRATE;
1031 flag.ctf_status = FLAG_BASE;
1034 if(flag.model == "") { flag.model = strzone(cvar_string(strcat("g_ctf_flag_", Static_Team_ColorName_Lower(teamnumber), "_model"))); }
1035 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1036 if(!flag.skin) { flag.skin = cvar(strcat("g_ctf_flag_", Static_Team_ColorName_Lower(teamnumber), "_skin")); }
1037 if(flag.toucheffect == "") { flag.toucheffect = strzone(strcat(Static_Team_ColorName_Lower(teamnumber), "_touch")); }
1038 if(flag.passeffect == "") { flag.passeffect = strzone(strcat(Static_Team_ColorName_Lower(teamnumber), "_pass")); }
1039 if(flag.capeffect == "") { flag.capeffect = strzone(strcat(Static_Team_ColorName_Lower(teamnumber), "_cap")); }
1042 if(flag.snd_flag_taken == "") { flag.snd_flag_taken = strzone(strcat("ctf/", Static_Team_ColorName_Lower(teamnumber), "_taken.wav")); }
1043 if(flag.snd_flag_returned == "") { flag.snd_flag_returned = strzone(strcat("ctf/", Static_Team_ColorName_Lower(teamnumber), "_returned.wav")); }
1044 if(flag.snd_flag_capture == "") { flag.snd_flag_capture = strzone(strcat("ctf/", Static_Team_ColorName_Lower(teamnumber), "_capture.wav")); } // blue team scores by capturing the red flag
1045 if(flag.snd_flag_respawn == "") { flag.snd_flag_respawn = "ctf/flag_respawn.wav"; } // if there is ever a team-based sound for this, update the code to match.
1046 if(flag.snd_flag_dropped == "") { flag.snd_flag_dropped = strzone(strcat("ctf/", Static_Team_ColorName_Lower(teamnumber), "_dropped.wav")); }
1047 if(flag.snd_flag_touch == "") { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
1048 if(flag.snd_flag_pass == "") { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
1051 precache_sound(flag.snd_flag_taken);
1052 precache_sound(flag.snd_flag_returned);
1053 precache_sound(flag.snd_flag_capture);
1054 precache_sound(flag.snd_flag_respawn);
1055 precache_sound(flag.snd_flag_dropped);
1056 precache_sound(flag.snd_flag_touch);
1057 precache_sound(flag.snd_flag_pass);
1058 precache_model(flag.model);
1059 precache_model("models/ctf/shield.md3");
1060 precache_model("models/ctf/shockwavetransring.md3");
1063 setmodel(flag, flag.model); // precision set below
1064 setsize(flag, FLAG_MIN, FLAG_MAX);
1065 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1067 if(autocvar_g_ctf_flag_glowtrails)
1071 case NUM_TEAM_1: flag.glow_color = 251; break; // red
1072 case NUM_TEAM_2: flag.glow_color = 210; break; // blue
1073 case NUM_TEAM_3: flag.glow_color = 110; break; // yellow
1074 case NUM_TEAM_4: flag.glow_color = 145; break; // pink
1076 flag.glow_size = 25;
1077 flag.glow_trail = 1;
1080 flag.effects |= EF_LOWPRECISION;
1081 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1082 if(autocvar_g_ctf_dynamiclights)
1086 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1087 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1088 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1089 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1094 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1096 flag.dropped_origin = flag.origin;
1097 flag.noalign = TRUE;
1098 flag.movetype = MOVETYPE_NONE;
1100 else // drop to floor, automatically find a platform and set that as spawn origin
1102 flag.noalign = FALSE;
1105 flag.movetype = MOVETYPE_TOSS;
1108 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1116 // NOTE: LEGACY CODE, needs to be re-written!
1118 void havocbot_calculate_middlepoint()
1122 vector fo = '0 0 0';
1125 f = ctf_worldflaglist;
1130 f = f.ctf_worldflagnext;
1134 havocbot_ctf_middlepoint = s * (1.0 / n);
1135 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1139 entity havocbot_ctf_find_flag(entity bot)
1142 f = ctf_worldflaglist;
1145 if (!ctf_IsDifferentTeam(bot, f))
1147 f = f.ctf_worldflagnext;
1152 entity havocbot_ctf_find_enemy_flag(entity bot)
1155 f = ctf_worldflaglist;
1158 if (ctf_IsDifferentTeam(bot, f))
1160 f = f.ctf_worldflagnext;
1165 float havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1173 FOR_EACH_PLAYER(head)
1175 if(head.team!=bot.team || head.deadflag != DEAD_NO || head == bot)
1178 if(vlen(head.origin - org) < tc_radius)
1185 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1188 head = ctf_worldflaglist;
1191 if (!ctf_IsDifferentTeam(self, head))
1193 head = head.ctf_worldflagnext;
1196 navigation_routerating(head, ratingscale, 10000);
1199 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1202 head = ctf_worldflaglist;
1205 if (!ctf_IsDifferentTeam(self, head))
1207 head = head.ctf_worldflagnext;
1212 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1215 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1218 head = ctf_worldflaglist;
1221 if (ctf_IsDifferentTeam(self, head))
1223 head = head.ctf_worldflagnext;
1226 navigation_routerating(head, ratingscale, 10000);
1229 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1231 if not(bot_waypoints_for_items)
1233 havocbot_goalrating_ctf_enemyflag(ratingscale);
1239 head = havocbot_ctf_find_enemy_flag(self);
1244 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1247 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1251 mf = havocbot_ctf_find_flag(self);
1253 if(mf.ctf_status == FLAG_BASE)
1257 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1260 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1263 head = ctf_worldflaglist;
1266 // flag is out in the field
1267 if(head.ctf_status != FLAG_BASE)
1268 if(head.tag_entity==world) // dropped
1272 if(vlen(org-head.origin)<df_radius)
1273 navigation_routerating(head, ratingscale, 10000);
1276 navigation_routerating(head, ratingscale, 10000);
1279 head = head.ctf_worldflagnext;
1283 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1287 head = findchainfloat(bot_pickup, TRUE);
1290 // gather health and armor only
1292 if (head.health || head.armorvalue)
1293 if (vlen(head.origin - org) < sradius)
1295 // get the value of the item
1296 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1298 navigation_routerating(head, t * ratingscale, 500);
1304 void havocbot_ctf_reset_role(entity bot)
1306 float cdefense, cmiddle, coffense;
1307 entity mf, ef, head;
1310 if(bot.deadflag != DEAD_NO)
1313 if(vlen(havocbot_ctf_middlepoint)==0)
1314 havocbot_calculate_middlepoint();
1317 if (bot.flagcarried)
1319 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1323 mf = havocbot_ctf_find_flag(bot);
1324 ef = havocbot_ctf_find_enemy_flag(bot);
1326 // Retrieve stolen flag
1327 if(mf.ctf_status!=FLAG_BASE)
1329 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1333 // If enemy flag is taken go to the middle to intercept pursuers
1334 if(ef.ctf_status!=FLAG_BASE)
1336 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1340 // if there is only me on the team switch to offense
1342 FOR_EACH_PLAYER(head)
1343 if(head.team==bot.team)
1348 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1352 // Evaluate best position to take
1353 // Count mates on middle position
1354 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1356 // Count mates on defense position
1357 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1359 // Count mates on offense position
1360 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1362 if(cdefense<=coffense)
1363 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1364 else if(coffense<=cmiddle)
1365 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1367 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1370 void havocbot_role_ctf_carrier()
1372 if(self.deadflag != DEAD_NO)
1374 havocbot_ctf_reset_role(self);
1378 if (self.flagcarried == world)
1380 havocbot_ctf_reset_role(self);
1384 if (self.bot_strategytime < time)
1386 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1388 navigation_goalrating_start();
1389 havocbot_goalrating_ctf_ourbase(50000);
1392 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1394 navigation_goalrating_end();
1396 if (self.navigation_hasgoals)
1397 self.havocbot_cantfindflag = time + 10;
1398 else if (time > self.havocbot_cantfindflag)
1400 // Can't navigate to my own base, suicide!
1401 // TODO: drop it and wander around
1402 Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1408 void havocbot_role_ctf_escort()
1412 if(self.deadflag != DEAD_NO)
1414 havocbot_ctf_reset_role(self);
1418 if (self.flagcarried)
1420 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1424 // If enemy flag is back on the base switch to previous role
1425 ef = havocbot_ctf_find_enemy_flag(self);
1426 if(ef.ctf_status==FLAG_BASE)
1428 self.havocbot_role = self.havocbot_previous_role;
1429 self.havocbot_role_timeout = 0;
1433 // If the flag carrier reached the base switch to defense
1434 mf = havocbot_ctf_find_flag(self);
1435 if(mf.ctf_status!=FLAG_BASE)
1436 if(vlen(ef.origin - mf.dropped_origin) < 300)
1438 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1442 // Set the role timeout if necessary
1443 if (!self.havocbot_role_timeout)
1445 self.havocbot_role_timeout = time + random() * 30 + 60;
1448 // If nothing happened just switch to previous role
1449 if (time > self.havocbot_role_timeout)
1451 self.havocbot_role = self.havocbot_previous_role;
1452 self.havocbot_role_timeout = 0;
1456 // Chase the flag carrier
1457 if (self.bot_strategytime < time)
1459 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1460 navigation_goalrating_start();
1461 havocbot_goalrating_ctf_enemyflag(30000);
1462 havocbot_goalrating_ctf_ourstolenflag(40000);
1463 havocbot_goalrating_items(10000, self.origin, 10000);
1464 navigation_goalrating_end();
1468 void havocbot_role_ctf_offense()
1473 if(self.deadflag != DEAD_NO)
1475 havocbot_ctf_reset_role(self);
1479 if (self.flagcarried)
1481 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1486 mf = havocbot_ctf_find_flag(self);
1487 ef = havocbot_ctf_find_enemy_flag(self);
1490 if(mf.ctf_status!=FLAG_BASE)
1493 pos = mf.tag_entity.origin;
1497 // Try to get it if closer than the enemy base
1498 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1500 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1505 // Escort flag carrier
1506 if(ef.ctf_status!=FLAG_BASE)
1509 pos = ef.tag_entity.origin;
1513 if(vlen(pos-mf.dropped_origin)>700)
1515 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1520 // About to fail, switch to middlefield
1523 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1527 // Set the role timeout if necessary
1528 if (!self.havocbot_role_timeout)
1529 self.havocbot_role_timeout = time + 120;
1531 if (time > self.havocbot_role_timeout)
1533 havocbot_ctf_reset_role(self);
1537 if (self.bot_strategytime < time)
1539 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1540 navigation_goalrating_start();
1541 havocbot_goalrating_ctf_ourstolenflag(50000);
1542 havocbot_goalrating_ctf_enemybase(20000);
1543 havocbot_goalrating_items(5000, self.origin, 1000);
1544 havocbot_goalrating_items(1000, self.origin, 10000);
1545 navigation_goalrating_end();
1549 // Retriever (temporary role):
1550 void havocbot_role_ctf_retriever()
1554 if(self.deadflag != DEAD_NO)
1556 havocbot_ctf_reset_role(self);
1560 if (self.flagcarried)
1562 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1566 // If flag is back on the base switch to previous role
1567 mf = havocbot_ctf_find_flag(self);
1568 if(mf.ctf_status==FLAG_BASE)
1570 havocbot_ctf_reset_role(self);
1574 if (!self.havocbot_role_timeout)
1575 self.havocbot_role_timeout = time + 20;
1577 if (time > self.havocbot_role_timeout)
1579 havocbot_ctf_reset_role(self);
1583 if (self.bot_strategytime < time)
1588 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1589 navigation_goalrating_start();
1590 havocbot_goalrating_ctf_ourstolenflag(50000);
1591 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1592 havocbot_goalrating_ctf_enemybase(30000);
1593 havocbot_goalrating_items(500, self.origin, rt_radius);
1594 navigation_goalrating_end();
1598 void havocbot_role_ctf_middle()
1602 if(self.deadflag != DEAD_NO)
1604 havocbot_ctf_reset_role(self);
1608 if (self.flagcarried)
1610 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1614 mf = havocbot_ctf_find_flag(self);
1615 if(mf.ctf_status!=FLAG_BASE)
1617 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1621 if (!self.havocbot_role_timeout)
1622 self.havocbot_role_timeout = time + 10;
1624 if (time > self.havocbot_role_timeout)
1626 havocbot_ctf_reset_role(self);
1630 if (self.bot_strategytime < time)
1634 org = havocbot_ctf_middlepoint;
1635 org_z = self.origin_z;
1637 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1638 navigation_goalrating_start();
1639 havocbot_goalrating_ctf_ourstolenflag(50000);
1640 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1641 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1642 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1643 havocbot_goalrating_items(2500, self.origin, 10000);
1644 havocbot_goalrating_ctf_enemybase(2500);
1645 navigation_goalrating_end();
1649 void havocbot_role_ctf_defense()
1653 if(self.deadflag != DEAD_NO)
1655 havocbot_ctf_reset_role(self);
1659 if (self.flagcarried)
1661 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1665 // If own flag was captured
1666 mf = havocbot_ctf_find_flag(self);
1667 if(mf.ctf_status!=FLAG_BASE)
1669 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1673 if (!self.havocbot_role_timeout)
1674 self.havocbot_role_timeout = time + 30;
1676 if (time > self.havocbot_role_timeout)
1678 havocbot_ctf_reset_role(self);
1681 if (self.bot_strategytime < time)
1686 org = mf.dropped_origin;
1687 mp_radius = havocbot_ctf_middlepoint_radius;
1689 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1690 navigation_goalrating_start();
1692 // if enemies are closer to our base, go there
1693 entity head, closestplayer = world;
1694 float distance, bestdistance = 10000;
1695 FOR_EACH_PLAYER(head)
1697 if(head.deadflag!=DEAD_NO)
1700 distance = vlen(org - head.origin);
1701 if(distance<bestdistance)
1703 closestplayer = head;
1704 bestdistance = distance;
1709 if(closestplayer.team!=self.team)
1710 if(vlen(org - self.origin)>1000)
1711 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1712 havocbot_goalrating_ctf_ourbase(30000);
1714 havocbot_goalrating_ctf_ourstolenflag(20000);
1715 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1716 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1717 havocbot_goalrating_items(10000, org, mp_radius);
1718 havocbot_goalrating_items(5000, self.origin, 10000);
1719 navigation_goalrating_end();
1723 void havocbot_role_ctf_setrole(entity bot, float role)
1725 dprint(strcat(bot.netname," switched to "));
1728 case HAVOCBOT_CTF_ROLE_CARRIER:
1730 bot.havocbot_role = havocbot_role_ctf_carrier;
1731 bot.havocbot_role_timeout = 0;
1732 bot.havocbot_cantfindflag = time + 10;
1733 bot.bot_strategytime = 0;
1735 case HAVOCBOT_CTF_ROLE_DEFENSE:
1737 bot.havocbot_role = havocbot_role_ctf_defense;
1738 bot.havocbot_role_timeout = 0;
1740 case HAVOCBOT_CTF_ROLE_MIDDLE:
1742 bot.havocbot_role = havocbot_role_ctf_middle;
1743 bot.havocbot_role_timeout = 0;
1745 case HAVOCBOT_CTF_ROLE_OFFENSE:
1747 bot.havocbot_role = havocbot_role_ctf_offense;
1748 bot.havocbot_role_timeout = 0;
1750 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1751 dprint("retriever");
1752 bot.havocbot_previous_role = bot.havocbot_role;
1753 bot.havocbot_role = havocbot_role_ctf_retriever;
1754 bot.havocbot_role_timeout = time + 10;
1755 bot.bot_strategytime = 0;
1757 case HAVOCBOT_CTF_ROLE_ESCORT:
1759 bot.havocbot_previous_role = bot.havocbot_role;
1760 bot.havocbot_role = havocbot_role_ctf_escort;
1761 bot.havocbot_role_timeout = time + 30;
1762 bot.bot_strategytime = 0;
1773 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1777 float t = 0, t2 = 0, t3 = 0;
1779 // initially clear items so they can be set as necessary later.
1780 self.items &~= (IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST
1781 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST
1782 | IT_YELLOW_FLAG_CARRYING | IT_YELLOW_FLAG_TAKEN | IT_YELLOW_FLAG_LOST
1783 | IT_PINK_FLAG_CARRYING | IT_PINK_FLAG_TAKEN | IT_PINK_FLAG_LOST
1786 // scan through all the flags and notify the client about them
1787 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1789 if(flag.team == NUM_TEAM_1) { t = IT_RED_FLAG_CARRYING; t2 = IT_RED_FLAG_TAKEN; t3 = IT_RED_FLAG_LOST; }
1790 if(flag.team == NUM_TEAM_2) { t = IT_BLUE_FLAG_CARRYING; t2 = IT_BLUE_FLAG_TAKEN; t3 = IT_BLUE_FLAG_LOST; }
1791 if(flag.team == NUM_TEAM_3) { t = IT_YELLOW_FLAG_CARRYING; t2 = IT_YELLOW_FLAG_TAKEN; t3 = IT_YELLOW_FLAG_LOST; }
1792 if(flag.team == NUM_TEAM_4) { t = IT_PINK_FLAG_CARRYING; t2 = IT_PINK_FLAG_TAKEN; t3 = IT_PINK_FLAG_LOST; }
1794 switch(flag.ctf_status)
1799 if((flag.owner == self) || (flag.pass_sender == self))
1800 self.items |= t; // carrying: self is currently carrying the flag
1802 self.items |= t2; // taken: someone else is carrying the flag
1807 self.items |= t3; // lost: the flag is dropped somewhere on the map
1813 // item for stopping players from capturing the flag too often
1814 if(self.ctf_captureshielded)
1815 self.items |= IT_CTF_SHIELDED;
1817 // update the health of the flag carrier waypointsprite
1818 if(self.wps_flagcarrier)
1819 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent));
1824 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1826 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1828 if(frag_target == frag_attacker) // damage done to yourself
1830 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1831 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1833 else // damage done to everyone else
1835 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1836 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1839 else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && ctf_IsDifferentTeam(frag_target, frag_attacker)) // if the target is a flagcarrier
1841 if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent)))
1842 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1844 frag_target.wps_helpme_time = time;
1845 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1847 // todo: add notification for when flag carrier needs help?
1852 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1854 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
1856 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1857 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1860 if(frag_target.flagcarried)
1861 { ctf_Handle_Throw(frag_target, world, DROP_NORMAL); }
1866 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1869 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1872 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1874 entity flag; // temporary entity for the search method
1876 if(self.flagcarried)
1877 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1879 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1881 if(flag.pass_sender == self) { flag.pass_sender = world; }
1882 if(flag.pass_target == self) { flag.pass_target = world; }
1883 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
1889 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
1891 if(self.flagcarried)
1892 if(!autocvar_g_ctf_portalteleport)
1893 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1898 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
1900 if(MUTATOR_RETURNVALUE || gameover) { return FALSE; }
1902 entity player = self;
1904 if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1906 // pass the flag to a team mate
1907 if(autocvar_g_ctf_pass)
1909 entity head, closest_target = world;
1910 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, TRUE);
1912 while(head) // find the closest acceptable target to pass to
1914 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
1915 if(head != player && !IsDifferentTeam(head, player))
1916 if(!head.speedrunning && !head.vehicle)
1918 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
1919 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
1920 vector passer_center = CENTER_OR_VIEWOFS(player);
1922 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
1924 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
1926 if(IS_BOT_CLIENT(head))
1928 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1929 ctf_Handle_Throw(head, player, DROP_PASS);
1933 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
1934 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1936 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
1939 else if(player.flagcarried)
1943 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
1944 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
1945 { closest_target = head; }
1947 else { closest_target = head; }
1954 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return TRUE; }
1957 // throw the flag in front of you
1958 if(autocvar_g_ctf_throw && player.flagcarried)
1960 if(player.throw_count == -1)
1962 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
1964 player.throw_prevtime = time;
1965 player.throw_count = 1;
1966 ctf_Handle_Throw(player, world, DROP_THROW);
1971 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
1977 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
1978 else { player.throw_count += 1; }
1979 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
1981 player.throw_prevtime = time;
1982 ctf_Handle_Throw(player, world, DROP_THROW);
1991 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
1993 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
1995 self.wps_helpme_time = time;
1996 WaypointSprite_HelpMePing(self.wps_flagcarrier);
1998 else // create a normal help me waypointsprite
2000 WaypointSprite_Spawn("helpme", waypointsprite_deployed_lifetime, waypointsprite_limitedrange, self, FLAG_WAYPOINT_OFFSET, world, self.team, self, wps_helpme, FALSE, RADARICON_HELPME, '1 0.5 0');
2001 WaypointSprite_Ping(self.wps_helpme);
2007 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
2009 if(vh_player.flagcarried)
2011 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2013 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
2017 setattachment(vh_player.flagcarried, vh_vehicle, "");
2018 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
2019 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2020 //vh_player.flagcarried.angles = '0 0 0';
2028 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
2030 if(vh_player.flagcarried)
2032 setattachment(vh_player.flagcarried, vh_player, "");
2033 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2034 vh_player.flagcarried.scale = FLAG_SCALE;
2035 vh_player.flagcarried.angles = '0 0 0';
2042 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
2044 if(self.flagcarried)
2046 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN_));
2047 ctf_RespawnFlag(self.flagcarried);
2054 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
2056 entity flag; // temporary entity for the search method
2058 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2060 switch(flag.ctf_status)
2065 // lock the flag, game is over
2066 flag.movetype = MOVETYPE_NONE;
2067 flag.takedamage = DAMAGE_NO;
2068 flag.solid = SOLID_NOT;
2069 flag.nextthink = FALSE; // stop thinking
2071 //dprint("stopping the ", flag.netname, " from moving.\n");
2079 // do nothing for these flags
2088 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
2090 havocbot_ctf_reset_role(self);
2094 MUTATOR_HOOKFUNCTION(ctf_GetCvars)
2096 GetCvars_handleFloat(get_cvars_s, get_cvars_f, CAPTURE_VERBOSE, "notification_ctf_capture_verbose");
2097 GetCvars_handleFloat(get_cvars_s, get_cvars_f, PICKUP_TEAM_VERBOSE, "notification_ctf_pickup_team_verbose");
2098 GetCvars_handleFloat(get_cvars_s, get_cvars_f, PICKUP_ENEMY_VERBOSE, "notification_ctf_pickup_enemy_verbose");
2102 MUTATOR_HOOKFUNCTION(ctf_GetTeamCount)
2104 ret_float = ctf_teams;
2113 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
2114 CTF Starting point for a player in team one (Red).
2115 Keys: "angle" viewing angle when spawning. */
2116 void spawnfunc_info_player_team1()
2118 if(g_assault) { remove(self); return; }
2120 self.team = NUM_TEAM_1; // red
2121 spawnfunc_info_player_deathmatch();
2125 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
2126 CTF Starting point for a player in team two (Blue).
2127 Keys: "angle" viewing angle when spawning. */
2128 void spawnfunc_info_player_team2()
2130 if(g_assault) { remove(self); return; }
2132 self.team = NUM_TEAM_2; // blue
2133 spawnfunc_info_player_deathmatch();
2136 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
2137 CTF Starting point for a player in team three (Yellow).
2138 Keys: "angle" viewing angle when spawning. */
2139 void spawnfunc_info_player_team3()
2141 if(g_assault) { remove(self); return; }
2143 self.team = NUM_TEAM_3; // yellow
2144 spawnfunc_info_player_deathmatch();
2148 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
2149 CTF Starting point for a player in team four (Purple).
2150 Keys: "angle" viewing angle when spawning. */
2151 void spawnfunc_info_player_team4()
2153 if(g_assault) { remove(self); return; }
2155 self.team = NUM_TEAM_4; // purple
2156 spawnfunc_info_player_deathmatch();
2159 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2160 CTF flag for team one (Red).
2162 "angle" Angle the flag will point (minus 90 degrees)...
2163 "model" model to use, note this needs red and blue as skins 0 and 1...
2164 "noise" sound played when flag is picked up...
2165 "noise1" sound played when flag is returned by a teammate...
2166 "noise2" sound played when flag is captured...
2167 "noise3" sound played when flag is lost in the field and respawns itself...
2168 "noise4" sound played when flag is dropped by a player...
2169 "noise5" sound played when flag touches the ground... */
2170 void spawnfunc_item_flag_team1()
2172 if(!g_ctf) { remove(self); return; }
2174 ctf_FlagSetup(NUM_TEAM_1, self);
2177 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2178 CTF flag for team two (Blue).
2180 "angle" Angle the flag will point (minus 90 degrees)...
2181 "model" model to use, note this needs red and blue as skins 0 and 1...
2182 "noise" sound played when flag is picked up...
2183 "noise1" sound played when flag is returned by a teammate...
2184 "noise2" sound played when flag is captured...
2185 "noise3" sound played when flag is lost in the field and respawns itself...
2186 "noise4" sound played when flag is dropped by a player...
2187 "noise5" sound played when flag touches the ground... */
2188 void spawnfunc_item_flag_team2()
2190 if(!g_ctf) { remove(self); return; }
2192 ctf_FlagSetup(NUM_TEAM_2, self);
2195 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2196 CTF flag for team three (Yellow).
2198 "angle" Angle the flag will point (minus 90 degrees)...
2199 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2200 "noise" sound played when flag is picked up...
2201 "noise1" sound played when flag is returned by a teammate...
2202 "noise2" sound played when flag is captured...
2203 "noise3" sound played when flag is lost in the field and respawns itself...
2204 "noise4" sound played when flag is dropped by a player...
2205 "noise5" sound played when flag touches the ground... */
2206 void spawnfunc_item_flag_team3()
2208 if(!g_ctf) { remove(self); return; }
2210 ctf_FlagSetup(NUM_TEAM_3, self);
2213 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2214 CTF flag for team two (Pink).
2216 "angle" Angle the flag will point (minus 90 degrees)...
2217 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2218 "noise" sound played when flag is picked up...
2219 "noise1" sound played when flag is returned by a teammate...
2220 "noise2" sound played when flag is captured...
2221 "noise3" sound played when flag is lost in the field and respawns itself...
2222 "noise4" sound played when flag is dropped by a player...
2223 "noise5" sound played when flag touches the ground... */
2224 void spawnfunc_item_flag_team4()
2226 if(!g_ctf) { remove(self); return; }
2228 ctf_FlagSetup(NUM_TEAM_4, self);
2231 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2232 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2233 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.
2235 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2236 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2237 void spawnfunc_ctf_team()
2239 if(!g_ctf) { remove(self); return; }
2241 self.classname = "ctf_team";
2242 self.team = self.cnt + 1;
2245 // compatibility for quake maps
2246 void spawnfunc_team_CTF_redflag() { spawnfunc_item_flag_team1(); }
2247 void spawnfunc_team_CTF_blueflag() { spawnfunc_item_flag_team2(); }
2248 void spawnfunc_team_CTF_redplayer() { spawnfunc_info_player_team1(); }
2249 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2(); }
2250 void spawnfunc_team_CTF_redspawn() { spawnfunc_info_player_team1(); }
2251 void spawnfunc_team_CTF_bluespawn() { spawnfunc_info_player_team2(); }
2259 void ctf_ScoreRules(float teams)
2261 CheckAllowedTeams(world);
2262 ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, TRUE);
2263 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2264 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2265 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2266 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2267 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2268 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2269 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2270 ScoreRules_basics_end();
2273 // code from here on is just to support maps that don't have flag and team entities
2274 void ctf_SpawnTeam (string teamname, float teamcolor)
2279 self.classname = "ctf_team";
2280 self.netname = teamname;
2281 self.cnt = teamcolor;
2283 spawnfunc_ctf_team();
2288 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2293 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2294 if(tmp_entity.team == NUM_TEAM_3)
2297 break; // found 1 flag for this team
2299 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2300 if(tmp_entity.team == NUM_TEAM_4)
2303 break; // found 1 flag for this team
2306 ctf_teams = bound(2, ctf_teams, 4);
2308 // if no teams are found, spawn defaults
2309 if(find(world, classname, "ctf_team") == world)
2311 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2312 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2313 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2315 ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2317 ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2320 ret_float = ctf_teams;
2321 ctf_ScoreRules(ctf_teams);
2324 void ctf_Initialize()
2326 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2328 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2329 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2330 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2332 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2336 MUTATOR_DEFINITION(gamemode_ctf)
2338 MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2339 MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2340 MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2341 MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2342 MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2343 MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2344 MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2345 MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2346 MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2347 MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2348 MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2349 MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2350 MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2351 MUTATOR_HOOK(HavocBot_ChooseRule, ctf_BotRoles, CBC_ORDER_ANY);
2352 MUTATOR_HOOK(GetCvars, ctf_GetCvars, CBC_ORDER_ANY);
2353 MUTATOR_HOOK(GetTeamCount, ctf_GetTeamCount, CBC_ORDER_ANY);
2357 if(time > 1) // game loads at time 1
2358 error("This is a game type and it cannot be added at runtime.");
2362 MUTATOR_ONROLLBACK_OR_REMOVE
2364 // we actually cannot roll back ctf_Initialize here
2365 // BUT: we don't need to! If this gets called, adding always
2371 print("This is a game type and it cannot be removed at runtime.");