1 #include "gamemode_ctf.qh"
6 #include "../../common/vehicles/all.qh"
7 #include "../teamplay.qh"
10 #include "../../lib/warpzone/common.qh"
12 bool autocvar_g_ctf_allow_vehicle_carry;
13 bool autocvar_g_ctf_allow_vehicle_touch;
14 bool autocvar_g_ctf_allow_monster_touch;
15 bool autocvar_g_ctf_throw;
16 float autocvar_g_ctf_throw_angle_max;
17 float autocvar_g_ctf_throw_angle_min;
18 int autocvar_g_ctf_throw_punish_count;
19 float autocvar_g_ctf_throw_punish_delay;
20 float autocvar_g_ctf_throw_punish_time;
21 float autocvar_g_ctf_throw_strengthmultiplier;
22 float autocvar_g_ctf_throw_velocity_forward;
23 float autocvar_g_ctf_throw_velocity_up;
24 float autocvar_g_ctf_drop_velocity_up;
25 float autocvar_g_ctf_drop_velocity_side;
26 bool autocvar_g_ctf_oneflag_reverse;
27 bool autocvar_g_ctf_portalteleport;
28 bool autocvar_g_ctf_pass;
29 float autocvar_g_ctf_pass_arc;
30 float autocvar_g_ctf_pass_arc_max;
31 float autocvar_g_ctf_pass_directional_max;
32 float autocvar_g_ctf_pass_directional_min;
33 float autocvar_g_ctf_pass_radius;
34 float autocvar_g_ctf_pass_wait;
35 bool autocvar_g_ctf_pass_request;
36 float autocvar_g_ctf_pass_turnrate;
37 float autocvar_g_ctf_pass_timelimit;
38 float autocvar_g_ctf_pass_velocity;
39 bool autocvar_g_ctf_dynamiclights;
40 float autocvar_g_ctf_flag_collect_delay;
41 float autocvar_g_ctf_flag_damageforcescale;
42 bool autocvar_g_ctf_flag_dropped_waypoint;
43 bool autocvar_g_ctf_flag_dropped_floatinwater;
44 bool autocvar_g_ctf_flag_glowtrails;
45 int autocvar_g_ctf_flag_health;
46 bool autocvar_g_ctf_flag_return;
47 float autocvar_g_ctf_flag_return_carried_radius;
48 float autocvar_g_ctf_flag_return_time;
49 bool autocvar_g_ctf_flag_return_when_unreachable;
50 float autocvar_g_ctf_flag_return_damage;
51 float autocvar_g_ctf_flag_return_damage_delay;
52 float autocvar_g_ctf_flag_return_dropped;
53 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
54 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
55 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
56 float autocvar_g_ctf_flagcarrier_selfforcefactor;
57 float autocvar_g_ctf_flagcarrier_damagefactor;
58 float autocvar_g_ctf_flagcarrier_forcefactor;
59 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
60 bool autocvar_g_ctf_fullbrightflags;
61 bool autocvar_g_ctf_ignore_frags;
62 int autocvar_g_ctf_score_capture;
63 int autocvar_g_ctf_score_capture_assist;
64 int autocvar_g_ctf_score_kill;
65 int autocvar_g_ctf_score_penalty_drop;
66 int autocvar_g_ctf_score_penalty_returned;
67 int autocvar_g_ctf_score_pickup_base;
68 int autocvar_g_ctf_score_pickup_dropped_early;
69 int autocvar_g_ctf_score_pickup_dropped_late;
70 int autocvar_g_ctf_score_return;
71 float autocvar_g_ctf_shield_force;
72 float autocvar_g_ctf_shield_max_ratio;
73 int autocvar_g_ctf_shield_min_negscore;
74 bool autocvar_g_ctf_stalemate;
75 int autocvar_g_ctf_stalemate_endcondition;
76 float autocvar_g_ctf_stalemate_time;
77 bool autocvar_g_ctf_reverse;
78 float autocvar_g_ctf_dropped_capture_delay;
79 float autocvar_g_ctf_dropped_capture_radius;
81 void ctf_FakeTimeLimit(entity e, float t)
84 WriteByte(MSG_ONE, 3); // svc_updatestat
85 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
87 WriteCoord(MSG_ONE, autocvar_timelimit);
89 WriteCoord(MSG_ONE, (t + 1) / 60);
92 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
94 if(autocvar_sv_eventlog)
95 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != world) ? ftos(actor.playerid) : "")));
96 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
99 void ctf_CaptureRecord(entity flag, entity player)
101 float cap_record = ctf_captimerecord;
102 float cap_time = (time - flag.ctf_pickuptime);
103 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
106 if(ctf_oneflag) { Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname); }
107 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)); }
108 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)); }
109 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)); }
111 // write that shit in the database
112 if(!ctf_oneflag) // but not in 1-flag mode
113 if((!ctf_captimerecord) || (cap_time < cap_record))
115 ctf_captimerecord = cap_time;
116 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
117 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
118 write_recordmarker(player, (time - cap_time), cap_time);
122 void ctf_FlagcarrierWaypoints(entity player)
124 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
125 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
126 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
127 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
130 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
132 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
133 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
134 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
135 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
138 if(current_height) // make sure we can actually do this arcing path
140 targpos = (to + ('0 0 1' * current_height));
141 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
142 if(trace_fraction < 1)
144 //print("normal arc line failed, trying to find new pos...");
145 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
146 targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
147 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
148 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
149 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
152 else { targpos = to; }
154 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
156 vector desired_direction = normalize(targpos - from);
157 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
158 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
161 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
163 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
165 // directional tracing only
167 makevectors(passer_angle);
169 // find the closest point on the enemy to the center of the attack
170 float h; // hypotenuse, which is the distance between attacker to head
171 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
173 h = vlen(head_center - passer_center);
174 a = h * (normalize(head_center - passer_center) * v_forward);
176 vector nearest_on_line = (passer_center + a * v_forward);
177 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
179 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
180 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
182 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
187 else { return true; }
191 // =======================
192 // CaptureShield Functions
193 // =======================
195 bool ctf_CaptureShield_CheckStatus(entity p)
197 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
199 int players_worseeq, players_total;
201 if(ctf_captureshield_max_ratio <= 0)
204 s = PlayerScore_Add(p, SP_CTF_CAPS, 0);
205 s2 = PlayerScore_Add(p, SP_CTF_PICKUPS, 0);
206 s3 = PlayerScore_Add(p, SP_CTF_RETURNS, 0);
207 s4 = PlayerScore_Add(p, SP_CTF_FCKILLS, 0);
209 sr = ((s - s2) + (s3 + s4));
211 if(sr >= -ctf_captureshield_min_negscore)
214 players_total = players_worseeq = 0;
219 se = PlayerScore_Add(e, SP_CTF_CAPS, 0);
220 se2 = PlayerScore_Add(e, SP_CTF_PICKUPS, 0);
221 se3 = PlayerScore_Add(e, SP_CTF_RETURNS, 0);
222 se4 = PlayerScore_Add(e, SP_CTF_FCKILLS, 0);
224 ser = ((se - se2) + (se3 + se4));
231 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
232 // use this rule here
234 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
240 void ctf_CaptureShield_Update(entity player, bool wanted_status)
242 bool updated_status = ctf_CaptureShield_CheckStatus(player);
243 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
245 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
246 player.ctf_captureshielded = updated_status;
250 bool ctf_CaptureShield_Customize()
252 if(!other.ctf_captureshielded) { return false; }
253 if(CTF_SAMETEAM(self, other)) { return false; }
258 void ctf_CaptureShield_Touch()
260 if(!other.ctf_captureshielded) { return; }
261 if(CTF_SAMETEAM(self, other)) { return; }
263 vector mymid = (self.absmin + self.absmax) * 0.5;
264 vector othermid = (other.absmin + other.absmax) * 0.5;
266 Damage(other, self, self, 0, DEATH_HURTTRIGGER.m_id, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
267 if(IS_REAL_CLIENT(other)) { Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
270 void ctf_CaptureShield_Spawn(entity flag)
272 entity shield = spawn();
275 shield.team = self.team;
276 shield.touch = ctf_CaptureShield_Touch;
277 shield.customizeentityforclient = ctf_CaptureShield_Customize;
278 shield.classname = "ctf_captureshield";
279 shield.effects = EF_ADDITIVE;
280 shield.movetype = MOVETYPE_NOCLIP;
281 shield.solid = SOLID_TRIGGER;
282 shield.avelocity = '7 0 11';
285 setorigin(shield, self.origin);
286 setmodel(shield, MDL_CTF_SHIELD);
287 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
291 // ====================
292 // Drop/Pass/Throw Code
293 // ====================
295 void ctf_Handle_Drop(entity flag, entity player, int droptype)
298 player = (player ? player : flag.pass_sender);
301 flag.movetype = MOVETYPE_TOSS;
302 flag.takedamage = DAMAGE_YES;
303 flag.angles = '0 0 0';
304 flag.health = flag.max_flag_health;
305 flag.ctf_droptime = time;
306 flag.ctf_dropper = player;
307 flag.ctf_status = FLAG_DROPPED;
309 // messages and sounds
310 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_LOST_) : INFO_CTF_LOST_NEUTRAL), player.netname);
311 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
312 ctf_EventLog("dropped", player.team, player);
315 PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);
316 PlayerScore_Add(player, SP_CTF_DROPS, 1);
319 if(autocvar_g_ctf_flag_dropped_waypoint) {
320 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);
321 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
324 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
326 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
327 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
330 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
332 if(droptype == DROP_PASS)
334 flag.pass_distance = 0;
335 flag.pass_sender = world;
336 flag.pass_target = world;
340 void ctf_Handle_Retrieve(entity flag, entity player)
342 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
343 entity sender = flag.pass_sender;
345 // transfer flag to player
347 flag.owner.flagcarried = flag;
352 setattachment(flag, player.vehicle, "");
353 setorigin(flag, VEHICLE_FLAG_OFFSET);
354 flag.scale = VEHICLE_FLAG_SCALE;
358 setattachment(flag, player, "");
359 setorigin(flag, FLAG_CARRY_OFFSET);
361 flag.movetype = MOVETYPE_NONE;
362 flag.takedamage = DAMAGE_NO;
363 flag.solid = SOLID_NOT;
364 flag.angles = '0 0 0';
365 flag.ctf_status = FLAG_CARRY;
367 // messages and sounds
368 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
369 ctf_EventLog("receive", flag.team, player);
371 FOR_EACH_REALPLAYER(tmp_player)
373 if(tmp_player == sender)
374 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);
375 else if(tmp_player == player)
376 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);
377 else if(SAME_TEAM(tmp_player, sender))
378 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);
381 // create new waypoint
382 ctf_FlagcarrierWaypoints(player);
384 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
385 player.throw_antispam = sender.throw_antispam;
387 flag.pass_distance = 0;
388 flag.pass_sender = world;
389 flag.pass_target = world;
392 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
394 entity flag = player.flagcarried;
395 vector targ_origin, flag_velocity;
397 if(!flag) { return; }
398 if((droptype == DROP_PASS) && !receiver) { return; }
400 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
403 setattachment(flag, world, "");
404 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
405 flag.owner.flagcarried = world;
407 flag.solid = SOLID_TRIGGER;
408 flag.ctf_dropper = player;
409 flag.ctf_droptime = time;
411 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
418 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
419 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
420 WarpZone_RefSys_Copy(flag, receiver);
421 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
422 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
424 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
425 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
428 flag.movetype = MOVETYPE_FLY;
429 flag.takedamage = DAMAGE_NO;
430 flag.pass_sender = player;
431 flag.pass_target = receiver;
432 flag.ctf_status = FLAG_PASSING;
435 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
436 WarpZone_TrailParticles(world, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
437 ctf_EventLog("pass", flag.team, player);
443 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'));
445 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)));
446 flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, false);
447 ctf_Handle_Drop(flag, player, droptype);
453 flag.velocity = '0 0 0'; // do nothing
460 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);
461 ctf_Handle_Drop(flag, player, droptype);
466 // kill old waypointsprite
467 WaypointSprite_Ping(player.wps_flagcarrier);
468 WaypointSprite_Kill(player.wps_flagcarrier);
470 if(player.wps_enemyflagcarrier)
471 WaypointSprite_Kill(player.wps_enemyflagcarrier);
474 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
482 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
484 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
485 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
486 entity player_team_flag = world, tmp_entity;
487 float old_time, new_time;
489 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
490 if(CTF_DIFFTEAM(player, flag)) { return; }
493 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
494 if(SAME_TEAM(tmp_entity, player))
496 player_team_flag = tmp_entity;
500 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
502 player.throw_prevtime = time;
503 player.throw_count = 0;
505 // messages and sounds
506 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((enemy_flag.team) ? APP_TEAM_ENT_4(enemy_flag, CENTER_CTF_CAPTURE_) : CENTER_CTF_CAPTURE_NEUTRAL));
507 ctf_CaptureRecord(enemy_flag, player);
508 _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);
512 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
513 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
518 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
519 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
521 old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
522 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
523 if(!old_time || new_time < old_time)
524 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
527 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
528 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
531 if(capturetype == CAPTURE_NORMAL)
533 WaypointSprite_Kill(player.wps_flagcarrier);
534 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
536 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
537 { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
541 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
542 ctf_RespawnFlag(enemy_flag);
545 void ctf_Handle_Return(entity flag, entity player)
547 // messages and sounds
548 if(IS_MONSTER(player))
550 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_MONSTER_), player.monster_name);
554 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_RETURN_));
555 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_), player.netname);
557 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
558 ctf_EventLog("return", flag.team, player);
561 if(IS_PLAYER(player))
563 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
564 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
566 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
569 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
573 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
574 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
575 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
579 if(player.flagcarried == flag)
580 WaypointSprite_Kill(player.wps_flagcarrier);
583 ctf_RespawnFlag(flag);
586 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
589 float pickup_dropped_score; // used to calculate dropped pickup score
590 entity tmp_entity; // temporary entity
592 // attach the flag to the player
594 player.flagcarried = flag;
597 setattachment(flag, player.vehicle, "");
598 setorigin(flag, VEHICLE_FLAG_OFFSET);
599 flag.scale = VEHICLE_FLAG_SCALE;
603 setattachment(flag, player, "");
604 setorigin(flag, FLAG_CARRY_OFFSET);
608 flag.movetype = MOVETYPE_NONE;
609 flag.takedamage = DAMAGE_NO;
610 flag.solid = SOLID_NOT;
611 flag.angles = '0 0 0';
612 flag.ctf_status = FLAG_CARRY;
616 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
617 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
621 // messages and sounds
622 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_PICKUP_) : INFO_CTF_PICKUP_NEUTRAL), player.netname);
623 if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
624 if(!flag.team) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL); }
625 else if(CTF_DIFFTEAM(player, flag)) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PICKUP_)); }
626 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)); }
628 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);
631 FOR_EACH_PLAYER(tmp_entity)
632 if(tmp_entity != player)
633 if(DIFF_TEAM(player, tmp_entity))
634 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, CHOICE_CTF_PICKUP_ENEMY_NEUTRAL, Team_ColorCode(player.team), player.netname);
637 FOR_EACH_PLAYER(tmp_entity)
638 if(tmp_entity != player)
639 if(CTF_SAMETEAM(flag, tmp_entity))
640 if(SAME_TEAM(player, tmp_entity))
641 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_PICKUP_TEAM_), Team_ColorCode(player.team), player.netname);
643 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);
645 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
648 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
649 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
654 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
655 ctf_EventLog("steal", flag.team, player);
661 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);
662 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);
663 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
664 PlayerTeamScore_AddScore(player, pickup_dropped_score);
665 ctf_EventLog("pickup", flag.team, player);
673 if(pickuptype == PICKUP_BASE)
675 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
676 if((player.speedrunning) && (ctf_captimerecord))
677 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
681 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
684 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
685 ctf_FlagcarrierWaypoints(player);
686 WaypointSprite_Ping(player.wps_flagcarrier);
690 // ===================
691 // Main Flag Functions
692 // ===================
694 void ctf_CheckFlagReturn(entity flag, int returntype)
696 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
698 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
700 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
704 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;
705 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;
706 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;
707 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;
711 { Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_TIMEOUT_) : INFO_CTF_FLAGRETURN_TIMEOUT_NEUTRAL)); break; }
713 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
714 ctf_EventLog("returned", flag.team, world);
715 ctf_RespawnFlag(flag);
720 bool ctf_Stalemate_Customize()
722 // make spectators see what the player would see
724 e = WaypointSprite_getviewentity(other);
725 wp_owner = self.owner;
728 if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
729 if(SAME_TEAM(wp_owner, e)) { return false; }
730 if(!IS_PLAYER(e)) { return false; }
735 void ctf_CheckStalemate(void)
738 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
741 entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
743 // build list of stale flags
744 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
746 if(autocvar_g_ctf_stalemate)
747 if(tmp_entity.ctf_status != FLAG_BASE)
748 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
750 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
751 ctf_staleflaglist = tmp_entity;
753 switch(tmp_entity.team)
755 case NUM_TEAM_1: ++stale_red_flags; break;
756 case NUM_TEAM_2: ++stale_blue_flags; break;
757 case NUM_TEAM_3: ++stale_yellow_flags; break;
758 case NUM_TEAM_4: ++stale_pink_flags; break;
759 default: ++stale_neutral_flags; break;
765 stale_flags = (stale_neutral_flags >= 1);
767 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
769 if(ctf_oneflag && stale_flags == 1)
770 ctf_stalemate = true;
771 else if(stale_flags >= 2)
772 ctf_stalemate = true;
773 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
774 { ctf_stalemate = false; wpforenemy_announced = false; }
775 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
776 { ctf_stalemate = false; wpforenemy_announced = false; }
778 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
781 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
783 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
785 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);
786 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
787 tmp_entity.owner.wps_enemyflagcarrier.customizeentityforclient = ctf_Stalemate_Customize;
791 if (!wpforenemy_announced)
793 FOR_EACH_REALPLAYER(tmp_entity)
794 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CENTER, ((tmp_entity.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER));
796 wpforenemy_announced = true;
801 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
803 if(ITEM_DAMAGE_NEEDKILL(deathtype))
805 if(autocvar_g_ctf_flag_return_damage_delay)
807 self.ctf_flagdamaged = true;
812 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
816 if(autocvar_g_ctf_flag_return_damage)
818 // reduce health and check if it should be returned
819 self.health = self.health - damage;
820 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
830 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
833 if(self == ctf_worldflaglist) // only for the first flag
834 FOR_EACH_CLIENT(tmp_entity)
835 ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
838 if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
839 LOG_TRACE("wtf the flag got squashed?\n");
840 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
841 if(!trace_startsolid || self.noalign) // can we resize it without getting stuck?
842 setsize(self, FLAG_MIN, FLAG_MAX); }
844 switch(self.ctf_status) // reset flag angles in case warpzones adjust it
848 self.angles = '0 0 0';
856 switch(self.ctf_status)
860 if(autocvar_g_ctf_dropped_capture_radius)
862 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
863 if(tmp_entity.ctf_status == FLAG_DROPPED)
864 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
865 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
866 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
873 if(autocvar_g_ctf_flag_dropped_floatinwater)
875 vector midpoint = ((self.absmin + self.absmax) * 0.5);
876 if(pointcontents(midpoint) == CONTENT_WATER)
878 self.velocity = self.velocity * 0.5;
880 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
881 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
883 { self.movetype = MOVETYPE_FLY; }
885 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
887 if(autocvar_g_ctf_flag_return_dropped)
889 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
892 ctf_CheckFlagReturn(self, RETURN_DROPPED);
896 if(self.ctf_flagdamaged)
898 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
899 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
902 else if(autocvar_g_ctf_flag_return_time)
904 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
905 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
913 if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
916 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
919 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
923 if(autocvar_g_ctf_stalemate)
925 if(time >= wpforenemy_nextthink)
927 ctf_CheckStalemate();
928 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
931 if(CTF_SAMETEAM(self, self.owner) && self.team)
933 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
934 ctf_Handle_Throw(self.owner, world, DROP_THROW);
935 else if(vlen(self.owner.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_carried_radius)
936 ctf_Handle_Return(self, self.owner);
943 vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
944 targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
945 WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
947 if((self.pass_target == world)
948 || (self.pass_target.deadflag != DEAD_NO)
949 || (self.pass_target.flagcarried)
950 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
951 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
952 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
954 // give up, pass failed
955 ctf_Handle_Drop(self, world, DROP_PASS);
959 // still a viable target, go for it
960 ctf_CalculatePassVelocity(self, targ_origin, self.origin, true);
965 default: // this should never happen
967 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?\n");
975 if(gameover) { return; }
976 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
978 entity toucher = other, tmp_entity;
979 bool is_not_monster = (!IS_MONSTER(toucher)), num_perteam = 0;
981 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
982 if(ITEM_TOUCH_NEEDKILL())
984 if(!autocvar_g_ctf_flag_return_damage_delay)
987 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
989 if(!self.ctf_flagdamaged) { return; }
992 FOR_EACH_PLAYER(tmp_entity) if(SAME_TEAM(toucher, tmp_entity)) { ++num_perteam; }
994 // special touch behaviors
995 if(toucher.frozen) { return; }
996 else if(IS_VEHICLE(toucher))
998 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
999 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1001 return; // do nothing
1003 else if(IS_MONSTER(toucher))
1005 if(!autocvar_g_ctf_allow_monster_touch)
1006 return; // do nothing
1008 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1010 if(time > self.wait) // if we haven't in a while, play a sound/effect
1012 Send_Effect_(self.toucheffect, self.origin, '0 0 0', 1);
1013 _sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1014 self.wait = time + FLAG_TOUCHRATE;
1018 else if(toucher.deadflag != DEAD_NO) { return; }
1020 switch(self.ctf_status)
1026 if(CTF_SAMETEAM(toucher, self) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1027 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1028 else if(!self.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1029 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1031 else if(CTF_SAMETEAM(toucher, self) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, self) && is_not_monster)
1032 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1033 else if(CTF_DIFFTEAM(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1034 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1040 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
1041 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
1042 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1043 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1049 LOG_TRACE("Someone touched a flag even though it was being carried?\n");
1055 if((IS_PLAYER(toucher)) && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
1057 if(DIFF_TEAM(toucher, self.pass_sender))
1058 ctf_Handle_Return(self, toucher);
1060 ctf_Handle_Retrieve(self, toucher);
1067 .float last_respawn;
1068 void ctf_RespawnFlag(entity flag)
1070 // check for flag respawn being called twice in a row
1071 if(flag.last_respawn > time - 0.5)
1072 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1074 flag.last_respawn = time;
1076 // reset the player (if there is one)
1077 if((flag.owner) && (flag.owner.flagcarried == flag))
1079 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1080 WaypointSprite_Kill(flag.wps_flagcarrier);
1082 flag.owner.flagcarried = world;
1084 if(flag.speedrunning)
1085 ctf_FakeTimeLimit(flag.owner, -1);
1088 if((flag.owner) && (flag.owner.vehicle))
1089 flag.scale = FLAG_SCALE;
1091 if(flag.ctf_status == FLAG_DROPPED)
1092 { WaypointSprite_Kill(flag.wps_flagdropped); }
1095 setattachment(flag, world, "");
1096 setorigin(flag, flag.ctf_spawnorigin);
1098 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
1099 flag.takedamage = DAMAGE_NO;
1100 flag.health = flag.max_flag_health;
1101 flag.solid = SOLID_TRIGGER;
1102 flag.velocity = '0 0 0';
1103 flag.angles = flag.mangle;
1104 flag.flags = FL_ITEM | FL_NOTARGET;
1106 flag.ctf_status = FLAG_BASE;
1108 flag.pass_distance = 0;
1109 flag.pass_sender = world;
1110 flag.pass_target = world;
1111 flag.ctf_dropper = world;
1112 flag.ctf_pickuptime = 0;
1113 flag.ctf_droptime = 0;
1114 flag.ctf_flagdamaged = 0;
1116 ctf_CheckStalemate();
1122 if(IS_PLAYER(self.owner))
1123 ctf_Handle_Throw(self.owner, world, DROP_RESET);
1125 ctf_RespawnFlag(self);
1128 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
1131 waypoint_spawnforitem_force(self, self.origin);
1132 self.nearestwaypointtimeout = 0; // activate waypointing again
1133 self.bot_basewaypoint = self.nearestwaypoint;
1139 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1140 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1141 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1142 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1143 default: basename = WP_FlagBaseNeutral; break;
1146 entity wp = WaypointSprite_SpawnFixed(basename, self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG);
1147 wp.colormod = ((self.team) ? Team_ColorRGB(self.team) : '1 1 1');
1148 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, ((self.team) ? colormapPaletteColor(self.team - 1, false) : '1 1 1'));
1150 // captureshield setup
1151 ctf_CaptureShield_Spawn(self);
1154 void set_flag_string(entity flag, .string field, string value, string teamname)
1156 if(flag.(field) == "")
1157 flag.(field) = strzone(sprintf(value,teamname));
1160 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1163 setself(flag); // for later usage with droptofloor()
1166 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1167 ctf_worldflaglist = flag;
1169 setattachment(flag, world, "");
1171 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1172 flag.team = teamnumber;
1173 flag.classname = "item_flag_team";
1174 flag.target = "###item###"; // wut?
1175 flag.flags = FL_ITEM | FL_NOTARGET;
1176 flag.solid = SOLID_TRIGGER;
1177 flag.takedamage = DAMAGE_NO;
1178 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1179 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1180 flag.health = flag.max_flag_health;
1181 flag.event_damage = ctf_FlagDamage;
1182 flag.pushable = true;
1183 flag.teleportable = TELEPORT_NORMAL;
1184 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP;
1185 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1186 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1187 flag.velocity = '0 0 0';
1188 flag.mangle = flag.angles;
1189 flag.reset = ctf_Reset;
1190 flag.touch = ctf_FlagTouch;
1191 flag.think = ctf_FlagThink;
1192 flag.nextthink = time + FLAG_THINKRATE;
1193 flag.ctf_status = FLAG_BASE;
1195 string teamname = Static_Team_ColorName_Lower(teamnumber);
1197 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1198 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1199 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1200 set_flag_string(flag, toucheffect, "%sflag_touch", teamname);
1201 set_flag_string(flag, passeffect, "%s_pass", teamname);
1202 set_flag_string(flag, capeffect, "%s_cap", teamname);
1205 flag.snd_flag_taken = SND(CTF_TAKEN(teamnumber));
1206 flag.snd_flag_returned = SND(CTF_RETURNED(teamnumber));
1207 flag.snd_flag_capture = SND(CTF_CAPTURE(teamnumber));
1208 flag.snd_flag_dropped = SND(CTF_DROPPED(teamnumber));
1209 if (flag.snd_flag_respawn == "") flag.snd_flag_respawn = SND(CTF_RESPAWN); // if there is ever a team-based sound for this, update the code to match.
1210 precache_sound(flag.snd_flag_respawn);
1211 if (flag.snd_flag_touch == "") flag.snd_flag_touch = SND(CTF_TOUCH); // again has no team-based sound
1212 precache_sound(flag.snd_flag_touch);
1213 if (flag.snd_flag_pass == "") flag.snd_flag_pass = SND(CTF_PASS); // same story here
1214 precache_sound(flag.snd_flag_pass);
1217 precache_model(flag.model);
1220 _setmodel(flag, flag.model); // precision set below
1221 setsize(flag, FLAG_MIN, FLAG_MAX);
1222 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1224 if(autocvar_g_ctf_flag_glowtrails)
1228 case NUM_TEAM_1: flag.glow_color = 251; break;
1229 case NUM_TEAM_2: flag.glow_color = 210; break;
1230 case NUM_TEAM_3: flag.glow_color = 110; break;
1231 case NUM_TEAM_4: flag.glow_color = 145; break;
1232 default: flag.glow_color = 254; break;
1234 flag.glow_size = 25;
1235 flag.glow_trail = 1;
1238 flag.effects |= EF_LOWPRECISION;
1239 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1240 if(autocvar_g_ctf_dynamiclights)
1244 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1245 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1246 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1247 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1248 default: flag.effects |= EF_DIMLIGHT; break;
1253 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1255 flag.dropped_origin = flag.origin;
1256 flag.noalign = true;
1257 flag.movetype = MOVETYPE_NONE;
1259 else // drop to floor, automatically find a platform and set that as spawn origin
1261 flag.noalign = false;
1264 flag.movetype = MOVETYPE_TOSS;
1267 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1275 // NOTE: LEGACY CODE, needs to be re-written!
1277 void havocbot_calculate_middlepoint()
1281 vector fo = '0 0 0';
1284 f = ctf_worldflaglist;
1289 f = f.ctf_worldflagnext;
1293 havocbot_ctf_middlepoint = s * (1.0 / n);
1294 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1298 entity havocbot_ctf_find_flag(entity bot)
1301 f = ctf_worldflaglist;
1304 if (CTF_SAMETEAM(bot, f))
1306 f = f.ctf_worldflagnext;
1311 entity havocbot_ctf_find_enemy_flag(entity bot)
1314 f = ctf_worldflaglist;
1319 if(CTF_DIFFTEAM(bot, f))
1326 else if(!bot.flagcarried)
1330 else if (CTF_DIFFTEAM(bot, f))
1332 f = f.ctf_worldflagnext;
1337 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1345 FOR_EACH_PLAYER(head)
1347 if(DIFF_TEAM(head, bot) || head.deadflag != DEAD_NO || head == bot)
1350 if(vlen(head.origin - org) < tc_radius)
1357 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1360 head = ctf_worldflaglist;
1363 if (CTF_SAMETEAM(self, head))
1365 head = head.ctf_worldflagnext;
1368 navigation_routerating(head, ratingscale, 10000);
1371 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1374 head = ctf_worldflaglist;
1377 if (CTF_SAMETEAM(self, head))
1379 head = head.ctf_worldflagnext;
1384 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1387 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1390 head = ctf_worldflaglist;
1395 if(CTF_DIFFTEAM(self, head))
1399 if(self.flagcarried)
1402 else if(!self.flagcarried)
1406 else if(CTF_DIFFTEAM(self, head))
1408 head = head.ctf_worldflagnext;
1411 navigation_routerating(head, ratingscale, 10000);
1414 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1416 if (!bot_waypoints_for_items)
1418 havocbot_goalrating_ctf_enemyflag(ratingscale);
1424 head = havocbot_ctf_find_enemy_flag(self);
1429 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1432 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1436 mf = havocbot_ctf_find_flag(self);
1438 if(mf.ctf_status == FLAG_BASE)
1442 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1445 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1448 head = ctf_worldflaglist;
1451 // flag is out in the field
1452 if(head.ctf_status != FLAG_BASE)
1453 if(head.tag_entity==world) // dropped
1457 if(vlen(org-head.origin)<df_radius)
1458 navigation_routerating(head, ratingscale, 10000);
1461 navigation_routerating(head, ratingscale, 10000);
1464 head = head.ctf_worldflagnext;
1468 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1472 head = findchainfloat(bot_pickup, true);
1475 // gather health and armor only
1477 if (head.health || head.armorvalue)
1478 if (vlen(head.origin - org) < sradius)
1480 // get the value of the item
1481 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1483 navigation_routerating(head, t * ratingscale, 500);
1489 void havocbot_ctf_reset_role(entity bot)
1491 float cdefense, cmiddle, coffense;
1492 entity mf, ef, head;
1495 if(bot.deadflag != DEAD_NO)
1498 if(vlen(havocbot_ctf_middlepoint)==0)
1499 havocbot_calculate_middlepoint();
1502 if (bot.flagcarried)
1504 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1508 mf = havocbot_ctf_find_flag(bot);
1509 ef = havocbot_ctf_find_enemy_flag(bot);
1511 // Retrieve stolen flag
1512 if(mf.ctf_status!=FLAG_BASE)
1514 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1518 // If enemy flag is taken go to the middle to intercept pursuers
1519 if(ef.ctf_status!=FLAG_BASE)
1521 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1525 // if there is only me on the team switch to offense
1527 FOR_EACH_PLAYER(head)
1528 if(SAME_TEAM(head, bot))
1533 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1537 // Evaluate best position to take
1538 // Count mates on middle position
1539 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1541 // Count mates on defense position
1542 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1544 // Count mates on offense position
1545 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1547 if(cdefense<=coffense)
1548 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1549 else if(coffense<=cmiddle)
1550 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1552 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1555 void havocbot_role_ctf_carrier()
1557 if(self.deadflag != DEAD_NO)
1559 havocbot_ctf_reset_role(self);
1563 if (self.flagcarried == world)
1565 havocbot_ctf_reset_role(self);
1569 if (self.bot_strategytime < time)
1571 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1573 navigation_goalrating_start();
1575 havocbot_goalrating_ctf_enemybase(50000);
1577 havocbot_goalrating_ctf_ourbase(50000);
1580 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1582 navigation_goalrating_end();
1584 if (self.navigation_hasgoals)
1585 self.havocbot_cantfindflag = time + 10;
1586 else if (time > self.havocbot_cantfindflag)
1588 // Can't navigate to my own base, suicide!
1589 // TODO: drop it and wander around
1590 Damage(self, self, self, 100000, DEATH_KILL.m_id, self.origin, '0 0 0');
1596 void havocbot_role_ctf_escort()
1600 if(self.deadflag != DEAD_NO)
1602 havocbot_ctf_reset_role(self);
1606 if (self.flagcarried)
1608 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1612 // If enemy flag is back on the base switch to previous role
1613 ef = havocbot_ctf_find_enemy_flag(self);
1614 if(ef.ctf_status==FLAG_BASE)
1616 self.havocbot_role = self.havocbot_previous_role;
1617 self.havocbot_role_timeout = 0;
1621 // If the flag carrier reached the base switch to defense
1622 mf = havocbot_ctf_find_flag(self);
1623 if(mf.ctf_status!=FLAG_BASE)
1624 if(vlen(ef.origin - mf.dropped_origin) < 300)
1626 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1630 // Set the role timeout if necessary
1631 if (!self.havocbot_role_timeout)
1633 self.havocbot_role_timeout = time + random() * 30 + 60;
1636 // If nothing happened just switch to previous role
1637 if (time > self.havocbot_role_timeout)
1639 self.havocbot_role = self.havocbot_previous_role;
1640 self.havocbot_role_timeout = 0;
1644 // Chase the flag carrier
1645 if (self.bot_strategytime < time)
1647 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1648 navigation_goalrating_start();
1649 havocbot_goalrating_ctf_enemyflag(30000);
1650 havocbot_goalrating_ctf_ourstolenflag(40000);
1651 havocbot_goalrating_items(10000, self.origin, 10000);
1652 navigation_goalrating_end();
1656 void havocbot_role_ctf_offense()
1661 if(self.deadflag != DEAD_NO)
1663 havocbot_ctf_reset_role(self);
1667 if (self.flagcarried)
1669 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1674 mf = havocbot_ctf_find_flag(self);
1675 ef = havocbot_ctf_find_enemy_flag(self);
1678 if(mf.ctf_status!=FLAG_BASE)
1681 pos = mf.tag_entity.origin;
1685 // Try to get it if closer than the enemy base
1686 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1688 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1693 // Escort flag carrier
1694 if(ef.ctf_status!=FLAG_BASE)
1697 pos = ef.tag_entity.origin;
1701 if(vlen(pos-mf.dropped_origin)>700)
1703 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1708 // About to fail, switch to middlefield
1711 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1715 // Set the role timeout if necessary
1716 if (!self.havocbot_role_timeout)
1717 self.havocbot_role_timeout = time + 120;
1719 if (time > self.havocbot_role_timeout)
1721 havocbot_ctf_reset_role(self);
1725 if (self.bot_strategytime < time)
1727 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1728 navigation_goalrating_start();
1729 havocbot_goalrating_ctf_ourstolenflag(50000);
1730 havocbot_goalrating_ctf_enemybase(20000);
1731 havocbot_goalrating_items(5000, self.origin, 1000);
1732 havocbot_goalrating_items(1000, self.origin, 10000);
1733 navigation_goalrating_end();
1737 // Retriever (temporary role):
1738 void havocbot_role_ctf_retriever()
1742 if(self.deadflag != DEAD_NO)
1744 havocbot_ctf_reset_role(self);
1748 if (self.flagcarried)
1750 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1754 // If flag is back on the base switch to previous role
1755 mf = havocbot_ctf_find_flag(self);
1756 if(mf.ctf_status==FLAG_BASE)
1758 havocbot_ctf_reset_role(self);
1762 if (!self.havocbot_role_timeout)
1763 self.havocbot_role_timeout = time + 20;
1765 if (time > self.havocbot_role_timeout)
1767 havocbot_ctf_reset_role(self);
1771 if (self.bot_strategytime < time)
1776 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1777 navigation_goalrating_start();
1778 havocbot_goalrating_ctf_ourstolenflag(50000);
1779 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1780 havocbot_goalrating_ctf_enemybase(30000);
1781 havocbot_goalrating_items(500, self.origin, rt_radius);
1782 navigation_goalrating_end();
1786 void havocbot_role_ctf_middle()
1790 if(self.deadflag != DEAD_NO)
1792 havocbot_ctf_reset_role(self);
1796 if (self.flagcarried)
1798 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1802 mf = havocbot_ctf_find_flag(self);
1803 if(mf.ctf_status!=FLAG_BASE)
1805 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1809 if (!self.havocbot_role_timeout)
1810 self.havocbot_role_timeout = time + 10;
1812 if (time > self.havocbot_role_timeout)
1814 havocbot_ctf_reset_role(self);
1818 if (self.bot_strategytime < time)
1822 org = havocbot_ctf_middlepoint;
1823 org.z = self.origin.z;
1825 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1826 navigation_goalrating_start();
1827 havocbot_goalrating_ctf_ourstolenflag(50000);
1828 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1829 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1830 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1831 havocbot_goalrating_items(2500, self.origin, 10000);
1832 havocbot_goalrating_ctf_enemybase(2500);
1833 navigation_goalrating_end();
1837 void havocbot_role_ctf_defense()
1841 if(self.deadflag != DEAD_NO)
1843 havocbot_ctf_reset_role(self);
1847 if (self.flagcarried)
1849 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1853 // If own flag was captured
1854 mf = havocbot_ctf_find_flag(self);
1855 if(mf.ctf_status!=FLAG_BASE)
1857 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1861 if (!self.havocbot_role_timeout)
1862 self.havocbot_role_timeout = time + 30;
1864 if (time > self.havocbot_role_timeout)
1866 havocbot_ctf_reset_role(self);
1869 if (self.bot_strategytime < time)
1874 org = mf.dropped_origin;
1875 mp_radius = havocbot_ctf_middlepoint_radius;
1877 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1878 navigation_goalrating_start();
1880 // if enemies are closer to our base, go there
1881 entity head, closestplayer = world;
1882 float distance, bestdistance = 10000;
1883 FOR_EACH_PLAYER(head)
1885 if(head.deadflag!=DEAD_NO)
1888 distance = vlen(org - head.origin);
1889 if(distance<bestdistance)
1891 closestplayer = head;
1892 bestdistance = distance;
1897 if(DIFF_TEAM(closestplayer, self))
1898 if(vlen(org - self.origin)>1000)
1899 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1900 havocbot_goalrating_ctf_ourbase(30000);
1902 havocbot_goalrating_ctf_ourstolenflag(20000);
1903 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1904 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1905 havocbot_goalrating_items(10000, org, mp_radius);
1906 havocbot_goalrating_items(5000, self.origin, 10000);
1907 navigation_goalrating_end();
1911 void havocbot_role_ctf_setrole(entity bot, int role)
1913 LOG_TRACE(strcat(bot.netname," switched to "));
1916 case HAVOCBOT_CTF_ROLE_CARRIER:
1917 LOG_TRACE("carrier");
1918 bot.havocbot_role = havocbot_role_ctf_carrier;
1919 bot.havocbot_role_timeout = 0;
1920 bot.havocbot_cantfindflag = time + 10;
1921 bot.bot_strategytime = 0;
1923 case HAVOCBOT_CTF_ROLE_DEFENSE:
1924 LOG_TRACE("defense");
1925 bot.havocbot_role = havocbot_role_ctf_defense;
1926 bot.havocbot_role_timeout = 0;
1928 case HAVOCBOT_CTF_ROLE_MIDDLE:
1929 LOG_TRACE("middle");
1930 bot.havocbot_role = havocbot_role_ctf_middle;
1931 bot.havocbot_role_timeout = 0;
1933 case HAVOCBOT_CTF_ROLE_OFFENSE:
1934 LOG_TRACE("offense");
1935 bot.havocbot_role = havocbot_role_ctf_offense;
1936 bot.havocbot_role_timeout = 0;
1938 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1939 LOG_TRACE("retriever");
1940 bot.havocbot_previous_role = bot.havocbot_role;
1941 bot.havocbot_role = havocbot_role_ctf_retriever;
1942 bot.havocbot_role_timeout = time + 10;
1943 bot.bot_strategytime = 0;
1945 case HAVOCBOT_CTF_ROLE_ESCORT:
1946 LOG_TRACE("escort");
1947 bot.havocbot_previous_role = bot.havocbot_role;
1948 bot.havocbot_role = havocbot_role_ctf_escort;
1949 bot.havocbot_role_timeout = time + 30;
1950 bot.bot_strategytime = 0;
1961 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
1964 int t = 0, t2 = 0, t3 = 0;
1966 // initially clear items so they can be set as necessary later.
1967 self.ctf_flagstatus &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
1968 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
1969 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
1970 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
1971 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
1972 | CTF_FLAG_NEUTRAL | CTF_SHIELDED);
1974 // scan through all the flags and notify the client about them
1975 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1977 if(flag.team == NUM_TEAM_1) { t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
1978 if(flag.team == NUM_TEAM_2) { t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
1979 if(flag.team == NUM_TEAM_3) { t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
1980 if(flag.team == NUM_TEAM_4) { t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
1981 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; }
1983 switch(flag.ctf_status)
1988 if((flag.owner == self) || (flag.pass_sender == self))
1989 self.ctf_flagstatus |= t; // carrying: self is currently carrying the flag
1991 self.ctf_flagstatus |= t2; // taken: someone else is carrying the flag
1996 self.ctf_flagstatus |= t3; // lost: the flag is dropped somewhere on the map
2002 // item for stopping players from capturing the flag too often
2003 if(self.ctf_captureshielded)
2004 self.ctf_flagstatus |= CTF_SHIELDED;
2006 // update the health of the flag carrier waypointsprite
2007 if(self.wps_flagcarrier)
2008 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
2013 MUTATOR_HOOKFUNCTION(ctf, PlayerDamage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2015 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2017 if(frag_target == frag_attacker) // damage done to yourself
2019 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2020 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2022 else // damage done to everyone else
2024 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2025 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2028 else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2030 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.m_id)))
2031 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2033 frag_target.wps_helpme_time = time;
2034 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2036 // todo: add notification for when flag carrier needs help?
2041 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2043 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2045 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
2046 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
2049 if(frag_target.flagcarried)
2051 entity tmp_entity = frag_target.flagcarried;
2052 ctf_Handle_Throw(frag_target, world, DROP_NORMAL);
2053 tmp_entity.ctf_dropper = world;
2059 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2062 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2065 void ctf_RemovePlayer(entity player)
2067 if(player.flagcarried)
2068 { ctf_Handle_Throw(player, world, DROP_NORMAL); }
2070 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2072 if(flag.pass_sender == player) { flag.pass_sender = world; }
2073 if(flag.pass_target == player) { flag.pass_target = world; }
2074 if(flag.ctf_dropper == player) { flag.ctf_dropper = world; }
2078 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2080 ctf_RemovePlayer(self);
2084 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2086 ctf_RemovePlayer(self);
2090 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2092 if(self.flagcarried)
2093 if(!autocvar_g_ctf_portalteleport)
2094 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2099 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2101 if(MUTATOR_RETURNVALUE || gameover) { return false; }
2103 entity player = self;
2105 if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2107 // pass the flag to a team mate
2108 if(autocvar_g_ctf_pass)
2110 entity head, closest_target = world;
2111 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2113 while(head) // find the closest acceptable target to pass to
2115 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
2116 if(head != player && SAME_TEAM(head, player))
2117 if(!head.speedrunning && !head.vehicle)
2119 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2120 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2121 vector passer_center = CENTER_OR_VIEWOFS(player);
2123 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2125 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2127 if(IS_BOT_CLIENT(head))
2129 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2130 ctf_Handle_Throw(head, player, DROP_PASS);
2134 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2135 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2137 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2140 else if(player.flagcarried)
2144 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2145 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
2146 { closest_target = head; }
2148 else { closest_target = head; }
2155 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2158 // throw the flag in front of you
2159 if(autocvar_g_ctf_throw && player.flagcarried)
2161 if(player.throw_count == -1)
2163 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2165 player.throw_prevtime = time;
2166 player.throw_count = 1;
2167 ctf_Handle_Throw(player, world, DROP_THROW);
2172 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2178 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2179 else { player.throw_count += 1; }
2180 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2182 player.throw_prevtime = time;
2183 ctf_Handle_Throw(player, world, DROP_THROW);
2192 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2194 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2196 self.wps_helpme_time = time;
2197 WaypointSprite_HelpMePing(self.wps_flagcarrier);
2199 else // create a normal help me waypointsprite
2201 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, self, FLAG_WAYPOINT_OFFSET, world, self.team, self, wps_helpme, false, RADARICON_HELPME);
2202 WaypointSprite_Ping(self.wps_helpme);
2208 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2210 if(vh_player.flagcarried)
2212 vh_player.flagcarried.nodrawtoclient = vh_player; // hide the flag from the driver
2214 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2216 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
2220 setattachment(vh_player.flagcarried, vh_vehicle, "");
2221 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
2222 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2223 //vh_player.flagcarried.angles = '0 0 0';
2231 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2233 if(vh_player.flagcarried)
2235 setattachment(vh_player.flagcarried, vh_player, "");
2236 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2237 vh_player.flagcarried.scale = FLAG_SCALE;
2238 vh_player.flagcarried.angles = '0 0 0';
2239 vh_player.flagcarried.nodrawtoclient = world;
2246 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2248 if(self.flagcarried)
2250 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));
2251 ctf_RespawnFlag(self.flagcarried);
2258 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2260 entity flag; // temporary entity for the search method
2262 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2264 switch(flag.ctf_status)
2269 // lock the flag, game is over
2270 flag.movetype = MOVETYPE_NONE;
2271 flag.takedamage = DAMAGE_NO;
2272 flag.solid = SOLID_NOT;
2273 flag.nextthink = false; // stop thinking
2275 //dprint("stopping the ", flag.netname, " from moving.\n");
2283 // do nothing for these flags
2292 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2294 havocbot_ctf_reset_role(self);
2298 MUTATOR_HOOKFUNCTION(ctf, GetTeamCount)
2300 //ret_float = ctf_teams;
2301 ret_string = "ctf_team";
2305 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2307 self.ctf_flagstatus = other.ctf_flagstatus;
2311 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2313 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2315 if (MapInfo_Get_ByID(i))
2317 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2323 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2324 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2331 bool superspec_Spectate(entity _player); // TODO
2332 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2333 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2335 if(IS_PLAYER(self) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2337 if(cmd_name == "followfc")
2350 case "red": _team = NUM_TEAM_1; break;
2351 case "blue": _team = NUM_TEAM_2; break;
2352 case "yellow": if(ctf_teams >= 3) _team = NUM_TEAM_3; break;
2353 case "pink": if(ctf_teams >= 4) _team = NUM_TEAM_4; break;
2357 FOR_EACH_PLAYER(_player)
2359 if(_player.flagcarried && (_player.team == _team || _team == 0))
2362 if(_team == 0 && IS_SPEC(self) && self.enemy == _player)
2363 continue; // already spectating a fc, try to find the other fc
2364 return superspec_Spectate(_player);
2369 superspec_msg("", "", self, "No active flag carrier\n", 1);
2381 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2382 CTF flag for team one (Red).
2384 "angle" Angle the flag will point (minus 90 degrees)...
2385 "model" model to use, note this needs red and blue as skins 0 and 1...
2386 "noise" sound played when flag is picked up...
2387 "noise1" sound played when flag is returned by a teammate...
2388 "noise2" sound played when flag is captured...
2389 "noise3" sound played when flag is lost in the field and respawns itself...
2390 "noise4" sound played when flag is dropped by a player...
2391 "noise5" sound played when flag touches the ground... */
2392 spawnfunc(item_flag_team1)
2394 if(!g_ctf) { remove(self); return; }
2396 ctf_FlagSetup(NUM_TEAM_1, self);
2399 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2400 CTF flag for team two (Blue).
2402 "angle" Angle the flag will point (minus 90 degrees)...
2403 "model" model to use, note this needs red and blue as skins 0 and 1...
2404 "noise" sound played when flag is picked up...
2405 "noise1" sound played when flag is returned by a teammate...
2406 "noise2" sound played when flag is captured...
2407 "noise3" sound played when flag is lost in the field and respawns itself...
2408 "noise4" sound played when flag is dropped by a player...
2409 "noise5" sound played when flag touches the ground... */
2410 spawnfunc(item_flag_team2)
2412 if(!g_ctf) { remove(self); return; }
2414 ctf_FlagSetup(NUM_TEAM_2, self);
2417 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2418 CTF flag for team three (Yellow).
2420 "angle" Angle the flag will point (minus 90 degrees)...
2421 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2422 "noise" sound played when flag is picked up...
2423 "noise1" sound played when flag is returned by a teammate...
2424 "noise2" sound played when flag is captured...
2425 "noise3" sound played when flag is lost in the field and respawns itself...
2426 "noise4" sound played when flag is dropped by a player...
2427 "noise5" sound played when flag touches the ground... */
2428 spawnfunc(item_flag_team3)
2430 if(!g_ctf) { remove(self); return; }
2432 ctf_FlagSetup(NUM_TEAM_3, self);
2435 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2436 CTF flag for team four (Pink).
2438 "angle" Angle the flag will point (minus 90 degrees)...
2439 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2440 "noise" sound played when flag is picked up...
2441 "noise1" sound played when flag is returned by a teammate...
2442 "noise2" sound played when flag is captured...
2443 "noise3" sound played when flag is lost in the field and respawns itself...
2444 "noise4" sound played when flag is dropped by a player...
2445 "noise5" sound played when flag touches the ground... */
2446 spawnfunc(item_flag_team4)
2448 if(!g_ctf) { remove(self); return; }
2450 ctf_FlagSetup(NUM_TEAM_4, self);
2453 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2456 "angle" Angle the flag will point (minus 90 degrees)...
2457 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2458 "noise" sound played when flag is picked up...
2459 "noise1" sound played when flag is returned by a teammate...
2460 "noise2" sound played when flag is captured...
2461 "noise3" sound played when flag is lost in the field and respawns itself...
2462 "noise4" sound played when flag is dropped by a player...
2463 "noise5" sound played when flag touches the ground... */
2464 spawnfunc(item_flag_neutral)
2466 if(!g_ctf) { remove(self); return; }
2467 if(!cvar("g_ctf_oneflag")) { remove(self); return; }
2469 ctf_FlagSetup(0, self);
2472 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2473 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2474 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.
2476 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2477 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2480 if(!g_ctf) { remove(self); return; }
2482 self.classname = "ctf_team";
2483 self.team = self.cnt + 1;
2486 // compatibility for quake maps
2487 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2488 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2489 spawnfunc(info_player_team1);
2490 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2491 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2492 spawnfunc(info_player_team2);
2493 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2494 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2496 void team_CTF_neutralflag() { SELFPARAM(); spawnfunc_item_flag_neutral(self); }
2497 void team_neutralobelisk() { SELFPARAM(); spawnfunc_item_flag_neutral(self); }
2505 void ctf_ScoreRules(int teams)
2507 CheckAllowedTeams(world);
2508 ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
2509 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2510 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2511 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2512 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2513 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2514 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2515 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2516 ScoreRules_basics_end();
2519 // code from here on is just to support maps that don't have flag and team entities
2520 void ctf_SpawnTeam (string teamname, int teamcolor)
2522 entity this = new(ctf_team);
2523 this.netname = teamname;
2524 this.cnt = teamcolor;
2525 this.spawnfunc_checked = true;
2526 WITH(entity, self, this, spawnfunc_ctf_team(this));
2529 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2534 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2536 if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2537 if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2538 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2541 ctf_teams = bound(2, ctf_teams, 4);
2543 // if no teams are found, spawn defaults
2544 if(find(world, classname, "ctf_team") == world)
2546 LOG_INFO("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2547 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2548 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2550 ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2552 ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2555 ctf_ScoreRules(ctf_teams);
2558 void ctf_Initialize()
2560 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2562 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2563 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2564 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2566 addstat(STAT_CTF_FLAGSTATUS, AS_INT, ctf_flagstatus);
2568 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2571 REGISTER_MUTATOR(ctf, g_ctf)
2574 SetLimits(autocvar_capturelimit_override, -1, autocvar_captureleadlimit_override, -1);
2575 have_team_spawns = -1; // request team spawns
2579 if(time > 1) // game loads at time 1
2580 error("This is a game type and it cannot be added at runtime.");
2584 MUTATOR_ONROLLBACK_OR_REMOVE
2586 // we actually cannot roll back ctf_Initialize here
2587 // BUT: we don't need to! If this gets called, adding always
2593 LOG_INFO("This is a game type and it cannot be removed at runtime.");