3 #include <common/effects/all.qh>
4 #include <common/mapobjects/teleporters.qh>
5 #include <common/mapobjects/triggers.qh>
6 #include <common/mutators/mutator/powerups/_mod.qh>
7 #include <common/vehicles/all.qh>
8 #include <server/command/vote.qh>
9 #include <server/client.qh>
10 #include <server/gamelog.qh>
11 #include <server/intermission.qh>
12 #include <server/damage.qh>
13 #include <server/world.qh>
14 #include <server/items/items.qh>
15 #include <server/race.qh>
16 #include <server/teamplay.qh>
18 #include <lib/warpzone/common.qh>
20 bool autocvar_g_ctf_allow_vehicle_carry;
21 bool autocvar_g_ctf_allow_vehicle_touch;
22 bool autocvar_g_ctf_allow_monster_touch;
23 bool autocvar_g_ctf_throw;
24 float autocvar_g_ctf_throw_angle_max;
25 float autocvar_g_ctf_throw_angle_min;
26 int autocvar_g_ctf_throw_punish_count;
27 float autocvar_g_ctf_throw_punish_delay;
28 float autocvar_g_ctf_throw_punish_time;
29 float autocvar_g_ctf_throw_strengthmultiplier;
30 float autocvar_g_ctf_throw_velocity_forward;
31 float autocvar_g_ctf_throw_velocity_up;
32 float autocvar_g_ctf_drop_velocity_up;
33 float autocvar_g_ctf_drop_velocity_side;
34 bool autocvar_g_ctf_oneflag_reverse;
35 bool autocvar_g_ctf_portalteleport;
36 bool autocvar_g_ctf_pass;
37 float autocvar_g_ctf_pass_arc;
38 float autocvar_g_ctf_pass_arc_max;
39 float autocvar_g_ctf_pass_directional_max;
40 float autocvar_g_ctf_pass_directional_min;
41 float autocvar_g_ctf_pass_radius;
42 float autocvar_g_ctf_pass_wait;
43 bool autocvar_g_ctf_pass_request;
44 float autocvar_g_ctf_pass_turnrate;
45 float autocvar_g_ctf_pass_timelimit;
46 float autocvar_g_ctf_pass_velocity;
47 bool autocvar_g_ctf_dynamiclights;
48 float autocvar_g_ctf_flag_collect_delay;
49 float autocvar_g_ctf_flag_damageforcescale;
50 bool autocvar_g_ctf_flag_dropped_waypoint;
51 bool autocvar_g_ctf_flag_dropped_floatinwater;
52 bool autocvar_g_ctf_flag_glowtrails;
53 int autocvar_g_ctf_flag_health;
54 bool autocvar_g_ctf_flag_return;
55 bool autocvar_g_ctf_flag_return_carrying;
56 float autocvar_g_ctf_flag_return_carried_radius;
57 float autocvar_g_ctf_flag_return_time;
58 bool autocvar_g_ctf_flag_return_when_unreachable;
59 float autocvar_g_ctf_flag_return_damage;
60 float autocvar_g_ctf_flag_return_damage_delay;
61 float autocvar_g_ctf_flag_return_dropped;
62 bool autocvar_g_ctf_flag_waypoint = true;
63 float autocvar_g_ctf_flag_waypoint_maxdistance;
64 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
65 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
66 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
67 float autocvar_g_ctf_flagcarrier_selfforcefactor;
68 float autocvar_g_ctf_flagcarrier_damagefactor;
69 float autocvar_g_ctf_flagcarrier_forcefactor;
70 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
71 bool autocvar_g_ctf_fullbrightflags;
72 bool autocvar_g_ctf_ignore_frags;
73 bool autocvar_g_ctf_score_ignore_fields;
74 int autocvar_g_ctf_score_capture;
75 int autocvar_g_ctf_score_capture_assist;
76 int autocvar_g_ctf_score_kill;
77 int autocvar_g_ctf_score_penalty_drop;
78 int autocvar_g_ctf_score_penalty_returned;
79 int autocvar_g_ctf_score_pickup_base;
80 int autocvar_g_ctf_score_pickup_dropped_early;
81 int autocvar_g_ctf_score_pickup_dropped_late;
82 int autocvar_g_ctf_score_return;
83 float autocvar_g_ctf_shield_force;
84 float autocvar_g_ctf_shield_max_ratio;
85 int autocvar_g_ctf_shield_min_negscore;
86 bool autocvar_g_ctf_stalemate;
87 int autocvar_g_ctf_stalemate_endcondition;
88 float autocvar_g_ctf_stalemate_time;
89 bool autocvar_g_ctf_reverse;
90 float autocvar_g_ctf_dropped_capture_delay;
91 float autocvar_g_ctf_dropped_capture_radius;
93 void ctf_FakeTimeLimit(entity e, float t)
96 WriteByte(MSG_ONE, 3); // svc_updatestat
97 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
99 WriteCoord(MSG_ONE, autocvar_timelimit);
101 WriteCoord(MSG_ONE, (t + 1) / 60);
104 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
106 if(autocvar_sv_eventlog)
107 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != NULL) ? ftos(actor.playerid) : "")));
108 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
111 void ctf_CaptureRecord(entity flag, entity player)
113 float cap_record = ctf_captimerecord;
114 float cap_time = (time - flag.ctf_pickuptime);
115 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
119 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname);
120 else if(!ctf_captimerecord)
121 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_TIME), player.netname, TIME_ENCODE(cap_time));
122 else if(cap_time < cap_record)
123 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_BROKEN), player.netname, refername, TIME_ENCODE(cap_time), TIME_ENCODE(cap_record));
125 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_UNBROKEN), player.netname, refername, TIME_ENCODE(cap_time), TIME_ENCODE(cap_record));
127 // write that shit in the database
128 if(!ctf_oneflag) // but not in 1-flag mode
129 if((!ctf_captimerecord) || (cap_time < cap_record))
131 ctf_captimerecord = cap_time;
132 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
133 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
134 write_recordmarker(player, flag.ctf_pickuptime, cap_time);
137 if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
138 race_setTime(GetMapname(), TIME_ENCODE(cap_time), player.crypto_idfp, player.netname, player, false);
141 bool ctf_Immediate_Return_Allowed(entity flag, entity toucher)
144 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), { ++num_perteam; });
146 // automatically return if there's only 1 player on the team
147 return ((autocvar_g_ctf_flag_return || num_perteam <= 1 || (autocvar_g_ctf_flag_return_carrying && toucher.flagcarried))
151 bool ctf_Return_Customize(entity this, entity client)
153 // only to the carrier
154 return boolean(client == this.owner);
157 void ctf_FlagcarrierWaypoints(entity player)
159 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
160 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, 2 * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
161 WaypointSprite_UpdateHealth(player.wps_flagcarrier, healtharmor_maxdamage(GetResource(player, RES_HEALTH), GetResource(player, RES_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
162 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
164 if(player.flagcarried && CTF_SAMETEAM(player, player.flagcarried))
166 if(!player.wps_enemyflagcarrier)
168 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, 0, player, wps_enemyflagcarrier, true, RADARICON_FLAG);
169 wp.colormod = WPCOLOR_ENEMYFC(player.team);
170 setcefc(wp, ctf_Stalemate_Customize);
172 if(IS_REAL_CLIENT(player) && !ctf_stalemate)
173 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_VISIBLE);
176 if(!player.wps_flagreturn)
178 entity owp = WaypointSprite_SpawnFixed(WP_FlagReturn, player.flagcarried.ctf_spawnorigin + FLAG_WAYPOINT_OFFSET, player, wps_flagreturn, RADARICON_FLAG);
179 owp.colormod = '0 0.8 0.8';
180 //WaypointSprite_UpdateTeamRadar(player.wps_flagreturn, RADARICON_FLAG, ((player.team) ? colormapPaletteColor(player.team - 1, false) : '1 1 1'));
181 setcefc(owp, ctf_Return_Customize);
186 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
188 float current_distance = vlen((('1 0 0' * to.x) + ('0 1 0' * to.y)) - (('1 0 0' * from.x) + ('0 1 0' * from.y))); // for the sake of this check, exclude Z axis
189 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
190 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
191 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
194 if(current_height) // make sure we can actually do this arcing path
196 targpos = (to + ('0 0 1' * current_height));
197 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
198 if(trace_fraction < 1)
200 //print("normal arc line failed, trying to find new pos...");
201 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
202 targpos = (trace_endpos + eZ * FLAG_PASS_ARC_OFFSET_Z);
203 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
204 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
205 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
208 else { targpos = to; }
210 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
212 vector desired_direction = normalize(targpos - from);
213 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
214 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
217 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
219 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
221 // directional tracing only
223 makevectors(passer_angle);
225 // find the closest point on the enemy to the center of the attack
226 float h; // hypotenuse, which is the distance between attacker to head
227 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
229 h = vlen(head_center - passer_center);
230 a = h * (normalize(head_center - passer_center) * v_forward);
232 vector nearest_on_line = (passer_center + a * v_forward);
233 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
235 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
236 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
238 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
243 else { return true; }
247 // =======================
248 // CaptureShield Functions
249 // =======================
251 bool ctf_CaptureShield_CheckStatus(entity p)
253 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
254 int players_worseeq, players_total;
256 if(ctf_captureshield_max_ratio <= 0)
259 s = GameRules_scoring_add(p, CTF_CAPS, 0);
260 s2 = GameRules_scoring_add(p, CTF_PICKUPS, 0);
261 s3 = GameRules_scoring_add(p, CTF_RETURNS, 0);
262 s4 = GameRules_scoring_add(p, CTF_FCKILLS, 0);
264 sr = ((s - s2) + (s3 + s4));
266 if(sr >= -ctf_captureshield_min_negscore)
269 players_total = players_worseeq = 0;
270 FOREACH_CLIENT(IS_PLAYER(it), {
273 se = GameRules_scoring_add(it, CTF_CAPS, 0);
274 se2 = GameRules_scoring_add(it, CTF_PICKUPS, 0);
275 se3 = GameRules_scoring_add(it, CTF_RETURNS, 0);
276 se4 = GameRules_scoring_add(it, CTF_FCKILLS, 0);
278 ser = ((se - se2) + (se3 + se4));
285 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
286 // use this rule here
288 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
294 void ctf_CaptureShield_Update(entity player, bool wanted_status)
296 bool updated_status = ctf_CaptureShield_CheckStatus(player);
297 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
299 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
300 player.ctf_captureshielded = updated_status;
304 bool ctf_CaptureShield_Customize(entity this, entity client)
306 if(!client.ctf_captureshielded) { return false; }
307 if(CTF_SAMETEAM(this, client)) { return false; }
312 void ctf_CaptureShield_Touch(entity this, entity toucher)
314 if(!toucher.ctf_captureshielded) { return; }
315 if(CTF_SAMETEAM(this, toucher)) { return; }
317 vector mymid = (this.absmin + this.absmax) * 0.5;
318 vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
320 Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
321 if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
324 void ctf_CaptureShield_Spawn(entity flag)
326 entity shield = new(ctf_captureshield);
329 shield.team = flag.team;
330 settouch(shield, ctf_CaptureShield_Touch);
331 setcefc(shield, ctf_CaptureShield_Customize);
332 shield.effects = EF_ADDITIVE;
333 set_movetype(shield, MOVETYPE_NOCLIP);
334 shield.solid = SOLID_TRIGGER;
335 shield.avelocity = '7 0 11';
338 setorigin(shield, flag.origin);
339 setmodel(shield, MDL_CTF_SHIELD);
340 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
344 // ====================
345 // Drop/Pass/Throw Code
346 // ====================
348 void ctf_Handle_Drop(entity flag, entity player, int droptype)
351 player = (player ? player : flag.pass_sender);
354 set_movetype(flag, MOVETYPE_TOSS);
355 flag.takedamage = DAMAGE_YES;
356 flag.angles = '0 0 0';
357 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
358 flag.ctf_droptime = time;
359 flag.ctf_landtime = 0;
360 flag.ctf_dropper = player;
361 flag.ctf_status = FLAG_DROPPED;
363 // messages and sounds
364 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
365 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
366 ctf_EventLog("dropped", player.team, player);
369 GameRules_scoring_add_team(player, SCORE, -((flag.score_drop) ? flag.score_drop : autocvar_g_ctf_score_penalty_drop));
370 GameRules_scoring_add(player, CTF_DROPS, 1);
373 if(autocvar_g_ctf_flag_dropped_waypoint) {
374 entity wp = WaypointSprite_Spawn(WP_FlagDropped, 0, 0, flag, FLAG_WAYPOINT_OFFSET, NULL, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, true, RADARICON_FLAG);
375 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
378 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
380 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_health);
381 WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH));
384 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
386 if(droptype == DROP_PASS)
388 flag.pass_distance = 0;
389 flag.pass_sender = NULL;
390 flag.pass_target = NULL;
394 void ctf_Handle_Retrieve(entity flag, entity player)
396 entity sender = flag.pass_sender;
398 // transfer flag to player
400 flag.owner.flagcarried = flag;
401 GameRules_scoring_vip(player, true);
406 setattachment(flag, player.vehicle, "");
407 setorigin(flag, VEHICLE_FLAG_OFFSET);
408 flag.scale = VEHICLE_FLAG_SCALE;
412 setattachment(flag, player, "");
413 setorigin(flag, FLAG_CARRY_OFFSET);
415 set_movetype(flag, MOVETYPE_NONE);
416 flag.takedamage = DAMAGE_NO;
417 flag.solid = SOLID_NOT;
418 flag.angles = '0 0 0';
419 flag.ctf_status = FLAG_CARRY;
421 // messages and sounds
422 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
423 ctf_EventLog("receive", flag.team, player);
425 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
427 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
428 else if(it == player)
429 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
430 else if(SAME_TEAM(it, sender))
431 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
434 // create new waypoint
435 ctf_FlagcarrierWaypoints(player);
437 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
438 player.throw_antispam = sender.throw_antispam;
440 flag.pass_distance = 0;
441 flag.pass_sender = NULL;
442 flag.pass_target = NULL;
445 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
447 entity flag = player.flagcarried;
448 vector targ_origin, flag_velocity;
450 if(!flag) { return; }
451 if((droptype == DROP_PASS) && !receiver) { return; }
453 if(flag.speedrunning)
455 // ensure old waypoints are removed before resetting the flag
456 WaypointSprite_Kill(player.wps_flagcarrier);
458 if(player.wps_enemyflagcarrier)
459 WaypointSprite_Kill(player.wps_enemyflagcarrier);
461 if(player.wps_flagreturn)
462 WaypointSprite_Kill(player.wps_flagreturn);
463 ctf_RespawnFlag(flag);
468 setattachment(flag, NULL, "");
469 tracebox(player.origin - FLAG_DROP_OFFSET, flag.m_mins, flag.m_maxs, player.origin + FLAG_DROP_OFFSET, MOVE_NOMONSTERS, flag);
470 setorigin(flag, trace_endpos);
471 flag.owner.flagcarried = NULL;
472 GameRules_scoring_vip(flag.owner, false);
474 flag.solid = SOLID_TRIGGER;
475 flag.ctf_dropper = player;
476 flag.ctf_droptime = time;
477 flag.ctf_landtime = 0;
479 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
486 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
487 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
488 WarpZone_RefSys_Copy(flag, receiver);
489 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
490 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
492 flag.pass_distance = vlen((('1 0 0' * targ_origin.x) + ('0 1 0' * targ_origin.y)) - (('1 0 0' * player.origin.x) + ('0 1 0' * player.origin.y))); // for the sake of this check, exclude Z axis
493 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
496 set_movetype(flag, MOVETYPE_FLY);
497 flag.takedamage = DAMAGE_NO;
498 flag.pass_sender = player;
499 flag.pass_target = receiver;
500 flag.ctf_status = FLAG_PASSING;
503 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
504 WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
505 ctf_EventLog("pass", flag.team, player);
511 makevectors((player.v_angle.y * '0 1 0') + (bound(autocvar_g_ctf_throw_angle_min, player.v_angle.x, autocvar_g_ctf_throw_angle_max) * '1 0 0'));
513 flag_velocity = (('0 0 1' * autocvar_g_ctf_throw_velocity_up) + ((v_forward * autocvar_g_ctf_throw_velocity_forward) * ((StatusEffects_active(STATUSEFFECT_Strength, player)) ? autocvar_g_ctf_throw_strengthmultiplier : 1)));
514 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
515 ctf_Handle_Drop(flag, player, droptype);
516 navigation_dynamicgoal_set(flag, player);
522 flag.velocity = '0 0 0'; // do nothing
529 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, (('0 0 1' * autocvar_g_ctf_drop_velocity_up) + ((('0 1 0' * crandom()) + ('1 0 0' * crandom())) * autocvar_g_ctf_drop_velocity_side)), false);
530 ctf_Handle_Drop(flag, player, droptype);
531 navigation_dynamicgoal_set(flag, player);
536 // kill old waypointsprite
537 WaypointSprite_Ping(player.wps_flagcarrier);
538 WaypointSprite_Kill(player.wps_flagcarrier);
540 if(player.wps_enemyflagcarrier)
541 WaypointSprite_Kill(player.wps_enemyflagcarrier);
543 if(player.wps_flagreturn)
544 WaypointSprite_Kill(player.wps_flagreturn);
547 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
551 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
553 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
561 void nades_GiveBonus(entity player, float score);
563 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
565 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
566 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
567 entity player_team_flag = NULL, tmp_entity;
568 float old_time, new_time;
570 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
571 if(CTF_DIFFTEAM(player, flag)) { return; }
572 if((flag.cnt || enemy_flag.cnt) && flag.cnt != enemy_flag.cnt) { return; } // this should catch some edge cases (capturing grouped flag at ungrouped flag disallowed etc)
574 if (toucher.goalentity == flag.bot_basewaypoint)
575 toucher.goalentity_lock_timeout = 0;
578 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
579 if(SAME_TEAM(tmp_entity, player))
581 player_team_flag = tmp_entity;
585 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
587 player.throw_prevtime = time;
588 player.throw_count = 0;
590 // messages and sounds
591 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
592 ctf_CaptureRecord(enemy_flag, player);
593 _sound(player, CH_TRIGGER, ((ctf_oneflag) ? player_team_flag.snd_flag_capture : ((DIFF_TEAM(player, flag)) ? enemy_flag.snd_flag_capture : flag.snd_flag_capture)), VOL_BASE, ATTEN_NONE);
597 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
598 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
604 if(enemy_flag.score_capture || flag.score_capture)
605 pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
606 GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
608 if(enemy_flag.score_team_capture || flag.score_team_capture)
609 capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
610 GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
612 old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
613 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
614 if(!old_time || new_time < old_time)
615 GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
618 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
620 shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
624 if(capturetype == CAPTURE_NORMAL)
626 WaypointSprite_Kill(player.wps_flagcarrier);
627 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
629 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
630 { GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist)); }
633 flag.enemy = toucher;
636 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
637 ctf_RespawnFlag(enemy_flag);
640 void ctf_Handle_Return(entity flag, entity player)
642 // messages and sounds
643 if(IS_MONSTER(player))
645 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
649 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
650 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
652 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
653 ctf_EventLog("return", flag.team, player);
656 if(IS_PLAYER(player))
658 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
659 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
661 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
664 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
668 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
669 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
670 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
674 if(player.flagcarried == flag)
675 WaypointSprite_Kill(player.wps_flagcarrier);
680 ctf_RespawnFlag(flag);
683 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
686 float pickup_dropped_score; // used to calculate dropped pickup score
688 // attach the flag to the player
690 player.flagcarried = flag;
691 GameRules_scoring_vip(player, true);
694 setattachment(flag, player.vehicle, "");
695 setorigin(flag, VEHICLE_FLAG_OFFSET);
696 flag.scale = VEHICLE_FLAG_SCALE;
700 setattachment(flag, player, "");
701 setorigin(flag, FLAG_CARRY_OFFSET);
705 set_movetype(flag, MOVETYPE_NONE);
706 flag.takedamage = DAMAGE_NO;
707 flag.solid = SOLID_NOT;
708 flag.angles = '0 0 0';
709 flag.ctf_status = FLAG_CARRY;
713 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
714 case PICKUP_DROPPED: SetResourceExplicit(flag, RES_HEALTH, flag.max_health); break; // reset health/return timelimit
718 // messages and sounds
719 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
721 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
723 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
724 else if(CTF_DIFFTEAM(player, flag))
725 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
727 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
729 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
732 FOREACH_CLIENT(IS_PLAYER(it) && it != player && DIFF_TEAM(it, player), { Send_Notification(NOTIF_ONE, it, MSG_CHOICE, CHOICE_CTF_PICKUP_ENEMY_NEUTRAL, Team_ColorCode(player.team), player.netname); });
735 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
736 if(CTF_SAMETEAM(flag, it))
737 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, ((SAME_TEAM(flag, player)) ? CHOICE_CTF_PICKUP_ENEMY_TEAM : CHOICE_CTF_PICKUP_ENEMY), Team_ColorCode(player.team), player.netname);
738 else if(DIFF_TEAM(player, it))
739 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_ENEMY_OTHER), Team_ColorCode(player.team), player.netname);
742 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
745 GameRules_scoring_add(player, CTF_PICKUPS, 1);
746 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
751 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
752 ctf_EventLog("steal", flag.team, player);
758 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);
759 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);
760 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
761 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
762 ctf_EventLog("pickup", flag.team, player);
770 if(pickuptype == PICKUP_BASE)
772 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
773 if((player.speedrunning) && (ctf_captimerecord))
774 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
778 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
781 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
782 ctf_FlagcarrierWaypoints(player);
783 WaypointSprite_Ping(player.wps_flagcarrier);
787 // ===================
788 // Main Flag Functions
789 // ===================
791 void ctf_CheckFlagReturn(entity flag, int returntype)
793 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
795 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH)); }
797 if((GetResource(flag, RES_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
802 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
804 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
805 case RETURN_SPEEDRUN:
806 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
807 case RETURN_NEEDKILL:
808 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
811 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
813 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
814 ctf_EventLog("returned", flag.team, NULL);
816 ctf_RespawnFlag(flag);
821 bool ctf_Stalemate_Customize(entity this, entity client)
823 // make spectators see what the player would see
824 entity e = WaypointSprite_getviewentity(client);
825 entity wp_owner = this.owner;
828 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
829 if(SAME_TEAM(wp_owner, e)) { return false; }
830 if(!IS_PLAYER(e)) { return false; }
835 void ctf_CheckStalemate()
838 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
841 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
843 // build list of stale flags
844 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
846 if(autocvar_g_ctf_stalemate)
847 if(tmp_entity.ctf_status != FLAG_BASE)
848 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
850 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
851 ctf_staleflaglist = tmp_entity;
853 switch(tmp_entity.team)
855 case NUM_TEAM_1: ++stale_red_flags; break;
856 case NUM_TEAM_2: ++stale_blue_flags; break;
857 case NUM_TEAM_3: ++stale_yellow_flags; break;
858 case NUM_TEAM_4: ++stale_pink_flags; break;
859 default: ++stale_neutral_flags; break;
865 stale_flags = (stale_neutral_flags >= 1);
867 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
869 if(ctf_oneflag && stale_flags == 1)
870 ctf_stalemate = true;
871 else if(stale_flags >= 2)
872 ctf_stalemate = true;
873 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
874 { ctf_stalemate = false; wpforenemy_announced = false; }
875 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
876 { ctf_stalemate = false; wpforenemy_announced = false; }
878 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
881 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
883 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
885 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);
886 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
887 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
891 if (!wpforenemy_announced)
893 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)); });
895 wpforenemy_announced = true;
900 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
902 if(ITEM_DAMAGE_NEEDKILL(deathtype))
904 if(autocvar_g_ctf_flag_return_damage_delay)
905 this.ctf_flagdamaged_byworld = true;
908 SetResourceExplicit(this, RES_HEALTH, 0);
909 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
913 if(autocvar_g_ctf_flag_return_damage)
915 // reduce health and check if it should be returned
916 TakeResource(this, RES_HEALTH, damage);
917 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
922 void ctf_FlagThink(entity this)
927 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
930 if(this == ctf_worldflaglist) // only for the first flag
931 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
934 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
935 LOG_TRACE("wtf the flag got squashed?");
936 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
937 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
938 setsize(this, this.m_mins, this.m_maxs);
942 switch(this.ctf_status)
946 if(autocvar_g_ctf_dropped_capture_radius)
948 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
949 if(tmp_entity.ctf_status == FLAG_DROPPED)
950 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
951 if((this.noalign || tmp_entity.ctf_landtime) && time > ((this.noalign) ? tmp_entity.ctf_droptime : tmp_entity.ctf_landtime) + autocvar_g_ctf_dropped_capture_delay)
952 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
959 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
960 if(IS_ONGROUND(this) && !this.ctf_landtime)
961 this.ctf_landtime = time; // landtime is reset when thrown, and we don't want to restart the timer if the flag is pushed
963 if(autocvar_g_ctf_flag_dropped_floatinwater)
965 vector midpoint = ((this.absmin + this.absmax) * 0.5);
966 if(pointcontents(midpoint) == CONTENT_WATER)
968 this.velocity = this.velocity * 0.5;
970 if (pointcontents(midpoint + eZ * FLAG_FLOAT_OFFSET_Z) == CONTENT_WATER)
971 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
973 { set_movetype(this, MOVETYPE_FLY); }
975 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
977 if(autocvar_g_ctf_flag_return_dropped)
979 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
981 SetResourceExplicit(this, RES_HEALTH, 0);
982 ctf_CheckFlagReturn(this, RETURN_DROPPED);
986 if(this.ctf_flagdamaged_byworld)
988 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
989 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
992 else if(autocvar_g_ctf_flag_return_time)
994 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
995 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
1003 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
1005 SetResourceExplicit(this, RES_HEALTH, 0);
1006 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
1008 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
1009 ImpulseCommands(this.owner);
1011 if(autocvar_g_ctf_stalemate)
1013 if(time >= wpforenemy_nextthink)
1015 ctf_CheckStalemate();
1016 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1019 if(CTF_SAMETEAM(this, this.owner) && this.team)
1021 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1022 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
1023 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
1024 ctf_Handle_Return(this, this.owner);
1031 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1032 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1033 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1035 if((this.pass_target == NULL)
1036 || (IS_DEAD(this.pass_target))
1037 || (this.pass_target.flagcarried)
1038 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1039 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1040 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1042 // give up, pass failed
1043 ctf_Handle_Drop(this, NULL, DROP_PASS);
1047 // still a viable target, go for it
1048 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1053 default: // this should never happen
1055 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1061 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1064 if(game_stopped) return;
1065 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1067 bool is_not_monster = (!IS_MONSTER(toucher));
1069 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1070 if(ITEM_TOUCH_NEEDKILL())
1072 if(!autocvar_g_ctf_flag_return_damage_delay)
1074 SetResourceExplicit(flag, RES_HEALTH, 0);
1075 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1077 if(!flag.ctf_flagdamaged_byworld) { return; }
1080 // special touch behaviors
1081 if(STAT(FROZEN, toucher)) { return; }
1082 else if(IS_VEHICLE(toucher))
1084 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1085 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1087 return; // do nothing
1089 else if(IS_MONSTER(toucher))
1091 if(!autocvar_g_ctf_allow_monster_touch)
1092 return; // do nothing
1094 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1096 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1098 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1099 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1100 flag.wait = time + FLAG_TOUCHRATE;
1104 else if(IS_DEAD(toucher)) { return; }
1106 switch(flag.ctf_status)
1112 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1113 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1114 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1115 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1117 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1118 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to their base
1119 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)
1121 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1122 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1124 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1125 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1131 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1132 ctf_Handle_Return(flag, toucher); // toucher just returned their own flag
1133 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1134 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1140 LOG_TRACE("Someone touched a flag even though it was being carried?");
1146 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1148 if(DIFF_TEAM(toucher, flag.pass_sender))
1150 if(ctf_Immediate_Return_Allowed(flag, toucher))
1151 ctf_Handle_Return(flag, toucher);
1152 else if(is_not_monster && (!toucher.flagcarried))
1153 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1155 else if(!toucher.flagcarried)
1156 ctf_Handle_Retrieve(flag, toucher);
1163 .float last_respawn;
1164 void ctf_RespawnFlag(entity flag)
1166 flag.watertype = CONTENT_EMPTY; // TODO: it is unclear why this workaround is needed, likely many other potential breakage points!!
1167 // check for flag respawn being called twice in a row
1168 if(flag.last_respawn > time - 0.5)
1169 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1171 flag.last_respawn = time;
1173 // reset the player (if there is one)
1174 if((flag.owner) && (flag.owner.flagcarried == flag))
1176 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1177 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1178 WaypointSprite_Kill(flag.wps_flagcarrier);
1180 flag.owner.flagcarried = NULL;
1181 GameRules_scoring_vip(flag.owner, false);
1183 if(flag.speedrunning)
1184 ctf_FakeTimeLimit(flag.owner, -1);
1187 if((flag.owner) && (flag.owner.vehicle))
1188 flag.scale = FLAG_SCALE;
1190 if(flag.ctf_status == FLAG_DROPPED)
1191 { WaypointSprite_Kill(flag.wps_flagdropped); }
1194 setattachment(flag, NULL, "");
1195 setorigin(flag, flag.ctf_spawnorigin);
1197 //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1198 set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1199 flag.takedamage = DAMAGE_NO;
1200 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1201 flag.solid = SOLID_TRIGGER;
1202 flag.velocity = '0 0 0';
1203 flag.angles = flag.mangle;
1204 flag.flags = FL_ITEM | FL_NOTARGET;
1206 flag.ctf_status = FLAG_BASE;
1208 flag.pass_distance = 0;
1209 flag.pass_sender = NULL;
1210 flag.pass_target = NULL;
1211 flag.ctf_dropper = NULL;
1212 flag.ctf_pickuptime = 0;
1213 flag.ctf_droptime = 0;
1214 flag.ctf_landtime = 0;
1215 flag.ctf_flagdamaged_byworld = false;
1216 navigation_dynamicgoal_unset(flag);
1218 ctf_CheckStalemate();
1221 void ctf_Reset(entity this)
1223 if(this.owner && IS_PLAYER(this.owner))
1224 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1227 ctf_RespawnFlag(this);
1230 bool ctf_FlagBase_Customize(entity this, entity client)
1232 entity e = WaypointSprite_getviewentity(client);
1233 entity wp_owner = this.owner;
1234 entity flag = e.flagcarried;
1235 if(flag && CTF_SAMETEAM(e, flag))
1237 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1242 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1245 waypoint_spawnforitem_force(this, this.origin);
1246 navigation_dynamicgoal_init(this, true);
1252 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1253 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1254 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1255 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1256 default: basename = WP_FlagBaseNeutral; break;
1259 if(autocvar_g_ctf_flag_waypoint)
1261 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1262 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1263 wp.fade_rate = autocvar_g_ctf_flag_waypoint_maxdistance;
1264 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1265 setcefc(wp, ctf_FlagBase_Customize);
1268 // captureshield setup
1269 ctf_CaptureShield_Spawn(this);
1274 void ctf_FlagSetup(int teamnum, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1277 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1278 ctf_worldflaglist = flag;
1280 setattachment(flag, NULL, "");
1282 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnum), Team_ColorName_Upper(teamnum)));
1283 flag.team = teamnum;
1284 flag.classname = "item_flag_team";
1285 flag.target = "###item###"; // for finding the nearest item using findnearest
1286 flag.flags = FL_ITEM | FL_NOTARGET;
1287 IL_PUSH(g_items, flag);
1288 flag.solid = SOLID_TRIGGER;
1289 flag.takedamage = DAMAGE_NO;
1290 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1291 flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1292 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1293 flag.event_damage = ctf_FlagDamage;
1294 flag.pushable = true;
1295 flag.teleportable = TELEPORT_NORMAL;
1296 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1297 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1298 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1299 if(flag.damagedbycontents)
1300 IL_PUSH(g_damagedbycontents, flag);
1301 flag.velocity = '0 0 0';
1302 flag.mangle = flag.angles;
1303 flag.reset = ctf_Reset;
1304 settouch(flag, ctf_FlagTouch);
1305 setthink(flag, ctf_FlagThink);
1306 flag.nextthink = time + FLAG_THINKRATE;
1307 flag.ctf_status = FLAG_BASE;
1309 // crudely force them all to 0
1310 if(autocvar_g_ctf_score_ignore_fields)
1311 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1313 string teamname = Static_Team_ColorName_Lower(teamnum);
1315 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1316 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1317 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1318 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnum).eent_eff_name; }
1319 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnum).eent_eff_name; }
1320 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnum).eent_eff_name; }
1324 if(flag.s == "") flag.s = b; \
1325 precache_sound(flag.s);
1327 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnum))))
1328 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnum))))
1329 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnum))))
1330 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnum))))
1331 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1332 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1333 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1337 precache_model(flag.model);
1340 _setmodel(flag, flag.model); // precision set below
1341 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1342 flag.m_mins = flag.mins; // store these for squash checks
1343 flag.m_maxs = flag.maxs;
1344 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1346 if(autocvar_g_ctf_flag_glowtrails)
1350 case NUM_TEAM_1: flag.glow_color = 251; break;
1351 case NUM_TEAM_2: flag.glow_color = 210; break;
1352 case NUM_TEAM_3: flag.glow_color = 110; break;
1353 case NUM_TEAM_4: flag.glow_color = 145; break;
1354 default: flag.glow_color = 254; break;
1356 flag.glow_size = 25;
1357 flag.glow_trail = 1;
1360 flag.effects |= EF_LOWPRECISION;
1361 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1362 if(autocvar_g_ctf_dynamiclights)
1366 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1367 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1368 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1369 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1370 default: flag.effects |= EF_DIMLIGHT; break;
1375 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1377 flag.dropped_origin = flag.origin;
1378 flag.noalign = true;
1379 set_movetype(flag, MOVETYPE_NONE);
1381 else // drop to floor, automatically find a platform and set that as spawn origin
1383 flag.noalign = false;
1385 set_movetype(flag, MOVETYPE_NONE);
1388 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1396 // NOTE: LEGACY CODE, needs to be re-written!
1398 void havocbot_ctf_calculate_middlepoint()
1402 vector fo = '0 0 0';
1405 f = ctf_worldflaglist;
1410 f = f.ctf_worldflagnext;
1416 havocbot_middlepoint = s / n;
1417 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1419 havocbot_symmetry_axis_m = 0;
1420 havocbot_symmetry_axis_q = 0;
1423 // for symmetrical editing of waypoints
1424 entity f1 = ctf_worldflaglist;
1425 entity f2 = f1.ctf_worldflagnext;
1426 float m = -(f1.origin.y - f2.origin.y) / (max(f1.origin.x - f2.origin.x, FLOAT_EPSILON));
1427 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1428 havocbot_symmetry_axis_m = m;
1429 havocbot_symmetry_axis_q = q;
1431 havocbot_symmetry_origin_order = n;
1435 entity havocbot_ctf_find_flag(entity bot)
1438 f = ctf_worldflaglist;
1441 if (CTF_SAMETEAM(bot, f))
1443 f = f.ctf_worldflagnext;
1448 entity havocbot_ctf_find_enemy_flag(entity bot)
1451 f = ctf_worldflaglist;
1456 if(CTF_DIFFTEAM(bot, f))
1463 else if(!bot.flagcarried)
1467 else if (CTF_DIFFTEAM(bot, f))
1469 f = f.ctf_worldflagnext;
1474 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1481 FOREACH_CLIENT(IS_PLAYER(it), {
1482 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1485 if(vdist(it.origin - org, <, tc_radius))
1494 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1497 head = ctf_worldflaglist;
1500 if (CTF_SAMETEAM(this, head))
1502 head = head.ctf_worldflagnext;
1505 navigation_routerating(this, head, ratingscale, 10000);
1509 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1512 head = ctf_worldflaglist;
1515 if (CTF_SAMETEAM(this, head))
1517 if (this.flagcarried)
1518 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1520 head = head.ctf_worldflagnext; // skip base if it has a different group
1525 head = head.ctf_worldflagnext;
1530 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1533 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1536 head = ctf_worldflaglist;
1541 if(CTF_DIFFTEAM(this, head))
1545 if(this.flagcarried)
1548 else if(!this.flagcarried)
1552 else if(CTF_DIFFTEAM(this, head))
1554 head = head.ctf_worldflagnext;
1558 if (head.ctf_status == FLAG_CARRY)
1560 // adjust rating of our flag carrier depending on their health
1561 head = head.tag_entity;
1562 float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1563 ratingscale += ratingscale * f * 0.1;
1565 navigation_routerating(this, head, ratingscale, 10000);
1569 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1571 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1573 if (!bot_waypoints_for_items)
1575 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1581 head = havocbot_ctf_find_enemy_flag(this);
1586 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1589 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1593 mf = havocbot_ctf_find_flag(this);
1595 if(mf.ctf_status == FLAG_BASE)
1599 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1602 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1605 head = ctf_worldflaglist;
1608 // flag is out in the field
1609 if(head.ctf_status != FLAG_BASE)
1610 if(head.tag_entity==NULL) // dropped
1614 if(vdist(org - head.origin, <, df_radius))
1615 navigation_routerating(this, head, ratingscale, 10000);
1618 navigation_routerating(this, head, ratingscale, 10000);
1621 head = head.ctf_worldflagnext;
1625 void havocbot_ctf_reset_role(entity this)
1627 float cdefense, cmiddle, coffense;
1634 if (this.flagcarried)
1636 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1640 mf = havocbot_ctf_find_flag(this);
1641 ef = havocbot_ctf_find_enemy_flag(this);
1643 // Retrieve stolen flag
1644 if(mf.ctf_status!=FLAG_BASE)
1646 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1650 // If enemy flag is taken go to the middle to intercept pursuers
1651 if(ef.ctf_status!=FLAG_BASE)
1653 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1657 // if there is no one else on the team switch to offense
1659 // don't check if this bot is a player since it isn't true when the bot is added to the server
1660 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1664 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1667 else if (time < CS(this).jointime + 1)
1669 // if bots spawn all at once set good default roles
1672 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1675 else if (count == 2)
1677 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1682 // Evaluate best position to take
1683 // Count mates on middle position
1684 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1686 // Count mates on defense position
1687 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1689 // Count mates on offense position
1690 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1692 if(cdefense<=coffense)
1693 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1694 else if(coffense<=cmiddle)
1695 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1697 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1699 // if bots spawn all at once assign them a more appropriated role after a while
1700 if (time < CS(this).jointime + 1 && count > 2)
1701 this.havocbot_role_timeout = time + 10 + random() * 10;
1704 bool havocbot_ctf_is_basewaypoint(entity item)
1706 if (item.classname != "waypoint")
1709 entity head = ctf_worldflaglist;
1712 if (item == head.bot_basewaypoint)
1714 head = head.ctf_worldflagnext;
1719 void havocbot_role_ctf_carrier(entity this)
1723 havocbot_ctf_reset_role(this);
1727 if (this.flagcarried == NULL)
1729 havocbot_ctf_reset_role(this);
1733 if (navigation_goalrating_timeout(this))
1735 navigation_goalrating_start(this);
1738 entity mf = havocbot_ctf_find_flag(this);
1739 vector base_org = mf.dropped_origin;
1740 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1742 havocbot_goalrating_ctf_enemybase(this, base_rating);
1744 havocbot_goalrating_ctf_ourbase(this, base_rating);
1746 // start collecting items very close to the bot but only inside of own base radius
1747 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1748 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1750 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1752 navigation_goalrating_end(this);
1754 navigation_goalrating_timeout_set(this);
1756 entity goal = this.goalentity;
1757 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1758 this.goalentity_lock_timeout = time + ((this.enemy) ? 2 : 3);
1761 this.havocbot_cantfindflag = time + 10;
1762 else if (time > this.havocbot_cantfindflag)
1764 // Can't navigate to my own base, suicide!
1765 // TODO: drop it and wander around
1766 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1772 void havocbot_role_ctf_escort(entity this)
1778 havocbot_ctf_reset_role(this);
1782 if (this.flagcarried)
1784 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1788 // If enemy flag is back on the base switch to previous role
1789 ef = havocbot_ctf_find_enemy_flag(this);
1790 if(ef.ctf_status==FLAG_BASE)
1792 this.havocbot_role = this.havocbot_previous_role;
1793 this.havocbot_role_timeout = 0;
1796 if (ef.ctf_status == FLAG_DROPPED)
1798 navigation_goalrating_timeout_expire(this, 1);
1802 // If the flag carrier reached the base switch to defense
1803 mf = havocbot_ctf_find_flag(this);
1804 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1806 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1810 // Set the role timeout if necessary
1811 if (!this.havocbot_role_timeout)
1813 this.havocbot_role_timeout = time + random() * 30 + 60;
1816 // If nothing happened just switch to previous role
1817 if (time > this.havocbot_role_timeout)
1819 this.havocbot_role = this.havocbot_previous_role;
1820 this.havocbot_role_timeout = 0;
1824 // Chase the flag carrier
1825 if (navigation_goalrating_timeout(this))
1827 navigation_goalrating_start(this);
1830 havocbot_goalrating_ctf_enemyflag(this, 10000);
1831 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1832 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1834 navigation_goalrating_end(this);
1836 navigation_goalrating_timeout_set(this);
1840 void havocbot_role_ctf_offense(entity this)
1847 havocbot_ctf_reset_role(this);
1851 if (this.flagcarried)
1853 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1858 mf = havocbot_ctf_find_flag(this);
1859 ef = havocbot_ctf_find_enemy_flag(this);
1862 if(mf.ctf_status!=FLAG_BASE)
1865 pos = mf.tag_entity.origin;
1869 // Try to get it if closer than the enemy base
1870 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1872 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1877 // Escort flag carrier
1878 if(ef.ctf_status!=FLAG_BASE)
1881 pos = ef.tag_entity.origin;
1885 if(vdist(pos - mf.dropped_origin, >, 700))
1887 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1892 // Set the role timeout if necessary
1893 if (!this.havocbot_role_timeout)
1894 this.havocbot_role_timeout = time + 120;
1896 if (time > this.havocbot_role_timeout)
1898 havocbot_ctf_reset_role(this);
1902 if (navigation_goalrating_timeout(this))
1904 navigation_goalrating_start(this);
1907 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1908 havocbot_goalrating_ctf_enemybase(this, 10000);
1909 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1911 navigation_goalrating_end(this);
1913 navigation_goalrating_timeout_set(this);
1917 // Retriever (temporary role):
1918 void havocbot_role_ctf_retriever(entity this)
1924 havocbot_ctf_reset_role(this);
1928 if (this.flagcarried)
1930 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1934 // If flag is back on the base switch to previous role
1935 mf = havocbot_ctf_find_flag(this);
1936 if(mf.ctf_status==FLAG_BASE)
1938 if (mf.enemy == this) // did this bot return the flag?
1939 navigation_goalrating_timeout_force(this);
1940 havocbot_ctf_reset_role(this);
1944 if (!this.havocbot_role_timeout)
1945 this.havocbot_role_timeout = time + 20;
1947 if (time > this.havocbot_role_timeout)
1949 havocbot_ctf_reset_role(this);
1953 if (navigation_goalrating_timeout(this))
1955 const float RT_RADIUS = 10000;
1957 navigation_goalrating_start(this);
1960 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1961 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1962 havocbot_goalrating_ctf_enemybase(this, 8000);
1963 entity ef = havocbot_ctf_find_enemy_flag(this);
1964 vector enemy_base_org = ef.dropped_origin;
1965 // start collecting items very close to the bot but only inside of enemy base radius
1966 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1967 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1968 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1970 navigation_goalrating_end(this);
1972 navigation_goalrating_timeout_set(this);
1976 void havocbot_role_ctf_middle(entity this)
1982 havocbot_ctf_reset_role(this);
1986 if (this.flagcarried)
1988 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1992 mf = havocbot_ctf_find_flag(this);
1993 if(mf.ctf_status!=FLAG_BASE)
1995 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1999 if (!this.havocbot_role_timeout)
2000 this.havocbot_role_timeout = time + 10;
2002 if (time > this.havocbot_role_timeout)
2004 havocbot_ctf_reset_role(this);
2008 if (navigation_goalrating_timeout(this))
2012 org = havocbot_middlepoint;
2013 org.z = this.origin.z;
2015 navigation_goalrating_start(this);
2018 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
2019 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
2020 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2021 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2022 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2023 havocbot_goalrating_ctf_enemybase(this, 3000);
2025 navigation_goalrating_end(this);
2027 entity goal = this.goalentity;
2028 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
2029 this.goalentity_lock_timeout = time + 2;
2031 navigation_goalrating_timeout_set(this);
2035 void havocbot_role_ctf_defense(entity this)
2041 havocbot_ctf_reset_role(this);
2045 if (this.flagcarried)
2047 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2051 // If own flag was captured
2052 mf = havocbot_ctf_find_flag(this);
2053 if(mf.ctf_status!=FLAG_BASE)
2055 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2059 if (!this.havocbot_role_timeout)
2060 this.havocbot_role_timeout = time + 30;
2062 if (time > this.havocbot_role_timeout)
2064 havocbot_ctf_reset_role(this);
2067 if (navigation_goalrating_timeout(this))
2069 vector org = mf.dropped_origin;
2071 navigation_goalrating_start(this);
2073 // if enemies are closer to our base, go there
2074 entity closestplayer = NULL;
2075 float distance, bestdistance = 10000;
2076 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2077 distance = vlen(org - it.origin);
2078 if(distance<bestdistance)
2081 bestdistance = distance;
2087 if(DIFF_TEAM(closestplayer, this))
2088 if(vdist(org - this.origin, >, 1000))
2089 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2090 havocbot_goalrating_ctf_ourbase(this, 10000);
2092 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2093 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2094 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2095 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2096 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2098 navigation_goalrating_end(this);
2100 navigation_goalrating_timeout_set(this);
2104 void havocbot_role_ctf_setrole(entity bot, int role)
2106 string s = "(null)";
2109 case HAVOCBOT_CTF_ROLE_CARRIER:
2111 bot.havocbot_role = havocbot_role_ctf_carrier;
2112 bot.havocbot_role_timeout = 0;
2113 bot.havocbot_cantfindflag = time + 10;
2114 if (bot.havocbot_previous_role != bot.havocbot_role)
2115 navigation_goalrating_timeout_force(bot);
2117 case HAVOCBOT_CTF_ROLE_DEFENSE:
2119 bot.havocbot_role = havocbot_role_ctf_defense;
2120 bot.havocbot_role_timeout = 0;
2122 case HAVOCBOT_CTF_ROLE_MIDDLE:
2124 bot.havocbot_role = havocbot_role_ctf_middle;
2125 bot.havocbot_role_timeout = 0;
2127 case HAVOCBOT_CTF_ROLE_OFFENSE:
2129 bot.havocbot_role = havocbot_role_ctf_offense;
2130 bot.havocbot_role_timeout = 0;
2132 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2134 bot.havocbot_previous_role = bot.havocbot_role;
2135 bot.havocbot_role = havocbot_role_ctf_retriever;
2136 bot.havocbot_role_timeout = time + 10;
2137 if (bot.havocbot_previous_role != bot.havocbot_role)
2138 navigation_goalrating_timeout_expire(bot, 2);
2140 case HAVOCBOT_CTF_ROLE_ESCORT:
2142 bot.havocbot_previous_role = bot.havocbot_role;
2143 bot.havocbot_role = havocbot_role_ctf_escort;
2144 bot.havocbot_role_timeout = time + 30;
2145 if (bot.havocbot_previous_role != bot.havocbot_role)
2146 navigation_goalrating_timeout_expire(bot, 2);
2149 LOG_TRACE(bot.netname, " switched to ", s);
2157 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2159 entity player = M_ARGV(0, entity);
2161 int t = 0, t2 = 0, t3 = 0;
2162 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)
2164 // initially clear items so they can be set as necessary later.
2165 STAT(OBJECTIVE_STATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2166 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2167 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2168 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2169 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2170 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2172 // scan through all the flags and notify the client about them
2173 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2175 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2176 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2177 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2178 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2179 if(flag.team == 0 && !b5) { b5 = true; t = CTF_NEUTRAL_FLAG_CARRYING; t2 = CTF_NEUTRAL_FLAG_TAKEN; t3 = CTF_NEUTRAL_FLAG_LOST; STAT(OBJECTIVE_STATUS, player) |= CTF_FLAG_NEUTRAL; }
2181 switch(flag.ctf_status)
2186 if((flag.owner == player) || (flag.pass_sender == player))
2187 STAT(OBJECTIVE_STATUS, player) |= t; // carrying: player is currently carrying the flag
2189 STAT(OBJECTIVE_STATUS, player) |= t2; // taken: someone else is carrying the flag
2194 STAT(OBJECTIVE_STATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2200 // item for stopping players from capturing the flag too often
2201 if(player.ctf_captureshielded)
2202 STAT(OBJECTIVE_STATUS, player) |= CTF_SHIELDED;
2205 STAT(OBJECTIVE_STATUS, player) |= CTF_STALEMATE;
2207 // update the health of the flag carrier waypointsprite
2208 if(player.wps_flagcarrier)
2209 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);
2212 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in damage.qc
2214 entity frag_attacker = M_ARGV(1, entity);
2215 entity frag_target = M_ARGV(2, entity);
2216 float frag_damage = M_ARGV(4, float);
2217 vector frag_force = M_ARGV(6, vector);
2219 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2221 if(frag_target == frag_attacker) // damage done to yourself
2223 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2224 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2226 else // damage done to everyone else
2228 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2229 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2232 M_ARGV(4, float) = frag_damage;
2233 M_ARGV(6, vector) = frag_force;
2235 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2237 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
2238 && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2240 frag_target.wps_helpme_time = time;
2241 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2243 // todo: add notification for when flag carrier needs help?
2247 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2249 entity frag_attacker = M_ARGV(1, entity);
2250 entity frag_target = M_ARGV(2, entity);
2252 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2254 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2255 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2258 if(frag_target.flagcarried)
2260 entity tmp_entity = frag_target.flagcarried;
2261 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2262 tmp_entity.ctf_dropper = NULL;
2266 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2268 M_ARGV(2, float) = 0; // frag score
2269 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2272 void ctf_RemovePlayer(entity player)
2274 if(player.flagcarried)
2275 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2277 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2279 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2280 if(flag.pass_target == player) { flag.pass_target = NULL; }
2281 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2285 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2287 entity player = M_ARGV(0, entity);
2289 ctf_RemovePlayer(player);
2292 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2294 entity player = M_ARGV(0, entity);
2296 ctf_RemovePlayer(player);
2299 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2301 if(!autocvar_g_ctf_leaderboard)
2304 entity player = M_ARGV(0, entity);
2306 race_SendAll(player, true);
2309 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2311 if(!autocvar_g_ctf_leaderboard)
2314 entity player = M_ARGV(0, entity);
2316 race_checkAndWriteName(player);
2319 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2321 entity player = M_ARGV(0, entity);
2323 if(player.flagcarried)
2324 if(!autocvar_g_ctf_portalteleport)
2325 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2328 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2330 if(MUTATOR_RETURNVALUE || game_stopped) return;
2332 entity player = M_ARGV(0, entity);
2334 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2336 // pass the flag to a team mate
2337 if(autocvar_g_ctf_pass)
2339 entity head, closest_target = NULL;
2340 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2342 while(head) // find the closest acceptable target to pass to
2344 if(IS_PLAYER(head) && !IS_DEAD(head))
2345 if(head != player && SAME_TEAM(head, player))
2346 if(!head.speedrunning && !head.vehicle)
2348 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in damage.qc)
2349 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2350 vector passer_center = CENTER_OR_VIEWOFS(player);
2352 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2354 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2356 if(IS_BOT_CLIENT(head))
2358 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2359 ctf_Handle_Throw(head, player, DROP_PASS);
2363 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2364 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2366 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2369 else if(player.flagcarried && !head.flagcarried)
2373 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2374 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2375 { closest_target = head; }
2377 else { closest_target = head; }
2384 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2387 // throw the flag in front of you
2388 if(autocvar_g_ctf_throw && player.flagcarried)
2390 if(player.throw_count == -1)
2392 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2394 player.throw_prevtime = time;
2395 player.throw_count = 1;
2396 ctf_Handle_Throw(player, NULL, DROP_THROW);
2401 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2407 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2408 else { player.throw_count += 1; }
2409 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2411 player.throw_prevtime = time;
2412 ctf_Handle_Throw(player, NULL, DROP_THROW);
2419 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2421 entity player = M_ARGV(0, entity);
2423 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2425 player.wps_helpme_time = time;
2426 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2428 else // create a normal help me waypointsprite
2430 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2431 WaypointSprite_Ping(player.wps_helpme);
2437 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2439 entity player = M_ARGV(0, entity);
2440 entity veh = M_ARGV(1, entity);
2442 if(player.flagcarried)
2444 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2446 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2450 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2451 setattachment(player.flagcarried, veh, "");
2452 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2453 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2454 //player.flagcarried.angles = '0 0 0';
2460 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2462 entity player = M_ARGV(0, entity);
2464 if(player.flagcarried)
2466 setattachment(player.flagcarried, player, "");
2467 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2468 player.flagcarried.scale = FLAG_SCALE;
2469 player.flagcarried.angles = '0 0 0';
2470 player.flagcarried.nodrawtoclient = NULL;
2475 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2477 entity player = M_ARGV(0, entity);
2479 if(player.flagcarried)
2481 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2482 ctf_RespawnFlag(player.flagcarried);
2487 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2489 entity flag; // temporary entity for the search method
2491 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2493 switch(flag.ctf_status)
2498 // lock the flag, game is over
2499 set_movetype(flag, MOVETYPE_NONE);
2500 flag.takedamage = DAMAGE_NO;
2501 flag.solid = SOLID_NOT;
2502 flag.nextthink = false; // stop thinking
2504 //dprint("stopping the ", flag.netname, " from moving.\n");
2512 // do nothing for these flags
2519 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2521 entity bot = M_ARGV(0, entity);
2523 havocbot_ctf_reset_role(bot);
2527 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2529 M_ARGV(1, string) = "ctf_team";
2532 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2534 int record_page = M_ARGV(0, int);
2535 string ret_string = M_ARGV(1, string);
2537 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2539 if (MapInfo_Get_ByID(i))
2541 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2547 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2548 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2552 M_ARGV(1, string) = ret_string;
2555 bool superspec_Spectate(entity this, entity targ); // TODO
2556 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2557 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2559 entity player = M_ARGV(0, entity);
2560 string cmd_name = M_ARGV(1, string);
2561 int cmd_argc = M_ARGV(2, int);
2563 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2565 if(cmd_name == "followfc")
2577 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2578 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2579 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2580 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2584 FOREACH_CLIENT(IS_PLAYER(it), {
2585 if(it.flagcarried && (it.team == _team || _team == 0))
2588 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2589 continue; // already spectating this fc, try another
2590 return superspec_Spectate(player, it);
2595 superspec_msg("", "", player, "No active flag carrier\n", 1);
2600 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2602 entity frag_target = M_ARGV(0, entity);
2604 if(frag_target.flagcarried)
2605 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2608 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2610 entity player = M_ARGV(0, entity);
2611 if(player.flagcarried)
2612 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2620 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2621 CTF flag for team one (Red).
2623 "angle" Angle the flag will point (minus 90 degrees)...
2624 "model" model to use, note this needs red and blue as skins 0 and 1...
2625 "noise" sound played when flag is picked up...
2626 "noise1" sound played when flag is returned by a teammate...
2627 "noise2" sound played when flag is captured...
2628 "noise3" sound played when flag is lost in the field and respawns itself...
2629 "noise4" sound played when flag is dropped by a player...
2630 "noise5" sound played when flag touches the ground... */
2631 spawnfunc(item_flag_team1)
2633 if(!g_ctf) { delete(this); return; }
2635 ctf_FlagSetup(NUM_TEAM_1, this);
2638 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2639 CTF flag for team two (Blue).
2641 "angle" Angle the flag will point (minus 90 degrees)...
2642 "model" model to use, note this needs red and blue as skins 0 and 1...
2643 "noise" sound played when flag is picked up...
2644 "noise1" sound played when flag is returned by a teammate...
2645 "noise2" sound played when flag is captured...
2646 "noise3" sound played when flag is lost in the field and respawns itself...
2647 "noise4" sound played when flag is dropped by a player...
2648 "noise5" sound played when flag touches the ground... */
2649 spawnfunc(item_flag_team2)
2651 if(!g_ctf) { delete(this); return; }
2653 ctf_FlagSetup(NUM_TEAM_2, this);
2656 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2657 CTF flag for team three (Yellow).
2659 "angle" Angle the flag will point (minus 90 degrees)...
2660 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2661 "noise" sound played when flag is picked up...
2662 "noise1" sound played when flag is returned by a teammate...
2663 "noise2" sound played when flag is captured...
2664 "noise3" sound played when flag is lost in the field and respawns itself...
2665 "noise4" sound played when flag is dropped by a player...
2666 "noise5" sound played when flag touches the ground... */
2667 spawnfunc(item_flag_team3)
2669 if(!g_ctf) { delete(this); return; }
2671 ctf_FlagSetup(NUM_TEAM_3, this);
2674 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2675 CTF flag for team four (Pink).
2677 "angle" Angle the flag will point (minus 90 degrees)...
2678 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2679 "noise" sound played when flag is picked up...
2680 "noise1" sound played when flag is returned by a teammate...
2681 "noise2" sound played when flag is captured...
2682 "noise3" sound played when flag is lost in the field and respawns itself...
2683 "noise4" sound played when flag is dropped by a player...
2684 "noise5" sound played when flag touches the ground... */
2685 spawnfunc(item_flag_team4)
2687 if(!g_ctf) { delete(this); return; }
2689 ctf_FlagSetup(NUM_TEAM_4, this);
2692 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2695 "angle" Angle the flag will point (minus 90 degrees)...
2696 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2697 "noise" sound played when flag is picked up...
2698 "noise1" sound played when flag is returned by a teammate...
2699 "noise2" sound played when flag is captured...
2700 "noise3" sound played when flag is lost in the field and respawns itself...
2701 "noise4" sound played when flag is dropped by a player...
2702 "noise5" sound played when flag touches the ground... */
2703 spawnfunc(item_flag_neutral)
2705 if(!g_ctf) { delete(this); return; }
2706 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2708 ctf_FlagSetup(0, this);
2711 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2712 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2713 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.
2715 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2716 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2719 if(!g_ctf) { delete(this); return; }
2721 this.team = this.cnt + 1;
2724 // compatibility for quake maps
2725 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2726 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2727 spawnfunc(info_player_team1);
2728 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2729 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2730 spawnfunc(info_player_team2);
2731 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2732 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2734 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2735 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2737 // compatibility for wop maps
2738 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2739 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2740 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2741 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2742 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2743 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2751 void ctf_ScoreRules(int teams)
2753 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2754 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2755 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2756 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2757 field(SP_CTF_PICKUPS, "pickups", 0);
2758 field(SP_CTF_FCKILLS, "fckills", 0);
2759 field(SP_CTF_RETURNS, "returns", 0);
2760 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2764 // code from here on is just to support maps that don't have flag and team entities
2765 void ctf_SpawnTeam (string teamname, int teamcolor)
2767 entity this = new_pure(ctf_team);
2768 this.netname = teamname;
2769 this.cnt = teamcolor - 1;
2770 this.spawnfunc_checked = true;
2771 this.team = teamcolor;
2774 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2779 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2781 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2782 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2784 switch(tmp_entity.team)
2786 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2787 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2788 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2789 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2791 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2794 havocbot_ctf_calculate_middlepoint();
2796 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2798 ctf_teams = 0; // so set the default red and blue teams
2799 BITSET_ASSIGN(ctf_teams, BIT(0));
2800 BITSET_ASSIGN(ctf_teams, BIT(1));
2803 //ctf_teams = bound(2, ctf_teams, 4);
2805 // if no teams are found, spawn defaults
2806 if(find(NULL, classname, "ctf_team") == NULL)
2808 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2809 if(ctf_teams & BIT(0))
2810 ctf_SpawnTeam("Red", NUM_TEAM_1);
2811 if(ctf_teams & BIT(1))
2812 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2813 if(ctf_teams & BIT(2))
2814 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2815 if(ctf_teams & BIT(3))
2816 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2819 ctf_ScoreRules(ctf_teams);
2822 void ctf_Initialize()
2824 CTF_FLAG = NEW(Flag);
2825 record_type = CTF_RECORD;
2826 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2828 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2829 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2830 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2832 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);