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(IS_PLAYER(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(IS_PLAYER(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 not(IS_PLAYER(toucher)) // 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((IS_PLAYER(toucher)) && (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(IS_PLAYER(self.owner))
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) ? "^1RED^7 flag" : "^4BLUE^7 flag"); // Primarily only used for debugging or when showing nearby item name
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;
1001 if(flag.model == "") { flag.model = ((teamnumber) ? autocvar_g_ctf_flag_red_model : autocvar_g_ctf_flag_blue_model); }
1002 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1003 if(!flag.skin) { flag.skin = ((teamnumber) ? autocvar_g_ctf_flag_red_skin : autocvar_g_ctf_flag_blue_skin); }
1004 if(flag.toucheffect == "") { flag.toucheffect = ((teamnumber) ? "redflag_touch" : "blueflag_touch"); }
1005 if(flag.passeffect == "") { flag.passeffect = ((teamnumber) ? "red_pass" : "blue_pass"); }
1006 if(flag.capeffect == "") { flag.capeffect = ((teamnumber) ? "red_cap" : "blue_cap"); }
1009 if(flag.snd_flag_taken == "") { flag.snd_flag_taken = ((teamnumber) ? "ctf/red_taken.wav" : "ctf/blue_taken.wav"); }
1010 if(flag.snd_flag_returned == "") { flag.snd_flag_returned = ((teamnumber) ? "ctf/red_returned.wav" : "ctf/blue_returned.wav"); }
1011 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
1012 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.
1013 if(flag.snd_flag_dropped == "") { flag.snd_flag_dropped = ((teamnumber) ? "ctf/red_dropped.wav" : "ctf/blue_dropped.wav"); }
1014 if(flag.snd_flag_touch == "") { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
1015 if(flag.snd_flag_pass == "") { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
1018 precache_sound(flag.snd_flag_taken);
1019 precache_sound(flag.snd_flag_returned);
1020 precache_sound(flag.snd_flag_capture);
1021 precache_sound(flag.snd_flag_respawn);
1022 precache_sound(flag.snd_flag_dropped);
1023 precache_sound(flag.snd_flag_touch);
1024 precache_sound(flag.snd_flag_pass);
1025 precache_model(flag.model);
1026 precache_model("models/ctf/shield.md3");
1027 precache_model("models/ctf/shockwavetransring.md3");
1030 setmodel(flag, flag.model); // precision set below
1031 setsize(flag, FLAG_MIN, FLAG_MAX);
1032 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1034 if(autocvar_g_ctf_flag_glowtrails)
1036 flag.glow_color = ((teamnumber) ? 251 : 210); // 251: red - 210: blue
1037 flag.glow_size = 25;
1038 flag.glow_trail = 1;
1041 flag.effects |= EF_LOWPRECISION;
1042 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1043 if(autocvar_g_ctf_dynamiclights) { flag.effects |= ((teamnumber) ? EF_RED : EF_BLUE); }
1046 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1048 flag.dropped_origin = flag.origin;
1049 flag.noalign = TRUE;
1050 flag.movetype = MOVETYPE_NONE;
1052 else // drop to floor, automatically find a platform and set that as spawn origin
1054 flag.noalign = FALSE;
1057 flag.movetype = MOVETYPE_TOSS;
1060 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1068 // NOTE: LEGACY CODE, needs to be re-written!
1070 void havocbot_calculate_middlepoint()
1074 vector fo = '0 0 0';
1077 f = ctf_worldflaglist;
1082 f = f.ctf_worldflagnext;
1086 havocbot_ctf_middlepoint = s * (1.0 / n);
1087 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1091 entity havocbot_ctf_find_flag(entity bot)
1094 f = ctf_worldflaglist;
1097 if (bot.team == f.team)
1099 f = f.ctf_worldflagnext;
1104 entity havocbot_ctf_find_enemy_flag(entity bot)
1107 f = ctf_worldflaglist;
1110 if (bot.team != f.team)
1112 f = f.ctf_worldflagnext;
1117 float havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1125 FOR_EACH_PLAYER(head)
1127 if(head.team!=bot.team || head.deadflag != DEAD_NO || head == bot)
1130 if(vlen(head.origin - org) < tc_radius)
1137 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1140 head = ctf_worldflaglist;
1143 if (self.team == head.team)
1145 head = head.ctf_worldflagnext;
1148 navigation_routerating(head, ratingscale, 10000);
1151 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1154 head = ctf_worldflaglist;
1157 if (self.team == head.team)
1159 head = head.ctf_worldflagnext;
1164 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1167 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1170 head = ctf_worldflaglist;
1173 if (self.team != head.team)
1175 head = head.ctf_worldflagnext;
1178 navigation_routerating(head, ratingscale, 10000);
1181 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1183 if not(bot_waypoints_for_items)
1185 havocbot_goalrating_ctf_enemyflag(ratingscale);
1191 head = havocbot_ctf_find_enemy_flag(self);
1196 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1199 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1203 mf = havocbot_ctf_find_flag(self);
1205 if(mf.ctf_status == FLAG_BASE)
1209 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1212 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1215 head = ctf_worldflaglist;
1218 // flag is out in the field
1219 if(head.ctf_status != FLAG_BASE)
1220 if(head.tag_entity==world) // dropped
1224 if(vlen(org-head.origin)<df_radius)
1225 navigation_routerating(head, ratingscale, 10000);
1228 navigation_routerating(head, ratingscale, 10000);
1231 head = head.ctf_worldflagnext;
1235 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1239 head = findchainfloat(bot_pickup, TRUE);
1242 // gather health and armor only
1244 if (head.health || head.armorvalue)
1245 if (vlen(head.origin - org) < sradius)
1247 // get the value of the item
1248 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1250 navigation_routerating(head, t * ratingscale, 500);
1256 void havocbot_ctf_reset_role(entity bot)
1258 float cdefense, cmiddle, coffense;
1259 entity mf, ef, head;
1262 if(bot.deadflag != DEAD_NO)
1265 if(vlen(havocbot_ctf_middlepoint)==0)
1266 havocbot_calculate_middlepoint();
1269 if (bot.flagcarried)
1271 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1275 mf = havocbot_ctf_find_flag(bot);
1276 ef = havocbot_ctf_find_enemy_flag(bot);
1278 // Retrieve stolen flag
1279 if(mf.ctf_status!=FLAG_BASE)
1281 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1285 // If enemy flag is taken go to the middle to intercept pursuers
1286 if(ef.ctf_status!=FLAG_BASE)
1288 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1292 // if there is only me on the team switch to offense
1294 FOR_EACH_PLAYER(head)
1295 if(head.team==bot.team)
1300 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1304 // Evaluate best position to take
1305 // Count mates on middle position
1306 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1308 // Count mates on defense position
1309 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1311 // Count mates on offense position
1312 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1314 if(cdefense<=coffense)
1315 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1316 else if(coffense<=cmiddle)
1317 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1319 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1322 void havocbot_role_ctf_carrier()
1324 if(self.deadflag != DEAD_NO)
1326 havocbot_ctf_reset_role(self);
1330 if (self.flagcarried == world)
1332 havocbot_ctf_reset_role(self);
1336 if (self.bot_strategytime < time)
1338 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1340 navigation_goalrating_start();
1341 havocbot_goalrating_ctf_ourbase(50000);
1344 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1346 navigation_goalrating_end();
1348 if (self.navigation_hasgoals)
1349 self.havocbot_cantfindflag = time + 10;
1350 else if (time > self.havocbot_cantfindflag)
1352 // Can't navigate to my own base, suicide!
1353 // TODO: drop it and wander around
1354 Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1360 void havocbot_role_ctf_escort()
1364 if(self.deadflag != DEAD_NO)
1366 havocbot_ctf_reset_role(self);
1370 if (self.flagcarried)
1372 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1376 // If enemy flag is back on the base switch to previous role
1377 ef = havocbot_ctf_find_enemy_flag(self);
1378 if(ef.ctf_status==FLAG_BASE)
1380 self.havocbot_role = self.havocbot_previous_role;
1381 self.havocbot_role_timeout = 0;
1385 // If the flag carrier reached the base switch to defense
1386 mf = havocbot_ctf_find_flag(self);
1387 if(mf.ctf_status!=FLAG_BASE)
1388 if(vlen(ef.origin - mf.dropped_origin) < 300)
1390 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1394 // Set the role timeout if necessary
1395 if (!self.havocbot_role_timeout)
1397 self.havocbot_role_timeout = time + random() * 30 + 60;
1400 // If nothing happened just switch to previous role
1401 if (time > self.havocbot_role_timeout)
1403 self.havocbot_role = self.havocbot_previous_role;
1404 self.havocbot_role_timeout = 0;
1408 // Chase the flag carrier
1409 if (self.bot_strategytime < time)
1411 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1412 navigation_goalrating_start();
1413 havocbot_goalrating_ctf_enemyflag(30000);
1414 havocbot_goalrating_ctf_ourstolenflag(40000);
1415 havocbot_goalrating_items(10000, self.origin, 10000);
1416 navigation_goalrating_end();
1420 void havocbot_role_ctf_offense()
1425 if(self.deadflag != DEAD_NO)
1427 havocbot_ctf_reset_role(self);
1431 if (self.flagcarried)
1433 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1438 mf = havocbot_ctf_find_flag(self);
1439 ef = havocbot_ctf_find_enemy_flag(self);
1442 if(mf.ctf_status!=FLAG_BASE)
1445 pos = mf.tag_entity.origin;
1449 // Try to get it if closer than the enemy base
1450 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1452 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1457 // Escort flag carrier
1458 if(ef.ctf_status!=FLAG_BASE)
1461 pos = ef.tag_entity.origin;
1465 if(vlen(pos-mf.dropped_origin)>700)
1467 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1472 // About to fail, switch to middlefield
1475 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1479 // Set the role timeout if necessary
1480 if (!self.havocbot_role_timeout)
1481 self.havocbot_role_timeout = time + 120;
1483 if (time > self.havocbot_role_timeout)
1485 havocbot_ctf_reset_role(self);
1489 if (self.bot_strategytime < time)
1491 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1492 navigation_goalrating_start();
1493 havocbot_goalrating_ctf_ourstolenflag(50000);
1494 havocbot_goalrating_ctf_enemybase(20000);
1495 havocbot_goalrating_items(5000, self.origin, 1000);
1496 havocbot_goalrating_items(1000, self.origin, 10000);
1497 navigation_goalrating_end();
1501 // Retriever (temporary role):
1502 void havocbot_role_ctf_retriever()
1506 if(self.deadflag != DEAD_NO)
1508 havocbot_ctf_reset_role(self);
1512 if (self.flagcarried)
1514 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1518 // If flag is back on the base switch to previous role
1519 mf = havocbot_ctf_find_flag(self);
1520 if(mf.ctf_status==FLAG_BASE)
1522 havocbot_ctf_reset_role(self);
1526 if (!self.havocbot_role_timeout)
1527 self.havocbot_role_timeout = time + 20;
1529 if (time > self.havocbot_role_timeout)
1531 havocbot_ctf_reset_role(self);
1535 if (self.bot_strategytime < time)
1540 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1541 navigation_goalrating_start();
1542 havocbot_goalrating_ctf_ourstolenflag(50000);
1543 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1544 havocbot_goalrating_ctf_enemybase(30000);
1545 havocbot_goalrating_items(500, self.origin, rt_radius);
1546 navigation_goalrating_end();
1550 void havocbot_role_ctf_middle()
1554 if(self.deadflag != DEAD_NO)
1556 havocbot_ctf_reset_role(self);
1560 if (self.flagcarried)
1562 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1566 mf = havocbot_ctf_find_flag(self);
1567 if(mf.ctf_status!=FLAG_BASE)
1569 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1573 if (!self.havocbot_role_timeout)
1574 self.havocbot_role_timeout = time + 10;
1576 if (time > self.havocbot_role_timeout)
1578 havocbot_ctf_reset_role(self);
1582 if (self.bot_strategytime < time)
1586 org = havocbot_ctf_middlepoint;
1587 org_z = self.origin_z;
1589 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1590 navigation_goalrating_start();
1591 havocbot_goalrating_ctf_ourstolenflag(50000);
1592 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1593 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1594 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1595 havocbot_goalrating_items(2500, self.origin, 10000);
1596 havocbot_goalrating_ctf_enemybase(2500);
1597 navigation_goalrating_end();
1601 void havocbot_role_ctf_defense()
1605 if(self.deadflag != DEAD_NO)
1607 havocbot_ctf_reset_role(self);
1611 if (self.flagcarried)
1613 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1617 // If own flag was captured
1618 mf = havocbot_ctf_find_flag(self);
1619 if(mf.ctf_status!=FLAG_BASE)
1621 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1625 if (!self.havocbot_role_timeout)
1626 self.havocbot_role_timeout = time + 30;
1628 if (time > self.havocbot_role_timeout)
1630 havocbot_ctf_reset_role(self);
1633 if (self.bot_strategytime < time)
1638 org = mf.dropped_origin;
1639 mp_radius = havocbot_ctf_middlepoint_radius;
1641 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1642 navigation_goalrating_start();
1644 // if enemies are closer to our base, go there
1645 entity head, closestplayer = world;
1646 float distance, bestdistance = 10000;
1647 FOR_EACH_PLAYER(head)
1649 if(head.deadflag!=DEAD_NO)
1652 distance = vlen(org - head.origin);
1653 if(distance<bestdistance)
1655 closestplayer = head;
1656 bestdistance = distance;
1661 if(closestplayer.team!=self.team)
1662 if(vlen(org - self.origin)>1000)
1663 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1664 havocbot_goalrating_ctf_ourbase(30000);
1666 havocbot_goalrating_ctf_ourstolenflag(20000);
1667 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1668 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1669 havocbot_goalrating_items(10000, org, mp_radius);
1670 havocbot_goalrating_items(5000, self.origin, 10000);
1671 navigation_goalrating_end();
1675 void havocbot_role_ctf_setrole(entity bot, float role)
1677 dprint(strcat(bot.netname," switched to "));
1680 case HAVOCBOT_CTF_ROLE_CARRIER:
1682 bot.havocbot_role = havocbot_role_ctf_carrier;
1683 bot.havocbot_role_timeout = 0;
1684 bot.havocbot_cantfindflag = time + 10;
1685 bot.bot_strategytime = 0;
1687 case HAVOCBOT_CTF_ROLE_DEFENSE:
1689 bot.havocbot_role = havocbot_role_ctf_defense;
1690 bot.havocbot_role_timeout = 0;
1692 case HAVOCBOT_CTF_ROLE_MIDDLE:
1694 bot.havocbot_role = havocbot_role_ctf_middle;
1695 bot.havocbot_role_timeout = 0;
1697 case HAVOCBOT_CTF_ROLE_OFFENSE:
1699 bot.havocbot_role = havocbot_role_ctf_offense;
1700 bot.havocbot_role_timeout = 0;
1702 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1703 dprint("retriever");
1704 bot.havocbot_previous_role = bot.havocbot_role;
1705 bot.havocbot_role = havocbot_role_ctf_retriever;
1706 bot.havocbot_role_timeout = time + 10;
1707 bot.bot_strategytime = 0;
1709 case HAVOCBOT_CTF_ROLE_ESCORT:
1711 bot.havocbot_previous_role = bot.havocbot_role;
1712 bot.havocbot_role = havocbot_role_ctf_escort;
1713 bot.havocbot_role_timeout = time + 30;
1714 bot.bot_strategytime = 0;
1725 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1729 // initially clear items so they can be set as necessary later.
1730 self.items &~= (IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST
1731 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST | IT_CTF_SHIELDED);
1733 // scan through all the flags and notify the client about them
1734 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1736 switch(flag.ctf_status)
1741 if((flag.owner == self) || (flag.pass_sender == self))
1742 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
1744 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
1749 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
1755 // item for stopping players from capturing the flag too often
1756 if(self.ctf_captureshielded)
1757 self.items |= IT_CTF_SHIELDED;
1759 // update the health of the flag carrier waypointsprite
1760 if(self.wps_flagcarrier)
1761 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent));
1766 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1768 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1770 if(frag_target == frag_attacker) // damage done to yourself
1772 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1773 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1775 else // damage done to everyone else
1777 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1778 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1781 else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && IsDifferentTeam(frag_target, frag_attacker)) // if the target is a flagcarrier
1783 if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent)))
1784 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1786 frag_target.wps_helpme_time = time;
1787 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1789 // todo: add notification for when flag carrier needs help?
1794 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1796 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
1798 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1799 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1802 if(frag_target.flagcarried)
1803 { ctf_Handle_Throw(frag_target, world, DROP_NORMAL); }
1808 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1811 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1814 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1816 entity flag; // temporary entity for the search method
1818 if(self.flagcarried)
1819 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1821 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1823 if(flag.pass_sender == self) { flag.pass_sender = world; }
1824 if(flag.pass_target == self) { flag.pass_target = world; }
1825 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
1831 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
1833 if(self.flagcarried)
1834 if(!autocvar_g_ctf_portalteleport)
1835 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1840 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
1842 if(MUTATOR_RETURNVALUE || gameover) { return FALSE; }
1844 entity player = self;
1846 if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1848 // pass the flag to a team mate
1849 if(autocvar_g_ctf_pass)
1851 entity head, closest_target = world;
1852 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, TRUE);
1854 while(head) // find the closest acceptable target to pass to
1856 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
1857 if(head != player && !IsDifferentTeam(head, player))
1858 if(!head.speedrunning && !head.vehicle)
1860 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
1861 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
1862 vector passer_center = CENTER_OR_VIEWOFS(player);
1864 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
1866 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
1868 if(IS_BOT_CLIENT(head))
1870 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1871 ctf_Handle_Throw(head, player, DROP_PASS);
1875 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
1876 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1878 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
1881 else if(player.flagcarried)
1885 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
1886 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
1887 { closest_target = head; }
1889 else { closest_target = head; }
1896 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return TRUE; }
1899 // throw the flag in front of you
1900 if(autocvar_g_ctf_throw && player.flagcarried)
1902 if(player.throw_count == -1)
1904 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
1906 player.throw_prevtime = time;
1907 player.throw_count = 1;
1908 ctf_Handle_Throw(player, world, DROP_THROW);
1913 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
1919 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
1920 else { player.throw_count += 1; }
1921 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
1923 player.throw_prevtime = time;
1924 ctf_Handle_Throw(player, world, DROP_THROW);
1933 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
1935 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
1937 self.wps_helpme_time = time;
1938 WaypointSprite_HelpMePing(self.wps_flagcarrier);
1940 else // create a normal help me waypointsprite
1942 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');
1943 WaypointSprite_Ping(self.wps_helpme);
1949 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
1951 if(vh_player.flagcarried)
1953 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
1955 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
1959 setattachment(vh_player.flagcarried, vh_vehicle, "");
1960 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
1961 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
1962 //vh_player.flagcarried.angles = '0 0 0';
1970 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
1972 if(vh_player.flagcarried)
1974 setattachment(vh_player.flagcarried, vh_player, "");
1975 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
1976 vh_player.flagcarried.scale = FLAG_SCALE;
1977 vh_player.flagcarried.angles = '0 0 0';
1984 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
1986 if(self.flagcarried)
1988 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN_));
1989 ctf_RespawnFlag(self.flagcarried);
1996 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
1998 entity flag; // temporary entity for the search method
2000 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2002 switch(flag.ctf_status)
2007 // lock the flag, game is over
2008 flag.movetype = MOVETYPE_NONE;
2009 flag.takedamage = DAMAGE_NO;
2010 flag.solid = SOLID_NOT;
2011 flag.nextthink = FALSE; // stop thinking
2013 //dprint("stopping the ", flag.netname, " from moving.\n");
2021 // do nothing for these flags
2030 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
2032 havocbot_ctf_reset_role(self);
2036 MUTATOR_HOOKFUNCTION(ctf_GetCvars)
2038 GetCvars_handleFloat(get_cvars_s, get_cvars_f, CAPTURE_VERBOSE, "notification_ctf_capture_verbose");
2039 GetCvars_handleFloat(get_cvars_s, get_cvars_f, PICKUP_TEAM_VERBOSE, "notification_ctf_pickup_team_verbose");
2040 GetCvars_handleFloat(get_cvars_s, get_cvars_f, PICKUP_ENEMY_VERBOSE, "notification_ctf_pickup_enemy_verbose");
2049 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
2050 CTF Starting point for a player in team one (Red).
2051 Keys: "angle" viewing angle when spawning. */
2052 void spawnfunc_info_player_team1()
2054 if(g_assault) { remove(self); return; }
2056 self.team = NUM_TEAM_1; // red
2057 spawnfunc_info_player_deathmatch();
2061 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
2062 CTF Starting point for a player in team two (Blue).
2063 Keys: "angle" viewing angle when spawning. */
2064 void spawnfunc_info_player_team2()
2066 if(g_assault) { remove(self); return; }
2068 self.team = NUM_TEAM_2; // blue
2069 spawnfunc_info_player_deathmatch();
2072 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
2073 CTF Starting point for a player in team three (Yellow).
2074 Keys: "angle" viewing angle when spawning. */
2075 void spawnfunc_info_player_team3()
2077 if(g_assault) { remove(self); return; }
2079 self.team = NUM_TEAM_3; // yellow
2080 spawnfunc_info_player_deathmatch();
2084 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
2085 CTF Starting point for a player in team four (Purple).
2086 Keys: "angle" viewing angle when spawning. */
2087 void spawnfunc_info_player_team4()
2089 if(g_assault) { remove(self); return; }
2091 self.team = NUM_TEAM_4; // purple
2092 spawnfunc_info_player_deathmatch();
2095 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2096 CTF flag for team one (Red).
2098 "angle" Angle the flag will point (minus 90 degrees)...
2099 "model" model to use, note this needs red and blue as skins 0 and 1...
2100 "noise" sound played when flag is picked up...
2101 "noise1" sound played when flag is returned by a teammate...
2102 "noise2" sound played when flag is captured...
2103 "noise3" sound played when flag is lost in the field and respawns itself...
2104 "noise4" sound played when flag is dropped by a player...
2105 "noise5" sound played when flag touches the ground... */
2106 void spawnfunc_item_flag_team1()
2108 if(!g_ctf) { remove(self); return; }
2110 ctf_FlagSetup(1, self); // 1 = red
2113 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2114 CTF flag for team two (Blue).
2116 "angle" Angle the flag will point (minus 90 degrees)...
2117 "model" model to use, note this needs red and blue as skins 0 and 1...
2118 "noise" sound played when flag is picked up...
2119 "noise1" sound played when flag is returned by a teammate...
2120 "noise2" sound played when flag is captured...
2121 "noise3" sound played when flag is lost in the field and respawns itself...
2122 "noise4" sound played when flag is dropped by a player...
2123 "noise5" sound played when flag touches the ground... */
2124 void spawnfunc_item_flag_team2()
2126 if(!g_ctf) { remove(self); return; }
2128 ctf_FlagSetup(0, self); // the 0 is misleading, but -- 0 = blue.
2131 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2132 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2133 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.
2135 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2136 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2137 void spawnfunc_ctf_team()
2139 if(!g_ctf) { remove(self); return; }
2141 self.classname = "ctf_team";
2142 self.team = self.cnt + 1;
2145 // compatibility for quake maps
2146 void spawnfunc_team_CTF_redflag() { spawnfunc_item_flag_team1(); }
2147 void spawnfunc_team_CTF_blueflag() { spawnfunc_item_flag_team2(); }
2148 void spawnfunc_team_CTF_redplayer() { spawnfunc_info_player_team1(); }
2149 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2(); }
2150 void spawnfunc_team_CTF_redspawn() { spawnfunc_info_player_team1(); }
2151 void spawnfunc_team_CTF_bluespawn() { spawnfunc_info_player_team2(); }
2159 void ctf_ScoreRules()
2161 ScoreRules_basics(2, SFL_SORT_PRIO_PRIMARY, 0, TRUE);
2162 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2163 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2164 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2165 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2166 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2167 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2168 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2169 ScoreRules_basics_end();
2172 // code from here on is just to support maps that don't have flag and team entities
2173 void ctf_SpawnTeam (string teamname, float teamcolor)
2178 self.classname = "ctf_team";
2179 self.netname = teamname;
2180 self.cnt = teamcolor;
2182 spawnfunc_ctf_team();
2187 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2189 // if no teams are found, spawn defaults
2190 if(find(world, classname, "ctf_team") == world)
2192 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2193 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2194 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2200 void ctf_Initialize()
2202 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2204 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2205 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2206 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2208 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2212 MUTATOR_DEFINITION(gamemode_ctf)
2214 MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2215 MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2216 MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2217 MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2218 MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2219 MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2220 MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2221 MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2222 MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2223 MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2224 MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2225 MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2226 MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2227 MUTATOR_HOOK(HavocBot_ChooseRule, ctf_BotRoles, CBC_ORDER_ANY);
2228 MUTATOR_HOOK(GetCvars, ctf_GetCvars, CBC_ORDER_ANY);
2232 if(time > 1) // game loads at time 1
2233 error("This is a game type and it cannot be added at runtime.");
2237 MUTATOR_ONROLLBACK_OR_REMOVE
2239 // we actually cannot roll back ctf_Initialize here
2240 // BUT: we don't need to! If this gets called, adding always
2246 print("This is a game type and it cannot be removed at runtime.");