3 #include <common/effects/all.qh>
4 #include <common/mapobjects/teleporters.qh>
5 #include <common/mapobjects/triggers.qh>
6 #include <common/mutators/mutator/powerups/_mod.qh>
7 #include <common/vehicles/all.qh>
8 #include <server/command/vote.qh>
9 #include <server/client.qh>
10 #include <server/gamelog.qh>
11 #include <server/intermission.qh>
12 #include <server/damage.qh>
13 #include <server/world.qh>
14 #include <server/items/items.qh>
15 #include <server/race.qh>
16 #include <server/teamplay.qh>
18 #include <lib/warpzone/common.qh>
20 bool autocvar_g_ctf_allow_vehicle_carry;
21 bool autocvar_g_ctf_allow_vehicle_touch;
22 bool autocvar_g_ctf_allow_monster_touch;
23 bool autocvar_g_ctf_throw;
24 float autocvar_g_ctf_throw_angle_max;
25 float autocvar_g_ctf_throw_angle_min;
26 int autocvar_g_ctf_throw_punish_count;
27 float autocvar_g_ctf_throw_punish_delay;
28 float autocvar_g_ctf_throw_punish_time;
29 float autocvar_g_ctf_throw_strengthmultiplier;
30 float autocvar_g_ctf_throw_velocity_forward;
31 float autocvar_g_ctf_throw_velocity_up;
32 float autocvar_g_ctf_drop_velocity_up;
33 float autocvar_g_ctf_drop_velocity_side;
34 bool autocvar_g_ctf_oneflag_reverse;
35 bool autocvar_g_ctf_portalteleport;
36 bool autocvar_g_ctf_pass;
37 float autocvar_g_ctf_pass_arc;
38 float autocvar_g_ctf_pass_arc_max;
39 float autocvar_g_ctf_pass_directional_max;
40 float autocvar_g_ctf_pass_directional_min;
41 float autocvar_g_ctf_pass_radius;
42 float autocvar_g_ctf_pass_wait;
43 bool autocvar_g_ctf_pass_request;
44 float autocvar_g_ctf_pass_turnrate;
45 float autocvar_g_ctf_pass_timelimit;
46 float autocvar_g_ctf_pass_velocity;
47 bool autocvar_g_ctf_dynamiclights;
48 float autocvar_g_ctf_flag_collect_delay;
49 float autocvar_g_ctf_flag_damageforcescale;
50 bool autocvar_g_ctf_flag_dropped_waypoint;
51 bool autocvar_g_ctf_flag_dropped_floatinwater;
52 bool autocvar_g_ctf_flag_glowtrails;
53 int autocvar_g_ctf_flag_health;
54 bool autocvar_g_ctf_flag_return;
55 bool autocvar_g_ctf_flag_return_carrying;
56 float autocvar_g_ctf_flag_return_carried_radius;
57 float autocvar_g_ctf_flag_return_time;
58 bool autocvar_g_ctf_flag_return_when_unreachable;
59 float autocvar_g_ctf_flag_return_damage;
60 float autocvar_g_ctf_flag_return_damage_delay;
61 float autocvar_g_ctf_flag_return_dropped;
62 bool autocvar_g_ctf_flag_waypoint = true;
63 float autocvar_g_ctf_flag_waypoint_maxdistance;
64 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
65 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
66 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
67 float autocvar_g_ctf_flagcarrier_selfforcefactor;
68 float autocvar_g_ctf_flagcarrier_damagefactor;
69 float autocvar_g_ctf_flagcarrier_forcefactor;
70 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
71 bool autocvar_g_ctf_fullbrightflags;
72 bool autocvar_g_ctf_ignore_frags;
73 bool autocvar_g_ctf_score_ignore_fields;
74 int autocvar_g_ctf_score_capture;
75 int autocvar_g_ctf_score_capture_assist;
76 int autocvar_g_ctf_score_kill;
77 int autocvar_g_ctf_score_penalty_drop;
78 int autocvar_g_ctf_score_penalty_returned;
79 int autocvar_g_ctf_score_pickup_base;
80 int autocvar_g_ctf_score_pickup_dropped_early;
81 int autocvar_g_ctf_score_pickup_dropped_late;
82 int autocvar_g_ctf_score_return;
83 float autocvar_g_ctf_shield_force;
84 float autocvar_g_ctf_shield_max_ratio;
85 int autocvar_g_ctf_shield_min_negscore;
86 bool autocvar_g_ctf_stalemate;
87 int autocvar_g_ctf_stalemate_endcondition;
88 float autocvar_g_ctf_stalemate_time;
89 bool autocvar_g_ctf_reverse;
90 float autocvar_g_ctf_dropped_capture_delay;
91 float autocvar_g_ctf_dropped_capture_radius;
93 void ctf_FakeTimeLimit(entity e, float t)
96 WriteByte(MSG_ONE, 3); // svc_updatestat
97 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
99 WriteCoord(MSG_ONE, autocvar_timelimit);
101 WriteCoord(MSG_ONE, (t + 1) / 60);
104 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
106 if(autocvar_sv_eventlog)
107 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != NULL) ? ftos(actor.playerid) : "")));
108 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
111 void ctf_CaptureRecord(entity flag, entity player)
113 float cap_record = ctf_captimerecord;
114 float cap_time = (time - flag.ctf_pickuptime);
115 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
119 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname);
120 else if(!ctf_captimerecord)
121 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_TIME), player.netname, TIME_ENCODE(cap_time));
122 else if(cap_time < cap_record)
123 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));
125 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));
127 // write that shit in the database
128 if(!ctf_oneflag) // but not in 1-flag mode
129 if((!ctf_captimerecord) || (cap_time < cap_record))
131 ctf_captimerecord = cap_time;
132 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
133 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
134 write_recordmarker(player, flag.ctf_pickuptime, cap_time);
137 if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
138 race_setTime(GetMapname(), TIME_ENCODE(cap_time), player.crypto_idfp, player.netname, player, false);
141 bool ctf_Immediate_Return_Allowed(entity flag, entity toucher)
144 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), { ++num_perteam; });
146 // automatically return if there's only 1 player on the team
147 return ((autocvar_g_ctf_flag_return || num_perteam <= 1 || (autocvar_g_ctf_flag_return_carrying && toucher.flagcarried))
151 bool ctf_Return_Customize(entity this, entity client)
153 // only to the carrier
154 return boolean(client == this.owner);
157 void ctf_FlagcarrierWaypoints(entity player)
159 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
160 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, 2 * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
161 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);
162 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
164 if(player.flagcarried && CTF_SAMETEAM(player, player.flagcarried))
166 if(!player.wps_enemyflagcarrier)
168 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, 0, player, wps_enemyflagcarrier, true, RADARICON_FLAG);
169 wp.colormod = WPCOLOR_ENEMYFC(player.team);
170 setcefc(wp, ctf_Stalemate_Customize);
172 if(IS_REAL_CLIENT(player) && !ctf_stalemate)
173 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_VISIBLE);
176 if(!player.wps_flagreturn)
178 entity owp = WaypointSprite_SpawnFixed(WP_FlagReturn, player.flagcarried.ctf_spawnorigin + FLAG_WAYPOINT_OFFSET, player, wps_flagreturn, RADARICON_FLAG);
179 owp.colormod = '0 0.8 0.8';
180 //WaypointSprite_UpdateTeamRadar(player.wps_flagreturn, RADARICON_FLAG, ((player.team) ? colormapPaletteColor(player.team - 1, false) : '1 1 1'));
181 setcefc(owp, ctf_Return_Customize);
186 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
188 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
189 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
190 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
191 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
194 if(current_height) // make sure we can actually do this arcing path
196 targpos = (to + ('0 0 1' * current_height));
197 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
198 if(trace_fraction < 1)
200 //print("normal arc line failed, trying to find new pos...");
201 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
202 targpos = (trace_endpos + eZ * FLAG_PASS_ARC_OFFSET_Z);
203 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
204 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
205 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
208 else { targpos = to; }
210 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
212 vector desired_direction = normalize(targpos - from);
213 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
214 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
217 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
219 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
221 // directional tracing only
223 makevectors(passer_angle);
225 // find the closest point on the enemy to the center of the attack
226 float h; // hypotenuse, which is the distance between attacker to head
227 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
229 h = vlen(head_center - passer_center);
230 a = h * (normalize(head_center - passer_center) * v_forward);
232 vector nearest_on_line = (passer_center + a * v_forward);
233 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
235 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
236 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
238 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
243 else { return true; }
247 // =======================
248 // CaptureShield Functions
249 // =======================
251 bool ctf_CaptureShield_CheckStatus(entity p)
253 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
254 int players_worseeq, players_total;
256 if(ctf_captureshield_max_ratio <= 0)
259 s = GameRules_scoring_add(p, CTF_CAPS, 0);
260 s2 = GameRules_scoring_add(p, CTF_PICKUPS, 0);
261 s3 = GameRules_scoring_add(p, CTF_RETURNS, 0);
262 s4 = GameRules_scoring_add(p, CTF_FCKILLS, 0);
264 sr = ((s - s2) + (s3 + s4));
266 if(sr >= -ctf_captureshield_min_negscore)
269 players_total = players_worseeq = 0;
270 FOREACH_CLIENT(IS_PLAYER(it), {
273 se = GameRules_scoring_add(it, CTF_CAPS, 0);
274 se2 = GameRules_scoring_add(it, CTF_PICKUPS, 0);
275 se3 = GameRules_scoring_add(it, CTF_RETURNS, 0);
276 se4 = GameRules_scoring_add(it, CTF_FCKILLS, 0);
278 ser = ((se - se2) + (se3 + se4));
285 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
286 // use this rule here
288 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
294 void ctf_CaptureShield_Update(entity player, bool wanted_status)
296 bool updated_status = ctf_CaptureShield_CheckStatus(player);
297 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
299 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
300 player.ctf_captureshielded = updated_status;
304 bool ctf_CaptureShield_Customize(entity this, entity client)
306 if(!client.ctf_captureshielded) { return false; }
307 if(CTF_SAMETEAM(this, client)) { return false; }
312 void ctf_CaptureShield_Touch(entity this, entity toucher)
314 if(!toucher.ctf_captureshielded) { return; }
315 if(CTF_SAMETEAM(this, toucher)) { return; }
317 vector mymid = (this.absmin + this.absmax) * 0.5;
318 vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
320 Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
321 if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
324 void ctf_CaptureShield_Spawn(entity flag)
326 entity shield = new(ctf_captureshield);
329 shield.team = flag.team;
330 settouch(shield, ctf_CaptureShield_Touch);
331 setcefc(shield, ctf_CaptureShield_Customize);
332 shield.effects = EF_ADDITIVE;
333 set_movetype(shield, MOVETYPE_NOCLIP);
334 shield.solid = SOLID_TRIGGER;
335 shield.avelocity = '7 0 11';
338 setorigin(shield, flag.origin);
339 setmodel(shield, MDL_CTF_SHIELD);
340 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
344 // ====================
345 // Drop/Pass/Throw Code
346 // ====================
348 void ctf_Handle_Drop(entity flag, entity player, int droptype)
351 player = (player ? player : flag.pass_sender);
354 set_movetype(flag, MOVETYPE_TOSS);
355 flag.takedamage = DAMAGE_YES;
356 flag.angles = '0 0 0';
357 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
358 flag.ctf_droptime = time;
359 flag.ctf_landtime = 0;
360 flag.ctf_dropper = player;
361 flag.ctf_status = FLAG_DROPPED;
363 // messages and sounds
364 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
365 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
366 ctf_EventLog("dropped", player.team, player);
369 GameRules_scoring_add_team(player, SCORE, -((flag.score_drop) ? flag.score_drop : autocvar_g_ctf_score_penalty_drop));
370 GameRules_scoring_add(player, CTF_DROPS, 1);
373 if(autocvar_g_ctf_flag_dropped_waypoint) {
374 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);
375 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
378 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
380 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_health);
381 WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH));
384 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
386 if(droptype == DROP_PASS)
388 flag.pass_distance = 0;
389 flag.pass_sender = NULL;
390 flag.pass_target = NULL;
394 void ctf_Handle_Retrieve(entity flag, entity player)
396 entity sender = flag.pass_sender;
398 // transfer flag to player
400 flag.owner.flagcarried = flag;
401 GameRules_scoring_vip(player, true);
406 setattachment(flag, player.vehicle, "");
407 setorigin(flag, VEHICLE_FLAG_OFFSET);
408 flag.scale = VEHICLE_FLAG_SCALE;
412 setattachment(flag, player, "");
413 setorigin(flag, FLAG_CARRY_OFFSET);
415 set_movetype(flag, MOVETYPE_NONE);
416 flag.takedamage = DAMAGE_NO;
417 flag.solid = SOLID_NOT;
418 flag.angles = '0 0 0';
419 flag.ctf_status = FLAG_CARRY;
421 // messages and sounds
422 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
423 ctf_EventLog("receive", flag.team, player);
425 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
427 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
428 else if(it == player)
429 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
430 else if(SAME_TEAM(it, sender))
431 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
434 // create new waypoint
435 ctf_FlagcarrierWaypoints(player);
437 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
438 player.throw_antispam = sender.throw_antispam;
440 flag.pass_distance = 0;
441 flag.pass_sender = NULL;
442 flag.pass_target = NULL;
445 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
447 entity flag = player.flagcarried;
448 vector targ_origin, flag_velocity;
450 if(!flag) { return; }
451 if((droptype == DROP_PASS) && !receiver) { return; }
453 if(flag.speedrunning)
455 // ensure old waypoints are removed before resetting the flag
456 WaypointSprite_Kill(player.wps_flagcarrier);
458 if(player.wps_enemyflagcarrier)
459 WaypointSprite_Kill(player.wps_enemyflagcarrier);
461 if(player.wps_flagreturn)
462 WaypointSprite_Kill(player.wps_flagreturn);
463 ctf_RespawnFlag(flag);
468 setattachment(flag, NULL, "");
469 tracebox(player.origin - FLAG_DROP_OFFSET, flag.m_mins, flag.m_maxs, player.origin + FLAG_DROP_OFFSET, MOVE_NOMONSTERS, flag);
470 setorigin(flag, trace_endpos);
471 flag.owner.flagcarried = NULL;
472 GameRules_scoring_vip(flag.owner, false);
474 flag.solid = SOLID_TRIGGER;
475 flag.ctf_dropper = player;
476 flag.ctf_droptime = time;
477 flag.ctf_landtime = 0;
479 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
486 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
487 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
488 WarpZone_RefSys_Copy(flag, receiver);
489 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
490 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
492 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
493 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
496 set_movetype(flag, MOVETYPE_FLY);
497 flag.takedamage = DAMAGE_NO;
498 flag.pass_sender = player;
499 flag.pass_target = receiver;
500 flag.ctf_status = FLAG_PASSING;
503 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
504 WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
505 ctf_EventLog("pass", flag.team, player);
511 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'));
513 flag_velocity = (('0 0 1' * autocvar_g_ctf_throw_velocity_up) + ((v_forward * autocvar_g_ctf_throw_velocity_forward) * ((StatusEffects_active(STATUSEFFECT_Strength, player)) ? autocvar_g_ctf_throw_strengthmultiplier : 1)));
514 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
515 ctf_Handle_Drop(flag, player, droptype);
516 navigation_dynamicgoal_set(flag, player);
522 flag.velocity = '0 0 0'; // do nothing
529 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);
530 ctf_Handle_Drop(flag, player, droptype);
531 navigation_dynamicgoal_set(flag, player);
536 // kill old waypointsprite
537 WaypointSprite_Ping(player.wps_flagcarrier);
538 WaypointSprite_Kill(player.wps_flagcarrier);
540 if(player.wps_enemyflagcarrier)
541 WaypointSprite_Kill(player.wps_enemyflagcarrier);
543 if(player.wps_flagreturn)
544 WaypointSprite_Kill(player.wps_flagreturn);
547 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
551 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
553 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
561 void nades_GiveBonus(entity player, float score);
563 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
565 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
566 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
567 entity player_team_flag = NULL, tmp_entity;
568 float old_time, new_time;
570 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
571 if(CTF_DIFFTEAM(player, flag)) { return; }
572 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)
574 if (toucher.goalentity == flag.bot_basewaypoint)
575 toucher.goalentity_lock_timeout = 0;
578 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
579 if(SAME_TEAM(tmp_entity, player))
581 player_team_flag = tmp_entity;
585 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
587 player.throw_prevtime = time;
588 player.throw_count = 0;
590 // messages and sounds
591 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
592 ctf_CaptureRecord(enemy_flag, player);
593 _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);
597 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
598 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
604 if(enemy_flag.score_capture || flag.score_capture)
605 pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
606 GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
608 if(enemy_flag.score_team_capture || flag.score_team_capture)
609 capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
610 GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
612 old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
613 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
614 if(!old_time || new_time < old_time)
615 GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
618 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
620 shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
624 if(capturetype == CAPTURE_NORMAL)
626 WaypointSprite_Kill(player.wps_flagcarrier);
627 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
629 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
630 { GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist)); }
633 flag.enemy = toucher;
636 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
637 ctf_RespawnFlag(enemy_flag);
640 void ctf_Handle_Return(entity flag, entity player)
642 // messages and sounds
643 if(IS_MONSTER(player))
645 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
649 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
650 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
652 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
653 ctf_EventLog("return", flag.team, player);
656 if(IS_PLAYER(player))
658 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
659 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
661 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
664 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
668 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
669 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
670 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
674 if(player.flagcarried == flag)
675 WaypointSprite_Kill(player.wps_flagcarrier);
680 ctf_RespawnFlag(flag);
683 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
686 float pickup_dropped_score; // used to calculate dropped pickup score
688 // attach the flag to the player
690 player.flagcarried = flag;
691 GameRules_scoring_vip(player, true);
694 setattachment(flag, player.vehicle, "");
695 setorigin(flag, VEHICLE_FLAG_OFFSET);
696 flag.scale = VEHICLE_FLAG_SCALE;
700 setattachment(flag, player, "");
701 setorigin(flag, FLAG_CARRY_OFFSET);
705 set_movetype(flag, MOVETYPE_NONE);
706 flag.takedamage = DAMAGE_NO;
707 flag.solid = SOLID_NOT;
708 flag.angles = '0 0 0';
709 flag.ctf_status = FLAG_CARRY;
713 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
714 case PICKUP_DROPPED: SetResourceExplicit(flag, RES_HEALTH, flag.max_health); break; // reset health/return timelimit
718 // messages and sounds
719 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
721 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
723 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
724 else if(CTF_DIFFTEAM(player, flag))
725 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
727 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
729 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
732 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); });
735 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
736 if(CTF_SAMETEAM(flag, it))
738 if(SAME_TEAM(player, it))
739 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
741 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);
745 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
748 GameRules_scoring_add(player, CTF_PICKUPS, 1);
749 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
754 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
755 ctf_EventLog("steal", flag.team, player);
761 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);
762 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);
763 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
764 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
765 ctf_EventLog("pickup", flag.team, player);
773 if(pickuptype == PICKUP_BASE)
775 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
776 if((player.speedrunning) && (ctf_captimerecord))
777 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
781 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
784 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
785 ctf_FlagcarrierWaypoints(player);
786 WaypointSprite_Ping(player.wps_flagcarrier);
790 // ===================
791 // Main Flag Functions
792 // ===================
794 void ctf_CheckFlagReturn(entity flag, int returntype)
796 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
798 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH)); }
800 if((GetResource(flag, RES_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
805 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
807 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
808 case RETURN_SPEEDRUN:
809 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
810 case RETURN_NEEDKILL:
811 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
814 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
816 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
817 ctf_EventLog("returned", flag.team, NULL);
819 ctf_RespawnFlag(flag);
824 bool ctf_Stalemate_Customize(entity this, entity client)
826 // make spectators see what the player would see
827 entity e = WaypointSprite_getviewentity(client);
828 entity wp_owner = this.owner;
831 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
832 if(SAME_TEAM(wp_owner, e)) { return false; }
833 if(!IS_PLAYER(e)) { return false; }
838 void ctf_CheckStalemate()
841 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
844 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
846 // build list of stale flags
847 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
849 if(autocvar_g_ctf_stalemate)
850 if(tmp_entity.ctf_status != FLAG_BASE)
851 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
853 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
854 ctf_staleflaglist = tmp_entity;
856 switch(tmp_entity.team)
858 case NUM_TEAM_1: ++stale_red_flags; break;
859 case NUM_TEAM_2: ++stale_blue_flags; break;
860 case NUM_TEAM_3: ++stale_yellow_flags; break;
861 case NUM_TEAM_4: ++stale_pink_flags; break;
862 default: ++stale_neutral_flags; break;
868 stale_flags = (stale_neutral_flags >= 1);
870 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
872 if(ctf_oneflag && stale_flags == 1)
873 ctf_stalemate = true;
874 else if(stale_flags >= 2)
875 ctf_stalemate = true;
876 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
877 { ctf_stalemate = false; wpforenemy_announced = false; }
878 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
879 { ctf_stalemate = false; wpforenemy_announced = false; }
881 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
884 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
886 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
888 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);
889 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
890 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
894 if (!wpforenemy_announced)
896 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)); });
898 wpforenemy_announced = true;
903 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
905 if(ITEM_DAMAGE_NEEDKILL(deathtype))
907 if(autocvar_g_ctf_flag_return_damage_delay)
908 this.ctf_flagdamaged_byworld = true;
911 SetResourceExplicit(this, RES_HEALTH, 0);
912 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
916 if(autocvar_g_ctf_flag_return_damage)
918 // reduce health and check if it should be returned
919 TakeResource(this, RES_HEALTH, damage);
920 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
925 void ctf_FlagThink(entity this)
930 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
933 if(this == ctf_worldflaglist) // only for the first flag
934 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
937 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
938 LOG_TRACE("wtf the flag got squashed?");
939 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
940 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
941 setsize(this, this.m_mins, this.m_maxs);
945 switch(this.ctf_status)
949 if(autocvar_g_ctf_dropped_capture_radius)
951 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
952 if(tmp_entity.ctf_status == FLAG_DROPPED)
953 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
954 if((this.noalign || tmp_entity.ctf_landtime) && time > ((this.noalign) ? tmp_entity.ctf_droptime : tmp_entity.ctf_landtime) + autocvar_g_ctf_dropped_capture_delay)
955 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
962 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
963 if(IS_ONGROUND(this) && !this.ctf_landtime)
964 this.ctf_landtime = time; // landtime is reset when thrown, and we don't want to restart the timer if the flag is pushed
966 if(autocvar_g_ctf_flag_dropped_floatinwater)
968 vector midpoint = ((this.absmin + this.absmax) * 0.5);
969 if(pointcontents(midpoint) == CONTENT_WATER)
971 this.velocity = this.velocity * 0.5;
973 if (pointcontents(midpoint + eZ * FLAG_FLOAT_OFFSET_Z) == CONTENT_WATER)
974 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
976 { set_movetype(this, MOVETYPE_FLY); }
978 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
980 if(autocvar_g_ctf_flag_return_dropped)
982 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
984 SetResourceExplicit(this, RES_HEALTH, 0);
985 ctf_CheckFlagReturn(this, RETURN_DROPPED);
989 if(this.ctf_flagdamaged_byworld)
991 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
992 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
995 else if(autocvar_g_ctf_flag_return_time)
997 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
998 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
1006 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
1008 SetResourceExplicit(this, RES_HEALTH, 0);
1009 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
1011 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
1012 ImpulseCommands(this.owner);
1014 if(autocvar_g_ctf_stalemate)
1016 if(time >= wpforenemy_nextthink)
1018 ctf_CheckStalemate();
1019 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1022 if(CTF_SAMETEAM(this, this.owner) && this.team)
1024 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1025 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
1026 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
1027 ctf_Handle_Return(this, this.owner);
1034 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1035 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1036 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1038 if((this.pass_target == NULL)
1039 || (IS_DEAD(this.pass_target))
1040 || (this.pass_target.flagcarried)
1041 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1042 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1043 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1045 // give up, pass failed
1046 ctf_Handle_Drop(this, NULL, DROP_PASS);
1050 // still a viable target, go for it
1051 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1056 default: // this should never happen
1058 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1064 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1067 if(game_stopped) return;
1068 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1070 bool is_not_monster = (!IS_MONSTER(toucher));
1072 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1073 if(ITEM_TOUCH_NEEDKILL())
1075 if(!autocvar_g_ctf_flag_return_damage_delay)
1077 SetResourceExplicit(flag, RES_HEALTH, 0);
1078 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1080 if(!flag.ctf_flagdamaged_byworld) { return; }
1083 // special touch behaviors
1084 if(STAT(FROZEN, toucher)) { return; }
1085 else if(IS_VEHICLE(toucher))
1087 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1088 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1090 return; // do nothing
1092 else if(IS_MONSTER(toucher))
1094 if(!autocvar_g_ctf_allow_monster_touch)
1095 return; // do nothing
1097 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1099 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1101 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1102 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1103 flag.wait = time + FLAG_TOUCHRATE;
1107 else if(IS_DEAD(toucher)) { return; }
1109 switch(flag.ctf_status)
1115 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1116 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1117 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1118 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1120 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1121 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1122 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)
1124 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1125 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1127 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1128 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1134 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1135 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1136 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1137 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1143 LOG_TRACE("Someone touched a flag even though it was being carried?");
1149 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1151 if(DIFF_TEAM(toucher, flag.pass_sender))
1153 if(ctf_Immediate_Return_Allowed(flag, toucher))
1154 ctf_Handle_Return(flag, toucher);
1155 else if(is_not_monster && (!toucher.flagcarried))
1156 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1158 else if(!toucher.flagcarried)
1159 ctf_Handle_Retrieve(flag, toucher);
1166 .float last_respawn;
1167 void ctf_RespawnFlag(entity flag)
1169 flag.watertype = CONTENT_EMPTY; // TODO: it is unclear why this workaround is needed, likely many other potential breakage points!!
1170 // check for flag respawn being called twice in a row
1171 if(flag.last_respawn > time - 0.5)
1172 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1174 flag.last_respawn = time;
1176 // reset the player (if there is one)
1177 if((flag.owner) && (flag.owner.flagcarried == flag))
1179 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1180 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1181 WaypointSprite_Kill(flag.wps_flagcarrier);
1183 flag.owner.flagcarried = NULL;
1184 GameRules_scoring_vip(flag.owner, false);
1186 if(flag.speedrunning)
1187 ctf_FakeTimeLimit(flag.owner, -1);
1190 if((flag.owner) && (flag.owner.vehicle))
1191 flag.scale = FLAG_SCALE;
1193 if(flag.ctf_status == FLAG_DROPPED)
1194 { WaypointSprite_Kill(flag.wps_flagdropped); }
1197 setattachment(flag, NULL, "");
1198 setorigin(flag, flag.ctf_spawnorigin);
1200 //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1201 set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1202 flag.takedamage = DAMAGE_NO;
1203 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1204 flag.solid = SOLID_TRIGGER;
1205 flag.velocity = '0 0 0';
1206 flag.angles = flag.mangle;
1207 flag.flags = FL_ITEM | FL_NOTARGET;
1209 flag.ctf_status = FLAG_BASE;
1211 flag.pass_distance = 0;
1212 flag.pass_sender = NULL;
1213 flag.pass_target = NULL;
1214 flag.ctf_dropper = NULL;
1215 flag.ctf_pickuptime = 0;
1216 flag.ctf_droptime = 0;
1217 flag.ctf_landtime = 0;
1218 flag.ctf_flagdamaged_byworld = false;
1219 navigation_dynamicgoal_unset(flag);
1221 ctf_CheckStalemate();
1224 void ctf_Reset(entity this)
1226 if(this.owner && IS_PLAYER(this.owner))
1227 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1230 ctf_RespawnFlag(this);
1233 bool ctf_FlagBase_Customize(entity this, entity client)
1235 entity e = WaypointSprite_getviewentity(client);
1236 entity wp_owner = this.owner;
1237 entity flag = e.flagcarried;
1238 if(flag && CTF_SAMETEAM(e, flag))
1240 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1245 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1248 waypoint_spawnforitem_force(this, this.origin);
1249 navigation_dynamicgoal_init(this, true);
1255 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1256 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1257 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1258 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1259 default: basename = WP_FlagBaseNeutral; break;
1262 if(autocvar_g_ctf_flag_waypoint)
1264 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1265 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1266 wp.fade_rate = autocvar_g_ctf_flag_waypoint_maxdistance;
1267 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1268 setcefc(wp, ctf_FlagBase_Customize);
1271 // captureshield setup
1272 ctf_CaptureShield_Spawn(this);
1277 void ctf_FlagSetup(int teamnum, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1280 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1281 ctf_worldflaglist = flag;
1283 setattachment(flag, NULL, "");
1285 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnum), Team_ColorName_Upper(teamnum)));
1286 flag.team = teamnum;
1287 flag.classname = "item_flag_team";
1288 flag.target = "###item###"; // for finding the nearest item using findnearest
1289 flag.flags = FL_ITEM | FL_NOTARGET;
1290 IL_PUSH(g_items, flag);
1291 flag.solid = SOLID_TRIGGER;
1292 flag.takedamage = DAMAGE_NO;
1293 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1294 flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1295 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1296 flag.event_damage = ctf_FlagDamage;
1297 flag.pushable = true;
1298 flag.teleportable = TELEPORT_NORMAL;
1299 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1300 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1301 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1302 if(flag.damagedbycontents)
1303 IL_PUSH(g_damagedbycontents, flag);
1304 flag.velocity = '0 0 0';
1305 flag.mangle = flag.angles;
1306 flag.reset = ctf_Reset;
1307 settouch(flag, ctf_FlagTouch);
1308 setthink(flag, ctf_FlagThink);
1309 flag.nextthink = time + FLAG_THINKRATE;
1310 flag.ctf_status = FLAG_BASE;
1312 // crudely force them all to 0
1313 if(autocvar_g_ctf_score_ignore_fields)
1314 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1316 string teamname = Static_Team_ColorName_Lower(teamnum);
1318 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1319 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1320 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1321 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnum).eent_eff_name; }
1322 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnum).eent_eff_name; }
1323 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnum).eent_eff_name; }
1327 if(flag.s == "") flag.s = b; \
1328 precache_sound(flag.s);
1330 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnum))))
1331 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnum))))
1332 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnum))))
1333 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnum))))
1334 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1335 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1336 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1340 precache_model(flag.model);
1343 _setmodel(flag, flag.model); // precision set below
1344 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1345 flag.m_mins = flag.mins; // store these for squash checks
1346 flag.m_maxs = flag.maxs;
1347 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1349 if(autocvar_g_ctf_flag_glowtrails)
1353 case NUM_TEAM_1: flag.glow_color = 251; break;
1354 case NUM_TEAM_2: flag.glow_color = 210; break;
1355 case NUM_TEAM_3: flag.glow_color = 110; break;
1356 case NUM_TEAM_4: flag.glow_color = 145; break;
1357 default: flag.glow_color = 254; break;
1359 flag.glow_size = 25;
1360 flag.glow_trail = 1;
1363 flag.effects |= EF_LOWPRECISION;
1364 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1365 if(autocvar_g_ctf_dynamiclights)
1369 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1370 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1371 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1372 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1373 default: flag.effects |= EF_DIMLIGHT; break;
1378 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1380 flag.dropped_origin = flag.origin;
1381 flag.noalign = true;
1382 set_movetype(flag, MOVETYPE_NONE);
1384 else // drop to floor, automatically find a platform and set that as spawn origin
1386 flag.noalign = false;
1388 set_movetype(flag, MOVETYPE_NONE);
1391 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1399 // NOTE: LEGACY CODE, needs to be re-written!
1401 void havocbot_ctf_calculate_middlepoint()
1405 vector fo = '0 0 0';
1408 f = ctf_worldflaglist;
1413 f = f.ctf_worldflagnext;
1419 havocbot_middlepoint = s / n;
1420 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1422 havocbot_symmetry_axis_m = 0;
1423 havocbot_symmetry_axis_q = 0;
1426 // for symmetrical editing of waypoints
1427 entity f1 = ctf_worldflaglist;
1428 entity f2 = f1.ctf_worldflagnext;
1429 float m = -(f1.origin.y - f2.origin.y) / (max(f1.origin.x - f2.origin.x, FLOAT_EPSILON));
1430 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1431 havocbot_symmetry_axis_m = m;
1432 havocbot_symmetry_axis_q = q;
1434 havocbot_symmetry_origin_order = n;
1438 entity havocbot_ctf_find_flag(entity bot)
1441 f = ctf_worldflaglist;
1444 if (CTF_SAMETEAM(bot, f))
1446 f = f.ctf_worldflagnext;
1451 entity havocbot_ctf_find_enemy_flag(entity bot)
1454 f = ctf_worldflaglist;
1459 if(CTF_DIFFTEAM(bot, f))
1466 else if(!bot.flagcarried)
1470 else if (CTF_DIFFTEAM(bot, f))
1472 f = f.ctf_worldflagnext;
1477 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1484 FOREACH_CLIENT(IS_PLAYER(it), {
1485 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1488 if(vdist(it.origin - org, <, tc_radius))
1497 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1500 head = ctf_worldflaglist;
1503 if (CTF_SAMETEAM(this, head))
1505 head = head.ctf_worldflagnext;
1508 navigation_routerating(this, head, ratingscale, 10000);
1512 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1515 head = ctf_worldflaglist;
1518 if (CTF_SAMETEAM(this, head))
1520 if (this.flagcarried)
1521 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1523 head = head.ctf_worldflagnext; // skip base if it has a different group
1528 head = head.ctf_worldflagnext;
1533 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1536 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1539 head = ctf_worldflaglist;
1544 if(CTF_DIFFTEAM(this, head))
1548 if(this.flagcarried)
1551 else if(!this.flagcarried)
1555 else if(CTF_DIFFTEAM(this, head))
1557 head = head.ctf_worldflagnext;
1561 if (head.ctf_status == FLAG_CARRY)
1563 // adjust rating of our flag carrier depending on his health
1564 head = head.tag_entity;
1565 float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1566 ratingscale += ratingscale * f * 0.1;
1568 navigation_routerating(this, head, ratingscale, 10000);
1572 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1574 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1576 if (!bot_waypoints_for_items)
1578 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1584 head = havocbot_ctf_find_enemy_flag(this);
1589 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1592 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1596 mf = havocbot_ctf_find_flag(this);
1598 if(mf.ctf_status == FLAG_BASE)
1602 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1605 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1608 head = ctf_worldflaglist;
1611 // flag is out in the field
1612 if(head.ctf_status != FLAG_BASE)
1613 if(head.tag_entity==NULL) // dropped
1617 if(vdist(org - head.origin, <, df_radius))
1618 navigation_routerating(this, head, ratingscale, 10000);
1621 navigation_routerating(this, head, ratingscale, 10000);
1624 head = head.ctf_worldflagnext;
1628 void havocbot_ctf_reset_role(entity this)
1630 float cdefense, cmiddle, coffense;
1637 if (this.flagcarried)
1639 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1643 mf = havocbot_ctf_find_flag(this);
1644 ef = havocbot_ctf_find_enemy_flag(this);
1646 // Retrieve stolen flag
1647 if(mf.ctf_status!=FLAG_BASE)
1649 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1653 // If enemy flag is taken go to the middle to intercept pursuers
1654 if(ef.ctf_status!=FLAG_BASE)
1656 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1660 // if there is no one else on the team switch to offense
1662 // don't check if this bot is a player since it isn't true when the bot is added to the server
1663 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1667 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1670 else if (time < CS(this).jointime + 1)
1672 // if bots spawn all at once set good default roles
1675 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1678 else if (count == 2)
1680 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1685 // Evaluate best position to take
1686 // Count mates on middle position
1687 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1689 // Count mates on defense position
1690 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1692 // Count mates on offense position
1693 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1695 if(cdefense<=coffense)
1696 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1697 else if(coffense<=cmiddle)
1698 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1700 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1702 // if bots spawn all at once assign them a more appropriated role after a while
1703 if (time < CS(this).jointime + 1 && count > 2)
1704 this.havocbot_role_timeout = time + 10 + random() * 10;
1707 bool havocbot_ctf_is_basewaypoint(entity item)
1709 if (item.classname != "waypoint")
1712 entity head = ctf_worldflaglist;
1715 if (item == head.bot_basewaypoint)
1717 head = head.ctf_worldflagnext;
1722 void havocbot_role_ctf_carrier(entity this)
1726 havocbot_ctf_reset_role(this);
1730 if (this.flagcarried == NULL)
1732 havocbot_ctf_reset_role(this);
1736 if (navigation_goalrating_timeout(this))
1738 navigation_goalrating_start(this);
1741 entity mf = havocbot_ctf_find_flag(this);
1742 vector base_org = mf.dropped_origin;
1743 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1745 havocbot_goalrating_ctf_enemybase(this, base_rating);
1747 havocbot_goalrating_ctf_ourbase(this, base_rating);
1749 // start collecting items very close to the bot but only inside of own base radius
1750 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1751 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1753 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1755 navigation_goalrating_end(this);
1757 navigation_goalrating_timeout_set(this);
1759 entity goal = this.goalentity;
1760 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1761 this.goalentity_lock_timeout = time + ((this.enemy) ? 2 : 3);
1764 this.havocbot_cantfindflag = time + 10;
1765 else if (time > this.havocbot_cantfindflag)
1767 // Can't navigate to my own base, suicide!
1768 // TODO: drop it and wander around
1769 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1775 void havocbot_role_ctf_escort(entity this)
1781 havocbot_ctf_reset_role(this);
1785 if (this.flagcarried)
1787 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1791 // If enemy flag is back on the base switch to previous role
1792 ef = havocbot_ctf_find_enemy_flag(this);
1793 if(ef.ctf_status==FLAG_BASE)
1795 this.havocbot_role = this.havocbot_previous_role;
1796 this.havocbot_role_timeout = 0;
1799 if (ef.ctf_status == FLAG_DROPPED)
1801 navigation_goalrating_timeout_expire(this, 1);
1805 // If the flag carrier reached the base switch to defense
1806 mf = havocbot_ctf_find_flag(this);
1807 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1809 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1813 // Set the role timeout if necessary
1814 if (!this.havocbot_role_timeout)
1816 this.havocbot_role_timeout = time + random() * 30 + 60;
1819 // If nothing happened just switch to previous role
1820 if (time > this.havocbot_role_timeout)
1822 this.havocbot_role = this.havocbot_previous_role;
1823 this.havocbot_role_timeout = 0;
1827 // Chase the flag carrier
1828 if (navigation_goalrating_timeout(this))
1830 navigation_goalrating_start(this);
1833 havocbot_goalrating_ctf_enemyflag(this, 10000);
1834 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1835 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1837 navigation_goalrating_end(this);
1839 navigation_goalrating_timeout_set(this);
1843 void havocbot_role_ctf_offense(entity this)
1850 havocbot_ctf_reset_role(this);
1854 if (this.flagcarried)
1856 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1861 mf = havocbot_ctf_find_flag(this);
1862 ef = havocbot_ctf_find_enemy_flag(this);
1865 if(mf.ctf_status!=FLAG_BASE)
1868 pos = mf.tag_entity.origin;
1872 // Try to get it if closer than the enemy base
1873 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1875 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1880 // Escort flag carrier
1881 if(ef.ctf_status!=FLAG_BASE)
1884 pos = ef.tag_entity.origin;
1888 if(vdist(pos - mf.dropped_origin, >, 700))
1890 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1895 // Set the role timeout if necessary
1896 if (!this.havocbot_role_timeout)
1897 this.havocbot_role_timeout = time + 120;
1899 if (time > this.havocbot_role_timeout)
1901 havocbot_ctf_reset_role(this);
1905 if (navigation_goalrating_timeout(this))
1907 navigation_goalrating_start(this);
1910 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1911 havocbot_goalrating_ctf_enemybase(this, 10000);
1912 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1914 navigation_goalrating_end(this);
1916 navigation_goalrating_timeout_set(this);
1920 // Retriever (temporary role):
1921 void havocbot_role_ctf_retriever(entity this)
1927 havocbot_ctf_reset_role(this);
1931 if (this.flagcarried)
1933 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1937 // If flag is back on the base switch to previous role
1938 mf = havocbot_ctf_find_flag(this);
1939 if(mf.ctf_status==FLAG_BASE)
1941 if (mf.enemy == this) // did this bot return the flag?
1942 navigation_goalrating_timeout_force(this);
1943 havocbot_ctf_reset_role(this);
1947 if (!this.havocbot_role_timeout)
1948 this.havocbot_role_timeout = time + 20;
1950 if (time > this.havocbot_role_timeout)
1952 havocbot_ctf_reset_role(this);
1956 if (navigation_goalrating_timeout(this))
1958 const float RT_RADIUS = 10000;
1960 navigation_goalrating_start(this);
1963 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1964 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1965 havocbot_goalrating_ctf_enemybase(this, 8000);
1966 entity ef = havocbot_ctf_find_enemy_flag(this);
1967 vector enemy_base_org = ef.dropped_origin;
1968 // start collecting items very close to the bot but only inside of enemy base radius
1969 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1970 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1971 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1973 navigation_goalrating_end(this);
1975 navigation_goalrating_timeout_set(this);
1979 void havocbot_role_ctf_middle(entity this)
1985 havocbot_ctf_reset_role(this);
1989 if (this.flagcarried)
1991 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1995 mf = havocbot_ctf_find_flag(this);
1996 if(mf.ctf_status!=FLAG_BASE)
1998 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2002 if (!this.havocbot_role_timeout)
2003 this.havocbot_role_timeout = time + 10;
2005 if (time > this.havocbot_role_timeout)
2007 havocbot_ctf_reset_role(this);
2011 if (navigation_goalrating_timeout(this))
2015 org = havocbot_middlepoint;
2016 org.z = this.origin.z;
2018 navigation_goalrating_start(this);
2021 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
2022 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
2023 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2024 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2025 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2026 havocbot_goalrating_ctf_enemybase(this, 3000);
2028 navigation_goalrating_end(this);
2030 entity goal = this.goalentity;
2031 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
2032 this.goalentity_lock_timeout = time + 2;
2034 navigation_goalrating_timeout_set(this);
2038 void havocbot_role_ctf_defense(entity this)
2044 havocbot_ctf_reset_role(this);
2048 if (this.flagcarried)
2050 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2054 // If own flag was captured
2055 mf = havocbot_ctf_find_flag(this);
2056 if(mf.ctf_status!=FLAG_BASE)
2058 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2062 if (!this.havocbot_role_timeout)
2063 this.havocbot_role_timeout = time + 30;
2065 if (time > this.havocbot_role_timeout)
2067 havocbot_ctf_reset_role(this);
2070 if (navigation_goalrating_timeout(this))
2072 vector org = mf.dropped_origin;
2074 navigation_goalrating_start(this);
2076 // if enemies are closer to our base, go there
2077 entity closestplayer = NULL;
2078 float distance, bestdistance = 10000;
2079 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2080 distance = vlen(org - it.origin);
2081 if(distance<bestdistance)
2084 bestdistance = distance;
2090 if(DIFF_TEAM(closestplayer, this))
2091 if(vdist(org - this.origin, >, 1000))
2092 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2093 havocbot_goalrating_ctf_ourbase(this, 10000);
2095 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2096 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2097 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2098 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2099 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2101 navigation_goalrating_end(this);
2103 navigation_goalrating_timeout_set(this);
2107 void havocbot_role_ctf_setrole(entity bot, int role)
2109 string s = "(null)";
2112 case HAVOCBOT_CTF_ROLE_CARRIER:
2114 bot.havocbot_role = havocbot_role_ctf_carrier;
2115 bot.havocbot_role_timeout = 0;
2116 bot.havocbot_cantfindflag = time + 10;
2117 if (bot.havocbot_previous_role != bot.havocbot_role)
2118 navigation_goalrating_timeout_force(bot);
2120 case HAVOCBOT_CTF_ROLE_DEFENSE:
2122 bot.havocbot_role = havocbot_role_ctf_defense;
2123 bot.havocbot_role_timeout = 0;
2125 case HAVOCBOT_CTF_ROLE_MIDDLE:
2127 bot.havocbot_role = havocbot_role_ctf_middle;
2128 bot.havocbot_role_timeout = 0;
2130 case HAVOCBOT_CTF_ROLE_OFFENSE:
2132 bot.havocbot_role = havocbot_role_ctf_offense;
2133 bot.havocbot_role_timeout = 0;
2135 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2137 bot.havocbot_previous_role = bot.havocbot_role;
2138 bot.havocbot_role = havocbot_role_ctf_retriever;
2139 bot.havocbot_role_timeout = time + 10;
2140 if (bot.havocbot_previous_role != bot.havocbot_role)
2141 navigation_goalrating_timeout_expire(bot, 2);
2143 case HAVOCBOT_CTF_ROLE_ESCORT:
2145 bot.havocbot_previous_role = bot.havocbot_role;
2146 bot.havocbot_role = havocbot_role_ctf_escort;
2147 bot.havocbot_role_timeout = time + 30;
2148 if (bot.havocbot_previous_role != bot.havocbot_role)
2149 navigation_goalrating_timeout_expire(bot, 2);
2152 LOG_TRACE(bot.netname, " switched to ", s);
2160 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2162 entity player = M_ARGV(0, entity);
2164 int t = 0, t2 = 0, t3 = 0;
2165 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)
2167 // initially clear items so they can be set as necessary later.
2168 STAT(OBJECTIVE_STATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2169 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2170 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2171 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2172 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2173 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2175 // scan through all the flags and notify the client about them
2176 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2178 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2179 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2180 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2181 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2182 if(flag.team == 0 && !b5) { b5 = true; t = CTF_NEUTRAL_FLAG_CARRYING; t2 = CTF_NEUTRAL_FLAG_TAKEN; t3 = CTF_NEUTRAL_FLAG_LOST; STAT(OBJECTIVE_STATUS, player) |= CTF_FLAG_NEUTRAL; }
2184 switch(flag.ctf_status)
2189 if((flag.owner == player) || (flag.pass_sender == player))
2190 STAT(OBJECTIVE_STATUS, player) |= t; // carrying: player is currently carrying the flag
2192 STAT(OBJECTIVE_STATUS, player) |= t2; // taken: someone else is carrying the flag
2197 STAT(OBJECTIVE_STATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2203 // item for stopping players from capturing the flag too often
2204 if(player.ctf_captureshielded)
2205 STAT(OBJECTIVE_STATUS, player) |= CTF_SHIELDED;
2208 STAT(OBJECTIVE_STATUS, player) |= CTF_STALEMATE;
2210 // update the health of the flag carrier waypointsprite
2211 if(player.wps_flagcarrier)
2212 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);
2215 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in damage.qc
2217 entity frag_attacker = M_ARGV(1, entity);
2218 entity frag_target = M_ARGV(2, entity);
2219 float frag_damage = M_ARGV(4, float);
2220 vector frag_force = M_ARGV(6, vector);
2222 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2224 if(frag_target == frag_attacker) // damage done to yourself
2226 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2227 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2229 else // damage done to everyone else
2231 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2232 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2235 M_ARGV(4, float) = frag_damage;
2236 M_ARGV(6, vector) = frag_force;
2238 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2240 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
2241 && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2243 frag_target.wps_helpme_time = time;
2244 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2246 // todo: add notification for when flag carrier needs help?
2250 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2252 entity frag_attacker = M_ARGV(1, entity);
2253 entity frag_target = M_ARGV(2, entity);
2255 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2257 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2258 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2261 if(frag_target.flagcarried)
2263 entity tmp_entity = frag_target.flagcarried;
2264 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2265 tmp_entity.ctf_dropper = NULL;
2269 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2271 M_ARGV(2, float) = 0; // frag score
2272 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2275 void ctf_RemovePlayer(entity player)
2277 if(player.flagcarried)
2278 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2280 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2282 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2283 if(flag.pass_target == player) { flag.pass_target = NULL; }
2284 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2288 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2290 entity player = M_ARGV(0, entity);
2292 ctf_RemovePlayer(player);
2295 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2297 entity player = M_ARGV(0, entity);
2299 ctf_RemovePlayer(player);
2302 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2304 if(!autocvar_g_ctf_leaderboard)
2307 entity player = M_ARGV(0, entity);
2309 race_SendAll(player, true);
2312 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2314 if(!autocvar_g_ctf_leaderboard)
2317 entity player = M_ARGV(0, entity);
2319 race_checkAndWriteName(player);
2322 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2324 entity player = M_ARGV(0, entity);
2326 if(player.flagcarried)
2327 if(!autocvar_g_ctf_portalteleport)
2328 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2331 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2333 if(MUTATOR_RETURNVALUE || game_stopped) return;
2335 entity player = M_ARGV(0, entity);
2337 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2339 // pass the flag to a team mate
2340 if(autocvar_g_ctf_pass)
2342 entity head, closest_target = NULL;
2343 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2345 while(head) // find the closest acceptable target to pass to
2347 if(IS_PLAYER(head) && !IS_DEAD(head))
2348 if(head != player && SAME_TEAM(head, player))
2349 if(!head.speedrunning && !head.vehicle)
2351 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in damage.qc)
2352 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2353 vector passer_center = CENTER_OR_VIEWOFS(player);
2355 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2357 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2359 if(IS_BOT_CLIENT(head))
2361 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2362 ctf_Handle_Throw(head, player, DROP_PASS);
2366 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2367 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2369 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2372 else if(player.flagcarried && !head.flagcarried)
2376 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2377 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2378 { closest_target = head; }
2380 else { closest_target = head; }
2387 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2390 // throw the flag in front of you
2391 if(autocvar_g_ctf_throw && player.flagcarried)
2393 if(player.throw_count == -1)
2395 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2397 player.throw_prevtime = time;
2398 player.throw_count = 1;
2399 ctf_Handle_Throw(player, NULL, DROP_THROW);
2404 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2410 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2411 else { player.throw_count += 1; }
2412 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2414 player.throw_prevtime = time;
2415 ctf_Handle_Throw(player, NULL, DROP_THROW);
2422 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2424 entity player = M_ARGV(0, entity);
2426 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2428 player.wps_helpme_time = time;
2429 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2431 else // create a normal help me waypointsprite
2433 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2434 WaypointSprite_Ping(player.wps_helpme);
2440 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2442 entity player = M_ARGV(0, entity);
2443 entity veh = M_ARGV(1, entity);
2445 if(player.flagcarried)
2447 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2449 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2453 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2454 setattachment(player.flagcarried, veh, "");
2455 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2456 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2457 //player.flagcarried.angles = '0 0 0';
2463 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2465 entity player = M_ARGV(0, entity);
2467 if(player.flagcarried)
2469 setattachment(player.flagcarried, player, "");
2470 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2471 player.flagcarried.scale = FLAG_SCALE;
2472 player.flagcarried.angles = '0 0 0';
2473 player.flagcarried.nodrawtoclient = NULL;
2478 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2480 entity player = M_ARGV(0, entity);
2482 if(player.flagcarried)
2484 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2485 ctf_RespawnFlag(player.flagcarried);
2490 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2492 entity flag; // temporary entity for the search method
2494 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2496 switch(flag.ctf_status)
2501 // lock the flag, game is over
2502 set_movetype(flag, MOVETYPE_NONE);
2503 flag.takedamage = DAMAGE_NO;
2504 flag.solid = SOLID_NOT;
2505 flag.nextthink = false; // stop thinking
2507 //dprint("stopping the ", flag.netname, " from moving.\n");
2515 // do nothing for these flags
2522 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2524 entity bot = M_ARGV(0, entity);
2526 havocbot_ctf_reset_role(bot);
2530 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2532 M_ARGV(1, string) = "ctf_team";
2535 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2537 int record_page = M_ARGV(0, int);
2538 string ret_string = M_ARGV(1, string);
2540 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2542 if (MapInfo_Get_ByID(i))
2544 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2550 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2551 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2555 M_ARGV(1, string) = ret_string;
2558 bool superspec_Spectate(entity this, entity targ); // TODO
2559 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2560 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2562 entity player = M_ARGV(0, entity);
2563 string cmd_name = M_ARGV(1, string);
2564 int cmd_argc = M_ARGV(2, int);
2566 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2568 if(cmd_name == "followfc")
2580 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2581 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2582 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2583 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2587 FOREACH_CLIENT(IS_PLAYER(it), {
2588 if(it.flagcarried && (it.team == _team || _team == 0))
2591 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2592 continue; // already spectating this fc, try another
2593 return superspec_Spectate(player, it);
2598 superspec_msg("", "", player, "No active flag carrier\n", 1);
2603 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2605 entity frag_target = M_ARGV(0, entity);
2607 if(frag_target.flagcarried)
2608 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2611 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2613 entity player = M_ARGV(0, entity);
2614 if(player.flagcarried)
2615 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2623 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2624 CTF flag for team one (Red).
2626 "angle" Angle the flag will point (minus 90 degrees)...
2627 "model" model to use, note this needs red and blue as skins 0 and 1...
2628 "noise" sound played when flag is picked up...
2629 "noise1" sound played when flag is returned by a teammate...
2630 "noise2" sound played when flag is captured...
2631 "noise3" sound played when flag is lost in the field and respawns itself...
2632 "noise4" sound played when flag is dropped by a player...
2633 "noise5" sound played when flag touches the ground... */
2634 spawnfunc(item_flag_team1)
2636 if(!g_ctf) { delete(this); return; }
2638 ctf_FlagSetup(NUM_TEAM_1, this);
2641 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2642 CTF flag for team two (Blue).
2644 "angle" Angle the flag will point (minus 90 degrees)...
2645 "model" model to use, note this needs red and blue as skins 0 and 1...
2646 "noise" sound played when flag is picked up...
2647 "noise1" sound played when flag is returned by a teammate...
2648 "noise2" sound played when flag is captured...
2649 "noise3" sound played when flag is lost in the field and respawns itself...
2650 "noise4" sound played when flag is dropped by a player...
2651 "noise5" sound played when flag touches the ground... */
2652 spawnfunc(item_flag_team2)
2654 if(!g_ctf) { delete(this); return; }
2656 ctf_FlagSetup(NUM_TEAM_2, this);
2659 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2660 CTF flag for team three (Yellow).
2662 "angle" Angle the flag will point (minus 90 degrees)...
2663 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2664 "noise" sound played when flag is picked up...
2665 "noise1" sound played when flag is returned by a teammate...
2666 "noise2" sound played when flag is captured...
2667 "noise3" sound played when flag is lost in the field and respawns itself...
2668 "noise4" sound played when flag is dropped by a player...
2669 "noise5" sound played when flag touches the ground... */
2670 spawnfunc(item_flag_team3)
2672 if(!g_ctf) { delete(this); return; }
2674 ctf_FlagSetup(NUM_TEAM_3, this);
2677 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2678 CTF flag for team four (Pink).
2680 "angle" Angle the flag will point (minus 90 degrees)...
2681 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2682 "noise" sound played when flag is picked up...
2683 "noise1" sound played when flag is returned by a teammate...
2684 "noise2" sound played when flag is captured...
2685 "noise3" sound played when flag is lost in the field and respawns itself...
2686 "noise4" sound played when flag is dropped by a player...
2687 "noise5" sound played when flag touches the ground... */
2688 spawnfunc(item_flag_team4)
2690 if(!g_ctf) { delete(this); return; }
2692 ctf_FlagSetup(NUM_TEAM_4, this);
2695 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2698 "angle" Angle the flag will point (minus 90 degrees)...
2699 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2700 "noise" sound played when flag is picked up...
2701 "noise1" sound played when flag is returned by a teammate...
2702 "noise2" sound played when flag is captured...
2703 "noise3" sound played when flag is lost in the field and respawns itself...
2704 "noise4" sound played when flag is dropped by a player...
2705 "noise5" sound played when flag touches the ground... */
2706 spawnfunc(item_flag_neutral)
2708 if(!g_ctf) { delete(this); return; }
2709 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2711 ctf_FlagSetup(0, this);
2714 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2715 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2716 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.
2718 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2719 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2722 if(!g_ctf) { delete(this); return; }
2724 this.team = this.cnt + 1;
2727 // compatibility for quake maps
2728 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2729 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2730 spawnfunc(info_player_team1);
2731 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2732 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2733 spawnfunc(info_player_team2);
2734 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2735 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2737 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2738 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2740 // compatibility for wop maps
2741 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2742 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2743 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2744 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2745 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2746 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2754 void ctf_ScoreRules(int teams)
2756 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2757 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2758 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2759 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2760 field(SP_CTF_PICKUPS, "pickups", 0);
2761 field(SP_CTF_FCKILLS, "fckills", 0);
2762 field(SP_CTF_RETURNS, "returns", 0);
2763 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2767 // code from here on is just to support maps that don't have flag and team entities
2768 void ctf_SpawnTeam (string teamname, int teamcolor)
2770 entity this = new_pure(ctf_team);
2771 this.netname = teamname;
2772 this.cnt = teamcolor - 1;
2773 this.spawnfunc_checked = true;
2774 this.team = teamcolor;
2777 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2782 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2784 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2785 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2787 switch(tmp_entity.team)
2789 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2790 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2791 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2792 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2794 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2797 havocbot_ctf_calculate_middlepoint();
2799 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2801 ctf_teams = 0; // so set the default red and blue teams
2802 BITSET_ASSIGN(ctf_teams, BIT(0));
2803 BITSET_ASSIGN(ctf_teams, BIT(1));
2806 //ctf_teams = bound(2, ctf_teams, 4);
2808 // if no teams are found, spawn defaults
2809 if(find(NULL, classname, "ctf_team") == NULL)
2811 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2812 if(ctf_teams & BIT(0))
2813 ctf_SpawnTeam("Red", NUM_TEAM_1);
2814 if(ctf_teams & BIT(1))
2815 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2816 if(ctf_teams & BIT(2))
2817 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2818 if(ctf_teams & BIT(3))
2819 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2822 ctf_ScoreRules(ctf_teams);
2825 void ctf_Initialize()
2827 CTF_FLAG = NEW(Flag);
2828 record_type = CTF_RECORD;
2829 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2831 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2832 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2833 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2835 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);