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, int flagteam, entity actor) // use an alias for easy changing and quick editing later
19 if(autocvar_sv_eventlog)
20 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != world) ? ftos(actor.playerid) : "")));
21 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
24 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 if(ctf_oneflag) { Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname); }
32 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)); }
33 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)); }
34 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)); }
36 // write that shit in the database
37 if(!ctf_oneflag) // but not in 1-flag mode
38 if((!ctf_captimerecord) || (cap_time < cap_record))
40 ctf_captimerecord = cap_time;
41 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
42 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
43 write_recordmarker(player, (time - cap_time), cap_time);
47 void ctf_FlagcarrierWaypoints(entity player)
49 WaypointSprite_Spawn("flagcarrier", 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, true, RADARICON_FLAG, WPCOLOR_FLAGCARRIER(player.team));
50 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON) * 2);
51 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
52 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
55 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
57 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
58 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
59 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
60 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
63 if(current_height) // make sure we can actually do this arcing path
65 targpos = (to + ('0 0 1' * current_height));
66 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
67 if(trace_fraction < 1)
69 //print("normal arc line failed, trying to find new pos...");
70 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
71 targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
72 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
73 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
74 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
77 else { targpos = to; }
79 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
81 vector desired_direction = normalize(targpos - from);
82 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
83 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
86 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
88 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
90 // directional tracing only
92 makevectors(passer_angle);
94 // find the closest point on the enemy to the center of the attack
95 float h; // hypotenuse, which is the distance between attacker to head
96 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
98 h = vlen(head_center - passer_center);
99 a = h * (normalize(head_center - passer_center) * v_forward);
101 vector nearest_on_line = (passer_center + a * v_forward);
102 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
104 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
105 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
107 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
112 else { return true; }
116 // =======================
117 // CaptureShield Functions
118 // =======================
120 bool ctf_CaptureShield_CheckStatus(entity p)
122 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
124 int players_worseeq, players_total;
126 if(ctf_captureshield_max_ratio <= 0)
129 s = PlayerScore_Add(p, SP_CTF_CAPS, 0);
130 s2 = PlayerScore_Add(p, SP_CTF_PICKUPS, 0);
131 s3 = PlayerScore_Add(p, SP_CTF_RETURNS, 0);
132 s4 = PlayerScore_Add(p, SP_CTF_FCKILLS, 0);
134 sr = ((s - s2) + (s3 + s4));
136 if(sr >= -ctf_captureshield_min_negscore)
139 players_total = players_worseeq = 0;
144 se = PlayerScore_Add(e, SP_CTF_CAPS, 0);
145 se2 = PlayerScore_Add(e, SP_CTF_PICKUPS, 0);
146 se3 = PlayerScore_Add(e, SP_CTF_RETURNS, 0);
147 se4 = PlayerScore_Add(e, SP_CTF_FCKILLS, 0);
149 ser = ((se - se2) + (se3 + se4));
156 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
157 // use this rule here
159 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
165 void ctf_CaptureShield_Update(entity player, bool wanted_status)
167 bool updated_status = ctf_CaptureShield_CheckStatus(player);
168 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
170 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
171 player.ctf_captureshielded = updated_status;
175 bool ctf_CaptureShield_Customize()
177 if(!other.ctf_captureshielded) { return false; }
178 if(CTF_SAMETEAM(self, other)) { return false; }
183 void ctf_CaptureShield_Touch()
185 if(!other.ctf_captureshielded) { return; }
186 if(CTF_SAMETEAM(self, other)) { return; }
188 vector mymid = (self.absmin + self.absmax) * 0.5;
189 vector othermid = (other.absmin + other.absmax) * 0.5;
191 Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
192 if(IS_REAL_CLIENT(other)) { Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
195 void ctf_CaptureShield_Spawn(entity flag)
197 entity shield = spawn();
200 shield.team = self.team;
201 shield.touch = ctf_CaptureShield_Touch;
202 shield.customizeentityforclient = ctf_CaptureShield_Customize;
203 shield.classname = "ctf_captureshield";
204 shield.effects = EF_ADDITIVE;
205 shield.movetype = MOVETYPE_NOCLIP;
206 shield.solid = SOLID_TRIGGER;
207 shield.avelocity = '7 0 11';
210 setorigin(shield, self.origin);
211 setmodel(shield, "models/ctf/shield.md3");
212 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
216 // ====================
217 // Drop/Pass/Throw Code
218 // ====================
220 void ctf_Handle_Drop(entity flag, entity player, int droptype)
223 player = (player ? player : flag.pass_sender);
226 flag.movetype = MOVETYPE_TOSS;
227 flag.takedamage = DAMAGE_YES;
228 flag.angles = '0 0 0';
229 flag.health = flag.max_flag_health;
230 flag.ctf_droptime = time;
231 flag.ctf_dropper = player;
232 flag.ctf_status = FLAG_DROPPED;
234 // messages and sounds
235 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_LOST_) : INFO_CTF_LOST_NEUTRAL), player.netname);
236 sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
237 ctf_EventLog("dropped", player.team, player);
240 PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);
241 PlayerScore_Add(player, SP_CTF_DROPS, 1);
244 if(autocvar_g_ctf_flag_dropped_waypoint)
245 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));
247 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
249 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
250 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
253 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
255 if(droptype == DROP_PASS)
257 flag.pass_distance = 0;
258 flag.pass_sender = world;
259 flag.pass_target = world;
263 void ctf_Handle_Retrieve(entity flag, entity player)
265 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
266 entity sender = flag.pass_sender;
268 // transfer flag to player
270 flag.owner.flagcarried = flag;
275 setattachment(flag, player.vehicle, "");
276 setorigin(flag, VEHICLE_FLAG_OFFSET);
277 flag.scale = VEHICLE_FLAG_SCALE;
281 setattachment(flag, player, "");
282 setorigin(flag, FLAG_CARRY_OFFSET);
284 flag.movetype = MOVETYPE_NONE;
285 flag.takedamage = DAMAGE_NO;
286 flag.solid = SOLID_NOT;
287 flag.angles = '0 0 0';
288 flag.ctf_status = FLAG_CARRY;
290 // messages and sounds
291 sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
292 ctf_EventLog("receive", flag.team, player);
294 FOR_EACH_REALPLAYER(tmp_player)
296 if(tmp_player == sender)
297 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);
298 else if(tmp_player == player)
299 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);
300 else if(SAME_TEAM(tmp_player, sender))
301 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);
304 // create new waypoint
305 ctf_FlagcarrierWaypoints(player);
307 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
308 player.throw_antispam = sender.throw_antispam;
310 flag.pass_distance = 0;
311 flag.pass_sender = world;
312 flag.pass_target = world;
315 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
317 entity flag = player.flagcarried;
318 vector targ_origin, flag_velocity;
320 if(!flag) { return; }
321 if((droptype == DROP_PASS) && !receiver) { return; }
323 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
326 setattachment(flag, world, "");
327 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
328 flag.owner.flagcarried = world;
330 flag.solid = SOLID_TRIGGER;
331 flag.ctf_dropper = player;
332 flag.ctf_droptime = time;
334 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
341 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
342 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
343 WarpZone_RefSys_Copy(flag, receiver);
344 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
345 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
347 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
348 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
351 flag.movetype = MOVETYPE_FLY;
352 flag.takedamage = DAMAGE_NO;
353 flag.pass_sender = player;
354 flag.pass_target = receiver;
355 flag.ctf_status = FLAG_PASSING;
358 sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
359 WarpZone_TrailParticles(world, particleeffectnum(flag.passeffect), player.origin, targ_origin);
360 ctf_EventLog("pass", flag.team, player);
366 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'));
368 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)));
369 flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, false);
370 ctf_Handle_Drop(flag, player, droptype);
376 flag.velocity = '0 0 0'; // do nothing
383 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);
384 ctf_Handle_Drop(flag, player, droptype);
389 // kill old waypointsprite
390 WaypointSprite_Ping(player.wps_flagcarrier);
391 WaypointSprite_Kill(player.wps_flagcarrier);
393 if(player.wps_enemyflagcarrier)
394 WaypointSprite_Kill(player.wps_enemyflagcarrier);
397 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
405 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
407 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
408 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
409 entity player_team_flag = world, tmp_entity;
410 float old_time, new_time;
412 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
413 if(CTF_DIFFTEAM(player, flag)) { return; }
416 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
417 if(SAME_TEAM(tmp_entity, player))
419 player_team_flag = tmp_entity;
423 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
425 player.throw_prevtime = time;
426 player.throw_count = 0;
428 // messages and sounds
429 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((enemy_flag.team) ? APP_TEAM_ENT_4(enemy_flag, CENTER_CTF_CAPTURE_) : CENTER_CTF_CAPTURE_NEUTRAL));
430 ctf_CaptureRecord(enemy_flag, player);
431 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);
435 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
436 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
441 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
442 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
444 old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
445 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
446 if(!old_time || new_time < old_time)
447 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
450 pointparticles(particleeffectnum(flag.capeffect), flag.origin, '0 0 0', 1);
451 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
454 if(capturetype == CAPTURE_NORMAL)
456 WaypointSprite_Kill(player.wps_flagcarrier);
457 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
459 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
460 { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
464 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
465 ctf_RespawnFlag(enemy_flag);
468 void ctf_Handle_Return(entity flag, entity player)
470 // messages and sounds
471 if(player.flags & FL_MONSTER)
473 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_MONSTER_), player.monster_name);
477 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_RETURN_));
478 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_), player.netname);
480 sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
481 ctf_EventLog("return", flag.team, player);
484 if(IS_PLAYER(player))
486 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
487 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
489 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
492 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
496 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
497 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
498 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
502 if(player.flagcarried == flag)
503 WaypointSprite_Kill(player.wps_flagcarrier);
506 ctf_RespawnFlag(flag);
509 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
512 float pickup_dropped_score; // used to calculate dropped pickup score
513 entity tmp_entity; // temporary entity
515 // attach the flag to the player
517 player.flagcarried = flag;
520 setattachment(flag, player.vehicle, "");
521 setorigin(flag, VEHICLE_FLAG_OFFSET);
522 flag.scale = VEHICLE_FLAG_SCALE;
526 setattachment(flag, player, "");
527 setorigin(flag, FLAG_CARRY_OFFSET);
531 flag.movetype = MOVETYPE_NONE;
532 flag.takedamage = DAMAGE_NO;
533 flag.solid = SOLID_NOT;
534 flag.angles = '0 0 0';
535 flag.ctf_status = FLAG_CARRY;
539 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
540 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
544 // messages and sounds
545 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_PICKUP_) : INFO_CTF_PICKUP_NEUTRAL), player.netname);
546 if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
547 if(!flag.team) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL); }
548 else if(CTF_DIFFTEAM(player, flag)) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PICKUP_)); }
549 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)); }
551 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);
554 FOR_EACH_PLAYER(tmp_entity)
555 if(tmp_entity != player)
556 if(DIFF_TEAM(player, tmp_entity))
557 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, CHOICE_CTF_PICKUP_ENEMY_NEUTRAL, Team_ColorCode(player.team), player.netname);
560 FOR_EACH_PLAYER(tmp_entity)
561 if(tmp_entity != player)
562 if(CTF_SAMETEAM(flag, tmp_entity))
563 if(SAME_TEAM(player, tmp_entity))
564 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_PICKUP_TEAM_), Team_ColorCode(player.team), player.netname);
566 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);
568 sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
571 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
572 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
577 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
578 ctf_EventLog("steal", flag.team, player);
584 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);
585 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);
586 dprint("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
587 PlayerTeamScore_AddScore(player, pickup_dropped_score);
588 ctf_EventLog("pickup", flag.team, player);
596 if(pickuptype == PICKUP_BASE)
598 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
599 if((player.speedrunning) && (ctf_captimerecord))
600 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
604 pointparticles(particleeffectnum(flag.toucheffect), player.origin, '0 0 0', 1);
607 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
608 ctf_FlagcarrierWaypoints(player);
609 WaypointSprite_Ping(player.wps_flagcarrier);
613 // ===================
614 // Main Flag Functions
615 // ===================
617 void ctf_CheckFlagReturn(entity flag, int returntype)
619 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
621 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
623 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
627 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;
628 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;
629 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;
630 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;
634 { Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_TIMEOUT_) : INFO_CTF_FLAGRETURN_TIMEOUT_NEUTRAL)); break; }
636 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
637 ctf_EventLog("returned", flag.team, world);
638 ctf_RespawnFlag(flag);
643 bool ctf_Stalemate_Customize()
645 // make spectators see what the player would see
647 e = WaypointSprite_getviewentity(other);
648 wp_owner = self.owner;
651 if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
652 if(SAME_TEAM(wp_owner, e)) { return false; }
653 if(!IS_PLAYER(e)) { return false; }
658 void ctf_CheckStalemate(void)
661 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
664 entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
666 // build list of stale flags
667 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
669 if(autocvar_g_ctf_stalemate)
670 if(tmp_entity.ctf_status != FLAG_BASE)
671 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
673 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
674 ctf_staleflaglist = tmp_entity;
676 switch(tmp_entity.team)
678 case NUM_TEAM_1: ++stale_red_flags; break;
679 case NUM_TEAM_2: ++stale_blue_flags; break;
680 case NUM_TEAM_3: ++stale_yellow_flags; break;
681 case NUM_TEAM_4: ++stale_pink_flags; break;
682 default: ++stale_neutral_flags; break;
688 stale_flags = (stale_neutral_flags >= 1);
690 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
692 if(ctf_oneflag && stale_flags == 1)
693 ctf_stalemate = true;
694 else if(stale_flags >= 2)
695 ctf_stalemate = true;
696 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
697 { ctf_stalemate = false; wpforenemy_announced = false; }
698 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
699 { ctf_stalemate = false; wpforenemy_announced = false; }
701 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
704 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
706 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
708 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));
709 tmp_entity.owner.wps_enemyflagcarrier.customizeentityforclient = ctf_Stalemate_Customize;
713 if (!wpforenemy_announced)
715 FOR_EACH_REALPLAYER(tmp_entity)
716 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CENTER, ((tmp_entity.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER));
718 wpforenemy_announced = true;
723 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
725 if(ITEM_DAMAGE_NEEDKILL(deathtype))
727 if(autocvar_g_ctf_flag_return_damage_delay)
729 self.ctf_flagdamaged = true;
734 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
738 if(autocvar_g_ctf_flag_return_damage)
740 // reduce health and check if it should be returned
741 self.health = self.health - damage;
742 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
752 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
755 if(self == ctf_worldflaglist) // only for the first flag
756 FOR_EACH_CLIENT(tmp_entity)
757 ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
760 if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
761 dprint("wtf the flag got squashed?\n");
762 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
763 if(!trace_startsolid || self.noalign) // can we resize it without getting stuck?
764 setsize(self, FLAG_MIN, FLAG_MAX); }
766 switch(self.ctf_status) // reset flag angles in case warpzones adjust it
770 self.angles = '0 0 0';
778 switch(self.ctf_status)
782 if(autocvar_g_ctf_dropped_capture_radius)
784 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
785 if(tmp_entity.ctf_status == FLAG_DROPPED)
786 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
787 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
788 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
795 if(autocvar_g_ctf_flag_dropped_floatinwater)
797 vector midpoint = ((self.absmin + self.absmax) * 0.5);
798 if(pointcontents(midpoint) == CONTENT_WATER)
800 self.velocity = self.velocity * 0.5;
802 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
803 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
805 { self.movetype = MOVETYPE_FLY; }
807 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
809 if(autocvar_g_ctf_flag_return_dropped)
811 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
814 ctf_CheckFlagReturn(self, RETURN_DROPPED);
818 if(self.ctf_flagdamaged)
820 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
821 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
824 else if(autocvar_g_ctf_flag_return_time)
826 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
827 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
835 if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
838 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
842 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
846 if(autocvar_g_ctf_stalemate)
848 if(time >= wpforenemy_nextthink)
850 ctf_CheckStalemate();
851 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
854 if(CTF_SAMETEAM(self, self.owner) && self.team)
856 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
857 ctf_Handle_Throw(self.owner, world, DROP_THROW);
858 else if(vlen(self.owner.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_carried_radius)
859 ctf_Handle_Return(self, self.owner);
866 vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
867 targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
868 WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
870 if((self.pass_target == world)
871 || (self.pass_target.deadflag != DEAD_NO)
872 || (self.pass_target.flagcarried)
873 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
874 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
875 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
877 // give up, pass failed
878 ctf_Handle_Drop(self, world, DROP_PASS);
882 // still a viable target, go for it
883 ctf_CalculatePassVelocity(self, targ_origin, self.origin, true);
888 default: // this should never happen
890 dprint("ctf_FlagThink(): Flag exists with no status?\n");
898 if(gameover) { return; }
899 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
901 entity toucher = other, tmp_entity;
902 bool is_not_monster = (!(toucher.flags & FL_MONSTER)), num_perteam = 0;
904 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
905 if(ITEM_TOUCH_NEEDKILL())
907 if(!autocvar_g_ctf_flag_return_damage_delay)
910 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
912 if(!self.ctf_flagdamaged) { return; }
915 FOR_EACH_PLAYER(tmp_entity) if(SAME_TEAM(toucher, tmp_entity)) { ++num_perteam; }
917 // special touch behaviors
918 if(toucher.frozen) { return; }
919 else if(toucher.vehicle_flags & VHF_ISVEHICLE)
921 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
922 toucher = toucher.owner; // the player is actually the vehicle owner, not other
924 return; // do nothing
926 else if(toucher.flags & FL_MONSTER)
928 if(!autocvar_g_ctf_allow_monster_touch)
929 return; // do nothing
931 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
933 if(time > self.wait) // if we haven't in a while, play a sound/effect
935 pointparticles(particleeffectnum(self.toucheffect), self.origin, '0 0 0', 1);
936 sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTEN_NORM);
937 self.wait = time + FLAG_TOUCHRATE;
941 else if(toucher.deadflag != DEAD_NO) { return; }
943 switch(self.ctf_status)
949 if(CTF_SAMETEAM(toucher, self) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
950 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
951 else if(!self.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
952 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the neutral flag
954 else if(CTF_SAMETEAM(toucher, self) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, self) && is_not_monster)
955 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
956 else if(CTF_DIFFTEAM(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
957 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
963 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
964 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
965 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
966 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
972 dprint("Someone touched a flag even though it was being carried?\n");
978 if((IS_PLAYER(toucher)) && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
980 if(DIFF_TEAM(toucher, self.pass_sender))
981 ctf_Handle_Return(self, toucher);
983 ctf_Handle_Retrieve(self, toucher);
991 void ctf_RespawnFlag(entity flag)
993 // check for flag respawn being called twice in a row
994 if(flag.last_respawn > time - 0.5)
995 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
997 flag.last_respawn = time;
999 // reset the player (if there is one)
1000 if((flag.owner) && (flag.owner.flagcarried == flag))
1002 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1003 WaypointSprite_Kill(flag.wps_flagcarrier);
1005 flag.owner.flagcarried = world;
1007 if(flag.speedrunning)
1008 ctf_FakeTimeLimit(flag.owner, -1);
1011 if((flag.owner) && (flag.owner.vehicle))
1012 flag.scale = FLAG_SCALE;
1014 if(flag.ctf_status == FLAG_DROPPED)
1015 { WaypointSprite_Kill(flag.wps_flagdropped); }
1018 setattachment(flag, world, "");
1019 setorigin(flag, flag.ctf_spawnorigin);
1021 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
1022 flag.takedamage = DAMAGE_NO;
1023 flag.health = flag.max_flag_health;
1024 flag.solid = SOLID_TRIGGER;
1025 flag.velocity = '0 0 0';
1026 flag.angles = flag.mangle;
1027 flag.flags = FL_ITEM | FL_NOTARGET;
1029 flag.ctf_status = FLAG_BASE;
1031 flag.pass_distance = 0;
1032 flag.pass_sender = world;
1033 flag.pass_target = world;
1034 flag.ctf_dropper = world;
1035 flag.ctf_pickuptime = 0;
1036 flag.ctf_droptime = 0;
1037 flag.ctf_flagdamaged = 0;
1039 ctf_CheckStalemate();
1045 if(IS_PLAYER(self.owner))
1046 ctf_Handle_Throw(self.owner, world, DROP_RESET);
1048 ctf_RespawnFlag(self);
1051 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
1054 waypoint_spawnforitem_force(self, self.origin);
1055 self.nearestwaypointtimeout = 0; // activate waypointing again
1056 self.bot_basewaypoint = self.nearestwaypoint;
1059 string basename = "base";
1063 case NUM_TEAM_1: basename = "redbase"; break;
1064 case NUM_TEAM_2: basename = "bluebase"; break;
1065 case NUM_TEAM_3: basename = "yellowbase"; break;
1066 case NUM_TEAM_4: basename = "pinkbase"; break;
1067 default: basename = "neutralbase"; break;
1070 WaypointSprite_SpawnFixed(basename, self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, ((self.team) ? Team_ColorRGB(self.team) : '1 1 1'));
1071 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, ((self.team) ? colormapPaletteColor(self.team - 1, false) : '1 1 1'));
1073 // captureshield setup
1074 ctf_CaptureShield_Spawn(self);
1077 void set_flag_string(entity flag, .string field, string value, string teamname)
1079 if(flag.field == "")
1080 flag.field = strzone(sprintf(value,teamname));
1083 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1086 string teamname = Static_Team_ColorName_Lower(teamnumber);
1087 self = flag; // for later usage with droptofloor()
1090 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1091 ctf_worldflaglist = flag;
1093 setattachment(flag, world, "");
1095 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1096 flag.team = teamnumber;
1097 flag.classname = "item_flag_team";
1098 flag.target = "###item###"; // wut?
1099 flag.flags = FL_ITEM | FL_NOTARGET;
1100 flag.solid = SOLID_TRIGGER;
1101 flag.takedamage = DAMAGE_NO;
1102 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1103 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1104 flag.health = flag.max_flag_health;
1105 flag.event_damage = ctf_FlagDamage;
1106 flag.pushable = true;
1107 flag.teleportable = TELEPORT_NORMAL;
1108 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP;
1109 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1110 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1111 flag.velocity = '0 0 0';
1112 flag.mangle = flag.angles;
1113 flag.reset = ctf_Reset;
1114 flag.touch = ctf_FlagTouch;
1115 flag.think = ctf_FlagThink;
1116 flag.nextthink = time + FLAG_THINKRATE;
1117 flag.ctf_status = FLAG_BASE;
1120 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1121 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1122 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1123 set_flag_string(flag, toucheffect, "%sflag_touch", teamname);
1124 set_flag_string(flag, passeffect, "%sflag_pass", teamname);
1125 set_flag_string(flag, capeffect, "%sflag_cap", teamname);
1128 set_flag_string(flag, snd_flag_taken, "ctf/%s_taken.wav", teamname);
1129 set_flag_string(flag, snd_flag_returned, "ctf/%s_returned.wav", teamname);
1130 set_flag_string(flag, snd_flag_capture, "ctf/%s_capture.wav", teamname);
1131 set_flag_string(flag, snd_flag_dropped, "ctf/%s_dropped.wav", teamname);
1132 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.
1133 if(flag.snd_flag_touch == "") { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
1134 if(flag.snd_flag_pass == "") { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
1137 precache_sound(flag.snd_flag_taken);
1138 precache_sound(flag.snd_flag_returned);
1139 precache_sound(flag.snd_flag_capture);
1140 precache_sound(flag.snd_flag_respawn);
1141 precache_sound(flag.snd_flag_dropped);
1142 precache_sound(flag.snd_flag_touch);
1143 precache_sound(flag.snd_flag_pass);
1144 precache_model(flag.model);
1145 precache_model("models/ctf/shield.md3");
1146 precache_model("models/ctf/shockwavetransring.md3");
1149 setmodel(flag, flag.model); // precision set below
1150 setsize(flag, FLAG_MIN, FLAG_MAX);
1151 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1153 if(autocvar_g_ctf_flag_glowtrails)
1157 case NUM_TEAM_1: flag.glow_color = 251; break;
1158 case NUM_TEAM_2: flag.glow_color = 210; break;
1159 case NUM_TEAM_3: flag.glow_color = 110; break;
1160 case NUM_TEAM_4: flag.glow_color = 145; break;
1161 default: flag.glow_color = 254; break;
1163 flag.glow_size = 25;
1164 flag.glow_trail = 1;
1167 flag.effects |= EF_LOWPRECISION;
1168 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1169 if(autocvar_g_ctf_dynamiclights)
1173 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1174 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1175 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1176 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1177 default: flag.effects |= EF_DIMLIGHT; break;
1182 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1184 flag.dropped_origin = flag.origin;
1185 flag.noalign = true;
1186 flag.movetype = MOVETYPE_NONE;
1188 else // drop to floor, automatically find a platform and set that as spawn origin
1190 flag.noalign = false;
1193 flag.movetype = MOVETYPE_TOSS;
1196 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1204 // NOTE: LEGACY CODE, needs to be re-written!
1206 void havocbot_calculate_middlepoint()
1210 vector fo = '0 0 0';
1213 f = ctf_worldflaglist;
1218 f = f.ctf_worldflagnext;
1222 havocbot_ctf_middlepoint = s * (1.0 / n);
1223 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1227 entity havocbot_ctf_find_flag(entity bot)
1230 f = ctf_worldflaglist;
1233 if (CTF_SAMETEAM(bot, f))
1235 f = f.ctf_worldflagnext;
1240 entity havocbot_ctf_find_enemy_flag(entity bot)
1243 f = ctf_worldflaglist;
1248 if(CTF_DIFFTEAM(bot, f))
1255 else if(!bot.flagcarried)
1259 else if (CTF_DIFFTEAM(bot, f))
1261 f = f.ctf_worldflagnext;
1266 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1274 FOR_EACH_PLAYER(head)
1276 if(DIFF_TEAM(head, bot) || head.deadflag != DEAD_NO || head == bot)
1279 if(vlen(head.origin - org) < tc_radius)
1286 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1289 head = ctf_worldflaglist;
1292 if (CTF_SAMETEAM(self, head))
1294 head = head.ctf_worldflagnext;
1297 navigation_routerating(head, ratingscale, 10000);
1300 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1303 head = ctf_worldflaglist;
1306 if (CTF_SAMETEAM(self, head))
1308 head = head.ctf_worldflagnext;
1313 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1316 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1319 head = ctf_worldflaglist;
1324 if(CTF_DIFFTEAM(self, head))
1328 if(self.flagcarried)
1331 else if(!self.flagcarried)
1335 else if(CTF_DIFFTEAM(self, head))
1337 head = head.ctf_worldflagnext;
1340 navigation_routerating(head, ratingscale, 10000);
1343 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1345 if (!bot_waypoints_for_items)
1347 havocbot_goalrating_ctf_enemyflag(ratingscale);
1353 head = havocbot_ctf_find_enemy_flag(self);
1358 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1361 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1365 mf = havocbot_ctf_find_flag(self);
1367 if(mf.ctf_status == FLAG_BASE)
1371 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1374 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1377 head = ctf_worldflaglist;
1380 // flag is out in the field
1381 if(head.ctf_status != FLAG_BASE)
1382 if(head.tag_entity==world) // dropped
1386 if(vlen(org-head.origin)<df_radius)
1387 navigation_routerating(head, ratingscale, 10000);
1390 navigation_routerating(head, ratingscale, 10000);
1393 head = head.ctf_worldflagnext;
1397 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1401 head = findchainfloat(bot_pickup, true);
1404 // gather health and armor only
1406 if (head.health || head.armorvalue)
1407 if (vlen(head.origin - org) < sradius)
1409 // get the value of the item
1410 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1412 navigation_routerating(head, t * ratingscale, 500);
1418 void havocbot_ctf_reset_role(entity bot)
1420 float cdefense, cmiddle, coffense;
1421 entity mf, ef, head;
1424 if(bot.deadflag != DEAD_NO)
1427 if(vlen(havocbot_ctf_middlepoint)==0)
1428 havocbot_calculate_middlepoint();
1431 if (bot.flagcarried)
1433 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1437 mf = havocbot_ctf_find_flag(bot);
1438 ef = havocbot_ctf_find_enemy_flag(bot);
1440 // Retrieve stolen flag
1441 if(mf.ctf_status!=FLAG_BASE)
1443 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1447 // If enemy flag is taken go to the middle to intercept pursuers
1448 if(ef.ctf_status!=FLAG_BASE)
1450 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1454 // if there is only me on the team switch to offense
1456 FOR_EACH_PLAYER(head)
1457 if(SAME_TEAM(head, bot))
1462 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1466 // Evaluate best position to take
1467 // Count mates on middle position
1468 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1470 // Count mates on defense position
1471 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1473 // Count mates on offense position
1474 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1476 if(cdefense<=coffense)
1477 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1478 else if(coffense<=cmiddle)
1479 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1481 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1484 void havocbot_role_ctf_carrier()
1486 if(self.deadflag != DEAD_NO)
1488 havocbot_ctf_reset_role(self);
1492 if (self.flagcarried == world)
1494 havocbot_ctf_reset_role(self);
1498 if (self.bot_strategytime < time)
1500 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1502 navigation_goalrating_start();
1504 havocbot_goalrating_ctf_enemybase(50000);
1506 havocbot_goalrating_ctf_ourbase(50000);
1509 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1511 navigation_goalrating_end();
1513 if (self.navigation_hasgoals)
1514 self.havocbot_cantfindflag = time + 10;
1515 else if (time > self.havocbot_cantfindflag)
1517 // Can't navigate to my own base, suicide!
1518 // TODO: drop it and wander around
1519 Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1525 void havocbot_role_ctf_escort()
1529 if(self.deadflag != DEAD_NO)
1531 havocbot_ctf_reset_role(self);
1535 if (self.flagcarried)
1537 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1541 // If enemy flag is back on the base switch to previous role
1542 ef = havocbot_ctf_find_enemy_flag(self);
1543 if(ef.ctf_status==FLAG_BASE)
1545 self.havocbot_role = self.havocbot_previous_role;
1546 self.havocbot_role_timeout = 0;
1550 // If the flag carrier reached the base switch to defense
1551 mf = havocbot_ctf_find_flag(self);
1552 if(mf.ctf_status!=FLAG_BASE)
1553 if(vlen(ef.origin - mf.dropped_origin) < 300)
1555 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1559 // Set the role timeout if necessary
1560 if (!self.havocbot_role_timeout)
1562 self.havocbot_role_timeout = time + random() * 30 + 60;
1565 // If nothing happened just switch to previous role
1566 if (time > self.havocbot_role_timeout)
1568 self.havocbot_role = self.havocbot_previous_role;
1569 self.havocbot_role_timeout = 0;
1573 // Chase the flag carrier
1574 if (self.bot_strategytime < time)
1576 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1577 navigation_goalrating_start();
1578 havocbot_goalrating_ctf_enemyflag(30000);
1579 havocbot_goalrating_ctf_ourstolenflag(40000);
1580 havocbot_goalrating_items(10000, self.origin, 10000);
1581 navigation_goalrating_end();
1585 void havocbot_role_ctf_offense()
1590 if(self.deadflag != DEAD_NO)
1592 havocbot_ctf_reset_role(self);
1596 if (self.flagcarried)
1598 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1603 mf = havocbot_ctf_find_flag(self);
1604 ef = havocbot_ctf_find_enemy_flag(self);
1607 if(mf.ctf_status!=FLAG_BASE)
1610 pos = mf.tag_entity.origin;
1614 // Try to get it if closer than the enemy base
1615 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1617 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1622 // Escort flag carrier
1623 if(ef.ctf_status!=FLAG_BASE)
1626 pos = ef.tag_entity.origin;
1630 if(vlen(pos-mf.dropped_origin)>700)
1632 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1637 // About to fail, switch to middlefield
1640 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1644 // Set the role timeout if necessary
1645 if (!self.havocbot_role_timeout)
1646 self.havocbot_role_timeout = time + 120;
1648 if (time > self.havocbot_role_timeout)
1650 havocbot_ctf_reset_role(self);
1654 if (self.bot_strategytime < time)
1656 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1657 navigation_goalrating_start();
1658 havocbot_goalrating_ctf_ourstolenflag(50000);
1659 havocbot_goalrating_ctf_enemybase(20000);
1660 havocbot_goalrating_items(5000, self.origin, 1000);
1661 havocbot_goalrating_items(1000, self.origin, 10000);
1662 navigation_goalrating_end();
1666 // Retriever (temporary role):
1667 void havocbot_role_ctf_retriever()
1671 if(self.deadflag != DEAD_NO)
1673 havocbot_ctf_reset_role(self);
1677 if (self.flagcarried)
1679 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1683 // If flag is back on the base switch to previous role
1684 mf = havocbot_ctf_find_flag(self);
1685 if(mf.ctf_status==FLAG_BASE)
1687 havocbot_ctf_reset_role(self);
1691 if (!self.havocbot_role_timeout)
1692 self.havocbot_role_timeout = time + 20;
1694 if (time > self.havocbot_role_timeout)
1696 havocbot_ctf_reset_role(self);
1700 if (self.bot_strategytime < time)
1705 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1706 navigation_goalrating_start();
1707 havocbot_goalrating_ctf_ourstolenflag(50000);
1708 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1709 havocbot_goalrating_ctf_enemybase(30000);
1710 havocbot_goalrating_items(500, self.origin, rt_radius);
1711 navigation_goalrating_end();
1715 void havocbot_role_ctf_middle()
1719 if(self.deadflag != DEAD_NO)
1721 havocbot_ctf_reset_role(self);
1725 if (self.flagcarried)
1727 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1731 mf = havocbot_ctf_find_flag(self);
1732 if(mf.ctf_status!=FLAG_BASE)
1734 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1738 if (!self.havocbot_role_timeout)
1739 self.havocbot_role_timeout = time + 10;
1741 if (time > self.havocbot_role_timeout)
1743 havocbot_ctf_reset_role(self);
1747 if (self.bot_strategytime < time)
1751 org = havocbot_ctf_middlepoint;
1752 org.z = self.origin.z;
1754 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1755 navigation_goalrating_start();
1756 havocbot_goalrating_ctf_ourstolenflag(50000);
1757 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1758 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1759 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1760 havocbot_goalrating_items(2500, self.origin, 10000);
1761 havocbot_goalrating_ctf_enemybase(2500);
1762 navigation_goalrating_end();
1766 void havocbot_role_ctf_defense()
1770 if(self.deadflag != DEAD_NO)
1772 havocbot_ctf_reset_role(self);
1776 if (self.flagcarried)
1778 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1782 // If own flag was captured
1783 mf = havocbot_ctf_find_flag(self);
1784 if(mf.ctf_status!=FLAG_BASE)
1786 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1790 if (!self.havocbot_role_timeout)
1791 self.havocbot_role_timeout = time + 30;
1793 if (time > self.havocbot_role_timeout)
1795 havocbot_ctf_reset_role(self);
1798 if (self.bot_strategytime < time)
1803 org = mf.dropped_origin;
1804 mp_radius = havocbot_ctf_middlepoint_radius;
1806 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1807 navigation_goalrating_start();
1809 // if enemies are closer to our base, go there
1810 entity head, closestplayer = world;
1811 float distance, bestdistance = 10000;
1812 FOR_EACH_PLAYER(head)
1814 if(head.deadflag!=DEAD_NO)
1817 distance = vlen(org - head.origin);
1818 if(distance<bestdistance)
1820 closestplayer = head;
1821 bestdistance = distance;
1826 if(DIFF_TEAM(closestplayer, self))
1827 if(vlen(org - self.origin)>1000)
1828 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1829 havocbot_goalrating_ctf_ourbase(30000);
1831 havocbot_goalrating_ctf_ourstolenflag(20000);
1832 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1833 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1834 havocbot_goalrating_items(10000, org, mp_radius);
1835 havocbot_goalrating_items(5000, self.origin, 10000);
1836 navigation_goalrating_end();
1840 void havocbot_role_ctf_setrole(entity bot, int role)
1842 dprint(strcat(bot.netname," switched to "));
1845 case HAVOCBOT_CTF_ROLE_CARRIER:
1847 bot.havocbot_role = havocbot_role_ctf_carrier;
1848 bot.havocbot_role_timeout = 0;
1849 bot.havocbot_cantfindflag = time + 10;
1850 bot.bot_strategytime = 0;
1852 case HAVOCBOT_CTF_ROLE_DEFENSE:
1854 bot.havocbot_role = havocbot_role_ctf_defense;
1855 bot.havocbot_role_timeout = 0;
1857 case HAVOCBOT_CTF_ROLE_MIDDLE:
1859 bot.havocbot_role = havocbot_role_ctf_middle;
1860 bot.havocbot_role_timeout = 0;
1862 case HAVOCBOT_CTF_ROLE_OFFENSE:
1864 bot.havocbot_role = havocbot_role_ctf_offense;
1865 bot.havocbot_role_timeout = 0;
1867 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1868 dprint("retriever");
1869 bot.havocbot_previous_role = bot.havocbot_role;
1870 bot.havocbot_role = havocbot_role_ctf_retriever;
1871 bot.havocbot_role_timeout = time + 10;
1872 bot.bot_strategytime = 0;
1874 case HAVOCBOT_CTF_ROLE_ESCORT:
1876 bot.havocbot_previous_role = bot.havocbot_role;
1877 bot.havocbot_role = havocbot_role_ctf_escort;
1878 bot.havocbot_role_timeout = time + 30;
1879 bot.bot_strategytime = 0;
1890 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1893 int t = 0, t2 = 0, t3 = 0;
1895 // initially clear items so they can be set as necessary later.
1896 self.ctf_flagstatus &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
1897 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
1898 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
1899 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
1900 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
1901 | CTF_FLAG_NEUTRAL | CTF_SHIELDED);
1903 // scan through all the flags and notify the client about them
1904 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1906 if(flag.team == NUM_TEAM_1) { t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
1907 if(flag.team == NUM_TEAM_2) { t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
1908 if(flag.team == NUM_TEAM_3) { t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
1909 if(flag.team == NUM_TEAM_4) { t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
1910 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; }
1912 switch(flag.ctf_status)
1917 if((flag.owner == self) || (flag.pass_sender == self))
1918 self.ctf_flagstatus |= t; // carrying: self is currently carrying the flag
1920 self.ctf_flagstatus |= t2; // taken: someone else is carrying the flag
1925 self.ctf_flagstatus |= t3; // lost: the flag is dropped somewhere on the map
1931 // item for stopping players from capturing the flag too often
1932 if(self.ctf_captureshielded)
1933 self.ctf_flagstatus |= CTF_SHIELDED;
1935 // update the health of the flag carrier waypointsprite
1936 if(self.wps_flagcarrier)
1937 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
1942 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1944 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1946 if(frag_target == frag_attacker) // damage done to yourself
1948 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1949 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1951 else // damage done to everyone else
1953 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1954 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1957 else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
1959 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)))
1960 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1962 frag_target.wps_helpme_time = time;
1963 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1965 // todo: add notification for when flag carrier needs help?
1970 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1972 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
1974 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1975 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1978 if(frag_target.flagcarried)
1980 entity tmp_entity = frag_target.flagcarried;
1981 ctf_Handle_Throw(frag_target, world, DROP_NORMAL);
1982 tmp_entity.ctf_dropper = world;
1988 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1991 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1994 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1996 entity flag; // temporary entity for the search method
1998 if(self.flagcarried)
1999 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2001 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2003 if(flag.pass_sender == self) { flag.pass_sender = world; }
2004 if(flag.pass_target == self) { flag.pass_target = world; }
2005 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
2011 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
2013 if(self.flagcarried)
2014 if(!autocvar_g_ctf_portalteleport)
2015 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2020 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
2022 if(MUTATOR_RETURNVALUE || gameover) { return false; }
2024 entity player = self;
2026 if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2028 // pass the flag to a team mate
2029 if(autocvar_g_ctf_pass)
2031 entity head, closest_target = world;
2032 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2034 while(head) // find the closest acceptable target to pass to
2036 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
2037 if(head != player && SAME_TEAM(head, player))
2038 if(!head.speedrunning && !head.vehicle)
2040 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2041 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2042 vector passer_center = CENTER_OR_VIEWOFS(player);
2044 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2046 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2048 if(IS_BOT_CLIENT(head))
2050 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2051 ctf_Handle_Throw(head, player, DROP_PASS);
2055 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2056 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2058 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2061 else if(player.flagcarried)
2065 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2066 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
2067 { closest_target = head; }
2069 else { closest_target = head; }
2076 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2079 // throw the flag in front of you
2080 if(autocvar_g_ctf_throw && player.flagcarried)
2082 if(player.throw_count == -1)
2084 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2086 player.throw_prevtime = time;
2087 player.throw_count = 1;
2088 ctf_Handle_Throw(player, world, DROP_THROW);
2093 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2099 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2100 else { player.throw_count += 1; }
2101 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2103 player.throw_prevtime = time;
2104 ctf_Handle_Throw(player, world, DROP_THROW);
2113 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
2115 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2117 self.wps_helpme_time = time;
2118 WaypointSprite_HelpMePing(self.wps_flagcarrier);
2120 else // create a normal help me waypointsprite
2122 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');
2123 WaypointSprite_Ping(self.wps_helpme);
2129 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
2131 if(vh_player.flagcarried)
2133 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2135 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
2139 setattachment(vh_player.flagcarried, vh_vehicle, "");
2140 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
2141 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2142 //vh_player.flagcarried.angles = '0 0 0';
2150 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
2152 if(vh_player.flagcarried)
2154 setattachment(vh_player.flagcarried, vh_player, "");
2155 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2156 vh_player.flagcarried.scale = FLAG_SCALE;
2157 vh_player.flagcarried.angles = '0 0 0';
2164 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
2166 if(self.flagcarried)
2168 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));
2169 ctf_RespawnFlag(self.flagcarried);
2176 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
2178 entity flag; // temporary entity for the search method
2180 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2182 switch(flag.ctf_status)
2187 // lock the flag, game is over
2188 flag.movetype = MOVETYPE_NONE;
2189 flag.takedamage = DAMAGE_NO;
2190 flag.solid = SOLID_NOT;
2191 flag.nextthink = false; // stop thinking
2193 //dprint("stopping the ", flag.netname, " from moving.\n");
2201 // do nothing for these flags
2210 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
2212 havocbot_ctf_reset_role(self);
2216 MUTATOR_HOOKFUNCTION(ctf_GetTeamCount)
2218 //ret_float = ctf_teams;
2219 ret_string = "ctf_team";
2223 MUTATOR_HOOKFUNCTION(ctf_SpectateCopy)
2225 self.ctf_flagstatus = other.ctf_flagstatus;
2234 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
2235 CTF Starting point for a player in team one (Red).
2236 Keys: "angle" viewing angle when spawning. */
2237 void spawnfunc_info_player_team1()
2239 if(g_assault) { remove(self); return; }
2241 self.team = NUM_TEAM_1; // red
2242 spawnfunc_info_player_deathmatch();
2246 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
2247 CTF Starting point for a player in team two (Blue).
2248 Keys: "angle" viewing angle when spawning. */
2249 void spawnfunc_info_player_team2()
2251 if(g_assault) { remove(self); return; }
2253 self.team = NUM_TEAM_2; // blue
2254 spawnfunc_info_player_deathmatch();
2257 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
2258 CTF Starting point for a player in team three (Yellow).
2259 Keys: "angle" viewing angle when spawning. */
2260 void spawnfunc_info_player_team3()
2262 if(g_assault) { remove(self); return; }
2264 self.team = NUM_TEAM_3; // yellow
2265 spawnfunc_info_player_deathmatch();
2269 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
2270 CTF Starting point for a player in team four (Purple).
2271 Keys: "angle" viewing angle when spawning. */
2272 void spawnfunc_info_player_team4()
2274 if(g_assault) { remove(self); return; }
2276 self.team = NUM_TEAM_4; // purple
2277 spawnfunc_info_player_deathmatch();
2280 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2281 CTF flag for team one (Red).
2283 "angle" Angle the flag will point (minus 90 degrees)...
2284 "model" model to use, note this needs red and blue as skins 0 and 1...
2285 "noise" sound played when flag is picked up...
2286 "noise1" sound played when flag is returned by a teammate...
2287 "noise2" sound played when flag is captured...
2288 "noise3" sound played when flag is lost in the field and respawns itself...
2289 "noise4" sound played when flag is dropped by a player...
2290 "noise5" sound played when flag touches the ground... */
2291 void spawnfunc_item_flag_team1()
2293 if(!g_ctf) { remove(self); return; }
2295 ctf_FlagSetup(NUM_TEAM_1, self);
2298 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2299 CTF flag for team two (Blue).
2301 "angle" Angle the flag will point (minus 90 degrees)...
2302 "model" model to use, note this needs red and blue as skins 0 and 1...
2303 "noise" sound played when flag is picked up...
2304 "noise1" sound played when flag is returned by a teammate...
2305 "noise2" sound played when flag is captured...
2306 "noise3" sound played when flag is lost in the field and respawns itself...
2307 "noise4" sound played when flag is dropped by a player...
2308 "noise5" sound played when flag touches the ground... */
2309 void spawnfunc_item_flag_team2()
2311 if(!g_ctf) { remove(self); return; }
2313 ctf_FlagSetup(NUM_TEAM_2, self);
2316 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2317 CTF flag for team three (Yellow).
2319 "angle" Angle the flag will point (minus 90 degrees)...
2320 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2321 "noise" sound played when flag is picked up...
2322 "noise1" sound played when flag is returned by a teammate...
2323 "noise2" sound played when flag is captured...
2324 "noise3" sound played when flag is lost in the field and respawns itself...
2325 "noise4" sound played when flag is dropped by a player...
2326 "noise5" sound played when flag touches the ground... */
2327 void spawnfunc_item_flag_team3()
2329 if(!g_ctf) { remove(self); return; }
2331 ctf_FlagSetup(NUM_TEAM_3, self);
2334 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2335 CTF flag for team four (Pink).
2337 "angle" Angle the flag will point (minus 90 degrees)...
2338 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2339 "noise" sound played when flag is picked up...
2340 "noise1" sound played when flag is returned by a teammate...
2341 "noise2" sound played when flag is captured...
2342 "noise3" sound played when flag is lost in the field and respawns itself...
2343 "noise4" sound played when flag is dropped by a player...
2344 "noise5" sound played when flag touches the ground... */
2345 void spawnfunc_item_flag_team4()
2347 if(!g_ctf) { remove(self); return; }
2349 ctf_FlagSetup(NUM_TEAM_4, self);
2352 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2355 "angle" Angle the flag will point (minus 90 degrees)...
2356 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2357 "noise" sound played when flag is picked up...
2358 "noise1" sound played when flag is returned by a teammate...
2359 "noise2" sound played when flag is captured...
2360 "noise3" sound played when flag is lost in the field and respawns itself...
2361 "noise4" sound played when flag is dropped by a player...
2362 "noise5" sound played when flag touches the ground... */
2363 void spawnfunc_item_flag_neutral()
2365 if(!g_ctf) { remove(self); return; }
2366 if(!cvar("g_ctf_oneflag")) { remove(self); return; }
2368 ctf_FlagSetup(0, self);
2371 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2372 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2373 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.
2375 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2376 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2377 void spawnfunc_ctf_team()
2379 if(!g_ctf) { remove(self); return; }
2381 self.classname = "ctf_team";
2382 self.team = self.cnt + 1;
2385 // compatibility for quake maps
2386 void spawnfunc_team_CTF_redflag() { spawnfunc_item_flag_team1(); }
2387 void spawnfunc_team_CTF_blueflag() { spawnfunc_item_flag_team2(); }
2388 void spawnfunc_team_CTF_redplayer() { spawnfunc_info_player_team1(); }
2389 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2(); }
2390 void spawnfunc_team_CTF_redspawn() { spawnfunc_info_player_team1(); }
2391 void spawnfunc_team_CTF_bluespawn() { spawnfunc_info_player_team2(); }
2399 void ctf_ScoreRules(int teams)
2401 CheckAllowedTeams(world);
2402 ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
2403 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2404 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2405 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2406 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2407 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2408 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2409 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2410 ScoreRules_basics_end();
2413 // code from here on is just to support maps that don't have flag and team entities
2414 void ctf_SpawnTeam (string teamname, int teamcolor)
2419 self.classname = "ctf_team";
2420 self.netname = teamname;
2421 self.cnt = teamcolor;
2423 spawnfunc_ctf_team();
2428 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2433 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2435 if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2436 if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2437 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2440 ctf_teams = bound(2, ctf_teams, 4);
2442 // if no teams are found, spawn defaults
2443 if(find(world, classname, "ctf_team") == world)
2445 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2446 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2447 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2449 ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2451 ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2454 ctf_ScoreRules(ctf_teams);
2457 void ctf_Initialize()
2459 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2461 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2462 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2463 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2465 addstat(STAT_CTF_FLAGSTATUS, AS_INT, ctf_flagstatus);
2467 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2471 MUTATOR_DEFINITION(gamemode_ctf)
2473 MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2474 MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2475 MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2476 MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2477 MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2478 MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2479 MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2480 MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2481 MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2482 MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2483 MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2484 MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2485 MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2486 MUTATOR_HOOK(HavocBot_ChooseRole, ctf_BotRoles, CBC_ORDER_ANY);
2487 MUTATOR_HOOK(GetTeamCount, ctf_GetTeamCount, CBC_ORDER_ANY);
2488 MUTATOR_HOOK(SpectateCopy, ctf_SpectateCopy, CBC_ORDER_ANY);
2492 if(time > 1) // game loads at time 1
2493 error("This is a game type and it cannot be added at runtime.");
2497 MUTATOR_ONROLLBACK_OR_REMOVE
2499 // we actually cannot roll back ctf_Initialize here
2500 // BUT: we don't need to! If this gets called, adding always
2506 print("This is a game type and it cannot be removed at runtime.");