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_4(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_4(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_4(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_4(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_4(flag, INFO_CTF_CAPTURE_TIME_), player.netname, (cap_time * 100)); }
48 else if(cap_time < cap_record) { Local_Notification(MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_CAPTURE_BROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
49 else { Local_Notification(MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_CAPTURE_UNBROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
51 else { Local_Notification(MSG_INFO, APP_TEAM_ENT_4(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_4(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_4(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_4(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_4(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_4(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_4(flag, CENTER_CTF_RETURN_));
455 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_), player.netname);
456 sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, 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_4(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_4(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_4(flag, INFO_CTF_FLAGRETURN_DROPPED_)); break;
586 case RETURN_DAMAGE: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DAMAGED_)); break;
587 case RETURN_SPEEDRUN: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_SPEEDRUN_), ctf_captimerecord); break;
588 case RETURN_NEEDKILL: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_NEEDKILL_)); break;
592 { Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(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 if(!IsDifferentTeam(self, tmp_entity.flagcarried))
713 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
720 if(autocvar_g_ctf_flag_dropped_floatinwater)
722 vector midpoint = ((self.absmin + self.absmax) * 0.5);
723 if(pointcontents(midpoint) == CONTENT_WATER)
725 self.velocity = self.velocity * 0.5;
727 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
728 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
730 { self.movetype = MOVETYPE_FLY; }
732 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
734 if(autocvar_g_ctf_flag_return_dropped)
736 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
739 ctf_CheckFlagReturn(self, RETURN_DROPPED);
743 if(autocvar_g_ctf_flag_return_time)
745 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
746 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
754 if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
757 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
761 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
765 if(autocvar_g_ctf_stalemate)
767 if(time >= wpforenemy_nextthink)
769 ctf_CheckStalemate();
770 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
778 vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
779 targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
780 WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
782 if((self.pass_target == world)
783 || (self.pass_target.deadflag != DEAD_NO)
784 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
785 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
786 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
788 // give up, pass failed
789 ctf_Handle_Drop(self, world, DROP_PASS);
793 // still a viable target, go for it
794 ctf_CalculatePassVelocity(self, targ_origin, self.origin, TRUE);
799 default: // this should never happen
801 dprint("ctf_FlagThink(): Flag exists with no status?\n");
809 if(gameover) { return; }
811 entity toucher = other;
813 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
814 if(ITEM_TOUCH_NEEDKILL())
817 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
821 // special touch behaviors
822 if(toucher.vehicle_flags & VHF_ISVEHICLE)
824 if(autocvar_g_ctf_allow_vehicle_touch)
825 toucher = toucher.owner; // the player is actually the vehicle owner, not other
827 return; // do nothing
829 else if not(IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
831 if(time > self.wait) // if we haven't in a while, play a sound/effect
833 pointparticles(particleeffectnum(self.toucheffect), self.origin, '0 0 0', 1);
834 sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTN_NORM);
835 self.wait = time + FLAG_TOUCHRATE;
839 else if(toucher.deadflag != DEAD_NO) { return; }
841 switch(self.ctf_status)
845 if(!IsDifferentTeam(toucher, self) && (toucher.flagcarried) && IsDifferentTeam(toucher.flagcarried, self))
846 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
847 else if(IsDifferentTeam(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time))
848 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
854 if(!IsDifferentTeam(toucher, self))
855 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
856 else if((!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
857 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
863 dprint("Someone touched a flag even though it was being carried?\n");
869 if((IS_PLAYER(toucher)) && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
871 if(IsDifferentTeam(toucher, self.pass_sender))
872 ctf_Handle_Return(self, toucher);
874 ctf_Handle_Retrieve(self, toucher);
882 void ctf_RespawnFlag(entity flag)
884 // check for flag respawn being called twice in a row
885 if(flag.last_respawn > time - 0.5)
886 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
888 flag.last_respawn = time;
890 // reset the player (if there is one)
891 if((flag.owner) && (flag.owner.flagcarried == flag))
893 if(flag.owner.wps_enemyflagcarrier)
894 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
896 WaypointSprite_Kill(flag.wps_flagcarrier);
898 flag.owner.flagcarried = world;
900 if(flag.speedrunning)
901 ctf_FakeTimeLimit(flag.owner, -1);
904 if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
905 { WaypointSprite_Kill(flag.wps_flagdropped); }
908 setattachment(flag, world, "");
909 setorigin(flag, flag.ctf_spawnorigin);
911 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
912 flag.takedamage = DAMAGE_NO;
913 flag.health = flag.max_flag_health;
914 flag.solid = SOLID_TRIGGER;
915 flag.velocity = '0 0 0';
916 flag.angles = flag.mangle;
917 flag.flags = FL_ITEM | FL_NOTARGET;
919 flag.ctf_status = FLAG_BASE;
921 flag.pass_distance = 0;
922 flag.pass_sender = world;
923 flag.pass_target = world;
924 flag.ctf_dropper = world;
925 flag.ctf_pickuptime = 0;
926 flag.ctf_droptime = 0;
932 if(IS_PLAYER(self.owner))
933 ctf_Handle_Throw(self.owner, world, DROP_RESET);
935 ctf_RespawnFlag(self);
938 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
941 waypoint_spawnforitem_force(self, self.origin);
942 self.nearestwaypointtimeout = 0; // activate waypointing again
943 self.bot_basewaypoint = self.nearestwaypoint;
946 string basename = "base";
950 case NUM_TEAM_1: basename = "redbase"; break;
951 case NUM_TEAM_2: basename = "bluebase"; break;
952 case NUM_TEAM_3: basename = "yellowbase"; break;
953 case NUM_TEAM_4: basename = "pinkbase"; break;
956 WaypointSprite_SpawnFixed(basename, self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
957 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
959 // captureshield setup
960 ctf_CaptureShield_Spawn(self);
963 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
966 //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.
967 self = flag; // for later usage with droptofloor()
972 teamnumber = ((autocvar_g_ctf_reverse) ? NUM_TEAM_1 : NUM_TEAM_2);
975 teamnumber = ((autocvar_g_ctf_reverse) ? NUM_TEAM_2 : NUM_TEAM_1);
978 teamnumber = ((autocvar_g_ctf_reverse) ? NUM_TEAM_4 : NUM_TEAM_3);
981 teamnumber = ((autocvar_g_ctf_reverse) ? NUM_TEAM_3 : NUM_TEAM_4);
986 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
987 ctf_worldflaglist = flag;
989 setattachment(flag, world, "");
991 flag.netname = ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag"); // Primarily only used for debugging or when showing nearby item name
992 //flag.team = ((teamnumber) ? NUM_TEAM_1 : NUM_TEAM_2); // NUM_TEAM_1: color 4 team (red) - NUM_TEAM_2: color 13 team (blue)
993 flag.team = teamnumber;
994 flag.items = ((teamnumber) ? IT_KEY2 : IT_KEY1); // IT_KEY2: gold key (redish enough) - IT_KEY1: silver key (bluish enough)
995 flag.classname = "item_flag_team";
996 flag.target = "###item###"; // wut?
997 flag.flags = FL_ITEM | FL_NOTARGET;
998 flag.solid = SOLID_TRIGGER;
999 flag.takedamage = DAMAGE_NO;
1000 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1001 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1002 flag.health = flag.max_flag_health;
1003 flag.event_damage = ctf_FlagDamage;
1004 flag.pushable = TRUE;
1005 flag.teleportable = TELEPORT_NORMAL;
1006 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1007 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1008 flag.velocity = '0 0 0';
1009 flag.mangle = flag.angles;
1010 flag.reset = ctf_Reset;
1011 flag.touch = ctf_FlagTouch;
1012 flag.think = ctf_FlagThink;
1013 flag.nextthink = time + FLAG_THINKRATE;
1014 flag.ctf_status = FLAG_BASE;
1017 if(flag.model == "") { flag.model = ((teamnumber) ? autocvar_g_ctf_flag_red_model : autocvar_g_ctf_flag_blue_model); }
1018 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1019 //if(!flag.skin) { flag.skin = ((teamnumber) ? autocvar_g_ctf_flag_red_skin : autocvar_g_ctf_flag_blue_skin); }
1020 if(flag.toucheffect == "") { flag.toucheffect = ((teamnumber) ? "redflag_touch" : "blueflag_touch"); }
1021 if(flag.passeffect == "") { flag.passeffect = ((teamnumber) ? "red_pass" : "blue_pass"); }
1022 if(flag.capeffect == "") { flag.capeffect = ((teamnumber) ? "red_cap" : "blue_cap"); }
1027 case NUM_TEAM_1: flag.skin = autocvar_g_ctf_flag_red_skin; break;
1028 case NUM_TEAM_2: flag.skin = autocvar_g_ctf_flag_blue_skin; break;
1029 case NUM_TEAM_3: flag.skin = 2; break;
1030 case NUM_TEAM_4: flag.skin = 3; break;
1035 if(flag.snd_flag_taken == "") { flag.snd_flag_taken = ((teamnumber) ? "ctf/red_taken.wav" : "ctf/blue_taken.wav"); }
1036 if(flag.snd_flag_returned == "") { flag.snd_flag_returned = ((teamnumber) ? "ctf/red_returned.wav" : "ctf/blue_returned.wav"); }
1037 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
1038 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.
1039 if(flag.snd_flag_dropped == "") { flag.snd_flag_dropped = ((teamnumber) ? "ctf/red_dropped.wav" : "ctf/blue_dropped.wav"); }
1040 if(flag.snd_flag_touch == "") { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
1041 if(flag.snd_flag_pass == "") { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
1044 precache_sound(flag.snd_flag_taken);
1045 precache_sound(flag.snd_flag_returned);
1046 precache_sound(flag.snd_flag_capture);
1047 precache_sound(flag.snd_flag_respawn);
1048 precache_sound(flag.snd_flag_dropped);
1049 precache_sound(flag.snd_flag_touch);
1050 precache_sound(flag.snd_flag_pass);
1051 precache_model(flag.model);
1052 precache_model("models/ctf/shield.md3");
1053 precache_model("models/ctf/shockwavetransring.md3");
1056 setmodel(flag, flag.model); // precision set below
1057 setsize(flag, FLAG_MIN, FLAG_MAX);
1058 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1060 if(autocvar_g_ctf_flag_glowtrails)
1062 flag.glow_color = ((teamnumber) ? 251 : 210); // 251: red - 210: blue
1063 flag.glow_size = 25;
1064 flag.glow_trail = 1;
1067 flag.effects |= EF_LOWPRECISION;
1068 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1069 if(autocvar_g_ctf_dynamiclights) { flag.effects |= ((teamnumber) ? EF_RED : EF_BLUE); }
1072 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1074 flag.dropped_origin = flag.origin;
1075 flag.noalign = TRUE;
1076 flag.movetype = MOVETYPE_NONE;
1078 else // drop to floor, automatically find a platform and set that as spawn origin
1080 flag.noalign = FALSE;
1083 flag.movetype = MOVETYPE_TOSS;
1086 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1094 // NOTE: LEGACY CODE, needs to be re-written!
1096 void havocbot_calculate_middlepoint()
1100 vector fo = '0 0 0';
1103 f = ctf_worldflaglist;
1108 f = f.ctf_worldflagnext;
1112 havocbot_ctf_middlepoint = s * (1.0 / n);
1113 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1117 entity havocbot_ctf_find_flag(entity bot)
1120 f = ctf_worldflaglist;
1123 if (bot.team == f.team)
1125 f = f.ctf_worldflagnext;
1130 entity havocbot_ctf_find_enemy_flag(entity bot)
1133 f = ctf_worldflaglist;
1136 if (bot.team != f.team)
1138 f = f.ctf_worldflagnext;
1143 float havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1151 FOR_EACH_PLAYER(head)
1153 if(head.team!=bot.team || head.deadflag != DEAD_NO || head == bot)
1156 if(vlen(head.origin - org) < tc_radius)
1163 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1166 head = ctf_worldflaglist;
1169 if (self.team == head.team)
1171 head = head.ctf_worldflagnext;
1174 navigation_routerating(head, ratingscale, 10000);
1177 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1180 head = ctf_worldflaglist;
1183 if (self.team == head.team)
1185 head = head.ctf_worldflagnext;
1190 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1193 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1196 head = ctf_worldflaglist;
1199 if (self.team != head.team)
1201 head = head.ctf_worldflagnext;
1204 navigation_routerating(head, ratingscale, 10000);
1207 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1209 if not(bot_waypoints_for_items)
1211 havocbot_goalrating_ctf_enemyflag(ratingscale);
1217 head = havocbot_ctf_find_enemy_flag(self);
1222 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1225 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1229 mf = havocbot_ctf_find_flag(self);
1231 if(mf.ctf_status == FLAG_BASE)
1235 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1238 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1241 head = ctf_worldflaglist;
1244 // flag is out in the field
1245 if(head.ctf_status != FLAG_BASE)
1246 if(head.tag_entity==world) // dropped
1250 if(vlen(org-head.origin)<df_radius)
1251 navigation_routerating(head, ratingscale, 10000);
1254 navigation_routerating(head, ratingscale, 10000);
1257 head = head.ctf_worldflagnext;
1261 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1265 head = findchainfloat(bot_pickup, TRUE);
1268 // gather health and armor only
1270 if (head.health || head.armorvalue)
1271 if (vlen(head.origin - org) < sradius)
1273 // get the value of the item
1274 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1276 navigation_routerating(head, t * ratingscale, 500);
1282 void havocbot_ctf_reset_role(entity bot)
1284 float cdefense, cmiddle, coffense;
1285 entity mf, ef, head;
1288 if(bot.deadflag != DEAD_NO)
1291 if(vlen(havocbot_ctf_middlepoint)==0)
1292 havocbot_calculate_middlepoint();
1295 if (bot.flagcarried)
1297 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1301 mf = havocbot_ctf_find_flag(bot);
1302 ef = havocbot_ctf_find_enemy_flag(bot);
1304 // Retrieve stolen flag
1305 if(mf.ctf_status!=FLAG_BASE)
1307 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1311 // If enemy flag is taken go to the middle to intercept pursuers
1312 if(ef.ctf_status!=FLAG_BASE)
1314 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1318 // if there is only me on the team switch to offense
1320 FOR_EACH_PLAYER(head)
1321 if(head.team==bot.team)
1326 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1330 // Evaluate best position to take
1331 // Count mates on middle position
1332 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1334 // Count mates on defense position
1335 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1337 // Count mates on offense position
1338 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1340 if(cdefense<=coffense)
1341 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1342 else if(coffense<=cmiddle)
1343 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1345 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1348 void havocbot_role_ctf_carrier()
1350 if(self.deadflag != DEAD_NO)
1352 havocbot_ctf_reset_role(self);
1356 if (self.flagcarried == world)
1358 havocbot_ctf_reset_role(self);
1362 if (self.bot_strategytime < time)
1364 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1366 navigation_goalrating_start();
1367 havocbot_goalrating_ctf_ourbase(50000);
1370 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1372 navigation_goalrating_end();
1374 if (self.navigation_hasgoals)
1375 self.havocbot_cantfindflag = time + 10;
1376 else if (time > self.havocbot_cantfindflag)
1378 // Can't navigate to my own base, suicide!
1379 // TODO: drop it and wander around
1380 Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1386 void havocbot_role_ctf_escort()
1390 if(self.deadflag != DEAD_NO)
1392 havocbot_ctf_reset_role(self);
1396 if (self.flagcarried)
1398 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1402 // If enemy flag is back on the base switch to previous role
1403 ef = havocbot_ctf_find_enemy_flag(self);
1404 if(ef.ctf_status==FLAG_BASE)
1406 self.havocbot_role = self.havocbot_previous_role;
1407 self.havocbot_role_timeout = 0;
1411 // If the flag carrier reached the base switch to defense
1412 mf = havocbot_ctf_find_flag(self);
1413 if(mf.ctf_status!=FLAG_BASE)
1414 if(vlen(ef.origin - mf.dropped_origin) < 300)
1416 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1420 // Set the role timeout if necessary
1421 if (!self.havocbot_role_timeout)
1423 self.havocbot_role_timeout = time + random() * 30 + 60;
1426 // If nothing happened just switch to previous role
1427 if (time > self.havocbot_role_timeout)
1429 self.havocbot_role = self.havocbot_previous_role;
1430 self.havocbot_role_timeout = 0;
1434 // Chase the flag carrier
1435 if (self.bot_strategytime < time)
1437 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1438 navigation_goalrating_start();
1439 havocbot_goalrating_ctf_enemyflag(30000);
1440 havocbot_goalrating_ctf_ourstolenflag(40000);
1441 havocbot_goalrating_items(10000, self.origin, 10000);
1442 navigation_goalrating_end();
1446 void havocbot_role_ctf_offense()
1451 if(self.deadflag != DEAD_NO)
1453 havocbot_ctf_reset_role(self);
1457 if (self.flagcarried)
1459 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1464 mf = havocbot_ctf_find_flag(self);
1465 ef = havocbot_ctf_find_enemy_flag(self);
1468 if(mf.ctf_status!=FLAG_BASE)
1471 pos = mf.tag_entity.origin;
1475 // Try to get it if closer than the enemy base
1476 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1478 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1483 // Escort flag carrier
1484 if(ef.ctf_status!=FLAG_BASE)
1487 pos = ef.tag_entity.origin;
1491 if(vlen(pos-mf.dropped_origin)>700)
1493 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1498 // About to fail, switch to middlefield
1501 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1505 // Set the role timeout if necessary
1506 if (!self.havocbot_role_timeout)
1507 self.havocbot_role_timeout = time + 120;
1509 if (time > self.havocbot_role_timeout)
1511 havocbot_ctf_reset_role(self);
1515 if (self.bot_strategytime < time)
1517 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1518 navigation_goalrating_start();
1519 havocbot_goalrating_ctf_ourstolenflag(50000);
1520 havocbot_goalrating_ctf_enemybase(20000);
1521 havocbot_goalrating_items(5000, self.origin, 1000);
1522 havocbot_goalrating_items(1000, self.origin, 10000);
1523 navigation_goalrating_end();
1527 // Retriever (temporary role):
1528 void havocbot_role_ctf_retriever()
1532 if(self.deadflag != DEAD_NO)
1534 havocbot_ctf_reset_role(self);
1538 if (self.flagcarried)
1540 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1544 // If flag is back on the base switch to previous role
1545 mf = havocbot_ctf_find_flag(self);
1546 if(mf.ctf_status==FLAG_BASE)
1548 havocbot_ctf_reset_role(self);
1552 if (!self.havocbot_role_timeout)
1553 self.havocbot_role_timeout = time + 20;
1555 if (time > self.havocbot_role_timeout)
1557 havocbot_ctf_reset_role(self);
1561 if (self.bot_strategytime < time)
1566 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1567 navigation_goalrating_start();
1568 havocbot_goalrating_ctf_ourstolenflag(50000);
1569 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1570 havocbot_goalrating_ctf_enemybase(30000);
1571 havocbot_goalrating_items(500, self.origin, rt_radius);
1572 navigation_goalrating_end();
1576 void havocbot_role_ctf_middle()
1580 if(self.deadflag != DEAD_NO)
1582 havocbot_ctf_reset_role(self);
1586 if (self.flagcarried)
1588 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1592 mf = havocbot_ctf_find_flag(self);
1593 if(mf.ctf_status!=FLAG_BASE)
1595 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1599 if (!self.havocbot_role_timeout)
1600 self.havocbot_role_timeout = time + 10;
1602 if (time > self.havocbot_role_timeout)
1604 havocbot_ctf_reset_role(self);
1608 if (self.bot_strategytime < time)
1612 org = havocbot_ctf_middlepoint;
1613 org_z = self.origin_z;
1615 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1616 navigation_goalrating_start();
1617 havocbot_goalrating_ctf_ourstolenflag(50000);
1618 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1619 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1620 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1621 havocbot_goalrating_items(2500, self.origin, 10000);
1622 havocbot_goalrating_ctf_enemybase(2500);
1623 navigation_goalrating_end();
1627 void havocbot_role_ctf_defense()
1631 if(self.deadflag != DEAD_NO)
1633 havocbot_ctf_reset_role(self);
1637 if (self.flagcarried)
1639 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1643 // If own flag was captured
1644 mf = havocbot_ctf_find_flag(self);
1645 if(mf.ctf_status!=FLAG_BASE)
1647 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1651 if (!self.havocbot_role_timeout)
1652 self.havocbot_role_timeout = time + 30;
1654 if (time > self.havocbot_role_timeout)
1656 havocbot_ctf_reset_role(self);
1659 if (self.bot_strategytime < time)
1664 org = mf.dropped_origin;
1665 mp_radius = havocbot_ctf_middlepoint_radius;
1667 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1668 navigation_goalrating_start();
1670 // if enemies are closer to our base, go there
1671 entity head, closestplayer = world;
1672 float distance, bestdistance = 10000;
1673 FOR_EACH_PLAYER(head)
1675 if(head.deadflag!=DEAD_NO)
1678 distance = vlen(org - head.origin);
1679 if(distance<bestdistance)
1681 closestplayer = head;
1682 bestdistance = distance;
1687 if(closestplayer.team!=self.team)
1688 if(vlen(org - self.origin)>1000)
1689 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1690 havocbot_goalrating_ctf_ourbase(30000);
1692 havocbot_goalrating_ctf_ourstolenflag(20000);
1693 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1694 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1695 havocbot_goalrating_items(10000, org, mp_radius);
1696 havocbot_goalrating_items(5000, self.origin, 10000);
1697 navigation_goalrating_end();
1701 void havocbot_role_ctf_setrole(entity bot, float role)
1703 dprint(strcat(bot.netname," switched to "));
1706 case HAVOCBOT_CTF_ROLE_CARRIER:
1708 bot.havocbot_role = havocbot_role_ctf_carrier;
1709 bot.havocbot_role_timeout = 0;
1710 bot.havocbot_cantfindflag = time + 10;
1711 bot.bot_strategytime = 0;
1713 case HAVOCBOT_CTF_ROLE_DEFENSE:
1715 bot.havocbot_role = havocbot_role_ctf_defense;
1716 bot.havocbot_role_timeout = 0;
1718 case HAVOCBOT_CTF_ROLE_MIDDLE:
1720 bot.havocbot_role = havocbot_role_ctf_middle;
1721 bot.havocbot_role_timeout = 0;
1723 case HAVOCBOT_CTF_ROLE_OFFENSE:
1725 bot.havocbot_role = havocbot_role_ctf_offense;
1726 bot.havocbot_role_timeout = 0;
1728 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1729 dprint("retriever");
1730 bot.havocbot_previous_role = bot.havocbot_role;
1731 bot.havocbot_role = havocbot_role_ctf_retriever;
1732 bot.havocbot_role_timeout = time + 10;
1733 bot.bot_strategytime = 0;
1735 case HAVOCBOT_CTF_ROLE_ESCORT:
1737 bot.havocbot_previous_role = bot.havocbot_role;
1738 bot.havocbot_role = havocbot_role_ctf_escort;
1739 bot.havocbot_role_timeout = time + 30;
1740 bot.bot_strategytime = 0;
1751 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1755 // initially clear items so they can be set as necessary later.
1756 self.items &~= (IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST
1757 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST | IT_CTF_SHIELDED);
1759 // scan through all the flags and notify the client about them
1760 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1762 switch(flag.ctf_status)
1767 if((flag.owner == self) || (flag.pass_sender == self))
1768 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
1770 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
1775 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
1781 // item for stopping players from capturing the flag too often
1782 if(self.ctf_captureshielded)
1783 self.items |= IT_CTF_SHIELDED;
1785 // update the health of the flag carrier waypointsprite
1786 if(self.wps_flagcarrier)
1787 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent));
1792 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1794 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1796 if(frag_target == frag_attacker) // damage done to yourself
1798 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1799 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1801 else // damage done to everyone else
1803 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1804 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1807 else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && IsDifferentTeam(frag_target, frag_attacker)) // if the target is a flagcarrier
1809 if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent)))
1810 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1812 frag_target.wps_helpme_time = time;
1813 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1815 // todo: add notification for when flag carrier needs help?
1820 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1822 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
1824 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1825 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1828 if(frag_target.flagcarried)
1829 { ctf_Handle_Throw(frag_target, world, DROP_NORMAL); }
1834 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1837 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1840 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1842 entity flag; // temporary entity for the search method
1844 if(self.flagcarried)
1845 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1847 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1849 if(flag.pass_sender == self) { flag.pass_sender = world; }
1850 if(flag.pass_target == self) { flag.pass_target = world; }
1851 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
1857 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
1859 if(self.flagcarried)
1860 if(!autocvar_g_ctf_portalteleport)
1861 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1866 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
1868 if(MUTATOR_RETURNVALUE || gameover) { return FALSE; }
1870 entity player = self;
1872 if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1874 // pass the flag to a team mate
1875 if(autocvar_g_ctf_pass)
1877 entity head, closest_target = world;
1878 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, TRUE);
1880 while(head) // find the closest acceptable target to pass to
1882 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
1883 if(head != player && !IsDifferentTeam(head, player))
1884 if(!head.speedrunning && !head.vehicle)
1886 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
1887 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
1888 vector passer_center = CENTER_OR_VIEWOFS(player);
1890 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
1892 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
1894 if(IS_BOT_CLIENT(head))
1896 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1897 ctf_Handle_Throw(head, player, DROP_PASS);
1901 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
1902 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1904 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
1907 else if(player.flagcarried)
1911 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
1912 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
1913 { closest_target = head; }
1915 else { closest_target = head; }
1922 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return TRUE; }
1925 // throw the flag in front of you
1926 if(autocvar_g_ctf_throw && player.flagcarried)
1928 if(player.throw_count == -1)
1930 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
1932 player.throw_prevtime = time;
1933 player.throw_count = 1;
1934 ctf_Handle_Throw(player, world, DROP_THROW);
1939 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
1945 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
1946 else { player.throw_count += 1; }
1947 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
1949 player.throw_prevtime = time;
1950 ctf_Handle_Throw(player, world, DROP_THROW);
1959 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
1961 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
1963 self.wps_helpme_time = time;
1964 WaypointSprite_HelpMePing(self.wps_flagcarrier);
1966 else // create a normal help me waypointsprite
1968 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');
1969 WaypointSprite_Ping(self.wps_helpme);
1975 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
1977 if(vh_player.flagcarried)
1979 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
1981 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
1985 setattachment(vh_player.flagcarried, vh_vehicle, "");
1986 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
1987 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
1988 //vh_player.flagcarried.angles = '0 0 0';
1996 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
1998 if(vh_player.flagcarried)
2000 setattachment(vh_player.flagcarried, vh_player, "");
2001 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2002 vh_player.flagcarried.scale = FLAG_SCALE;
2003 vh_player.flagcarried.angles = '0 0 0';
2010 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
2012 if(self.flagcarried)
2014 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN_));
2015 ctf_RespawnFlag(self.flagcarried);
2022 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
2024 entity flag; // temporary entity for the search method
2026 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2028 switch(flag.ctf_status)
2033 // lock the flag, game is over
2034 flag.movetype = MOVETYPE_NONE;
2035 flag.takedamage = DAMAGE_NO;
2036 flag.solid = SOLID_NOT;
2037 flag.nextthink = FALSE; // stop thinking
2039 //dprint("stopping the ", flag.netname, " from moving.\n");
2047 // do nothing for these flags
2056 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
2058 havocbot_ctf_reset_role(self);
2062 MUTATOR_HOOKFUNCTION(ctf_GetCvars)
2064 GetCvars_handleFloat(get_cvars_s, get_cvars_f, CAPTURE_VERBOSE, "notification_ctf_capture_verbose");
2065 GetCvars_handleFloat(get_cvars_s, get_cvars_f, PICKUP_TEAM_VERBOSE, "notification_ctf_pickup_team_verbose");
2066 GetCvars_handleFloat(get_cvars_s, get_cvars_f, PICKUP_ENEMY_VERBOSE, "notification_ctf_pickup_enemy_verbose");
2070 MUTATOR_HOOKFUNCTION(ctf_GetTeamCount)
2072 ret_float = ctf_teams;
2081 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
2082 CTF Starting point for a player in team one (Red).
2083 Keys: "angle" viewing angle when spawning. */
2084 void spawnfunc_info_player_team1()
2086 if(g_assault) { remove(self); return; }
2088 self.team = NUM_TEAM_1; // red
2089 spawnfunc_info_player_deathmatch();
2093 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
2094 CTF Starting point for a player in team two (Blue).
2095 Keys: "angle" viewing angle when spawning. */
2096 void spawnfunc_info_player_team2()
2098 if(g_assault) { remove(self); return; }
2100 self.team = NUM_TEAM_2; // blue
2101 spawnfunc_info_player_deathmatch();
2104 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
2105 CTF Starting point for a player in team three (Yellow).
2106 Keys: "angle" viewing angle when spawning. */
2107 void spawnfunc_info_player_team3()
2109 if(g_assault) { remove(self); return; }
2111 self.team = NUM_TEAM_3; // yellow
2112 spawnfunc_info_player_deathmatch();
2116 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
2117 CTF Starting point for a player in team four (Purple).
2118 Keys: "angle" viewing angle when spawning. */
2119 void spawnfunc_info_player_team4()
2121 if(g_assault) { remove(self); return; }
2123 self.team = NUM_TEAM_4; // purple
2124 spawnfunc_info_player_deathmatch();
2127 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2128 CTF flag for team one (Red).
2130 "angle" Angle the flag will point (minus 90 degrees)...
2131 "model" model to use, note this needs red and blue as skins 0 and 1...
2132 "noise" sound played when flag is picked up...
2133 "noise1" sound played when flag is returned by a teammate...
2134 "noise2" sound played when flag is captured...
2135 "noise3" sound played when flag is lost in the field and respawns itself...
2136 "noise4" sound played when flag is dropped by a player...
2137 "noise5" sound played when flag touches the ground... */
2138 void spawnfunc_item_flag_team1()
2140 if(!g_ctf) { remove(self); return; }
2142 ctf_FlagSetup(1, self); // 1 = red
2145 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2146 CTF flag for team two (Blue).
2148 "angle" Angle the flag will point (minus 90 degrees)...
2149 "model" model to use, note this needs red and blue as skins 0 and 1...
2150 "noise" sound played when flag is picked up...
2151 "noise1" sound played when flag is returned by a teammate...
2152 "noise2" sound played when flag is captured...
2153 "noise3" sound played when flag is lost in the field and respawns itself...
2154 "noise4" sound played when flag is dropped by a player...
2155 "noise5" sound played when flag touches the ground... */
2156 void spawnfunc_item_flag_team2()
2158 if(!g_ctf) { remove(self); return; }
2160 ctf_FlagSetup(0, self); // the 0 is misleading, but -- 0 = blue.
2163 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2164 CTF flag for team three (Yellow).
2166 "angle" Angle the flag will point (minus 90 degrees)...
2167 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2168 "noise" sound played when flag is picked up...
2169 "noise1" sound played when flag is returned by a teammate...
2170 "noise2" sound played when flag is captured...
2171 "noise3" sound played when flag is lost in the field and respawns itself...
2172 "noise4" sound played when flag is dropped by a player...
2173 "noise5" sound played when flag touches the ground... */
2174 void spawnfunc_item_flag_team3()
2176 if(!g_ctf) { remove(self); return; }
2178 ctf_FlagSetup(2, self); // 2 = yellow?
2181 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2182 CTF flag for team two (Pink).
2184 "angle" Angle the flag will point (minus 90 degrees)...
2185 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2186 "noise" sound played when flag is picked up...
2187 "noise1" sound played when flag is returned by a teammate...
2188 "noise2" sound played when flag is captured...
2189 "noise3" sound played when flag is lost in the field and respawns itself...
2190 "noise4" sound played when flag is dropped by a player...
2191 "noise5" sound played when flag touches the ground... */
2192 void spawnfunc_item_flag_team4()
2194 if(!g_ctf) { remove(self); return; }
2196 ctf_FlagSetup(3, self); // 3 = pink?
2199 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2200 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2201 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.
2203 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2204 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2205 void spawnfunc_ctf_team()
2207 if(!g_ctf) { remove(self); return; }
2209 self.classname = "ctf_team";
2210 self.team = self.cnt + 1;
2213 // compatibility for quake maps
2214 void spawnfunc_team_CTF_redflag() { spawnfunc_item_flag_team1(); }
2215 void spawnfunc_team_CTF_blueflag() { spawnfunc_item_flag_team2(); }
2216 void spawnfunc_team_CTF_redplayer() { spawnfunc_info_player_team1(); }
2217 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2(); }
2218 void spawnfunc_team_CTF_redspawn() { spawnfunc_info_player_team1(); }
2219 void spawnfunc_team_CTF_bluespawn() { spawnfunc_info_player_team2(); }
2227 void ctf_ScoreRules(float teams)
2229 CheckAllowedTeams(world);
2230 ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, TRUE);
2231 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2232 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2233 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2234 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2235 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2236 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2237 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2238 ScoreRules_basics_end();
2241 // code from here on is just to support maps that don't have flag and team entities
2242 void ctf_SpawnTeam (string teamname, float teamcolor)
2247 self.classname = "ctf_team";
2248 self.netname = teamname;
2249 self.cnt = teamcolor;
2251 spawnfunc_ctf_team();
2256 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2258 ctf_teams = autocvar_g_ctf_teams_override;
2260 ctf_teams = autocvar_g_ctf_teams;
2264 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2265 switch(tmp_entity.team)
2267 case NUM_TEAM_3: ++ctf_teams;
2268 case NUM_TEAM_4: ++ctf_teams;
2271 ctf_teams = bound(2, ctf_teams, 4);
2273 // if no teams are found, spawn defaults
2274 if(find(world, classname, "ctf_team") == world)
2276 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2277 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2278 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2281 ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2283 ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2287 ret_float = ctf_teams;
2288 ctf_ScoreRules(ctf_teams);
2291 void ctf_Initialize()
2293 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2295 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2296 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2297 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2299 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2303 MUTATOR_DEFINITION(gamemode_ctf)
2305 MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2306 MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2307 MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2308 MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2309 MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2310 MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2311 MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2312 MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2313 MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2314 MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2315 MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2316 MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2317 MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2318 MUTATOR_HOOK(HavocBot_ChooseRule, ctf_BotRoles, CBC_ORDER_ANY);
2319 MUTATOR_HOOK(GetCvars, ctf_GetCvars, CBC_ORDER_ANY);
2320 MUTATOR_HOOK(GetTeamCount, ctf_GetTeamCount, CBC_ORDER_ANY);
2324 if(time > 1) // game loads at time 1
2325 error("This is a game type and it cannot be added at runtime.");
2329 MUTATOR_ONROLLBACK_OR_REMOVE
2331 // we actually cannot roll back ctf_Initialize here
2332 // BUT: we don't need to! If this gets called, adding always
2338 print("This is a game type and it cannot be removed at runtime.");