1 #include "gamemode_ctf.qh"
3 #include <common/effects/all.qh>
4 #include <common/vehicles/all.qh>
5 #include <server/teamplay.qh>
7 #include <lib/warpzone/common.qh>
9 bool autocvar_g_ctf_allow_vehicle_carry;
10 bool autocvar_g_ctf_allow_vehicle_touch;
11 bool autocvar_g_ctf_allow_monster_touch;
12 bool autocvar_g_ctf_throw;
13 float autocvar_g_ctf_throw_angle_max;
14 float autocvar_g_ctf_throw_angle_min;
15 int autocvar_g_ctf_throw_punish_count;
16 float autocvar_g_ctf_throw_punish_delay;
17 float autocvar_g_ctf_throw_punish_time;
18 float autocvar_g_ctf_throw_strengthmultiplier;
19 float autocvar_g_ctf_throw_velocity_forward;
20 float autocvar_g_ctf_throw_velocity_up;
21 float autocvar_g_ctf_drop_velocity_up;
22 float autocvar_g_ctf_drop_velocity_side;
23 bool autocvar_g_ctf_oneflag_reverse;
24 bool autocvar_g_ctf_portalteleport;
25 bool autocvar_g_ctf_pass;
26 float autocvar_g_ctf_pass_arc;
27 float autocvar_g_ctf_pass_arc_max;
28 float autocvar_g_ctf_pass_directional_max;
29 float autocvar_g_ctf_pass_directional_min;
30 float autocvar_g_ctf_pass_radius;
31 float autocvar_g_ctf_pass_wait;
32 bool autocvar_g_ctf_pass_request;
33 float autocvar_g_ctf_pass_turnrate;
34 float autocvar_g_ctf_pass_timelimit;
35 float autocvar_g_ctf_pass_velocity;
36 bool autocvar_g_ctf_dynamiclights;
37 float autocvar_g_ctf_flag_collect_delay;
38 float autocvar_g_ctf_flag_damageforcescale;
39 bool autocvar_g_ctf_flag_dropped_waypoint;
40 bool autocvar_g_ctf_flag_dropped_floatinwater;
41 bool autocvar_g_ctf_flag_glowtrails;
42 int autocvar_g_ctf_flag_health;
43 bool autocvar_g_ctf_flag_return;
44 bool autocvar_g_ctf_flag_return_carrying;
45 float autocvar_g_ctf_flag_return_carried_radius;
46 float autocvar_g_ctf_flag_return_time;
47 bool autocvar_g_ctf_flag_return_when_unreachable;
48 float autocvar_g_ctf_flag_return_damage;
49 float autocvar_g_ctf_flag_return_damage_delay;
50 float autocvar_g_ctf_flag_return_dropped;
51 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
52 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
53 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
54 float autocvar_g_ctf_flagcarrier_selfforcefactor;
55 float autocvar_g_ctf_flagcarrier_damagefactor;
56 float autocvar_g_ctf_flagcarrier_forcefactor;
57 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
58 bool autocvar_g_ctf_fullbrightflags;
59 bool autocvar_g_ctf_ignore_frags;
60 bool autocvar_g_ctf_score_ignore_fields;
61 int autocvar_g_ctf_score_capture;
62 int autocvar_g_ctf_score_capture_assist;
63 int autocvar_g_ctf_score_kill;
64 int autocvar_g_ctf_score_penalty_drop;
65 int autocvar_g_ctf_score_penalty_returned;
66 int autocvar_g_ctf_score_pickup_base;
67 int autocvar_g_ctf_score_pickup_dropped_early;
68 int autocvar_g_ctf_score_pickup_dropped_late;
69 int autocvar_g_ctf_score_return;
70 float autocvar_g_ctf_shield_force;
71 float autocvar_g_ctf_shield_max_ratio;
72 int autocvar_g_ctf_shield_min_negscore;
73 bool autocvar_g_ctf_stalemate;
74 int autocvar_g_ctf_stalemate_endcondition;
75 float autocvar_g_ctf_stalemate_time;
76 bool autocvar_g_ctf_reverse;
77 float autocvar_g_ctf_dropped_capture_delay;
78 float autocvar_g_ctf_dropped_capture_radius;
80 void ctf_FakeTimeLimit(entity e, float t)
83 WriteByte(MSG_ONE, 3); // svc_updatestat
84 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
86 WriteCoord(MSG_ONE, autocvar_timelimit);
88 WriteCoord(MSG_ONE, (t + 1) / 60);
91 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
93 if(autocvar_sv_eventlog)
94 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != NULL) ? ftos(actor.playerid) : "")));
95 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
98 void ctf_CaptureRecord(entity flag, entity player)
100 float cap_record = ctf_captimerecord;
101 float cap_time = (time - flag.ctf_pickuptime);
102 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
106 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname);
107 else if(!ctf_captimerecord)
108 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_TIME), player.netname, TIME_ENCODE(cap_time));
109 else if(cap_time < cap_record)
110 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));
112 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));
114 // write that shit in the database
115 if(!ctf_oneflag) // but not in 1-flag mode
116 if((!ctf_captimerecord) || (cap_time < cap_record))
118 ctf_captimerecord = cap_time;
119 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
120 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
121 write_recordmarker(player, flag.ctf_pickuptime, cap_time);
124 if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
125 race_setTime(GetMapname(), TIME_ENCODE(cap_time), player.crypto_idfp, player.netname, player, false);
128 bool ctf_Immediate_Return_Allowed(entity flag, entity toucher)
131 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), { ++num_perteam; });
133 // automatically return if there's only 1 player on the team
134 return ((autocvar_g_ctf_flag_return || num_perteam <= 1 || (autocvar_g_ctf_flag_return_carrying && toucher.flagcarried))
138 bool ctf_Return_Customize(entity this, entity client)
140 // only to the carrier
141 return boolean(client == this.owner);
144 void ctf_FlagcarrierWaypoints(entity player)
146 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
147 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
148 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
149 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
151 if(player.flagcarried && CTF_SAMETEAM(player, player.flagcarried))
153 if(!player.wps_enemyflagcarrier)
155 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, 0, player, wps_enemyflagcarrier, true, RADARICON_FLAG);
156 wp.colormod = WPCOLOR_ENEMYFC(player.team);
157 setcefc(wp, ctf_Stalemate_Customize);
159 if(IS_REAL_CLIENT(player) && !ctf_stalemate)
160 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_VISIBLE);
163 if(!player.wps_flagreturn)
165 entity owp = WaypointSprite_SpawnFixed(WP_FlagReturn, player.flagcarried.ctf_spawnorigin + FLAG_WAYPOINT_OFFSET, player, wps_flagreturn, RADARICON_FLAG);
166 owp.colormod = '0 0.8 0.8';
167 //WaypointSprite_UpdateTeamRadar(player.wps_flagreturn, RADARICON_FLAG, ((player.team) ? colormapPaletteColor(player.team - 1, false) : '1 1 1'));
168 setcefc(owp, ctf_Return_Customize);
173 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
175 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
176 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
177 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
178 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
181 if(current_height) // make sure we can actually do this arcing path
183 targpos = (to + ('0 0 1' * current_height));
184 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
185 if(trace_fraction < 1)
187 //print("normal arc line failed, trying to find new pos...");
188 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
189 targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
190 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
191 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
192 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
195 else { targpos = to; }
197 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
199 vector desired_direction = normalize(targpos - from);
200 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
201 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
204 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
206 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
208 // directional tracing only
210 makevectors(passer_angle);
212 // find the closest point on the enemy to the center of the attack
213 float h; // hypotenuse, which is the distance between attacker to head
214 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
216 h = vlen(head_center - passer_center);
217 a = h * (normalize(head_center - passer_center) * v_forward);
219 vector nearest_on_line = (passer_center + a * v_forward);
220 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
222 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
223 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
225 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
230 else { return true; }
234 // =======================
235 // CaptureShield Functions
236 // =======================
238 bool ctf_CaptureShield_CheckStatus(entity p)
240 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
241 int players_worseeq, players_total;
243 if(ctf_captureshield_max_ratio <= 0)
246 s = GameRules_scoring_add(p, CTF_CAPS, 0);
247 s2 = GameRules_scoring_add(p, CTF_PICKUPS, 0);
248 s3 = GameRules_scoring_add(p, CTF_RETURNS, 0);
249 s4 = GameRules_scoring_add(p, CTF_FCKILLS, 0);
251 sr = ((s - s2) + (s3 + s4));
253 if(sr >= -ctf_captureshield_min_negscore)
256 players_total = players_worseeq = 0;
257 FOREACH_CLIENT(IS_PLAYER(it), {
260 se = GameRules_scoring_add(it, CTF_CAPS, 0);
261 se2 = GameRules_scoring_add(it, CTF_PICKUPS, 0);
262 se3 = GameRules_scoring_add(it, CTF_RETURNS, 0);
263 se4 = GameRules_scoring_add(it, CTF_FCKILLS, 0);
265 ser = ((se - se2) + (se3 + se4));
272 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
273 // use this rule here
275 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
281 void ctf_CaptureShield_Update(entity player, bool wanted_status)
283 bool updated_status = ctf_CaptureShield_CheckStatus(player);
284 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
286 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
287 player.ctf_captureshielded = updated_status;
291 bool ctf_CaptureShield_Customize(entity this, entity client)
293 if(!client.ctf_captureshielded) { return false; }
294 if(CTF_SAMETEAM(this, client)) { return false; }
299 void ctf_CaptureShield_Touch(entity this, entity toucher)
301 if(!toucher.ctf_captureshielded) { return; }
302 if(CTF_SAMETEAM(this, toucher)) { return; }
304 vector mymid = (this.absmin + this.absmax) * 0.5;
305 vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
307 Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
308 if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
311 void ctf_CaptureShield_Spawn(entity flag)
313 entity shield = new(ctf_captureshield);
316 shield.team = flag.team;
317 settouch(shield, ctf_CaptureShield_Touch);
318 setcefc(shield, ctf_CaptureShield_Customize);
319 shield.effects = EF_ADDITIVE;
320 set_movetype(shield, MOVETYPE_NOCLIP);
321 shield.solid = SOLID_TRIGGER;
322 shield.avelocity = '7 0 11';
325 setorigin(shield, flag.origin);
326 setmodel(shield, MDL_CTF_SHIELD);
327 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
331 // ====================
332 // Drop/Pass/Throw Code
333 // ====================
335 void ctf_Handle_Drop(entity flag, entity player, int droptype)
338 player = (player ? player : flag.pass_sender);
341 set_movetype(flag, MOVETYPE_TOSS);
342 flag.takedamage = DAMAGE_YES;
343 flag.angles = '0 0 0';
344 flag.health = flag.max_flag_health;
345 flag.ctf_droptime = time;
346 flag.ctf_dropper = player;
347 flag.ctf_status = FLAG_DROPPED;
349 // messages and sounds
350 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
351 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
352 ctf_EventLog("dropped", player.team, player);
355 GameRules_scoring_add_team(player, SCORE, -((flag.score_drop) ? flag.score_drop : autocvar_g_ctf_score_penalty_drop));
356 GameRules_scoring_add(player, CTF_DROPS, 1);
359 if(autocvar_g_ctf_flag_dropped_waypoint) {
360 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);
361 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
364 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
366 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
367 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
370 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
372 if(droptype == DROP_PASS)
374 flag.pass_distance = 0;
375 flag.pass_sender = NULL;
376 flag.pass_target = NULL;
380 void ctf_Handle_Retrieve(entity flag, entity player)
382 entity sender = flag.pass_sender;
384 // transfer flag to player
386 flag.owner.flagcarried = flag;
387 GameRules_scoring_vip(player, true);
392 setattachment(flag, player.vehicle, "");
393 setorigin(flag, VEHICLE_FLAG_OFFSET);
394 flag.scale = VEHICLE_FLAG_SCALE;
398 setattachment(flag, player, "");
399 setorigin(flag, FLAG_CARRY_OFFSET);
401 set_movetype(flag, MOVETYPE_NONE);
402 flag.takedamage = DAMAGE_NO;
403 flag.solid = SOLID_NOT;
404 flag.angles = '0 0 0';
405 flag.ctf_status = FLAG_CARRY;
407 // messages and sounds
408 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
409 ctf_EventLog("receive", flag.team, player);
411 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
413 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
414 else if(it == player)
415 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
416 else if(SAME_TEAM(it, sender))
417 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
420 // create new waypoint
421 ctf_FlagcarrierWaypoints(player);
423 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
424 player.throw_antispam = sender.throw_antispam;
426 flag.pass_distance = 0;
427 flag.pass_sender = NULL;
428 flag.pass_target = NULL;
431 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
433 entity flag = player.flagcarried;
434 vector targ_origin, flag_velocity;
436 if(!flag) { return; }
437 if((droptype == DROP_PASS) && !receiver) { return; }
439 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
442 setattachment(flag, NULL, "");
443 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
444 flag.owner.flagcarried = NULL;
445 GameRules_scoring_vip(flag.owner, false);
447 flag.solid = SOLID_TRIGGER;
448 flag.ctf_dropper = player;
449 flag.ctf_droptime = time;
450 navigation_dynamicgoal_set(flag);
452 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
459 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
460 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
461 WarpZone_RefSys_Copy(flag, receiver);
462 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
463 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
465 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
466 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
469 set_movetype(flag, MOVETYPE_FLY);
470 flag.takedamage = DAMAGE_NO;
471 flag.pass_sender = player;
472 flag.pass_target = receiver;
473 flag.ctf_status = FLAG_PASSING;
476 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
477 WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
478 ctf_EventLog("pass", flag.team, player);
484 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'));
486 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)));
487 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
488 ctf_Handle_Drop(flag, player, droptype);
494 flag.velocity = '0 0 0'; // do nothing
501 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);
502 ctf_Handle_Drop(flag, player, droptype);
507 // kill old waypointsprite
508 WaypointSprite_Ping(player.wps_flagcarrier);
509 WaypointSprite_Kill(player.wps_flagcarrier);
511 if(player.wps_enemyflagcarrier)
512 WaypointSprite_Kill(player.wps_enemyflagcarrier);
514 if(player.wps_flagreturn)
515 WaypointSprite_Kill(player.wps_flagreturn);
518 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
521 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
523 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
530 void nades_GiveBonus(entity player, float score);
532 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
534 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
535 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
536 entity player_team_flag = NULL, tmp_entity;
537 float old_time, new_time;
539 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
540 if(CTF_DIFFTEAM(player, flag)) { return; }
541 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)
543 if (toucher.goalentity == flag.bot_basewaypoint)
544 toucher.goalentity_lock_timeout = 0;
547 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
548 if(SAME_TEAM(tmp_entity, player))
550 player_team_flag = tmp_entity;
554 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
556 player.throw_prevtime = time;
557 player.throw_count = 0;
559 // messages and sounds
560 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
561 ctf_CaptureRecord(enemy_flag, player);
562 _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);
566 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
567 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
573 if(enemy_flag.score_capture || flag.score_capture)
574 pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
575 GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
577 if(enemy_flag.score_team_capture || flag.score_team_capture)
578 capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
579 GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
581 old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
582 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
583 if(!old_time || new_time < old_time)
584 GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
587 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
588 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
591 if(capturetype == CAPTURE_NORMAL)
593 WaypointSprite_Kill(player.wps_flagcarrier);
594 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
596 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
597 { GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist)); }
600 flag.enemy = toucher;
603 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
604 ctf_RespawnFlag(enemy_flag);
607 void ctf_Handle_Return(entity flag, entity player)
609 // messages and sounds
610 if(IS_MONSTER(player))
612 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
616 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
617 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
619 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
620 ctf_EventLog("return", flag.team, player);
623 if(IS_PLAYER(player))
625 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
626 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
628 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
631 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
635 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
636 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
637 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
641 if(player.flagcarried == flag)
642 WaypointSprite_Kill(player.wps_flagcarrier);
647 ctf_RespawnFlag(flag);
650 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
653 float pickup_dropped_score; // used to calculate dropped pickup score
655 // attach the flag to the player
657 player.flagcarried = flag;
658 GameRules_scoring_vip(player, true);
661 setattachment(flag, player.vehicle, "");
662 setorigin(flag, VEHICLE_FLAG_OFFSET);
663 flag.scale = VEHICLE_FLAG_SCALE;
667 setattachment(flag, player, "");
668 setorigin(flag, FLAG_CARRY_OFFSET);
672 set_movetype(flag, MOVETYPE_NONE);
673 flag.takedamage = DAMAGE_NO;
674 flag.solid = SOLID_NOT;
675 flag.angles = '0 0 0';
676 flag.ctf_status = FLAG_CARRY;
680 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
681 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
685 // messages and sounds
686 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
688 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
690 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
691 else if(CTF_DIFFTEAM(player, flag))
692 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
694 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
696 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
699 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); });
702 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
703 if(CTF_SAMETEAM(flag, it))
704 if(SAME_TEAM(player, it))
705 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
707 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);
710 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
713 GameRules_scoring_add(player, CTF_PICKUPS, 1);
714 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
719 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
720 ctf_EventLog("steal", flag.team, player);
726 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);
727 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);
728 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
729 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
730 ctf_EventLog("pickup", flag.team, player);
738 if(pickuptype == PICKUP_BASE)
740 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
741 if((player.speedrunning) && (ctf_captimerecord))
742 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
746 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
749 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
750 ctf_FlagcarrierWaypoints(player);
751 WaypointSprite_Ping(player.wps_flagcarrier);
755 // ===================
756 // Main Flag Functions
757 // ===================
759 void ctf_CheckFlagReturn(entity flag, int returntype)
761 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
763 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
765 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
770 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
772 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
773 case RETURN_SPEEDRUN:
774 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
775 case RETURN_NEEDKILL:
776 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
779 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
781 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
782 ctf_EventLog("returned", flag.team, NULL);
784 ctf_RespawnFlag(flag);
789 bool ctf_Stalemate_Customize(entity this, entity client)
791 // make spectators see what the player would see
792 entity e = WaypointSprite_getviewentity(client);
793 entity wp_owner = this.owner;
796 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
797 if(SAME_TEAM(wp_owner, e)) { return false; }
798 if(!IS_PLAYER(e)) { return false; }
803 void ctf_CheckStalemate()
806 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
809 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
811 // build list of stale flags
812 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
814 if(autocvar_g_ctf_stalemate)
815 if(tmp_entity.ctf_status != FLAG_BASE)
816 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
818 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
819 ctf_staleflaglist = tmp_entity;
821 switch(tmp_entity.team)
823 case NUM_TEAM_1: ++stale_red_flags; break;
824 case NUM_TEAM_2: ++stale_blue_flags; break;
825 case NUM_TEAM_3: ++stale_yellow_flags; break;
826 case NUM_TEAM_4: ++stale_pink_flags; break;
827 default: ++stale_neutral_flags; break;
833 stale_flags = (stale_neutral_flags >= 1);
835 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
837 if(ctf_oneflag && stale_flags == 1)
838 ctf_stalemate = true;
839 else if(stale_flags >= 2)
840 ctf_stalemate = true;
841 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
842 { ctf_stalemate = false; wpforenemy_announced = false; }
843 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
844 { ctf_stalemate = false; wpforenemy_announced = false; }
846 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
849 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
851 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
853 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);
854 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
855 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
859 if (!wpforenemy_announced)
861 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)); });
863 wpforenemy_announced = true;
868 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
870 if(ITEM_DAMAGE_NEEDKILL(deathtype))
872 if(autocvar_g_ctf_flag_return_damage_delay)
873 this.ctf_flagdamaged_byworld = true;
877 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
881 if(autocvar_g_ctf_flag_return_damage)
883 // reduce health and check if it should be returned
884 this.health = this.health - damage;
885 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
890 void ctf_FlagThink(entity this)
895 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
898 if(this == ctf_worldflaglist) // only for the first flag
899 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
902 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
903 LOG_TRACE("wtf the flag got squashed?");
904 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
905 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
906 setsize(this, this.m_mins, this.m_maxs);
910 switch(this.ctf_status)
914 if(autocvar_g_ctf_dropped_capture_radius)
916 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
917 if(tmp_entity.ctf_status == FLAG_DROPPED)
918 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
919 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
920 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
927 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
929 if(autocvar_g_ctf_flag_dropped_floatinwater)
931 vector midpoint = ((this.absmin + this.absmax) * 0.5);
932 if(pointcontents(midpoint) == CONTENT_WATER)
934 this.velocity = this.velocity * 0.5;
936 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
937 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
939 { set_movetype(this, MOVETYPE_FLY); }
941 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
943 if(autocvar_g_ctf_flag_return_dropped)
945 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
948 ctf_CheckFlagReturn(this, RETURN_DROPPED);
952 if(this.ctf_flagdamaged_byworld)
954 this.health -= ((this.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
955 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
958 else if(autocvar_g_ctf_flag_return_time)
960 this.health -= ((this.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
961 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
969 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
972 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
974 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
975 ImpulseCommands(this.owner);
977 if(autocvar_g_ctf_stalemate)
979 if(time >= wpforenemy_nextthink)
981 ctf_CheckStalemate();
982 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
985 if(CTF_SAMETEAM(this, this.owner) && this.team)
987 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
988 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
989 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
990 ctf_Handle_Return(this, this.owner);
997 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
998 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
999 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1001 if((this.pass_target == NULL)
1002 || (IS_DEAD(this.pass_target))
1003 || (this.pass_target.flagcarried)
1004 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1005 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1006 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1008 // give up, pass failed
1009 ctf_Handle_Drop(this, NULL, DROP_PASS);
1013 // still a viable target, go for it
1014 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1019 default: // this should never happen
1021 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1027 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1030 if(game_stopped) return;
1031 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1033 bool is_not_monster = (!IS_MONSTER(toucher));
1035 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1036 if(ITEM_TOUCH_NEEDKILL())
1038 if(!autocvar_g_ctf_flag_return_damage_delay)
1041 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1043 if(!flag.ctf_flagdamaged_byworld) { return; }
1046 // special touch behaviors
1047 if(STAT(FROZEN, toucher)) { return; }
1048 else if(IS_VEHICLE(toucher))
1050 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1051 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1053 return; // do nothing
1055 else if(IS_MONSTER(toucher))
1057 if(!autocvar_g_ctf_allow_monster_touch)
1058 return; // do nothing
1060 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1062 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1064 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1065 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1066 flag.wait = time + FLAG_TOUCHRATE;
1070 else if(IS_DEAD(toucher)) { return; }
1072 switch(flag.ctf_status)
1078 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1079 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1080 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1081 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1083 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1084 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1085 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)
1087 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1088 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1090 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1091 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1097 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1098 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1099 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1100 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1106 LOG_TRACE("Someone touched a flag even though it was being carried?");
1112 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1114 if(DIFF_TEAM(toucher, flag.pass_sender))
1116 if(ctf_Immediate_Return_Allowed(flag, toucher))
1117 ctf_Handle_Return(flag, toucher);
1118 else if(is_not_monster && (!toucher.flagcarried))
1119 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1121 else if(!toucher.flagcarried)
1122 ctf_Handle_Retrieve(flag, toucher);
1129 .float last_respawn;
1130 void ctf_RespawnFlag(entity flag)
1132 // check for flag respawn being called twice in a row
1133 if(flag.last_respawn > time - 0.5)
1134 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1136 flag.last_respawn = time;
1138 // reset the player (if there is one)
1139 if((flag.owner) && (flag.owner.flagcarried == flag))
1141 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1142 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1143 WaypointSprite_Kill(flag.wps_flagcarrier);
1145 flag.owner.flagcarried = NULL;
1146 GameRules_scoring_vip(flag.owner, false);
1148 if(flag.speedrunning)
1149 ctf_FakeTimeLimit(flag.owner, -1);
1152 if((flag.owner) && (flag.owner.vehicle))
1153 flag.scale = FLAG_SCALE;
1155 if(flag.ctf_status == FLAG_DROPPED)
1156 { WaypointSprite_Kill(flag.wps_flagdropped); }
1159 setattachment(flag, NULL, "");
1160 setorigin(flag, flag.ctf_spawnorigin);
1162 set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS));
1163 flag.takedamage = DAMAGE_NO;
1164 flag.health = flag.max_flag_health;
1165 flag.solid = SOLID_TRIGGER;
1166 flag.velocity = '0 0 0';
1167 flag.angles = flag.mangle;
1168 flag.flags = FL_ITEM | FL_NOTARGET;
1170 flag.ctf_status = FLAG_BASE;
1172 flag.pass_distance = 0;
1173 flag.pass_sender = NULL;
1174 flag.pass_target = NULL;
1175 flag.ctf_dropper = NULL;
1176 flag.ctf_pickuptime = 0;
1177 flag.ctf_droptime = 0;
1178 flag.ctf_flagdamaged_byworld = false;
1179 navigation_dynamicgoal_unset(flag);
1181 ctf_CheckStalemate();
1184 void ctf_Reset(entity this)
1186 if(this.owner && IS_PLAYER(this.owner))
1187 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1190 ctf_RespawnFlag(this);
1193 bool ctf_FlagBase_Customize(entity this, entity client)
1195 entity e = WaypointSprite_getviewentity(client);
1196 entity wp_owner = this.owner;
1197 entity flag = e.flagcarried;
1198 if(flag && CTF_SAMETEAM(e, flag))
1200 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1205 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1208 waypoint_spawnforitem_force(this, this.origin);
1209 navigation_dynamicgoal_init(this, true);
1215 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1216 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1217 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1218 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1219 default: basename = WP_FlagBaseNeutral; break;
1222 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1223 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1224 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1225 setcefc(wp, ctf_FlagBase_Customize);
1227 // captureshield setup
1228 ctf_CaptureShield_Spawn(this);
1233 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1236 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1237 ctf_worldflaglist = flag;
1239 setattachment(flag, NULL, "");
1241 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1242 flag.team = teamnumber;
1243 flag.classname = "item_flag_team";
1244 flag.target = "###item###"; // wut?
1245 flag.flags = FL_ITEM | FL_NOTARGET;
1246 IL_PUSH(g_items, flag);
1247 flag.solid = SOLID_TRIGGER;
1248 flag.takedamage = DAMAGE_NO;
1249 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1250 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1251 flag.health = flag.max_flag_health;
1252 flag.event_damage = ctf_FlagDamage;
1253 flag.pushable = true;
1254 flag.teleportable = TELEPORT_NORMAL;
1255 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1256 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1257 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1258 if(flag.damagedbycontents)
1259 IL_PUSH(g_damagedbycontents, flag);
1260 flag.velocity = '0 0 0';
1261 flag.mangle = flag.angles;
1262 flag.reset = ctf_Reset;
1263 settouch(flag, ctf_FlagTouch);
1264 setthink(flag, ctf_FlagThink);
1265 flag.nextthink = time + FLAG_THINKRATE;
1266 flag.ctf_status = FLAG_BASE;
1268 // crudely force them all to 0
1269 if(autocvar_g_ctf_score_ignore_fields)
1270 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1272 string teamname = Static_Team_ColorName_Lower(teamnumber);
1274 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1275 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1276 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1277 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnumber).eent_eff_name; }
1278 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnumber).eent_eff_name; }
1279 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnumber).eent_eff_name; }
1283 if(flag.s == "") flag.s = b; \
1284 precache_sound(flag.s);
1286 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnumber))))
1287 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnumber))))
1288 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnumber))))
1289 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnumber))))
1290 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1291 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1292 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1296 precache_model(flag.model);
1299 _setmodel(flag, flag.model); // precision set below
1300 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1301 flag.m_mins = flag.mins; // store these for squash checks
1302 flag.m_maxs = flag.maxs;
1303 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1305 if(autocvar_g_ctf_flag_glowtrails)
1309 case NUM_TEAM_1: flag.glow_color = 251; break;
1310 case NUM_TEAM_2: flag.glow_color = 210; break;
1311 case NUM_TEAM_3: flag.glow_color = 110; break;
1312 case NUM_TEAM_4: flag.glow_color = 145; break;
1313 default: flag.glow_color = 254; break;
1315 flag.glow_size = 25;
1316 flag.glow_trail = 1;
1319 flag.effects |= EF_LOWPRECISION;
1320 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1321 if(autocvar_g_ctf_dynamiclights)
1325 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1326 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1327 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1328 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1329 default: flag.effects |= EF_DIMLIGHT; break;
1334 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1336 flag.dropped_origin = flag.origin;
1337 flag.noalign = true;
1338 set_movetype(flag, MOVETYPE_NONE);
1340 else // drop to floor, automatically find a platform and set that as spawn origin
1342 flag.noalign = false;
1344 set_movetype(flag, MOVETYPE_NONE);
1347 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1355 // NOTE: LEGACY CODE, needs to be re-written!
1357 void havocbot_ctf_calculate_middlepoint()
1361 vector fo = '0 0 0';
1364 f = ctf_worldflaglist;
1369 f = f.ctf_worldflagnext;
1375 havocbot_middlepoint = s / n;
1376 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1378 havocbot_symmetryaxis_equation = '0 0 0';
1381 // for symmetrical editing of waypoints
1382 entity f1 = ctf_worldflaglist;
1383 entity f2 = f1.ctf_worldflagnext;
1384 float m = -(f1.origin.y - f2.origin.y) / (f1.origin.x - f2.origin.x);
1385 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1386 havocbot_symmetryaxis_equation.x = m;
1387 havocbot_symmetryaxis_equation.y = q;
1389 // store number of flags in this otherwise unused vector component
1390 havocbot_symmetryaxis_equation.z = n;
1394 entity havocbot_ctf_find_flag(entity bot)
1397 f = ctf_worldflaglist;
1400 if (CTF_SAMETEAM(bot, f))
1402 f = f.ctf_worldflagnext;
1407 entity havocbot_ctf_find_enemy_flag(entity bot)
1410 f = ctf_worldflaglist;
1415 if(CTF_DIFFTEAM(bot, f))
1422 else if(!bot.flagcarried)
1426 else if (CTF_DIFFTEAM(bot, f))
1428 f = f.ctf_worldflagnext;
1433 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1440 FOREACH_CLIENT(IS_PLAYER(it), {
1441 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1444 if(vdist(it.origin - org, <, tc_radius))
1453 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1456 head = ctf_worldflaglist;
1459 if (CTF_SAMETEAM(this, head))
1461 head = head.ctf_worldflagnext;
1464 navigation_routerating(this, head, ratingscale, 10000);
1468 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1471 head = ctf_worldflaglist;
1474 if (CTF_SAMETEAM(this, head))
1476 if (this.flagcarried)
1477 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1479 head = head.ctf_worldflagnext; // skip base if it has a different group
1484 head = head.ctf_worldflagnext;
1489 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1492 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1495 head = ctf_worldflaglist;
1500 if(CTF_DIFFTEAM(this, head))
1504 if(this.flagcarried)
1507 else if(!this.flagcarried)
1511 else if(CTF_DIFFTEAM(this, head))
1513 head = head.ctf_worldflagnext;
1516 navigation_routerating(this, head, ratingscale, 10000);
1519 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1521 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1523 if (!bot_waypoints_for_items)
1525 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1531 head = havocbot_ctf_find_enemy_flag(this);
1536 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1539 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1543 mf = havocbot_ctf_find_flag(this);
1545 if(mf.ctf_status == FLAG_BASE)
1549 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1552 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1555 head = ctf_worldflaglist;
1558 // flag is out in the field
1559 if(head.ctf_status != FLAG_BASE)
1560 if(head.tag_entity==NULL) // dropped
1564 if(vdist(org - head.origin, <, df_radius))
1565 navigation_routerating(this, head, ratingscale, 10000);
1568 navigation_routerating(this, head, ratingscale, 10000);
1571 head = head.ctf_worldflagnext;
1575 void havocbot_ctf_reset_role(entity this)
1577 float cdefense, cmiddle, coffense;
1584 if (this.flagcarried)
1586 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1590 mf = havocbot_ctf_find_flag(this);
1591 ef = havocbot_ctf_find_enemy_flag(this);
1593 // Retrieve stolen flag
1594 if(mf.ctf_status!=FLAG_BASE)
1596 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1600 // If enemy flag is taken go to the middle to intercept pursuers
1601 if(ef.ctf_status!=FLAG_BASE)
1603 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1607 // if there is no one else on the team switch to offense
1609 // don't check if this bot is a player since it isn't true when the bot is added to the server
1610 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1614 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1618 // Evaluate best position to take
1619 // Count mates on middle position
1620 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1622 // Count mates on defense position
1623 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1625 // Count mates on offense position
1626 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1628 if(cdefense<=coffense)
1629 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1630 else if(coffense<=cmiddle)
1631 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1633 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1636 bool havocbot_ctf_is_basewaypoint(entity item)
1638 if (item.classname != "waypoint")
1641 entity head = ctf_worldflaglist;
1644 if (item == head.bot_basewaypoint)
1646 head = head.ctf_worldflagnext;
1651 void havocbot_role_ctf_carrier(entity this)
1655 havocbot_ctf_reset_role(this);
1659 if (this.flagcarried == NULL)
1661 havocbot_ctf_reset_role(this);
1665 if (navigation_goalrating_timeout(this))
1667 navigation_goalrating_start(this);
1670 entity mf = havocbot_ctf_find_flag(this);
1671 vector base_org = mf.dropped_origin;
1672 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1674 havocbot_goalrating_ctf_enemybase(this, base_rating);
1676 havocbot_goalrating_ctf_ourbase(this, base_rating);
1678 // start collecting items very close to the bot but only inside of own base radius
1679 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1680 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1682 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1684 navigation_goalrating_end(this);
1686 navigation_goalrating_timeout_set(this);
1688 entity goal = this.goalentity;
1689 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1690 this.goalentity_lock_timeout = time + ((this.bot_aimtarg) ? 2 : 3);
1693 this.havocbot_cantfindflag = time + 10;
1694 else if (time > this.havocbot_cantfindflag)
1696 // Can't navigate to my own base, suicide!
1697 // TODO: drop it and wander around
1698 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1704 void havocbot_role_ctf_escort(entity this)
1710 havocbot_ctf_reset_role(this);
1714 if (this.flagcarried)
1716 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1720 // If enemy flag is back on the base switch to previous role
1721 ef = havocbot_ctf_find_enemy_flag(this);
1722 if(ef.ctf_status==FLAG_BASE)
1724 this.havocbot_role = this.havocbot_previous_role;
1725 this.havocbot_role_timeout = 0;
1728 if (ef.ctf_status == FLAG_DROPPED)
1730 navigation_goalrating_timeout_expire(this, 1);
1734 // If the flag carrier reached the base switch to defense
1735 mf = havocbot_ctf_find_flag(this);
1736 if(mf.ctf_status!=FLAG_BASE)
1737 if(vdist(ef.origin - mf.dropped_origin, <, 300))
1739 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1743 // Set the role timeout if necessary
1744 if (!this.havocbot_role_timeout)
1746 this.havocbot_role_timeout = time + random() * 30 + 60;
1749 // If nothing happened just switch to previous role
1750 if (time > this.havocbot_role_timeout)
1752 this.havocbot_role = this.havocbot_previous_role;
1753 this.havocbot_role_timeout = 0;
1757 // Chase the flag carrier
1758 if (navigation_goalrating_timeout(this))
1760 navigation_goalrating_start(this);
1762 havocbot_goalrating_ctf_enemyflag(this, 30000);
1763 havocbot_goalrating_ctf_ourstolenflag(this, 40000);
1764 havocbot_goalrating_items(this, 10000, this.origin, 10000);
1766 navigation_goalrating_end(this);
1768 navigation_goalrating_timeout_set(this);
1772 void havocbot_role_ctf_offense(entity this)
1779 havocbot_ctf_reset_role(this);
1783 if (this.flagcarried)
1785 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1790 mf = havocbot_ctf_find_flag(this);
1791 ef = havocbot_ctf_find_enemy_flag(this);
1794 if(mf.ctf_status!=FLAG_BASE)
1797 pos = mf.tag_entity.origin;
1801 // Try to get it if closer than the enemy base
1802 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1804 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1809 // Escort flag carrier
1810 if(ef.ctf_status!=FLAG_BASE)
1813 pos = ef.tag_entity.origin;
1817 if(vdist(pos - mf.dropped_origin, >, 700))
1819 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1824 // Set the role timeout if necessary
1825 if (!this.havocbot_role_timeout)
1826 this.havocbot_role_timeout = time + 120;
1828 if (time > this.havocbot_role_timeout)
1830 havocbot_ctf_reset_role(this);
1834 if (navigation_goalrating_timeout(this))
1836 navigation_goalrating_start(this);
1839 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1840 havocbot_goalrating_ctf_enemybase(this, 10000);
1841 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1843 navigation_goalrating_end(this);
1845 navigation_goalrating_timeout_set(this);
1849 // Retriever (temporary role):
1850 void havocbot_role_ctf_retriever(entity this)
1856 havocbot_ctf_reset_role(this);
1860 if (this.flagcarried)
1862 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1866 // If flag is back on the base switch to previous role
1867 mf = havocbot_ctf_find_flag(this);
1868 if(mf.ctf_status==FLAG_BASE)
1870 if (mf.enemy == this) // did this bot return the flag?
1871 navigation_goalrating_timeout_force(this);
1872 havocbot_ctf_reset_role(this);
1876 if (!this.havocbot_role_timeout)
1877 this.havocbot_role_timeout = time + 20;
1879 if (time > this.havocbot_role_timeout)
1881 havocbot_ctf_reset_role(this);
1885 if (navigation_goalrating_timeout(this))
1887 const float RT_RADIUS = 10000;
1889 navigation_goalrating_start(this);
1892 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1893 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1894 havocbot_goalrating_ctf_enemybase(this, 8000);
1895 entity ef = havocbot_ctf_find_enemy_flag(this);
1896 vector enemy_base_org = ef.dropped_origin;
1897 // start collecting items very close to the bot but only inside of enemy base radius
1898 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1899 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1900 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1902 navigation_goalrating_end(this);
1904 navigation_goalrating_timeout_set(this);
1908 void havocbot_role_ctf_middle(entity this)
1914 havocbot_ctf_reset_role(this);
1918 if (this.flagcarried)
1920 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1924 mf = havocbot_ctf_find_flag(this);
1925 if(mf.ctf_status!=FLAG_BASE)
1927 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1931 if (!this.havocbot_role_timeout)
1932 this.havocbot_role_timeout = time + 10;
1934 if (time > this.havocbot_role_timeout)
1936 havocbot_ctf_reset_role(this);
1940 if (navigation_goalrating_timeout(this))
1944 org = havocbot_middlepoint;
1945 org.z = this.origin.z;
1947 navigation_goalrating_start(this);
1949 havocbot_goalrating_ctf_ourstolenflag(this, 50000);
1950 havocbot_goalrating_ctf_droppedflags(this, 30000, this.origin, 10000);
1951 havocbot_goalrating_enemyplayers(this, 5000, org, havocbot_middlepoint_radius * 0.5);
1952 havocbot_goalrating_items(this, 5000, org, havocbot_middlepoint_radius * 0.5);
1953 havocbot_goalrating_items(this, 2500, this.origin, 10000);
1954 havocbot_goalrating_ctf_enemybase(this, 2500);
1956 navigation_goalrating_end(this);
1958 entity goal = this.goalentity;
1959 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1960 this.goalentity_lock_timeout = time + 2;
1962 navigation_goalrating_timeout_set(this);
1966 void havocbot_role_ctf_defense(entity this)
1972 havocbot_ctf_reset_role(this);
1976 if (this.flagcarried)
1978 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1982 // If own flag was captured
1983 mf = havocbot_ctf_find_flag(this);
1984 if(mf.ctf_status!=FLAG_BASE)
1986 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1990 if (!this.havocbot_role_timeout)
1991 this.havocbot_role_timeout = time + 30;
1993 if (time > this.havocbot_role_timeout)
1995 havocbot_ctf_reset_role(this);
1998 if (navigation_goalrating_timeout(this))
2000 vector org = mf.dropped_origin;
2002 navigation_goalrating_start(this);
2004 // if enemies are closer to our base, go there
2005 entity closestplayer = NULL;
2006 float distance, bestdistance = 10000;
2007 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2008 distance = vlen(org - it.origin);
2009 if(distance<bestdistance)
2012 bestdistance = distance;
2017 if(DIFF_TEAM(closestplayer, this))
2018 if(vdist(org - this.origin, >, 1000))
2019 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2020 havocbot_goalrating_ctf_ourbase(this, 30000);
2022 havocbot_goalrating_ctf_ourstolenflag(this, 20000);
2023 havocbot_goalrating_ctf_droppedflags(this, 20000, org, havocbot_middlepoint_radius);
2024 havocbot_goalrating_enemyplayers(this, 7500, org, havocbot_middlepoint_radius);
2025 havocbot_goalrating_items(this, 10000, org, havocbot_middlepoint_radius);
2026 havocbot_goalrating_items(this, 5000, this.origin, 10000);
2028 navigation_goalrating_end(this);
2030 navigation_goalrating_timeout_set(this);
2034 void havocbot_role_ctf_setrole(entity bot, int role)
2036 string s = "(null)";
2039 case HAVOCBOT_CTF_ROLE_CARRIER:
2041 bot.havocbot_role = havocbot_role_ctf_carrier;
2042 bot.havocbot_role_timeout = 0;
2043 bot.havocbot_cantfindflag = time + 10;
2044 if (bot.havocbot_previous_role != bot.havocbot_role)
2045 navigation_goalrating_timeout_force(bot);
2047 case HAVOCBOT_CTF_ROLE_DEFENSE:
2049 bot.havocbot_role = havocbot_role_ctf_defense;
2050 bot.havocbot_role_timeout = 0;
2052 case HAVOCBOT_CTF_ROLE_MIDDLE:
2054 bot.havocbot_role = havocbot_role_ctf_middle;
2055 bot.havocbot_role_timeout = 0;
2057 case HAVOCBOT_CTF_ROLE_OFFENSE:
2059 bot.havocbot_role = havocbot_role_ctf_offense;
2060 bot.havocbot_role_timeout = 0;
2062 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2064 bot.havocbot_previous_role = bot.havocbot_role;
2065 bot.havocbot_role = havocbot_role_ctf_retriever;
2066 bot.havocbot_role_timeout = time + 10;
2067 if (bot.havocbot_previous_role != bot.havocbot_role)
2068 navigation_goalrating_timeout_expire(bot, 2);
2070 case HAVOCBOT_CTF_ROLE_ESCORT:
2072 bot.havocbot_previous_role = bot.havocbot_role;
2073 bot.havocbot_role = havocbot_role_ctf_escort;
2074 bot.havocbot_role_timeout = time + 30;
2075 if (bot.havocbot_previous_role != bot.havocbot_role)
2076 navigation_goalrating_timeout_expire(bot, 2);
2079 LOG_TRACE(bot.netname, " switched to ", s);
2087 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2089 entity player = M_ARGV(0, entity);
2091 int t = 0, t2 = 0, t3 = 0;
2092 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)
2094 // initially clear items so they can be set as necessary later.
2095 STAT(CTF_FLAGSTATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2096 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2097 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2098 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2099 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2100 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2102 // scan through all the flags and notify the client about them
2103 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2105 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2106 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2107 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2108 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2109 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; }
2111 switch(flag.ctf_status)
2116 if((flag.owner == player) || (flag.pass_sender == player))
2117 STAT(CTF_FLAGSTATUS, player) |= t; // carrying: player is currently carrying the flag
2119 STAT(CTF_FLAGSTATUS, player) |= t2; // taken: someone else is carrying the flag
2124 STAT(CTF_FLAGSTATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2130 // item for stopping players from capturing the flag too often
2131 if(player.ctf_captureshielded)
2132 STAT(CTF_FLAGSTATUS, player) |= CTF_SHIELDED;
2135 STAT(CTF_FLAGSTATUS, player) |= CTF_STALEMATE;
2137 // update the health of the flag carrier waypointsprite
2138 if(player.wps_flagcarrier)
2139 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
2142 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2144 entity frag_attacker = M_ARGV(1, entity);
2145 entity frag_target = M_ARGV(2, entity);
2146 float frag_damage = M_ARGV(4, float);
2147 vector frag_force = M_ARGV(6, vector);
2149 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2151 if(frag_target == frag_attacker) // damage done to yourself
2153 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2154 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2156 else // damage done to everyone else
2158 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2159 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2162 M_ARGV(4, float) = frag_damage;
2163 M_ARGV(6, vector) = frag_force;
2165 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2167 if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id)))
2168 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2170 frag_target.wps_helpme_time = time;
2171 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2173 // todo: add notification for when flag carrier needs help?
2177 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2179 entity frag_attacker = M_ARGV(1, entity);
2180 entity frag_target = M_ARGV(2, entity);
2182 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2184 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2185 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2188 if(frag_target.flagcarried)
2190 entity tmp_entity = frag_target.flagcarried;
2191 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2192 tmp_entity.ctf_dropper = NULL;
2196 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2198 M_ARGV(2, float) = 0; // frag score
2199 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2202 void ctf_RemovePlayer(entity player)
2204 if(player.flagcarried)
2205 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2207 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2209 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2210 if(flag.pass_target == player) { flag.pass_target = NULL; }
2211 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2215 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2217 entity player = M_ARGV(0, entity);
2219 ctf_RemovePlayer(player);
2222 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2224 entity player = M_ARGV(0, entity);
2226 ctf_RemovePlayer(player);
2229 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2231 if(!autocvar_g_ctf_leaderboard)
2234 entity player = M_ARGV(0, entity);
2236 if(IS_REAL_CLIENT(player))
2238 for(int i = 1; i <= RANKINGS_CNT; ++i)
2240 race_SendRankings(i, 0, 0, MSG_ONE);
2245 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2247 if(!autocvar_g_ctf_leaderboard)
2250 entity player = M_ARGV(0, entity);
2252 if(CS(player).cvar_cl_allow_uidtracking == 1 && CS(player).cvar_cl_allow_uid2name == 1)
2254 if (!player.stored_netname)
2255 player.stored_netname = strzone(uid2name(player.crypto_idfp));
2256 if(player.stored_netname != player.netname)
2258 db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
2259 strcpy(player.stored_netname, player.netname);
2264 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2266 entity player = M_ARGV(0, entity);
2268 if(player.flagcarried)
2269 if(!autocvar_g_ctf_portalteleport)
2270 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2273 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2275 if(MUTATOR_RETURNVALUE || game_stopped) return;
2277 entity player = M_ARGV(0, entity);
2279 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2281 // pass the flag to a team mate
2282 if(autocvar_g_ctf_pass)
2284 entity head, closest_target = NULL;
2285 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2287 while(head) // find the closest acceptable target to pass to
2289 if(IS_PLAYER(head) && !IS_DEAD(head))
2290 if(head != player && SAME_TEAM(head, player))
2291 if(!head.speedrunning && !head.vehicle)
2293 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2294 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2295 vector passer_center = CENTER_OR_VIEWOFS(player);
2297 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2299 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2301 if(IS_BOT_CLIENT(head))
2303 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2304 ctf_Handle_Throw(head, player, DROP_PASS);
2308 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2309 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2311 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2314 else if(player.flagcarried && !head.flagcarried)
2318 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2319 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2320 { closest_target = head; }
2322 else { closest_target = head; }
2329 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2332 // throw the flag in front of you
2333 if(autocvar_g_ctf_throw && player.flagcarried)
2335 if(player.throw_count == -1)
2337 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2339 player.throw_prevtime = time;
2340 player.throw_count = 1;
2341 ctf_Handle_Throw(player, NULL, DROP_THROW);
2346 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2352 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2353 else { player.throw_count += 1; }
2354 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2356 player.throw_prevtime = time;
2357 ctf_Handle_Throw(player, NULL, DROP_THROW);
2364 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2366 entity player = M_ARGV(0, entity);
2368 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2370 player.wps_helpme_time = time;
2371 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2373 else // create a normal help me waypointsprite
2375 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2376 WaypointSprite_Ping(player.wps_helpme);
2382 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2384 entity player = M_ARGV(0, entity);
2385 entity veh = M_ARGV(1, entity);
2387 if(player.flagcarried)
2389 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2391 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2395 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2396 setattachment(player.flagcarried, veh, "");
2397 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2398 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2399 //player.flagcarried.angles = '0 0 0';
2405 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2407 entity player = M_ARGV(0, entity);
2409 if(player.flagcarried)
2411 setattachment(player.flagcarried, player, "");
2412 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2413 player.flagcarried.scale = FLAG_SCALE;
2414 player.flagcarried.angles = '0 0 0';
2415 player.flagcarried.nodrawtoclient = NULL;
2420 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2422 entity player = M_ARGV(0, entity);
2424 if(player.flagcarried)
2426 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2427 ctf_RespawnFlag(player.flagcarried);
2432 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2434 entity flag; // temporary entity for the search method
2436 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2438 switch(flag.ctf_status)
2443 // lock the flag, game is over
2444 set_movetype(flag, MOVETYPE_NONE);
2445 flag.takedamage = DAMAGE_NO;
2446 flag.solid = SOLID_NOT;
2447 flag.nextthink = false; // stop thinking
2449 //dprint("stopping the ", flag.netname, " from moving.\n");
2457 // do nothing for these flags
2464 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2466 entity bot = M_ARGV(0, entity);
2468 havocbot_ctf_reset_role(bot);
2472 MUTATOR_HOOKFUNCTION(ctf, CheckAllowedTeams)
2474 //M_ARGV(0, float) = ctf_teams;
2475 M_ARGV(1, string) = "ctf_team";
2479 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2481 entity spectatee = M_ARGV(0, entity);
2482 entity client = M_ARGV(1, entity);
2484 STAT(CTF_FLAGSTATUS, client) = STAT(CTF_FLAGSTATUS, spectatee);
2487 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2489 int record_page = M_ARGV(0, int);
2490 string ret_string = M_ARGV(1, string);
2492 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2494 if (MapInfo_Get_ByID(i))
2496 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2502 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2503 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2507 M_ARGV(1, string) = ret_string;
2510 bool superspec_Spectate(entity this, entity targ); // TODO
2511 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2512 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2514 entity player = M_ARGV(0, entity);
2515 string cmd_name = M_ARGV(1, string);
2516 int cmd_argc = M_ARGV(2, int);
2518 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2520 if(cmd_name == "followfc")
2532 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2533 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2534 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2535 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2539 FOREACH_CLIENT(IS_PLAYER(it), {
2540 if(it.flagcarried && (it.team == _team || _team == 0))
2543 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2544 continue; // already spectating this fc, try another
2545 return superspec_Spectate(player, it);
2550 superspec_msg("", "", player, "No active flag carrier\n", 1);
2555 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2557 entity frag_target = M_ARGV(0, entity);
2559 if(frag_target.flagcarried)
2560 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2568 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2569 CTF flag for team one (Red).
2571 "angle" Angle the flag will point (minus 90 degrees)...
2572 "model" model to use, note this needs red and blue as skins 0 and 1...
2573 "noise" sound played when flag is picked up...
2574 "noise1" sound played when flag is returned by a teammate...
2575 "noise2" sound played when flag is captured...
2576 "noise3" sound played when flag is lost in the field and respawns itself...
2577 "noise4" sound played when flag is dropped by a player...
2578 "noise5" sound played when flag touches the ground... */
2579 spawnfunc(item_flag_team1)
2581 if(!g_ctf) { delete(this); return; }
2583 ctf_FlagSetup(NUM_TEAM_1, this);
2586 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2587 CTF flag for team two (Blue).
2589 "angle" Angle the flag will point (minus 90 degrees)...
2590 "model" model to use, note this needs red and blue as skins 0 and 1...
2591 "noise" sound played when flag is picked up...
2592 "noise1" sound played when flag is returned by a teammate...
2593 "noise2" sound played when flag is captured...
2594 "noise3" sound played when flag is lost in the field and respawns itself...
2595 "noise4" sound played when flag is dropped by a player...
2596 "noise5" sound played when flag touches the ground... */
2597 spawnfunc(item_flag_team2)
2599 if(!g_ctf) { delete(this); return; }
2601 ctf_FlagSetup(NUM_TEAM_2, this);
2604 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2605 CTF flag for team three (Yellow).
2607 "angle" Angle the flag will point (minus 90 degrees)...
2608 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2609 "noise" sound played when flag is picked up...
2610 "noise1" sound played when flag is returned by a teammate...
2611 "noise2" sound played when flag is captured...
2612 "noise3" sound played when flag is lost in the field and respawns itself...
2613 "noise4" sound played when flag is dropped by a player...
2614 "noise5" sound played when flag touches the ground... */
2615 spawnfunc(item_flag_team3)
2617 if(!g_ctf) { delete(this); return; }
2619 ctf_FlagSetup(NUM_TEAM_3, this);
2622 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2623 CTF flag for team four (Pink).
2625 "angle" Angle the flag will point (minus 90 degrees)...
2626 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2627 "noise" sound played when flag is picked up...
2628 "noise1" sound played when flag is returned by a teammate...
2629 "noise2" sound played when flag is captured...
2630 "noise3" sound played when flag is lost in the field and respawns itself...
2631 "noise4" sound played when flag is dropped by a player...
2632 "noise5" sound played when flag touches the ground... */
2633 spawnfunc(item_flag_team4)
2635 if(!g_ctf) { delete(this); return; }
2637 ctf_FlagSetup(NUM_TEAM_4, this);
2640 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2643 "angle" Angle the flag will point (minus 90 degrees)...
2644 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2645 "noise" sound played when flag is picked up...
2646 "noise1" sound played when flag is returned by a teammate...
2647 "noise2" sound played when flag is captured...
2648 "noise3" sound played when flag is lost in the field and respawns itself...
2649 "noise4" sound played when flag is dropped by a player...
2650 "noise5" sound played when flag touches the ground... */
2651 spawnfunc(item_flag_neutral)
2653 if(!g_ctf) { delete(this); return; }
2654 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2656 ctf_FlagSetup(0, this);
2659 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2660 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2661 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.
2663 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2664 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2667 if(!g_ctf) { delete(this); return; }
2669 this.classname = "ctf_team";
2670 this.team = this.cnt + 1;
2673 // compatibility for quake maps
2674 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2675 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2676 spawnfunc(info_player_team1);
2677 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2678 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2679 spawnfunc(info_player_team2);
2680 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2681 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2683 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2684 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2686 // compatibility for wop maps
2687 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2688 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2689 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2690 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2691 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2692 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2700 void ctf_ScoreRules(int teams)
2702 CheckAllowedTeams(NULL);
2703 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2704 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2705 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2706 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2707 field(SP_CTF_PICKUPS, "pickups", 0);
2708 field(SP_CTF_FCKILLS, "fckills", 0);
2709 field(SP_CTF_RETURNS, "returns", 0);
2710 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2714 // code from here on is just to support maps that don't have flag and team entities
2715 void ctf_SpawnTeam (string teamname, int teamcolor)
2717 entity this = new_pure(ctf_team);
2718 this.netname = teamname;
2719 this.cnt = teamcolor - 1;
2720 this.spawnfunc_checked = true;
2721 this.team = teamcolor;
2724 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2729 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2731 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2732 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2734 switch(tmp_entity.team)
2736 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2737 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2738 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2739 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2741 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2744 havocbot_ctf_calculate_middlepoint();
2746 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2748 ctf_teams = 0; // so set the default red and blue teams
2749 BITSET_ASSIGN(ctf_teams, BIT(0));
2750 BITSET_ASSIGN(ctf_teams, BIT(1));
2753 //ctf_teams = bound(2, ctf_teams, 4);
2755 // if no teams are found, spawn defaults
2756 if(find(NULL, classname, "ctf_team") == NULL)
2758 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2759 if(ctf_teams & BIT(0))
2760 ctf_SpawnTeam("Red", NUM_TEAM_1);
2761 if(ctf_teams & BIT(1))
2762 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2763 if(ctf_teams & BIT(2))
2764 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2765 if(ctf_teams & BIT(3))
2766 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2769 ctf_ScoreRules(ctf_teams);
2772 void ctf_Initialize()
2774 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2776 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2777 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2778 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2780 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);