3 #include <common/effects/all.qh>
4 #include <common/mapobjects/teleporters.qh>
5 #include <common/mapobjects/triggers.qh>
6 #include <common/vehicles/all.qh>
7 #include <server/command/vote.qh>
8 #include <server/client.qh>
9 #include <server/gamelog.qh>
10 #include <server/g_damage.qh>
11 #include <server/g_world.qh>
12 #include <server/items/items.qh>
13 #include <server/race.qh>
14 #include <server/teamplay.qh>
16 #include <lib/warpzone/common.qh>
18 bool autocvar_g_ctf_allow_vehicle_carry;
19 bool autocvar_g_ctf_allow_vehicle_touch;
20 bool autocvar_g_ctf_allow_monster_touch;
21 bool autocvar_g_ctf_throw;
22 float autocvar_g_ctf_throw_angle_max;
23 float autocvar_g_ctf_throw_angle_min;
24 int autocvar_g_ctf_throw_punish_count;
25 float autocvar_g_ctf_throw_punish_delay;
26 float autocvar_g_ctf_throw_punish_time;
27 float autocvar_g_ctf_throw_strengthmultiplier;
28 float autocvar_g_ctf_throw_velocity_forward;
29 float autocvar_g_ctf_throw_velocity_up;
30 float autocvar_g_ctf_drop_velocity_up;
31 float autocvar_g_ctf_drop_velocity_side;
32 bool autocvar_g_ctf_oneflag_reverse;
33 bool autocvar_g_ctf_portalteleport;
34 bool autocvar_g_ctf_pass;
35 float autocvar_g_ctf_pass_arc;
36 float autocvar_g_ctf_pass_arc_max;
37 float autocvar_g_ctf_pass_directional_max;
38 float autocvar_g_ctf_pass_directional_min;
39 float autocvar_g_ctf_pass_radius;
40 float autocvar_g_ctf_pass_wait;
41 bool autocvar_g_ctf_pass_request;
42 float autocvar_g_ctf_pass_turnrate;
43 float autocvar_g_ctf_pass_timelimit;
44 float autocvar_g_ctf_pass_velocity;
45 bool autocvar_g_ctf_dynamiclights;
46 float autocvar_g_ctf_flag_collect_delay;
47 float autocvar_g_ctf_flag_damageforcescale;
48 bool autocvar_g_ctf_flag_dropped_waypoint;
49 bool autocvar_g_ctf_flag_dropped_floatinwater;
50 bool autocvar_g_ctf_flag_glowtrails;
51 int autocvar_g_ctf_flag_health;
52 bool autocvar_g_ctf_flag_return;
53 bool autocvar_g_ctf_flag_return_carrying;
54 float autocvar_g_ctf_flag_return_carried_radius;
55 float autocvar_g_ctf_flag_return_time;
56 bool autocvar_g_ctf_flag_return_when_unreachable;
57 float autocvar_g_ctf_flag_return_damage;
58 float autocvar_g_ctf_flag_return_damage_delay;
59 float autocvar_g_ctf_flag_return_dropped;
60 bool autocvar_g_ctf_flag_waypoint = true;
61 float autocvar_g_ctf_flag_waypoint_maxdistance;
62 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
63 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
64 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
65 float autocvar_g_ctf_flagcarrier_selfforcefactor;
66 float autocvar_g_ctf_flagcarrier_damagefactor;
67 float autocvar_g_ctf_flagcarrier_forcefactor;
68 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
69 bool autocvar_g_ctf_fullbrightflags;
70 bool autocvar_g_ctf_ignore_frags;
71 bool autocvar_g_ctf_score_ignore_fields;
72 int autocvar_g_ctf_score_capture;
73 int autocvar_g_ctf_score_capture_assist;
74 int autocvar_g_ctf_score_kill;
75 int autocvar_g_ctf_score_penalty_drop;
76 int autocvar_g_ctf_score_penalty_returned;
77 int autocvar_g_ctf_score_pickup_base;
78 int autocvar_g_ctf_score_pickup_dropped_early;
79 int autocvar_g_ctf_score_pickup_dropped_late;
80 int autocvar_g_ctf_score_return;
81 float autocvar_g_ctf_shield_force;
82 float autocvar_g_ctf_shield_max_ratio;
83 int autocvar_g_ctf_shield_min_negscore;
84 bool autocvar_g_ctf_stalemate;
85 int autocvar_g_ctf_stalemate_endcondition;
86 float autocvar_g_ctf_stalemate_time;
87 bool autocvar_g_ctf_reverse;
88 float autocvar_g_ctf_dropped_capture_delay;
89 float autocvar_g_ctf_dropped_capture_radius;
91 void ctf_FakeTimeLimit(entity e, float t)
94 WriteByte(MSG_ONE, 3); // svc_updatestat
95 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
97 WriteCoord(MSG_ONE, autocvar_timelimit);
99 WriteCoord(MSG_ONE, (t + 1) / 60);
102 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
104 if(autocvar_sv_eventlog)
105 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != NULL) ? ftos(actor.playerid) : "")));
106 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
109 void ctf_CaptureRecord(entity flag, entity player)
111 float cap_record = ctf_captimerecord;
112 float cap_time = (time - flag.ctf_pickuptime);
113 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
117 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname);
118 else if(!ctf_captimerecord)
119 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_TIME), player.netname, TIME_ENCODE(cap_time));
120 else if(cap_time < cap_record)
121 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));
123 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));
125 // write that shit in the database
126 if(!ctf_oneflag) // but not in 1-flag mode
127 if((!ctf_captimerecord) || (cap_time < cap_record))
129 ctf_captimerecord = cap_time;
130 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
131 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
132 write_recordmarker(player, flag.ctf_pickuptime, cap_time);
135 if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
136 race_setTime(GetMapname(), TIME_ENCODE(cap_time), player.crypto_idfp, player.netname, player, false);
139 bool ctf_Immediate_Return_Allowed(entity flag, entity toucher)
142 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), { ++num_perteam; });
144 // automatically return if there's only 1 player on the team
145 return ((autocvar_g_ctf_flag_return || num_perteam <= 1 || (autocvar_g_ctf_flag_return_carrying && toucher.flagcarried))
149 bool ctf_Return_Customize(entity this, entity client)
151 // only to the carrier
152 return boolean(client == this.owner);
155 void ctf_FlagcarrierWaypoints(entity player)
157 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
158 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, 2 * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
159 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);
160 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
162 if(player.flagcarried && CTF_SAMETEAM(player, player.flagcarried))
164 if(!player.wps_enemyflagcarrier)
166 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, 0, player, wps_enemyflagcarrier, true, RADARICON_FLAG);
167 wp.colormod = WPCOLOR_ENEMYFC(player.team);
168 setcefc(wp, ctf_Stalemate_Customize);
170 if(IS_REAL_CLIENT(player) && !ctf_stalemate)
171 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_VISIBLE);
174 if(!player.wps_flagreturn)
176 entity owp = WaypointSprite_SpawnFixed(WP_FlagReturn, player.flagcarried.ctf_spawnorigin + FLAG_WAYPOINT_OFFSET, player, wps_flagreturn, RADARICON_FLAG);
177 owp.colormod = '0 0.8 0.8';
178 //WaypointSprite_UpdateTeamRadar(player.wps_flagreturn, RADARICON_FLAG, ((player.team) ? colormapPaletteColor(player.team - 1, false) : '1 1 1'));
179 setcefc(owp, ctf_Return_Customize);
184 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
186 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
187 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
188 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
189 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
192 if(current_height) // make sure we can actually do this arcing path
194 targpos = (to + ('0 0 1' * current_height));
195 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
196 if(trace_fraction < 1)
198 //print("normal arc line failed, trying to find new pos...");
199 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
200 targpos = (trace_endpos + eZ * FLAG_PASS_ARC_OFFSET_Z);
201 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
202 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
203 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
206 else { targpos = to; }
208 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
210 vector desired_direction = normalize(targpos - from);
211 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
212 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
215 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
217 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
219 // directional tracing only
221 makevectors(passer_angle);
223 // find the closest point on the enemy to the center of the attack
224 float h; // hypotenuse, which is the distance between attacker to head
225 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
227 h = vlen(head_center - passer_center);
228 a = h * (normalize(head_center - passer_center) * v_forward);
230 vector nearest_on_line = (passer_center + a * v_forward);
231 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
233 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
234 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
236 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
241 else { return true; }
245 // =======================
246 // CaptureShield Functions
247 // =======================
249 bool ctf_CaptureShield_CheckStatus(entity p)
251 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
252 int players_worseeq, players_total;
254 if(ctf_captureshield_max_ratio <= 0)
257 s = GameRules_scoring_add(p, CTF_CAPS, 0);
258 s2 = GameRules_scoring_add(p, CTF_PICKUPS, 0);
259 s3 = GameRules_scoring_add(p, CTF_RETURNS, 0);
260 s4 = GameRules_scoring_add(p, CTF_FCKILLS, 0);
262 sr = ((s - s2) + (s3 + s4));
264 if(sr >= -ctf_captureshield_min_negscore)
267 players_total = players_worseeq = 0;
268 FOREACH_CLIENT(IS_PLAYER(it), {
271 se = GameRules_scoring_add(it, CTF_CAPS, 0);
272 se2 = GameRules_scoring_add(it, CTF_PICKUPS, 0);
273 se3 = GameRules_scoring_add(it, CTF_RETURNS, 0);
274 se4 = GameRules_scoring_add(it, CTF_FCKILLS, 0);
276 ser = ((se - se2) + (se3 + se4));
283 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
284 // use this rule here
286 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
292 void ctf_CaptureShield_Update(entity player, bool wanted_status)
294 bool updated_status = ctf_CaptureShield_CheckStatus(player);
295 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
297 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
298 player.ctf_captureshielded = updated_status;
302 bool ctf_CaptureShield_Customize(entity this, entity client)
304 if(!client.ctf_captureshielded) { return false; }
305 if(CTF_SAMETEAM(this, client)) { return false; }
310 void ctf_CaptureShield_Touch(entity this, entity toucher)
312 if(!toucher.ctf_captureshielded) { return; }
313 if(CTF_SAMETEAM(this, toucher)) { return; }
315 vector mymid = (this.absmin + this.absmax) * 0.5;
316 vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
318 Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
319 if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
322 void ctf_CaptureShield_Spawn(entity flag)
324 entity shield = new(ctf_captureshield);
327 shield.team = flag.team;
328 settouch(shield, ctf_CaptureShield_Touch);
329 setcefc(shield, ctf_CaptureShield_Customize);
330 shield.effects = EF_ADDITIVE;
331 set_movetype(shield, MOVETYPE_NOCLIP);
332 shield.solid = SOLID_TRIGGER;
333 shield.avelocity = '7 0 11';
336 setorigin(shield, flag.origin);
337 setmodel(shield, MDL_CTF_SHIELD);
338 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
342 // ====================
343 // Drop/Pass/Throw Code
344 // ====================
346 void ctf_Handle_Drop(entity flag, entity player, int droptype)
349 player = (player ? player : flag.pass_sender);
352 set_movetype(flag, MOVETYPE_TOSS);
353 flag.takedamage = DAMAGE_YES;
354 flag.angles = '0 0 0';
355 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
356 flag.ctf_droptime = time;
357 flag.ctf_dropper = player;
358 flag.ctf_status = FLAG_DROPPED;
360 // messages and sounds
361 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
362 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
363 ctf_EventLog("dropped", player.team, player);
366 GameRules_scoring_add_team(player, SCORE, -((flag.score_drop) ? flag.score_drop : autocvar_g_ctf_score_penalty_drop));
367 GameRules_scoring_add(player, CTF_DROPS, 1);
370 if(autocvar_g_ctf_flag_dropped_waypoint) {
371 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);
372 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
375 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
377 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_health);
378 WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH));
381 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
383 if(droptype == DROP_PASS)
385 flag.pass_distance = 0;
386 flag.pass_sender = NULL;
387 flag.pass_target = NULL;
391 void ctf_Handle_Retrieve(entity flag, entity player)
393 entity sender = flag.pass_sender;
395 // transfer flag to player
397 flag.owner.flagcarried = flag;
398 GameRules_scoring_vip(player, true);
403 setattachment(flag, player.vehicle, "");
404 setorigin(flag, VEHICLE_FLAG_OFFSET);
405 flag.scale = VEHICLE_FLAG_SCALE;
409 setattachment(flag, player, "");
410 setorigin(flag, FLAG_CARRY_OFFSET);
412 set_movetype(flag, MOVETYPE_NONE);
413 flag.takedamage = DAMAGE_NO;
414 flag.solid = SOLID_NOT;
415 flag.angles = '0 0 0';
416 flag.ctf_status = FLAG_CARRY;
418 // messages and sounds
419 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
420 ctf_EventLog("receive", flag.team, player);
422 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
424 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
425 else if(it == player)
426 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
427 else if(SAME_TEAM(it, sender))
428 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
431 // create new waypoint
432 ctf_FlagcarrierWaypoints(player);
434 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
435 player.throw_antispam = sender.throw_antispam;
437 flag.pass_distance = 0;
438 flag.pass_sender = NULL;
439 flag.pass_target = NULL;
442 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
444 entity flag = player.flagcarried;
445 vector targ_origin, flag_velocity;
447 if(!flag) { return; }
448 if((droptype == DROP_PASS) && !receiver) { return; }
450 if(flag.speedrunning)
452 // ensure old waypoints are removed before resetting the flag
453 WaypointSprite_Kill(player.wps_flagcarrier);
455 if(player.wps_enemyflagcarrier)
456 WaypointSprite_Kill(player.wps_enemyflagcarrier);
458 if(player.wps_flagreturn)
459 WaypointSprite_Kill(player.wps_flagreturn);
460 ctf_RespawnFlag(flag);
465 setattachment(flag, NULL, "");
466 tracebox(player.origin - FLAG_DROP_OFFSET, flag.m_mins, flag.m_maxs, player.origin + FLAG_DROP_OFFSET, MOVE_NOMONSTERS, flag);
467 setorigin(flag, trace_endpos);
468 flag.owner.flagcarried = NULL;
469 GameRules_scoring_vip(flag.owner, false);
471 flag.solid = SOLID_TRIGGER;
472 flag.ctf_dropper = player;
473 flag.ctf_droptime = time;
475 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
482 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
483 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
484 WarpZone_RefSys_Copy(flag, receiver);
485 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
486 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
488 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
489 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
492 set_movetype(flag, MOVETYPE_FLY);
493 flag.takedamage = DAMAGE_NO;
494 flag.pass_sender = player;
495 flag.pass_target = receiver;
496 flag.ctf_status = FLAG_PASSING;
499 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
500 WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
501 ctf_EventLog("pass", flag.team, player);
507 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'));
509 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)));
510 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
511 ctf_Handle_Drop(flag, player, droptype);
512 navigation_dynamicgoal_set(flag, player);
518 flag.velocity = '0 0 0'; // do nothing
525 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);
526 ctf_Handle_Drop(flag, player, droptype);
527 navigation_dynamicgoal_set(flag, player);
532 // kill old waypointsprite
533 WaypointSprite_Ping(player.wps_flagcarrier);
534 WaypointSprite_Kill(player.wps_flagcarrier);
536 if(player.wps_enemyflagcarrier)
537 WaypointSprite_Kill(player.wps_enemyflagcarrier);
539 if(player.wps_flagreturn)
540 WaypointSprite_Kill(player.wps_flagreturn);
543 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
547 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
549 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
557 void nades_GiveBonus(entity player, float score);
559 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
561 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
562 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
563 entity player_team_flag = NULL, tmp_entity;
564 float old_time, new_time;
566 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
567 if(CTF_DIFFTEAM(player, flag)) { return; }
568 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)
570 if (toucher.goalentity == flag.bot_basewaypoint)
571 toucher.goalentity_lock_timeout = 0;
574 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
575 if(SAME_TEAM(tmp_entity, player))
577 player_team_flag = tmp_entity;
581 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
583 player.throw_prevtime = time;
584 player.throw_count = 0;
586 // messages and sounds
587 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
588 ctf_CaptureRecord(enemy_flag, player);
589 _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);
593 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
594 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
600 if(enemy_flag.score_capture || flag.score_capture)
601 pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
602 GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
604 if(enemy_flag.score_team_capture || flag.score_team_capture)
605 capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
606 GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
608 old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
609 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
610 if(!old_time || new_time < old_time)
611 GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
614 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
616 shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
620 if(capturetype == CAPTURE_NORMAL)
622 WaypointSprite_Kill(player.wps_flagcarrier);
623 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
625 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
626 { GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist)); }
629 flag.enemy = toucher;
632 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
633 ctf_RespawnFlag(enemy_flag);
636 void ctf_Handle_Return(entity flag, entity player)
638 // messages and sounds
639 if(IS_MONSTER(player))
641 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
645 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
646 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
648 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
649 ctf_EventLog("return", flag.team, player);
652 if(IS_PLAYER(player))
654 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
655 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
657 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
660 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
664 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
665 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
666 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
670 if(player.flagcarried == flag)
671 WaypointSprite_Kill(player.wps_flagcarrier);
676 ctf_RespawnFlag(flag);
679 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
682 float pickup_dropped_score; // used to calculate dropped pickup score
684 // attach the flag to the player
686 player.flagcarried = flag;
687 GameRules_scoring_vip(player, true);
690 setattachment(flag, player.vehicle, "");
691 setorigin(flag, VEHICLE_FLAG_OFFSET);
692 flag.scale = VEHICLE_FLAG_SCALE;
696 setattachment(flag, player, "");
697 setorigin(flag, FLAG_CARRY_OFFSET);
701 set_movetype(flag, MOVETYPE_NONE);
702 flag.takedamage = DAMAGE_NO;
703 flag.solid = SOLID_NOT;
704 flag.angles = '0 0 0';
705 flag.ctf_status = FLAG_CARRY;
709 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
710 case PICKUP_DROPPED: SetResourceExplicit(flag, RES_HEALTH, flag.max_health); break; // reset health/return timelimit
714 // messages and sounds
715 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
717 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
719 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
720 else if(CTF_DIFFTEAM(player, flag))
721 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
723 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
725 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
728 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); });
731 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
732 if(CTF_SAMETEAM(flag, it))
734 if(SAME_TEAM(player, it))
735 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
737 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);
741 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
744 GameRules_scoring_add(player, CTF_PICKUPS, 1);
745 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
750 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
751 ctf_EventLog("steal", flag.team, player);
757 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);
758 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);
759 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
760 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
761 ctf_EventLog("pickup", flag.team, player);
769 if(pickuptype == PICKUP_BASE)
771 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
772 if((player.speedrunning) && (ctf_captimerecord))
773 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
777 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
780 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
781 ctf_FlagcarrierWaypoints(player);
782 WaypointSprite_Ping(player.wps_flagcarrier);
786 // ===================
787 // Main Flag Functions
788 // ===================
790 void ctf_CheckFlagReturn(entity flag, int returntype)
792 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
794 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH)); }
796 if((GetResource(flag, RES_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
801 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
803 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
804 case RETURN_SPEEDRUN:
805 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
806 case RETURN_NEEDKILL:
807 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
810 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
812 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
813 ctf_EventLog("returned", flag.team, NULL);
815 ctf_RespawnFlag(flag);
820 bool ctf_Stalemate_Customize(entity this, entity client)
822 // make spectators see what the player would see
823 entity e = WaypointSprite_getviewentity(client);
824 entity wp_owner = this.owner;
827 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
828 if(SAME_TEAM(wp_owner, e)) { return false; }
829 if(!IS_PLAYER(e)) { return false; }
834 void ctf_CheckStalemate()
837 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
840 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
842 // build list of stale flags
843 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
845 if(autocvar_g_ctf_stalemate)
846 if(tmp_entity.ctf_status != FLAG_BASE)
847 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
849 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
850 ctf_staleflaglist = tmp_entity;
852 switch(tmp_entity.team)
854 case NUM_TEAM_1: ++stale_red_flags; break;
855 case NUM_TEAM_2: ++stale_blue_flags; break;
856 case NUM_TEAM_3: ++stale_yellow_flags; break;
857 case NUM_TEAM_4: ++stale_pink_flags; break;
858 default: ++stale_neutral_flags; break;
864 stale_flags = (stale_neutral_flags >= 1);
866 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
868 if(ctf_oneflag && stale_flags == 1)
869 ctf_stalemate = true;
870 else if(stale_flags >= 2)
871 ctf_stalemate = true;
872 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
873 { ctf_stalemate = false; wpforenemy_announced = false; }
874 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
875 { ctf_stalemate = false; wpforenemy_announced = false; }
877 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
880 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
882 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
884 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);
885 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
886 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
890 if (!wpforenemy_announced)
892 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)); });
894 wpforenemy_announced = true;
899 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
901 if(ITEM_DAMAGE_NEEDKILL(deathtype))
903 if(autocvar_g_ctf_flag_return_damage_delay)
904 this.ctf_flagdamaged_byworld = true;
907 SetResourceExplicit(this, RES_HEALTH, 0);
908 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
912 if(autocvar_g_ctf_flag_return_damage)
914 // reduce health and check if it should be returned
915 TakeResource(this, RES_HEALTH, damage);
916 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
921 void ctf_FlagThink(entity this)
926 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
929 if(this == ctf_worldflaglist) // only for the first flag
930 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
933 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
934 LOG_TRACE("wtf the flag got squashed?");
935 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
936 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
937 setsize(this, this.m_mins, this.m_maxs);
941 switch(this.ctf_status)
945 if(autocvar_g_ctf_dropped_capture_radius)
947 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
948 if(tmp_entity.ctf_status == FLAG_DROPPED)
949 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
950 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
951 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
958 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
960 if(autocvar_g_ctf_flag_dropped_floatinwater)
962 vector midpoint = ((this.absmin + this.absmax) * 0.5);
963 if(pointcontents(midpoint) == CONTENT_WATER)
965 this.velocity = this.velocity * 0.5;
967 if (pointcontents(midpoint + eZ * FLAG_FLOAT_OFFSET_Z) == CONTENT_WATER)
968 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
970 { set_movetype(this, MOVETYPE_FLY); }
972 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
974 if(autocvar_g_ctf_flag_return_dropped)
976 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
978 SetResourceExplicit(this, RES_HEALTH, 0);
979 ctf_CheckFlagReturn(this, RETURN_DROPPED);
983 if(this.ctf_flagdamaged_byworld)
985 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
986 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
989 else if(autocvar_g_ctf_flag_return_time)
991 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
992 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
1000 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
1002 SetResourceExplicit(this, RES_HEALTH, 0);
1003 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
1005 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
1006 ImpulseCommands(this.owner);
1008 if(autocvar_g_ctf_stalemate)
1010 if(time >= wpforenemy_nextthink)
1012 ctf_CheckStalemate();
1013 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1016 if(CTF_SAMETEAM(this, this.owner) && this.team)
1018 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1019 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
1020 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
1021 ctf_Handle_Return(this, this.owner);
1028 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1029 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1030 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1032 if((this.pass_target == NULL)
1033 || (IS_DEAD(this.pass_target))
1034 || (this.pass_target.flagcarried)
1035 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1036 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1037 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1039 // give up, pass failed
1040 ctf_Handle_Drop(this, NULL, DROP_PASS);
1044 // still a viable target, go for it
1045 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1050 default: // this should never happen
1052 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1058 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1061 if(game_stopped) return;
1062 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1064 bool is_not_monster = (!IS_MONSTER(toucher));
1066 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1067 if(ITEM_TOUCH_NEEDKILL())
1069 if(!autocvar_g_ctf_flag_return_damage_delay)
1071 SetResourceExplicit(flag, RES_HEALTH, 0);
1072 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1074 if(!flag.ctf_flagdamaged_byworld) { return; }
1077 // special touch behaviors
1078 if(STAT(FROZEN, toucher)) { return; }
1079 else if(IS_VEHICLE(toucher))
1081 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1082 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1084 return; // do nothing
1086 else if(IS_MONSTER(toucher))
1088 if(!autocvar_g_ctf_allow_monster_touch)
1089 return; // do nothing
1091 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1093 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1095 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1096 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1097 flag.wait = time + FLAG_TOUCHRATE;
1101 else if(IS_DEAD(toucher)) { return; }
1103 switch(flag.ctf_status)
1109 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1110 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1111 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1112 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1114 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1115 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1116 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)
1118 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1119 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1121 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1122 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1128 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1129 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1130 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1131 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1137 LOG_TRACE("Someone touched a flag even though it was being carried?");
1143 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1145 if(DIFF_TEAM(toucher, flag.pass_sender))
1147 if(ctf_Immediate_Return_Allowed(flag, toucher))
1148 ctf_Handle_Return(flag, toucher);
1149 else if(is_not_monster && (!toucher.flagcarried))
1150 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1152 else if(!toucher.flagcarried)
1153 ctf_Handle_Retrieve(flag, toucher);
1160 .float last_respawn;
1161 void ctf_RespawnFlag(entity flag)
1163 flag.watertype = CONTENT_EMPTY; // TODO: it is unclear why this workaround is needed, likely many other potential breakage points!!
1164 // check for flag respawn being called twice in a row
1165 if(flag.last_respawn > time - 0.5)
1166 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1168 flag.last_respawn = time;
1170 // reset the player (if there is one)
1171 if((flag.owner) && (flag.owner.flagcarried == flag))
1173 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1174 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1175 WaypointSprite_Kill(flag.wps_flagcarrier);
1177 flag.owner.flagcarried = NULL;
1178 GameRules_scoring_vip(flag.owner, false);
1180 if(flag.speedrunning)
1181 ctf_FakeTimeLimit(flag.owner, -1);
1184 if((flag.owner) && (flag.owner.vehicle))
1185 flag.scale = FLAG_SCALE;
1187 if(flag.ctf_status == FLAG_DROPPED)
1188 { WaypointSprite_Kill(flag.wps_flagdropped); }
1191 setattachment(flag, NULL, "");
1192 setorigin(flag, flag.ctf_spawnorigin);
1194 //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1195 set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1196 flag.takedamage = DAMAGE_NO;
1197 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1198 flag.solid = SOLID_TRIGGER;
1199 flag.velocity = '0 0 0';
1200 flag.angles = flag.mangle;
1201 flag.flags = FL_ITEM | FL_NOTARGET;
1203 flag.ctf_status = FLAG_BASE;
1205 flag.pass_distance = 0;
1206 flag.pass_sender = NULL;
1207 flag.pass_target = NULL;
1208 flag.ctf_dropper = NULL;
1209 flag.ctf_pickuptime = 0;
1210 flag.ctf_droptime = 0;
1211 flag.ctf_flagdamaged_byworld = false;
1212 navigation_dynamicgoal_unset(flag);
1214 ctf_CheckStalemate();
1217 void ctf_Reset(entity this)
1219 if(this.owner && IS_PLAYER(this.owner))
1220 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1223 ctf_RespawnFlag(this);
1226 bool ctf_FlagBase_Customize(entity this, entity client)
1228 entity e = WaypointSprite_getviewentity(client);
1229 entity wp_owner = this.owner;
1230 entity flag = e.flagcarried;
1231 if(flag && CTF_SAMETEAM(e, flag))
1233 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1238 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1241 waypoint_spawnforitem_force(this, this.origin);
1242 navigation_dynamicgoal_init(this, true);
1248 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1249 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1250 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1251 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1252 default: basename = WP_FlagBaseNeutral; break;
1255 if(autocvar_g_ctf_flag_waypoint)
1257 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1258 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1259 wp.fade_rate = autocvar_g_ctf_flag_waypoint_maxdistance;
1260 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1261 setcefc(wp, ctf_FlagBase_Customize);
1264 // captureshield setup
1265 ctf_CaptureShield_Spawn(this);
1270 void ctf_FlagSetup(int teamnum, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1273 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1274 ctf_worldflaglist = flag;
1276 setattachment(flag, NULL, "");
1278 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnum), Team_ColorName_Upper(teamnum)));
1279 flag.team = teamnum;
1280 flag.classname = "item_flag_team";
1281 flag.target = "###item###"; // for finding the nearest item using findnearest
1282 flag.flags = FL_ITEM | FL_NOTARGET;
1283 IL_PUSH(g_items, flag);
1284 flag.solid = SOLID_TRIGGER;
1285 flag.takedamage = DAMAGE_NO;
1286 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1287 flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1288 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1289 flag.event_damage = ctf_FlagDamage;
1290 flag.pushable = true;
1291 flag.teleportable = TELEPORT_NORMAL;
1292 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1293 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1294 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1295 if(flag.damagedbycontents)
1296 IL_PUSH(g_damagedbycontents, flag);
1297 flag.velocity = '0 0 0';
1298 flag.mangle = flag.angles;
1299 flag.reset = ctf_Reset;
1300 settouch(flag, ctf_FlagTouch);
1301 setthink(flag, ctf_FlagThink);
1302 flag.nextthink = time + FLAG_THINKRATE;
1303 flag.ctf_status = FLAG_BASE;
1305 // crudely force them all to 0
1306 if(autocvar_g_ctf_score_ignore_fields)
1307 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1309 string teamname = Static_Team_ColorName_Lower(teamnum);
1311 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1312 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1313 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1314 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnum).eent_eff_name; }
1315 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnum).eent_eff_name; }
1316 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnum).eent_eff_name; }
1320 if(flag.s == "") flag.s = b; \
1321 precache_sound(flag.s);
1323 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnum))))
1324 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnum))))
1325 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnum))))
1326 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnum))))
1327 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1328 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1329 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1333 precache_model(flag.model);
1336 _setmodel(flag, flag.model); // precision set below
1337 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1338 flag.m_mins = flag.mins; // store these for squash checks
1339 flag.m_maxs = flag.maxs;
1340 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1342 if(autocvar_g_ctf_flag_glowtrails)
1346 case NUM_TEAM_1: flag.glow_color = 251; break;
1347 case NUM_TEAM_2: flag.glow_color = 210; break;
1348 case NUM_TEAM_3: flag.glow_color = 110; break;
1349 case NUM_TEAM_4: flag.glow_color = 145; break;
1350 default: flag.glow_color = 254; break;
1352 flag.glow_size = 25;
1353 flag.glow_trail = 1;
1356 flag.effects |= EF_LOWPRECISION;
1357 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1358 if(autocvar_g_ctf_dynamiclights)
1362 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1363 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1364 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1365 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1366 default: flag.effects |= EF_DIMLIGHT; break;
1371 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1373 flag.dropped_origin = flag.origin;
1374 flag.noalign = true;
1375 set_movetype(flag, MOVETYPE_NONE);
1377 else // drop to floor, automatically find a platform and set that as spawn origin
1379 flag.noalign = false;
1381 set_movetype(flag, MOVETYPE_NONE);
1384 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1392 // NOTE: LEGACY CODE, needs to be re-written!
1394 void havocbot_ctf_calculate_middlepoint()
1398 vector fo = '0 0 0';
1401 f = ctf_worldflaglist;
1406 f = f.ctf_worldflagnext;
1412 havocbot_middlepoint = s / n;
1413 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1415 havocbot_symmetry_axis_m = 0;
1416 havocbot_symmetry_axis_q = 0;
1419 // for symmetrical editing of waypoints
1420 entity f1 = ctf_worldflaglist;
1421 entity f2 = f1.ctf_worldflagnext;
1422 float m = -(f1.origin.y - f2.origin.y) / (max(f1.origin.x - f2.origin.x, FLOAT_EPSILON));
1423 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1424 havocbot_symmetry_axis_m = m;
1425 havocbot_symmetry_axis_q = q;
1427 havocbot_symmetry_origin_order = n;
1431 entity havocbot_ctf_find_flag(entity bot)
1434 f = ctf_worldflaglist;
1437 if (CTF_SAMETEAM(bot, f))
1439 f = f.ctf_worldflagnext;
1444 entity havocbot_ctf_find_enemy_flag(entity bot)
1447 f = ctf_worldflaglist;
1452 if(CTF_DIFFTEAM(bot, f))
1459 else if(!bot.flagcarried)
1463 else if (CTF_DIFFTEAM(bot, f))
1465 f = f.ctf_worldflagnext;
1470 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1477 FOREACH_CLIENT(IS_PLAYER(it), {
1478 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1481 if(vdist(it.origin - org, <, tc_radius))
1490 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1493 head = ctf_worldflaglist;
1496 if (CTF_SAMETEAM(this, head))
1498 head = head.ctf_worldflagnext;
1501 navigation_routerating(this, head, ratingscale, 10000);
1505 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1508 head = ctf_worldflaglist;
1511 if (CTF_SAMETEAM(this, head))
1513 if (this.flagcarried)
1514 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1516 head = head.ctf_worldflagnext; // skip base if it has a different group
1521 head = head.ctf_worldflagnext;
1526 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1529 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1532 head = ctf_worldflaglist;
1537 if(CTF_DIFFTEAM(this, head))
1541 if(this.flagcarried)
1544 else if(!this.flagcarried)
1548 else if(CTF_DIFFTEAM(this, head))
1550 head = head.ctf_worldflagnext;
1554 if (head.ctf_status == FLAG_CARRY)
1556 // adjust rating of our flag carrier depending on his health
1557 head = head.tag_entity;
1558 float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1559 ratingscale += ratingscale * f * 0.1;
1561 navigation_routerating(this, head, ratingscale, 10000);
1565 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1567 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1569 if (!bot_waypoints_for_items)
1571 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1577 head = havocbot_ctf_find_enemy_flag(this);
1582 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1585 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1589 mf = havocbot_ctf_find_flag(this);
1591 if(mf.ctf_status == FLAG_BASE)
1595 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1598 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1601 head = ctf_worldflaglist;
1604 // flag is out in the field
1605 if(head.ctf_status != FLAG_BASE)
1606 if(head.tag_entity==NULL) // dropped
1610 if(vdist(org - head.origin, <, df_radius))
1611 navigation_routerating(this, head, ratingscale, 10000);
1614 navigation_routerating(this, head, ratingscale, 10000);
1617 head = head.ctf_worldflagnext;
1621 void havocbot_ctf_reset_role(entity this)
1623 float cdefense, cmiddle, coffense;
1630 if (this.flagcarried)
1632 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1636 mf = havocbot_ctf_find_flag(this);
1637 ef = havocbot_ctf_find_enemy_flag(this);
1639 // Retrieve stolen flag
1640 if(mf.ctf_status!=FLAG_BASE)
1642 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1646 // If enemy flag is taken go to the middle to intercept pursuers
1647 if(ef.ctf_status!=FLAG_BASE)
1649 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1653 // if there is no one else on the team switch to offense
1655 // don't check if this bot is a player since it isn't true when the bot is added to the server
1656 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1660 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1663 else if (time < CS(this).jointime + 1)
1665 // if bots spawn all at once set good default roles
1668 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1671 else if (count == 2)
1673 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1678 // Evaluate best position to take
1679 // Count mates on middle position
1680 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1682 // Count mates on defense position
1683 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1685 // Count mates on offense position
1686 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1688 if(cdefense<=coffense)
1689 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1690 else if(coffense<=cmiddle)
1691 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1693 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1695 // if bots spawn all at once assign them a more appropriated role after a while
1696 if (time < CS(this).jointime + 1 && count > 2)
1697 this.havocbot_role_timeout = time + 10 + random() * 10;
1700 bool havocbot_ctf_is_basewaypoint(entity item)
1702 if (item.classname != "waypoint")
1705 entity head = ctf_worldflaglist;
1708 if (item == head.bot_basewaypoint)
1710 head = head.ctf_worldflagnext;
1715 void havocbot_role_ctf_carrier(entity this)
1719 havocbot_ctf_reset_role(this);
1723 if (this.flagcarried == NULL)
1725 havocbot_ctf_reset_role(this);
1729 if (navigation_goalrating_timeout(this))
1731 navigation_goalrating_start(this);
1734 entity mf = havocbot_ctf_find_flag(this);
1735 vector base_org = mf.dropped_origin;
1736 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1738 havocbot_goalrating_ctf_enemybase(this, base_rating);
1740 havocbot_goalrating_ctf_ourbase(this, base_rating);
1742 // start collecting items very close to the bot but only inside of own base radius
1743 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1744 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1746 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1748 navigation_goalrating_end(this);
1750 navigation_goalrating_timeout_set(this);
1752 entity goal = this.goalentity;
1753 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1754 this.goalentity_lock_timeout = time + ((this.bot_aimtarg) ? 2 : 3);
1757 this.havocbot_cantfindflag = time + 10;
1758 else if (time > this.havocbot_cantfindflag)
1760 // Can't navigate to my own base, suicide!
1761 // TODO: drop it and wander around
1762 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1768 void havocbot_role_ctf_escort(entity this)
1774 havocbot_ctf_reset_role(this);
1778 if (this.flagcarried)
1780 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1784 // If enemy flag is back on the base switch to previous role
1785 ef = havocbot_ctf_find_enemy_flag(this);
1786 if(ef.ctf_status==FLAG_BASE)
1788 this.havocbot_role = this.havocbot_previous_role;
1789 this.havocbot_role_timeout = 0;
1792 if (ef.ctf_status == FLAG_DROPPED)
1794 navigation_goalrating_timeout_expire(this, 1);
1798 // If the flag carrier reached the base switch to defense
1799 mf = havocbot_ctf_find_flag(this);
1800 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1802 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1806 // Set the role timeout if necessary
1807 if (!this.havocbot_role_timeout)
1809 this.havocbot_role_timeout = time + random() * 30 + 60;
1812 // If nothing happened just switch to previous role
1813 if (time > this.havocbot_role_timeout)
1815 this.havocbot_role = this.havocbot_previous_role;
1816 this.havocbot_role_timeout = 0;
1820 // Chase the flag carrier
1821 if (navigation_goalrating_timeout(this))
1823 navigation_goalrating_start(this);
1826 havocbot_goalrating_ctf_enemyflag(this, 10000);
1827 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1828 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1830 navigation_goalrating_end(this);
1832 navigation_goalrating_timeout_set(this);
1836 void havocbot_role_ctf_offense(entity this)
1843 havocbot_ctf_reset_role(this);
1847 if (this.flagcarried)
1849 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1854 mf = havocbot_ctf_find_flag(this);
1855 ef = havocbot_ctf_find_enemy_flag(this);
1858 if(mf.ctf_status!=FLAG_BASE)
1861 pos = mf.tag_entity.origin;
1865 // Try to get it if closer than the enemy base
1866 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1868 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1873 // Escort flag carrier
1874 if(ef.ctf_status!=FLAG_BASE)
1877 pos = ef.tag_entity.origin;
1881 if(vdist(pos - mf.dropped_origin, >, 700))
1883 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1888 // Set the role timeout if necessary
1889 if (!this.havocbot_role_timeout)
1890 this.havocbot_role_timeout = time + 120;
1892 if (time > this.havocbot_role_timeout)
1894 havocbot_ctf_reset_role(this);
1898 if (navigation_goalrating_timeout(this))
1900 navigation_goalrating_start(this);
1903 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1904 havocbot_goalrating_ctf_enemybase(this, 10000);
1905 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1907 navigation_goalrating_end(this);
1909 navigation_goalrating_timeout_set(this);
1913 // Retriever (temporary role):
1914 void havocbot_role_ctf_retriever(entity this)
1920 havocbot_ctf_reset_role(this);
1924 if (this.flagcarried)
1926 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1930 // If flag is back on the base switch to previous role
1931 mf = havocbot_ctf_find_flag(this);
1932 if(mf.ctf_status==FLAG_BASE)
1934 if (mf.enemy == this) // did this bot return the flag?
1935 navigation_goalrating_timeout_force(this);
1936 havocbot_ctf_reset_role(this);
1940 if (!this.havocbot_role_timeout)
1941 this.havocbot_role_timeout = time + 20;
1943 if (time > this.havocbot_role_timeout)
1945 havocbot_ctf_reset_role(this);
1949 if (navigation_goalrating_timeout(this))
1951 const float RT_RADIUS = 10000;
1953 navigation_goalrating_start(this);
1956 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1957 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1958 havocbot_goalrating_ctf_enemybase(this, 8000);
1959 entity ef = havocbot_ctf_find_enemy_flag(this);
1960 vector enemy_base_org = ef.dropped_origin;
1961 // start collecting items very close to the bot but only inside of enemy base radius
1962 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1963 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1964 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1966 navigation_goalrating_end(this);
1968 navigation_goalrating_timeout_set(this);
1972 void havocbot_role_ctf_middle(entity this)
1978 havocbot_ctf_reset_role(this);
1982 if (this.flagcarried)
1984 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1988 mf = havocbot_ctf_find_flag(this);
1989 if(mf.ctf_status!=FLAG_BASE)
1991 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1995 if (!this.havocbot_role_timeout)
1996 this.havocbot_role_timeout = time + 10;
1998 if (time > this.havocbot_role_timeout)
2000 havocbot_ctf_reset_role(this);
2004 if (navigation_goalrating_timeout(this))
2008 org = havocbot_middlepoint;
2009 org.z = this.origin.z;
2011 navigation_goalrating_start(this);
2014 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
2015 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
2016 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2017 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2018 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2019 havocbot_goalrating_ctf_enemybase(this, 3000);
2021 navigation_goalrating_end(this);
2023 entity goal = this.goalentity;
2024 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
2025 this.goalentity_lock_timeout = time + 2;
2027 navigation_goalrating_timeout_set(this);
2031 void havocbot_role_ctf_defense(entity this)
2037 havocbot_ctf_reset_role(this);
2041 if (this.flagcarried)
2043 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2047 // If own flag was captured
2048 mf = havocbot_ctf_find_flag(this);
2049 if(mf.ctf_status!=FLAG_BASE)
2051 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2055 if (!this.havocbot_role_timeout)
2056 this.havocbot_role_timeout = time + 30;
2058 if (time > this.havocbot_role_timeout)
2060 havocbot_ctf_reset_role(this);
2063 if (navigation_goalrating_timeout(this))
2065 vector org = mf.dropped_origin;
2067 navigation_goalrating_start(this);
2069 // if enemies are closer to our base, go there
2070 entity closestplayer = NULL;
2071 float distance, bestdistance = 10000;
2072 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2073 distance = vlen(org - it.origin);
2074 if(distance<bestdistance)
2077 bestdistance = distance;
2083 if(DIFF_TEAM(closestplayer, this))
2084 if(vdist(org - this.origin, >, 1000))
2085 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2086 havocbot_goalrating_ctf_ourbase(this, 10000);
2088 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2089 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2090 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2091 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2092 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2094 navigation_goalrating_end(this);
2096 navigation_goalrating_timeout_set(this);
2100 void havocbot_role_ctf_setrole(entity bot, int role)
2102 string s = "(null)";
2105 case HAVOCBOT_CTF_ROLE_CARRIER:
2107 bot.havocbot_role = havocbot_role_ctf_carrier;
2108 bot.havocbot_role_timeout = 0;
2109 bot.havocbot_cantfindflag = time + 10;
2110 if (bot.havocbot_previous_role != bot.havocbot_role)
2111 navigation_goalrating_timeout_force(bot);
2113 case HAVOCBOT_CTF_ROLE_DEFENSE:
2115 bot.havocbot_role = havocbot_role_ctf_defense;
2116 bot.havocbot_role_timeout = 0;
2118 case HAVOCBOT_CTF_ROLE_MIDDLE:
2120 bot.havocbot_role = havocbot_role_ctf_middle;
2121 bot.havocbot_role_timeout = 0;
2123 case HAVOCBOT_CTF_ROLE_OFFENSE:
2125 bot.havocbot_role = havocbot_role_ctf_offense;
2126 bot.havocbot_role_timeout = 0;
2128 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2130 bot.havocbot_previous_role = bot.havocbot_role;
2131 bot.havocbot_role = havocbot_role_ctf_retriever;
2132 bot.havocbot_role_timeout = time + 10;
2133 if (bot.havocbot_previous_role != bot.havocbot_role)
2134 navigation_goalrating_timeout_expire(bot, 2);
2136 case HAVOCBOT_CTF_ROLE_ESCORT:
2138 bot.havocbot_previous_role = bot.havocbot_role;
2139 bot.havocbot_role = havocbot_role_ctf_escort;
2140 bot.havocbot_role_timeout = time + 30;
2141 if (bot.havocbot_previous_role != bot.havocbot_role)
2142 navigation_goalrating_timeout_expire(bot, 2);
2145 LOG_TRACE(bot.netname, " switched to ", s);
2153 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2155 entity player = M_ARGV(0, entity);
2157 int t = 0, t2 = 0, t3 = 0;
2158 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)
2160 // initially clear items so they can be set as necessary later.
2161 STAT(CTF_FLAGSTATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2162 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2163 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2164 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2165 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2166 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2168 // scan through all the flags and notify the client about them
2169 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2171 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2172 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2173 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2174 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2175 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; }
2177 switch(flag.ctf_status)
2182 if((flag.owner == player) || (flag.pass_sender == player))
2183 STAT(CTF_FLAGSTATUS, player) |= t; // carrying: player is currently carrying the flag
2185 STAT(CTF_FLAGSTATUS, player) |= t2; // taken: someone else is carrying the flag
2190 STAT(CTF_FLAGSTATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2196 // item for stopping players from capturing the flag too often
2197 if(player.ctf_captureshielded)
2198 STAT(CTF_FLAGSTATUS, player) |= CTF_SHIELDED;
2201 STAT(CTF_FLAGSTATUS, player) |= CTF_STALEMATE;
2203 // update the health of the flag carrier waypointsprite
2204 if(player.wps_flagcarrier)
2205 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);
2208 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2210 entity frag_attacker = M_ARGV(1, entity);
2211 entity frag_target = M_ARGV(2, entity);
2212 float frag_damage = M_ARGV(4, float);
2213 vector frag_force = M_ARGV(6, vector);
2215 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2217 if(frag_target == frag_attacker) // damage done to yourself
2219 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2220 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2222 else // damage done to everyone else
2224 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2225 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2228 M_ARGV(4, float) = frag_damage;
2229 M_ARGV(6, vector) = frag_force;
2231 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2233 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
2234 && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2236 frag_target.wps_helpme_time = time;
2237 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2239 // todo: add notification for when flag carrier needs help?
2243 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2245 entity frag_attacker = M_ARGV(1, entity);
2246 entity frag_target = M_ARGV(2, entity);
2248 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2250 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2251 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2254 if(frag_target.flagcarried)
2256 entity tmp_entity = frag_target.flagcarried;
2257 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2258 tmp_entity.ctf_dropper = NULL;
2262 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2264 M_ARGV(2, float) = 0; // frag score
2265 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2268 void ctf_RemovePlayer(entity player)
2270 if(player.flagcarried)
2271 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2273 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2275 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2276 if(flag.pass_target == player) { flag.pass_target = NULL; }
2277 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2281 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2283 entity player = M_ARGV(0, entity);
2285 ctf_RemovePlayer(player);
2288 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2290 entity player = M_ARGV(0, entity);
2292 ctf_RemovePlayer(player);
2295 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2297 if(!autocvar_g_ctf_leaderboard)
2300 entity player = M_ARGV(0, entity);
2302 if(IS_REAL_CLIENT(player))
2304 int m = min(RANKINGS_CNT, autocvar_g_cts_send_rankings_cnt);
2305 race_send_rankings_cnt(MSG_ONE);
2306 for (int i = 1; i <= m; ++i)
2308 race_SendRankings(i, 0, 0, MSG_ONE);
2313 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2315 if(!autocvar_g_ctf_leaderboard)
2318 entity player = M_ARGV(0, entity);
2320 if(CS(player).cvar_cl_allow_uidtracking == 1 && CS(player).cvar_cl_allow_uid2name == 1)
2322 if (!player.stored_netname)
2323 player.stored_netname = strzone(uid2name(player.crypto_idfp));
2324 if(player.stored_netname != player.netname)
2326 db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
2327 strcpy(player.stored_netname, player.netname);
2332 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2334 entity player = M_ARGV(0, entity);
2336 if(player.flagcarried)
2337 if(!autocvar_g_ctf_portalteleport)
2338 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2341 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2343 if(MUTATOR_RETURNVALUE || game_stopped) return;
2345 entity player = M_ARGV(0, entity);
2347 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2349 // pass the flag to a team mate
2350 if(autocvar_g_ctf_pass)
2352 entity head, closest_target = NULL;
2353 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2355 while(head) // find the closest acceptable target to pass to
2357 if(IS_PLAYER(head) && !IS_DEAD(head))
2358 if(head != player && SAME_TEAM(head, player))
2359 if(!head.speedrunning && !head.vehicle)
2361 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2362 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2363 vector passer_center = CENTER_OR_VIEWOFS(player);
2365 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2367 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2369 if(IS_BOT_CLIENT(head))
2371 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2372 ctf_Handle_Throw(head, player, DROP_PASS);
2376 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2377 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2379 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2382 else if(player.flagcarried && !head.flagcarried)
2386 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2387 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2388 { closest_target = head; }
2390 else { closest_target = head; }
2397 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2400 // throw the flag in front of you
2401 if(autocvar_g_ctf_throw && player.flagcarried)
2403 if(player.throw_count == -1)
2405 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2407 player.throw_prevtime = time;
2408 player.throw_count = 1;
2409 ctf_Handle_Throw(player, NULL, DROP_THROW);
2414 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2420 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2421 else { player.throw_count += 1; }
2422 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2424 player.throw_prevtime = time;
2425 ctf_Handle_Throw(player, NULL, DROP_THROW);
2432 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2434 entity player = M_ARGV(0, entity);
2436 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2438 player.wps_helpme_time = time;
2439 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2441 else // create a normal help me waypointsprite
2443 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2444 WaypointSprite_Ping(player.wps_helpme);
2450 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2452 entity player = M_ARGV(0, entity);
2453 entity veh = M_ARGV(1, entity);
2455 if(player.flagcarried)
2457 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2459 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2463 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2464 setattachment(player.flagcarried, veh, "");
2465 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2466 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2467 //player.flagcarried.angles = '0 0 0';
2473 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2475 entity player = M_ARGV(0, entity);
2477 if(player.flagcarried)
2479 setattachment(player.flagcarried, player, "");
2480 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2481 player.flagcarried.scale = FLAG_SCALE;
2482 player.flagcarried.angles = '0 0 0';
2483 player.flagcarried.nodrawtoclient = NULL;
2488 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2490 entity player = M_ARGV(0, entity);
2492 if(player.flagcarried)
2494 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2495 ctf_RespawnFlag(player.flagcarried);
2500 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2502 entity flag; // temporary entity for the search method
2504 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2506 switch(flag.ctf_status)
2511 // lock the flag, game is over
2512 set_movetype(flag, MOVETYPE_NONE);
2513 flag.takedamage = DAMAGE_NO;
2514 flag.solid = SOLID_NOT;
2515 flag.nextthink = false; // stop thinking
2517 //dprint("stopping the ", flag.netname, " from moving.\n");
2525 // do nothing for these flags
2532 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2534 entity bot = M_ARGV(0, entity);
2536 havocbot_ctf_reset_role(bot);
2540 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2542 M_ARGV(1, string) = "ctf_team";
2545 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2547 entity spectatee = M_ARGV(0, entity);
2548 entity client = M_ARGV(1, entity);
2550 STAT(CTF_FLAGSTATUS, client) = STAT(CTF_FLAGSTATUS, spectatee);
2553 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2555 int record_page = M_ARGV(0, int);
2556 string ret_string = M_ARGV(1, string);
2558 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2560 if (MapInfo_Get_ByID(i))
2562 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2568 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2569 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2573 M_ARGV(1, string) = ret_string;
2576 bool superspec_Spectate(entity this, entity targ); // TODO
2577 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2578 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2580 entity player = M_ARGV(0, entity);
2581 string cmd_name = M_ARGV(1, string);
2582 int cmd_argc = M_ARGV(2, int);
2584 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2586 if(cmd_name == "followfc")
2598 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2599 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2600 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2601 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2605 FOREACH_CLIENT(IS_PLAYER(it), {
2606 if(it.flagcarried && (it.team == _team || _team == 0))
2609 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2610 continue; // already spectating this fc, try another
2611 return superspec_Spectate(player, it);
2616 superspec_msg("", "", player, "No active flag carrier\n", 1);
2621 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2623 entity frag_target = M_ARGV(0, entity);
2625 if(frag_target.flagcarried)
2626 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2629 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2631 entity player = M_ARGV(0, entity);
2632 if(player.flagcarried)
2633 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2641 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2642 CTF flag for team one (Red).
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_team1)
2654 if(!g_ctf) { delete(this); return; }
2656 ctf_FlagSetup(NUM_TEAM_1, this);
2659 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2660 CTF flag for team two (Blue).
2662 "angle" Angle the flag will point (minus 90 degrees)...
2663 "model" model to use, note this needs red and blue as skins 0 and 1...
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_team2)
2672 if(!g_ctf) { delete(this); return; }
2674 ctf_FlagSetup(NUM_TEAM_2, this);
2677 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2678 CTF flag for team three (Yellow).
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_team3)
2690 if(!g_ctf) { delete(this); return; }
2692 ctf_FlagSetup(NUM_TEAM_3, this);
2695 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2696 CTF flag for team four (Pink).
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_team4)
2708 if(!g_ctf) { delete(this); return; }
2710 ctf_FlagSetup(NUM_TEAM_4, this);
2713 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2716 "angle" Angle the flag will point (minus 90 degrees)...
2717 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2718 "noise" sound played when flag is picked up...
2719 "noise1" sound played when flag is returned by a teammate...
2720 "noise2" sound played when flag is captured...
2721 "noise3" sound played when flag is lost in the field and respawns itself...
2722 "noise4" sound played when flag is dropped by a player...
2723 "noise5" sound played when flag touches the ground... */
2724 spawnfunc(item_flag_neutral)
2726 if(!g_ctf) { delete(this); return; }
2727 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2729 ctf_FlagSetup(0, this);
2732 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2733 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2734 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.
2736 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2737 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2740 if(!g_ctf) { delete(this); return; }
2742 this.classname = "ctf_team";
2743 this.team = this.cnt + 1;
2746 // compatibility for quake maps
2747 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2748 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2749 spawnfunc(info_player_team1);
2750 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2751 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2752 spawnfunc(info_player_team2);
2753 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2754 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2756 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2757 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2759 // compatibility for wop maps
2760 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2761 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2762 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2763 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2764 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2765 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2773 void ctf_ScoreRules(int teams)
2775 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2776 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2777 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2778 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2779 field(SP_CTF_PICKUPS, "pickups", 0);
2780 field(SP_CTF_FCKILLS, "fckills", 0);
2781 field(SP_CTF_RETURNS, "returns", 0);
2782 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2786 // code from here on is just to support maps that don't have flag and team entities
2787 void ctf_SpawnTeam (string teamname, int teamcolor)
2789 entity this = new_pure(ctf_team);
2790 this.netname = teamname;
2791 this.cnt = teamcolor - 1;
2792 this.spawnfunc_checked = true;
2793 this.team = teamcolor;
2796 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2801 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2803 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2804 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2806 switch(tmp_entity.team)
2808 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2809 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2810 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2811 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2813 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2816 havocbot_ctf_calculate_middlepoint();
2818 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2820 ctf_teams = 0; // so set the default red and blue teams
2821 BITSET_ASSIGN(ctf_teams, BIT(0));
2822 BITSET_ASSIGN(ctf_teams, BIT(1));
2825 //ctf_teams = bound(2, ctf_teams, 4);
2827 // if no teams are found, spawn defaults
2828 if(find(NULL, classname, "ctf_team") == NULL)
2830 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2831 if(ctf_teams & BIT(0))
2832 ctf_SpawnTeam("Red", NUM_TEAM_1);
2833 if(ctf_teams & BIT(1))
2834 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2835 if(ctf_teams & BIT(2))
2836 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2837 if(ctf_teams & BIT(3))
2838 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2841 ctf_ScoreRules(ctf_teams);
2844 void ctf_Initialize()
2846 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2848 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2849 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2850 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2852 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);