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 bool autocvar_g_ctf_flag_waypoint = true;
52 float autocvar_g_ctf_flag_waypoint_maxdistance;
53 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
54 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
55 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
56 float autocvar_g_ctf_flagcarrier_selfforcefactor;
57 float autocvar_g_ctf_flagcarrier_damagefactor;
58 float autocvar_g_ctf_flagcarrier_forcefactor;
59 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
60 bool autocvar_g_ctf_fullbrightflags;
61 bool autocvar_g_ctf_ignore_frags;
62 bool autocvar_g_ctf_score_ignore_fields;
63 int autocvar_g_ctf_score_capture;
64 int autocvar_g_ctf_score_capture_assist;
65 int autocvar_g_ctf_score_kill;
66 int autocvar_g_ctf_score_penalty_drop;
67 int autocvar_g_ctf_score_penalty_returned;
68 int autocvar_g_ctf_score_pickup_base;
69 int autocvar_g_ctf_score_pickup_dropped_early;
70 int autocvar_g_ctf_score_pickup_dropped_late;
71 int autocvar_g_ctf_score_return;
72 float autocvar_g_ctf_shield_force;
73 float autocvar_g_ctf_shield_max_ratio;
74 int autocvar_g_ctf_shield_min_negscore;
75 bool autocvar_g_ctf_stalemate;
76 int autocvar_g_ctf_stalemate_endcondition;
77 float autocvar_g_ctf_stalemate_time;
78 bool autocvar_g_ctf_reverse;
79 float autocvar_g_ctf_dropped_capture_delay;
80 float autocvar_g_ctf_dropped_capture_radius;
82 void ctf_FakeTimeLimit(entity e, float t)
85 WriteByte(MSG_ONE, 3); // svc_updatestat
86 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
88 WriteCoord(MSG_ONE, autocvar_timelimit);
90 WriteCoord(MSG_ONE, (t + 1) / 60);
93 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
95 if(autocvar_sv_eventlog)
96 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != NULL) ? ftos(actor.playerid) : "")));
97 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
100 void ctf_CaptureRecord(entity flag, entity player)
102 float cap_record = ctf_captimerecord;
103 float cap_time = (time - flag.ctf_pickuptime);
104 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
108 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname);
109 else if(!ctf_captimerecord)
110 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_TIME), player.netname, TIME_ENCODE(cap_time));
111 else if(cap_time < cap_record)
112 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));
114 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));
116 // write that shit in the database
117 if(!ctf_oneflag) // but not in 1-flag mode
118 if((!ctf_captimerecord) || (cap_time < cap_record))
120 ctf_captimerecord = cap_time;
121 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
122 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
123 write_recordmarker(player, flag.ctf_pickuptime, cap_time);
126 if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
127 race_setTime(GetMapname(), TIME_ENCODE(cap_time), player.crypto_idfp, player.netname, player, false);
130 bool ctf_Immediate_Return_Allowed(entity flag, entity toucher)
133 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), { ++num_perteam; });
135 // automatically return if there's only 1 player on the team
136 return ((autocvar_g_ctf_flag_return || num_perteam <= 1 || (autocvar_g_ctf_flag_return_carrying && toucher.flagcarried))
140 bool ctf_Return_Customize(entity this, entity client)
142 // only to the carrier
143 return boolean(client == this.owner);
146 void ctf_FlagcarrierWaypoints(entity player)
148 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
149 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, 2 * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
150 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);
151 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
153 if(player.flagcarried && CTF_SAMETEAM(player, player.flagcarried))
155 if(!player.wps_enemyflagcarrier)
157 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, 0, player, wps_enemyflagcarrier, true, RADARICON_FLAG);
158 wp.colormod = WPCOLOR_ENEMYFC(player.team);
159 setcefc(wp, ctf_Stalemate_Customize);
161 if(IS_REAL_CLIENT(player) && !ctf_stalemate)
162 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_VISIBLE);
165 if(!player.wps_flagreturn)
167 entity owp = WaypointSprite_SpawnFixed(WP_FlagReturn, player.flagcarried.ctf_spawnorigin + FLAG_WAYPOINT_OFFSET, player, wps_flagreturn, RADARICON_FLAG);
168 owp.colormod = '0 0.8 0.8';
169 //WaypointSprite_UpdateTeamRadar(player.wps_flagreturn, RADARICON_FLAG, ((player.team) ? colormapPaletteColor(player.team - 1, false) : '1 1 1'));
170 setcefc(owp, ctf_Return_Customize);
175 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
177 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
178 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
179 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
180 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
183 if(current_height) // make sure we can actually do this arcing path
185 targpos = (to + ('0 0 1' * current_height));
186 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
187 if(trace_fraction < 1)
189 //print("normal arc line failed, trying to find new pos...");
190 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
191 targpos = (trace_endpos + eZ * FLAG_PASS_ARC_OFFSET_Z);
192 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
193 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
194 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
197 else { targpos = to; }
199 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
201 vector desired_direction = normalize(targpos - from);
202 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
203 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
206 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
208 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
210 // directional tracing only
212 makevectors(passer_angle);
214 // find the closest point on the enemy to the center of the attack
215 float h; // hypotenuse, which is the distance between attacker to head
216 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
218 h = vlen(head_center - passer_center);
219 a = h * (normalize(head_center - passer_center) * v_forward);
221 vector nearest_on_line = (passer_center + a * v_forward);
222 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
224 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
225 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
227 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
232 else { return true; }
236 // =======================
237 // CaptureShield Functions
238 // =======================
240 bool ctf_CaptureShield_CheckStatus(entity p)
242 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
243 int players_worseeq, players_total;
245 if(ctf_captureshield_max_ratio <= 0)
248 s = GameRules_scoring_add(p, CTF_CAPS, 0);
249 s2 = GameRules_scoring_add(p, CTF_PICKUPS, 0);
250 s3 = GameRules_scoring_add(p, CTF_RETURNS, 0);
251 s4 = GameRules_scoring_add(p, CTF_FCKILLS, 0);
253 sr = ((s - s2) + (s3 + s4));
255 if(sr >= -ctf_captureshield_min_negscore)
258 players_total = players_worseeq = 0;
259 FOREACH_CLIENT(IS_PLAYER(it), {
262 se = GameRules_scoring_add(it, CTF_CAPS, 0);
263 se2 = GameRules_scoring_add(it, CTF_PICKUPS, 0);
264 se3 = GameRules_scoring_add(it, CTF_RETURNS, 0);
265 se4 = GameRules_scoring_add(it, CTF_FCKILLS, 0);
267 ser = ((se - se2) + (se3 + se4));
274 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
275 // use this rule here
277 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
283 void ctf_CaptureShield_Update(entity player, bool wanted_status)
285 bool updated_status = ctf_CaptureShield_CheckStatus(player);
286 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
288 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
289 player.ctf_captureshielded = updated_status;
293 bool ctf_CaptureShield_Customize(entity this, entity client)
295 if(!client.ctf_captureshielded) { return false; }
296 if(CTF_SAMETEAM(this, client)) { return false; }
301 void ctf_CaptureShield_Touch(entity this, entity toucher)
303 if(!toucher.ctf_captureshielded) { return; }
304 if(CTF_SAMETEAM(this, toucher)) { return; }
306 vector mymid = (this.absmin + this.absmax) * 0.5;
307 vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
309 Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
310 if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
313 void ctf_CaptureShield_Spawn(entity flag)
315 entity shield = new(ctf_captureshield);
318 shield.team = flag.team;
319 settouch(shield, ctf_CaptureShield_Touch);
320 setcefc(shield, ctf_CaptureShield_Customize);
321 shield.effects = EF_ADDITIVE;
322 set_movetype(shield, MOVETYPE_NOCLIP);
323 shield.solid = SOLID_TRIGGER;
324 shield.avelocity = '7 0 11';
327 setorigin(shield, flag.origin);
328 setmodel(shield, MDL_CTF_SHIELD);
329 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
333 // ====================
334 // Drop/Pass/Throw Code
335 // ====================
337 void ctf_Handle_Drop(entity flag, entity player, int droptype)
340 player = (player ? player : flag.pass_sender);
343 set_movetype(flag, MOVETYPE_TOSS);
344 flag.takedamage = DAMAGE_YES;
345 flag.angles = '0 0 0';
346 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
347 flag.ctf_droptime = time;
348 flag.ctf_dropper = player;
349 flag.ctf_status = FLAG_DROPPED;
351 // messages and sounds
352 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
353 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
354 ctf_EventLog("dropped", player.team, player);
357 GameRules_scoring_add_team(player, SCORE, -((flag.score_drop) ? flag.score_drop : autocvar_g_ctf_score_penalty_drop));
358 GameRules_scoring_add(player, CTF_DROPS, 1);
361 if(autocvar_g_ctf_flag_dropped_waypoint) {
362 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);
363 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
366 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
368 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_health);
369 WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH));
372 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
374 if(droptype == DROP_PASS)
376 flag.pass_distance = 0;
377 flag.pass_sender = NULL;
378 flag.pass_target = NULL;
382 void ctf_Handle_Retrieve(entity flag, entity player)
384 entity sender = flag.pass_sender;
386 // transfer flag to player
388 flag.owner.flagcarried = flag;
389 GameRules_scoring_vip(player, true);
394 setattachment(flag, player.vehicle, "");
395 setorigin(flag, VEHICLE_FLAG_OFFSET);
396 flag.scale = VEHICLE_FLAG_SCALE;
400 setattachment(flag, player, "");
401 setorigin(flag, FLAG_CARRY_OFFSET);
403 set_movetype(flag, MOVETYPE_NONE);
404 flag.takedamage = DAMAGE_NO;
405 flag.solid = SOLID_NOT;
406 flag.angles = '0 0 0';
407 flag.ctf_status = FLAG_CARRY;
409 // messages and sounds
410 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
411 ctf_EventLog("receive", flag.team, player);
413 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
415 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
416 else if(it == player)
417 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
418 else if(SAME_TEAM(it, sender))
419 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
422 // create new waypoint
423 ctf_FlagcarrierWaypoints(player);
425 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
426 player.throw_antispam = sender.throw_antispam;
428 flag.pass_distance = 0;
429 flag.pass_sender = NULL;
430 flag.pass_target = NULL;
433 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
435 entity flag = player.flagcarried;
436 vector targ_origin, flag_velocity;
438 if(!flag) { return; }
439 if((droptype == DROP_PASS) && !receiver) { return; }
441 if(flag.speedrunning)
443 // ensure old waypoints are removed before resetting the flag
444 WaypointSprite_Kill(player.wps_flagcarrier);
446 if(player.wps_enemyflagcarrier)
447 WaypointSprite_Kill(player.wps_enemyflagcarrier);
449 if(player.wps_flagreturn)
450 WaypointSprite_Kill(player.wps_flagreturn);
451 ctf_RespawnFlag(flag);
456 setattachment(flag, NULL, "");
457 tracebox(player.origin - FLAG_DROP_OFFSET, flag.m_mins, flag.m_maxs, player.origin + FLAG_DROP_OFFSET, MOVE_NOMONSTERS, flag);
458 setorigin(flag, trace_endpos);
459 flag.owner.flagcarried = NULL;
460 GameRules_scoring_vip(flag.owner, false);
462 flag.solid = SOLID_TRIGGER;
463 flag.ctf_dropper = player;
464 flag.ctf_droptime = time;
466 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
473 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
474 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
475 WarpZone_RefSys_Copy(flag, receiver);
476 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
477 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
479 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
480 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
483 set_movetype(flag, MOVETYPE_FLY);
484 flag.takedamage = DAMAGE_NO;
485 flag.pass_sender = player;
486 flag.pass_target = receiver;
487 flag.ctf_status = FLAG_PASSING;
490 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
491 WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
492 ctf_EventLog("pass", flag.team, player);
498 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'));
500 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)));
501 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
502 ctf_Handle_Drop(flag, player, droptype);
503 navigation_dynamicgoal_set(flag, player);
509 flag.velocity = '0 0 0'; // do nothing
516 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);
517 ctf_Handle_Drop(flag, player, droptype);
518 navigation_dynamicgoal_set(flag, player);
523 // kill old waypointsprite
524 WaypointSprite_Ping(player.wps_flagcarrier);
525 WaypointSprite_Kill(player.wps_flagcarrier);
527 if(player.wps_enemyflagcarrier)
528 WaypointSprite_Kill(player.wps_enemyflagcarrier);
530 if(player.wps_flagreturn)
531 WaypointSprite_Kill(player.wps_flagreturn);
534 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
537 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
539 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
546 void nades_GiveBonus(entity player, float score);
548 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
550 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
551 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
552 entity player_team_flag = NULL, tmp_entity;
553 float old_time, new_time;
555 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
556 if(CTF_DIFFTEAM(player, flag)) { return; }
557 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)
559 if (toucher.goalentity == flag.bot_basewaypoint)
560 toucher.goalentity_lock_timeout = 0;
563 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
564 if(SAME_TEAM(tmp_entity, player))
566 player_team_flag = tmp_entity;
570 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
572 player.throw_prevtime = time;
573 player.throw_count = 0;
575 // messages and sounds
576 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
577 ctf_CaptureRecord(enemy_flag, player);
578 _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);
582 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
583 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
589 if(enemy_flag.score_capture || flag.score_capture)
590 pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
591 GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
593 if(enemy_flag.score_team_capture || flag.score_team_capture)
594 capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
595 GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
597 old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
598 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
599 if(!old_time || new_time < old_time)
600 GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
603 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
604 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
607 if(capturetype == CAPTURE_NORMAL)
609 WaypointSprite_Kill(player.wps_flagcarrier);
610 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
612 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
613 { GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist)); }
616 flag.enemy = toucher;
619 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
620 ctf_RespawnFlag(enemy_flag);
623 void ctf_Handle_Return(entity flag, entity player)
625 // messages and sounds
626 if(IS_MONSTER(player))
628 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
632 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
633 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
635 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
636 ctf_EventLog("return", flag.team, player);
639 if(IS_PLAYER(player))
641 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
642 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
644 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
647 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
651 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
652 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
653 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
657 if(player.flagcarried == flag)
658 WaypointSprite_Kill(player.wps_flagcarrier);
663 ctf_RespawnFlag(flag);
666 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
669 float pickup_dropped_score; // used to calculate dropped pickup score
671 // attach the flag to the player
673 player.flagcarried = flag;
674 GameRules_scoring_vip(player, true);
677 setattachment(flag, player.vehicle, "");
678 setorigin(flag, VEHICLE_FLAG_OFFSET);
679 flag.scale = VEHICLE_FLAG_SCALE;
683 setattachment(flag, player, "");
684 setorigin(flag, FLAG_CARRY_OFFSET);
688 set_movetype(flag, MOVETYPE_NONE);
689 flag.takedamage = DAMAGE_NO;
690 flag.solid = SOLID_NOT;
691 flag.angles = '0 0 0';
692 flag.ctf_status = FLAG_CARRY;
696 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
697 case PICKUP_DROPPED: SetResourceExplicit(flag, RES_HEALTH, flag.max_health); break; // reset health/return timelimit
701 // messages and sounds
702 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
704 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
706 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
707 else if(CTF_DIFFTEAM(player, flag))
708 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
710 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
712 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
715 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); });
718 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
719 if(CTF_SAMETEAM(flag, it))
721 if(SAME_TEAM(player, it))
722 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
724 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);
728 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
731 GameRules_scoring_add(player, CTF_PICKUPS, 1);
732 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
737 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
738 ctf_EventLog("steal", flag.team, player);
744 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);
745 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);
746 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
747 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
748 ctf_EventLog("pickup", flag.team, player);
756 if(pickuptype == PICKUP_BASE)
758 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
759 if((player.speedrunning) && (ctf_captimerecord))
760 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
764 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
767 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
768 ctf_FlagcarrierWaypoints(player);
769 WaypointSprite_Ping(player.wps_flagcarrier);
773 // ===================
774 // Main Flag Functions
775 // ===================
777 void ctf_CheckFlagReturn(entity flag, int returntype)
779 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
781 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH)); }
783 if((GetResource(flag, RES_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
788 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
790 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
791 case RETURN_SPEEDRUN:
792 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
793 case RETURN_NEEDKILL:
794 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
797 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
799 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
800 ctf_EventLog("returned", flag.team, NULL);
802 ctf_RespawnFlag(flag);
807 bool ctf_Stalemate_Customize(entity this, entity client)
809 // make spectators see what the player would see
810 entity e = WaypointSprite_getviewentity(client);
811 entity wp_owner = this.owner;
814 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
815 if(SAME_TEAM(wp_owner, e)) { return false; }
816 if(!IS_PLAYER(e)) { return false; }
821 void ctf_CheckStalemate()
824 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
827 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
829 // build list of stale flags
830 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
832 if(autocvar_g_ctf_stalemate)
833 if(tmp_entity.ctf_status != FLAG_BASE)
834 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
836 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
837 ctf_staleflaglist = tmp_entity;
839 switch(tmp_entity.team)
841 case NUM_TEAM_1: ++stale_red_flags; break;
842 case NUM_TEAM_2: ++stale_blue_flags; break;
843 case NUM_TEAM_3: ++stale_yellow_flags; break;
844 case NUM_TEAM_4: ++stale_pink_flags; break;
845 default: ++stale_neutral_flags; break;
851 stale_flags = (stale_neutral_flags >= 1);
853 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
855 if(ctf_oneflag && stale_flags == 1)
856 ctf_stalemate = true;
857 else if(stale_flags >= 2)
858 ctf_stalemate = true;
859 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
860 { ctf_stalemate = false; wpforenemy_announced = false; }
861 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
862 { ctf_stalemate = false; wpforenemy_announced = false; }
864 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
867 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
869 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
871 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);
872 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
873 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
877 if (!wpforenemy_announced)
879 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)); });
881 wpforenemy_announced = true;
886 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
888 if(ITEM_DAMAGE_NEEDKILL(deathtype))
890 if(autocvar_g_ctf_flag_return_damage_delay)
891 this.ctf_flagdamaged_byworld = true;
894 SetResourceExplicit(this, RES_HEALTH, 0);
895 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
899 if(autocvar_g_ctf_flag_return_damage)
901 // reduce health and check if it should be returned
902 TakeResource(this, RES_HEALTH, damage);
903 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
908 void ctf_FlagThink(entity this)
913 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
916 if(this == ctf_worldflaglist) // only for the first flag
917 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
920 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
921 LOG_TRACE("wtf the flag got squashed?");
922 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
923 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
924 setsize(this, this.m_mins, this.m_maxs);
928 switch(this.ctf_status)
932 if(autocvar_g_ctf_dropped_capture_radius)
934 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
935 if(tmp_entity.ctf_status == FLAG_DROPPED)
936 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
937 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
938 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
945 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
947 if(autocvar_g_ctf_flag_dropped_floatinwater)
949 vector midpoint = ((this.absmin + this.absmax) * 0.5);
950 if(pointcontents(midpoint) == CONTENT_WATER)
952 this.velocity = this.velocity * 0.5;
954 if (pointcontents(midpoint + eZ * FLAG_FLOAT_OFFSET_Z) == CONTENT_WATER)
955 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
957 { set_movetype(this, MOVETYPE_FLY); }
959 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
961 if(autocvar_g_ctf_flag_return_dropped)
963 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
965 SetResourceExplicit(this, RES_HEALTH, 0);
966 ctf_CheckFlagReturn(this, RETURN_DROPPED);
970 if(this.ctf_flagdamaged_byworld)
972 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
973 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
976 else if(autocvar_g_ctf_flag_return_time)
978 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
979 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
987 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
989 SetResourceExplicit(this, RES_HEALTH, 0);
990 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
992 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
993 ImpulseCommands(this.owner);
995 if(autocvar_g_ctf_stalemate)
997 if(time >= wpforenemy_nextthink)
999 ctf_CheckStalemate();
1000 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1003 if(CTF_SAMETEAM(this, this.owner) && this.team)
1005 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1006 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
1007 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
1008 ctf_Handle_Return(this, this.owner);
1015 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1016 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1017 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1019 if((this.pass_target == NULL)
1020 || (IS_DEAD(this.pass_target))
1021 || (this.pass_target.flagcarried)
1022 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1023 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1024 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1026 // give up, pass failed
1027 ctf_Handle_Drop(this, NULL, DROP_PASS);
1031 // still a viable target, go for it
1032 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1037 default: // this should never happen
1039 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1045 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1048 if(game_stopped) return;
1049 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1051 bool is_not_monster = (!IS_MONSTER(toucher));
1053 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1054 if(ITEM_TOUCH_NEEDKILL())
1056 if(!autocvar_g_ctf_flag_return_damage_delay)
1058 SetResourceExplicit(flag, RES_HEALTH, 0);
1059 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1061 if(!flag.ctf_flagdamaged_byworld) { return; }
1064 // special touch behaviors
1065 if(STAT(FROZEN, toucher)) { return; }
1066 else if(IS_VEHICLE(toucher))
1068 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1069 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1071 return; // do nothing
1073 else if(IS_MONSTER(toucher))
1075 if(!autocvar_g_ctf_allow_monster_touch)
1076 return; // do nothing
1078 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1080 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1082 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1083 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1084 flag.wait = time + FLAG_TOUCHRATE;
1088 else if(IS_DEAD(toucher)) { return; }
1090 switch(flag.ctf_status)
1096 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1097 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1098 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1099 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1101 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1102 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1103 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)
1105 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1106 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1108 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1109 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1115 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1116 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1117 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1118 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1124 LOG_TRACE("Someone touched a flag even though it was being carried?");
1130 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1132 if(DIFF_TEAM(toucher, flag.pass_sender))
1134 if(ctf_Immediate_Return_Allowed(flag, toucher))
1135 ctf_Handle_Return(flag, toucher);
1136 else if(is_not_monster && (!toucher.flagcarried))
1137 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1139 else if(!toucher.flagcarried)
1140 ctf_Handle_Retrieve(flag, toucher);
1147 .float last_respawn;
1148 void ctf_RespawnFlag(entity flag)
1150 // check for flag respawn being called twice in a row
1151 if(flag.last_respawn > time - 0.5)
1152 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1154 flag.last_respawn = time;
1156 // reset the player (if there is one)
1157 if((flag.owner) && (flag.owner.flagcarried == flag))
1159 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1160 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1161 WaypointSprite_Kill(flag.wps_flagcarrier);
1163 flag.owner.flagcarried = NULL;
1164 GameRules_scoring_vip(flag.owner, false);
1166 if(flag.speedrunning)
1167 ctf_FakeTimeLimit(flag.owner, -1);
1170 if((flag.owner) && (flag.owner.vehicle))
1171 flag.scale = FLAG_SCALE;
1173 if(flag.ctf_status == FLAG_DROPPED)
1174 { WaypointSprite_Kill(flag.wps_flagdropped); }
1177 setattachment(flag, NULL, "");
1178 setorigin(flag, flag.ctf_spawnorigin);
1180 //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1181 set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1182 flag.takedamage = DAMAGE_NO;
1183 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1184 flag.solid = SOLID_TRIGGER;
1185 flag.velocity = '0 0 0';
1186 flag.angles = flag.mangle;
1187 flag.flags = FL_ITEM | FL_NOTARGET;
1189 flag.ctf_status = FLAG_BASE;
1191 flag.pass_distance = 0;
1192 flag.pass_sender = NULL;
1193 flag.pass_target = NULL;
1194 flag.ctf_dropper = NULL;
1195 flag.ctf_pickuptime = 0;
1196 flag.ctf_droptime = 0;
1197 flag.ctf_flagdamaged_byworld = false;
1198 navigation_dynamicgoal_unset(flag);
1200 ctf_CheckStalemate();
1203 void ctf_Reset(entity this)
1205 if(this.owner && IS_PLAYER(this.owner))
1206 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1209 ctf_RespawnFlag(this);
1212 bool ctf_FlagBase_Customize(entity this, entity client)
1214 entity e = WaypointSprite_getviewentity(client);
1215 entity wp_owner = this.owner;
1216 entity flag = e.flagcarried;
1217 if(flag && CTF_SAMETEAM(e, flag))
1219 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1224 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1227 waypoint_spawnforitem_force(this, this.origin);
1228 navigation_dynamicgoal_init(this, true);
1234 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1235 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1236 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1237 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1238 default: basename = WP_FlagBaseNeutral; break;
1241 if(autocvar_g_ctf_flag_waypoint)
1243 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1244 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1245 wp.fade_rate = autocvar_g_ctf_flag_waypoint_maxdistance;
1246 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1247 setcefc(wp, ctf_FlagBase_Customize);
1250 // captureshield setup
1251 ctf_CaptureShield_Spawn(this);
1256 void ctf_FlagSetup(int teamnum, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1259 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1260 ctf_worldflaglist = flag;
1262 setattachment(flag, NULL, "");
1264 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnum), Team_ColorName_Upper(teamnum)));
1265 flag.team = teamnum;
1266 flag.classname = "item_flag_team";
1267 flag.target = "###item###"; // for finding the nearest item using findnearest
1268 flag.flags = FL_ITEM | FL_NOTARGET;
1269 IL_PUSH(g_items, flag);
1270 flag.solid = SOLID_TRIGGER;
1271 flag.takedamage = DAMAGE_NO;
1272 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1273 flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1274 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1275 flag.event_damage = ctf_FlagDamage;
1276 flag.pushable = true;
1277 flag.teleportable = TELEPORT_NORMAL;
1278 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1279 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1280 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1281 if(flag.damagedbycontents)
1282 IL_PUSH(g_damagedbycontents, flag);
1283 flag.velocity = '0 0 0';
1284 flag.mangle = flag.angles;
1285 flag.reset = ctf_Reset;
1286 settouch(flag, ctf_FlagTouch);
1287 setthink(flag, ctf_FlagThink);
1288 flag.nextthink = time + FLAG_THINKRATE;
1289 flag.ctf_status = FLAG_BASE;
1291 // crudely force them all to 0
1292 if(autocvar_g_ctf_score_ignore_fields)
1293 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1295 string teamname = Static_Team_ColorName_Lower(teamnum);
1297 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1298 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1299 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1300 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnum).eent_eff_name; }
1301 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnum).eent_eff_name; }
1302 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnum).eent_eff_name; }
1306 if(flag.s == "") flag.s = b; \
1307 precache_sound(flag.s);
1309 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnum))))
1310 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnum))))
1311 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnum))))
1312 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnum))))
1313 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1314 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1315 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1319 precache_model(flag.model);
1322 _setmodel(flag, flag.model); // precision set below
1323 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1324 flag.m_mins = flag.mins; // store these for squash checks
1325 flag.m_maxs = flag.maxs;
1326 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1328 if(autocvar_g_ctf_flag_glowtrails)
1332 case NUM_TEAM_1: flag.glow_color = 251; break;
1333 case NUM_TEAM_2: flag.glow_color = 210; break;
1334 case NUM_TEAM_3: flag.glow_color = 110; break;
1335 case NUM_TEAM_4: flag.glow_color = 145; break;
1336 default: flag.glow_color = 254; break;
1338 flag.glow_size = 25;
1339 flag.glow_trail = 1;
1342 flag.effects |= EF_LOWPRECISION;
1343 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1344 if(autocvar_g_ctf_dynamiclights)
1348 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1349 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1350 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1351 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1352 default: flag.effects |= EF_DIMLIGHT; break;
1357 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1359 flag.dropped_origin = flag.origin;
1360 flag.noalign = true;
1361 set_movetype(flag, MOVETYPE_NONE);
1363 else // drop to floor, automatically find a platform and set that as spawn origin
1365 flag.noalign = false;
1367 set_movetype(flag, MOVETYPE_NONE);
1370 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1378 // NOTE: LEGACY CODE, needs to be re-written!
1380 void havocbot_ctf_calculate_middlepoint()
1384 vector fo = '0 0 0';
1387 f = ctf_worldflaglist;
1392 f = f.ctf_worldflagnext;
1398 havocbot_middlepoint = s / n;
1399 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1401 havocbot_symmetry_axis_m = 0;
1402 havocbot_symmetry_axis_q = 0;
1405 // for symmetrical editing of waypoints
1406 entity f1 = ctf_worldflaglist;
1407 entity f2 = f1.ctf_worldflagnext;
1408 float m = -(f1.origin.y - f2.origin.y) / (max(f1.origin.x - f2.origin.x, FLOAT_EPSILON));
1409 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1410 havocbot_symmetry_axis_m = m;
1411 havocbot_symmetry_axis_q = q;
1413 havocbot_symmetry_origin_order = n;
1417 entity havocbot_ctf_find_flag(entity bot)
1420 f = ctf_worldflaglist;
1423 if (CTF_SAMETEAM(bot, f))
1425 f = f.ctf_worldflagnext;
1430 entity havocbot_ctf_find_enemy_flag(entity bot)
1433 f = ctf_worldflaglist;
1438 if(CTF_DIFFTEAM(bot, f))
1445 else if(!bot.flagcarried)
1449 else if (CTF_DIFFTEAM(bot, f))
1451 f = f.ctf_worldflagnext;
1456 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1463 FOREACH_CLIENT(IS_PLAYER(it), {
1464 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1467 if(vdist(it.origin - org, <, tc_radius))
1476 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1479 head = ctf_worldflaglist;
1482 if (CTF_SAMETEAM(this, head))
1484 head = head.ctf_worldflagnext;
1487 navigation_routerating(this, head, ratingscale, 10000);
1491 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1494 head = ctf_worldflaglist;
1497 if (CTF_SAMETEAM(this, head))
1499 if (this.flagcarried)
1500 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1502 head = head.ctf_worldflagnext; // skip base if it has a different group
1507 head = head.ctf_worldflagnext;
1512 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1515 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1518 head = ctf_worldflaglist;
1523 if(CTF_DIFFTEAM(this, head))
1527 if(this.flagcarried)
1530 else if(!this.flagcarried)
1534 else if(CTF_DIFFTEAM(this, head))
1536 head = head.ctf_worldflagnext;
1540 if (head.ctf_status == FLAG_CARRY)
1542 // adjust rating of our flag carrier depending on his health
1543 head = head.tag_entity;
1544 float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1545 ratingscale += ratingscale * f * 0.1;
1547 navigation_routerating(this, head, ratingscale, 10000);
1551 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1553 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1555 if (!bot_waypoints_for_items)
1557 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1563 head = havocbot_ctf_find_enemy_flag(this);
1568 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1571 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1575 mf = havocbot_ctf_find_flag(this);
1577 if(mf.ctf_status == FLAG_BASE)
1581 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1584 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1587 head = ctf_worldflaglist;
1590 // flag is out in the field
1591 if(head.ctf_status != FLAG_BASE)
1592 if(head.tag_entity==NULL) // dropped
1596 if(vdist(org - head.origin, <, df_radius))
1597 navigation_routerating(this, head, ratingscale, 10000);
1600 navigation_routerating(this, head, ratingscale, 10000);
1603 head = head.ctf_worldflagnext;
1607 void havocbot_ctf_reset_role(entity this)
1609 float cdefense, cmiddle, coffense;
1616 if (this.flagcarried)
1618 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1622 mf = havocbot_ctf_find_flag(this);
1623 ef = havocbot_ctf_find_enemy_flag(this);
1625 // Retrieve stolen flag
1626 if(mf.ctf_status!=FLAG_BASE)
1628 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1632 // If enemy flag is taken go to the middle to intercept pursuers
1633 if(ef.ctf_status!=FLAG_BASE)
1635 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1639 // if there is no one else on the team switch to offense
1641 // don't check if this bot is a player since it isn't true when the bot is added to the server
1642 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1646 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1649 else if (time < CS(this).jointime + 1)
1651 // if bots spawn all at once set good default roles
1654 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1657 else if (count == 2)
1659 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1664 // Evaluate best position to take
1665 // Count mates on middle position
1666 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1668 // Count mates on defense position
1669 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1671 // Count mates on offense position
1672 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1674 if(cdefense<=coffense)
1675 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1676 else if(coffense<=cmiddle)
1677 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1679 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1681 // if bots spawn all at once assign them a more appropriated role after a while
1682 if (time < CS(this).jointime + 1 && count > 2)
1683 this.havocbot_role_timeout = time + 10 + random() * 10;
1686 bool havocbot_ctf_is_basewaypoint(entity item)
1688 if (item.classname != "waypoint")
1691 entity head = ctf_worldflaglist;
1694 if (item == head.bot_basewaypoint)
1696 head = head.ctf_worldflagnext;
1701 void havocbot_role_ctf_carrier(entity this)
1705 havocbot_ctf_reset_role(this);
1709 if (this.flagcarried == NULL)
1711 havocbot_ctf_reset_role(this);
1715 if (navigation_goalrating_timeout(this))
1717 navigation_goalrating_start(this);
1720 entity mf = havocbot_ctf_find_flag(this);
1721 vector base_org = mf.dropped_origin;
1722 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1724 havocbot_goalrating_ctf_enemybase(this, base_rating);
1726 havocbot_goalrating_ctf_ourbase(this, base_rating);
1728 // start collecting items very close to the bot but only inside of own base radius
1729 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1730 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1732 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1734 navigation_goalrating_end(this);
1736 navigation_goalrating_timeout_set(this);
1738 entity goal = this.goalentity;
1739 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1740 this.goalentity_lock_timeout = time + ((this.bot_aimtarg) ? 2 : 3);
1743 this.havocbot_cantfindflag = time + 10;
1744 else if (time > this.havocbot_cantfindflag)
1746 // Can't navigate to my own base, suicide!
1747 // TODO: drop it and wander around
1748 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1754 void havocbot_role_ctf_escort(entity this)
1760 havocbot_ctf_reset_role(this);
1764 if (this.flagcarried)
1766 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1770 // If enemy flag is back on the base switch to previous role
1771 ef = havocbot_ctf_find_enemy_flag(this);
1772 if(ef.ctf_status==FLAG_BASE)
1774 this.havocbot_role = this.havocbot_previous_role;
1775 this.havocbot_role_timeout = 0;
1778 if (ef.ctf_status == FLAG_DROPPED)
1780 navigation_goalrating_timeout_expire(this, 1);
1784 // If the flag carrier reached the base switch to defense
1785 mf = havocbot_ctf_find_flag(this);
1786 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1788 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1792 // Set the role timeout if necessary
1793 if (!this.havocbot_role_timeout)
1795 this.havocbot_role_timeout = time + random() * 30 + 60;
1798 // If nothing happened just switch to previous role
1799 if (time > this.havocbot_role_timeout)
1801 this.havocbot_role = this.havocbot_previous_role;
1802 this.havocbot_role_timeout = 0;
1806 // Chase the flag carrier
1807 if (navigation_goalrating_timeout(this))
1809 navigation_goalrating_start(this);
1812 havocbot_goalrating_ctf_enemyflag(this, 10000);
1813 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1814 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1816 navigation_goalrating_end(this);
1818 navigation_goalrating_timeout_set(this);
1822 void havocbot_role_ctf_offense(entity this)
1829 havocbot_ctf_reset_role(this);
1833 if (this.flagcarried)
1835 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1840 mf = havocbot_ctf_find_flag(this);
1841 ef = havocbot_ctf_find_enemy_flag(this);
1844 if(mf.ctf_status!=FLAG_BASE)
1847 pos = mf.tag_entity.origin;
1851 // Try to get it if closer than the enemy base
1852 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1854 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1859 // Escort flag carrier
1860 if(ef.ctf_status!=FLAG_BASE)
1863 pos = ef.tag_entity.origin;
1867 if(vdist(pos - mf.dropped_origin, >, 700))
1869 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1874 // Set the role timeout if necessary
1875 if (!this.havocbot_role_timeout)
1876 this.havocbot_role_timeout = time + 120;
1878 if (time > this.havocbot_role_timeout)
1880 havocbot_ctf_reset_role(this);
1884 if (navigation_goalrating_timeout(this))
1886 navigation_goalrating_start(this);
1889 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1890 havocbot_goalrating_ctf_enemybase(this, 10000);
1891 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1893 navigation_goalrating_end(this);
1895 navigation_goalrating_timeout_set(this);
1899 // Retriever (temporary role):
1900 void havocbot_role_ctf_retriever(entity this)
1906 havocbot_ctf_reset_role(this);
1910 if (this.flagcarried)
1912 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1916 // If flag is back on the base switch to previous role
1917 mf = havocbot_ctf_find_flag(this);
1918 if(mf.ctf_status==FLAG_BASE)
1920 if (mf.enemy == this) // did this bot return the flag?
1921 navigation_goalrating_timeout_force(this);
1922 havocbot_ctf_reset_role(this);
1926 if (!this.havocbot_role_timeout)
1927 this.havocbot_role_timeout = time + 20;
1929 if (time > this.havocbot_role_timeout)
1931 havocbot_ctf_reset_role(this);
1935 if (navigation_goalrating_timeout(this))
1937 const float RT_RADIUS = 10000;
1939 navigation_goalrating_start(this);
1942 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1943 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1944 havocbot_goalrating_ctf_enemybase(this, 8000);
1945 entity ef = havocbot_ctf_find_enemy_flag(this);
1946 vector enemy_base_org = ef.dropped_origin;
1947 // start collecting items very close to the bot but only inside of enemy base radius
1948 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1949 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1950 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1952 navigation_goalrating_end(this);
1954 navigation_goalrating_timeout_set(this);
1958 void havocbot_role_ctf_middle(entity this)
1964 havocbot_ctf_reset_role(this);
1968 if (this.flagcarried)
1970 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1974 mf = havocbot_ctf_find_flag(this);
1975 if(mf.ctf_status!=FLAG_BASE)
1977 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1981 if (!this.havocbot_role_timeout)
1982 this.havocbot_role_timeout = time + 10;
1984 if (time > this.havocbot_role_timeout)
1986 havocbot_ctf_reset_role(this);
1990 if (navigation_goalrating_timeout(this))
1994 org = havocbot_middlepoint;
1995 org.z = this.origin.z;
1997 navigation_goalrating_start(this);
2000 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
2001 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
2002 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2003 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2004 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2005 havocbot_goalrating_ctf_enemybase(this, 3000);
2007 navigation_goalrating_end(this);
2009 entity goal = this.goalentity;
2010 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
2011 this.goalentity_lock_timeout = time + 2;
2013 navigation_goalrating_timeout_set(this);
2017 void havocbot_role_ctf_defense(entity this)
2023 havocbot_ctf_reset_role(this);
2027 if (this.flagcarried)
2029 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2033 // If own flag was captured
2034 mf = havocbot_ctf_find_flag(this);
2035 if(mf.ctf_status!=FLAG_BASE)
2037 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2041 if (!this.havocbot_role_timeout)
2042 this.havocbot_role_timeout = time + 30;
2044 if (time > this.havocbot_role_timeout)
2046 havocbot_ctf_reset_role(this);
2049 if (navigation_goalrating_timeout(this))
2051 vector org = mf.dropped_origin;
2053 navigation_goalrating_start(this);
2055 // if enemies are closer to our base, go there
2056 entity closestplayer = NULL;
2057 float distance, bestdistance = 10000;
2058 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2059 distance = vlen(org - it.origin);
2060 if(distance<bestdistance)
2063 bestdistance = distance;
2069 if(DIFF_TEAM(closestplayer, this))
2070 if(vdist(org - this.origin, >, 1000))
2071 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2072 havocbot_goalrating_ctf_ourbase(this, 10000);
2074 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2075 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2076 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2077 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2078 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2080 navigation_goalrating_end(this);
2082 navigation_goalrating_timeout_set(this);
2086 void havocbot_role_ctf_setrole(entity bot, int role)
2088 string s = "(null)";
2091 case HAVOCBOT_CTF_ROLE_CARRIER:
2093 bot.havocbot_role = havocbot_role_ctf_carrier;
2094 bot.havocbot_role_timeout = 0;
2095 bot.havocbot_cantfindflag = time + 10;
2096 if (bot.havocbot_previous_role != bot.havocbot_role)
2097 navigation_goalrating_timeout_force(bot);
2099 case HAVOCBOT_CTF_ROLE_DEFENSE:
2101 bot.havocbot_role = havocbot_role_ctf_defense;
2102 bot.havocbot_role_timeout = 0;
2104 case HAVOCBOT_CTF_ROLE_MIDDLE:
2106 bot.havocbot_role = havocbot_role_ctf_middle;
2107 bot.havocbot_role_timeout = 0;
2109 case HAVOCBOT_CTF_ROLE_OFFENSE:
2111 bot.havocbot_role = havocbot_role_ctf_offense;
2112 bot.havocbot_role_timeout = 0;
2114 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2116 bot.havocbot_previous_role = bot.havocbot_role;
2117 bot.havocbot_role = havocbot_role_ctf_retriever;
2118 bot.havocbot_role_timeout = time + 10;
2119 if (bot.havocbot_previous_role != bot.havocbot_role)
2120 navigation_goalrating_timeout_expire(bot, 2);
2122 case HAVOCBOT_CTF_ROLE_ESCORT:
2124 bot.havocbot_previous_role = bot.havocbot_role;
2125 bot.havocbot_role = havocbot_role_ctf_escort;
2126 bot.havocbot_role_timeout = time + 30;
2127 if (bot.havocbot_previous_role != bot.havocbot_role)
2128 navigation_goalrating_timeout_expire(bot, 2);
2131 LOG_TRACE(bot.netname, " switched to ", s);
2139 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2141 entity player = M_ARGV(0, entity);
2143 int t = 0, t2 = 0, t3 = 0;
2144 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)
2146 // initially clear items so they can be set as necessary later.
2147 STAT(CTF_FLAGSTATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2148 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2149 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2150 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2151 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2152 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2154 // scan through all the flags and notify the client about them
2155 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2157 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2158 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2159 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2160 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2161 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; }
2163 switch(flag.ctf_status)
2168 if((flag.owner == player) || (flag.pass_sender == player))
2169 STAT(CTF_FLAGSTATUS, player) |= t; // carrying: player is currently carrying the flag
2171 STAT(CTF_FLAGSTATUS, player) |= t2; // taken: someone else is carrying the flag
2176 STAT(CTF_FLAGSTATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2182 // item for stopping players from capturing the flag too often
2183 if(player.ctf_captureshielded)
2184 STAT(CTF_FLAGSTATUS, player) |= CTF_SHIELDED;
2187 STAT(CTF_FLAGSTATUS, player) |= CTF_STALEMATE;
2189 // update the health of the flag carrier waypointsprite
2190 if(player.wps_flagcarrier)
2191 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);
2194 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2196 entity frag_attacker = M_ARGV(1, entity);
2197 entity frag_target = M_ARGV(2, entity);
2198 float frag_damage = M_ARGV(4, float);
2199 vector frag_force = M_ARGV(6, vector);
2201 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2203 if(frag_target == frag_attacker) // damage done to yourself
2205 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2206 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2208 else // damage done to everyone else
2210 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2211 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2214 M_ARGV(4, float) = frag_damage;
2215 M_ARGV(6, vector) = frag_force;
2217 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2219 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
2220 && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2222 frag_target.wps_helpme_time = time;
2223 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2225 // todo: add notification for when flag carrier needs help?
2229 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2231 entity frag_attacker = M_ARGV(1, entity);
2232 entity frag_target = M_ARGV(2, entity);
2234 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2236 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2237 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2240 if(frag_target.flagcarried)
2242 entity tmp_entity = frag_target.flagcarried;
2243 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2244 tmp_entity.ctf_dropper = NULL;
2248 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2250 M_ARGV(2, float) = 0; // frag score
2251 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2254 void ctf_RemovePlayer(entity player)
2256 if(player.flagcarried)
2257 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2259 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2261 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2262 if(flag.pass_target == player) { flag.pass_target = NULL; }
2263 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2267 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2269 entity player = M_ARGV(0, entity);
2271 ctf_RemovePlayer(player);
2274 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2276 entity player = M_ARGV(0, entity);
2278 ctf_RemovePlayer(player);
2281 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2283 if(!autocvar_g_ctf_leaderboard)
2286 entity player = M_ARGV(0, entity);
2288 if(IS_REAL_CLIENT(player))
2290 int m = min(RANKINGS_CNT, autocvar_g_cts_send_rankings_cnt);
2291 race_send_rankings_cnt(MSG_ONE);
2292 for (int i = 1; i <= m; ++i)
2294 race_SendRankings(i, 0, 0, MSG_ONE);
2299 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2301 if(!autocvar_g_ctf_leaderboard)
2304 entity player = M_ARGV(0, entity);
2306 if(CS(player).cvar_cl_allow_uidtracking == 1 && CS(player).cvar_cl_allow_uid2name == 1)
2308 if (!player.stored_netname)
2309 player.stored_netname = strzone(uid2name(player.crypto_idfp));
2310 if(player.stored_netname != player.netname)
2312 db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
2313 strcpy(player.stored_netname, player.netname);
2318 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2320 entity player = M_ARGV(0, entity);
2322 if(player.flagcarried)
2323 if(!autocvar_g_ctf_portalteleport)
2324 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2327 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2329 if(MUTATOR_RETURNVALUE || game_stopped) return;
2331 entity player = M_ARGV(0, entity);
2333 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2335 // pass the flag to a team mate
2336 if(autocvar_g_ctf_pass)
2338 entity head, closest_target = NULL;
2339 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2341 while(head) // find the closest acceptable target to pass to
2343 if(IS_PLAYER(head) && !IS_DEAD(head))
2344 if(head != player && SAME_TEAM(head, player))
2345 if(!head.speedrunning && !head.vehicle)
2347 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2348 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2349 vector passer_center = CENTER_OR_VIEWOFS(player);
2351 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2353 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2355 if(IS_BOT_CLIENT(head))
2357 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2358 ctf_Handle_Throw(head, player, DROP_PASS);
2362 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2363 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2365 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2368 else if(player.flagcarried && !head.flagcarried)
2372 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2373 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2374 { closest_target = head; }
2376 else { closest_target = head; }
2383 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2386 // throw the flag in front of you
2387 if(autocvar_g_ctf_throw && player.flagcarried)
2389 if(player.throw_count == -1)
2391 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2393 player.throw_prevtime = time;
2394 player.throw_count = 1;
2395 ctf_Handle_Throw(player, NULL, DROP_THROW);
2400 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2406 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2407 else { player.throw_count += 1; }
2408 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2410 player.throw_prevtime = time;
2411 ctf_Handle_Throw(player, NULL, DROP_THROW);
2418 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2420 entity player = M_ARGV(0, entity);
2422 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2424 player.wps_helpme_time = time;
2425 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2427 else // create a normal help me waypointsprite
2429 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2430 WaypointSprite_Ping(player.wps_helpme);
2436 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2438 entity player = M_ARGV(0, entity);
2439 entity veh = M_ARGV(1, entity);
2441 if(player.flagcarried)
2443 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2445 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2449 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2450 setattachment(player.flagcarried, veh, "");
2451 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2452 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2453 //player.flagcarried.angles = '0 0 0';
2459 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2461 entity player = M_ARGV(0, entity);
2463 if(player.flagcarried)
2465 setattachment(player.flagcarried, player, "");
2466 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2467 player.flagcarried.scale = FLAG_SCALE;
2468 player.flagcarried.angles = '0 0 0';
2469 player.flagcarried.nodrawtoclient = NULL;
2474 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2476 entity player = M_ARGV(0, entity);
2478 if(player.flagcarried)
2480 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2481 ctf_RespawnFlag(player.flagcarried);
2486 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2488 entity flag; // temporary entity for the search method
2490 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2492 switch(flag.ctf_status)
2497 // lock the flag, game is over
2498 set_movetype(flag, MOVETYPE_NONE);
2499 flag.takedamage = DAMAGE_NO;
2500 flag.solid = SOLID_NOT;
2501 flag.nextthink = false; // stop thinking
2503 //dprint("stopping the ", flag.netname, " from moving.\n");
2511 // do nothing for these flags
2518 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2520 entity bot = M_ARGV(0, entity);
2522 havocbot_ctf_reset_role(bot);
2526 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2528 M_ARGV(1, string) = "ctf_team";
2531 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2533 entity spectatee = M_ARGV(0, entity);
2534 entity client = M_ARGV(1, entity);
2536 STAT(CTF_FLAGSTATUS, client) = STAT(CTF_FLAGSTATUS, spectatee);
2539 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2541 int record_page = M_ARGV(0, int);
2542 string ret_string = M_ARGV(1, string);
2544 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2546 if (MapInfo_Get_ByID(i))
2548 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2554 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2555 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2559 M_ARGV(1, string) = ret_string;
2562 bool superspec_Spectate(entity this, entity targ); // TODO
2563 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2564 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2566 entity player = M_ARGV(0, entity);
2567 string cmd_name = M_ARGV(1, string);
2568 int cmd_argc = M_ARGV(2, int);
2570 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2572 if(cmd_name == "followfc")
2584 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2585 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2586 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2587 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2591 FOREACH_CLIENT(IS_PLAYER(it), {
2592 if(it.flagcarried && (it.team == _team || _team == 0))
2595 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2596 continue; // already spectating this fc, try another
2597 return superspec_Spectate(player, it);
2602 superspec_msg("", "", player, "No active flag carrier\n", 1);
2607 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2609 entity frag_target = M_ARGV(0, entity);
2611 if(frag_target.flagcarried)
2612 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2615 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2617 entity player = M_ARGV(0, entity);
2618 if(player.flagcarried)
2619 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2627 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2628 CTF flag for team one (Red).
2630 "angle" Angle the flag will point (minus 90 degrees)...
2631 "model" model to use, note this needs red and blue as skins 0 and 1...
2632 "noise" sound played when flag is picked up...
2633 "noise1" sound played when flag is returned by a teammate...
2634 "noise2" sound played when flag is captured...
2635 "noise3" sound played when flag is lost in the field and respawns itself...
2636 "noise4" sound played when flag is dropped by a player...
2637 "noise5" sound played when flag touches the ground... */
2638 spawnfunc(item_flag_team1)
2640 if(!g_ctf) { delete(this); return; }
2642 ctf_FlagSetup(NUM_TEAM_1, this);
2645 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2646 CTF flag for team two (Blue).
2648 "angle" Angle the flag will point (minus 90 degrees)...
2649 "model" model to use, note this needs red and blue as skins 0 and 1...
2650 "noise" sound played when flag is picked up...
2651 "noise1" sound played when flag is returned by a teammate...
2652 "noise2" sound played when flag is captured...
2653 "noise3" sound played when flag is lost in the field and respawns itself...
2654 "noise4" sound played when flag is dropped by a player...
2655 "noise5" sound played when flag touches the ground... */
2656 spawnfunc(item_flag_team2)
2658 if(!g_ctf) { delete(this); return; }
2660 ctf_FlagSetup(NUM_TEAM_2, this);
2663 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2664 CTF flag for team three (Yellow).
2666 "angle" Angle the flag will point (minus 90 degrees)...
2667 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2668 "noise" sound played when flag is picked up...
2669 "noise1" sound played when flag is returned by a teammate...
2670 "noise2" sound played when flag is captured...
2671 "noise3" sound played when flag is lost in the field and respawns itself...
2672 "noise4" sound played when flag is dropped by a player...
2673 "noise5" sound played when flag touches the ground... */
2674 spawnfunc(item_flag_team3)
2676 if(!g_ctf) { delete(this); return; }
2678 ctf_FlagSetup(NUM_TEAM_3, this);
2681 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2682 CTF flag for team four (Pink).
2684 "angle" Angle the flag will point (minus 90 degrees)...
2685 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2686 "noise" sound played when flag is picked up...
2687 "noise1" sound played when flag is returned by a teammate...
2688 "noise2" sound played when flag is captured...
2689 "noise3" sound played when flag is lost in the field and respawns itself...
2690 "noise4" sound played when flag is dropped by a player...
2691 "noise5" sound played when flag touches the ground... */
2692 spawnfunc(item_flag_team4)
2694 if(!g_ctf) { delete(this); return; }
2696 ctf_FlagSetup(NUM_TEAM_4, this);
2699 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2702 "angle" Angle the flag will point (minus 90 degrees)...
2703 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2704 "noise" sound played when flag is picked up...
2705 "noise1" sound played when flag is returned by a teammate...
2706 "noise2" sound played when flag is captured...
2707 "noise3" sound played when flag is lost in the field and respawns itself...
2708 "noise4" sound played when flag is dropped by a player...
2709 "noise5" sound played when flag touches the ground... */
2710 spawnfunc(item_flag_neutral)
2712 if(!g_ctf) { delete(this); return; }
2713 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2715 ctf_FlagSetup(0, this);
2718 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2719 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2720 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.
2722 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2723 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2726 if(!g_ctf) { delete(this); return; }
2728 this.classname = "ctf_team";
2729 this.team = this.cnt + 1;
2732 // compatibility for quake maps
2733 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2734 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2735 spawnfunc(info_player_team1);
2736 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2737 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2738 spawnfunc(info_player_team2);
2739 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2740 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2742 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2743 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2745 // compatibility for wop maps
2746 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2747 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2748 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2749 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2750 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2751 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2759 void ctf_ScoreRules(int teams)
2761 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2762 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2763 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2764 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2765 field(SP_CTF_PICKUPS, "pickups", 0);
2766 field(SP_CTF_FCKILLS, "fckills", 0);
2767 field(SP_CTF_RETURNS, "returns", 0);
2768 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2772 // code from here on is just to support maps that don't have flag and team entities
2773 void ctf_SpawnTeam (string teamname, int teamcolor)
2775 entity this = new_pure(ctf_team);
2776 this.netname = teamname;
2777 this.cnt = teamcolor - 1;
2778 this.spawnfunc_checked = true;
2779 this.team = teamcolor;
2782 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2787 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2789 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2790 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2792 switch(tmp_entity.team)
2794 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2795 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2796 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2797 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2799 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2802 havocbot_ctf_calculate_middlepoint();
2804 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2806 ctf_teams = 0; // so set the default red and blue teams
2807 BITSET_ASSIGN(ctf_teams, BIT(0));
2808 BITSET_ASSIGN(ctf_teams, BIT(1));
2811 //ctf_teams = bound(2, ctf_teams, 4);
2813 // if no teams are found, spawn defaults
2814 if(find(NULL, classname, "ctf_team") == NULL)
2816 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2817 if(ctf_teams & BIT(0))
2818 ctf_SpawnTeam("Red", NUM_TEAM_1);
2819 if(ctf_teams & BIT(1))
2820 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2821 if(ctf_teams & BIT(2))
2822 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2823 if(ctf_teams & BIT(3))
2824 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2827 ctf_ScoreRules(ctf_teams);
2830 void ctf_Initialize()
2832 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2834 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2835 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2836 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2838 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);