3 #include <common/effects/all.qh>
4 #include <common/mapobjects/teleporters.qh>
5 #include <common/mapobjects/triggers.qh>
6 #include <common/mutators/mutator/powerups/_mod.qh>
7 #include <common/vehicles/all.qh>
8 #include <server/command/vote.qh>
9 #include <server/client.qh>
10 #include <server/gamelog.qh>
11 #include <server/intermission.qh>
12 #include <server/damage.qh>
13 #include <server/world.qh>
14 #include <server/items/items.qh>
15 #include <server/race.qh>
16 #include <server/teamplay.qh>
18 #include <lib/warpzone/common.qh>
20 bool autocvar_g_ctf_allow_vehicle_carry;
21 bool autocvar_g_ctf_allow_vehicle_touch;
22 bool autocvar_g_ctf_allow_monster_touch;
23 bool autocvar_g_ctf_throw;
24 float autocvar_g_ctf_throw_angle_max;
25 float autocvar_g_ctf_throw_angle_min;
26 int autocvar_g_ctf_throw_punish_count;
27 float autocvar_g_ctf_throw_punish_delay;
28 float autocvar_g_ctf_throw_punish_time;
29 float autocvar_g_ctf_throw_strengthmultiplier;
30 float autocvar_g_ctf_throw_velocity_forward;
31 float autocvar_g_ctf_throw_velocity_up;
32 float autocvar_g_ctf_drop_velocity_up;
33 float autocvar_g_ctf_drop_velocity_side;
34 bool autocvar_g_ctf_oneflag_reverse;
35 bool autocvar_g_ctf_portalteleport;
36 bool autocvar_g_ctf_pass;
37 float autocvar_g_ctf_pass_arc;
38 float autocvar_g_ctf_pass_arc_max;
39 float autocvar_g_ctf_pass_directional_max;
40 float autocvar_g_ctf_pass_directional_min;
41 float autocvar_g_ctf_pass_radius;
42 float autocvar_g_ctf_pass_wait;
43 bool autocvar_g_ctf_pass_request;
44 float autocvar_g_ctf_pass_turnrate;
45 float autocvar_g_ctf_pass_timelimit;
46 float autocvar_g_ctf_pass_velocity;
47 bool autocvar_g_ctf_dynamiclights;
48 float autocvar_g_ctf_flag_collect_delay;
49 float autocvar_g_ctf_flag_damageforcescale;
50 bool autocvar_g_ctf_flag_dropped_waypoint;
51 bool autocvar_g_ctf_flag_dropped_floatinwater;
52 bool autocvar_g_ctf_flag_glowtrails;
53 int autocvar_g_ctf_flag_health;
54 bool autocvar_g_ctf_flag_return;
55 bool autocvar_g_ctf_flag_return_carrying;
56 float autocvar_g_ctf_flag_return_carried_radius;
57 float autocvar_g_ctf_flag_return_time;
58 bool autocvar_g_ctf_flag_return_when_unreachable;
59 float autocvar_g_ctf_flag_return_damage;
60 float autocvar_g_ctf_flag_return_damage_delay;
61 float autocvar_g_ctf_flag_return_dropped;
62 bool autocvar_g_ctf_flag_waypoint = true;
63 float autocvar_g_ctf_flag_waypoint_maxdistance;
64 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
65 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
66 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
67 float autocvar_g_ctf_flagcarrier_selfforcefactor;
68 float autocvar_g_ctf_flagcarrier_damagefactor;
69 float autocvar_g_ctf_flagcarrier_forcefactor;
70 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
71 bool autocvar_g_ctf_fullbrightflags;
72 bool autocvar_g_ctf_ignore_frags;
73 bool autocvar_g_ctf_score_ignore_fields;
74 int autocvar_g_ctf_score_capture;
75 int autocvar_g_ctf_score_capture_assist;
76 int autocvar_g_ctf_score_kill;
77 int autocvar_g_ctf_score_penalty_drop;
78 int autocvar_g_ctf_score_penalty_returned;
79 int autocvar_g_ctf_score_pickup_base;
80 int autocvar_g_ctf_score_pickup_dropped_early;
81 int autocvar_g_ctf_score_pickup_dropped_late;
82 int autocvar_g_ctf_score_return;
83 float autocvar_g_ctf_shield_force;
84 float autocvar_g_ctf_shield_max_ratio;
85 int autocvar_g_ctf_shield_min_negscore;
86 bool autocvar_g_ctf_stalemate;
87 int autocvar_g_ctf_stalemate_endcondition;
88 float autocvar_g_ctf_stalemate_time;
89 bool autocvar_g_ctf_reverse;
90 float autocvar_g_ctf_dropped_capture_delay;
91 float autocvar_g_ctf_dropped_capture_radius;
93 void ctf_FakeTimeLimit(entity e, float t)
96 WriteByte(MSG_ONE, 3); // svc_updatestat
97 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
99 WriteCoord(MSG_ONE, autocvar_timelimit);
101 WriteCoord(MSG_ONE, (t + 1) / 60);
104 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
106 if(autocvar_sv_eventlog)
107 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != NULL) ? ftos(actor.playerid) : "")));
108 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
111 void ctf_CaptureRecord(entity flag, entity player)
113 float cap_record = ctf_captimerecord;
114 float cap_time = (time - flag.ctf_pickuptime);
115 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
119 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname);
120 else if(!ctf_captimerecord)
121 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_TIME), player.netname, TIME_ENCODE(cap_time));
122 else if(cap_time < cap_record)
123 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_BROKEN), player.netname, refername, TIME_ENCODE(cap_time), TIME_ENCODE(cap_record));
125 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_UNBROKEN), player.netname, refername, TIME_ENCODE(cap_time), TIME_ENCODE(cap_record));
127 // write that shit in the database
128 if(!ctf_oneflag) // but not in 1-flag mode
129 if((!ctf_captimerecord) || (cap_time < cap_record))
131 ctf_captimerecord = cap_time;
132 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
133 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
134 write_recordmarker(player, flag.ctf_pickuptime, cap_time);
137 if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
138 race_setTime(GetMapname(), TIME_ENCODE(cap_time), player.crypto_idfp, player.netname, player, false);
141 bool ctf_Immediate_Return_Allowed(entity flag, entity toucher)
144 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), { ++num_perteam; });
146 // automatically return if there's only 1 player on the team
147 return ((autocvar_g_ctf_flag_return || num_perteam <= 1 || (autocvar_g_ctf_flag_return_carrying && toucher.flagcarried))
151 bool ctf_Return_Customize(entity this, entity client)
153 // only to the carrier
154 return boolean(client == this.owner);
157 void ctf_FlagcarrierWaypoints(entity player)
159 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
160 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, 2 * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
161 WaypointSprite_UpdateHealth(player.wps_flagcarrier, healtharmor_maxdamage(GetResource(player, RES_HEALTH), GetResource(player, RES_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
162 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
164 if(player.flagcarried && CTF_SAMETEAM(player, player.flagcarried))
166 if(!player.wps_enemyflagcarrier)
168 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, 0, player, wps_enemyflagcarrier, true, RADARICON_FLAG);
169 wp.colormod = WPCOLOR_ENEMYFC(player.team);
170 setcefc(wp, ctf_Stalemate_Customize);
172 if(IS_REAL_CLIENT(player) && !ctf_stalemate)
173 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_VISIBLE);
176 if(!player.wps_flagreturn)
178 entity owp = WaypointSprite_SpawnFixed(WP_FlagReturn, player.flagcarried.ctf_spawnorigin + FLAG_WAYPOINT_OFFSET, player, wps_flagreturn, RADARICON_FLAG);
179 owp.colormod = '0 0.8 0.8';
180 //WaypointSprite_UpdateTeamRadar(player.wps_flagreturn, RADARICON_FLAG, ((player.team) ? colormapPaletteColor(player.team - 1, false) : '1 1 1'));
181 setcefc(owp, ctf_Return_Customize);
186 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
188 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
189 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
190 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
191 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
194 if(current_height) // make sure we can actually do this arcing path
196 targpos = (to + ('0 0 1' * current_height));
197 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
198 if(trace_fraction < 1)
200 //print("normal arc line failed, trying to find new pos...");
201 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
202 targpos = (trace_endpos + eZ * FLAG_PASS_ARC_OFFSET_Z);
203 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
204 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
205 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
208 else { targpos = to; }
210 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
212 vector desired_direction = normalize(targpos - from);
213 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
214 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
217 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
219 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
221 // directional tracing only
223 makevectors(passer_angle);
225 // find the closest point on the enemy to the center of the attack
226 float h; // hypotenuse, which is the distance between attacker to head
227 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
229 h = vlen(head_center - passer_center);
230 a = h * (normalize(head_center - passer_center) * v_forward);
232 vector nearest_on_line = (passer_center + a * v_forward);
233 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
235 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
236 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
238 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
243 else { return true; }
247 // =======================
248 // CaptureShield Functions
249 // =======================
251 bool ctf_CaptureShield_CheckStatus(entity p)
253 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
254 int players_worseeq, players_total;
256 if(ctf_captureshield_max_ratio <= 0)
259 s = GameRules_scoring_add(p, CTF_CAPS, 0);
260 s2 = GameRules_scoring_add(p, CTF_PICKUPS, 0);
261 s3 = GameRules_scoring_add(p, CTF_RETURNS, 0);
262 s4 = GameRules_scoring_add(p, CTF_FCKILLS, 0);
264 sr = ((s - s2) + (s3 + s4));
266 if(sr >= -ctf_captureshield_min_negscore)
269 players_total = players_worseeq = 0;
270 FOREACH_CLIENT(IS_PLAYER(it), {
273 se = GameRules_scoring_add(it, CTF_CAPS, 0);
274 se2 = GameRules_scoring_add(it, CTF_PICKUPS, 0);
275 se3 = GameRules_scoring_add(it, CTF_RETURNS, 0);
276 se4 = GameRules_scoring_add(it, CTF_FCKILLS, 0);
278 ser = ((se - se2) + (se3 + se4));
285 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
286 // use this rule here
288 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
294 void ctf_CaptureShield_Update(entity player, bool wanted_status)
296 bool updated_status = ctf_CaptureShield_CheckStatus(player);
297 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
299 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
300 player.ctf_captureshielded = updated_status;
304 bool ctf_CaptureShield_Customize(entity this, entity client)
306 if(!client.ctf_captureshielded) { return false; }
307 if(CTF_SAMETEAM(this, client)) { return false; }
312 void ctf_CaptureShield_Touch(entity this, entity toucher)
314 if(!toucher.ctf_captureshielded) { return; }
315 if(CTF_SAMETEAM(this, toucher)) { return; }
317 vector mymid = (this.absmin + this.absmax) * 0.5;
318 vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
320 Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
321 if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
324 void ctf_CaptureShield_Spawn(entity flag)
326 entity shield = new(ctf_captureshield);
329 shield.team = flag.team;
330 settouch(shield, ctf_CaptureShield_Touch);
331 setcefc(shield, ctf_CaptureShield_Customize);
332 shield.effects = EF_ADDITIVE;
333 set_movetype(shield, MOVETYPE_NOCLIP);
334 shield.solid = SOLID_TRIGGER;
335 shield.avelocity = '7 0 11';
338 setorigin(shield, flag.origin);
339 setmodel(shield, MDL_CTF_SHIELD);
340 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
344 // ====================
345 // Drop/Pass/Throw Code
346 // ====================
348 void ctf_Handle_Drop(entity flag, entity player, int droptype)
351 player = (player ? player : flag.pass_sender);
354 set_movetype(flag, MOVETYPE_TOSS);
355 flag.takedamage = DAMAGE_YES;
356 flag.angles = '0 0 0';
357 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
358 flag.ctf_droptime = time;
359 flag.ctf_dropper = player;
360 flag.ctf_status = FLAG_DROPPED;
362 // messages and sounds
363 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
364 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
365 ctf_EventLog("dropped", player.team, player);
368 GameRules_scoring_add_team(player, SCORE, -((flag.score_drop) ? flag.score_drop : autocvar_g_ctf_score_penalty_drop));
369 GameRules_scoring_add(player, CTF_DROPS, 1);
372 if(autocvar_g_ctf_flag_dropped_waypoint) {
373 entity wp = WaypointSprite_Spawn(WP_FlagDropped, 0, 0, flag, FLAG_WAYPOINT_OFFSET, NULL, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, true, RADARICON_FLAG);
374 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
377 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
379 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_health);
380 WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH));
383 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
385 if(droptype == DROP_PASS)
387 flag.pass_distance = 0;
388 flag.pass_sender = NULL;
389 flag.pass_target = NULL;
393 void ctf_Handle_Retrieve(entity flag, entity player)
395 entity sender = flag.pass_sender;
397 // transfer flag to player
399 flag.owner.flagcarried = flag;
400 GameRules_scoring_vip(player, true);
405 setattachment(flag, player.vehicle, "");
406 setorigin(flag, VEHICLE_FLAG_OFFSET);
407 flag.scale = VEHICLE_FLAG_SCALE;
411 setattachment(flag, player, "");
412 setorigin(flag, FLAG_CARRY_OFFSET);
414 set_movetype(flag, MOVETYPE_NONE);
415 flag.takedamage = DAMAGE_NO;
416 flag.solid = SOLID_NOT;
417 flag.angles = '0 0 0';
418 flag.ctf_status = FLAG_CARRY;
420 // messages and sounds
421 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
422 ctf_EventLog("receive", flag.team, player);
424 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
426 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
427 else if(it == player)
428 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
429 else if(SAME_TEAM(it, sender))
430 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
433 // create new waypoint
434 ctf_FlagcarrierWaypoints(player);
436 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
437 player.throw_antispam = sender.throw_antispam;
439 flag.pass_distance = 0;
440 flag.pass_sender = NULL;
441 flag.pass_target = NULL;
444 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
446 entity flag = player.flagcarried;
447 vector targ_origin, flag_velocity;
449 if(!flag) { return; }
450 if((droptype == DROP_PASS) && !receiver) { return; }
452 if(flag.speedrunning)
454 // ensure old waypoints are removed before resetting the flag
455 WaypointSprite_Kill(player.wps_flagcarrier);
457 if(player.wps_enemyflagcarrier)
458 WaypointSprite_Kill(player.wps_enemyflagcarrier);
460 if(player.wps_flagreturn)
461 WaypointSprite_Kill(player.wps_flagreturn);
462 ctf_RespawnFlag(flag);
467 setattachment(flag, NULL, "");
468 tracebox(player.origin - FLAG_DROP_OFFSET, flag.m_mins, flag.m_maxs, player.origin + FLAG_DROP_OFFSET, MOVE_NOMONSTERS, flag);
469 setorigin(flag, trace_endpos);
470 flag.owner.flagcarried = NULL;
471 GameRules_scoring_vip(flag.owner, false);
473 flag.solid = SOLID_TRIGGER;
474 flag.ctf_dropper = player;
475 flag.ctf_droptime = time;
477 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
484 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
485 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
486 WarpZone_RefSys_Copy(flag, receiver);
487 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
488 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
490 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
491 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
494 set_movetype(flag, MOVETYPE_FLY);
495 flag.takedamage = DAMAGE_NO;
496 flag.pass_sender = player;
497 flag.pass_target = receiver;
498 flag.ctf_status = FLAG_PASSING;
501 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
502 WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
503 ctf_EventLog("pass", flag.team, player);
509 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'));
511 flag_velocity = (('0 0 1' * autocvar_g_ctf_throw_velocity_up) + ((v_forward * autocvar_g_ctf_throw_velocity_forward) * ((StatusEffects_active(STATUSEFFECT_Strength, player)) ? autocvar_g_ctf_throw_strengthmultiplier : 1)));
512 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
513 ctf_Handle_Drop(flag, player, droptype);
514 navigation_dynamicgoal_set(flag, player);
520 flag.velocity = '0 0 0'; // do nothing
527 flag.velocity = W_CalculateProjectileVelocity(player, 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);
528 ctf_Handle_Drop(flag, player, droptype);
529 navigation_dynamicgoal_set(flag, player);
534 // kill old waypointsprite
535 WaypointSprite_Ping(player.wps_flagcarrier);
536 WaypointSprite_Kill(player.wps_flagcarrier);
538 if(player.wps_enemyflagcarrier)
539 WaypointSprite_Kill(player.wps_enemyflagcarrier);
541 if(player.wps_flagreturn)
542 WaypointSprite_Kill(player.wps_flagreturn);
545 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
549 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
551 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
559 void nades_GiveBonus(entity player, float score);
561 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
563 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
564 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
565 entity player_team_flag = NULL, tmp_entity;
566 float old_time, new_time;
568 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
569 if(CTF_DIFFTEAM(player, flag)) { return; }
570 if((flag.cnt || enemy_flag.cnt) && flag.cnt != enemy_flag.cnt) { return; } // this should catch some edge cases (capturing grouped flag at ungrouped flag disallowed etc)
572 if (toucher.goalentity == flag.bot_basewaypoint)
573 toucher.goalentity_lock_timeout = 0;
576 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
577 if(SAME_TEAM(tmp_entity, player))
579 player_team_flag = tmp_entity;
583 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
585 player.throw_prevtime = time;
586 player.throw_count = 0;
588 // messages and sounds
589 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
590 ctf_CaptureRecord(enemy_flag, player);
591 _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);
595 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
596 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
602 if(enemy_flag.score_capture || flag.score_capture)
603 pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
604 GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
606 if(enemy_flag.score_team_capture || flag.score_team_capture)
607 capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
608 GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
610 old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
611 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
612 if(!old_time || new_time < old_time)
613 GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
616 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
618 shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
622 if(capturetype == CAPTURE_NORMAL)
624 WaypointSprite_Kill(player.wps_flagcarrier);
625 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
627 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
628 { GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist)); }
631 flag.enemy = toucher;
634 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
635 ctf_RespawnFlag(enemy_flag);
638 void ctf_Handle_Return(entity flag, entity player)
640 // messages and sounds
641 if(IS_MONSTER(player))
643 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
647 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
648 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
650 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
651 ctf_EventLog("return", flag.team, player);
654 if(IS_PLAYER(player))
656 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
657 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
659 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
662 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
666 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
667 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
668 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
672 if(player.flagcarried == flag)
673 WaypointSprite_Kill(player.wps_flagcarrier);
678 ctf_RespawnFlag(flag);
681 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
684 float pickup_dropped_score; // used to calculate dropped pickup score
686 // attach the flag to the player
688 player.flagcarried = flag;
689 GameRules_scoring_vip(player, true);
692 setattachment(flag, player.vehicle, "");
693 setorigin(flag, VEHICLE_FLAG_OFFSET);
694 flag.scale = VEHICLE_FLAG_SCALE;
698 setattachment(flag, player, "");
699 setorigin(flag, FLAG_CARRY_OFFSET);
703 set_movetype(flag, MOVETYPE_NONE);
704 flag.takedamage = DAMAGE_NO;
705 flag.solid = SOLID_NOT;
706 flag.angles = '0 0 0';
707 flag.ctf_status = FLAG_CARRY;
711 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
712 case PICKUP_DROPPED: SetResourceExplicit(flag, RES_HEALTH, flag.max_health); break; // reset health/return timelimit
716 // messages and sounds
717 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
719 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
721 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
722 else if(CTF_DIFFTEAM(player, flag))
723 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
725 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
727 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
730 FOREACH_CLIENT(IS_PLAYER(it) && it != player && DIFF_TEAM(it, player), { Send_Notification(NOTIF_ONE, it, MSG_CHOICE, CHOICE_CTF_PICKUP_ENEMY_NEUTRAL, Team_ColorCode(player.team), player.netname); });
733 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
734 if(CTF_SAMETEAM(flag, it))
736 if(SAME_TEAM(player, it))
737 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
739 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, ((SAME_TEAM(flag, player)) ? CHOICE_CTF_PICKUP_ENEMY_TEAM : CHOICE_CTF_PICKUP_ENEMY), Team_ColorCode(player.team), player.netname);
743 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
746 GameRules_scoring_add(player, CTF_PICKUPS, 1);
747 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
752 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
753 ctf_EventLog("steal", flag.team, player);
759 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);
760 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);
761 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
762 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
763 ctf_EventLog("pickup", flag.team, player);
771 if(pickuptype == PICKUP_BASE)
773 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
774 if((player.speedrunning) && (ctf_captimerecord))
775 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
779 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
782 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
783 ctf_FlagcarrierWaypoints(player);
784 WaypointSprite_Ping(player.wps_flagcarrier);
788 // ===================
789 // Main Flag Functions
790 // ===================
792 void ctf_CheckFlagReturn(entity flag, int returntype)
794 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
796 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH)); }
798 if((GetResource(flag, RES_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
803 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
805 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
806 case RETURN_SPEEDRUN:
807 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
808 case RETURN_NEEDKILL:
809 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
812 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
814 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
815 ctf_EventLog("returned", flag.team, NULL);
817 ctf_RespawnFlag(flag);
822 bool ctf_Stalemate_Customize(entity this, entity client)
824 // make spectators see what the player would see
825 entity e = WaypointSprite_getviewentity(client);
826 entity wp_owner = this.owner;
829 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
830 if(SAME_TEAM(wp_owner, e)) { return false; }
831 if(!IS_PLAYER(e)) { return false; }
836 void ctf_CheckStalemate()
839 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
842 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
844 // build list of stale flags
845 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
847 if(autocvar_g_ctf_stalemate)
848 if(tmp_entity.ctf_status != FLAG_BASE)
849 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
851 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
852 ctf_staleflaglist = tmp_entity;
854 switch(tmp_entity.team)
856 case NUM_TEAM_1: ++stale_red_flags; break;
857 case NUM_TEAM_2: ++stale_blue_flags; break;
858 case NUM_TEAM_3: ++stale_yellow_flags; break;
859 case NUM_TEAM_4: ++stale_pink_flags; break;
860 default: ++stale_neutral_flags; break;
866 stale_flags = (stale_neutral_flags >= 1);
868 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
870 if(ctf_oneflag && stale_flags == 1)
871 ctf_stalemate = true;
872 else if(stale_flags >= 2)
873 ctf_stalemate = true;
874 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
875 { ctf_stalemate = false; wpforenemy_announced = false; }
876 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
877 { ctf_stalemate = false; wpforenemy_announced = false; }
879 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
882 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
884 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
886 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, tmp_entity.owner, FLAG_WAYPOINT_OFFSET, NULL, 0, tmp_entity.owner, wps_enemyflagcarrier, true, RADARICON_FLAG);
887 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
888 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
892 if (!wpforenemy_announced)
894 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), { Send_Notification(NOTIF_ONE, it, MSG_CENTER, ((it.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER)); });
896 wpforenemy_announced = true;
901 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
903 if(ITEM_DAMAGE_NEEDKILL(deathtype))
905 if(autocvar_g_ctf_flag_return_damage_delay)
906 this.ctf_flagdamaged_byworld = true;
909 SetResourceExplicit(this, RES_HEALTH, 0);
910 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
914 if(autocvar_g_ctf_flag_return_damage)
916 // reduce health and check if it should be returned
917 TakeResource(this, RES_HEALTH, damage);
918 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
923 void ctf_FlagThink(entity this)
928 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
931 if(this == ctf_worldflaglist) // only for the first flag
932 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
935 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
936 LOG_TRACE("wtf the flag got squashed?");
937 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
938 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
939 setsize(this, this.m_mins, this.m_maxs);
943 switch(this.ctf_status)
947 if(autocvar_g_ctf_dropped_capture_radius)
949 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
950 if(tmp_entity.ctf_status == FLAG_DROPPED)
951 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
952 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
953 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
960 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
962 if(autocvar_g_ctf_flag_dropped_floatinwater)
964 vector midpoint = ((this.absmin + this.absmax) * 0.5);
965 if(pointcontents(midpoint) == CONTENT_WATER)
967 this.velocity = this.velocity * 0.5;
969 if (pointcontents(midpoint + eZ * FLAG_FLOAT_OFFSET_Z) == CONTENT_WATER)
970 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
972 { set_movetype(this, MOVETYPE_FLY); }
974 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
976 if(autocvar_g_ctf_flag_return_dropped)
978 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
980 SetResourceExplicit(this, RES_HEALTH, 0);
981 ctf_CheckFlagReturn(this, RETURN_DROPPED);
985 if(this.ctf_flagdamaged_byworld)
987 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
988 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
991 else if(autocvar_g_ctf_flag_return_time)
993 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
994 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
1002 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
1004 SetResourceExplicit(this, RES_HEALTH, 0);
1005 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
1007 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
1008 ImpulseCommands(this.owner);
1010 if(autocvar_g_ctf_stalemate)
1012 if(time >= wpforenemy_nextthink)
1014 ctf_CheckStalemate();
1015 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1018 if(CTF_SAMETEAM(this, this.owner) && this.team)
1020 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1021 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
1022 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
1023 ctf_Handle_Return(this, this.owner);
1030 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1031 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1032 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1034 if((this.pass_target == NULL)
1035 || (IS_DEAD(this.pass_target))
1036 || (this.pass_target.flagcarried)
1037 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1038 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1039 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1041 // give up, pass failed
1042 ctf_Handle_Drop(this, NULL, DROP_PASS);
1046 // still a viable target, go for it
1047 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1052 default: // this should never happen
1054 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1060 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1063 if(game_stopped) return;
1064 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1066 bool is_not_monster = (!IS_MONSTER(toucher));
1068 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1069 if(ITEM_TOUCH_NEEDKILL())
1071 if(!autocvar_g_ctf_flag_return_damage_delay)
1073 SetResourceExplicit(flag, RES_HEALTH, 0);
1074 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1076 if(!flag.ctf_flagdamaged_byworld) { return; }
1079 // special touch behaviors
1080 if(STAT(FROZEN, toucher)) { return; }
1081 else if(IS_VEHICLE(toucher))
1083 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1084 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1086 return; // do nothing
1088 else if(IS_MONSTER(toucher))
1090 if(!autocvar_g_ctf_allow_monster_touch)
1091 return; // do nothing
1093 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1095 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1097 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1098 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1099 flag.wait = time + FLAG_TOUCHRATE;
1103 else if(IS_DEAD(toucher)) { return; }
1105 switch(flag.ctf_status)
1111 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1112 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1113 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1114 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1116 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1117 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1118 else if(CTF_DIFFTEAM(toucher, flag) && (toucher.flagcarried) && CTF_SAMETEAM(toucher.flagcarried, toucher) && (!toucher.ctf_captureshielded) && autocvar_g_ctf_flag_return_carrying && (time > toucher.next_take_time) && is_not_monster)
1120 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1121 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1123 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1124 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1130 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1131 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1132 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1133 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1139 LOG_TRACE("Someone touched a flag even though it was being carried?");
1145 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1147 if(DIFF_TEAM(toucher, flag.pass_sender))
1149 if(ctf_Immediate_Return_Allowed(flag, toucher))
1150 ctf_Handle_Return(flag, toucher);
1151 else if(is_not_monster && (!toucher.flagcarried))
1152 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1154 else if(!toucher.flagcarried)
1155 ctf_Handle_Retrieve(flag, toucher);
1162 .float last_respawn;
1163 void ctf_RespawnFlag(entity flag)
1165 flag.watertype = CONTENT_EMPTY; // TODO: it is unclear why this workaround is needed, likely many other potential breakage points!!
1166 // check for flag respawn being called twice in a row
1167 if(flag.last_respawn > time - 0.5)
1168 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1170 flag.last_respawn = time;
1172 // reset the player (if there is one)
1173 if((flag.owner) && (flag.owner.flagcarried == flag))
1175 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1176 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1177 WaypointSprite_Kill(flag.wps_flagcarrier);
1179 flag.owner.flagcarried = NULL;
1180 GameRules_scoring_vip(flag.owner, false);
1182 if(flag.speedrunning)
1183 ctf_FakeTimeLimit(flag.owner, -1);
1186 if((flag.owner) && (flag.owner.vehicle))
1187 flag.scale = FLAG_SCALE;
1189 if(flag.ctf_status == FLAG_DROPPED)
1190 { WaypointSprite_Kill(flag.wps_flagdropped); }
1193 setattachment(flag, NULL, "");
1194 setorigin(flag, flag.ctf_spawnorigin);
1196 //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1197 set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1198 flag.takedamage = DAMAGE_NO;
1199 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1200 flag.solid = SOLID_TRIGGER;
1201 flag.velocity = '0 0 0';
1202 flag.angles = flag.mangle;
1203 flag.flags = FL_ITEM | FL_NOTARGET;
1205 flag.ctf_status = FLAG_BASE;
1207 flag.pass_distance = 0;
1208 flag.pass_sender = NULL;
1209 flag.pass_target = NULL;
1210 flag.ctf_dropper = NULL;
1211 flag.ctf_pickuptime = 0;
1212 flag.ctf_droptime = 0;
1213 flag.ctf_flagdamaged_byworld = false;
1214 navigation_dynamicgoal_unset(flag);
1216 ctf_CheckStalemate();
1219 void ctf_Reset(entity this)
1221 if(this.owner && IS_PLAYER(this.owner))
1222 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1225 ctf_RespawnFlag(this);
1228 bool ctf_FlagBase_Customize(entity this, entity client)
1230 entity e = WaypointSprite_getviewentity(client);
1231 entity wp_owner = this.owner;
1232 entity flag = e.flagcarried;
1233 if(flag && CTF_SAMETEAM(e, flag))
1235 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1240 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1243 waypoint_spawnforitem_force(this, this.origin);
1244 navigation_dynamicgoal_init(this, true);
1250 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1251 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1252 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1253 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1254 default: basename = WP_FlagBaseNeutral; break;
1257 if(autocvar_g_ctf_flag_waypoint)
1259 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1260 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1261 wp.fade_rate = autocvar_g_ctf_flag_waypoint_maxdistance;
1262 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1263 setcefc(wp, ctf_FlagBase_Customize);
1266 // captureshield setup
1267 ctf_CaptureShield_Spawn(this);
1272 void ctf_FlagSetup(int teamnum, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1275 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1276 ctf_worldflaglist = flag;
1278 setattachment(flag, NULL, "");
1280 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnum), Team_ColorName_Upper(teamnum)));
1281 flag.team = teamnum;
1282 flag.classname = "item_flag_team";
1283 flag.target = "###item###"; // for finding the nearest item using findnearest
1284 flag.flags = FL_ITEM | FL_NOTARGET;
1285 IL_PUSH(g_items, flag);
1286 flag.solid = SOLID_TRIGGER;
1287 flag.takedamage = DAMAGE_NO;
1288 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1289 flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1290 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1291 flag.event_damage = ctf_FlagDamage;
1292 flag.pushable = true;
1293 flag.teleportable = TELEPORT_NORMAL;
1294 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1295 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1296 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1297 if(flag.damagedbycontents)
1298 IL_PUSH(g_damagedbycontents, flag);
1299 flag.velocity = '0 0 0';
1300 flag.mangle = flag.angles;
1301 flag.reset = ctf_Reset;
1302 settouch(flag, ctf_FlagTouch);
1303 setthink(flag, ctf_FlagThink);
1304 flag.nextthink = time + FLAG_THINKRATE;
1305 flag.ctf_status = FLAG_BASE;
1307 // crudely force them all to 0
1308 if(autocvar_g_ctf_score_ignore_fields)
1309 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1311 string teamname = Static_Team_ColorName_Lower(teamnum);
1313 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1314 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1315 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1316 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnum).eent_eff_name; }
1317 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnum).eent_eff_name; }
1318 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnum).eent_eff_name; }
1322 if(flag.s == "") flag.s = b; \
1323 precache_sound(flag.s);
1325 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnum))))
1326 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnum))))
1327 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnum))))
1328 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnum))))
1329 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1330 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1331 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1335 precache_model(flag.model);
1338 _setmodel(flag, flag.model); // precision set below
1339 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1340 flag.m_mins = flag.mins; // store these for squash checks
1341 flag.m_maxs = flag.maxs;
1342 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1344 if(autocvar_g_ctf_flag_glowtrails)
1348 case NUM_TEAM_1: flag.glow_color = 251; break;
1349 case NUM_TEAM_2: flag.glow_color = 210; break;
1350 case NUM_TEAM_3: flag.glow_color = 110; break;
1351 case NUM_TEAM_4: flag.glow_color = 145; break;
1352 default: flag.glow_color = 254; break;
1354 flag.glow_size = 25;
1355 flag.glow_trail = 1;
1358 flag.effects |= EF_LOWPRECISION;
1359 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1360 if(autocvar_g_ctf_dynamiclights)
1364 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1365 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1366 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1367 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1368 default: flag.effects |= EF_DIMLIGHT; break;
1373 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1375 flag.dropped_origin = flag.origin;
1376 flag.noalign = true;
1377 set_movetype(flag, MOVETYPE_NONE);
1379 else // drop to floor, automatically find a platform and set that as spawn origin
1381 flag.noalign = false;
1383 set_movetype(flag, MOVETYPE_NONE);
1386 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1394 // NOTE: LEGACY CODE, needs to be re-written!
1396 void havocbot_ctf_calculate_middlepoint()
1400 vector fo = '0 0 0';
1403 f = ctf_worldflaglist;
1408 f = f.ctf_worldflagnext;
1414 havocbot_middlepoint = s / n;
1415 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1417 havocbot_symmetry_axis_m = 0;
1418 havocbot_symmetry_axis_q = 0;
1421 // for symmetrical editing of waypoints
1422 entity f1 = ctf_worldflaglist;
1423 entity f2 = f1.ctf_worldflagnext;
1424 float m = -(f1.origin.y - f2.origin.y) / (max(f1.origin.x - f2.origin.x, FLOAT_EPSILON));
1425 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1426 havocbot_symmetry_axis_m = m;
1427 havocbot_symmetry_axis_q = q;
1429 havocbot_symmetry_origin_order = n;
1433 entity havocbot_ctf_find_flag(entity bot)
1436 f = ctf_worldflaglist;
1439 if (CTF_SAMETEAM(bot, f))
1441 f = f.ctf_worldflagnext;
1446 entity havocbot_ctf_find_enemy_flag(entity bot)
1449 f = ctf_worldflaglist;
1454 if(CTF_DIFFTEAM(bot, f))
1461 else if(!bot.flagcarried)
1465 else if (CTF_DIFFTEAM(bot, f))
1467 f = f.ctf_worldflagnext;
1472 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1479 FOREACH_CLIENT(IS_PLAYER(it), {
1480 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1483 if(vdist(it.origin - org, <, tc_radius))
1492 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1495 head = ctf_worldflaglist;
1498 if (CTF_SAMETEAM(this, head))
1500 head = head.ctf_worldflagnext;
1503 navigation_routerating(this, head, ratingscale, 10000);
1507 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1510 head = ctf_worldflaglist;
1513 if (CTF_SAMETEAM(this, head))
1515 if (this.flagcarried)
1516 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1518 head = head.ctf_worldflagnext; // skip base if it has a different group
1523 head = head.ctf_worldflagnext;
1528 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1531 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1534 head = ctf_worldflaglist;
1539 if(CTF_DIFFTEAM(this, head))
1543 if(this.flagcarried)
1546 else if(!this.flagcarried)
1550 else if(CTF_DIFFTEAM(this, head))
1552 head = head.ctf_worldflagnext;
1556 if (head.ctf_status == FLAG_CARRY)
1558 // adjust rating of our flag carrier depending on his health
1559 head = head.tag_entity;
1560 float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1561 ratingscale += ratingscale * f * 0.1;
1563 navigation_routerating(this, head, ratingscale, 10000);
1567 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1569 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1571 if (!bot_waypoints_for_items)
1573 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1579 head = havocbot_ctf_find_enemy_flag(this);
1584 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1587 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1591 mf = havocbot_ctf_find_flag(this);
1593 if(mf.ctf_status == FLAG_BASE)
1597 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1600 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1603 head = ctf_worldflaglist;
1606 // flag is out in the field
1607 if(head.ctf_status != FLAG_BASE)
1608 if(head.tag_entity==NULL) // dropped
1612 if(vdist(org - head.origin, <, df_radius))
1613 navigation_routerating(this, head, ratingscale, 10000);
1616 navigation_routerating(this, head, ratingscale, 10000);
1619 head = head.ctf_worldflagnext;
1623 void havocbot_ctf_reset_role(entity this)
1625 float cdefense, cmiddle, coffense;
1632 if (this.flagcarried)
1634 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1638 mf = havocbot_ctf_find_flag(this);
1639 ef = havocbot_ctf_find_enemy_flag(this);
1641 // Retrieve stolen flag
1642 if(mf.ctf_status!=FLAG_BASE)
1644 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1648 // If enemy flag is taken go to the middle to intercept pursuers
1649 if(ef.ctf_status!=FLAG_BASE)
1651 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1655 // if there is no one else on the team switch to offense
1657 // don't check if this bot is a player since it isn't true when the bot is added to the server
1658 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1662 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1665 else if (time < CS(this).jointime + 1)
1667 // if bots spawn all at once set good default roles
1670 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1673 else if (count == 2)
1675 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1680 // Evaluate best position to take
1681 // Count mates on middle position
1682 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1684 // Count mates on defense position
1685 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1687 // Count mates on offense position
1688 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1690 if(cdefense<=coffense)
1691 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1692 else if(coffense<=cmiddle)
1693 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1695 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1697 // if bots spawn all at once assign them a more appropriated role after a while
1698 if (time < CS(this).jointime + 1 && count > 2)
1699 this.havocbot_role_timeout = time + 10 + random() * 10;
1702 bool havocbot_ctf_is_basewaypoint(entity item)
1704 if (item.classname != "waypoint")
1707 entity head = ctf_worldflaglist;
1710 if (item == head.bot_basewaypoint)
1712 head = head.ctf_worldflagnext;
1717 void havocbot_role_ctf_carrier(entity this)
1721 havocbot_ctf_reset_role(this);
1725 if (this.flagcarried == NULL)
1727 havocbot_ctf_reset_role(this);
1731 if (navigation_goalrating_timeout(this))
1733 navigation_goalrating_start(this);
1736 entity mf = havocbot_ctf_find_flag(this);
1737 vector base_org = mf.dropped_origin;
1738 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1740 havocbot_goalrating_ctf_enemybase(this, base_rating);
1742 havocbot_goalrating_ctf_ourbase(this, base_rating);
1744 // start collecting items very close to the bot but only inside of own base radius
1745 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1746 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1748 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1750 navigation_goalrating_end(this);
1752 navigation_goalrating_timeout_set(this);
1754 entity goal = this.goalentity;
1755 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1756 this.goalentity_lock_timeout = time + ((this.bot_aimtarg) ? 2 : 3);
1759 this.havocbot_cantfindflag = time + 10;
1760 else if (time > this.havocbot_cantfindflag)
1762 // Can't navigate to my own base, suicide!
1763 // TODO: drop it and wander around
1764 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1770 void havocbot_role_ctf_escort(entity this)
1776 havocbot_ctf_reset_role(this);
1780 if (this.flagcarried)
1782 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1786 // If enemy flag is back on the base switch to previous role
1787 ef = havocbot_ctf_find_enemy_flag(this);
1788 if(ef.ctf_status==FLAG_BASE)
1790 this.havocbot_role = this.havocbot_previous_role;
1791 this.havocbot_role_timeout = 0;
1794 if (ef.ctf_status == FLAG_DROPPED)
1796 navigation_goalrating_timeout_expire(this, 1);
1800 // If the flag carrier reached the base switch to defense
1801 mf = havocbot_ctf_find_flag(this);
1802 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1804 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1808 // Set the role timeout if necessary
1809 if (!this.havocbot_role_timeout)
1811 this.havocbot_role_timeout = time + random() * 30 + 60;
1814 // If nothing happened just switch to previous role
1815 if (time > this.havocbot_role_timeout)
1817 this.havocbot_role = this.havocbot_previous_role;
1818 this.havocbot_role_timeout = 0;
1822 // Chase the flag carrier
1823 if (navigation_goalrating_timeout(this))
1825 navigation_goalrating_start(this);
1828 havocbot_goalrating_ctf_enemyflag(this, 10000);
1829 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1830 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1832 navigation_goalrating_end(this);
1834 navigation_goalrating_timeout_set(this);
1838 void havocbot_role_ctf_offense(entity this)
1845 havocbot_ctf_reset_role(this);
1849 if (this.flagcarried)
1851 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1856 mf = havocbot_ctf_find_flag(this);
1857 ef = havocbot_ctf_find_enemy_flag(this);
1860 if(mf.ctf_status!=FLAG_BASE)
1863 pos = mf.tag_entity.origin;
1867 // Try to get it if closer than the enemy base
1868 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1870 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1875 // Escort flag carrier
1876 if(ef.ctf_status!=FLAG_BASE)
1879 pos = ef.tag_entity.origin;
1883 if(vdist(pos - mf.dropped_origin, >, 700))
1885 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1890 // Set the role timeout if necessary
1891 if (!this.havocbot_role_timeout)
1892 this.havocbot_role_timeout = time + 120;
1894 if (time > this.havocbot_role_timeout)
1896 havocbot_ctf_reset_role(this);
1900 if (navigation_goalrating_timeout(this))
1902 navigation_goalrating_start(this);
1905 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1906 havocbot_goalrating_ctf_enemybase(this, 10000);
1907 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1909 navigation_goalrating_end(this);
1911 navigation_goalrating_timeout_set(this);
1915 // Retriever (temporary role):
1916 void havocbot_role_ctf_retriever(entity this)
1922 havocbot_ctf_reset_role(this);
1926 if (this.flagcarried)
1928 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1932 // If flag is back on the base switch to previous role
1933 mf = havocbot_ctf_find_flag(this);
1934 if(mf.ctf_status==FLAG_BASE)
1936 if (mf.enemy == this) // did this bot return the flag?
1937 navigation_goalrating_timeout_force(this);
1938 havocbot_ctf_reset_role(this);
1942 if (!this.havocbot_role_timeout)
1943 this.havocbot_role_timeout = time + 20;
1945 if (time > this.havocbot_role_timeout)
1947 havocbot_ctf_reset_role(this);
1951 if (navigation_goalrating_timeout(this))
1953 const float RT_RADIUS = 10000;
1955 navigation_goalrating_start(this);
1958 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1959 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1960 havocbot_goalrating_ctf_enemybase(this, 8000);
1961 entity ef = havocbot_ctf_find_enemy_flag(this);
1962 vector enemy_base_org = ef.dropped_origin;
1963 // start collecting items very close to the bot but only inside of enemy base radius
1964 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1965 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1966 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1968 navigation_goalrating_end(this);
1970 navigation_goalrating_timeout_set(this);
1974 void havocbot_role_ctf_middle(entity this)
1980 havocbot_ctf_reset_role(this);
1984 if (this.flagcarried)
1986 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1990 mf = havocbot_ctf_find_flag(this);
1991 if(mf.ctf_status!=FLAG_BASE)
1993 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1997 if (!this.havocbot_role_timeout)
1998 this.havocbot_role_timeout = time + 10;
2000 if (time > this.havocbot_role_timeout)
2002 havocbot_ctf_reset_role(this);
2006 if (navigation_goalrating_timeout(this))
2010 org = havocbot_middlepoint;
2011 org.z = this.origin.z;
2013 navigation_goalrating_start(this);
2016 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
2017 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
2018 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2019 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2020 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2021 havocbot_goalrating_ctf_enemybase(this, 3000);
2023 navigation_goalrating_end(this);
2025 entity goal = this.goalentity;
2026 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
2027 this.goalentity_lock_timeout = time + 2;
2029 navigation_goalrating_timeout_set(this);
2033 void havocbot_role_ctf_defense(entity this)
2039 havocbot_ctf_reset_role(this);
2043 if (this.flagcarried)
2045 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2049 // If own flag was captured
2050 mf = havocbot_ctf_find_flag(this);
2051 if(mf.ctf_status!=FLAG_BASE)
2053 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2057 if (!this.havocbot_role_timeout)
2058 this.havocbot_role_timeout = time + 30;
2060 if (time > this.havocbot_role_timeout)
2062 havocbot_ctf_reset_role(this);
2065 if (navigation_goalrating_timeout(this))
2067 vector org = mf.dropped_origin;
2069 navigation_goalrating_start(this);
2071 // if enemies are closer to our base, go there
2072 entity closestplayer = NULL;
2073 float distance, bestdistance = 10000;
2074 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2075 distance = vlen(org - it.origin);
2076 if(distance<bestdistance)
2079 bestdistance = distance;
2085 if(DIFF_TEAM(closestplayer, this))
2086 if(vdist(org - this.origin, >, 1000))
2087 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2088 havocbot_goalrating_ctf_ourbase(this, 10000);
2090 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2091 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2092 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2093 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2094 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2096 navigation_goalrating_end(this);
2098 navigation_goalrating_timeout_set(this);
2102 void havocbot_role_ctf_setrole(entity bot, int role)
2104 string s = "(null)";
2107 case HAVOCBOT_CTF_ROLE_CARRIER:
2109 bot.havocbot_role = havocbot_role_ctf_carrier;
2110 bot.havocbot_role_timeout = 0;
2111 bot.havocbot_cantfindflag = time + 10;
2112 if (bot.havocbot_previous_role != bot.havocbot_role)
2113 navigation_goalrating_timeout_force(bot);
2115 case HAVOCBOT_CTF_ROLE_DEFENSE:
2117 bot.havocbot_role = havocbot_role_ctf_defense;
2118 bot.havocbot_role_timeout = 0;
2120 case HAVOCBOT_CTF_ROLE_MIDDLE:
2122 bot.havocbot_role = havocbot_role_ctf_middle;
2123 bot.havocbot_role_timeout = 0;
2125 case HAVOCBOT_CTF_ROLE_OFFENSE:
2127 bot.havocbot_role = havocbot_role_ctf_offense;
2128 bot.havocbot_role_timeout = 0;
2130 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2132 bot.havocbot_previous_role = bot.havocbot_role;
2133 bot.havocbot_role = havocbot_role_ctf_retriever;
2134 bot.havocbot_role_timeout = time + 10;
2135 if (bot.havocbot_previous_role != bot.havocbot_role)
2136 navigation_goalrating_timeout_expire(bot, 2);
2138 case HAVOCBOT_CTF_ROLE_ESCORT:
2140 bot.havocbot_previous_role = bot.havocbot_role;
2141 bot.havocbot_role = havocbot_role_ctf_escort;
2142 bot.havocbot_role_timeout = time + 30;
2143 if (bot.havocbot_previous_role != bot.havocbot_role)
2144 navigation_goalrating_timeout_expire(bot, 2);
2147 LOG_TRACE(bot.netname, " switched to ", s);
2155 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2157 entity player = M_ARGV(0, entity);
2159 int t = 0, t2 = 0, t3 = 0;
2160 bool b1 = false, b2 = false, b3 = false, b4 = false, b5 = false; // TODO: kill this, we WANT to show the other flags, somehow! (note: also means you don't see if you're FC)
2162 // initially clear items so they can be set as necessary later.
2163 STAT(CTF_FLAGSTATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2164 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2165 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2166 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2167 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2168 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2170 // scan through all the flags and notify the client about them
2171 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2173 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2174 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2175 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2176 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2177 if(flag.team == 0 && !b5) { b5 = true; t = CTF_NEUTRAL_FLAG_CARRYING; t2 = CTF_NEUTRAL_FLAG_TAKEN; t3 = CTF_NEUTRAL_FLAG_LOST; STAT(CTF_FLAGSTATUS, player) |= CTF_FLAG_NEUTRAL; }
2179 switch(flag.ctf_status)
2184 if((flag.owner == player) || (flag.pass_sender == player))
2185 STAT(CTF_FLAGSTATUS, player) |= t; // carrying: player is currently carrying the flag
2187 STAT(CTF_FLAGSTATUS, player) |= t2; // taken: someone else is carrying the flag
2192 STAT(CTF_FLAGSTATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2198 // item for stopping players from capturing the flag too often
2199 if(player.ctf_captureshielded)
2200 STAT(CTF_FLAGSTATUS, player) |= CTF_SHIELDED;
2203 STAT(CTF_FLAGSTATUS, player) |= CTF_STALEMATE;
2205 // update the health of the flag carrier waypointsprite
2206 if(player.wps_flagcarrier)
2207 WaypointSprite_UpdateHealth(player.wps_flagcarrier, healtharmor_maxdamage(GetResource(player, RES_HEALTH), GetResource(player, RES_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
2210 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in damage.qc
2212 entity frag_attacker = M_ARGV(1, entity);
2213 entity frag_target = M_ARGV(2, entity);
2214 float frag_damage = M_ARGV(4, float);
2215 vector frag_force = M_ARGV(6, vector);
2217 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2219 if(frag_target == frag_attacker) // damage done to yourself
2221 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2222 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2224 else // damage done to everyone else
2226 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2227 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2230 M_ARGV(4, float) = frag_damage;
2231 M_ARGV(6, vector) = frag_force;
2233 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2235 if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > healtharmor_maxdamage(GetResource(frag_target, RES_HEALTH), GetResource(frag_target, RES_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x
2236 && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2238 frag_target.wps_helpme_time = time;
2239 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2241 // todo: add notification for when flag carrier needs help?
2245 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2247 entity frag_attacker = M_ARGV(1, entity);
2248 entity frag_target = M_ARGV(2, entity);
2250 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2252 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2253 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2256 if(frag_target.flagcarried)
2258 entity tmp_entity = frag_target.flagcarried;
2259 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2260 tmp_entity.ctf_dropper = NULL;
2264 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2266 M_ARGV(2, float) = 0; // frag score
2267 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2270 void ctf_RemovePlayer(entity player)
2272 if(player.flagcarried)
2273 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2275 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2277 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2278 if(flag.pass_target == player) { flag.pass_target = NULL; }
2279 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2283 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2285 entity player = M_ARGV(0, entity);
2287 ctf_RemovePlayer(player);
2290 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2292 entity player = M_ARGV(0, entity);
2294 ctf_RemovePlayer(player);
2297 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2299 if(!autocvar_g_ctf_leaderboard)
2302 entity player = M_ARGV(0, entity);
2304 race_SendAll(player, true);
2307 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2309 if(!autocvar_g_ctf_leaderboard)
2312 entity player = M_ARGV(0, entity);
2314 race_checkAndWriteName(player);
2317 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2319 entity player = M_ARGV(0, entity);
2321 if(player.flagcarried)
2322 if(!autocvar_g_ctf_portalteleport)
2323 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2326 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2328 if(MUTATOR_RETURNVALUE || game_stopped) return;
2330 entity player = M_ARGV(0, entity);
2332 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2334 // pass the flag to a team mate
2335 if(autocvar_g_ctf_pass)
2337 entity head, closest_target = NULL;
2338 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2340 while(head) // find the closest acceptable target to pass to
2342 if(IS_PLAYER(head) && !IS_DEAD(head))
2343 if(head != player && SAME_TEAM(head, player))
2344 if(!head.speedrunning && !head.vehicle)
2346 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in damage.qc)
2347 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2348 vector passer_center = CENTER_OR_VIEWOFS(player);
2350 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2352 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2354 if(IS_BOT_CLIENT(head))
2356 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2357 ctf_Handle_Throw(head, player, DROP_PASS);
2361 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2362 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2364 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2367 else if(player.flagcarried && !head.flagcarried)
2371 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2372 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2373 { closest_target = head; }
2375 else { closest_target = head; }
2382 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2385 // throw the flag in front of you
2386 if(autocvar_g_ctf_throw && player.flagcarried)
2388 if(player.throw_count == -1)
2390 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2392 player.throw_prevtime = time;
2393 player.throw_count = 1;
2394 ctf_Handle_Throw(player, NULL, DROP_THROW);
2399 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2405 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2406 else { player.throw_count += 1; }
2407 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2409 player.throw_prevtime = time;
2410 ctf_Handle_Throw(player, NULL, DROP_THROW);
2417 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2419 entity player = M_ARGV(0, entity);
2421 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2423 player.wps_helpme_time = time;
2424 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2426 else // create a normal help me waypointsprite
2428 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2429 WaypointSprite_Ping(player.wps_helpme);
2435 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2437 entity player = M_ARGV(0, entity);
2438 entity veh = M_ARGV(1, entity);
2440 if(player.flagcarried)
2442 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2444 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2448 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2449 setattachment(player.flagcarried, veh, "");
2450 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2451 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2452 //player.flagcarried.angles = '0 0 0';
2458 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2460 entity player = M_ARGV(0, entity);
2462 if(player.flagcarried)
2464 setattachment(player.flagcarried, player, "");
2465 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2466 player.flagcarried.scale = FLAG_SCALE;
2467 player.flagcarried.angles = '0 0 0';
2468 player.flagcarried.nodrawtoclient = NULL;
2473 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2475 entity player = M_ARGV(0, entity);
2477 if(player.flagcarried)
2479 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2480 ctf_RespawnFlag(player.flagcarried);
2485 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2487 entity flag; // temporary entity for the search method
2489 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2491 switch(flag.ctf_status)
2496 // lock the flag, game is over
2497 set_movetype(flag, MOVETYPE_NONE);
2498 flag.takedamage = DAMAGE_NO;
2499 flag.solid = SOLID_NOT;
2500 flag.nextthink = false; // stop thinking
2502 //dprint("stopping the ", flag.netname, " from moving.\n");
2510 // do nothing for these flags
2517 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2519 entity bot = M_ARGV(0, entity);
2521 havocbot_ctf_reset_role(bot);
2525 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2527 M_ARGV(1, string) = "ctf_team";
2530 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2532 entity spectatee = M_ARGV(0, entity);
2533 entity client = M_ARGV(1, entity);
2535 STAT(CTF_FLAGSTATUS, client) = STAT(CTF_FLAGSTATUS, spectatee);
2538 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2540 int record_page = M_ARGV(0, int);
2541 string ret_string = M_ARGV(1, string);
2543 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2545 if (MapInfo_Get_ByID(i))
2547 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2553 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2554 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2558 M_ARGV(1, string) = ret_string;
2561 bool superspec_Spectate(entity this, entity targ); // TODO
2562 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2563 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2565 entity player = M_ARGV(0, entity);
2566 string cmd_name = M_ARGV(1, string);
2567 int cmd_argc = M_ARGV(2, int);
2569 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2571 if(cmd_name == "followfc")
2583 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2584 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2585 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2586 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2590 FOREACH_CLIENT(IS_PLAYER(it), {
2591 if(it.flagcarried && (it.team == _team || _team == 0))
2594 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2595 continue; // already spectating this fc, try another
2596 return superspec_Spectate(player, it);
2601 superspec_msg("", "", player, "No active flag carrier\n", 1);
2606 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2608 entity frag_target = M_ARGV(0, entity);
2610 if(frag_target.flagcarried)
2611 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2614 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2616 entity player = M_ARGV(0, entity);
2617 if(player.flagcarried)
2618 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2626 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2627 CTF flag for team one (Red).
2629 "angle" Angle the flag will point (minus 90 degrees)...
2630 "model" model to use, note this needs red and blue as skins 0 and 1...
2631 "noise" sound played when flag is picked up...
2632 "noise1" sound played when flag is returned by a teammate...
2633 "noise2" sound played when flag is captured...
2634 "noise3" sound played when flag is lost in the field and respawns itself...
2635 "noise4" sound played when flag is dropped by a player...
2636 "noise5" sound played when flag touches the ground... */
2637 spawnfunc(item_flag_team1)
2639 if(!g_ctf) { delete(this); return; }
2641 ctf_FlagSetup(NUM_TEAM_1, this);
2644 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2645 CTF flag for team two (Blue).
2647 "angle" Angle the flag will point (minus 90 degrees)...
2648 "model" model to use, note this needs red and blue as skins 0 and 1...
2649 "noise" sound played when flag is picked up...
2650 "noise1" sound played when flag is returned by a teammate...
2651 "noise2" sound played when flag is captured...
2652 "noise3" sound played when flag is lost in the field and respawns itself...
2653 "noise4" sound played when flag is dropped by a player...
2654 "noise5" sound played when flag touches the ground... */
2655 spawnfunc(item_flag_team2)
2657 if(!g_ctf) { delete(this); return; }
2659 ctf_FlagSetup(NUM_TEAM_2, this);
2662 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2663 CTF flag for team three (Yellow).
2665 "angle" Angle the flag will point (minus 90 degrees)...
2666 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2667 "noise" sound played when flag is picked up...
2668 "noise1" sound played when flag is returned by a teammate...
2669 "noise2" sound played when flag is captured...
2670 "noise3" sound played when flag is lost in the field and respawns itself...
2671 "noise4" sound played when flag is dropped by a player...
2672 "noise5" sound played when flag touches the ground... */
2673 spawnfunc(item_flag_team3)
2675 if(!g_ctf) { delete(this); return; }
2677 ctf_FlagSetup(NUM_TEAM_3, this);
2680 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2681 CTF flag for team four (Pink).
2683 "angle" Angle the flag will point (minus 90 degrees)...
2684 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2685 "noise" sound played when flag is picked up...
2686 "noise1" sound played when flag is returned by a teammate...
2687 "noise2" sound played when flag is captured...
2688 "noise3" sound played when flag is lost in the field and respawns itself...
2689 "noise4" sound played when flag is dropped by a player...
2690 "noise5" sound played when flag touches the ground... */
2691 spawnfunc(item_flag_team4)
2693 if(!g_ctf) { delete(this); return; }
2695 ctf_FlagSetup(NUM_TEAM_4, this);
2698 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2701 "angle" Angle the flag will point (minus 90 degrees)...
2702 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2703 "noise" sound played when flag is picked up...
2704 "noise1" sound played when flag is returned by a teammate...
2705 "noise2" sound played when flag is captured...
2706 "noise3" sound played when flag is lost in the field and respawns itself...
2707 "noise4" sound played when flag is dropped by a player...
2708 "noise5" sound played when flag touches the ground... */
2709 spawnfunc(item_flag_neutral)
2711 if(!g_ctf) { delete(this); return; }
2712 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2714 ctf_FlagSetup(0, this);
2717 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2718 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2719 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.
2721 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2722 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2725 if(!g_ctf) { delete(this); return; }
2727 this.team = this.cnt + 1;
2730 // compatibility for quake maps
2731 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2732 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2733 spawnfunc(info_player_team1);
2734 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2735 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2736 spawnfunc(info_player_team2);
2737 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2738 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2740 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2741 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2743 // compatibility for wop maps
2744 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2745 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2746 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2747 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2748 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2749 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2757 void ctf_ScoreRules(int teams)
2759 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2760 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2761 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2762 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2763 field(SP_CTF_PICKUPS, "pickups", 0);
2764 field(SP_CTF_FCKILLS, "fckills", 0);
2765 field(SP_CTF_RETURNS, "returns", 0);
2766 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2770 // code from here on is just to support maps that don't have flag and team entities
2771 void ctf_SpawnTeam (string teamname, int teamcolor)
2773 entity this = new_pure(ctf_team);
2774 this.netname = teamname;
2775 this.cnt = teamcolor - 1;
2776 this.spawnfunc_checked = true;
2777 this.team = teamcolor;
2780 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2785 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2787 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2788 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2790 switch(tmp_entity.team)
2792 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2793 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2794 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2795 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2797 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2800 havocbot_ctf_calculate_middlepoint();
2802 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2804 ctf_teams = 0; // so set the default red and blue teams
2805 BITSET_ASSIGN(ctf_teams, BIT(0));
2806 BITSET_ASSIGN(ctf_teams, BIT(1));
2809 //ctf_teams = bound(2, ctf_teams, 4);
2811 // if no teams are found, spawn defaults
2812 if(find(NULL, classname, "ctf_team") == NULL)
2814 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2815 if(ctf_teams & BIT(0))
2816 ctf_SpawnTeam("Red", NUM_TEAM_1);
2817 if(ctf_teams & BIT(1))
2818 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2819 if(ctf_teams & BIT(2))
2820 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2821 if(ctf_teams & BIT(3))
2822 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2825 ctf_ScoreRules(ctf_teams);
2828 void ctf_Initialize()
2830 CTF_FLAG = NEW(Flag);
2831 record_type = CTF_RECORD;
2832 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2834 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2835 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2836 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2838 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);