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_landtime = 0;
360 flag.ctf_dropper = player;
361 flag.ctf_status = FLAG_DROPPED;
363 // messages and sounds
364 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
365 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
366 ctf_EventLog("dropped", player.team, player);
369 GameRules_scoring_add_team(player, SCORE, -((flag.score_drop) ? flag.score_drop : autocvar_g_ctf_score_penalty_drop));
370 GameRules_scoring_add(player, CTF_DROPS, 1);
373 if(autocvar_g_ctf_flag_dropped_waypoint) {
374 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);
375 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
378 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
380 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_health);
381 WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH));
384 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
386 if(droptype == DROP_PASS)
388 flag.pass_distance = 0;
389 flag.pass_sender = NULL;
390 flag.pass_target = NULL;
394 void ctf_Handle_Retrieve(entity flag, entity player)
396 entity sender = flag.pass_sender;
398 // transfer flag to player
400 flag.owner.flagcarried = flag;
401 GameRules_scoring_vip(player, true);
404 flag.solid = SOLID_NOT; // before setorigin to prevent area grid linking
407 setattachment(flag, player.vehicle, "");
408 setorigin(flag, VEHICLE_FLAG_OFFSET);
409 flag.scale = VEHICLE_FLAG_SCALE;
413 setattachment(flag, player, "");
414 setorigin(flag, FLAG_CARRY_OFFSET);
416 set_movetype(flag, MOVETYPE_NONE);
417 flag.takedamage = DAMAGE_NO;
418 flag.angles = '0 0 0';
419 flag.ctf_status = FLAG_CARRY;
421 // messages and sounds
422 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
423 ctf_EventLog("receive", flag.team, player);
425 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
427 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
428 else if(it == player)
429 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
430 else if(SAME_TEAM(it, sender))
431 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
434 // create new waypoint
435 ctf_FlagcarrierWaypoints(player);
437 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
438 player.throw_antispam = sender.throw_antispam;
440 flag.pass_distance = 0;
441 flag.pass_sender = NULL;
442 flag.pass_target = NULL;
445 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
447 entity flag = player.flagcarried;
448 vector targ_origin, flag_velocity;
450 if(!flag) { return; }
451 if((droptype == DROP_PASS) && !receiver) { return; }
453 if(flag.speedrunning)
455 // ensure old waypoints are removed before resetting the flag
456 WaypointSprite_Kill(player.wps_flagcarrier);
458 if(player.wps_enemyflagcarrier)
459 WaypointSprite_Kill(player.wps_enemyflagcarrier);
461 if(player.wps_flagreturn)
462 WaypointSprite_Kill(player.wps_flagreturn);
463 ctf_RespawnFlag(flag);
468 setattachment(flag, NULL, "");
469 flag.solid = SOLID_TRIGGER; // before setorigin to ensure area grid linking
470 setorigin(flag, player.origin);
471 WarpZoneLib_MoveOutOfSolid(flag); // a flag has a bigger bbox than a player
472 flag.owner.flagcarried = NULL;
473 GameRules_scoring_vip(flag.owner, false);
475 flag.ctf_dropper = player;
476 flag.ctf_droptime = time;
477 flag.ctf_landtime = 0;
479 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
486 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
487 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
488 WarpZone_RefSys_Copy(flag, receiver);
489 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
490 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
492 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
493 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
496 set_movetype(flag, MOVETYPE_FLY);
497 flag.takedamage = DAMAGE_NO;
498 flag.pass_sender = player;
499 flag.pass_target = receiver;
500 flag.ctf_status = FLAG_PASSING;
503 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
504 WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
505 ctf_EventLog("pass", flag.team, player);
511 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'));
513 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)));
514 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
515 ctf_Handle_Drop(flag, player, droptype);
516 navigation_dynamicgoal_set(flag, player);
522 flag.velocity = '0 0 0'; // do nothing
529 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);
530 ctf_Handle_Drop(flag, player, droptype);
531 navigation_dynamicgoal_set(flag, player);
536 // kill old waypointsprite
537 WaypointSprite_Ping(player.wps_flagcarrier);
538 WaypointSprite_Kill(player.wps_flagcarrier);
540 if(player.wps_enemyflagcarrier)
541 WaypointSprite_Kill(player.wps_enemyflagcarrier);
543 if(player.wps_flagreturn)
544 WaypointSprite_Kill(player.wps_flagreturn);
547 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
551 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
553 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
561 void nades_GiveBonus(entity player, float score);
563 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
565 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
566 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
567 entity player_team_flag = NULL, tmp_entity;
568 float old_time, new_time;
570 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
571 if(CTF_DIFFTEAM(player, flag)) { return; }
572 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)
574 if (toucher.goalentity == flag.bot_basewaypoint)
575 toucher.goalentity_lock_timeout = 0;
578 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
579 if(SAME_TEAM(tmp_entity, player))
581 player_team_flag = tmp_entity;
585 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
587 player.throw_prevtime = time;
588 player.throw_count = 0;
590 // messages and sounds
591 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
592 ctf_CaptureRecord(enemy_flag, player);
593 _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);
597 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
598 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
604 if(enemy_flag.score_capture || flag.score_capture)
605 pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
606 GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
608 if(enemy_flag.score_team_capture || flag.score_team_capture)
609 capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
610 GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
612 old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
613 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
614 if(!old_time || new_time < old_time)
615 GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
618 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
620 shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
624 if(capturetype == CAPTURE_NORMAL)
626 WaypointSprite_Kill(player.wps_flagcarrier);
627 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
629 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
630 { GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist)); }
633 flag.enemy = toucher;
636 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
637 ctf_RespawnFlag(enemy_flag);
640 void ctf_Handle_Return(entity flag, entity player)
642 // messages and sounds
643 if(IS_MONSTER(player))
645 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
649 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
650 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
652 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
653 ctf_EventLog("return", flag.team, player);
656 if(IS_PLAYER(player))
658 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
659 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
661 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
664 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
668 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
669 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
670 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
674 if(player.flagcarried == flag)
675 WaypointSprite_Kill(player.wps_flagcarrier);
680 ctf_RespawnFlag(flag);
683 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
686 float pickup_dropped_score; // used to calculate dropped pickup score
688 // attach the flag to the player
690 player.flagcarried = flag;
691 GameRules_scoring_vip(player, true);
692 flag.solid = SOLID_NOT; // before setorigin to prevent area grid linking
695 setattachment(flag, player.vehicle, "");
696 setorigin(flag, VEHICLE_FLAG_OFFSET);
697 flag.scale = VEHICLE_FLAG_SCALE;
701 setattachment(flag, player, "");
702 setorigin(flag, FLAG_CARRY_OFFSET);
706 set_movetype(flag, MOVETYPE_NONE);
707 flag.takedamage = DAMAGE_NO;
708 flag.angles = '0 0 0';
709 flag.ctf_status = FLAG_CARRY;
713 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
714 case PICKUP_DROPPED: SetResourceExplicit(flag, RES_HEALTH, flag.max_health); break; // reset health/return timelimit
718 // messages and sounds
719 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
721 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
723 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
724 else if(CTF_DIFFTEAM(player, flag))
725 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
727 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
729 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
732 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); });
735 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
736 if(CTF_SAMETEAM(flag, it))
737 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);
738 else if(DIFF_TEAM(player, it))
739 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_ENEMY_OTHER), Team_ColorCode(player.team), player.netname);
742 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
745 GameRules_scoring_add(player, CTF_PICKUPS, 1);
746 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
751 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
752 ctf_EventLog("steal", flag.team, player);
758 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);
759 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);
760 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
761 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
762 ctf_EventLog("pickup", flag.team, player);
770 if(pickuptype == PICKUP_BASE)
772 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
773 if((player.speedrunning) && (ctf_captimerecord))
774 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
778 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
781 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
782 ctf_FlagcarrierWaypoints(player);
783 WaypointSprite_Ping(player.wps_flagcarrier);
787 // ===================
788 // Main Flag Functions
789 // ===================
791 void ctf_CheckFlagReturn(entity flag, int returntype)
793 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
795 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH)); }
797 if((GetResource(flag, RES_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
802 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
804 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
805 case RETURN_SPEEDRUN:
806 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
807 case RETURN_NEEDKILL:
808 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
811 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
813 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
814 ctf_EventLog("returned", flag.team, NULL);
816 ctf_RespawnFlag(flag);
821 bool ctf_Stalemate_Customize(entity this, entity client)
823 // make spectators see what the player would see
824 entity e = WaypointSprite_getviewentity(client);
825 entity wp_owner = this.owner;
828 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
829 if(SAME_TEAM(wp_owner, e)) { return false; }
830 if(!IS_PLAYER(e)) { return false; }
835 void ctf_CheckStalemate()
838 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
841 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
843 // build list of stale flags
844 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
846 if(autocvar_g_ctf_stalemate)
847 if(tmp_entity.ctf_status != FLAG_BASE)
848 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
850 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
851 ctf_staleflaglist = tmp_entity;
853 switch(tmp_entity.team)
855 case NUM_TEAM_1: ++stale_red_flags; break;
856 case NUM_TEAM_2: ++stale_blue_flags; break;
857 case NUM_TEAM_3: ++stale_yellow_flags; break;
858 case NUM_TEAM_4: ++stale_pink_flags; break;
859 default: ++stale_neutral_flags; break;
865 stale_flags = (stale_neutral_flags >= 1);
867 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
869 if(ctf_oneflag && stale_flags == 1)
870 ctf_stalemate = true;
871 else if(stale_flags >= 2)
872 ctf_stalemate = true;
873 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
874 { ctf_stalemate = false; wpforenemy_announced = false; }
875 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
876 { ctf_stalemate = false; wpforenemy_announced = false; }
878 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
881 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
883 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
885 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);
886 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
887 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
891 if (!wpforenemy_announced)
893 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)); });
895 wpforenemy_announced = true;
900 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
902 if(ITEM_DAMAGE_NEEDKILL(deathtype))
904 if(autocvar_g_ctf_flag_return_damage_delay)
905 this.ctf_flagdamaged_byworld = true;
908 SetResourceExplicit(this, RES_HEALTH, 0);
909 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
913 if(autocvar_g_ctf_flag_return_damage)
915 // reduce health and check if it should be returned
916 TakeResource(this, RES_HEALTH, damage);
917 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
922 void ctf_FlagThink(entity this)
927 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
930 if(this == ctf_worldflaglist) // only for the first flag
931 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
934 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
935 LOG_TRACE("wtf the flag got squashed?");
936 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
937 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
938 setsize(this, this.m_mins, this.m_maxs);
942 switch(this.ctf_status)
946 if(autocvar_g_ctf_dropped_capture_radius)
948 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
949 if(tmp_entity.ctf_status == FLAG_DROPPED)
950 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
951 if((this.noalign || tmp_entity.ctf_landtime) && time > ((this.noalign) ? tmp_entity.ctf_droptime : tmp_entity.ctf_landtime) + autocvar_g_ctf_dropped_capture_delay)
952 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
959 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
960 if(IS_ONGROUND(this) && !this.ctf_landtime)
961 this.ctf_landtime = time; // landtime is reset when thrown, and we don't want to restart the timer if the flag is pushed
963 if(autocvar_g_ctf_flag_dropped_floatinwater)
965 vector midpoint = ((this.absmin + this.absmax) * 0.5);
966 if(pointcontents(midpoint) == CONTENT_WATER)
968 this.velocity = this.velocity * 0.5;
970 if (pointcontents(midpoint + eZ * FLAG_FLOAT_OFFSET_Z) == CONTENT_WATER)
971 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
973 { set_movetype(this, MOVETYPE_FLY); }
975 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
977 if(autocvar_g_ctf_flag_return_dropped)
979 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
981 SetResourceExplicit(this, RES_HEALTH, 0);
982 ctf_CheckFlagReturn(this, RETURN_DROPPED);
986 if(this.ctf_flagdamaged_byworld)
988 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
989 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
992 else if(autocvar_g_ctf_flag_return_time)
994 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
995 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
1003 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
1005 SetResourceExplicit(this, RES_HEALTH, 0);
1006 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
1008 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
1009 ImpulseCommands(this.owner);
1011 if(autocvar_g_ctf_stalemate)
1013 if(time >= wpforenemy_nextthink)
1015 ctf_CheckStalemate();
1016 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1019 if(CTF_SAMETEAM(this, this.owner) && this.team)
1021 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1022 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
1023 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
1024 ctf_Handle_Return(this, this.owner);
1031 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1032 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1033 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1035 if((this.pass_target == NULL)
1036 || (IS_DEAD(this.pass_target))
1037 || (this.pass_target.flagcarried)
1038 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1039 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1040 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1042 // give up, pass failed
1043 ctf_Handle_Drop(this, NULL, DROP_PASS);
1047 // still a viable target, go for it
1048 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1053 default: // this should never happen
1055 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1061 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1064 if(game_stopped) return;
1065 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1067 bool is_not_monster = (!IS_MONSTER(toucher));
1069 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1070 if(ITEM_TOUCH_NEEDKILL())
1072 if(!autocvar_g_ctf_flag_return_damage_delay)
1074 SetResourceExplicit(flag, RES_HEALTH, 0);
1075 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1077 if(!flag.ctf_flagdamaged_byworld) { return; }
1080 // special touch behaviors
1081 if(STAT(FROZEN, toucher)) { return; }
1082 else if(IS_VEHICLE(toucher))
1084 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1085 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1087 return; // do nothing
1089 else if(IS_MONSTER(toucher))
1091 if(!autocvar_g_ctf_allow_monster_touch)
1092 return; // do nothing
1094 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1096 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1098 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1099 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1100 flag.wait = time + FLAG_TOUCHRATE;
1104 else if(IS_DEAD(toucher)) { return; }
1106 switch(flag.ctf_status)
1112 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1113 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1114 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1115 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1117 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1118 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to their base
1119 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)
1121 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1122 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1124 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1125 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1131 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1132 ctf_Handle_Return(flag, toucher); // toucher just returned their own flag
1133 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1134 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1140 LOG_TRACE("Someone touched a flag even though it was being carried?");
1146 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1148 if(DIFF_TEAM(toucher, flag.pass_sender))
1150 if(ctf_Immediate_Return_Allowed(flag, toucher))
1151 ctf_Handle_Return(flag, toucher);
1152 else if(is_not_monster && (!toucher.flagcarried))
1153 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1155 else if(!toucher.flagcarried)
1156 ctf_Handle_Retrieve(flag, toucher);
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!!
1167 // reset the player (if there is one)
1168 if((flag.owner) && (flag.owner.flagcarried == flag))
1170 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1171 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1172 WaypointSprite_Kill(flag.wps_flagcarrier);
1174 flag.owner.flagcarried = NULL;
1175 GameRules_scoring_vip(flag.owner, false);
1177 if(flag.speedrunning)
1178 ctf_FakeTimeLimit(flag.owner, -1);
1181 if((flag.owner) && (flag.owner.vehicle))
1182 flag.scale = FLAG_SCALE;
1184 if(flag.ctf_status == FLAG_DROPPED)
1185 { WaypointSprite_Kill(flag.wps_flagdropped); }
1188 setattachment(flag, NULL, "");
1189 flag.solid = SOLID_TRIGGER; // before setorigin to ensure area grid linking
1190 setorigin(flag, flag.ctf_spawnorigin);
1192 //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1193 set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1194 flag.takedamage = DAMAGE_NO;
1195 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1196 flag.velocity = '0 0 0';
1197 flag.angles = flag.mangle;
1198 flag.flags = FL_ITEM | FL_NOTARGET;
1200 flag.ctf_status = FLAG_BASE;
1202 flag.pass_distance = 0;
1203 flag.pass_sender = NULL;
1204 flag.pass_target = NULL;
1205 flag.ctf_dropper = NULL;
1206 flag.ctf_pickuptime = 0;
1207 flag.ctf_droptime = 0;
1208 flag.ctf_landtime = 0;
1209 flag.ctf_flagdamaged_byworld = false;
1210 navigation_dynamicgoal_unset(flag);
1212 ctf_CheckStalemate();
1215 void ctf_Reset(entity this)
1217 if(this.owner && IS_PLAYER(this.owner))
1218 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1221 ctf_RespawnFlag(this);
1224 bool ctf_FlagBase_Customize(entity this, entity client)
1226 entity e = WaypointSprite_getviewentity(client);
1227 entity wp_owner = this.owner;
1228 entity flag = e.flagcarried;
1229 if(flag && CTF_SAMETEAM(e, flag))
1231 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1236 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1239 waypoint_spawnforitem_force(this, this.origin);
1240 navigation_dynamicgoal_init(this, true);
1246 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1247 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1248 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1249 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1250 default: basename = WP_FlagBaseNeutral; break;
1253 if(autocvar_g_ctf_flag_waypoint)
1255 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1256 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1257 wp.fade_rate = autocvar_g_ctf_flag_waypoint_maxdistance;
1258 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1259 setcefc(wp, ctf_FlagBase_Customize);
1262 // captureshield setup
1263 ctf_CaptureShield_Spawn(this);
1268 void ctf_FlagSetup(int teamnum, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1271 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1272 ctf_worldflaglist = flag;
1274 setattachment(flag, NULL, "");
1276 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnum), Team_ColorName_Upper(teamnum)));
1277 flag.team = teamnum;
1278 flag.classname = "item_flag_team";
1279 flag.target = "###item###"; // for finding the nearest item using findnearest
1280 flag.flags = FL_ITEM | FL_NOTARGET;
1281 IL_PUSH(g_items, flag);
1282 flag.solid = SOLID_TRIGGER;
1283 flag.takedamage = DAMAGE_NO;
1284 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1285 flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1286 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1287 flag.event_damage = ctf_FlagDamage;
1288 flag.pushable = true;
1289 flag.teleportable = TELEPORT_NORMAL;
1290 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1291 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1292 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1293 if(flag.damagedbycontents)
1294 IL_PUSH(g_damagedbycontents, flag);
1295 flag.velocity = '0 0 0';
1296 flag.mangle = flag.angles;
1297 flag.reset = ctf_Reset;
1298 settouch(flag, ctf_FlagTouch);
1299 setthink(flag, ctf_FlagThink);
1300 flag.nextthink = time + FLAG_THINKRATE;
1301 flag.ctf_status = FLAG_BASE;
1303 // crudely force them all to 0
1304 if(autocvar_g_ctf_score_ignore_fields)
1305 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1307 string teamname = Static_Team_ColorName_Lower(teamnum);
1309 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1310 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1311 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1312 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnum).eent_eff_name; }
1313 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnum).eent_eff_name; }
1314 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnum).eent_eff_name; }
1318 if(flag.s == "") flag.s = b; \
1319 precache_sound(flag.s);
1321 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnum))))
1322 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnum))))
1323 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnum))))
1324 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnum))))
1325 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1326 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1327 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1331 precache_model(flag.model);
1334 _setmodel(flag, flag.model); // precision set below
1335 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1336 flag.m_mins = flag.mins; // store these for squash checks
1337 flag.m_maxs = flag.maxs;
1338 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1340 if(autocvar_g_ctf_flag_glowtrails)
1344 case NUM_TEAM_1: flag.glow_color = 251; break;
1345 case NUM_TEAM_2: flag.glow_color = 210; break;
1346 case NUM_TEAM_3: flag.glow_color = 110; break;
1347 case NUM_TEAM_4: flag.glow_color = 145; break;
1348 default: flag.glow_color = 254; break;
1350 flag.glow_size = 25;
1351 flag.glow_trail = 1;
1354 flag.effects |= EF_LOWPRECISION;
1355 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1356 if(autocvar_g_ctf_dynamiclights)
1360 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1361 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1362 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1363 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1364 default: flag.effects |= EF_DIMLIGHT; break;
1369 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1371 flag.dropped_origin = flag.origin;
1372 flag.noalign = true;
1373 set_movetype(flag, MOVETYPE_NONE);
1375 else // drop to floor, automatically find a platform and set that as spawn origin
1377 flag.noalign = false;
1379 set_movetype(flag, MOVETYPE_NONE);
1382 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1390 // NOTE: LEGACY CODE, needs to be re-written!
1392 void havocbot_ctf_calculate_middlepoint()
1396 vector fo = '0 0 0';
1399 f = ctf_worldflaglist;
1404 f = f.ctf_worldflagnext;
1410 havocbot_middlepoint = s / n;
1411 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1413 havocbot_symmetry_axis_m = 0;
1414 havocbot_symmetry_axis_q = 0;
1417 // for symmetrical editing of waypoints
1418 entity f1 = ctf_worldflaglist;
1419 entity f2 = f1.ctf_worldflagnext;
1420 float m = -(f1.origin.y - f2.origin.y) / (max(f1.origin.x - f2.origin.x, FLOAT_EPSILON));
1421 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1422 havocbot_symmetry_axis_m = m;
1423 havocbot_symmetry_axis_q = q;
1425 havocbot_symmetry_origin_order = n;
1429 entity havocbot_ctf_find_flag(entity bot)
1432 f = ctf_worldflaglist;
1435 if (CTF_SAMETEAM(bot, f))
1437 f = f.ctf_worldflagnext;
1442 entity havocbot_ctf_find_enemy_flag(entity bot)
1445 f = ctf_worldflaglist;
1450 if(CTF_DIFFTEAM(bot, f))
1457 else if(!bot.flagcarried)
1461 else if (CTF_DIFFTEAM(bot, f))
1463 f = f.ctf_worldflagnext;
1468 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1475 FOREACH_CLIENT(IS_PLAYER(it), {
1476 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1479 if(vdist(it.origin - org, <, tc_radius))
1488 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1491 head = ctf_worldflaglist;
1494 if (CTF_SAMETEAM(this, head))
1496 head = head.ctf_worldflagnext;
1499 navigation_routerating(this, head, ratingscale, 10000);
1503 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1506 head = ctf_worldflaglist;
1509 if (CTF_SAMETEAM(this, head))
1511 if (this.flagcarried)
1512 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1514 head = head.ctf_worldflagnext; // skip base if it has a different group
1519 head = head.ctf_worldflagnext;
1524 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1527 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1530 head = ctf_worldflaglist;
1535 if(CTF_DIFFTEAM(this, head))
1539 if(this.flagcarried)
1542 else if(!this.flagcarried)
1546 else if(CTF_DIFFTEAM(this, head))
1548 head = head.ctf_worldflagnext;
1552 if (head.ctf_status == FLAG_CARRY)
1554 // adjust rating of our flag carrier depending on their health
1555 head = head.tag_entity;
1556 float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1557 ratingscale += ratingscale * f * 0.1;
1559 navigation_routerating(this, head, ratingscale, 10000);
1563 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1565 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1567 if (!bot_waypoints_for_items)
1569 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1575 head = havocbot_ctf_find_enemy_flag(this);
1580 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1583 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1587 mf = havocbot_ctf_find_flag(this);
1589 if(mf.ctf_status == FLAG_BASE)
1593 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1596 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1599 head = ctf_worldflaglist;
1602 // flag is out in the field
1603 if(head.ctf_status != FLAG_BASE)
1604 if(head.tag_entity==NULL) // dropped
1608 if(vdist(org - head.origin, <, df_radius))
1609 navigation_routerating(this, head, ratingscale, 10000);
1612 navigation_routerating(this, head, ratingscale, 10000);
1615 head = head.ctf_worldflagnext;
1619 void havocbot_ctf_reset_role(entity this)
1621 float cdefense, cmiddle, coffense;
1628 if (this.flagcarried)
1630 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1634 mf = havocbot_ctf_find_flag(this);
1635 ef = havocbot_ctf_find_enemy_flag(this);
1637 // Retrieve stolen flag
1638 if(mf.ctf_status!=FLAG_BASE)
1640 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1644 // If enemy flag is taken go to the middle to intercept pursuers
1645 if(ef.ctf_status!=FLAG_BASE)
1647 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1651 // if there is no one else on the team switch to offense
1653 // don't check if this bot is a player since it isn't true when the bot is added to the server
1654 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1658 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1661 else if (time < CS(this).jointime + 1)
1663 // if bots spawn all at once set good default roles
1666 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1669 else if (count == 2)
1671 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1676 // Evaluate best position to take
1677 // Count mates on middle position
1678 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1680 // Count mates on defense position
1681 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1683 // Count mates on offense position
1684 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1686 if(cdefense<=coffense)
1687 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1688 else if(coffense<=cmiddle)
1689 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1691 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1693 // if bots spawn all at once assign them a more appropriated role after a while
1694 if (time < CS(this).jointime + 1 && count > 2)
1695 this.havocbot_role_timeout = time + 10 + random() * 10;
1698 bool havocbot_ctf_is_basewaypoint(entity item)
1700 if (item.classname != "waypoint")
1703 entity head = ctf_worldflaglist;
1706 if (item == head.bot_basewaypoint)
1708 head = head.ctf_worldflagnext;
1713 void havocbot_role_ctf_carrier(entity this)
1717 havocbot_ctf_reset_role(this);
1721 if (this.flagcarried == NULL)
1723 havocbot_ctf_reset_role(this);
1727 if (navigation_goalrating_timeout(this))
1729 navigation_goalrating_start(this);
1732 entity mf = havocbot_ctf_find_flag(this);
1733 vector base_org = mf.dropped_origin;
1734 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1736 havocbot_goalrating_ctf_enemybase(this, base_rating);
1738 havocbot_goalrating_ctf_ourbase(this, base_rating);
1740 // start collecting items very close to the bot but only inside of own base radius
1741 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1742 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1744 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1746 navigation_goalrating_end(this);
1748 navigation_goalrating_timeout_set(this);
1750 entity goal = this.goalentity;
1751 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1752 this.goalentity_lock_timeout = time + ((this.enemy) ? 2 : 3);
1755 this.havocbot_cantfindflag = time + 10;
1756 else if (time > this.havocbot_cantfindflag)
1758 // Can't navigate to my own base, suicide!
1759 // TODO: drop it and wander around
1760 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1766 void havocbot_role_ctf_escort(entity this)
1772 havocbot_ctf_reset_role(this);
1776 if (this.flagcarried)
1778 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1782 // If enemy flag is back on the base switch to previous role
1783 ef = havocbot_ctf_find_enemy_flag(this);
1784 if(ef.ctf_status==FLAG_BASE)
1786 this.havocbot_role = this.havocbot_previous_role;
1787 this.havocbot_role_timeout = 0;
1790 if (ef.ctf_status == FLAG_DROPPED)
1792 navigation_goalrating_timeout_expire(this, 1);
1796 // If the flag carrier reached the base switch to defense
1797 mf = havocbot_ctf_find_flag(this);
1798 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1800 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1804 // Set the role timeout if necessary
1805 if (!this.havocbot_role_timeout)
1807 this.havocbot_role_timeout = time + random() * 30 + 60;
1810 // If nothing happened just switch to previous role
1811 if (time > this.havocbot_role_timeout)
1813 this.havocbot_role = this.havocbot_previous_role;
1814 this.havocbot_role_timeout = 0;
1818 // Chase the flag carrier
1819 if (navigation_goalrating_timeout(this))
1821 navigation_goalrating_start(this);
1824 havocbot_goalrating_ctf_enemyflag(this, 10000);
1825 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1826 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1828 navigation_goalrating_end(this);
1830 navigation_goalrating_timeout_set(this);
1834 void havocbot_role_ctf_offense(entity this)
1841 havocbot_ctf_reset_role(this);
1845 if (this.flagcarried)
1847 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1852 mf = havocbot_ctf_find_flag(this);
1853 ef = havocbot_ctf_find_enemy_flag(this);
1856 if(mf.ctf_status!=FLAG_BASE)
1859 pos = mf.tag_entity.origin;
1863 // Try to get it if closer than the enemy base
1864 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1866 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1871 // Escort flag carrier
1872 if(ef.ctf_status!=FLAG_BASE)
1875 pos = ef.tag_entity.origin;
1879 if(vdist(pos - mf.dropped_origin, >, 700))
1881 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1886 // Set the role timeout if necessary
1887 if (!this.havocbot_role_timeout)
1888 this.havocbot_role_timeout = time + 120;
1890 if (time > this.havocbot_role_timeout)
1892 havocbot_ctf_reset_role(this);
1896 if (navigation_goalrating_timeout(this))
1898 navigation_goalrating_start(this);
1901 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1902 havocbot_goalrating_ctf_enemybase(this, 10000);
1903 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1905 navigation_goalrating_end(this);
1907 navigation_goalrating_timeout_set(this);
1911 // Retriever (temporary role):
1912 void havocbot_role_ctf_retriever(entity this)
1918 havocbot_ctf_reset_role(this);
1922 if (this.flagcarried)
1924 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1928 // If flag is back on the base switch to previous role
1929 mf = havocbot_ctf_find_flag(this);
1930 if(mf.ctf_status==FLAG_BASE)
1932 if (mf.enemy == this) // did this bot return the flag?
1933 navigation_goalrating_timeout_force(this);
1934 havocbot_ctf_reset_role(this);
1938 if (!this.havocbot_role_timeout)
1939 this.havocbot_role_timeout = time + 20;
1941 if (time > this.havocbot_role_timeout)
1943 havocbot_ctf_reset_role(this);
1947 if (navigation_goalrating_timeout(this))
1949 const float RT_RADIUS = 10000;
1951 navigation_goalrating_start(this);
1954 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1955 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1956 havocbot_goalrating_ctf_enemybase(this, 8000);
1957 entity ef = havocbot_ctf_find_enemy_flag(this);
1958 vector enemy_base_org = ef.dropped_origin;
1959 // start collecting items very close to the bot but only inside of enemy base radius
1960 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1961 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1962 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1964 navigation_goalrating_end(this);
1966 navigation_goalrating_timeout_set(this);
1970 void havocbot_role_ctf_middle(entity this)
1976 havocbot_ctf_reset_role(this);
1980 if (this.flagcarried)
1982 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1986 mf = havocbot_ctf_find_flag(this);
1987 if(mf.ctf_status!=FLAG_BASE)
1989 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1993 if (!this.havocbot_role_timeout)
1994 this.havocbot_role_timeout = time + 10;
1996 if (time > this.havocbot_role_timeout)
1998 havocbot_ctf_reset_role(this);
2002 if (navigation_goalrating_timeout(this))
2006 org = havocbot_middlepoint;
2007 org.z = this.origin.z;
2009 navigation_goalrating_start(this);
2012 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
2013 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
2014 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2015 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2016 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2017 havocbot_goalrating_ctf_enemybase(this, 3000);
2019 navigation_goalrating_end(this);
2021 entity goal = this.goalentity;
2022 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
2023 this.goalentity_lock_timeout = time + 2;
2025 navigation_goalrating_timeout_set(this);
2029 void havocbot_role_ctf_defense(entity this)
2035 havocbot_ctf_reset_role(this);
2039 if (this.flagcarried)
2041 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2045 // If own flag was captured
2046 mf = havocbot_ctf_find_flag(this);
2047 if(mf.ctf_status!=FLAG_BASE)
2049 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2053 if (!this.havocbot_role_timeout)
2054 this.havocbot_role_timeout = time + 30;
2056 if (time > this.havocbot_role_timeout)
2058 havocbot_ctf_reset_role(this);
2061 if (navigation_goalrating_timeout(this))
2063 vector org = mf.dropped_origin;
2065 navigation_goalrating_start(this);
2067 // if enemies are closer to our base, go there
2068 entity closestplayer = NULL;
2069 float distance, bestdistance = 10000;
2070 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2071 distance = vlen(org - it.origin);
2072 if(distance<bestdistance)
2075 bestdistance = distance;
2081 if(DIFF_TEAM(closestplayer, this))
2082 if(vdist(org - this.origin, >, 1000))
2083 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2084 havocbot_goalrating_ctf_ourbase(this, 10000);
2086 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2087 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2088 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2089 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2090 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2092 navigation_goalrating_end(this);
2094 navigation_goalrating_timeout_set(this);
2098 void havocbot_role_ctf_setrole(entity bot, int role)
2100 string s = "(null)";
2103 case HAVOCBOT_CTF_ROLE_CARRIER:
2105 bot.havocbot_role = havocbot_role_ctf_carrier;
2106 bot.havocbot_role_timeout = 0;
2107 bot.havocbot_cantfindflag = time + 10;
2108 if (bot.havocbot_previous_role != bot.havocbot_role)
2109 navigation_goalrating_timeout_force(bot);
2111 case HAVOCBOT_CTF_ROLE_DEFENSE:
2113 bot.havocbot_role = havocbot_role_ctf_defense;
2114 bot.havocbot_role_timeout = 0;
2116 case HAVOCBOT_CTF_ROLE_MIDDLE:
2118 bot.havocbot_role = havocbot_role_ctf_middle;
2119 bot.havocbot_role_timeout = 0;
2121 case HAVOCBOT_CTF_ROLE_OFFENSE:
2123 bot.havocbot_role = havocbot_role_ctf_offense;
2124 bot.havocbot_role_timeout = 0;
2126 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2128 bot.havocbot_previous_role = bot.havocbot_role;
2129 bot.havocbot_role = havocbot_role_ctf_retriever;
2130 bot.havocbot_role_timeout = time + 10;
2131 if (bot.havocbot_previous_role != bot.havocbot_role)
2132 navigation_goalrating_timeout_expire(bot, 2);
2134 case HAVOCBOT_CTF_ROLE_ESCORT:
2136 bot.havocbot_previous_role = bot.havocbot_role;
2137 bot.havocbot_role = havocbot_role_ctf_escort;
2138 bot.havocbot_role_timeout = time + 30;
2139 if (bot.havocbot_previous_role != bot.havocbot_role)
2140 navigation_goalrating_timeout_expire(bot, 2);
2143 LOG_TRACE(bot.netname, " switched to ", s);
2151 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2153 entity player = M_ARGV(0, entity);
2155 int t = 0, t2 = 0, t3 = 0;
2156 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)
2158 // initially clear items so they can be set as necessary later.
2159 STAT(OBJECTIVE_STATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2160 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2161 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2162 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2163 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2164 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2166 // scan through all the flags and notify the client about them
2167 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2169 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2170 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2171 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2172 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2173 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; }
2175 switch(flag.ctf_status)
2180 if((flag.owner == player) || (flag.pass_sender == player))
2181 STAT(OBJECTIVE_STATUS, player) |= t; // carrying: player is currently carrying the flag
2183 STAT(OBJECTIVE_STATUS, player) |= t2; // taken: someone else is carrying the flag
2188 STAT(OBJECTIVE_STATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2194 // item for stopping players from capturing the flag too often
2195 if(player.ctf_captureshielded)
2196 STAT(OBJECTIVE_STATUS, player) |= CTF_SHIELDED;
2199 STAT(OBJECTIVE_STATUS, player) |= CTF_STALEMATE;
2201 // update the health of the flag carrier waypointsprite
2202 if(player.wps_flagcarrier)
2203 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);
2206 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in damage.qc
2208 entity frag_attacker = M_ARGV(1, entity);
2209 entity frag_target = M_ARGV(2, entity);
2210 float frag_damage = M_ARGV(4, float);
2211 vector frag_force = M_ARGV(6, vector);
2213 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2215 if(frag_target == frag_attacker) // damage done to yourself
2217 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2218 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2220 else // damage done to everyone else
2222 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2223 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2226 M_ARGV(4, float) = frag_damage;
2227 M_ARGV(6, vector) = frag_force;
2229 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2231 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
2232 && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2234 frag_target.wps_helpme_time = time;
2235 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2237 // todo: add notification for when flag carrier needs help?
2241 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2243 entity frag_attacker = M_ARGV(1, entity);
2244 entity frag_target = M_ARGV(2, entity);
2246 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2248 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2249 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2252 if(frag_target.flagcarried)
2254 entity tmp_entity = frag_target.flagcarried;
2255 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2256 tmp_entity.ctf_dropper = NULL;
2260 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2262 M_ARGV(2, float) = 0; // frag score
2263 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2266 void ctf_RemovePlayer(entity player)
2268 if(player.flagcarried)
2269 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2271 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2273 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2274 if(flag.pass_target == player) { flag.pass_target = NULL; }
2275 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2279 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2281 entity player = M_ARGV(0, entity);
2283 ctf_RemovePlayer(player);
2286 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2288 entity player = M_ARGV(0, entity);
2290 ctf_RemovePlayer(player);
2293 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2295 if(!autocvar_g_ctf_leaderboard)
2298 entity player = M_ARGV(0, entity);
2300 race_SendAll(player, true);
2303 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2305 if(!autocvar_g_ctf_leaderboard)
2308 entity player = M_ARGV(0, entity);
2310 race_checkAndWriteName(player);
2313 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2315 entity player = M_ARGV(0, entity);
2317 if(player.flagcarried)
2318 if(!autocvar_g_ctf_portalteleport)
2319 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2322 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2324 if(MUTATOR_RETURNVALUE || game_stopped) return;
2326 entity player = M_ARGV(0, entity);
2328 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2330 // pass the flag to a team mate
2331 if(autocvar_g_ctf_pass)
2333 entity head, closest_target = NULL;
2334 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2336 while(head) // find the closest acceptable target to pass to
2338 if(IS_PLAYER(head) && !IS_DEAD(head))
2339 if(head != player && SAME_TEAM(head, player))
2340 if(!head.speedrunning && !head.vehicle)
2342 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in damage.qc)
2343 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2344 vector passer_center = CENTER_OR_VIEWOFS(player);
2346 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2348 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2350 if(IS_BOT_CLIENT(head))
2352 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2353 ctf_Handle_Throw(head, player, DROP_PASS);
2357 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2358 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2360 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2363 else if(player.flagcarried && !head.flagcarried)
2367 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2368 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2369 { closest_target = head; }
2371 else { closest_target = head; }
2378 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2381 // throw the flag in front of you
2382 if(autocvar_g_ctf_throw && player.flagcarried)
2384 if(player.throw_count == -1)
2386 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2388 player.throw_prevtime = time;
2389 player.throw_count = 1;
2390 ctf_Handle_Throw(player, NULL, DROP_THROW);
2395 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2401 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2402 else { player.throw_count += 1; }
2403 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2405 player.throw_prevtime = time;
2406 ctf_Handle_Throw(player, NULL, DROP_THROW);
2413 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2415 entity player = M_ARGV(0, entity);
2417 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2419 player.wps_helpme_time = time;
2420 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2422 else // create a normal help me waypointsprite
2424 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2425 WaypointSprite_Ping(player.wps_helpme);
2431 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2433 entity player = M_ARGV(0, entity);
2434 entity veh = M_ARGV(1, entity);
2436 if(player.flagcarried)
2438 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2440 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2444 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2445 setattachment(player.flagcarried, veh, "");
2446 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2447 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2448 //player.flagcarried.angles = '0 0 0';
2454 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2456 entity player = M_ARGV(0, entity);
2458 if(player.flagcarried)
2460 setattachment(player.flagcarried, player, "");
2461 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2462 player.flagcarried.scale = FLAG_SCALE;
2463 player.flagcarried.angles = '0 0 0';
2464 player.flagcarried.nodrawtoclient = NULL;
2469 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2471 entity player = M_ARGV(0, entity);
2473 if(player.flagcarried)
2475 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2476 ctf_RespawnFlag(player.flagcarried);
2481 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2483 entity flag; // temporary entity for the search method
2485 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2487 switch(flag.ctf_status)
2492 // lock the flag, game is over
2493 set_movetype(flag, MOVETYPE_NONE);
2494 flag.takedamage = DAMAGE_NO;
2495 flag.solid = SOLID_NOT;
2496 flag.nextthink = false; // stop thinking
2498 //dprint("stopping the ", flag.netname, " from moving.\n");
2506 // do nothing for these flags
2513 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2515 entity bot = M_ARGV(0, entity);
2517 havocbot_ctf_reset_role(bot);
2521 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2523 M_ARGV(1, string) = "ctf_team";
2526 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2528 int record_page = M_ARGV(0, int);
2529 string ret_string = M_ARGV(1, string);
2531 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2533 if (MapInfo_Get_ByID(i))
2535 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2541 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2542 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2546 M_ARGV(1, string) = ret_string;
2549 bool superspec_Spectate(entity this, entity targ); // TODO
2550 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2551 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2553 entity player = M_ARGV(0, entity);
2554 string cmd_name = M_ARGV(1, string);
2555 int cmd_argc = M_ARGV(2, int);
2557 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2559 if(cmd_name == "followfc")
2571 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2572 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2573 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2574 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2578 FOREACH_CLIENT(IS_PLAYER(it), {
2579 if(it.flagcarried && (it.team == _team || _team == 0))
2582 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2583 continue; // already spectating this fc, try another
2584 return superspec_Spectate(player, it);
2589 superspec_msg("", "", player, "No active flag carrier\n", 1);
2594 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2596 entity frag_target = M_ARGV(0, entity);
2598 if(frag_target.flagcarried)
2599 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2602 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2604 entity player = M_ARGV(0, entity);
2605 if(player.flagcarried)
2606 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2614 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2615 CTF flag for team one (Red).
2617 "angle" Angle the flag will point (minus 90 degrees)...
2618 "model" model to use, note this needs red and blue as skins 0 and 1...
2619 "noise" sound played when flag is picked up...
2620 "noise1" sound played when flag is returned by a teammate...
2621 "noise2" sound played when flag is captured...
2622 "noise3" sound played when flag is lost in the field and respawns itself...
2623 "noise4" sound played when flag is dropped by a player...
2624 "noise5" sound played when flag touches the ground... */
2625 spawnfunc(item_flag_team1)
2627 if(!g_ctf) { delete(this); return; }
2629 ctf_FlagSetup(NUM_TEAM_1, this);
2632 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2633 CTF flag for team two (Blue).
2635 "angle" Angle the flag will point (minus 90 degrees)...
2636 "model" model to use, note this needs red and blue as skins 0 and 1...
2637 "noise" sound played when flag is picked up...
2638 "noise1" sound played when flag is returned by a teammate...
2639 "noise2" sound played when flag is captured...
2640 "noise3" sound played when flag is lost in the field and respawns itself...
2641 "noise4" sound played when flag is dropped by a player...
2642 "noise5" sound played when flag touches the ground... */
2643 spawnfunc(item_flag_team2)
2645 if(!g_ctf) { delete(this); return; }
2647 ctf_FlagSetup(NUM_TEAM_2, this);
2650 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2651 CTF flag for team three (Yellow).
2653 "angle" Angle the flag will point (minus 90 degrees)...
2654 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2655 "noise" sound played when flag is picked up...
2656 "noise1" sound played when flag is returned by a teammate...
2657 "noise2" sound played when flag is captured...
2658 "noise3" sound played when flag is lost in the field and respawns itself...
2659 "noise4" sound played when flag is dropped by a player...
2660 "noise5" sound played when flag touches the ground... */
2661 spawnfunc(item_flag_team3)
2663 if(!g_ctf) { delete(this); return; }
2665 ctf_FlagSetup(NUM_TEAM_3, this);
2668 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2669 CTF flag for team four (Pink).
2671 "angle" Angle the flag will point (minus 90 degrees)...
2672 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2673 "noise" sound played when flag is picked up...
2674 "noise1" sound played when flag is returned by a teammate...
2675 "noise2" sound played when flag is captured...
2676 "noise3" sound played when flag is lost in the field and respawns itself...
2677 "noise4" sound played when flag is dropped by a player...
2678 "noise5" sound played when flag touches the ground... */
2679 spawnfunc(item_flag_team4)
2681 if(!g_ctf) { delete(this); return; }
2683 ctf_FlagSetup(NUM_TEAM_4, this);
2686 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2689 "angle" Angle the flag will point (minus 90 degrees)...
2690 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2691 "noise" sound played when flag is picked up...
2692 "noise1" sound played when flag is returned by a teammate...
2693 "noise2" sound played when flag is captured...
2694 "noise3" sound played when flag is lost in the field and respawns itself...
2695 "noise4" sound played when flag is dropped by a player...
2696 "noise5" sound played when flag touches the ground... */
2697 spawnfunc(item_flag_neutral)
2699 if(!g_ctf) { delete(this); return; }
2700 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2702 ctf_FlagSetup(0, this);
2705 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2706 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2707 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.
2709 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2710 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2713 if(!g_ctf) { delete(this); return; }
2715 this.team = this.cnt + 1;
2718 // compatibility for quake maps
2719 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2720 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2721 spawnfunc(info_player_team1);
2722 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2723 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2724 spawnfunc(info_player_team2);
2725 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2726 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2728 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2729 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2731 // compatibility for wop maps
2732 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2733 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2734 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2735 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2736 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2737 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2745 void ctf_ScoreRules(int teams)
2747 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2748 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2749 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2750 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2751 field(SP_CTF_PICKUPS, "pickups", 0);
2752 field(SP_CTF_FCKILLS, "fckills", 0);
2753 field(SP_CTF_RETURNS, "returns", 0);
2754 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2758 // code from here on is just to support maps that don't have flag and team entities
2759 void ctf_SpawnTeam (string teamname, int teamcolor)
2761 entity this = new_pure(ctf_team);
2762 this.netname = teamname;
2763 this.cnt = teamcolor - 1;
2764 this.spawnfunc_checked = true;
2765 this.team = teamcolor;
2768 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2773 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2775 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2776 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2778 switch(tmp_entity.team)
2780 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2781 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2782 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2783 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2785 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2788 havocbot_ctf_calculate_middlepoint();
2790 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2792 ctf_teams = 0; // so set the default red and blue teams
2793 BITSET_ASSIGN(ctf_teams, BIT(0));
2794 BITSET_ASSIGN(ctf_teams, BIT(1));
2797 //ctf_teams = bound(2, ctf_teams, 4);
2799 // if no teams are found, spawn defaults
2800 if(find(NULL, classname, "ctf_team") == NULL)
2802 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2803 if(ctf_teams & BIT(0))
2804 ctf_SpawnTeam("Red", NUM_TEAM_1);
2805 if(ctf_teams & BIT(1))
2806 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2807 if(ctf_teams & BIT(2))
2808 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2809 if(ctf_teams & BIT(3))
2810 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2813 ctf_ScoreRules(ctf_teams);
2816 void ctf_Initialize()
2818 CTF_FLAG = NEW(Flag);
2819 record_type = CTF_RECORD;
2820 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2822 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2823 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2824 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2826 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);