3 #include <common/effects/all.qh>
4 #include <common/vehicles/all.qh>
5 #include <server/gamelog.qh>
6 #include <server/g_damage.qh>
7 #include <server/teamplay.qh>
9 #include <lib/warpzone/common.qh>
11 bool autocvar_g_ctf_allow_vehicle_carry;
12 bool autocvar_g_ctf_allow_vehicle_touch;
13 bool autocvar_g_ctf_allow_monster_touch;
14 bool autocvar_g_ctf_throw;
15 float autocvar_g_ctf_throw_angle_max;
16 float autocvar_g_ctf_throw_angle_min;
17 int autocvar_g_ctf_throw_punish_count;
18 float autocvar_g_ctf_throw_punish_delay;
19 float autocvar_g_ctf_throw_punish_time;
20 float autocvar_g_ctf_throw_strengthmultiplier;
21 float autocvar_g_ctf_throw_velocity_forward;
22 float autocvar_g_ctf_throw_velocity_up;
23 float autocvar_g_ctf_drop_velocity_up;
24 float autocvar_g_ctf_drop_velocity_side;
25 bool autocvar_g_ctf_oneflag_reverse;
26 bool autocvar_g_ctf_portalteleport;
27 bool autocvar_g_ctf_pass;
28 float autocvar_g_ctf_pass_arc;
29 float autocvar_g_ctf_pass_arc_max;
30 float autocvar_g_ctf_pass_directional_max;
31 float autocvar_g_ctf_pass_directional_min;
32 float autocvar_g_ctf_pass_radius;
33 float autocvar_g_ctf_pass_wait;
34 bool autocvar_g_ctf_pass_request;
35 float autocvar_g_ctf_pass_turnrate;
36 float autocvar_g_ctf_pass_timelimit;
37 float autocvar_g_ctf_pass_velocity;
38 bool autocvar_g_ctf_dynamiclights;
39 float autocvar_g_ctf_flag_collect_delay;
40 float autocvar_g_ctf_flag_damageforcescale;
41 bool autocvar_g_ctf_flag_dropped_waypoint;
42 bool autocvar_g_ctf_flag_dropped_floatinwater;
43 bool autocvar_g_ctf_flag_glowtrails;
44 int autocvar_g_ctf_flag_health;
45 bool autocvar_g_ctf_flag_return;
46 bool autocvar_g_ctf_flag_return_carrying;
47 float autocvar_g_ctf_flag_return_carried_radius;
48 float autocvar_g_ctf_flag_return_time;
49 bool autocvar_g_ctf_flag_return_when_unreachable;
50 float autocvar_g_ctf_flag_return_damage;
51 float autocvar_g_ctf_flag_return_damage_delay;
52 float autocvar_g_ctf_flag_return_dropped;
53 bool autocvar_g_ctf_flag_waypoint = true;
54 float autocvar_g_ctf_flag_waypoint_maxdistance;
55 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
56 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
57 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
58 float autocvar_g_ctf_flagcarrier_selfforcefactor;
59 float autocvar_g_ctf_flagcarrier_damagefactor;
60 float autocvar_g_ctf_flagcarrier_forcefactor;
61 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
62 bool autocvar_g_ctf_fullbrightflags;
63 bool autocvar_g_ctf_ignore_frags;
64 bool autocvar_g_ctf_score_ignore_fields;
65 int autocvar_g_ctf_score_capture;
66 int autocvar_g_ctf_score_capture_assist;
67 int autocvar_g_ctf_score_kill;
68 int autocvar_g_ctf_score_penalty_drop;
69 int autocvar_g_ctf_score_penalty_returned;
70 int autocvar_g_ctf_score_pickup_base;
71 int autocvar_g_ctf_score_pickup_dropped_early;
72 int autocvar_g_ctf_score_pickup_dropped_late;
73 int autocvar_g_ctf_score_return;
74 float autocvar_g_ctf_shield_force;
75 float autocvar_g_ctf_shield_max_ratio;
76 int autocvar_g_ctf_shield_min_negscore;
77 bool autocvar_g_ctf_stalemate;
78 int autocvar_g_ctf_stalemate_endcondition;
79 float autocvar_g_ctf_stalemate_time;
80 bool autocvar_g_ctf_reverse;
81 float autocvar_g_ctf_dropped_capture_delay;
82 float autocvar_g_ctf_dropped_capture_radius;
84 void ctf_FakeTimeLimit(entity e, float t)
87 WriteByte(MSG_ONE, 3); // svc_updatestat
88 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
90 WriteCoord(MSG_ONE, autocvar_timelimit);
92 WriteCoord(MSG_ONE, (t + 1) / 60);
95 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
97 if(autocvar_sv_eventlog)
98 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != NULL) ? ftos(actor.playerid) : "")));
99 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
102 void ctf_CaptureRecord(entity flag, entity player)
104 float cap_record = ctf_captimerecord;
105 float cap_time = (time - flag.ctf_pickuptime);
106 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
110 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname);
111 else if(!ctf_captimerecord)
112 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_TIME), player.netname, TIME_ENCODE(cap_time));
113 else if(cap_time < cap_record)
114 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));
116 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));
118 // write that shit in the database
119 if(!ctf_oneflag) // but not in 1-flag mode
120 if((!ctf_captimerecord) || (cap_time < cap_record))
122 ctf_captimerecord = cap_time;
123 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
124 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
125 write_recordmarker(player, flag.ctf_pickuptime, cap_time);
128 if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
129 race_setTime(GetMapname(), TIME_ENCODE(cap_time), player.crypto_idfp, player.netname, player, false);
132 bool ctf_Immediate_Return_Allowed(entity flag, entity toucher)
135 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), { ++num_perteam; });
137 // automatically return if there's only 1 player on the team
138 return ((autocvar_g_ctf_flag_return || num_perteam <= 1 || (autocvar_g_ctf_flag_return_carrying && toucher.flagcarried))
142 bool ctf_Return_Customize(entity this, entity client)
144 // only to the carrier
145 return boolean(client == this.owner);
148 void ctf_FlagcarrierWaypoints(entity player)
150 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
151 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, 2 * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
152 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);
153 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
155 if(player.flagcarried && CTF_SAMETEAM(player, player.flagcarried))
157 if(!player.wps_enemyflagcarrier)
159 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, 0, player, wps_enemyflagcarrier, true, RADARICON_FLAG);
160 wp.colormod = WPCOLOR_ENEMYFC(player.team);
161 setcefc(wp, ctf_Stalemate_Customize);
163 if(IS_REAL_CLIENT(player) && !ctf_stalemate)
164 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_VISIBLE);
167 if(!player.wps_flagreturn)
169 entity owp = WaypointSprite_SpawnFixed(WP_FlagReturn, player.flagcarried.ctf_spawnorigin + FLAG_WAYPOINT_OFFSET, player, wps_flagreturn, RADARICON_FLAG);
170 owp.colormod = '0 0.8 0.8';
171 //WaypointSprite_UpdateTeamRadar(player.wps_flagreturn, RADARICON_FLAG, ((player.team) ? colormapPaletteColor(player.team - 1, false) : '1 1 1'));
172 setcefc(owp, ctf_Return_Customize);
177 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
179 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
180 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
181 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
182 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
185 if(current_height) // make sure we can actually do this arcing path
187 targpos = (to + ('0 0 1' * current_height));
188 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
189 if(trace_fraction < 1)
191 //print("normal arc line failed, trying to find new pos...");
192 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
193 targpos = (trace_endpos + eZ * FLAG_PASS_ARC_OFFSET_Z);
194 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
195 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
196 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
199 else { targpos = to; }
201 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
203 vector desired_direction = normalize(targpos - from);
204 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
205 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
208 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
210 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
212 // directional tracing only
214 makevectors(passer_angle);
216 // find the closest point on the enemy to the center of the attack
217 float h; // hypotenuse, which is the distance between attacker to head
218 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
220 h = vlen(head_center - passer_center);
221 a = h * (normalize(head_center - passer_center) * v_forward);
223 vector nearest_on_line = (passer_center + a * v_forward);
224 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
226 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
227 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
229 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
234 else { return true; }
238 // =======================
239 // CaptureShield Functions
240 // =======================
242 bool ctf_CaptureShield_CheckStatus(entity p)
244 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
245 int players_worseeq, players_total;
247 if(ctf_captureshield_max_ratio <= 0)
250 s = GameRules_scoring_add(p, CTF_CAPS, 0);
251 s2 = GameRules_scoring_add(p, CTF_PICKUPS, 0);
252 s3 = GameRules_scoring_add(p, CTF_RETURNS, 0);
253 s4 = GameRules_scoring_add(p, CTF_FCKILLS, 0);
255 sr = ((s - s2) + (s3 + s4));
257 if(sr >= -ctf_captureshield_min_negscore)
260 players_total = players_worseeq = 0;
261 FOREACH_CLIENT(IS_PLAYER(it), {
264 se = GameRules_scoring_add(it, CTF_CAPS, 0);
265 se2 = GameRules_scoring_add(it, CTF_PICKUPS, 0);
266 se3 = GameRules_scoring_add(it, CTF_RETURNS, 0);
267 se4 = GameRules_scoring_add(it, CTF_FCKILLS, 0);
269 ser = ((se - se2) + (se3 + se4));
276 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
277 // use this rule here
279 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
285 void ctf_CaptureShield_Update(entity player, bool wanted_status)
287 bool updated_status = ctf_CaptureShield_CheckStatus(player);
288 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
290 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
291 player.ctf_captureshielded = updated_status;
295 bool ctf_CaptureShield_Customize(entity this, entity client)
297 if(!client.ctf_captureshielded) { return false; }
298 if(CTF_SAMETEAM(this, client)) { return false; }
303 void ctf_CaptureShield_Touch(entity this, entity toucher)
305 if(!toucher.ctf_captureshielded) { return; }
306 if(CTF_SAMETEAM(this, toucher)) { return; }
308 vector mymid = (this.absmin + this.absmax) * 0.5;
309 vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
311 Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
312 if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
315 void ctf_CaptureShield_Spawn(entity flag)
317 entity shield = new(ctf_captureshield);
320 shield.team = flag.team;
321 settouch(shield, ctf_CaptureShield_Touch);
322 setcefc(shield, ctf_CaptureShield_Customize);
323 shield.effects = EF_ADDITIVE;
324 set_movetype(shield, MOVETYPE_NOCLIP);
325 shield.solid = SOLID_TRIGGER;
326 shield.avelocity = '7 0 11';
329 setorigin(shield, flag.origin);
330 setmodel(shield, MDL_CTF_SHIELD);
331 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
335 // ====================
336 // Drop/Pass/Throw Code
337 // ====================
339 void ctf_Handle_Drop(entity flag, entity player, int droptype)
342 player = (player ? player : flag.pass_sender);
345 set_movetype(flag, MOVETYPE_TOSS);
346 flag.takedamage = DAMAGE_YES;
347 flag.angles = '0 0 0';
348 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
349 flag.ctf_droptime = time;
350 flag.ctf_dropper = player;
351 flag.ctf_status = FLAG_DROPPED;
353 // messages and sounds
354 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
355 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
356 ctf_EventLog("dropped", player.team, player);
359 GameRules_scoring_add_team(player, SCORE, -((flag.score_drop) ? flag.score_drop : autocvar_g_ctf_score_penalty_drop));
360 GameRules_scoring_add(player, CTF_DROPS, 1);
363 if(autocvar_g_ctf_flag_dropped_waypoint) {
364 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);
365 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
368 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
370 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_health);
371 WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH));
374 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
376 if(droptype == DROP_PASS)
378 flag.pass_distance = 0;
379 flag.pass_sender = NULL;
380 flag.pass_target = NULL;
384 void ctf_Handle_Retrieve(entity flag, entity player)
386 entity sender = flag.pass_sender;
388 // transfer flag to player
390 flag.owner.flagcarried = flag;
391 GameRules_scoring_vip(player, true);
396 setattachment(flag, player.vehicle, "");
397 setorigin(flag, VEHICLE_FLAG_OFFSET);
398 flag.scale = VEHICLE_FLAG_SCALE;
402 setattachment(flag, player, "");
403 setorigin(flag, FLAG_CARRY_OFFSET);
405 set_movetype(flag, MOVETYPE_NONE);
406 flag.takedamage = DAMAGE_NO;
407 flag.solid = SOLID_NOT;
408 flag.angles = '0 0 0';
409 flag.ctf_status = FLAG_CARRY;
411 // messages and sounds
412 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
413 ctf_EventLog("receive", flag.team, player);
415 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
417 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
418 else if(it == player)
419 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
420 else if(SAME_TEAM(it, sender))
421 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
424 // create new waypoint
425 ctf_FlagcarrierWaypoints(player);
427 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
428 player.throw_antispam = sender.throw_antispam;
430 flag.pass_distance = 0;
431 flag.pass_sender = NULL;
432 flag.pass_target = NULL;
435 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
437 entity flag = player.flagcarried;
438 vector targ_origin, flag_velocity;
440 if(!flag) { return; }
441 if((droptype == DROP_PASS) && !receiver) { return; }
443 if(flag.speedrunning)
445 // ensure old waypoints are removed before resetting the flag
446 WaypointSprite_Kill(player.wps_flagcarrier);
448 if(player.wps_enemyflagcarrier)
449 WaypointSprite_Kill(player.wps_enemyflagcarrier);
451 if(player.wps_flagreturn)
452 WaypointSprite_Kill(player.wps_flagreturn);
453 ctf_RespawnFlag(flag);
458 setattachment(flag, NULL, "");
459 tracebox(player.origin - FLAG_DROP_OFFSET, flag.m_mins, flag.m_maxs, player.origin + FLAG_DROP_OFFSET, MOVE_NOMONSTERS, flag);
460 setorigin(flag, trace_endpos);
461 flag.owner.flagcarried = NULL;
462 GameRules_scoring_vip(flag.owner, false);
464 flag.solid = SOLID_TRIGGER;
465 flag.ctf_dropper = player;
466 flag.ctf_droptime = time;
468 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
475 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
476 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
477 WarpZone_RefSys_Copy(flag, receiver);
478 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
479 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
481 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
482 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
485 set_movetype(flag, MOVETYPE_FLY);
486 flag.takedamage = DAMAGE_NO;
487 flag.pass_sender = player;
488 flag.pass_target = receiver;
489 flag.ctf_status = FLAG_PASSING;
492 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
493 WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
494 ctf_EventLog("pass", flag.team, player);
500 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'));
502 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)));
503 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
504 ctf_Handle_Drop(flag, player, droptype);
505 navigation_dynamicgoal_set(flag, player);
511 flag.velocity = '0 0 0'; // do nothing
518 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);
519 ctf_Handle_Drop(flag, player, droptype);
520 navigation_dynamicgoal_set(flag, player);
525 // kill old waypointsprite
526 WaypointSprite_Ping(player.wps_flagcarrier);
527 WaypointSprite_Kill(player.wps_flagcarrier);
529 if(player.wps_enemyflagcarrier)
530 WaypointSprite_Kill(player.wps_enemyflagcarrier);
532 if(player.wps_flagreturn)
533 WaypointSprite_Kill(player.wps_flagreturn);
536 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
540 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
542 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
550 void nades_GiveBonus(entity player, float score);
552 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
554 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
555 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
556 entity player_team_flag = NULL, tmp_entity;
557 float old_time, new_time;
559 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
560 if(CTF_DIFFTEAM(player, flag)) { return; }
561 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)
563 if (toucher.goalentity == flag.bot_basewaypoint)
564 toucher.goalentity_lock_timeout = 0;
567 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
568 if(SAME_TEAM(tmp_entity, player))
570 player_team_flag = tmp_entity;
574 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
576 player.throw_prevtime = time;
577 player.throw_count = 0;
579 // messages and sounds
580 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
581 ctf_CaptureRecord(enemy_flag, player);
582 _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);
586 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
587 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
593 if(enemy_flag.score_capture || flag.score_capture)
594 pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
595 GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
597 if(enemy_flag.score_team_capture || flag.score_team_capture)
598 capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
599 GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
601 old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
602 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
603 if(!old_time || new_time < old_time)
604 GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
607 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
609 shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
613 if(capturetype == CAPTURE_NORMAL)
615 WaypointSprite_Kill(player.wps_flagcarrier);
616 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
618 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
619 { GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist)); }
622 flag.enemy = toucher;
625 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
626 ctf_RespawnFlag(enemy_flag);
629 void ctf_Handle_Return(entity flag, entity player)
631 // messages and sounds
632 if(IS_MONSTER(player))
634 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
638 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
639 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
641 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
642 ctf_EventLog("return", flag.team, player);
645 if(IS_PLAYER(player))
647 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
648 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
650 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
653 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
657 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
658 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
659 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
663 if(player.flagcarried == flag)
664 WaypointSprite_Kill(player.wps_flagcarrier);
669 ctf_RespawnFlag(flag);
672 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
675 float pickup_dropped_score; // used to calculate dropped pickup score
677 // attach the flag to the player
679 player.flagcarried = flag;
680 GameRules_scoring_vip(player, true);
683 setattachment(flag, player.vehicle, "");
684 setorigin(flag, VEHICLE_FLAG_OFFSET);
685 flag.scale = VEHICLE_FLAG_SCALE;
689 setattachment(flag, player, "");
690 setorigin(flag, FLAG_CARRY_OFFSET);
694 set_movetype(flag, MOVETYPE_NONE);
695 flag.takedamage = DAMAGE_NO;
696 flag.solid = SOLID_NOT;
697 flag.angles = '0 0 0';
698 flag.ctf_status = FLAG_CARRY;
702 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
703 case PICKUP_DROPPED: SetResourceExplicit(flag, RES_HEALTH, flag.max_health); break; // reset health/return timelimit
707 // messages and sounds
708 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
710 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
712 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
713 else if(CTF_DIFFTEAM(player, flag))
714 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
716 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
718 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
721 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); });
724 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
725 if(CTF_SAMETEAM(flag, it))
727 if(SAME_TEAM(player, it))
728 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
730 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);
734 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
737 GameRules_scoring_add(player, CTF_PICKUPS, 1);
738 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
743 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
744 ctf_EventLog("steal", flag.team, player);
750 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);
751 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);
752 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
753 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
754 ctf_EventLog("pickup", flag.team, player);
762 if(pickuptype == PICKUP_BASE)
764 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
765 if((player.speedrunning) && (ctf_captimerecord))
766 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
770 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
773 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
774 ctf_FlagcarrierWaypoints(player);
775 WaypointSprite_Ping(player.wps_flagcarrier);
779 // ===================
780 // Main Flag Functions
781 // ===================
783 void ctf_CheckFlagReturn(entity flag, int returntype)
785 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
787 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH)); }
789 if((GetResource(flag, RES_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
794 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
796 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
797 case RETURN_SPEEDRUN:
798 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
799 case RETURN_NEEDKILL:
800 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
803 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
805 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
806 ctf_EventLog("returned", flag.team, NULL);
808 ctf_RespawnFlag(flag);
813 bool ctf_Stalemate_Customize(entity this, entity client)
815 // make spectators see what the player would see
816 entity e = WaypointSprite_getviewentity(client);
817 entity wp_owner = this.owner;
820 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
821 if(SAME_TEAM(wp_owner, e)) { return false; }
822 if(!IS_PLAYER(e)) { return false; }
827 void ctf_CheckStalemate()
830 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
833 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
835 // build list of stale flags
836 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
838 if(autocvar_g_ctf_stalemate)
839 if(tmp_entity.ctf_status != FLAG_BASE)
840 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
842 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
843 ctf_staleflaglist = tmp_entity;
845 switch(tmp_entity.team)
847 case NUM_TEAM_1: ++stale_red_flags; break;
848 case NUM_TEAM_2: ++stale_blue_flags; break;
849 case NUM_TEAM_3: ++stale_yellow_flags; break;
850 case NUM_TEAM_4: ++stale_pink_flags; break;
851 default: ++stale_neutral_flags; break;
857 stale_flags = (stale_neutral_flags >= 1);
859 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
861 if(ctf_oneflag && stale_flags == 1)
862 ctf_stalemate = true;
863 else if(stale_flags >= 2)
864 ctf_stalemate = true;
865 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
866 { ctf_stalemate = false; wpforenemy_announced = false; }
867 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
868 { ctf_stalemate = false; wpforenemy_announced = false; }
870 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
873 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
875 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
877 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);
878 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
879 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
883 if (!wpforenemy_announced)
885 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)); });
887 wpforenemy_announced = true;
892 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
894 if(ITEM_DAMAGE_NEEDKILL(deathtype))
896 if(autocvar_g_ctf_flag_return_damage_delay)
897 this.ctf_flagdamaged_byworld = true;
900 SetResourceExplicit(this, RES_HEALTH, 0);
901 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
905 if(autocvar_g_ctf_flag_return_damage)
907 // reduce health and check if it should be returned
908 TakeResource(this, RES_HEALTH, damage);
909 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
914 void ctf_FlagThink(entity this)
919 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
922 if(this == ctf_worldflaglist) // only for the first flag
923 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
926 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
927 LOG_TRACE("wtf the flag got squashed?");
928 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
929 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
930 setsize(this, this.m_mins, this.m_maxs);
934 switch(this.ctf_status)
938 if(autocvar_g_ctf_dropped_capture_radius)
940 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
941 if(tmp_entity.ctf_status == FLAG_DROPPED)
942 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
943 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
944 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
951 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
953 if(autocvar_g_ctf_flag_dropped_floatinwater)
955 vector midpoint = ((this.absmin + this.absmax) * 0.5);
956 if(pointcontents(midpoint) == CONTENT_WATER)
958 this.velocity = this.velocity * 0.5;
960 if (pointcontents(midpoint + eZ * FLAG_FLOAT_OFFSET_Z) == CONTENT_WATER)
961 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
963 { set_movetype(this, MOVETYPE_FLY); }
965 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
967 if(autocvar_g_ctf_flag_return_dropped)
969 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
971 SetResourceExplicit(this, RES_HEALTH, 0);
972 ctf_CheckFlagReturn(this, RETURN_DROPPED);
976 if(this.ctf_flagdamaged_byworld)
978 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
979 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
982 else if(autocvar_g_ctf_flag_return_time)
984 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
985 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
993 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
995 SetResourceExplicit(this, RES_HEALTH, 0);
996 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
998 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
999 ImpulseCommands(this.owner);
1001 if(autocvar_g_ctf_stalemate)
1003 if(time >= wpforenemy_nextthink)
1005 ctf_CheckStalemate();
1006 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1009 if(CTF_SAMETEAM(this, this.owner) && this.team)
1011 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1012 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
1013 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
1014 ctf_Handle_Return(this, this.owner);
1021 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1022 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1023 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1025 if((this.pass_target == NULL)
1026 || (IS_DEAD(this.pass_target))
1027 || (this.pass_target.flagcarried)
1028 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1029 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1030 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1032 // give up, pass failed
1033 ctf_Handle_Drop(this, NULL, DROP_PASS);
1037 // still a viable target, go for it
1038 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1043 default: // this should never happen
1045 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1051 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1054 if(game_stopped) return;
1055 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1057 bool is_not_monster = (!IS_MONSTER(toucher));
1059 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1060 if(ITEM_TOUCH_NEEDKILL())
1062 if(!autocvar_g_ctf_flag_return_damage_delay)
1064 SetResourceExplicit(flag, RES_HEALTH, 0);
1065 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1067 if(!flag.ctf_flagdamaged_byworld) { return; }
1070 // special touch behaviors
1071 if(STAT(FROZEN, toucher)) { return; }
1072 else if(IS_VEHICLE(toucher))
1074 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1075 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1077 return; // do nothing
1079 else if(IS_MONSTER(toucher))
1081 if(!autocvar_g_ctf_allow_monster_touch)
1082 return; // do nothing
1084 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1086 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1088 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1089 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1090 flag.wait = time + FLAG_TOUCHRATE;
1094 else if(IS_DEAD(toucher)) { return; }
1096 switch(flag.ctf_status)
1102 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1103 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1104 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1105 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1107 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1108 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1109 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)
1111 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1112 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1114 else if(CTF_DIFFTEAM(toucher, flag) && (!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 enemies flag
1121 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1122 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1123 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1124 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1130 LOG_TRACE("Someone touched a flag even though it was being carried?");
1136 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1138 if(DIFF_TEAM(toucher, flag.pass_sender))
1140 if(ctf_Immediate_Return_Allowed(flag, toucher))
1141 ctf_Handle_Return(flag, toucher);
1142 else if(is_not_monster && (!toucher.flagcarried))
1143 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1145 else if(!toucher.flagcarried)
1146 ctf_Handle_Retrieve(flag, toucher);
1153 .float last_respawn;
1154 void ctf_RespawnFlag(entity flag)
1156 flag.watertype = CONTENT_EMPTY; // TODO: it is unclear why this workaround is needed, likely many other potential breakage points!!
1157 // check for flag respawn being called twice in a row
1158 if(flag.last_respawn > time - 0.5)
1159 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1161 flag.last_respawn = time;
1163 // reset the player (if there is one)
1164 if((flag.owner) && (flag.owner.flagcarried == flag))
1166 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1167 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1168 WaypointSprite_Kill(flag.wps_flagcarrier);
1170 flag.owner.flagcarried = NULL;
1171 GameRules_scoring_vip(flag.owner, false);
1173 if(flag.speedrunning)
1174 ctf_FakeTimeLimit(flag.owner, -1);
1177 if((flag.owner) && (flag.owner.vehicle))
1178 flag.scale = FLAG_SCALE;
1180 if(flag.ctf_status == FLAG_DROPPED)
1181 { WaypointSprite_Kill(flag.wps_flagdropped); }
1184 setattachment(flag, NULL, "");
1185 setorigin(flag, flag.ctf_spawnorigin);
1187 //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1188 set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1189 flag.takedamage = DAMAGE_NO;
1190 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1191 flag.solid = SOLID_TRIGGER;
1192 flag.velocity = '0 0 0';
1193 flag.angles = flag.mangle;
1194 flag.flags = FL_ITEM | FL_NOTARGET;
1196 flag.ctf_status = FLAG_BASE;
1198 flag.pass_distance = 0;
1199 flag.pass_sender = NULL;
1200 flag.pass_target = NULL;
1201 flag.ctf_dropper = NULL;
1202 flag.ctf_pickuptime = 0;
1203 flag.ctf_droptime = 0;
1204 flag.ctf_flagdamaged_byworld = false;
1205 navigation_dynamicgoal_unset(flag);
1207 ctf_CheckStalemate();
1210 void ctf_Reset(entity this)
1212 if(this.owner && IS_PLAYER(this.owner))
1213 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1216 ctf_RespawnFlag(this);
1219 bool ctf_FlagBase_Customize(entity this, entity client)
1221 entity e = WaypointSprite_getviewentity(client);
1222 entity wp_owner = this.owner;
1223 entity flag = e.flagcarried;
1224 if(flag && CTF_SAMETEAM(e, flag))
1226 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1231 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1234 waypoint_spawnforitem_force(this, this.origin);
1235 navigation_dynamicgoal_init(this, true);
1241 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1242 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1243 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1244 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1245 default: basename = WP_FlagBaseNeutral; break;
1248 if(autocvar_g_ctf_flag_waypoint)
1250 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1251 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1252 wp.fade_rate = autocvar_g_ctf_flag_waypoint_maxdistance;
1253 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1254 setcefc(wp, ctf_FlagBase_Customize);
1257 // captureshield setup
1258 ctf_CaptureShield_Spawn(this);
1263 void ctf_FlagSetup(int teamnum, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1266 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1267 ctf_worldflaglist = flag;
1269 setattachment(flag, NULL, "");
1271 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnum), Team_ColorName_Upper(teamnum)));
1272 flag.team = teamnum;
1273 flag.classname = "item_flag_team";
1274 flag.target = "###item###"; // for finding the nearest item using findnearest
1275 flag.flags = FL_ITEM | FL_NOTARGET;
1276 IL_PUSH(g_items, flag);
1277 flag.solid = SOLID_TRIGGER;
1278 flag.takedamage = DAMAGE_NO;
1279 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1280 flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1281 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1282 flag.event_damage = ctf_FlagDamage;
1283 flag.pushable = true;
1284 flag.teleportable = TELEPORT_NORMAL;
1285 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1286 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1287 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1288 if(flag.damagedbycontents)
1289 IL_PUSH(g_damagedbycontents, flag);
1290 flag.velocity = '0 0 0';
1291 flag.mangle = flag.angles;
1292 flag.reset = ctf_Reset;
1293 settouch(flag, ctf_FlagTouch);
1294 setthink(flag, ctf_FlagThink);
1295 flag.nextthink = time + FLAG_THINKRATE;
1296 flag.ctf_status = FLAG_BASE;
1298 // crudely force them all to 0
1299 if(autocvar_g_ctf_score_ignore_fields)
1300 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1302 string teamname = Static_Team_ColorName_Lower(teamnum);
1304 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1305 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1306 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1307 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnum).eent_eff_name; }
1308 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnum).eent_eff_name; }
1309 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnum).eent_eff_name; }
1313 if(flag.s == "") flag.s = b; \
1314 precache_sound(flag.s);
1316 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnum))))
1317 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnum))))
1318 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnum))))
1319 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnum))))
1320 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1321 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1322 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1326 precache_model(flag.model);
1329 _setmodel(flag, flag.model); // precision set below
1330 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1331 flag.m_mins = flag.mins; // store these for squash checks
1332 flag.m_maxs = flag.maxs;
1333 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1335 if(autocvar_g_ctf_flag_glowtrails)
1339 case NUM_TEAM_1: flag.glow_color = 251; break;
1340 case NUM_TEAM_2: flag.glow_color = 210; break;
1341 case NUM_TEAM_3: flag.glow_color = 110; break;
1342 case NUM_TEAM_4: flag.glow_color = 145; break;
1343 default: flag.glow_color = 254; break;
1345 flag.glow_size = 25;
1346 flag.glow_trail = 1;
1349 flag.effects |= EF_LOWPRECISION;
1350 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1351 if(autocvar_g_ctf_dynamiclights)
1355 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1356 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1357 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1358 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1359 default: flag.effects |= EF_DIMLIGHT; break;
1364 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1366 flag.dropped_origin = flag.origin;
1367 flag.noalign = true;
1368 set_movetype(flag, MOVETYPE_NONE);
1370 else // drop to floor, automatically find a platform and set that as spawn origin
1372 flag.noalign = false;
1374 set_movetype(flag, MOVETYPE_NONE);
1377 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1385 // NOTE: LEGACY CODE, needs to be re-written!
1387 void havocbot_ctf_calculate_middlepoint()
1391 vector fo = '0 0 0';
1394 f = ctf_worldflaglist;
1399 f = f.ctf_worldflagnext;
1405 havocbot_middlepoint = s / n;
1406 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1408 havocbot_symmetry_axis_m = 0;
1409 havocbot_symmetry_axis_q = 0;
1412 // for symmetrical editing of waypoints
1413 entity f1 = ctf_worldflaglist;
1414 entity f2 = f1.ctf_worldflagnext;
1415 float m = -(f1.origin.y - f2.origin.y) / (max(f1.origin.x - f2.origin.x, FLOAT_EPSILON));
1416 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1417 havocbot_symmetry_axis_m = m;
1418 havocbot_symmetry_axis_q = q;
1420 havocbot_symmetry_origin_order = n;
1424 entity havocbot_ctf_find_flag(entity bot)
1427 f = ctf_worldflaglist;
1430 if (CTF_SAMETEAM(bot, f))
1432 f = f.ctf_worldflagnext;
1437 entity havocbot_ctf_find_enemy_flag(entity bot)
1440 f = ctf_worldflaglist;
1445 if(CTF_DIFFTEAM(bot, f))
1452 else if(!bot.flagcarried)
1456 else if (CTF_DIFFTEAM(bot, f))
1458 f = f.ctf_worldflagnext;
1463 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1470 FOREACH_CLIENT(IS_PLAYER(it), {
1471 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1474 if(vdist(it.origin - org, <, tc_radius))
1483 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1486 head = ctf_worldflaglist;
1489 if (CTF_SAMETEAM(this, head))
1491 head = head.ctf_worldflagnext;
1494 navigation_routerating(this, head, ratingscale, 10000);
1498 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1501 head = ctf_worldflaglist;
1504 if (CTF_SAMETEAM(this, head))
1506 if (this.flagcarried)
1507 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1509 head = head.ctf_worldflagnext; // skip base if it has a different group
1514 head = head.ctf_worldflagnext;
1519 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1522 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1525 head = ctf_worldflaglist;
1530 if(CTF_DIFFTEAM(this, head))
1534 if(this.flagcarried)
1537 else if(!this.flagcarried)
1541 else if(CTF_DIFFTEAM(this, head))
1543 head = head.ctf_worldflagnext;
1547 if (head.ctf_status == FLAG_CARRY)
1549 // adjust rating of our flag carrier depending on his health
1550 head = head.tag_entity;
1551 float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1552 ratingscale += ratingscale * f * 0.1;
1554 navigation_routerating(this, head, ratingscale, 10000);
1558 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1560 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1562 if (!bot_waypoints_for_items)
1564 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1570 head = havocbot_ctf_find_enemy_flag(this);
1575 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1578 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1582 mf = havocbot_ctf_find_flag(this);
1584 if(mf.ctf_status == FLAG_BASE)
1588 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1591 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1594 head = ctf_worldflaglist;
1597 // flag is out in the field
1598 if(head.ctf_status != FLAG_BASE)
1599 if(head.tag_entity==NULL) // dropped
1603 if(vdist(org - head.origin, <, df_radius))
1604 navigation_routerating(this, head, ratingscale, 10000);
1607 navigation_routerating(this, head, ratingscale, 10000);
1610 head = head.ctf_worldflagnext;
1614 void havocbot_ctf_reset_role(entity this)
1616 float cdefense, cmiddle, coffense;
1623 if (this.flagcarried)
1625 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1629 mf = havocbot_ctf_find_flag(this);
1630 ef = havocbot_ctf_find_enemy_flag(this);
1632 // Retrieve stolen flag
1633 if(mf.ctf_status!=FLAG_BASE)
1635 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1639 // If enemy flag is taken go to the middle to intercept pursuers
1640 if(ef.ctf_status!=FLAG_BASE)
1642 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1646 // if there is no one else on the team switch to offense
1648 // don't check if this bot is a player since it isn't true when the bot is added to the server
1649 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1653 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1656 else if (time < CS(this).jointime + 1)
1658 // if bots spawn all at once set good default roles
1661 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1664 else if (count == 2)
1666 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1671 // Evaluate best position to take
1672 // Count mates on middle position
1673 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1675 // Count mates on defense position
1676 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1678 // Count mates on offense position
1679 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1681 if(cdefense<=coffense)
1682 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1683 else if(coffense<=cmiddle)
1684 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1686 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1688 // if bots spawn all at once assign them a more appropriated role after a while
1689 if (time < CS(this).jointime + 1 && count > 2)
1690 this.havocbot_role_timeout = time + 10 + random() * 10;
1693 bool havocbot_ctf_is_basewaypoint(entity item)
1695 if (item.classname != "waypoint")
1698 entity head = ctf_worldflaglist;
1701 if (item == head.bot_basewaypoint)
1703 head = head.ctf_worldflagnext;
1708 void havocbot_role_ctf_carrier(entity this)
1712 havocbot_ctf_reset_role(this);
1716 if (this.flagcarried == NULL)
1718 havocbot_ctf_reset_role(this);
1722 if (navigation_goalrating_timeout(this))
1724 navigation_goalrating_start(this);
1727 entity mf = havocbot_ctf_find_flag(this);
1728 vector base_org = mf.dropped_origin;
1729 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1731 havocbot_goalrating_ctf_enemybase(this, base_rating);
1733 havocbot_goalrating_ctf_ourbase(this, base_rating);
1735 // start collecting items very close to the bot but only inside of own base radius
1736 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1737 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1739 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1741 navigation_goalrating_end(this);
1743 navigation_goalrating_timeout_set(this);
1745 entity goal = this.goalentity;
1746 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1747 this.goalentity_lock_timeout = time + ((this.bot_aimtarg) ? 2 : 3);
1750 this.havocbot_cantfindflag = time + 10;
1751 else if (time > this.havocbot_cantfindflag)
1753 // Can't navigate to my own base, suicide!
1754 // TODO: drop it and wander around
1755 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1761 void havocbot_role_ctf_escort(entity this)
1767 havocbot_ctf_reset_role(this);
1771 if (this.flagcarried)
1773 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1777 // If enemy flag is back on the base switch to previous role
1778 ef = havocbot_ctf_find_enemy_flag(this);
1779 if(ef.ctf_status==FLAG_BASE)
1781 this.havocbot_role = this.havocbot_previous_role;
1782 this.havocbot_role_timeout = 0;
1785 if (ef.ctf_status == FLAG_DROPPED)
1787 navigation_goalrating_timeout_expire(this, 1);
1791 // If the flag carrier reached the base switch to defense
1792 mf = havocbot_ctf_find_flag(this);
1793 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1795 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1799 // Set the role timeout if necessary
1800 if (!this.havocbot_role_timeout)
1802 this.havocbot_role_timeout = time + random() * 30 + 60;
1805 // If nothing happened just switch to previous role
1806 if (time > this.havocbot_role_timeout)
1808 this.havocbot_role = this.havocbot_previous_role;
1809 this.havocbot_role_timeout = 0;
1813 // Chase the flag carrier
1814 if (navigation_goalrating_timeout(this))
1816 navigation_goalrating_start(this);
1819 havocbot_goalrating_ctf_enemyflag(this, 10000);
1820 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1821 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1823 navigation_goalrating_end(this);
1825 navigation_goalrating_timeout_set(this);
1829 void havocbot_role_ctf_offense(entity this)
1836 havocbot_ctf_reset_role(this);
1840 if (this.flagcarried)
1842 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1847 mf = havocbot_ctf_find_flag(this);
1848 ef = havocbot_ctf_find_enemy_flag(this);
1851 if(mf.ctf_status!=FLAG_BASE)
1854 pos = mf.tag_entity.origin;
1858 // Try to get it if closer than the enemy base
1859 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1861 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1866 // Escort flag carrier
1867 if(ef.ctf_status!=FLAG_BASE)
1870 pos = ef.tag_entity.origin;
1874 if(vdist(pos - mf.dropped_origin, >, 700))
1876 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1881 // Set the role timeout if necessary
1882 if (!this.havocbot_role_timeout)
1883 this.havocbot_role_timeout = time + 120;
1885 if (time > this.havocbot_role_timeout)
1887 havocbot_ctf_reset_role(this);
1891 if (navigation_goalrating_timeout(this))
1893 navigation_goalrating_start(this);
1896 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1897 havocbot_goalrating_ctf_enemybase(this, 10000);
1898 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1900 navigation_goalrating_end(this);
1902 navigation_goalrating_timeout_set(this);
1906 // Retriever (temporary role):
1907 void havocbot_role_ctf_retriever(entity this)
1913 havocbot_ctf_reset_role(this);
1917 if (this.flagcarried)
1919 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1923 // If flag is back on the base switch to previous role
1924 mf = havocbot_ctf_find_flag(this);
1925 if(mf.ctf_status==FLAG_BASE)
1927 if (mf.enemy == this) // did this bot return the flag?
1928 navigation_goalrating_timeout_force(this);
1929 havocbot_ctf_reset_role(this);
1933 if (!this.havocbot_role_timeout)
1934 this.havocbot_role_timeout = time + 20;
1936 if (time > this.havocbot_role_timeout)
1938 havocbot_ctf_reset_role(this);
1942 if (navigation_goalrating_timeout(this))
1944 const float RT_RADIUS = 10000;
1946 navigation_goalrating_start(this);
1949 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1950 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1951 havocbot_goalrating_ctf_enemybase(this, 8000);
1952 entity ef = havocbot_ctf_find_enemy_flag(this);
1953 vector enemy_base_org = ef.dropped_origin;
1954 // start collecting items very close to the bot but only inside of enemy base radius
1955 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1956 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1957 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1959 navigation_goalrating_end(this);
1961 navigation_goalrating_timeout_set(this);
1965 void havocbot_role_ctf_middle(entity this)
1971 havocbot_ctf_reset_role(this);
1975 if (this.flagcarried)
1977 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1981 mf = havocbot_ctf_find_flag(this);
1982 if(mf.ctf_status!=FLAG_BASE)
1984 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1988 if (!this.havocbot_role_timeout)
1989 this.havocbot_role_timeout = time + 10;
1991 if (time > this.havocbot_role_timeout)
1993 havocbot_ctf_reset_role(this);
1997 if (navigation_goalrating_timeout(this))
2001 org = havocbot_middlepoint;
2002 org.z = this.origin.z;
2004 navigation_goalrating_start(this);
2007 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
2008 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
2009 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2010 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2011 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2012 havocbot_goalrating_ctf_enemybase(this, 3000);
2014 navigation_goalrating_end(this);
2016 entity goal = this.goalentity;
2017 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
2018 this.goalentity_lock_timeout = time + 2;
2020 navigation_goalrating_timeout_set(this);
2024 void havocbot_role_ctf_defense(entity this)
2030 havocbot_ctf_reset_role(this);
2034 if (this.flagcarried)
2036 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2040 // If own flag was captured
2041 mf = havocbot_ctf_find_flag(this);
2042 if(mf.ctf_status!=FLAG_BASE)
2044 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2048 if (!this.havocbot_role_timeout)
2049 this.havocbot_role_timeout = time + 30;
2051 if (time > this.havocbot_role_timeout)
2053 havocbot_ctf_reset_role(this);
2056 if (navigation_goalrating_timeout(this))
2058 vector org = mf.dropped_origin;
2060 navigation_goalrating_start(this);
2062 // if enemies are closer to our base, go there
2063 entity closestplayer = NULL;
2064 float distance, bestdistance = 10000;
2065 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2066 distance = vlen(org - it.origin);
2067 if(distance<bestdistance)
2070 bestdistance = distance;
2076 if(DIFF_TEAM(closestplayer, this))
2077 if(vdist(org - this.origin, >, 1000))
2078 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2079 havocbot_goalrating_ctf_ourbase(this, 10000);
2081 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2082 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2083 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2084 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2085 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2087 navigation_goalrating_end(this);
2089 navigation_goalrating_timeout_set(this);
2093 void havocbot_role_ctf_setrole(entity bot, int role)
2095 string s = "(null)";
2098 case HAVOCBOT_CTF_ROLE_CARRIER:
2100 bot.havocbot_role = havocbot_role_ctf_carrier;
2101 bot.havocbot_role_timeout = 0;
2102 bot.havocbot_cantfindflag = time + 10;
2103 if (bot.havocbot_previous_role != bot.havocbot_role)
2104 navigation_goalrating_timeout_force(bot);
2106 case HAVOCBOT_CTF_ROLE_DEFENSE:
2108 bot.havocbot_role = havocbot_role_ctf_defense;
2109 bot.havocbot_role_timeout = 0;
2111 case HAVOCBOT_CTF_ROLE_MIDDLE:
2113 bot.havocbot_role = havocbot_role_ctf_middle;
2114 bot.havocbot_role_timeout = 0;
2116 case HAVOCBOT_CTF_ROLE_OFFENSE:
2118 bot.havocbot_role = havocbot_role_ctf_offense;
2119 bot.havocbot_role_timeout = 0;
2121 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2123 bot.havocbot_previous_role = bot.havocbot_role;
2124 bot.havocbot_role = havocbot_role_ctf_retriever;
2125 bot.havocbot_role_timeout = time + 10;
2126 if (bot.havocbot_previous_role != bot.havocbot_role)
2127 navigation_goalrating_timeout_expire(bot, 2);
2129 case HAVOCBOT_CTF_ROLE_ESCORT:
2131 bot.havocbot_previous_role = bot.havocbot_role;
2132 bot.havocbot_role = havocbot_role_ctf_escort;
2133 bot.havocbot_role_timeout = time + 30;
2134 if (bot.havocbot_previous_role != bot.havocbot_role)
2135 navigation_goalrating_timeout_expire(bot, 2);
2138 LOG_TRACE(bot.netname, " switched to ", s);
2146 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2148 entity player = M_ARGV(0, entity);
2150 int t = 0, t2 = 0, t3 = 0;
2151 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)
2153 // initially clear items so they can be set as necessary later.
2154 STAT(CTF_FLAGSTATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2155 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2156 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2157 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2158 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2159 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2161 // scan through all the flags and notify the client about them
2162 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2164 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2165 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2166 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2167 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2168 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; }
2170 switch(flag.ctf_status)
2175 if((flag.owner == player) || (flag.pass_sender == player))
2176 STAT(CTF_FLAGSTATUS, player) |= t; // carrying: player is currently carrying the flag
2178 STAT(CTF_FLAGSTATUS, player) |= t2; // taken: someone else is carrying the flag
2183 STAT(CTF_FLAGSTATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2189 // item for stopping players from capturing the flag too often
2190 if(player.ctf_captureshielded)
2191 STAT(CTF_FLAGSTATUS, player) |= CTF_SHIELDED;
2194 STAT(CTF_FLAGSTATUS, player) |= CTF_STALEMATE;
2196 // update the health of the flag carrier waypointsprite
2197 if(player.wps_flagcarrier)
2198 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);
2201 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2203 entity frag_attacker = M_ARGV(1, entity);
2204 entity frag_target = M_ARGV(2, entity);
2205 float frag_damage = M_ARGV(4, float);
2206 vector frag_force = M_ARGV(6, vector);
2208 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2210 if(frag_target == frag_attacker) // damage done to yourself
2212 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2213 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2215 else // damage done to everyone else
2217 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2218 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2221 M_ARGV(4, float) = frag_damage;
2222 M_ARGV(6, vector) = frag_force;
2224 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2226 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
2227 && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2229 frag_target.wps_helpme_time = time;
2230 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2232 // todo: add notification for when flag carrier needs help?
2236 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2238 entity frag_attacker = M_ARGV(1, entity);
2239 entity frag_target = M_ARGV(2, entity);
2241 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2243 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2244 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2247 if(frag_target.flagcarried)
2249 entity tmp_entity = frag_target.flagcarried;
2250 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2251 tmp_entity.ctf_dropper = NULL;
2255 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2257 M_ARGV(2, float) = 0; // frag score
2258 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2261 void ctf_RemovePlayer(entity player)
2263 if(player.flagcarried)
2264 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2266 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2268 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2269 if(flag.pass_target == player) { flag.pass_target = NULL; }
2270 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2274 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2276 entity player = M_ARGV(0, entity);
2278 ctf_RemovePlayer(player);
2281 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2283 entity player = M_ARGV(0, entity);
2285 ctf_RemovePlayer(player);
2288 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2290 if(!autocvar_g_ctf_leaderboard)
2293 entity player = M_ARGV(0, entity);
2295 if(IS_REAL_CLIENT(player))
2297 int m = min(RANKINGS_CNT, autocvar_g_cts_send_rankings_cnt);
2298 race_send_rankings_cnt(MSG_ONE);
2299 for (int i = 1; i <= m; ++i)
2301 race_SendRankings(i, 0, 0, MSG_ONE);
2306 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2308 if(!autocvar_g_ctf_leaderboard)
2311 entity player = M_ARGV(0, entity);
2313 if(CS(player).cvar_cl_allow_uidtracking == 1 && CS(player).cvar_cl_allow_uid2name == 1)
2315 if (!player.stored_netname)
2316 player.stored_netname = strzone(uid2name(player.crypto_idfp));
2317 if(player.stored_netname != player.netname)
2319 db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
2320 strcpy(player.stored_netname, player.netname);
2325 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2327 entity player = M_ARGV(0, entity);
2329 if(player.flagcarried)
2330 if(!autocvar_g_ctf_portalteleport)
2331 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2334 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2336 if(MUTATOR_RETURNVALUE || game_stopped) return;
2338 entity player = M_ARGV(0, entity);
2340 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2342 // pass the flag to a team mate
2343 if(autocvar_g_ctf_pass)
2345 entity head, closest_target = NULL;
2346 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2348 while(head) // find the closest acceptable target to pass to
2350 if(IS_PLAYER(head) && !IS_DEAD(head))
2351 if(head != player && SAME_TEAM(head, player))
2352 if(!head.speedrunning && !head.vehicle)
2354 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2355 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2356 vector passer_center = CENTER_OR_VIEWOFS(player);
2358 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2360 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2362 if(IS_BOT_CLIENT(head))
2364 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2365 ctf_Handle_Throw(head, player, DROP_PASS);
2369 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2370 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2372 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2375 else if(player.flagcarried && !head.flagcarried)
2379 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2380 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2381 { closest_target = head; }
2383 else { closest_target = head; }
2390 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2393 // throw the flag in front of you
2394 if(autocvar_g_ctf_throw && player.flagcarried)
2396 if(player.throw_count == -1)
2398 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2400 player.throw_prevtime = time;
2401 player.throw_count = 1;
2402 ctf_Handle_Throw(player, NULL, DROP_THROW);
2407 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2413 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2414 else { player.throw_count += 1; }
2415 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2417 player.throw_prevtime = time;
2418 ctf_Handle_Throw(player, NULL, DROP_THROW);
2425 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2427 entity player = M_ARGV(0, entity);
2429 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2431 player.wps_helpme_time = time;
2432 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2434 else // create a normal help me waypointsprite
2436 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2437 WaypointSprite_Ping(player.wps_helpme);
2443 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2445 entity player = M_ARGV(0, entity);
2446 entity veh = M_ARGV(1, entity);
2448 if(player.flagcarried)
2450 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2452 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2456 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2457 setattachment(player.flagcarried, veh, "");
2458 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2459 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2460 //player.flagcarried.angles = '0 0 0';
2466 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2468 entity player = M_ARGV(0, entity);
2470 if(player.flagcarried)
2472 setattachment(player.flagcarried, player, "");
2473 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2474 player.flagcarried.scale = FLAG_SCALE;
2475 player.flagcarried.angles = '0 0 0';
2476 player.flagcarried.nodrawtoclient = NULL;
2481 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2483 entity player = M_ARGV(0, entity);
2485 if(player.flagcarried)
2487 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2488 ctf_RespawnFlag(player.flagcarried);
2493 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2495 entity flag; // temporary entity for the search method
2497 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2499 switch(flag.ctf_status)
2504 // lock the flag, game is over
2505 set_movetype(flag, MOVETYPE_NONE);
2506 flag.takedamage = DAMAGE_NO;
2507 flag.solid = SOLID_NOT;
2508 flag.nextthink = false; // stop thinking
2510 //dprint("stopping the ", flag.netname, " from moving.\n");
2518 // do nothing for these flags
2525 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2527 entity bot = M_ARGV(0, entity);
2529 havocbot_ctf_reset_role(bot);
2533 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2535 M_ARGV(1, string) = "ctf_team";
2538 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2540 entity spectatee = M_ARGV(0, entity);
2541 entity client = M_ARGV(1, entity);
2543 STAT(CTF_FLAGSTATUS, client) = STAT(CTF_FLAGSTATUS, spectatee);
2546 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2548 int record_page = M_ARGV(0, int);
2549 string ret_string = M_ARGV(1, string);
2551 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2553 if (MapInfo_Get_ByID(i))
2555 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2561 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2562 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2566 M_ARGV(1, string) = ret_string;
2569 bool superspec_Spectate(entity this, entity targ); // TODO
2570 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2571 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2573 entity player = M_ARGV(0, entity);
2574 string cmd_name = M_ARGV(1, string);
2575 int cmd_argc = M_ARGV(2, int);
2577 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2579 if(cmd_name == "followfc")
2591 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2592 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2593 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2594 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2598 FOREACH_CLIENT(IS_PLAYER(it), {
2599 if(it.flagcarried && (it.team == _team || _team == 0))
2602 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2603 continue; // already spectating this fc, try another
2604 return superspec_Spectate(player, it);
2609 superspec_msg("", "", player, "No active flag carrier\n", 1);
2614 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2616 entity frag_target = M_ARGV(0, entity);
2618 if(frag_target.flagcarried)
2619 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2622 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2624 entity player = M_ARGV(0, entity);
2625 if(player.flagcarried)
2626 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2634 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2635 CTF flag for team one (Red).
2637 "angle" Angle the flag will point (minus 90 degrees)...
2638 "model" model to use, note this needs red and blue as skins 0 and 1...
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_team1)
2647 if(!g_ctf) { delete(this); return; }
2649 ctf_FlagSetup(NUM_TEAM_1, this);
2652 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2653 CTF flag for team two (Blue).
2655 "angle" Angle the flag will point (minus 90 degrees)...
2656 "model" model to use, note this needs red and blue as skins 0 and 1...
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_team2)
2665 if(!g_ctf) { delete(this); return; }
2667 ctf_FlagSetup(NUM_TEAM_2, this);
2670 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2671 CTF flag for team three (Yellow).
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_team3)
2683 if(!g_ctf) { delete(this); return; }
2685 ctf_FlagSetup(NUM_TEAM_3, this);
2688 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2689 CTF flag for team four (Pink).
2691 "angle" Angle the flag will point (minus 90 degrees)...
2692 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2693 "noise" sound played when flag is picked up...
2694 "noise1" sound played when flag is returned by a teammate...
2695 "noise2" sound played when flag is captured...
2696 "noise3" sound played when flag is lost in the field and respawns itself...
2697 "noise4" sound played when flag is dropped by a player...
2698 "noise5" sound played when flag touches the ground... */
2699 spawnfunc(item_flag_team4)
2701 if(!g_ctf) { delete(this); return; }
2703 ctf_FlagSetup(NUM_TEAM_4, this);
2706 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2709 "angle" Angle the flag will point (minus 90 degrees)...
2710 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2711 "noise" sound played when flag is picked up...
2712 "noise1" sound played when flag is returned by a teammate...
2713 "noise2" sound played when flag is captured...
2714 "noise3" sound played when flag is lost in the field and respawns itself...
2715 "noise4" sound played when flag is dropped by a player...
2716 "noise5" sound played when flag touches the ground... */
2717 spawnfunc(item_flag_neutral)
2719 if(!g_ctf) { delete(this); return; }
2720 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2722 ctf_FlagSetup(0, this);
2725 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2726 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2727 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.
2729 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2730 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2733 if(!g_ctf) { delete(this); return; }
2735 this.classname = "ctf_team";
2736 this.team = this.cnt + 1;
2739 // compatibility for quake maps
2740 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2741 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2742 spawnfunc(info_player_team1);
2743 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2744 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2745 spawnfunc(info_player_team2);
2746 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2747 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2749 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2750 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2752 // compatibility for wop maps
2753 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2754 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2755 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2756 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2757 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2758 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2766 void ctf_ScoreRules(int teams)
2768 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2769 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2770 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2771 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2772 field(SP_CTF_PICKUPS, "pickups", 0);
2773 field(SP_CTF_FCKILLS, "fckills", 0);
2774 field(SP_CTF_RETURNS, "returns", 0);
2775 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2779 // code from here on is just to support maps that don't have flag and team entities
2780 void ctf_SpawnTeam (string teamname, int teamcolor)
2782 entity this = new_pure(ctf_team);
2783 this.netname = teamname;
2784 this.cnt = teamcolor - 1;
2785 this.spawnfunc_checked = true;
2786 this.team = teamcolor;
2789 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2794 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2796 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2797 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2799 switch(tmp_entity.team)
2801 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2802 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2803 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2804 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2806 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2809 havocbot_ctf_calculate_middlepoint();
2811 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2813 ctf_teams = 0; // so set the default red and blue teams
2814 BITSET_ASSIGN(ctf_teams, BIT(0));
2815 BITSET_ASSIGN(ctf_teams, BIT(1));
2818 //ctf_teams = bound(2, ctf_teams, 4);
2820 // if no teams are found, spawn defaults
2821 if(find(NULL, classname, "ctf_team") == NULL)
2823 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2824 if(ctf_teams & BIT(0))
2825 ctf_SpawnTeam("Red", NUM_TEAM_1);
2826 if(ctf_teams & BIT(1))
2827 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2828 if(ctf_teams & BIT(2))
2829 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2830 if(ctf_teams & BIT(3))
2831 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2834 ctf_ScoreRules(ctf_teams);
2837 void ctf_Initialize()
2839 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2841 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2842 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2843 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2845 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);