1 #include "gamemode_ctf.qh"
6 #include "../../common/vehicles/all.qh"
7 #include "../teamplay.qh"
10 #include "../../warpzonelib/common.qh"
12 void ctf_FakeTimeLimit(entity e, float t)
15 WriteByte(MSG_ONE, 3); // svc_updatestat
16 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
18 WriteCoord(MSG_ONE, autocvar_timelimit);
20 WriteCoord(MSG_ONE, (t + 1) / 60);
23 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
25 if(autocvar_sv_eventlog)
26 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != world) ? ftos(actor.playerid) : "")));
27 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
30 void ctf_CaptureRecord(entity flag, entity player)
32 float cap_record = ctf_captimerecord;
33 float cap_time = (time - flag.ctf_pickuptime);
34 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
37 if(ctf_oneflag) { Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname); }
38 else if(!ctf_captimerecord) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_TIME_), player.netname, (cap_time * 100)); }
39 else if(cap_time < cap_record) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_BROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
40 else { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_UNBROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
42 // write that shit in the database
43 if(!ctf_oneflag) // but not in 1-flag mode
44 if((!ctf_captimerecord) || (cap_time < cap_record))
46 ctf_captimerecord = cap_time;
47 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
48 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
49 write_recordmarker(player, (time - cap_time), cap_time);
53 void ctf_FlagcarrierWaypoints(entity player)
55 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
56 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON) * 2);
57 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
58 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
61 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
63 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
64 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
65 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
66 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
69 if(current_height) // make sure we can actually do this arcing path
71 targpos = (to + ('0 0 1' * current_height));
72 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
73 if(trace_fraction < 1)
75 //print("normal arc line failed, trying to find new pos...");
76 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
77 targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
78 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
79 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
80 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
83 else { targpos = to; }
85 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
87 vector desired_direction = normalize(targpos - from);
88 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
89 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
92 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
94 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
96 // directional tracing only
98 makevectors(passer_angle);
100 // find the closest point on the enemy to the center of the attack
101 float h; // hypotenuse, which is the distance between attacker to head
102 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
104 h = vlen(head_center - passer_center);
105 a = h * (normalize(head_center - passer_center) * v_forward);
107 vector nearest_on_line = (passer_center + a * v_forward);
108 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
110 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
111 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
113 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
118 else { return true; }
122 // =======================
123 // CaptureShield Functions
124 // =======================
126 bool ctf_CaptureShield_CheckStatus(entity p)
128 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
130 int players_worseeq, players_total;
132 if(ctf_captureshield_max_ratio <= 0)
135 s = PlayerScore_Add(p, SP_CTF_CAPS, 0);
136 s2 = PlayerScore_Add(p, SP_CTF_PICKUPS, 0);
137 s3 = PlayerScore_Add(p, SP_CTF_RETURNS, 0);
138 s4 = PlayerScore_Add(p, SP_CTF_FCKILLS, 0);
140 sr = ((s - s2) + (s3 + s4));
142 if(sr >= -ctf_captureshield_min_negscore)
145 players_total = players_worseeq = 0;
150 se = PlayerScore_Add(e, SP_CTF_CAPS, 0);
151 se2 = PlayerScore_Add(e, SP_CTF_PICKUPS, 0);
152 se3 = PlayerScore_Add(e, SP_CTF_RETURNS, 0);
153 se4 = PlayerScore_Add(e, SP_CTF_FCKILLS, 0);
155 ser = ((se - se2) + (se3 + se4));
162 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
163 // use this rule here
165 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
171 void ctf_CaptureShield_Update(entity player, bool wanted_status)
173 bool updated_status = ctf_CaptureShield_CheckStatus(player);
174 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
176 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
177 player.ctf_captureshielded = updated_status;
181 bool ctf_CaptureShield_Customize()
183 if(!other.ctf_captureshielded) { return false; }
184 if(CTF_SAMETEAM(self, other)) { return false; }
189 void ctf_CaptureShield_Touch()
191 if(!other.ctf_captureshielded) { return; }
192 if(CTF_SAMETEAM(self, other)) { return; }
194 vector mymid = (self.absmin + self.absmax) * 0.5;
195 vector othermid = (other.absmin + other.absmax) * 0.5;
197 Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
198 if(IS_REAL_CLIENT(other)) { Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
201 void ctf_CaptureShield_Spawn(entity flag)
203 entity shield = spawn();
206 shield.team = self.team;
207 shield.touch = ctf_CaptureShield_Touch;
208 shield.customizeentityforclient = ctf_CaptureShield_Customize;
209 shield.classname = "ctf_captureshield";
210 shield.effects = EF_ADDITIVE;
211 shield.movetype = MOVETYPE_NOCLIP;
212 shield.solid = SOLID_TRIGGER;
213 shield.avelocity = '7 0 11';
216 setorigin(shield, self.origin);
217 setmodel(shield, MDL_CTF_SHIELD);
218 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
222 // ====================
223 // Drop/Pass/Throw Code
224 // ====================
226 void ctf_Handle_Drop(entity flag, entity player, int droptype)
229 player = (player ? player : flag.pass_sender);
232 flag.movetype = MOVETYPE_TOSS;
233 flag.takedamage = DAMAGE_YES;
234 flag.angles = '0 0 0';
235 flag.health = flag.max_flag_health;
236 flag.ctf_droptime = time;
237 flag.ctf_dropper = player;
238 flag.ctf_status = FLAG_DROPPED;
240 // messages and sounds
241 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_LOST_) : INFO_CTF_LOST_NEUTRAL), player.netname);
242 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
243 ctf_EventLog("dropped", player.team, player);
246 PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);
247 PlayerScore_Add(player, SP_CTF_DROPS, 1);
250 if(autocvar_g_ctf_flag_dropped_waypoint) {
251 entity wp = WaypointSprite_Spawn(WP_FlagDropped, 0, 0, flag, FLAG_WAYPOINT_OFFSET, world, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, true, RADARICON_FLAG);
252 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
255 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
257 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
258 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
261 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
263 if(droptype == DROP_PASS)
265 flag.pass_distance = 0;
266 flag.pass_sender = world;
267 flag.pass_target = world;
271 void ctf_Handle_Retrieve(entity flag, entity player)
273 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
274 entity sender = flag.pass_sender;
276 // transfer flag to player
278 flag.owner.flagcarried = flag;
283 setattachment(flag, player.vehicle, "");
284 setorigin(flag, VEHICLE_FLAG_OFFSET);
285 flag.scale = VEHICLE_FLAG_SCALE;
289 setattachment(flag, player, "");
290 setorigin(flag, FLAG_CARRY_OFFSET);
292 flag.movetype = MOVETYPE_NONE;
293 flag.takedamage = DAMAGE_NO;
294 flag.solid = SOLID_NOT;
295 flag.angles = '0 0 0';
296 flag.ctf_status = FLAG_CARRY;
298 // messages and sounds
299 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
300 ctf_EventLog("receive", flag.team, player);
302 FOR_EACH_REALPLAYER(tmp_player)
304 if(tmp_player == sender)
305 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_SENT_) : CENTER_CTF_PASS_SENT_NEUTRAL), player.netname);
306 else if(tmp_player == player)
307 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_RECEIVED_) : CENTER_CTF_PASS_RECEIVED_NEUTRAL), sender.netname);
308 else if(SAME_TEAM(tmp_player, sender))
309 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_OTHER_) : CENTER_CTF_PASS_OTHER_NEUTRAL), sender.netname, player.netname);
312 // create new waypoint
313 ctf_FlagcarrierWaypoints(player);
315 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
316 player.throw_antispam = sender.throw_antispam;
318 flag.pass_distance = 0;
319 flag.pass_sender = world;
320 flag.pass_target = world;
323 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
325 entity flag = player.flagcarried;
326 vector targ_origin, flag_velocity;
328 if(!flag) { return; }
329 if((droptype == DROP_PASS) && !receiver) { return; }
331 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
334 setattachment(flag, world, "");
335 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
336 flag.owner.flagcarried = world;
338 flag.solid = SOLID_TRIGGER;
339 flag.ctf_dropper = player;
340 flag.ctf_droptime = time;
342 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
349 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
350 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
351 WarpZone_RefSys_Copy(flag, receiver);
352 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
353 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
355 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
356 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
359 flag.movetype = MOVETYPE_FLY;
360 flag.takedamage = DAMAGE_NO;
361 flag.pass_sender = player;
362 flag.pass_target = receiver;
363 flag.ctf_status = FLAG_PASSING;
366 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
367 WarpZone_TrailParticles(world, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
368 ctf_EventLog("pass", flag.team, player);
374 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'));
376 flag_velocity = (('0 0 1' * autocvar_g_ctf_throw_velocity_up) + ((v_forward * autocvar_g_ctf_throw_velocity_forward) * ((player.items & ITEM_Strength.m_itemid) ? autocvar_g_ctf_throw_strengthmultiplier : 1)));
377 flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, false);
378 ctf_Handle_Drop(flag, player, droptype);
384 flag.velocity = '0 0 0'; // do nothing
391 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);
392 ctf_Handle_Drop(flag, player, droptype);
397 // kill old waypointsprite
398 WaypointSprite_Ping(player.wps_flagcarrier);
399 WaypointSprite_Kill(player.wps_flagcarrier);
401 if(player.wps_enemyflagcarrier)
402 WaypointSprite_Kill(player.wps_enemyflagcarrier);
405 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
413 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
415 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
416 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
417 entity player_team_flag = world, tmp_entity;
418 float old_time, new_time;
420 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
421 if(CTF_DIFFTEAM(player, flag)) { return; }
424 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
425 if(SAME_TEAM(tmp_entity, player))
427 player_team_flag = tmp_entity;
431 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
433 player.throw_prevtime = time;
434 player.throw_count = 0;
436 // messages and sounds
437 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((enemy_flag.team) ? APP_TEAM_ENT_4(enemy_flag, CENTER_CTF_CAPTURE_) : CENTER_CTF_CAPTURE_NEUTRAL));
438 ctf_CaptureRecord(enemy_flag, player);
439 _sound(player, CH_TRIGGER, ((ctf_oneflag) ? player_team_flag.snd_flag_capture : ((DIFF_TEAM(player, flag)) ? enemy_flag.snd_flag_capture : flag.snd_flag_capture)), VOL_BASE, ATTEN_NONE);
443 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
444 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
449 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
450 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
452 old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
453 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
454 if(!old_time || new_time < old_time)
455 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
458 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
459 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
462 if(capturetype == CAPTURE_NORMAL)
464 WaypointSprite_Kill(player.wps_flagcarrier);
465 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
467 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
468 { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
472 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
473 ctf_RespawnFlag(enemy_flag);
476 void ctf_Handle_Return(entity flag, entity player)
478 // messages and sounds
479 if(IS_MONSTER(player))
481 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_MONSTER_), player.monster_name);
485 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_RETURN_));
486 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_), player.netname);
488 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
489 ctf_EventLog("return", flag.team, player);
492 if(IS_PLAYER(player))
494 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
495 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
497 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
500 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
504 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
505 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
506 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
510 if(player.flagcarried == flag)
511 WaypointSprite_Kill(player.wps_flagcarrier);
514 ctf_RespawnFlag(flag);
517 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
520 float pickup_dropped_score; // used to calculate dropped pickup score
521 entity tmp_entity; // temporary entity
523 // attach the flag to the player
525 player.flagcarried = flag;
528 setattachment(flag, player.vehicle, "");
529 setorigin(flag, VEHICLE_FLAG_OFFSET);
530 flag.scale = VEHICLE_FLAG_SCALE;
534 setattachment(flag, player, "");
535 setorigin(flag, FLAG_CARRY_OFFSET);
539 flag.movetype = MOVETYPE_NONE;
540 flag.takedamage = DAMAGE_NO;
541 flag.solid = SOLID_NOT;
542 flag.angles = '0 0 0';
543 flag.ctf_status = FLAG_CARRY;
547 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
548 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
552 // messages and sounds
553 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_PICKUP_) : INFO_CTF_PICKUP_NEUTRAL), player.netname);
554 if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
555 if(!flag.team) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL); }
556 else if(CTF_DIFFTEAM(player, flag)) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PICKUP_)); }
557 else { Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_TEAM : CENTER_CTF_PICKUP_TEAM_ENEMY), Team_ColorCode(flag.team)); }
559 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, ((flag.team) ? APP_TEAM_ENT_4(flag, CHOICE_CTF_PICKUP_TEAM_) : CHOICE_CTF_PICKUP_TEAM_NEUTRAL), Team_ColorCode(player.team), player.netname);
562 FOR_EACH_PLAYER(tmp_entity)
563 if(tmp_entity != player)
564 if(DIFF_TEAM(player, tmp_entity))
565 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, CHOICE_CTF_PICKUP_ENEMY_NEUTRAL, Team_ColorCode(player.team), player.netname);
568 FOR_EACH_PLAYER(tmp_entity)
569 if(tmp_entity != player)
570 if(CTF_SAMETEAM(flag, tmp_entity))
571 if(SAME_TEAM(player, tmp_entity))
572 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_PICKUP_TEAM_), Team_ColorCode(player.team), player.netname);
574 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, ((SAME_TEAM(flag, player)) ? CHOICE_CTF_PICKUP_ENEMY_TEAM : CHOICE_CTF_PICKUP_ENEMY), Team_ColorCode(player.team), player.netname);
576 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
579 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
580 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
585 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
586 ctf_EventLog("steal", flag.team, player);
592 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);
593 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);
594 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
595 PlayerTeamScore_AddScore(player, pickup_dropped_score);
596 ctf_EventLog("pickup", flag.team, player);
604 if(pickuptype == PICKUP_BASE)
606 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
607 if((player.speedrunning) && (ctf_captimerecord))
608 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
612 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
615 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
616 ctf_FlagcarrierWaypoints(player);
617 WaypointSprite_Ping(player.wps_flagcarrier);
621 // ===================
622 // Main Flag Functions
623 // ===================
625 void ctf_CheckFlagReturn(entity flag, int returntype)
627 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
629 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
631 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
635 case RETURN_DROPPED: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DROPPED_) : INFO_CTF_FLAGRETURN_DROPPED_NEUTRAL)); break;
636 case RETURN_DAMAGE: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DAMAGED_) : INFO_CTF_FLAGRETURN_DAMAGED_NEUTRAL)); break;
637 case RETURN_SPEEDRUN: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_SPEEDRUN_) : INFO_CTF_FLAGRETURN_SPEEDRUN_NEUTRAL), ctf_captimerecord); break;
638 case RETURN_NEEDKILL: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_NEEDKILL_) : INFO_CTF_FLAGRETURN_NEEDKILL_NEUTRAL)); break;
642 { Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_TIMEOUT_) : INFO_CTF_FLAGRETURN_TIMEOUT_NEUTRAL)); break; }
644 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
645 ctf_EventLog("returned", flag.team, world);
646 ctf_RespawnFlag(flag);
651 bool ctf_Stalemate_Customize()
653 // make spectators see what the player would see
655 e = WaypointSprite_getviewentity(other);
656 wp_owner = self.owner;
659 if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
660 if(SAME_TEAM(wp_owner, e)) { return false; }
661 if(!IS_PLAYER(e)) { return false; }
666 void ctf_CheckStalemate(void)
669 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
672 entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
674 // build list of stale flags
675 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
677 if(autocvar_g_ctf_stalemate)
678 if(tmp_entity.ctf_status != FLAG_BASE)
679 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
681 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
682 ctf_staleflaglist = tmp_entity;
684 switch(tmp_entity.team)
686 case NUM_TEAM_1: ++stale_red_flags; break;
687 case NUM_TEAM_2: ++stale_blue_flags; break;
688 case NUM_TEAM_3: ++stale_yellow_flags; break;
689 case NUM_TEAM_4: ++stale_pink_flags; break;
690 default: ++stale_neutral_flags; break;
696 stale_flags = (stale_neutral_flags >= 1);
698 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
700 if(ctf_oneflag && stale_flags == 1)
701 ctf_stalemate = true;
702 else if(stale_flags >= 2)
703 ctf_stalemate = true;
704 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
705 { ctf_stalemate = false; wpforenemy_announced = false; }
706 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
707 { ctf_stalemate = false; wpforenemy_announced = false; }
709 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
712 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
714 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
716 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, tmp_entity.owner, FLAG_WAYPOINT_OFFSET, world, 0, tmp_entity.owner, wps_enemyflagcarrier, true, RADARICON_FLAG);
717 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
718 tmp_entity.owner.wps_enemyflagcarrier.customizeentityforclient = ctf_Stalemate_Customize;
722 if (!wpforenemy_announced)
724 FOR_EACH_REALPLAYER(tmp_entity)
725 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CENTER, ((tmp_entity.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER));
727 wpforenemy_announced = true;
732 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
734 if(ITEM_DAMAGE_NEEDKILL(deathtype))
736 if(autocvar_g_ctf_flag_return_damage_delay)
738 self.ctf_flagdamaged = true;
743 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
747 if(autocvar_g_ctf_flag_return_damage)
749 // reduce health and check if it should be returned
750 self.health = self.health - damage;
751 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
761 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
764 if(self == ctf_worldflaglist) // only for the first flag
765 FOR_EACH_CLIENT(tmp_entity)
766 ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
769 if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
770 LOG_TRACE("wtf the flag got squashed?\n");
771 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
772 if(!trace_startsolid || self.noalign) // can we resize it without getting stuck?
773 setsize(self, FLAG_MIN, FLAG_MAX); }
775 switch(self.ctf_status) // reset flag angles in case warpzones adjust it
779 self.angles = '0 0 0';
787 switch(self.ctf_status)
791 if(autocvar_g_ctf_dropped_capture_radius)
793 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
794 if(tmp_entity.ctf_status == FLAG_DROPPED)
795 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
796 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
797 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
804 if(autocvar_g_ctf_flag_dropped_floatinwater)
806 vector midpoint = ((self.absmin + self.absmax) * 0.5);
807 if(pointcontents(midpoint) == CONTENT_WATER)
809 self.velocity = self.velocity * 0.5;
811 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
812 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
814 { self.movetype = MOVETYPE_FLY; }
816 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
818 if(autocvar_g_ctf_flag_return_dropped)
820 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
823 ctf_CheckFlagReturn(self, RETURN_DROPPED);
827 if(self.ctf_flagdamaged)
829 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
830 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
833 else if(autocvar_g_ctf_flag_return_time)
835 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
836 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
844 if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
847 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
850 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
854 if(autocvar_g_ctf_stalemate)
856 if(time >= wpforenemy_nextthink)
858 ctf_CheckStalemate();
859 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
862 if(CTF_SAMETEAM(self, self.owner) && self.team)
864 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
865 ctf_Handle_Throw(self.owner, world, DROP_THROW);
866 else if(vlen(self.owner.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_carried_radius)
867 ctf_Handle_Return(self, self.owner);
874 vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
875 targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
876 WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
878 if((self.pass_target == world)
879 || (self.pass_target.deadflag != DEAD_NO)
880 || (self.pass_target.flagcarried)
881 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
882 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
883 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
885 // give up, pass failed
886 ctf_Handle_Drop(self, world, DROP_PASS);
890 // still a viable target, go for it
891 ctf_CalculatePassVelocity(self, targ_origin, self.origin, true);
896 default: // this should never happen
898 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?\n");
906 if(gameover) { return; }
907 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
909 entity toucher = other, tmp_entity;
910 bool is_not_monster = (!IS_MONSTER(toucher)), num_perteam = 0;
912 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
913 if(ITEM_TOUCH_NEEDKILL())
915 if(!autocvar_g_ctf_flag_return_damage_delay)
918 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
920 if(!self.ctf_flagdamaged) { return; }
923 FOR_EACH_PLAYER(tmp_entity) if(SAME_TEAM(toucher, tmp_entity)) { ++num_perteam; }
925 // special touch behaviors
926 if(toucher.frozen) { return; }
927 else if(IS_VEHICLE(toucher))
929 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
930 toucher = toucher.owner; // the player is actually the vehicle owner, not other
932 return; // do nothing
934 else if(IS_MONSTER(toucher))
936 if(!autocvar_g_ctf_allow_monster_touch)
937 return; // do nothing
939 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
941 if(time > self.wait) // if we haven't in a while, play a sound/effect
943 Send_Effect_(self.toucheffect, self.origin, '0 0 0', 1);
944 _sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTEN_NORM);
945 self.wait = time + FLAG_TOUCHRATE;
949 else if(toucher.deadflag != DEAD_NO) { return; }
951 switch(self.ctf_status)
957 if(CTF_SAMETEAM(toucher, self) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
958 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
959 else if(!self.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
960 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the neutral flag
962 else if(CTF_SAMETEAM(toucher, self) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, self) && is_not_monster)
963 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
964 else if(CTF_DIFFTEAM(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
965 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
971 if(CTF_SAMETEAM(toucher, self) && (autocvar_g_ctf_flag_return || num_perteam <= 1) && self.team) // automatically return if there's only 1 player on the team
972 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
973 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
974 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
980 LOG_TRACE("Someone touched a flag even though it was being carried?\n");
986 if((IS_PLAYER(toucher)) && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
988 if(DIFF_TEAM(toucher, self.pass_sender))
989 ctf_Handle_Return(self, toucher);
991 ctf_Handle_Retrieve(self, toucher);
999 void ctf_RespawnFlag(entity flag)
1001 // check for flag respawn being called twice in a row
1002 if(flag.last_respawn > time - 0.5)
1003 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1005 flag.last_respawn = time;
1007 // reset the player (if there is one)
1008 if((flag.owner) && (flag.owner.flagcarried == flag))
1010 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1011 WaypointSprite_Kill(flag.wps_flagcarrier);
1013 flag.owner.flagcarried = world;
1015 if(flag.speedrunning)
1016 ctf_FakeTimeLimit(flag.owner, -1);
1019 if((flag.owner) && (flag.owner.vehicle))
1020 flag.scale = FLAG_SCALE;
1022 if(flag.ctf_status == FLAG_DROPPED)
1023 { WaypointSprite_Kill(flag.wps_flagdropped); }
1026 setattachment(flag, world, "");
1027 setorigin(flag, flag.ctf_spawnorigin);
1029 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
1030 flag.takedamage = DAMAGE_NO;
1031 flag.health = flag.max_flag_health;
1032 flag.solid = SOLID_TRIGGER;
1033 flag.velocity = '0 0 0';
1034 flag.angles = flag.mangle;
1035 flag.flags = FL_ITEM | FL_NOTARGET;
1037 flag.ctf_status = FLAG_BASE;
1039 flag.pass_distance = 0;
1040 flag.pass_sender = world;
1041 flag.pass_target = world;
1042 flag.ctf_dropper = world;
1043 flag.ctf_pickuptime = 0;
1044 flag.ctf_droptime = 0;
1045 flag.ctf_flagdamaged = 0;
1047 ctf_CheckStalemate();
1053 if(IS_PLAYER(self.owner))
1054 ctf_Handle_Throw(self.owner, world, DROP_RESET);
1056 ctf_RespawnFlag(self);
1059 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
1062 waypoint_spawnforitem_force(self, self.origin);
1063 self.nearestwaypointtimeout = 0; // activate waypointing again
1064 self.bot_basewaypoint = self.nearestwaypoint;
1070 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1071 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1072 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1073 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1074 default: basename = WP_FlagBaseNeutral; break;
1077 entity wp = WaypointSprite_SpawnFixed(basename, self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG);
1078 wp.colormod = ((self.team) ? Team_ColorRGB(self.team) : '1 1 1');
1079 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, ((self.team) ? colormapPaletteColor(self.team - 1, false) : '1 1 1'));
1081 // captureshield setup
1082 ctf_CaptureShield_Spawn(self);
1085 void set_flag_string(entity flag, .string field, string value, string teamname)
1087 if(flag.(field) == "")
1088 flag.(field) = strzone(sprintf(value,teamname));
1091 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1094 setself(flag); // for later usage with droptofloor()
1097 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1098 ctf_worldflaglist = flag;
1100 setattachment(flag, world, "");
1102 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1103 flag.team = teamnumber;
1104 flag.classname = "item_flag_team";
1105 flag.target = "###item###"; // wut?
1106 flag.flags = FL_ITEM | FL_NOTARGET;
1107 flag.solid = SOLID_TRIGGER;
1108 flag.takedamage = DAMAGE_NO;
1109 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1110 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1111 flag.health = flag.max_flag_health;
1112 flag.event_damage = ctf_FlagDamage;
1113 flag.pushable = true;
1114 flag.teleportable = TELEPORT_NORMAL;
1115 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP;
1116 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1117 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1118 flag.velocity = '0 0 0';
1119 flag.mangle = flag.angles;
1120 flag.reset = ctf_Reset;
1121 flag.touch = ctf_FlagTouch;
1122 flag.think = ctf_FlagThink;
1123 flag.nextthink = time + FLAG_THINKRATE;
1124 flag.ctf_status = FLAG_BASE;
1126 string teamname = Static_Team_ColorName_Lower(teamnumber);
1128 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1129 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1130 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1131 set_flag_string(flag, toucheffect, "%sflag_touch", teamname);
1132 set_flag_string(flag, passeffect, "%s_pass", teamname);
1133 set_flag_string(flag, capeffect, "%s_cap", teamname);
1136 flag.snd_flag_taken = SND(CTF_TAKEN(teamnumber));
1137 flag.snd_flag_returned = SND(CTF_RETURNED(teamnumber));
1138 flag.snd_flag_capture = SND(CTF_CAPTURE(teamnumber));
1139 flag.snd_flag_dropped = SND(CTF_DROPPED(teamnumber));
1140 if (flag.snd_flag_respawn == "") flag.snd_flag_respawn = SND(CTF_RESPAWN); // if there is ever a team-based sound for this, update the code to match.
1141 precache_sound(flag.snd_flag_respawn);
1142 if (flag.snd_flag_touch == "") flag.snd_flag_touch = SND(CTF_TOUCH); // again has no team-based sound
1143 precache_sound(flag.snd_flag_touch);
1144 if (flag.snd_flag_pass == "") flag.snd_flag_pass = SND(CTF_PASS); // same story here
1145 precache_sound(flag.snd_flag_pass);
1148 precache_model(flag.model);
1151 _setmodel(flag, flag.model); // precision set below
1152 setsize(flag, FLAG_MIN, FLAG_MAX);
1153 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1155 if(autocvar_g_ctf_flag_glowtrails)
1159 case NUM_TEAM_1: flag.glow_color = 251; break;
1160 case NUM_TEAM_2: flag.glow_color = 210; break;
1161 case NUM_TEAM_3: flag.glow_color = 110; break;
1162 case NUM_TEAM_4: flag.glow_color = 145; break;
1163 default: flag.glow_color = 254; break;
1165 flag.glow_size = 25;
1166 flag.glow_trail = 1;
1169 flag.effects |= EF_LOWPRECISION;
1170 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1171 if(autocvar_g_ctf_dynamiclights)
1175 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1176 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1177 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1178 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1179 default: flag.effects |= EF_DIMLIGHT; break;
1184 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1186 flag.dropped_origin = flag.origin;
1187 flag.noalign = true;
1188 flag.movetype = MOVETYPE_NONE;
1190 else // drop to floor, automatically find a platform and set that as spawn origin
1192 flag.noalign = false;
1195 flag.movetype = MOVETYPE_TOSS;
1198 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1206 // NOTE: LEGACY CODE, needs to be re-written!
1208 void havocbot_calculate_middlepoint()
1212 vector fo = '0 0 0';
1215 f = ctf_worldflaglist;
1220 f = f.ctf_worldflagnext;
1224 havocbot_ctf_middlepoint = s * (1.0 / n);
1225 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1229 entity havocbot_ctf_find_flag(entity bot)
1232 f = ctf_worldflaglist;
1235 if (CTF_SAMETEAM(bot, f))
1237 f = f.ctf_worldflagnext;
1242 entity havocbot_ctf_find_enemy_flag(entity bot)
1245 f = ctf_worldflaglist;
1250 if(CTF_DIFFTEAM(bot, f))
1257 else if(!bot.flagcarried)
1261 else if (CTF_DIFFTEAM(bot, f))
1263 f = f.ctf_worldflagnext;
1268 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1276 FOR_EACH_PLAYER(head)
1278 if(DIFF_TEAM(head, bot) || head.deadflag != DEAD_NO || head == bot)
1281 if(vlen(head.origin - org) < tc_radius)
1288 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1291 head = ctf_worldflaglist;
1294 if (CTF_SAMETEAM(self, head))
1296 head = head.ctf_worldflagnext;
1299 navigation_routerating(head, ratingscale, 10000);
1302 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1305 head = ctf_worldflaglist;
1308 if (CTF_SAMETEAM(self, head))
1310 head = head.ctf_worldflagnext;
1315 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1318 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1321 head = ctf_worldflaglist;
1326 if(CTF_DIFFTEAM(self, head))
1330 if(self.flagcarried)
1333 else if(!self.flagcarried)
1337 else if(CTF_DIFFTEAM(self, head))
1339 head = head.ctf_worldflagnext;
1342 navigation_routerating(head, ratingscale, 10000);
1345 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1347 if (!bot_waypoints_for_items)
1349 havocbot_goalrating_ctf_enemyflag(ratingscale);
1355 head = havocbot_ctf_find_enemy_flag(self);
1360 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1363 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1367 mf = havocbot_ctf_find_flag(self);
1369 if(mf.ctf_status == FLAG_BASE)
1373 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1376 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1379 head = ctf_worldflaglist;
1382 // flag is out in the field
1383 if(head.ctf_status != FLAG_BASE)
1384 if(head.tag_entity==world) // dropped
1388 if(vlen(org-head.origin)<df_radius)
1389 navigation_routerating(head, ratingscale, 10000);
1392 navigation_routerating(head, ratingscale, 10000);
1395 head = head.ctf_worldflagnext;
1399 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1403 head = findchainfloat(bot_pickup, true);
1406 // gather health and armor only
1408 if (head.health || head.armorvalue)
1409 if (vlen(head.origin - org) < sradius)
1411 // get the value of the item
1412 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1414 navigation_routerating(head, t * ratingscale, 500);
1420 void havocbot_ctf_reset_role(entity bot)
1422 float cdefense, cmiddle, coffense;
1423 entity mf, ef, head;
1426 if(bot.deadflag != DEAD_NO)
1429 if(vlen(havocbot_ctf_middlepoint)==0)
1430 havocbot_calculate_middlepoint();
1433 if (bot.flagcarried)
1435 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1439 mf = havocbot_ctf_find_flag(bot);
1440 ef = havocbot_ctf_find_enemy_flag(bot);
1442 // Retrieve stolen flag
1443 if(mf.ctf_status!=FLAG_BASE)
1445 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1449 // If enemy flag is taken go to the middle to intercept pursuers
1450 if(ef.ctf_status!=FLAG_BASE)
1452 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1456 // if there is only me on the team switch to offense
1458 FOR_EACH_PLAYER(head)
1459 if(SAME_TEAM(head, bot))
1464 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1468 // Evaluate best position to take
1469 // Count mates on middle position
1470 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1472 // Count mates on defense position
1473 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1475 // Count mates on offense position
1476 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1478 if(cdefense<=coffense)
1479 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1480 else if(coffense<=cmiddle)
1481 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1483 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1486 void havocbot_role_ctf_carrier()
1488 if(self.deadflag != DEAD_NO)
1490 havocbot_ctf_reset_role(self);
1494 if (self.flagcarried == world)
1496 havocbot_ctf_reset_role(self);
1500 if (self.bot_strategytime < time)
1502 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1504 navigation_goalrating_start();
1506 havocbot_goalrating_ctf_enemybase(50000);
1508 havocbot_goalrating_ctf_ourbase(50000);
1511 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1513 navigation_goalrating_end();
1515 if (self.navigation_hasgoals)
1516 self.havocbot_cantfindflag = time + 10;
1517 else if (time > self.havocbot_cantfindflag)
1519 // Can't navigate to my own base, suicide!
1520 // TODO: drop it and wander around
1521 Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1527 void havocbot_role_ctf_escort()
1531 if(self.deadflag != DEAD_NO)
1533 havocbot_ctf_reset_role(self);
1537 if (self.flagcarried)
1539 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1543 // If enemy flag is back on the base switch to previous role
1544 ef = havocbot_ctf_find_enemy_flag(self);
1545 if(ef.ctf_status==FLAG_BASE)
1547 self.havocbot_role = self.havocbot_previous_role;
1548 self.havocbot_role_timeout = 0;
1552 // If the flag carrier reached the base switch to defense
1553 mf = havocbot_ctf_find_flag(self);
1554 if(mf.ctf_status!=FLAG_BASE)
1555 if(vlen(ef.origin - mf.dropped_origin) < 300)
1557 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1561 // Set the role timeout if necessary
1562 if (!self.havocbot_role_timeout)
1564 self.havocbot_role_timeout = time + random() * 30 + 60;
1567 // If nothing happened just switch to previous role
1568 if (time > self.havocbot_role_timeout)
1570 self.havocbot_role = self.havocbot_previous_role;
1571 self.havocbot_role_timeout = 0;
1575 // Chase the flag carrier
1576 if (self.bot_strategytime < time)
1578 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1579 navigation_goalrating_start();
1580 havocbot_goalrating_ctf_enemyflag(30000);
1581 havocbot_goalrating_ctf_ourstolenflag(40000);
1582 havocbot_goalrating_items(10000, self.origin, 10000);
1583 navigation_goalrating_end();
1587 void havocbot_role_ctf_offense()
1592 if(self.deadflag != DEAD_NO)
1594 havocbot_ctf_reset_role(self);
1598 if (self.flagcarried)
1600 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1605 mf = havocbot_ctf_find_flag(self);
1606 ef = havocbot_ctf_find_enemy_flag(self);
1609 if(mf.ctf_status!=FLAG_BASE)
1612 pos = mf.tag_entity.origin;
1616 // Try to get it if closer than the enemy base
1617 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1619 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1624 // Escort flag carrier
1625 if(ef.ctf_status!=FLAG_BASE)
1628 pos = ef.tag_entity.origin;
1632 if(vlen(pos-mf.dropped_origin)>700)
1634 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1639 // About to fail, switch to middlefield
1642 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1646 // Set the role timeout if necessary
1647 if (!self.havocbot_role_timeout)
1648 self.havocbot_role_timeout = time + 120;
1650 if (time > self.havocbot_role_timeout)
1652 havocbot_ctf_reset_role(self);
1656 if (self.bot_strategytime < time)
1658 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1659 navigation_goalrating_start();
1660 havocbot_goalrating_ctf_ourstolenflag(50000);
1661 havocbot_goalrating_ctf_enemybase(20000);
1662 havocbot_goalrating_items(5000, self.origin, 1000);
1663 havocbot_goalrating_items(1000, self.origin, 10000);
1664 navigation_goalrating_end();
1668 // Retriever (temporary role):
1669 void havocbot_role_ctf_retriever()
1673 if(self.deadflag != DEAD_NO)
1675 havocbot_ctf_reset_role(self);
1679 if (self.flagcarried)
1681 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1685 // If flag is back on the base switch to previous role
1686 mf = havocbot_ctf_find_flag(self);
1687 if(mf.ctf_status==FLAG_BASE)
1689 havocbot_ctf_reset_role(self);
1693 if (!self.havocbot_role_timeout)
1694 self.havocbot_role_timeout = time + 20;
1696 if (time > self.havocbot_role_timeout)
1698 havocbot_ctf_reset_role(self);
1702 if (self.bot_strategytime < time)
1707 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1708 navigation_goalrating_start();
1709 havocbot_goalrating_ctf_ourstolenflag(50000);
1710 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1711 havocbot_goalrating_ctf_enemybase(30000);
1712 havocbot_goalrating_items(500, self.origin, rt_radius);
1713 navigation_goalrating_end();
1717 void havocbot_role_ctf_middle()
1721 if(self.deadflag != DEAD_NO)
1723 havocbot_ctf_reset_role(self);
1727 if (self.flagcarried)
1729 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1733 mf = havocbot_ctf_find_flag(self);
1734 if(mf.ctf_status!=FLAG_BASE)
1736 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1740 if (!self.havocbot_role_timeout)
1741 self.havocbot_role_timeout = time + 10;
1743 if (time > self.havocbot_role_timeout)
1745 havocbot_ctf_reset_role(self);
1749 if (self.bot_strategytime < time)
1753 org = havocbot_ctf_middlepoint;
1754 org.z = self.origin.z;
1756 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1757 navigation_goalrating_start();
1758 havocbot_goalrating_ctf_ourstolenflag(50000);
1759 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1760 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1761 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1762 havocbot_goalrating_items(2500, self.origin, 10000);
1763 havocbot_goalrating_ctf_enemybase(2500);
1764 navigation_goalrating_end();
1768 void havocbot_role_ctf_defense()
1772 if(self.deadflag != DEAD_NO)
1774 havocbot_ctf_reset_role(self);
1778 if (self.flagcarried)
1780 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1784 // If own flag was captured
1785 mf = havocbot_ctf_find_flag(self);
1786 if(mf.ctf_status!=FLAG_BASE)
1788 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1792 if (!self.havocbot_role_timeout)
1793 self.havocbot_role_timeout = time + 30;
1795 if (time > self.havocbot_role_timeout)
1797 havocbot_ctf_reset_role(self);
1800 if (self.bot_strategytime < time)
1805 org = mf.dropped_origin;
1806 mp_radius = havocbot_ctf_middlepoint_radius;
1808 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1809 navigation_goalrating_start();
1811 // if enemies are closer to our base, go there
1812 entity head, closestplayer = world;
1813 float distance, bestdistance = 10000;
1814 FOR_EACH_PLAYER(head)
1816 if(head.deadflag!=DEAD_NO)
1819 distance = vlen(org - head.origin);
1820 if(distance<bestdistance)
1822 closestplayer = head;
1823 bestdistance = distance;
1828 if(DIFF_TEAM(closestplayer, self))
1829 if(vlen(org - self.origin)>1000)
1830 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1831 havocbot_goalrating_ctf_ourbase(30000);
1833 havocbot_goalrating_ctf_ourstolenflag(20000);
1834 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1835 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1836 havocbot_goalrating_items(10000, org, mp_radius);
1837 havocbot_goalrating_items(5000, self.origin, 10000);
1838 navigation_goalrating_end();
1842 void havocbot_role_ctf_setrole(entity bot, int role)
1844 LOG_TRACE(strcat(bot.netname," switched to "));
1847 case HAVOCBOT_CTF_ROLE_CARRIER:
1848 LOG_TRACE("carrier");
1849 bot.havocbot_role = havocbot_role_ctf_carrier;
1850 bot.havocbot_role_timeout = 0;
1851 bot.havocbot_cantfindflag = time + 10;
1852 bot.bot_strategytime = 0;
1854 case HAVOCBOT_CTF_ROLE_DEFENSE:
1855 LOG_TRACE("defense");
1856 bot.havocbot_role = havocbot_role_ctf_defense;
1857 bot.havocbot_role_timeout = 0;
1859 case HAVOCBOT_CTF_ROLE_MIDDLE:
1860 LOG_TRACE("middle");
1861 bot.havocbot_role = havocbot_role_ctf_middle;
1862 bot.havocbot_role_timeout = 0;
1864 case HAVOCBOT_CTF_ROLE_OFFENSE:
1865 LOG_TRACE("offense");
1866 bot.havocbot_role = havocbot_role_ctf_offense;
1867 bot.havocbot_role_timeout = 0;
1869 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1870 LOG_TRACE("retriever");
1871 bot.havocbot_previous_role = bot.havocbot_role;
1872 bot.havocbot_role = havocbot_role_ctf_retriever;
1873 bot.havocbot_role_timeout = time + 10;
1874 bot.bot_strategytime = 0;
1876 case HAVOCBOT_CTF_ROLE_ESCORT:
1877 LOG_TRACE("escort");
1878 bot.havocbot_previous_role = bot.havocbot_role;
1879 bot.havocbot_role = havocbot_role_ctf_escort;
1880 bot.havocbot_role_timeout = time + 30;
1881 bot.bot_strategytime = 0;
1892 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1895 int t = 0, t2 = 0, t3 = 0;
1897 // initially clear items so they can be set as necessary later.
1898 self.ctf_flagstatus &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
1899 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
1900 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
1901 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
1902 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
1903 | CTF_FLAG_NEUTRAL | CTF_SHIELDED);
1905 // scan through all the flags and notify the client about them
1906 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1908 if(flag.team == NUM_TEAM_1) { t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
1909 if(flag.team == NUM_TEAM_2) { t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
1910 if(flag.team == NUM_TEAM_3) { t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
1911 if(flag.team == NUM_TEAM_4) { t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
1912 if(flag.team == 0) { t = CTF_NEUTRAL_FLAG_CARRYING; t2 = CTF_NEUTRAL_FLAG_TAKEN; t3 = CTF_NEUTRAL_FLAG_LOST; self.ctf_flagstatus |= CTF_FLAG_NEUTRAL; }
1914 switch(flag.ctf_status)
1919 if((flag.owner == self) || (flag.pass_sender == self))
1920 self.ctf_flagstatus |= t; // carrying: self is currently carrying the flag
1922 self.ctf_flagstatus |= t2; // taken: someone else is carrying the flag
1927 self.ctf_flagstatus |= t3; // lost: the flag is dropped somewhere on the map
1933 // item for stopping players from capturing the flag too often
1934 if(self.ctf_captureshielded)
1935 self.ctf_flagstatus |= CTF_SHIELDED;
1937 // update the health of the flag carrier waypointsprite
1938 if(self.wps_flagcarrier)
1939 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
1944 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1946 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1948 if(frag_target == frag_attacker) // damage done to yourself
1950 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1951 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1953 else // damage done to everyone else
1955 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1956 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1959 else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
1961 if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON)))
1962 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1964 frag_target.wps_helpme_time = time;
1965 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1967 // todo: add notification for when flag carrier needs help?
1972 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1974 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
1976 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1977 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1980 if(frag_target.flagcarried)
1982 entity tmp_entity = frag_target.flagcarried;
1983 ctf_Handle_Throw(frag_target, world, DROP_NORMAL);
1984 tmp_entity.ctf_dropper = world;
1990 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1993 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1996 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1998 entity flag; // temporary entity for the search method
2000 if(self.flagcarried)
2001 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2003 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2005 if(flag.pass_sender == self) { flag.pass_sender = world; }
2006 if(flag.pass_target == self) { flag.pass_target = world; }
2007 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
2013 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
2015 if(self.flagcarried)
2016 if(!autocvar_g_ctf_portalteleport)
2017 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2022 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
2024 if(MUTATOR_RETURNVALUE || gameover) { return false; }
2026 entity player = self;
2028 if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2030 // pass the flag to a team mate
2031 if(autocvar_g_ctf_pass)
2033 entity head, closest_target = world;
2034 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2036 while(head) // find the closest acceptable target to pass to
2038 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
2039 if(head != player && SAME_TEAM(head, player))
2040 if(!head.speedrunning && !head.vehicle)
2042 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2043 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2044 vector passer_center = CENTER_OR_VIEWOFS(player);
2046 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2048 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2050 if(IS_BOT_CLIENT(head))
2052 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2053 ctf_Handle_Throw(head, player, DROP_PASS);
2057 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2058 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2060 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2063 else if(player.flagcarried)
2067 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2068 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
2069 { closest_target = head; }
2071 else { closest_target = head; }
2078 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2081 // throw the flag in front of you
2082 if(autocvar_g_ctf_throw && player.flagcarried)
2084 if(player.throw_count == -1)
2086 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2088 player.throw_prevtime = time;
2089 player.throw_count = 1;
2090 ctf_Handle_Throw(player, world, DROP_THROW);
2095 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2101 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2102 else { player.throw_count += 1; }
2103 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2105 player.throw_prevtime = time;
2106 ctf_Handle_Throw(player, world, DROP_THROW);
2115 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
2117 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2119 self.wps_helpme_time = time;
2120 WaypointSprite_HelpMePing(self.wps_flagcarrier);
2122 else // create a normal help me waypointsprite
2124 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, self, FLAG_WAYPOINT_OFFSET, world, self.team, self, wps_helpme, false, RADARICON_HELPME);
2125 WaypointSprite_Ping(self.wps_helpme);
2131 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
2133 if(vh_player.flagcarried)
2135 vh_player.flagcarried.nodrawtoclient = vh_player; // hide the flag from the driver
2137 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2139 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
2143 setattachment(vh_player.flagcarried, vh_vehicle, "");
2144 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
2145 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2146 //vh_player.flagcarried.angles = '0 0 0';
2154 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
2156 if(vh_player.flagcarried)
2158 setattachment(vh_player.flagcarried, vh_player, "");
2159 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2160 vh_player.flagcarried.scale = FLAG_SCALE;
2161 vh_player.flagcarried.angles = '0 0 0';
2162 vh_player.flagcarried.nodrawtoclient = world;
2169 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
2171 if(self.flagcarried)
2173 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((self.flagcarried.team) ? APP_TEAM_ENT_4(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN_) : INFO_CTF_FLAGRETURN_ABORTRUN_NEUTRAL));
2174 ctf_RespawnFlag(self.flagcarried);
2181 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
2183 entity flag; // temporary entity for the search method
2185 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2187 switch(flag.ctf_status)
2192 // lock the flag, game is over
2193 flag.movetype = MOVETYPE_NONE;
2194 flag.takedamage = DAMAGE_NO;
2195 flag.solid = SOLID_NOT;
2196 flag.nextthink = false; // stop thinking
2198 //dprint("stopping the ", flag.netname, " from moving.\n");
2206 // do nothing for these flags
2215 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
2217 havocbot_ctf_reset_role(self);
2221 MUTATOR_HOOKFUNCTION(ctf_GetTeamCount)
2223 //ret_float = ctf_teams;
2224 ret_string = "ctf_team";
2228 MUTATOR_HOOKFUNCTION(ctf_SpectateCopy)
2230 self.ctf_flagstatus = other.ctf_flagstatus;
2239 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2240 CTF flag for team one (Red).
2242 "angle" Angle the flag will point (minus 90 degrees)...
2243 "model" model to use, note this needs red and blue as skins 0 and 1...
2244 "noise" sound played when flag is picked up...
2245 "noise1" sound played when flag is returned by a teammate...
2246 "noise2" sound played when flag is captured...
2247 "noise3" sound played when flag is lost in the field and respawns itself...
2248 "noise4" sound played when flag is dropped by a player...
2249 "noise5" sound played when flag touches the ground... */
2250 spawnfunc(item_flag_team1)
2252 if(!g_ctf) { remove(self); return; }
2254 ctf_FlagSetup(NUM_TEAM_1, self);
2257 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2258 CTF flag for team two (Blue).
2260 "angle" Angle the flag will point (minus 90 degrees)...
2261 "model" model to use, note this needs red and blue as skins 0 and 1...
2262 "noise" sound played when flag is picked up...
2263 "noise1" sound played when flag is returned by a teammate...
2264 "noise2" sound played when flag is captured...
2265 "noise3" sound played when flag is lost in the field and respawns itself...
2266 "noise4" sound played when flag is dropped by a player...
2267 "noise5" sound played when flag touches the ground... */
2268 spawnfunc(item_flag_team2)
2270 if(!g_ctf) { remove(self); return; }
2272 ctf_FlagSetup(NUM_TEAM_2, self);
2275 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2276 CTF flag for team three (Yellow).
2278 "angle" Angle the flag will point (minus 90 degrees)...
2279 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2280 "noise" sound played when flag is picked up...
2281 "noise1" sound played when flag is returned by a teammate...
2282 "noise2" sound played when flag is captured...
2283 "noise3" sound played when flag is lost in the field and respawns itself...
2284 "noise4" sound played when flag is dropped by a player...
2285 "noise5" sound played when flag touches the ground... */
2286 spawnfunc(item_flag_team3)
2288 if(!g_ctf) { remove(self); return; }
2290 ctf_FlagSetup(NUM_TEAM_3, self);
2293 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2294 CTF flag for team four (Pink).
2296 "angle" Angle the flag will point (minus 90 degrees)...
2297 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2298 "noise" sound played when flag is picked up...
2299 "noise1" sound played when flag is returned by a teammate...
2300 "noise2" sound played when flag is captured...
2301 "noise3" sound played when flag is lost in the field and respawns itself...
2302 "noise4" sound played when flag is dropped by a player...
2303 "noise5" sound played when flag touches the ground... */
2304 spawnfunc(item_flag_team4)
2306 if(!g_ctf) { remove(self); return; }
2308 ctf_FlagSetup(NUM_TEAM_4, self);
2311 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2314 "angle" Angle the flag will point (minus 90 degrees)...
2315 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2316 "noise" sound played when flag is picked up...
2317 "noise1" sound played when flag is returned by a teammate...
2318 "noise2" sound played when flag is captured...
2319 "noise3" sound played when flag is lost in the field and respawns itself...
2320 "noise4" sound played when flag is dropped by a player...
2321 "noise5" sound played when flag touches the ground... */
2322 spawnfunc(item_flag_neutral)
2324 if(!g_ctf) { remove(self); return; }
2325 if(!cvar("g_ctf_oneflag")) { remove(self); return; }
2327 ctf_FlagSetup(0, self);
2330 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2331 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2332 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.
2334 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2335 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2338 if(!g_ctf) { remove(self); return; }
2340 self.classname = "ctf_team";
2341 self.team = self.cnt + 1;
2344 // compatibility for quake maps
2345 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2346 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2347 spawnfunc(info_player_team1);
2348 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2349 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2350 spawnfunc(info_player_team2);
2351 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2352 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2354 void team_CTF_neutralflag() { SELFPARAM(); spawnfunc_item_flag_neutral(self); }
2355 void team_neutralobelisk() { SELFPARAM(); spawnfunc_item_flag_neutral(self); }
2363 void ctf_ScoreRules(int teams)
2365 CheckAllowedTeams(world);
2366 ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
2367 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2368 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2369 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2370 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2371 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2372 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2373 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2374 ScoreRules_basics_end();
2377 // code from here on is just to support maps that don't have flag and team entities
2378 void ctf_SpawnTeam (string teamname, int teamcolor)
2380 entity this = new(ctf_team);
2381 this.netname = teamname;
2382 this.cnt = teamcolor;
2383 this.spawnfunc_checked = true;
2384 WITH(entity, self, this, spawnfunc_ctf_team(this));
2387 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2392 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2394 if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2395 if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2396 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2399 ctf_teams = bound(2, ctf_teams, 4);
2401 // if no teams are found, spawn defaults
2402 if(find(world, classname, "ctf_team") == world)
2404 LOG_INFO("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2405 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2406 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2408 ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2410 ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2413 ctf_ScoreRules(ctf_teams);
2416 void ctf_Initialize()
2418 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2420 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2421 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2422 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2424 addstat(STAT_CTF_FLAGSTATUS, AS_INT, ctf_flagstatus);
2426 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2430 MUTATOR_DEFINITION(gamemode_ctf)
2432 MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2433 MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2434 MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2435 MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2436 MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2437 MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2438 MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2439 MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2440 MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2441 MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2442 MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2443 MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2444 MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2445 MUTATOR_HOOK(HavocBot_ChooseRole, ctf_BotRoles, CBC_ORDER_ANY);
2446 MUTATOR_HOOK(GetTeamCount, ctf_GetTeamCount, CBC_ORDER_ANY);
2447 MUTATOR_HOOK(SpectateCopy, ctf_SpectateCopy, CBC_ORDER_ANY);
2451 if(time > 1) // game loads at time 1
2452 error("This is a game type and it cannot be added at runtime.");
2456 MUTATOR_ONROLLBACK_OR_REMOVE
2458 // we actually cannot roll back ctf_Initialize here
2459 // BUT: we don't need to! If this gets called, adding always
2465 LOG_INFO("This is a game type and it cannot be removed at runtime.");