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 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_RETURN_));
455 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_RETURN_), player.netname);
456 sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTN_NONE);
457 ctf_EventLog("return", flag.team, player);
460 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
461 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
463 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
467 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
468 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
469 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
473 ctf_RespawnFlag(flag);
476 void ctf_Handle_Pickup(entity flag, entity player, float pickuptype)
479 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
480 float pickup_dropped_score; // used to calculate dropped pickup score
482 // attach the flag to the player
484 player.flagcarried = flag;
485 setattachment(flag, player, "");
486 setorigin(flag, FLAG_CARRY_OFFSET);
489 flag.movetype = MOVETYPE_NONE;
490 flag.takedamage = DAMAGE_NO;
491 flag.solid = SOLID_NOT;
492 flag.angles = '0 0 0';
493 flag.ctf_status = FLAG_CARRY;
497 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
498 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
502 // messages and sounds
503 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_PICKUP_), player.netname);
504 sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE);
506 FOR_EACH_REALPLAYER(tmp_player)
508 if(tmp_player == player)
510 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PICKUP_));
511 if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
513 else if(!IsDifferentTeam(tmp_player, player) && tmp_player != player)
515 if(tmp_player.PICKUP_TEAM_VERBOSE)
516 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, CENTER_CTF_PICKUP_TEAM_VERBOSE, Team_ColorCode(player.team), player.netname);
518 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, CENTER_CTF_PICKUP_TEAM, Team_ColorCode(player.team));
520 else if(IsDifferentTeam(tmp_player, player))
522 if(tmp_player.PICKUP_ENEMY_VERBOSE)
523 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, CENTER_CTF_PICKUP_ENEMY_VERBOSE, Team_ColorCode(player.team), player.netname);
525 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, CENTER_CTF_PICKUP_ENEMY, Team_ColorCode(player.team));
530 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
535 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
536 ctf_EventLog("steal", flag.team, player);
542 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);
543 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);
544 dprint("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
545 PlayerTeamScore_AddScore(player, pickup_dropped_score);
546 ctf_EventLog("pickup", flag.team, player);
554 if(pickuptype == PICKUP_BASE)
556 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
557 if((player.speedrunning) && (ctf_captimerecord))
558 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
562 pointparticles(particleeffectnum(flag.toucheffect), player.origin, '0 0 0', 1);
565 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
566 ctf_FlagcarrierWaypoints(player);
567 WaypointSprite_Ping(player.wps_flagcarrier);
571 // ===================
572 // Main Flag Functions
573 // ===================
575 void ctf_CheckFlagReturn(entity flag, float returntype)
577 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
579 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
581 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
585 case RETURN_DROPPED: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_DROPPED_)); break;
586 case RETURN_DAMAGE: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_DAMAGED_)); break;
587 case RETURN_SPEEDRUN: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_SPEEDRUN_), ctf_captimerecord); break;
588 case RETURN_NEEDKILL: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_NEEDKILL_)); break;
592 { Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_TIMEOUT_)); break; }
594 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTN_NONE);
595 ctf_EventLog("returned", flag.team, world);
596 ctf_RespawnFlag(flag);
601 void ctf_CheckStalemate(void)
604 float stale_red_flags = 0, stale_blue_flags = 0;
607 entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
609 // build list of stale flags
610 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
612 if(autocvar_g_ctf_stalemate)
613 if(tmp_entity.ctf_status != FLAG_BASE)
614 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time)
616 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
617 ctf_staleflaglist = tmp_entity;
619 switch(tmp_entity.team)
621 case NUM_TEAM_1: ++stale_red_flags; break;
622 case NUM_TEAM_2: ++stale_blue_flags; break;
627 if(stale_red_flags && stale_blue_flags)
628 ctf_stalemate = TRUE;
629 else if((!stale_red_flags && !stale_blue_flags) && autocvar_g_ctf_stalemate_endcondition == 2)
630 { ctf_stalemate = FALSE; wpforenemy_announced = FALSE; }
631 else if((!stale_red_flags || !stale_blue_flags) && autocvar_g_ctf_stalemate_endcondition == 1)
632 { ctf_stalemate = FALSE; wpforenemy_announced = FALSE; }
634 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
637 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
639 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
640 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));
643 if not(wpforenemy_announced)
645 FOR_EACH_REALPLAYER(tmp_entity)
646 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CENTER, ((tmp_entity.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER));
648 wpforenemy_announced = TRUE;
653 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
655 if(ITEM_DAMAGE_NEEDKILL(deathtype))
657 // automatically kill the flag and return it
659 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
662 if(autocvar_g_ctf_flag_return_damage)
664 // reduce health and check if it should be returned
665 self.health = self.health - damage;
666 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
676 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
679 if(self == ctf_worldflaglist) // only for the first flag
680 FOR_EACH_CLIENT(tmp_entity)
681 ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
684 if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
685 dprint("wtf the flag got squashed?\n");
686 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
687 if(!trace_startsolid) // can we resize it without getting stuck?
688 setsize(self, FLAG_MIN, FLAG_MAX); }
690 switch(self.ctf_status) // reset flag angles in case warpzones adjust it
694 self.angles = '0 0 0';
702 switch(self.ctf_status)
706 if(autocvar_g_ctf_dropped_capture_radius)
708 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
709 if(tmp_entity.ctf_status == FLAG_DROPPED)
710 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
711 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
712 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
719 if(autocvar_g_ctf_flag_dropped_floatinwater)
721 vector midpoint = ((self.absmin + self.absmax) * 0.5);
722 if(pointcontents(midpoint) == CONTENT_WATER)
724 self.velocity = self.velocity * 0.5;
726 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
727 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
729 { self.movetype = MOVETYPE_FLY; }
731 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
733 if(autocvar_g_ctf_flag_return_dropped)
735 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
738 ctf_CheckFlagReturn(self, RETURN_DROPPED);
742 if(autocvar_g_ctf_flag_return_time)
744 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
745 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
753 if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
756 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
760 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
764 if(autocvar_g_ctf_stalemate)
766 if(time >= wpforenemy_nextthink)
768 ctf_CheckStalemate();
769 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
777 vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
778 targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
779 WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
781 if((self.pass_target == world)
782 || (self.pass_target.deadflag != DEAD_NO)
783 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
784 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
785 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
787 // give up, pass failed
788 ctf_Handle_Drop(self, world, DROP_PASS);
792 // still a viable target, go for it
793 ctf_CalculatePassVelocity(self, targ_origin, self.origin, TRUE);
798 default: // this should never happen
800 dprint("ctf_FlagThink(): Flag exists with no status?\n");
808 if(gameover) { return; }
810 entity toucher = other;
812 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
813 if(ITEM_TOUCH_NEEDKILL())
816 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
820 // special touch behaviors
821 if(toucher.vehicle_flags & VHF_ISVEHICLE)
823 if(autocvar_g_ctf_allow_vehicle_touch)
824 toucher = toucher.owner; // the player is actually the vehicle owner, not other
826 return; // do nothing
828 else if not(IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
830 if(time > self.wait) // if we haven't in a while, play a sound/effect
832 pointparticles(particleeffectnum(self.toucheffect), self.origin, '0 0 0', 1);
833 sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTN_NORM);
834 self.wait = time + FLAG_TOUCHRATE;
838 else if(toucher.deadflag != DEAD_NO) { return; }
840 switch(self.ctf_status)
844 if(!IsDifferentTeam(toucher, self) && (toucher.flagcarried) && IsDifferentTeam(toucher.flagcarried, self))
845 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
846 else if(IsDifferentTeam(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time))
847 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
853 if(!IsDifferentTeam(toucher, self))
854 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
855 else if((!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
856 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
862 dprint("Someone touched a flag even though it was being carried?\n");
868 if((IS_PLAYER(toucher)) && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
870 if(IsDifferentTeam(toucher, self.pass_sender))
871 ctf_Handle_Return(self, toucher);
873 ctf_Handle_Retrieve(self, toucher);
881 void ctf_RespawnFlag(entity flag)
883 // check for flag respawn being called twice in a row
884 if(flag.last_respawn > time - 0.5)
885 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
887 flag.last_respawn = time;
889 // reset the player (if there is one)
890 if((flag.owner) && (flag.owner.flagcarried == flag))
892 if(flag.owner.wps_enemyflagcarrier)
893 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
895 WaypointSprite_Kill(flag.wps_flagcarrier);
897 flag.owner.flagcarried = world;
899 if(flag.speedrunning)
900 ctf_FakeTimeLimit(flag.owner, -1);
903 if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
904 { WaypointSprite_Kill(flag.wps_flagdropped); }
907 setattachment(flag, world, "");
908 setorigin(flag, flag.ctf_spawnorigin);
910 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
911 flag.takedamage = DAMAGE_NO;
912 flag.health = flag.max_flag_health;
913 flag.solid = SOLID_TRIGGER;
914 flag.velocity = '0 0 0';
915 flag.angles = flag.mangle;
916 flag.flags = FL_ITEM | FL_NOTARGET;
918 flag.ctf_status = FLAG_BASE;
920 flag.pass_distance = 0;
921 flag.pass_sender = world;
922 flag.pass_target = world;
923 flag.ctf_dropper = world;
924 flag.ctf_pickuptime = 0;
925 flag.ctf_droptime = 0;
931 if(IS_PLAYER(self.owner))
932 ctf_Handle_Throw(self.owner, world, DROP_RESET);
934 ctf_RespawnFlag(self);
937 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
940 waypoint_spawnforitem_force(self, self.origin);
941 self.nearestwaypointtimeout = 0; // activate waypointing again
942 self.bot_basewaypoint = self.nearestwaypoint;
945 WaypointSprite_SpawnFixed(((self.team == NUM_TEAM_1) ? "redbase" : "bluebase"), self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
946 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
948 // captureshield setup
949 ctf_CaptureShield_Spawn(self);
952 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
955 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.
956 self = flag; // for later usage with droptofloor()
959 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
960 ctf_worldflaglist = flag;
962 setattachment(flag, world, "");
964 flag.netname = ((teamnumber) ? "^1REPLACETHIS^7" : "^4REPLACETHIS^7"); // ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag");
965 flag.team = ((teamnumber) ? NUM_TEAM_1 : NUM_TEAM_2); // NUM_TEAM_1: color 4 team (red) - NUM_TEAM_2: color 13 team (blue)
966 flag.items = ((teamnumber) ? IT_KEY2 : IT_KEY1); // IT_KEY2: gold key (redish enough) - IT_KEY1: silver key (bluish enough)
967 flag.classname = "item_flag_team";
968 flag.target = "###item###"; // wut?
969 flag.flags = FL_ITEM | FL_NOTARGET;
970 flag.solid = SOLID_TRIGGER;
971 flag.takedamage = DAMAGE_NO;
972 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
973 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
974 flag.health = flag.max_flag_health;
975 flag.event_damage = ctf_FlagDamage;
976 flag.pushable = TRUE;
977 flag.teleportable = TELEPORT_NORMAL;
978 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
979 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
980 flag.velocity = '0 0 0';
981 flag.mangle = flag.angles;
982 flag.reset = ctf_Reset;
983 flag.touch = ctf_FlagTouch;
984 flag.think = ctf_FlagThink;
985 flag.nextthink = time + FLAG_THINKRATE;
986 flag.ctf_status = FLAG_BASE;
989 if(flag.model == "") { flag.model = ((teamnumber) ? autocvar_g_ctf_flag_red_model : autocvar_g_ctf_flag_blue_model); }
990 if(!flag.scale) { flag.scale = FLAG_SCALE; }
991 if(!flag.skin) { flag.skin = ((teamnumber) ? autocvar_g_ctf_flag_red_skin : autocvar_g_ctf_flag_blue_skin); }
992 if(flag.toucheffect == "") { flag.toucheffect = ((teamnumber) ? "redflag_touch" : "blueflag_touch"); }
993 if(flag.passeffect == "") { flag.passeffect = ((teamnumber) ? "red_pass" : "blue_pass"); }
994 if(flag.capeffect == "") { flag.capeffect = ((teamnumber) ? "red_cap" : "blue_cap"); }
997 if(flag.snd_flag_taken == "") { flag.snd_flag_taken = ((teamnumber) ? "ctf/red_taken.wav" : "ctf/blue_taken.wav"); }
998 if(flag.snd_flag_returned == "") { flag.snd_flag_returned = ((teamnumber) ? "ctf/red_returned.wav" : "ctf/blue_returned.wav"); }
999 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
1000 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.
1001 if(flag.snd_flag_dropped == "") { flag.snd_flag_dropped = ((teamnumber) ? "ctf/red_dropped.wav" : "ctf/blue_dropped.wav"); }
1002 if(flag.snd_flag_touch == "") { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
1003 if(flag.snd_flag_pass == "") { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
1006 precache_sound(flag.snd_flag_taken);
1007 precache_sound(flag.snd_flag_returned);
1008 precache_sound(flag.snd_flag_capture);
1009 precache_sound(flag.snd_flag_respawn);
1010 precache_sound(flag.snd_flag_dropped);
1011 precache_sound(flag.snd_flag_touch);
1012 precache_sound(flag.snd_flag_pass);
1013 precache_model(flag.model);
1014 precache_model("models/ctf/shield.md3");
1015 precache_model("models/ctf/shockwavetransring.md3");
1018 setmodel(flag, flag.model); // precision set below
1019 setsize(flag, FLAG_MIN, FLAG_MAX);
1020 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1022 if(autocvar_g_ctf_flag_glowtrails)
1024 flag.glow_color = ((teamnumber) ? 251 : 210); // 251: red - 210: blue
1025 flag.glow_size = 25;
1026 flag.glow_trail = 1;
1029 flag.effects |= EF_LOWPRECISION;
1030 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1031 if(autocvar_g_ctf_dynamiclights) { flag.effects |= ((teamnumber) ? EF_RED : EF_BLUE); }
1034 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1036 flag.dropped_origin = flag.origin;
1037 flag.noalign = TRUE;
1038 flag.movetype = MOVETYPE_NONE;
1040 else // drop to floor, automatically find a platform and set that as spawn origin
1042 flag.noalign = FALSE;
1045 flag.movetype = MOVETYPE_TOSS;
1048 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1056 // NOTE: LEGACY CODE, needs to be re-written!
1058 void havocbot_calculate_middlepoint()
1062 vector fo = '0 0 0';
1065 f = ctf_worldflaglist;
1070 f = f.ctf_worldflagnext;
1074 havocbot_ctf_middlepoint = s * (1.0 / n);
1075 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1079 entity havocbot_ctf_find_flag(entity bot)
1082 f = ctf_worldflaglist;
1085 if (bot.team == f.team)
1087 f = f.ctf_worldflagnext;
1092 entity havocbot_ctf_find_enemy_flag(entity bot)
1095 f = ctf_worldflaglist;
1098 if (bot.team != f.team)
1100 f = f.ctf_worldflagnext;
1105 float havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1113 FOR_EACH_PLAYER(head)
1115 if(head.team!=bot.team || head.deadflag != DEAD_NO || head == bot)
1118 if(vlen(head.origin - org) < tc_radius)
1125 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1128 head = ctf_worldflaglist;
1131 if (self.team == head.team)
1133 head = head.ctf_worldflagnext;
1136 navigation_routerating(head, ratingscale, 10000);
1139 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1142 head = ctf_worldflaglist;
1145 if (self.team == head.team)
1147 head = head.ctf_worldflagnext;
1152 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1155 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1158 head = ctf_worldflaglist;
1161 if (self.team != head.team)
1163 head = head.ctf_worldflagnext;
1166 navigation_routerating(head, ratingscale, 10000);
1169 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1171 if not(bot_waypoints_for_items)
1173 havocbot_goalrating_ctf_enemyflag(ratingscale);
1179 head = havocbot_ctf_find_enemy_flag(self);
1184 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1187 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1191 mf = havocbot_ctf_find_flag(self);
1193 if(mf.ctf_status == FLAG_BASE)
1197 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1200 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1203 head = ctf_worldflaglist;
1206 // flag is out in the field
1207 if(head.ctf_status != FLAG_BASE)
1208 if(head.tag_entity==world) // dropped
1212 if(vlen(org-head.origin)<df_radius)
1213 navigation_routerating(head, ratingscale, 10000);
1216 navigation_routerating(head, ratingscale, 10000);
1219 head = head.ctf_worldflagnext;
1223 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1227 head = findchainfloat(bot_pickup, TRUE);
1230 // gather health and armor only
1232 if (head.health || head.armorvalue)
1233 if (vlen(head.origin - org) < sradius)
1235 // get the value of the item
1236 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1238 navigation_routerating(head, t * ratingscale, 500);
1244 void havocbot_ctf_reset_role(entity bot)
1246 float cdefense, cmiddle, coffense;
1247 entity mf, ef, head;
1250 if(bot.deadflag != DEAD_NO)
1253 if(vlen(havocbot_ctf_middlepoint)==0)
1254 havocbot_calculate_middlepoint();
1257 if (bot.flagcarried)
1259 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1263 mf = havocbot_ctf_find_flag(bot);
1264 ef = havocbot_ctf_find_enemy_flag(bot);
1266 // Retrieve stolen flag
1267 if(mf.ctf_status!=FLAG_BASE)
1269 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1273 // If enemy flag is taken go to the middle to intercept pursuers
1274 if(ef.ctf_status!=FLAG_BASE)
1276 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1280 // if there is only me on the team switch to offense
1282 FOR_EACH_PLAYER(head)
1283 if(head.team==bot.team)
1288 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1292 // Evaluate best position to take
1293 // Count mates on middle position
1294 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1296 // Count mates on defense position
1297 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1299 // Count mates on offense position
1300 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1302 if(cdefense<=coffense)
1303 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1304 else if(coffense<=cmiddle)
1305 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1307 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1310 void havocbot_role_ctf_carrier()
1312 if(self.deadflag != DEAD_NO)
1314 havocbot_ctf_reset_role(self);
1318 if (self.flagcarried == world)
1320 havocbot_ctf_reset_role(self);
1324 if (self.bot_strategytime < time)
1326 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1328 navigation_goalrating_start();
1329 havocbot_goalrating_ctf_ourbase(50000);
1332 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1334 navigation_goalrating_end();
1336 if (self.navigation_hasgoals)
1337 self.havocbot_cantfindflag = time + 10;
1338 else if (time > self.havocbot_cantfindflag)
1340 // Can't navigate to my own base, suicide!
1341 // TODO: drop it and wander around
1342 Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1348 void havocbot_role_ctf_escort()
1352 if(self.deadflag != DEAD_NO)
1354 havocbot_ctf_reset_role(self);
1358 if (self.flagcarried)
1360 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1364 // If enemy flag is back on the base switch to previous role
1365 ef = havocbot_ctf_find_enemy_flag(self);
1366 if(ef.ctf_status==FLAG_BASE)
1368 self.havocbot_role = self.havocbot_previous_role;
1369 self.havocbot_role_timeout = 0;
1373 // If the flag carrier reached the base switch to defense
1374 mf = havocbot_ctf_find_flag(self);
1375 if(mf.ctf_status!=FLAG_BASE)
1376 if(vlen(ef.origin - mf.dropped_origin) < 300)
1378 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1382 // Set the role timeout if necessary
1383 if (!self.havocbot_role_timeout)
1385 self.havocbot_role_timeout = time + random() * 30 + 60;
1388 // If nothing happened just switch to previous role
1389 if (time > self.havocbot_role_timeout)
1391 self.havocbot_role = self.havocbot_previous_role;
1392 self.havocbot_role_timeout = 0;
1396 // Chase the flag carrier
1397 if (self.bot_strategytime < time)
1399 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1400 navigation_goalrating_start();
1401 havocbot_goalrating_ctf_enemyflag(30000);
1402 havocbot_goalrating_ctf_ourstolenflag(40000);
1403 havocbot_goalrating_items(10000, self.origin, 10000);
1404 navigation_goalrating_end();
1408 void havocbot_role_ctf_offense()
1413 if(self.deadflag != DEAD_NO)
1415 havocbot_ctf_reset_role(self);
1419 if (self.flagcarried)
1421 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1426 mf = havocbot_ctf_find_flag(self);
1427 ef = havocbot_ctf_find_enemy_flag(self);
1430 if(mf.ctf_status!=FLAG_BASE)
1433 pos = mf.tag_entity.origin;
1437 // Try to get it if closer than the enemy base
1438 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1440 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1445 // Escort flag carrier
1446 if(ef.ctf_status!=FLAG_BASE)
1449 pos = ef.tag_entity.origin;
1453 if(vlen(pos-mf.dropped_origin)>700)
1455 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1460 // About to fail, switch to middlefield
1463 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1467 // Set the role timeout if necessary
1468 if (!self.havocbot_role_timeout)
1469 self.havocbot_role_timeout = time + 120;
1471 if (time > self.havocbot_role_timeout)
1473 havocbot_ctf_reset_role(self);
1477 if (self.bot_strategytime < time)
1479 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1480 navigation_goalrating_start();
1481 havocbot_goalrating_ctf_ourstolenflag(50000);
1482 havocbot_goalrating_ctf_enemybase(20000);
1483 havocbot_goalrating_items(5000, self.origin, 1000);
1484 havocbot_goalrating_items(1000, self.origin, 10000);
1485 navigation_goalrating_end();
1489 // Retriever (temporary role):
1490 void havocbot_role_ctf_retriever()
1494 if(self.deadflag != DEAD_NO)
1496 havocbot_ctf_reset_role(self);
1500 if (self.flagcarried)
1502 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1506 // If flag is back on the base switch to previous role
1507 mf = havocbot_ctf_find_flag(self);
1508 if(mf.ctf_status==FLAG_BASE)
1510 havocbot_ctf_reset_role(self);
1514 if (!self.havocbot_role_timeout)
1515 self.havocbot_role_timeout = time + 20;
1517 if (time > self.havocbot_role_timeout)
1519 havocbot_ctf_reset_role(self);
1523 if (self.bot_strategytime < time)
1528 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1529 navigation_goalrating_start();
1530 havocbot_goalrating_ctf_ourstolenflag(50000);
1531 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1532 havocbot_goalrating_ctf_enemybase(30000);
1533 havocbot_goalrating_items(500, self.origin, rt_radius);
1534 navigation_goalrating_end();
1538 void havocbot_role_ctf_middle()
1542 if(self.deadflag != DEAD_NO)
1544 havocbot_ctf_reset_role(self);
1548 if (self.flagcarried)
1550 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1554 mf = havocbot_ctf_find_flag(self);
1555 if(mf.ctf_status!=FLAG_BASE)
1557 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1561 if (!self.havocbot_role_timeout)
1562 self.havocbot_role_timeout = time + 10;
1564 if (time > self.havocbot_role_timeout)
1566 havocbot_ctf_reset_role(self);
1570 if (self.bot_strategytime < time)
1574 org = havocbot_ctf_middlepoint;
1575 org_z = self.origin_z;
1577 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1578 navigation_goalrating_start();
1579 havocbot_goalrating_ctf_ourstolenflag(50000);
1580 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1581 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1582 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1583 havocbot_goalrating_items(2500, self.origin, 10000);
1584 havocbot_goalrating_ctf_enemybase(2500);
1585 navigation_goalrating_end();
1589 void havocbot_role_ctf_defense()
1593 if(self.deadflag != DEAD_NO)
1595 havocbot_ctf_reset_role(self);
1599 if (self.flagcarried)
1601 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1605 // If own flag was captured
1606 mf = havocbot_ctf_find_flag(self);
1607 if(mf.ctf_status!=FLAG_BASE)
1609 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1613 if (!self.havocbot_role_timeout)
1614 self.havocbot_role_timeout = time + 30;
1616 if (time > self.havocbot_role_timeout)
1618 havocbot_ctf_reset_role(self);
1621 if (self.bot_strategytime < time)
1626 org = mf.dropped_origin;
1627 mp_radius = havocbot_ctf_middlepoint_radius;
1629 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1630 navigation_goalrating_start();
1632 // if enemies are closer to our base, go there
1633 entity head, closestplayer = world;
1634 float distance, bestdistance = 10000;
1635 FOR_EACH_PLAYER(head)
1637 if(head.deadflag!=DEAD_NO)
1640 distance = vlen(org - head.origin);
1641 if(distance<bestdistance)
1643 closestplayer = head;
1644 bestdistance = distance;
1649 if(closestplayer.team!=self.team)
1650 if(vlen(org - self.origin)>1000)
1651 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1652 havocbot_goalrating_ctf_ourbase(30000);
1654 havocbot_goalrating_ctf_ourstolenflag(20000);
1655 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1656 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1657 havocbot_goalrating_items(10000, org, mp_radius);
1658 havocbot_goalrating_items(5000, self.origin, 10000);
1659 navigation_goalrating_end();
1663 void havocbot_role_ctf_setrole(entity bot, float role)
1665 dprint(strcat(bot.netname," switched to "));
1668 case HAVOCBOT_CTF_ROLE_CARRIER:
1670 bot.havocbot_role = havocbot_role_ctf_carrier;
1671 bot.havocbot_role_timeout = 0;
1672 bot.havocbot_cantfindflag = time + 10;
1673 bot.bot_strategytime = 0;
1675 case HAVOCBOT_CTF_ROLE_DEFENSE:
1677 bot.havocbot_role = havocbot_role_ctf_defense;
1678 bot.havocbot_role_timeout = 0;
1680 case HAVOCBOT_CTF_ROLE_MIDDLE:
1682 bot.havocbot_role = havocbot_role_ctf_middle;
1683 bot.havocbot_role_timeout = 0;
1685 case HAVOCBOT_CTF_ROLE_OFFENSE:
1687 bot.havocbot_role = havocbot_role_ctf_offense;
1688 bot.havocbot_role_timeout = 0;
1690 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1691 dprint("retriever");
1692 bot.havocbot_previous_role = bot.havocbot_role;
1693 bot.havocbot_role = havocbot_role_ctf_retriever;
1694 bot.havocbot_role_timeout = time + 10;
1695 bot.bot_strategytime = 0;
1697 case HAVOCBOT_CTF_ROLE_ESCORT:
1699 bot.havocbot_previous_role = bot.havocbot_role;
1700 bot.havocbot_role = havocbot_role_ctf_escort;
1701 bot.havocbot_role_timeout = time + 30;
1702 bot.bot_strategytime = 0;
1713 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1717 // initially clear items so they can be set as necessary later.
1718 self.items &~= (IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST
1719 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST | IT_CTF_SHIELDED);
1721 // scan through all the flags and notify the client about them
1722 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1724 switch(flag.ctf_status)
1729 if((flag.owner == self) || (flag.pass_sender == self))
1730 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
1732 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
1737 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
1743 // item for stopping players from capturing the flag too often
1744 if(self.ctf_captureshielded)
1745 self.items |= IT_CTF_SHIELDED;
1747 // update the health of the flag carrier waypointsprite
1748 if(self.wps_flagcarrier)
1749 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent));
1754 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1756 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1758 if(frag_target == frag_attacker) // damage done to yourself
1760 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1761 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1763 else // damage done to everyone else
1765 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1766 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1769 else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && IsDifferentTeam(frag_target, frag_attacker)) // if the target is a flagcarrier
1771 if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent)))
1772 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1774 frag_target.wps_helpme_time = time;
1775 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1777 // todo: add notification for when flag carrier needs help?
1782 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1784 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
1786 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1787 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1790 if(frag_target.flagcarried)
1791 { ctf_Handle_Throw(frag_target, world, DROP_NORMAL); }
1796 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1799 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1802 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1804 entity flag; // temporary entity for the search method
1806 if(self.flagcarried)
1807 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1809 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1811 if(flag.pass_sender == self) { flag.pass_sender = world; }
1812 if(flag.pass_target == self) { flag.pass_target = world; }
1813 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
1819 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
1821 if(self.flagcarried)
1822 if(!autocvar_g_ctf_portalteleport)
1823 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1828 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
1830 if(MUTATOR_RETURNVALUE || gameover) { return FALSE; }
1832 entity player = self;
1834 if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1836 // pass the flag to a team mate
1837 if(autocvar_g_ctf_pass)
1839 entity head, closest_target = world;
1840 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, TRUE);
1842 while(head) // find the closest acceptable target to pass to
1844 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
1845 if(head != player && !IsDifferentTeam(head, player))
1846 if(!head.speedrunning && !head.vehicle)
1848 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
1849 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
1850 vector passer_center = CENTER_OR_VIEWOFS(player);
1852 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
1854 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
1856 if(IS_BOT_CLIENT(head))
1858 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1859 ctf_Handle_Throw(head, player, DROP_PASS);
1863 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
1864 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1866 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
1869 else if(player.flagcarried)
1873 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
1874 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
1875 { closest_target = head; }
1877 else { closest_target = head; }
1884 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return TRUE; }
1887 // throw the flag in front of you
1888 if(autocvar_g_ctf_throw && player.flagcarried)
1890 if(player.throw_count == -1)
1892 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
1894 player.throw_prevtime = time;
1895 player.throw_count = 1;
1896 ctf_Handle_Throw(player, world, DROP_THROW);
1901 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
1907 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
1908 else { player.throw_count += 1; }
1909 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
1911 player.throw_prevtime = time;
1912 ctf_Handle_Throw(player, world, DROP_THROW);
1921 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
1923 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
1925 self.wps_helpme_time = time;
1926 WaypointSprite_HelpMePing(self.wps_flagcarrier);
1928 else // create a normal help me waypointsprite
1930 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');
1931 WaypointSprite_Ping(self.wps_helpme);
1937 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
1939 if(vh_player.flagcarried)
1941 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
1943 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
1947 setattachment(vh_player.flagcarried, vh_vehicle, "");
1948 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
1949 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
1950 //vh_player.flagcarried.angles = '0 0 0';
1958 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
1960 if(vh_player.flagcarried)
1962 setattachment(vh_player.flagcarried, vh_player, "");
1963 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
1964 vh_player.flagcarried.scale = FLAG_SCALE;
1965 vh_player.flagcarried.angles = '0 0 0';
1972 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
1974 if(self.flagcarried)
1976 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN_));
1977 ctf_RespawnFlag(self.flagcarried);
1984 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
1986 entity flag; // temporary entity for the search method
1988 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1990 switch(flag.ctf_status)
1995 // lock the flag, game is over
1996 flag.movetype = MOVETYPE_NONE;
1997 flag.takedamage = DAMAGE_NO;
1998 flag.solid = SOLID_NOT;
1999 flag.nextthink = FALSE; // stop thinking
2001 //dprint("stopping the ", flag.netname, " from moving.\n");
2009 // do nothing for these flags
2018 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
2020 havocbot_ctf_reset_role(self);
2024 MUTATOR_HOOKFUNCTION(ctf_GetCvars)
2026 GetCvars_handleFloat(get_cvars_s, get_cvars_f, CAPTURE_VERBOSE, "notification_ctf_capture_verbose");
2027 GetCvars_handleFloat(get_cvars_s, get_cvars_f, PICKUP_TEAM_VERBOSE, "notification_ctf_pickup_team_verbose");
2028 GetCvars_handleFloat(get_cvars_s, get_cvars_f, PICKUP_ENEMY_VERBOSE, "notification_ctf_pickup_enemy_verbose");
2037 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
2038 CTF Starting point for a player in team one (Red).
2039 Keys: "angle" viewing angle when spawning. */
2040 void spawnfunc_info_player_team1()
2042 if(g_assault) { remove(self); return; }
2044 self.team = NUM_TEAM_1; // red
2045 spawnfunc_info_player_deathmatch();
2049 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
2050 CTF Starting point for a player in team two (Blue).
2051 Keys: "angle" viewing angle when spawning. */
2052 void spawnfunc_info_player_team2()
2054 if(g_assault) { remove(self); return; }
2056 self.team = NUM_TEAM_2; // blue
2057 spawnfunc_info_player_deathmatch();
2060 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
2061 CTF Starting point for a player in team three (Yellow).
2062 Keys: "angle" viewing angle when spawning. */
2063 void spawnfunc_info_player_team3()
2065 if(g_assault) { remove(self); return; }
2067 self.team = NUM_TEAM_3; // yellow
2068 spawnfunc_info_player_deathmatch();
2072 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
2073 CTF Starting point for a player in team four (Purple).
2074 Keys: "angle" viewing angle when spawning. */
2075 void spawnfunc_info_player_team4()
2077 if(g_assault) { remove(self); return; }
2079 self.team = NUM_TEAM_4; // purple
2080 spawnfunc_info_player_deathmatch();
2083 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2084 CTF flag for team one (Red).
2086 "angle" Angle the flag will point (minus 90 degrees)...
2087 "model" model to use, note this needs red and blue as skins 0 and 1...
2088 "noise" sound played when flag is picked up...
2089 "noise1" sound played when flag is returned by a teammate...
2090 "noise2" sound played when flag is captured...
2091 "noise3" sound played when flag is lost in the field and respawns itself...
2092 "noise4" sound played when flag is dropped by a player...
2093 "noise5" sound played when flag touches the ground... */
2094 void spawnfunc_item_flag_team1()
2096 if(!g_ctf) { remove(self); return; }
2098 ctf_FlagSetup(1, self); // 1 = red
2101 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2102 CTF flag for team two (Blue).
2104 "angle" Angle the flag will point (minus 90 degrees)...
2105 "model" model to use, note this needs red and blue as skins 0 and 1...
2106 "noise" sound played when flag is picked up...
2107 "noise1" sound played when flag is returned by a teammate...
2108 "noise2" sound played when flag is captured...
2109 "noise3" sound played when flag is lost in the field and respawns itself...
2110 "noise4" sound played when flag is dropped by a player...
2111 "noise5" sound played when flag touches the ground... */
2112 void spawnfunc_item_flag_team2()
2114 if(!g_ctf) { remove(self); return; }
2116 ctf_FlagSetup(0, self); // the 0 is misleading, but -- 0 = blue.
2119 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2120 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2121 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.
2123 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2124 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2125 void spawnfunc_ctf_team()
2127 if(!g_ctf) { remove(self); return; }
2129 self.classname = "ctf_team";
2130 self.team = self.cnt + 1;
2133 // compatibility for quake maps
2134 void spawnfunc_team_CTF_redflag() { spawnfunc_item_flag_team1(); }
2135 void spawnfunc_team_CTF_blueflag() { spawnfunc_item_flag_team2(); }
2136 void spawnfunc_team_CTF_redplayer() { spawnfunc_info_player_team1(); }
2137 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2(); }
2138 void spawnfunc_team_CTF_redspawn() { spawnfunc_info_player_team1(); }
2139 void spawnfunc_team_CTF_bluespawn() { spawnfunc_info_player_team2(); }
2147 void ctf_ScoreRules()
2149 ScoreRules_basics(2, SFL_SORT_PRIO_PRIMARY, 0, TRUE);
2150 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2151 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2152 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2153 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2154 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2155 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2156 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2157 ScoreRules_basics_end();
2160 // code from here on is just to support maps that don't have flag and team entities
2161 void ctf_SpawnTeam (string teamname, float teamcolor)
2166 self.classname = "ctf_team";
2167 self.netname = teamname;
2168 self.cnt = teamcolor;
2170 spawnfunc_ctf_team();
2175 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2177 // if no teams are found, spawn defaults
2178 if(find(world, classname, "ctf_team") == world)
2180 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2181 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2182 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2188 void ctf_Initialize()
2190 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2192 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2193 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2194 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2196 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2200 MUTATOR_DEFINITION(gamemode_ctf)
2202 MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2203 MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2204 MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2205 MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2206 MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2207 MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2208 MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2209 MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2210 MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2211 MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2212 MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2213 MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2214 MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2215 MUTATOR_HOOK(HavocBot_ChooseRule, ctf_BotRoles, CBC_ORDER_ANY);
2216 MUTATOR_HOOK(GetCvars, ctf_GetCvars, CBC_ORDER_ANY);
2220 if(time > 1) // game loads at time 1
2221 error("This is a game type and it cannot be added at runtime.");
2225 MUTATOR_ONROLLBACK_OR_REMOVE
2227 // we actually cannot roll back ctf_Initialize here
2228 // BUT: we don't need to! If this gets called, adding always
2234 print("This is a game type and it cannot be removed at runtime.");