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, 2 * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
148 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);
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 SetResourceExplicit(flag, RES_HEALTH, flag.max_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_health);
367 WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_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: SetResourceExplicit(flag, RES_HEALTH, flag.max_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))
706 if(SAME_TEAM(player, it))
707 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
709 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);
713 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
716 GameRules_scoring_add(player, CTF_PICKUPS, 1);
717 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
722 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
723 ctf_EventLog("steal", flag.team, player);
729 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);
730 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);
731 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
732 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
733 ctf_EventLog("pickup", flag.team, player);
741 if(pickuptype == PICKUP_BASE)
743 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
744 if((player.speedrunning) && (ctf_captimerecord))
745 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
749 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
752 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
753 ctf_FlagcarrierWaypoints(player);
754 WaypointSprite_Ping(player.wps_flagcarrier);
758 // ===================
759 // Main Flag Functions
760 // ===================
762 void ctf_CheckFlagReturn(entity flag, int returntype)
764 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
766 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH)); }
768 if((GetResource(flag, RES_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
773 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
775 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
776 case RETURN_SPEEDRUN:
777 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
778 case RETURN_NEEDKILL:
779 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
782 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
784 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
785 ctf_EventLog("returned", flag.team, NULL);
787 ctf_RespawnFlag(flag);
792 bool ctf_Stalemate_Customize(entity this, entity client)
794 // make spectators see what the player would see
795 entity e = WaypointSprite_getviewentity(client);
796 entity wp_owner = this.owner;
799 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
800 if(SAME_TEAM(wp_owner, e)) { return false; }
801 if(!IS_PLAYER(e)) { return false; }
806 void ctf_CheckStalemate()
809 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
812 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
814 // build list of stale flags
815 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
817 if(autocvar_g_ctf_stalemate)
818 if(tmp_entity.ctf_status != FLAG_BASE)
819 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
821 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
822 ctf_staleflaglist = tmp_entity;
824 switch(tmp_entity.team)
826 case NUM_TEAM_1: ++stale_red_flags; break;
827 case NUM_TEAM_2: ++stale_blue_flags; break;
828 case NUM_TEAM_3: ++stale_yellow_flags; break;
829 case NUM_TEAM_4: ++stale_pink_flags; break;
830 default: ++stale_neutral_flags; break;
836 stale_flags = (stale_neutral_flags >= 1);
838 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
840 if(ctf_oneflag && stale_flags == 1)
841 ctf_stalemate = true;
842 else if(stale_flags >= 2)
843 ctf_stalemate = true;
844 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
845 { ctf_stalemate = false; wpforenemy_announced = false; }
846 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
847 { ctf_stalemate = false; wpforenemy_announced = false; }
849 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
852 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
854 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
856 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);
857 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
858 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
862 if (!wpforenemy_announced)
864 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)); });
866 wpforenemy_announced = true;
871 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
873 if(ITEM_DAMAGE_NEEDKILL(deathtype))
875 if(autocvar_g_ctf_flag_return_damage_delay)
876 this.ctf_flagdamaged_byworld = true;
879 SetResourceExplicit(this, RES_HEALTH, 0);
880 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
884 if(autocvar_g_ctf_flag_return_damage)
886 // reduce health and check if it should be returned
887 TakeResource(this, RES_HEALTH, damage);
888 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
893 void ctf_FlagThink(entity this)
898 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
901 if(this == ctf_worldflaglist) // only for the first flag
902 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
905 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
906 LOG_TRACE("wtf the flag got squashed?");
907 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
908 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
909 setsize(this, this.m_mins, this.m_maxs);
913 switch(this.ctf_status)
917 if(autocvar_g_ctf_dropped_capture_radius)
919 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
920 if(tmp_entity.ctf_status == FLAG_DROPPED)
921 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
922 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
923 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
930 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
932 if(autocvar_g_ctf_flag_dropped_floatinwater)
934 vector midpoint = ((this.absmin + this.absmax) * 0.5);
935 if(pointcontents(midpoint) == CONTENT_WATER)
937 this.velocity = this.velocity * 0.5;
939 if (pointcontents(midpoint + eZ * FLAG_FLOAT_OFFSET_Z) == CONTENT_WATER)
940 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
942 { set_movetype(this, MOVETYPE_FLY); }
944 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
946 if(autocvar_g_ctf_flag_return_dropped)
948 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
950 SetResourceExplicit(this, RES_HEALTH, 0);
951 ctf_CheckFlagReturn(this, RETURN_DROPPED);
955 if(this.ctf_flagdamaged_byworld)
957 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
958 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
961 else if(autocvar_g_ctf_flag_return_time)
963 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
964 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
972 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
974 SetResourceExplicit(this, RES_HEALTH, 0);
975 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
977 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
978 ImpulseCommands(this.owner);
980 if(autocvar_g_ctf_stalemate)
982 if(time >= wpforenemy_nextthink)
984 ctf_CheckStalemate();
985 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
988 if(CTF_SAMETEAM(this, this.owner) && this.team)
990 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
991 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
992 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
993 ctf_Handle_Return(this, this.owner);
1000 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1001 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1002 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1004 if((this.pass_target == NULL)
1005 || (IS_DEAD(this.pass_target))
1006 || (this.pass_target.flagcarried)
1007 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1008 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1009 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1011 // give up, pass failed
1012 ctf_Handle_Drop(this, NULL, DROP_PASS);
1016 // still a viable target, go for it
1017 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1022 default: // this should never happen
1024 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1030 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1033 if(game_stopped) return;
1034 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1036 bool is_not_monster = (!IS_MONSTER(toucher));
1038 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1039 if(ITEM_TOUCH_NEEDKILL())
1041 if(!autocvar_g_ctf_flag_return_damage_delay)
1043 SetResourceExplicit(flag, RES_HEALTH, 0);
1044 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1046 if(!flag.ctf_flagdamaged_byworld) { return; }
1049 // special touch behaviors
1050 if(STAT(FROZEN, toucher)) { return; }
1051 else if(IS_VEHICLE(toucher))
1053 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1054 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1056 return; // do nothing
1058 else if(IS_MONSTER(toucher))
1060 if(!autocvar_g_ctf_allow_monster_touch)
1061 return; // do nothing
1063 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1065 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1067 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1068 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1069 flag.wait = time + FLAG_TOUCHRATE;
1073 else if(IS_DEAD(toucher)) { return; }
1075 switch(flag.ctf_status)
1081 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1082 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1083 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1084 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1086 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1087 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1088 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)
1090 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1091 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1093 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1094 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1100 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1101 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1102 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1103 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1109 LOG_TRACE("Someone touched a flag even though it was being carried?");
1115 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1117 if(DIFF_TEAM(toucher, flag.pass_sender))
1119 if(ctf_Immediate_Return_Allowed(flag, toucher))
1120 ctf_Handle_Return(flag, toucher);
1121 else if(is_not_monster && (!toucher.flagcarried))
1122 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1124 else if(!toucher.flagcarried)
1125 ctf_Handle_Retrieve(flag, toucher);
1132 .float last_respawn;
1133 void ctf_RespawnFlag(entity flag)
1135 // check for flag respawn being called twice in a row
1136 if(flag.last_respawn > time - 0.5)
1137 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1139 flag.last_respawn = time;
1141 // reset the player (if there is one)
1142 if((flag.owner) && (flag.owner.flagcarried == flag))
1144 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1145 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1146 WaypointSprite_Kill(flag.wps_flagcarrier);
1148 flag.owner.flagcarried = NULL;
1149 GameRules_scoring_vip(flag.owner, false);
1151 if(flag.speedrunning)
1152 ctf_FakeTimeLimit(flag.owner, -1);
1155 if((flag.owner) && (flag.owner.vehicle))
1156 flag.scale = FLAG_SCALE;
1158 if(flag.ctf_status == FLAG_DROPPED)
1159 { WaypointSprite_Kill(flag.wps_flagdropped); }
1162 setattachment(flag, NULL, "");
1163 setorigin(flag, flag.ctf_spawnorigin);
1165 //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1166 set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1167 flag.takedamage = DAMAGE_NO;
1168 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1169 flag.solid = SOLID_TRIGGER;
1170 flag.velocity = '0 0 0';
1171 flag.angles = flag.mangle;
1172 flag.flags = FL_ITEM | FL_NOTARGET;
1174 flag.ctf_status = FLAG_BASE;
1176 flag.pass_distance = 0;
1177 flag.pass_sender = NULL;
1178 flag.pass_target = NULL;
1179 flag.ctf_dropper = NULL;
1180 flag.ctf_pickuptime = 0;
1181 flag.ctf_droptime = 0;
1182 flag.ctf_flagdamaged_byworld = false;
1183 navigation_dynamicgoal_unset(flag);
1185 ctf_CheckStalemate();
1188 void ctf_Reset(entity this)
1190 if(this.owner && IS_PLAYER(this.owner))
1191 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1194 ctf_RespawnFlag(this);
1197 bool ctf_FlagBase_Customize(entity this, entity client)
1199 entity e = WaypointSprite_getviewentity(client);
1200 entity wp_owner = this.owner;
1201 entity flag = e.flagcarried;
1202 if(flag && CTF_SAMETEAM(e, flag))
1204 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1209 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1212 waypoint_spawnforitem_force(this, this.origin);
1213 navigation_dynamicgoal_init(this, true);
1219 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1220 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1221 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1222 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1223 default: basename = WP_FlagBaseNeutral; break;
1226 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1227 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1228 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1229 setcefc(wp, ctf_FlagBase_Customize);
1231 // captureshield setup
1232 ctf_CaptureShield_Spawn(this);
1237 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1240 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1241 ctf_worldflaglist = flag;
1243 setattachment(flag, NULL, "");
1245 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1246 flag.team = teamnumber;
1247 flag.classname = "item_flag_team";
1248 flag.target = "###item###"; // for finding the nearest item using findnearest
1249 flag.flags = FL_ITEM | FL_NOTARGET;
1250 IL_PUSH(g_items, flag);
1251 flag.solid = SOLID_TRIGGER;
1252 flag.takedamage = DAMAGE_NO;
1253 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1254 flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1255 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1256 flag.event_damage = ctf_FlagDamage;
1257 flag.pushable = true;
1258 flag.teleportable = TELEPORT_NORMAL;
1259 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1260 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1261 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1262 if(flag.damagedbycontents)
1263 IL_PUSH(g_damagedbycontents, flag);
1264 flag.velocity = '0 0 0';
1265 flag.mangle = flag.angles;
1266 flag.reset = ctf_Reset;
1267 settouch(flag, ctf_FlagTouch);
1268 setthink(flag, ctf_FlagThink);
1269 flag.nextthink = time + FLAG_THINKRATE;
1270 flag.ctf_status = FLAG_BASE;
1272 // crudely force them all to 0
1273 if(autocvar_g_ctf_score_ignore_fields)
1274 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1276 string teamname = Static_Team_ColorName_Lower(teamnumber);
1278 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1279 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1280 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1281 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnumber).eent_eff_name; }
1282 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnumber).eent_eff_name; }
1283 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnumber).eent_eff_name; }
1287 if(flag.s == "") flag.s = b; \
1288 precache_sound(flag.s);
1290 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnumber))))
1291 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnumber))))
1292 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnumber))))
1293 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnumber))))
1294 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1295 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1296 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1300 precache_model(flag.model);
1303 _setmodel(flag, flag.model); // precision set below
1304 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1305 flag.m_mins = flag.mins; // store these for squash checks
1306 flag.m_maxs = flag.maxs;
1307 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1309 if(autocvar_g_ctf_flag_glowtrails)
1313 case NUM_TEAM_1: flag.glow_color = 251; break;
1314 case NUM_TEAM_2: flag.glow_color = 210; break;
1315 case NUM_TEAM_3: flag.glow_color = 110; break;
1316 case NUM_TEAM_4: flag.glow_color = 145; break;
1317 default: flag.glow_color = 254; break;
1319 flag.glow_size = 25;
1320 flag.glow_trail = 1;
1323 flag.effects |= EF_LOWPRECISION;
1324 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1325 if(autocvar_g_ctf_dynamiclights)
1329 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1330 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1331 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1332 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1333 default: flag.effects |= EF_DIMLIGHT; break;
1338 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1340 flag.dropped_origin = flag.origin;
1341 flag.noalign = true;
1342 set_movetype(flag, MOVETYPE_NONE);
1344 else // drop to floor, automatically find a platform and set that as spawn origin
1346 flag.noalign = false;
1348 set_movetype(flag, MOVETYPE_NONE);
1351 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1359 // NOTE: LEGACY CODE, needs to be re-written!
1361 void havocbot_ctf_calculate_middlepoint()
1365 vector fo = '0 0 0';
1368 f = ctf_worldflaglist;
1373 f = f.ctf_worldflagnext;
1379 havocbot_middlepoint = s / n;
1380 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1382 havocbot_symmetry_axis_m = 0;
1383 havocbot_symmetry_axis_q = 0;
1386 // for symmetrical editing of waypoints
1387 entity f1 = ctf_worldflaglist;
1388 entity f2 = f1.ctf_worldflagnext;
1389 float m = -(f1.origin.y - f2.origin.y) / (f1.origin.x - f2.origin.x);
1390 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1391 havocbot_symmetry_axis_m = m;
1392 havocbot_symmetry_axis_q = q;
1394 havocbot_symmetry_origin_order = n;
1398 entity havocbot_ctf_find_flag(entity bot)
1401 f = ctf_worldflaglist;
1404 if (CTF_SAMETEAM(bot, f))
1406 f = f.ctf_worldflagnext;
1411 entity havocbot_ctf_find_enemy_flag(entity bot)
1414 f = ctf_worldflaglist;
1419 if(CTF_DIFFTEAM(bot, f))
1426 else if(!bot.flagcarried)
1430 else if (CTF_DIFFTEAM(bot, f))
1432 f = f.ctf_worldflagnext;
1437 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1444 FOREACH_CLIENT(IS_PLAYER(it), {
1445 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1448 if(vdist(it.origin - org, <, tc_radius))
1457 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1460 head = ctf_worldflaglist;
1463 if (CTF_SAMETEAM(this, head))
1465 head = head.ctf_worldflagnext;
1468 navigation_routerating(this, head, ratingscale, 10000);
1472 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1475 head = ctf_worldflaglist;
1478 if (CTF_SAMETEAM(this, head))
1480 if (this.flagcarried)
1481 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1483 head = head.ctf_worldflagnext; // skip base if it has a different group
1488 head = head.ctf_worldflagnext;
1493 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1496 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1499 head = ctf_worldflaglist;
1504 if(CTF_DIFFTEAM(this, head))
1508 if(this.flagcarried)
1511 else if(!this.flagcarried)
1515 else if(CTF_DIFFTEAM(this, head))
1517 head = head.ctf_worldflagnext;
1521 if (head.ctf_status == FLAG_CARRY)
1523 // adjust rating of our flag carrier depending on his health
1524 head = head.tag_entity;
1525 float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1526 ratingscale += ratingscale * f * 0.1;
1528 navigation_routerating(this, head, ratingscale, 10000);
1532 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1534 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1536 if (!bot_waypoints_for_items)
1538 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1544 head = havocbot_ctf_find_enemy_flag(this);
1549 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1552 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1556 mf = havocbot_ctf_find_flag(this);
1558 if(mf.ctf_status == FLAG_BASE)
1562 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1565 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1568 head = ctf_worldflaglist;
1571 // flag is out in the field
1572 if(head.ctf_status != FLAG_BASE)
1573 if(head.tag_entity==NULL) // dropped
1577 if(vdist(org - head.origin, <, df_radius))
1578 navigation_routerating(this, head, ratingscale, 10000);
1581 navigation_routerating(this, head, ratingscale, 10000);
1584 head = head.ctf_worldflagnext;
1588 void havocbot_ctf_reset_role(entity this)
1590 float cdefense, cmiddle, coffense;
1597 if (this.flagcarried)
1599 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1603 mf = havocbot_ctf_find_flag(this);
1604 ef = havocbot_ctf_find_enemy_flag(this);
1606 // Retrieve stolen flag
1607 if(mf.ctf_status!=FLAG_BASE)
1609 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1613 // If enemy flag is taken go to the middle to intercept pursuers
1614 if(ef.ctf_status!=FLAG_BASE)
1616 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1620 // if there is no one else on the team switch to offense
1622 // don't check if this bot is a player since it isn't true when the bot is added to the server
1623 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1627 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1630 else if (time < CS(this).jointime + 1)
1632 // if bots spawn all at once set good default roles
1635 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1638 else if (count == 2)
1640 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1645 // Evaluate best position to take
1646 // Count mates on middle position
1647 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1649 // Count mates on defense position
1650 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1652 // Count mates on offense position
1653 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1655 if(cdefense<=coffense)
1656 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1657 else if(coffense<=cmiddle)
1658 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1660 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1662 // if bots spawn all at once assign them a more appropriated role after a while
1663 if (time < CS(this).jointime + 1 && count > 2)
1664 this.havocbot_role_timeout = time + 10 + random() * 10;
1667 bool havocbot_ctf_is_basewaypoint(entity item)
1669 if (item.classname != "waypoint")
1672 entity head = ctf_worldflaglist;
1675 if (item == head.bot_basewaypoint)
1677 head = head.ctf_worldflagnext;
1682 void havocbot_role_ctf_carrier(entity this)
1686 havocbot_ctf_reset_role(this);
1690 if (this.flagcarried == NULL)
1692 havocbot_ctf_reset_role(this);
1696 if (navigation_goalrating_timeout(this))
1698 navigation_goalrating_start(this);
1701 entity mf = havocbot_ctf_find_flag(this);
1702 vector base_org = mf.dropped_origin;
1703 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1705 havocbot_goalrating_ctf_enemybase(this, base_rating);
1707 havocbot_goalrating_ctf_ourbase(this, base_rating);
1709 // start collecting items very close to the bot but only inside of own base radius
1710 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1711 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1713 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1715 navigation_goalrating_end(this);
1717 navigation_goalrating_timeout_set(this);
1719 entity goal = this.goalentity;
1720 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1721 this.goalentity_lock_timeout = time + ((this.bot_aimtarg) ? 2 : 3);
1724 this.havocbot_cantfindflag = time + 10;
1725 else if (time > this.havocbot_cantfindflag)
1727 // Can't navigate to my own base, suicide!
1728 // TODO: drop it and wander around
1729 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1735 void havocbot_role_ctf_escort(entity this)
1741 havocbot_ctf_reset_role(this);
1745 if (this.flagcarried)
1747 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1751 // If enemy flag is back on the base switch to previous role
1752 ef = havocbot_ctf_find_enemy_flag(this);
1753 if(ef.ctf_status==FLAG_BASE)
1755 this.havocbot_role = this.havocbot_previous_role;
1756 this.havocbot_role_timeout = 0;
1759 if (ef.ctf_status == FLAG_DROPPED)
1761 navigation_goalrating_timeout_expire(this, 1);
1765 // If the flag carrier reached the base switch to defense
1766 mf = havocbot_ctf_find_flag(this);
1767 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1769 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1773 // Set the role timeout if necessary
1774 if (!this.havocbot_role_timeout)
1776 this.havocbot_role_timeout = time + random() * 30 + 60;
1779 // If nothing happened just switch to previous role
1780 if (time > this.havocbot_role_timeout)
1782 this.havocbot_role = this.havocbot_previous_role;
1783 this.havocbot_role_timeout = 0;
1787 // Chase the flag carrier
1788 if (navigation_goalrating_timeout(this))
1790 navigation_goalrating_start(this);
1793 havocbot_goalrating_ctf_enemyflag(this, 10000);
1794 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1795 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1797 navigation_goalrating_end(this);
1799 navigation_goalrating_timeout_set(this);
1803 void havocbot_role_ctf_offense(entity this)
1810 havocbot_ctf_reset_role(this);
1814 if (this.flagcarried)
1816 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1821 mf = havocbot_ctf_find_flag(this);
1822 ef = havocbot_ctf_find_enemy_flag(this);
1825 if(mf.ctf_status!=FLAG_BASE)
1828 pos = mf.tag_entity.origin;
1832 // Try to get it if closer than the enemy base
1833 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1835 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1840 // Escort flag carrier
1841 if(ef.ctf_status!=FLAG_BASE)
1844 pos = ef.tag_entity.origin;
1848 if(vdist(pos - mf.dropped_origin, >, 700))
1850 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1855 // Set the role timeout if necessary
1856 if (!this.havocbot_role_timeout)
1857 this.havocbot_role_timeout = time + 120;
1859 if (time > this.havocbot_role_timeout)
1861 havocbot_ctf_reset_role(this);
1865 if (navigation_goalrating_timeout(this))
1867 navigation_goalrating_start(this);
1870 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1871 havocbot_goalrating_ctf_enemybase(this, 10000);
1872 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1874 navigation_goalrating_end(this);
1876 navigation_goalrating_timeout_set(this);
1880 // Retriever (temporary role):
1881 void havocbot_role_ctf_retriever(entity this)
1887 havocbot_ctf_reset_role(this);
1891 if (this.flagcarried)
1893 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1897 // If flag is back on the base switch to previous role
1898 mf = havocbot_ctf_find_flag(this);
1899 if(mf.ctf_status==FLAG_BASE)
1901 if (mf.enemy == this) // did this bot return the flag?
1902 navigation_goalrating_timeout_force(this);
1903 havocbot_ctf_reset_role(this);
1907 if (!this.havocbot_role_timeout)
1908 this.havocbot_role_timeout = time + 20;
1910 if (time > this.havocbot_role_timeout)
1912 havocbot_ctf_reset_role(this);
1916 if (navigation_goalrating_timeout(this))
1918 const float RT_RADIUS = 10000;
1920 navigation_goalrating_start(this);
1923 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1924 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1925 havocbot_goalrating_ctf_enemybase(this, 8000);
1926 entity ef = havocbot_ctf_find_enemy_flag(this);
1927 vector enemy_base_org = ef.dropped_origin;
1928 // start collecting items very close to the bot but only inside of enemy base radius
1929 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1930 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1931 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1933 navigation_goalrating_end(this);
1935 navigation_goalrating_timeout_set(this);
1939 void havocbot_role_ctf_middle(entity this)
1945 havocbot_ctf_reset_role(this);
1949 if (this.flagcarried)
1951 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1955 mf = havocbot_ctf_find_flag(this);
1956 if(mf.ctf_status!=FLAG_BASE)
1958 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1962 if (!this.havocbot_role_timeout)
1963 this.havocbot_role_timeout = time + 10;
1965 if (time > this.havocbot_role_timeout)
1967 havocbot_ctf_reset_role(this);
1971 if (navigation_goalrating_timeout(this))
1975 org = havocbot_middlepoint;
1976 org.z = this.origin.z;
1978 navigation_goalrating_start(this);
1981 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
1982 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
1983 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
1984 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
1985 havocbot_goalrating_items(this, 18000, this.origin, 10000);
1986 havocbot_goalrating_ctf_enemybase(this, 3000);
1988 navigation_goalrating_end(this);
1990 entity goal = this.goalentity;
1991 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1992 this.goalentity_lock_timeout = time + 2;
1994 navigation_goalrating_timeout_set(this);
1998 void havocbot_role_ctf_defense(entity this)
2004 havocbot_ctf_reset_role(this);
2008 if (this.flagcarried)
2010 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2014 // If own flag was captured
2015 mf = havocbot_ctf_find_flag(this);
2016 if(mf.ctf_status!=FLAG_BASE)
2018 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2022 if (!this.havocbot_role_timeout)
2023 this.havocbot_role_timeout = time + 30;
2025 if (time > this.havocbot_role_timeout)
2027 havocbot_ctf_reset_role(this);
2030 if (navigation_goalrating_timeout(this))
2032 vector org = mf.dropped_origin;
2034 navigation_goalrating_start(this);
2036 // if enemies are closer to our base, go there
2037 entity closestplayer = NULL;
2038 float distance, bestdistance = 10000;
2039 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2040 distance = vlen(org - it.origin);
2041 if(distance<bestdistance)
2044 bestdistance = distance;
2050 if(DIFF_TEAM(closestplayer, this))
2051 if(vdist(org - this.origin, >, 1000))
2052 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2053 havocbot_goalrating_ctf_ourbase(this, 10000);
2055 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2056 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2057 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2058 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2059 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2061 navigation_goalrating_end(this);
2063 navigation_goalrating_timeout_set(this);
2067 void havocbot_role_ctf_setrole(entity bot, int role)
2069 string s = "(null)";
2072 case HAVOCBOT_CTF_ROLE_CARRIER:
2074 bot.havocbot_role = havocbot_role_ctf_carrier;
2075 bot.havocbot_role_timeout = 0;
2076 bot.havocbot_cantfindflag = time + 10;
2077 if (bot.havocbot_previous_role != bot.havocbot_role)
2078 navigation_goalrating_timeout_force(bot);
2080 case HAVOCBOT_CTF_ROLE_DEFENSE:
2082 bot.havocbot_role = havocbot_role_ctf_defense;
2083 bot.havocbot_role_timeout = 0;
2085 case HAVOCBOT_CTF_ROLE_MIDDLE:
2087 bot.havocbot_role = havocbot_role_ctf_middle;
2088 bot.havocbot_role_timeout = 0;
2090 case HAVOCBOT_CTF_ROLE_OFFENSE:
2092 bot.havocbot_role = havocbot_role_ctf_offense;
2093 bot.havocbot_role_timeout = 0;
2095 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2097 bot.havocbot_previous_role = bot.havocbot_role;
2098 bot.havocbot_role = havocbot_role_ctf_retriever;
2099 bot.havocbot_role_timeout = time + 10;
2100 if (bot.havocbot_previous_role != bot.havocbot_role)
2101 navigation_goalrating_timeout_expire(bot, 2);
2103 case HAVOCBOT_CTF_ROLE_ESCORT:
2105 bot.havocbot_previous_role = bot.havocbot_role;
2106 bot.havocbot_role = havocbot_role_ctf_escort;
2107 bot.havocbot_role_timeout = time + 30;
2108 if (bot.havocbot_previous_role != bot.havocbot_role)
2109 navigation_goalrating_timeout_expire(bot, 2);
2112 LOG_TRACE(bot.netname, " switched to ", s);
2120 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2122 entity player = M_ARGV(0, entity);
2124 int t = 0, t2 = 0, t3 = 0;
2125 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)
2127 // initially clear items so they can be set as necessary later.
2128 STAT(CTF_FLAGSTATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2129 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2130 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2131 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2132 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2133 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2135 // scan through all the flags and notify the client about them
2136 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2138 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2139 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2140 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2141 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2142 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; }
2144 switch(flag.ctf_status)
2149 if((flag.owner == player) || (flag.pass_sender == player))
2150 STAT(CTF_FLAGSTATUS, player) |= t; // carrying: player is currently carrying the flag
2152 STAT(CTF_FLAGSTATUS, player) |= t2; // taken: someone else is carrying the flag
2157 STAT(CTF_FLAGSTATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2163 // item for stopping players from capturing the flag too often
2164 if(player.ctf_captureshielded)
2165 STAT(CTF_FLAGSTATUS, player) |= CTF_SHIELDED;
2168 STAT(CTF_FLAGSTATUS, player) |= CTF_STALEMATE;
2170 // update the health of the flag carrier waypointsprite
2171 if(player.wps_flagcarrier)
2172 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);
2175 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2177 entity frag_attacker = M_ARGV(1, entity);
2178 entity frag_target = M_ARGV(2, entity);
2179 float frag_damage = M_ARGV(4, float);
2180 vector frag_force = M_ARGV(6, vector);
2182 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2184 if(frag_target == frag_attacker) // damage done to yourself
2186 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2187 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2189 else // damage done to everyone else
2191 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2192 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2195 M_ARGV(4, float) = frag_damage;
2196 M_ARGV(6, vector) = frag_force;
2198 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2200 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
2201 && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2203 frag_target.wps_helpme_time = time;
2204 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2206 // todo: add notification for when flag carrier needs help?
2210 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2212 entity frag_attacker = M_ARGV(1, entity);
2213 entity frag_target = M_ARGV(2, entity);
2215 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2217 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2218 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2221 if(frag_target.flagcarried)
2223 entity tmp_entity = frag_target.flagcarried;
2224 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2225 tmp_entity.ctf_dropper = NULL;
2229 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2231 M_ARGV(2, float) = 0; // frag score
2232 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2235 void ctf_RemovePlayer(entity player)
2237 if(player.flagcarried)
2238 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2240 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2242 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2243 if(flag.pass_target == player) { flag.pass_target = NULL; }
2244 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2248 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2250 entity player = M_ARGV(0, entity);
2252 ctf_RemovePlayer(player);
2255 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2257 entity player = M_ARGV(0, entity);
2259 ctf_RemovePlayer(player);
2262 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2264 if(!autocvar_g_ctf_leaderboard)
2267 entity player = M_ARGV(0, entity);
2269 if(IS_REAL_CLIENT(player))
2271 int m = min(RANKINGS_CNT, autocvar_g_cts_send_rankings_cnt);
2272 race_send_rankings_cnt(MSG_ONE);
2273 for (int i = 1; i <= m; ++i)
2275 race_SendRankings(i, 0, 0, MSG_ONE);
2280 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2282 if(!autocvar_g_ctf_leaderboard)
2285 entity player = M_ARGV(0, entity);
2287 if(CS(player).cvar_cl_allow_uidtracking == 1 && CS(player).cvar_cl_allow_uid2name == 1)
2289 if (!player.stored_netname)
2290 player.stored_netname = strzone(uid2name(player.crypto_idfp));
2291 if(player.stored_netname != player.netname)
2293 db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
2294 strcpy(player.stored_netname, player.netname);
2299 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2301 entity player = M_ARGV(0, entity);
2303 if(player.flagcarried)
2304 if(!autocvar_g_ctf_portalteleport)
2305 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2308 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2310 if(MUTATOR_RETURNVALUE || game_stopped) return;
2312 entity player = M_ARGV(0, entity);
2314 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2316 // pass the flag to a team mate
2317 if(autocvar_g_ctf_pass)
2319 entity head, closest_target = NULL;
2320 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2322 while(head) // find the closest acceptable target to pass to
2324 if(IS_PLAYER(head) && !IS_DEAD(head))
2325 if(head != player && SAME_TEAM(head, player))
2326 if(!head.speedrunning && !head.vehicle)
2328 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2329 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2330 vector passer_center = CENTER_OR_VIEWOFS(player);
2332 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2334 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2336 if(IS_BOT_CLIENT(head))
2338 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2339 ctf_Handle_Throw(head, player, DROP_PASS);
2343 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2344 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2346 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2349 else if(player.flagcarried && !head.flagcarried)
2353 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2354 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2355 { closest_target = head; }
2357 else { closest_target = head; }
2364 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2367 // throw the flag in front of you
2368 if(autocvar_g_ctf_throw && player.flagcarried)
2370 if(player.throw_count == -1)
2372 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2374 player.throw_prevtime = time;
2375 player.throw_count = 1;
2376 ctf_Handle_Throw(player, NULL, DROP_THROW);
2381 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2387 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2388 else { player.throw_count += 1; }
2389 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2391 player.throw_prevtime = time;
2392 ctf_Handle_Throw(player, NULL, DROP_THROW);
2399 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2401 entity player = M_ARGV(0, entity);
2403 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2405 player.wps_helpme_time = time;
2406 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2408 else // create a normal help me waypointsprite
2410 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2411 WaypointSprite_Ping(player.wps_helpme);
2417 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2419 entity player = M_ARGV(0, entity);
2420 entity veh = M_ARGV(1, entity);
2422 if(player.flagcarried)
2424 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2426 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2430 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2431 setattachment(player.flagcarried, veh, "");
2432 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2433 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2434 //player.flagcarried.angles = '0 0 0';
2440 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2442 entity player = M_ARGV(0, entity);
2444 if(player.flagcarried)
2446 setattachment(player.flagcarried, player, "");
2447 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2448 player.flagcarried.scale = FLAG_SCALE;
2449 player.flagcarried.angles = '0 0 0';
2450 player.flagcarried.nodrawtoclient = NULL;
2455 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2457 entity player = M_ARGV(0, entity);
2459 if(player.flagcarried)
2461 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2462 ctf_RespawnFlag(player.flagcarried);
2467 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2469 entity flag; // temporary entity for the search method
2471 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2473 switch(flag.ctf_status)
2478 // lock the flag, game is over
2479 set_movetype(flag, MOVETYPE_NONE);
2480 flag.takedamage = DAMAGE_NO;
2481 flag.solid = SOLID_NOT;
2482 flag.nextthink = false; // stop thinking
2484 //dprint("stopping the ", flag.netname, " from moving.\n");
2492 // do nothing for these flags
2499 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2501 entity bot = M_ARGV(0, entity);
2503 havocbot_ctf_reset_role(bot);
2507 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2509 M_ARGV(1, string) = "ctf_team";
2512 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2514 entity spectatee = M_ARGV(0, entity);
2515 entity client = M_ARGV(1, entity);
2517 STAT(CTF_FLAGSTATUS, client) = STAT(CTF_FLAGSTATUS, spectatee);
2520 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2522 int record_page = M_ARGV(0, int);
2523 string ret_string = M_ARGV(1, string);
2525 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2527 if (MapInfo_Get_ByID(i))
2529 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2535 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2536 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2540 M_ARGV(1, string) = ret_string;
2543 bool superspec_Spectate(entity this, entity targ); // TODO
2544 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2545 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2547 entity player = M_ARGV(0, entity);
2548 string cmd_name = M_ARGV(1, string);
2549 int cmd_argc = M_ARGV(2, int);
2551 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2553 if(cmd_name == "followfc")
2565 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2566 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2567 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2568 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2572 FOREACH_CLIENT(IS_PLAYER(it), {
2573 if(it.flagcarried && (it.team == _team || _team == 0))
2576 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2577 continue; // already spectating this fc, try another
2578 return superspec_Spectate(player, it);
2583 superspec_msg("", "", player, "No active flag carrier\n", 1);
2588 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2590 entity frag_target = M_ARGV(0, entity);
2592 if(frag_target.flagcarried)
2593 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2601 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2602 CTF flag for team one (Red).
2604 "angle" Angle the flag will point (minus 90 degrees)...
2605 "model" model to use, note this needs red and blue as skins 0 and 1...
2606 "noise" sound played when flag is picked up...
2607 "noise1" sound played when flag is returned by a teammate...
2608 "noise2" sound played when flag is captured...
2609 "noise3" sound played when flag is lost in the field and respawns itself...
2610 "noise4" sound played when flag is dropped by a player...
2611 "noise5" sound played when flag touches the ground... */
2612 spawnfunc(item_flag_team1)
2614 if(!g_ctf) { delete(this); return; }
2616 ctf_FlagSetup(NUM_TEAM_1, this);
2619 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2620 CTF flag for team two (Blue).
2622 "angle" Angle the flag will point (minus 90 degrees)...
2623 "model" model to use, note this needs red and blue as skins 0 and 1...
2624 "noise" sound played when flag is picked up...
2625 "noise1" sound played when flag is returned by a teammate...
2626 "noise2" sound played when flag is captured...
2627 "noise3" sound played when flag is lost in the field and respawns itself...
2628 "noise4" sound played when flag is dropped by a player...
2629 "noise5" sound played when flag touches the ground... */
2630 spawnfunc(item_flag_team2)
2632 if(!g_ctf) { delete(this); return; }
2634 ctf_FlagSetup(NUM_TEAM_2, this);
2637 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2638 CTF flag for team three (Yellow).
2640 "angle" Angle the flag will point (minus 90 degrees)...
2641 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2642 "noise" sound played when flag is picked up...
2643 "noise1" sound played when flag is returned by a teammate...
2644 "noise2" sound played when flag is captured...
2645 "noise3" sound played when flag is lost in the field and respawns itself...
2646 "noise4" sound played when flag is dropped by a player...
2647 "noise5" sound played when flag touches the ground... */
2648 spawnfunc(item_flag_team3)
2650 if(!g_ctf) { delete(this); return; }
2652 ctf_FlagSetup(NUM_TEAM_3, this);
2655 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2656 CTF flag for team four (Pink).
2658 "angle" Angle the flag will point (minus 90 degrees)...
2659 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2660 "noise" sound played when flag is picked up...
2661 "noise1" sound played when flag is returned by a teammate...
2662 "noise2" sound played when flag is captured...
2663 "noise3" sound played when flag is lost in the field and respawns itself...
2664 "noise4" sound played when flag is dropped by a player...
2665 "noise5" sound played when flag touches the ground... */
2666 spawnfunc(item_flag_team4)
2668 if(!g_ctf) { delete(this); return; }
2670 ctf_FlagSetup(NUM_TEAM_4, this);
2673 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2676 "angle" Angle the flag will point (minus 90 degrees)...
2677 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2678 "noise" sound played when flag is picked up...
2679 "noise1" sound played when flag is returned by a teammate...
2680 "noise2" sound played when flag is captured...
2681 "noise3" sound played when flag is lost in the field and respawns itself...
2682 "noise4" sound played when flag is dropped by a player...
2683 "noise5" sound played when flag touches the ground... */
2684 spawnfunc(item_flag_neutral)
2686 if(!g_ctf) { delete(this); return; }
2687 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2689 ctf_FlagSetup(0, this);
2692 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2693 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2694 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.
2696 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2697 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2700 if(!g_ctf) { delete(this); return; }
2702 this.classname = "ctf_team";
2703 this.team = this.cnt + 1;
2706 // compatibility for quake maps
2707 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2708 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2709 spawnfunc(info_player_team1);
2710 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2711 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2712 spawnfunc(info_player_team2);
2713 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2714 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2716 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2717 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2719 // compatibility for wop maps
2720 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2721 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2722 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2723 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2724 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2725 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2733 void ctf_ScoreRules(int teams)
2735 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2736 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2737 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2738 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2739 field(SP_CTF_PICKUPS, "pickups", 0);
2740 field(SP_CTF_FCKILLS, "fckills", 0);
2741 field(SP_CTF_RETURNS, "returns", 0);
2742 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2746 // code from here on is just to support maps that don't have flag and team entities
2747 void ctf_SpawnTeam (string teamname, int teamcolor)
2749 entity this = new_pure(ctf_team);
2750 this.netname = teamname;
2751 this.cnt = teamcolor - 1;
2752 this.spawnfunc_checked = true;
2753 this.team = teamcolor;
2756 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2761 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2763 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2764 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2766 switch(tmp_entity.team)
2768 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2769 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2770 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2771 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2773 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2776 havocbot_ctf_calculate_middlepoint();
2778 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2780 ctf_teams = 0; // so set the default red and blue teams
2781 BITSET_ASSIGN(ctf_teams, BIT(0));
2782 BITSET_ASSIGN(ctf_teams, BIT(1));
2785 //ctf_teams = bound(2, ctf_teams, 4);
2787 // if no teams are found, spawn defaults
2788 if(find(NULL, classname, "ctf_team") == NULL)
2790 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2791 if(ctf_teams & BIT(0))
2792 ctf_SpawnTeam("Red", NUM_TEAM_1);
2793 if(ctf_teams & BIT(1))
2794 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2795 if(ctf_teams & BIT(2))
2796 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2797 if(ctf_teams & BIT(3))
2798 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2801 ctf_ScoreRules(ctf_teams);
2804 void ctf_Initialize()
2806 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2808 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2809 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2810 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2812 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);