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 flag.watertype = CONTENT_EMPTY;
1151 // check for flag respawn being called twice in a row
1152 if(flag.last_respawn > time - 0.5)
1153 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1155 flag.last_respawn = time;
1157 // reset the player (if there is one)
1158 if((flag.owner) && (flag.owner.flagcarried == flag))
1160 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1161 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1162 WaypointSprite_Kill(flag.wps_flagcarrier);
1164 flag.owner.flagcarried = NULL;
1165 GameRules_scoring_vip(flag.owner, false);
1167 if(flag.speedrunning)
1168 ctf_FakeTimeLimit(flag.owner, -1);
1171 if((flag.owner) && (flag.owner.vehicle))
1172 flag.scale = FLAG_SCALE;
1174 if(flag.ctf_status == FLAG_DROPPED)
1175 { WaypointSprite_Kill(flag.wps_flagdropped); }
1178 setattachment(flag, NULL, "");
1179 setorigin(flag, flag.ctf_spawnorigin);
1181 //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1182 set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1183 flag.takedamage = DAMAGE_NO;
1184 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1185 flag.solid = SOLID_TRIGGER;
1186 flag.velocity = '0 0 0';
1187 flag.angles = flag.mangle;
1188 flag.flags = FL_ITEM | FL_NOTARGET;
1190 flag.ctf_status = FLAG_BASE;
1192 flag.pass_distance = 0;
1193 flag.pass_sender = NULL;
1194 flag.pass_target = NULL;
1195 flag.ctf_dropper = NULL;
1196 flag.ctf_pickuptime = 0;
1197 flag.ctf_droptime = 0;
1198 flag.ctf_flagdamaged_byworld = false;
1199 navigation_dynamicgoal_unset(flag);
1201 ctf_CheckStalemate();
1204 void ctf_Reset(entity this)
1206 if(this.owner && IS_PLAYER(this.owner))
1207 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1210 ctf_RespawnFlag(this);
1213 bool ctf_FlagBase_Customize(entity this, entity client)
1215 entity e = WaypointSprite_getviewentity(client);
1216 entity wp_owner = this.owner;
1217 entity flag = e.flagcarried;
1218 if(flag && CTF_SAMETEAM(e, flag))
1220 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1225 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1228 waypoint_spawnforitem_force(this, this.origin);
1229 navigation_dynamicgoal_init(this, true);
1235 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1236 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1237 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1238 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1239 default: basename = WP_FlagBaseNeutral; break;
1242 if(autocvar_g_ctf_flag_waypoint)
1244 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1245 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1246 wp.fade_rate = autocvar_g_ctf_flag_waypoint_maxdistance;
1247 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1248 setcefc(wp, ctf_FlagBase_Customize);
1251 // captureshield setup
1252 ctf_CaptureShield_Spawn(this);
1257 void ctf_FlagSetup(int teamnum, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1260 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1261 ctf_worldflaglist = flag;
1263 setattachment(flag, NULL, "");
1265 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnum), Team_ColorName_Upper(teamnum)));
1266 flag.team = teamnum;
1267 flag.classname = "item_flag_team";
1268 flag.target = "###item###"; // for finding the nearest item using findnearest
1269 flag.flags = FL_ITEM | FL_NOTARGET;
1270 IL_PUSH(g_items, flag);
1271 flag.solid = SOLID_TRIGGER;
1272 flag.takedamage = DAMAGE_NO;
1273 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1274 flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1275 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1276 flag.event_damage = ctf_FlagDamage;
1277 flag.pushable = true;
1278 flag.teleportable = TELEPORT_NORMAL;
1279 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1280 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1281 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1282 if(flag.damagedbycontents)
1283 IL_PUSH(g_damagedbycontents, flag);
1284 flag.velocity = '0 0 0';
1285 flag.mangle = flag.angles;
1286 flag.reset = ctf_Reset;
1287 settouch(flag, ctf_FlagTouch);
1288 setthink(flag, ctf_FlagThink);
1289 flag.nextthink = time + FLAG_THINKRATE;
1290 flag.ctf_status = FLAG_BASE;
1292 // crudely force them all to 0
1293 if(autocvar_g_ctf_score_ignore_fields)
1294 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1296 string teamname = Static_Team_ColorName_Lower(teamnum);
1298 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1299 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1300 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1301 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnum).eent_eff_name; }
1302 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnum).eent_eff_name; }
1303 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnum).eent_eff_name; }
1307 if(flag.s == "") flag.s = b; \
1308 precache_sound(flag.s);
1310 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnum))))
1311 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnum))))
1312 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnum))))
1313 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnum))))
1314 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1315 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1316 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1320 precache_model(flag.model);
1323 _setmodel(flag, flag.model); // precision set below
1324 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1325 flag.m_mins = flag.mins; // store these for squash checks
1326 flag.m_maxs = flag.maxs;
1327 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1329 if(autocvar_g_ctf_flag_glowtrails)
1333 case NUM_TEAM_1: flag.glow_color = 251; break;
1334 case NUM_TEAM_2: flag.glow_color = 210; break;
1335 case NUM_TEAM_3: flag.glow_color = 110; break;
1336 case NUM_TEAM_4: flag.glow_color = 145; break;
1337 default: flag.glow_color = 254; break;
1339 flag.glow_size = 25;
1340 flag.glow_trail = 1;
1343 flag.effects |= EF_LOWPRECISION;
1344 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1345 if(autocvar_g_ctf_dynamiclights)
1349 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1350 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1351 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1352 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1353 default: flag.effects |= EF_DIMLIGHT; break;
1358 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1360 flag.dropped_origin = flag.origin;
1361 flag.noalign = true;
1362 set_movetype(flag, MOVETYPE_NONE);
1364 else // drop to floor, automatically find a platform and set that as spawn origin
1366 flag.noalign = false;
1368 set_movetype(flag, MOVETYPE_NONE);
1371 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1379 // NOTE: LEGACY CODE, needs to be re-written!
1381 void havocbot_ctf_calculate_middlepoint()
1385 vector fo = '0 0 0';
1388 f = ctf_worldflaglist;
1393 f = f.ctf_worldflagnext;
1399 havocbot_middlepoint = s / n;
1400 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1402 havocbot_symmetry_axis_m = 0;
1403 havocbot_symmetry_axis_q = 0;
1406 // for symmetrical editing of waypoints
1407 entity f1 = ctf_worldflaglist;
1408 entity f2 = f1.ctf_worldflagnext;
1409 float m = -(f1.origin.y - f2.origin.y) / (max(f1.origin.x - f2.origin.x, FLOAT_EPSILON));
1410 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1411 havocbot_symmetry_axis_m = m;
1412 havocbot_symmetry_axis_q = q;
1414 havocbot_symmetry_origin_order = n;
1418 entity havocbot_ctf_find_flag(entity bot)
1421 f = ctf_worldflaglist;
1424 if (CTF_SAMETEAM(bot, f))
1426 f = f.ctf_worldflagnext;
1431 entity havocbot_ctf_find_enemy_flag(entity bot)
1434 f = ctf_worldflaglist;
1439 if(CTF_DIFFTEAM(bot, f))
1446 else if(!bot.flagcarried)
1450 else if (CTF_DIFFTEAM(bot, f))
1452 f = f.ctf_worldflagnext;
1457 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1464 FOREACH_CLIENT(IS_PLAYER(it), {
1465 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1468 if(vdist(it.origin - org, <, tc_radius))
1477 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1480 head = ctf_worldflaglist;
1483 if (CTF_SAMETEAM(this, head))
1485 head = head.ctf_worldflagnext;
1488 navigation_routerating(this, head, ratingscale, 10000);
1492 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1495 head = ctf_worldflaglist;
1498 if (CTF_SAMETEAM(this, head))
1500 if (this.flagcarried)
1501 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1503 head = head.ctf_worldflagnext; // skip base if it has a different group
1508 head = head.ctf_worldflagnext;
1513 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1516 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1519 head = ctf_worldflaglist;
1524 if(CTF_DIFFTEAM(this, head))
1528 if(this.flagcarried)
1531 else if(!this.flagcarried)
1535 else if(CTF_DIFFTEAM(this, head))
1537 head = head.ctf_worldflagnext;
1541 if (head.ctf_status == FLAG_CARRY)
1543 // adjust rating of our flag carrier depending on his health
1544 head = head.tag_entity;
1545 float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1546 ratingscale += ratingscale * f * 0.1;
1548 navigation_routerating(this, head, ratingscale, 10000);
1552 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1554 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1556 if (!bot_waypoints_for_items)
1558 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1564 head = havocbot_ctf_find_enemy_flag(this);
1569 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1572 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1576 mf = havocbot_ctf_find_flag(this);
1578 if(mf.ctf_status == FLAG_BASE)
1582 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1585 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1588 head = ctf_worldflaglist;
1591 // flag is out in the field
1592 if(head.ctf_status != FLAG_BASE)
1593 if(head.tag_entity==NULL) // dropped
1597 if(vdist(org - head.origin, <, df_radius))
1598 navigation_routerating(this, head, ratingscale, 10000);
1601 navigation_routerating(this, head, ratingscale, 10000);
1604 head = head.ctf_worldflagnext;
1608 void havocbot_ctf_reset_role(entity this)
1610 float cdefense, cmiddle, coffense;
1617 if (this.flagcarried)
1619 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1623 mf = havocbot_ctf_find_flag(this);
1624 ef = havocbot_ctf_find_enemy_flag(this);
1626 // Retrieve stolen flag
1627 if(mf.ctf_status!=FLAG_BASE)
1629 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1633 // If enemy flag is taken go to the middle to intercept pursuers
1634 if(ef.ctf_status!=FLAG_BASE)
1636 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1640 // if there is no one else on the team switch to offense
1642 // don't check if this bot is a player since it isn't true when the bot is added to the server
1643 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1647 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1650 else if (time < CS(this).jointime + 1)
1652 // if bots spawn all at once set good default roles
1655 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1658 else if (count == 2)
1660 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1665 // Evaluate best position to take
1666 // Count mates on middle position
1667 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1669 // Count mates on defense position
1670 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1672 // Count mates on offense position
1673 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1675 if(cdefense<=coffense)
1676 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1677 else if(coffense<=cmiddle)
1678 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1680 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1682 // if bots spawn all at once assign them a more appropriated role after a while
1683 if (time < CS(this).jointime + 1 && count > 2)
1684 this.havocbot_role_timeout = time + 10 + random() * 10;
1687 bool havocbot_ctf_is_basewaypoint(entity item)
1689 if (item.classname != "waypoint")
1692 entity head = ctf_worldflaglist;
1695 if (item == head.bot_basewaypoint)
1697 head = head.ctf_worldflagnext;
1702 void havocbot_role_ctf_carrier(entity this)
1706 havocbot_ctf_reset_role(this);
1710 if (this.flagcarried == NULL)
1712 havocbot_ctf_reset_role(this);
1716 if (navigation_goalrating_timeout(this))
1718 navigation_goalrating_start(this);
1721 entity mf = havocbot_ctf_find_flag(this);
1722 vector base_org = mf.dropped_origin;
1723 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1725 havocbot_goalrating_ctf_enemybase(this, base_rating);
1727 havocbot_goalrating_ctf_ourbase(this, base_rating);
1729 // start collecting items very close to the bot but only inside of own base radius
1730 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1731 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1733 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1735 navigation_goalrating_end(this);
1737 navigation_goalrating_timeout_set(this);
1739 entity goal = this.goalentity;
1740 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1741 this.goalentity_lock_timeout = time + ((this.bot_aimtarg) ? 2 : 3);
1744 this.havocbot_cantfindflag = time + 10;
1745 else if (time > this.havocbot_cantfindflag)
1747 // Can't navigate to my own base, suicide!
1748 // TODO: drop it and wander around
1749 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1755 void havocbot_role_ctf_escort(entity this)
1761 havocbot_ctf_reset_role(this);
1765 if (this.flagcarried)
1767 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1771 // If enemy flag is back on the base switch to previous role
1772 ef = havocbot_ctf_find_enemy_flag(this);
1773 if(ef.ctf_status==FLAG_BASE)
1775 this.havocbot_role = this.havocbot_previous_role;
1776 this.havocbot_role_timeout = 0;
1779 if (ef.ctf_status == FLAG_DROPPED)
1781 navigation_goalrating_timeout_expire(this, 1);
1785 // If the flag carrier reached the base switch to defense
1786 mf = havocbot_ctf_find_flag(this);
1787 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1789 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1793 // Set the role timeout if necessary
1794 if (!this.havocbot_role_timeout)
1796 this.havocbot_role_timeout = time + random() * 30 + 60;
1799 // If nothing happened just switch to previous role
1800 if (time > this.havocbot_role_timeout)
1802 this.havocbot_role = this.havocbot_previous_role;
1803 this.havocbot_role_timeout = 0;
1807 // Chase the flag carrier
1808 if (navigation_goalrating_timeout(this))
1810 navigation_goalrating_start(this);
1813 havocbot_goalrating_ctf_enemyflag(this, 10000);
1814 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1815 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1817 navigation_goalrating_end(this);
1819 navigation_goalrating_timeout_set(this);
1823 void havocbot_role_ctf_offense(entity this)
1830 havocbot_ctf_reset_role(this);
1834 if (this.flagcarried)
1836 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1841 mf = havocbot_ctf_find_flag(this);
1842 ef = havocbot_ctf_find_enemy_flag(this);
1845 if(mf.ctf_status!=FLAG_BASE)
1848 pos = mf.tag_entity.origin;
1852 // Try to get it if closer than the enemy base
1853 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1855 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1860 // Escort flag carrier
1861 if(ef.ctf_status!=FLAG_BASE)
1864 pos = ef.tag_entity.origin;
1868 if(vdist(pos - mf.dropped_origin, >, 700))
1870 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1875 // Set the role timeout if necessary
1876 if (!this.havocbot_role_timeout)
1877 this.havocbot_role_timeout = time + 120;
1879 if (time > this.havocbot_role_timeout)
1881 havocbot_ctf_reset_role(this);
1885 if (navigation_goalrating_timeout(this))
1887 navigation_goalrating_start(this);
1890 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1891 havocbot_goalrating_ctf_enemybase(this, 10000);
1892 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1894 navigation_goalrating_end(this);
1896 navigation_goalrating_timeout_set(this);
1900 // Retriever (temporary role):
1901 void havocbot_role_ctf_retriever(entity this)
1907 havocbot_ctf_reset_role(this);
1911 if (this.flagcarried)
1913 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1917 // If flag is back on the base switch to previous role
1918 mf = havocbot_ctf_find_flag(this);
1919 if(mf.ctf_status==FLAG_BASE)
1921 if (mf.enemy == this) // did this bot return the flag?
1922 navigation_goalrating_timeout_force(this);
1923 havocbot_ctf_reset_role(this);
1927 if (!this.havocbot_role_timeout)
1928 this.havocbot_role_timeout = time + 20;
1930 if (time > this.havocbot_role_timeout)
1932 havocbot_ctf_reset_role(this);
1936 if (navigation_goalrating_timeout(this))
1938 const float RT_RADIUS = 10000;
1940 navigation_goalrating_start(this);
1943 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1944 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1945 havocbot_goalrating_ctf_enemybase(this, 8000);
1946 entity ef = havocbot_ctf_find_enemy_flag(this);
1947 vector enemy_base_org = ef.dropped_origin;
1948 // start collecting items very close to the bot but only inside of enemy base radius
1949 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1950 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1951 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1953 navigation_goalrating_end(this);
1955 navigation_goalrating_timeout_set(this);
1959 void havocbot_role_ctf_middle(entity this)
1965 havocbot_ctf_reset_role(this);
1969 if (this.flagcarried)
1971 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1975 mf = havocbot_ctf_find_flag(this);
1976 if(mf.ctf_status!=FLAG_BASE)
1978 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1982 if (!this.havocbot_role_timeout)
1983 this.havocbot_role_timeout = time + 10;
1985 if (time > this.havocbot_role_timeout)
1987 havocbot_ctf_reset_role(this);
1991 if (navigation_goalrating_timeout(this))
1995 org = havocbot_middlepoint;
1996 org.z = this.origin.z;
1998 navigation_goalrating_start(this);
2001 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
2002 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
2003 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2004 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2005 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2006 havocbot_goalrating_ctf_enemybase(this, 3000);
2008 navigation_goalrating_end(this);
2010 entity goal = this.goalentity;
2011 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
2012 this.goalentity_lock_timeout = time + 2;
2014 navigation_goalrating_timeout_set(this);
2018 void havocbot_role_ctf_defense(entity this)
2024 havocbot_ctf_reset_role(this);
2028 if (this.flagcarried)
2030 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2034 // If own flag was captured
2035 mf = havocbot_ctf_find_flag(this);
2036 if(mf.ctf_status!=FLAG_BASE)
2038 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2042 if (!this.havocbot_role_timeout)
2043 this.havocbot_role_timeout = time + 30;
2045 if (time > this.havocbot_role_timeout)
2047 havocbot_ctf_reset_role(this);
2050 if (navigation_goalrating_timeout(this))
2052 vector org = mf.dropped_origin;
2054 navigation_goalrating_start(this);
2056 // if enemies are closer to our base, go there
2057 entity closestplayer = NULL;
2058 float distance, bestdistance = 10000;
2059 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2060 distance = vlen(org - it.origin);
2061 if(distance<bestdistance)
2064 bestdistance = distance;
2070 if(DIFF_TEAM(closestplayer, this))
2071 if(vdist(org - this.origin, >, 1000))
2072 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2073 havocbot_goalrating_ctf_ourbase(this, 10000);
2075 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2076 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2077 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2078 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2079 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2081 navigation_goalrating_end(this);
2083 navigation_goalrating_timeout_set(this);
2087 void havocbot_role_ctf_setrole(entity bot, int role)
2089 string s = "(null)";
2092 case HAVOCBOT_CTF_ROLE_CARRIER:
2094 bot.havocbot_role = havocbot_role_ctf_carrier;
2095 bot.havocbot_role_timeout = 0;
2096 bot.havocbot_cantfindflag = time + 10;
2097 if (bot.havocbot_previous_role != bot.havocbot_role)
2098 navigation_goalrating_timeout_force(bot);
2100 case HAVOCBOT_CTF_ROLE_DEFENSE:
2102 bot.havocbot_role = havocbot_role_ctf_defense;
2103 bot.havocbot_role_timeout = 0;
2105 case HAVOCBOT_CTF_ROLE_MIDDLE:
2107 bot.havocbot_role = havocbot_role_ctf_middle;
2108 bot.havocbot_role_timeout = 0;
2110 case HAVOCBOT_CTF_ROLE_OFFENSE:
2112 bot.havocbot_role = havocbot_role_ctf_offense;
2113 bot.havocbot_role_timeout = 0;
2115 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2117 bot.havocbot_previous_role = bot.havocbot_role;
2118 bot.havocbot_role = havocbot_role_ctf_retriever;
2119 bot.havocbot_role_timeout = time + 10;
2120 if (bot.havocbot_previous_role != bot.havocbot_role)
2121 navigation_goalrating_timeout_expire(bot, 2);
2123 case HAVOCBOT_CTF_ROLE_ESCORT:
2125 bot.havocbot_previous_role = bot.havocbot_role;
2126 bot.havocbot_role = havocbot_role_ctf_escort;
2127 bot.havocbot_role_timeout = time + 30;
2128 if (bot.havocbot_previous_role != bot.havocbot_role)
2129 navigation_goalrating_timeout_expire(bot, 2);
2132 LOG_TRACE(bot.netname, " switched to ", s);
2140 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2142 entity player = M_ARGV(0, entity);
2144 int t = 0, t2 = 0, t3 = 0;
2145 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)
2147 // initially clear items so they can be set as necessary later.
2148 STAT(CTF_FLAGSTATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2149 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2150 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2151 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2152 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2153 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2155 // scan through all the flags and notify the client about them
2156 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2158 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2159 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2160 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2161 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2162 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; }
2164 switch(flag.ctf_status)
2169 if((flag.owner == player) || (flag.pass_sender == player))
2170 STAT(CTF_FLAGSTATUS, player) |= t; // carrying: player is currently carrying the flag
2172 STAT(CTF_FLAGSTATUS, player) |= t2; // taken: someone else is carrying the flag
2177 STAT(CTF_FLAGSTATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2183 // item for stopping players from capturing the flag too often
2184 if(player.ctf_captureshielded)
2185 STAT(CTF_FLAGSTATUS, player) |= CTF_SHIELDED;
2188 STAT(CTF_FLAGSTATUS, player) |= CTF_STALEMATE;
2190 // update the health of the flag carrier waypointsprite
2191 if(player.wps_flagcarrier)
2192 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);
2195 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2197 entity frag_attacker = M_ARGV(1, entity);
2198 entity frag_target = M_ARGV(2, entity);
2199 float frag_damage = M_ARGV(4, float);
2200 vector frag_force = M_ARGV(6, vector);
2202 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2204 if(frag_target == frag_attacker) // damage done to yourself
2206 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2207 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2209 else // damage done to everyone else
2211 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2212 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2215 M_ARGV(4, float) = frag_damage;
2216 M_ARGV(6, vector) = frag_force;
2218 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2220 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
2221 && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2223 frag_target.wps_helpme_time = time;
2224 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2226 // todo: add notification for when flag carrier needs help?
2230 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2232 entity frag_attacker = M_ARGV(1, entity);
2233 entity frag_target = M_ARGV(2, entity);
2235 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2237 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2238 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2241 if(frag_target.flagcarried)
2243 entity tmp_entity = frag_target.flagcarried;
2244 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2245 tmp_entity.ctf_dropper = NULL;
2249 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2251 M_ARGV(2, float) = 0; // frag score
2252 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2255 void ctf_RemovePlayer(entity player)
2257 if(player.flagcarried)
2258 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2260 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2262 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2263 if(flag.pass_target == player) { flag.pass_target = NULL; }
2264 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2268 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2270 entity player = M_ARGV(0, entity);
2272 ctf_RemovePlayer(player);
2275 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2277 entity player = M_ARGV(0, entity);
2279 ctf_RemovePlayer(player);
2282 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2284 if(!autocvar_g_ctf_leaderboard)
2287 entity player = M_ARGV(0, entity);
2289 if(IS_REAL_CLIENT(player))
2291 int m = min(RANKINGS_CNT, autocvar_g_cts_send_rankings_cnt);
2292 race_send_rankings_cnt(MSG_ONE);
2293 for (int i = 1; i <= m; ++i)
2295 race_SendRankings(i, 0, 0, MSG_ONE);
2300 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2302 if(!autocvar_g_ctf_leaderboard)
2305 entity player = M_ARGV(0, entity);
2307 if(CS(player).cvar_cl_allow_uidtracking == 1 && CS(player).cvar_cl_allow_uid2name == 1)
2309 if (!player.stored_netname)
2310 player.stored_netname = strzone(uid2name(player.crypto_idfp));
2311 if(player.stored_netname != player.netname)
2313 db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
2314 strcpy(player.stored_netname, player.netname);
2319 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2321 entity player = M_ARGV(0, entity);
2323 if(player.flagcarried)
2324 if(!autocvar_g_ctf_portalteleport)
2325 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2328 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2330 if(MUTATOR_RETURNVALUE || game_stopped) return;
2332 entity player = M_ARGV(0, entity);
2334 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2336 // pass the flag to a team mate
2337 if(autocvar_g_ctf_pass)
2339 entity head, closest_target = NULL;
2340 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2342 while(head) // find the closest acceptable target to pass to
2344 if(IS_PLAYER(head) && !IS_DEAD(head))
2345 if(head != player && SAME_TEAM(head, player))
2346 if(!head.speedrunning && !head.vehicle)
2348 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2349 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2350 vector passer_center = CENTER_OR_VIEWOFS(player);
2352 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2354 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2356 if(IS_BOT_CLIENT(head))
2358 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2359 ctf_Handle_Throw(head, player, DROP_PASS);
2363 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2364 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2366 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2369 else if(player.flagcarried && !head.flagcarried)
2373 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2374 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2375 { closest_target = head; }
2377 else { closest_target = head; }
2384 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2387 // throw the flag in front of you
2388 if(autocvar_g_ctf_throw && player.flagcarried)
2390 if(player.throw_count == -1)
2392 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2394 player.throw_prevtime = time;
2395 player.throw_count = 1;
2396 ctf_Handle_Throw(player, NULL, DROP_THROW);
2401 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2407 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2408 else { player.throw_count += 1; }
2409 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2411 player.throw_prevtime = time;
2412 ctf_Handle_Throw(player, NULL, DROP_THROW);
2419 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2421 entity player = M_ARGV(0, entity);
2423 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2425 player.wps_helpme_time = time;
2426 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2428 else // create a normal help me waypointsprite
2430 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2431 WaypointSprite_Ping(player.wps_helpme);
2437 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2439 entity player = M_ARGV(0, entity);
2440 entity veh = M_ARGV(1, entity);
2442 if(player.flagcarried)
2444 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2446 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2450 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2451 setattachment(player.flagcarried, veh, "");
2452 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2453 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2454 //player.flagcarried.angles = '0 0 0';
2460 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2462 entity player = M_ARGV(0, entity);
2464 if(player.flagcarried)
2466 setattachment(player.flagcarried, player, "");
2467 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2468 player.flagcarried.scale = FLAG_SCALE;
2469 player.flagcarried.angles = '0 0 0';
2470 player.flagcarried.nodrawtoclient = NULL;
2475 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2477 entity player = M_ARGV(0, entity);
2479 if(player.flagcarried)
2481 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2482 ctf_RespawnFlag(player.flagcarried);
2487 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2489 entity flag; // temporary entity for the search method
2491 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2493 switch(flag.ctf_status)
2498 // lock the flag, game is over
2499 set_movetype(flag, MOVETYPE_NONE);
2500 flag.takedamage = DAMAGE_NO;
2501 flag.solid = SOLID_NOT;
2502 flag.nextthink = false; // stop thinking
2504 //dprint("stopping the ", flag.netname, " from moving.\n");
2512 // do nothing for these flags
2519 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2521 entity bot = M_ARGV(0, entity);
2523 havocbot_ctf_reset_role(bot);
2527 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2529 M_ARGV(1, string) = "ctf_team";
2532 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2534 entity spectatee = M_ARGV(0, entity);
2535 entity client = M_ARGV(1, entity);
2537 STAT(CTF_FLAGSTATUS, client) = STAT(CTF_FLAGSTATUS, spectatee);
2540 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2542 int record_page = M_ARGV(0, int);
2543 string ret_string = M_ARGV(1, string);
2545 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2547 if (MapInfo_Get_ByID(i))
2549 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2555 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2556 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2560 M_ARGV(1, string) = ret_string;
2563 bool superspec_Spectate(entity this, entity targ); // TODO
2564 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2565 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2567 entity player = M_ARGV(0, entity);
2568 string cmd_name = M_ARGV(1, string);
2569 int cmd_argc = M_ARGV(2, int);
2571 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2573 if(cmd_name == "followfc")
2585 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2586 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2587 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2588 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2592 FOREACH_CLIENT(IS_PLAYER(it), {
2593 if(it.flagcarried && (it.team == _team || _team == 0))
2596 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2597 continue; // already spectating this fc, try another
2598 return superspec_Spectate(player, it);
2603 superspec_msg("", "", player, "No active flag carrier\n", 1);
2608 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2610 entity frag_target = M_ARGV(0, entity);
2612 if(frag_target.flagcarried)
2613 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2616 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2618 entity player = M_ARGV(0, entity);
2619 if(player.flagcarried)
2620 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2628 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2629 CTF flag for team one (Red).
2631 "angle" Angle the flag will point (minus 90 degrees)...
2632 "model" model to use, note this needs red and blue as skins 0 and 1...
2633 "noise" sound played when flag is picked up...
2634 "noise1" sound played when flag is returned by a teammate...
2635 "noise2" sound played when flag is captured...
2636 "noise3" sound played when flag is lost in the field and respawns itself...
2637 "noise4" sound played when flag is dropped by a player...
2638 "noise5" sound played when flag touches the ground... */
2639 spawnfunc(item_flag_team1)
2641 if(!g_ctf) { delete(this); return; }
2643 ctf_FlagSetup(NUM_TEAM_1, this);
2646 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2647 CTF flag for team two (Blue).
2649 "angle" Angle the flag will point (minus 90 degrees)...
2650 "model" model to use, note this needs red and blue as skins 0 and 1...
2651 "noise" sound played when flag is picked up...
2652 "noise1" sound played when flag is returned by a teammate...
2653 "noise2" sound played when flag is captured...
2654 "noise3" sound played when flag is lost in the field and respawns itself...
2655 "noise4" sound played when flag is dropped by a player...
2656 "noise5" sound played when flag touches the ground... */
2657 spawnfunc(item_flag_team2)
2659 if(!g_ctf) { delete(this); return; }
2661 ctf_FlagSetup(NUM_TEAM_2, this);
2664 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2665 CTF flag for team three (Yellow).
2667 "angle" Angle the flag will point (minus 90 degrees)...
2668 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2669 "noise" sound played when flag is picked up...
2670 "noise1" sound played when flag is returned by a teammate...
2671 "noise2" sound played when flag is captured...
2672 "noise3" sound played when flag is lost in the field and respawns itself...
2673 "noise4" sound played when flag is dropped by a player...
2674 "noise5" sound played when flag touches the ground... */
2675 spawnfunc(item_flag_team3)
2677 if(!g_ctf) { delete(this); return; }
2679 ctf_FlagSetup(NUM_TEAM_3, this);
2682 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2683 CTF flag for team four (Pink).
2685 "angle" Angle the flag will point (minus 90 degrees)...
2686 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2687 "noise" sound played when flag is picked up...
2688 "noise1" sound played when flag is returned by a teammate...
2689 "noise2" sound played when flag is captured...
2690 "noise3" sound played when flag is lost in the field and respawns itself...
2691 "noise4" sound played when flag is dropped by a player...
2692 "noise5" sound played when flag touches the ground... */
2693 spawnfunc(item_flag_team4)
2695 if(!g_ctf) { delete(this); return; }
2697 ctf_FlagSetup(NUM_TEAM_4, this);
2700 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2703 "angle" Angle the flag will point (minus 90 degrees)...
2704 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2705 "noise" sound played when flag is picked up...
2706 "noise1" sound played when flag is returned by a teammate...
2707 "noise2" sound played when flag is captured...
2708 "noise3" sound played when flag is lost in the field and respawns itself...
2709 "noise4" sound played when flag is dropped by a player...
2710 "noise5" sound played when flag touches the ground... */
2711 spawnfunc(item_flag_neutral)
2713 if(!g_ctf) { delete(this); return; }
2714 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2716 ctf_FlagSetup(0, this);
2719 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2720 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2721 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.
2723 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2724 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2727 if(!g_ctf) { delete(this); return; }
2729 this.classname = "ctf_team";
2730 this.team = this.cnt + 1;
2733 // compatibility for quake maps
2734 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2735 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2736 spawnfunc(info_player_team1);
2737 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2738 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2739 spawnfunc(info_player_team2);
2740 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2741 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2743 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2744 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2746 // compatibility for wop maps
2747 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2748 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2749 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2750 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2751 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2752 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2760 void ctf_ScoreRules(int teams)
2762 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2763 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2764 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2765 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2766 field(SP_CTF_PICKUPS, "pickups", 0);
2767 field(SP_CTF_FCKILLS, "fckills", 0);
2768 field(SP_CTF_RETURNS, "returns", 0);
2769 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2773 // code from here on is just to support maps that don't have flag and team entities
2774 void ctf_SpawnTeam (string teamname, int teamcolor)
2776 entity this = new_pure(ctf_team);
2777 this.netname = teamname;
2778 this.cnt = teamcolor - 1;
2779 this.spawnfunc_checked = true;
2780 this.team = teamcolor;
2783 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2788 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2790 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2791 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2793 switch(tmp_entity.team)
2795 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2796 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2797 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2798 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2800 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2803 havocbot_ctf_calculate_middlepoint();
2805 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2807 ctf_teams = 0; // so set the default red and blue teams
2808 BITSET_ASSIGN(ctf_teams, BIT(0));
2809 BITSET_ASSIGN(ctf_teams, BIT(1));
2812 //ctf_teams = bound(2, ctf_teams, 4);
2814 // if no teams are found, spawn defaults
2815 if(find(NULL, classname, "ctf_team") == NULL)
2817 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2818 if(ctf_teams & BIT(0))
2819 ctf_SpawnTeam("Red", NUM_TEAM_1);
2820 if(ctf_teams & BIT(1))
2821 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2822 if(ctf_teams & BIT(2))
2823 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2824 if(ctf_teams & BIT(3))
2825 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2828 ctf_ScoreRules(ctf_teams);
2831 void ctf_Initialize()
2833 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2835 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2836 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2837 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2839 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);