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 notification, success;
27 float cap_record = ctf_captimerecord;
28 float cap_time = (time - flag.ctf_pickuptime);
29 float f1, f2 = NO_FL_ARG;
30 string s1, s2 = NO_STR_ARG;
31 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
34 if(!ctf_captimerecord)
35 { notification = APP_TEAM_ENT_2(flag, INFO_CTF_CAPTURE_TIME_); s1 = player.netname; f1 = (cap_time * 100); success = TRUE; }
36 else if(cap_time < cap_record)
37 { notification = APP_TEAM_ENT_2(flag, INFO_CTF_CAPTURE_BROKEN_); s1 = player.netname; s2 = refername; f1 = (cap_time * 100); f2 = (cap_record * 100); success = TRUE; }
39 { notification = APP_TEAM_ENT_2(flag, INFO_CTF_CAPTURE_UNBROKEN_); s1 = player.netname; s2 = refername; f1 = (cap_time * 100); f2 = (cap_record * 100); success = FALSE; }
42 FOR_EACH_REALCLIENT(tmp_entity)
44 if not(tmp_entity.CAPTURE_VERBOSE) { notification = APP_TEAM_ENT_2(flag, INFO_CTF_CAPTURE_); s2 = NO_STR_ARG; f1 = f2 = NO_FL_ARG; }
45 Send_Notification_Legacy_Wrapper(NOTIF_ONE, tmp_entity, MSG_INFO, notification, s1, s2, f1, f2, NO_FL_ARG);
48 // write that shit in the database
51 ctf_captimerecord = cap_time;
52 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
53 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
54 write_recordmarker(player, (time - cap_time), cap_time);
58 void ctf_FlagcarrierWaypoints(entity player)
60 WaypointSprite_Spawn("flagcarrier", 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, TRUE, RADARICON_FLAG, WPCOLOR_FLAGCARRIER(player.team));
61 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
62 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
63 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
66 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
68 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
69 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
70 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
71 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
74 if(current_height) // make sure we can actually do this arcing path
76 targpos = (to + ('0 0 1' * current_height));
77 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
78 if(trace_fraction < 1)
80 //print("normal arc line failed, trying to find new pos...");
81 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
82 targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
83 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
84 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
85 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
88 else { targpos = to; }
90 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
92 vector desired_direction = normalize(targpos - from);
93 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
94 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
97 float ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
99 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
101 // directional tracing only
103 makevectors(passer_angle);
105 // find the closest point on the enemy to the center of the attack
106 float ang; // angle between shotdir and h
107 float h; // hypotenuse, which is the distance between attacker to head
108 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
110 h = vlen(head_center - passer_center);
111 ang = acos(dotproduct(normalize(head_center - passer_center), v_forward));
114 vector nearest_on_line = (passer_center + a * v_forward);
115 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
117 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
118 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
120 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
125 else { return TRUE; }
129 // =======================
130 // CaptureShield Functions
131 // =======================
133 float ctf_CaptureShield_CheckStatus(entity p)
137 float players_worseeq, players_total;
139 if(ctf_captureshield_max_ratio <= 0)
142 s = PlayerScore_Add(p, SP_SCORE, 0);
143 if(s >= -ctf_captureshield_min_negscore)
146 players_total = players_worseeq = 0;
149 if(IsDifferentTeam(e, p))
151 se = PlayerScore_Add(e, SP_SCORE, 0);
157 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
158 // use this rule here
160 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
166 void ctf_CaptureShield_Update(entity player, float wanted_status)
168 float updated_status = ctf_CaptureShield_CheckStatus(player);
169 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
171 if(updated_status) // TODO csqc notifier for this // Samual: How?
172 Send_Notification_Legacy_Wrapper(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED, NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG);
174 Send_Notification_Legacy_Wrapper(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_FREE, NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG);
176 player.ctf_captureshielded = updated_status;
180 float ctf_CaptureShield_Customize()
182 if(!other.ctf_captureshielded) { return FALSE; }
183 if(!IsDifferentTeam(self, other)) { return FALSE; }
188 void ctf_CaptureShield_Touch()
190 if(!other.ctf_captureshielded) { return; }
191 if(!IsDifferentTeam(self, other)) { return; }
193 vector mymid = (self.absmin + self.absmax) * 0.5;
194 vector othermid = (other.absmin + other.absmax) * 0.5;
196 Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
197 Send_Notification_Legacy_Wrapper(NOTIF_ONE, other, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED, NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG);
200 void ctf_CaptureShield_Spawn(entity flag)
202 entity shield = spawn();
205 shield.team = self.team;
206 shield.touch = ctf_CaptureShield_Touch;
207 shield.customizeentityforclient = ctf_CaptureShield_Customize;
208 shield.classname = "ctf_captureshield";
209 shield.effects = EF_ADDITIVE;
210 shield.movetype = MOVETYPE_NOCLIP;
211 shield.solid = SOLID_TRIGGER;
212 shield.avelocity = '7 0 11';
215 setorigin(shield, self.origin);
216 setmodel(shield, "models/ctf/shield.md3");
217 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
221 // ====================
222 // Drop/Pass/Throw Code
223 // ====================
225 void ctf_Handle_Drop(entity flag, entity player, float droptype)
228 player = (player ? player : flag.pass_sender);
231 flag.movetype = MOVETYPE_TOSS;
232 flag.takedamage = DAMAGE_YES;
233 flag.angles = '0 0 0';
234 flag.health = flag.max_flag_health;
235 flag.ctf_droptime = time;
236 flag.ctf_dropper = player;
237 flag.ctf_status = FLAG_DROPPED;
239 // messages and sounds
240 Send_Notification_Legacy_Wrapper(NOTIF_ANY, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_LOST_), player.netname, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG);
241 sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTN_NONE);
242 ctf_EventLog("dropped", player.team, player);
245 PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);
246 PlayerScore_Add(player, SP_CTF_DROPS, 1);
249 if(autocvar_g_ctf_flag_dropped_waypoint)
250 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));
252 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
254 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
255 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
258 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
260 if(droptype == DROP_PASS)
262 flag.pass_distance = 0;
263 flag.pass_sender = world;
264 flag.pass_target = world;
268 void ctf_Handle_Retrieve(entity flag, entity player)
270 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
271 entity sender = flag.pass_sender;
273 // transfer flag to player
275 flag.owner.flagcarried = flag;
278 setattachment(flag, player, "");
279 setorigin(flag, FLAG_CARRY_OFFSET);
280 flag.movetype = MOVETYPE_NONE;
281 flag.takedamage = DAMAGE_NO;
282 flag.solid = SOLID_NOT;
283 flag.angles = '0 0 0';
284 flag.ctf_status = FLAG_CARRY;
286 // messages and sounds
287 sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTN_NORM);
288 ctf_EventLog("receive", flag.team, player);
290 FOR_EACH_REALPLAYER(tmp_player)
292 if(tmp_player == sender)
293 Send_Notification_Legacy_Wrapper(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PASS_SENT_), player.netname, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG);
294 else if(tmp_player == player)
295 Send_Notification_Legacy_Wrapper(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PASS_RECEIVED_), sender.netname, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG);
296 else if(!IsDifferentTeam(tmp_player, sender))
297 Send_Notification_Legacy_Wrapper(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PASS_OTHER_), sender.netname, player.netname, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG);
300 // create new waypoint
301 ctf_FlagcarrierWaypoints(player);
303 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
304 player.throw_antispam = sender.throw_antispam;
306 flag.pass_distance = 0;
307 flag.pass_sender = world;
308 flag.pass_target = world;
311 void ctf_Handle_Throw(entity player, entity receiver, float droptype)
313 entity flag = player.flagcarried;
314 vector targ_origin, flag_velocity;
316 if(!flag) { return; }
317 if((droptype == DROP_PASS) && !receiver) { return; }
319 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
322 setattachment(flag, world, "");
323 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
324 flag.owner.flagcarried = world;
326 flag.solid = SOLID_TRIGGER;
327 flag.ctf_dropper = player;
328 flag.ctf_droptime = time;
330 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
337 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
338 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
339 WarpZone_RefSys_Copy(flag, receiver);
340 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
341 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
343 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
344 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, FALSE);
347 flag.movetype = MOVETYPE_FLY;
348 flag.takedamage = DAMAGE_NO;
349 flag.pass_sender = player;
350 flag.pass_target = receiver;
351 flag.ctf_status = FLAG_PASSING;
354 sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTN_NORM);
355 WarpZone_TrailParticles(world, particleeffectnum(flag.passeffect), player.origin, targ_origin);
356 ctf_EventLog("pass", flag.team, player);
362 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'));
364 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)));
365 flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, FALSE);
366 ctf_Handle_Drop(flag, player, droptype);
372 flag.velocity = '0 0 0'; // do nothing
379 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);
380 ctf_Handle_Drop(flag, player, droptype);
385 // kill old waypointsprite
386 WaypointSprite_Ping(player.wps_flagcarrier);
387 WaypointSprite_Kill(player.wps_flagcarrier);
389 if(player.wps_enemyflagcarrier)
390 WaypointSprite_Kill(player.wps_enemyflagcarrier);
393 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
401 void ctf_Handle_Capture(entity flag, entity toucher, float capturetype)
403 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
404 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
405 float old_time, new_time;
407 if not(player) { return; } // without someone to give the reward to, we can't possibly cap
409 // messages and sounds
410 ctf_CaptureRecord(enemy_flag, player);
411 sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE);
415 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
416 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
421 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
422 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
424 old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
425 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
426 if(!old_time || new_time < old_time)
427 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
430 pointparticles(particleeffectnum(flag.capeffect), flag.origin, '0 0 0', 1);
431 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
434 if(capturetype == CAPTURE_NORMAL)
436 WaypointSprite_Kill(player.wps_flagcarrier);
437 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
439 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
440 { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
444 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
445 ctf_RespawnFlag(enemy_flag);
448 void ctf_Handle_Return(entity flag, entity player)
450 // messages and sounds
451 Send_Notification_Legacy_Wrapper(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_RETURN_), NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG);
452 Send_Notification_Legacy_Wrapper(NOTIF_ANY, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_RETURN_), player.netname, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG);
453 sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTN_NONE);
454 ctf_EventLog("return", flag.team, player);
457 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
458 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
460 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
464 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
465 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
466 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
470 ctf_RespawnFlag(flag);
473 void ctf_Handle_Pickup(entity flag, entity player, float pickuptype)
476 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
477 float pickup_dropped_score; // used to calculate dropped pickup score
479 // attach the flag to the player
481 player.flagcarried = flag;
482 setattachment(flag, player, "");
483 setorigin(flag, FLAG_CARRY_OFFSET);
486 flag.movetype = MOVETYPE_NONE;
487 flag.takedamage = DAMAGE_NO;
488 flag.solid = SOLID_NOT;
489 flag.angles = '0 0 0';
490 flag.ctf_status = FLAG_CARRY;
494 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
495 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
499 // messages and sounds
500 Send_Notification_Legacy_Wrapper(NOTIF_ANY, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_PICKUP_), player.netname, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG);
501 sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE);
503 FOR_EACH_REALPLAYER(tmp_player)
505 if(tmp_player == player)
507 Send_Notification_Legacy_Wrapper(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PICKUP_), NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG);
508 if(ctf_stalemate) { Send_Notification_Legacy_Wrapper(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER, NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); }
510 else if(!IsDifferentTeam(tmp_player, player) && tmp_player != player)
511 Send_Notification_Legacy_Wrapper(NOTIF_ONE, tmp_player, MSG_CENTER, (tmp_player.PICKUP_TEAM_VERBOSE ? CENTER_CTF_PICKUP_TEAM_VERBOSE : CENTER_CTF_PICKUP_TEAM), Team_ColorCode(player.team), (tmp_player.PICKUP_TEAM_VERBOSE ? player.netname : NO_STR_ARG), NO_FL_ARG, NO_FL_ARG, NO_FL_ARG);
512 else if(IsDifferentTeam(tmp_player, player))
513 Send_Notification_Legacy_Wrapper(NOTIF_ONE, tmp_player, MSG_CENTER, (tmp_player.PICKUP_ENEMY_VERBOSE ? CENTER_CTF_PICKUP_ENEMY_VERBOSE : CENTER_CTF_PICKUP_ENEMY), Team_ColorCode(player.team), (tmp_player.PICKUP_ENEMY_VERBOSE ? player.netname : NO_STR_ARG), NO_FL_ARG, NO_FL_ARG, NO_FL_ARG);
517 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
522 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
523 ctf_EventLog("steal", flag.team, player);
529 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);
530 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);
531 dprint("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
532 PlayerTeamScore_AddScore(player, pickup_dropped_score);
533 ctf_EventLog("pickup", flag.team, player);
541 if(pickuptype == PICKUP_BASE)
543 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
544 if((player.speedrunning) && (ctf_captimerecord))
545 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
549 pointparticles(particleeffectnum(flag.toucheffect), player.origin, '0 0 0', 1);
552 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
553 ctf_FlagcarrierWaypoints(player);
554 WaypointSprite_Ping(player.wps_flagcarrier);
558 // ===================
559 // Main Flag Functions
560 // ===================
562 void ctf_CheckFlagReturn(entity flag, float returntype)
564 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
566 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
568 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
572 case RETURN_DROPPED: Send_Notification_Legacy_Wrapper(NOTIF_ANY, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_DROPPED_), NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); break;
573 case RETURN_DAMAGE: Send_Notification_Legacy_Wrapper(NOTIF_ANY, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_DAMAGED_), NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); break;
574 case RETURN_SPEEDRUN: Send_Notification_Legacy_Wrapper(NOTIF_ANY, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_SPEEDRUN_), NO_STR_ARG, NO_STR_ARG, ctf_captimerecord, NO_FL_ARG, NO_FL_ARG); break;
575 case RETURN_NEEDKILL: Send_Notification_Legacy_Wrapper(NOTIF_ANY, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_NEEDKILL_), NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); break;
579 { Send_Notification_Legacy_Wrapper(NOTIF_ANY, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_TIMEOUT_), NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); break; }
581 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTN_NONE);
582 ctf_EventLog("returned", flag.team, world);
583 ctf_RespawnFlag(flag);
588 void ctf_CheckStalemate(void)
591 float stale_red_flags = 0, stale_blue_flags = 0;
594 entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
596 // build list of stale flags
597 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
599 if(autocvar_g_ctf_stalemate)
600 if(tmp_entity.ctf_status != FLAG_BASE)
601 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time)
603 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
604 ctf_staleflaglist = tmp_entity;
606 switch(tmp_entity.team)
608 case FL_TEAM_1: ++stale_red_flags; break;
609 case FL_TEAM_2: ++stale_blue_flags; break;
614 if(stale_red_flags && stale_blue_flags)
615 ctf_stalemate = TRUE;
616 else if((!stale_red_flags && !stale_blue_flags) && autocvar_g_ctf_stalemate_endcondition == 2)
617 { ctf_stalemate = FALSE; wpforenemy_announced = FALSE; }
618 else if((!stale_red_flags || !stale_blue_flags) && autocvar_g_ctf_stalemate_endcondition == 1)
619 { ctf_stalemate = FALSE; wpforenemy_announced = FALSE; }
621 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
624 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
626 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
627 WaypointSprite_Spawn("enemyflagcarrier", 0, 0, tmp_entity.owner, FLAG_WAYPOINT_OFFSET, world, tmp_entity.team, tmp_entity.owner, wps_enemyflagcarrier, TRUE, RADARICON_FLAG, WPCOLOR_ENEMYFC(tmp_entity.owner.team));
630 if not(wpforenemy_announced)
632 FOR_EACH_REALPLAYER(tmp_entity)
633 if(tmp_entity.flagcarried)
634 Send_Notification_Legacy_Wrapper(NOTIF_ONE, tmp_entity, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER, NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG);
636 Send_Notification_Legacy_Wrapper(NOTIF_ONE, tmp_entity, MSG_CENTER, CENTER_CTF_STALEMATE_OTHER, NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG);
638 wpforenemy_announced = TRUE;
643 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
645 if(ITEM_DAMAGE_NEEDKILL(deathtype))
647 // automatically kill the flag and return it
649 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
652 if(autocvar_g_ctf_flag_return_damage)
654 // reduce health and check if it should be returned
655 self.health = self.health - damage;
656 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
666 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
669 if(self == ctf_worldflaglist) // only for the first flag
670 FOR_EACH_CLIENT(tmp_entity)
671 ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
674 if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
675 dprint("wtf the flag got squashed?\n");
676 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
677 if(!trace_startsolid) // can we resize it without getting stuck?
678 setsize(self, FLAG_MIN, FLAG_MAX); }
680 switch(self.ctf_status) // reset flag angles in case warpzones adjust it
684 self.angles = '0 0 0';
692 switch(self.ctf_status)
696 if(autocvar_g_ctf_dropped_capture_radius)
698 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
699 if(tmp_entity.ctf_status == FLAG_DROPPED)
700 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
701 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
702 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
709 if(autocvar_g_ctf_flag_dropped_floatinwater)
711 vector midpoint = ((self.absmin + self.absmax) * 0.5);
712 if(pointcontents(midpoint) == CONTENT_WATER)
714 self.velocity = self.velocity * 0.5;
716 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
717 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
719 { self.movetype = MOVETYPE_FLY; }
721 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
723 if(autocvar_g_ctf_flag_return_dropped)
725 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
728 ctf_CheckFlagReturn(self, RETURN_DROPPED);
732 if(autocvar_g_ctf_flag_return_time)
734 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
735 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
743 if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
746 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
750 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
754 if(autocvar_g_ctf_stalemate)
756 if(time >= wpforenemy_nextthink)
758 ctf_CheckStalemate();
759 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
767 vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
768 targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
769 WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
771 if((self.pass_target == world)
772 || (self.pass_target.deadflag != DEAD_NO)
773 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
774 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
775 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
777 // give up, pass failed
778 ctf_Handle_Drop(self, world, DROP_PASS);
782 // still a viable target, go for it
783 ctf_CalculatePassVelocity(self, targ_origin, self.origin, TRUE);
788 default: // this should never happen
790 dprint("ctf_FlagThink(): Flag exists with no status?\n");
798 if(gameover) { return; }
800 entity toucher = other;
802 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
803 if(ITEM_TOUCH_NEEDKILL())
806 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
810 // special touch behaviors
811 if(toucher.vehicle_flags & VHF_ISVEHICLE)
813 if(autocvar_g_ctf_allow_vehicle_touch)
814 toucher = toucher.owner; // the player is actually the vehicle owner, not other
816 return; // do nothing
818 else if(toucher.classname != "player") // The flag just touched an object, most likely the world
820 if(time > self.wait) // if we haven't in a while, play a sound/effect
822 pointparticles(particleeffectnum(self.toucheffect), self.origin, '0 0 0', 1);
823 sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTN_NORM);
824 self.wait = time + FLAG_TOUCHRATE;
828 else if(toucher.deadflag != DEAD_NO) { return; }
830 switch(self.ctf_status)
834 if(!IsDifferentTeam(toucher, self) && (toucher.flagcarried) && IsDifferentTeam(toucher.flagcarried, self))
835 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
836 else if(IsDifferentTeam(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time))
837 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
843 if(!IsDifferentTeam(toucher, self))
844 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
845 else if((!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
846 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
852 dprint("Someone touched a flag even though it was being carried?\n");
858 if((toucher.classname == "player") && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
860 if(IsDifferentTeam(toucher, self.pass_sender))
861 ctf_Handle_Return(self, toucher);
863 ctf_Handle_Retrieve(self, toucher);
871 void ctf_RespawnFlag(entity flag)
873 // check for flag respawn being called twice in a row
874 if(flag.last_respawn > time - 0.5)
875 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
877 flag.last_respawn = time;
879 // reset the player (if there is one)
880 if((flag.owner) && (flag.owner.flagcarried == flag))
882 if(flag.owner.wps_enemyflagcarrier)
883 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
885 WaypointSprite_Kill(flag.wps_flagcarrier);
887 flag.owner.flagcarried = world;
889 if(flag.speedrunning)
890 ctf_FakeTimeLimit(flag.owner, -1);
893 if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
894 { WaypointSprite_Kill(flag.wps_flagdropped); }
897 setattachment(flag, world, "");
898 setorigin(flag, flag.ctf_spawnorigin);
900 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
901 flag.takedamage = DAMAGE_NO;
902 flag.health = flag.max_flag_health;
903 flag.solid = SOLID_TRIGGER;
904 flag.velocity = '0 0 0';
905 flag.angles = flag.mangle;
906 flag.flags = FL_ITEM | FL_NOTARGET;
908 flag.ctf_status = FLAG_BASE;
910 flag.pass_distance = 0;
911 flag.pass_sender = world;
912 flag.pass_target = world;
913 flag.ctf_dropper = world;
914 flag.ctf_pickuptime = 0;
915 flag.ctf_droptime = 0;
921 if(self.owner.classname == "player")
922 ctf_Handle_Throw(self.owner, world, DROP_RESET);
924 ctf_RespawnFlag(self);
927 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
930 waypoint_spawnforitem_force(self, self.origin);
931 self.nearestwaypointtimeout = 0; // activate waypointing again
932 self.bot_basewaypoint = self.nearestwaypoint;
935 WaypointSprite_SpawnFixed(((self.team == FL_TEAM_1) ? "redbase" : "bluebase"), self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
936 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
938 // captureshield setup
939 ctf_CaptureShield_Spawn(self);
942 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
945 teamnumber = fabs(teamnumber - bound(0, autocvar_g_ctf_reverse, 1)); // if we were originally 1, this will become 0. If we were originally 0, this will become 1.
946 self = flag; // for later usage with droptofloor()
949 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
950 ctf_worldflaglist = flag;
952 setattachment(flag, world, "");
954 flag.netname = ((teamnumber) ? "^1REPLACETHIS^7" : "^4REPLACETHIS^7"); // ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag");
955 flag.team = ((teamnumber) ? FL_TEAM_1 : FL_TEAM_2); // FL_TEAM_1: color 4 team (red) - FL_TEAM_2: color 13 team (blue)
956 flag.items = ((teamnumber) ? IT_KEY2 : IT_KEY1); // IT_KEY2: gold key (redish enough) - IT_KEY1: silver key (bluish enough)
957 flag.classname = "item_flag_team";
958 flag.target = "###item###"; // wut?
959 flag.flags = FL_ITEM | FL_NOTARGET;
960 flag.solid = SOLID_TRIGGER;
961 flag.takedamage = DAMAGE_NO;
962 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
963 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
964 flag.health = flag.max_flag_health;
965 flag.event_damage = ctf_FlagDamage;
966 flag.pushable = TRUE;
967 flag.teleportable = TELEPORT_NORMAL;
968 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
969 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
970 flag.velocity = '0 0 0';
971 flag.mangle = flag.angles;
972 flag.reset = ctf_Reset;
973 flag.touch = ctf_FlagTouch;
974 flag.think = ctf_FlagThink;
975 flag.nextthink = time + FLAG_THINKRATE;
976 flag.ctf_status = FLAG_BASE;
978 if(!flag.model) { flag.model = ((teamnumber) ? autocvar_g_ctf_flag_red_model : autocvar_g_ctf_flag_blue_model); }
979 if(!flag.scale) { flag.scale = FLAG_SCALE; }
980 if(!flag.skin) { flag.skin = ((teamnumber) ? autocvar_g_ctf_flag_red_skin : autocvar_g_ctf_flag_blue_skin); }
981 if(!flag.toucheffect) { flag.toucheffect = ((teamnumber) ? "redflag_touch" : "blueflag_touch"); }
982 if(!flag.passeffect) { flag.passeffect = ((teamnumber) ? "red_pass" : "blue_pass"); }
983 if(!flag.capeffect) { flag.capeffect = ((teamnumber) ? "red_cap" : "blue_cap"); }
986 if(!flag.snd_flag_taken) { flag.snd_flag_taken = ((teamnumber) ? "ctf/red_taken.wav" : "ctf/blue_taken.wav"); }
987 if(!flag.snd_flag_returned) { flag.snd_flag_returned = ((teamnumber) ? "ctf/red_returned.wav" : "ctf/blue_returned.wav"); }
988 if(!flag.snd_flag_capture) { flag.snd_flag_capture = ((teamnumber) ? "ctf/red_capture.wav" : "ctf/blue_capture.wav"); } // blue team scores by capturing the red flag
989 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.
990 if(!flag.snd_flag_dropped) { flag.snd_flag_dropped = ((teamnumber) ? "ctf/red_dropped.wav" : "ctf/blue_dropped.wav"); }
991 if(!flag.snd_flag_touch) { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
992 if(!flag.snd_flag_pass) { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
995 precache_sound(flag.snd_flag_taken);
996 precache_sound(flag.snd_flag_returned);
997 precache_sound(flag.snd_flag_capture);
998 precache_sound(flag.snd_flag_respawn);
999 precache_sound(flag.snd_flag_dropped);
1000 precache_sound(flag.snd_flag_touch);
1001 precache_sound(flag.snd_flag_pass);
1002 precache_model(flag.model);
1003 precache_model("models/ctf/shield.md3");
1004 precache_model("models/ctf/shockwavetransring.md3");
1007 setmodel(flag, flag.model); // precision set below
1008 setsize(flag, FLAG_MIN, FLAG_MAX);
1009 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1011 if(autocvar_g_ctf_flag_glowtrails)
1013 flag.glow_color = ((teamnumber) ? 251 : 210); // 251: red - 210: blue
1014 flag.glow_size = 25;
1015 flag.glow_trail = 1;
1018 flag.effects |= EF_LOWPRECISION;
1019 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1020 if(autocvar_g_ctf_dynamiclights) { flag.effects |= ((teamnumber) ? EF_RED : EF_BLUE); }
1023 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1025 flag.dropped_origin = flag.origin;
1026 flag.noalign = TRUE;
1027 flag.movetype = MOVETYPE_NONE;
1029 else // drop to floor, automatically find a platform and set that as spawn origin
1031 flag.noalign = FALSE;
1034 flag.movetype = MOVETYPE_TOSS;
1037 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1045 // NOTE: LEGACY CODE, needs to be re-written!
1047 void havocbot_calculate_middlepoint()
1051 vector fo = '0 0 0';
1054 f = ctf_worldflaglist;
1059 f = f.ctf_worldflagnext;
1063 havocbot_ctf_middlepoint = s * (1.0 / n);
1064 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1068 entity havocbot_ctf_find_flag(entity bot)
1071 f = ctf_worldflaglist;
1074 if (bot.team == f.team)
1076 f = f.ctf_worldflagnext;
1081 entity havocbot_ctf_find_enemy_flag(entity bot)
1084 f = ctf_worldflaglist;
1087 if (bot.team != f.team)
1089 f = f.ctf_worldflagnext;
1094 float havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1102 FOR_EACH_PLAYER(head)
1104 if(head.team!=bot.team || head.deadflag != DEAD_NO || head == bot)
1107 if(vlen(head.origin - org) < tc_radius)
1114 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1117 head = ctf_worldflaglist;
1120 if (self.team == head.team)
1122 head = head.ctf_worldflagnext;
1125 navigation_routerating(head, ratingscale, 10000);
1128 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1131 head = ctf_worldflaglist;
1134 if (self.team == head.team)
1136 head = head.ctf_worldflagnext;
1141 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1144 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1147 head = ctf_worldflaglist;
1150 if (self.team != head.team)
1152 head = head.ctf_worldflagnext;
1155 navigation_routerating(head, ratingscale, 10000);
1158 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1160 if not(bot_waypoints_for_items)
1162 havocbot_goalrating_ctf_enemyflag(ratingscale);
1168 head = havocbot_ctf_find_enemy_flag(self);
1173 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1176 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1180 mf = havocbot_ctf_find_flag(self);
1182 if(mf.ctf_status == FLAG_BASE)
1186 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1189 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1192 head = ctf_worldflaglist;
1195 // flag is out in the field
1196 if(head.ctf_status != FLAG_BASE)
1197 if(head.tag_entity==world) // dropped
1201 if(vlen(org-head.origin)<df_radius)
1202 navigation_routerating(head, ratingscale, 10000);
1205 navigation_routerating(head, ratingscale, 10000);
1208 head = head.ctf_worldflagnext;
1212 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1216 head = findchainfloat(bot_pickup, TRUE);
1219 // gather health and armor only
1221 if (head.health || head.armorvalue)
1222 if (vlen(head.origin - org) < sradius)
1224 // get the value of the item
1225 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1227 navigation_routerating(head, t * ratingscale, 500);
1233 void havocbot_ctf_reset_role(entity bot)
1235 float cdefense, cmiddle, coffense;
1236 entity mf, ef, head;
1239 if(bot.deadflag != DEAD_NO)
1242 if(vlen(havocbot_ctf_middlepoint)==0)
1243 havocbot_calculate_middlepoint();
1246 if (bot.flagcarried)
1248 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1252 mf = havocbot_ctf_find_flag(bot);
1253 ef = havocbot_ctf_find_enemy_flag(bot);
1255 // Retrieve stolen flag
1256 if(mf.ctf_status!=FLAG_BASE)
1258 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1262 // If enemy flag is taken go to the middle to intercept pursuers
1263 if(ef.ctf_status!=FLAG_BASE)
1265 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1269 // if there is only me on the team switch to offense
1271 FOR_EACH_PLAYER(head)
1272 if(head.team==bot.team)
1277 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1281 // Evaluate best position to take
1282 // Count mates on middle position
1283 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1285 // Count mates on defense position
1286 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1288 // Count mates on offense position
1289 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1291 if(cdefense<=coffense)
1292 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1293 else if(coffense<=cmiddle)
1294 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1296 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1299 void havocbot_role_ctf_carrier()
1301 if(self.deadflag != DEAD_NO)
1303 havocbot_ctf_reset_role(self);
1307 if (self.flagcarried == world)
1309 havocbot_ctf_reset_role(self);
1313 if (self.bot_strategytime < time)
1315 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1317 navigation_goalrating_start();
1318 havocbot_goalrating_ctf_ourbase(50000);
1321 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1323 navigation_goalrating_end();
1325 if (self.navigation_hasgoals)
1326 self.havocbot_cantfindflag = time + 10;
1327 else if (time > self.havocbot_cantfindflag)
1329 // Can't navigate to my own base, suicide!
1330 // TODO: drop it and wander around
1331 Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1337 void havocbot_role_ctf_escort()
1341 if(self.deadflag != DEAD_NO)
1343 havocbot_ctf_reset_role(self);
1347 if (self.flagcarried)
1349 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1353 // If enemy flag is back on the base switch to previous role
1354 ef = havocbot_ctf_find_enemy_flag(self);
1355 if(ef.ctf_status==FLAG_BASE)
1357 self.havocbot_role = self.havocbot_previous_role;
1358 self.havocbot_role_timeout = 0;
1362 // If the flag carrier reached the base switch to defense
1363 mf = havocbot_ctf_find_flag(self);
1364 if(mf.ctf_status!=FLAG_BASE)
1365 if(vlen(ef.origin - mf.dropped_origin) < 300)
1367 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1371 // Set the role timeout if necessary
1372 if (!self.havocbot_role_timeout)
1374 self.havocbot_role_timeout = time + random() * 30 + 60;
1377 // If nothing happened just switch to previous role
1378 if (time > self.havocbot_role_timeout)
1380 self.havocbot_role = self.havocbot_previous_role;
1381 self.havocbot_role_timeout = 0;
1385 // Chase the flag carrier
1386 if (self.bot_strategytime < time)
1388 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1389 navigation_goalrating_start();
1390 havocbot_goalrating_ctf_enemyflag(30000);
1391 havocbot_goalrating_ctf_ourstolenflag(40000);
1392 havocbot_goalrating_items(10000, self.origin, 10000);
1393 navigation_goalrating_end();
1397 void havocbot_role_ctf_offense()
1402 if(self.deadflag != DEAD_NO)
1404 havocbot_ctf_reset_role(self);
1408 if (self.flagcarried)
1410 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1415 mf = havocbot_ctf_find_flag(self);
1416 ef = havocbot_ctf_find_enemy_flag(self);
1419 if(mf.ctf_status!=FLAG_BASE)
1422 pos = mf.tag_entity.origin;
1426 // Try to get it if closer than the enemy base
1427 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1429 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1434 // Escort flag carrier
1435 if(ef.ctf_status!=FLAG_BASE)
1438 pos = ef.tag_entity.origin;
1442 if(vlen(pos-mf.dropped_origin)>700)
1444 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1449 // About to fail, switch to middlefield
1452 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1456 // Set the role timeout if necessary
1457 if (!self.havocbot_role_timeout)
1458 self.havocbot_role_timeout = time + 120;
1460 if (time > self.havocbot_role_timeout)
1462 havocbot_ctf_reset_role(self);
1466 if (self.bot_strategytime < time)
1468 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1469 navigation_goalrating_start();
1470 havocbot_goalrating_ctf_ourstolenflag(50000);
1471 havocbot_goalrating_ctf_enemybase(20000);
1472 havocbot_goalrating_items(5000, self.origin, 1000);
1473 havocbot_goalrating_items(1000, self.origin, 10000);
1474 navigation_goalrating_end();
1478 // Retriever (temporary role):
1479 void havocbot_role_ctf_retriever()
1483 if(self.deadflag != DEAD_NO)
1485 havocbot_ctf_reset_role(self);
1489 if (self.flagcarried)
1491 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1495 // If flag is back on the base switch to previous role
1496 mf = havocbot_ctf_find_flag(self);
1497 if(mf.ctf_status==FLAG_BASE)
1499 havocbot_ctf_reset_role(self);
1503 if (!self.havocbot_role_timeout)
1504 self.havocbot_role_timeout = time + 20;
1506 if (time > self.havocbot_role_timeout)
1508 havocbot_ctf_reset_role(self);
1512 if (self.bot_strategytime < time)
1517 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1518 navigation_goalrating_start();
1519 havocbot_goalrating_ctf_ourstolenflag(50000);
1520 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1521 havocbot_goalrating_ctf_enemybase(30000);
1522 havocbot_goalrating_items(500, self.origin, rt_radius);
1523 navigation_goalrating_end();
1527 void havocbot_role_ctf_middle()
1531 if(self.deadflag != DEAD_NO)
1533 havocbot_ctf_reset_role(self);
1537 if (self.flagcarried)
1539 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1543 mf = havocbot_ctf_find_flag(self);
1544 if(mf.ctf_status!=FLAG_BASE)
1546 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1550 if (!self.havocbot_role_timeout)
1551 self.havocbot_role_timeout = time + 10;
1553 if (time > self.havocbot_role_timeout)
1555 havocbot_ctf_reset_role(self);
1559 if (self.bot_strategytime < time)
1563 org = havocbot_ctf_middlepoint;
1564 org_z = self.origin_z;
1566 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1567 navigation_goalrating_start();
1568 havocbot_goalrating_ctf_ourstolenflag(50000);
1569 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1570 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1571 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1572 havocbot_goalrating_items(2500, self.origin, 10000);
1573 havocbot_goalrating_ctf_enemybase(2500);
1574 navigation_goalrating_end();
1578 void havocbot_role_ctf_defense()
1582 if(self.deadflag != DEAD_NO)
1584 havocbot_ctf_reset_role(self);
1588 if (self.flagcarried)
1590 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1594 // If own flag was captured
1595 mf = havocbot_ctf_find_flag(self);
1596 if(mf.ctf_status!=FLAG_BASE)
1598 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1602 if (!self.havocbot_role_timeout)
1603 self.havocbot_role_timeout = time + 30;
1605 if (time > self.havocbot_role_timeout)
1607 havocbot_ctf_reset_role(self);
1610 if (self.bot_strategytime < time)
1615 org = mf.dropped_origin;
1616 mp_radius = havocbot_ctf_middlepoint_radius;
1618 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1619 navigation_goalrating_start();
1621 // if enemies are closer to our base, go there
1622 entity head, closestplayer = world;
1623 float distance, bestdistance = 10000;
1624 FOR_EACH_PLAYER(head)
1626 if(head.deadflag!=DEAD_NO)
1629 distance = vlen(org - head.origin);
1630 if(distance<bestdistance)
1632 closestplayer = head;
1633 bestdistance = distance;
1638 if(closestplayer.team!=self.team)
1639 if(vlen(org - self.origin)>1000)
1640 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1641 havocbot_goalrating_ctf_ourbase(30000);
1643 havocbot_goalrating_ctf_ourstolenflag(20000);
1644 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1645 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1646 havocbot_goalrating_items(10000, org, mp_radius);
1647 havocbot_goalrating_items(5000, self.origin, 10000);
1648 navigation_goalrating_end();
1652 void havocbot_role_ctf_setrole(entity bot, float role)
1654 dprint(strcat(bot.netname," switched to "));
1657 case HAVOCBOT_CTF_ROLE_CARRIER:
1659 bot.havocbot_role = havocbot_role_ctf_carrier;
1660 bot.havocbot_role_timeout = 0;
1661 bot.havocbot_cantfindflag = time + 10;
1662 bot.bot_strategytime = 0;
1664 case HAVOCBOT_CTF_ROLE_DEFENSE:
1666 bot.havocbot_role = havocbot_role_ctf_defense;
1667 bot.havocbot_role_timeout = 0;
1669 case HAVOCBOT_CTF_ROLE_MIDDLE:
1671 bot.havocbot_role = havocbot_role_ctf_middle;
1672 bot.havocbot_role_timeout = 0;
1674 case HAVOCBOT_CTF_ROLE_OFFENSE:
1676 bot.havocbot_role = havocbot_role_ctf_offense;
1677 bot.havocbot_role_timeout = 0;
1679 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1680 dprint("retriever");
1681 bot.havocbot_previous_role = bot.havocbot_role;
1682 bot.havocbot_role = havocbot_role_ctf_retriever;
1683 bot.havocbot_role_timeout = time + 10;
1684 bot.bot_strategytime = 0;
1686 case HAVOCBOT_CTF_ROLE_ESCORT:
1688 bot.havocbot_previous_role = bot.havocbot_role;
1689 bot.havocbot_role = havocbot_role_ctf_escort;
1690 bot.havocbot_role_timeout = time + 30;
1691 bot.bot_strategytime = 0;
1702 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1706 // initially clear items so they can be set as necessary later.
1707 self.items &~= (IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST
1708 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST | IT_CTF_SHIELDED);
1710 // scan through all the flags and notify the client about them
1711 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1713 switch(flag.ctf_status)
1718 if((flag.owner == self) || (flag.pass_sender == self))
1719 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
1721 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
1726 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
1732 // item for stopping players from capturing the flag too often
1733 if(self.ctf_captureshielded)
1734 self.items |= IT_CTF_SHIELDED;
1736 // update the health of the flag carrier waypointsprite
1737 if(self.wps_flagcarrier)
1738 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent));
1743 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1745 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1747 if(frag_target == frag_attacker) // damage done to yourself
1749 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1750 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1752 else // damage done to everyone else
1754 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1755 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1758 else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && IsDifferentTeam(frag_target, frag_attacker)) // if the target is a flagcarrier
1760 if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent)))
1761 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1763 frag_target.wps_helpme_time = time;
1764 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1766 // todo: add notification for when flag carrier needs help?
1771 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1773 if((frag_attacker != frag_target) && (frag_attacker.classname == "player") && (frag_target.flagcarried))
1775 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1776 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1779 if(frag_target.flagcarried)
1780 { ctf_Handle_Throw(frag_target, world, DROP_NORMAL); }
1785 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1788 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1791 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1793 entity flag; // temporary entity for the search method
1795 if(self.flagcarried)
1796 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1798 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1800 if(flag.pass_sender == self) { flag.pass_sender = world; }
1801 if(flag.pass_target == self) { flag.pass_target = world; }
1802 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
1808 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
1810 if(self.flagcarried)
1811 if(!autocvar_g_ctf_portalteleport)
1812 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1817 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
1819 if(MUTATOR_RETURNVALUE || gameover) { return FALSE; }
1821 entity player = self;
1823 if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1825 // pass the flag to a team mate
1826 if(autocvar_g_ctf_pass)
1828 entity head, closest_target = world;
1829 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, TRUE);
1831 while(head) // find the closest acceptable target to pass to
1833 if(head.classname == "player" && head.deadflag == DEAD_NO)
1834 if(head != player && !IsDifferentTeam(head, player))
1835 if(!head.speedrunning && !head.vehicle)
1837 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
1838 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
1839 vector passer_center = CENTER_OR_VIEWOFS(player);
1841 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
1843 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
1845 if(clienttype(head) == CLIENTTYPE_BOT)
1847 Send_Notification_Legacy_Wrapper(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG);
1848 ctf_Handle_Throw(head, player, DROP_PASS);
1852 Send_Notification_Legacy_Wrapper(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG);
1853 Send_Notification_Legacy_Wrapper(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG);
1855 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
1858 else if(player.flagcarried)
1862 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
1863 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
1864 { closest_target = head; }
1866 else { closest_target = head; }
1873 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return TRUE; }
1876 // throw the flag in front of you
1877 if(autocvar_g_ctf_throw && player.flagcarried)
1879 if(player.throw_count == -1)
1881 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
1883 player.throw_prevtime = time;
1884 player.throw_count = 1;
1885 ctf_Handle_Throw(player, world, DROP_THROW);
1890 Send_Notification_Legacy_Wrapper(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, NO_STR_ARG, NO_STR_ARG, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time), NO_FL_ARG, NO_FL_ARG);
1896 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
1897 else { player.throw_count += 1; }
1898 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
1900 player.throw_prevtime = time;
1901 ctf_Handle_Throw(player, world, DROP_THROW);
1910 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
1912 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
1914 self.wps_helpme_time = time;
1915 WaypointSprite_HelpMePing(self.wps_flagcarrier);
1917 else // create a normal help me waypointsprite
1919 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');
1920 WaypointSprite_Ping(self.wps_helpme);
1926 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
1928 if(vh_player.flagcarried)
1930 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
1932 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
1936 setattachment(vh_player.flagcarried, vh_vehicle, "");
1937 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
1938 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
1939 //vh_player.flagcarried.angles = '0 0 0';
1947 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
1949 if(vh_player.flagcarried)
1951 setattachment(vh_player.flagcarried, vh_player, "");
1952 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
1953 vh_player.flagcarried.scale = FLAG_SCALE;
1954 vh_player.flagcarried.angles = '0 0 0';
1961 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
1963 if(self.flagcarried)
1965 Send_Notification_Legacy_Wrapper(NOTIF_ANY, world, MSG_INFO, APP_TEAM_ENT_2(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN_), NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG);
1966 ctf_RespawnFlag(self.flagcarried);
1973 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
1975 entity flag; // temporary entity for the search method
1977 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1979 switch(flag.ctf_status)
1984 // lock the flag, game is over
1985 flag.movetype = MOVETYPE_NONE;
1986 flag.takedamage = DAMAGE_NO;
1987 flag.solid = SOLID_NOT;
1988 flag.nextthink = FALSE; // stop thinking
1990 //dprint("stopping the ", flag.netname, " from moving.\n");
1998 // do nothing for these flags
2007 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
2009 havocbot_ctf_reset_role(self);
2013 MUTATOR_HOOKFUNCTION(ctf_GetCvars)
2015 GetCvars_handleFloat(get_cvars_s, get_cvars_f, CAPTURE_VERBOSE, "notification_ctf_capture_verbose");
2016 GetCvars_handleFloat(get_cvars_s, get_cvars_f, PICKUP_TEAM_VERBOSE, "notification_ctf_pickup_team_verbose");
2017 GetCvars_handleFloat(get_cvars_s, get_cvars_f, PICKUP_ENEMY_VERBOSE, "notification_ctf_pickup_enemy_verbose");
2026 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
2027 CTF Starting point for a player in team one (Red).
2028 Keys: "angle" viewing angle when spawning. */
2029 void spawnfunc_info_player_team1()
2031 if(g_assault) { remove(self); return; }
2033 self.team = FL_TEAM_1; // red
2034 spawnfunc_info_player_deathmatch();
2038 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
2039 CTF Starting point for a player in team two (Blue).
2040 Keys: "angle" viewing angle when spawning. */
2041 void spawnfunc_info_player_team2()
2043 if(g_assault) { remove(self); return; }
2045 self.team = FL_TEAM_2; // blue
2046 spawnfunc_info_player_deathmatch();
2049 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
2050 CTF Starting point for a player in team three (Yellow).
2051 Keys: "angle" viewing angle when spawning. */
2052 void spawnfunc_info_player_team3()
2054 if(g_assault) { remove(self); return; }
2056 self.team = FL_TEAM_3; // yellow
2057 spawnfunc_info_player_deathmatch();
2061 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
2062 CTF Starting point for a player in team four (Purple).
2063 Keys: "angle" viewing angle when spawning. */
2064 void spawnfunc_info_player_team4()
2066 if(g_assault) { remove(self); return; }
2068 self.team = FL_TEAM_4; // purple
2069 spawnfunc_info_player_deathmatch();
2072 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2073 CTF flag for team one (Red).
2075 "angle" Angle the flag will point (minus 90 degrees)...
2076 "model" model to use, note this needs red and blue as skins 0 and 1...
2077 "noise" sound played when flag is picked up...
2078 "noise1" sound played when flag is returned by a teammate...
2079 "noise2" sound played when flag is captured...
2080 "noise3" sound played when flag is lost in the field and respawns itself...
2081 "noise4" sound played when flag is dropped by a player...
2082 "noise5" sound played when flag touches the ground... */
2083 void spawnfunc_item_flag_team1()
2085 if(!g_ctf) { remove(self); return; }
2087 ctf_FlagSetup(1, self); // 1 = red
2090 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2091 CTF flag for team two (Blue).
2093 "angle" Angle the flag will point (minus 90 degrees)...
2094 "model" model to use, note this needs red and blue as skins 0 and 1...
2095 "noise" sound played when flag is picked up...
2096 "noise1" sound played when flag is returned by a teammate...
2097 "noise2" sound played when flag is captured...
2098 "noise3" sound played when flag is lost in the field and respawns itself...
2099 "noise4" sound played when flag is dropped by a player...
2100 "noise5" sound played when flag touches the ground... */
2101 void spawnfunc_item_flag_team2()
2103 if(!g_ctf) { remove(self); return; }
2105 ctf_FlagSetup(0, self); // the 0 is misleading, but -- 0 = blue.
2108 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2109 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2110 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.
2112 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2113 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2114 void spawnfunc_ctf_team()
2116 if(!g_ctf) { remove(self); return; }
2118 self.classname = "ctf_team";
2119 self.team = self.cnt + 1;
2122 // compatibility for quake maps
2123 void spawnfunc_team_CTF_redflag() { spawnfunc_item_flag_team1(); }
2124 void spawnfunc_team_CTF_blueflag() { spawnfunc_item_flag_team2(); }
2125 void spawnfunc_team_CTF_redplayer() { spawnfunc_info_player_team1(); }
2126 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2(); }
2127 void spawnfunc_team_CTF_redspawn() { spawnfunc_info_player_team1(); }
2128 void spawnfunc_team_CTF_bluespawn() { spawnfunc_info_player_team2(); }
2136 void ctf_ScoreRules()
2138 ScoreRules_basics(2, SFL_SORT_PRIO_PRIMARY, 0, TRUE);
2139 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2140 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2141 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2142 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2143 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2144 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2145 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2146 ScoreRules_basics_end();
2149 // code from here on is just to support maps that don't have flag and team entities
2150 void ctf_SpawnTeam (string teamname, float teamcolor)
2155 self.classname = "ctf_team";
2156 self.netname = teamname;
2157 self.cnt = teamcolor;
2159 spawnfunc_ctf_team();
2164 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2166 // if no teams are found, spawn defaults
2167 if(find(world, classname, "ctf_team") == world)
2169 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2170 ctf_SpawnTeam("Red", FL_TEAM_1 - 1);
2171 ctf_SpawnTeam("Blue", FL_TEAM_2 - 1);
2177 void ctf_Initialize()
2179 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2181 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2182 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2183 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2185 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2189 MUTATOR_DEFINITION(gamemode_ctf)
2191 MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2192 MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2193 MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2194 MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2195 MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2196 MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2197 MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2198 MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2199 MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2200 MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2201 MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2202 MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2203 MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2204 MUTATOR_HOOK(HavocBot_ChooseRule, ctf_BotRoles, CBC_ORDER_ANY);
2205 MUTATOR_HOOK(GetCvars, ctf_GetCvars, CBC_ORDER_ANY);
2209 if(time > 1) // game loads at time 1
2210 error("This is a game type and it cannot be added at runtime.");
2214 MUTATOR_ONROLLBACK_OR_REMOVE
2216 // we actually cannot roll back ctf_Initialize here
2217 // BUT: we don't need to! If this gets called, adding always
2223 print("This is a game type and it cannot be removed at runtime.");