1 // ================================================================
2 // Official capture the flag game mode coding, reworked by Samual
3 // Last updated: September, 2012
4 // ================================================================
6 void ctf_FakeTimeLimit(entity e, float t)
9 WriteByte(MSG_ONE, 3); // svc_updatestat
10 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
12 WriteCoord(MSG_ONE, autocvar_timelimit);
14 WriteCoord(MSG_ONE, (t + 1) / 60);
17 void ctf_EventLog(string mode, float flagteam, entity actor) // use an alias for easy changing and quick editing later
19 if(autocvar_sv_eventlog)
20 GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
23 void ctf_CaptureRecord(entity flag, entity player)
25 float cap_record = ctf_captimerecord;
26 float cap_time = (time - flag.ctf_pickuptime);
27 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
30 if(!ctf_captimerecord) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_TIME_), player.netname, (cap_time * 100)); }
31 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)); }
32 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)); }
34 // write that shit in the database
35 if((!ctf_captimerecord) || (cap_time < cap_record))
37 ctf_captimerecord = cap_time;
38 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
39 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
40 write_recordmarker(player, (time - cap_time), cap_time);
44 void ctf_FlagcarrierWaypoints(entity player)
46 WaypointSprite_Spawn("flagcarrier", 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, TRUE, RADARICON_FLAG, WPCOLOR_FLAGCARRIER(player.team));
47 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON) * 2);
48 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
49 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
52 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
54 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
55 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
56 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
57 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
60 if(current_height) // make sure we can actually do this arcing path
62 targpos = (to + ('0 0 1' * current_height));
63 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
64 if(trace_fraction < 1)
66 //print("normal arc line failed, trying to find new pos...");
67 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
68 targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
69 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
70 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
71 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
74 else { targpos = to; }
76 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
78 vector desired_direction = normalize(targpos - from);
79 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
80 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
83 float ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
85 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
87 // directional tracing only
89 makevectors(passer_angle);
91 // find the closest point on the enemy to the center of the attack
92 float ang; // angle between shotdir and h
93 float h; // hypotenuse, which is the distance between attacker to head
94 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
96 h = vlen(head_center - passer_center);
97 ang = acos(dotproduct(normalize(head_center - passer_center), v_forward));
100 vector nearest_on_line = (passer_center + a * v_forward);
101 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
103 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
104 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
106 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
111 else { return TRUE; }
115 // =======================
116 // CaptureShield Functions
117 // =======================
119 float ctf_CaptureShield_CheckStatus(entity p)
121 float s, s2, s3, s4, se, se2, se3, se4, sr, ser;
123 float players_worseeq, players_total;
125 if(ctf_captureshield_max_ratio <= 0)
128 s = PlayerScore_Add(p, SP_CTF_CAPS, 0);
129 s2 = PlayerScore_Add(p, SP_CTF_PICKUPS, 0);
130 s3 = PlayerScore_Add(p, SP_CTF_RETURNS, 0);
131 s4 = PlayerScore_Add(p, SP_CTF_FCKILLS, 0);
133 sr = ((s - s2) + (s3 + s4));
135 if(sr >= -ctf_captureshield_min_negscore)
138 players_total = players_worseeq = 0;
143 se = PlayerScore_Add(e, SP_CTF_CAPS, 0);
144 se2 = PlayerScore_Add(e, SP_CTF_PICKUPS, 0);
145 se3 = PlayerScore_Add(e, SP_CTF_RETURNS, 0);
146 se4 = PlayerScore_Add(e, SP_CTF_FCKILLS, 0);
148 ser = ((se - se2) + (se3 + se4));
155 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
156 // use this rule here
158 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
164 void ctf_CaptureShield_Update(entity player, float wanted_status)
166 float updated_status = ctf_CaptureShield_CheckStatus(player);
167 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
169 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
170 player.ctf_captureshielded = updated_status;
174 float ctf_CaptureShield_Customize()
176 if(!other.ctf_captureshielded) { return FALSE; }
177 if(CTF_SAMETEAM(self, other)) { return FALSE; }
182 void ctf_CaptureShield_Touch()
184 if(!other.ctf_captureshielded) { return; }
185 if(CTF_SAMETEAM(self, other)) { return; }
187 vector mymid = (self.absmin + self.absmax) * 0.5;
188 vector othermid = (other.absmin + other.absmax) * 0.5;
190 Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
191 Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED);
194 void ctf_CaptureShield_Spawn(entity flag)
196 entity shield = spawn();
199 shield.team = self.team;
200 shield.touch = ctf_CaptureShield_Touch;
201 shield.customizeentityforclient = ctf_CaptureShield_Customize;
202 shield.classname = "ctf_captureshield";
203 shield.effects = EF_ADDITIVE;
204 shield.movetype = MOVETYPE_NOCLIP;
205 shield.solid = SOLID_TRIGGER;
206 shield.avelocity = '7 0 11';
209 setorigin(shield, self.origin);
210 setmodel(shield, "models/ctf/shield.md3");
211 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
215 // ====================
216 // Drop/Pass/Throw Code
217 // ====================
219 void ctf_Handle_Drop(entity flag, entity player, float droptype)
222 player = (player ? player : flag.pass_sender);
225 flag.movetype = MOVETYPE_TOSS;
226 flag.takedamage = DAMAGE_YES;
227 flag.angles = '0 0 0';
228 flag.health = flag.max_flag_health;
229 flag.ctf_droptime = time;
230 flag.ctf_dropper = player;
231 flag.ctf_status = FLAG_DROPPED;
233 // messages and sounds
234 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_LOST_), player.netname);
235 sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
236 ctf_EventLog("dropped", player.team, player);
239 PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);
240 PlayerScore_Add(player, SP_CTF_DROPS, 1);
243 if(autocvar_g_ctf_flag_dropped_waypoint)
244 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));
246 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
248 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
249 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
252 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
254 if(droptype == DROP_PASS)
256 flag.pass_distance = 0;
257 flag.pass_sender = world;
258 flag.pass_target = world;
262 void ctf_Handle_Retrieve(entity flag, entity player)
264 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
265 entity sender = flag.pass_sender;
267 // transfer flag to player
269 flag.owner.flagcarried = flag;
272 setattachment(flag, player, "");
273 setorigin(flag, FLAG_CARRY_OFFSET);
274 flag.movetype = MOVETYPE_NONE;
275 flag.takedamage = DAMAGE_NO;
276 flag.solid = SOLID_NOT;
277 flag.angles = '0 0 0';
278 flag.ctf_status = FLAG_CARRY;
280 // messages and sounds
281 sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
282 ctf_EventLog("receive", flag.team, player);
284 FOR_EACH_REALPLAYER(tmp_player)
286 if(tmp_player == sender)
287 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_SENT_), player.netname);
288 else if(tmp_player == player)
289 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_RECEIVED_), sender.netname);
290 else if(SAME_TEAM(tmp_player, sender))
291 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_OTHER_), sender.netname, player.netname);
294 // create new waypoint
295 ctf_FlagcarrierWaypoints(player);
297 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
298 player.throw_antispam = sender.throw_antispam;
300 flag.pass_distance = 0;
301 flag.pass_sender = world;
302 flag.pass_target = world;
305 void ctf_Handle_Throw(entity player, entity receiver, float droptype)
307 entity flag = player.flagcarried;
308 vector targ_origin, flag_velocity;
310 if(!flag) { return; }
311 if((droptype == DROP_PASS) && !receiver) { return; }
313 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
316 setattachment(flag, world, "");
317 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
318 flag.owner.flagcarried = world;
320 flag.solid = SOLID_TRIGGER;
321 flag.ctf_dropper = player;
322 flag.ctf_droptime = time;
324 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
331 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
332 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
333 WarpZone_RefSys_Copy(flag, receiver);
334 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
335 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
337 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
338 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, FALSE);
341 flag.movetype = MOVETYPE_FLY;
342 flag.takedamage = DAMAGE_NO;
343 flag.pass_sender = player;
344 flag.pass_target = receiver;
345 flag.ctf_status = FLAG_PASSING;
348 sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
349 WarpZone_TrailParticles(world, particleeffectnum(flag.passeffect), player.origin, targ_origin);
350 ctf_EventLog("pass", flag.team, player);
356 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'));
358 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)));
359 flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, FALSE);
360 ctf_Handle_Drop(flag, player, droptype);
366 flag.velocity = '0 0 0'; // do nothing
373 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);
374 ctf_Handle_Drop(flag, player, droptype);
379 // kill old waypointsprite
380 WaypointSprite_Ping(player.wps_flagcarrier);
381 WaypointSprite_Kill(player.wps_flagcarrier);
383 if(player.wps_enemyflagcarrier)
384 WaypointSprite_Kill(player.wps_enemyflagcarrier);
387 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
395 void ctf_Handle_Capture(entity flag, entity toucher, float capturetype)
397 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
398 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
399 float old_time, new_time;
401 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
402 if(CTF_DIFFTEAM(player, flag)) { return; }
404 // messages and sounds
405 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(enemy_flag, CENTER_CTF_CAPTURE_));
406 ctf_CaptureRecord(enemy_flag, player);
407 sound(player, CH_TRIGGER, ((DIFF_TEAM(player, flag)) ? enemy_flag.snd_flag_capture : flag.snd_flag_capture), VOL_BASE, ATTEN_NONE);
411 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
412 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
417 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
418 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
420 old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
421 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
422 if(!old_time || new_time < old_time)
423 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
426 pointparticles(particleeffectnum(flag.capeffect), flag.origin, '0 0 0', 1);
427 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
430 if(capturetype == CAPTURE_NORMAL)
432 WaypointSprite_Kill(player.wps_flagcarrier);
433 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
435 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
436 { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
440 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
441 ctf_RespawnFlag(enemy_flag);
444 void ctf_Handle_Return(entity flag, entity player)
446 // messages and sounds
447 if(player.flags & FL_MONSTER)
449 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_MONSTER_), player.monster_name);
453 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_RETURN_));
454 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_), player.netname);
456 sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
457 ctf_EventLog("return", flag.team, player);
460 if(IS_PLAYER(player))
462 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
463 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
466 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
470 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
471 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
472 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
476 if(player.flagcarried == flag)
477 WaypointSprite_Kill(player.wps_flagcarrier);
480 ctf_RespawnFlag(flag);
483 void ctf_Handle_Pickup(entity flag, entity player, float pickuptype)
486 float pickup_dropped_score; // used to calculate dropped pickup score
487 entity tmp_entity; // temporary entity
489 // attach the flag to the player
491 player.flagcarried = flag;
492 setattachment(flag, player, "");
493 setorigin(flag, FLAG_CARRY_OFFSET);
496 flag.movetype = MOVETYPE_NONE;
497 flag.takedamage = DAMAGE_NO;
498 flag.solid = SOLID_NOT;
499 flag.angles = '0 0 0';
500 flag.ctf_status = FLAG_CARRY;
504 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
505 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
509 // messages and sounds
510 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_PICKUP_), player.netname);
511 if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
512 if(CTF_DIFFTEAM(player, flag)) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PICKUP_)); }
513 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)); }
515 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_PICKUP_TEAM_), Team_ColorCode(player.team), player.netname);
517 FOR_EACH_PLAYER(tmp_entity)
518 if(tmp_entity != player)
519 if(CTF_SAMETEAM(flag, tmp_entity))
520 if(SAME_TEAM(player, tmp_entity))
521 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_PICKUP_TEAM_), Team_ColorCode(player.team), player.netname);
523 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);
525 sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
528 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
533 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
534 ctf_EventLog("steal", flag.team, player);
540 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);
541 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);
542 dprint("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
543 PlayerTeamScore_AddScore(player, pickup_dropped_score);
544 ctf_EventLog("pickup", flag.team, player);
552 if(pickuptype == PICKUP_BASE)
554 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
555 if((player.speedrunning) && (ctf_captimerecord))
556 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
560 pointparticles(particleeffectnum(flag.toucheffect), player.origin, '0 0 0', 1);
563 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
564 ctf_FlagcarrierWaypoints(player);
565 WaypointSprite_Ping(player.wps_flagcarrier);
569 // ===================
570 // Main Flag Functions
571 // ===================
573 void ctf_CheckFlagReturn(entity flag, float returntype)
575 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
577 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
579 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
583 case RETURN_DROPPED: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DROPPED_)); break;
584 case RETURN_DAMAGE: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DAMAGED_)); break;
585 case RETURN_SPEEDRUN: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_SPEEDRUN_), ctf_captimerecord); break;
586 case RETURN_NEEDKILL: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_NEEDKILL_)); break;
590 { Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_TIMEOUT_)); break; }
592 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
593 ctf_EventLog("returned", flag.team, world);
594 ctf_RespawnFlag(flag);
599 float ctf_Stalemate_Customize()
601 // make spectators see what the player would see
603 e = WaypointSprite_getviewentity(other);
604 wp_owner = self.owner;
607 if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return FALSE; }
608 if(SAME_TEAM(wp_owner, e)) { return FALSE; }
609 if(!IS_PLAYER(e)) { return FALSE; }
614 void ctf_CheckStalemate(void)
617 float stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0;
620 entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
622 // build list of stale flags
623 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
625 if(autocvar_g_ctf_stalemate)
626 if(tmp_entity.ctf_status != FLAG_BASE)
627 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time)
629 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
630 ctf_staleflaglist = tmp_entity;
632 switch(tmp_entity.team)
634 case NUM_TEAM_1: ++stale_red_flags; break;
635 case NUM_TEAM_2: ++stale_blue_flags; break;
636 case NUM_TEAM_3: ++stale_yellow_flags; break;
637 case NUM_TEAM_4: ++stale_pink_flags; break;
642 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
644 if(stale_flags == ctf_teams)
645 ctf_stalemate = TRUE;
646 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
647 { ctf_stalemate = FALSE; wpforenemy_announced = FALSE; }
648 else if(stale_flags < ctf_teams && autocvar_g_ctf_stalemate_endcondition == 1)
649 { ctf_stalemate = FALSE; wpforenemy_announced = FALSE; }
651 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
654 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
656 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
658 WaypointSprite_Spawn("enemyflagcarrier", 0, 0, tmp_entity.owner, FLAG_WAYPOINT_OFFSET, world, 0, tmp_entity.owner, wps_enemyflagcarrier, TRUE, RADARICON_FLAG, WPCOLOR_ENEMYFC(tmp_entity.owner.team));
659 tmp_entity.owner.wps_enemyflagcarrier.customizeentityforclient = ctf_Stalemate_Customize;
663 if (!wpforenemy_announced)
665 FOR_EACH_REALPLAYER(tmp_entity)
666 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CENTER, ((tmp_entity.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER));
668 wpforenemy_announced = TRUE;
673 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
675 if(ITEM_DAMAGE_NEEDKILL(deathtype))
677 // automatically kill the flag and return it
679 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
682 if(autocvar_g_ctf_flag_return_damage)
684 // reduce health and check if it should be returned
685 self.health = self.health - damage;
686 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
696 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
699 if(self == ctf_worldflaglist) // only for the first flag
700 FOR_EACH_CLIENT(tmp_entity)
701 ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
704 if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
705 dprint("wtf the flag got squashed?\n");
706 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
707 if(!trace_startsolid) // can we resize it without getting stuck?
708 setsize(self, FLAG_MIN, FLAG_MAX); }
710 switch(self.ctf_status) // reset flag angles in case warpzones adjust it
714 self.angles = '0 0 0';
722 switch(self.ctf_status)
726 if(autocvar_g_ctf_dropped_capture_radius)
728 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
729 if(tmp_entity.ctf_status == FLAG_DROPPED)
730 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
731 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
732 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
739 if(autocvar_g_ctf_flag_dropped_floatinwater)
741 vector midpoint = ((self.absmin + self.absmax) * 0.5);
742 if(pointcontents(midpoint) == CONTENT_WATER)
744 self.velocity = self.velocity * 0.5;
746 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
747 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
749 { self.movetype = MOVETYPE_FLY; }
751 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
753 if(autocvar_g_ctf_flag_return_dropped)
755 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
758 ctf_CheckFlagReturn(self, RETURN_DROPPED);
762 if(autocvar_g_ctf_flag_return_time)
764 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
765 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
773 if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
776 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
780 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
784 if(autocvar_g_ctf_stalemate)
786 if(time >= wpforenemy_nextthink)
788 ctf_CheckStalemate();
789 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
792 if(CTF_SAMETEAM(self, self.owner))
794 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
795 ctf_Handle_Throw(self.owner, world, DROP_THROW);
796 else if(vlen(self.owner.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_carried_radius)
797 ctf_Handle_Return(self, self.owner);
804 vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
805 targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
806 WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
808 if((self.pass_target == world)
809 || (self.pass_target.deadflag != DEAD_NO)
810 || (self.pass_target.flagcarried)
811 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
812 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
813 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
815 // give up, pass failed
816 ctf_Handle_Drop(self, world, DROP_PASS);
820 // still a viable target, go for it
821 ctf_CalculatePassVelocity(self, targ_origin, self.origin, TRUE);
826 default: // this should never happen
828 dprint("ctf_FlagThink(): Flag exists with no status?\n");
836 if(gameover) { return; }
838 entity toucher = other;
839 float is_not_monster = (!(toucher.flags & FL_MONSTER));
841 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
842 if(ITEM_TOUCH_NEEDKILL())
845 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
849 // special touch behaviors
850 if(toucher.vehicle_flags & VHF_ISVEHICLE)
852 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
853 toucher = toucher.owner; // the player is actually the vehicle owner, not other
855 return; // do nothing
857 else if(toucher.flags & FL_MONSTER)
859 if(!autocvar_g_ctf_allow_monster_touch)
860 return; // do nothing
862 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
864 if(time > self.wait) // if we haven't in a while, play a sound/effect
866 pointparticles(particleeffectnum(self.toucheffect), self.origin, '0 0 0', 1);
867 sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTEN_NORM);
868 self.wait = time + FLAG_TOUCHRATE;
872 else if(toucher.deadflag != DEAD_NO) { return; }
874 switch(self.ctf_status)
878 if(CTF_SAMETEAM(toucher, self) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, self) && is_not_monster)
879 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
880 else if(CTF_DIFFTEAM(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
881 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
887 if(CTF_SAMETEAM(toucher, self) && autocvar_g_ctf_flag_return)
888 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
889 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
890 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
896 dprint("Someone touched a flag even though it was being carried?\n");
902 if((IS_PLAYER(toucher)) && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
904 if(DIFF_TEAM(toucher, self.pass_sender))
905 ctf_Handle_Return(self, toucher);
907 ctf_Handle_Retrieve(self, toucher);
915 void ctf_RespawnFlag(entity flag)
917 // check for flag respawn being called twice in a row
918 if(flag.last_respawn > time - 0.5)
919 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
921 flag.last_respawn = time;
923 // reset the player (if there is one)
924 if((flag.owner) && (flag.owner.flagcarried == flag))
926 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
927 WaypointSprite_Kill(flag.wps_flagcarrier);
929 flag.owner.flagcarried = world;
931 if(flag.speedrunning)
932 ctf_FakeTimeLimit(flag.owner, -1);
935 if(flag.ctf_status == FLAG_DROPPED)
936 { WaypointSprite_Kill(flag.wps_flagdropped); }
939 setattachment(flag, world, "");
940 setorigin(flag, flag.ctf_spawnorigin);
942 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
943 flag.takedamage = DAMAGE_NO;
944 flag.health = flag.max_flag_health;
945 flag.solid = SOLID_TRIGGER;
946 flag.velocity = '0 0 0';
947 flag.angles = flag.mangle;
948 flag.flags = FL_ITEM | FL_NOTARGET;
950 flag.ctf_status = FLAG_BASE;
952 flag.pass_distance = 0;
953 flag.pass_sender = world;
954 flag.pass_target = world;
955 flag.ctf_dropper = world;
956 flag.ctf_pickuptime = 0;
957 flag.ctf_droptime = 0;
963 if(IS_PLAYER(self.owner))
964 ctf_Handle_Throw(self.owner, world, DROP_RESET);
966 ctf_RespawnFlag(self);
969 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
972 waypoint_spawnforitem_force(self, self.origin);
973 self.nearestwaypointtimeout = 0; // activate waypointing again
974 self.bot_basewaypoint = self.nearestwaypoint;
977 string basename = "base";
981 case NUM_TEAM_1: basename = "redbase"; break;
982 case NUM_TEAM_2: basename = "bluebase"; break;
983 case NUM_TEAM_3: basename = "yellowbase"; break;
984 case NUM_TEAM_4: basename = "pinkbase"; break;
987 WaypointSprite_SpawnFixed(basename, self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
988 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
990 // captureshield setup
991 ctf_CaptureShield_Spawn(self);
994 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
997 self = flag; // for later usage with droptofloor()
1000 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1001 ctf_worldflaglist = flag;
1003 setattachment(flag, world, "");
1005 flag.netname = ((teamnumber == NUM_TEAM_1) ? "^1RED^7 flag" : ((teamnumber == NUM_TEAM_2) ? "^2BLUE^7 flag" : ((teamnumber == NUM_TEAM_3) ? "^3YELLOW^7 flag" : "^6PINK^7 flag")));
1006 flag.team = teamnumber;
1007 flag.classname = "item_flag_team";
1008 flag.target = "###item###"; // wut?
1009 flag.flags = FL_ITEM | FL_NOTARGET;
1010 flag.solid = SOLID_TRIGGER;
1011 flag.takedamage = DAMAGE_NO;
1012 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1013 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1014 flag.health = flag.max_flag_health;
1015 flag.event_damage = ctf_FlagDamage;
1016 flag.pushable = TRUE;
1017 flag.teleportable = TELEPORT_NORMAL;
1018 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1019 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1020 flag.velocity = '0 0 0';
1021 flag.mangle = flag.angles;
1022 flag.reset = ctf_Reset;
1023 flag.touch = ctf_FlagTouch;
1024 flag.think = ctf_FlagThink;
1025 flag.nextthink = time + FLAG_THINKRATE;
1026 flag.ctf_status = FLAG_BASE;
1029 if(flag.model == "") { flag.model = ((teamnumber == NUM_TEAM_1) ? autocvar_g_ctf_flag_red_model : ((teamnumber == NUM_TEAM_2) ? autocvar_g_ctf_flag_blue_model : ((teamnumber == NUM_TEAM_3) ? autocvar_g_ctf_flag_yellow_model : autocvar_g_ctf_flag_pink_model))); }
1030 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1031 if(!flag.skin) { flag.skin = ((teamnumber == NUM_TEAM_1) ? autocvar_g_ctf_flag_red_skin : ((teamnumber == NUM_TEAM_2) ? autocvar_g_ctf_flag_blue_skin : ((teamnumber == NUM_TEAM_3) ? autocvar_g_ctf_flag_yellow_skin : autocvar_g_ctf_flag_pink_skin))); }
1032 if(flag.toucheffect == "") { flag.toucheffect = ((teamnumber == NUM_TEAM_1) ? "redflag_touch" : ((teamnumber == NUM_TEAM_2) ? "blueflag_touch" : ((teamnumber == NUM_TEAM_3) ? "yellowflag_touch" : "pinkflag_touch"))); }
1033 if(flag.passeffect == "") { flag.passeffect = ((teamnumber == NUM_TEAM_1) ? "red_pass" : ((teamnumber == NUM_TEAM_2) ? "blue_pass" : ((teamnumber == NUM_TEAM_3) ? "yellow_pass" : "pink_pass"))); }
1034 if(flag.capeffect == "") { flag.capeffect = ((teamnumber == NUM_TEAM_1) ? "red_cap" : ((teamnumber == NUM_TEAM_2) ? "blue_cap" : ((teamnumber == NUM_TEAM_3) ? "yellow_cap" : "pink_cap"))); }
1037 if(flag.snd_flag_taken == "") { flag.snd_flag_taken = ((teamnumber == NUM_TEAM_1) ? "ctf/red_taken.wav" : ((teamnumber == NUM_TEAM_2) ? "ctf/blue_taken.wav" : ((teamnumber == NUM_TEAM_3) ? "ctf/yellow_taken.wav" : "ctf/pink_taken.wav"))); }
1038 if(flag.snd_flag_returned == "") { flag.snd_flag_returned = ((teamnumber == NUM_TEAM_1) ? "ctf/red_returned.wav" : ((teamnumber == NUM_TEAM_2) ? "ctf/blue_returned.wav" : ((teamnumber == NUM_TEAM_3) ? "ctf/yellow_returned.wav" : "ctf/pink_returned.wav"))); }
1039 if(flag.snd_flag_capture == "") { flag.snd_flag_capture = ((teamnumber == NUM_TEAM_1) ? "ctf/red_capture.wav" : ((teamnumber == NUM_TEAM_2) ? "ctf/blue_capture.wav" : ((teamnumber == NUM_TEAM_3) ? "ctf/yellow_capture.wav" : "ctf/pink_capture.wav"))); }
1040 if(flag.snd_flag_dropped == "") { flag.snd_flag_dropped = ((teamnumber == NUM_TEAM_1) ? "ctf/red_dropped.wav" : ((teamnumber == NUM_TEAM_2) ? "ctf/blue_dropped.wav" : ((teamnumber == NUM_TEAM_3) ? "ctf/yellow_dropped.wav" : "ctf/pink_dropped.wav"))); }
1041 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.
1042 if(flag.snd_flag_touch == "") { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
1043 if(flag.snd_flag_pass == "") { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
1046 precache_sound(flag.snd_flag_taken);
1047 precache_sound(flag.snd_flag_returned);
1048 precache_sound(flag.snd_flag_capture);
1049 precache_sound(flag.snd_flag_respawn);
1050 precache_sound(flag.snd_flag_dropped);
1051 precache_sound(flag.snd_flag_touch);
1052 precache_sound(flag.snd_flag_pass);
1053 precache_model(flag.model);
1054 precache_model("models/ctf/shield.md3");
1055 precache_model("models/ctf/shockwavetransring.md3");
1058 setmodel(flag, flag.model); // precision set below
1059 setsize(flag, FLAG_MIN, FLAG_MAX);
1060 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1062 if(autocvar_g_ctf_flag_glowtrails)
1064 flag.glow_color = ((teamnumber == NUM_TEAM_1) ? 251 : ((teamnumber == NUM_TEAM_2) ? 210 : ((teamnumber == NUM_TEAM_3) ? 110 : 145)));
1065 flag.glow_size = 25;
1066 flag.glow_trail = 1;
1069 flag.effects |= EF_LOWPRECISION;
1070 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1071 if(autocvar_g_ctf_dynamiclights)
1075 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1076 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1077 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1078 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1083 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1085 flag.dropped_origin = flag.origin;
1086 flag.noalign = TRUE;
1087 flag.movetype = MOVETYPE_NONE;
1089 else // drop to floor, automatically find a platform and set that as spawn origin
1091 flag.noalign = FALSE;
1094 flag.movetype = MOVETYPE_TOSS;
1097 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1105 // NOTE: LEGACY CODE, needs to be re-written!
1107 void havocbot_calculate_middlepoint()
1111 vector fo = '0 0 0';
1114 f = ctf_worldflaglist;
1119 f = f.ctf_worldflagnext;
1123 havocbot_ctf_middlepoint = s * (1.0 / n);
1124 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1128 entity havocbot_ctf_find_flag(entity bot)
1131 f = ctf_worldflaglist;
1134 if (CTF_SAMETEAM(bot, f))
1136 f = f.ctf_worldflagnext;
1141 entity havocbot_ctf_find_enemy_flag(entity bot)
1144 f = ctf_worldflaglist;
1147 if (CTF_DIFFTEAM(bot, f))
1149 f = f.ctf_worldflagnext;
1154 float havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1162 FOR_EACH_PLAYER(head)
1164 if(head.team!=bot.team || head.deadflag != DEAD_NO || head == bot)
1167 if(vlen(head.origin - org) < tc_radius)
1174 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1177 head = ctf_worldflaglist;
1180 if (CTF_SAMETEAM(self, head))
1182 head = head.ctf_worldflagnext;
1185 navigation_routerating(head, ratingscale, 10000);
1188 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1191 head = ctf_worldflaglist;
1194 if (CTF_SAMETEAM(self, head))
1196 head = head.ctf_worldflagnext;
1201 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1204 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1207 head = ctf_worldflaglist;
1210 if(CTF_DIFFTEAM(self, head))
1212 head = head.ctf_worldflagnext;
1215 navigation_routerating(head, ratingscale, 10000);
1218 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1220 if (!bot_waypoints_for_items)
1222 havocbot_goalrating_ctf_enemyflag(ratingscale);
1228 head = havocbot_ctf_find_enemy_flag(self);
1233 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1236 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1240 mf = havocbot_ctf_find_flag(self);
1242 if(mf.ctf_status == FLAG_BASE)
1246 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1249 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1252 head = ctf_worldflaglist;
1255 // flag is out in the field
1256 if(head.ctf_status != FLAG_BASE)
1257 if(head.tag_entity==world) // dropped
1261 if(vlen(org-head.origin)<df_radius)
1262 navigation_routerating(head, ratingscale, 10000);
1265 navigation_routerating(head, ratingscale, 10000);
1268 head = head.ctf_worldflagnext;
1272 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1276 head = findchainfloat(bot_pickup, TRUE);
1279 // gather health and armor only
1281 if (head.health || head.armorvalue)
1282 if (vlen(head.origin - org) < sradius)
1284 // get the value of the item
1285 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1287 navigation_routerating(head, t * ratingscale, 500);
1293 void havocbot_ctf_reset_role(entity bot)
1295 float cdefense, cmiddle, coffense;
1296 entity mf, ef, head;
1299 if(bot.deadflag != DEAD_NO)
1302 if(vlen(havocbot_ctf_middlepoint)==0)
1303 havocbot_calculate_middlepoint();
1306 if (bot.flagcarried)
1308 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1312 mf = havocbot_ctf_find_flag(bot);
1313 ef = havocbot_ctf_find_enemy_flag(bot);
1315 // Retrieve stolen flag
1316 if(mf.ctf_status!=FLAG_BASE)
1318 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1322 // If enemy flag is taken go to the middle to intercept pursuers
1323 if(ef.ctf_status!=FLAG_BASE)
1325 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1329 // if there is only me on the team switch to offense
1331 FOR_EACH_PLAYER(head)
1332 if(head.team==bot.team)
1337 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1341 // Evaluate best position to take
1342 // Count mates on middle position
1343 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1345 // Count mates on defense position
1346 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1348 // Count mates on offense position
1349 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1351 if(cdefense<=coffense)
1352 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1353 else if(coffense<=cmiddle)
1354 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1356 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1359 void havocbot_role_ctf_carrier()
1361 if(self.deadflag != DEAD_NO)
1363 havocbot_ctf_reset_role(self);
1367 if (self.flagcarried == world)
1369 havocbot_ctf_reset_role(self);
1373 if (self.bot_strategytime < time)
1375 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1377 navigation_goalrating_start();
1378 havocbot_goalrating_ctf_ourbase(50000);
1381 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1383 navigation_goalrating_end();
1385 if (self.navigation_hasgoals)
1386 self.havocbot_cantfindflag = time + 10;
1387 else if (time > self.havocbot_cantfindflag)
1389 // Can't navigate to my own base, suicide!
1390 // TODO: drop it and wander around
1391 Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1397 void havocbot_role_ctf_escort()
1401 if(self.deadflag != DEAD_NO)
1403 havocbot_ctf_reset_role(self);
1407 if (self.flagcarried)
1409 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1413 // If enemy flag is back on the base switch to previous role
1414 ef = havocbot_ctf_find_enemy_flag(self);
1415 if(ef.ctf_status==FLAG_BASE)
1417 self.havocbot_role = self.havocbot_previous_role;
1418 self.havocbot_role_timeout = 0;
1422 // If the flag carrier reached the base switch to defense
1423 mf = havocbot_ctf_find_flag(self);
1424 if(mf.ctf_status!=FLAG_BASE)
1425 if(vlen(ef.origin - mf.dropped_origin) < 300)
1427 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1431 // Set the role timeout if necessary
1432 if (!self.havocbot_role_timeout)
1434 self.havocbot_role_timeout = time + random() * 30 + 60;
1437 // If nothing happened just switch to previous role
1438 if (time > self.havocbot_role_timeout)
1440 self.havocbot_role = self.havocbot_previous_role;
1441 self.havocbot_role_timeout = 0;
1445 // Chase the flag carrier
1446 if (self.bot_strategytime < time)
1448 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1449 navigation_goalrating_start();
1450 havocbot_goalrating_ctf_enemyflag(30000);
1451 havocbot_goalrating_ctf_ourstolenflag(40000);
1452 havocbot_goalrating_items(10000, self.origin, 10000);
1453 navigation_goalrating_end();
1457 void havocbot_role_ctf_offense()
1462 if(self.deadflag != DEAD_NO)
1464 havocbot_ctf_reset_role(self);
1468 if (self.flagcarried)
1470 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1475 mf = havocbot_ctf_find_flag(self);
1476 ef = havocbot_ctf_find_enemy_flag(self);
1479 if(mf.ctf_status!=FLAG_BASE)
1482 pos = mf.tag_entity.origin;
1486 // Try to get it if closer than the enemy base
1487 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1489 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1494 // Escort flag carrier
1495 if(ef.ctf_status!=FLAG_BASE)
1498 pos = ef.tag_entity.origin;
1502 if(vlen(pos-mf.dropped_origin)>700)
1504 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1509 // About to fail, switch to middlefield
1512 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1516 // Set the role timeout if necessary
1517 if (!self.havocbot_role_timeout)
1518 self.havocbot_role_timeout = time + 120;
1520 if (time > self.havocbot_role_timeout)
1522 havocbot_ctf_reset_role(self);
1526 if (self.bot_strategytime < time)
1528 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1529 navigation_goalrating_start();
1530 havocbot_goalrating_ctf_ourstolenflag(50000);
1531 havocbot_goalrating_ctf_enemybase(20000);
1532 havocbot_goalrating_items(5000, self.origin, 1000);
1533 havocbot_goalrating_items(1000, self.origin, 10000);
1534 navigation_goalrating_end();
1538 // Retriever (temporary role):
1539 void havocbot_role_ctf_retriever()
1543 if(self.deadflag != DEAD_NO)
1545 havocbot_ctf_reset_role(self);
1549 if (self.flagcarried)
1551 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1555 // If flag is back on the base switch to previous role
1556 mf = havocbot_ctf_find_flag(self);
1557 if(mf.ctf_status==FLAG_BASE)
1559 havocbot_ctf_reset_role(self);
1563 if (!self.havocbot_role_timeout)
1564 self.havocbot_role_timeout = time + 20;
1566 if (time > self.havocbot_role_timeout)
1568 havocbot_ctf_reset_role(self);
1572 if (self.bot_strategytime < time)
1577 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1578 navigation_goalrating_start();
1579 havocbot_goalrating_ctf_ourstolenflag(50000);
1580 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1581 havocbot_goalrating_ctf_enemybase(30000);
1582 havocbot_goalrating_items(500, self.origin, rt_radius);
1583 navigation_goalrating_end();
1587 void havocbot_role_ctf_middle()
1591 if(self.deadflag != DEAD_NO)
1593 havocbot_ctf_reset_role(self);
1597 if (self.flagcarried)
1599 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1603 mf = havocbot_ctf_find_flag(self);
1604 if(mf.ctf_status!=FLAG_BASE)
1606 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1610 if (!self.havocbot_role_timeout)
1611 self.havocbot_role_timeout = time + 10;
1613 if (time > self.havocbot_role_timeout)
1615 havocbot_ctf_reset_role(self);
1619 if (self.bot_strategytime < time)
1623 org = havocbot_ctf_middlepoint;
1624 org_z = self.origin_z;
1626 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1627 navigation_goalrating_start();
1628 havocbot_goalrating_ctf_ourstolenflag(50000);
1629 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1630 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1631 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1632 havocbot_goalrating_items(2500, self.origin, 10000);
1633 havocbot_goalrating_ctf_enemybase(2500);
1634 navigation_goalrating_end();
1638 void havocbot_role_ctf_defense()
1642 if(self.deadflag != DEAD_NO)
1644 havocbot_ctf_reset_role(self);
1648 if (self.flagcarried)
1650 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1654 // If own flag was captured
1655 mf = havocbot_ctf_find_flag(self);
1656 if(mf.ctf_status!=FLAG_BASE)
1658 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1662 if (!self.havocbot_role_timeout)
1663 self.havocbot_role_timeout = time + 30;
1665 if (time > self.havocbot_role_timeout)
1667 havocbot_ctf_reset_role(self);
1670 if (self.bot_strategytime < time)
1675 org = mf.dropped_origin;
1676 mp_radius = havocbot_ctf_middlepoint_radius;
1678 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1679 navigation_goalrating_start();
1681 // if enemies are closer to our base, go there
1682 entity head, closestplayer = world;
1683 float distance, bestdistance = 10000;
1684 FOR_EACH_PLAYER(head)
1686 if(head.deadflag!=DEAD_NO)
1689 distance = vlen(org - head.origin);
1690 if(distance<bestdistance)
1692 closestplayer = head;
1693 bestdistance = distance;
1698 if(closestplayer.team!=self.team)
1699 if(vlen(org - self.origin)>1000)
1700 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1701 havocbot_goalrating_ctf_ourbase(30000);
1703 havocbot_goalrating_ctf_ourstolenflag(20000);
1704 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1705 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1706 havocbot_goalrating_items(10000, org, mp_radius);
1707 havocbot_goalrating_items(5000, self.origin, 10000);
1708 navigation_goalrating_end();
1712 void havocbot_role_ctf_setrole(entity bot, float role)
1714 dprint(strcat(bot.netname," switched to "));
1717 case HAVOCBOT_CTF_ROLE_CARRIER:
1719 bot.havocbot_role = havocbot_role_ctf_carrier;
1720 bot.havocbot_role_timeout = 0;
1721 bot.havocbot_cantfindflag = time + 10;
1722 bot.bot_strategytime = 0;
1724 case HAVOCBOT_CTF_ROLE_DEFENSE:
1726 bot.havocbot_role = havocbot_role_ctf_defense;
1727 bot.havocbot_role_timeout = 0;
1729 case HAVOCBOT_CTF_ROLE_MIDDLE:
1731 bot.havocbot_role = havocbot_role_ctf_middle;
1732 bot.havocbot_role_timeout = 0;
1734 case HAVOCBOT_CTF_ROLE_OFFENSE:
1736 bot.havocbot_role = havocbot_role_ctf_offense;
1737 bot.havocbot_role_timeout = 0;
1739 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1740 dprint("retriever");
1741 bot.havocbot_previous_role = bot.havocbot_role;
1742 bot.havocbot_role = havocbot_role_ctf_retriever;
1743 bot.havocbot_role_timeout = time + 10;
1744 bot.bot_strategytime = 0;
1746 case HAVOCBOT_CTF_ROLE_ESCORT:
1748 bot.havocbot_previous_role = bot.havocbot_role;
1749 bot.havocbot_role = havocbot_role_ctf_escort;
1750 bot.havocbot_role_timeout = time + 30;
1751 bot.bot_strategytime = 0;
1762 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1765 float t = 0, t2 = 0, t3 = 0;
1767 // initially clear items so they can be set as necessary later.
1768 self.items &= ~(IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST
1769 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST
1770 | IT_YELLOW_FLAG_CARRYING | IT_YELLOW_FLAG_TAKEN | IT_YELLOW_FLAG_LOST
1771 | IT_PINK_FLAG_CARRYING | IT_PINK_FLAG_TAKEN | IT_PINK_FLAG_LOST
1774 // scan through all the flags and notify the client about them
1775 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1777 if(flag.team == NUM_TEAM_1) { t = IT_RED_FLAG_CARRYING; t2 = IT_RED_FLAG_TAKEN; t3 = IT_RED_FLAG_LOST; }
1778 if(flag.team == NUM_TEAM_2) { t = IT_BLUE_FLAG_CARRYING; t2 = IT_BLUE_FLAG_TAKEN; t3 = IT_BLUE_FLAG_LOST; }
1779 if(flag.team == NUM_TEAM_3) { t = IT_YELLOW_FLAG_CARRYING; t2 = IT_YELLOW_FLAG_TAKEN; t3 = IT_YELLOW_FLAG_LOST; }
1780 if(flag.team == NUM_TEAM_4) { t = IT_PINK_FLAG_CARRYING; t2 = IT_PINK_FLAG_TAKEN; t3 = IT_PINK_FLAG_LOST; }
1782 switch(flag.ctf_status)
1787 if((flag.owner == self) || (flag.pass_sender == self))
1788 self.items |= t; // carrying: self is currently carrying the flag
1790 self.items |= t2; // taken: someone else is carrying the flag
1795 self.items |= t3; // lost: the flag is dropped somewhere on the map
1801 // item for stopping players from capturing the flag too often
1802 if(self.ctf_captureshielded)
1803 self.items |= IT_CTF_SHIELDED;
1805 // update the health of the flag carrier waypointsprite
1806 if(self.wps_flagcarrier)
1807 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
1812 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1814 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1816 if(frag_target == frag_attacker) // damage done to yourself
1818 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1819 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1821 else // damage done to everyone else
1823 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1824 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1827 else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
1829 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)))
1830 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1832 frag_target.wps_helpme_time = time;
1833 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1835 // todo: add notification for when flag carrier needs help?
1840 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1842 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
1844 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1845 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1848 if(frag_target.flagcarried)
1849 { ctf_Handle_Throw(frag_target, world, DROP_NORMAL); }
1854 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1857 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1860 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1862 entity flag; // temporary entity for the search method
1864 if(self.flagcarried)
1865 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1867 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1869 if(flag.pass_sender == self) { flag.pass_sender = world; }
1870 if(flag.pass_target == self) { flag.pass_target = world; }
1871 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
1877 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
1879 if(self.flagcarried)
1880 if(!autocvar_g_ctf_portalteleport)
1881 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1886 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
1888 if(MUTATOR_RETURNVALUE || gameover) { return FALSE; }
1890 entity player = self;
1892 if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1894 // pass the flag to a team mate
1895 if(autocvar_g_ctf_pass)
1897 entity head, closest_target = world;
1898 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, TRUE);
1900 while(head) // find the closest acceptable target to pass to
1902 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
1903 if(head != player && SAME_TEAM(head, player))
1904 if(!head.speedrunning && !head.vehicle)
1906 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
1907 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
1908 vector passer_center = CENTER_OR_VIEWOFS(player);
1910 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
1912 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
1914 if(IS_BOT_CLIENT(head))
1916 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1917 ctf_Handle_Throw(head, player, DROP_PASS);
1921 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
1922 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1924 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
1927 else if(player.flagcarried)
1931 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
1932 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
1933 { closest_target = head; }
1935 else { closest_target = head; }
1942 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return TRUE; }
1945 // throw the flag in front of you
1946 if(autocvar_g_ctf_throw && player.flagcarried)
1948 if(player.throw_count == -1)
1950 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
1952 player.throw_prevtime = time;
1953 player.throw_count = 1;
1954 ctf_Handle_Throw(player, world, DROP_THROW);
1959 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
1965 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
1966 else { player.throw_count += 1; }
1967 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
1969 player.throw_prevtime = time;
1970 ctf_Handle_Throw(player, world, DROP_THROW);
1979 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
1981 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
1983 self.wps_helpme_time = time;
1984 WaypointSprite_HelpMePing(self.wps_flagcarrier);
1986 else // create a normal help me waypointsprite
1988 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');
1989 WaypointSprite_Ping(self.wps_helpme);
1995 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
1997 if(vh_player.flagcarried)
1999 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2001 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
2005 setattachment(vh_player.flagcarried, vh_vehicle, "");
2006 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
2007 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2008 //vh_player.flagcarried.angles = '0 0 0';
2016 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
2018 if(vh_player.flagcarried)
2020 setattachment(vh_player.flagcarried, vh_player, "");
2021 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2022 vh_player.flagcarried.scale = FLAG_SCALE;
2023 vh_player.flagcarried.angles = '0 0 0';
2030 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
2032 if(self.flagcarried)
2034 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN_));
2035 ctf_RespawnFlag(self.flagcarried);
2042 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
2044 entity flag; // temporary entity for the search method
2046 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2048 switch(flag.ctf_status)
2053 // lock the flag, game is over
2054 flag.movetype = MOVETYPE_NONE;
2055 flag.takedamage = DAMAGE_NO;
2056 flag.solid = SOLID_NOT;
2057 flag.nextthink = FALSE; // stop thinking
2059 //dprint("stopping the ", flag.netname, " from moving.\n");
2067 // do nothing for these flags
2076 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
2078 havocbot_ctf_reset_role(self);
2082 MUTATOR_HOOKFUNCTION(ctf_GetTeamCount)
2084 ret_float = ctf_teams;
2092 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
2093 CTF Starting point for a player in team one (Red).
2094 Keys: "angle" viewing angle when spawning. */
2095 void spawnfunc_info_player_team1()
2097 if(g_assault) { remove(self); return; }
2099 self.team = NUM_TEAM_1; // red
2100 spawnfunc_info_player_deathmatch();
2104 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
2105 CTF Starting point for a player in team two (Blue).
2106 Keys: "angle" viewing angle when spawning. */
2107 void spawnfunc_info_player_team2()
2109 if(g_assault) { remove(self); return; }
2111 self.team = NUM_TEAM_2; // blue
2112 spawnfunc_info_player_deathmatch();
2115 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
2116 CTF Starting point for a player in team three (Yellow).
2117 Keys: "angle" viewing angle when spawning. */
2118 void spawnfunc_info_player_team3()
2120 if(g_assault) { remove(self); return; }
2122 self.team = NUM_TEAM_3; // yellow
2123 spawnfunc_info_player_deathmatch();
2127 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
2128 CTF Starting point for a player in team four (Purple).
2129 Keys: "angle" viewing angle when spawning. */
2130 void spawnfunc_info_player_team4()
2132 if(g_assault) { remove(self); return; }
2134 self.team = NUM_TEAM_4; // purple
2135 spawnfunc_info_player_deathmatch();
2138 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2139 CTF flag for team one (Red).
2141 "angle" Angle the flag will point (minus 90 degrees)...
2142 "model" model to use, note this needs red and blue as skins 0 and 1...
2143 "noise" sound played when flag is picked up...
2144 "noise1" sound played when flag is returned by a teammate...
2145 "noise2" sound played when flag is captured...
2146 "noise3" sound played when flag is lost in the field and respawns itself...
2147 "noise4" sound played when flag is dropped by a player...
2148 "noise5" sound played when flag touches the ground... */
2149 void spawnfunc_item_flag_team1()
2151 if(!g_ctf) { remove(self); return; }
2153 ctf_FlagSetup(NUM_TEAM_1, self);
2156 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2157 CTF flag for team two (Blue).
2159 "angle" Angle the flag will point (minus 90 degrees)...
2160 "model" model to use, note this needs red and blue as skins 0 and 1...
2161 "noise" sound played when flag is picked up...
2162 "noise1" sound played when flag is returned by a teammate...
2163 "noise2" sound played when flag is captured...
2164 "noise3" sound played when flag is lost in the field and respawns itself...
2165 "noise4" sound played when flag is dropped by a player...
2166 "noise5" sound played when flag touches the ground... */
2167 void spawnfunc_item_flag_team2()
2169 if(!g_ctf) { remove(self); return; }
2171 ctf_FlagSetup(NUM_TEAM_2, self);
2174 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2175 CTF flag for team three (Yellow).
2177 "angle" Angle the flag will point (minus 90 degrees)...
2178 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2179 "noise" sound played when flag is picked up...
2180 "noise1" sound played when flag is returned by a teammate...
2181 "noise2" sound played when flag is captured...
2182 "noise3" sound played when flag is lost in the field and respawns itself...
2183 "noise4" sound played when flag is dropped by a player...
2184 "noise5" sound played when flag touches the ground... */
2185 void spawnfunc_item_flag_team3()
2187 if(!g_ctf) { remove(self); return; }
2189 ctf_FlagSetup(NUM_TEAM_3, self);
2192 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2193 CTF flag for team two (Pink).
2195 "angle" Angle the flag will point (minus 90 degrees)...
2196 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2197 "noise" sound played when flag is picked up...
2198 "noise1" sound played when flag is returned by a teammate...
2199 "noise2" sound played when flag is captured...
2200 "noise3" sound played when flag is lost in the field and respawns itself...
2201 "noise4" sound played when flag is dropped by a player...
2202 "noise5" sound played when flag touches the ground... */
2203 void spawnfunc_item_flag_team4()
2205 if(!g_ctf) { remove(self); return; }
2207 ctf_FlagSetup(NUM_TEAM_4, self);
2210 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2211 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2212 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.
2214 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2215 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2216 void spawnfunc_ctf_team()
2218 if(!g_ctf) { remove(self); return; }
2220 self.classname = "ctf_team";
2221 self.team = self.cnt + 1;
2224 // compatibility for quake maps
2225 void spawnfunc_team_CTF_redflag() { spawnfunc_item_flag_team1(); }
2226 void spawnfunc_team_CTF_blueflag() { spawnfunc_item_flag_team2(); }
2227 void spawnfunc_team_CTF_redplayer() { spawnfunc_info_player_team1(); }
2228 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2(); }
2229 void spawnfunc_team_CTF_redspawn() { spawnfunc_info_player_team1(); }
2230 void spawnfunc_team_CTF_bluespawn() { spawnfunc_info_player_team2(); }
2238 void ctf_ScoreRules(float teams)
2240 CheckAllowedTeams(world);
2241 ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, TRUE);
2242 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2243 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2244 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2245 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2246 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2247 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2248 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2249 ScoreRules_basics_end();
2252 // code from here on is just to support maps that don't have flag and team entities
2253 void ctf_SpawnTeam (string teamname, float teamcolor)
2258 self.classname = "ctf_team";
2259 self.netname = teamname;
2260 self.cnt = teamcolor;
2262 spawnfunc_ctf_team();
2267 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2272 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2274 if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2275 if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2278 ctf_teams = bound(2, ctf_teams, 4);
2280 // if no teams are found, spawn defaults
2281 if(find(world, classname, "ctf_team") == world)
2283 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2284 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2285 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2287 ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2289 ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2292 ctf_ScoreRules(ctf_teams);
2295 void ctf_Initialize()
2297 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2299 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2300 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2301 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2303 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2307 MUTATOR_DEFINITION(gamemode_ctf)
2309 MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2310 MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2311 MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2312 MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2313 MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2314 MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2315 MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2316 MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2317 MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2318 MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2319 MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2320 MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2321 MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2322 MUTATOR_HOOK(HavocBot_ChooseRule, ctf_BotRoles, CBC_ORDER_ANY);
2323 MUTATOR_HOOK(GetTeamCount, ctf_GetTeamCount, CBC_ORDER_ANY);
2327 if(time > 1) // game loads at time 1
2328 error("This is a game type and it cannot be added at runtime.");
2332 MUTATOR_ONROLLBACK_OR_REMOVE
2334 // we actually cannot roll back ctf_Initialize here
2335 // BUT: we don't need to! If this gets called, adding always
2341 print("This is a game type and it cannot be removed at runtime.");