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))
735 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);
736 else if(DIFF_TEAM(player, it))
737 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_ENEMY_OTHER), Team_ColorCode(player.team), player.netname);
740 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
743 GameRules_scoring_add(player, CTF_PICKUPS, 1);
744 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
749 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
750 ctf_EventLog("steal", flag.team, player);
756 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);
757 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);
758 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
759 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
760 ctf_EventLog("pickup", flag.team, player);
768 if(pickuptype == PICKUP_BASE)
770 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
771 if((player.speedrunning) && (ctf_captimerecord))
772 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
776 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
779 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
780 ctf_FlagcarrierWaypoints(player);
781 WaypointSprite_Ping(player.wps_flagcarrier);
785 // ===================
786 // Main Flag Functions
787 // ===================
789 void ctf_CheckFlagReturn(entity flag, int returntype)
791 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
793 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH)); }
795 if((GetResource(flag, RES_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
800 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
802 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
803 case RETURN_SPEEDRUN:
804 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
805 case RETURN_NEEDKILL:
806 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
809 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
811 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
812 ctf_EventLog("returned", flag.team, NULL);
814 ctf_RespawnFlag(flag);
819 bool ctf_Stalemate_Customize(entity this, entity client)
821 // make spectators see what the player would see
822 entity e = WaypointSprite_getviewentity(client);
823 entity wp_owner = this.owner;
826 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
827 if(SAME_TEAM(wp_owner, e)) { return false; }
828 if(!IS_PLAYER(e)) { return false; }
833 void ctf_CheckStalemate()
836 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
839 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
841 // build list of stale flags
842 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
844 if(autocvar_g_ctf_stalemate)
845 if(tmp_entity.ctf_status != FLAG_BASE)
846 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
848 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
849 ctf_staleflaglist = tmp_entity;
851 switch(tmp_entity.team)
853 case NUM_TEAM_1: ++stale_red_flags; break;
854 case NUM_TEAM_2: ++stale_blue_flags; break;
855 case NUM_TEAM_3: ++stale_yellow_flags; break;
856 case NUM_TEAM_4: ++stale_pink_flags; break;
857 default: ++stale_neutral_flags; break;
863 stale_flags = (stale_neutral_flags >= 1);
865 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
867 if(ctf_oneflag && stale_flags == 1)
868 ctf_stalemate = true;
869 else if(stale_flags >= 2)
870 ctf_stalemate = true;
871 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
872 { ctf_stalemate = false; wpforenemy_announced = false; }
873 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
874 { ctf_stalemate = false; wpforenemy_announced = false; }
876 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
879 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
881 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
883 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);
884 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
885 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
889 if (!wpforenemy_announced)
891 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)); });
893 wpforenemy_announced = true;
898 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
900 if(ITEM_DAMAGE_NEEDKILL(deathtype))
902 if(autocvar_g_ctf_flag_return_damage_delay)
903 this.ctf_flagdamaged_byworld = true;
906 SetResourceExplicit(this, RES_HEALTH, 0);
907 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
911 if(autocvar_g_ctf_flag_return_damage)
913 // reduce health and check if it should be returned
914 TakeResource(this, RES_HEALTH, damage);
915 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
920 void ctf_FlagThink(entity this)
925 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
928 if(this == ctf_worldflaglist) // only for the first flag
929 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
932 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
933 LOG_TRACE("wtf the flag got squashed?");
934 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
935 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
936 setsize(this, this.m_mins, this.m_maxs);
940 switch(this.ctf_status)
944 if(autocvar_g_ctf_dropped_capture_radius)
946 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
947 if(tmp_entity.ctf_status == FLAG_DROPPED)
948 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
949 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
950 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
957 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
959 if(autocvar_g_ctf_flag_dropped_floatinwater)
961 vector midpoint = ((this.absmin + this.absmax) * 0.5);
962 if(pointcontents(midpoint) == CONTENT_WATER)
964 this.velocity = this.velocity * 0.5;
966 if (pointcontents(midpoint + eZ * FLAG_FLOAT_OFFSET_Z) == CONTENT_WATER)
967 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
969 { set_movetype(this, MOVETYPE_FLY); }
971 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
973 if(autocvar_g_ctf_flag_return_dropped)
975 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
977 SetResourceExplicit(this, RES_HEALTH, 0);
978 ctf_CheckFlagReturn(this, RETURN_DROPPED);
982 if(this.ctf_flagdamaged_byworld)
984 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
985 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
988 else if(autocvar_g_ctf_flag_return_time)
990 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
991 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
999 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
1001 SetResourceExplicit(this, RES_HEALTH, 0);
1002 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
1004 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
1005 ImpulseCommands(this.owner);
1007 if(autocvar_g_ctf_stalemate)
1009 if(time >= wpforenemy_nextthink)
1011 ctf_CheckStalemate();
1012 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1015 if(CTF_SAMETEAM(this, this.owner) && this.team)
1017 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1018 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
1019 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
1020 ctf_Handle_Return(this, this.owner);
1027 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1028 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1029 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1031 if((this.pass_target == NULL)
1032 || (IS_DEAD(this.pass_target))
1033 || (this.pass_target.flagcarried)
1034 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1035 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1036 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1038 // give up, pass failed
1039 ctf_Handle_Drop(this, NULL, DROP_PASS);
1043 // still a viable target, go for it
1044 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1049 default: // this should never happen
1051 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1057 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1060 if(game_stopped) return;
1061 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1063 bool is_not_monster = (!IS_MONSTER(toucher));
1065 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1066 if(ITEM_TOUCH_NEEDKILL())
1068 if(!autocvar_g_ctf_flag_return_damage_delay)
1070 SetResourceExplicit(flag, RES_HEALTH, 0);
1071 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1073 if(!flag.ctf_flagdamaged_byworld) { return; }
1076 // special touch behaviors
1077 if(STAT(FROZEN, toucher)) { return; }
1078 else if(IS_VEHICLE(toucher))
1080 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1081 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1083 return; // do nothing
1085 else if(IS_MONSTER(toucher))
1087 if(!autocvar_g_ctf_allow_monster_touch)
1088 return; // do nothing
1090 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1092 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1094 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1095 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1096 flag.wait = time + FLAG_TOUCHRATE;
1100 else if(IS_DEAD(toucher)) { return; }
1102 switch(flag.ctf_status)
1108 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1109 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1110 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1111 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1113 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1114 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1115 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)
1117 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1118 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1120 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1121 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1127 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1128 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1129 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1130 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1136 LOG_TRACE("Someone touched a flag even though it was being carried?");
1142 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1144 if(DIFF_TEAM(toucher, flag.pass_sender))
1146 if(ctf_Immediate_Return_Allowed(flag, toucher))
1147 ctf_Handle_Return(flag, toucher);
1148 else if(is_not_monster && (!toucher.flagcarried))
1149 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1151 else if(!toucher.flagcarried)
1152 ctf_Handle_Retrieve(flag, toucher);
1159 .float last_respawn;
1160 void ctf_RespawnFlag(entity flag)
1162 flag.watertype = CONTENT_EMPTY; // TODO: it is unclear why this workaround is needed, likely many other potential breakage points!!
1163 // check for flag respawn being called twice in a row
1164 if(flag.last_respawn > time - 0.5)
1165 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1167 flag.last_respawn = time;
1169 // reset the player (if there is one)
1170 if((flag.owner) && (flag.owner.flagcarried == flag))
1172 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1173 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1174 WaypointSprite_Kill(flag.wps_flagcarrier);
1176 flag.owner.flagcarried = NULL;
1177 GameRules_scoring_vip(flag.owner, false);
1179 if(flag.speedrunning)
1180 ctf_FakeTimeLimit(flag.owner, -1);
1183 if((flag.owner) && (flag.owner.vehicle))
1184 flag.scale = FLAG_SCALE;
1186 if(flag.ctf_status == FLAG_DROPPED)
1187 { WaypointSprite_Kill(flag.wps_flagdropped); }
1190 setattachment(flag, NULL, "");
1191 setorigin(flag, flag.ctf_spawnorigin);
1193 //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1194 set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1195 flag.takedamage = DAMAGE_NO;
1196 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1197 flag.solid = SOLID_TRIGGER;
1198 flag.velocity = '0 0 0';
1199 flag.angles = flag.mangle;
1200 flag.flags = FL_ITEM | FL_NOTARGET;
1202 flag.ctf_status = FLAG_BASE;
1204 flag.pass_distance = 0;
1205 flag.pass_sender = NULL;
1206 flag.pass_target = NULL;
1207 flag.ctf_dropper = NULL;
1208 flag.ctf_pickuptime = 0;
1209 flag.ctf_droptime = 0;
1210 flag.ctf_flagdamaged_byworld = false;
1211 navigation_dynamicgoal_unset(flag);
1213 ctf_CheckStalemate();
1216 void ctf_Reset(entity this)
1218 if(this.owner && IS_PLAYER(this.owner))
1219 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1222 ctf_RespawnFlag(this);
1225 bool ctf_FlagBase_Customize(entity this, entity client)
1227 entity e = WaypointSprite_getviewentity(client);
1228 entity wp_owner = this.owner;
1229 entity flag = e.flagcarried;
1230 if(flag && CTF_SAMETEAM(e, flag))
1232 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1237 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1240 waypoint_spawnforitem_force(this, this.origin);
1241 navigation_dynamicgoal_init(this, true);
1247 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1248 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1249 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1250 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1251 default: basename = WP_FlagBaseNeutral; break;
1254 if(autocvar_g_ctf_flag_waypoint)
1256 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1257 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1258 wp.fade_rate = autocvar_g_ctf_flag_waypoint_maxdistance;
1259 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1260 setcefc(wp, ctf_FlagBase_Customize);
1263 // captureshield setup
1264 ctf_CaptureShield_Spawn(this);
1269 void ctf_FlagSetup(int teamnum, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1272 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1273 ctf_worldflaglist = flag;
1275 setattachment(flag, NULL, "");
1277 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnum), Team_ColorName_Upper(teamnum)));
1278 flag.team = teamnum;
1279 flag.classname = "item_flag_team";
1280 flag.target = "###item###"; // for finding the nearest item using findnearest
1281 flag.flags = FL_ITEM | FL_NOTARGET;
1282 IL_PUSH(g_items, flag);
1283 flag.solid = SOLID_TRIGGER;
1284 flag.takedamage = DAMAGE_NO;
1285 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1286 flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1287 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1288 flag.event_damage = ctf_FlagDamage;
1289 flag.pushable = true;
1290 flag.teleportable = TELEPORT_NORMAL;
1291 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1292 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1293 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1294 if(flag.damagedbycontents)
1295 IL_PUSH(g_damagedbycontents, flag);
1296 flag.velocity = '0 0 0';
1297 flag.mangle = flag.angles;
1298 flag.reset = ctf_Reset;
1299 settouch(flag, ctf_FlagTouch);
1300 setthink(flag, ctf_FlagThink);
1301 flag.nextthink = time + FLAG_THINKRATE;
1302 flag.ctf_status = FLAG_BASE;
1304 // crudely force them all to 0
1305 if(autocvar_g_ctf_score_ignore_fields)
1306 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1308 string teamname = Static_Team_ColorName_Lower(teamnum);
1310 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1311 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1312 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1313 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnum).eent_eff_name; }
1314 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnum).eent_eff_name; }
1315 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnum).eent_eff_name; }
1319 if(flag.s == "") flag.s = b; \
1320 precache_sound(flag.s);
1322 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnum))))
1323 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnum))))
1324 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnum))))
1325 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnum))))
1326 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1327 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1328 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1332 precache_model(flag.model);
1335 _setmodel(flag, flag.model); // precision set below
1336 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1337 flag.m_mins = flag.mins; // store these for squash checks
1338 flag.m_maxs = flag.maxs;
1339 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1341 if(autocvar_g_ctf_flag_glowtrails)
1345 case NUM_TEAM_1: flag.glow_color = 251; break;
1346 case NUM_TEAM_2: flag.glow_color = 210; break;
1347 case NUM_TEAM_3: flag.glow_color = 110; break;
1348 case NUM_TEAM_4: flag.glow_color = 145; break;
1349 default: flag.glow_color = 254; break;
1351 flag.glow_size = 25;
1352 flag.glow_trail = 1;
1355 flag.effects |= EF_LOWPRECISION;
1356 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1357 if(autocvar_g_ctf_dynamiclights)
1361 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1362 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1363 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1364 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1365 default: flag.effects |= EF_DIMLIGHT; break;
1370 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1372 flag.dropped_origin = flag.origin;
1373 flag.noalign = true;
1374 set_movetype(flag, MOVETYPE_NONE);
1376 else // drop to floor, automatically find a platform and set that as spawn origin
1378 flag.noalign = false;
1380 set_movetype(flag, MOVETYPE_NONE);
1383 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1391 // NOTE: LEGACY CODE, needs to be re-written!
1393 void havocbot_ctf_calculate_middlepoint()
1397 vector fo = '0 0 0';
1400 f = ctf_worldflaglist;
1405 f = f.ctf_worldflagnext;
1411 havocbot_middlepoint = s / n;
1412 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1414 havocbot_symmetry_axis_m = 0;
1415 havocbot_symmetry_axis_q = 0;
1418 // for symmetrical editing of waypoints
1419 entity f1 = ctf_worldflaglist;
1420 entity f2 = f1.ctf_worldflagnext;
1421 float m = -(f1.origin.y - f2.origin.y) / (max(f1.origin.x - f2.origin.x, FLOAT_EPSILON));
1422 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1423 havocbot_symmetry_axis_m = m;
1424 havocbot_symmetry_axis_q = q;
1426 havocbot_symmetry_origin_order = n;
1430 entity havocbot_ctf_find_flag(entity bot)
1433 f = ctf_worldflaglist;
1436 if (CTF_SAMETEAM(bot, f))
1438 f = f.ctf_worldflagnext;
1443 entity havocbot_ctf_find_enemy_flag(entity bot)
1446 f = ctf_worldflaglist;
1451 if(CTF_DIFFTEAM(bot, f))
1458 else if(!bot.flagcarried)
1462 else if (CTF_DIFFTEAM(bot, f))
1464 f = f.ctf_worldflagnext;
1469 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1476 FOREACH_CLIENT(IS_PLAYER(it), {
1477 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1480 if(vdist(it.origin - org, <, tc_radius))
1489 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1492 head = ctf_worldflaglist;
1495 if (CTF_SAMETEAM(this, head))
1497 head = head.ctf_worldflagnext;
1500 navigation_routerating(this, head, ratingscale, 10000);
1504 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1507 head = ctf_worldflaglist;
1510 if (CTF_SAMETEAM(this, head))
1512 if (this.flagcarried)
1513 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1515 head = head.ctf_worldflagnext; // skip base if it has a different group
1520 head = head.ctf_worldflagnext;
1525 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1528 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1531 head = ctf_worldflaglist;
1536 if(CTF_DIFFTEAM(this, head))
1540 if(this.flagcarried)
1543 else if(!this.flagcarried)
1547 else if(CTF_DIFFTEAM(this, head))
1549 head = head.ctf_worldflagnext;
1553 if (head.ctf_status == FLAG_CARRY)
1555 // adjust rating of our flag carrier depending on his health
1556 head = head.tag_entity;
1557 float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1558 ratingscale += ratingscale * f * 0.1;
1560 navigation_routerating(this, head, ratingscale, 10000);
1564 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1566 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1568 if (!bot_waypoints_for_items)
1570 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1576 head = havocbot_ctf_find_enemy_flag(this);
1581 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1584 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1588 mf = havocbot_ctf_find_flag(this);
1590 if(mf.ctf_status == FLAG_BASE)
1594 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1597 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1600 head = ctf_worldflaglist;
1603 // flag is out in the field
1604 if(head.ctf_status != FLAG_BASE)
1605 if(head.tag_entity==NULL) // dropped
1609 if(vdist(org - head.origin, <, df_radius))
1610 navigation_routerating(this, head, ratingscale, 10000);
1613 navigation_routerating(this, head, ratingscale, 10000);
1616 head = head.ctf_worldflagnext;
1620 void havocbot_ctf_reset_role(entity this)
1622 float cdefense, cmiddle, coffense;
1629 if (this.flagcarried)
1631 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1635 mf = havocbot_ctf_find_flag(this);
1636 ef = havocbot_ctf_find_enemy_flag(this);
1638 // Retrieve stolen flag
1639 if(mf.ctf_status!=FLAG_BASE)
1641 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1645 // If enemy flag is taken go to the middle to intercept pursuers
1646 if(ef.ctf_status!=FLAG_BASE)
1648 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1652 // if there is no one else on the team switch to offense
1654 // don't check if this bot is a player since it isn't true when the bot is added to the server
1655 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1659 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1662 else if (time < CS(this).jointime + 1)
1664 // if bots spawn all at once set good default roles
1667 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1670 else if (count == 2)
1672 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1677 // Evaluate best position to take
1678 // Count mates on middle position
1679 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1681 // Count mates on defense position
1682 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1684 // Count mates on offense position
1685 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1687 if(cdefense<=coffense)
1688 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1689 else if(coffense<=cmiddle)
1690 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1692 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1694 // if bots spawn all at once assign them a more appropriated role after a while
1695 if (time < CS(this).jointime + 1 && count > 2)
1696 this.havocbot_role_timeout = time + 10 + random() * 10;
1699 bool havocbot_ctf_is_basewaypoint(entity item)
1701 if (item.classname != "waypoint")
1704 entity head = ctf_worldflaglist;
1707 if (item == head.bot_basewaypoint)
1709 head = head.ctf_worldflagnext;
1714 void havocbot_role_ctf_carrier(entity this)
1718 havocbot_ctf_reset_role(this);
1722 if (this.flagcarried == NULL)
1724 havocbot_ctf_reset_role(this);
1728 if (navigation_goalrating_timeout(this))
1730 navigation_goalrating_start(this);
1733 entity mf = havocbot_ctf_find_flag(this);
1734 vector base_org = mf.dropped_origin;
1735 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1737 havocbot_goalrating_ctf_enemybase(this, base_rating);
1739 havocbot_goalrating_ctf_ourbase(this, base_rating);
1741 // start collecting items very close to the bot but only inside of own base radius
1742 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1743 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1745 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1747 navigation_goalrating_end(this);
1749 navigation_goalrating_timeout_set(this);
1751 entity goal = this.goalentity;
1752 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1753 this.goalentity_lock_timeout = time + ((this.enemy) ? 2 : 3);
1756 this.havocbot_cantfindflag = time + 10;
1757 else if (time > this.havocbot_cantfindflag)
1759 // Can't navigate to my own base, suicide!
1760 // TODO: drop it and wander around
1761 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1767 void havocbot_role_ctf_escort(entity this)
1773 havocbot_ctf_reset_role(this);
1777 if (this.flagcarried)
1779 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1783 // If enemy flag is back on the base switch to previous role
1784 ef = havocbot_ctf_find_enemy_flag(this);
1785 if(ef.ctf_status==FLAG_BASE)
1787 this.havocbot_role = this.havocbot_previous_role;
1788 this.havocbot_role_timeout = 0;
1791 if (ef.ctf_status == FLAG_DROPPED)
1793 navigation_goalrating_timeout_expire(this, 1);
1797 // If the flag carrier reached the base switch to defense
1798 mf = havocbot_ctf_find_flag(this);
1799 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1801 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1805 // Set the role timeout if necessary
1806 if (!this.havocbot_role_timeout)
1808 this.havocbot_role_timeout = time + random() * 30 + 60;
1811 // If nothing happened just switch to previous role
1812 if (time > this.havocbot_role_timeout)
1814 this.havocbot_role = this.havocbot_previous_role;
1815 this.havocbot_role_timeout = 0;
1819 // Chase the flag carrier
1820 if (navigation_goalrating_timeout(this))
1822 navigation_goalrating_start(this);
1825 havocbot_goalrating_ctf_enemyflag(this, 10000);
1826 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1827 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1829 navigation_goalrating_end(this);
1831 navigation_goalrating_timeout_set(this);
1835 void havocbot_role_ctf_offense(entity this)
1842 havocbot_ctf_reset_role(this);
1846 if (this.flagcarried)
1848 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1853 mf = havocbot_ctf_find_flag(this);
1854 ef = havocbot_ctf_find_enemy_flag(this);
1857 if(mf.ctf_status!=FLAG_BASE)
1860 pos = mf.tag_entity.origin;
1864 // Try to get it if closer than the enemy base
1865 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1867 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1872 // Escort flag carrier
1873 if(ef.ctf_status!=FLAG_BASE)
1876 pos = ef.tag_entity.origin;
1880 if(vdist(pos - mf.dropped_origin, >, 700))
1882 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1887 // Set the role timeout if necessary
1888 if (!this.havocbot_role_timeout)
1889 this.havocbot_role_timeout = time + 120;
1891 if (time > this.havocbot_role_timeout)
1893 havocbot_ctf_reset_role(this);
1897 if (navigation_goalrating_timeout(this))
1899 navigation_goalrating_start(this);
1902 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1903 havocbot_goalrating_ctf_enemybase(this, 10000);
1904 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1906 navigation_goalrating_end(this);
1908 navigation_goalrating_timeout_set(this);
1912 // Retriever (temporary role):
1913 void havocbot_role_ctf_retriever(entity this)
1919 havocbot_ctf_reset_role(this);
1923 if (this.flagcarried)
1925 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1929 // If flag is back on the base switch to previous role
1930 mf = havocbot_ctf_find_flag(this);
1931 if(mf.ctf_status==FLAG_BASE)
1933 if (mf.enemy == this) // did this bot return the flag?
1934 navigation_goalrating_timeout_force(this);
1935 havocbot_ctf_reset_role(this);
1939 if (!this.havocbot_role_timeout)
1940 this.havocbot_role_timeout = time + 20;
1942 if (time > this.havocbot_role_timeout)
1944 havocbot_ctf_reset_role(this);
1948 if (navigation_goalrating_timeout(this))
1950 const float RT_RADIUS = 10000;
1952 navigation_goalrating_start(this);
1955 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1956 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1957 havocbot_goalrating_ctf_enemybase(this, 8000);
1958 entity ef = havocbot_ctf_find_enemy_flag(this);
1959 vector enemy_base_org = ef.dropped_origin;
1960 // start collecting items very close to the bot but only inside of enemy base radius
1961 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1962 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1963 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1965 navigation_goalrating_end(this);
1967 navigation_goalrating_timeout_set(this);
1971 void havocbot_role_ctf_middle(entity this)
1977 havocbot_ctf_reset_role(this);
1981 if (this.flagcarried)
1983 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1987 mf = havocbot_ctf_find_flag(this);
1988 if(mf.ctf_status!=FLAG_BASE)
1990 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1994 if (!this.havocbot_role_timeout)
1995 this.havocbot_role_timeout = time + 10;
1997 if (time > this.havocbot_role_timeout)
1999 havocbot_ctf_reset_role(this);
2003 if (navigation_goalrating_timeout(this))
2007 org = havocbot_middlepoint;
2008 org.z = this.origin.z;
2010 navigation_goalrating_start(this);
2013 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
2014 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
2015 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2016 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2017 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2018 havocbot_goalrating_ctf_enemybase(this, 3000);
2020 navigation_goalrating_end(this);
2022 entity goal = this.goalentity;
2023 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
2024 this.goalentity_lock_timeout = time + 2;
2026 navigation_goalrating_timeout_set(this);
2030 void havocbot_role_ctf_defense(entity this)
2036 havocbot_ctf_reset_role(this);
2040 if (this.flagcarried)
2042 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2046 // If own flag was captured
2047 mf = havocbot_ctf_find_flag(this);
2048 if(mf.ctf_status!=FLAG_BASE)
2050 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2054 if (!this.havocbot_role_timeout)
2055 this.havocbot_role_timeout = time + 30;
2057 if (time > this.havocbot_role_timeout)
2059 havocbot_ctf_reset_role(this);
2062 if (navigation_goalrating_timeout(this))
2064 vector org = mf.dropped_origin;
2066 navigation_goalrating_start(this);
2068 // if enemies are closer to our base, go there
2069 entity closestplayer = NULL;
2070 float distance, bestdistance = 10000;
2071 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2072 distance = vlen(org - it.origin);
2073 if(distance<bestdistance)
2076 bestdistance = distance;
2082 if(DIFF_TEAM(closestplayer, this))
2083 if(vdist(org - this.origin, >, 1000))
2084 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2085 havocbot_goalrating_ctf_ourbase(this, 10000);
2087 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2088 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2089 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2090 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2091 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2093 navigation_goalrating_end(this);
2095 navigation_goalrating_timeout_set(this);
2099 void havocbot_role_ctf_setrole(entity bot, int role)
2101 string s = "(null)";
2104 case HAVOCBOT_CTF_ROLE_CARRIER:
2106 bot.havocbot_role = havocbot_role_ctf_carrier;
2107 bot.havocbot_role_timeout = 0;
2108 bot.havocbot_cantfindflag = time + 10;
2109 if (bot.havocbot_previous_role != bot.havocbot_role)
2110 navigation_goalrating_timeout_force(bot);
2112 case HAVOCBOT_CTF_ROLE_DEFENSE:
2114 bot.havocbot_role = havocbot_role_ctf_defense;
2115 bot.havocbot_role_timeout = 0;
2117 case HAVOCBOT_CTF_ROLE_MIDDLE:
2119 bot.havocbot_role = havocbot_role_ctf_middle;
2120 bot.havocbot_role_timeout = 0;
2122 case HAVOCBOT_CTF_ROLE_OFFENSE:
2124 bot.havocbot_role = havocbot_role_ctf_offense;
2125 bot.havocbot_role_timeout = 0;
2127 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2129 bot.havocbot_previous_role = bot.havocbot_role;
2130 bot.havocbot_role = havocbot_role_ctf_retriever;
2131 bot.havocbot_role_timeout = time + 10;
2132 if (bot.havocbot_previous_role != bot.havocbot_role)
2133 navigation_goalrating_timeout_expire(bot, 2);
2135 case HAVOCBOT_CTF_ROLE_ESCORT:
2137 bot.havocbot_previous_role = bot.havocbot_role;
2138 bot.havocbot_role = havocbot_role_ctf_escort;
2139 bot.havocbot_role_timeout = time + 30;
2140 if (bot.havocbot_previous_role != bot.havocbot_role)
2141 navigation_goalrating_timeout_expire(bot, 2);
2144 LOG_TRACE(bot.netname, " switched to ", s);
2152 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2154 entity player = M_ARGV(0, entity);
2156 int t = 0, t2 = 0, t3 = 0;
2157 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)
2159 // initially clear items so they can be set as necessary later.
2160 STAT(OBJECTIVE_STATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2161 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2162 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2163 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2164 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2165 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2167 // scan through all the flags and notify the client about them
2168 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2170 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2171 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2172 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2173 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2174 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; }
2176 switch(flag.ctf_status)
2181 if((flag.owner == player) || (flag.pass_sender == player))
2182 STAT(OBJECTIVE_STATUS, player) |= t; // carrying: player is currently carrying the flag
2184 STAT(OBJECTIVE_STATUS, player) |= t2; // taken: someone else is carrying the flag
2189 STAT(OBJECTIVE_STATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2195 // item for stopping players from capturing the flag too often
2196 if(player.ctf_captureshielded)
2197 STAT(OBJECTIVE_STATUS, player) |= CTF_SHIELDED;
2200 STAT(OBJECTIVE_STATUS, player) |= CTF_STALEMATE;
2202 // update the health of the flag carrier waypointsprite
2203 if(player.wps_flagcarrier)
2204 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);
2207 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in damage.qc
2209 entity frag_attacker = M_ARGV(1, entity);
2210 entity frag_target = M_ARGV(2, entity);
2211 float frag_damage = M_ARGV(4, float);
2212 vector frag_force = M_ARGV(6, vector);
2214 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2216 if(frag_target == frag_attacker) // damage done to yourself
2218 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2219 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2221 else // damage done to everyone else
2223 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2224 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2227 M_ARGV(4, float) = frag_damage;
2228 M_ARGV(6, vector) = frag_force;
2230 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2232 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
2233 && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2235 frag_target.wps_helpme_time = time;
2236 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2238 // todo: add notification for when flag carrier needs help?
2242 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2244 entity frag_attacker = M_ARGV(1, entity);
2245 entity frag_target = M_ARGV(2, entity);
2247 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2249 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2250 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2253 if(frag_target.flagcarried)
2255 entity tmp_entity = frag_target.flagcarried;
2256 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2257 tmp_entity.ctf_dropper = NULL;
2261 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2263 M_ARGV(2, float) = 0; // frag score
2264 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2267 void ctf_RemovePlayer(entity player)
2269 if(player.flagcarried)
2270 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2272 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2274 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2275 if(flag.pass_target == player) { flag.pass_target = NULL; }
2276 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2280 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2282 entity player = M_ARGV(0, entity);
2284 ctf_RemovePlayer(player);
2287 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2289 entity player = M_ARGV(0, entity);
2291 ctf_RemovePlayer(player);
2294 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2296 if(!autocvar_g_ctf_leaderboard)
2299 entity player = M_ARGV(0, entity);
2301 race_SendAll(player, true);
2304 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2306 if(!autocvar_g_ctf_leaderboard)
2309 entity player = M_ARGV(0, entity);
2311 race_checkAndWriteName(player);
2314 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2316 entity player = M_ARGV(0, entity);
2318 if(player.flagcarried)
2319 if(!autocvar_g_ctf_portalteleport)
2320 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2323 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2325 if(MUTATOR_RETURNVALUE || game_stopped) return;
2327 entity player = M_ARGV(0, entity);
2329 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2331 // pass the flag to a team mate
2332 if(autocvar_g_ctf_pass)
2334 entity head, closest_target = NULL;
2335 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2337 while(head) // find the closest acceptable target to pass to
2339 if(IS_PLAYER(head) && !IS_DEAD(head))
2340 if(head != player && SAME_TEAM(head, player))
2341 if(!head.speedrunning && !head.vehicle)
2343 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in damage.qc)
2344 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2345 vector passer_center = CENTER_OR_VIEWOFS(player);
2347 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2349 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2351 if(IS_BOT_CLIENT(head))
2353 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2354 ctf_Handle_Throw(head, player, DROP_PASS);
2358 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2359 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2361 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2364 else if(player.flagcarried && !head.flagcarried)
2368 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2369 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2370 { closest_target = head; }
2372 else { closest_target = head; }
2379 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2382 // throw the flag in front of you
2383 if(autocvar_g_ctf_throw && player.flagcarried)
2385 if(player.throw_count == -1)
2387 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2389 player.throw_prevtime = time;
2390 player.throw_count = 1;
2391 ctf_Handle_Throw(player, NULL, DROP_THROW);
2396 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2402 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2403 else { player.throw_count += 1; }
2404 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2406 player.throw_prevtime = time;
2407 ctf_Handle_Throw(player, NULL, DROP_THROW);
2414 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2416 entity player = M_ARGV(0, entity);
2418 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2420 player.wps_helpme_time = time;
2421 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2423 else // create a normal help me waypointsprite
2425 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2426 WaypointSprite_Ping(player.wps_helpme);
2432 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2434 entity player = M_ARGV(0, entity);
2435 entity veh = M_ARGV(1, entity);
2437 if(player.flagcarried)
2439 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2441 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2445 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2446 setattachment(player.flagcarried, veh, "");
2447 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2448 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2449 //player.flagcarried.angles = '0 0 0';
2455 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2457 entity player = M_ARGV(0, entity);
2459 if(player.flagcarried)
2461 setattachment(player.flagcarried, player, "");
2462 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2463 player.flagcarried.scale = FLAG_SCALE;
2464 player.flagcarried.angles = '0 0 0';
2465 player.flagcarried.nodrawtoclient = NULL;
2470 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2472 entity player = M_ARGV(0, entity);
2474 if(player.flagcarried)
2476 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2477 ctf_RespawnFlag(player.flagcarried);
2482 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2484 entity flag; // temporary entity for the search method
2486 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2488 switch(flag.ctf_status)
2493 // lock the flag, game is over
2494 set_movetype(flag, MOVETYPE_NONE);
2495 flag.takedamage = DAMAGE_NO;
2496 flag.solid = SOLID_NOT;
2497 flag.nextthink = false; // stop thinking
2499 //dprint("stopping the ", flag.netname, " from moving.\n");
2507 // do nothing for these flags
2514 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2516 entity bot = M_ARGV(0, entity);
2518 havocbot_ctf_reset_role(bot);
2522 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2524 M_ARGV(1, string) = "ctf_team";
2527 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2529 int record_page = M_ARGV(0, int);
2530 string ret_string = M_ARGV(1, string);
2532 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2534 if (MapInfo_Get_ByID(i))
2536 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2542 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2543 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2547 M_ARGV(1, string) = ret_string;
2550 bool superspec_Spectate(entity this, entity targ); // TODO
2551 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2552 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2554 entity player = M_ARGV(0, entity);
2555 string cmd_name = M_ARGV(1, string);
2556 int cmd_argc = M_ARGV(2, int);
2558 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2560 if(cmd_name == "followfc")
2572 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2573 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2574 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2575 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2579 FOREACH_CLIENT(IS_PLAYER(it), {
2580 if(it.flagcarried && (it.team == _team || _team == 0))
2583 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2584 continue; // already spectating this fc, try another
2585 return superspec_Spectate(player, it);
2590 superspec_msg("", "", player, "No active flag carrier\n", 1);
2595 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2597 entity frag_target = M_ARGV(0, entity);
2599 if(frag_target.flagcarried)
2600 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2603 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2605 entity player = M_ARGV(0, entity);
2606 if(player.flagcarried)
2607 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2615 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2616 CTF flag for team one (Red).
2618 "angle" Angle the flag will point (minus 90 degrees)...
2619 "model" model to use, note this needs red and blue as skins 0 and 1...
2620 "noise" sound played when flag is picked up...
2621 "noise1" sound played when flag is returned by a teammate...
2622 "noise2" sound played when flag is captured...
2623 "noise3" sound played when flag is lost in the field and respawns itself...
2624 "noise4" sound played when flag is dropped by a player...
2625 "noise5" sound played when flag touches the ground... */
2626 spawnfunc(item_flag_team1)
2628 if(!g_ctf) { delete(this); return; }
2630 ctf_FlagSetup(NUM_TEAM_1, this);
2633 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2634 CTF flag for team two (Blue).
2636 "angle" Angle the flag will point (minus 90 degrees)...
2637 "model" model to use, note this needs red and blue as skins 0 and 1...
2638 "noise" sound played when flag is picked up...
2639 "noise1" sound played when flag is returned by a teammate...
2640 "noise2" sound played when flag is captured...
2641 "noise3" sound played when flag is lost in the field and respawns itself...
2642 "noise4" sound played when flag is dropped by a player...
2643 "noise5" sound played when flag touches the ground... */
2644 spawnfunc(item_flag_team2)
2646 if(!g_ctf) { delete(this); return; }
2648 ctf_FlagSetup(NUM_TEAM_2, this);
2651 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2652 CTF flag for team three (Yellow).
2654 "angle" Angle the flag will point (minus 90 degrees)...
2655 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2656 "noise" sound played when flag is picked up...
2657 "noise1" sound played when flag is returned by a teammate...
2658 "noise2" sound played when flag is captured...
2659 "noise3" sound played when flag is lost in the field and respawns itself...
2660 "noise4" sound played when flag is dropped by a player...
2661 "noise5" sound played when flag touches the ground... */
2662 spawnfunc(item_flag_team3)
2664 if(!g_ctf) { delete(this); return; }
2666 ctf_FlagSetup(NUM_TEAM_3, this);
2669 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2670 CTF flag for team four (Pink).
2672 "angle" Angle the flag will point (minus 90 degrees)...
2673 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2674 "noise" sound played when flag is picked up...
2675 "noise1" sound played when flag is returned by a teammate...
2676 "noise2" sound played when flag is captured...
2677 "noise3" sound played when flag is lost in the field and respawns itself...
2678 "noise4" sound played when flag is dropped by a player...
2679 "noise5" sound played when flag touches the ground... */
2680 spawnfunc(item_flag_team4)
2682 if(!g_ctf) { delete(this); return; }
2684 ctf_FlagSetup(NUM_TEAM_4, this);
2687 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2690 "angle" Angle the flag will point (minus 90 degrees)...
2691 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2692 "noise" sound played when flag is picked up...
2693 "noise1" sound played when flag is returned by a teammate...
2694 "noise2" sound played when flag is captured...
2695 "noise3" sound played when flag is lost in the field and respawns itself...
2696 "noise4" sound played when flag is dropped by a player...
2697 "noise5" sound played when flag touches the ground... */
2698 spawnfunc(item_flag_neutral)
2700 if(!g_ctf) { delete(this); return; }
2701 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2703 ctf_FlagSetup(0, this);
2706 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2707 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2708 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.
2710 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2711 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2714 if(!g_ctf) { delete(this); return; }
2716 this.team = this.cnt + 1;
2719 // compatibility for quake maps
2720 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2721 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2722 spawnfunc(info_player_team1);
2723 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2724 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2725 spawnfunc(info_player_team2);
2726 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2727 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2729 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2730 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2732 // compatibility for wop maps
2733 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2734 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2735 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2736 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2737 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2738 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2746 void ctf_ScoreRules(int teams)
2748 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2749 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2750 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2751 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2752 field(SP_CTF_PICKUPS, "pickups", 0);
2753 field(SP_CTF_FCKILLS, "fckills", 0);
2754 field(SP_CTF_RETURNS, "returns", 0);
2755 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2759 // code from here on is just to support maps that don't have flag and team entities
2760 void ctf_SpawnTeam (string teamname, int teamcolor)
2762 entity this = new_pure(ctf_team);
2763 this.netname = teamname;
2764 this.cnt = teamcolor - 1;
2765 this.spawnfunc_checked = true;
2766 this.team = teamcolor;
2769 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2774 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2776 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2777 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2779 switch(tmp_entity.team)
2781 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2782 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2783 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2784 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2786 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2789 havocbot_ctf_calculate_middlepoint();
2791 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2793 ctf_teams = 0; // so set the default red and blue teams
2794 BITSET_ASSIGN(ctf_teams, BIT(0));
2795 BITSET_ASSIGN(ctf_teams, BIT(1));
2798 //ctf_teams = bound(2, ctf_teams, 4);
2800 // if no teams are found, spawn defaults
2801 if(find(NULL, classname, "ctf_team") == NULL)
2803 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2804 if(ctf_teams & BIT(0))
2805 ctf_SpawnTeam("Red", NUM_TEAM_1);
2806 if(ctf_teams & BIT(1))
2807 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2808 if(ctf_teams & BIT(2))
2809 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2810 if(ctf_teams & BIT(3))
2811 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2814 ctf_ScoreRules(ctf_teams);
2817 void ctf_Initialize()
2819 CTF_FLAG = NEW(Flag);
2820 record_type = CTF_RECORD;
2821 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2823 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2824 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2825 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2827 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);