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) * 2);
48 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
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; }
114 float ctf_Stalemate_customizeentityforclient()
116 // make spectators see what the player would see
118 e = WaypointSprite_getviewentity(other);
119 wp_owner = self.owner;
122 if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner))
132 // =======================
133 // CaptureShield Functions
134 // =======================
136 float ctf_CaptureShield_CheckStatus(entity p)
138 float s, s2, s3, s4, se, se2, se3, se4, sr, ser;
140 float players_worseeq, players_total;
142 if(ctf_captureshield_max_ratio <= 0)
145 s = PlayerScore_Add(p, SP_CTF_CAPS, 0);
146 s2 = PlayerScore_Add(p, SP_CTF_PICKUPS, 0);
147 s3 = PlayerScore_Add(p, SP_CTF_RETURNS, 0);
148 s4 = PlayerScore_Add(p, SP_CTF_FCKILLS, 0);
150 sr = ((s - s2) + (s3 + s4));
152 if(sr >= -ctf_captureshield_min_negscore)
155 players_total = players_worseeq = 0;
160 se = PlayerScore_Add(e, SP_CTF_CAPS, 0);
161 se2 = PlayerScore_Add(e, SP_CTF_PICKUPS, 0);
162 se3 = PlayerScore_Add(e, SP_CTF_RETURNS, 0);
163 se4 = PlayerScore_Add(e, SP_CTF_FCKILLS, 0);
165 ser = ((se - se2) + (se3 + se4));
172 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
173 // use this rule here
175 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
181 void ctf_CaptureShield_Update(entity player, float wanted_status)
183 float updated_status = ctf_CaptureShield_CheckStatus(player);
184 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
186 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
187 player.ctf_captureshielded = updated_status;
191 float ctf_CaptureShield_Customize()
193 if(!other.ctf_captureshielded) { return FALSE; }
194 if(SAME_TEAM(self, other)) { return FALSE; }
199 void ctf_CaptureShield_Touch()
201 if(!other.ctf_captureshielded) { return; }
202 if(SAME_TEAM(self, other)) { return; }
204 vector mymid = (self.absmin + self.absmax) * 0.5;
205 vector othermid = (other.absmin + other.absmax) * 0.5;
207 Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
208 Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED);
211 void ctf_CaptureShield_Spawn(entity flag)
213 entity shield = spawn();
216 shield.team = self.team;
217 shield.touch = ctf_CaptureShield_Touch;
218 shield.customizeentityforclient = ctf_CaptureShield_Customize;
219 shield.classname = "ctf_captureshield";
220 shield.effects = EF_ADDITIVE;
221 shield.movetype = MOVETYPE_NOCLIP;
222 shield.solid = SOLID_TRIGGER;
223 shield.avelocity = '7 0 11';
226 setorigin(shield, self.origin);
227 setmodel(shield, "models/ctf/shield.md3");
228 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
232 // ====================
233 // Drop/Pass/Throw Code
234 // ====================
236 void ctf_Handle_Drop(entity flag, entity player, float droptype)
239 player = (player ? player : flag.pass_sender);
242 flag.movetype = MOVETYPE_TOSS;
243 flag.takedamage = DAMAGE_YES;
244 flag.angles = '0 0 0';
245 flag.health = flag.max_flag_health;
246 flag.ctf_droptime = time;
247 flag.ctf_dropper = player;
248 flag.ctf_status = FLAG_DROPPED;
250 // messages and sounds
251 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_LOST_), player.netname);
252 sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
253 ctf_EventLog("dropped", player.team, player);
256 PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);
257 PlayerScore_Add(player, SP_CTF_DROPS, 1);
260 if(autocvar_g_ctf_flag_dropped_waypoint)
261 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));
263 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
265 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
266 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
269 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
271 if(droptype == DROP_PASS)
273 flag.pass_distance = 0;
274 flag.pass_sender = world;
275 flag.pass_target = world;
279 void ctf_Handle_Retrieve(entity flag, entity player)
281 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
282 entity sender = flag.pass_sender;
284 // transfer flag to player
286 flag.owner.flagcarried = flag;
289 setattachment(flag, player, "");
290 setorigin(flag, FLAG_CARRY_OFFSET);
291 flag.movetype = MOVETYPE_NONE;
292 flag.takedamage = DAMAGE_NO;
293 flag.solid = SOLID_NOT;
294 flag.angles = '0 0 0';
295 flag.ctf_status = FLAG_CARRY;
297 // messages and sounds
298 sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
299 ctf_EventLog("receive", flag.team, player);
301 FOR_EACH_REALPLAYER(tmp_player)
303 if(tmp_player == sender)
304 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_SENT_), player.netname);
305 else if(tmp_player == player)
306 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_RECEIVED_), sender.netname);
307 else if(SAME_TEAM(tmp_player, sender))
308 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_OTHER_), sender.netname, player.netname);
311 // create new waypoint
312 ctf_FlagcarrierWaypoints(player);
314 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
315 player.throw_antispam = sender.throw_antispam;
317 flag.pass_distance = 0;
318 flag.pass_sender = world;
319 flag.pass_target = world;
322 void ctf_Handle_Throw(entity player, entity receiver, float droptype)
324 entity flag = player.flagcarried;
325 vector targ_origin, flag_velocity;
327 if(!flag) { return; }
328 if((droptype == DROP_PASS) && !receiver) { return; }
330 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
333 setattachment(flag, world, "");
334 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
335 flag.owner.flagcarried = world;
337 flag.solid = SOLID_TRIGGER;
338 flag.ctf_dropper = player;
339 flag.ctf_droptime = time;
341 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
348 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
349 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
350 WarpZone_RefSys_Copy(flag, receiver);
351 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
352 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
354 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
355 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, FALSE);
358 flag.movetype = MOVETYPE_FLY;
359 flag.takedamage = DAMAGE_NO;
360 flag.pass_sender = player;
361 flag.pass_target = receiver;
362 flag.ctf_status = FLAG_PASSING;
365 sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
366 WarpZone_TrailParticles(world, particleeffectnum(flag.passeffect), player.origin, targ_origin);
367 ctf_EventLog("pass", flag.team, player);
373 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'));
375 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)));
376 flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, FALSE);
377 ctf_Handle_Drop(flag, player, droptype);
383 flag.velocity = '0 0 0'; // do nothing
390 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);
391 ctf_Handle_Drop(flag, player, droptype);
396 // kill old waypointsprite
397 WaypointSprite_Ping(player.wps_flagcarrier);
398 WaypointSprite_Kill(player.wps_flagcarrier);
400 if(player.wps_enemyflagcarrier)
401 WaypointSprite_Kill(player.wps_enemyflagcarrier);
404 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
412 void ctf_Handle_Capture(entity flag, entity toucher, float capturetype)
414 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
415 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
416 float old_time, new_time;
418 if not(player) { return; } // without someone to give the reward to, we can't possibly cap
419 if(CTF_DIFFTEAM(player, flag)) { return; }
421 // messages and sounds
422 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(enemy_flag, CENTER_CTF_CAPTURE_));
423 ctf_CaptureRecord(enemy_flag, player);
424 sound(player, CH_TRIGGER, ((CTF_DIFFTEAM(player, flag)) ? enemy_flag.snd_flag_capture : flag.snd_flag_capture), VOL_BASE, ATTEN_NONE);
428 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
429 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
434 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
435 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
437 old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
438 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
439 if(!old_time || new_time < old_time)
440 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
443 pointparticles(particleeffectnum(flag.capeffect), flag.origin, '0 0 0', 1);
444 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
447 if(capturetype == CAPTURE_NORMAL)
449 WaypointSprite_Kill(player.wps_flagcarrier);
450 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
452 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
453 { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
457 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
458 ctf_RespawnFlag(enemy_flag);
461 void ctf_Handle_Return(entity flag, entity player)
463 // messages and sounds
464 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_RETURN_));
465 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_), player.netname);
466 sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
467 ctf_EventLog("return", flag.team, player);
470 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
471 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
473 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
477 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
478 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
479 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
483 ctf_RespawnFlag(flag);
486 void ctf_Handle_Pickup(entity flag, entity player, float pickuptype)
489 float pickup_dropped_score; // used to calculate dropped pickup score
491 // attach the flag to the player
493 player.flagcarried = flag;
494 setattachment(flag, player, "");
495 setorigin(flag, FLAG_CARRY_OFFSET);
498 flag.movetype = MOVETYPE_NONE;
499 flag.takedamage = DAMAGE_NO;
500 flag.solid = SOLID_NOT;
501 flag.angles = '0 0 0';
502 flag.ctf_status = FLAG_CARRY;
506 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
507 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
511 // messages and sounds
512 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_PICKUP_), player.netname);
513 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PICKUP_));
514 if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
516 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_PICKUP_TEAM_), Team_ColorCode(player.team), player.netname);
517 Send_Notification(NOTIF_TEAM, flag, MSG_CHOICE, CHOICE_CTF_PICKUP_ENEMY, Team_ColorCode(player.team), player.netname);
519 sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
522 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
527 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
528 ctf_EventLog("steal", flag.team, player);
534 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);
535 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);
536 dprint("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
537 PlayerTeamScore_AddScore(player, pickup_dropped_score);
538 ctf_EventLog("pickup", flag.team, player);
546 if(pickuptype == PICKUP_BASE)
548 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
549 if((player.speedrunning) && (ctf_captimerecord))
550 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
554 pointparticles(particleeffectnum(flag.toucheffect), player.origin, '0 0 0', 1);
557 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
558 ctf_FlagcarrierWaypoints(player);
559 WaypointSprite_Ping(player.wps_flagcarrier);
563 // ===================
564 // Main Flag Functions
565 // ===================
567 void ctf_CheckFlagReturn(entity flag, float returntype)
569 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
571 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
573 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
577 case RETURN_DROPPED: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DROPPED_)); break;
578 case RETURN_DAMAGE: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DAMAGED_)); break;
579 case RETURN_SPEEDRUN: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_SPEEDRUN_), ctf_captimerecord); break;
580 case RETURN_NEEDKILL: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_NEEDKILL_)); break;
584 { Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_TIMEOUT_)); break; }
586 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
587 ctf_EventLog("returned", flag.team, world);
588 ctf_RespawnFlag(flag);
593 void ctf_CheckStalemate(void)
596 float stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0;
599 entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
601 // build list of stale flags
602 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
604 if(autocvar_g_ctf_stalemate)
605 if(tmp_entity.ctf_status != FLAG_BASE)
606 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time)
608 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
609 ctf_staleflaglist = tmp_entity;
611 switch(tmp_entity.team)
613 case NUM_TEAM_1: ++stale_red_flags; break;
614 case NUM_TEAM_2: ++stale_blue_flags; break;
615 case NUM_TEAM_3: ++stale_yellow_flags; break;
616 case NUM_TEAM_4: ++stale_pink_flags; break;
621 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
623 if(stale_flags == ctf_teams)
624 ctf_stalemate = TRUE;
625 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
626 { ctf_stalemate = FALSE; wpforenemy_announced = FALSE; }
627 else if(stale_flags < ctf_teams && autocvar_g_ctf_stalemate_endcondition == 1)
628 { ctf_stalemate = FALSE; wpforenemy_announced = FALSE; }
630 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
633 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
635 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
637 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));
638 tmp_entity.owner.wps_enemyflagcarrier.customizeentityforclient = ctf_Stalemate_customizeentityforclient;
642 if not(wpforenemy_announced)
644 FOR_EACH_REALPLAYER(tmp_entity)
645 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CENTER, ((tmp_entity.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER));
647 wpforenemy_announced = TRUE;
652 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
654 if(ITEM_DAMAGE_NEEDKILL(deathtype))
656 // automatically kill the flag and return it
658 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
661 if(autocvar_g_ctf_flag_return_damage)
663 // reduce health and check if it should be returned
664 self.health = self.health - damage;
665 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
675 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
678 if(self == ctf_worldflaglist) // only for the first flag
679 FOR_EACH_CLIENT(tmp_entity)
680 ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
683 if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
684 dprint("wtf the flag got squashed?\n");
685 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
686 if(!trace_startsolid) // can we resize it without getting stuck?
687 setsize(self, FLAG_MIN, FLAG_MAX); }
689 switch(self.ctf_status) // reset flag angles in case warpzones adjust it
693 self.angles = '0 0 0';
701 switch(self.ctf_status)
705 if(autocvar_g_ctf_dropped_capture_radius)
707 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
708 if(tmp_entity.ctf_status == FLAG_DROPPED)
709 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
710 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
711 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
718 if(autocvar_g_ctf_flag_dropped_floatinwater)
720 vector midpoint = ((self.absmin + self.absmax) * 0.5);
721 if(pointcontents(midpoint) == CONTENT_WATER)
723 self.velocity = self.velocity * 0.5;
725 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
726 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
728 { self.movetype = MOVETYPE_FLY; }
730 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
732 if(autocvar_g_ctf_flag_return_dropped)
734 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
737 ctf_CheckFlagReturn(self, RETURN_DROPPED);
741 if(autocvar_g_ctf_flag_return_time)
743 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
744 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
752 if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
755 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
759 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
763 if(autocvar_g_ctf_stalemate)
765 if(time >= wpforenemy_nextthink)
767 ctf_CheckStalemate();
768 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
771 if(CTF_SAMETEAM(self, self.owner))
773 // drop the flag if reverse status has changed
774 ctf_Handle_Throw(self.owner, world, DROP_THROW);
781 vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
782 targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
783 WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
785 if((self.pass_target == world)
786 || (self.pass_target.deadflag != DEAD_NO)
787 || (self.pass_target.flagcarried)
788 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
789 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
790 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
792 // give up, pass failed
793 ctf_Handle_Drop(self, world, DROP_PASS);
797 // still a viable target, go for it
798 ctf_CalculatePassVelocity(self, targ_origin, self.origin, TRUE);
803 default: // this should never happen
805 dprint("ctf_FlagThink(): Flag exists with no status?\n");
813 if(gameover) { return; }
815 entity toucher = other;
817 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
818 if(ITEM_TOUCH_NEEDKILL())
821 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
825 // special touch behaviors
826 if(toucher.vehicle_flags & VHF_ISVEHICLE)
828 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
829 toucher = toucher.owner; // the player is actually the vehicle owner, not other
831 return; // do nothing
833 else if not(IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
835 if(time > self.wait) // if we haven't in a while, play a sound/effect
837 pointparticles(particleeffectnum(self.toucheffect), self.origin, '0 0 0', 1);
838 sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTEN_NORM);
839 self.wait = time + FLAG_TOUCHRATE;
843 else if(toucher.deadflag != DEAD_NO) { return; }
845 switch(self.ctf_status)
849 if(SAME_TEAM(toucher, self) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, self))
850 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
851 else if(DIFF_TEAM(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time))
852 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
858 if(SAME_TEAM(toucher, self))
859 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
860 else if((!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
861 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
867 dprint("Someone touched a flag even though it was being carried?\n");
873 if((IS_PLAYER(toucher)) && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
875 if(DIFF_TEAM(toucher, self.pass_sender))
876 ctf_Handle_Return(self, toucher);
878 ctf_Handle_Retrieve(self, toucher);
886 void ctf_RespawnFlag(entity flag)
888 // check for flag respawn being called twice in a row
889 if(flag.last_respawn > time - 0.5)
890 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
892 flag.last_respawn = time;
894 // reset the player (if there is one)
895 if((flag.owner) && (flag.owner.flagcarried == flag))
897 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
898 WaypointSprite_Kill(flag.wps_flagcarrier);
900 flag.owner.flagcarried = world;
902 if(flag.speedrunning)
903 ctf_FakeTimeLimit(flag.owner, -1);
906 if(flag.ctf_status == FLAG_DROPPED)
907 { WaypointSprite_Kill(flag.wps_flagdropped); }
910 setattachment(flag, world, "");
911 setorigin(flag, flag.ctf_spawnorigin);
913 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
914 flag.takedamage = DAMAGE_NO;
915 flag.health = flag.max_flag_health;
916 flag.solid = SOLID_TRIGGER;
917 flag.velocity = '0 0 0';
918 flag.angles = flag.mangle;
919 flag.flags = FL_ITEM | FL_NOTARGET;
921 flag.ctf_status = FLAG_BASE;
923 flag.pass_distance = 0;
924 flag.pass_sender = world;
925 flag.pass_target = world;
926 flag.ctf_dropper = world;
927 flag.ctf_pickuptime = 0;
928 flag.ctf_droptime = 0;
934 if(IS_PLAYER(self.owner))
935 ctf_Handle_Throw(self.owner, world, DROP_RESET);
937 ctf_RespawnFlag(self);
940 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
943 waypoint_spawnforitem_force(self, self.origin);
944 self.nearestwaypointtimeout = 0; // activate waypointing again
945 self.bot_basewaypoint = self.nearestwaypoint;
948 string basename = "base";
952 case NUM_TEAM_1: basename = "redbase"; break;
953 case NUM_TEAM_2: basename = "bluebase"; break;
954 case NUM_TEAM_3: basename = "yellowbase"; break;
955 case NUM_TEAM_4: basename = "pinkbase"; break;
958 WaypointSprite_SpawnFixed(basename, self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
959 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
961 // captureshield setup
962 ctf_CaptureShield_Spawn(self);
965 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
968 self = flag; // for later usage with droptofloor()
971 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
972 ctf_worldflaglist = flag;
974 setattachment(flag, world, "");
976 flag.netname = strcat(Static_Team_ColorName_Upper(teamnumber), "^7 flag");
977 flag.team = teamnumber;
978 flag.classname = "item_flag_team";
979 flag.target = "###item###"; // wut?
980 flag.flags = FL_ITEM | FL_NOTARGET;
981 flag.solid = SOLID_TRIGGER;
982 flag.takedamage = DAMAGE_NO;
983 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
984 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
985 flag.health = flag.max_flag_health;
986 flag.event_damage = ctf_FlagDamage;
987 flag.pushable = TRUE;
988 flag.teleportable = TELEPORT_NORMAL;
989 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
990 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
991 flag.velocity = '0 0 0';
992 flag.mangle = flag.angles;
993 flag.reset = ctf_Reset;
994 flag.touch = ctf_FlagTouch;
995 flag.think = ctf_FlagThink;
996 flag.nextthink = time + FLAG_THINKRATE;
997 flag.ctf_status = FLAG_BASE;
1000 if(flag.model == "") { flag.model = strzone(cvar_string(strcat("g_ctf_flag_", Static_Team_ColorName_Lower(teamnumber), "_model"))); }
1001 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1002 if(!flag.skin) { flag.skin = cvar(strcat("g_ctf_flag_", Static_Team_ColorName_Lower(teamnumber), "_skin")); }
1003 if(flag.toucheffect == "") { flag.toucheffect = strzone(strcat(Static_Team_ColorName_Lower(teamnumber), "_touch")); }
1004 if(flag.passeffect == "") { flag.passeffect = strzone(strcat(Static_Team_ColorName_Lower(teamnumber), "_pass")); }
1005 if(flag.capeffect == "") { flag.capeffect = strzone(strcat(Static_Team_ColorName_Lower(teamnumber), "_cap")); }
1008 if(flag.snd_flag_taken == "") { flag.snd_flag_taken = strzone(strcat("ctf/", Static_Team_ColorName_Lower(teamnumber), "_taken.wav")); }
1009 if(flag.snd_flag_returned == "") { flag.snd_flag_returned = strzone(strcat("ctf/", Static_Team_ColorName_Lower(teamnumber), "_returned.wav")); }
1010 if(flag.snd_flag_capture == "") { flag.snd_flag_capture = strzone(strcat("ctf/", Static_Team_ColorName_Lower(teamnumber), "_capture.wav")); } // blue team scores by capturing the red flag
1011 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.
1012 if(flag.snd_flag_dropped == "") { flag.snd_flag_dropped = strzone(strcat("ctf/", Static_Team_ColorName_Lower(teamnumber), "_dropped.wav")); }
1013 if(flag.snd_flag_touch == "") { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
1014 if(flag.snd_flag_pass == "") { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
1017 precache_sound(flag.snd_flag_taken);
1018 precache_sound(flag.snd_flag_returned);
1019 precache_sound(flag.snd_flag_capture);
1020 precache_sound(flag.snd_flag_respawn);
1021 precache_sound(flag.snd_flag_dropped);
1022 precache_sound(flag.snd_flag_touch);
1023 precache_sound(flag.snd_flag_pass);
1024 precache_model(flag.model);
1025 precache_model("models/ctf/shield.md3");
1026 precache_model("models/ctf/shockwavetransring.md3");
1029 setmodel(flag, flag.model); // precision set below
1030 setsize(flag, FLAG_MIN, FLAG_MAX);
1031 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1033 if(autocvar_g_ctf_flag_glowtrails)
1037 case NUM_TEAM_1: flag.glow_color = 251; break; // red
1038 case NUM_TEAM_2: flag.glow_color = 210; break; // blue
1039 case NUM_TEAM_3: flag.glow_color = 110; break; // yellow
1040 case NUM_TEAM_4: flag.glow_color = 145; break; // pink
1042 flag.glow_size = 25;
1043 flag.glow_trail = 1;
1046 flag.effects |= EF_LOWPRECISION;
1047 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1048 if(autocvar_g_ctf_dynamiclights)
1052 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1053 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1054 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1055 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1060 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1062 flag.dropped_origin = flag.origin;
1063 flag.noalign = TRUE;
1064 flag.movetype = MOVETYPE_NONE;
1066 else // drop to floor, automatically find a platform and set that as spawn origin
1068 flag.noalign = FALSE;
1071 flag.movetype = MOVETYPE_TOSS;
1074 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1082 // NOTE: LEGACY CODE, needs to be re-written!
1084 void havocbot_calculate_middlepoint()
1088 vector fo = '0 0 0';
1091 f = ctf_worldflaglist;
1096 f = f.ctf_worldflagnext;
1100 havocbot_ctf_middlepoint = s * (1.0 / n);
1101 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1105 entity havocbot_ctf_find_flag(entity bot)
1108 f = ctf_worldflaglist;
1111 if (CTF_SAMETEAM(bot, f))
1113 f = f.ctf_worldflagnext;
1118 entity havocbot_ctf_find_enemy_flag(entity bot)
1121 f = ctf_worldflaglist;
1124 if (CTF_DIFFTEAM(bot, f))
1126 f = f.ctf_worldflagnext;
1131 float havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1139 FOR_EACH_PLAYER(head)
1141 if(head.team!=bot.team || head.deadflag != DEAD_NO || head == bot)
1144 if(vlen(head.origin - org) < tc_radius)
1151 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1154 head = ctf_worldflaglist;
1157 if (CTF_SAMETEAM(self, head))
1159 head = head.ctf_worldflagnext;
1162 navigation_routerating(head, ratingscale, 10000);
1165 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1168 head = ctf_worldflaglist;
1171 if (CTF_SAMETEAM(self, head))
1173 head = head.ctf_worldflagnext;
1178 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1181 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1184 head = ctf_worldflaglist;
1187 if(CTF_DIFFTEAM(self, head))
1189 head = head.ctf_worldflagnext;
1192 navigation_routerating(head, ratingscale, 10000);
1195 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1197 if not(bot_waypoints_for_items)
1199 havocbot_goalrating_ctf_enemyflag(ratingscale);
1205 head = havocbot_ctf_find_enemy_flag(self);
1210 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1213 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1217 mf = havocbot_ctf_find_flag(self);
1219 if(mf.ctf_status == FLAG_BASE)
1223 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1226 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1229 head = ctf_worldflaglist;
1232 // flag is out in the field
1233 if(head.ctf_status != FLAG_BASE)
1234 if(head.tag_entity==world) // dropped
1238 if(vlen(org-head.origin)<df_radius)
1239 navigation_routerating(head, ratingscale, 10000);
1242 navigation_routerating(head, ratingscale, 10000);
1245 head = head.ctf_worldflagnext;
1249 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1253 head = findchainfloat(bot_pickup, TRUE);
1256 // gather health and armor only
1258 if (head.health || head.armorvalue)
1259 if (vlen(head.origin - org) < sradius)
1261 // get the value of the item
1262 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1264 navigation_routerating(head, t * ratingscale, 500);
1270 void havocbot_ctf_reset_role(entity bot)
1272 float cdefense, cmiddle, coffense;
1273 entity mf, ef, head;
1276 if(bot.deadflag != DEAD_NO)
1279 if(vlen(havocbot_ctf_middlepoint)==0)
1280 havocbot_calculate_middlepoint();
1283 if (bot.flagcarried)
1285 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1289 mf = havocbot_ctf_find_flag(bot);
1290 ef = havocbot_ctf_find_enemy_flag(bot);
1292 // Retrieve stolen flag
1293 if(mf.ctf_status!=FLAG_BASE)
1295 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1299 // If enemy flag is taken go to the middle to intercept pursuers
1300 if(ef.ctf_status!=FLAG_BASE)
1302 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1306 // if there is only me on the team switch to offense
1308 FOR_EACH_PLAYER(head)
1309 if(head.team==bot.team)
1314 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1318 // Evaluate best position to take
1319 // Count mates on middle position
1320 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1322 // Count mates on defense position
1323 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1325 // Count mates on offense position
1326 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1328 if(cdefense<=coffense)
1329 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1330 else if(coffense<=cmiddle)
1331 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1333 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1336 void havocbot_role_ctf_carrier()
1338 if(self.deadflag != DEAD_NO)
1340 havocbot_ctf_reset_role(self);
1344 if (self.flagcarried == world)
1346 havocbot_ctf_reset_role(self);
1350 if (self.bot_strategytime < time)
1352 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1354 navigation_goalrating_start();
1355 havocbot_goalrating_ctf_ourbase(50000);
1358 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1360 navigation_goalrating_end();
1362 if (self.navigation_hasgoals)
1363 self.havocbot_cantfindflag = time + 10;
1364 else if (time > self.havocbot_cantfindflag)
1366 // Can't navigate to my own base, suicide!
1367 // TODO: drop it and wander around
1368 //Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1374 void havocbot_role_ctf_escort()
1378 if(self.deadflag != DEAD_NO)
1380 havocbot_ctf_reset_role(self);
1384 if (self.flagcarried)
1386 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1390 // If enemy flag is back on the base switch to previous role
1391 ef = havocbot_ctf_find_enemy_flag(self);
1392 if(ef.ctf_status==FLAG_BASE)
1394 self.havocbot_role = self.havocbot_previous_role;
1395 self.havocbot_role_timeout = 0;
1399 // If the flag carrier reached the base switch to defense
1400 mf = havocbot_ctf_find_flag(self);
1401 if(mf.ctf_status!=FLAG_BASE)
1402 if(vlen(ef.origin - mf.dropped_origin) < 300)
1404 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1408 // Set the role timeout if necessary
1409 if (!self.havocbot_role_timeout)
1411 self.havocbot_role_timeout = time + random() * 30 + 60;
1414 // If nothing happened just switch to previous role
1415 if (time > self.havocbot_role_timeout)
1417 self.havocbot_role = self.havocbot_previous_role;
1418 self.havocbot_role_timeout = 0;
1422 // Chase the flag carrier
1423 if (self.bot_strategytime < time)
1425 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1426 navigation_goalrating_start();
1427 havocbot_goalrating_ctf_enemyflag(30000);
1428 havocbot_goalrating_ctf_ourstolenflag(40000);
1429 havocbot_goalrating_items(10000, self.origin, 10000);
1430 navigation_goalrating_end();
1434 void havocbot_role_ctf_offense()
1439 if(self.deadflag != DEAD_NO)
1441 havocbot_ctf_reset_role(self);
1445 if (self.flagcarried)
1447 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1452 mf = havocbot_ctf_find_flag(self);
1453 ef = havocbot_ctf_find_enemy_flag(self);
1456 if(mf.ctf_status!=FLAG_BASE)
1459 pos = mf.tag_entity.origin;
1463 // Try to get it if closer than the enemy base
1464 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1466 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1471 // Escort flag carrier
1472 if(ef.ctf_status!=FLAG_BASE)
1475 pos = ef.tag_entity.origin;
1479 if(vlen(pos-mf.dropped_origin)>700)
1481 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1486 // About to fail, switch to middlefield
1489 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1493 // Set the role timeout if necessary
1494 if (!self.havocbot_role_timeout)
1495 self.havocbot_role_timeout = time + 120;
1497 if (time > self.havocbot_role_timeout)
1499 havocbot_ctf_reset_role(self);
1503 if (self.bot_strategytime < time)
1505 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1506 navigation_goalrating_start();
1507 havocbot_goalrating_ctf_ourstolenflag(50000);
1508 havocbot_goalrating_ctf_enemybase(20000);
1509 havocbot_goalrating_items(5000, self.origin, 1000);
1510 havocbot_goalrating_items(1000, self.origin, 10000);
1511 navigation_goalrating_end();
1515 // Retriever (temporary role):
1516 void havocbot_role_ctf_retriever()
1520 if(self.deadflag != DEAD_NO)
1522 havocbot_ctf_reset_role(self);
1526 if (self.flagcarried)
1528 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1532 // If flag is back on the base switch to previous role
1533 mf = havocbot_ctf_find_flag(self);
1534 if(mf.ctf_status==FLAG_BASE)
1536 havocbot_ctf_reset_role(self);
1540 if (!self.havocbot_role_timeout)
1541 self.havocbot_role_timeout = time + 20;
1543 if (time > self.havocbot_role_timeout)
1545 havocbot_ctf_reset_role(self);
1549 if (self.bot_strategytime < time)
1554 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1555 navigation_goalrating_start();
1556 havocbot_goalrating_ctf_ourstolenflag(50000);
1557 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1558 havocbot_goalrating_ctf_enemybase(30000);
1559 havocbot_goalrating_items(500, self.origin, rt_radius);
1560 navigation_goalrating_end();
1564 void havocbot_role_ctf_middle()
1568 if(self.deadflag != DEAD_NO)
1570 havocbot_ctf_reset_role(self);
1574 if (self.flagcarried)
1576 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1580 mf = havocbot_ctf_find_flag(self);
1581 if(mf.ctf_status!=FLAG_BASE)
1583 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1587 if (!self.havocbot_role_timeout)
1588 self.havocbot_role_timeout = time + 10;
1590 if (time > self.havocbot_role_timeout)
1592 havocbot_ctf_reset_role(self);
1596 if (self.bot_strategytime < time)
1600 org = havocbot_ctf_middlepoint;
1601 org_z = self.origin_z;
1603 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1604 navigation_goalrating_start();
1605 havocbot_goalrating_ctf_ourstolenflag(50000);
1606 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1607 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1608 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1609 havocbot_goalrating_items(2500, self.origin, 10000);
1610 havocbot_goalrating_ctf_enemybase(2500);
1611 navigation_goalrating_end();
1615 void havocbot_role_ctf_defense()
1619 if(self.deadflag != DEAD_NO)
1621 havocbot_ctf_reset_role(self);
1625 if (self.flagcarried)
1627 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1631 // If own flag was captured
1632 mf = havocbot_ctf_find_flag(self);
1633 if(mf.ctf_status!=FLAG_BASE)
1635 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1639 if (!self.havocbot_role_timeout)
1640 self.havocbot_role_timeout = time + 30;
1642 if (time > self.havocbot_role_timeout)
1644 havocbot_ctf_reset_role(self);
1647 if (self.bot_strategytime < time)
1652 org = mf.dropped_origin;
1653 mp_radius = havocbot_ctf_middlepoint_radius;
1655 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1656 navigation_goalrating_start();
1658 // if enemies are closer to our base, go there
1659 entity head, closestplayer = world;
1660 float distance, bestdistance = 10000;
1661 FOR_EACH_PLAYER(head)
1663 if(head.deadflag!=DEAD_NO)
1666 distance = vlen(org - head.origin);
1667 if(distance<bestdistance)
1669 closestplayer = head;
1670 bestdistance = distance;
1675 if(closestplayer.team!=self.team)
1676 if(vlen(org - self.origin)>1000)
1677 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1678 havocbot_goalrating_ctf_ourbase(30000);
1680 havocbot_goalrating_ctf_ourstolenflag(20000);
1681 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1682 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1683 havocbot_goalrating_items(10000, org, mp_radius);
1684 havocbot_goalrating_items(5000, self.origin, 10000);
1685 navigation_goalrating_end();
1689 void havocbot_role_ctf_setrole(entity bot, float role)
1691 dprint(strcat(bot.netname," switched to "));
1694 case HAVOCBOT_CTF_ROLE_CARRIER:
1696 bot.havocbot_role = havocbot_role_ctf_carrier;
1697 bot.havocbot_role_timeout = 0;
1698 bot.havocbot_cantfindflag = time + 10;
1699 bot.bot_strategytime = 0;
1701 case HAVOCBOT_CTF_ROLE_DEFENSE:
1703 bot.havocbot_role = havocbot_role_ctf_defense;
1704 bot.havocbot_role_timeout = 0;
1706 case HAVOCBOT_CTF_ROLE_MIDDLE:
1708 bot.havocbot_role = havocbot_role_ctf_middle;
1709 bot.havocbot_role_timeout = 0;
1711 case HAVOCBOT_CTF_ROLE_OFFENSE:
1713 bot.havocbot_role = havocbot_role_ctf_offense;
1714 bot.havocbot_role_timeout = 0;
1716 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1717 dprint("retriever");
1718 bot.havocbot_previous_role = bot.havocbot_role;
1719 bot.havocbot_role = havocbot_role_ctf_retriever;
1720 bot.havocbot_role_timeout = time + 10;
1721 bot.bot_strategytime = 0;
1723 case HAVOCBOT_CTF_ROLE_ESCORT:
1725 bot.havocbot_previous_role = bot.havocbot_role;
1726 bot.havocbot_role = havocbot_role_ctf_escort;
1727 bot.havocbot_role_timeout = time + 30;
1728 bot.bot_strategytime = 0;
1739 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1743 float t = 0, t2 = 0, t3 = 0;
1745 // initially clear items so they can be set as necessary later.
1746 self.items &= ~(IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST
1747 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST
1748 | IT_YELLOW_FLAG_CARRYING | IT_YELLOW_FLAG_TAKEN | IT_YELLOW_FLAG_LOST
1749 | IT_PINK_FLAG_CARRYING | IT_PINK_FLAG_TAKEN | IT_PINK_FLAG_LOST
1752 // scan through all the flags and notify the client about them
1753 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1755 if(flag.team == NUM_TEAM_1) { t = IT_RED_FLAG_CARRYING; t2 = IT_RED_FLAG_TAKEN; t3 = IT_RED_FLAG_LOST; }
1756 if(flag.team == NUM_TEAM_2) { t = IT_BLUE_FLAG_CARRYING; t2 = IT_BLUE_FLAG_TAKEN; t3 = IT_BLUE_FLAG_LOST; }
1757 if(flag.team == NUM_TEAM_3) { t = IT_YELLOW_FLAG_CARRYING; t2 = IT_YELLOW_FLAG_TAKEN; t3 = IT_YELLOW_FLAG_LOST; }
1758 if(flag.team == NUM_TEAM_4) { t = IT_PINK_FLAG_CARRYING; t2 = IT_PINK_FLAG_TAKEN; t3 = IT_PINK_FLAG_LOST; }
1760 switch(flag.ctf_status)
1765 if((flag.owner == self) || (flag.pass_sender == self))
1766 self.items |= t; // carrying: self is currently carrying the flag
1768 self.items |= t2; // taken: someone else is carrying the flag
1773 self.items |= t3; // lost: the flag is dropped somewhere on the map
1779 // item for stopping players from capturing the flag too often
1780 if(self.ctf_captureshielded)
1781 self.items |= IT_CTF_SHIELDED;
1783 // update the health of the flag carrier waypointsprite
1784 if(self.wps_flagcarrier)
1785 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent));
1790 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1792 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1794 if(frag_target == frag_attacker) // damage done to yourself
1796 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1797 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1799 else // damage done to everyone else
1801 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1802 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1805 else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && DIFF_TEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
1807 if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent)))
1808 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1810 frag_target.wps_helpme_time = time;
1811 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1813 // todo: add notification for when flag carrier needs help?
1818 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1820 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
1822 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1823 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1826 if(frag_target.flagcarried)
1827 { ctf_Handle_Throw(frag_target, world, DROP_NORMAL); }
1832 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1835 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1838 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1840 entity flag; // temporary entity for the search method
1842 if(self.flagcarried)
1843 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1845 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1847 if(flag.pass_sender == self) { flag.pass_sender = world; }
1848 if(flag.pass_target == self) { flag.pass_target = world; }
1849 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
1855 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
1857 if(self.flagcarried)
1858 if(!autocvar_g_ctf_portalteleport)
1859 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1864 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
1866 if(MUTATOR_RETURNVALUE || gameover) { return FALSE; }
1868 entity player = self;
1870 if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1872 // pass the flag to a team mate
1873 if(autocvar_g_ctf_pass)
1875 entity head, closest_target = world;
1876 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, TRUE);
1878 while(head) // find the closest acceptable target to pass to
1880 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
1881 if(head != player && SAME_TEAM(head, player))
1882 if(!head.speedrunning && !head.vehicle)
1884 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
1885 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
1886 vector passer_center = CENTER_OR_VIEWOFS(player);
1888 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
1890 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
1892 if(IS_BOT_CLIENT(head))
1894 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1895 ctf_Handle_Throw(head, player, DROP_PASS);
1899 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
1900 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1902 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
1905 else if(player.flagcarried)
1909 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
1910 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
1911 { closest_target = head; }
1913 else { closest_target = head; }
1920 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return TRUE; }
1923 // throw the flag in front of you
1924 if(autocvar_g_ctf_throw && player.flagcarried)
1926 if(player.throw_count == -1)
1928 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
1930 player.throw_prevtime = time;
1931 player.throw_count = 1;
1932 ctf_Handle_Throw(player, world, DROP_THROW);
1937 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
1943 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
1944 else { player.throw_count += 1; }
1945 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
1947 player.throw_prevtime = time;
1948 ctf_Handle_Throw(player, world, DROP_THROW);
1957 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
1959 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
1961 self.wps_helpme_time = time;
1962 WaypointSprite_HelpMePing(self.wps_flagcarrier);
1964 else // create a normal help me waypointsprite
1966 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');
1967 WaypointSprite_Ping(self.wps_helpme);
1973 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
1975 if(vh_player.flagcarried)
1977 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
1979 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
1983 setattachment(vh_player.flagcarried, vh_vehicle, "");
1984 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
1985 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
1986 //vh_player.flagcarried.angles = '0 0 0';
1994 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
1996 if(vh_player.flagcarried)
1998 setattachment(vh_player.flagcarried, vh_player, "");
1999 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2000 vh_player.flagcarried.scale = FLAG_SCALE;
2001 vh_player.flagcarried.angles = '0 0 0';
2008 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
2010 if(self.flagcarried)
2012 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN_));
2013 ctf_RespawnFlag(self.flagcarried);
2020 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
2022 entity flag; // temporary entity for the search method
2024 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2026 switch(flag.ctf_status)
2031 // lock the flag, game is over
2032 flag.movetype = MOVETYPE_NONE;
2033 flag.takedamage = DAMAGE_NO;
2034 flag.solid = SOLID_NOT;
2035 flag.nextthink = FALSE; // stop thinking
2037 //dprint("stopping the ", flag.netname, " from moving.\n");
2045 // do nothing for these flags
2054 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
2056 havocbot_ctf_reset_role(self);
2060 MUTATOR_HOOKFUNCTION(ctf_GetTeamCount)
2062 ret_float = ctf_teams;
2070 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
2071 CTF Starting point for a player in team one (Red).
2072 Keys: "angle" viewing angle when spawning. */
2073 void spawnfunc_info_player_team1()
2075 if(g_assault) { remove(self); return; }
2077 self.team = NUM_TEAM_1; // red
2078 spawnfunc_info_player_deathmatch();
2082 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
2083 CTF Starting point for a player in team two (Blue).
2084 Keys: "angle" viewing angle when spawning. */
2085 void spawnfunc_info_player_team2()
2087 if(g_assault) { remove(self); return; }
2089 self.team = NUM_TEAM_2; // blue
2090 spawnfunc_info_player_deathmatch();
2093 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
2094 CTF Starting point for a player in team three (Yellow).
2095 Keys: "angle" viewing angle when spawning. */
2096 void spawnfunc_info_player_team3()
2098 if(g_assault) { remove(self); return; }
2100 self.team = NUM_TEAM_3; // yellow
2101 spawnfunc_info_player_deathmatch();
2105 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
2106 CTF Starting point for a player in team four (Purple).
2107 Keys: "angle" viewing angle when spawning. */
2108 void spawnfunc_info_player_team4()
2110 if(g_assault) { remove(self); return; }
2112 self.team = NUM_TEAM_4; // purple
2113 spawnfunc_info_player_deathmatch();
2116 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2117 CTF flag for team one (Red).
2119 "angle" Angle the flag will point (minus 90 degrees)...
2120 "model" model to use, note this needs red and blue as skins 0 and 1...
2121 "noise" sound played when flag is picked up...
2122 "noise1" sound played when flag is returned by a teammate...
2123 "noise2" sound played when flag is captured...
2124 "noise3" sound played when flag is lost in the field and respawns itself...
2125 "noise4" sound played when flag is dropped by a player...
2126 "noise5" sound played when flag touches the ground... */
2127 void spawnfunc_item_flag_team1()
2129 if(!g_ctf) { remove(self); return; }
2131 ctf_FlagSetup(NUM_TEAM_1, self);
2134 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2135 CTF flag for team two (Blue).
2137 "angle" Angle the flag will point (minus 90 degrees)...
2138 "model" model to use, note this needs red and blue as skins 0 and 1...
2139 "noise" sound played when flag is picked up...
2140 "noise1" sound played when flag is returned by a teammate...
2141 "noise2" sound played when flag is captured...
2142 "noise3" sound played when flag is lost in the field and respawns itself...
2143 "noise4" sound played when flag is dropped by a player...
2144 "noise5" sound played when flag touches the ground... */
2145 void spawnfunc_item_flag_team2()
2147 if(!g_ctf) { remove(self); return; }
2149 ctf_FlagSetup(NUM_TEAM_2, self);
2152 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2153 CTF flag for team three (Yellow).
2155 "angle" Angle the flag will point (minus 90 degrees)...
2156 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2157 "noise" sound played when flag is picked up...
2158 "noise1" sound played when flag is returned by a teammate...
2159 "noise2" sound played when flag is captured...
2160 "noise3" sound played when flag is lost in the field and respawns itself...
2161 "noise4" sound played when flag is dropped by a player...
2162 "noise5" sound played when flag touches the ground... */
2163 void spawnfunc_item_flag_team3()
2165 if(!g_ctf) { remove(self); return; }
2167 ctf_FlagSetup(NUM_TEAM_3, self);
2170 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2171 CTF flag for team two (Pink).
2173 "angle" Angle the flag will point (minus 90 degrees)...
2174 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2175 "noise" sound played when flag is picked up...
2176 "noise1" sound played when flag is returned by a teammate...
2177 "noise2" sound played when flag is captured...
2178 "noise3" sound played when flag is lost in the field and respawns itself...
2179 "noise4" sound played when flag is dropped by a player...
2180 "noise5" sound played when flag touches the ground... */
2181 void spawnfunc_item_flag_team4()
2183 if(!g_ctf) { remove(self); return; }
2185 ctf_FlagSetup(NUM_TEAM_4, self);
2188 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2189 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2190 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.
2192 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2193 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2194 void spawnfunc_ctf_team()
2196 if(!g_ctf) { remove(self); return; }
2198 self.classname = "ctf_team";
2199 self.team = self.cnt + 1;
2202 // compatibility for quake maps
2203 void spawnfunc_team_CTF_redflag() { spawnfunc_item_flag_team1(); }
2204 void spawnfunc_team_CTF_blueflag() { spawnfunc_item_flag_team2(); }
2205 void spawnfunc_team_CTF_redplayer() { spawnfunc_info_player_team1(); }
2206 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2(); }
2207 void spawnfunc_team_CTF_redspawn() { spawnfunc_info_player_team1(); }
2208 void spawnfunc_team_CTF_bluespawn() { spawnfunc_info_player_team2(); }
2216 void ctf_ScoreRules(float teams)
2218 CheckAllowedTeams(world);
2219 ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, TRUE);
2220 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2221 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2222 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2223 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2224 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2225 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2226 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2227 ScoreRules_basics_end();
2230 // code from here on is just to support maps that don't have flag and team entities
2231 void ctf_SpawnTeam (string teamname, float teamcolor)
2236 self.classname = "ctf_team";
2237 self.netname = teamname;
2238 self.cnt = teamcolor;
2240 spawnfunc_ctf_team();
2245 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2250 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2252 if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2253 if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2256 ctf_teams = bound(2, ctf_teams, 4);
2258 // if no teams are found, spawn defaults
2259 if(find(world, classname, "ctf_team") == world)
2261 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2262 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2263 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2265 ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2267 ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2270 ctf_ScoreRules(ctf_teams);
2273 void ctf_Initialize()
2275 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2277 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2278 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2279 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2281 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2285 MUTATOR_DEFINITION(gamemode_ctf)
2287 MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2288 MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2289 MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2290 MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2291 MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2292 MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2293 MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2294 MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2295 MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2296 MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2297 MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2298 MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2299 MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2300 MUTATOR_HOOK(HavocBot_ChooseRule, ctf_BotRoles, CBC_ORDER_ANY);
2301 MUTATOR_HOOK(GetTeamCount, ctf_GetTeamCount, CBC_ORDER_ANY);
2305 if(time > 1) // game loads at time 1
2306 error("This is a game type and it cannot be added at runtime.");
2310 MUTATOR_ONROLLBACK_OR_REMOVE
2312 // we actually cannot roll back ctf_Initialize here
2313 // BUT: we don't need to! If this gets called, adding always
2319 print("This is a game type and it cannot be removed at runtime.");