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)
142 // personal waypoints
148 if(self.rule == SPRITERULE_DEFAULT)
149 if(ctf_IsDifferentTeam(self.owner.flagcarried, self.owner))
150 if(ctf_IsDifferentTeam(self.owner.flagcarried, e))
151 if(!IsDifferentTeam(self.owner, e) || !IS_PLAYER(e))
158 // =======================
159 // CaptureShield Functions
160 // =======================
162 float ctf_CaptureShield_CheckStatus(entity p)
164 float s, s2, s3, s4, se, se2, se3, se4, sr, ser;
166 float players_worseeq, players_total;
168 if(ctf_captureshield_max_ratio <= 0)
171 s = PlayerScore_Add(p, SP_CTF_CAPS, 0);
172 s2 = PlayerScore_Add(p, SP_CTF_PICKUPS, 0);
173 s3 = PlayerScore_Add(p, SP_CTF_RETURNS, 0);
174 s4 = PlayerScore_Add(p, SP_CTF_FCKILLS, 0);
176 sr = ((s - s2) + (s3 + s4));
178 if(sr >= -ctf_captureshield_min_negscore)
181 players_total = players_worseeq = 0;
184 if(IsDifferentTeam(e, p))
186 se = PlayerScore_Add(e, SP_CTF_CAPS, 0);
187 se2 = PlayerScore_Add(e, SP_CTF_PICKUPS, 0);
188 se3 = PlayerScore_Add(e, SP_CTF_RETURNS, 0);
189 se4 = PlayerScore_Add(e, SP_CTF_FCKILLS, 0);
191 ser = ((se - se2) + (se3 + se4));
198 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
199 // use this rule here
201 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
207 void ctf_CaptureShield_Update(entity player, float wanted_status)
209 float updated_status = ctf_CaptureShield_CheckStatus(player);
210 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
212 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
213 player.ctf_captureshielded = updated_status;
217 float ctf_CaptureShield_Customize()
219 if(!other.ctf_captureshielded) { return FALSE; }
220 if(!ctf_IsDifferentTeam(self, other)) { return FALSE; }
225 void ctf_CaptureShield_Touch()
227 if(!other.ctf_captureshielded) { return; }
228 if(!ctf_IsDifferentTeam(self, other)) { return; }
230 vector mymid = (self.absmin + self.absmax) * 0.5;
231 vector othermid = (other.absmin + other.absmax) * 0.5;
233 Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
234 Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED);
237 void ctf_CaptureShield_Spawn(entity flag)
239 entity shield = spawn();
242 shield.team = self.team;
243 shield.touch = ctf_CaptureShield_Touch;
244 shield.customizeentityforclient = ctf_CaptureShield_Customize;
245 shield.classname = "ctf_captureshield";
246 shield.effects = EF_ADDITIVE;
247 shield.movetype = MOVETYPE_NOCLIP;
248 shield.solid = SOLID_TRIGGER;
249 shield.avelocity = '7 0 11';
252 setorigin(shield, self.origin);
253 setmodel(shield, "models/ctf/shield.md3");
254 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
258 // ====================
259 // Drop/Pass/Throw Code
260 // ====================
262 void ctf_Handle_Drop(entity flag, entity player, float droptype)
265 player = (player ? player : flag.pass_sender);
268 flag.movetype = MOVETYPE_TOSS;
269 flag.takedamage = DAMAGE_YES;
270 flag.angles = '0 0 0';
271 flag.health = flag.max_flag_health;
272 flag.ctf_droptime = time;
273 flag.ctf_dropper = player;
274 flag.ctf_status = FLAG_DROPPED;
276 // messages and sounds
277 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_LOST_), player.netname);
278 sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
279 ctf_EventLog("dropped", player.team, player);
282 PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);
283 PlayerScore_Add(player, SP_CTF_DROPS, 1);
286 if(autocvar_g_ctf_flag_dropped_waypoint)
287 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));
289 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
291 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
292 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
295 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
297 if(droptype == DROP_PASS)
299 flag.pass_distance = 0;
300 flag.pass_sender = world;
301 flag.pass_target = world;
305 void ctf_Handle_Retrieve(entity flag, entity player)
307 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
308 entity sender = flag.pass_sender;
310 // transfer flag to player
312 flag.owner.flagcarried = flag;
315 setattachment(flag, player, "");
316 setorigin(flag, FLAG_CARRY_OFFSET);
317 flag.movetype = MOVETYPE_NONE;
318 flag.takedamage = DAMAGE_NO;
319 flag.solid = SOLID_NOT;
320 flag.angles = '0 0 0';
321 flag.ctf_status = FLAG_CARRY;
323 // messages and sounds
324 sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
325 ctf_EventLog("receive", flag.team, player);
327 FOR_EACH_REALPLAYER(tmp_player)
329 if(tmp_player == sender)
330 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_SENT_), player.netname);
331 else if(tmp_player == player)
332 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_RECEIVED_), sender.netname);
333 else if(!IsDifferentTeam(tmp_player, sender))
334 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_OTHER_), sender.netname, player.netname);
337 // create new waypoint
338 ctf_FlagcarrierWaypoints(player);
340 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
341 player.throw_antispam = sender.throw_antispam;
343 flag.pass_distance = 0;
344 flag.pass_sender = world;
345 flag.pass_target = world;
348 void ctf_Handle_Throw(entity player, entity receiver, float droptype)
350 entity flag = player.flagcarried;
351 vector targ_origin, flag_velocity;
353 if(!flag) { return; }
354 if((droptype == DROP_PASS) && !receiver) { return; }
356 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
359 setattachment(flag, world, "");
360 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
361 flag.owner.flagcarried = world;
363 flag.solid = SOLID_TRIGGER;
364 flag.ctf_dropper = player;
365 flag.ctf_droptime = time;
367 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
374 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
375 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
376 WarpZone_RefSys_Copy(flag, receiver);
377 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
378 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
380 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
381 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, FALSE);
384 flag.movetype = MOVETYPE_FLY;
385 flag.takedamage = DAMAGE_NO;
386 flag.pass_sender = player;
387 flag.pass_target = receiver;
388 flag.ctf_status = FLAG_PASSING;
391 sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
392 WarpZone_TrailParticles(world, particleeffectnum(flag.passeffect), player.origin, targ_origin);
393 ctf_EventLog("pass", flag.team, player);
399 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'));
401 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)));
402 flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, FALSE);
403 ctf_Handle_Drop(flag, player, droptype);
409 flag.velocity = '0 0 0'; // do nothing
416 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);
417 ctf_Handle_Drop(flag, player, droptype);
422 // kill old waypointsprite
423 WaypointSprite_Ping(player.wps_flagcarrier);
424 WaypointSprite_Kill(player.wps_flagcarrier);
426 if(player.wps_enemyflagcarrier)
427 WaypointSprite_Kill(player.wps_enemyflagcarrier);
430 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
438 void ctf_Handle_Capture(entity flag, entity toucher, float capturetype)
440 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
441 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
442 float old_time, new_time;
444 if not(player) { return; } // without someone to give the reward to, we can't possibly cap
445 if(ctf_IsDifferentTeam(player, flag)) { return; }
447 // messages and sounds
448 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(enemy_flag, CENTER_CTF_CAPTURE_));
449 ctf_CaptureRecord(enemy_flag, player);
450 sound(player, CH_TRIGGER, ((IsDifferentTeam(player, flag)) ? enemy_flag.snd_flag_capture : flag.snd_flag_capture), VOL_BASE, ATTEN_NONE);
454 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
455 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
460 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
461 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
463 old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
464 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
465 if(!old_time || new_time < old_time)
466 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
469 pointparticles(particleeffectnum(flag.capeffect), flag.origin, '0 0 0', 1);
470 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
473 if(capturetype == CAPTURE_NORMAL)
475 WaypointSprite_Kill(player.wps_flagcarrier);
476 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
478 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
479 { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
483 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
484 ctf_RespawnFlag(enemy_flag);
487 void ctf_Handle_Return(entity flag, entity player)
489 // messages and sounds
490 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_RETURN_));
491 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_), player.netname);
492 sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
493 ctf_EventLog("return", flag.team, player);
496 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
497 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
499 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
503 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
504 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
505 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
509 ctf_RespawnFlag(flag);
512 void ctf_Handle_Pickup(entity flag, entity player, float pickuptype)
515 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
516 float pickup_dropped_score; // used to calculate dropped pickup score
518 // attach the flag to the player
520 player.flagcarried = flag;
521 setattachment(flag, player, "");
522 setorigin(flag, FLAG_CARRY_OFFSET);
525 flag.movetype = MOVETYPE_NONE;
526 flag.takedamage = DAMAGE_NO;
527 flag.solid = SOLID_NOT;
528 flag.angles = '0 0 0';
529 flag.ctf_status = FLAG_CARRY;
533 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
534 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
538 // messages and sounds
539 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_PICKUP_), player.netname);
540 sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
542 FOR_EACH_REALPLAYER(tmp_player)
544 if(tmp_player == player)
546 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PICKUP_));
547 if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
549 else if(!IsDifferentTeam(tmp_player, player) && tmp_player != player)
551 if(tmp_player.PICKUP_TEAM_VERBOSE)
552 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PICKUP_TEAM_VERBOSE_), Team_ColorCode(player.team), player.netname);
554 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PICKUP_TEAM_), Team_ColorCode(player.team));
556 else if(IsDifferentTeam(tmp_player, player) && !ctf_IsDifferentTeam(flag, tmp_player))
558 if(tmp_player.PICKUP_ENEMY_VERBOSE)
559 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, CENTER_CTF_PICKUP_ENEMY_VERBOSE, Team_ColorCode(player.team), player.netname);
561 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, CENTER_CTF_PICKUP_ENEMY, Team_ColorCode(player.team));
566 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
571 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
572 ctf_EventLog("steal", flag.team, player);
578 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);
579 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);
580 dprint("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
581 PlayerTeamScore_AddScore(player, pickup_dropped_score);
582 ctf_EventLog("pickup", flag.team, player);
590 if(pickuptype == PICKUP_BASE)
592 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
593 if((player.speedrunning) && (ctf_captimerecord))
594 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
598 pointparticles(particleeffectnum(flag.toucheffect), player.origin, '0 0 0', 1);
601 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
602 ctf_FlagcarrierWaypoints(player);
603 WaypointSprite_Ping(player.wps_flagcarrier);
607 // ===================
608 // Main Flag Functions
609 // ===================
611 void ctf_CheckFlagReturn(entity flag, float returntype)
613 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
615 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
617 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
621 case RETURN_DROPPED: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DROPPED_)); break;
622 case RETURN_DAMAGE: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DAMAGED_)); break;
623 case RETURN_SPEEDRUN: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_SPEEDRUN_), ctf_captimerecord); break;
624 case RETURN_NEEDKILL: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_NEEDKILL_)); break;
628 { Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_TIMEOUT_)); break; }
630 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
631 ctf_EventLog("returned", flag.team, world);
632 ctf_RespawnFlag(flag);
637 void ctf_CheckStalemate(void)
640 float stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0;
643 entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
645 // build list of stale flags
646 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
648 if(autocvar_g_ctf_stalemate)
649 if(tmp_entity.ctf_status != FLAG_BASE)
650 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time)
652 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
653 ctf_staleflaglist = tmp_entity;
655 switch(tmp_entity.team)
657 case NUM_TEAM_1: ++stale_red_flags; break;
658 case NUM_TEAM_2: ++stale_blue_flags; break;
659 case NUM_TEAM_3: ++stale_yellow_flags; break;
660 case NUM_TEAM_4: ++stale_pink_flags; break;
665 stale_flags = (stale_red_flags > 0) + (stale_blue_flags > 0) + (stale_yellow_flags > 0) + (stale_pink_flags > 0);
667 if(stale_flags == ctf_teams)
668 ctf_stalemate = TRUE;
669 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
670 { ctf_stalemate = FALSE; wpforenemy_announced = FALSE; }
671 else if(stale_flags < ctf_teams && stale_flags > 0 && autocvar_g_ctf_stalemate_endcondition == 1)
672 { ctf_stalemate = FALSE; wpforenemy_announced = FALSE; }
674 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
677 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
679 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
681 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));
682 tmp_entity.owner.wps_enemyflagcarrier.waypointsprite_visible_for_player = ctf_Stalemate_waypointsprite_visible_for_player;
686 if not(wpforenemy_announced)
688 FOR_EACH_REALPLAYER(tmp_entity)
689 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CENTER, ((tmp_entity.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER));
691 wpforenemy_announced = TRUE;
696 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
698 if(ITEM_DAMAGE_NEEDKILL(deathtype))
700 // automatically kill the flag and return it
702 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
705 if(autocvar_g_ctf_flag_return_damage)
707 // reduce health and check if it should be returned
708 self.health = self.health - damage;
709 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
719 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
722 if(self == ctf_worldflaglist) // only for the first flag
723 FOR_EACH_CLIENT(tmp_entity)
724 ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
727 if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
728 dprint("wtf the flag got squashed?\n");
729 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
730 if(!trace_startsolid) // can we resize it without getting stuck?
731 setsize(self, FLAG_MIN, FLAG_MAX); }
733 switch(self.ctf_status) // reset flag angles in case warpzones adjust it
737 self.angles = '0 0 0';
745 switch(self.ctf_status)
749 if(autocvar_g_ctf_dropped_capture_radius)
751 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
752 if(tmp_entity.ctf_status == FLAG_DROPPED)
753 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
754 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
755 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
762 if(autocvar_g_ctf_flag_dropped_floatinwater)
764 vector midpoint = ((self.absmin + self.absmax) * 0.5);
765 if(pointcontents(midpoint) == CONTENT_WATER)
767 self.velocity = self.velocity * 0.5;
769 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
770 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
772 { self.movetype = MOVETYPE_FLY; }
774 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
776 if(autocvar_g_ctf_flag_return_dropped)
778 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
781 ctf_CheckFlagReturn(self, RETURN_DROPPED);
785 if(autocvar_g_ctf_flag_return_time)
787 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
788 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
796 if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
799 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
803 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
807 if(autocvar_g_ctf_stalemate)
809 if(time >= wpforenemy_nextthink)
811 ctf_CheckStalemate();
812 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
815 if(!ctf_IsDifferentTeam(self, self.owner))
817 // drop the flag if reverse status has changed
818 ctf_Handle_Throw(self.owner, world, DROP_THROW);
825 vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
826 targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
827 WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
829 if((self.pass_target == world)
830 || (self.pass_target.deadflag != DEAD_NO)
831 || (self.pass_target.flagcarried)
832 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
833 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
834 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
836 // give up, pass failed
837 ctf_Handle_Drop(self, world, DROP_PASS);
841 // still a viable target, go for it
842 ctf_CalculatePassVelocity(self, targ_origin, self.origin, TRUE);
847 default: // this should never happen
849 dprint("ctf_FlagThink(): Flag exists with no status?\n");
857 if(gameover) { return; }
859 entity toucher = other;
861 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
862 if(ITEM_TOUCH_NEEDKILL())
865 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
869 // special touch behaviors
870 if(toucher.vehicle_flags & VHF_ISVEHICLE)
872 if(autocvar_g_ctf_allow_vehicle_touch)
873 toucher = toucher.owner; // the player is actually the vehicle owner, not other
875 return; // do nothing
877 else if not(IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
879 if(time > self.wait) // if we haven't in a while, play a sound/effect
881 pointparticles(particleeffectnum(self.toucheffect), self.origin, '0 0 0', 1);
882 sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTEN_NORM);
883 self.wait = time + FLAG_TOUCHRATE;
887 else if(toucher.deadflag != DEAD_NO) { return; }
889 switch(self.ctf_status)
893 if(!ctf_IsDifferentTeam(toucher, self) && (toucher.flagcarried) && IsDifferentTeam(toucher.flagcarried, self))
894 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
895 else if(ctf_IsDifferentTeam(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time))
896 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
902 if(!ctf_IsDifferentTeam(toucher, self))
903 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
904 else if((!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
905 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
911 dprint("Someone touched a flag even though it was being carried?\n");
917 if((IS_PLAYER(toucher)) && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
919 if(IsDifferentTeam(toucher, self.pass_sender))
920 ctf_Handle_Return(self, toucher);
922 ctf_Handle_Retrieve(self, toucher);
930 void ctf_RespawnFlag(entity flag)
932 // check for flag respawn being called twice in a row
933 if(flag.last_respawn > time - 0.5)
934 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
936 flag.last_respawn = time;
938 // reset the player (if there is one)
939 if((flag.owner) && (flag.owner.flagcarried == flag))
941 if(flag.owner.wps_enemyflagcarrier)
942 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
944 WaypointSprite_Kill(flag.wps_flagcarrier);
946 flag.owner.flagcarried = world;
948 if(flag.speedrunning)
949 ctf_FakeTimeLimit(flag.owner, -1);
952 if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
953 { WaypointSprite_Kill(flag.wps_flagdropped); }
956 setattachment(flag, world, "");
957 setorigin(flag, flag.ctf_spawnorigin);
959 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
960 flag.takedamage = DAMAGE_NO;
961 flag.health = flag.max_flag_health;
962 flag.solid = SOLID_TRIGGER;
963 flag.velocity = '0 0 0';
964 flag.angles = flag.mangle;
965 flag.flags = FL_ITEM | FL_NOTARGET;
967 flag.ctf_status = FLAG_BASE;
969 flag.pass_distance = 0;
970 flag.pass_sender = world;
971 flag.pass_target = world;
972 flag.ctf_dropper = world;
973 flag.ctf_pickuptime = 0;
974 flag.ctf_droptime = 0;
980 if(IS_PLAYER(self.owner))
981 ctf_Handle_Throw(self.owner, world, DROP_RESET);
983 ctf_RespawnFlag(self);
986 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
989 waypoint_spawnforitem_force(self, self.origin);
990 self.nearestwaypointtimeout = 0; // activate waypointing again
991 self.bot_basewaypoint = self.nearestwaypoint;
994 string basename = "base";
998 case NUM_TEAM_1: basename = "redbase"; break;
999 case NUM_TEAM_2: basename = "bluebase"; break;
1000 case NUM_TEAM_3: basename = "yellowbase"; break;
1001 case NUM_TEAM_4: basename = "pinkbase"; break;
1004 WaypointSprite_SpawnFixed(basename, self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
1005 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
1007 // captureshield setup
1008 ctf_CaptureShield_Spawn(self);
1011 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1014 self = flag; // for later usage with droptofloor()
1017 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1018 ctf_worldflaglist = flag;
1020 setattachment(flag, world, "");
1022 flag.netname = strcat(Static_Team_ColorName_Upper(teamnumber), "^7 flag");
1023 flag.team = teamnumber;
1024 flag.classname = "item_flag_team";
1025 flag.target = "###item###"; // wut?
1026 flag.flags = FL_ITEM | FL_NOTARGET;
1027 flag.solid = SOLID_TRIGGER;
1028 flag.takedamage = DAMAGE_NO;
1029 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1030 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1031 flag.health = flag.max_flag_health;
1032 flag.event_damage = ctf_FlagDamage;
1033 flag.pushable = TRUE;
1034 flag.teleportable = TELEPORT_NORMAL;
1035 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1036 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1037 flag.velocity = '0 0 0';
1038 flag.mangle = flag.angles;
1039 flag.reset = ctf_Reset;
1040 flag.touch = ctf_FlagTouch;
1041 flag.think = ctf_FlagThink;
1042 flag.nextthink = time + FLAG_THINKRATE;
1043 flag.ctf_status = FLAG_BASE;
1046 if(flag.model == "") { flag.model = strzone(cvar_string(strcat("g_ctf_flag_", Static_Team_ColorName_Lower(teamnumber), "_model"))); }
1047 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1048 if(!flag.skin) { flag.skin = cvar(strcat("g_ctf_flag_", Static_Team_ColorName_Lower(teamnumber), "_skin")); }
1049 if(flag.toucheffect == "") { flag.toucheffect = strzone(strcat(Static_Team_ColorName_Lower(teamnumber), "_touch")); }
1050 if(flag.passeffect == "") { flag.passeffect = strzone(strcat(Static_Team_ColorName_Lower(teamnumber), "_pass")); }
1051 if(flag.capeffect == "") { flag.capeffect = strzone(strcat(Static_Team_ColorName_Lower(teamnumber), "_cap")); }
1054 if(flag.snd_flag_taken == "") { flag.snd_flag_taken = strzone(strcat("ctf/", Static_Team_ColorName_Lower(teamnumber), "_taken.wav")); }
1055 if(flag.snd_flag_returned == "") { flag.snd_flag_returned = strzone(strcat("ctf/", Static_Team_ColorName_Lower(teamnumber), "_returned.wav")); }
1056 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
1057 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.
1058 if(flag.snd_flag_dropped == "") { flag.snd_flag_dropped = strzone(strcat("ctf/", Static_Team_ColorName_Lower(teamnumber), "_dropped.wav")); }
1059 if(flag.snd_flag_touch == "") { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
1060 if(flag.snd_flag_pass == "") { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
1063 precache_sound(flag.snd_flag_taken);
1064 precache_sound(flag.snd_flag_returned);
1065 precache_sound(flag.snd_flag_capture);
1066 precache_sound(flag.snd_flag_respawn);
1067 precache_sound(flag.snd_flag_dropped);
1068 precache_sound(flag.snd_flag_touch);
1069 precache_sound(flag.snd_flag_pass);
1070 precache_model(flag.model);
1071 precache_model("models/ctf/shield.md3");
1072 precache_model("models/ctf/shockwavetransring.md3");
1075 setmodel(flag, flag.model); // precision set below
1076 setsize(flag, FLAG_MIN, FLAG_MAX);
1077 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1079 if(autocvar_g_ctf_flag_glowtrails)
1083 case NUM_TEAM_1: flag.glow_color = 251; break; // red
1084 case NUM_TEAM_2: flag.glow_color = 210; break; // blue
1085 case NUM_TEAM_3: flag.glow_color = 110; break; // yellow
1086 case NUM_TEAM_4: flag.glow_color = 145; break; // pink
1088 flag.glow_size = 25;
1089 flag.glow_trail = 1;
1092 flag.effects |= EF_LOWPRECISION;
1093 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1094 if(autocvar_g_ctf_dynamiclights)
1098 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1099 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1100 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1101 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1106 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1108 flag.dropped_origin = flag.origin;
1109 flag.noalign = TRUE;
1110 flag.movetype = MOVETYPE_NONE;
1112 else // drop to floor, automatically find a platform and set that as spawn origin
1114 flag.noalign = FALSE;
1117 flag.movetype = MOVETYPE_TOSS;
1120 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1128 // NOTE: LEGACY CODE, needs to be re-written!
1130 void havocbot_calculate_middlepoint()
1134 vector fo = '0 0 0';
1137 f = ctf_worldflaglist;
1142 f = f.ctf_worldflagnext;
1146 havocbot_ctf_middlepoint = s * (1.0 / n);
1147 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1151 entity havocbot_ctf_find_flag(entity bot)
1154 f = ctf_worldflaglist;
1157 if (!ctf_IsDifferentTeam(bot, f))
1159 f = f.ctf_worldflagnext;
1164 entity havocbot_ctf_find_enemy_flag(entity bot)
1167 f = ctf_worldflaglist;
1170 if (ctf_IsDifferentTeam(bot, f))
1172 f = f.ctf_worldflagnext;
1177 float havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1185 FOR_EACH_PLAYER(head)
1187 if(head.team!=bot.team || head.deadflag != DEAD_NO || head == bot)
1190 if(vlen(head.origin - org) < tc_radius)
1197 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1200 head = ctf_worldflaglist;
1203 if (!ctf_IsDifferentTeam(self, head))
1205 head = head.ctf_worldflagnext;
1208 navigation_routerating(head, ratingscale, 10000);
1211 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1214 head = ctf_worldflaglist;
1217 if (!ctf_IsDifferentTeam(self, head))
1219 head = head.ctf_worldflagnext;
1224 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1227 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1230 head = ctf_worldflaglist;
1233 if (ctf_IsDifferentTeam(self, head))
1235 head = head.ctf_worldflagnext;
1238 navigation_routerating(head, ratingscale, 10000);
1241 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1243 if not(bot_waypoints_for_items)
1245 havocbot_goalrating_ctf_enemyflag(ratingscale);
1251 head = havocbot_ctf_find_enemy_flag(self);
1256 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1259 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1263 mf = havocbot_ctf_find_flag(self);
1265 if(mf.ctf_status == FLAG_BASE)
1269 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1272 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1275 head = ctf_worldflaglist;
1278 // flag is out in the field
1279 if(head.ctf_status != FLAG_BASE)
1280 if(head.tag_entity==world) // dropped
1284 if(vlen(org-head.origin)<df_radius)
1285 navigation_routerating(head, ratingscale, 10000);
1288 navigation_routerating(head, ratingscale, 10000);
1291 head = head.ctf_worldflagnext;
1295 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1299 head = findchainfloat(bot_pickup, TRUE);
1302 // gather health and armor only
1304 if (head.health || head.armorvalue)
1305 if (vlen(head.origin - org) < sradius)
1307 // get the value of the item
1308 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1310 navigation_routerating(head, t * ratingscale, 500);
1316 void havocbot_ctf_reset_role(entity bot)
1318 float cdefense, cmiddle, coffense;
1319 entity mf, ef, head;
1322 if(bot.deadflag != DEAD_NO)
1325 if(vlen(havocbot_ctf_middlepoint)==0)
1326 havocbot_calculate_middlepoint();
1329 if (bot.flagcarried)
1331 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1335 mf = havocbot_ctf_find_flag(bot);
1336 ef = havocbot_ctf_find_enemy_flag(bot);
1338 // Retrieve stolen flag
1339 if(mf.ctf_status!=FLAG_BASE)
1341 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1345 // If enemy flag is taken go to the middle to intercept pursuers
1346 if(ef.ctf_status!=FLAG_BASE)
1348 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1352 // if there is only me on the team switch to offense
1354 FOR_EACH_PLAYER(head)
1355 if(head.team==bot.team)
1360 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1364 // Evaluate best position to take
1365 // Count mates on middle position
1366 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1368 // Count mates on defense position
1369 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1371 // Count mates on offense position
1372 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1374 if(cdefense<=coffense)
1375 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1376 else if(coffense<=cmiddle)
1377 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1379 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1382 void havocbot_role_ctf_carrier()
1384 if(self.deadflag != DEAD_NO)
1386 havocbot_ctf_reset_role(self);
1390 if (self.flagcarried == world)
1392 havocbot_ctf_reset_role(self);
1396 if (self.bot_strategytime < time)
1398 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1400 navigation_goalrating_start();
1401 havocbot_goalrating_ctf_ourbase(50000);
1404 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1406 navigation_goalrating_end();
1408 if (self.navigation_hasgoals)
1409 self.havocbot_cantfindflag = time + 10;
1410 else if (time > self.havocbot_cantfindflag)
1412 // Can't navigate to my own base, suicide!
1413 // TODO: drop it and wander around
1414 Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1420 void havocbot_role_ctf_escort()
1424 if(self.deadflag != DEAD_NO)
1426 havocbot_ctf_reset_role(self);
1430 if (self.flagcarried)
1432 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1436 // If enemy flag is back on the base switch to previous role
1437 ef = havocbot_ctf_find_enemy_flag(self);
1438 if(ef.ctf_status==FLAG_BASE)
1440 self.havocbot_role = self.havocbot_previous_role;
1441 self.havocbot_role_timeout = 0;
1445 // If the flag carrier reached the base switch to defense
1446 mf = havocbot_ctf_find_flag(self);
1447 if(mf.ctf_status!=FLAG_BASE)
1448 if(vlen(ef.origin - mf.dropped_origin) < 300)
1450 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1454 // Set the role timeout if necessary
1455 if (!self.havocbot_role_timeout)
1457 self.havocbot_role_timeout = time + random() * 30 + 60;
1460 // If nothing happened just switch to previous role
1461 if (time > self.havocbot_role_timeout)
1463 self.havocbot_role = self.havocbot_previous_role;
1464 self.havocbot_role_timeout = 0;
1468 // Chase the flag carrier
1469 if (self.bot_strategytime < time)
1471 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1472 navigation_goalrating_start();
1473 havocbot_goalrating_ctf_enemyflag(30000);
1474 havocbot_goalrating_ctf_ourstolenflag(40000);
1475 havocbot_goalrating_items(10000, self.origin, 10000);
1476 navigation_goalrating_end();
1480 void havocbot_role_ctf_offense()
1485 if(self.deadflag != DEAD_NO)
1487 havocbot_ctf_reset_role(self);
1491 if (self.flagcarried)
1493 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1498 mf = havocbot_ctf_find_flag(self);
1499 ef = havocbot_ctf_find_enemy_flag(self);
1502 if(mf.ctf_status!=FLAG_BASE)
1505 pos = mf.tag_entity.origin;
1509 // Try to get it if closer than the enemy base
1510 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1512 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1517 // Escort flag carrier
1518 if(ef.ctf_status!=FLAG_BASE)
1521 pos = ef.tag_entity.origin;
1525 if(vlen(pos-mf.dropped_origin)>700)
1527 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1532 // About to fail, switch to middlefield
1535 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1539 // Set the role timeout if necessary
1540 if (!self.havocbot_role_timeout)
1541 self.havocbot_role_timeout = time + 120;
1543 if (time > self.havocbot_role_timeout)
1545 havocbot_ctf_reset_role(self);
1549 if (self.bot_strategytime < time)
1551 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1552 navigation_goalrating_start();
1553 havocbot_goalrating_ctf_ourstolenflag(50000);
1554 havocbot_goalrating_ctf_enemybase(20000);
1555 havocbot_goalrating_items(5000, self.origin, 1000);
1556 havocbot_goalrating_items(1000, self.origin, 10000);
1557 navigation_goalrating_end();
1561 // Retriever (temporary role):
1562 void havocbot_role_ctf_retriever()
1566 if(self.deadflag != DEAD_NO)
1568 havocbot_ctf_reset_role(self);
1572 if (self.flagcarried)
1574 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1578 // If flag is back on the base switch to previous role
1579 mf = havocbot_ctf_find_flag(self);
1580 if(mf.ctf_status==FLAG_BASE)
1582 havocbot_ctf_reset_role(self);
1586 if (!self.havocbot_role_timeout)
1587 self.havocbot_role_timeout = time + 20;
1589 if (time > self.havocbot_role_timeout)
1591 havocbot_ctf_reset_role(self);
1595 if (self.bot_strategytime < time)
1600 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1601 navigation_goalrating_start();
1602 havocbot_goalrating_ctf_ourstolenflag(50000);
1603 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1604 havocbot_goalrating_ctf_enemybase(30000);
1605 havocbot_goalrating_items(500, self.origin, rt_radius);
1606 navigation_goalrating_end();
1610 void havocbot_role_ctf_middle()
1614 if(self.deadflag != DEAD_NO)
1616 havocbot_ctf_reset_role(self);
1620 if (self.flagcarried)
1622 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1626 mf = havocbot_ctf_find_flag(self);
1627 if(mf.ctf_status!=FLAG_BASE)
1629 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1633 if (!self.havocbot_role_timeout)
1634 self.havocbot_role_timeout = time + 10;
1636 if (time > self.havocbot_role_timeout)
1638 havocbot_ctf_reset_role(self);
1642 if (self.bot_strategytime < time)
1646 org = havocbot_ctf_middlepoint;
1647 org_z = self.origin_z;
1649 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1650 navigation_goalrating_start();
1651 havocbot_goalrating_ctf_ourstolenflag(50000);
1652 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1653 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1654 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1655 havocbot_goalrating_items(2500, self.origin, 10000);
1656 havocbot_goalrating_ctf_enemybase(2500);
1657 navigation_goalrating_end();
1661 void havocbot_role_ctf_defense()
1665 if(self.deadflag != DEAD_NO)
1667 havocbot_ctf_reset_role(self);
1671 if (self.flagcarried)
1673 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1677 // If own flag was captured
1678 mf = havocbot_ctf_find_flag(self);
1679 if(mf.ctf_status!=FLAG_BASE)
1681 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1685 if (!self.havocbot_role_timeout)
1686 self.havocbot_role_timeout = time + 30;
1688 if (time > self.havocbot_role_timeout)
1690 havocbot_ctf_reset_role(self);
1693 if (self.bot_strategytime < time)
1698 org = mf.dropped_origin;
1699 mp_radius = havocbot_ctf_middlepoint_radius;
1701 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1702 navigation_goalrating_start();
1704 // if enemies are closer to our base, go there
1705 entity head, closestplayer = world;
1706 float distance, bestdistance = 10000;
1707 FOR_EACH_PLAYER(head)
1709 if(head.deadflag!=DEAD_NO)
1712 distance = vlen(org - head.origin);
1713 if(distance<bestdistance)
1715 closestplayer = head;
1716 bestdistance = distance;
1721 if(closestplayer.team!=self.team)
1722 if(vlen(org - self.origin)>1000)
1723 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1724 havocbot_goalrating_ctf_ourbase(30000);
1726 havocbot_goalrating_ctf_ourstolenflag(20000);
1727 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1728 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1729 havocbot_goalrating_items(10000, org, mp_radius);
1730 havocbot_goalrating_items(5000, self.origin, 10000);
1731 navigation_goalrating_end();
1735 void havocbot_role_ctf_setrole(entity bot, float role)
1737 dprint(strcat(bot.netname," switched to "));
1740 case HAVOCBOT_CTF_ROLE_CARRIER:
1742 bot.havocbot_role = havocbot_role_ctf_carrier;
1743 bot.havocbot_role_timeout = 0;
1744 bot.havocbot_cantfindflag = time + 10;
1745 bot.bot_strategytime = 0;
1747 case HAVOCBOT_CTF_ROLE_DEFENSE:
1749 bot.havocbot_role = havocbot_role_ctf_defense;
1750 bot.havocbot_role_timeout = 0;
1752 case HAVOCBOT_CTF_ROLE_MIDDLE:
1754 bot.havocbot_role = havocbot_role_ctf_middle;
1755 bot.havocbot_role_timeout = 0;
1757 case HAVOCBOT_CTF_ROLE_OFFENSE:
1759 bot.havocbot_role = havocbot_role_ctf_offense;
1760 bot.havocbot_role_timeout = 0;
1762 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1763 dprint("retriever");
1764 bot.havocbot_previous_role = bot.havocbot_role;
1765 bot.havocbot_role = havocbot_role_ctf_retriever;
1766 bot.havocbot_role_timeout = time + 10;
1767 bot.bot_strategytime = 0;
1769 case HAVOCBOT_CTF_ROLE_ESCORT:
1771 bot.havocbot_previous_role = bot.havocbot_role;
1772 bot.havocbot_role = havocbot_role_ctf_escort;
1773 bot.havocbot_role_timeout = time + 30;
1774 bot.bot_strategytime = 0;
1785 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1789 float t = 0, t2 = 0, t3 = 0;
1791 // initially clear items so they can be set as necessary later.
1792 self.items &= ~(IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST
1793 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST
1794 | IT_YELLOW_FLAG_CARRYING | IT_YELLOW_FLAG_TAKEN | IT_YELLOW_FLAG_LOST
1795 | IT_PINK_FLAG_CARRYING | IT_PINK_FLAG_TAKEN | IT_PINK_FLAG_LOST
1798 // scan through all the flags and notify the client about them
1799 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1801 if(flag.team == NUM_TEAM_1) { t = IT_RED_FLAG_CARRYING; t2 = IT_RED_FLAG_TAKEN; t3 = IT_RED_FLAG_LOST; }
1802 if(flag.team == NUM_TEAM_2) { t = IT_BLUE_FLAG_CARRYING; t2 = IT_BLUE_FLAG_TAKEN; t3 = IT_BLUE_FLAG_LOST; }
1803 if(flag.team == NUM_TEAM_3) { t = IT_YELLOW_FLAG_CARRYING; t2 = IT_YELLOW_FLAG_TAKEN; t3 = IT_YELLOW_FLAG_LOST; }
1804 if(flag.team == NUM_TEAM_4) { t = IT_PINK_FLAG_CARRYING; t2 = IT_PINK_FLAG_TAKEN; t3 = IT_PINK_FLAG_LOST; }
1806 switch(flag.ctf_status)
1811 if((flag.owner == self) || (flag.pass_sender == self))
1812 self.items |= t; // carrying: self is currently carrying the flag
1814 self.items |= t2; // taken: someone else is carrying the flag
1819 self.items |= t3; // lost: the flag is dropped somewhere on the map
1825 // item for stopping players from capturing the flag too often
1826 if(self.ctf_captureshielded)
1827 self.items |= IT_CTF_SHIELDED;
1829 // update the health of the flag carrier waypointsprite
1830 if(self.wps_flagcarrier)
1831 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent));
1836 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1838 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1840 if(frag_target == frag_attacker) // damage done to yourself
1842 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1843 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1845 else // damage done to everyone else
1847 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1848 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1851 else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && ctf_IsDifferentTeam(frag_target, frag_attacker)) // if the target is a flagcarrier
1853 if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent)))
1854 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1856 frag_target.wps_helpme_time = time;
1857 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1859 // todo: add notification for when flag carrier needs help?
1864 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1866 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
1868 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1869 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1872 if(frag_target.flagcarried)
1873 { ctf_Handle_Throw(frag_target, world, DROP_NORMAL); }
1878 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1881 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1884 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1886 entity flag; // temporary entity for the search method
1888 if(self.flagcarried)
1889 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1891 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1893 if(flag.pass_sender == self) { flag.pass_sender = world; }
1894 if(flag.pass_target == self) { flag.pass_target = world; }
1895 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
1901 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
1903 if(self.flagcarried)
1904 if(!autocvar_g_ctf_portalteleport)
1905 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1910 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
1912 if(MUTATOR_RETURNVALUE || gameover) { return FALSE; }
1914 entity player = self;
1916 if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1918 // pass the flag to a team mate
1919 if(autocvar_g_ctf_pass)
1921 entity head, closest_target = world;
1922 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, TRUE);
1924 while(head) // find the closest acceptable target to pass to
1926 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
1927 if(head != player && !IsDifferentTeam(head, player))
1928 if(!head.speedrunning && !head.vehicle)
1930 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
1931 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
1932 vector passer_center = CENTER_OR_VIEWOFS(player);
1934 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
1936 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
1938 if(IS_BOT_CLIENT(head))
1940 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1941 ctf_Handle_Throw(head, player, DROP_PASS);
1945 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
1946 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1948 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
1951 else if(player.flagcarried)
1955 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
1956 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
1957 { closest_target = head; }
1959 else { closest_target = head; }
1966 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return TRUE; }
1969 // throw the flag in front of you
1970 if(autocvar_g_ctf_throw && player.flagcarried)
1972 if(player.throw_count == -1)
1974 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
1976 player.throw_prevtime = time;
1977 player.throw_count = 1;
1978 ctf_Handle_Throw(player, world, DROP_THROW);
1983 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
1989 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
1990 else { player.throw_count += 1; }
1991 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
1993 player.throw_prevtime = time;
1994 ctf_Handle_Throw(player, world, DROP_THROW);
2003 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
2005 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2007 self.wps_helpme_time = time;
2008 WaypointSprite_HelpMePing(self.wps_flagcarrier);
2010 else // create a normal help me waypointsprite
2012 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');
2013 WaypointSprite_Ping(self.wps_helpme);
2019 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
2021 if(vh_player.flagcarried)
2023 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2025 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
2029 setattachment(vh_player.flagcarried, vh_vehicle, "");
2030 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
2031 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2032 //vh_player.flagcarried.angles = '0 0 0';
2040 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
2042 if(vh_player.flagcarried)
2044 setattachment(vh_player.flagcarried, vh_player, "");
2045 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2046 vh_player.flagcarried.scale = FLAG_SCALE;
2047 vh_player.flagcarried.angles = '0 0 0';
2054 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
2056 if(self.flagcarried)
2058 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN_));
2059 ctf_RespawnFlag(self.flagcarried);
2066 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
2068 entity flag; // temporary entity for the search method
2070 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2072 switch(flag.ctf_status)
2077 // lock the flag, game is over
2078 flag.movetype = MOVETYPE_NONE;
2079 flag.takedamage = DAMAGE_NO;
2080 flag.solid = SOLID_NOT;
2081 flag.nextthink = FALSE; // stop thinking
2083 //dprint("stopping the ", flag.netname, " from moving.\n");
2091 // do nothing for these flags
2100 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
2102 havocbot_ctf_reset_role(self);
2106 MUTATOR_HOOKFUNCTION(ctf_GetCvars)
2108 GetCvars_handleFloat(get_cvars_s, get_cvars_f, CAPTURE_VERBOSE, "notification_ctf_capture_verbose");
2109 GetCvars_handleFloat(get_cvars_s, get_cvars_f, PICKUP_TEAM_VERBOSE, "notification_ctf_pickup_team_verbose");
2110 GetCvars_handleFloat(get_cvars_s, get_cvars_f, PICKUP_ENEMY_VERBOSE, "notification_ctf_pickup_enemy_verbose");
2114 MUTATOR_HOOKFUNCTION(ctf_GetTeamCount)
2116 ret_float = ctf_teams;
2125 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
2126 CTF Starting point for a player in team one (Red).
2127 Keys: "angle" viewing angle when spawning. */
2128 void spawnfunc_info_player_team1()
2130 if(g_assault) { remove(self); return; }
2132 self.team = NUM_TEAM_1; // red
2133 spawnfunc_info_player_deathmatch();
2137 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
2138 CTF Starting point for a player in team two (Blue).
2139 Keys: "angle" viewing angle when spawning. */
2140 void spawnfunc_info_player_team2()
2142 if(g_assault) { remove(self); return; }
2144 self.team = NUM_TEAM_2; // blue
2145 spawnfunc_info_player_deathmatch();
2148 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
2149 CTF Starting point for a player in team three (Yellow).
2150 Keys: "angle" viewing angle when spawning. */
2151 void spawnfunc_info_player_team3()
2153 if(g_assault) { remove(self); return; }
2155 self.team = NUM_TEAM_3; // yellow
2156 spawnfunc_info_player_deathmatch();
2160 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
2161 CTF Starting point for a player in team four (Purple).
2162 Keys: "angle" viewing angle when spawning. */
2163 void spawnfunc_info_player_team4()
2165 if(g_assault) { remove(self); return; }
2167 self.team = NUM_TEAM_4; // purple
2168 spawnfunc_info_player_deathmatch();
2171 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2172 CTF flag for team one (Red).
2174 "angle" Angle the flag will point (minus 90 degrees)...
2175 "model" model to use, note this needs red and blue as skins 0 and 1...
2176 "noise" sound played when flag is picked up...
2177 "noise1" sound played when flag is returned by a teammate...
2178 "noise2" sound played when flag is captured...
2179 "noise3" sound played when flag is lost in the field and respawns itself...
2180 "noise4" sound played when flag is dropped by a player...
2181 "noise5" sound played when flag touches the ground... */
2182 void spawnfunc_item_flag_team1()
2184 if(!g_ctf) { remove(self); return; }
2186 ctf_FlagSetup(NUM_TEAM_1, self);
2189 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2190 CTF flag for team two (Blue).
2192 "angle" Angle the flag will point (minus 90 degrees)...
2193 "model" model to use, note this needs red and blue as skins 0 and 1...
2194 "noise" sound played when flag is picked up...
2195 "noise1" sound played when flag is returned by a teammate...
2196 "noise2" sound played when flag is captured...
2197 "noise3" sound played when flag is lost in the field and respawns itself...
2198 "noise4" sound played when flag is dropped by a player...
2199 "noise5" sound played when flag touches the ground... */
2200 void spawnfunc_item_flag_team2()
2202 if(!g_ctf) { remove(self); return; }
2204 ctf_FlagSetup(NUM_TEAM_2, self);
2207 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2208 CTF flag for team three (Yellow).
2210 "angle" Angle the flag will point (minus 90 degrees)...
2211 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2212 "noise" sound played when flag is picked up...
2213 "noise1" sound played when flag is returned by a teammate...
2214 "noise2" sound played when flag is captured...
2215 "noise3" sound played when flag is lost in the field and respawns itself...
2216 "noise4" sound played when flag is dropped by a player...
2217 "noise5" sound played when flag touches the ground... */
2218 void spawnfunc_item_flag_team3()
2220 if(!g_ctf) { remove(self); return; }
2222 ctf_FlagSetup(NUM_TEAM_3, self);
2225 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2226 CTF flag for team two (Pink).
2228 "angle" Angle the flag will point (minus 90 degrees)...
2229 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2230 "noise" sound played when flag is picked up...
2231 "noise1" sound played when flag is returned by a teammate...
2232 "noise2" sound played when flag is captured...
2233 "noise3" sound played when flag is lost in the field and respawns itself...
2234 "noise4" sound played when flag is dropped by a player...
2235 "noise5" sound played when flag touches the ground... */
2236 void spawnfunc_item_flag_team4()
2238 if(!g_ctf) { remove(self); return; }
2240 ctf_FlagSetup(NUM_TEAM_4, self);
2243 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2244 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2245 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.
2247 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2248 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2249 void spawnfunc_ctf_team()
2251 if(!g_ctf) { remove(self); return; }
2253 self.classname = "ctf_team";
2254 self.team = self.cnt + 1;
2257 // compatibility for quake maps
2258 void spawnfunc_team_CTF_redflag() { spawnfunc_item_flag_team1(); }
2259 void spawnfunc_team_CTF_blueflag() { spawnfunc_item_flag_team2(); }
2260 void spawnfunc_team_CTF_redplayer() { spawnfunc_info_player_team1(); }
2261 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2(); }
2262 void spawnfunc_team_CTF_redspawn() { spawnfunc_info_player_team1(); }
2263 void spawnfunc_team_CTF_bluespawn() { spawnfunc_info_player_team2(); }
2271 void ctf_ScoreRules(float teams)
2273 CheckAllowedTeams(world);
2274 ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, TRUE);
2275 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2276 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2277 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2278 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2279 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2280 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2281 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2282 ScoreRules_basics_end();
2285 // code from here on is just to support maps that don't have flag and team entities
2286 void ctf_SpawnTeam (string teamname, float teamcolor)
2291 self.classname = "ctf_team";
2292 self.netname = teamname;
2293 self.cnt = teamcolor;
2295 spawnfunc_ctf_team();
2300 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2305 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2306 if(tmp_entity.team == NUM_TEAM_3)
2309 break; // found 1 flag for this team
2311 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2312 if(tmp_entity.team == NUM_TEAM_4)
2315 break; // found 1 flag for this team
2318 ctf_teams = bound(2, ctf_teams, 4);
2320 // if no teams are found, spawn defaults
2321 if(find(world, classname, "ctf_team") == world)
2323 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2324 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2325 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2327 ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2329 ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2332 ctf_ScoreRules(ctf_teams);
2335 void ctf_Initialize()
2337 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2339 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2340 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2341 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2343 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2347 MUTATOR_DEFINITION(gamemode_ctf)
2349 MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2350 MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2351 MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2352 MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2353 MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2354 MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2355 MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2356 MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2357 MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2358 MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2359 MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2360 MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2361 MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2362 MUTATOR_HOOK(HavocBot_ChooseRule, ctf_BotRoles, CBC_ORDER_ANY);
2363 MUTATOR_HOOK(GetCvars, ctf_GetCvars, CBC_ORDER_ANY);
2364 MUTATOR_HOOK(GetTeamCount, ctf_GetTeamCount, CBC_ORDER_ANY);
2368 if(time > 1) // game loads at time 1
2369 error("This is a game type and it cannot be added at runtime.");
2373 MUTATOR_ONROLLBACK_OR_REMOVE
2375 // we actually cannot roll back ctf_Initialize here
2376 // BUT: we don't need to! If this gets called, adding always
2382 print("This is a game type and it cannot be removed at runtime.");