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, 2 * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
148 WaypointSprite_UpdateHealth(player.wps_flagcarrier, healtharmor_maxdamage(GetResource(player, RES_HEALTH), GetResource(player, RES_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
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 + eZ * FLAG_PASS_ARC_OFFSET_Z);
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 SetResourceExplicit(flag, RES_HEALTH, flag.max_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_health);
367 WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_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 tracebox(player.origin - FLAG_DROP_OFFSET, flag.m_mins, flag.m_maxs, player.origin + FLAG_DROP_OFFSET, MOVE_NOMONSTERS, flag);
444 setorigin(flag, trace_endpos);
445 flag.owner.flagcarried = NULL;
446 GameRules_scoring_vip(flag.owner, false);
448 flag.solid = SOLID_TRIGGER;
449 flag.ctf_dropper = player;
450 flag.ctf_droptime = time;
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);
489 navigation_dynamicgoal_set(flag, player);
495 flag.velocity = '0 0 0'; // do nothing
502 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);
503 ctf_Handle_Drop(flag, player, droptype);
504 navigation_dynamicgoal_set(flag, player);
509 // kill old waypointsprite
510 WaypointSprite_Ping(player.wps_flagcarrier);
511 WaypointSprite_Kill(player.wps_flagcarrier);
513 if(player.wps_enemyflagcarrier)
514 WaypointSprite_Kill(player.wps_enemyflagcarrier);
516 if(player.wps_flagreturn)
517 WaypointSprite_Kill(player.wps_flagreturn);
520 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
523 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
525 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
532 void nades_GiveBonus(entity player, float score);
534 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
536 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
537 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
538 entity player_team_flag = NULL, tmp_entity;
539 float old_time, new_time;
541 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
542 if(CTF_DIFFTEAM(player, flag)) { return; }
543 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)
545 if (toucher.goalentity == flag.bot_basewaypoint)
546 toucher.goalentity_lock_timeout = 0;
549 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
550 if(SAME_TEAM(tmp_entity, player))
552 player_team_flag = tmp_entity;
556 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
558 player.throw_prevtime = time;
559 player.throw_count = 0;
561 // messages and sounds
562 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
563 ctf_CaptureRecord(enemy_flag, player);
564 _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);
568 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
569 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
575 if(enemy_flag.score_capture || flag.score_capture)
576 pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
577 GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
579 if(enemy_flag.score_team_capture || flag.score_team_capture)
580 capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
581 GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
583 old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
584 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
585 if(!old_time || new_time < old_time)
586 GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
589 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
590 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
593 if(capturetype == CAPTURE_NORMAL)
595 WaypointSprite_Kill(player.wps_flagcarrier);
596 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
598 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
599 { GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist)); }
602 flag.enemy = toucher;
605 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
606 ctf_RespawnFlag(enemy_flag);
609 void ctf_Handle_Return(entity flag, entity player)
611 // messages and sounds
612 if(IS_MONSTER(player))
614 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
618 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
619 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
621 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
622 ctf_EventLog("return", flag.team, player);
625 if(IS_PLAYER(player))
627 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
628 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
630 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
633 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
637 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
638 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
639 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
643 if(player.flagcarried == flag)
644 WaypointSprite_Kill(player.wps_flagcarrier);
649 ctf_RespawnFlag(flag);
652 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
655 float pickup_dropped_score; // used to calculate dropped pickup score
657 // attach the flag to the player
659 player.flagcarried = flag;
660 GameRules_scoring_vip(player, true);
663 setattachment(flag, player.vehicle, "");
664 setorigin(flag, VEHICLE_FLAG_OFFSET);
665 flag.scale = VEHICLE_FLAG_SCALE;
669 setattachment(flag, player, "");
670 setorigin(flag, FLAG_CARRY_OFFSET);
674 set_movetype(flag, MOVETYPE_NONE);
675 flag.takedamage = DAMAGE_NO;
676 flag.solid = SOLID_NOT;
677 flag.angles = '0 0 0';
678 flag.ctf_status = FLAG_CARRY;
682 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
683 case PICKUP_DROPPED: SetResourceExplicit(flag, RES_HEALTH, flag.max_health); break; // reset health/return timelimit
687 // messages and sounds
688 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
690 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
692 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
693 else if(CTF_DIFFTEAM(player, flag))
694 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
696 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
698 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
701 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); });
704 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
705 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);
714 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
717 GameRules_scoring_add(player, CTF_PICKUPS, 1);
718 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
723 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
724 ctf_EventLog("steal", flag.team, player);
730 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);
731 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);
732 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
733 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
734 ctf_EventLog("pickup", flag.team, player);
742 if(pickuptype == PICKUP_BASE)
744 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
745 if((player.speedrunning) && (ctf_captimerecord))
746 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
750 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
753 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
754 ctf_FlagcarrierWaypoints(player);
755 WaypointSprite_Ping(player.wps_flagcarrier);
759 // ===================
760 // Main Flag Functions
761 // ===================
763 void ctf_CheckFlagReturn(entity flag, int returntype)
765 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
767 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH)); }
769 if((GetResource(flag, RES_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
774 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
776 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
777 case RETURN_SPEEDRUN:
778 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
779 case RETURN_NEEDKILL:
780 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
783 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
785 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
786 ctf_EventLog("returned", flag.team, NULL);
788 ctf_RespawnFlag(flag);
793 bool ctf_Stalemate_Customize(entity this, entity client)
795 // make spectators see what the player would see
796 entity e = WaypointSprite_getviewentity(client);
797 entity wp_owner = this.owner;
800 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
801 if(SAME_TEAM(wp_owner, e)) { return false; }
802 if(!IS_PLAYER(e)) { return false; }
807 void ctf_CheckStalemate()
810 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
813 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
815 // build list of stale flags
816 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
818 if(autocvar_g_ctf_stalemate)
819 if(tmp_entity.ctf_status != FLAG_BASE)
820 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
822 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
823 ctf_staleflaglist = tmp_entity;
825 switch(tmp_entity.team)
827 case NUM_TEAM_1: ++stale_red_flags; break;
828 case NUM_TEAM_2: ++stale_blue_flags; break;
829 case NUM_TEAM_3: ++stale_yellow_flags; break;
830 case NUM_TEAM_4: ++stale_pink_flags; break;
831 default: ++stale_neutral_flags; break;
837 stale_flags = (stale_neutral_flags >= 1);
839 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
841 if(ctf_oneflag && stale_flags == 1)
842 ctf_stalemate = true;
843 else if(stale_flags >= 2)
844 ctf_stalemate = true;
845 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
846 { ctf_stalemate = false; wpforenemy_announced = false; }
847 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
848 { ctf_stalemate = false; wpforenemy_announced = false; }
850 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
853 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
855 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
857 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);
858 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
859 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
863 if (!wpforenemy_announced)
865 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)); });
867 wpforenemy_announced = true;
872 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
874 if(ITEM_DAMAGE_NEEDKILL(deathtype))
876 if(autocvar_g_ctf_flag_return_damage_delay)
877 this.ctf_flagdamaged_byworld = true;
880 SetResourceExplicit(this, RES_HEALTH, 0);
881 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
885 if(autocvar_g_ctf_flag_return_damage)
887 // reduce health and check if it should be returned
888 TakeResource(this, RES_HEALTH, damage);
889 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
894 void ctf_FlagThink(entity this)
899 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
902 if(this == ctf_worldflaglist) // only for the first flag
903 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
906 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
907 LOG_TRACE("wtf the flag got squashed?");
908 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
909 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
910 setsize(this, this.m_mins, this.m_maxs);
914 switch(this.ctf_status)
918 if(autocvar_g_ctf_dropped_capture_radius)
920 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
921 if(tmp_entity.ctf_status == FLAG_DROPPED)
922 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
923 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
924 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
931 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
933 if(autocvar_g_ctf_flag_dropped_floatinwater)
935 vector midpoint = ((this.absmin + this.absmax) * 0.5);
936 if(pointcontents(midpoint) == CONTENT_WATER)
938 this.velocity = this.velocity * 0.5;
940 if (pointcontents(midpoint + eZ * FLAG_FLOAT_OFFSET_Z) == CONTENT_WATER)
941 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
943 { set_movetype(this, MOVETYPE_FLY); }
945 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
947 if(autocvar_g_ctf_flag_return_dropped)
949 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
951 SetResourceExplicit(this, RES_HEALTH, 0);
952 ctf_CheckFlagReturn(this, RETURN_DROPPED);
956 if(this.ctf_flagdamaged_byworld)
958 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
959 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
962 else if(autocvar_g_ctf_flag_return_time)
964 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
965 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
973 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
975 SetResourceExplicit(this, RES_HEALTH, 0);
976 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
978 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
979 ImpulseCommands(this.owner);
981 if(autocvar_g_ctf_stalemate)
983 if(time >= wpforenemy_nextthink)
985 ctf_CheckStalemate();
986 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
989 if(CTF_SAMETEAM(this, this.owner) && this.team)
991 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
992 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
993 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
994 ctf_Handle_Return(this, this.owner);
1001 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1002 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1003 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1005 if((this.pass_target == NULL)
1006 || (IS_DEAD(this.pass_target))
1007 || (this.pass_target.flagcarried)
1008 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1009 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1010 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1012 // give up, pass failed
1013 ctf_Handle_Drop(this, NULL, DROP_PASS);
1017 // still a viable target, go for it
1018 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1023 default: // this should never happen
1025 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1031 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1034 if(game_stopped) return;
1035 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1037 bool is_not_monster = (!IS_MONSTER(toucher));
1039 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1040 if(ITEM_TOUCH_NEEDKILL())
1042 if(!autocvar_g_ctf_flag_return_damage_delay)
1044 SetResourceExplicit(flag, RES_HEALTH, 0);
1045 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1047 if(!flag.ctf_flagdamaged_byworld) { return; }
1050 // special touch behaviors
1051 if(STAT(FROZEN, toucher)) { return; }
1052 else if(IS_VEHICLE(toucher))
1054 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1055 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1057 return; // do nothing
1059 else if(IS_MONSTER(toucher))
1061 if(!autocvar_g_ctf_allow_monster_touch)
1062 return; // do nothing
1064 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1066 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1068 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1069 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1070 flag.wait = time + FLAG_TOUCHRATE;
1074 else if(IS_DEAD(toucher)) { return; }
1076 switch(flag.ctf_status)
1082 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1083 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1084 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1085 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1087 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1088 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1089 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)
1091 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1092 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1094 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1095 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1101 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1102 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1103 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1104 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1110 LOG_TRACE("Someone touched a flag even though it was being carried?");
1116 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1118 if(DIFF_TEAM(toucher, flag.pass_sender))
1120 if(ctf_Immediate_Return_Allowed(flag, toucher))
1121 ctf_Handle_Return(flag, toucher);
1122 else if(is_not_monster && (!toucher.flagcarried))
1123 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1125 else if(!toucher.flagcarried)
1126 ctf_Handle_Retrieve(flag, toucher);
1133 .float last_respawn;
1134 void ctf_RespawnFlag(entity flag)
1136 // check for flag respawn being called twice in a row
1137 if(flag.last_respawn > time - 0.5)
1138 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1140 flag.last_respawn = time;
1142 // reset the player (if there is one)
1143 if((flag.owner) && (flag.owner.flagcarried == flag))
1145 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1146 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1147 WaypointSprite_Kill(flag.wps_flagcarrier);
1149 flag.owner.flagcarried = NULL;
1150 GameRules_scoring_vip(flag.owner, false);
1152 if(flag.speedrunning)
1153 ctf_FakeTimeLimit(flag.owner, -1);
1156 if((flag.owner) && (flag.owner.vehicle))
1157 flag.scale = FLAG_SCALE;
1159 if(flag.ctf_status == FLAG_DROPPED)
1160 { WaypointSprite_Kill(flag.wps_flagdropped); }
1163 setattachment(flag, NULL, "");
1164 setorigin(flag, flag.ctf_spawnorigin);
1166 //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1167 set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1168 flag.takedamage = DAMAGE_NO;
1169 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1170 flag.solid = SOLID_TRIGGER;
1171 flag.velocity = '0 0 0';
1172 flag.angles = flag.mangle;
1173 flag.flags = FL_ITEM | FL_NOTARGET;
1175 flag.ctf_status = FLAG_BASE;
1177 flag.pass_distance = 0;
1178 flag.pass_sender = NULL;
1179 flag.pass_target = NULL;
1180 flag.ctf_dropper = NULL;
1181 flag.ctf_pickuptime = 0;
1182 flag.ctf_droptime = 0;
1183 flag.ctf_flagdamaged_byworld = false;
1184 navigation_dynamicgoal_unset(flag);
1186 ctf_CheckStalemate();
1189 void ctf_Reset(entity this)
1191 if(this.owner && IS_PLAYER(this.owner))
1192 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1195 ctf_RespawnFlag(this);
1198 bool ctf_FlagBase_Customize(entity this, entity client)
1200 entity e = WaypointSprite_getviewentity(client);
1201 entity wp_owner = this.owner;
1202 entity flag = e.flagcarried;
1203 if(flag && CTF_SAMETEAM(e, flag))
1205 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1210 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1213 waypoint_spawnforitem_force(this, this.origin);
1214 navigation_dynamicgoal_init(this, true);
1220 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1221 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1222 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1223 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1224 default: basename = WP_FlagBaseNeutral; break;
1227 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1228 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1229 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1230 setcefc(wp, ctf_FlagBase_Customize);
1232 // captureshield setup
1233 ctf_CaptureShield_Spawn(this);
1238 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1241 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1242 ctf_worldflaglist = flag;
1244 setattachment(flag, NULL, "");
1246 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1247 flag.team = teamnumber;
1248 flag.classname = "item_flag_team";
1249 flag.target = "###item###"; // for finding the nearest item using findnearest
1250 flag.flags = FL_ITEM | FL_NOTARGET;
1251 IL_PUSH(g_items, flag);
1252 flag.solid = SOLID_TRIGGER;
1253 flag.takedamage = DAMAGE_NO;
1254 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1255 flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1256 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1257 flag.event_damage = ctf_FlagDamage;
1258 flag.pushable = true;
1259 flag.teleportable = TELEPORT_NORMAL;
1260 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1261 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1262 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1263 if(flag.damagedbycontents)
1264 IL_PUSH(g_damagedbycontents, flag);
1265 flag.velocity = '0 0 0';
1266 flag.mangle = flag.angles;
1267 flag.reset = ctf_Reset;
1268 settouch(flag, ctf_FlagTouch);
1269 setthink(flag, ctf_FlagThink);
1270 flag.nextthink = time + FLAG_THINKRATE;
1271 flag.ctf_status = FLAG_BASE;
1273 // crudely force them all to 0
1274 if(autocvar_g_ctf_score_ignore_fields)
1275 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1277 string teamname = Static_Team_ColorName_Lower(teamnumber);
1279 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1280 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1281 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1282 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnumber).eent_eff_name; }
1283 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnumber).eent_eff_name; }
1284 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnumber).eent_eff_name; }
1288 if(flag.s == "") flag.s = b; \
1289 precache_sound(flag.s);
1291 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnumber))))
1292 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnumber))))
1293 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnumber))))
1294 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnumber))))
1295 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1296 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1297 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1301 precache_model(flag.model);
1304 _setmodel(flag, flag.model); // precision set below
1305 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1306 flag.m_mins = flag.mins; // store these for squash checks
1307 flag.m_maxs = flag.maxs;
1308 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1310 if(autocvar_g_ctf_flag_glowtrails)
1314 case NUM_TEAM_1: flag.glow_color = 251; break;
1315 case NUM_TEAM_2: flag.glow_color = 210; break;
1316 case NUM_TEAM_3: flag.glow_color = 110; break;
1317 case NUM_TEAM_4: flag.glow_color = 145; break;
1318 default: flag.glow_color = 254; break;
1320 flag.glow_size = 25;
1321 flag.glow_trail = 1;
1324 flag.effects |= EF_LOWPRECISION;
1325 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1326 if(autocvar_g_ctf_dynamiclights)
1330 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1331 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1332 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1333 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1334 default: flag.effects |= EF_DIMLIGHT; break;
1339 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1341 flag.dropped_origin = flag.origin;
1342 flag.noalign = true;
1343 set_movetype(flag, MOVETYPE_NONE);
1345 else // drop to floor, automatically find a platform and set that as spawn origin
1347 flag.noalign = false;
1349 set_movetype(flag, MOVETYPE_NONE);
1352 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1360 // NOTE: LEGACY CODE, needs to be re-written!
1362 void havocbot_ctf_calculate_middlepoint()
1366 vector fo = '0 0 0';
1369 f = ctf_worldflaglist;
1374 f = f.ctf_worldflagnext;
1380 havocbot_middlepoint = s / n;
1381 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1383 havocbot_symmetry_axis_m = 0;
1384 havocbot_symmetry_axis_q = 0;
1387 // for symmetrical editing of waypoints
1388 entity f1 = ctf_worldflaglist;
1389 entity f2 = f1.ctf_worldflagnext;
1390 float m = -(f1.origin.y - f2.origin.y) / (f1.origin.x - f2.origin.x);
1391 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1392 havocbot_symmetry_axis_m = m;
1393 havocbot_symmetry_axis_q = q;
1395 havocbot_symmetry_origin_order = n;
1399 entity havocbot_ctf_find_flag(entity bot)
1402 f = ctf_worldflaglist;
1405 if (CTF_SAMETEAM(bot, f))
1407 f = f.ctf_worldflagnext;
1412 entity havocbot_ctf_find_enemy_flag(entity bot)
1415 f = ctf_worldflaglist;
1420 if(CTF_DIFFTEAM(bot, f))
1427 else if(!bot.flagcarried)
1431 else if (CTF_DIFFTEAM(bot, f))
1433 f = f.ctf_worldflagnext;
1438 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1445 FOREACH_CLIENT(IS_PLAYER(it), {
1446 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1449 if(vdist(it.origin - org, <, tc_radius))
1458 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1461 head = ctf_worldflaglist;
1464 if (CTF_SAMETEAM(this, head))
1466 head = head.ctf_worldflagnext;
1469 navigation_routerating(this, head, ratingscale, 10000);
1473 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1476 head = ctf_worldflaglist;
1479 if (CTF_SAMETEAM(this, head))
1481 if (this.flagcarried)
1482 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1484 head = head.ctf_worldflagnext; // skip base if it has a different group
1489 head = head.ctf_worldflagnext;
1494 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1497 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1500 head = ctf_worldflaglist;
1505 if(CTF_DIFFTEAM(this, head))
1509 if(this.flagcarried)
1512 else if(!this.flagcarried)
1516 else if(CTF_DIFFTEAM(this, head))
1518 head = head.ctf_worldflagnext;
1522 if (head.ctf_status == FLAG_CARRY)
1524 // adjust rating of our flag carrier depending on his health
1525 head = head.tag_entity;
1526 float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1527 ratingscale += ratingscale * f * 0.1;
1529 navigation_routerating(this, head, ratingscale, 10000);
1533 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1535 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1537 if (!bot_waypoints_for_items)
1539 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1545 head = havocbot_ctf_find_enemy_flag(this);
1550 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1553 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1557 mf = havocbot_ctf_find_flag(this);
1559 if(mf.ctf_status == FLAG_BASE)
1563 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1566 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1569 head = ctf_worldflaglist;
1572 // flag is out in the field
1573 if(head.ctf_status != FLAG_BASE)
1574 if(head.tag_entity==NULL) // dropped
1578 if(vdist(org - head.origin, <, df_radius))
1579 navigation_routerating(this, head, ratingscale, 10000);
1582 navigation_routerating(this, head, ratingscale, 10000);
1585 head = head.ctf_worldflagnext;
1589 void havocbot_ctf_reset_role(entity this)
1591 float cdefense, cmiddle, coffense;
1598 if (this.flagcarried)
1600 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1604 mf = havocbot_ctf_find_flag(this);
1605 ef = havocbot_ctf_find_enemy_flag(this);
1607 // Retrieve stolen flag
1608 if(mf.ctf_status!=FLAG_BASE)
1610 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1614 // If enemy flag is taken go to the middle to intercept pursuers
1615 if(ef.ctf_status!=FLAG_BASE)
1617 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1621 // if there is no one else on the team switch to offense
1623 // don't check if this bot is a player since it isn't true when the bot is added to the server
1624 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1628 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1631 else if (time < CS(this).jointime + 1)
1633 // if bots spawn all at once set good default roles
1636 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1639 else if (count == 2)
1641 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1646 // Evaluate best position to take
1647 // Count mates on middle position
1648 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1650 // Count mates on defense position
1651 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1653 // Count mates on offense position
1654 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1656 if(cdefense<=coffense)
1657 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1658 else if(coffense<=cmiddle)
1659 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1661 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1663 // if bots spawn all at once assign them a more appropriated role after a while
1664 if (time < CS(this).jointime + 1 && count > 2)
1665 this.havocbot_role_timeout = time + 10 + random() * 10;
1668 bool havocbot_ctf_is_basewaypoint(entity item)
1670 if (item.classname != "waypoint")
1673 entity head = ctf_worldflaglist;
1676 if (item == head.bot_basewaypoint)
1678 head = head.ctf_worldflagnext;
1683 void havocbot_role_ctf_carrier(entity this)
1687 havocbot_ctf_reset_role(this);
1691 if (this.flagcarried == NULL)
1693 havocbot_ctf_reset_role(this);
1697 if (navigation_goalrating_timeout(this))
1699 navigation_goalrating_start(this);
1702 entity mf = havocbot_ctf_find_flag(this);
1703 vector base_org = mf.dropped_origin;
1704 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1706 havocbot_goalrating_ctf_enemybase(this, base_rating);
1708 havocbot_goalrating_ctf_ourbase(this, base_rating);
1710 // start collecting items very close to the bot but only inside of own base radius
1711 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1712 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1714 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1716 navigation_goalrating_end(this);
1718 navigation_goalrating_timeout_set(this);
1720 entity goal = this.goalentity;
1721 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1722 this.goalentity_lock_timeout = time + ((this.bot_aimtarg) ? 2 : 3);
1725 this.havocbot_cantfindflag = time + 10;
1726 else if (time > this.havocbot_cantfindflag)
1728 // Can't navigate to my own base, suicide!
1729 // TODO: drop it and wander around
1730 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1736 void havocbot_role_ctf_escort(entity this)
1742 havocbot_ctf_reset_role(this);
1746 if (this.flagcarried)
1748 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1752 // If enemy flag is back on the base switch to previous role
1753 ef = havocbot_ctf_find_enemy_flag(this);
1754 if(ef.ctf_status==FLAG_BASE)
1756 this.havocbot_role = this.havocbot_previous_role;
1757 this.havocbot_role_timeout = 0;
1760 if (ef.ctf_status == FLAG_DROPPED)
1762 navigation_goalrating_timeout_expire(this, 1);
1766 // If the flag carrier reached the base switch to defense
1767 mf = havocbot_ctf_find_flag(this);
1768 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1770 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1774 // Set the role timeout if necessary
1775 if (!this.havocbot_role_timeout)
1777 this.havocbot_role_timeout = time + random() * 30 + 60;
1780 // If nothing happened just switch to previous role
1781 if (time > this.havocbot_role_timeout)
1783 this.havocbot_role = this.havocbot_previous_role;
1784 this.havocbot_role_timeout = 0;
1788 // Chase the flag carrier
1789 if (navigation_goalrating_timeout(this))
1791 navigation_goalrating_start(this);
1794 havocbot_goalrating_ctf_enemyflag(this, 10000);
1795 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1796 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1798 navigation_goalrating_end(this);
1800 navigation_goalrating_timeout_set(this);
1804 void havocbot_role_ctf_offense(entity this)
1811 havocbot_ctf_reset_role(this);
1815 if (this.flagcarried)
1817 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1822 mf = havocbot_ctf_find_flag(this);
1823 ef = havocbot_ctf_find_enemy_flag(this);
1826 if(mf.ctf_status!=FLAG_BASE)
1829 pos = mf.tag_entity.origin;
1833 // Try to get it if closer than the enemy base
1834 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1836 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1841 // Escort flag carrier
1842 if(ef.ctf_status!=FLAG_BASE)
1845 pos = ef.tag_entity.origin;
1849 if(vdist(pos - mf.dropped_origin, >, 700))
1851 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1856 // Set the role timeout if necessary
1857 if (!this.havocbot_role_timeout)
1858 this.havocbot_role_timeout = time + 120;
1860 if (time > this.havocbot_role_timeout)
1862 havocbot_ctf_reset_role(this);
1866 if (navigation_goalrating_timeout(this))
1868 navigation_goalrating_start(this);
1871 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1872 havocbot_goalrating_ctf_enemybase(this, 10000);
1873 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1875 navigation_goalrating_end(this);
1877 navigation_goalrating_timeout_set(this);
1881 // Retriever (temporary role):
1882 void havocbot_role_ctf_retriever(entity this)
1888 havocbot_ctf_reset_role(this);
1892 if (this.flagcarried)
1894 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1898 // If flag is back on the base switch to previous role
1899 mf = havocbot_ctf_find_flag(this);
1900 if(mf.ctf_status==FLAG_BASE)
1902 if (mf.enemy == this) // did this bot return the flag?
1903 navigation_goalrating_timeout_force(this);
1904 havocbot_ctf_reset_role(this);
1908 if (!this.havocbot_role_timeout)
1909 this.havocbot_role_timeout = time + 20;
1911 if (time > this.havocbot_role_timeout)
1913 havocbot_ctf_reset_role(this);
1917 if (navigation_goalrating_timeout(this))
1919 const float RT_RADIUS = 10000;
1921 navigation_goalrating_start(this);
1924 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1925 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1926 havocbot_goalrating_ctf_enemybase(this, 8000);
1927 entity ef = havocbot_ctf_find_enemy_flag(this);
1928 vector enemy_base_org = ef.dropped_origin;
1929 // start collecting items very close to the bot but only inside of enemy base radius
1930 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1931 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1932 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1934 navigation_goalrating_end(this);
1936 navigation_goalrating_timeout_set(this);
1940 void havocbot_role_ctf_middle(entity this)
1946 havocbot_ctf_reset_role(this);
1950 if (this.flagcarried)
1952 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1956 mf = havocbot_ctf_find_flag(this);
1957 if(mf.ctf_status!=FLAG_BASE)
1959 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1963 if (!this.havocbot_role_timeout)
1964 this.havocbot_role_timeout = time + 10;
1966 if (time > this.havocbot_role_timeout)
1968 havocbot_ctf_reset_role(this);
1972 if (navigation_goalrating_timeout(this))
1976 org = havocbot_middlepoint;
1977 org.z = this.origin.z;
1979 navigation_goalrating_start(this);
1982 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
1983 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
1984 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
1985 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
1986 havocbot_goalrating_items(this, 18000, this.origin, 10000);
1987 havocbot_goalrating_ctf_enemybase(this, 3000);
1989 navigation_goalrating_end(this);
1991 entity goal = this.goalentity;
1992 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1993 this.goalentity_lock_timeout = time + 2;
1995 navigation_goalrating_timeout_set(this);
1999 void havocbot_role_ctf_defense(entity this)
2005 havocbot_ctf_reset_role(this);
2009 if (this.flagcarried)
2011 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2015 // If own flag was captured
2016 mf = havocbot_ctf_find_flag(this);
2017 if(mf.ctf_status!=FLAG_BASE)
2019 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2023 if (!this.havocbot_role_timeout)
2024 this.havocbot_role_timeout = time + 30;
2026 if (time > this.havocbot_role_timeout)
2028 havocbot_ctf_reset_role(this);
2031 if (navigation_goalrating_timeout(this))
2033 vector org = mf.dropped_origin;
2035 navigation_goalrating_start(this);
2037 // if enemies are closer to our base, go there
2038 entity closestplayer = NULL;
2039 float distance, bestdistance = 10000;
2040 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2041 distance = vlen(org - it.origin);
2042 if(distance<bestdistance)
2045 bestdistance = distance;
2051 if(DIFF_TEAM(closestplayer, this))
2052 if(vdist(org - this.origin, >, 1000))
2053 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2054 havocbot_goalrating_ctf_ourbase(this, 10000);
2056 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2057 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2058 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2059 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2060 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2062 navigation_goalrating_end(this);
2064 navigation_goalrating_timeout_set(this);
2068 void havocbot_role_ctf_setrole(entity bot, int role)
2070 string s = "(null)";
2073 case HAVOCBOT_CTF_ROLE_CARRIER:
2075 bot.havocbot_role = havocbot_role_ctf_carrier;
2076 bot.havocbot_role_timeout = 0;
2077 bot.havocbot_cantfindflag = time + 10;
2078 if (bot.havocbot_previous_role != bot.havocbot_role)
2079 navigation_goalrating_timeout_force(bot);
2081 case HAVOCBOT_CTF_ROLE_DEFENSE:
2083 bot.havocbot_role = havocbot_role_ctf_defense;
2084 bot.havocbot_role_timeout = 0;
2086 case HAVOCBOT_CTF_ROLE_MIDDLE:
2088 bot.havocbot_role = havocbot_role_ctf_middle;
2089 bot.havocbot_role_timeout = 0;
2091 case HAVOCBOT_CTF_ROLE_OFFENSE:
2093 bot.havocbot_role = havocbot_role_ctf_offense;
2094 bot.havocbot_role_timeout = 0;
2096 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2098 bot.havocbot_previous_role = bot.havocbot_role;
2099 bot.havocbot_role = havocbot_role_ctf_retriever;
2100 bot.havocbot_role_timeout = time + 10;
2101 if (bot.havocbot_previous_role != bot.havocbot_role)
2102 navigation_goalrating_timeout_expire(bot, 2);
2104 case HAVOCBOT_CTF_ROLE_ESCORT:
2106 bot.havocbot_previous_role = bot.havocbot_role;
2107 bot.havocbot_role = havocbot_role_ctf_escort;
2108 bot.havocbot_role_timeout = time + 30;
2109 if (bot.havocbot_previous_role != bot.havocbot_role)
2110 navigation_goalrating_timeout_expire(bot, 2);
2113 LOG_TRACE(bot.netname, " switched to ", s);
2121 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2123 entity player = M_ARGV(0, entity);
2125 int t = 0, t2 = 0, t3 = 0;
2126 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)
2128 // initially clear items so they can be set as necessary later.
2129 STAT(CTF_FLAGSTATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2130 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2131 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2132 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2133 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2134 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2136 // scan through all the flags and notify the client about them
2137 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2139 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2140 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2141 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2142 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2143 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; }
2145 switch(flag.ctf_status)
2150 if((flag.owner == player) || (flag.pass_sender == player))
2151 STAT(CTF_FLAGSTATUS, player) |= t; // carrying: player is currently carrying the flag
2153 STAT(CTF_FLAGSTATUS, player) |= t2; // taken: someone else is carrying the flag
2158 STAT(CTF_FLAGSTATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2164 // item for stopping players from capturing the flag too often
2165 if(player.ctf_captureshielded)
2166 STAT(CTF_FLAGSTATUS, player) |= CTF_SHIELDED;
2169 STAT(CTF_FLAGSTATUS, player) |= CTF_STALEMATE;
2171 // update the health of the flag carrier waypointsprite
2172 if(player.wps_flagcarrier)
2173 WaypointSprite_UpdateHealth(player.wps_flagcarrier, healtharmor_maxdamage(GetResource(player, RES_HEALTH), GetResource(player, RES_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
2176 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2178 entity frag_attacker = M_ARGV(1, entity);
2179 entity frag_target = M_ARGV(2, entity);
2180 float frag_damage = M_ARGV(4, float);
2181 vector frag_force = M_ARGV(6, vector);
2183 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2185 if(frag_target == frag_attacker) // damage done to yourself
2187 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2188 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2190 else // damage done to everyone else
2192 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2193 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2196 M_ARGV(4, float) = frag_damage;
2197 M_ARGV(6, vector) = frag_force;
2199 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2201 if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > healtharmor_maxdamage(GetResource(frag_target, RES_HEALTH), GetResource(frag_target, RES_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x
2202 && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2204 frag_target.wps_helpme_time = time;
2205 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2207 // todo: add notification for when flag carrier needs help?
2211 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2213 entity frag_attacker = M_ARGV(1, entity);
2214 entity frag_target = M_ARGV(2, entity);
2216 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2218 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2219 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2222 if(frag_target.flagcarried)
2224 entity tmp_entity = frag_target.flagcarried;
2225 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2226 tmp_entity.ctf_dropper = NULL;
2230 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2232 M_ARGV(2, float) = 0; // frag score
2233 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2236 void ctf_RemovePlayer(entity player)
2238 if(player.flagcarried)
2239 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2241 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2243 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2244 if(flag.pass_target == player) { flag.pass_target = NULL; }
2245 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2249 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2251 entity player = M_ARGV(0, entity);
2253 ctf_RemovePlayer(player);
2256 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2258 entity player = M_ARGV(0, entity);
2260 ctf_RemovePlayer(player);
2263 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2265 if(!autocvar_g_ctf_leaderboard)
2268 entity player = M_ARGV(0, entity);
2270 if(IS_REAL_CLIENT(player))
2272 int m = min(RANKINGS_CNT, autocvar_g_cts_send_rankings_cnt);
2273 race_send_rankings_cnt(MSG_ONE);
2274 for (int i = 1; i <= m; ++i)
2276 race_SendRankings(i, 0, 0, MSG_ONE);
2281 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2283 if(!autocvar_g_ctf_leaderboard)
2286 entity player = M_ARGV(0, entity);
2288 if(CS(player).cvar_cl_allow_uidtracking == 1 && CS(player).cvar_cl_allow_uid2name == 1)
2290 if (!player.stored_netname)
2291 player.stored_netname = strzone(uid2name(player.crypto_idfp));
2292 if(player.stored_netname != player.netname)
2294 db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
2295 strcpy(player.stored_netname, player.netname);
2300 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2302 entity player = M_ARGV(0, entity);
2304 if(player.flagcarried)
2305 if(!autocvar_g_ctf_portalteleport)
2306 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2309 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2311 if(MUTATOR_RETURNVALUE || game_stopped) return;
2313 entity player = M_ARGV(0, entity);
2315 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2317 // pass the flag to a team mate
2318 if(autocvar_g_ctf_pass)
2320 entity head, closest_target = NULL;
2321 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2323 while(head) // find the closest acceptable target to pass to
2325 if(IS_PLAYER(head) && !IS_DEAD(head))
2326 if(head != player && SAME_TEAM(head, player))
2327 if(!head.speedrunning && !head.vehicle)
2329 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2330 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2331 vector passer_center = CENTER_OR_VIEWOFS(player);
2333 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2335 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2337 if(IS_BOT_CLIENT(head))
2339 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2340 ctf_Handle_Throw(head, player, DROP_PASS);
2344 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2345 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2347 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2350 else if(player.flagcarried && !head.flagcarried)
2354 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2355 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2356 { closest_target = head; }
2358 else { closest_target = head; }
2365 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2368 // throw the flag in front of you
2369 if(autocvar_g_ctf_throw && player.flagcarried)
2371 if(player.throw_count == -1)
2373 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2375 player.throw_prevtime = time;
2376 player.throw_count = 1;
2377 ctf_Handle_Throw(player, NULL, DROP_THROW);
2382 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2388 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2389 else { player.throw_count += 1; }
2390 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2392 player.throw_prevtime = time;
2393 ctf_Handle_Throw(player, NULL, DROP_THROW);
2400 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2402 entity player = M_ARGV(0, entity);
2404 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2406 player.wps_helpme_time = time;
2407 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2409 else // create a normal help me waypointsprite
2411 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2412 WaypointSprite_Ping(player.wps_helpme);
2418 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2420 entity player = M_ARGV(0, entity);
2421 entity veh = M_ARGV(1, entity);
2423 if(player.flagcarried)
2425 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2427 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2431 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2432 setattachment(player.flagcarried, veh, "");
2433 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2434 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2435 //player.flagcarried.angles = '0 0 0';
2441 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2443 entity player = M_ARGV(0, entity);
2445 if(player.flagcarried)
2447 setattachment(player.flagcarried, player, "");
2448 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2449 player.flagcarried.scale = FLAG_SCALE;
2450 player.flagcarried.angles = '0 0 0';
2451 player.flagcarried.nodrawtoclient = NULL;
2456 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2458 entity player = M_ARGV(0, entity);
2460 if(player.flagcarried)
2462 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2463 ctf_RespawnFlag(player.flagcarried);
2468 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2470 entity flag; // temporary entity for the search method
2472 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2474 switch(flag.ctf_status)
2479 // lock the flag, game is over
2480 set_movetype(flag, MOVETYPE_NONE);
2481 flag.takedamage = DAMAGE_NO;
2482 flag.solid = SOLID_NOT;
2483 flag.nextthink = false; // stop thinking
2485 //dprint("stopping the ", flag.netname, " from moving.\n");
2493 // do nothing for these flags
2500 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2502 entity bot = M_ARGV(0, entity);
2504 havocbot_ctf_reset_role(bot);
2508 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2510 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);
2597 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2599 entity player = M_ARGV(0, entity);
2600 if(player.flagcarried)
2601 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2609 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2610 CTF flag for team one (Red).
2612 "angle" Angle the flag will point (minus 90 degrees)...
2613 "model" model to use, note this needs red and blue as skins 0 and 1...
2614 "noise" sound played when flag is picked up...
2615 "noise1" sound played when flag is returned by a teammate...
2616 "noise2" sound played when flag is captured...
2617 "noise3" sound played when flag is lost in the field and respawns itself...
2618 "noise4" sound played when flag is dropped by a player...
2619 "noise5" sound played when flag touches the ground... */
2620 spawnfunc(item_flag_team1)
2622 if(!g_ctf) { delete(this); return; }
2624 ctf_FlagSetup(NUM_TEAM_1, this);
2627 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2628 CTF flag for team two (Blue).
2630 "angle" Angle the flag will point (minus 90 degrees)...
2631 "model" model to use, note this needs red and blue as skins 0 and 1...
2632 "noise" sound played when flag is picked up...
2633 "noise1" sound played when flag is returned by a teammate...
2634 "noise2" sound played when flag is captured...
2635 "noise3" sound played when flag is lost in the field and respawns itself...
2636 "noise4" sound played when flag is dropped by a player...
2637 "noise5" sound played when flag touches the ground... */
2638 spawnfunc(item_flag_team2)
2640 if(!g_ctf) { delete(this); return; }
2642 ctf_FlagSetup(NUM_TEAM_2, this);
2645 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2646 CTF flag for team three (Yellow).
2648 "angle" Angle the flag will point (minus 90 degrees)...
2649 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2650 "noise" sound played when flag is picked up...
2651 "noise1" sound played when flag is returned by a teammate...
2652 "noise2" sound played when flag is captured...
2653 "noise3" sound played when flag is lost in the field and respawns itself...
2654 "noise4" sound played when flag is dropped by a player...
2655 "noise5" sound played when flag touches the ground... */
2656 spawnfunc(item_flag_team3)
2658 if(!g_ctf) { delete(this); return; }
2660 ctf_FlagSetup(NUM_TEAM_3, this);
2663 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2664 CTF flag for team four (Pink).
2666 "angle" Angle the flag will point (minus 90 degrees)...
2667 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2668 "noise" sound played when flag is picked up...
2669 "noise1" sound played when flag is returned by a teammate...
2670 "noise2" sound played when flag is captured...
2671 "noise3" sound played when flag is lost in the field and respawns itself...
2672 "noise4" sound played when flag is dropped by a player...
2673 "noise5" sound played when flag touches the ground... */
2674 spawnfunc(item_flag_team4)
2676 if(!g_ctf) { delete(this); return; }
2678 ctf_FlagSetup(NUM_TEAM_4, this);
2681 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2684 "angle" Angle the flag will point (minus 90 degrees)...
2685 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2686 "noise" sound played when flag is picked up...
2687 "noise1" sound played when flag is returned by a teammate...
2688 "noise2" sound played when flag is captured...
2689 "noise3" sound played when flag is lost in the field and respawns itself...
2690 "noise4" sound played when flag is dropped by a player...
2691 "noise5" sound played when flag touches the ground... */
2692 spawnfunc(item_flag_neutral)
2694 if(!g_ctf) { delete(this); return; }
2695 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2697 ctf_FlagSetup(0, this);
2700 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2701 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2702 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.
2704 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2705 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2708 if(!g_ctf) { delete(this); return; }
2710 this.classname = "ctf_team";
2711 this.team = this.cnt + 1;
2714 // compatibility for quake maps
2715 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2716 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2717 spawnfunc(info_player_team1);
2718 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2719 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2720 spawnfunc(info_player_team2);
2721 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2722 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2724 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2725 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2727 // compatibility for wop maps
2728 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2729 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2730 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2731 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2732 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2733 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2741 void ctf_ScoreRules(int teams)
2743 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2744 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2745 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2746 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2747 field(SP_CTF_PICKUPS, "pickups", 0);
2748 field(SP_CTF_FCKILLS, "fckills", 0);
2749 field(SP_CTF_RETURNS, "returns", 0);
2750 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2754 // code from here on is just to support maps that don't have flag and team entities
2755 void ctf_SpawnTeam (string teamname, int teamcolor)
2757 entity this = new_pure(ctf_team);
2758 this.netname = teamname;
2759 this.cnt = teamcolor - 1;
2760 this.spawnfunc_checked = true;
2761 this.team = teamcolor;
2764 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2769 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2771 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2772 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2774 switch(tmp_entity.team)
2776 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2777 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2778 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2779 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2781 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2784 havocbot_ctf_calculate_middlepoint();
2786 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2788 ctf_teams = 0; // so set the default red and blue teams
2789 BITSET_ASSIGN(ctf_teams, BIT(0));
2790 BITSET_ASSIGN(ctf_teams, BIT(1));
2793 //ctf_teams = bound(2, ctf_teams, 4);
2795 // if no teams are found, spawn defaults
2796 if(find(NULL, classname, "ctf_team") == NULL)
2798 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2799 if(ctf_teams & BIT(0))
2800 ctf_SpawnTeam("Red", NUM_TEAM_1);
2801 if(ctf_teams & BIT(1))
2802 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2803 if(ctf_teams & BIT(2))
2804 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2805 if(ctf_teams & BIT(3))
2806 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2809 ctf_ScoreRules(ctf_teams);
2812 void ctf_Initialize()
2814 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2816 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2817 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2818 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2820 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);