1 #include "gamemode_ctf.qh"
7 #include "../vehicles/vehicle.qh"
10 #include "../../warpzonelib/common.qh"
11 #include "../../warpzonelib/mathlib.qh"
13 // ================================================================
14 // Official capture the flag game mode coding, reworked by Samual
15 // Last updated: September, 2012
16 // ================================================================
18 void ctf_FakeTimeLimit(entity e, float t)
21 WriteByte(MSG_ONE, 3); // svc_updatestat
22 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
24 WriteCoord(MSG_ONE, autocvar_timelimit);
26 WriteCoord(MSG_ONE, (t + 1) / 60);
29 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
31 if(autocvar_sv_eventlog)
32 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != world) ? ftos(actor.playerid) : "")));
33 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
36 void ctf_CaptureRecord(entity flag, entity player)
38 float cap_record = ctf_captimerecord;
39 float cap_time = (time - flag.ctf_pickuptime);
40 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
43 if(ctf_oneflag) { Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname); }
44 else if(!ctf_captimerecord) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_TIME_), player.netname, (cap_time * 100)); }
45 else if(cap_time < cap_record) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_BROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
46 else { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_UNBROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
48 // write that shit in the database
49 if(!ctf_oneflag) // but not in 1-flag mode
50 if((!ctf_captimerecord) || (cap_time < cap_record))
52 ctf_captimerecord = cap_time;
53 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
54 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
55 write_recordmarker(player, (time - cap_time), cap_time);
59 void ctf_FlagcarrierWaypoints(entity player)
61 WaypointSprite_Spawn("flagcarrier", 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, true, RADARICON_FLAG, WPCOLOR_FLAGCARRIER(player.team));
62 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON) * 2);
63 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
64 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
67 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
69 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
70 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
71 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
72 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
75 if(current_height) // make sure we can actually do this arcing path
77 targpos = (to + ('0 0 1' * current_height));
78 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
79 if(trace_fraction < 1)
81 //print("normal arc line failed, trying to find new pos...");
82 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
83 targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
84 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
85 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
86 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
89 else { targpos = to; }
91 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
93 vector desired_direction = normalize(targpos - from);
94 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
95 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
98 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
100 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
102 // directional tracing only
104 makevectors(passer_angle);
106 // find the closest point on the enemy to the center of the attack
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 a = h * (normalize(head_center - passer_center) * v_forward);
113 vector nearest_on_line = (passer_center + a * v_forward);
114 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
116 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
117 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
119 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
124 else { return true; }
128 // =======================
129 // CaptureShield Functions
130 // =======================
132 bool ctf_CaptureShield_CheckStatus(entity p)
134 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
136 int players_worseeq, players_total;
138 if(ctf_captureshield_max_ratio <= 0)
141 s = PlayerScore_Add(p, SP_CTF_CAPS, 0);
142 s2 = PlayerScore_Add(p, SP_CTF_PICKUPS, 0);
143 s3 = PlayerScore_Add(p, SP_CTF_RETURNS, 0);
144 s4 = PlayerScore_Add(p, SP_CTF_FCKILLS, 0);
146 sr = ((s - s2) + (s3 + s4));
148 if(sr >= -ctf_captureshield_min_negscore)
151 players_total = players_worseeq = 0;
156 se = PlayerScore_Add(e, SP_CTF_CAPS, 0);
157 se2 = PlayerScore_Add(e, SP_CTF_PICKUPS, 0);
158 se3 = PlayerScore_Add(e, SP_CTF_RETURNS, 0);
159 se4 = PlayerScore_Add(e, SP_CTF_FCKILLS, 0);
161 ser = ((se - se2) + (se3 + se4));
168 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
169 // use this rule here
171 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
177 void ctf_CaptureShield_Update(entity player, bool wanted_status)
179 bool updated_status = ctf_CaptureShield_CheckStatus(player);
180 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
182 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
183 player.ctf_captureshielded = updated_status;
187 bool ctf_CaptureShield_Customize()
189 if(!other.ctf_captureshielded) { return false; }
190 if(CTF_SAMETEAM(self, other)) { return false; }
195 void ctf_CaptureShield_Touch()
197 if(!other.ctf_captureshielded) { return; }
198 if(CTF_SAMETEAM(self, other)) { return; }
200 vector mymid = (self.absmin + self.absmax) * 0.5;
201 vector othermid = (other.absmin + other.absmax) * 0.5;
203 Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
204 if(IS_REAL_CLIENT(other)) { Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
207 void ctf_CaptureShield_Spawn(entity flag)
209 entity shield = spawn();
212 shield.team = self.team;
213 shield.touch = ctf_CaptureShield_Touch;
214 shield.customizeentityforclient = ctf_CaptureShield_Customize;
215 shield.classname = "ctf_captureshield";
216 shield.effects = EF_ADDITIVE;
217 shield.movetype = MOVETYPE_NOCLIP;
218 shield.solid = SOLID_TRIGGER;
219 shield.avelocity = '7 0 11';
222 setorigin(shield, self.origin);
223 setmodel(shield, "models/ctf/shield.md3");
224 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
228 // ====================
229 // Drop/Pass/Throw Code
230 // ====================
232 void ctf_Handle_Drop(entity flag, entity player, int droptype)
235 player = (player ? player : flag.pass_sender);
238 flag.movetype = MOVETYPE_TOSS;
239 flag.takedamage = DAMAGE_YES;
240 flag.angles = '0 0 0';
241 flag.health = flag.max_flag_health;
242 flag.ctf_droptime = time;
243 flag.ctf_dropper = player;
244 flag.ctf_status = FLAG_DROPPED;
246 // messages and sounds
247 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_LOST_) : INFO_CTF_LOST_NEUTRAL), player.netname);
248 sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
249 ctf_EventLog("dropped", player.team, player);
252 PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);
253 PlayerScore_Add(player, SP_CTF_DROPS, 1);
256 if(autocvar_g_ctf_flag_dropped_waypoint)
257 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));
259 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
261 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
262 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
265 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
267 if(droptype == DROP_PASS)
269 flag.pass_distance = 0;
270 flag.pass_sender = world;
271 flag.pass_target = world;
275 void ctf_Handle_Retrieve(entity flag, entity player)
277 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
278 entity sender = flag.pass_sender;
280 // transfer flag to player
282 flag.owner.flagcarried = flag;
287 setattachment(flag, player.vehicle, "");
288 setorigin(flag, VEHICLE_FLAG_OFFSET);
289 flag.scale = VEHICLE_FLAG_SCALE;
293 setattachment(flag, player, "");
294 setorigin(flag, FLAG_CARRY_OFFSET);
296 flag.movetype = MOVETYPE_NONE;
297 flag.takedamage = DAMAGE_NO;
298 flag.solid = SOLID_NOT;
299 flag.angles = '0 0 0';
300 flag.ctf_status = FLAG_CARRY;
302 // messages and sounds
303 sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
304 ctf_EventLog("receive", flag.team, player);
306 FOR_EACH_REALPLAYER(tmp_player)
308 if(tmp_player == sender)
309 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_SENT_) : CENTER_CTF_PASS_SENT_NEUTRAL), player.netname);
310 else if(tmp_player == player)
311 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_RECEIVED_) : CENTER_CTF_PASS_RECEIVED_NEUTRAL), sender.netname);
312 else if(SAME_TEAM(tmp_player, sender))
313 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_OTHER_) : CENTER_CTF_PASS_OTHER_NEUTRAL), sender.netname, player.netname);
316 // create new waypoint
317 ctf_FlagcarrierWaypoints(player);
319 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
320 player.throw_antispam = sender.throw_antispam;
322 flag.pass_distance = 0;
323 flag.pass_sender = world;
324 flag.pass_target = world;
327 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
329 entity flag = player.flagcarried;
330 vector targ_origin, flag_velocity;
332 if(!flag) { return; }
333 if((droptype == DROP_PASS) && !receiver) { return; }
335 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
338 setattachment(flag, world, "");
339 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
340 flag.owner.flagcarried = world;
342 flag.solid = SOLID_TRIGGER;
343 flag.ctf_dropper = player;
344 flag.ctf_droptime = time;
346 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
353 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
354 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
355 WarpZone_RefSys_Copy(flag, receiver);
356 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
357 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
359 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
360 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
363 flag.movetype = MOVETYPE_FLY;
364 flag.takedamage = DAMAGE_NO;
365 flag.pass_sender = player;
366 flag.pass_target = receiver;
367 flag.ctf_status = FLAG_PASSING;
370 sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
371 WarpZone_TrailParticles(world, particleeffectnum(flag.passeffect), player.origin, targ_origin);
372 ctf_EventLog("pass", flag.team, player);
378 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'));
380 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)));
381 flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, false);
382 ctf_Handle_Drop(flag, player, droptype);
388 flag.velocity = '0 0 0'; // do nothing
395 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);
396 ctf_Handle_Drop(flag, player, droptype);
401 // kill old waypointsprite
402 WaypointSprite_Ping(player.wps_flagcarrier);
403 WaypointSprite_Kill(player.wps_flagcarrier);
405 if(player.wps_enemyflagcarrier)
406 WaypointSprite_Kill(player.wps_enemyflagcarrier);
409 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
417 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
419 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
420 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
421 entity player_team_flag = world, tmp_entity;
422 float old_time, new_time;
424 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
425 if(CTF_DIFFTEAM(player, flag)) { return; }
428 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
429 if(SAME_TEAM(tmp_entity, player))
431 player_team_flag = tmp_entity;
435 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
437 player.throw_prevtime = time;
438 player.throw_count = 0;
440 // messages and sounds
441 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((enemy_flag.team) ? APP_TEAM_ENT_4(enemy_flag, CENTER_CTF_CAPTURE_) : CENTER_CTF_CAPTURE_NEUTRAL));
442 ctf_CaptureRecord(enemy_flag, player);
443 sound(player, CH_TRIGGER, ((ctf_oneflag) ? player_team_flag.snd_flag_capture : ((DIFF_TEAM(player, flag)) ? enemy_flag.snd_flag_capture : flag.snd_flag_capture)), VOL_BASE, ATTEN_NONE);
447 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
448 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
453 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
454 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
456 old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
457 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
458 if(!old_time || new_time < old_time)
459 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
462 pointparticles(particleeffectnum(flag.capeffect), flag.origin, '0 0 0', 1);
463 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
466 if(capturetype == CAPTURE_NORMAL)
468 WaypointSprite_Kill(player.wps_flagcarrier);
469 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
471 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
472 { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
476 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
477 ctf_RespawnFlag(enemy_flag);
480 void ctf_Handle_Return(entity flag, entity player)
482 // messages and sounds
483 if(player.flags & FL_MONSTER)
485 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_MONSTER_), player.monster_name);
489 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_RETURN_));
490 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 if(IS_PLAYER(player))
498 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
499 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
501 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
504 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
508 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
509 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
510 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
514 if(player.flagcarried == flag)
515 WaypointSprite_Kill(player.wps_flagcarrier);
518 ctf_RespawnFlag(flag);
521 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
524 float pickup_dropped_score; // used to calculate dropped pickup score
525 entity tmp_entity; // temporary entity
527 // attach the flag to the player
529 player.flagcarried = flag;
532 setattachment(flag, player.vehicle, "");
533 setorigin(flag, VEHICLE_FLAG_OFFSET);
534 flag.scale = VEHICLE_FLAG_SCALE;
538 setattachment(flag, player, "");
539 setorigin(flag, FLAG_CARRY_OFFSET);
543 flag.movetype = MOVETYPE_NONE;
544 flag.takedamage = DAMAGE_NO;
545 flag.solid = SOLID_NOT;
546 flag.angles = '0 0 0';
547 flag.ctf_status = FLAG_CARRY;
551 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
552 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
556 // messages and sounds
557 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_PICKUP_) : INFO_CTF_PICKUP_NEUTRAL), player.netname);
558 if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
559 if(!flag.team) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL); }
560 else if(CTF_DIFFTEAM(player, flag)) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PICKUP_)); }
561 else { Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_TEAM : CENTER_CTF_PICKUP_TEAM_ENEMY), Team_ColorCode(flag.team)); }
563 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, ((flag.team) ? APP_TEAM_ENT_4(flag, CHOICE_CTF_PICKUP_TEAM_) : CHOICE_CTF_PICKUP_TEAM_NEUTRAL), Team_ColorCode(player.team), player.netname);
566 FOR_EACH_PLAYER(tmp_entity)
567 if(tmp_entity != player)
568 if(DIFF_TEAM(player, tmp_entity))
569 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, CHOICE_CTF_PICKUP_ENEMY_NEUTRAL, Team_ColorCode(player.team), player.netname);
572 FOR_EACH_PLAYER(tmp_entity)
573 if(tmp_entity != player)
574 if(CTF_SAMETEAM(flag, tmp_entity))
575 if(SAME_TEAM(player, tmp_entity))
576 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_PICKUP_TEAM_), Team_ColorCode(player.team), player.netname);
578 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, ((SAME_TEAM(flag, player)) ? CHOICE_CTF_PICKUP_ENEMY_TEAM : CHOICE_CTF_PICKUP_ENEMY), Team_ColorCode(player.team), player.netname);
580 sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
583 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
584 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
589 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
590 ctf_EventLog("steal", flag.team, player);
596 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);
597 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);
598 dprint("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
599 PlayerTeamScore_AddScore(player, pickup_dropped_score);
600 ctf_EventLog("pickup", flag.team, player);
608 if(pickuptype == PICKUP_BASE)
610 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
611 if((player.speedrunning) && (ctf_captimerecord))
612 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
616 pointparticles(particleeffectnum(flag.toucheffect), player.origin, '0 0 0', 1);
619 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
620 ctf_FlagcarrierWaypoints(player);
621 WaypointSprite_Ping(player.wps_flagcarrier);
625 // ===================
626 // Main Flag Functions
627 // ===================
629 void ctf_CheckFlagReturn(entity flag, int returntype)
631 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
633 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
635 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
639 case RETURN_DROPPED: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DROPPED_) : INFO_CTF_FLAGRETURN_DROPPED_NEUTRAL)); break;
640 case RETURN_DAMAGE: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DAMAGED_) : INFO_CTF_FLAGRETURN_DAMAGED_NEUTRAL)); break;
641 case RETURN_SPEEDRUN: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_SPEEDRUN_) : INFO_CTF_FLAGRETURN_SPEEDRUN_NEUTRAL), ctf_captimerecord); break;
642 case RETURN_NEEDKILL: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_NEEDKILL_) : INFO_CTF_FLAGRETURN_NEEDKILL_NEUTRAL)); break;
646 { Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_TIMEOUT_) : INFO_CTF_FLAGRETURN_TIMEOUT_NEUTRAL)); break; }
648 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
649 ctf_EventLog("returned", flag.team, world);
650 ctf_RespawnFlag(flag);
655 bool ctf_Stalemate_Customize()
657 // make spectators see what the player would see
659 e = WaypointSprite_getviewentity(other);
660 wp_owner = self.owner;
663 if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
664 if(SAME_TEAM(wp_owner, e)) { return false; }
665 if(!IS_PLAYER(e)) { return false; }
670 void ctf_CheckStalemate(void)
673 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
676 entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
678 // build list of stale flags
679 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
681 if(autocvar_g_ctf_stalemate)
682 if(tmp_entity.ctf_status != FLAG_BASE)
683 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
685 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
686 ctf_staleflaglist = tmp_entity;
688 switch(tmp_entity.team)
690 case NUM_TEAM_1: ++stale_red_flags; break;
691 case NUM_TEAM_2: ++stale_blue_flags; break;
692 case NUM_TEAM_3: ++stale_yellow_flags; break;
693 case NUM_TEAM_4: ++stale_pink_flags; break;
694 default: ++stale_neutral_flags; break;
700 stale_flags = (stale_neutral_flags >= 1);
702 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
704 if(ctf_oneflag && stale_flags == 1)
705 ctf_stalemate = true;
706 else if(stale_flags >= 2)
707 ctf_stalemate = true;
708 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
709 { ctf_stalemate = false; wpforenemy_announced = false; }
710 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
711 { ctf_stalemate = false; wpforenemy_announced = false; }
713 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
716 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
718 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
720 WaypointSprite_Spawn(((ctf_oneflag) ? "flagcarrier" : "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));
721 tmp_entity.owner.wps_enemyflagcarrier.customizeentityforclient = ctf_Stalemate_Customize;
725 if (!wpforenemy_announced)
727 FOR_EACH_REALPLAYER(tmp_entity)
728 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CENTER, ((tmp_entity.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER));
730 wpforenemy_announced = true;
735 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
737 if(ITEM_DAMAGE_NEEDKILL(deathtype))
739 if(autocvar_g_ctf_flag_return_damage_delay)
741 self.ctf_flagdamaged = true;
746 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
750 if(autocvar_g_ctf_flag_return_damage)
752 // reduce health and check if it should be returned
753 self.health = self.health - damage;
754 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
764 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
767 if(self == ctf_worldflaglist) // only for the first flag
768 FOR_EACH_CLIENT(tmp_entity)
769 ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
772 if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
773 dprint("wtf the flag got squashed?\n");
774 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
775 if(!trace_startsolid || self.noalign) // can we resize it without getting stuck?
776 setsize(self, FLAG_MIN, FLAG_MAX); }
778 switch(self.ctf_status) // reset flag angles in case warpzones adjust it
782 self.angles = '0 0 0';
790 switch(self.ctf_status)
794 if(autocvar_g_ctf_dropped_capture_radius)
796 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
797 if(tmp_entity.ctf_status == FLAG_DROPPED)
798 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
799 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
800 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
807 if(autocvar_g_ctf_flag_dropped_floatinwater)
809 vector midpoint = ((self.absmin + self.absmax) * 0.5);
810 if(pointcontents(midpoint) == CONTENT_WATER)
812 self.velocity = self.velocity * 0.5;
814 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
815 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
817 { self.movetype = MOVETYPE_FLY; }
819 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
821 if(autocvar_g_ctf_flag_return_dropped)
823 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
826 ctf_CheckFlagReturn(self, RETURN_DROPPED);
830 if(self.ctf_flagdamaged)
832 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
833 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
836 else if(autocvar_g_ctf_flag_return_time)
838 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
839 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
847 if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
850 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
854 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
858 if(autocvar_g_ctf_stalemate)
860 if(time >= wpforenemy_nextthink)
862 ctf_CheckStalemate();
863 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
866 if(CTF_SAMETEAM(self, self.owner) && self.team)
868 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
869 ctf_Handle_Throw(self.owner, world, DROP_THROW);
870 else if(vlen(self.owner.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_carried_radius)
871 ctf_Handle_Return(self, self.owner);
878 vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
879 targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
880 WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
882 if((self.pass_target == world)
883 || (self.pass_target.deadflag != DEAD_NO)
884 || (self.pass_target.flagcarried)
885 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
886 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
887 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
889 // give up, pass failed
890 ctf_Handle_Drop(self, world, DROP_PASS);
894 // still a viable target, go for it
895 ctf_CalculatePassVelocity(self, targ_origin, self.origin, true);
900 default: // this should never happen
902 dprint("ctf_FlagThink(): Flag exists with no status?\n");
910 if(gameover) { return; }
911 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
913 entity toucher = other, tmp_entity;
914 bool is_not_monster = (!(toucher.flags & FL_MONSTER)), num_perteam = 0;
916 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
917 if(ITEM_TOUCH_NEEDKILL())
919 if(!autocvar_g_ctf_flag_return_damage_delay)
922 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
924 if(!self.ctf_flagdamaged) { return; }
927 FOR_EACH_PLAYER(tmp_entity) if(SAME_TEAM(toucher, tmp_entity)) { ++num_perteam; }
929 // special touch behaviors
930 if(toucher.frozen) { return; }
931 else if(toucher.vehicle_flags & VHF_ISVEHICLE)
933 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
934 toucher = toucher.owner; // the player is actually the vehicle owner, not other
936 return; // do nothing
938 else if(toucher.flags & FL_MONSTER)
940 if(!autocvar_g_ctf_allow_monster_touch)
941 return; // do nothing
943 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
945 if(time > self.wait) // if we haven't in a while, play a sound/effect
947 pointparticles(particleeffectnum(self.toucheffect), self.origin, '0 0 0', 1);
948 sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTEN_NORM);
949 self.wait = time + FLAG_TOUCHRATE;
953 else if(toucher.deadflag != DEAD_NO) { return; }
955 switch(self.ctf_status)
961 if(CTF_SAMETEAM(toucher, self) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
962 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
963 else if(!self.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
964 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the neutral flag
966 else if(CTF_SAMETEAM(toucher, self) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, self) && is_not_monster)
967 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
968 else if(CTF_DIFFTEAM(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
969 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
975 if(CTF_SAMETEAM(toucher, self) && (autocvar_g_ctf_flag_return || num_perteam <= 1) && self.team) // automatically return if there's only 1 player on the team
976 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
977 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
978 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
984 dprint("Someone touched a flag even though it was being carried?\n");
990 if((IS_PLAYER(toucher)) && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
992 if(DIFF_TEAM(toucher, self.pass_sender))
993 ctf_Handle_Return(self, toucher);
995 ctf_Handle_Retrieve(self, toucher);
1002 .float last_respawn;
1003 void ctf_RespawnFlag(entity flag)
1005 // check for flag respawn being called twice in a row
1006 if(flag.last_respawn > time - 0.5)
1007 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1009 flag.last_respawn = time;
1011 // reset the player (if there is one)
1012 if((flag.owner) && (flag.owner.flagcarried == flag))
1014 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1015 WaypointSprite_Kill(flag.wps_flagcarrier);
1017 flag.owner.flagcarried = world;
1019 if(flag.speedrunning)
1020 ctf_FakeTimeLimit(flag.owner, -1);
1023 if((flag.owner) && (flag.owner.vehicle))
1024 flag.scale = FLAG_SCALE;
1026 if(flag.ctf_status == FLAG_DROPPED)
1027 { WaypointSprite_Kill(flag.wps_flagdropped); }
1030 setattachment(flag, world, "");
1031 setorigin(flag, flag.ctf_spawnorigin);
1033 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
1034 flag.takedamage = DAMAGE_NO;
1035 flag.health = flag.max_flag_health;
1036 flag.solid = SOLID_TRIGGER;
1037 flag.velocity = '0 0 0';
1038 flag.angles = flag.mangle;
1039 flag.flags = FL_ITEM | FL_NOTARGET;
1041 flag.ctf_status = FLAG_BASE;
1043 flag.pass_distance = 0;
1044 flag.pass_sender = world;
1045 flag.pass_target = world;
1046 flag.ctf_dropper = world;
1047 flag.ctf_pickuptime = 0;
1048 flag.ctf_droptime = 0;
1049 flag.ctf_flagdamaged = 0;
1051 ctf_CheckStalemate();
1057 if(IS_PLAYER(self.owner))
1058 ctf_Handle_Throw(self.owner, world, DROP_RESET);
1060 ctf_RespawnFlag(self);
1063 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
1066 waypoint_spawnforitem_force(self, self.origin);
1067 self.nearestwaypointtimeout = 0; // activate waypointing again
1068 self.bot_basewaypoint = self.nearestwaypoint;
1071 string basename = "base";
1075 case NUM_TEAM_1: basename = "redbase"; break;
1076 case NUM_TEAM_2: basename = "bluebase"; break;
1077 case NUM_TEAM_3: basename = "yellowbase"; break;
1078 case NUM_TEAM_4: basename = "pinkbase"; break;
1079 default: basename = "neutralbase"; break;
1082 WaypointSprite_SpawnFixed(basename, self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, ((self.team) ? Team_ColorRGB(self.team) : '1 1 1'));
1083 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, ((self.team) ? colormapPaletteColor(self.team - 1, false) : '1 1 1'));
1085 // captureshield setup
1086 ctf_CaptureShield_Spawn(self);
1089 void set_flag_string(entity flag, .string field, string value, string teamname)
1091 if(flag.field == "")
1092 flag.field = strzone(sprintf(value,teamname));
1095 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1098 string teamname = Static_Team_ColorName_Lower(teamnumber);
1099 self = flag; // for later usage with droptofloor()
1102 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1103 ctf_worldflaglist = flag;
1105 setattachment(flag, world, "");
1107 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1108 flag.team = teamnumber;
1109 flag.classname = "item_flag_team";
1110 flag.target = "###item###"; // wut?
1111 flag.flags = FL_ITEM | FL_NOTARGET;
1112 flag.solid = SOLID_TRIGGER;
1113 flag.takedamage = DAMAGE_NO;
1114 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1115 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1116 flag.health = flag.max_flag_health;
1117 flag.event_damage = ctf_FlagDamage;
1118 flag.pushable = true;
1119 flag.teleportable = TELEPORT_NORMAL;
1120 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP;
1121 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1122 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1123 flag.velocity = '0 0 0';
1124 flag.mangle = flag.angles;
1125 flag.reset = ctf_Reset;
1126 flag.touch = ctf_FlagTouch;
1127 flag.think = ctf_FlagThink;
1128 flag.nextthink = time + FLAG_THINKRATE;
1129 flag.ctf_status = FLAG_BASE;
1132 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1133 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1134 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1135 set_flag_string(flag, toucheffect, "%sflag_touch", teamname);
1136 set_flag_string(flag, passeffect, "%sflag_pass", teamname);
1137 set_flag_string(flag, capeffect, "%sflag_cap", teamname);
1140 set_flag_string(flag, snd_flag_taken, "ctf/%s_taken.wav", teamname);
1141 set_flag_string(flag, snd_flag_returned, "ctf/%s_returned.wav", teamname);
1142 set_flag_string(flag, snd_flag_capture, "ctf/%s_capture.wav", teamname);
1143 set_flag_string(flag, snd_flag_dropped, "ctf/%s_dropped.wav", teamname);
1144 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.
1145 if(flag.snd_flag_touch == "") { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
1146 if(flag.snd_flag_pass == "") { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
1149 precache_sound(flag.snd_flag_taken);
1150 precache_sound(flag.snd_flag_returned);
1151 precache_sound(flag.snd_flag_capture);
1152 precache_sound(flag.snd_flag_respawn);
1153 precache_sound(flag.snd_flag_dropped);
1154 precache_sound(flag.snd_flag_touch);
1155 precache_sound(flag.snd_flag_pass);
1156 precache_model(flag.model);
1157 precache_model("models/ctf/shield.md3");
1158 precache_model("models/ctf/shockwavetransring.md3");
1161 setmodel(flag, flag.model); // precision set below
1162 setsize(flag, FLAG_MIN, FLAG_MAX);
1163 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1165 if(autocvar_g_ctf_flag_glowtrails)
1169 case NUM_TEAM_1: flag.glow_color = 251; break;
1170 case NUM_TEAM_2: flag.glow_color = 210; break;
1171 case NUM_TEAM_3: flag.glow_color = 110; break;
1172 case NUM_TEAM_4: flag.glow_color = 145; break;
1173 default: flag.glow_color = 254; break;
1175 flag.glow_size = 25;
1176 flag.glow_trail = 1;
1179 flag.effects |= EF_LOWPRECISION;
1180 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1181 if(autocvar_g_ctf_dynamiclights)
1185 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1186 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1187 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1188 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1189 default: flag.effects |= EF_DIMLIGHT; break;
1194 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1196 flag.dropped_origin = flag.origin;
1197 flag.noalign = true;
1198 flag.movetype = MOVETYPE_NONE;
1200 else // drop to floor, automatically find a platform and set that as spawn origin
1202 flag.noalign = false;
1205 flag.movetype = MOVETYPE_TOSS;
1208 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1216 // NOTE: LEGACY CODE, needs to be re-written!
1218 void havocbot_calculate_middlepoint()
1222 vector fo = '0 0 0';
1225 f = ctf_worldflaglist;
1230 f = f.ctf_worldflagnext;
1234 havocbot_ctf_middlepoint = s * (1.0 / n);
1235 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1239 entity havocbot_ctf_find_flag(entity bot)
1242 f = ctf_worldflaglist;
1245 if (CTF_SAMETEAM(bot, f))
1247 f = f.ctf_worldflagnext;
1252 entity havocbot_ctf_find_enemy_flag(entity bot)
1255 f = ctf_worldflaglist;
1260 if(CTF_DIFFTEAM(bot, f))
1267 else if(!bot.flagcarried)
1271 else if (CTF_DIFFTEAM(bot, f))
1273 f = f.ctf_worldflagnext;
1278 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1286 FOR_EACH_PLAYER(head)
1288 if(DIFF_TEAM(head, bot) || head.deadflag != DEAD_NO || head == bot)
1291 if(vlen(head.origin - org) < tc_radius)
1298 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1301 head = ctf_worldflaglist;
1304 if (CTF_SAMETEAM(self, head))
1306 head = head.ctf_worldflagnext;
1309 navigation_routerating(head, ratingscale, 10000);
1312 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1315 head = ctf_worldflaglist;
1318 if (CTF_SAMETEAM(self, head))
1320 head = head.ctf_worldflagnext;
1325 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1328 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1331 head = ctf_worldflaglist;
1336 if(CTF_DIFFTEAM(self, head))
1340 if(self.flagcarried)
1343 else if(!self.flagcarried)
1347 else if(CTF_DIFFTEAM(self, head))
1349 head = head.ctf_worldflagnext;
1352 navigation_routerating(head, ratingscale, 10000);
1355 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1357 if (!bot_waypoints_for_items)
1359 havocbot_goalrating_ctf_enemyflag(ratingscale);
1365 head = havocbot_ctf_find_enemy_flag(self);
1370 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1373 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1377 mf = havocbot_ctf_find_flag(self);
1379 if(mf.ctf_status == FLAG_BASE)
1383 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1386 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1389 head = ctf_worldflaglist;
1392 // flag is out in the field
1393 if(head.ctf_status != FLAG_BASE)
1394 if(head.tag_entity==world) // dropped
1398 if(vlen(org-head.origin)<df_radius)
1399 navigation_routerating(head, ratingscale, 10000);
1402 navigation_routerating(head, ratingscale, 10000);
1405 head = head.ctf_worldflagnext;
1409 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1413 head = findchainfloat(bot_pickup, true);
1416 // gather health and armor only
1418 if (head.health || head.armorvalue)
1419 if (vlen(head.origin - org) < sradius)
1421 // get the value of the item
1422 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1424 navigation_routerating(head, t * ratingscale, 500);
1430 void havocbot_ctf_reset_role(entity bot)
1432 float cdefense, cmiddle, coffense;
1433 entity mf, ef, head;
1436 if(bot.deadflag != DEAD_NO)
1439 if(vlen(havocbot_ctf_middlepoint)==0)
1440 havocbot_calculate_middlepoint();
1443 if (bot.flagcarried)
1445 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1449 mf = havocbot_ctf_find_flag(bot);
1450 ef = havocbot_ctf_find_enemy_flag(bot);
1452 // Retrieve stolen flag
1453 if(mf.ctf_status!=FLAG_BASE)
1455 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1459 // If enemy flag is taken go to the middle to intercept pursuers
1460 if(ef.ctf_status!=FLAG_BASE)
1462 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1466 // if there is only me on the team switch to offense
1468 FOR_EACH_PLAYER(head)
1469 if(SAME_TEAM(head, bot))
1474 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1478 // Evaluate best position to take
1479 // Count mates on middle position
1480 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1482 // Count mates on defense position
1483 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1485 // Count mates on offense position
1486 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1488 if(cdefense<=coffense)
1489 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1490 else if(coffense<=cmiddle)
1491 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1493 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1496 void havocbot_role_ctf_carrier()
1498 if(self.deadflag != DEAD_NO)
1500 havocbot_ctf_reset_role(self);
1504 if (self.flagcarried == world)
1506 havocbot_ctf_reset_role(self);
1510 if (self.bot_strategytime < time)
1512 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1514 navigation_goalrating_start();
1515 havocbot_goalrating_ctf_ourbase(50000);
1518 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1520 navigation_goalrating_end();
1522 if (self.navigation_hasgoals)
1523 self.havocbot_cantfindflag = time + 10;
1524 else if (time > self.havocbot_cantfindflag)
1526 // Can't navigate to my own base, suicide!
1527 // TODO: drop it and wander around
1528 Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1534 void havocbot_role_ctf_escort()
1538 if(self.deadflag != DEAD_NO)
1540 havocbot_ctf_reset_role(self);
1544 if (self.flagcarried)
1546 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1550 // If enemy flag is back on the base switch to previous role
1551 ef = havocbot_ctf_find_enemy_flag(self);
1552 if(ef.ctf_status==FLAG_BASE)
1554 self.havocbot_role = self.havocbot_previous_role;
1555 self.havocbot_role_timeout = 0;
1559 // If the flag carrier reached the base switch to defense
1560 mf = havocbot_ctf_find_flag(self);
1561 if(mf.ctf_status!=FLAG_BASE)
1562 if(vlen(ef.origin - mf.dropped_origin) < 300)
1564 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1568 // Set the role timeout if necessary
1569 if (!self.havocbot_role_timeout)
1571 self.havocbot_role_timeout = time + random() * 30 + 60;
1574 // If nothing happened just switch to previous role
1575 if (time > self.havocbot_role_timeout)
1577 self.havocbot_role = self.havocbot_previous_role;
1578 self.havocbot_role_timeout = 0;
1582 // Chase the flag carrier
1583 if (self.bot_strategytime < time)
1585 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1586 navigation_goalrating_start();
1587 havocbot_goalrating_ctf_enemyflag(30000);
1588 havocbot_goalrating_ctf_ourstolenflag(40000);
1589 havocbot_goalrating_items(10000, self.origin, 10000);
1590 navigation_goalrating_end();
1594 void havocbot_role_ctf_offense()
1599 if(self.deadflag != DEAD_NO)
1601 havocbot_ctf_reset_role(self);
1605 if (self.flagcarried)
1607 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1612 mf = havocbot_ctf_find_flag(self);
1613 ef = havocbot_ctf_find_enemy_flag(self);
1616 if(mf.ctf_status!=FLAG_BASE)
1619 pos = mf.tag_entity.origin;
1623 // Try to get it if closer than the enemy base
1624 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1626 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1631 // Escort flag carrier
1632 if(ef.ctf_status!=FLAG_BASE)
1635 pos = ef.tag_entity.origin;
1639 if(vlen(pos-mf.dropped_origin)>700)
1641 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1646 // About to fail, switch to middlefield
1649 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1653 // Set the role timeout if necessary
1654 if (!self.havocbot_role_timeout)
1655 self.havocbot_role_timeout = time + 120;
1657 if (time > self.havocbot_role_timeout)
1659 havocbot_ctf_reset_role(self);
1663 if (self.bot_strategytime < time)
1665 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1666 navigation_goalrating_start();
1667 havocbot_goalrating_ctf_ourstolenflag(50000);
1668 havocbot_goalrating_ctf_enemybase(20000);
1669 havocbot_goalrating_items(5000, self.origin, 1000);
1670 havocbot_goalrating_items(1000, self.origin, 10000);
1671 navigation_goalrating_end();
1675 // Retriever (temporary role):
1676 void havocbot_role_ctf_retriever()
1680 if(self.deadflag != DEAD_NO)
1682 havocbot_ctf_reset_role(self);
1686 if (self.flagcarried)
1688 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1692 // If flag is back on the base switch to previous role
1693 mf = havocbot_ctf_find_flag(self);
1694 if(mf.ctf_status==FLAG_BASE)
1696 havocbot_ctf_reset_role(self);
1700 if (!self.havocbot_role_timeout)
1701 self.havocbot_role_timeout = time + 20;
1703 if (time > self.havocbot_role_timeout)
1705 havocbot_ctf_reset_role(self);
1709 if (self.bot_strategytime < time)
1714 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1715 navigation_goalrating_start();
1716 havocbot_goalrating_ctf_ourstolenflag(50000);
1717 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1718 havocbot_goalrating_ctf_enemybase(30000);
1719 havocbot_goalrating_items(500, self.origin, rt_radius);
1720 navigation_goalrating_end();
1724 void havocbot_role_ctf_middle()
1728 if(self.deadflag != DEAD_NO)
1730 havocbot_ctf_reset_role(self);
1734 if (self.flagcarried)
1736 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1740 mf = havocbot_ctf_find_flag(self);
1741 if(mf.ctf_status!=FLAG_BASE)
1743 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1747 if (!self.havocbot_role_timeout)
1748 self.havocbot_role_timeout = time + 10;
1750 if (time > self.havocbot_role_timeout)
1752 havocbot_ctf_reset_role(self);
1756 if (self.bot_strategytime < time)
1760 org = havocbot_ctf_middlepoint;
1761 org.z = self.origin.z;
1763 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1764 navigation_goalrating_start();
1765 havocbot_goalrating_ctf_ourstolenflag(50000);
1766 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1767 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1768 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1769 havocbot_goalrating_items(2500, self.origin, 10000);
1770 havocbot_goalrating_ctf_enemybase(2500);
1771 navigation_goalrating_end();
1775 void havocbot_role_ctf_defense()
1779 if(self.deadflag != DEAD_NO)
1781 havocbot_ctf_reset_role(self);
1785 if (self.flagcarried)
1787 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1791 // If own flag was captured
1792 mf = havocbot_ctf_find_flag(self);
1793 if(mf.ctf_status!=FLAG_BASE)
1795 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1799 if (!self.havocbot_role_timeout)
1800 self.havocbot_role_timeout = time + 30;
1802 if (time > self.havocbot_role_timeout)
1804 havocbot_ctf_reset_role(self);
1807 if (self.bot_strategytime < time)
1812 org = mf.dropped_origin;
1813 mp_radius = havocbot_ctf_middlepoint_radius;
1815 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1816 navigation_goalrating_start();
1818 // if enemies are closer to our base, go there
1819 entity head, closestplayer = world;
1820 float distance, bestdistance = 10000;
1821 FOR_EACH_PLAYER(head)
1823 if(head.deadflag!=DEAD_NO)
1826 distance = vlen(org - head.origin);
1827 if(distance<bestdistance)
1829 closestplayer = head;
1830 bestdistance = distance;
1835 if(DIFF_TEAM(closestplayer, self))
1836 if(vlen(org - self.origin)>1000)
1837 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1838 havocbot_goalrating_ctf_ourbase(30000);
1840 havocbot_goalrating_ctf_ourstolenflag(20000);
1841 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1842 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1843 havocbot_goalrating_items(10000, org, mp_radius);
1844 havocbot_goalrating_items(5000, self.origin, 10000);
1845 navigation_goalrating_end();
1849 void havocbot_role_ctf_setrole(entity bot, int role)
1851 dprint(strcat(bot.netname," switched to "));
1854 case HAVOCBOT_CTF_ROLE_CARRIER:
1856 bot.havocbot_role = havocbot_role_ctf_carrier;
1857 bot.havocbot_role_timeout = 0;
1858 bot.havocbot_cantfindflag = time + 10;
1859 bot.bot_strategytime = 0;
1861 case HAVOCBOT_CTF_ROLE_DEFENSE:
1863 bot.havocbot_role = havocbot_role_ctf_defense;
1864 bot.havocbot_role_timeout = 0;
1866 case HAVOCBOT_CTF_ROLE_MIDDLE:
1868 bot.havocbot_role = havocbot_role_ctf_middle;
1869 bot.havocbot_role_timeout = 0;
1871 case HAVOCBOT_CTF_ROLE_OFFENSE:
1873 bot.havocbot_role = havocbot_role_ctf_offense;
1874 bot.havocbot_role_timeout = 0;
1876 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1877 dprint("retriever");
1878 bot.havocbot_previous_role = bot.havocbot_role;
1879 bot.havocbot_role = havocbot_role_ctf_retriever;
1880 bot.havocbot_role_timeout = time + 10;
1881 bot.bot_strategytime = 0;
1883 case HAVOCBOT_CTF_ROLE_ESCORT:
1885 bot.havocbot_previous_role = bot.havocbot_role;
1886 bot.havocbot_role = havocbot_role_ctf_escort;
1887 bot.havocbot_role_timeout = time + 30;
1888 bot.bot_strategytime = 0;
1899 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1902 int t = 0, t2 = 0, t3 = 0;
1904 // initially clear items so they can be set as necessary later.
1905 self.ctf_flagstatus &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
1906 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
1907 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
1908 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
1909 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
1910 | CTF_FLAG_NEUTRAL | CTF_SHIELDED);
1912 // scan through all the flags and notify the client about them
1913 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1915 if(flag.team == NUM_TEAM_1) { t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
1916 if(flag.team == NUM_TEAM_2) { t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
1917 if(flag.team == NUM_TEAM_3) { t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
1918 if(flag.team == NUM_TEAM_4) { t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
1919 if(flag.team == 0) { t = CTF_NEUTRAL_FLAG_CARRYING; t2 = CTF_NEUTRAL_FLAG_TAKEN; t3 = CTF_NEUTRAL_FLAG_LOST; self.ctf_flagstatus |= CTF_FLAG_NEUTRAL; }
1921 switch(flag.ctf_status)
1926 if((flag.owner == self) || (flag.pass_sender == self))
1927 self.ctf_flagstatus |= t; // carrying: self is currently carrying the flag
1929 self.ctf_flagstatus |= t2; // taken: someone else is carrying the flag
1934 self.ctf_flagstatus |= t3; // lost: the flag is dropped somewhere on the map
1940 // item for stopping players from capturing the flag too often
1941 if(self.ctf_captureshielded)
1942 self.ctf_flagstatus |= CTF_SHIELDED;
1944 // update the health of the flag carrier waypointsprite
1945 if(self.wps_flagcarrier)
1946 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
1951 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1953 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1955 if(frag_target == frag_attacker) // damage done to yourself
1957 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1958 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1960 else // damage done to everyone else
1962 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1963 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1966 else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
1968 if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON)))
1969 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1971 frag_target.wps_helpme_time = time;
1972 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1974 // todo: add notification for when flag carrier needs help?
1979 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1981 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
1983 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1984 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1987 if(frag_target.flagcarried)
1989 entity tmp_entity = frag_target.flagcarried;
1990 ctf_Handle_Throw(frag_target, world, DROP_NORMAL);
1991 tmp_entity.ctf_dropper = world;
1997 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
2000 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2003 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
2005 entity flag; // temporary entity for the search method
2007 if(self.flagcarried)
2008 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2010 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2012 if(flag.pass_sender == self) { flag.pass_sender = world; }
2013 if(flag.pass_target == self) { flag.pass_target = world; }
2014 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
2020 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
2022 if(self.flagcarried)
2023 if(!autocvar_g_ctf_portalteleport)
2024 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2029 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
2031 if(MUTATOR_RETURNVALUE || gameover) { return false; }
2033 entity player = self;
2035 if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2037 // pass the flag to a team mate
2038 if(autocvar_g_ctf_pass)
2040 entity head, closest_target = world;
2041 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2043 while(head) // find the closest acceptable target to pass to
2045 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
2046 if(head != player && SAME_TEAM(head, player))
2047 if(!head.speedrunning && !head.vehicle)
2049 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2050 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2051 vector passer_center = CENTER_OR_VIEWOFS(player);
2053 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2055 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2057 if(IS_BOT_CLIENT(head))
2059 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2060 ctf_Handle_Throw(head, player, DROP_PASS);
2064 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2065 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2067 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2070 else if(player.flagcarried)
2074 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2075 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
2076 { closest_target = head; }
2078 else { closest_target = head; }
2085 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2088 // throw the flag in front of you
2089 if(autocvar_g_ctf_throw && player.flagcarried)
2091 if(player.throw_count == -1)
2093 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2095 player.throw_prevtime = time;
2096 player.throw_count = 1;
2097 ctf_Handle_Throw(player, world, DROP_THROW);
2102 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2108 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2109 else { player.throw_count += 1; }
2110 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2112 player.throw_prevtime = time;
2113 ctf_Handle_Throw(player, world, DROP_THROW);
2122 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
2124 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2126 self.wps_helpme_time = time;
2127 WaypointSprite_HelpMePing(self.wps_flagcarrier);
2129 else // create a normal help me waypointsprite
2131 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');
2132 WaypointSprite_Ping(self.wps_helpme);
2138 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
2140 if(vh_player.flagcarried)
2142 vh_player.flagcarried.nodrawtoclient = vh_player; // hide the flag from the driver
2144 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2146 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
2150 setattachment(vh_player.flagcarried, vh_vehicle, "");
2151 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
2152 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2153 //vh_player.flagcarried.angles = '0 0 0';
2161 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
2163 if(vh_player.flagcarried)
2165 setattachment(vh_player.flagcarried, vh_player, "");
2166 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2167 vh_player.flagcarried.scale = FLAG_SCALE;
2168 vh_player.flagcarried.angles = '0 0 0';
2169 vh_player.flagcarried.nodrawtoclient = world;
2176 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
2178 if(self.flagcarried)
2180 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((self.flagcarried.team) ? APP_TEAM_ENT_4(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN_) : INFO_CTF_FLAGRETURN_ABORTRUN_NEUTRAL));
2181 ctf_RespawnFlag(self.flagcarried);
2188 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
2190 entity flag; // temporary entity for the search method
2192 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2194 switch(flag.ctf_status)
2199 // lock the flag, game is over
2200 flag.movetype = MOVETYPE_NONE;
2201 flag.takedamage = DAMAGE_NO;
2202 flag.solid = SOLID_NOT;
2203 flag.nextthink = false; // stop thinking
2205 //dprint("stopping the ", flag.netname, " from moving.\n");
2213 // do nothing for these flags
2222 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
2224 havocbot_ctf_reset_role(self);
2228 MUTATOR_HOOKFUNCTION(ctf_GetTeamCount)
2230 //ret_float = ctf_teams;
2231 ret_string = "ctf_team";
2235 MUTATOR_HOOKFUNCTION(ctf_SpectateCopy)
2237 self.ctf_flagstatus = other.ctf_flagstatus;
2246 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
2247 CTF Starting point for a player in team one (Red).
2248 Keys: "angle" viewing angle when spawning. */
2249 void spawnfunc_info_player_team1()
2251 if(g_assault) { remove(self); return; }
2253 self.team = NUM_TEAM_1; // red
2254 spawnfunc_info_player_deathmatch();
2258 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
2259 CTF Starting point for a player in team two (Blue).
2260 Keys: "angle" viewing angle when spawning. */
2261 void spawnfunc_info_player_team2()
2263 if(g_assault) { remove(self); return; }
2265 self.team = NUM_TEAM_2; // blue
2266 spawnfunc_info_player_deathmatch();
2269 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
2270 CTF Starting point for a player in team three (Yellow).
2271 Keys: "angle" viewing angle when spawning. */
2272 void spawnfunc_info_player_team3()
2274 if(g_assault) { remove(self); return; }
2276 self.team = NUM_TEAM_3; // yellow
2277 spawnfunc_info_player_deathmatch();
2281 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
2282 CTF Starting point for a player in team four (Purple).
2283 Keys: "angle" viewing angle when spawning. */
2284 void spawnfunc_info_player_team4()
2286 if(g_assault) { remove(self); return; }
2288 self.team = NUM_TEAM_4; // purple
2289 spawnfunc_info_player_deathmatch();
2292 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2293 CTF flag for team one (Red).
2295 "angle" Angle the flag will point (minus 90 degrees)...
2296 "model" model to use, note this needs red and blue as skins 0 and 1...
2297 "noise" sound played when flag is picked up...
2298 "noise1" sound played when flag is returned by a teammate...
2299 "noise2" sound played when flag is captured...
2300 "noise3" sound played when flag is lost in the field and respawns itself...
2301 "noise4" sound played when flag is dropped by a player...
2302 "noise5" sound played when flag touches the ground... */
2303 void spawnfunc_item_flag_team1()
2305 if(!g_ctf) { remove(self); return; }
2307 ctf_FlagSetup(NUM_TEAM_1, self);
2310 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2311 CTF flag for team two (Blue).
2313 "angle" Angle the flag will point (minus 90 degrees)...
2314 "model" model to use, note this needs red and blue as skins 0 and 1...
2315 "noise" sound played when flag is picked up...
2316 "noise1" sound played when flag is returned by a teammate...
2317 "noise2" sound played when flag is captured...
2318 "noise3" sound played when flag is lost in the field and respawns itself...
2319 "noise4" sound played when flag is dropped by a player...
2320 "noise5" sound played when flag touches the ground... */
2321 void spawnfunc_item_flag_team2()
2323 if(!g_ctf) { remove(self); return; }
2325 ctf_FlagSetup(NUM_TEAM_2, self);
2328 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2329 CTF flag for team three (Yellow).
2331 "angle" Angle the flag will point (minus 90 degrees)...
2332 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2333 "noise" sound played when flag is picked up...
2334 "noise1" sound played when flag is returned by a teammate...
2335 "noise2" sound played when flag is captured...
2336 "noise3" sound played when flag is lost in the field and respawns itself...
2337 "noise4" sound played when flag is dropped by a player...
2338 "noise5" sound played when flag touches the ground... */
2339 void spawnfunc_item_flag_team3()
2341 if(!g_ctf) { remove(self); return; }
2343 ctf_FlagSetup(NUM_TEAM_3, self);
2346 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2347 CTF flag for team four (Pink).
2349 "angle" Angle the flag will point (minus 90 degrees)...
2350 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2351 "noise" sound played when flag is picked up...
2352 "noise1" sound played when flag is returned by a teammate...
2353 "noise2" sound played when flag is captured...
2354 "noise3" sound played when flag is lost in the field and respawns itself...
2355 "noise4" sound played when flag is dropped by a player...
2356 "noise5" sound played when flag touches the ground... */
2357 void spawnfunc_item_flag_team4()
2359 if(!g_ctf) { remove(self); return; }
2361 ctf_FlagSetup(NUM_TEAM_4, self);
2364 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2367 "angle" Angle the flag will point (minus 90 degrees)...
2368 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2369 "noise" sound played when flag is picked up...
2370 "noise1" sound played when flag is returned by a teammate...
2371 "noise2" sound played when flag is captured...
2372 "noise3" sound played when flag is lost in the field and respawns itself...
2373 "noise4" sound played when flag is dropped by a player...
2374 "noise5" sound played when flag touches the ground... */
2375 void spawnfunc_item_flag_neutral()
2377 if(!g_ctf) { remove(self); return; }
2378 if(!cvar("g_ctf_oneflag")) { remove(self); return; }
2380 ctf_FlagSetup(0, self);
2383 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2384 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2385 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.
2387 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2388 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2389 void spawnfunc_ctf_team()
2391 if(!g_ctf) { remove(self); return; }
2393 self.classname = "ctf_team";
2394 self.team = self.cnt + 1;
2397 // compatibility for quake maps
2398 void spawnfunc_team_CTF_redflag() { spawnfunc_item_flag_team1(); }
2399 void spawnfunc_team_CTF_blueflag() { spawnfunc_item_flag_team2(); }
2400 void spawnfunc_team_CTF_redplayer() { spawnfunc_info_player_team1(); }
2401 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2(); }
2402 void spawnfunc_team_CTF_redspawn() { spawnfunc_info_player_team1(); }
2403 void spawnfunc_team_CTF_bluespawn() { spawnfunc_info_player_team2(); }
2405 void team_CTF_neutralflag() { spawnfunc_item_flag_neutral(); }
2406 void team_neutralobelisk() { spawnfunc_item_flag_neutral(); }
2414 void ctf_ScoreRules(int teams)
2416 CheckAllowedTeams(world);
2417 ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
2418 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2419 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2420 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2421 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2422 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2423 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2424 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2425 ScoreRules_basics_end();
2428 // code from here on is just to support maps that don't have flag and team entities
2429 void ctf_SpawnTeam (string teamname, int teamcolor)
2434 self.classname = "ctf_team";
2435 self.netname = teamname;
2436 self.cnt = teamcolor;
2438 spawnfunc_ctf_team();
2443 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2448 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2450 if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2451 if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2452 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2455 ctf_teams = bound(2, ctf_teams, 4);
2457 // if no teams are found, spawn defaults
2458 if(find(world, classname, "ctf_team") == world)
2460 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2461 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2462 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2464 ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2466 ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2469 ctf_ScoreRules(ctf_teams);
2472 void ctf_Initialize()
2474 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2476 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2477 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2478 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2480 addstat(STAT_CTF_FLAGSTATUS, AS_INT, ctf_flagstatus);
2482 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2486 MUTATOR_DEFINITION(gamemode_ctf)
2488 MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2489 MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2490 MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2491 MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2492 MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2493 MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2494 MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2495 MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2496 MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2497 MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2498 MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2499 MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2500 MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2501 MUTATOR_HOOK(HavocBot_ChooseRole, ctf_BotRoles, CBC_ORDER_ANY);
2502 MUTATOR_HOOK(GetTeamCount, ctf_GetTeamCount, CBC_ORDER_ANY);
2503 MUTATOR_HOOK(SpectateCopy, ctf_SpectateCopy, CBC_ORDER_ANY);
2507 if(time > 1) // game loads at time 1
2508 error("This is a game type and it cannot be added at runtime.");
2512 MUTATOR_ONROLLBACK_OR_REMOVE
2514 // we actually cannot roll back ctf_Initialize here
2515 // BUT: we don't need to! If this gets called, adding always
2521 print("This is a game type and it cannot be removed at runtime.");