1 // ================================================================
2 // Official capture the flag game mode coding, reworked by Samual
3 // Last updated: September, 2012
4 // ================================================================
6 void ctf_FakeTimeLimit(entity e, float t)
9 WriteByte(MSG_ONE, 3); // svc_updatestat
10 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
12 WriteCoord(MSG_ONE, autocvar_timelimit);
14 WriteCoord(MSG_ONE, (t + 1) / 60);
17 void ctf_EventLog(string mode, float flagteam, entity actor) // use an alias for easy changing and quick editing later
19 if(autocvar_sv_eventlog)
20 GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
23 void ctf_CaptureRecord(entity flag, entity player)
26 float cap_record = ctf_captimerecord;
27 float cap_time = (time - flag.ctf_pickuptime);
28 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
31 FOR_EACH_REALCLIENT(tmp_entity)
33 if(tmp_entity.CAPTURE_VERBOSE)
35 if(!ctf_captimerecord) { Send_Notification(NOTIF_ONE_ONLY, tmp_entity, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_CAPTURE_TIME_), player.netname, (cap_time * 100)); }
36 else if(cap_time < cap_record) { Send_Notification(NOTIF_ONE_ONLY, tmp_entity, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_CAPTURE_BROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
37 else { Send_Notification(NOTIF_ONE_ONLY, tmp_entity, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_CAPTURE_UNBROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
39 else { Send_Notification(NOTIF_ONE_ONLY, tmp_entity, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_CAPTURE_), player.netname); }
42 // the previous notification broadcast is only sent to real clients, this will notify server log too
43 if(server_is_dedicated)
45 if(autocvar_notification_ctf_capture_verbose)
47 if(!ctf_captimerecord) { Local_Notification(MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_CAPTURE_TIME_), player.netname, (cap_time * 100)); }
48 else if(cap_time < cap_record) { Local_Notification(MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_CAPTURE_BROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
49 else { Local_Notification(MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_CAPTURE_UNBROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
51 else { Local_Notification(MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_CAPTURE_), player.netname); }
54 // write that shit in the database
55 if((!ctf_captimerecord) || (cap_time < cap_record))
57 ctf_captimerecord = cap_time;
58 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
59 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
60 write_recordmarker(player, (time - cap_time), cap_time);
64 void ctf_FlagcarrierWaypoints(entity player)
66 WaypointSprite_Spawn("flagcarrier", 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, TRUE, RADARICON_FLAG, WPCOLOR_FLAGCARRIER(player.team));
67 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
68 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
69 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
72 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
74 float current_distance = vlen((('1 0 0' * to_x) + ('0 1 0' * to_y)) - (('1 0 0' * from_x) + ('0 1 0' * from_y))); // for the sake of this check, exclude Z axis
75 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
76 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
77 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
80 if(current_height) // make sure we can actually do this arcing path
82 targpos = (to + ('0 0 1' * current_height));
83 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
84 if(trace_fraction < 1)
86 //print("normal arc line failed, trying to find new pos...");
87 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
88 targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
89 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
90 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
91 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
94 else { targpos = to; }
96 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
98 vector desired_direction = normalize(targpos - from);
99 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
100 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
103 float ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
105 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
107 // directional tracing only
109 makevectors(passer_angle);
111 // find the closest point on the enemy to the center of the attack
112 float ang; // angle between shotdir and h
113 float h; // hypotenuse, which is the distance between attacker to head
114 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
116 h = vlen(head_center - passer_center);
117 ang = acos(dotproduct(normalize(head_center - passer_center), v_forward));
120 vector nearest_on_line = (passer_center + a * v_forward);
121 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
123 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
124 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
126 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
131 else { return TRUE; }
135 // =======================
136 // CaptureShield Functions
137 // =======================
139 float ctf_CaptureShield_CheckStatus(entity p)
143 float players_worseeq, players_total;
145 if(ctf_captureshield_max_ratio <= 0)
148 s = PlayerScore_Add(p, SP_SCORE, 0);
149 if(s >= -ctf_captureshield_min_negscore)
152 players_total = players_worseeq = 0;
155 if(IsDifferentTeam(e, p))
157 se = PlayerScore_Add(e, SP_SCORE, 0);
163 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
164 // use this rule here
166 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
172 void ctf_CaptureShield_Update(entity player, float wanted_status)
174 float updated_status = ctf_CaptureShield_CheckStatus(player);
175 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
177 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
178 player.ctf_captureshielded = updated_status;
182 float ctf_CaptureShield_Customize()
184 if(!other.ctf_captureshielded) { return FALSE; }
185 if(!IsDifferentTeam(self, other)) { return FALSE; }
190 void ctf_CaptureShield_Touch()
192 if(!other.ctf_captureshielded) { return; }
193 if(!IsDifferentTeam(self, other)) { return; }
195 vector mymid = (self.absmin + self.absmax) * 0.5;
196 vector othermid = (other.absmin + other.absmax) * 0.5;
198 Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
199 Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED);
202 void ctf_CaptureShield_Spawn(entity flag)
204 entity shield = spawn();
207 shield.team = self.team;
208 shield.touch = ctf_CaptureShield_Touch;
209 shield.customizeentityforclient = ctf_CaptureShield_Customize;
210 shield.classname = "ctf_captureshield";
211 shield.effects = EF_ADDITIVE;
212 shield.movetype = MOVETYPE_NOCLIP;
213 shield.solid = SOLID_TRIGGER;
214 shield.avelocity = '7 0 11';
217 setorigin(shield, self.origin);
218 setmodel(shield, "models/ctf/shield.md3");
219 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
223 // ====================
224 // Drop/Pass/Throw Code
225 // ====================
227 void ctf_Handle_Drop(entity flag, entity player, float droptype)
230 player = (player ? player : flag.pass_sender);
233 flag.movetype = MOVETYPE_TOSS;
234 flag.takedamage = DAMAGE_YES;
235 flag.angles = '0 0 0';
236 flag.health = flag.max_flag_health;
237 flag.ctf_droptime = time;
238 flag.ctf_dropper = player;
239 flag.ctf_status = FLAG_DROPPED;
241 // messages and sounds
242 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_LOST_), player.netname);
243 sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTN_NONE);
244 ctf_EventLog("dropped", player.team, player);
247 PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);
248 PlayerScore_Add(player, SP_CTF_DROPS, 1);
251 if(autocvar_g_ctf_flag_dropped_waypoint)
252 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));
254 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
256 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
257 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
260 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
262 if(droptype == DROP_PASS)
264 flag.pass_distance = 0;
265 flag.pass_sender = world;
266 flag.pass_target = world;
270 void ctf_Handle_Retrieve(entity flag, entity player)
272 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
273 entity sender = flag.pass_sender;
275 // transfer flag to player
277 flag.owner.flagcarried = flag;
280 setattachment(flag, player, "");
281 setorigin(flag, FLAG_CARRY_OFFSET);
282 flag.movetype = MOVETYPE_NONE;
283 flag.takedamage = DAMAGE_NO;
284 flag.solid = SOLID_NOT;
285 flag.angles = '0 0 0';
286 flag.ctf_status = FLAG_CARRY;
288 // messages and sounds
289 sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTN_NORM);
290 ctf_EventLog("receive", flag.team, player);
292 FOR_EACH_REALPLAYER(tmp_player)
294 if(tmp_player == sender)
295 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PASS_SENT_), player.netname);
296 else if(tmp_player == player)
297 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PASS_RECEIVED_), sender.netname);
298 else if(!IsDifferentTeam(tmp_player, sender))
299 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PASS_OTHER_), sender.netname, player.netname);
302 // create new waypoint
303 ctf_FlagcarrierWaypoints(player);
305 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
306 player.throw_antispam = sender.throw_antispam;
308 flag.pass_distance = 0;
309 flag.pass_sender = world;
310 flag.pass_target = world;
313 void ctf_Handle_Throw(entity player, entity receiver, float droptype)
315 entity flag = player.flagcarried;
316 vector targ_origin, flag_velocity;
318 if(!flag) { return; }
319 if((droptype == DROP_PASS) && !receiver) { return; }
321 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
324 setattachment(flag, world, "");
325 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
326 flag.owner.flagcarried = world;
328 flag.solid = SOLID_TRIGGER;
329 flag.ctf_dropper = player;
330 flag.ctf_droptime = time;
332 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
339 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
340 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
341 WarpZone_RefSys_Copy(flag, receiver);
342 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
343 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
345 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
346 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, FALSE);
349 flag.movetype = MOVETYPE_FLY;
350 flag.takedamage = DAMAGE_NO;
351 flag.pass_sender = player;
352 flag.pass_target = receiver;
353 flag.ctf_status = FLAG_PASSING;
356 sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTN_NORM);
357 WarpZone_TrailParticles(world, particleeffectnum(flag.passeffect), player.origin, targ_origin);
358 ctf_EventLog("pass", flag.team, player);
364 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'));
366 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)));
367 flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, FALSE);
368 ctf_Handle_Drop(flag, player, droptype);
374 flag.velocity = '0 0 0'; // do nothing
381 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);
382 ctf_Handle_Drop(flag, player, droptype);
387 // kill old waypointsprite
388 WaypointSprite_Ping(player.wps_flagcarrier);
389 WaypointSprite_Kill(player.wps_flagcarrier);
391 if(player.wps_enemyflagcarrier)
392 WaypointSprite_Kill(player.wps_enemyflagcarrier);
395 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
403 void ctf_Handle_Capture(entity flag, entity toucher, float capturetype)
405 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
406 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
407 float old_time, new_time;
409 if not(player) { return; } // without someone to give the reward to, we can't possibly cap
411 // messages and sounds
412 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_2(enemy_flag, CENTER_CTF_CAPTURE_));
413 ctf_CaptureRecord(enemy_flag, player);
414 sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE);
418 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
419 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
424 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
425 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
427 old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
428 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
429 if(!old_time || new_time < old_time)
430 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
433 pointparticles(particleeffectnum(flag.capeffect), flag.origin, '0 0 0', 1);
434 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
437 if(capturetype == CAPTURE_NORMAL)
439 WaypointSprite_Kill(player.wps_flagcarrier);
440 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
442 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
443 { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
447 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
448 ctf_RespawnFlag(enemy_flag);
451 void ctf_Handle_Return(entity flag, entity player)
453 // messages and sounds
454 if(player.classname == "player")
455 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_RETURN_));
457 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_RETURN_), player.netname);
458 sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTN_NONE);
459 ctf_EventLog("return", flag.team, player);
462 if(player.classname == "player")
464 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
465 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
468 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
472 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
473 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
474 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
478 ctf_RespawnFlag(flag);
481 void ctf_Handle_Pickup(entity flag, entity player, float pickuptype)
484 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
485 float pickup_dropped_score; // used to calculate dropped pickup score
487 // attach the flag to the player
489 player.flagcarried = flag;
490 setattachment(flag, player, "");
491 setorigin(flag, FLAG_CARRY_OFFSET);
494 flag.movetype = MOVETYPE_NONE;
495 flag.takedamage = DAMAGE_NO;
496 flag.solid = SOLID_NOT;
497 flag.angles = '0 0 0';
498 flag.ctf_status = FLAG_CARRY;
502 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
503 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
507 // messages and sounds
508 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_PICKUP_), player.netname);
509 sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE);
511 FOR_EACH_REALPLAYER(tmp_player)
513 if(tmp_player == player)
515 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PICKUP_));
516 if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
518 else if(!IsDifferentTeam(tmp_player, player) && tmp_player != player)
520 if(tmp_player.PICKUP_TEAM_VERBOSE)
521 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, CENTER_CTF_PICKUP_TEAM_VERBOSE, Team_ColorCode(player.team), player.netname);
523 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, CENTER_CTF_PICKUP_TEAM, Team_ColorCode(player.team));
525 else if(IsDifferentTeam(tmp_player, player))
527 if(tmp_player.PICKUP_ENEMY_VERBOSE)
528 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, CENTER_CTF_PICKUP_ENEMY_VERBOSE, Team_ColorCode(player.team), player.netname);
530 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, CENTER_CTF_PICKUP_ENEMY, Team_ColorCode(player.team));
535 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
540 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
541 ctf_EventLog("steal", flag.team, player);
547 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);
548 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);
549 dprint("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
550 PlayerTeamScore_AddScore(player, pickup_dropped_score);
551 ctf_EventLog("pickup", flag.team, player);
559 if(pickuptype == PICKUP_BASE)
561 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
562 if((player.speedrunning) && (ctf_captimerecord))
563 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
567 pointparticles(particleeffectnum(flag.toucheffect), player.origin, '0 0 0', 1);
570 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
571 ctf_FlagcarrierWaypoints(player);
572 WaypointSprite_Ping(player.wps_flagcarrier);
576 // ===================
577 // Main Flag Functions
578 // ===================
580 void ctf_CheckFlagReturn(entity flag, float returntype)
582 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
584 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
586 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
590 case RETURN_DROPPED: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_DROPPED_)); break;
591 case RETURN_DAMAGE: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_DAMAGED_)); break;
592 case RETURN_SPEEDRUN: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_SPEEDRUN_), ctf_captimerecord); break;
593 case RETURN_NEEDKILL: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_NEEDKILL_)); break;
597 { Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_TIMEOUT_)); break; }
599 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTN_NONE);
600 ctf_EventLog("returned", flag.team, world);
601 ctf_RespawnFlag(flag);
606 void ctf_CheckStalemate(void)
609 float stale_red_flags = 0, stale_blue_flags = 0;
612 entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
614 // build list of stale flags
615 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
617 if(autocvar_g_ctf_stalemate)
618 if(tmp_entity.ctf_status != FLAG_BASE)
619 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time)
621 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
622 ctf_staleflaglist = tmp_entity;
624 switch(tmp_entity.team)
626 case NUM_TEAM_1: ++stale_red_flags; break;
627 case NUM_TEAM_2: ++stale_blue_flags; break;
632 if(stale_red_flags && stale_blue_flags)
633 ctf_stalemate = TRUE;
634 else if((!stale_red_flags && !stale_blue_flags) && autocvar_g_ctf_stalemate_endcondition == 2)
635 { ctf_stalemate = FALSE; wpforenemy_announced = FALSE; }
636 else if((!stale_red_flags || !stale_blue_flags) && autocvar_g_ctf_stalemate_endcondition == 1)
637 { ctf_stalemate = FALSE; wpforenemy_announced = FALSE; }
639 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
642 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
644 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
645 WaypointSprite_Spawn("enemyflagcarrier", 0, 0, tmp_entity.owner, FLAG_WAYPOINT_OFFSET, world, tmp_entity.team, tmp_entity.owner, wps_enemyflagcarrier, TRUE, RADARICON_FLAG, WPCOLOR_ENEMYFC(tmp_entity.owner.team));
648 if not(wpforenemy_announced)
650 FOR_EACH_REALPLAYER(tmp_entity)
651 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CENTER, ((tmp_entity.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER));
653 wpforenemy_announced = TRUE;
658 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
660 if(ITEM_DAMAGE_NEEDKILL(deathtype))
662 // automatically kill the flag and return it
664 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
667 if(autocvar_g_ctf_flag_return_damage)
669 // reduce health and check if it should be returned
670 self.health = self.health - damage;
671 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
681 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
684 if(self == ctf_worldflaglist) // only for the first flag
685 FOR_EACH_CLIENT(tmp_entity)
686 ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
689 if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
690 dprint("wtf the flag got squashed?\n");
691 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
692 if(!trace_startsolid) // can we resize it without getting stuck?
693 setsize(self, FLAG_MIN, FLAG_MAX); }
695 switch(self.ctf_status) // reset flag angles in case warpzones adjust it
699 self.angles = '0 0 0';
707 switch(self.ctf_status)
711 if(autocvar_g_ctf_dropped_capture_radius)
713 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
714 if(tmp_entity.ctf_status == FLAG_DROPPED)
715 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
716 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
717 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
724 if(autocvar_g_ctf_flag_dropped_floatinwater)
726 vector midpoint = ((self.absmin + self.absmax) * 0.5);
727 if(pointcontents(midpoint) == CONTENT_WATER)
729 self.velocity = self.velocity * 0.5;
731 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
732 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
734 { self.movetype = MOVETYPE_FLY; }
736 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
738 if(autocvar_g_ctf_flag_return_dropped)
740 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
743 ctf_CheckFlagReturn(self, RETURN_DROPPED);
747 if(autocvar_g_ctf_flag_return_time)
749 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
750 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
758 if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
761 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
765 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
769 if(autocvar_g_ctf_stalemate)
771 if(time >= wpforenemy_nextthink)
773 ctf_CheckStalemate();
774 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
782 vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
783 targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
784 WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
786 if((self.pass_target == world)
787 || (self.pass_target.deadflag != DEAD_NO)
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 if(toucher.frozen) { return; }
819 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
820 if(ITEM_TOUCH_NEEDKILL())
823 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
827 // special touch behaviors
828 if(toucher.vehicle_flags & VHF_ISVEHICLE)
830 if(autocvar_g_ctf_allow_vehicle_touch)
831 toucher = toucher.owner; // the player is actually the vehicle owner, not other
833 return; // do nothing
835 else if(toucher.flags & FL_MONSTER)
837 if not(autocvar_g_ctf_allow_monster_touch)
838 return; // do nothing
840 else if(toucher.classname != "player") // The flag just touched an object, most likely the world
842 if(time > self.wait) // if we haven't in a while, play a sound/effect
844 pointparticles(particleeffectnum(self.toucheffect), self.origin, '0 0 0', 1);
845 sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTN_NORM);
846 self.wait = time + FLAG_TOUCHRATE;
850 else if(toucher.deadflag != DEAD_NO) { return; }
852 switch(self.ctf_status)
856 if(!IsDifferentTeam(toucher, self) && (toucher.flagcarried) && IsDifferentTeam(toucher.flagcarried, self) && !(toucher.flags & FL_MONSTER))
857 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
858 else if(IsDifferentTeam(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && !(toucher.flags & FL_MONSTER))
859 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
865 if(!IsDifferentTeam(toucher, self))
866 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
867 else if(!(toucher.flags & FL_MONSTER) && (!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
868 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
874 dprint("Someone touched a flag even though it was being carried?\n");
880 if((toucher.classname == "player") && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
882 if(IsDifferentTeam(toucher, self.pass_sender))
883 ctf_Handle_Return(self, toucher);
885 ctf_Handle_Retrieve(self, toucher);
893 void ctf_RespawnFlag(entity flag)
895 // check for flag respawn being called twice in a row
896 if(flag.last_respawn > time - 0.5)
897 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
899 flag.last_respawn = time;
901 // reset the player (if there is one)
902 if((flag.owner) && (flag.owner.flagcarried == flag))
904 if(flag.owner.wps_enemyflagcarrier)
905 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
907 WaypointSprite_Kill(flag.wps_flagcarrier);
909 flag.owner.flagcarried = world;
911 if(flag.speedrunning)
912 ctf_FakeTimeLimit(flag.owner, -1);
915 if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
916 { WaypointSprite_Kill(flag.wps_flagdropped); }
919 setattachment(flag, world, "");
920 setorigin(flag, flag.ctf_spawnorigin);
922 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
923 flag.takedamage = DAMAGE_NO;
924 flag.health = flag.max_flag_health;
925 flag.solid = SOLID_TRIGGER;
926 flag.velocity = '0 0 0';
927 flag.angles = flag.mangle;
928 flag.flags = FL_ITEM | FL_NOTARGET;
930 flag.ctf_status = FLAG_BASE;
932 flag.pass_distance = 0;
933 flag.pass_sender = world;
934 flag.pass_target = world;
935 flag.ctf_dropper = world;
936 flag.ctf_pickuptime = 0;
937 flag.ctf_droptime = 0;
943 if(self.owner.classname == "player")
944 ctf_Handle_Throw(self.owner, world, DROP_RESET);
946 ctf_RespawnFlag(self);
949 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
952 waypoint_spawnforitem_force(self, self.origin);
953 self.nearestwaypointtimeout = 0; // activate waypointing again
954 self.bot_basewaypoint = self.nearestwaypoint;
957 WaypointSprite_SpawnFixed(((self.team == NUM_TEAM_1) ? "redbase" : "bluebase"), self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
958 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
960 // captureshield setup
961 ctf_CaptureShield_Spawn(self);
964 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
967 teamnumber = fabs(teamnumber - bound(0, autocvar_g_ctf_reverse, 1)); // if we were originally 1, this will become 0. If we were originally 0, this will become 1.
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 = ((teamnumber) ? "^1REPLACETHIS^7" : "^4REPLACETHIS^7"); // ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag");
977 flag.team = ((teamnumber) ? NUM_TEAM_1 : NUM_TEAM_2); // NUM_TEAM_1: color 4 team (red) - NUM_TEAM_2: color 13 team (blue)
978 flag.items = ((teamnumber) ? IT_KEY2 : IT_KEY1); // IT_KEY2: gold key (redish enough) - IT_KEY1: silver key (bluish enough)
979 flag.classname = "item_flag_team";
980 flag.target = "###item###"; // wut?
981 flag.flags = FL_ITEM | FL_NOTARGET;
982 flag.solid = SOLID_TRIGGER;
983 flag.takedamage = DAMAGE_NO;
984 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
985 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
986 flag.health = flag.max_flag_health;
987 flag.event_damage = ctf_FlagDamage;
988 flag.pushable = TRUE;
989 flag.teleportable = TELEPORT_NORMAL;
990 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
991 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
992 flag.velocity = '0 0 0';
993 flag.mangle = flag.angles;
994 flag.reset = ctf_Reset;
995 flag.touch = ctf_FlagTouch;
996 flag.think = ctf_FlagThink;
997 flag.nextthink = time + FLAG_THINKRATE;
998 flag.ctf_status = FLAG_BASE;
1000 if(!flag.model) { flag.model = ((teamnumber) ? autocvar_g_ctf_flag_red_model : autocvar_g_ctf_flag_blue_model); }
1001 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1002 if(!flag.skin) { flag.skin = ((teamnumber) ? autocvar_g_ctf_flag_red_skin : autocvar_g_ctf_flag_blue_skin); }
1003 if(!flag.toucheffect) { flag.toucheffect = ((teamnumber) ? "redflag_touch" : "blueflag_touch"); }
1004 if(!flag.passeffect) { flag.passeffect = ((teamnumber) ? "red_pass" : "blue_pass"); }
1005 if(!flag.capeffect) { flag.capeffect = ((teamnumber) ? "red_cap" : "blue_cap"); }
1008 if(!flag.snd_flag_taken) { flag.snd_flag_taken = ((teamnumber) ? "ctf/red_taken.wav" : "ctf/blue_taken.wav"); }
1009 if(!flag.snd_flag_returned) { flag.snd_flag_returned = ((teamnumber) ? "ctf/red_returned.wav" : "ctf/blue_returned.wav"); }
1010 if(!flag.snd_flag_capture) { flag.snd_flag_capture = ((teamnumber) ? "ctf/red_capture.wav" : "ctf/blue_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 = ((teamnumber) ? "ctf/red_dropped.wav" : "ctf/blue_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)
1035 flag.glow_color = ((teamnumber) ? 251 : 210); // 251: red - 210: blue
1036 flag.glow_size = 25;
1037 flag.glow_trail = 1;
1040 flag.effects |= EF_LOWPRECISION;
1041 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1042 if(autocvar_g_ctf_dynamiclights) { flag.effects |= ((teamnumber) ? EF_RED : EF_BLUE); }
1045 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1047 flag.dropped_origin = flag.origin;
1048 flag.noalign = TRUE;
1049 flag.movetype = MOVETYPE_NONE;
1051 else // drop to floor, automatically find a platform and set that as spawn origin
1053 flag.noalign = FALSE;
1056 flag.movetype = MOVETYPE_TOSS;
1059 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1067 // NOTE: LEGACY CODE, needs to be re-written!
1069 void havocbot_calculate_middlepoint()
1073 vector fo = '0 0 0';
1076 f = ctf_worldflaglist;
1081 f = f.ctf_worldflagnext;
1085 havocbot_ctf_middlepoint = s * (1.0 / n);
1086 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1090 entity havocbot_ctf_find_flag(entity bot)
1093 f = ctf_worldflaglist;
1096 if (bot.team == f.team)
1098 f = f.ctf_worldflagnext;
1103 entity havocbot_ctf_find_enemy_flag(entity bot)
1106 f = ctf_worldflaglist;
1109 if (bot.team != f.team)
1111 f = f.ctf_worldflagnext;
1116 float havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1124 FOR_EACH_PLAYER(head)
1126 if(head.team!=bot.team || head.deadflag != DEAD_NO || head == bot)
1129 if(vlen(head.origin - org) < tc_radius)
1136 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1139 head = ctf_worldflaglist;
1142 if (self.team == head.team)
1144 head = head.ctf_worldflagnext;
1147 navigation_routerating(head, ratingscale, 10000);
1150 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1153 head = ctf_worldflaglist;
1156 if (self.team == head.team)
1158 head = head.ctf_worldflagnext;
1163 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1166 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1169 head = ctf_worldflaglist;
1172 if (self.team != head.team)
1174 head = head.ctf_worldflagnext;
1177 navigation_routerating(head, ratingscale, 10000);
1180 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1182 if not(bot_waypoints_for_items)
1184 havocbot_goalrating_ctf_enemyflag(ratingscale);
1190 head = havocbot_ctf_find_enemy_flag(self);
1195 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1198 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1202 mf = havocbot_ctf_find_flag(self);
1204 if(mf.ctf_status == FLAG_BASE)
1208 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1211 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1214 head = ctf_worldflaglist;
1217 // flag is out in the field
1218 if(head.ctf_status != FLAG_BASE)
1219 if(head.tag_entity==world) // dropped
1223 if(vlen(org-head.origin)<df_radius)
1224 navigation_routerating(head, ratingscale, 10000);
1227 navigation_routerating(head, ratingscale, 10000);
1230 head = head.ctf_worldflagnext;
1234 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1238 head = findchainfloat(bot_pickup, TRUE);
1241 // gather health and armor only
1243 if (head.health || head.armorvalue)
1244 if (vlen(head.origin - org) < sradius)
1246 // get the value of the item
1247 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1249 navigation_routerating(head, t * ratingscale, 500);
1255 void havocbot_ctf_reset_role(entity bot)
1257 float cdefense, cmiddle, coffense;
1258 entity mf, ef, head;
1261 if(bot.deadflag != DEAD_NO)
1264 if(vlen(havocbot_ctf_middlepoint)==0)
1265 havocbot_calculate_middlepoint();
1268 if (bot.flagcarried)
1270 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1274 mf = havocbot_ctf_find_flag(bot);
1275 ef = havocbot_ctf_find_enemy_flag(bot);
1277 // Retrieve stolen flag
1278 if(mf.ctf_status!=FLAG_BASE)
1280 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1284 // If enemy flag is taken go to the middle to intercept pursuers
1285 if(ef.ctf_status!=FLAG_BASE)
1287 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1291 // if there is only me on the team switch to offense
1293 FOR_EACH_PLAYER(head)
1294 if(head.team==bot.team)
1299 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1303 // Evaluate best position to take
1304 // Count mates on middle position
1305 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1307 // Count mates on defense position
1308 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1310 // Count mates on offense position
1311 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1313 if(cdefense<=coffense)
1314 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1315 else if(coffense<=cmiddle)
1316 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1318 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1321 void havocbot_role_ctf_carrier()
1323 if(self.deadflag != DEAD_NO)
1325 havocbot_ctf_reset_role(self);
1329 if (self.flagcarried == world)
1331 havocbot_ctf_reset_role(self);
1335 if (self.bot_strategytime < time)
1337 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1339 navigation_goalrating_start();
1340 havocbot_goalrating_ctf_ourbase(50000);
1343 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1345 navigation_goalrating_end();
1347 if (self.navigation_hasgoals)
1348 self.havocbot_cantfindflag = time + 10;
1349 else if (time > self.havocbot_cantfindflag)
1351 // Can't navigate to my own base, suicide!
1352 // TODO: drop it and wander around
1353 Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1359 void havocbot_role_ctf_escort()
1363 if(self.deadflag != DEAD_NO)
1365 havocbot_ctf_reset_role(self);
1369 if (self.flagcarried)
1371 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1375 // If enemy flag is back on the base switch to previous role
1376 ef = havocbot_ctf_find_enemy_flag(self);
1377 if(ef.ctf_status==FLAG_BASE)
1379 self.havocbot_role = self.havocbot_previous_role;
1380 self.havocbot_role_timeout = 0;
1384 // If the flag carrier reached the base switch to defense
1385 mf = havocbot_ctf_find_flag(self);
1386 if(mf.ctf_status!=FLAG_BASE)
1387 if(vlen(ef.origin - mf.dropped_origin) < 300)
1389 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1393 // Set the role timeout if necessary
1394 if (!self.havocbot_role_timeout)
1396 self.havocbot_role_timeout = time + random() * 30 + 60;
1399 // If nothing happened just switch to previous role
1400 if (time > self.havocbot_role_timeout)
1402 self.havocbot_role = self.havocbot_previous_role;
1403 self.havocbot_role_timeout = 0;
1407 // Chase the flag carrier
1408 if (self.bot_strategytime < time)
1410 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1411 navigation_goalrating_start();
1412 havocbot_goalrating_ctf_enemyflag(30000);
1413 havocbot_goalrating_ctf_ourstolenflag(40000);
1414 havocbot_goalrating_items(10000, self.origin, 10000);
1415 navigation_goalrating_end();
1419 void havocbot_role_ctf_offense()
1424 if(self.deadflag != DEAD_NO)
1426 havocbot_ctf_reset_role(self);
1430 if (self.flagcarried)
1432 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1437 mf = havocbot_ctf_find_flag(self);
1438 ef = havocbot_ctf_find_enemy_flag(self);
1441 if(mf.ctf_status!=FLAG_BASE)
1444 pos = mf.tag_entity.origin;
1448 // Try to get it if closer than the enemy base
1449 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1451 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1456 // Escort flag carrier
1457 if(ef.ctf_status!=FLAG_BASE)
1460 pos = ef.tag_entity.origin;
1464 if(vlen(pos-mf.dropped_origin)>700)
1466 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1471 // About to fail, switch to middlefield
1474 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1478 // Set the role timeout if necessary
1479 if (!self.havocbot_role_timeout)
1480 self.havocbot_role_timeout = time + 120;
1482 if (time > self.havocbot_role_timeout)
1484 havocbot_ctf_reset_role(self);
1488 if (self.bot_strategytime < time)
1490 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1491 navigation_goalrating_start();
1492 havocbot_goalrating_ctf_ourstolenflag(50000);
1493 havocbot_goalrating_ctf_enemybase(20000);
1494 havocbot_goalrating_items(5000, self.origin, 1000);
1495 havocbot_goalrating_items(1000, self.origin, 10000);
1496 navigation_goalrating_end();
1500 // Retriever (temporary role):
1501 void havocbot_role_ctf_retriever()
1505 if(self.deadflag != DEAD_NO)
1507 havocbot_ctf_reset_role(self);
1511 if (self.flagcarried)
1513 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1517 // If flag is back on the base switch to previous role
1518 mf = havocbot_ctf_find_flag(self);
1519 if(mf.ctf_status==FLAG_BASE)
1521 havocbot_ctf_reset_role(self);
1525 if (!self.havocbot_role_timeout)
1526 self.havocbot_role_timeout = time + 20;
1528 if (time > self.havocbot_role_timeout)
1530 havocbot_ctf_reset_role(self);
1534 if (self.bot_strategytime < time)
1539 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1540 navigation_goalrating_start();
1541 havocbot_goalrating_ctf_ourstolenflag(50000);
1542 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1543 havocbot_goalrating_ctf_enemybase(30000);
1544 havocbot_goalrating_items(500, self.origin, rt_radius);
1545 navigation_goalrating_end();
1549 void havocbot_role_ctf_middle()
1553 if(self.deadflag != DEAD_NO)
1555 havocbot_ctf_reset_role(self);
1559 if (self.flagcarried)
1561 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1565 mf = havocbot_ctf_find_flag(self);
1566 if(mf.ctf_status!=FLAG_BASE)
1568 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1572 if (!self.havocbot_role_timeout)
1573 self.havocbot_role_timeout = time + 10;
1575 if (time > self.havocbot_role_timeout)
1577 havocbot_ctf_reset_role(self);
1581 if (self.bot_strategytime < time)
1585 org = havocbot_ctf_middlepoint;
1586 org_z = self.origin_z;
1588 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1589 navigation_goalrating_start();
1590 havocbot_goalrating_ctf_ourstolenflag(50000);
1591 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1592 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1593 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1594 havocbot_goalrating_items(2500, self.origin, 10000);
1595 havocbot_goalrating_ctf_enemybase(2500);
1596 navigation_goalrating_end();
1600 void havocbot_role_ctf_defense()
1604 if(self.deadflag != DEAD_NO)
1606 havocbot_ctf_reset_role(self);
1610 if (self.flagcarried)
1612 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1616 // If own flag was captured
1617 mf = havocbot_ctf_find_flag(self);
1618 if(mf.ctf_status!=FLAG_BASE)
1620 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1624 if (!self.havocbot_role_timeout)
1625 self.havocbot_role_timeout = time + 30;
1627 if (time > self.havocbot_role_timeout)
1629 havocbot_ctf_reset_role(self);
1632 if (self.bot_strategytime < time)
1637 org = mf.dropped_origin;
1638 mp_radius = havocbot_ctf_middlepoint_radius;
1640 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1641 navigation_goalrating_start();
1643 // if enemies are closer to our base, go there
1644 entity head, closestplayer = world;
1645 float distance, bestdistance = 10000;
1646 FOR_EACH_PLAYER(head)
1648 if(head.deadflag!=DEAD_NO)
1651 distance = vlen(org - head.origin);
1652 if(distance<bestdistance)
1654 closestplayer = head;
1655 bestdistance = distance;
1660 if(closestplayer.team!=self.team)
1661 if(vlen(org - self.origin)>1000)
1662 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1663 havocbot_goalrating_ctf_ourbase(30000);
1665 havocbot_goalrating_ctf_ourstolenflag(20000);
1666 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1667 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1668 havocbot_goalrating_items(10000, org, mp_radius);
1669 havocbot_goalrating_items(5000, self.origin, 10000);
1670 navigation_goalrating_end();
1674 void havocbot_role_ctf_setrole(entity bot, float role)
1676 dprint(strcat(bot.netname," switched to "));
1679 case HAVOCBOT_CTF_ROLE_CARRIER:
1681 bot.havocbot_role = havocbot_role_ctf_carrier;
1682 bot.havocbot_role_timeout = 0;
1683 bot.havocbot_cantfindflag = time + 10;
1684 bot.bot_strategytime = 0;
1686 case HAVOCBOT_CTF_ROLE_DEFENSE:
1688 bot.havocbot_role = havocbot_role_ctf_defense;
1689 bot.havocbot_role_timeout = 0;
1691 case HAVOCBOT_CTF_ROLE_MIDDLE:
1693 bot.havocbot_role = havocbot_role_ctf_middle;
1694 bot.havocbot_role_timeout = 0;
1696 case HAVOCBOT_CTF_ROLE_OFFENSE:
1698 bot.havocbot_role = havocbot_role_ctf_offense;
1699 bot.havocbot_role_timeout = 0;
1701 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1702 dprint("retriever");
1703 bot.havocbot_previous_role = bot.havocbot_role;
1704 bot.havocbot_role = havocbot_role_ctf_retriever;
1705 bot.havocbot_role_timeout = time + 10;
1706 bot.bot_strategytime = 0;
1708 case HAVOCBOT_CTF_ROLE_ESCORT:
1710 bot.havocbot_previous_role = bot.havocbot_role;
1711 bot.havocbot_role = havocbot_role_ctf_escort;
1712 bot.havocbot_role_timeout = time + 30;
1713 bot.bot_strategytime = 0;
1724 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1728 // initially clear items so they can be set as necessary later.
1729 self.items &~= (IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST
1730 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST | IT_CTF_SHIELDED);
1732 // scan through all the flags and notify the client about them
1733 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1735 switch(flag.ctf_status)
1740 if((flag.owner == self) || (flag.pass_sender == self))
1741 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
1743 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
1748 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
1754 // item for stopping players from capturing the flag too often
1755 if(self.ctf_captureshielded)
1756 self.items |= IT_CTF_SHIELDED;
1758 // update the health of the flag carrier waypointsprite
1759 if(self.wps_flagcarrier)
1760 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent));
1765 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1767 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1769 if(frag_target == frag_attacker) // damage done to yourself
1771 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1772 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1774 else // damage done to everyone else
1776 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1777 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1780 else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && IsDifferentTeam(frag_target, frag_attacker)) // if the target is a flagcarrier
1782 if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent)))
1783 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1785 frag_target.wps_helpme_time = time;
1786 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1788 // todo: add notification for when flag carrier needs help?
1793 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1795 if((frag_attacker != frag_target) && (frag_attacker.classname == "player") && (frag_target.flagcarried))
1797 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1798 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1801 if(frag_target.flagcarried)
1802 { ctf_Handle_Throw(frag_target, world, DROP_NORMAL); }
1807 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1810 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1813 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1815 entity flag; // temporary entity for the search method
1817 if(self.flagcarried)
1818 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1820 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1822 if(flag.pass_sender == self) { flag.pass_sender = world; }
1823 if(flag.pass_target == self) { flag.pass_target = world; }
1824 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
1830 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
1832 if(self.flagcarried)
1833 if(!autocvar_g_ctf_portalteleport)
1834 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1839 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
1841 if(MUTATOR_RETURNVALUE || gameover) { return FALSE; }
1843 entity player = self;
1845 if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1847 // pass the flag to a team mate
1848 if(autocvar_g_ctf_pass)
1850 entity head, closest_target = world;
1851 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, TRUE);
1853 while(head) // find the closest acceptable target to pass to
1855 if(head.classname == "player" && head.deadflag == DEAD_NO)
1856 if(head != player && !IsDifferentTeam(head, player))
1857 if(!head.speedrunning && !head.vehicle)
1859 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
1860 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
1861 vector passer_center = CENTER_OR_VIEWOFS(player);
1863 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
1865 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
1867 if(clienttype(head) == CLIENTTYPE_BOT)
1869 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1870 ctf_Handle_Throw(head, player, DROP_PASS);
1874 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
1875 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1877 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
1880 else if(player.flagcarried)
1884 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
1885 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
1886 { closest_target = head; }
1888 else { closest_target = head; }
1895 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return TRUE; }
1898 // throw the flag in front of you
1899 if(autocvar_g_ctf_throw && player.flagcarried)
1901 if(player.throw_count == -1)
1903 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
1905 player.throw_prevtime = time;
1906 player.throw_count = 1;
1907 ctf_Handle_Throw(player, world, DROP_THROW);
1912 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
1918 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
1919 else { player.throw_count += 1; }
1920 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
1922 player.throw_prevtime = time;
1923 ctf_Handle_Throw(player, world, DROP_THROW);
1932 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
1934 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
1936 self.wps_helpme_time = time;
1937 WaypointSprite_HelpMePing(self.wps_flagcarrier);
1939 else // create a normal help me waypointsprite
1941 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');
1942 WaypointSprite_Ping(self.wps_helpme);
1948 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
1950 if(vh_player.flagcarried)
1952 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
1954 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
1958 setattachment(vh_player.flagcarried, vh_vehicle, "");
1959 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
1960 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
1961 //vh_player.flagcarried.angles = '0 0 0';
1969 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
1971 if(vh_player.flagcarried)
1973 setattachment(vh_player.flagcarried, vh_player, "");
1974 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
1975 vh_player.flagcarried.scale = FLAG_SCALE;
1976 vh_player.flagcarried.angles = '0 0 0';
1983 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
1985 if(self.flagcarried)
1987 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN_));
1988 ctf_RespawnFlag(self.flagcarried);
1995 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
1997 entity flag; // temporary entity for the search method
1999 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2001 switch(flag.ctf_status)
2006 // lock the flag, game is over
2007 flag.movetype = MOVETYPE_NONE;
2008 flag.takedamage = DAMAGE_NO;
2009 flag.solid = SOLID_NOT;
2010 flag.nextthink = FALSE; // stop thinking
2012 //dprint("stopping the ", flag.netname, " from moving.\n");
2020 // do nothing for these flags
2029 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
2031 havocbot_ctf_reset_role(self);
2035 MUTATOR_HOOKFUNCTION(ctf_GetCvars)
2037 GetCvars_handleFloat(get_cvars_s, get_cvars_f, CAPTURE_VERBOSE, "notification_ctf_capture_verbose");
2038 GetCvars_handleFloat(get_cvars_s, get_cvars_f, PICKUP_TEAM_VERBOSE, "notification_ctf_pickup_team_verbose");
2039 GetCvars_handleFloat(get_cvars_s, get_cvars_f, PICKUP_ENEMY_VERBOSE, "notification_ctf_pickup_enemy_verbose");
2048 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
2049 CTF Starting point for a player in team one (Red).
2050 Keys: "angle" viewing angle when spawning. */
2051 void spawnfunc_info_player_team1()
2053 if(g_assault) { remove(self); return; }
2055 self.team = NUM_TEAM_1; // red
2056 spawnfunc_info_player_deathmatch();
2060 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
2061 CTF Starting point for a player in team two (Blue).
2062 Keys: "angle" viewing angle when spawning. */
2063 void spawnfunc_info_player_team2()
2065 if(g_assault) { remove(self); return; }
2067 self.team = NUM_TEAM_2; // blue
2068 spawnfunc_info_player_deathmatch();
2071 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
2072 CTF Starting point for a player in team three (Yellow).
2073 Keys: "angle" viewing angle when spawning. */
2074 void spawnfunc_info_player_team3()
2076 if(g_assault) { remove(self); return; }
2078 self.team = NUM_TEAM_3; // yellow
2079 spawnfunc_info_player_deathmatch();
2083 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
2084 CTF Starting point for a player in team four (Purple).
2085 Keys: "angle" viewing angle when spawning. */
2086 void spawnfunc_info_player_team4()
2088 if(g_assault) { remove(self); return; }
2090 self.team = NUM_TEAM_4; // purple
2091 spawnfunc_info_player_deathmatch();
2094 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2095 CTF flag for team one (Red).
2097 "angle" Angle the flag will point (minus 90 degrees)...
2098 "model" model to use, note this needs red and blue as skins 0 and 1...
2099 "noise" sound played when flag is picked up...
2100 "noise1" sound played when flag is returned by a teammate...
2101 "noise2" sound played when flag is captured...
2102 "noise3" sound played when flag is lost in the field and respawns itself...
2103 "noise4" sound played when flag is dropped by a player...
2104 "noise5" sound played when flag touches the ground... */
2105 void spawnfunc_item_flag_team1()
2107 if(!g_ctf) { remove(self); return; }
2109 ctf_FlagSetup(1, self); // 1 = red
2112 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2113 CTF flag for team two (Blue).
2115 "angle" Angle the flag will point (minus 90 degrees)...
2116 "model" model to use, note this needs red and blue as skins 0 and 1...
2117 "noise" sound played when flag is picked up...
2118 "noise1" sound played when flag is returned by a teammate...
2119 "noise2" sound played when flag is captured...
2120 "noise3" sound played when flag is lost in the field and respawns itself...
2121 "noise4" sound played when flag is dropped by a player...
2122 "noise5" sound played when flag touches the ground... */
2123 void spawnfunc_item_flag_team2()
2125 if(!g_ctf) { remove(self); return; }
2127 ctf_FlagSetup(0, self); // the 0 is misleading, but -- 0 = blue.
2130 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2131 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2132 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.
2134 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2135 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2136 void spawnfunc_ctf_team()
2138 if(!g_ctf) { remove(self); return; }
2140 self.classname = "ctf_team";
2141 self.team = self.cnt + 1;
2144 // compatibility for quake maps
2145 void spawnfunc_team_CTF_redflag() { spawnfunc_item_flag_team1(); }
2146 void spawnfunc_team_CTF_blueflag() { spawnfunc_item_flag_team2(); }
2147 void spawnfunc_team_CTF_redplayer() { spawnfunc_info_player_team1(); }
2148 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2(); }
2149 void spawnfunc_team_CTF_redspawn() { spawnfunc_info_player_team1(); }
2150 void spawnfunc_team_CTF_bluespawn() { spawnfunc_info_player_team2(); }
2158 void ctf_ScoreRules()
2160 ScoreRules_basics(2, SFL_SORT_PRIO_PRIMARY, 0, TRUE);
2161 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2162 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2163 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2164 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2165 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2166 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2167 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2168 ScoreRules_basics_end();
2171 // code from here on is just to support maps that don't have flag and team entities
2172 void ctf_SpawnTeam (string teamname, float teamcolor)
2177 self.classname = "ctf_team";
2178 self.netname = teamname;
2179 self.cnt = teamcolor;
2181 spawnfunc_ctf_team();
2186 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2188 // if no teams are found, spawn defaults
2189 if(find(world, classname, "ctf_team") == world)
2191 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2192 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2193 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2199 void ctf_Initialize()
2201 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2203 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2204 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2205 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2207 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2211 MUTATOR_DEFINITION(gamemode_ctf)
2213 MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2214 MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2215 MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2216 MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2217 MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2218 MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2219 MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2220 MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2221 MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2222 MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2223 MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2224 MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2225 MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2226 MUTATOR_HOOK(HavocBot_ChooseRule, ctf_BotRoles, CBC_ORDER_ANY);
2227 MUTATOR_HOOK(GetCvars, ctf_GetCvars, CBC_ORDER_ANY);
2231 if(time > 1) // game loads at time 1
2232 error("This is a game type and it cannot be added at runtime.");
2236 MUTATOR_ONROLLBACK_OR_REMOVE
2238 // we actually cannot roll back ctf_Initialize here
2239 // BUT: we don't need to! If this gets called, adding always
2245 print("This is a game type and it cannot be removed at runtime.");