3 #include <common/effects/all.qh>
4 #include <common/vehicles/all.qh>
5 #include <server/teamplay.qh>
7 #include <lib/warpzone/common.qh>
9 bool autocvar_g_ctf_allow_vehicle_carry;
10 bool autocvar_g_ctf_allow_vehicle_touch;
11 bool autocvar_g_ctf_allow_monster_touch;
12 bool autocvar_g_ctf_throw;
13 float autocvar_g_ctf_throw_angle_max;
14 float autocvar_g_ctf_throw_angle_min;
15 int autocvar_g_ctf_throw_punish_count;
16 float autocvar_g_ctf_throw_punish_delay;
17 float autocvar_g_ctf_throw_punish_time;
18 float autocvar_g_ctf_throw_strengthmultiplier;
19 float autocvar_g_ctf_throw_velocity_forward;
20 float autocvar_g_ctf_throw_velocity_up;
21 float autocvar_g_ctf_drop_velocity_up;
22 float autocvar_g_ctf_drop_velocity_side;
23 bool autocvar_g_ctf_oneflag_reverse;
24 bool autocvar_g_ctf_portalteleport;
25 bool autocvar_g_ctf_pass;
26 float autocvar_g_ctf_pass_arc;
27 float autocvar_g_ctf_pass_arc_max;
28 float autocvar_g_ctf_pass_directional_max;
29 float autocvar_g_ctf_pass_directional_min;
30 float autocvar_g_ctf_pass_radius;
31 float autocvar_g_ctf_pass_wait;
32 bool autocvar_g_ctf_pass_request;
33 float autocvar_g_ctf_pass_turnrate;
34 float autocvar_g_ctf_pass_timelimit;
35 float autocvar_g_ctf_pass_velocity;
36 bool autocvar_g_ctf_dynamiclights;
37 float autocvar_g_ctf_flag_collect_delay;
38 float autocvar_g_ctf_flag_damageforcescale;
39 bool autocvar_g_ctf_flag_dropped_waypoint;
40 bool autocvar_g_ctf_flag_dropped_floatinwater;
41 bool autocvar_g_ctf_flag_glowtrails;
42 int autocvar_g_ctf_flag_health;
43 bool autocvar_g_ctf_flag_return;
44 bool autocvar_g_ctf_flag_return_carrying;
45 float autocvar_g_ctf_flag_return_carried_radius;
46 float autocvar_g_ctf_flag_return_time;
47 bool autocvar_g_ctf_flag_return_when_unreachable;
48 float autocvar_g_ctf_flag_return_damage;
49 float autocvar_g_ctf_flag_return_damage_delay;
50 float autocvar_g_ctf_flag_return_dropped;
51 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
52 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
53 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
54 float autocvar_g_ctf_flagcarrier_selfforcefactor;
55 float autocvar_g_ctf_flagcarrier_damagefactor;
56 float autocvar_g_ctf_flagcarrier_forcefactor;
57 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
58 bool autocvar_g_ctf_fullbrightflags;
59 bool autocvar_g_ctf_ignore_frags;
60 bool autocvar_g_ctf_score_ignore_fields;
61 int autocvar_g_ctf_score_capture;
62 int autocvar_g_ctf_score_capture_assist;
63 int autocvar_g_ctf_score_kill;
64 int autocvar_g_ctf_score_penalty_drop;
65 int autocvar_g_ctf_score_penalty_returned;
66 int autocvar_g_ctf_score_pickup_base;
67 int autocvar_g_ctf_score_pickup_dropped_early;
68 int autocvar_g_ctf_score_pickup_dropped_late;
69 int autocvar_g_ctf_score_return;
70 float autocvar_g_ctf_shield_force;
71 float autocvar_g_ctf_shield_max_ratio;
72 int autocvar_g_ctf_shield_min_negscore;
73 bool autocvar_g_ctf_stalemate;
74 int autocvar_g_ctf_stalemate_endcondition;
75 float autocvar_g_ctf_stalemate_time;
76 bool autocvar_g_ctf_reverse;
77 float autocvar_g_ctf_dropped_capture_delay;
78 float autocvar_g_ctf_dropped_capture_radius;
80 void ctf_FakeTimeLimit(entity e, float t)
83 WriteByte(MSG_ONE, 3); // svc_updatestat
84 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
86 WriteCoord(MSG_ONE, autocvar_timelimit);
88 WriteCoord(MSG_ONE, (t + 1) / 60);
91 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
93 if(autocvar_sv_eventlog)
94 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != NULL) ? ftos(actor.playerid) : "")));
95 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
98 void ctf_CaptureRecord(entity flag, entity player)
100 float cap_record = ctf_captimerecord;
101 float cap_time = (time - flag.ctf_pickuptime);
102 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
106 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname);
107 else if(!ctf_captimerecord)
108 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_TIME), player.netname, TIME_ENCODE(cap_time));
109 else if(cap_time < cap_record)
110 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));
112 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));
114 // write that shit in the database
115 if(!ctf_oneflag) // but not in 1-flag mode
116 if((!ctf_captimerecord) || (cap_time < cap_record))
118 ctf_captimerecord = cap_time;
119 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
120 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
121 write_recordmarker(player, flag.ctf_pickuptime, cap_time);
124 if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
125 race_setTime(GetMapname(), TIME_ENCODE(cap_time), player.crypto_idfp, player.netname, player, false);
128 bool ctf_Immediate_Return_Allowed(entity flag, entity toucher)
131 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), { ++num_perteam; });
133 // automatically return if there's only 1 player on the team
134 return ((autocvar_g_ctf_flag_return || num_perteam <= 1 || (autocvar_g_ctf_flag_return_carrying && toucher.flagcarried))
138 bool ctf_Return_Customize(entity this, entity client)
140 // only to the carrier
141 return boolean(client == this.owner);
144 void ctf_FlagcarrierWaypoints(entity player)
146 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
147 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
148 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(GetResourceAmount(player, RESOURCE_HEALTH), GetResourceAmount(player, RESOURCE_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
149 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
151 if(player.flagcarried && CTF_SAMETEAM(player, player.flagcarried))
153 if(!player.wps_enemyflagcarrier)
155 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, 0, player, wps_enemyflagcarrier, true, RADARICON_FLAG);
156 wp.colormod = WPCOLOR_ENEMYFC(player.team);
157 setcefc(wp, ctf_Stalemate_Customize);
159 if(IS_REAL_CLIENT(player) && !ctf_stalemate)
160 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_VISIBLE);
163 if(!player.wps_flagreturn)
165 entity owp = WaypointSprite_SpawnFixed(WP_FlagReturn, player.flagcarried.ctf_spawnorigin + FLAG_WAYPOINT_OFFSET, player, wps_flagreturn, RADARICON_FLAG);
166 owp.colormod = '0 0.8 0.8';
167 //WaypointSprite_UpdateTeamRadar(player.wps_flagreturn, RADARICON_FLAG, ((player.team) ? colormapPaletteColor(player.team - 1, false) : '1 1 1'));
168 setcefc(owp, ctf_Return_Customize);
173 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
175 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
176 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
177 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
178 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
181 if(current_height) // make sure we can actually do this arcing path
183 targpos = (to + ('0 0 1' * current_height));
184 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
185 if(trace_fraction < 1)
187 //print("normal arc line failed, trying to find new pos...");
188 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
189 targpos = (trace_endpos + eZ * FLAG_PASS_ARC_OFFSET_Z);
190 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
191 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
192 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
195 else { targpos = to; }
197 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
199 vector desired_direction = normalize(targpos - from);
200 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
201 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
204 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
206 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
208 // directional tracing only
210 makevectors(passer_angle);
212 // find the closest point on the enemy to the center of the attack
213 float h; // hypotenuse, which is the distance between attacker to head
214 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
216 h = vlen(head_center - passer_center);
217 a = h * (normalize(head_center - passer_center) * v_forward);
219 vector nearest_on_line = (passer_center + a * v_forward);
220 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
222 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
223 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
225 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
230 else { return true; }
234 // =======================
235 // CaptureShield Functions
236 // =======================
238 bool ctf_CaptureShield_CheckStatus(entity p)
240 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
241 int players_worseeq, players_total;
243 if(ctf_captureshield_max_ratio <= 0)
246 s = GameRules_scoring_add(p, CTF_CAPS, 0);
247 s2 = GameRules_scoring_add(p, CTF_PICKUPS, 0);
248 s3 = GameRules_scoring_add(p, CTF_RETURNS, 0);
249 s4 = GameRules_scoring_add(p, CTF_FCKILLS, 0);
251 sr = ((s - s2) + (s3 + s4));
253 if(sr >= -ctf_captureshield_min_negscore)
256 players_total = players_worseeq = 0;
257 FOREACH_CLIENT(IS_PLAYER(it), {
260 se = GameRules_scoring_add(it, CTF_CAPS, 0);
261 se2 = GameRules_scoring_add(it, CTF_PICKUPS, 0);
262 se3 = GameRules_scoring_add(it, CTF_RETURNS, 0);
263 se4 = GameRules_scoring_add(it, CTF_FCKILLS, 0);
265 ser = ((se - se2) + (se3 + se4));
272 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
273 // use this rule here
275 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
281 void ctf_CaptureShield_Update(entity player, bool wanted_status)
283 bool updated_status = ctf_CaptureShield_CheckStatus(player);
284 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
286 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
287 player.ctf_captureshielded = updated_status;
291 bool ctf_CaptureShield_Customize(entity this, entity client)
293 if(!client.ctf_captureshielded) { return false; }
294 if(CTF_SAMETEAM(this, client)) { return false; }
299 void ctf_CaptureShield_Touch(entity this, entity toucher)
301 if(!toucher.ctf_captureshielded) { return; }
302 if(CTF_SAMETEAM(this, toucher)) { return; }
304 vector mymid = (this.absmin + this.absmax) * 0.5;
305 vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
307 Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
308 if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
311 void ctf_CaptureShield_Spawn(entity flag)
313 entity shield = new(ctf_captureshield);
316 shield.team = flag.team;
317 settouch(shield, ctf_CaptureShield_Touch);
318 setcefc(shield, ctf_CaptureShield_Customize);
319 shield.effects = EF_ADDITIVE;
320 set_movetype(shield, MOVETYPE_NOCLIP);
321 shield.solid = SOLID_TRIGGER;
322 shield.avelocity = '7 0 11';
325 setorigin(shield, flag.origin);
326 setmodel(shield, MDL_CTF_SHIELD);
327 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
331 // ====================
332 // Drop/Pass/Throw Code
333 // ====================
335 void ctf_Handle_Drop(entity flag, entity player, int droptype)
338 player = (player ? player : flag.pass_sender);
341 set_movetype(flag, MOVETYPE_TOSS);
342 flag.takedamage = DAMAGE_YES;
343 flag.angles = '0 0 0';
344 SetResourceAmountExplicit(flag, RESOURCE_HEALTH, flag.max_flag_health);
345 flag.ctf_droptime = time;
346 flag.ctf_dropper = player;
347 flag.ctf_status = FLAG_DROPPED;
349 // messages and sounds
350 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
351 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
352 ctf_EventLog("dropped", player.team, player);
355 GameRules_scoring_add_team(player, SCORE, -((flag.score_drop) ? flag.score_drop : autocvar_g_ctf_score_penalty_drop));
356 GameRules_scoring_add(player, CTF_DROPS, 1);
359 if(autocvar_g_ctf_flag_dropped_waypoint) {
360 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);
361 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
364 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
366 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
367 WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResourceAmount(flag, RESOURCE_HEALTH));
370 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
372 if(droptype == DROP_PASS)
374 flag.pass_distance = 0;
375 flag.pass_sender = NULL;
376 flag.pass_target = NULL;
380 void ctf_Handle_Retrieve(entity flag, entity player)
382 entity sender = flag.pass_sender;
384 // transfer flag to player
386 flag.owner.flagcarried = flag;
387 GameRules_scoring_vip(player, true);
392 setattachment(flag, player.vehicle, "");
393 setorigin(flag, VEHICLE_FLAG_OFFSET);
394 flag.scale = VEHICLE_FLAG_SCALE;
398 setattachment(flag, player, "");
399 setorigin(flag, FLAG_CARRY_OFFSET);
401 set_movetype(flag, MOVETYPE_NONE);
402 flag.takedamage = DAMAGE_NO;
403 flag.solid = SOLID_NOT;
404 flag.angles = '0 0 0';
405 flag.ctf_status = FLAG_CARRY;
407 // messages and sounds
408 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
409 ctf_EventLog("receive", flag.team, player);
411 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
413 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
414 else if(it == player)
415 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
416 else if(SAME_TEAM(it, sender))
417 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
420 // create new waypoint
421 ctf_FlagcarrierWaypoints(player);
423 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
424 player.throw_antispam = sender.throw_antispam;
426 flag.pass_distance = 0;
427 flag.pass_sender = NULL;
428 flag.pass_target = NULL;
431 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
433 entity flag = player.flagcarried;
434 vector targ_origin, flag_velocity;
436 if(!flag) { return; }
437 if((droptype == DROP_PASS) && !receiver) { return; }
439 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
442 setattachment(flag, NULL, "");
443 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
444 flag.owner.flagcarried = NULL;
445 GameRules_scoring_vip(flag.owner, false);
447 flag.solid = SOLID_TRIGGER;
448 flag.ctf_dropper = player;
449 flag.ctf_droptime = time;
451 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
458 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
459 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
460 WarpZone_RefSys_Copy(flag, receiver);
461 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
462 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
464 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
465 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
468 set_movetype(flag, MOVETYPE_FLY);
469 flag.takedamage = DAMAGE_NO;
470 flag.pass_sender = player;
471 flag.pass_target = receiver;
472 flag.ctf_status = FLAG_PASSING;
475 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
476 WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
477 ctf_EventLog("pass", flag.team, player);
483 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'));
485 flag_velocity = (('0 0 1' * autocvar_g_ctf_throw_velocity_up) + ((v_forward * autocvar_g_ctf_throw_velocity_forward) * ((player.items & ITEM_Strength.m_itemid) ? autocvar_g_ctf_throw_strengthmultiplier : 1)));
486 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
487 ctf_Handle_Drop(flag, player, droptype);
488 navigation_dynamicgoal_set(flag, player);
494 flag.velocity = '0 0 0'; // do nothing
501 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);
502 ctf_Handle_Drop(flag, player, droptype);
503 navigation_dynamicgoal_set(flag, player);
508 // kill old waypointsprite
509 WaypointSprite_Ping(player.wps_flagcarrier);
510 WaypointSprite_Kill(player.wps_flagcarrier);
512 if(player.wps_enemyflagcarrier)
513 WaypointSprite_Kill(player.wps_enemyflagcarrier);
515 if(player.wps_flagreturn)
516 WaypointSprite_Kill(player.wps_flagreturn);
519 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
522 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
524 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
531 void nades_GiveBonus(entity player, float score);
533 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
535 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
536 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
537 entity player_team_flag = NULL, tmp_entity;
538 float old_time, new_time;
540 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
541 if(CTF_DIFFTEAM(player, flag)) { return; }
542 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)
544 if (toucher.goalentity == flag.bot_basewaypoint)
545 toucher.goalentity_lock_timeout = 0;
548 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
549 if(SAME_TEAM(tmp_entity, player))
551 player_team_flag = tmp_entity;
555 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
557 player.throw_prevtime = time;
558 player.throw_count = 0;
560 // messages and sounds
561 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
562 ctf_CaptureRecord(enemy_flag, player);
563 _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);
567 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
568 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
574 if(enemy_flag.score_capture || flag.score_capture)
575 pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
576 GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
578 if(enemy_flag.score_team_capture || flag.score_team_capture)
579 capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
580 GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
582 old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
583 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
584 if(!old_time || new_time < old_time)
585 GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
588 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
589 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
592 if(capturetype == CAPTURE_NORMAL)
594 WaypointSprite_Kill(player.wps_flagcarrier);
595 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
597 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
598 { GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist)); }
601 flag.enemy = toucher;
604 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
605 ctf_RespawnFlag(enemy_flag);
608 void ctf_Handle_Return(entity flag, entity player)
610 // messages and sounds
611 if(IS_MONSTER(player))
613 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
617 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
618 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
620 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
621 ctf_EventLog("return", flag.team, player);
624 if(IS_PLAYER(player))
626 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
627 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
629 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
632 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
636 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
637 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
638 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
642 if(player.flagcarried == flag)
643 WaypointSprite_Kill(player.wps_flagcarrier);
648 ctf_RespawnFlag(flag);
651 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
654 float pickup_dropped_score; // used to calculate dropped pickup score
656 // attach the flag to the player
658 player.flagcarried = flag;
659 GameRules_scoring_vip(player, true);
662 setattachment(flag, player.vehicle, "");
663 setorigin(flag, VEHICLE_FLAG_OFFSET);
664 flag.scale = VEHICLE_FLAG_SCALE;
668 setattachment(flag, player, "");
669 setorigin(flag, FLAG_CARRY_OFFSET);
673 set_movetype(flag, MOVETYPE_NONE);
674 flag.takedamage = DAMAGE_NO;
675 flag.solid = SOLID_NOT;
676 flag.angles = '0 0 0';
677 flag.ctf_status = FLAG_CARRY;
681 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
682 case PICKUP_DROPPED: SetResourceAmountExplicit(flag, RESOURCE_HEALTH, flag.max_flag_health); break; // reset health/return timelimit
686 // messages and sounds
687 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
689 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
691 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
692 else if(CTF_DIFFTEAM(player, flag))
693 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
695 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
697 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
700 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); });
703 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
704 if(CTF_SAMETEAM(flag, it))
705 if(SAME_TEAM(player, it))
706 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
708 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);
711 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
714 GameRules_scoring_add(player, CTF_PICKUPS, 1);
715 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
720 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
721 ctf_EventLog("steal", flag.team, player);
727 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);
728 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);
729 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
730 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
731 ctf_EventLog("pickup", flag.team, player);
739 if(pickuptype == PICKUP_BASE)
741 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
742 if((player.speedrunning) && (ctf_captimerecord))
743 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
747 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
750 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
751 ctf_FlagcarrierWaypoints(player);
752 WaypointSprite_Ping(player.wps_flagcarrier);
756 // ===================
757 // Main Flag Functions
758 // ===================
760 void ctf_CheckFlagReturn(entity flag, int returntype)
762 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
764 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResourceAmount(flag, RESOURCE_HEALTH)); }
766 if((GetResourceAmount(flag, RESOURCE_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
771 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
773 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
774 case RETURN_SPEEDRUN:
775 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
776 case RETURN_NEEDKILL:
777 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
780 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
782 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
783 ctf_EventLog("returned", flag.team, NULL);
785 ctf_RespawnFlag(flag);
790 bool ctf_Stalemate_Customize(entity this, entity client)
792 // make spectators see what the player would see
793 entity e = WaypointSprite_getviewentity(client);
794 entity wp_owner = this.owner;
797 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
798 if(SAME_TEAM(wp_owner, e)) { return false; }
799 if(!IS_PLAYER(e)) { return false; }
804 void ctf_CheckStalemate()
807 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
810 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
812 // build list of stale flags
813 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
815 if(autocvar_g_ctf_stalemate)
816 if(tmp_entity.ctf_status != FLAG_BASE)
817 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
819 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
820 ctf_staleflaglist = tmp_entity;
822 switch(tmp_entity.team)
824 case NUM_TEAM_1: ++stale_red_flags; break;
825 case NUM_TEAM_2: ++stale_blue_flags; break;
826 case NUM_TEAM_3: ++stale_yellow_flags; break;
827 case NUM_TEAM_4: ++stale_pink_flags; break;
828 default: ++stale_neutral_flags; break;
834 stale_flags = (stale_neutral_flags >= 1);
836 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
838 if(ctf_oneflag && stale_flags == 1)
839 ctf_stalemate = true;
840 else if(stale_flags >= 2)
841 ctf_stalemate = true;
842 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
843 { ctf_stalemate = false; wpforenemy_announced = false; }
844 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
845 { ctf_stalemate = false; wpforenemy_announced = false; }
847 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
850 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
852 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
854 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);
855 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
856 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
860 if (!wpforenemy_announced)
862 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)); });
864 wpforenemy_announced = true;
869 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
871 if(ITEM_DAMAGE_NEEDKILL(deathtype))
873 if(autocvar_g_ctf_flag_return_damage_delay)
874 this.ctf_flagdamaged_byworld = true;
877 SetResourceAmountExplicit(this, RESOURCE_HEALTH, 0);
878 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
882 if(autocvar_g_ctf_flag_return_damage)
884 // reduce health and check if it should be returned
885 TakeResource(this, RESOURCE_HEALTH, damage);
886 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
891 void ctf_FlagThink(entity this)
896 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
899 if(this == ctf_worldflaglist) // only for the first flag
900 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
903 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
904 LOG_TRACE("wtf the flag got squashed?");
905 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
906 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
907 setsize(this, this.m_mins, this.m_maxs);
911 switch(this.ctf_status)
915 if(autocvar_g_ctf_dropped_capture_radius)
917 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
918 if(tmp_entity.ctf_status == FLAG_DROPPED)
919 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
920 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
921 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
928 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
930 if(autocvar_g_ctf_flag_dropped_floatinwater)
932 vector midpoint = ((this.absmin + this.absmax) * 0.5);
933 if(pointcontents(midpoint) == CONTENT_WATER)
935 this.velocity = this.velocity * 0.5;
937 if (pointcontents(midpoint + eZ * FLAG_FLOAT_OFFSET_Z) == CONTENT_WATER)
938 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
940 { set_movetype(this, MOVETYPE_FLY); }
942 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
944 if(autocvar_g_ctf_flag_return_dropped)
946 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
948 SetResourceAmountExplicit(this, RESOURCE_HEALTH, 0);
949 ctf_CheckFlagReturn(this, RETURN_DROPPED);
953 if(this.ctf_flagdamaged_byworld)
955 TakeResource(this, RESOURCE_HEALTH, ((this.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE));
956 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
959 else if(autocvar_g_ctf_flag_return_time)
961 TakeResource(this, RESOURCE_HEALTH, ((this.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE));
962 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
970 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
972 SetResourceAmountExplicit(this, RESOURCE_HEALTH, 0);
973 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
975 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
976 ImpulseCommands(this.owner);
978 if(autocvar_g_ctf_stalemate)
980 if(time >= wpforenemy_nextthink)
982 ctf_CheckStalemate();
983 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
986 if(CTF_SAMETEAM(this, this.owner) && this.team)
988 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
989 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
990 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
991 ctf_Handle_Return(this, this.owner);
998 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
999 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1000 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1002 if((this.pass_target == NULL)
1003 || (IS_DEAD(this.pass_target))
1004 || (this.pass_target.flagcarried)
1005 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1006 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1007 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1009 // give up, pass failed
1010 ctf_Handle_Drop(this, NULL, DROP_PASS);
1014 // still a viable target, go for it
1015 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1020 default: // this should never happen
1022 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1028 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1031 if(game_stopped) return;
1032 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1034 bool is_not_monster = (!IS_MONSTER(toucher));
1036 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1037 if(ITEM_TOUCH_NEEDKILL())
1039 if(!autocvar_g_ctf_flag_return_damage_delay)
1041 SetResourceAmountExplicit(flag, RESOURCE_HEALTH, 0);
1042 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1044 if(!flag.ctf_flagdamaged_byworld) { return; }
1047 // special touch behaviors
1048 if(STAT(FROZEN, toucher)) { return; }
1049 else if(IS_VEHICLE(toucher))
1051 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1052 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1054 return; // do nothing
1056 else if(IS_MONSTER(toucher))
1058 if(!autocvar_g_ctf_allow_monster_touch)
1059 return; // do nothing
1061 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1063 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1065 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1066 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1067 flag.wait = time + FLAG_TOUCHRATE;
1071 else if(IS_DEAD(toucher)) { return; }
1073 switch(flag.ctf_status)
1079 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1080 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1081 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1082 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1084 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1085 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1086 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)
1088 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1089 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1091 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1092 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1098 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1099 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1100 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1101 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1107 LOG_TRACE("Someone touched a flag even though it was being carried?");
1113 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1115 if(DIFF_TEAM(toucher, flag.pass_sender))
1117 if(ctf_Immediate_Return_Allowed(flag, toucher))
1118 ctf_Handle_Return(flag, toucher);
1119 else if(is_not_monster && (!toucher.flagcarried))
1120 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1122 else if(!toucher.flagcarried)
1123 ctf_Handle_Retrieve(flag, toucher);
1130 .float last_respawn;
1131 void ctf_RespawnFlag(entity flag)
1133 // check for flag respawn being called twice in a row
1134 if(flag.last_respawn > time - 0.5)
1135 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1137 flag.last_respawn = time;
1139 // reset the player (if there is one)
1140 if((flag.owner) && (flag.owner.flagcarried == flag))
1142 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1143 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1144 WaypointSprite_Kill(flag.wps_flagcarrier);
1146 flag.owner.flagcarried = NULL;
1147 GameRules_scoring_vip(flag.owner, false);
1149 if(flag.speedrunning)
1150 ctf_FakeTimeLimit(flag.owner, -1);
1153 if((flag.owner) && (flag.owner.vehicle))
1154 flag.scale = FLAG_SCALE;
1156 if(flag.ctf_status == FLAG_DROPPED)
1157 { WaypointSprite_Kill(flag.wps_flagdropped); }
1160 setattachment(flag, NULL, "");
1161 setorigin(flag, flag.ctf_spawnorigin);
1163 set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS));
1164 flag.takedamage = DAMAGE_NO;
1165 SetResourceAmountExplicit(flag, RESOURCE_HEALTH, flag.max_flag_health);
1166 flag.solid = SOLID_TRIGGER;
1167 flag.velocity = '0 0 0';
1168 flag.angles = flag.mangle;
1169 flag.flags = FL_ITEM | FL_NOTARGET;
1171 flag.ctf_status = FLAG_BASE;
1173 flag.pass_distance = 0;
1174 flag.pass_sender = NULL;
1175 flag.pass_target = NULL;
1176 flag.ctf_dropper = NULL;
1177 flag.ctf_pickuptime = 0;
1178 flag.ctf_droptime = 0;
1179 flag.ctf_flagdamaged_byworld = false;
1180 navigation_dynamicgoal_unset(flag);
1182 ctf_CheckStalemate();
1185 void ctf_Reset(entity this)
1187 if(this.owner && IS_PLAYER(this.owner))
1188 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1191 ctf_RespawnFlag(this);
1194 bool ctf_FlagBase_Customize(entity this, entity client)
1196 entity e = WaypointSprite_getviewentity(client);
1197 entity wp_owner = this.owner;
1198 entity flag = e.flagcarried;
1199 if(flag && CTF_SAMETEAM(e, flag))
1201 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1206 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1209 waypoint_spawnforitem_force(this, this.origin);
1210 navigation_dynamicgoal_init(this, true);
1216 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1217 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1218 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1219 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1220 default: basename = WP_FlagBaseNeutral; break;
1223 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1224 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1225 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1226 setcefc(wp, ctf_FlagBase_Customize);
1228 // captureshield setup
1229 ctf_CaptureShield_Spawn(this);
1234 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1237 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1238 ctf_worldflaglist = flag;
1240 setattachment(flag, NULL, "");
1242 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1243 flag.team = teamnumber;
1244 flag.classname = "item_flag_team";
1245 flag.target = "###item###"; // for finding the nearest item using findnearest
1246 flag.flags = FL_ITEM | FL_NOTARGET;
1247 IL_PUSH(g_items, flag);
1248 flag.solid = SOLID_TRIGGER;
1249 flag.takedamage = DAMAGE_NO;
1250 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1251 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1252 SetResourceAmountExplicit(flag, RESOURCE_HEALTH, flag.max_flag_health);
1253 flag.event_damage = ctf_FlagDamage;
1254 flag.pushable = true;
1255 flag.teleportable = TELEPORT_NORMAL;
1256 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1257 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1258 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1259 if(flag.damagedbycontents)
1260 IL_PUSH(g_damagedbycontents, flag);
1261 flag.velocity = '0 0 0';
1262 flag.mangle = flag.angles;
1263 flag.reset = ctf_Reset;
1264 settouch(flag, ctf_FlagTouch);
1265 setthink(flag, ctf_FlagThink);
1266 flag.nextthink = time + FLAG_THINKRATE;
1267 flag.ctf_status = FLAG_BASE;
1269 // crudely force them all to 0
1270 if(autocvar_g_ctf_score_ignore_fields)
1271 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1273 string teamname = Static_Team_ColorName_Lower(teamnumber);
1275 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1276 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1277 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1278 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnumber).eent_eff_name; }
1279 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnumber).eent_eff_name; }
1280 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnumber).eent_eff_name; }
1284 if(flag.s == "") flag.s = b; \
1285 precache_sound(flag.s);
1287 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnumber))))
1288 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnumber))))
1289 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnumber))))
1290 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnumber))))
1291 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1292 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1293 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1297 precache_model(flag.model);
1300 _setmodel(flag, flag.model); // precision set below
1301 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1302 flag.m_mins = flag.mins; // store these for squash checks
1303 flag.m_maxs = flag.maxs;
1304 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1306 if(autocvar_g_ctf_flag_glowtrails)
1310 case NUM_TEAM_1: flag.glow_color = 251; break;
1311 case NUM_TEAM_2: flag.glow_color = 210; break;
1312 case NUM_TEAM_3: flag.glow_color = 110; break;
1313 case NUM_TEAM_4: flag.glow_color = 145; break;
1314 default: flag.glow_color = 254; break;
1316 flag.glow_size = 25;
1317 flag.glow_trail = 1;
1320 flag.effects |= EF_LOWPRECISION;
1321 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1322 if(autocvar_g_ctf_dynamiclights)
1326 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1327 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1328 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1329 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1330 default: flag.effects |= EF_DIMLIGHT; break;
1335 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1337 flag.dropped_origin = flag.origin;
1338 flag.noalign = true;
1339 set_movetype(flag, MOVETYPE_NONE);
1341 else // drop to floor, automatically find a platform and set that as spawn origin
1343 flag.noalign = false;
1345 set_movetype(flag, MOVETYPE_NONE);
1348 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1356 // NOTE: LEGACY CODE, needs to be re-written!
1358 void havocbot_ctf_calculate_middlepoint()
1362 vector fo = '0 0 0';
1365 f = ctf_worldflaglist;
1370 f = f.ctf_worldflagnext;
1376 havocbot_middlepoint = s / n;
1377 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1379 havocbot_symmetry_axis_m = 0;
1380 havocbot_symmetry_axis_q = 0;
1383 // for symmetrical editing of waypoints
1384 entity f1 = ctf_worldflaglist;
1385 entity f2 = f1.ctf_worldflagnext;
1386 float m = -(f1.origin.y - f2.origin.y) / (f1.origin.x - f2.origin.x);
1387 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1388 havocbot_symmetry_axis_m = m;
1389 havocbot_symmetry_axis_q = q;
1391 havocbot_symmetry_origin_order = n;
1395 entity havocbot_ctf_find_flag(entity bot)
1398 f = ctf_worldflaglist;
1401 if (CTF_SAMETEAM(bot, f))
1403 f = f.ctf_worldflagnext;
1408 entity havocbot_ctf_find_enemy_flag(entity bot)
1411 f = ctf_worldflaglist;
1416 if(CTF_DIFFTEAM(bot, f))
1423 else if(!bot.flagcarried)
1427 else if (CTF_DIFFTEAM(bot, f))
1429 f = f.ctf_worldflagnext;
1434 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1441 FOREACH_CLIENT(IS_PLAYER(it), {
1442 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1445 if(vdist(it.origin - org, <, tc_radius))
1454 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1457 head = ctf_worldflaglist;
1460 if (CTF_SAMETEAM(this, head))
1462 head = head.ctf_worldflagnext;
1465 navigation_routerating(this, head, ratingscale, 10000);
1469 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1472 head = ctf_worldflaglist;
1475 if (CTF_SAMETEAM(this, head))
1477 if (this.flagcarried)
1478 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1480 head = head.ctf_worldflagnext; // skip base if it has a different group
1485 head = head.ctf_worldflagnext;
1490 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1493 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1496 head = ctf_worldflaglist;
1501 if(CTF_DIFFTEAM(this, head))
1505 if(this.flagcarried)
1508 else if(!this.flagcarried)
1512 else if(CTF_DIFFTEAM(this, head))
1514 head = head.ctf_worldflagnext;
1518 if (head.ctf_status == FLAG_CARRY)
1520 // adjust rating of our flag carrier depending on his health
1521 head = head.tag_entity;
1522 float f = bound(0, (GetResourceAmount(head, RESOURCE_HEALTH) + GetResourceAmount(head, RESOURCE_ARMOR)) / 100, 2) - 1;
1523 ratingscale += ratingscale * f * 0.1;
1525 navigation_routerating(this, head, ratingscale, 10000);
1529 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1531 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1533 if (!bot_waypoints_for_items)
1535 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1541 head = havocbot_ctf_find_enemy_flag(this);
1546 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1549 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1553 mf = havocbot_ctf_find_flag(this);
1555 if(mf.ctf_status == FLAG_BASE)
1559 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1562 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1565 head = ctf_worldflaglist;
1568 // flag is out in the field
1569 if(head.ctf_status != FLAG_BASE)
1570 if(head.tag_entity==NULL) // dropped
1574 if(vdist(org - head.origin, <, df_radius))
1575 navigation_routerating(this, head, ratingscale, 10000);
1578 navigation_routerating(this, head, ratingscale, 10000);
1581 head = head.ctf_worldflagnext;
1585 void havocbot_ctf_reset_role(entity this)
1587 float cdefense, cmiddle, coffense;
1594 if (this.flagcarried)
1596 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1600 mf = havocbot_ctf_find_flag(this);
1601 ef = havocbot_ctf_find_enemy_flag(this);
1603 // Retrieve stolen flag
1604 if(mf.ctf_status!=FLAG_BASE)
1606 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1610 // If enemy flag is taken go to the middle to intercept pursuers
1611 if(ef.ctf_status!=FLAG_BASE)
1613 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1617 // if there is no one else on the team switch to offense
1619 // don't check if this bot is a player since it isn't true when the bot is added to the server
1620 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1624 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1627 else if (time < CS(this).jointime + 1)
1629 // if bots spawn all at once set good default roles
1632 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1635 else if (count == 2)
1637 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1642 // Evaluate best position to take
1643 // Count mates on middle position
1644 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1646 // Count mates on defense position
1647 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1649 // Count mates on offense position
1650 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1652 if(cdefense<=coffense)
1653 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1654 else if(coffense<=cmiddle)
1655 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1657 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1659 // if bots spawn all at once assign them a more appropriated role after a while
1660 if (time < CS(this).jointime + 1 && count > 2)
1661 this.havocbot_role_timeout = time + 10 + random() * 10;
1664 bool havocbot_ctf_is_basewaypoint(entity item)
1666 if (item.classname != "waypoint")
1669 entity head = ctf_worldflaglist;
1672 if (item == head.bot_basewaypoint)
1674 head = head.ctf_worldflagnext;
1679 void havocbot_role_ctf_carrier(entity this)
1683 havocbot_ctf_reset_role(this);
1687 if (this.flagcarried == NULL)
1689 havocbot_ctf_reset_role(this);
1693 if (navigation_goalrating_timeout(this))
1695 navigation_goalrating_start(this);
1698 entity mf = havocbot_ctf_find_flag(this);
1699 vector base_org = mf.dropped_origin;
1700 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1702 havocbot_goalrating_ctf_enemybase(this, base_rating);
1704 havocbot_goalrating_ctf_ourbase(this, base_rating);
1706 // start collecting items very close to the bot but only inside of own base radius
1707 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1708 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1710 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1712 navigation_goalrating_end(this);
1714 navigation_goalrating_timeout_set(this);
1716 entity goal = this.goalentity;
1717 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1718 this.goalentity_lock_timeout = time + ((this.bot_aimtarg) ? 2 : 3);
1721 this.havocbot_cantfindflag = time + 10;
1722 else if (time > this.havocbot_cantfindflag)
1724 // Can't navigate to my own base, suicide!
1725 // TODO: drop it and wander around
1726 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1732 void havocbot_role_ctf_escort(entity this)
1738 havocbot_ctf_reset_role(this);
1742 if (this.flagcarried)
1744 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1748 // If enemy flag is back on the base switch to previous role
1749 ef = havocbot_ctf_find_enemy_flag(this);
1750 if(ef.ctf_status==FLAG_BASE)
1752 this.havocbot_role = this.havocbot_previous_role;
1753 this.havocbot_role_timeout = 0;
1756 if (ef.ctf_status == FLAG_DROPPED)
1758 navigation_goalrating_timeout_expire(this, 1);
1762 // If the flag carrier reached the base switch to defense
1763 mf = havocbot_ctf_find_flag(this);
1764 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1766 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1770 // Set the role timeout if necessary
1771 if (!this.havocbot_role_timeout)
1773 this.havocbot_role_timeout = time + random() * 30 + 60;
1776 // If nothing happened just switch to previous role
1777 if (time > this.havocbot_role_timeout)
1779 this.havocbot_role = this.havocbot_previous_role;
1780 this.havocbot_role_timeout = 0;
1784 // Chase the flag carrier
1785 if (navigation_goalrating_timeout(this))
1787 navigation_goalrating_start(this);
1790 havocbot_goalrating_ctf_enemyflag(this, 10000);
1791 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1792 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1794 navigation_goalrating_end(this);
1796 navigation_goalrating_timeout_set(this);
1800 void havocbot_role_ctf_offense(entity this)
1807 havocbot_ctf_reset_role(this);
1811 if (this.flagcarried)
1813 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1818 mf = havocbot_ctf_find_flag(this);
1819 ef = havocbot_ctf_find_enemy_flag(this);
1822 if(mf.ctf_status!=FLAG_BASE)
1825 pos = mf.tag_entity.origin;
1829 // Try to get it if closer than the enemy base
1830 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1832 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1837 // Escort flag carrier
1838 if(ef.ctf_status!=FLAG_BASE)
1841 pos = ef.tag_entity.origin;
1845 if(vdist(pos - mf.dropped_origin, >, 700))
1847 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1852 // Set the role timeout if necessary
1853 if (!this.havocbot_role_timeout)
1854 this.havocbot_role_timeout = time + 120;
1856 if (time > this.havocbot_role_timeout)
1858 havocbot_ctf_reset_role(this);
1862 if (navigation_goalrating_timeout(this))
1864 navigation_goalrating_start(this);
1867 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1868 havocbot_goalrating_ctf_enemybase(this, 10000);
1869 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1871 navigation_goalrating_end(this);
1873 navigation_goalrating_timeout_set(this);
1877 // Retriever (temporary role):
1878 void havocbot_role_ctf_retriever(entity this)
1884 havocbot_ctf_reset_role(this);
1888 if (this.flagcarried)
1890 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1894 // If flag is back on the base switch to previous role
1895 mf = havocbot_ctf_find_flag(this);
1896 if(mf.ctf_status==FLAG_BASE)
1898 if (mf.enemy == this) // did this bot return the flag?
1899 navigation_goalrating_timeout_force(this);
1900 havocbot_ctf_reset_role(this);
1904 if (!this.havocbot_role_timeout)
1905 this.havocbot_role_timeout = time + 20;
1907 if (time > this.havocbot_role_timeout)
1909 havocbot_ctf_reset_role(this);
1913 if (navigation_goalrating_timeout(this))
1915 const float RT_RADIUS = 10000;
1917 navigation_goalrating_start(this);
1920 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1921 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1922 havocbot_goalrating_ctf_enemybase(this, 8000);
1923 entity ef = havocbot_ctf_find_enemy_flag(this);
1924 vector enemy_base_org = ef.dropped_origin;
1925 // start collecting items very close to the bot but only inside of enemy base radius
1926 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1927 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1928 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1930 navigation_goalrating_end(this);
1932 navigation_goalrating_timeout_set(this);
1936 void havocbot_role_ctf_middle(entity this)
1942 havocbot_ctf_reset_role(this);
1946 if (this.flagcarried)
1948 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1952 mf = havocbot_ctf_find_flag(this);
1953 if(mf.ctf_status!=FLAG_BASE)
1955 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1959 if (!this.havocbot_role_timeout)
1960 this.havocbot_role_timeout = time + 10;
1962 if (time > this.havocbot_role_timeout)
1964 havocbot_ctf_reset_role(this);
1968 if (navigation_goalrating_timeout(this))
1972 org = havocbot_middlepoint;
1973 org.z = this.origin.z;
1975 navigation_goalrating_start(this);
1978 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
1979 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
1980 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
1981 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
1982 havocbot_goalrating_items(this, 18000, this.origin, 10000);
1983 havocbot_goalrating_ctf_enemybase(this, 3000);
1985 navigation_goalrating_end(this);
1987 entity goal = this.goalentity;
1988 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1989 this.goalentity_lock_timeout = time + 2;
1991 navigation_goalrating_timeout_set(this);
1995 void havocbot_role_ctf_defense(entity this)
2001 havocbot_ctf_reset_role(this);
2005 if (this.flagcarried)
2007 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2011 // If own flag was captured
2012 mf = havocbot_ctf_find_flag(this);
2013 if(mf.ctf_status!=FLAG_BASE)
2015 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2019 if (!this.havocbot_role_timeout)
2020 this.havocbot_role_timeout = time + 30;
2022 if (time > this.havocbot_role_timeout)
2024 havocbot_ctf_reset_role(this);
2027 if (navigation_goalrating_timeout(this))
2029 vector org = mf.dropped_origin;
2031 navigation_goalrating_start(this);
2033 // if enemies are closer to our base, go there
2034 entity closestplayer = NULL;
2035 float distance, bestdistance = 10000;
2036 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2037 distance = vlen(org - it.origin);
2038 if(distance<bestdistance)
2041 bestdistance = distance;
2047 if(DIFF_TEAM(closestplayer, this))
2048 if(vdist(org - this.origin, >, 1000))
2049 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2050 havocbot_goalrating_ctf_ourbase(this, 10000);
2052 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2053 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2054 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2055 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2056 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2058 navigation_goalrating_end(this);
2060 navigation_goalrating_timeout_set(this);
2064 void havocbot_role_ctf_setrole(entity bot, int role)
2066 string s = "(null)";
2069 case HAVOCBOT_CTF_ROLE_CARRIER:
2071 bot.havocbot_role = havocbot_role_ctf_carrier;
2072 bot.havocbot_role_timeout = 0;
2073 bot.havocbot_cantfindflag = time + 10;
2074 if (bot.havocbot_previous_role != bot.havocbot_role)
2075 navigation_goalrating_timeout_force(bot);
2077 case HAVOCBOT_CTF_ROLE_DEFENSE:
2079 bot.havocbot_role = havocbot_role_ctf_defense;
2080 bot.havocbot_role_timeout = 0;
2082 case HAVOCBOT_CTF_ROLE_MIDDLE:
2084 bot.havocbot_role = havocbot_role_ctf_middle;
2085 bot.havocbot_role_timeout = 0;
2087 case HAVOCBOT_CTF_ROLE_OFFENSE:
2089 bot.havocbot_role = havocbot_role_ctf_offense;
2090 bot.havocbot_role_timeout = 0;
2092 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2094 bot.havocbot_previous_role = bot.havocbot_role;
2095 bot.havocbot_role = havocbot_role_ctf_retriever;
2096 bot.havocbot_role_timeout = time + 10;
2097 if (bot.havocbot_previous_role != bot.havocbot_role)
2098 navigation_goalrating_timeout_expire(bot, 2);
2100 case HAVOCBOT_CTF_ROLE_ESCORT:
2102 bot.havocbot_previous_role = bot.havocbot_role;
2103 bot.havocbot_role = havocbot_role_ctf_escort;
2104 bot.havocbot_role_timeout = time + 30;
2105 if (bot.havocbot_previous_role != bot.havocbot_role)
2106 navigation_goalrating_timeout_expire(bot, 2);
2109 LOG_TRACE(bot.netname, " switched to ", s);
2117 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2119 entity player = M_ARGV(0, entity);
2121 int t = 0, t2 = 0, t3 = 0;
2122 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)
2124 // initially clear items so they can be set as necessary later.
2125 STAT(CTF_FLAGSTATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2126 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2127 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2128 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2129 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2130 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2132 // scan through all the flags and notify the client about them
2133 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2135 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2136 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2137 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2138 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2139 if(flag.team == 0 && !b5) { b5 = true; t = CTF_NEUTRAL_FLAG_CARRYING; t2 = CTF_NEUTRAL_FLAG_TAKEN; t3 = CTF_NEUTRAL_FLAG_LOST; STAT(CTF_FLAGSTATUS, player) |= CTF_FLAG_NEUTRAL; }
2141 switch(flag.ctf_status)
2146 if((flag.owner == player) || (flag.pass_sender == player))
2147 STAT(CTF_FLAGSTATUS, player) |= t; // carrying: player is currently carrying the flag
2149 STAT(CTF_FLAGSTATUS, player) |= t2; // taken: someone else is carrying the flag
2154 STAT(CTF_FLAGSTATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2160 // item for stopping players from capturing the flag too often
2161 if(player.ctf_captureshielded)
2162 STAT(CTF_FLAGSTATUS, player) |= CTF_SHIELDED;
2165 STAT(CTF_FLAGSTATUS, player) |= CTF_STALEMATE;
2167 // update the health of the flag carrier waypointsprite
2168 if(player.wps_flagcarrier)
2169 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(GetResourceAmount(player, RESOURCE_HEALTH), GetResourceAmount(player, RESOURCE_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
2172 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2174 entity frag_attacker = M_ARGV(1, entity);
2175 entity frag_target = M_ARGV(2, entity);
2176 float frag_damage = M_ARGV(4, float);
2177 vector frag_force = M_ARGV(6, vector);
2179 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2181 if(frag_target == frag_attacker) // damage done to yourself
2183 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2184 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2186 else // damage done to everyone else
2188 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2189 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2192 M_ARGV(4, float) = frag_damage;
2193 M_ARGV(6, vector) = frag_force;
2195 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2197 if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(GetResourceAmount(frag_target, RESOURCE_HEALTH), GetResourceAmount(frag_target, RESOURCE_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id)))
2198 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2200 frag_target.wps_helpme_time = time;
2201 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2203 // todo: add notification for when flag carrier needs help?
2207 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2209 entity frag_attacker = M_ARGV(1, entity);
2210 entity frag_target = M_ARGV(2, entity);
2212 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2214 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2215 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2218 if(frag_target.flagcarried)
2220 entity tmp_entity = frag_target.flagcarried;
2221 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2222 tmp_entity.ctf_dropper = NULL;
2226 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2228 M_ARGV(2, float) = 0; // frag score
2229 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2232 void ctf_RemovePlayer(entity player)
2234 if(player.flagcarried)
2235 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2237 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2239 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2240 if(flag.pass_target == player) { flag.pass_target = NULL; }
2241 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2245 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2247 entity player = M_ARGV(0, entity);
2249 ctf_RemovePlayer(player);
2252 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2254 entity player = M_ARGV(0, entity);
2256 ctf_RemovePlayer(player);
2259 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2261 if(!autocvar_g_ctf_leaderboard)
2264 entity player = M_ARGV(0, entity);
2266 if(IS_REAL_CLIENT(player))
2268 int m = min(RANKINGS_CNT, autocvar_g_cts_send_rankings_cnt);
2269 race_send_rankings_cnt(MSG_ONE);
2270 for (int i = 1; i <= m; ++i)
2272 race_SendRankings(i, 0, 0, MSG_ONE);
2277 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2279 if(!autocvar_g_ctf_leaderboard)
2282 entity player = M_ARGV(0, entity);
2284 if(CS(player).cvar_cl_allow_uidtracking == 1 && CS(player).cvar_cl_allow_uid2name == 1)
2286 if (!player.stored_netname)
2287 player.stored_netname = strzone(uid2name(player.crypto_idfp));
2288 if(player.stored_netname != player.netname)
2290 db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
2291 strcpy(player.stored_netname, player.netname);
2296 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2298 entity player = M_ARGV(0, entity);
2300 if(player.flagcarried)
2301 if(!autocvar_g_ctf_portalteleport)
2302 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2305 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2307 if(MUTATOR_RETURNVALUE || game_stopped) return;
2309 entity player = M_ARGV(0, entity);
2311 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2313 // pass the flag to a team mate
2314 if(autocvar_g_ctf_pass)
2316 entity head, closest_target = NULL;
2317 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2319 while(head) // find the closest acceptable target to pass to
2321 if(IS_PLAYER(head) && !IS_DEAD(head))
2322 if(head != player && SAME_TEAM(head, player))
2323 if(!head.speedrunning && !head.vehicle)
2325 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2326 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2327 vector passer_center = CENTER_OR_VIEWOFS(player);
2329 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2331 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2333 if(IS_BOT_CLIENT(head))
2335 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2336 ctf_Handle_Throw(head, player, DROP_PASS);
2340 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2341 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2343 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2346 else if(player.flagcarried && !head.flagcarried)
2350 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2351 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2352 { closest_target = head; }
2354 else { closest_target = head; }
2361 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2364 // throw the flag in front of you
2365 if(autocvar_g_ctf_throw && player.flagcarried)
2367 if(player.throw_count == -1)
2369 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2371 player.throw_prevtime = time;
2372 player.throw_count = 1;
2373 ctf_Handle_Throw(player, NULL, DROP_THROW);
2378 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2384 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2385 else { player.throw_count += 1; }
2386 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2388 player.throw_prevtime = time;
2389 ctf_Handle_Throw(player, NULL, DROP_THROW);
2396 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2398 entity player = M_ARGV(0, entity);
2400 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2402 player.wps_helpme_time = time;
2403 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2405 else // create a normal help me waypointsprite
2407 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2408 WaypointSprite_Ping(player.wps_helpme);
2414 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2416 entity player = M_ARGV(0, entity);
2417 entity veh = M_ARGV(1, entity);
2419 if(player.flagcarried)
2421 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2423 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2427 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2428 setattachment(player.flagcarried, veh, "");
2429 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2430 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2431 //player.flagcarried.angles = '0 0 0';
2437 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2439 entity player = M_ARGV(0, entity);
2441 if(player.flagcarried)
2443 setattachment(player.flagcarried, player, "");
2444 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2445 player.flagcarried.scale = FLAG_SCALE;
2446 player.flagcarried.angles = '0 0 0';
2447 player.flagcarried.nodrawtoclient = NULL;
2452 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2454 entity player = M_ARGV(0, entity);
2456 if(player.flagcarried)
2458 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2459 ctf_RespawnFlag(player.flagcarried);
2464 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2466 entity flag; // temporary entity for the search method
2468 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2470 switch(flag.ctf_status)
2475 // lock the flag, game is over
2476 set_movetype(flag, MOVETYPE_NONE);
2477 flag.takedamage = DAMAGE_NO;
2478 flag.solid = SOLID_NOT;
2479 flag.nextthink = false; // stop thinking
2481 //dprint("stopping the ", flag.netname, " from moving.\n");
2489 // do nothing for these flags
2496 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2498 entity bot = M_ARGV(0, entity);
2500 havocbot_ctf_reset_role(bot);
2504 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2506 M_ARGV(1, string) = "ctf_team";
2509 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2511 entity spectatee = M_ARGV(0, entity);
2512 entity client = M_ARGV(1, entity);
2514 STAT(CTF_FLAGSTATUS, client) = STAT(CTF_FLAGSTATUS, spectatee);
2517 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2519 int record_page = M_ARGV(0, int);
2520 string ret_string = M_ARGV(1, string);
2522 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2524 if (MapInfo_Get_ByID(i))
2526 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2532 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2533 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2537 M_ARGV(1, string) = ret_string;
2540 bool superspec_Spectate(entity this, entity targ); // TODO
2541 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2542 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2544 entity player = M_ARGV(0, entity);
2545 string cmd_name = M_ARGV(1, string);
2546 int cmd_argc = M_ARGV(2, int);
2548 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2550 if(cmd_name == "followfc")
2562 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2563 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2564 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2565 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2569 FOREACH_CLIENT(IS_PLAYER(it), {
2570 if(it.flagcarried && (it.team == _team || _team == 0))
2573 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2574 continue; // already spectating this fc, try another
2575 return superspec_Spectate(player, it);
2580 superspec_msg("", "", player, "No active flag carrier\n", 1);
2585 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2587 entity frag_target = M_ARGV(0, entity);
2589 if(frag_target.flagcarried)
2590 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2598 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2599 CTF flag for team one (Red).
2601 "angle" Angle the flag will point (minus 90 degrees)...
2602 "model" model to use, note this needs red and blue as skins 0 and 1...
2603 "noise" sound played when flag is picked up...
2604 "noise1" sound played when flag is returned by a teammate...
2605 "noise2" sound played when flag is captured...
2606 "noise3" sound played when flag is lost in the field and respawns itself...
2607 "noise4" sound played when flag is dropped by a player...
2608 "noise5" sound played when flag touches the ground... */
2609 spawnfunc(item_flag_team1)
2611 if(!g_ctf) { delete(this); return; }
2613 ctf_FlagSetup(NUM_TEAM_1, this);
2616 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2617 CTF flag for team two (Blue).
2619 "angle" Angle the flag will point (minus 90 degrees)...
2620 "model" model to use, note this needs red and blue as skins 0 and 1...
2621 "noise" sound played when flag is picked up...
2622 "noise1" sound played when flag is returned by a teammate...
2623 "noise2" sound played when flag is captured...
2624 "noise3" sound played when flag is lost in the field and respawns itself...
2625 "noise4" sound played when flag is dropped by a player...
2626 "noise5" sound played when flag touches the ground... */
2627 spawnfunc(item_flag_team2)
2629 if(!g_ctf) { delete(this); return; }
2631 ctf_FlagSetup(NUM_TEAM_2, this);
2634 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2635 CTF flag for team three (Yellow).
2637 "angle" Angle the flag will point (minus 90 degrees)...
2638 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2639 "noise" sound played when flag is picked up...
2640 "noise1" sound played when flag is returned by a teammate...
2641 "noise2" sound played when flag is captured...
2642 "noise3" sound played when flag is lost in the field and respawns itself...
2643 "noise4" sound played when flag is dropped by a player...
2644 "noise5" sound played when flag touches the ground... */
2645 spawnfunc(item_flag_team3)
2647 if(!g_ctf) { delete(this); return; }
2649 ctf_FlagSetup(NUM_TEAM_3, this);
2652 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2653 CTF flag for team four (Pink).
2655 "angle" Angle the flag will point (minus 90 degrees)...
2656 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2657 "noise" sound played when flag is picked up...
2658 "noise1" sound played when flag is returned by a teammate...
2659 "noise2" sound played when flag is captured...
2660 "noise3" sound played when flag is lost in the field and respawns itself...
2661 "noise4" sound played when flag is dropped by a player...
2662 "noise5" sound played when flag touches the ground... */
2663 spawnfunc(item_flag_team4)
2665 if(!g_ctf) { delete(this); return; }
2667 ctf_FlagSetup(NUM_TEAM_4, this);
2670 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2673 "angle" Angle the flag will point (minus 90 degrees)...
2674 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2675 "noise" sound played when flag is picked up...
2676 "noise1" sound played when flag is returned by a teammate...
2677 "noise2" sound played when flag is captured...
2678 "noise3" sound played when flag is lost in the field and respawns itself...
2679 "noise4" sound played when flag is dropped by a player...
2680 "noise5" sound played when flag touches the ground... */
2681 spawnfunc(item_flag_neutral)
2683 if(!g_ctf) { delete(this); return; }
2684 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2686 ctf_FlagSetup(0, this);
2689 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2690 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2691 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.
2693 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2694 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2697 if(!g_ctf) { delete(this); return; }
2699 this.classname = "ctf_team";
2700 this.team = this.cnt + 1;
2703 // compatibility for quake maps
2704 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2705 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2706 spawnfunc(info_player_team1);
2707 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2708 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2709 spawnfunc(info_player_team2);
2710 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2711 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2713 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2714 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2716 // compatibility for wop maps
2717 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2718 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2719 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2720 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2721 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2722 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2730 void ctf_ScoreRules(int teams)
2732 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2733 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2734 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2735 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2736 field(SP_CTF_PICKUPS, "pickups", 0);
2737 field(SP_CTF_FCKILLS, "fckills", 0);
2738 field(SP_CTF_RETURNS, "returns", 0);
2739 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2743 // code from here on is just to support maps that don't have flag and team entities
2744 void ctf_SpawnTeam (string teamname, int teamcolor)
2746 entity this = new_pure(ctf_team);
2747 this.netname = teamname;
2748 this.cnt = teamcolor - 1;
2749 this.spawnfunc_checked = true;
2750 this.team = teamcolor;
2753 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2758 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2760 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2761 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2763 switch(tmp_entity.team)
2765 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2766 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2767 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2768 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2770 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2773 havocbot_ctf_calculate_middlepoint();
2775 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2777 ctf_teams = 0; // so set the default red and blue teams
2778 BITSET_ASSIGN(ctf_teams, BIT(0));
2779 BITSET_ASSIGN(ctf_teams, BIT(1));
2782 //ctf_teams = bound(2, ctf_teams, 4);
2784 // if no teams are found, spawn defaults
2785 if(find(NULL, classname, "ctf_team") == NULL)
2787 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2788 if(ctf_teams & BIT(0))
2789 ctf_SpawnTeam("Red", NUM_TEAM_1);
2790 if(ctf_teams & BIT(1))
2791 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2792 if(ctf_teams & BIT(2))
2793 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2794 if(ctf_teams & BIT(3))
2795 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2798 ctf_ScoreRules(ctf_teams);
2801 void ctf_Initialize()
2803 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2805 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2806 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2807 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2809 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);