1 #include "gamemode_ctf.qh"
7 #include "../../common/vehicles/all.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, "models/ctf/shield.md3");
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 string teamname = Static_Team_ColorName_Lower(teamnumber);
1095 setself(flag); // for later usage with droptofloor()
1098 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1099 ctf_worldflaglist = flag;
1101 setattachment(flag, world, "");
1103 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1104 flag.team = teamnumber;
1105 flag.classname = "item_flag_team";
1106 flag.target = "###item###"; // wut?
1107 flag.flags = FL_ITEM | FL_NOTARGET;
1108 flag.solid = SOLID_TRIGGER;
1109 flag.takedamage = DAMAGE_NO;
1110 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1111 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1112 flag.health = flag.max_flag_health;
1113 flag.event_damage = ctf_FlagDamage;
1114 flag.pushable = true;
1115 flag.teleportable = TELEPORT_NORMAL;
1116 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP;
1117 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1118 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1119 flag.velocity = '0 0 0';
1120 flag.mangle = flag.angles;
1121 flag.reset = ctf_Reset;
1122 flag.touch = ctf_FlagTouch;
1123 flag.think = ctf_FlagThink;
1124 flag.nextthink = time + FLAG_THINKRATE;
1125 flag.ctf_status = FLAG_BASE;
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 set_flag_string(flag, snd_flag_taken, "ctf/%s_taken.wav", teamname);
1137 set_flag_string(flag, snd_flag_returned, "ctf/%s_returned.wav", teamname);
1138 set_flag_string(flag, snd_flag_capture, "ctf/%s_capture.wav", teamname);
1139 set_flag_string(flag, snd_flag_dropped, "ctf/%s_dropped.wav", teamname);
1140 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.
1141 if(flag.snd_flag_touch == "") { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
1142 if(flag.snd_flag_pass == "") { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
1145 precache_sound(flag.snd_flag_taken);
1146 precache_sound(flag.snd_flag_returned);
1147 precache_sound(flag.snd_flag_capture);
1148 precache_sound(flag.snd_flag_respawn);
1149 precache_sound(flag.snd_flag_dropped);
1150 precache_sound(flag.snd_flag_touch);
1151 precache_sound(flag.snd_flag_pass);
1152 precache_model(flag.model);
1153 precache_model("models/ctf/shield.md3");
1154 precache_model("models/ctf/shockwavetransring.md3");
1157 setmodel(flag, flag.model); // precision set below
1158 setsize(flag, FLAG_MIN, FLAG_MAX);
1159 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1161 if(autocvar_g_ctf_flag_glowtrails)
1165 case NUM_TEAM_1: flag.glow_color = 251; break;
1166 case NUM_TEAM_2: flag.glow_color = 210; break;
1167 case NUM_TEAM_3: flag.glow_color = 110; break;
1168 case NUM_TEAM_4: flag.glow_color = 145; break;
1169 default: flag.glow_color = 254; break;
1171 flag.glow_size = 25;
1172 flag.glow_trail = 1;
1175 flag.effects |= EF_LOWPRECISION;
1176 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1177 if(autocvar_g_ctf_dynamiclights)
1181 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1182 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1183 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1184 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1185 default: flag.effects |= EF_DIMLIGHT; break;
1190 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1192 flag.dropped_origin = flag.origin;
1193 flag.noalign = true;
1194 flag.movetype = MOVETYPE_NONE;
1196 else // drop to floor, automatically find a platform and set that as spawn origin
1198 flag.noalign = false;
1201 flag.movetype = MOVETYPE_TOSS;
1204 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1212 // NOTE: LEGACY CODE, needs to be re-written!
1214 void havocbot_calculate_middlepoint()
1218 vector fo = '0 0 0';
1221 f = ctf_worldflaglist;
1226 f = f.ctf_worldflagnext;
1230 havocbot_ctf_middlepoint = s * (1.0 / n);
1231 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1235 entity havocbot_ctf_find_flag(entity bot)
1238 f = ctf_worldflaglist;
1241 if (CTF_SAMETEAM(bot, f))
1243 f = f.ctf_worldflagnext;
1248 entity havocbot_ctf_find_enemy_flag(entity bot)
1251 f = ctf_worldflaglist;
1256 if(CTF_DIFFTEAM(bot, f))
1263 else if(!bot.flagcarried)
1267 else if (CTF_DIFFTEAM(bot, f))
1269 f = f.ctf_worldflagnext;
1274 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1282 FOR_EACH_PLAYER(head)
1284 if(DIFF_TEAM(head, bot) || head.deadflag != DEAD_NO || head == bot)
1287 if(vlen(head.origin - org) < tc_radius)
1294 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1297 head = ctf_worldflaglist;
1300 if (CTF_SAMETEAM(self, head))
1302 head = head.ctf_worldflagnext;
1305 navigation_routerating(head, ratingscale, 10000);
1308 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1311 head = ctf_worldflaglist;
1314 if (CTF_SAMETEAM(self, head))
1316 head = head.ctf_worldflagnext;
1321 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1324 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1327 head = ctf_worldflaglist;
1332 if(CTF_DIFFTEAM(self, head))
1336 if(self.flagcarried)
1339 else if(!self.flagcarried)
1343 else if(CTF_DIFFTEAM(self, head))
1345 head = head.ctf_worldflagnext;
1348 navigation_routerating(head, ratingscale, 10000);
1351 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1353 if (!bot_waypoints_for_items)
1355 havocbot_goalrating_ctf_enemyflag(ratingscale);
1361 head = havocbot_ctf_find_enemy_flag(self);
1366 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1369 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1373 mf = havocbot_ctf_find_flag(self);
1375 if(mf.ctf_status == FLAG_BASE)
1379 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1382 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1385 head = ctf_worldflaglist;
1388 // flag is out in the field
1389 if(head.ctf_status != FLAG_BASE)
1390 if(head.tag_entity==world) // dropped
1394 if(vlen(org-head.origin)<df_radius)
1395 navigation_routerating(head, ratingscale, 10000);
1398 navigation_routerating(head, ratingscale, 10000);
1401 head = head.ctf_worldflagnext;
1405 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1409 head = findchainfloat(bot_pickup, true);
1412 // gather health and armor only
1414 if (head.health || head.armorvalue)
1415 if (vlen(head.origin - org) < sradius)
1417 // get the value of the item
1418 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1420 navigation_routerating(head, t * ratingscale, 500);
1426 void havocbot_ctf_reset_role(entity bot)
1428 float cdefense, cmiddle, coffense;
1429 entity mf, ef, head;
1432 if(bot.deadflag != DEAD_NO)
1435 if(vlen(havocbot_ctf_middlepoint)==0)
1436 havocbot_calculate_middlepoint();
1439 if (bot.flagcarried)
1441 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1445 mf = havocbot_ctf_find_flag(bot);
1446 ef = havocbot_ctf_find_enemy_flag(bot);
1448 // Retrieve stolen flag
1449 if(mf.ctf_status!=FLAG_BASE)
1451 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1455 // If enemy flag is taken go to the middle to intercept pursuers
1456 if(ef.ctf_status!=FLAG_BASE)
1458 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1462 // if there is only me on the team switch to offense
1464 FOR_EACH_PLAYER(head)
1465 if(SAME_TEAM(head, bot))
1470 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1474 // Evaluate best position to take
1475 // Count mates on middle position
1476 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1478 // Count mates on defense position
1479 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1481 // Count mates on offense position
1482 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1484 if(cdefense<=coffense)
1485 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1486 else if(coffense<=cmiddle)
1487 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1489 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1492 void havocbot_role_ctf_carrier()
1494 if(self.deadflag != DEAD_NO)
1496 havocbot_ctf_reset_role(self);
1500 if (self.flagcarried == world)
1502 havocbot_ctf_reset_role(self);
1506 if (self.bot_strategytime < time)
1508 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1510 navigation_goalrating_start();
1512 havocbot_goalrating_ctf_enemybase(50000);
1514 havocbot_goalrating_ctf_ourbase(50000);
1517 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1519 navigation_goalrating_end();
1521 if (self.navigation_hasgoals)
1522 self.havocbot_cantfindflag = time + 10;
1523 else if (time > self.havocbot_cantfindflag)
1525 // Can't navigate to my own base, suicide!
1526 // TODO: drop it and wander around
1527 Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1533 void havocbot_role_ctf_escort()
1537 if(self.deadflag != DEAD_NO)
1539 havocbot_ctf_reset_role(self);
1543 if (self.flagcarried)
1545 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1549 // If enemy flag is back on the base switch to previous role
1550 ef = havocbot_ctf_find_enemy_flag(self);
1551 if(ef.ctf_status==FLAG_BASE)
1553 self.havocbot_role = self.havocbot_previous_role;
1554 self.havocbot_role_timeout = 0;
1558 // If the flag carrier reached the base switch to defense
1559 mf = havocbot_ctf_find_flag(self);
1560 if(mf.ctf_status!=FLAG_BASE)
1561 if(vlen(ef.origin - mf.dropped_origin) < 300)
1563 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1567 // Set the role timeout if necessary
1568 if (!self.havocbot_role_timeout)
1570 self.havocbot_role_timeout = time + random() * 30 + 60;
1573 // If nothing happened just switch to previous role
1574 if (time > self.havocbot_role_timeout)
1576 self.havocbot_role = self.havocbot_previous_role;
1577 self.havocbot_role_timeout = 0;
1581 // Chase the flag carrier
1582 if (self.bot_strategytime < time)
1584 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1585 navigation_goalrating_start();
1586 havocbot_goalrating_ctf_enemyflag(30000);
1587 havocbot_goalrating_ctf_ourstolenflag(40000);
1588 havocbot_goalrating_items(10000, self.origin, 10000);
1589 navigation_goalrating_end();
1593 void havocbot_role_ctf_offense()
1598 if(self.deadflag != DEAD_NO)
1600 havocbot_ctf_reset_role(self);
1604 if (self.flagcarried)
1606 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1611 mf = havocbot_ctf_find_flag(self);
1612 ef = havocbot_ctf_find_enemy_flag(self);
1615 if(mf.ctf_status!=FLAG_BASE)
1618 pos = mf.tag_entity.origin;
1622 // Try to get it if closer than the enemy base
1623 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1625 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1630 // Escort flag carrier
1631 if(ef.ctf_status!=FLAG_BASE)
1634 pos = ef.tag_entity.origin;
1638 if(vlen(pos-mf.dropped_origin)>700)
1640 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1645 // About to fail, switch to middlefield
1648 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1652 // Set the role timeout if necessary
1653 if (!self.havocbot_role_timeout)
1654 self.havocbot_role_timeout = time + 120;
1656 if (time > self.havocbot_role_timeout)
1658 havocbot_ctf_reset_role(self);
1662 if (self.bot_strategytime < time)
1664 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1665 navigation_goalrating_start();
1666 havocbot_goalrating_ctf_ourstolenflag(50000);
1667 havocbot_goalrating_ctf_enemybase(20000);
1668 havocbot_goalrating_items(5000, self.origin, 1000);
1669 havocbot_goalrating_items(1000, self.origin, 10000);
1670 navigation_goalrating_end();
1674 // Retriever (temporary role):
1675 void havocbot_role_ctf_retriever()
1679 if(self.deadflag != DEAD_NO)
1681 havocbot_ctf_reset_role(self);
1685 if (self.flagcarried)
1687 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1691 // If flag is back on the base switch to previous role
1692 mf = havocbot_ctf_find_flag(self);
1693 if(mf.ctf_status==FLAG_BASE)
1695 havocbot_ctf_reset_role(self);
1699 if (!self.havocbot_role_timeout)
1700 self.havocbot_role_timeout = time + 20;
1702 if (time > self.havocbot_role_timeout)
1704 havocbot_ctf_reset_role(self);
1708 if (self.bot_strategytime < time)
1713 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1714 navigation_goalrating_start();
1715 havocbot_goalrating_ctf_ourstolenflag(50000);
1716 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1717 havocbot_goalrating_ctf_enemybase(30000);
1718 havocbot_goalrating_items(500, self.origin, rt_radius);
1719 navigation_goalrating_end();
1723 void havocbot_role_ctf_middle()
1727 if(self.deadflag != DEAD_NO)
1729 havocbot_ctf_reset_role(self);
1733 if (self.flagcarried)
1735 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1739 mf = havocbot_ctf_find_flag(self);
1740 if(mf.ctf_status!=FLAG_BASE)
1742 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1746 if (!self.havocbot_role_timeout)
1747 self.havocbot_role_timeout = time + 10;
1749 if (time > self.havocbot_role_timeout)
1751 havocbot_ctf_reset_role(self);
1755 if (self.bot_strategytime < time)
1759 org = havocbot_ctf_middlepoint;
1760 org.z = self.origin.z;
1762 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1763 navigation_goalrating_start();
1764 havocbot_goalrating_ctf_ourstolenflag(50000);
1765 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1766 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1767 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1768 havocbot_goalrating_items(2500, self.origin, 10000);
1769 havocbot_goalrating_ctf_enemybase(2500);
1770 navigation_goalrating_end();
1774 void havocbot_role_ctf_defense()
1778 if(self.deadflag != DEAD_NO)
1780 havocbot_ctf_reset_role(self);
1784 if (self.flagcarried)
1786 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1790 // If own flag was captured
1791 mf = havocbot_ctf_find_flag(self);
1792 if(mf.ctf_status!=FLAG_BASE)
1794 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1798 if (!self.havocbot_role_timeout)
1799 self.havocbot_role_timeout = time + 30;
1801 if (time > self.havocbot_role_timeout)
1803 havocbot_ctf_reset_role(self);
1806 if (self.bot_strategytime < time)
1811 org = mf.dropped_origin;
1812 mp_radius = havocbot_ctf_middlepoint_radius;
1814 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1815 navigation_goalrating_start();
1817 // if enemies are closer to our base, go there
1818 entity head, closestplayer = world;
1819 float distance, bestdistance = 10000;
1820 FOR_EACH_PLAYER(head)
1822 if(head.deadflag!=DEAD_NO)
1825 distance = vlen(org - head.origin);
1826 if(distance<bestdistance)
1828 closestplayer = head;
1829 bestdistance = distance;
1834 if(DIFF_TEAM(closestplayer, self))
1835 if(vlen(org - self.origin)>1000)
1836 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1837 havocbot_goalrating_ctf_ourbase(30000);
1839 havocbot_goalrating_ctf_ourstolenflag(20000);
1840 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1841 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1842 havocbot_goalrating_items(10000, org, mp_radius);
1843 havocbot_goalrating_items(5000, self.origin, 10000);
1844 navigation_goalrating_end();
1848 void havocbot_role_ctf_setrole(entity bot, int role)
1850 LOG_TRACE(strcat(bot.netname," switched to "));
1853 case HAVOCBOT_CTF_ROLE_CARRIER:
1854 LOG_TRACE("carrier");
1855 bot.havocbot_role = havocbot_role_ctf_carrier;
1856 bot.havocbot_role_timeout = 0;
1857 bot.havocbot_cantfindflag = time + 10;
1858 bot.bot_strategytime = 0;
1860 case HAVOCBOT_CTF_ROLE_DEFENSE:
1861 LOG_TRACE("defense");
1862 bot.havocbot_role = havocbot_role_ctf_defense;
1863 bot.havocbot_role_timeout = 0;
1865 case HAVOCBOT_CTF_ROLE_MIDDLE:
1866 LOG_TRACE("middle");
1867 bot.havocbot_role = havocbot_role_ctf_middle;
1868 bot.havocbot_role_timeout = 0;
1870 case HAVOCBOT_CTF_ROLE_OFFENSE:
1871 LOG_TRACE("offense");
1872 bot.havocbot_role = havocbot_role_ctf_offense;
1873 bot.havocbot_role_timeout = 0;
1875 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1876 LOG_TRACE("retriever");
1877 bot.havocbot_previous_role = bot.havocbot_role;
1878 bot.havocbot_role = havocbot_role_ctf_retriever;
1879 bot.havocbot_role_timeout = time + 10;
1880 bot.bot_strategytime = 0;
1882 case HAVOCBOT_CTF_ROLE_ESCORT:
1883 LOG_TRACE("escort");
1884 bot.havocbot_previous_role = bot.havocbot_role;
1885 bot.havocbot_role = havocbot_role_ctf_escort;
1886 bot.havocbot_role_timeout = time + 30;
1887 bot.bot_strategytime = 0;
1898 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1901 int t = 0, t2 = 0, t3 = 0;
1903 // initially clear items so they can be set as necessary later.
1904 self.ctf_flagstatus &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
1905 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
1906 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
1907 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
1908 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
1909 | CTF_FLAG_NEUTRAL | CTF_SHIELDED);
1911 // scan through all the flags and notify the client about them
1912 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1914 if(flag.team == NUM_TEAM_1) { t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
1915 if(flag.team == NUM_TEAM_2) { t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
1916 if(flag.team == NUM_TEAM_3) { t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
1917 if(flag.team == NUM_TEAM_4) { t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
1918 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; }
1920 switch(flag.ctf_status)
1925 if((flag.owner == self) || (flag.pass_sender == self))
1926 self.ctf_flagstatus |= t; // carrying: self is currently carrying the flag
1928 self.ctf_flagstatus |= t2; // taken: someone else is carrying the flag
1933 self.ctf_flagstatus |= t3; // lost: the flag is dropped somewhere on the map
1939 // item for stopping players from capturing the flag too often
1940 if(self.ctf_captureshielded)
1941 self.ctf_flagstatus |= CTF_SHIELDED;
1943 // update the health of the flag carrier waypointsprite
1944 if(self.wps_flagcarrier)
1945 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
1950 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1952 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1954 if(frag_target == frag_attacker) // damage done to yourself
1956 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1957 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1959 else // damage done to everyone else
1961 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1962 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1965 else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
1967 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)))
1968 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1970 frag_target.wps_helpme_time = time;
1971 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1973 // todo: add notification for when flag carrier needs help?
1978 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1980 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
1982 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1983 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1986 if(frag_target.flagcarried)
1988 entity tmp_entity = frag_target.flagcarried;
1989 ctf_Handle_Throw(frag_target, world, DROP_NORMAL);
1990 tmp_entity.ctf_dropper = world;
1996 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1999 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2002 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
2004 entity flag; // temporary entity for the search method
2006 if(self.flagcarried)
2007 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2009 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2011 if(flag.pass_sender == self) { flag.pass_sender = world; }
2012 if(flag.pass_target == self) { flag.pass_target = world; }
2013 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
2019 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
2021 if(self.flagcarried)
2022 if(!autocvar_g_ctf_portalteleport)
2023 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2028 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
2030 if(MUTATOR_RETURNVALUE || gameover) { return false; }
2032 entity player = self;
2034 if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2036 // pass the flag to a team mate
2037 if(autocvar_g_ctf_pass)
2039 entity head, closest_target = world;
2040 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2042 while(head) // find the closest acceptable target to pass to
2044 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
2045 if(head != player && SAME_TEAM(head, player))
2046 if(!head.speedrunning && !head.vehicle)
2048 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2049 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2050 vector passer_center = CENTER_OR_VIEWOFS(player);
2052 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2054 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2056 if(IS_BOT_CLIENT(head))
2058 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2059 ctf_Handle_Throw(head, player, DROP_PASS);
2063 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2064 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2066 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2069 else if(player.flagcarried)
2073 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2074 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
2075 { closest_target = head; }
2077 else { closest_target = head; }
2084 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2087 // throw the flag in front of you
2088 if(autocvar_g_ctf_throw && player.flagcarried)
2090 if(player.throw_count == -1)
2092 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2094 player.throw_prevtime = time;
2095 player.throw_count = 1;
2096 ctf_Handle_Throw(player, world, DROP_THROW);
2101 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2107 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2108 else { player.throw_count += 1; }
2109 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2111 player.throw_prevtime = time;
2112 ctf_Handle_Throw(player, world, DROP_THROW);
2121 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
2123 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2125 self.wps_helpme_time = time;
2126 WaypointSprite_HelpMePing(self.wps_flagcarrier);
2128 else // create a normal help me waypointsprite
2130 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, self, FLAG_WAYPOINT_OFFSET, world, self.team, self, wps_helpme, false, RADARICON_HELPME);
2131 WaypointSprite_Ping(self.wps_helpme);
2137 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
2139 if(vh_player.flagcarried)
2141 vh_player.flagcarried.nodrawtoclient = vh_player; // hide the flag from the driver
2143 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2145 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
2149 setattachment(vh_player.flagcarried, vh_vehicle, "");
2150 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
2151 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2152 //vh_player.flagcarried.angles = '0 0 0';
2160 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
2162 if(vh_player.flagcarried)
2164 setattachment(vh_player.flagcarried, vh_player, "");
2165 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2166 vh_player.flagcarried.scale = FLAG_SCALE;
2167 vh_player.flagcarried.angles = '0 0 0';
2168 vh_player.flagcarried.nodrawtoclient = world;
2175 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
2177 if(self.flagcarried)
2179 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));
2180 ctf_RespawnFlag(self.flagcarried);
2187 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
2189 entity flag; // temporary entity for the search method
2191 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2193 switch(flag.ctf_status)
2198 // lock the flag, game is over
2199 flag.movetype = MOVETYPE_NONE;
2200 flag.takedamage = DAMAGE_NO;
2201 flag.solid = SOLID_NOT;
2202 flag.nextthink = false; // stop thinking
2204 //dprint("stopping the ", flag.netname, " from moving.\n");
2212 // do nothing for these flags
2221 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
2223 havocbot_ctf_reset_role(self);
2227 MUTATOR_HOOKFUNCTION(ctf_GetTeamCount)
2229 //ret_float = ctf_teams;
2230 ret_string = "ctf_team";
2234 MUTATOR_HOOKFUNCTION(ctf_SpectateCopy)
2236 self.ctf_flagstatus = other.ctf_flagstatus;
2245 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2246 CTF flag for team one (Red).
2248 "angle" Angle the flag will point (minus 90 degrees)...
2249 "model" model to use, note this needs red and blue as skins 0 and 1...
2250 "noise" sound played when flag is picked up...
2251 "noise1" sound played when flag is returned by a teammate...
2252 "noise2" sound played when flag is captured...
2253 "noise3" sound played when flag is lost in the field and respawns itself...
2254 "noise4" sound played when flag is dropped by a player...
2255 "noise5" sound played when flag touches the ground... */
2256 void spawnfunc_item_flag_team1()
2258 if(!g_ctf) { remove(self); return; }
2260 ctf_FlagSetup(NUM_TEAM_1, self);
2263 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2264 CTF flag for team two (Blue).
2266 "angle" Angle the flag will point (minus 90 degrees)...
2267 "model" model to use, note this needs red and blue as skins 0 and 1...
2268 "noise" sound played when flag is picked up...
2269 "noise1" sound played when flag is returned by a teammate...
2270 "noise2" sound played when flag is captured...
2271 "noise3" sound played when flag is lost in the field and respawns itself...
2272 "noise4" sound played when flag is dropped by a player...
2273 "noise5" sound played when flag touches the ground... */
2274 void spawnfunc_item_flag_team2()
2276 if(!g_ctf) { remove(self); return; }
2278 ctf_FlagSetup(NUM_TEAM_2, self);
2281 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2282 CTF flag for team three (Yellow).
2284 "angle" Angle the flag will point (minus 90 degrees)...
2285 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2286 "noise" sound played when flag is picked up...
2287 "noise1" sound played when flag is returned by a teammate...
2288 "noise2" sound played when flag is captured...
2289 "noise3" sound played when flag is lost in the field and respawns itself...
2290 "noise4" sound played when flag is dropped by a player...
2291 "noise5" sound played when flag touches the ground... */
2292 void spawnfunc_item_flag_team3()
2294 if(!g_ctf) { remove(self); return; }
2296 ctf_FlagSetup(NUM_TEAM_3, self);
2299 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2300 CTF flag for team four (Pink).
2302 "angle" Angle the flag will point (minus 90 degrees)...
2303 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2304 "noise" sound played when flag is picked up...
2305 "noise1" sound played when flag is returned by a teammate...
2306 "noise2" sound played when flag is captured...
2307 "noise3" sound played when flag is lost in the field and respawns itself...
2308 "noise4" sound played when flag is dropped by a player...
2309 "noise5" sound played when flag touches the ground... */
2310 void spawnfunc_item_flag_team4()
2312 if(!g_ctf) { remove(self); return; }
2314 ctf_FlagSetup(NUM_TEAM_4, self);
2317 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2320 "angle" Angle the flag will point (minus 90 degrees)...
2321 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2322 "noise" sound played when flag is picked up...
2323 "noise1" sound played when flag is returned by a teammate...
2324 "noise2" sound played when flag is captured...
2325 "noise3" sound played when flag is lost in the field and respawns itself...
2326 "noise4" sound played when flag is dropped by a player...
2327 "noise5" sound played when flag touches the ground... */
2328 void spawnfunc_item_flag_neutral()
2330 if(!g_ctf) { remove(self); return; }
2331 if(!cvar("g_ctf_oneflag")) { remove(self); return; }
2333 ctf_FlagSetup(0, self);
2336 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2337 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2338 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.
2340 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2341 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2342 void spawnfunc_ctf_team()
2344 if(!g_ctf) { remove(self); return; }
2346 self.classname = "ctf_team";
2347 self.team = self.cnt + 1;
2350 // compatibility for quake maps
2351 void spawnfunc_team_CTF_redflag() { spawnfunc_item_flag_team1(); }
2352 void spawnfunc_team_CTF_blueflag() { spawnfunc_item_flag_team2(); }
2353 void spawnfunc_team_CTF_redplayer() { spawnfunc_info_player_team1(); }
2354 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2(); }
2355 void spawnfunc_team_CTF_redspawn() { spawnfunc_info_player_team1(); }
2356 void spawnfunc_team_CTF_bluespawn() { spawnfunc_info_player_team2(); }
2358 void team_CTF_neutralflag() { spawnfunc_item_flag_neutral(); }
2359 void team_neutralobelisk() { spawnfunc_item_flag_neutral(); }
2367 void ctf_ScoreRules(int teams)
2369 CheckAllowedTeams(world);
2370 ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
2371 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2372 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2373 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2374 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2375 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2376 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2377 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2378 ScoreRules_basics_end();
2381 // code from here on is just to support maps that don't have flag and team entities
2382 void ctf_SpawnTeam (string teamname, int teamcolor)
2385 self.classname = "ctf_team";
2386 self.netname = teamname;
2387 self.cnt = teamcolor;
2389 spawnfunc_ctf_team();
2394 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2399 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2401 if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2402 if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2403 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2406 ctf_teams = bound(2, ctf_teams, 4);
2408 // if no teams are found, spawn defaults
2409 if(find(world, classname, "ctf_team") == world)
2411 LOG_INFO("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2412 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2413 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2415 ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2417 ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2420 ctf_ScoreRules(ctf_teams);
2423 void ctf_Initialize()
2425 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2427 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2428 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2429 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2431 addstat(STAT_CTF_FLAGSTATUS, AS_INT, ctf_flagstatus);
2433 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2437 MUTATOR_DEFINITION(gamemode_ctf)
2439 MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2440 MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2441 MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2442 MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2443 MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2444 MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2445 MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2446 MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2447 MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2448 MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2449 MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2450 MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2451 MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2452 MUTATOR_HOOK(HavocBot_ChooseRole, ctf_BotRoles, CBC_ORDER_ANY);
2453 MUTATOR_HOOK(GetTeamCount, ctf_GetTeamCount, CBC_ORDER_ANY);
2454 MUTATOR_HOOK(SpectateCopy, ctf_SpectateCopy, CBC_ORDER_ANY);
2458 if(time > 1) // game loads at time 1
2459 error("This is a game type and it cannot be added at runtime.");
2463 MUTATOR_ONROLLBACK_OR_REMOVE
2465 // we actually cannot roll back ctf_Initialize here
2466 // BUT: we don't need to! If this gets called, adding always
2472 LOG_INFO("This is a game type and it cannot be removed at runtime.");