1 #include "gamemode_ctf.qh"
7 #include "../../common/vehicles/sv_vehicles.qh"
10 #include "../../warpzonelib/common.qh"
11 #include "../../warpzonelib/mathlib.qh"
13 void ctf_FakeTimeLimit(entity e, float t)
16 WriteByte(MSG_ONE, 3); // svc_updatestat
17 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
19 WriteCoord(MSG_ONE, autocvar_timelimit);
21 WriteCoord(MSG_ONE, (t + 1) / 60);
24 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
26 if(autocvar_sv_eventlog)
27 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != world) ? ftos(actor.playerid) : "")));
28 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
31 void ctf_CaptureRecord(entity flag, entity player)
33 float cap_record = ctf_captimerecord;
34 float cap_time = (time - flag.ctf_pickuptime);
35 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
38 if(ctf_oneflag) { Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname); }
39 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)); }
40 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)); }
41 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)); }
43 // write that shit in the database
44 if(!ctf_oneflag) // but not in 1-flag mode
45 if((!ctf_captimerecord) || (cap_time < cap_record))
47 ctf_captimerecord = cap_time;
48 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
49 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
50 write_recordmarker(player, (time - cap_time), cap_time);
54 void ctf_FlagcarrierWaypoints(entity player)
56 WaypointSprite_Spawn("flagcarrier", 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, true, RADARICON_FLAG, WPCOLOR_FLAGCARRIER(player.team));
57 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON) * 2);
58 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
59 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
62 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
64 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
65 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
66 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
67 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
70 if(current_height) // make sure we can actually do this arcing path
72 targpos = (to + ('0 0 1' * current_height));
73 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
74 if(trace_fraction < 1)
76 //print("normal arc line failed, trying to find new pos...");
77 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
78 targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
79 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
80 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
81 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
84 else { targpos = to; }
86 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
88 vector desired_direction = normalize(targpos - from);
89 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
90 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
93 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
95 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
97 // directional tracing only
99 makevectors(passer_angle);
101 // find the closest point on the enemy to the center of the attack
102 float h; // hypotenuse, which is the distance between attacker to head
103 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
105 h = vlen(head_center - passer_center);
106 a = h * (normalize(head_center - passer_center) * v_forward);
108 vector nearest_on_line = (passer_center + a * v_forward);
109 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
111 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
112 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
114 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
119 else { return true; }
123 // =======================
124 // CaptureShield Functions
125 // =======================
127 bool ctf_CaptureShield_CheckStatus(entity p)
129 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
131 int players_worseeq, players_total;
133 if(ctf_captureshield_max_ratio <= 0)
136 s = PlayerScore_Add(p, SP_CTF_CAPS, 0);
137 s2 = PlayerScore_Add(p, SP_CTF_PICKUPS, 0);
138 s3 = PlayerScore_Add(p, SP_CTF_RETURNS, 0);
139 s4 = PlayerScore_Add(p, SP_CTF_FCKILLS, 0);
141 sr = ((s - s2) + (s3 + s4));
143 if(sr >= -ctf_captureshield_min_negscore)
146 players_total = players_worseeq = 0;
151 se = PlayerScore_Add(e, SP_CTF_CAPS, 0);
152 se2 = PlayerScore_Add(e, SP_CTF_PICKUPS, 0);
153 se3 = PlayerScore_Add(e, SP_CTF_RETURNS, 0);
154 se4 = PlayerScore_Add(e, SP_CTF_FCKILLS, 0);
156 ser = ((se - se2) + (se3 + se4));
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, bool wanted_status)
174 bool 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 bool ctf_CaptureShield_Customize()
184 if(!other.ctf_captureshielded) { return false; }
185 if(CTF_SAMETEAM(self, other)) { return false; }
190 void ctf_CaptureShield_Touch()
192 if(!other.ctf_captureshielded) { return; }
193 if(CTF_SAMETEAM(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 if(IS_REAL_CLIENT(other)) { 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, int 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, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_LOST_) : INFO_CTF_LOST_NEUTRAL), player.netname);
243 sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_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;
282 setattachment(flag, player.vehicle, "");
283 setorigin(flag, VEHICLE_FLAG_OFFSET);
284 flag.scale = VEHICLE_FLAG_SCALE;
288 setattachment(flag, player, "");
289 setorigin(flag, FLAG_CARRY_OFFSET);
291 flag.movetype = MOVETYPE_NONE;
292 flag.takedamage = DAMAGE_NO;
293 flag.solid = SOLID_NOT;
294 flag.angles = '0 0 0';
295 flag.ctf_status = FLAG_CARRY;
297 // messages and sounds
298 sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
299 ctf_EventLog("receive", flag.team, player);
301 FOR_EACH_REALPLAYER(tmp_player)
303 if(tmp_player == sender)
304 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);
305 else if(tmp_player == player)
306 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);
307 else if(SAME_TEAM(tmp_player, sender))
308 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);
311 // create new waypoint
312 ctf_FlagcarrierWaypoints(player);
314 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
315 player.throw_antispam = sender.throw_antispam;
317 flag.pass_distance = 0;
318 flag.pass_sender = world;
319 flag.pass_target = world;
322 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
324 entity flag = player.flagcarried;
325 vector targ_origin, flag_velocity;
327 if(!flag) { return; }
328 if((droptype == DROP_PASS) && !receiver) { return; }
330 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
333 setattachment(flag, world, "");
334 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
335 flag.owner.flagcarried = world;
337 flag.solid = SOLID_TRIGGER;
338 flag.ctf_dropper = player;
339 flag.ctf_droptime = time;
341 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
348 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
349 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
350 WarpZone_RefSys_Copy(flag, receiver);
351 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
352 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
354 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
355 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
358 flag.movetype = MOVETYPE_FLY;
359 flag.takedamage = DAMAGE_NO;
360 flag.pass_sender = player;
361 flag.pass_target = receiver;
362 flag.ctf_status = FLAG_PASSING;
365 sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
366 WarpZone_TrailParticles(world, particleeffectnum(flag.passeffect), player.origin, targ_origin);
367 ctf_EventLog("pass", flag.team, player);
373 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'));
375 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)));
376 flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, false);
377 ctf_Handle_Drop(flag, player, droptype);
383 flag.velocity = '0 0 0'; // do nothing
390 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);
391 ctf_Handle_Drop(flag, player, droptype);
396 // kill old waypointsprite
397 WaypointSprite_Ping(player.wps_flagcarrier);
398 WaypointSprite_Kill(player.wps_flagcarrier);
400 if(player.wps_enemyflagcarrier)
401 WaypointSprite_Kill(player.wps_enemyflagcarrier);
404 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
412 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
414 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
415 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
416 entity player_team_flag = world, tmp_entity;
417 float old_time, new_time;
419 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
420 if(CTF_DIFFTEAM(player, flag)) { return; }
423 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
424 if(SAME_TEAM(tmp_entity, player))
426 player_team_flag = tmp_entity;
430 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
432 player.throw_prevtime = time;
433 player.throw_count = 0;
435 // messages and sounds
436 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((enemy_flag.team) ? APP_TEAM_ENT_4(enemy_flag, CENTER_CTF_CAPTURE_) : CENTER_CTF_CAPTURE_NEUTRAL));
437 ctf_CaptureRecord(enemy_flag, player);
438 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);
442 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
443 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
448 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
449 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
451 old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
452 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
453 if(!old_time || new_time < old_time)
454 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
457 Send_Effect(flag.capeffect, flag.origin, '0 0 0', 1);
458 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
461 if(capturetype == CAPTURE_NORMAL)
463 WaypointSprite_Kill(player.wps_flagcarrier);
464 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
466 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
467 { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
471 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
472 ctf_RespawnFlag(enemy_flag);
475 void ctf_Handle_Return(entity flag, entity player)
477 // messages and sounds
478 if(IS_MONSTER(player))
480 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_MONSTER_), player.monster_name);
484 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_RETURN_));
485 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_), player.netname);
487 sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
488 ctf_EventLog("return", flag.team, player);
491 if(IS_PLAYER(player))
493 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
494 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
496 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
499 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
503 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
504 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
505 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
509 if(player.flagcarried == flag)
510 WaypointSprite_Kill(player.wps_flagcarrier);
513 ctf_RespawnFlag(flag);
516 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
519 float pickup_dropped_score; // used to calculate dropped pickup score
520 entity tmp_entity; // temporary entity
522 // attach the flag to the player
524 player.flagcarried = flag;
527 setattachment(flag, player.vehicle, "");
528 setorigin(flag, VEHICLE_FLAG_OFFSET);
529 flag.scale = VEHICLE_FLAG_SCALE;
533 setattachment(flag, player, "");
534 setorigin(flag, FLAG_CARRY_OFFSET);
538 flag.movetype = MOVETYPE_NONE;
539 flag.takedamage = DAMAGE_NO;
540 flag.solid = SOLID_NOT;
541 flag.angles = '0 0 0';
542 flag.ctf_status = FLAG_CARRY;
546 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
547 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
551 // messages and sounds
552 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_PICKUP_) : INFO_CTF_PICKUP_NEUTRAL), player.netname);
553 if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
554 if(!flag.team) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL); }
555 else if(CTF_DIFFTEAM(player, flag)) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PICKUP_)); }
556 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)); }
558 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);
561 FOR_EACH_PLAYER(tmp_entity)
562 if(tmp_entity != player)
563 if(DIFF_TEAM(player, tmp_entity))
564 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, CHOICE_CTF_PICKUP_ENEMY_NEUTRAL, Team_ColorCode(player.team), player.netname);
567 FOR_EACH_PLAYER(tmp_entity)
568 if(tmp_entity != player)
569 if(CTF_SAMETEAM(flag, tmp_entity))
570 if(SAME_TEAM(player, tmp_entity))
571 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_PICKUP_TEAM_), Team_ColorCode(player.team), player.netname);
573 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);
575 sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
578 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
579 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
584 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
585 ctf_EventLog("steal", flag.team, player);
591 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);
592 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);
593 dprint("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
594 PlayerTeamScore_AddScore(player, pickup_dropped_score);
595 ctf_EventLog("pickup", flag.team, player);
603 if(pickuptype == PICKUP_BASE)
605 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
606 if((player.speedrunning) && (ctf_captimerecord))
607 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
611 Send_Effect(flag.toucheffect, player.origin, '0 0 0', 1);
614 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
615 ctf_FlagcarrierWaypoints(player);
616 WaypointSprite_Ping(player.wps_flagcarrier);
620 // ===================
621 // Main Flag Functions
622 // ===================
624 void ctf_CheckFlagReturn(entity flag, int returntype)
626 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
628 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
630 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
634 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;
635 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;
636 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;
637 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;
641 { Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_TIMEOUT_) : INFO_CTF_FLAGRETURN_TIMEOUT_NEUTRAL)); break; }
643 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
644 ctf_EventLog("returned", flag.team, world);
645 ctf_RespawnFlag(flag);
650 bool ctf_Stalemate_Customize()
652 // make spectators see what the player would see
654 e = WaypointSprite_getviewentity(other);
655 wp_owner = self.owner;
658 if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
659 if(SAME_TEAM(wp_owner, e)) { return false; }
660 if(!IS_PLAYER(e)) { return false; }
665 void ctf_CheckStalemate(void)
668 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
671 entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
673 // build list of stale flags
674 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
676 if(autocvar_g_ctf_stalemate)
677 if(tmp_entity.ctf_status != FLAG_BASE)
678 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
680 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
681 ctf_staleflaglist = tmp_entity;
683 switch(tmp_entity.team)
685 case NUM_TEAM_1: ++stale_red_flags; break;
686 case NUM_TEAM_2: ++stale_blue_flags; break;
687 case NUM_TEAM_3: ++stale_yellow_flags; break;
688 case NUM_TEAM_4: ++stale_pink_flags; break;
689 default: ++stale_neutral_flags; break;
695 stale_flags = (stale_neutral_flags >= 1);
697 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
699 if(ctf_oneflag && stale_flags == 1)
700 ctf_stalemate = true;
701 else if(stale_flags >= 2)
702 ctf_stalemate = true;
703 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
704 { ctf_stalemate = false; wpforenemy_announced = false; }
705 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
706 { ctf_stalemate = false; wpforenemy_announced = false; }
708 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
711 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
713 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
715 WaypointSprite_Spawn(((ctf_oneflag) ? "flagcarrier" : "enemyflagcarrier"), 0, 0, tmp_entity.owner, FLAG_WAYPOINT_OFFSET, world, 0, tmp_entity.owner, wps_enemyflagcarrier, true, RADARICON_FLAG, WPCOLOR_ENEMYFC(tmp_entity.owner.team));
716 tmp_entity.owner.wps_enemyflagcarrier.customizeentityforclient = ctf_Stalemate_Customize;
720 if (!wpforenemy_announced)
722 FOR_EACH_REALPLAYER(tmp_entity)
723 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CENTER, ((tmp_entity.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER));
725 wpforenemy_announced = true;
730 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
732 if(ITEM_DAMAGE_NEEDKILL(deathtype))
734 if(autocvar_g_ctf_flag_return_damage_delay)
736 self.ctf_flagdamaged = true;
741 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
745 if(autocvar_g_ctf_flag_return_damage)
747 // reduce health and check if it should be returned
748 self.health = self.health - damage;
749 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
759 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
762 if(self == ctf_worldflaglist) // only for the first flag
763 FOR_EACH_CLIENT(tmp_entity)
764 ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
767 if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
768 dprint("wtf the flag got squashed?\n");
769 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
770 if(!trace_startsolid || self.noalign) // can we resize it without getting stuck?
771 setsize(self, FLAG_MIN, FLAG_MAX); }
773 switch(self.ctf_status) // reset flag angles in case warpzones adjust it
777 self.angles = '0 0 0';
785 switch(self.ctf_status)
789 if(autocvar_g_ctf_dropped_capture_radius)
791 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
792 if(tmp_entity.ctf_status == FLAG_DROPPED)
793 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
794 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
795 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
802 if(autocvar_g_ctf_flag_dropped_floatinwater)
804 vector midpoint = ((self.absmin + self.absmax) * 0.5);
805 if(pointcontents(midpoint) == CONTENT_WATER)
807 self.velocity = self.velocity * 0.5;
809 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
810 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
812 { self.movetype = MOVETYPE_FLY; }
814 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
816 if(autocvar_g_ctf_flag_return_dropped)
818 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
821 ctf_CheckFlagReturn(self, RETURN_DROPPED);
825 if(self.ctf_flagdamaged)
827 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
828 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
831 else if(autocvar_g_ctf_flag_return_time)
833 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
834 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
842 if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
845 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
849 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
853 if(autocvar_g_ctf_stalemate)
855 if(time >= wpforenemy_nextthink)
857 ctf_CheckStalemate();
858 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
861 if(CTF_SAMETEAM(self, self.owner) && self.team)
863 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
864 ctf_Handle_Throw(self.owner, world, DROP_THROW);
865 else if(vlen(self.owner.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_carried_radius)
866 ctf_Handle_Return(self, self.owner);
873 vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
874 targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
875 WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
877 if((self.pass_target == world)
878 || (self.pass_target.deadflag != DEAD_NO)
879 || (self.pass_target.flagcarried)
880 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
881 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
882 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
884 // give up, pass failed
885 ctf_Handle_Drop(self, world, DROP_PASS);
889 // still a viable target, go for it
890 ctf_CalculatePassVelocity(self, targ_origin, self.origin, true);
895 default: // this should never happen
897 dprint("ctf_FlagThink(): Flag exists with no status?\n");
905 if(gameover) { return; }
906 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
908 entity toucher = other, tmp_entity;
909 bool is_not_monster = (!IS_MONSTER(toucher)), num_perteam = 0;
911 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
912 if(ITEM_TOUCH_NEEDKILL())
914 if(!autocvar_g_ctf_flag_return_damage_delay)
917 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
919 if(!self.ctf_flagdamaged) { return; }
922 FOR_EACH_PLAYER(tmp_entity) if(SAME_TEAM(toucher, tmp_entity)) { ++num_perteam; }
924 // special touch behaviors
925 if(toucher.frozen) { return; }
926 else if(IS_VEHICLE(toucher))
928 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
929 toucher = toucher.owner; // the player is actually the vehicle owner, not other
931 return; // do nothing
933 else if(IS_MONSTER(toucher))
935 if(!autocvar_g_ctf_allow_monster_touch)
936 return; // do nothing
938 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
940 if(time > self.wait) // if we haven't in a while, play a sound/effect
942 Send_Effect(self.toucheffect, self.origin, '0 0 0', 1);
943 sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTEN_NORM);
944 self.wait = time + FLAG_TOUCHRATE;
948 else if(toucher.deadflag != DEAD_NO) { return; }
950 switch(self.ctf_status)
956 if(CTF_SAMETEAM(toucher, self) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
957 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
958 else if(!self.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
959 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the neutral flag
961 else if(CTF_SAMETEAM(toucher, self) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, self) && is_not_monster)
962 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
963 else if(CTF_DIFFTEAM(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
964 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
970 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
971 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
972 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
973 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
979 dprint("Someone touched a flag even though it was being carried?\n");
985 if((IS_PLAYER(toucher)) && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
987 if(DIFF_TEAM(toucher, self.pass_sender))
988 ctf_Handle_Return(self, toucher);
990 ctf_Handle_Retrieve(self, toucher);
998 void ctf_RespawnFlag(entity flag)
1000 // check for flag respawn being called twice in a row
1001 if(flag.last_respawn > time - 0.5)
1002 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1004 flag.last_respawn = time;
1006 // reset the player (if there is one)
1007 if((flag.owner) && (flag.owner.flagcarried == flag))
1009 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1010 WaypointSprite_Kill(flag.wps_flagcarrier);
1012 flag.owner.flagcarried = world;
1014 if(flag.speedrunning)
1015 ctf_FakeTimeLimit(flag.owner, -1);
1018 if((flag.owner) && (flag.owner.vehicle))
1019 flag.scale = FLAG_SCALE;
1021 if(flag.ctf_status == FLAG_DROPPED)
1022 { WaypointSprite_Kill(flag.wps_flagdropped); }
1025 setattachment(flag, world, "");
1026 setorigin(flag, flag.ctf_spawnorigin);
1028 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
1029 flag.takedamage = DAMAGE_NO;
1030 flag.health = flag.max_flag_health;
1031 flag.solid = SOLID_TRIGGER;
1032 flag.velocity = '0 0 0';
1033 flag.angles = flag.mangle;
1034 flag.flags = FL_ITEM | FL_NOTARGET;
1036 flag.ctf_status = FLAG_BASE;
1038 flag.pass_distance = 0;
1039 flag.pass_sender = world;
1040 flag.pass_target = world;
1041 flag.ctf_dropper = world;
1042 flag.ctf_pickuptime = 0;
1043 flag.ctf_droptime = 0;
1044 flag.ctf_flagdamaged = 0;
1046 ctf_CheckStalemate();
1052 if(IS_PLAYER(self.owner))
1053 ctf_Handle_Throw(self.owner, world, DROP_RESET);
1055 ctf_RespawnFlag(self);
1058 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
1061 waypoint_spawnforitem_force(self, self.origin);
1062 self.nearestwaypointtimeout = 0; // activate waypointing again
1063 self.bot_basewaypoint = self.nearestwaypoint;
1066 string basename = "base";
1070 case NUM_TEAM_1: basename = "redbase"; break;
1071 case NUM_TEAM_2: basename = "bluebase"; break;
1072 case NUM_TEAM_3: basename = "yellowbase"; break;
1073 case NUM_TEAM_4: basename = "pinkbase"; break;
1074 default: basename = "neutralbase"; break;
1077 WaypointSprite_SpawnFixed(basename, self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, ((self.team) ? Team_ColorRGB(self.team) : '1 1 1'));
1078 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, ((self.team) ? colormapPaletteColor(self.team - 1, false) : '1 1 1'));
1080 // captureshield setup
1081 ctf_CaptureShield_Spawn(self);
1084 void set_flag_string(entity flag, .string field, string value, string teamname)
1086 if(flag.field == "")
1087 flag.field = strzone(sprintf(value,teamname));
1090 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1093 string teamname = Static_Team_ColorName_Lower(teamnumber);
1094 self = 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;
1127 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1128 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1129 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1130 set_flag_string(flag, toucheffect, "%sflag_touch", teamname);
1131 set_flag_string(flag, passeffect, "%s_pass", teamname);
1132 set_flag_string(flag, capeffect, "%s_cap", teamname);
1135 set_flag_string(flag, snd_flag_taken, "ctf/%s_taken.wav", teamname);
1136 set_flag_string(flag, snd_flag_returned, "ctf/%s_returned.wav", teamname);
1137 set_flag_string(flag, snd_flag_capture, "ctf/%s_capture.wav", teamname);
1138 set_flag_string(flag, snd_flag_dropped, "ctf/%s_dropped.wav", teamname);
1139 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.
1140 if(flag.snd_flag_touch == "") { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
1141 if(flag.snd_flag_pass == "") { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
1144 precache_sound(flag.snd_flag_taken);
1145 precache_sound(flag.snd_flag_returned);
1146 precache_sound(flag.snd_flag_capture);
1147 precache_sound(flag.snd_flag_respawn);
1148 precache_sound(flag.snd_flag_dropped);
1149 precache_sound(flag.snd_flag_touch);
1150 precache_sound(flag.snd_flag_pass);
1151 precache_model(flag.model);
1152 precache_model("models/ctf/shield.md3");
1153 precache_model("models/ctf/shockwavetransring.md3");
1156 setmodel(flag, flag.model); // precision set below
1157 setsize(flag, FLAG_MIN, FLAG_MAX);
1158 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1160 if(autocvar_g_ctf_flag_glowtrails)
1164 case NUM_TEAM_1: flag.glow_color = 251; break;
1165 case NUM_TEAM_2: flag.glow_color = 210; break;
1166 case NUM_TEAM_3: flag.glow_color = 110; break;
1167 case NUM_TEAM_4: flag.glow_color = 145; break;
1168 default: flag.glow_color = 254; break;
1170 flag.glow_size = 25;
1171 flag.glow_trail = 1;
1174 flag.effects |= EF_LOWPRECISION;
1175 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1176 if(autocvar_g_ctf_dynamiclights)
1180 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1181 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1182 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1183 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1184 default: flag.effects |= EF_DIMLIGHT; break;
1189 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1191 flag.dropped_origin = flag.origin;
1192 flag.noalign = true;
1193 flag.movetype = MOVETYPE_NONE;
1195 else // drop to floor, automatically find a platform and set that as spawn origin
1197 flag.noalign = false;
1200 flag.movetype = MOVETYPE_TOSS;
1203 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1211 // NOTE: LEGACY CODE, needs to be re-written!
1213 void havocbot_calculate_middlepoint()
1217 vector fo = '0 0 0';
1220 f = ctf_worldflaglist;
1225 f = f.ctf_worldflagnext;
1229 havocbot_ctf_middlepoint = s * (1.0 / n);
1230 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1234 entity havocbot_ctf_find_flag(entity bot)
1237 f = ctf_worldflaglist;
1240 if (CTF_SAMETEAM(bot, f))
1242 f = f.ctf_worldflagnext;
1247 entity havocbot_ctf_find_enemy_flag(entity bot)
1250 f = ctf_worldflaglist;
1255 if(CTF_DIFFTEAM(bot, f))
1262 else if(!bot.flagcarried)
1266 else if (CTF_DIFFTEAM(bot, f))
1268 f = f.ctf_worldflagnext;
1273 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1281 FOR_EACH_PLAYER(head)
1283 if(DIFF_TEAM(head, bot) || head.deadflag != DEAD_NO || head == bot)
1286 if(vlen(head.origin - org) < tc_radius)
1293 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1296 head = ctf_worldflaglist;
1299 if (CTF_SAMETEAM(self, head))
1301 head = head.ctf_worldflagnext;
1304 navigation_routerating(head, ratingscale, 10000);
1307 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1310 head = ctf_worldflaglist;
1313 if (CTF_SAMETEAM(self, head))
1315 head = head.ctf_worldflagnext;
1320 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1323 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1326 head = ctf_worldflaglist;
1331 if(CTF_DIFFTEAM(self, head))
1335 if(self.flagcarried)
1338 else if(!self.flagcarried)
1342 else if(CTF_DIFFTEAM(self, head))
1344 head = head.ctf_worldflagnext;
1347 navigation_routerating(head, ratingscale, 10000);
1350 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1352 if (!bot_waypoints_for_items)
1354 havocbot_goalrating_ctf_enemyflag(ratingscale);
1360 head = havocbot_ctf_find_enemy_flag(self);
1365 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1368 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1372 mf = havocbot_ctf_find_flag(self);
1374 if(mf.ctf_status == FLAG_BASE)
1378 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1381 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1384 head = ctf_worldflaglist;
1387 // flag is out in the field
1388 if(head.ctf_status != FLAG_BASE)
1389 if(head.tag_entity==world) // dropped
1393 if(vlen(org-head.origin)<df_radius)
1394 navigation_routerating(head, ratingscale, 10000);
1397 navigation_routerating(head, ratingscale, 10000);
1400 head = head.ctf_worldflagnext;
1404 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1408 head = findchainfloat(bot_pickup, true);
1411 // gather health and armor only
1413 if (head.health || head.armorvalue)
1414 if (vlen(head.origin - org) < sradius)
1416 // get the value of the item
1417 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1419 navigation_routerating(head, t * ratingscale, 500);
1425 void havocbot_ctf_reset_role(entity bot)
1427 float cdefense, cmiddle, coffense;
1428 entity mf, ef, head;
1431 if(bot.deadflag != DEAD_NO)
1434 if(vlen(havocbot_ctf_middlepoint)==0)
1435 havocbot_calculate_middlepoint();
1438 if (bot.flagcarried)
1440 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1444 mf = havocbot_ctf_find_flag(bot);
1445 ef = havocbot_ctf_find_enemy_flag(bot);
1447 // Retrieve stolen flag
1448 if(mf.ctf_status!=FLAG_BASE)
1450 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1454 // If enemy flag is taken go to the middle to intercept pursuers
1455 if(ef.ctf_status!=FLAG_BASE)
1457 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1461 // if there is only me on the team switch to offense
1463 FOR_EACH_PLAYER(head)
1464 if(SAME_TEAM(head, bot))
1469 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1473 // Evaluate best position to take
1474 // Count mates on middle position
1475 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1477 // Count mates on defense position
1478 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1480 // Count mates on offense position
1481 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1483 if(cdefense<=coffense)
1484 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1485 else if(coffense<=cmiddle)
1486 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1488 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1491 void havocbot_role_ctf_carrier()
1493 if(self.deadflag != DEAD_NO)
1495 havocbot_ctf_reset_role(self);
1499 if (self.flagcarried == world)
1501 havocbot_ctf_reset_role(self);
1505 if (self.bot_strategytime < time)
1507 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1509 navigation_goalrating_start();
1511 havocbot_goalrating_ctf_enemybase(50000);
1513 havocbot_goalrating_ctf_ourbase(50000);
1516 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1518 navigation_goalrating_end();
1520 if (self.navigation_hasgoals)
1521 self.havocbot_cantfindflag = time + 10;
1522 else if (time > self.havocbot_cantfindflag)
1524 // Can't navigate to my own base, suicide!
1525 // TODO: drop it and wander around
1526 Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1532 void havocbot_role_ctf_escort()
1536 if(self.deadflag != DEAD_NO)
1538 havocbot_ctf_reset_role(self);
1542 if (self.flagcarried)
1544 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1548 // If enemy flag is back on the base switch to previous role
1549 ef = havocbot_ctf_find_enemy_flag(self);
1550 if(ef.ctf_status==FLAG_BASE)
1552 self.havocbot_role = self.havocbot_previous_role;
1553 self.havocbot_role_timeout = 0;
1557 // If the flag carrier reached the base switch to defense
1558 mf = havocbot_ctf_find_flag(self);
1559 if(mf.ctf_status!=FLAG_BASE)
1560 if(vlen(ef.origin - mf.dropped_origin) < 300)
1562 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1566 // Set the role timeout if necessary
1567 if (!self.havocbot_role_timeout)
1569 self.havocbot_role_timeout = time + random() * 30 + 60;
1572 // If nothing happened just switch to previous role
1573 if (time > self.havocbot_role_timeout)
1575 self.havocbot_role = self.havocbot_previous_role;
1576 self.havocbot_role_timeout = 0;
1580 // Chase the flag carrier
1581 if (self.bot_strategytime < time)
1583 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1584 navigation_goalrating_start();
1585 havocbot_goalrating_ctf_enemyflag(30000);
1586 havocbot_goalrating_ctf_ourstolenflag(40000);
1587 havocbot_goalrating_items(10000, self.origin, 10000);
1588 navigation_goalrating_end();
1592 void havocbot_role_ctf_offense()
1597 if(self.deadflag != DEAD_NO)
1599 havocbot_ctf_reset_role(self);
1603 if (self.flagcarried)
1605 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1610 mf = havocbot_ctf_find_flag(self);
1611 ef = havocbot_ctf_find_enemy_flag(self);
1614 if(mf.ctf_status!=FLAG_BASE)
1617 pos = mf.tag_entity.origin;
1621 // Try to get it if closer than the enemy base
1622 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1624 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1629 // Escort flag carrier
1630 if(ef.ctf_status!=FLAG_BASE)
1633 pos = ef.tag_entity.origin;
1637 if(vlen(pos-mf.dropped_origin)>700)
1639 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1644 // About to fail, switch to middlefield
1647 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1651 // Set the role timeout if necessary
1652 if (!self.havocbot_role_timeout)
1653 self.havocbot_role_timeout = time + 120;
1655 if (time > self.havocbot_role_timeout)
1657 havocbot_ctf_reset_role(self);
1661 if (self.bot_strategytime < time)
1663 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1664 navigation_goalrating_start();
1665 havocbot_goalrating_ctf_ourstolenflag(50000);
1666 havocbot_goalrating_ctf_enemybase(20000);
1667 havocbot_goalrating_items(5000, self.origin, 1000);
1668 havocbot_goalrating_items(1000, self.origin, 10000);
1669 navigation_goalrating_end();
1673 // Retriever (temporary role):
1674 void havocbot_role_ctf_retriever()
1678 if(self.deadflag != DEAD_NO)
1680 havocbot_ctf_reset_role(self);
1684 if (self.flagcarried)
1686 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1690 // If flag is back on the base switch to previous role
1691 mf = havocbot_ctf_find_flag(self);
1692 if(mf.ctf_status==FLAG_BASE)
1694 havocbot_ctf_reset_role(self);
1698 if (!self.havocbot_role_timeout)
1699 self.havocbot_role_timeout = time + 20;
1701 if (time > self.havocbot_role_timeout)
1703 havocbot_ctf_reset_role(self);
1707 if (self.bot_strategytime < time)
1712 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1713 navigation_goalrating_start();
1714 havocbot_goalrating_ctf_ourstolenflag(50000);
1715 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1716 havocbot_goalrating_ctf_enemybase(30000);
1717 havocbot_goalrating_items(500, self.origin, rt_radius);
1718 navigation_goalrating_end();
1722 void havocbot_role_ctf_middle()
1726 if(self.deadflag != DEAD_NO)
1728 havocbot_ctf_reset_role(self);
1732 if (self.flagcarried)
1734 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1738 mf = havocbot_ctf_find_flag(self);
1739 if(mf.ctf_status!=FLAG_BASE)
1741 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1745 if (!self.havocbot_role_timeout)
1746 self.havocbot_role_timeout = time + 10;
1748 if (time > self.havocbot_role_timeout)
1750 havocbot_ctf_reset_role(self);
1754 if (self.bot_strategytime < time)
1758 org = havocbot_ctf_middlepoint;
1759 org.z = self.origin.z;
1761 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1762 navigation_goalrating_start();
1763 havocbot_goalrating_ctf_ourstolenflag(50000);
1764 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1765 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1766 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1767 havocbot_goalrating_items(2500, self.origin, 10000);
1768 havocbot_goalrating_ctf_enemybase(2500);
1769 navigation_goalrating_end();
1773 void havocbot_role_ctf_defense()
1777 if(self.deadflag != DEAD_NO)
1779 havocbot_ctf_reset_role(self);
1783 if (self.flagcarried)
1785 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1789 // If own flag was captured
1790 mf = havocbot_ctf_find_flag(self);
1791 if(mf.ctf_status!=FLAG_BASE)
1793 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1797 if (!self.havocbot_role_timeout)
1798 self.havocbot_role_timeout = time + 30;
1800 if (time > self.havocbot_role_timeout)
1802 havocbot_ctf_reset_role(self);
1805 if (self.bot_strategytime < time)
1810 org = mf.dropped_origin;
1811 mp_radius = havocbot_ctf_middlepoint_radius;
1813 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1814 navigation_goalrating_start();
1816 // if enemies are closer to our base, go there
1817 entity head, closestplayer = world;
1818 float distance, bestdistance = 10000;
1819 FOR_EACH_PLAYER(head)
1821 if(head.deadflag!=DEAD_NO)
1824 distance = vlen(org - head.origin);
1825 if(distance<bestdistance)
1827 closestplayer = head;
1828 bestdistance = distance;
1833 if(DIFF_TEAM(closestplayer, self))
1834 if(vlen(org - self.origin)>1000)
1835 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1836 havocbot_goalrating_ctf_ourbase(30000);
1838 havocbot_goalrating_ctf_ourstolenflag(20000);
1839 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1840 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1841 havocbot_goalrating_items(10000, org, mp_radius);
1842 havocbot_goalrating_items(5000, self.origin, 10000);
1843 navigation_goalrating_end();
1847 void havocbot_role_ctf_setrole(entity bot, int role)
1849 dprint(strcat(bot.netname," switched to "));
1852 case HAVOCBOT_CTF_ROLE_CARRIER:
1854 bot.havocbot_role = havocbot_role_ctf_carrier;
1855 bot.havocbot_role_timeout = 0;
1856 bot.havocbot_cantfindflag = time + 10;
1857 bot.bot_strategytime = 0;
1859 case HAVOCBOT_CTF_ROLE_DEFENSE:
1861 bot.havocbot_role = havocbot_role_ctf_defense;
1862 bot.havocbot_role_timeout = 0;
1864 case HAVOCBOT_CTF_ROLE_MIDDLE:
1866 bot.havocbot_role = havocbot_role_ctf_middle;
1867 bot.havocbot_role_timeout = 0;
1869 case HAVOCBOT_CTF_ROLE_OFFENSE:
1871 bot.havocbot_role = havocbot_role_ctf_offense;
1872 bot.havocbot_role_timeout = 0;
1874 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1875 dprint("retriever");
1876 bot.havocbot_previous_role = bot.havocbot_role;
1877 bot.havocbot_role = havocbot_role_ctf_retriever;
1878 bot.havocbot_role_timeout = time + 10;
1879 bot.bot_strategytime = 0;
1881 case HAVOCBOT_CTF_ROLE_ESCORT:
1883 bot.havocbot_previous_role = bot.havocbot_role;
1884 bot.havocbot_role = havocbot_role_ctf_escort;
1885 bot.havocbot_role_timeout = time + 30;
1886 bot.bot_strategytime = 0;
1897 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1900 int t = 0, t2 = 0, t3 = 0;
1902 // initially clear items so they can be set as necessary later.
1903 self.ctf_flagstatus &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
1904 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
1905 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
1906 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
1907 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
1908 | CTF_FLAG_NEUTRAL | CTF_SHIELDED);
1910 // scan through all the flags and notify the client about them
1911 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1913 if(flag.team == NUM_TEAM_1) { t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
1914 if(flag.team == NUM_TEAM_2) { t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
1915 if(flag.team == NUM_TEAM_3) { t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
1916 if(flag.team == NUM_TEAM_4) { t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
1917 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; }
1919 switch(flag.ctf_status)
1924 if((flag.owner == self) || (flag.pass_sender == self))
1925 self.ctf_flagstatus |= t; // carrying: self is currently carrying the flag
1927 self.ctf_flagstatus |= t2; // taken: someone else is carrying the flag
1932 self.ctf_flagstatus |= t3; // lost: the flag is dropped somewhere on the map
1938 // item for stopping players from capturing the flag too often
1939 if(self.ctf_captureshielded)
1940 self.ctf_flagstatus |= CTF_SHIELDED;
1942 // update the health of the flag carrier waypointsprite
1943 if(self.wps_flagcarrier)
1944 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
1949 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1951 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1953 if(frag_target == frag_attacker) // damage done to yourself
1955 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1956 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1958 else // damage done to everyone else
1960 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1961 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1964 else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
1966 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)))
1967 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1969 frag_target.wps_helpme_time = time;
1970 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1972 // todo: add notification for when flag carrier needs help?
1977 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1979 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
1981 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1982 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1985 if(frag_target.flagcarried)
1987 entity tmp_entity = frag_target.flagcarried;
1988 ctf_Handle_Throw(frag_target, world, DROP_NORMAL);
1989 tmp_entity.ctf_dropper = world;
1995 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1998 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2001 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
2003 entity flag; // temporary entity for the search method
2005 if(self.flagcarried)
2006 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2008 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2010 if(flag.pass_sender == self) { flag.pass_sender = world; }
2011 if(flag.pass_target == self) { flag.pass_target = world; }
2012 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
2018 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
2020 if(self.flagcarried)
2021 if(!autocvar_g_ctf_portalteleport)
2022 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2027 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
2029 if(MUTATOR_RETURNVALUE || gameover) { return false; }
2031 entity player = self;
2033 if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2035 // pass the flag to a team mate
2036 if(autocvar_g_ctf_pass)
2038 entity head, closest_target = world;
2039 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2041 while(head) // find the closest acceptable target to pass to
2043 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
2044 if(head != player && SAME_TEAM(head, player))
2045 if(!head.speedrunning && !head.vehicle)
2047 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2048 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2049 vector passer_center = CENTER_OR_VIEWOFS(player);
2051 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2053 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2055 if(IS_BOT_CLIENT(head))
2057 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2058 ctf_Handle_Throw(head, player, DROP_PASS);
2062 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2063 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2065 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2068 else if(player.flagcarried)
2072 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2073 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
2074 { closest_target = head; }
2076 else { closest_target = head; }
2083 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2086 // throw the flag in front of you
2087 if(autocvar_g_ctf_throw && player.flagcarried)
2089 if(player.throw_count == -1)
2091 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2093 player.throw_prevtime = time;
2094 player.throw_count = 1;
2095 ctf_Handle_Throw(player, world, DROP_THROW);
2100 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2106 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2107 else { player.throw_count += 1; }
2108 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2110 player.throw_prevtime = time;
2111 ctf_Handle_Throw(player, world, DROP_THROW);
2120 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
2122 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2124 self.wps_helpme_time = time;
2125 WaypointSprite_HelpMePing(self.wps_flagcarrier);
2127 else // create a normal help me waypointsprite
2129 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');
2130 WaypointSprite_Ping(self.wps_helpme);
2136 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
2138 if(vh_player.flagcarried)
2140 vh_player.flagcarried.nodrawtoclient = vh_player; // hide the flag from the driver
2142 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2144 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
2148 setattachment(vh_player.flagcarried, vh_vehicle, "");
2149 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
2150 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2151 //vh_player.flagcarried.angles = '0 0 0';
2159 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
2161 if(vh_player.flagcarried)
2163 setattachment(vh_player.flagcarried, vh_player, "");
2164 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2165 vh_player.flagcarried.scale = FLAG_SCALE;
2166 vh_player.flagcarried.angles = '0 0 0';
2167 vh_player.flagcarried.nodrawtoclient = world;
2174 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
2176 if(self.flagcarried)
2178 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));
2179 ctf_RespawnFlag(self.flagcarried);
2186 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
2188 entity flag; // temporary entity for the search method
2190 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2192 switch(flag.ctf_status)
2197 // lock the flag, game is over
2198 flag.movetype = MOVETYPE_NONE;
2199 flag.takedamage = DAMAGE_NO;
2200 flag.solid = SOLID_NOT;
2201 flag.nextthink = false; // stop thinking
2203 //dprint("stopping the ", flag.netname, " from moving.\n");
2211 // do nothing for these flags
2220 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
2222 havocbot_ctf_reset_role(self);
2226 MUTATOR_HOOKFUNCTION(ctf_GetTeamCount)
2228 //ret_float = ctf_teams;
2229 ret_string = "ctf_team";
2233 MUTATOR_HOOKFUNCTION(ctf_SpectateCopy)
2235 self.ctf_flagstatus = other.ctf_flagstatus;
2244 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
2245 CTF Starting point for a player in team one (Red).
2246 Keys: "angle" viewing angle when spawning. */
2247 void spawnfunc_info_player_team1()
2249 if(g_assault) { remove(self); return; }
2251 self.team = NUM_TEAM_1; // red
2252 spawnfunc_info_player_deathmatch();
2256 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
2257 CTF Starting point for a player in team two (Blue).
2258 Keys: "angle" viewing angle when spawning. */
2259 void spawnfunc_info_player_team2()
2261 if(g_assault) { remove(self); return; }
2263 self.team = NUM_TEAM_2; // blue
2264 spawnfunc_info_player_deathmatch();
2267 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
2268 CTF Starting point for a player in team three (Yellow).
2269 Keys: "angle" viewing angle when spawning. */
2270 void spawnfunc_info_player_team3()
2272 if(g_assault) { remove(self); return; }
2274 self.team = NUM_TEAM_3; // yellow
2275 spawnfunc_info_player_deathmatch();
2279 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
2280 CTF Starting point for a player in team four (Purple).
2281 Keys: "angle" viewing angle when spawning. */
2282 void spawnfunc_info_player_team4()
2284 if(g_assault) { remove(self); return; }
2286 self.team = NUM_TEAM_4; // purple
2287 spawnfunc_info_player_deathmatch();
2290 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2291 CTF flag for team one (Red).
2293 "angle" Angle the flag will point (minus 90 degrees)...
2294 "model" model to use, note this needs red and blue as skins 0 and 1...
2295 "noise" sound played when flag is picked up...
2296 "noise1" sound played when flag is returned by a teammate...
2297 "noise2" sound played when flag is captured...
2298 "noise3" sound played when flag is lost in the field and respawns itself...
2299 "noise4" sound played when flag is dropped by a player...
2300 "noise5" sound played when flag touches the ground... */
2301 void spawnfunc_item_flag_team1()
2303 if(!g_ctf) { remove(self); return; }
2305 ctf_FlagSetup(NUM_TEAM_1, self);
2308 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2309 CTF flag for team two (Blue).
2311 "angle" Angle the flag will point (minus 90 degrees)...
2312 "model" model to use, note this needs red and blue as skins 0 and 1...
2313 "noise" sound played when flag is picked up...
2314 "noise1" sound played when flag is returned by a teammate...
2315 "noise2" sound played when flag is captured...
2316 "noise3" sound played when flag is lost in the field and respawns itself...
2317 "noise4" sound played when flag is dropped by a player...
2318 "noise5" sound played when flag touches the ground... */
2319 void spawnfunc_item_flag_team2()
2321 if(!g_ctf) { remove(self); return; }
2323 ctf_FlagSetup(NUM_TEAM_2, self);
2326 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2327 CTF flag for team three (Yellow).
2329 "angle" Angle the flag will point (minus 90 degrees)...
2330 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2331 "noise" sound played when flag is picked up...
2332 "noise1" sound played when flag is returned by a teammate...
2333 "noise2" sound played when flag is captured...
2334 "noise3" sound played when flag is lost in the field and respawns itself...
2335 "noise4" sound played when flag is dropped by a player...
2336 "noise5" sound played when flag touches the ground... */
2337 void spawnfunc_item_flag_team3()
2339 if(!g_ctf) { remove(self); return; }
2341 ctf_FlagSetup(NUM_TEAM_3, self);
2344 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2345 CTF flag for team four (Pink).
2347 "angle" Angle the flag will point (minus 90 degrees)...
2348 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2349 "noise" sound played when flag is picked up...
2350 "noise1" sound played when flag is returned by a teammate...
2351 "noise2" sound played when flag is captured...
2352 "noise3" sound played when flag is lost in the field and respawns itself...
2353 "noise4" sound played when flag is dropped by a player...
2354 "noise5" sound played when flag touches the ground... */
2355 void spawnfunc_item_flag_team4()
2357 if(!g_ctf) { remove(self); return; }
2359 ctf_FlagSetup(NUM_TEAM_4, self);
2362 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2365 "angle" Angle the flag will point (minus 90 degrees)...
2366 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2367 "noise" sound played when flag is picked up...
2368 "noise1" sound played when flag is returned by a teammate...
2369 "noise2" sound played when flag is captured...
2370 "noise3" sound played when flag is lost in the field and respawns itself...
2371 "noise4" sound played when flag is dropped by a player...
2372 "noise5" sound played when flag touches the ground... */
2373 void spawnfunc_item_flag_neutral()
2375 if(!g_ctf) { remove(self); return; }
2376 if(!cvar("g_ctf_oneflag")) { remove(self); return; }
2378 ctf_FlagSetup(0, self);
2381 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2382 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2383 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.
2385 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2386 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2387 void spawnfunc_ctf_team()
2389 if(!g_ctf) { remove(self); return; }
2391 self.classname = "ctf_team";
2392 self.team = self.cnt + 1;
2395 // compatibility for quake maps
2396 void spawnfunc_team_CTF_redflag() { spawnfunc_item_flag_team1(); }
2397 void spawnfunc_team_CTF_blueflag() { spawnfunc_item_flag_team2(); }
2398 void spawnfunc_team_CTF_redplayer() { spawnfunc_info_player_team1(); }
2399 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2(); }
2400 void spawnfunc_team_CTF_redspawn() { spawnfunc_info_player_team1(); }
2401 void spawnfunc_team_CTF_bluespawn() { spawnfunc_info_player_team2(); }
2403 void team_CTF_neutralflag() { spawnfunc_item_flag_neutral(); }
2404 void team_neutralobelisk() { spawnfunc_item_flag_neutral(); }
2412 void ctf_ScoreRules(int teams)
2414 CheckAllowedTeams(world);
2415 ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
2416 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2417 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2418 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2419 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2420 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2421 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2422 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2423 ScoreRules_basics_end();
2426 // code from here on is just to support maps that don't have flag and team entities
2427 void ctf_SpawnTeam (string teamname, int teamcolor)
2432 self.classname = "ctf_team";
2433 self.netname = teamname;
2434 self.cnt = teamcolor;
2436 spawnfunc_ctf_team();
2441 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2446 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2448 if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2449 if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2450 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2453 ctf_teams = bound(2, ctf_teams, 4);
2455 // if no teams are found, spawn defaults
2456 if(find(world, classname, "ctf_team") == world)
2458 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2459 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2460 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2462 ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2464 ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2467 ctf_ScoreRules(ctf_teams);
2470 void ctf_Initialize()
2472 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2474 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2475 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2476 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2478 addstat(STAT_CTF_FLAGSTATUS, AS_INT, ctf_flagstatus);
2480 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2484 MUTATOR_DEFINITION(gamemode_ctf)
2486 MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2487 MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2488 MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2489 MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2490 MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2491 MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2492 MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2493 MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2494 MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2495 MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2496 MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2497 MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2498 MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2499 MUTATOR_HOOK(HavocBot_ChooseRole, ctf_BotRoles, CBC_ORDER_ANY);
2500 MUTATOR_HOOK(GetTeamCount, ctf_GetTeamCount, CBC_ORDER_ANY);
2501 MUTATOR_HOOK(SpectateCopy, ctf_SpectateCopy, CBC_ORDER_ANY);
2505 if(time > 1) // game loads at time 1
2506 error("This is a game type and it cannot be added at runtime.");
2510 MUTATOR_ONROLLBACK_OR_REMOVE
2512 // we actually cannot roll back ctf_Initialize here
2513 // BUT: we don't need to! If this gets called, adding always
2519 print("This is a game type and it cannot be removed at runtime.");