3 // TODO: split into sv_ctf
5 #include <common/effects/all.qh>
6 #include <common/vehicles/all.qh>
7 #include <server/teamplay.qh>
9 #include <lib/warpzone/common.qh>
11 bool autocvar_g_ctf_allow_vehicle_carry;
12 bool autocvar_g_ctf_allow_vehicle_touch;
13 bool autocvar_g_ctf_allow_monster_touch;
14 bool autocvar_g_ctf_throw;
15 float autocvar_g_ctf_throw_angle_max;
16 float autocvar_g_ctf_throw_angle_min;
17 int autocvar_g_ctf_throw_punish_count;
18 float autocvar_g_ctf_throw_punish_delay;
19 float autocvar_g_ctf_throw_punish_time;
20 float autocvar_g_ctf_throw_strengthmultiplier;
21 float autocvar_g_ctf_throw_velocity_forward;
22 float autocvar_g_ctf_throw_velocity_up;
23 float autocvar_g_ctf_drop_velocity_up;
24 float autocvar_g_ctf_drop_velocity_side;
25 bool autocvar_g_ctf_oneflag_reverse;
26 bool autocvar_g_ctf_portalteleport;
27 bool autocvar_g_ctf_pass;
28 float autocvar_g_ctf_pass_arc;
29 float autocvar_g_ctf_pass_arc_max;
30 float autocvar_g_ctf_pass_directional_max;
31 float autocvar_g_ctf_pass_directional_min;
32 float autocvar_g_ctf_pass_radius;
33 float autocvar_g_ctf_pass_wait;
34 bool autocvar_g_ctf_pass_request;
35 float autocvar_g_ctf_pass_turnrate;
36 float autocvar_g_ctf_pass_timelimit;
37 float autocvar_g_ctf_pass_velocity;
38 bool autocvar_g_ctf_dynamiclights;
39 float autocvar_g_ctf_flag_collect_delay;
40 float autocvar_g_ctf_flag_damageforcescale;
41 bool autocvar_g_ctf_flag_dropped_waypoint;
42 bool autocvar_g_ctf_flag_dropped_floatinwater;
43 bool autocvar_g_ctf_flag_glowtrails;
44 int autocvar_g_ctf_flag_health;
45 bool autocvar_g_ctf_flag_return;
46 bool autocvar_g_ctf_flag_return_carrying;
47 float autocvar_g_ctf_flag_return_carried_radius;
48 float autocvar_g_ctf_flag_return_time;
49 bool autocvar_g_ctf_flag_return_when_unreachable;
50 float autocvar_g_ctf_flag_return_damage;
51 float autocvar_g_ctf_flag_return_damage_delay;
52 float autocvar_g_ctf_flag_return_dropped;
53 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
54 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
55 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
56 float autocvar_g_ctf_flagcarrier_selfforcefactor;
57 float autocvar_g_ctf_flagcarrier_damagefactor;
58 float autocvar_g_ctf_flagcarrier_forcefactor;
59 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
60 bool autocvar_g_ctf_fullbrightflags;
61 bool autocvar_g_ctf_ignore_frags;
62 bool autocvar_g_ctf_score_ignore_fields;
63 int autocvar_g_ctf_score_capture;
64 int autocvar_g_ctf_score_capture_assist;
65 int autocvar_g_ctf_score_kill;
66 int autocvar_g_ctf_score_penalty_drop;
67 int autocvar_g_ctf_score_penalty_returned;
68 int autocvar_g_ctf_score_pickup_base;
69 int autocvar_g_ctf_score_pickup_dropped_early;
70 int autocvar_g_ctf_score_pickup_dropped_late;
71 int autocvar_g_ctf_score_return;
72 float autocvar_g_ctf_shield_force;
73 float autocvar_g_ctf_shield_max_ratio;
74 int autocvar_g_ctf_shield_min_negscore;
75 bool autocvar_g_ctf_stalemate;
76 int autocvar_g_ctf_stalemate_endcondition;
77 float autocvar_g_ctf_stalemate_time;
78 bool autocvar_g_ctf_reverse;
79 float autocvar_g_ctf_dropped_capture_delay;
80 float autocvar_g_ctf_dropped_capture_radius;
82 void ctf_FakeTimeLimit(entity e, float t)
85 WriteByte(MSG_ONE, 3); // svc_updatestat
86 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
88 WriteCoord(MSG_ONE, autocvar_timelimit);
90 WriteCoord(MSG_ONE, (t + 1) / 60);
93 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
95 if(autocvar_sv_eventlog)
96 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != NULL) ? ftos(actor.playerid) : "")));
97 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
100 void ctf_CaptureRecord(entity flag, entity player)
102 float cap_record = ctf_captimerecord;
103 float cap_time = (time - flag.ctf_pickuptime);
104 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
108 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname);
109 else if(!ctf_captimerecord)
110 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_TIME), player.netname, TIME_ENCODE(cap_time));
111 else if(cap_time < cap_record)
112 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_BROKEN), player.netname, refername, TIME_ENCODE(cap_time), TIME_ENCODE(cap_record));
114 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_UNBROKEN), player.netname, refername, TIME_ENCODE(cap_time), TIME_ENCODE(cap_record));
116 // write that shit in the database
117 if(!ctf_oneflag) // but not in 1-flag mode
118 if((!ctf_captimerecord) || (cap_time < cap_record))
120 ctf_captimerecord = cap_time;
121 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
122 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
123 write_recordmarker(player, flag.ctf_pickuptime, cap_time);
126 if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
127 race_setTime(GetMapname(), TIME_ENCODE(cap_time), player.crypto_idfp, player.netname, player, false);
130 bool ctf_Immediate_Return_Allowed(entity flag, entity toucher)
133 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), { ++num_perteam; });
135 // automatically return if there's only 1 player on the team
136 return ((autocvar_g_ctf_flag_return || num_perteam <= 1 || (autocvar_g_ctf_flag_return_carrying && toucher.flagcarried))
140 bool ctf_Return_Customize(entity this, entity client)
142 // only to the carrier
143 return boolean(client == this.owner);
146 void ctf_FlagcarrierWaypoints(entity player)
148 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
149 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
150 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(GetResourceAmount(player, RESOURCE_HEALTH), GetResourceAmount(player, RESOURCE_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
151 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
153 if(player.flagcarried && CTF_SAMETEAM(player, player.flagcarried))
155 if(!player.wps_enemyflagcarrier)
157 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, 0, player, wps_enemyflagcarrier, true, RADARICON_FLAG);
158 wp.colormod = WPCOLOR_ENEMYFC(player.team);
159 setcefc(wp, ctf_Stalemate_Customize);
161 if(IS_REAL_CLIENT(player) && !ctf_stalemate)
162 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_VISIBLE);
165 if(!player.wps_flagreturn)
167 entity owp = WaypointSprite_SpawnFixed(WP_FlagReturn, player.flagcarried.ctf_spawnorigin + FLAG_WAYPOINT_OFFSET, player, wps_flagreturn, RADARICON_FLAG);
168 owp.colormod = '0 0.8 0.8';
169 //WaypointSprite_UpdateTeamRadar(player.wps_flagreturn, RADARICON_FLAG, ((player.team) ? colormapPaletteColor(player.team - 1, false) : '1 1 1'));
170 setcefc(owp, ctf_Return_Customize);
175 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
177 float current_distance = vlen((('1 0 0' * to.x) + ('0 1 0' * to.y)) - (('1 0 0' * from.x) + ('0 1 0' * from.y))); // for the sake of this check, exclude Z axis
178 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
179 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
180 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
183 if(current_height) // make sure we can actually do this arcing path
185 targpos = (to + ('0 0 1' * current_height));
186 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
187 if(trace_fraction < 1)
189 //print("normal arc line failed, trying to find new pos...");
190 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
191 targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
192 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
193 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
194 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
197 else { targpos = to; }
199 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
201 vector desired_direction = normalize(targpos - from);
202 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
203 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
206 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
208 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
210 // directional tracing only
212 makevectors(passer_angle);
214 // find the closest point on the enemy to the center of the attack
215 float h; // hypotenuse, which is the distance between attacker to head
216 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
218 h = vlen(head_center - passer_center);
219 a = h * (normalize(head_center - passer_center) * v_forward);
221 vector nearest_on_line = (passer_center + a * v_forward);
222 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
224 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
225 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
227 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
232 else { return true; }
236 // =======================
237 // CaptureShield Functions
238 // =======================
240 bool ctf_CaptureShield_CheckStatus(entity p)
242 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
243 int players_worseeq, players_total;
245 if(ctf_captureshield_max_ratio <= 0)
248 s = GameRules_scoring_add(p, CTF_CAPS, 0);
249 s2 = GameRules_scoring_add(p, CTF_PICKUPS, 0);
250 s3 = GameRules_scoring_add(p, CTF_RETURNS, 0);
251 s4 = GameRules_scoring_add(p, CTF_FCKILLS, 0);
253 sr = ((s - s2) + (s3 + s4));
255 if(sr >= -ctf_captureshield_min_negscore)
258 players_total = players_worseeq = 0;
259 FOREACH_CLIENT(IS_PLAYER(it), {
262 se = GameRules_scoring_add(it, CTF_CAPS, 0);
263 se2 = GameRules_scoring_add(it, CTF_PICKUPS, 0);
264 se3 = GameRules_scoring_add(it, CTF_RETURNS, 0);
265 se4 = GameRules_scoring_add(it, CTF_FCKILLS, 0);
267 ser = ((se - se2) + (se3 + se4));
274 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
275 // use this rule here
277 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
283 void ctf_CaptureShield_Update(entity player, bool wanted_status)
285 bool updated_status = ctf_CaptureShield_CheckStatus(player);
286 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
288 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
289 player.ctf_captureshielded = updated_status;
293 bool ctf_CaptureShield_Customize(entity this, entity client)
295 if(!client.ctf_captureshielded) { return false; }
296 if(CTF_SAMETEAM(this, client)) { return false; }
301 void ctf_CaptureShield_Touch(entity this, entity toucher)
303 if(!toucher.ctf_captureshielded) { return; }
304 if(CTF_SAMETEAM(this, toucher)) { return; }
306 vector mymid = (this.absmin + this.absmax) * 0.5;
307 vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
309 Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
310 if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
313 void ctf_CaptureShield_Spawn(entity flag)
315 entity shield = new(ctf_captureshield);
318 shield.team = flag.team;
319 settouch(shield, ctf_CaptureShield_Touch);
320 setcefc(shield, ctf_CaptureShield_Customize);
321 shield.effects = EF_ADDITIVE;
322 set_movetype(shield, MOVETYPE_NOCLIP);
323 shield.solid = SOLID_TRIGGER;
324 shield.avelocity = '7 0 11';
327 setorigin(shield, flag.origin);
328 setmodel(shield, MDL_CTF_SHIELD);
329 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
333 // ====================
334 // Drop/Pass/Throw Code
335 // ====================
337 void ctf_Handle_Drop(entity flag, entity player, int droptype)
340 player = (player ? player : flag.pass_sender);
343 set_movetype(flag, MOVETYPE_TOSS);
344 flag.takedamage = DAMAGE_YES;
345 flag.angles = '0 0 0';
346 SetResourceAmountExplicit(flag, RESOURCE_HEALTH, flag.max_flag_health);
347 flag.ctf_droptime = time;
348 flag.ctf_dropper = player;
349 flag.ctf_status = FLAG_DROPPED;
351 // messages and sounds
352 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
353 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
354 ctf_EventLog("dropped", player.team, player);
357 GameRules_scoring_add_team(player, SCORE, -((flag.score_drop) ? flag.score_drop : autocvar_g_ctf_score_penalty_drop));
358 GameRules_scoring_add(player, CTF_DROPS, 1);
361 if(autocvar_g_ctf_flag_dropped_waypoint) {
362 entity wp = WaypointSprite_Spawn(WP_FlagDropped, 0, 0, flag, FLAG_WAYPOINT_OFFSET, NULL, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, true, RADARICON_FLAG);
363 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
366 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
368 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
369 WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResourceAmount(flag, RESOURCE_HEALTH));
372 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
374 if(droptype == DROP_PASS)
376 flag.pass_distance = 0;
377 flag.pass_sender = NULL;
378 flag.pass_target = NULL;
382 void ctf_Handle_Retrieve(entity flag, entity player)
384 entity sender = flag.pass_sender;
386 // transfer flag to player
388 flag.owner.flagcarried = flag;
389 GameRules_scoring_vip(player, true);
394 setattachment(flag, player.vehicle, "");
395 setorigin(flag, VEHICLE_FLAG_OFFSET);
396 flag.scale = VEHICLE_FLAG_SCALE;
400 setattachment(flag, player, "");
401 setorigin(flag, FLAG_CARRY_OFFSET);
403 set_movetype(flag, MOVETYPE_NONE);
404 flag.takedamage = DAMAGE_NO;
405 flag.solid = SOLID_NOT;
406 flag.angles = '0 0 0';
407 flag.ctf_status = FLAG_CARRY;
409 // messages and sounds
410 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
411 ctf_EventLog("receive", flag.team, player);
413 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
415 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
416 else if(it == player)
417 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
418 else if(SAME_TEAM(it, sender))
419 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
422 // create new waypoint
423 ctf_FlagcarrierWaypoints(player);
425 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
426 player.throw_antispam = sender.throw_antispam;
428 flag.pass_distance = 0;
429 flag.pass_sender = NULL;
430 flag.pass_target = NULL;
433 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
435 entity flag = player.flagcarried;
436 vector targ_origin, flag_velocity;
438 if(!flag) { return; }
439 if((droptype == DROP_PASS) && !receiver) { return; }
441 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
444 setattachment(flag, NULL, "");
445 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
446 flag.owner.flagcarried = NULL;
447 GameRules_scoring_vip(flag.owner, false);
449 flag.solid = SOLID_TRIGGER;
450 flag.ctf_dropper = player;
451 flag.ctf_droptime = time;
453 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
460 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
461 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
462 WarpZone_RefSys_Copy(flag, receiver);
463 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
464 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
466 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
467 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
470 set_movetype(flag, MOVETYPE_FLY);
471 flag.takedamage = DAMAGE_NO;
472 flag.pass_sender = player;
473 flag.pass_target = receiver;
474 flag.ctf_status = FLAG_PASSING;
477 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
478 WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
479 ctf_EventLog("pass", flag.team, player);
485 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'));
487 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)));
488 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
489 ctf_Handle_Drop(flag, player, droptype);
490 navigation_dynamicgoal_set(flag, player);
496 flag.velocity = '0 0 0'; // do nothing
503 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);
504 ctf_Handle_Drop(flag, player, droptype);
505 navigation_dynamicgoal_set(flag, player);
510 // kill old waypointsprite
511 WaypointSprite_Ping(player.wps_flagcarrier);
512 WaypointSprite_Kill(player.wps_flagcarrier);
514 if(player.wps_enemyflagcarrier)
515 WaypointSprite_Kill(player.wps_enemyflagcarrier);
517 if(player.wps_flagreturn)
518 WaypointSprite_Kill(player.wps_flagreturn);
521 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
524 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
526 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
533 void nades_GiveBonus(entity player, float score);
535 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
537 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
538 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
539 entity player_team_flag = NULL, tmp_entity;
540 float old_time, new_time;
542 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
543 if(CTF_DIFFTEAM(player, flag)) { return; }
544 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)
546 if (toucher.goalentity == flag.bot_basewaypoint)
547 toucher.goalentity_lock_timeout = 0;
550 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
551 if(SAME_TEAM(tmp_entity, player))
553 player_team_flag = tmp_entity;
557 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
559 player.throw_prevtime = time;
560 player.throw_count = 0;
562 // messages and sounds
563 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
564 ctf_CaptureRecord(enemy_flag, player);
565 _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);
569 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
570 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
576 if(enemy_flag.score_capture || flag.score_capture)
577 pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
578 GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
580 if(enemy_flag.score_team_capture || flag.score_team_capture)
581 capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
582 GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
584 old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
585 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
586 if(!old_time || new_time < old_time)
587 GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
590 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
591 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
594 if(capturetype == CAPTURE_NORMAL)
596 WaypointSprite_Kill(player.wps_flagcarrier);
597 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
599 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
600 { GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist)); }
603 flag.enemy = toucher;
606 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
607 ctf_RespawnFlag(enemy_flag);
610 void ctf_Handle_Return(entity flag, entity player)
612 // messages and sounds
613 if(IS_MONSTER(player))
615 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
619 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
620 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
622 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
623 ctf_EventLog("return", flag.team, player);
626 if(IS_PLAYER(player))
628 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
629 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
631 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
634 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
638 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
639 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
640 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
644 if(player.flagcarried == flag)
645 WaypointSprite_Kill(player.wps_flagcarrier);
650 ctf_RespawnFlag(flag);
653 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
656 float pickup_dropped_score; // used to calculate dropped pickup score
658 // attach the flag to the player
660 player.flagcarried = flag;
661 GameRules_scoring_vip(player, true);
664 setattachment(flag, player.vehicle, "");
665 setorigin(flag, VEHICLE_FLAG_OFFSET);
666 flag.scale = VEHICLE_FLAG_SCALE;
670 setattachment(flag, player, "");
671 setorigin(flag, FLAG_CARRY_OFFSET);
675 set_movetype(flag, MOVETYPE_NONE);
676 flag.takedamage = DAMAGE_NO;
677 flag.solid = SOLID_NOT;
678 flag.angles = '0 0 0';
679 flag.ctf_status = FLAG_CARRY;
683 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
684 case PICKUP_DROPPED: SetResourceAmountExplicit(flag, RESOURCE_HEALTH, flag.max_flag_health); break; // reset health/return timelimit
688 // messages and sounds
689 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
691 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
693 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
694 else if(CTF_DIFFTEAM(player, flag))
695 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
697 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
699 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
702 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); });
705 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
706 if(CTF_SAMETEAM(flag, it))
707 if(SAME_TEAM(player, it))
708 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
710 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, ((SAME_TEAM(flag, player)) ? CHOICE_CTF_PICKUP_ENEMY_TEAM : CHOICE_CTF_PICKUP_ENEMY), Team_ColorCode(player.team), player.netname);
713 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
716 GameRules_scoring_add(player, CTF_PICKUPS, 1);
717 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
722 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
723 ctf_EventLog("steal", flag.team, player);
729 pickup_dropped_score = (autocvar_g_ctf_flag_return_time ? bound(0, ((flag.ctf_droptime + autocvar_g_ctf_flag_return_time) - time) / autocvar_g_ctf_flag_return_time, 1) : 1);
730 pickup_dropped_score = floor((autocvar_g_ctf_score_pickup_dropped_late * (1 - pickup_dropped_score) + autocvar_g_ctf_score_pickup_dropped_early * pickup_dropped_score) + 0.5);
731 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
732 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
733 ctf_EventLog("pickup", flag.team, player);
741 if(pickuptype == PICKUP_BASE)
743 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
744 if((player.speedrunning) && (ctf_captimerecord))
745 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
749 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
752 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
753 ctf_FlagcarrierWaypoints(player);
754 WaypointSprite_Ping(player.wps_flagcarrier);
758 // ===================
759 // Main Flag Functions
760 // ===================
762 void ctf_CheckFlagReturn(entity flag, int returntype)
764 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
766 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResourceAmount(flag, RESOURCE_HEALTH)); }
768 if((GetResourceAmount(flag, RESOURCE_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
773 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
775 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
776 case RETURN_SPEEDRUN:
777 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
778 case RETURN_NEEDKILL:
779 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
782 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
784 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
785 ctf_EventLog("returned", flag.team, NULL);
787 ctf_RespawnFlag(flag);
792 bool ctf_Stalemate_Customize(entity this, entity client)
794 // make spectators see what the player would see
795 entity e = WaypointSprite_getviewentity(client);
796 entity wp_owner = this.owner;
799 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
800 if(SAME_TEAM(wp_owner, e)) { return false; }
801 if(!IS_PLAYER(e)) { return false; }
806 void ctf_CheckStalemate()
809 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
812 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
814 // build list of stale flags
815 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
817 if(autocvar_g_ctf_stalemate)
818 if(tmp_entity.ctf_status != FLAG_BASE)
819 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
821 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
822 ctf_staleflaglist = tmp_entity;
824 switch(tmp_entity.team)
826 case NUM_TEAM_1: ++stale_red_flags; break;
827 case NUM_TEAM_2: ++stale_blue_flags; break;
828 case NUM_TEAM_3: ++stale_yellow_flags; break;
829 case NUM_TEAM_4: ++stale_pink_flags; break;
830 default: ++stale_neutral_flags; break;
836 stale_flags = (stale_neutral_flags >= 1);
838 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
840 if(ctf_oneflag && stale_flags == 1)
841 ctf_stalemate = true;
842 else if(stale_flags >= 2)
843 ctf_stalemate = true;
844 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
845 { ctf_stalemate = false; wpforenemy_announced = false; }
846 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
847 { ctf_stalemate = false; wpforenemy_announced = false; }
849 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
852 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
854 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
856 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, tmp_entity.owner, FLAG_WAYPOINT_OFFSET, NULL, 0, tmp_entity.owner, wps_enemyflagcarrier, true, RADARICON_FLAG);
857 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
858 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
862 if (!wpforenemy_announced)
864 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), { Send_Notification(NOTIF_ONE, it, MSG_CENTER, ((it.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER)); });
866 wpforenemy_announced = true;
871 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
873 if(ITEM_DAMAGE_NEEDKILL(deathtype))
875 if(autocvar_g_ctf_flag_return_damage_delay)
876 this.ctf_flagdamaged_byworld = true;
879 SetResourceAmountExplicit(this, RESOURCE_HEALTH, 0);
880 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
884 if(autocvar_g_ctf_flag_return_damage)
886 // reduce health and check if it should be returned
887 TakeResource(this, RESOURCE_HEALTH, damage);
888 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
893 void ctf_FlagThink(entity this)
898 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
901 if(this == ctf_worldflaglist) // only for the first flag
902 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
905 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
906 LOG_TRACE("wtf the flag got squashed?");
907 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
908 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
909 setsize(this, this.m_mins, this.m_maxs);
913 switch(this.ctf_status)
917 if(autocvar_g_ctf_dropped_capture_radius)
919 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
920 if(tmp_entity.ctf_status == FLAG_DROPPED)
921 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
922 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
923 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
930 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
932 if(autocvar_g_ctf_flag_dropped_floatinwater)
934 vector midpoint = ((this.absmin + this.absmax) * 0.5);
935 if(pointcontents(midpoint) == CONTENT_WATER)
937 this.velocity = this.velocity * 0.5;
939 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
940 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
942 { set_movetype(this, MOVETYPE_FLY); }
944 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
946 if(autocvar_g_ctf_flag_return_dropped)
948 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
950 SetResourceAmountExplicit(this, RESOURCE_HEALTH, 0);
951 ctf_CheckFlagReturn(this, RETURN_DROPPED);
955 if(this.ctf_flagdamaged_byworld)
957 TakeResource(this, RESOURCE_HEALTH, ((this.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE));
958 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
961 else if(autocvar_g_ctf_flag_return_time)
963 TakeResource(this, RESOURCE_HEALTH, ((this.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE));
964 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
972 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
974 SetResourceAmountExplicit(this, RESOURCE_HEALTH, 0);
975 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
977 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
978 ImpulseCommands(this.owner);
980 if(autocvar_g_ctf_stalemate)
982 if(time >= wpforenemy_nextthink)
984 ctf_CheckStalemate();
985 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
988 if(CTF_SAMETEAM(this, this.owner) && this.team)
990 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
991 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
992 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
993 ctf_Handle_Return(this, this.owner);
1000 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1001 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1002 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1004 if((this.pass_target == NULL)
1005 || (IS_DEAD(this.pass_target))
1006 || (this.pass_target.flagcarried)
1007 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1008 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1009 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1011 // give up, pass failed
1012 ctf_Handle_Drop(this, NULL, DROP_PASS);
1016 // still a viable target, go for it
1017 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1022 default: // this should never happen
1024 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1030 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1033 if(game_stopped) return;
1034 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1036 bool is_not_monster = (!IS_MONSTER(toucher));
1038 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1039 if(ITEM_TOUCH_NEEDKILL())
1041 if(!autocvar_g_ctf_flag_return_damage_delay)
1043 SetResourceAmountExplicit(flag, RESOURCE_HEALTH, 0);
1044 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1046 if(!flag.ctf_flagdamaged_byworld) { return; }
1049 // special touch behaviors
1050 if(STAT(FROZEN, toucher)) { return; }
1051 else if(IS_VEHICLE(toucher))
1053 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1054 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1056 return; // do nothing
1058 else if(IS_MONSTER(toucher))
1060 if(!autocvar_g_ctf_allow_monster_touch)
1061 return; // do nothing
1063 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1065 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1067 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1068 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1069 flag.wait = time + FLAG_TOUCHRATE;
1073 else if(IS_DEAD(toucher)) { return; }
1075 switch(flag.ctf_status)
1081 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1082 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1083 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1084 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1086 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1087 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1088 else if(CTF_DIFFTEAM(toucher, flag) && (toucher.flagcarried) && CTF_SAMETEAM(toucher.flagcarried, toucher) && (!toucher.ctf_captureshielded) && autocvar_g_ctf_flag_return_carrying && (time > toucher.next_take_time) && is_not_monster)
1090 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1091 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1093 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1094 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1100 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1101 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1102 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1103 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1109 LOG_TRACE("Someone touched a flag even though it was being carried?");
1115 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1117 if(DIFF_TEAM(toucher, flag.pass_sender))
1119 if(ctf_Immediate_Return_Allowed(flag, toucher))
1120 ctf_Handle_Return(flag, toucher);
1121 else if(is_not_monster && (!toucher.flagcarried))
1122 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1124 else if(!toucher.flagcarried)
1125 ctf_Handle_Retrieve(flag, toucher);
1132 .float last_respawn;
1133 void ctf_RespawnFlag(entity flag)
1135 // check for flag respawn being called twice in a row
1136 if(flag.last_respawn > time - 0.5)
1137 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1139 flag.last_respawn = time;
1141 // reset the player (if there is one)
1142 if((flag.owner) && (flag.owner.flagcarried == flag))
1144 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1145 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1146 WaypointSprite_Kill(flag.wps_flagcarrier);
1148 flag.owner.flagcarried = NULL;
1149 GameRules_scoring_vip(flag.owner, false);
1151 if(flag.speedrunning)
1152 ctf_FakeTimeLimit(flag.owner, -1);
1155 if((flag.owner) && (flag.owner.vehicle))
1156 flag.scale = FLAG_SCALE;
1158 if(flag.ctf_status == FLAG_DROPPED)
1159 { WaypointSprite_Kill(flag.wps_flagdropped); }
1162 setattachment(flag, NULL, "");
1163 setorigin(flag, flag.ctf_spawnorigin);
1165 set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS));
1166 flag.takedamage = DAMAGE_NO;
1167 SetResourceAmountExplicit(flag, RESOURCE_HEALTH, flag.max_flag_health);
1168 flag.solid = SOLID_TRIGGER;
1169 flag.velocity = '0 0 0';
1170 flag.angles = flag.mangle;
1171 flag.flags = FL_ITEM | FL_NOTARGET;
1173 flag.ctf_status = FLAG_BASE;
1175 flag.pass_distance = 0;
1176 flag.pass_sender = NULL;
1177 flag.pass_target = NULL;
1178 flag.ctf_dropper = NULL;
1179 flag.ctf_pickuptime = 0;
1180 flag.ctf_droptime = 0;
1181 flag.ctf_flagdamaged_byworld = false;
1182 navigation_dynamicgoal_unset(flag);
1184 ctf_CheckStalemate();
1187 void ctf_Reset(entity this)
1189 if(this.owner && IS_PLAYER(this.owner))
1190 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1193 ctf_RespawnFlag(this);
1196 bool ctf_FlagBase_Customize(entity this, entity client)
1198 entity e = WaypointSprite_getviewentity(client);
1199 entity wp_owner = this.owner;
1200 entity flag = e.flagcarried;
1201 if(flag && CTF_SAMETEAM(e, flag))
1203 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1208 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1211 waypoint_spawnforitem_force(this, this.origin);
1212 navigation_dynamicgoal_init(this, true);
1218 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1219 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1220 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1221 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1222 default: basename = WP_FlagBaseNeutral; break;
1225 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1226 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1227 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1228 setcefc(wp, ctf_FlagBase_Customize);
1230 // captureshield setup
1231 ctf_CaptureShield_Spawn(this);
1236 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1239 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1240 ctf_worldflaglist = flag;
1242 setattachment(flag, NULL, "");
1244 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1245 flag.team = teamnumber;
1246 flag.classname = "item_flag_team";
1247 flag.target = "###item###"; // wut?
1248 flag.flags = FL_ITEM | FL_NOTARGET;
1249 IL_PUSH(g_items, flag);
1250 flag.solid = SOLID_TRIGGER;
1251 flag.takedamage = DAMAGE_NO;
1252 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1253 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1254 SetResourceAmountExplicit(flag, RESOURCE_HEALTH, flag.max_flag_health);
1255 flag.event_damage = ctf_FlagDamage;
1256 flag.pushable = true;
1257 flag.teleportable = TELEPORT_NORMAL;
1258 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1259 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1260 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1261 if(flag.damagedbycontents)
1262 IL_PUSH(g_damagedbycontents, flag);
1263 flag.velocity = '0 0 0';
1264 flag.mangle = flag.angles;
1265 flag.reset = ctf_Reset;
1266 settouch(flag, ctf_FlagTouch);
1267 setthink(flag, ctf_FlagThink);
1268 flag.nextthink = time + FLAG_THINKRATE;
1269 flag.ctf_status = FLAG_BASE;
1271 // crudely force them all to 0
1272 if(autocvar_g_ctf_score_ignore_fields)
1273 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1275 string teamname = Static_Team_ColorName_Lower(teamnumber);
1277 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1278 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1279 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1280 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnumber).eent_eff_name; }
1281 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnumber).eent_eff_name; }
1282 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnumber).eent_eff_name; }
1286 if(flag.s == "") flag.s = b; \
1287 precache_sound(flag.s);
1289 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnumber))))
1290 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnumber))))
1291 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnumber))))
1292 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnumber))))
1293 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1294 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1295 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1299 precache_model(flag.model);
1302 _setmodel(flag, flag.model); // precision set below
1303 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1304 flag.m_mins = flag.mins; // store these for squash checks
1305 flag.m_maxs = flag.maxs;
1306 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1308 if(autocvar_g_ctf_flag_glowtrails)
1312 case NUM_TEAM_1: flag.glow_color = 251; break;
1313 case NUM_TEAM_2: flag.glow_color = 210; break;
1314 case NUM_TEAM_3: flag.glow_color = 110; break;
1315 case NUM_TEAM_4: flag.glow_color = 145; break;
1316 default: flag.glow_color = 254; break;
1318 flag.glow_size = 25;
1319 flag.glow_trail = 1;
1322 flag.effects |= EF_LOWPRECISION;
1323 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1324 if(autocvar_g_ctf_dynamiclights)
1328 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1329 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1330 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1331 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1332 default: flag.effects |= EF_DIMLIGHT; break;
1337 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1339 flag.dropped_origin = flag.origin;
1340 flag.noalign = true;
1341 set_movetype(flag, MOVETYPE_NONE);
1343 else // drop to floor, automatically find a platform and set that as spawn origin
1345 flag.noalign = false;
1347 set_movetype(flag, MOVETYPE_NONE);
1350 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1358 // NOTE: LEGACY CODE, needs to be re-written!
1360 void havocbot_ctf_calculate_middlepoint()
1364 vector fo = '0 0 0';
1367 f = ctf_worldflaglist;
1372 f = f.ctf_worldflagnext;
1378 havocbot_middlepoint = s / n;
1379 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1381 havocbot_symmetry_axis_m = 0;
1382 havocbot_symmetry_axis_q = 0;
1385 // for symmetrical editing of waypoints
1386 entity f1 = ctf_worldflaglist;
1387 entity f2 = f1.ctf_worldflagnext;
1388 float m = -(f1.origin.y - f2.origin.y) / (f1.origin.x - f2.origin.x);
1389 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1390 havocbot_symmetry_axis_m = m;
1391 havocbot_symmetry_axis_q = q;
1393 havocbot_symmetry_origin_order = n;
1397 entity havocbot_ctf_find_flag(entity bot)
1400 f = ctf_worldflaglist;
1403 if (CTF_SAMETEAM(bot, f))
1405 f = f.ctf_worldflagnext;
1410 entity havocbot_ctf_find_enemy_flag(entity bot)
1413 f = ctf_worldflaglist;
1418 if(CTF_DIFFTEAM(bot, f))
1425 else if(!bot.flagcarried)
1429 else if (CTF_DIFFTEAM(bot, f))
1431 f = f.ctf_worldflagnext;
1436 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1443 FOREACH_CLIENT(IS_PLAYER(it), {
1444 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1447 if(vdist(it.origin - org, <, tc_radius))
1456 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1459 head = ctf_worldflaglist;
1462 if (CTF_SAMETEAM(this, head))
1464 head = head.ctf_worldflagnext;
1467 navigation_routerating(this, head, ratingscale, 10000);
1471 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1474 head = ctf_worldflaglist;
1477 if (CTF_SAMETEAM(this, head))
1479 if (this.flagcarried)
1480 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1482 head = head.ctf_worldflagnext; // skip base if it has a different group
1487 head = head.ctf_worldflagnext;
1492 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1495 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1498 head = ctf_worldflaglist;
1503 if(CTF_DIFFTEAM(this, head))
1507 if(this.flagcarried)
1510 else if(!this.flagcarried)
1514 else if(CTF_DIFFTEAM(this, head))
1516 head = head.ctf_worldflagnext;
1520 if (head.ctf_status == FLAG_CARRY)
1522 // adjust rating of our flag carrier depending on his health
1523 head = head.tag_entity;
1524 float f = bound(0, (head.health + head.armorvalue) / 100, 2) - 1;
1525 ratingscale += ratingscale * f * 0.1;
1527 navigation_routerating(this, head, ratingscale, 10000);
1531 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1533 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1535 if (!bot_waypoints_for_items)
1537 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1543 head = havocbot_ctf_find_enemy_flag(this);
1548 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1551 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1555 mf = havocbot_ctf_find_flag(this);
1557 if(mf.ctf_status == FLAG_BASE)
1561 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1564 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1567 head = ctf_worldflaglist;
1570 // flag is out in the field
1571 if(head.ctf_status != FLAG_BASE)
1572 if(head.tag_entity==NULL) // dropped
1576 if(vdist(org - head.origin, <, df_radius))
1577 navigation_routerating(this, head, ratingscale, 10000);
1580 navigation_routerating(this, head, ratingscale, 10000);
1583 head = head.ctf_worldflagnext;
1587 void havocbot_ctf_reset_role(entity this)
1589 float cdefense, cmiddle, coffense;
1596 if (this.flagcarried)
1598 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1602 mf = havocbot_ctf_find_flag(this);
1603 ef = havocbot_ctf_find_enemy_flag(this);
1605 // Retrieve stolen flag
1606 if(mf.ctf_status!=FLAG_BASE)
1608 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1612 // If enemy flag is taken go to the middle to intercept pursuers
1613 if(ef.ctf_status!=FLAG_BASE)
1615 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1619 // if there is no one else on the team switch to offense
1621 // don't check if this bot is a player since it isn't true when the bot is added to the server
1622 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1626 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1629 else if (CS(this).jointime < time + 1)
1631 // if bots spawn all at once set good default roles
1634 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1637 else if (count == 2)
1639 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1644 // Evaluate best position to take
1645 // Count mates on middle position
1646 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1648 // Count mates on defense position
1649 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1651 // Count mates on offense position
1652 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1654 if(cdefense<=coffense)
1655 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1656 else if(coffense<=cmiddle)
1657 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1659 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1661 // if bots spawn all at once assign them a more appropriated role after a while
1662 if (CS(this).jointime < time + 1 && count > 2)
1663 this.havocbot_role_timeout = time + 10 + random() * 10;
1666 bool havocbot_ctf_is_basewaypoint(entity item)
1668 if (item.classname != "waypoint")
1671 entity head = ctf_worldflaglist;
1674 if (item == head.bot_basewaypoint)
1676 head = head.ctf_worldflagnext;
1681 void havocbot_role_ctf_carrier(entity this)
1685 havocbot_ctf_reset_role(this);
1689 if (this.flagcarried == NULL)
1691 havocbot_ctf_reset_role(this);
1695 if (navigation_goalrating_timeout(this))
1697 navigation_goalrating_start(this);
1700 entity mf = havocbot_ctf_find_flag(this);
1701 vector base_org = mf.dropped_origin;
1702 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1704 havocbot_goalrating_ctf_enemybase(this, base_rating);
1706 havocbot_goalrating_ctf_ourbase(this, base_rating);
1708 // start collecting items very close to the bot but only inside of own base radius
1709 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1710 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1712 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1714 navigation_goalrating_end(this);
1716 navigation_goalrating_timeout_set(this);
1718 entity goal = this.goalentity;
1719 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1720 this.goalentity_lock_timeout = time + ((this.bot_aimtarg) ? 2 : 3);
1723 this.havocbot_cantfindflag = time + 10;
1724 else if (time > this.havocbot_cantfindflag)
1726 // Can't navigate to my own base, suicide!
1727 // TODO: drop it and wander around
1728 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1734 void havocbot_role_ctf_escort(entity this)
1740 havocbot_ctf_reset_role(this);
1744 if (this.flagcarried)
1746 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1750 // If enemy flag is back on the base switch to previous role
1751 ef = havocbot_ctf_find_enemy_flag(this);
1752 if(ef.ctf_status==FLAG_BASE)
1754 this.havocbot_role = this.havocbot_previous_role;
1755 this.havocbot_role_timeout = 0;
1758 if (ef.ctf_status == FLAG_DROPPED)
1760 navigation_goalrating_timeout_expire(this, 1);
1764 // If the flag carrier reached the base switch to defense
1765 mf = havocbot_ctf_find_flag(this);
1766 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1768 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1772 // Set the role timeout if necessary
1773 if (!this.havocbot_role_timeout)
1775 this.havocbot_role_timeout = time + random() * 30 + 60;
1778 // If nothing happened just switch to previous role
1779 if (time > this.havocbot_role_timeout)
1781 this.havocbot_role = this.havocbot_previous_role;
1782 this.havocbot_role_timeout = 0;
1786 // Chase the flag carrier
1787 if (navigation_goalrating_timeout(this))
1789 navigation_goalrating_start(this);
1792 havocbot_goalrating_ctf_enemyflag(this, 10000);
1793 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1794 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1796 navigation_goalrating_end(this);
1798 navigation_goalrating_timeout_set(this);
1802 void havocbot_role_ctf_offense(entity this)
1809 havocbot_ctf_reset_role(this);
1813 if (this.flagcarried)
1815 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1820 mf = havocbot_ctf_find_flag(this);
1821 ef = havocbot_ctf_find_enemy_flag(this);
1824 if(mf.ctf_status!=FLAG_BASE)
1827 pos = mf.tag_entity.origin;
1831 // Try to get it if closer than the enemy base
1832 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1834 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1839 // Escort flag carrier
1840 if(ef.ctf_status!=FLAG_BASE)
1843 pos = ef.tag_entity.origin;
1847 if(vdist(pos - mf.dropped_origin, >, 700))
1849 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1854 // Set the role timeout if necessary
1855 if (!this.havocbot_role_timeout)
1856 this.havocbot_role_timeout = time + 120;
1858 if (time > this.havocbot_role_timeout)
1860 havocbot_ctf_reset_role(this);
1864 if (navigation_goalrating_timeout(this))
1866 navigation_goalrating_start(this);
1869 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1870 havocbot_goalrating_ctf_enemybase(this, 10000);
1871 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1873 navigation_goalrating_end(this);
1875 navigation_goalrating_timeout_set(this);
1879 // Retriever (temporary role):
1880 void havocbot_role_ctf_retriever(entity this)
1886 havocbot_ctf_reset_role(this);
1890 if (this.flagcarried)
1892 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1896 // If flag is back on the base switch to previous role
1897 mf = havocbot_ctf_find_flag(this);
1898 if(mf.ctf_status==FLAG_BASE)
1900 if (mf.enemy == this) // did this bot return the flag?
1901 navigation_goalrating_timeout_force(this);
1902 havocbot_ctf_reset_role(this);
1906 if (!this.havocbot_role_timeout)
1907 this.havocbot_role_timeout = time + 20;
1909 if (time > this.havocbot_role_timeout)
1911 havocbot_ctf_reset_role(this);
1915 if (navigation_goalrating_timeout(this))
1917 const float RT_RADIUS = 10000;
1919 navigation_goalrating_start(this);
1922 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1923 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1924 havocbot_goalrating_ctf_enemybase(this, 8000);
1925 entity ef = havocbot_ctf_find_enemy_flag(this);
1926 vector enemy_base_org = ef.dropped_origin;
1927 // start collecting items very close to the bot but only inside of enemy base radius
1928 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1929 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1930 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1932 navigation_goalrating_end(this);
1934 navigation_goalrating_timeout_set(this);
1938 void havocbot_role_ctf_middle(entity this)
1944 havocbot_ctf_reset_role(this);
1948 if (this.flagcarried)
1950 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1954 mf = havocbot_ctf_find_flag(this);
1955 if(mf.ctf_status!=FLAG_BASE)
1957 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1961 if (!this.havocbot_role_timeout)
1962 this.havocbot_role_timeout = time + 10;
1964 if (time > this.havocbot_role_timeout)
1966 havocbot_ctf_reset_role(this);
1970 if (navigation_goalrating_timeout(this))
1974 org = havocbot_middlepoint;
1975 org.z = this.origin.z;
1977 navigation_goalrating_start(this);
1980 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
1981 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
1982 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
1983 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
1984 havocbot_goalrating_items(this, 18000, this.origin, 10000);
1985 havocbot_goalrating_ctf_enemybase(this, 3000);
1987 navigation_goalrating_end(this);
1989 entity goal = this.goalentity;
1990 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1991 this.goalentity_lock_timeout = time + 2;
1993 navigation_goalrating_timeout_set(this);
1997 void havocbot_role_ctf_defense(entity this)
2003 havocbot_ctf_reset_role(this);
2007 if (this.flagcarried)
2009 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2013 // If own flag was captured
2014 mf = havocbot_ctf_find_flag(this);
2015 if(mf.ctf_status!=FLAG_BASE)
2017 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2021 if (!this.havocbot_role_timeout)
2022 this.havocbot_role_timeout = time + 30;
2024 if (time > this.havocbot_role_timeout)
2026 havocbot_ctf_reset_role(this);
2029 if (navigation_goalrating_timeout(this))
2031 vector org = mf.dropped_origin;
2033 navigation_goalrating_start(this);
2035 // if enemies are closer to our base, go there
2036 entity closestplayer = NULL;
2037 float distance, bestdistance = 10000;
2038 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2039 distance = vlen(org - it.origin);
2040 if(distance<bestdistance)
2043 bestdistance = distance;
2049 if(DIFF_TEAM(closestplayer, this))
2050 if(vdist(org - this.origin, >, 1000))
2051 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2052 havocbot_goalrating_ctf_ourbase(this, 10000);
2054 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2055 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2056 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2057 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2058 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2060 navigation_goalrating_end(this);
2062 navigation_goalrating_timeout_set(this);
2066 void havocbot_role_ctf_setrole(entity bot, int role)
2068 string s = "(null)";
2071 case HAVOCBOT_CTF_ROLE_CARRIER:
2073 bot.havocbot_role = havocbot_role_ctf_carrier;
2074 bot.havocbot_role_timeout = 0;
2075 bot.havocbot_cantfindflag = time + 10;
2076 if (bot.havocbot_previous_role != bot.havocbot_role)
2077 navigation_goalrating_timeout_force(bot);
2079 case HAVOCBOT_CTF_ROLE_DEFENSE:
2081 bot.havocbot_role = havocbot_role_ctf_defense;
2082 bot.havocbot_role_timeout = 0;
2084 case HAVOCBOT_CTF_ROLE_MIDDLE:
2086 bot.havocbot_role = havocbot_role_ctf_middle;
2087 bot.havocbot_role_timeout = 0;
2089 case HAVOCBOT_CTF_ROLE_OFFENSE:
2091 bot.havocbot_role = havocbot_role_ctf_offense;
2092 bot.havocbot_role_timeout = 0;
2094 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2096 bot.havocbot_previous_role = bot.havocbot_role;
2097 bot.havocbot_role = havocbot_role_ctf_retriever;
2098 bot.havocbot_role_timeout = time + 10;
2099 if (bot.havocbot_previous_role != bot.havocbot_role)
2100 navigation_goalrating_timeout_expire(bot, 2);
2102 case HAVOCBOT_CTF_ROLE_ESCORT:
2104 bot.havocbot_previous_role = bot.havocbot_role;
2105 bot.havocbot_role = havocbot_role_ctf_escort;
2106 bot.havocbot_role_timeout = time + 30;
2107 if (bot.havocbot_previous_role != bot.havocbot_role)
2108 navigation_goalrating_timeout_expire(bot, 2);
2111 LOG_TRACE(bot.netname, " switched to ", s);
2119 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2121 entity player = M_ARGV(0, entity);
2123 int t = 0, t2 = 0, t3 = 0;
2124 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)
2126 // initially clear items so they can be set as necessary later.
2127 STAT(CTF_FLAGSTATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2128 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2129 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2130 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2131 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2132 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2134 // scan through all the flags and notify the client about them
2135 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2137 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2138 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2139 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2140 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2141 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; }
2143 switch(flag.ctf_status)
2148 if((flag.owner == player) || (flag.pass_sender == player))
2149 STAT(CTF_FLAGSTATUS, player) |= t; // carrying: player is currently carrying the flag
2151 STAT(CTF_FLAGSTATUS, player) |= t2; // taken: someone else is carrying the flag
2156 STAT(CTF_FLAGSTATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2162 // item for stopping players from capturing the flag too often
2163 if(player.ctf_captureshielded)
2164 STAT(CTF_FLAGSTATUS, player) |= CTF_SHIELDED;
2167 STAT(CTF_FLAGSTATUS, player) |= CTF_STALEMATE;
2169 // update the health of the flag carrier waypointsprite
2170 if(player.wps_flagcarrier)
2171 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(GetResourceAmount(player, RESOURCE_HEALTH), GetResourceAmount(player, RESOURCE_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
2174 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2176 entity frag_attacker = M_ARGV(1, entity);
2177 entity frag_target = M_ARGV(2, entity);
2178 float frag_damage = M_ARGV(4, float);
2179 vector frag_force = M_ARGV(6, vector);
2181 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2183 if(frag_target == frag_attacker) // damage done to yourself
2185 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2186 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2188 else // damage done to everyone else
2190 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2191 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2194 M_ARGV(4, float) = frag_damage;
2195 M_ARGV(6, vector) = frag_force;
2197 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2199 if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(GetResourceAmount(frag_target, RESOURCE_HEALTH), GetResourceAmount(frag_target, RESOURCE_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id)))
2200 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2202 frag_target.wps_helpme_time = time;
2203 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2205 // todo: add notification for when flag carrier needs help?
2209 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2211 entity frag_attacker = M_ARGV(1, entity);
2212 entity frag_target = M_ARGV(2, entity);
2214 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2216 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2217 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2220 if(frag_target.flagcarried)
2222 entity tmp_entity = frag_target.flagcarried;
2223 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2224 tmp_entity.ctf_dropper = NULL;
2228 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2230 M_ARGV(2, float) = 0; // frag score
2231 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2234 void ctf_RemovePlayer(entity player)
2236 if(player.flagcarried)
2237 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2239 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2241 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2242 if(flag.pass_target == player) { flag.pass_target = NULL; }
2243 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2247 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2249 entity player = M_ARGV(0, entity);
2251 ctf_RemovePlayer(player);
2254 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2256 entity player = M_ARGV(0, entity);
2258 ctf_RemovePlayer(player);
2261 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2263 if(!autocvar_g_ctf_leaderboard)
2266 entity player = M_ARGV(0, entity);
2268 if(IS_REAL_CLIENT(player))
2270 int m = min(RANKINGS_CNT, autocvar_g_cts_send_rankings_cnt);
2271 race_send_rankings_cnt(MSG_ONE);
2272 for (int i = 1; i <= m; ++i)
2274 race_SendRankings(i, 0, 0, MSG_ONE);
2279 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2281 if(!autocvar_g_ctf_leaderboard)
2284 entity player = M_ARGV(0, entity);
2286 if(CS(player).cvar_cl_allow_uidtracking == 1 && CS(player).cvar_cl_allow_uid2name == 1)
2288 if (!player.stored_netname)
2289 player.stored_netname = strzone(uid2name(player.crypto_idfp));
2290 if(player.stored_netname != player.netname)
2292 db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
2293 strcpy(player.stored_netname, player.netname);
2298 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2300 entity player = M_ARGV(0, entity);
2302 if(player.flagcarried)
2303 if(!autocvar_g_ctf_portalteleport)
2304 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2307 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2309 if(MUTATOR_RETURNVALUE || game_stopped) return;
2311 entity player = M_ARGV(0, entity);
2313 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2315 // pass the flag to a team mate
2316 if(autocvar_g_ctf_pass)
2318 entity head, closest_target = NULL;
2319 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2321 while(head) // find the closest acceptable target to pass to
2323 if(IS_PLAYER(head) && !IS_DEAD(head))
2324 if(head != player && SAME_TEAM(head, player))
2325 if(!head.speedrunning && !head.vehicle)
2327 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2328 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2329 vector passer_center = CENTER_OR_VIEWOFS(player);
2331 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2333 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2335 if(IS_BOT_CLIENT(head))
2337 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2338 ctf_Handle_Throw(head, player, DROP_PASS);
2342 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2343 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2345 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2348 else if(player.flagcarried && !head.flagcarried)
2352 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2353 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2354 { closest_target = head; }
2356 else { closest_target = head; }
2363 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2366 // throw the flag in front of you
2367 if(autocvar_g_ctf_throw && player.flagcarried)
2369 if(player.throw_count == -1)
2371 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2373 player.throw_prevtime = time;
2374 player.throw_count = 1;
2375 ctf_Handle_Throw(player, NULL, DROP_THROW);
2380 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2386 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2387 else { player.throw_count += 1; }
2388 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2390 player.throw_prevtime = time;
2391 ctf_Handle_Throw(player, NULL, DROP_THROW);
2398 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2400 entity player = M_ARGV(0, entity);
2402 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2404 player.wps_helpme_time = time;
2405 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2407 else // create a normal help me waypointsprite
2409 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2410 WaypointSprite_Ping(player.wps_helpme);
2416 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2418 entity player = M_ARGV(0, entity);
2419 entity veh = M_ARGV(1, entity);
2421 if(player.flagcarried)
2423 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2425 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2429 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2430 setattachment(player.flagcarried, veh, "");
2431 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2432 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2433 //player.flagcarried.angles = '0 0 0';
2439 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2441 entity player = M_ARGV(0, entity);
2443 if(player.flagcarried)
2445 setattachment(player.flagcarried, player, "");
2446 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2447 player.flagcarried.scale = FLAG_SCALE;
2448 player.flagcarried.angles = '0 0 0';
2449 player.flagcarried.nodrawtoclient = NULL;
2454 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2456 entity player = M_ARGV(0, entity);
2458 if(player.flagcarried)
2460 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2461 ctf_RespawnFlag(player.flagcarried);
2466 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2468 entity flag; // temporary entity for the search method
2470 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2472 switch(flag.ctf_status)
2477 // lock the flag, game is over
2478 set_movetype(flag, MOVETYPE_NONE);
2479 flag.takedamage = DAMAGE_NO;
2480 flag.solid = SOLID_NOT;
2481 flag.nextthink = false; // stop thinking
2483 //dprint("stopping the ", flag.netname, " from moving.\n");
2491 // do nothing for these flags
2498 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2500 entity bot = M_ARGV(0, entity);
2502 havocbot_ctf_reset_role(bot);
2506 MUTATOR_HOOKFUNCTION(ctf, CheckAllowedTeams)
2508 //M_ARGV(0, float) = ctf_teams;
2509 M_ARGV(1, string) = "ctf_team";
2513 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2515 entity spectatee = M_ARGV(0, entity);
2516 entity client = M_ARGV(1, entity);
2518 STAT(CTF_FLAGSTATUS, client) = STAT(CTF_FLAGSTATUS, spectatee);
2521 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2523 int record_page = M_ARGV(0, int);
2524 string ret_string = M_ARGV(1, string);
2526 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2528 if (MapInfo_Get_ByID(i))
2530 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2536 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2537 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2541 M_ARGV(1, string) = ret_string;
2544 bool superspec_Spectate(entity this, entity targ); // TODO
2545 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2546 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2548 entity player = M_ARGV(0, entity);
2549 string cmd_name = M_ARGV(1, string);
2550 int cmd_argc = M_ARGV(2, int);
2552 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2554 if(cmd_name == "followfc")
2566 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2567 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2568 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2569 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2573 FOREACH_CLIENT(IS_PLAYER(it), {
2574 if(it.flagcarried && (it.team == _team || _team == 0))
2577 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2578 continue; // already spectating this fc, try another
2579 return superspec_Spectate(player, it);
2584 superspec_msg("", "", player, "No active flag carrier\n", 1);
2589 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2591 entity frag_target = M_ARGV(0, entity);
2593 if(frag_target.flagcarried)
2594 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2602 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2603 CTF flag for team one (Red).
2605 "angle" Angle the flag will point (minus 90 degrees)...
2606 "model" model to use, note this needs red and blue as skins 0 and 1...
2607 "noise" sound played when flag is picked up...
2608 "noise1" sound played when flag is returned by a teammate...
2609 "noise2" sound played when flag is captured...
2610 "noise3" sound played when flag is lost in the field and respawns itself...
2611 "noise4" sound played when flag is dropped by a player...
2612 "noise5" sound played when flag touches the ground... */
2613 spawnfunc(item_flag_team1)
2615 if(!g_ctf) { delete(this); return; }
2617 ctf_FlagSetup(NUM_TEAM_1, this);
2620 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2621 CTF flag for team two (Blue).
2623 "angle" Angle the flag will point (minus 90 degrees)...
2624 "model" model to use, note this needs red and blue as skins 0 and 1...
2625 "noise" sound played when flag is picked up...
2626 "noise1" sound played when flag is returned by a teammate...
2627 "noise2" sound played when flag is captured...
2628 "noise3" sound played when flag is lost in the field and respawns itself...
2629 "noise4" sound played when flag is dropped by a player...
2630 "noise5" sound played when flag touches the ground... */
2631 spawnfunc(item_flag_team2)
2633 if(!g_ctf) { delete(this); return; }
2635 ctf_FlagSetup(NUM_TEAM_2, this);
2638 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2639 CTF flag for team three (Yellow).
2641 "angle" Angle the flag will point (minus 90 degrees)...
2642 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2643 "noise" sound played when flag is picked up...
2644 "noise1" sound played when flag is returned by a teammate...
2645 "noise2" sound played when flag is captured...
2646 "noise3" sound played when flag is lost in the field and respawns itself...
2647 "noise4" sound played when flag is dropped by a player...
2648 "noise5" sound played when flag touches the ground... */
2649 spawnfunc(item_flag_team3)
2651 if(!g_ctf) { delete(this); return; }
2653 ctf_FlagSetup(NUM_TEAM_3, this);
2656 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2657 CTF flag for team four (Pink).
2659 "angle" Angle the flag will point (minus 90 degrees)...
2660 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2661 "noise" sound played when flag is picked up...
2662 "noise1" sound played when flag is returned by a teammate...
2663 "noise2" sound played when flag is captured...
2664 "noise3" sound played when flag is lost in the field and respawns itself...
2665 "noise4" sound played when flag is dropped by a player...
2666 "noise5" sound played when flag touches the ground... */
2667 spawnfunc(item_flag_team4)
2669 if(!g_ctf) { delete(this); return; }
2671 ctf_FlagSetup(NUM_TEAM_4, this);
2674 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2677 "angle" Angle the flag will point (minus 90 degrees)...
2678 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2679 "noise" sound played when flag is picked up...
2680 "noise1" sound played when flag is returned by a teammate...
2681 "noise2" sound played when flag is captured...
2682 "noise3" sound played when flag is lost in the field and respawns itself...
2683 "noise4" sound played when flag is dropped by a player...
2684 "noise5" sound played when flag touches the ground... */
2685 spawnfunc(item_flag_neutral)
2687 if(!g_ctf) { delete(this); return; }
2688 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2690 ctf_FlagSetup(0, this);
2693 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2694 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2695 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.
2697 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2698 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2701 if(!g_ctf) { delete(this); return; }
2703 this.classname = "ctf_team";
2704 this.team = this.cnt + 1;
2707 // compatibility for quake maps
2708 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2709 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2710 spawnfunc(info_player_team1);
2711 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2712 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2713 spawnfunc(info_player_team2);
2714 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2715 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2717 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2718 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2720 // compatibility for wop maps
2721 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2722 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2723 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2724 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2725 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2726 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2734 void ctf_ScoreRules(int teams)
2736 CheckAllowedTeams(NULL);
2737 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2738 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2739 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2740 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2741 field(SP_CTF_PICKUPS, "pickups", 0);
2742 field(SP_CTF_FCKILLS, "fckills", 0);
2743 field(SP_CTF_RETURNS, "returns", 0);
2744 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2748 // code from here on is just to support maps that don't have flag and team entities
2749 void ctf_SpawnTeam (string teamname, int teamcolor)
2751 entity this = new_pure(ctf_team);
2752 this.netname = teamname;
2753 this.cnt = teamcolor - 1;
2754 this.spawnfunc_checked = true;
2755 this.team = teamcolor;
2758 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2763 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2765 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2766 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2768 switch(tmp_entity.team)
2770 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2771 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2772 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2773 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2775 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2778 havocbot_ctf_calculate_middlepoint();
2780 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2782 ctf_teams = 0; // so set the default red and blue teams
2783 BITSET_ASSIGN(ctf_teams, BIT(0));
2784 BITSET_ASSIGN(ctf_teams, BIT(1));
2787 //ctf_teams = bound(2, ctf_teams, 4);
2789 // if no teams are found, spawn defaults
2790 if(find(NULL, classname, "ctf_team") == NULL)
2792 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2793 if(ctf_teams & BIT(0))
2794 ctf_SpawnTeam("Red", NUM_TEAM_1);
2795 if(ctf_teams & BIT(1))
2796 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2797 if(ctf_teams & BIT(2))
2798 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2799 if(ctf_teams & BIT(3))
2800 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2803 ctf_ScoreRules(ctf_teams);
2806 void ctf_Initialize()
2808 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2810 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2811 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2812 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2814 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);