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 // set correct team colors
1308 flag.glowmod = Team_ColorRGB(teamnum);
1309 flag.colormap = (teamnum) ? (teamnum - 1) * 0x11 : 0x00;
1310 flag.colormap |= BIT(10); // RENDER_COLORMAPPED
1312 // crudely force them all to 0
1313 if(autocvar_g_ctf_score_ignore_fields)
1314 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1316 string teamname = Static_Team_ColorName_Lower(teamnum);
1318 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1319 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1320 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1321 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnum).eent_eff_name; }
1322 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnum).eent_eff_name; }
1323 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnum).eent_eff_name; }
1327 if(flag.s == "") flag.s = b; \
1328 precache_sound(flag.s);
1330 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnum))))
1331 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnum))))
1332 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnum))))
1333 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnum))))
1334 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1335 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1336 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1340 precache_model(flag.model);
1343 _setmodel(flag, flag.model); // precision set below
1344 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1345 flag.m_mins = flag.mins; // store these for squash checks
1346 flag.m_maxs = flag.maxs;
1347 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1349 if(autocvar_g_ctf_flag_glowtrails)
1353 case NUM_TEAM_1: flag.glow_color = 251; break;
1354 case NUM_TEAM_2: flag.glow_color = 210; break;
1355 case NUM_TEAM_3: flag.glow_color = 110; break;
1356 case NUM_TEAM_4: flag.glow_color = 145; break;
1357 default: flag.glow_color = 254; break;
1359 flag.glow_size = 25;
1360 flag.glow_trail = 1;
1363 flag.effects |= EF_LOWPRECISION;
1364 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1365 if(autocvar_g_ctf_dynamiclights)
1369 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1370 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1371 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1372 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1373 default: flag.effects |= EF_DIMLIGHT; break;
1378 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1380 flag.dropped_origin = flag.origin;
1381 flag.noalign = true;
1382 set_movetype(flag, MOVETYPE_NONE);
1384 else // drop to floor, automatically find a platform and set that as spawn origin
1386 flag.noalign = false;
1388 set_movetype(flag, MOVETYPE_NONE);
1391 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1399 // NOTE: LEGACY CODE, needs to be re-written!
1401 void havocbot_ctf_calculate_middlepoint()
1405 vector fo = '0 0 0';
1408 f = ctf_worldflaglist;
1413 f = f.ctf_worldflagnext;
1419 havocbot_middlepoint = s / n;
1420 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1422 havocbot_symmetry_axis_m = 0;
1423 havocbot_symmetry_axis_q = 0;
1426 // for symmetrical editing of waypoints
1427 entity f1 = ctf_worldflaglist;
1428 entity f2 = f1.ctf_worldflagnext;
1429 float m = -(f1.origin.y - f2.origin.y) / (max(f1.origin.x - f2.origin.x, FLOAT_EPSILON));
1430 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1431 havocbot_symmetry_axis_m = m;
1432 havocbot_symmetry_axis_q = q;
1434 havocbot_symmetry_origin_order = n;
1438 entity havocbot_ctf_find_flag(entity bot)
1441 f = ctf_worldflaglist;
1444 if (CTF_SAMETEAM(bot, f))
1446 f = f.ctf_worldflagnext;
1451 entity havocbot_ctf_find_enemy_flag(entity bot)
1454 f = ctf_worldflaglist;
1459 if(CTF_DIFFTEAM(bot, f))
1466 else if(!bot.flagcarried)
1470 else if (CTF_DIFFTEAM(bot, f))
1472 f = f.ctf_worldflagnext;
1477 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1484 FOREACH_CLIENT(IS_PLAYER(it), {
1485 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1488 if(vdist(it.origin - org, <, tc_radius))
1497 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1500 head = ctf_worldflaglist;
1503 if (CTF_SAMETEAM(this, head))
1505 head = head.ctf_worldflagnext;
1508 navigation_routerating(this, head, ratingscale, 10000);
1512 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1515 head = ctf_worldflaglist;
1518 if (CTF_SAMETEAM(this, head))
1520 if (this.flagcarried)
1521 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1523 head = head.ctf_worldflagnext; // skip base if it has a different group
1528 head = head.ctf_worldflagnext;
1533 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1536 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1539 head = ctf_worldflaglist;
1544 if(CTF_DIFFTEAM(this, head))
1548 if(this.flagcarried)
1551 else if(!this.flagcarried)
1555 else if(CTF_DIFFTEAM(this, head))
1557 head = head.ctf_worldflagnext;
1561 if (head.ctf_status == FLAG_CARRY)
1563 // adjust rating of our flag carrier depending on his health
1564 head = head.tag_entity;
1565 float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1566 ratingscale += ratingscale * f * 0.1;
1568 navigation_routerating(this, head, ratingscale, 10000);
1572 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1574 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1576 if (!bot_waypoints_for_items)
1578 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1584 head = havocbot_ctf_find_enemy_flag(this);
1589 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1592 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1596 mf = havocbot_ctf_find_flag(this);
1598 if(mf.ctf_status == FLAG_BASE)
1602 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1605 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1608 head = ctf_worldflaglist;
1611 // flag is out in the field
1612 if(head.ctf_status != FLAG_BASE)
1613 if(head.tag_entity==NULL) // dropped
1617 if(vdist(org - head.origin, <, df_radius))
1618 navigation_routerating(this, head, ratingscale, 10000);
1621 navigation_routerating(this, head, ratingscale, 10000);
1624 head = head.ctf_worldflagnext;
1628 void havocbot_ctf_reset_role(entity this)
1630 float cdefense, cmiddle, coffense;
1637 if (this.flagcarried)
1639 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1643 mf = havocbot_ctf_find_flag(this);
1644 ef = havocbot_ctf_find_enemy_flag(this);
1646 // Retrieve stolen flag
1647 if(mf.ctf_status!=FLAG_BASE)
1649 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1653 // If enemy flag is taken go to the middle to intercept pursuers
1654 if(ef.ctf_status!=FLAG_BASE)
1656 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1660 // if there is no one else on the team switch to offense
1662 // don't check if this bot is a player since it isn't true when the bot is added to the server
1663 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1667 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1670 else if (time < CS(this).jointime + 1)
1672 // if bots spawn all at once set good default roles
1675 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1678 else if (count == 2)
1680 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1685 // Evaluate best position to take
1686 // Count mates on middle position
1687 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1689 // Count mates on defense position
1690 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1692 // Count mates on offense position
1693 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1695 if(cdefense<=coffense)
1696 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1697 else if(coffense<=cmiddle)
1698 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1700 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1702 // if bots spawn all at once assign them a more appropriated role after a while
1703 if (time < CS(this).jointime + 1 && count > 2)
1704 this.havocbot_role_timeout = time + 10 + random() * 10;
1707 bool havocbot_ctf_is_basewaypoint(entity item)
1709 if (item.classname != "waypoint")
1712 entity head = ctf_worldflaglist;
1715 if (item == head.bot_basewaypoint)
1717 head = head.ctf_worldflagnext;
1722 void havocbot_role_ctf_carrier(entity this)
1726 havocbot_ctf_reset_role(this);
1730 if (this.flagcarried == NULL)
1732 havocbot_ctf_reset_role(this);
1736 if (navigation_goalrating_timeout(this))
1738 navigation_goalrating_start(this);
1741 entity mf = havocbot_ctf_find_flag(this);
1742 vector base_org = mf.dropped_origin;
1743 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1745 havocbot_goalrating_ctf_enemybase(this, base_rating);
1747 havocbot_goalrating_ctf_ourbase(this, base_rating);
1749 // start collecting items very close to the bot but only inside of own base radius
1750 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1751 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1753 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1755 navigation_goalrating_end(this);
1757 navigation_goalrating_timeout_set(this);
1759 entity goal = this.goalentity;
1760 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1761 this.goalentity_lock_timeout = time + ((this.enemy) ? 2 : 3);
1764 this.havocbot_cantfindflag = time + 10;
1765 else if (time > this.havocbot_cantfindflag)
1767 // Can't navigate to my own base, suicide!
1768 // TODO: drop it and wander around
1769 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1775 void havocbot_role_ctf_escort(entity this)
1781 havocbot_ctf_reset_role(this);
1785 if (this.flagcarried)
1787 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1791 // If enemy flag is back on the base switch to previous role
1792 ef = havocbot_ctf_find_enemy_flag(this);
1793 if(ef.ctf_status==FLAG_BASE)
1795 this.havocbot_role = this.havocbot_previous_role;
1796 this.havocbot_role_timeout = 0;
1799 if (ef.ctf_status == FLAG_DROPPED)
1801 navigation_goalrating_timeout_expire(this, 1);
1805 // If the flag carrier reached the base switch to defense
1806 mf = havocbot_ctf_find_flag(this);
1807 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1809 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1813 // Set the role timeout if necessary
1814 if (!this.havocbot_role_timeout)
1816 this.havocbot_role_timeout = time + random() * 30 + 60;
1819 // If nothing happened just switch to previous role
1820 if (time > this.havocbot_role_timeout)
1822 this.havocbot_role = this.havocbot_previous_role;
1823 this.havocbot_role_timeout = 0;
1827 // Chase the flag carrier
1828 if (navigation_goalrating_timeout(this))
1830 navigation_goalrating_start(this);
1833 havocbot_goalrating_ctf_enemyflag(this, 10000);
1834 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1835 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1837 navigation_goalrating_end(this);
1839 navigation_goalrating_timeout_set(this);
1843 void havocbot_role_ctf_offense(entity this)
1850 havocbot_ctf_reset_role(this);
1854 if (this.flagcarried)
1856 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1861 mf = havocbot_ctf_find_flag(this);
1862 ef = havocbot_ctf_find_enemy_flag(this);
1865 if(mf.ctf_status!=FLAG_BASE)
1868 pos = mf.tag_entity.origin;
1872 // Try to get it if closer than the enemy base
1873 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1875 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1880 // Escort flag carrier
1881 if(ef.ctf_status!=FLAG_BASE)
1884 pos = ef.tag_entity.origin;
1888 if(vdist(pos - mf.dropped_origin, >, 700))
1890 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1895 // Set the role timeout if necessary
1896 if (!this.havocbot_role_timeout)
1897 this.havocbot_role_timeout = time + 120;
1899 if (time > this.havocbot_role_timeout)
1901 havocbot_ctf_reset_role(this);
1905 if (navigation_goalrating_timeout(this))
1907 navigation_goalrating_start(this);
1910 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1911 havocbot_goalrating_ctf_enemybase(this, 10000);
1912 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1914 navigation_goalrating_end(this);
1916 navigation_goalrating_timeout_set(this);
1920 // Retriever (temporary role):
1921 void havocbot_role_ctf_retriever(entity this)
1927 havocbot_ctf_reset_role(this);
1931 if (this.flagcarried)
1933 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1937 // If flag is back on the base switch to previous role
1938 mf = havocbot_ctf_find_flag(this);
1939 if(mf.ctf_status==FLAG_BASE)
1941 if (mf.enemy == this) // did this bot return the flag?
1942 navigation_goalrating_timeout_force(this);
1943 havocbot_ctf_reset_role(this);
1947 if (!this.havocbot_role_timeout)
1948 this.havocbot_role_timeout = time + 20;
1950 if (time > this.havocbot_role_timeout)
1952 havocbot_ctf_reset_role(this);
1956 if (navigation_goalrating_timeout(this))
1958 const float RT_RADIUS = 10000;
1960 navigation_goalrating_start(this);
1963 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1964 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1965 havocbot_goalrating_ctf_enemybase(this, 8000);
1966 entity ef = havocbot_ctf_find_enemy_flag(this);
1967 vector enemy_base_org = ef.dropped_origin;
1968 // start collecting items very close to the bot but only inside of enemy base radius
1969 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1970 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1971 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1973 navigation_goalrating_end(this);
1975 navigation_goalrating_timeout_set(this);
1979 void havocbot_role_ctf_middle(entity this)
1985 havocbot_ctf_reset_role(this);
1989 if (this.flagcarried)
1991 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1995 mf = havocbot_ctf_find_flag(this);
1996 if(mf.ctf_status!=FLAG_BASE)
1998 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2002 if (!this.havocbot_role_timeout)
2003 this.havocbot_role_timeout = time + 10;
2005 if (time > this.havocbot_role_timeout)
2007 havocbot_ctf_reset_role(this);
2011 if (navigation_goalrating_timeout(this))
2015 org = havocbot_middlepoint;
2016 org.z = this.origin.z;
2018 navigation_goalrating_start(this);
2021 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
2022 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
2023 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2024 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2025 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2026 havocbot_goalrating_ctf_enemybase(this, 3000);
2028 navigation_goalrating_end(this);
2030 entity goal = this.goalentity;
2031 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
2032 this.goalentity_lock_timeout = time + 2;
2034 navigation_goalrating_timeout_set(this);
2038 void havocbot_role_ctf_defense(entity this)
2044 havocbot_ctf_reset_role(this);
2048 if (this.flagcarried)
2050 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2054 // If own flag was captured
2055 mf = havocbot_ctf_find_flag(this);
2056 if(mf.ctf_status!=FLAG_BASE)
2058 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2062 if (!this.havocbot_role_timeout)
2063 this.havocbot_role_timeout = time + 30;
2065 if (time > this.havocbot_role_timeout)
2067 havocbot_ctf_reset_role(this);
2070 if (navigation_goalrating_timeout(this))
2072 vector org = mf.dropped_origin;
2074 navigation_goalrating_start(this);
2076 // if enemies are closer to our base, go there
2077 entity closestplayer = NULL;
2078 float distance, bestdistance = 10000;
2079 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2080 distance = vlen(org - it.origin);
2081 if(distance<bestdistance)
2084 bestdistance = distance;
2090 if(DIFF_TEAM(closestplayer, this))
2091 if(vdist(org - this.origin, >, 1000))
2092 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2093 havocbot_goalrating_ctf_ourbase(this, 10000);
2095 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2096 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2097 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2098 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2099 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2101 navigation_goalrating_end(this);
2103 navigation_goalrating_timeout_set(this);
2107 void havocbot_role_ctf_setrole(entity bot, int role)
2109 string s = "(null)";
2112 case HAVOCBOT_CTF_ROLE_CARRIER:
2114 bot.havocbot_role = havocbot_role_ctf_carrier;
2115 bot.havocbot_role_timeout = 0;
2116 bot.havocbot_cantfindflag = time + 10;
2117 if (bot.havocbot_previous_role != bot.havocbot_role)
2118 navigation_goalrating_timeout_force(bot);
2120 case HAVOCBOT_CTF_ROLE_DEFENSE:
2122 bot.havocbot_role = havocbot_role_ctf_defense;
2123 bot.havocbot_role_timeout = 0;
2125 case HAVOCBOT_CTF_ROLE_MIDDLE:
2127 bot.havocbot_role = havocbot_role_ctf_middle;
2128 bot.havocbot_role_timeout = 0;
2130 case HAVOCBOT_CTF_ROLE_OFFENSE:
2132 bot.havocbot_role = havocbot_role_ctf_offense;
2133 bot.havocbot_role_timeout = 0;
2135 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2137 bot.havocbot_previous_role = bot.havocbot_role;
2138 bot.havocbot_role = havocbot_role_ctf_retriever;
2139 bot.havocbot_role_timeout = time + 10;
2140 if (bot.havocbot_previous_role != bot.havocbot_role)
2141 navigation_goalrating_timeout_expire(bot, 2);
2143 case HAVOCBOT_CTF_ROLE_ESCORT:
2145 bot.havocbot_previous_role = bot.havocbot_role;
2146 bot.havocbot_role = havocbot_role_ctf_escort;
2147 bot.havocbot_role_timeout = time + 30;
2148 if (bot.havocbot_previous_role != bot.havocbot_role)
2149 navigation_goalrating_timeout_expire(bot, 2);
2152 LOG_TRACE(bot.netname, " switched to ", s);
2160 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2162 entity player = M_ARGV(0, entity);
2164 int t = 0, t2 = 0, t3 = 0;
2165 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)
2167 // initially clear items so they can be set as necessary later.
2168 STAT(OBJECTIVE_STATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2169 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2170 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2171 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2172 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2173 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2175 // scan through all the flags and notify the client about them
2176 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2178 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2179 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2180 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2181 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2182 if(flag.team == 0 && !b5) { b5 = true; t = CTF_NEUTRAL_FLAG_CARRYING; t2 = CTF_NEUTRAL_FLAG_TAKEN; t3 = CTF_NEUTRAL_FLAG_LOST; STAT(OBJECTIVE_STATUS, player) |= CTF_FLAG_NEUTRAL; }
2184 switch(flag.ctf_status)
2189 if((flag.owner == player) || (flag.pass_sender == player))
2190 STAT(OBJECTIVE_STATUS, player) |= t; // carrying: player is currently carrying the flag
2192 STAT(OBJECTIVE_STATUS, player) |= t2; // taken: someone else is carrying the flag
2197 STAT(OBJECTIVE_STATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2203 // item for stopping players from capturing the flag too often
2204 if(player.ctf_captureshielded)
2205 STAT(OBJECTIVE_STATUS, player) |= CTF_SHIELDED;
2208 STAT(OBJECTIVE_STATUS, player) |= CTF_STALEMATE;
2210 // update the health of the flag carrier waypointsprite
2211 if(player.wps_flagcarrier)
2212 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);
2215 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in damage.qc
2217 entity frag_attacker = M_ARGV(1, entity);
2218 entity frag_target = M_ARGV(2, entity);
2219 float frag_damage = M_ARGV(4, float);
2220 vector frag_force = M_ARGV(6, vector);
2222 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2224 if(frag_target == frag_attacker) // damage done to yourself
2226 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2227 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2229 else // damage done to everyone else
2231 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2232 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2235 M_ARGV(4, float) = frag_damage;
2236 M_ARGV(6, vector) = frag_force;
2238 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2240 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
2241 && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2243 frag_target.wps_helpme_time = time;
2244 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2246 // todo: add notification for when flag carrier needs help?
2250 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2252 entity frag_attacker = M_ARGV(1, entity);
2253 entity frag_target = M_ARGV(2, entity);
2255 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2257 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2258 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2261 if(frag_target.flagcarried)
2263 entity tmp_entity = frag_target.flagcarried;
2264 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2265 tmp_entity.ctf_dropper = NULL;
2269 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2271 M_ARGV(2, float) = 0; // frag score
2272 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2275 void ctf_RemovePlayer(entity player)
2277 if(player.flagcarried)
2278 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2280 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2282 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2283 if(flag.pass_target == player) { flag.pass_target = NULL; }
2284 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2288 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2290 entity player = M_ARGV(0, entity);
2292 ctf_RemovePlayer(player);
2295 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2297 entity player = M_ARGV(0, entity);
2299 ctf_RemovePlayer(player);
2302 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2304 if(!autocvar_g_ctf_leaderboard)
2307 entity player = M_ARGV(0, entity);
2309 race_SendAll(player, true);
2312 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2314 if(!autocvar_g_ctf_leaderboard)
2317 entity player = M_ARGV(0, entity);
2319 race_checkAndWriteName(player);
2322 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2324 entity player = M_ARGV(0, entity);
2326 if(player.flagcarried)
2327 if(!autocvar_g_ctf_portalteleport)
2328 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2331 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2333 if(MUTATOR_RETURNVALUE || game_stopped) return;
2335 entity player = M_ARGV(0, entity);
2337 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2339 // pass the flag to a team mate
2340 if(autocvar_g_ctf_pass)
2342 entity head, closest_target = NULL;
2343 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2345 while(head) // find the closest acceptable target to pass to
2347 if(IS_PLAYER(head) && !IS_DEAD(head))
2348 if(head != player && SAME_TEAM(head, player))
2349 if(!head.speedrunning && !head.vehicle)
2351 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in damage.qc)
2352 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2353 vector passer_center = CENTER_OR_VIEWOFS(player);
2355 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2357 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2359 if(IS_BOT_CLIENT(head))
2361 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2362 ctf_Handle_Throw(head, player, DROP_PASS);
2366 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2367 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2369 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2372 else if(player.flagcarried && !head.flagcarried)
2376 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2377 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2378 { closest_target = head; }
2380 else { closest_target = head; }
2387 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2390 // throw the flag in front of you
2391 if(autocvar_g_ctf_throw && player.flagcarried)
2393 if(player.throw_count == -1)
2395 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2397 player.throw_prevtime = time;
2398 player.throw_count = 1;
2399 ctf_Handle_Throw(player, NULL, DROP_THROW);
2404 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2410 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2411 else { player.throw_count += 1; }
2412 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2414 player.throw_prevtime = time;
2415 ctf_Handle_Throw(player, NULL, DROP_THROW);
2422 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2424 entity player = M_ARGV(0, entity);
2426 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2428 player.wps_helpme_time = time;
2429 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2431 else // create a normal help me waypointsprite
2433 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2434 WaypointSprite_Ping(player.wps_helpme);
2440 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2442 entity player = M_ARGV(0, entity);
2443 entity veh = M_ARGV(1, entity);
2445 if(player.flagcarried)
2447 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2449 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2453 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2454 setattachment(player.flagcarried, veh, "");
2455 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2456 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2457 //player.flagcarried.angles = '0 0 0';
2463 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2465 entity player = M_ARGV(0, entity);
2467 if(player.flagcarried)
2469 setattachment(player.flagcarried, player, "");
2470 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2471 player.flagcarried.scale = FLAG_SCALE;
2472 player.flagcarried.angles = '0 0 0';
2473 player.flagcarried.nodrawtoclient = NULL;
2478 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2480 entity player = M_ARGV(0, entity);
2482 if(player.flagcarried)
2484 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2485 ctf_RespawnFlag(player.flagcarried);
2490 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2492 entity flag; // temporary entity for the search method
2494 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2496 switch(flag.ctf_status)
2501 // lock the flag, game is over
2502 set_movetype(flag, MOVETYPE_NONE);
2503 flag.takedamage = DAMAGE_NO;
2504 flag.solid = SOLID_NOT;
2505 flag.nextthink = false; // stop thinking
2507 //dprint("stopping the ", flag.netname, " from moving.\n");
2515 // do nothing for these flags
2522 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2524 entity bot = M_ARGV(0, entity);
2526 havocbot_ctf_reset_role(bot);
2530 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2532 M_ARGV(1, string) = "ctf_team";
2535 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2537 int record_page = M_ARGV(0, int);
2538 string ret_string = M_ARGV(1, string);
2540 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2542 if (MapInfo_Get_ByID(i))
2544 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2550 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2551 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2555 M_ARGV(1, string) = ret_string;
2558 bool superspec_Spectate(entity this, entity targ); // TODO
2559 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2560 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2562 entity player = M_ARGV(0, entity);
2563 string cmd_name = M_ARGV(1, string);
2564 int cmd_argc = M_ARGV(2, int);
2566 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2568 if(cmd_name == "followfc")
2580 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2581 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2582 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2583 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2587 FOREACH_CLIENT(IS_PLAYER(it), {
2588 if(it.flagcarried && (it.team == _team || _team == 0))
2591 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2592 continue; // already spectating this fc, try another
2593 return superspec_Spectate(player, it);
2598 superspec_msg("", "", player, "No active flag carrier\n", 1);
2603 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2605 entity frag_target = M_ARGV(0, entity);
2607 if(frag_target.flagcarried)
2608 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2611 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2613 entity player = M_ARGV(0, entity);
2614 if(player.flagcarried)
2615 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2623 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2624 CTF flag for team one (Red).
2626 "angle" Angle the flag will point (minus 90 degrees)...
2627 "model" model to use, note this needs red and blue as skins 0 and 1...
2628 "noise" sound played when flag is picked up...
2629 "noise1" sound played when flag is returned by a teammate...
2630 "noise2" sound played when flag is captured...
2631 "noise3" sound played when flag is lost in the field and respawns itself...
2632 "noise4" sound played when flag is dropped by a player...
2633 "noise5" sound played when flag touches the ground... */
2634 spawnfunc(item_flag_team1)
2636 if(!g_ctf) { delete(this); return; }
2638 ctf_FlagSetup(NUM_TEAM_1, this);
2641 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2642 CTF flag for team two (Blue).
2644 "angle" Angle the flag will point (minus 90 degrees)...
2645 "model" model to use, note this needs red and blue as skins 0 and 1...
2646 "noise" sound played when flag is picked up...
2647 "noise1" sound played when flag is returned by a teammate...
2648 "noise2" sound played when flag is captured...
2649 "noise3" sound played when flag is lost in the field and respawns itself...
2650 "noise4" sound played when flag is dropped by a player...
2651 "noise5" sound played when flag touches the ground... */
2652 spawnfunc(item_flag_team2)
2654 if(!g_ctf) { delete(this); return; }
2656 ctf_FlagSetup(NUM_TEAM_2, this);
2659 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2660 CTF flag for team three (Yellow).
2662 "angle" Angle the flag will point (minus 90 degrees)...
2663 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2664 "noise" sound played when flag is picked up...
2665 "noise1" sound played when flag is returned by a teammate...
2666 "noise2" sound played when flag is captured...
2667 "noise3" sound played when flag is lost in the field and respawns itself...
2668 "noise4" sound played when flag is dropped by a player...
2669 "noise5" sound played when flag touches the ground... */
2670 spawnfunc(item_flag_team3)
2672 if(!g_ctf) { delete(this); return; }
2674 ctf_FlagSetup(NUM_TEAM_3, this);
2677 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2678 CTF flag for team four (Pink).
2680 "angle" Angle the flag will point (minus 90 degrees)...
2681 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2682 "noise" sound played when flag is picked up...
2683 "noise1" sound played when flag is returned by a teammate...
2684 "noise2" sound played when flag is captured...
2685 "noise3" sound played when flag is lost in the field and respawns itself...
2686 "noise4" sound played when flag is dropped by a player...
2687 "noise5" sound played when flag touches the ground... */
2688 spawnfunc(item_flag_team4)
2690 if(!g_ctf) { delete(this); return; }
2692 ctf_FlagSetup(NUM_TEAM_4, this);
2695 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2698 "angle" Angle the flag will point (minus 90 degrees)...
2699 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2700 "noise" sound played when flag is picked up...
2701 "noise1" sound played when flag is returned by a teammate...
2702 "noise2" sound played when flag is captured...
2703 "noise3" sound played when flag is lost in the field and respawns itself...
2704 "noise4" sound played when flag is dropped by a player...
2705 "noise5" sound played when flag touches the ground... */
2706 spawnfunc(item_flag_neutral)
2708 if(!g_ctf) { delete(this); return; }
2709 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2711 ctf_FlagSetup(0, this);
2714 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2715 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2716 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.
2718 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2719 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2722 if(!g_ctf) { delete(this); return; }
2724 this.team = this.cnt + 1;
2727 // compatibility for quake maps
2728 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2729 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2730 spawnfunc(info_player_team1);
2731 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2732 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2733 spawnfunc(info_player_team2);
2734 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2735 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2737 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2738 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2740 // compatibility for wop maps
2741 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2742 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2743 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2744 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2745 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2746 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2754 void ctf_ScoreRules(int teams)
2756 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2757 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2758 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2759 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2760 field(SP_CTF_PICKUPS, "pickups", 0);
2761 field(SP_CTF_FCKILLS, "fckills", 0);
2762 field(SP_CTF_RETURNS, "returns", 0);
2763 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2767 // code from here on is just to support maps that don't have flag and team entities
2768 void ctf_SpawnTeam (string teamname, int teamcolor)
2770 entity this = new_pure(ctf_team);
2771 this.netname = teamname;
2772 this.cnt = teamcolor - 1;
2773 this.spawnfunc_checked = true;
2774 this.team = teamcolor;
2777 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2782 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2784 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2785 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2787 switch(tmp_entity.team)
2789 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2790 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2791 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2792 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2794 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2797 havocbot_ctf_calculate_middlepoint();
2799 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2801 ctf_teams = 0; // so set the default red and blue teams
2802 BITSET_ASSIGN(ctf_teams, BIT(0));
2803 BITSET_ASSIGN(ctf_teams, BIT(1));
2806 //ctf_teams = bound(2, ctf_teams, 4);
2808 // if no teams are found, spawn defaults
2809 if(find(NULL, classname, "ctf_team") == NULL)
2811 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2812 if(ctf_teams & BIT(0))
2813 ctf_SpawnTeam("Red", NUM_TEAM_1);
2814 if(ctf_teams & BIT(1))
2815 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2816 if(ctf_teams & BIT(2))
2817 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2818 if(ctf_teams & BIT(3))
2819 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2822 ctf_ScoreRules(ctf_teams);
2825 void ctf_Initialize()
2827 CTF_FLAG = NEW(Flag);
2828 record_type = CTF_RECORD;
2829 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2831 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2832 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2833 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2835 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);