3 #include <common/effects/all.qh>
4 #include <common/mapobjects/teleporters.qh>
5 #include <common/vehicles/all.qh>
6 #include <server/gamelog.qh>
7 #include <server/g_damage.qh>
8 #include <server/g_world.qh>
9 #include <server/race.qh>
10 #include <server/teamplay.qh>
12 #include <lib/warpzone/common.qh>
14 bool autocvar_g_ctf_allow_vehicle_carry;
15 bool autocvar_g_ctf_allow_vehicle_touch;
16 bool autocvar_g_ctf_allow_monster_touch;
17 bool autocvar_g_ctf_throw;
18 float autocvar_g_ctf_throw_angle_max;
19 float autocvar_g_ctf_throw_angle_min;
20 int autocvar_g_ctf_throw_punish_count;
21 float autocvar_g_ctf_throw_punish_delay;
22 float autocvar_g_ctf_throw_punish_time;
23 float autocvar_g_ctf_throw_strengthmultiplier;
24 float autocvar_g_ctf_throw_velocity_forward;
25 float autocvar_g_ctf_throw_velocity_up;
26 float autocvar_g_ctf_drop_velocity_up;
27 float autocvar_g_ctf_drop_velocity_side;
28 bool autocvar_g_ctf_oneflag_reverse;
29 bool autocvar_g_ctf_portalteleport;
30 bool autocvar_g_ctf_pass;
31 float autocvar_g_ctf_pass_arc;
32 float autocvar_g_ctf_pass_arc_max;
33 float autocvar_g_ctf_pass_directional_max;
34 float autocvar_g_ctf_pass_directional_min;
35 float autocvar_g_ctf_pass_radius;
36 float autocvar_g_ctf_pass_wait;
37 bool autocvar_g_ctf_pass_request;
38 float autocvar_g_ctf_pass_turnrate;
39 float autocvar_g_ctf_pass_timelimit;
40 float autocvar_g_ctf_pass_velocity;
41 bool autocvar_g_ctf_dynamiclights;
42 float autocvar_g_ctf_flag_collect_delay;
43 float autocvar_g_ctf_flag_damageforcescale;
44 bool autocvar_g_ctf_flag_dropped_waypoint;
45 bool autocvar_g_ctf_flag_dropped_floatinwater;
46 bool autocvar_g_ctf_flag_glowtrails;
47 int autocvar_g_ctf_flag_health;
48 bool autocvar_g_ctf_flag_return;
49 bool autocvar_g_ctf_flag_return_carrying;
50 float autocvar_g_ctf_flag_return_carried_radius;
51 float autocvar_g_ctf_flag_return_time;
52 bool autocvar_g_ctf_flag_return_when_unreachable;
53 float autocvar_g_ctf_flag_return_damage;
54 float autocvar_g_ctf_flag_return_damage_delay;
55 float autocvar_g_ctf_flag_return_dropped;
56 bool autocvar_g_ctf_flag_waypoint = true;
57 float autocvar_g_ctf_flag_waypoint_maxdistance;
58 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
59 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
60 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
61 float autocvar_g_ctf_flagcarrier_selfforcefactor;
62 float autocvar_g_ctf_flagcarrier_damagefactor;
63 float autocvar_g_ctf_flagcarrier_forcefactor;
64 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
65 bool autocvar_g_ctf_fullbrightflags;
66 bool autocvar_g_ctf_ignore_frags;
67 bool autocvar_g_ctf_score_ignore_fields;
68 int autocvar_g_ctf_score_capture;
69 int autocvar_g_ctf_score_capture_assist;
70 int autocvar_g_ctf_score_kill;
71 int autocvar_g_ctf_score_penalty_drop;
72 int autocvar_g_ctf_score_penalty_returned;
73 int autocvar_g_ctf_score_pickup_base;
74 int autocvar_g_ctf_score_pickup_dropped_early;
75 int autocvar_g_ctf_score_pickup_dropped_late;
76 int autocvar_g_ctf_score_return;
77 float autocvar_g_ctf_shield_force;
78 float autocvar_g_ctf_shield_max_ratio;
79 int autocvar_g_ctf_shield_min_negscore;
80 bool autocvar_g_ctf_stalemate;
81 int autocvar_g_ctf_stalemate_endcondition;
82 float autocvar_g_ctf_stalemate_time;
83 bool autocvar_g_ctf_reverse;
84 float autocvar_g_ctf_dropped_capture_delay;
85 float autocvar_g_ctf_dropped_capture_radius;
87 void ctf_FakeTimeLimit(entity e, float t)
90 WriteByte(MSG_ONE, 3); // svc_updatestat
91 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
93 WriteCoord(MSG_ONE, autocvar_timelimit);
95 WriteCoord(MSG_ONE, (t + 1) / 60);
98 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
100 if(autocvar_sv_eventlog)
101 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != NULL) ? ftos(actor.playerid) : "")));
102 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
105 void ctf_CaptureRecord(entity flag, entity player)
107 float cap_record = ctf_captimerecord;
108 float cap_time = (time - flag.ctf_pickuptime);
109 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
113 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname);
114 else if(!ctf_captimerecord)
115 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_TIME), player.netname, TIME_ENCODE(cap_time));
116 else if(cap_time < cap_record)
117 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));
119 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));
121 // write that shit in the database
122 if(!ctf_oneflag) // but not in 1-flag mode
123 if((!ctf_captimerecord) || (cap_time < cap_record))
125 ctf_captimerecord = cap_time;
126 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
127 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
128 write_recordmarker(player, flag.ctf_pickuptime, cap_time);
131 if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
132 race_setTime(GetMapname(), TIME_ENCODE(cap_time), player.crypto_idfp, player.netname, player, false);
135 bool ctf_Immediate_Return_Allowed(entity flag, entity toucher)
138 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), { ++num_perteam; });
140 // automatically return if there's only 1 player on the team
141 return ((autocvar_g_ctf_flag_return || num_perteam <= 1 || (autocvar_g_ctf_flag_return_carrying && toucher.flagcarried))
145 bool ctf_Return_Customize(entity this, entity client)
147 // only to the carrier
148 return boolean(client == this.owner);
151 void ctf_FlagcarrierWaypoints(entity player)
153 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
154 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, 2 * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
155 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);
156 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
158 if(player.flagcarried && CTF_SAMETEAM(player, player.flagcarried))
160 if(!player.wps_enemyflagcarrier)
162 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, 0, player, wps_enemyflagcarrier, true, RADARICON_FLAG);
163 wp.colormod = WPCOLOR_ENEMYFC(player.team);
164 setcefc(wp, ctf_Stalemate_Customize);
166 if(IS_REAL_CLIENT(player) && !ctf_stalemate)
167 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_VISIBLE);
170 if(!player.wps_flagreturn)
172 entity owp = WaypointSprite_SpawnFixed(WP_FlagReturn, player.flagcarried.ctf_spawnorigin + FLAG_WAYPOINT_OFFSET, player, wps_flagreturn, RADARICON_FLAG);
173 owp.colormod = '0 0.8 0.8';
174 //WaypointSprite_UpdateTeamRadar(player.wps_flagreturn, RADARICON_FLAG, ((player.team) ? colormapPaletteColor(player.team - 1, false) : '1 1 1'));
175 setcefc(owp, ctf_Return_Customize);
180 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
182 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
183 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
184 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
185 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
188 if(current_height) // make sure we can actually do this arcing path
190 targpos = (to + ('0 0 1' * current_height));
191 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
192 if(trace_fraction < 1)
194 //print("normal arc line failed, trying to find new pos...");
195 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
196 targpos = (trace_endpos + eZ * FLAG_PASS_ARC_OFFSET_Z);
197 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
198 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
199 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
202 else { targpos = to; }
204 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
206 vector desired_direction = normalize(targpos - from);
207 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
208 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
211 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
213 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
215 // directional tracing only
217 makevectors(passer_angle);
219 // find the closest point on the enemy to the center of the attack
220 float h; // hypotenuse, which is the distance between attacker to head
221 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
223 h = vlen(head_center - passer_center);
224 a = h * (normalize(head_center - passer_center) * v_forward);
226 vector nearest_on_line = (passer_center + a * v_forward);
227 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
229 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
230 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
232 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
237 else { return true; }
241 // =======================
242 // CaptureShield Functions
243 // =======================
245 bool ctf_CaptureShield_CheckStatus(entity p)
247 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
248 int players_worseeq, players_total;
250 if(ctf_captureshield_max_ratio <= 0)
253 s = GameRules_scoring_add(p, CTF_CAPS, 0);
254 s2 = GameRules_scoring_add(p, CTF_PICKUPS, 0);
255 s3 = GameRules_scoring_add(p, CTF_RETURNS, 0);
256 s4 = GameRules_scoring_add(p, CTF_FCKILLS, 0);
258 sr = ((s - s2) + (s3 + s4));
260 if(sr >= -ctf_captureshield_min_negscore)
263 players_total = players_worseeq = 0;
264 FOREACH_CLIENT(IS_PLAYER(it), {
267 se = GameRules_scoring_add(it, CTF_CAPS, 0);
268 se2 = GameRules_scoring_add(it, CTF_PICKUPS, 0);
269 se3 = GameRules_scoring_add(it, CTF_RETURNS, 0);
270 se4 = GameRules_scoring_add(it, CTF_FCKILLS, 0);
272 ser = ((se - se2) + (se3 + se4));
279 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
280 // use this rule here
282 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
288 void ctf_CaptureShield_Update(entity player, bool wanted_status)
290 bool updated_status = ctf_CaptureShield_CheckStatus(player);
291 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
293 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
294 player.ctf_captureshielded = updated_status;
298 bool ctf_CaptureShield_Customize(entity this, entity client)
300 if(!client.ctf_captureshielded) { return false; }
301 if(CTF_SAMETEAM(this, client)) { return false; }
306 void ctf_CaptureShield_Touch(entity this, entity toucher)
308 if(!toucher.ctf_captureshielded) { return; }
309 if(CTF_SAMETEAM(this, toucher)) { return; }
311 vector mymid = (this.absmin + this.absmax) * 0.5;
312 vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
314 Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
315 if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
318 void ctf_CaptureShield_Spawn(entity flag)
320 entity shield = new(ctf_captureshield);
323 shield.team = flag.team;
324 settouch(shield, ctf_CaptureShield_Touch);
325 setcefc(shield, ctf_CaptureShield_Customize);
326 shield.effects = EF_ADDITIVE;
327 set_movetype(shield, MOVETYPE_NOCLIP);
328 shield.solid = SOLID_TRIGGER;
329 shield.avelocity = '7 0 11';
332 setorigin(shield, flag.origin);
333 setmodel(shield, MDL_CTF_SHIELD);
334 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
338 // ====================
339 // Drop/Pass/Throw Code
340 // ====================
342 void ctf_Handle_Drop(entity flag, entity player, int droptype)
345 player = (player ? player : flag.pass_sender);
348 set_movetype(flag, MOVETYPE_TOSS);
349 flag.takedamage = DAMAGE_YES;
350 flag.angles = '0 0 0';
351 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
352 flag.ctf_droptime = time;
353 flag.ctf_dropper = player;
354 flag.ctf_status = FLAG_DROPPED;
356 // messages and sounds
357 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
358 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
359 ctf_EventLog("dropped", player.team, player);
362 GameRules_scoring_add_team(player, SCORE, -((flag.score_drop) ? flag.score_drop : autocvar_g_ctf_score_penalty_drop));
363 GameRules_scoring_add(player, CTF_DROPS, 1);
366 if(autocvar_g_ctf_flag_dropped_waypoint) {
367 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);
368 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
371 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
373 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_health);
374 WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH));
377 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
379 if(droptype == DROP_PASS)
381 flag.pass_distance = 0;
382 flag.pass_sender = NULL;
383 flag.pass_target = NULL;
387 void ctf_Handle_Retrieve(entity flag, entity player)
389 entity sender = flag.pass_sender;
391 // transfer flag to player
393 flag.owner.flagcarried = flag;
394 GameRules_scoring_vip(player, true);
399 setattachment(flag, player.vehicle, "");
400 setorigin(flag, VEHICLE_FLAG_OFFSET);
401 flag.scale = VEHICLE_FLAG_SCALE;
405 setattachment(flag, player, "");
406 setorigin(flag, FLAG_CARRY_OFFSET);
408 set_movetype(flag, MOVETYPE_NONE);
409 flag.takedamage = DAMAGE_NO;
410 flag.solid = SOLID_NOT;
411 flag.angles = '0 0 0';
412 flag.ctf_status = FLAG_CARRY;
414 // messages and sounds
415 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
416 ctf_EventLog("receive", flag.team, player);
418 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
420 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
421 else if(it == player)
422 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
423 else if(SAME_TEAM(it, sender))
424 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
427 // create new waypoint
428 ctf_FlagcarrierWaypoints(player);
430 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
431 player.throw_antispam = sender.throw_antispam;
433 flag.pass_distance = 0;
434 flag.pass_sender = NULL;
435 flag.pass_target = NULL;
438 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
440 entity flag = player.flagcarried;
441 vector targ_origin, flag_velocity;
443 if(!flag) { return; }
444 if((droptype == DROP_PASS) && !receiver) { return; }
446 if(flag.speedrunning)
448 // ensure old waypoints are removed before resetting the flag
449 WaypointSprite_Kill(player.wps_flagcarrier);
451 if(player.wps_enemyflagcarrier)
452 WaypointSprite_Kill(player.wps_enemyflagcarrier);
454 if(player.wps_flagreturn)
455 WaypointSprite_Kill(player.wps_flagreturn);
456 ctf_RespawnFlag(flag);
461 setattachment(flag, NULL, "");
462 tracebox(player.origin - FLAG_DROP_OFFSET, flag.m_mins, flag.m_maxs, player.origin + FLAG_DROP_OFFSET, MOVE_NOMONSTERS, flag);
463 setorigin(flag, trace_endpos);
464 flag.owner.flagcarried = NULL;
465 GameRules_scoring_vip(flag.owner, false);
467 flag.solid = SOLID_TRIGGER;
468 flag.ctf_dropper = player;
469 flag.ctf_droptime = time;
471 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
478 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
479 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
480 WarpZone_RefSys_Copy(flag, receiver);
481 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
482 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
484 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
485 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
488 set_movetype(flag, MOVETYPE_FLY);
489 flag.takedamage = DAMAGE_NO;
490 flag.pass_sender = player;
491 flag.pass_target = receiver;
492 flag.ctf_status = FLAG_PASSING;
495 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
496 WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
497 ctf_EventLog("pass", flag.team, player);
503 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'));
505 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)));
506 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
507 ctf_Handle_Drop(flag, player, droptype);
508 navigation_dynamicgoal_set(flag, player);
514 flag.velocity = '0 0 0'; // do nothing
521 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);
522 ctf_Handle_Drop(flag, player, droptype);
523 navigation_dynamicgoal_set(flag, player);
528 // kill old waypointsprite
529 WaypointSprite_Ping(player.wps_flagcarrier);
530 WaypointSprite_Kill(player.wps_flagcarrier);
532 if(player.wps_enemyflagcarrier)
533 WaypointSprite_Kill(player.wps_enemyflagcarrier);
535 if(player.wps_flagreturn)
536 WaypointSprite_Kill(player.wps_flagreturn);
539 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
543 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
545 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
553 void nades_GiveBonus(entity player, float score);
555 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
557 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
558 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
559 entity player_team_flag = NULL, tmp_entity;
560 float old_time, new_time;
562 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
563 if(CTF_DIFFTEAM(player, flag)) { return; }
564 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)
566 if (toucher.goalentity == flag.bot_basewaypoint)
567 toucher.goalentity_lock_timeout = 0;
570 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
571 if(SAME_TEAM(tmp_entity, player))
573 player_team_flag = tmp_entity;
577 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
579 player.throw_prevtime = time;
580 player.throw_count = 0;
582 // messages and sounds
583 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
584 ctf_CaptureRecord(enemy_flag, player);
585 _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);
589 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
590 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
596 if(enemy_flag.score_capture || flag.score_capture)
597 pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
598 GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
600 if(enemy_flag.score_team_capture || flag.score_team_capture)
601 capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
602 GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
604 old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
605 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
606 if(!old_time || new_time < old_time)
607 GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
610 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
612 shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
616 if(capturetype == CAPTURE_NORMAL)
618 WaypointSprite_Kill(player.wps_flagcarrier);
619 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
621 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
622 { GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist)); }
625 flag.enemy = toucher;
628 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
629 ctf_RespawnFlag(enemy_flag);
632 void ctf_Handle_Return(entity flag, entity player)
634 // messages and sounds
635 if(IS_MONSTER(player))
637 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
641 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
642 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
644 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
645 ctf_EventLog("return", flag.team, player);
648 if(IS_PLAYER(player))
650 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
651 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
653 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
656 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
660 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
661 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
662 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
666 if(player.flagcarried == flag)
667 WaypointSprite_Kill(player.wps_flagcarrier);
672 ctf_RespawnFlag(flag);
675 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
678 float pickup_dropped_score; // used to calculate dropped pickup score
680 // attach the flag to the player
682 player.flagcarried = flag;
683 GameRules_scoring_vip(player, true);
686 setattachment(flag, player.vehicle, "");
687 setorigin(flag, VEHICLE_FLAG_OFFSET);
688 flag.scale = VEHICLE_FLAG_SCALE;
692 setattachment(flag, player, "");
693 setorigin(flag, FLAG_CARRY_OFFSET);
697 set_movetype(flag, MOVETYPE_NONE);
698 flag.takedamage = DAMAGE_NO;
699 flag.solid = SOLID_NOT;
700 flag.angles = '0 0 0';
701 flag.ctf_status = FLAG_CARRY;
705 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
706 case PICKUP_DROPPED: SetResourceExplicit(flag, RES_HEALTH, flag.max_health); break; // reset health/return timelimit
710 // messages and sounds
711 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
713 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
715 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
716 else if(CTF_DIFFTEAM(player, flag))
717 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
719 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
721 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
724 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); });
727 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
728 if(CTF_SAMETEAM(flag, it))
730 if(SAME_TEAM(player, it))
731 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
733 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);
737 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
740 GameRules_scoring_add(player, CTF_PICKUPS, 1);
741 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
746 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
747 ctf_EventLog("steal", flag.team, player);
753 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);
754 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);
755 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
756 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
757 ctf_EventLog("pickup", flag.team, player);
765 if(pickuptype == PICKUP_BASE)
767 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
768 if((player.speedrunning) && (ctf_captimerecord))
769 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
773 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
776 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
777 ctf_FlagcarrierWaypoints(player);
778 WaypointSprite_Ping(player.wps_flagcarrier);
782 // ===================
783 // Main Flag Functions
784 // ===================
786 void ctf_CheckFlagReturn(entity flag, int returntype)
788 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
790 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH)); }
792 if((GetResource(flag, RES_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
797 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
799 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
800 case RETURN_SPEEDRUN:
801 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
802 case RETURN_NEEDKILL:
803 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
806 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
808 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
809 ctf_EventLog("returned", flag.team, NULL);
811 ctf_RespawnFlag(flag);
816 bool ctf_Stalemate_Customize(entity this, entity client)
818 // make spectators see what the player would see
819 entity e = WaypointSprite_getviewentity(client);
820 entity wp_owner = this.owner;
823 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
824 if(SAME_TEAM(wp_owner, e)) { return false; }
825 if(!IS_PLAYER(e)) { return false; }
830 void ctf_CheckStalemate()
833 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
836 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
838 // build list of stale flags
839 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
841 if(autocvar_g_ctf_stalemate)
842 if(tmp_entity.ctf_status != FLAG_BASE)
843 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
845 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
846 ctf_staleflaglist = tmp_entity;
848 switch(tmp_entity.team)
850 case NUM_TEAM_1: ++stale_red_flags; break;
851 case NUM_TEAM_2: ++stale_blue_flags; break;
852 case NUM_TEAM_3: ++stale_yellow_flags; break;
853 case NUM_TEAM_4: ++stale_pink_flags; break;
854 default: ++stale_neutral_flags; break;
860 stale_flags = (stale_neutral_flags >= 1);
862 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
864 if(ctf_oneflag && stale_flags == 1)
865 ctf_stalemate = true;
866 else if(stale_flags >= 2)
867 ctf_stalemate = true;
868 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
869 { ctf_stalemate = false; wpforenemy_announced = false; }
870 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
871 { ctf_stalemate = false; wpforenemy_announced = false; }
873 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
876 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
878 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
880 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);
881 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
882 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
886 if (!wpforenemy_announced)
888 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)); });
890 wpforenemy_announced = true;
895 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
897 if(ITEM_DAMAGE_NEEDKILL(deathtype))
899 if(autocvar_g_ctf_flag_return_damage_delay)
900 this.ctf_flagdamaged_byworld = true;
903 SetResourceExplicit(this, RES_HEALTH, 0);
904 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
908 if(autocvar_g_ctf_flag_return_damage)
910 // reduce health and check if it should be returned
911 TakeResource(this, RES_HEALTH, damage);
912 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
917 void ctf_FlagThink(entity this)
922 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
925 if(this == ctf_worldflaglist) // only for the first flag
926 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
929 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
930 LOG_TRACE("wtf the flag got squashed?");
931 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
932 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
933 setsize(this, this.m_mins, this.m_maxs);
937 switch(this.ctf_status)
941 if(autocvar_g_ctf_dropped_capture_radius)
943 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
944 if(tmp_entity.ctf_status == FLAG_DROPPED)
945 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
946 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
947 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
954 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
956 if(autocvar_g_ctf_flag_dropped_floatinwater)
958 vector midpoint = ((this.absmin + this.absmax) * 0.5);
959 if(pointcontents(midpoint) == CONTENT_WATER)
961 this.velocity = this.velocity * 0.5;
963 if (pointcontents(midpoint + eZ * FLAG_FLOAT_OFFSET_Z) == CONTENT_WATER)
964 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
966 { set_movetype(this, MOVETYPE_FLY); }
968 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
970 if(autocvar_g_ctf_flag_return_dropped)
972 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
974 SetResourceExplicit(this, RES_HEALTH, 0);
975 ctf_CheckFlagReturn(this, RETURN_DROPPED);
979 if(this.ctf_flagdamaged_byworld)
981 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
982 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
985 else if(autocvar_g_ctf_flag_return_time)
987 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
988 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
996 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
998 SetResourceExplicit(this, RES_HEALTH, 0);
999 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
1001 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
1002 ImpulseCommands(this.owner);
1004 if(autocvar_g_ctf_stalemate)
1006 if(time >= wpforenemy_nextthink)
1008 ctf_CheckStalemate();
1009 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1012 if(CTF_SAMETEAM(this, this.owner) && this.team)
1014 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1015 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
1016 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
1017 ctf_Handle_Return(this, this.owner);
1024 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1025 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1026 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1028 if((this.pass_target == NULL)
1029 || (IS_DEAD(this.pass_target))
1030 || (this.pass_target.flagcarried)
1031 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1032 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1033 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1035 // give up, pass failed
1036 ctf_Handle_Drop(this, NULL, DROP_PASS);
1040 // still a viable target, go for it
1041 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1046 default: // this should never happen
1048 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1054 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1057 if(game_stopped) return;
1058 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1060 bool is_not_monster = (!IS_MONSTER(toucher));
1062 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1063 if(ITEM_TOUCH_NEEDKILL())
1065 if(!autocvar_g_ctf_flag_return_damage_delay)
1067 SetResourceExplicit(flag, RES_HEALTH, 0);
1068 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1070 if(!flag.ctf_flagdamaged_byworld) { return; }
1073 // special touch behaviors
1074 if(STAT(FROZEN, toucher)) { return; }
1075 else if(IS_VEHICLE(toucher))
1077 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1078 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1080 return; // do nothing
1082 else if(IS_MONSTER(toucher))
1084 if(!autocvar_g_ctf_allow_monster_touch)
1085 return; // do nothing
1087 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1089 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1091 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1092 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1093 flag.wait = time + FLAG_TOUCHRATE;
1097 else if(IS_DEAD(toucher)) { return; }
1099 switch(flag.ctf_status)
1105 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1106 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1107 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1108 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1110 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1111 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1112 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)
1114 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1115 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1117 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1118 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1124 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1125 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1126 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1127 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1133 LOG_TRACE("Someone touched a flag even though it was being carried?");
1139 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1141 if(DIFF_TEAM(toucher, flag.pass_sender))
1143 if(ctf_Immediate_Return_Allowed(flag, toucher))
1144 ctf_Handle_Return(flag, toucher);
1145 else if(is_not_monster && (!toucher.flagcarried))
1146 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1148 else if(!toucher.flagcarried)
1149 ctf_Handle_Retrieve(flag, toucher);
1156 .float last_respawn;
1157 void ctf_RespawnFlag(entity flag)
1159 flag.watertype = CONTENT_EMPTY; // TODO: it is unclear why this workaround is needed, likely many other potential breakage points!!
1160 // check for flag respawn being called twice in a row
1161 if(flag.last_respawn > time - 0.5)
1162 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1164 flag.last_respawn = time;
1166 // reset the player (if there is one)
1167 if((flag.owner) && (flag.owner.flagcarried == flag))
1169 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1170 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1171 WaypointSprite_Kill(flag.wps_flagcarrier);
1173 flag.owner.flagcarried = NULL;
1174 GameRules_scoring_vip(flag.owner, false);
1176 if(flag.speedrunning)
1177 ctf_FakeTimeLimit(flag.owner, -1);
1180 if((flag.owner) && (flag.owner.vehicle))
1181 flag.scale = FLAG_SCALE;
1183 if(flag.ctf_status == FLAG_DROPPED)
1184 { WaypointSprite_Kill(flag.wps_flagdropped); }
1187 setattachment(flag, NULL, "");
1188 setorigin(flag, flag.ctf_spawnorigin);
1190 //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1191 set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1192 flag.takedamage = DAMAGE_NO;
1193 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1194 flag.solid = SOLID_TRIGGER;
1195 flag.velocity = '0 0 0';
1196 flag.angles = flag.mangle;
1197 flag.flags = FL_ITEM | FL_NOTARGET;
1199 flag.ctf_status = FLAG_BASE;
1201 flag.pass_distance = 0;
1202 flag.pass_sender = NULL;
1203 flag.pass_target = NULL;
1204 flag.ctf_dropper = NULL;
1205 flag.ctf_pickuptime = 0;
1206 flag.ctf_droptime = 0;
1207 flag.ctf_flagdamaged_byworld = false;
1208 navigation_dynamicgoal_unset(flag);
1210 ctf_CheckStalemate();
1213 void ctf_Reset(entity this)
1215 if(this.owner && IS_PLAYER(this.owner))
1216 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1219 ctf_RespawnFlag(this);
1222 bool ctf_FlagBase_Customize(entity this, entity client)
1224 entity e = WaypointSprite_getviewentity(client);
1225 entity wp_owner = this.owner;
1226 entity flag = e.flagcarried;
1227 if(flag && CTF_SAMETEAM(e, flag))
1229 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1234 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1237 waypoint_spawnforitem_force(this, this.origin);
1238 navigation_dynamicgoal_init(this, true);
1244 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1245 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1246 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1247 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1248 default: basename = WP_FlagBaseNeutral; break;
1251 if(autocvar_g_ctf_flag_waypoint)
1253 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1254 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1255 wp.fade_rate = autocvar_g_ctf_flag_waypoint_maxdistance;
1256 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1257 setcefc(wp, ctf_FlagBase_Customize);
1260 // captureshield setup
1261 ctf_CaptureShield_Spawn(this);
1266 void ctf_FlagSetup(int teamnum, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1269 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1270 ctf_worldflaglist = flag;
1272 setattachment(flag, NULL, "");
1274 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnum), Team_ColorName_Upper(teamnum)));
1275 flag.team = teamnum;
1276 flag.classname = "item_flag_team";
1277 flag.target = "###item###"; // for finding the nearest item using findnearest
1278 flag.flags = FL_ITEM | FL_NOTARGET;
1279 IL_PUSH(g_items, flag);
1280 flag.solid = SOLID_TRIGGER;
1281 flag.takedamage = DAMAGE_NO;
1282 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1283 flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1284 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1285 flag.event_damage = ctf_FlagDamage;
1286 flag.pushable = true;
1287 flag.teleportable = TELEPORT_NORMAL;
1288 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1289 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1290 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1291 if(flag.damagedbycontents)
1292 IL_PUSH(g_damagedbycontents, flag);
1293 flag.velocity = '0 0 0';
1294 flag.mangle = flag.angles;
1295 flag.reset = ctf_Reset;
1296 settouch(flag, ctf_FlagTouch);
1297 setthink(flag, ctf_FlagThink);
1298 flag.nextthink = time + FLAG_THINKRATE;
1299 flag.ctf_status = FLAG_BASE;
1301 // crudely force them all to 0
1302 if(autocvar_g_ctf_score_ignore_fields)
1303 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1305 string teamname = Static_Team_ColorName_Lower(teamnum);
1307 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1308 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1309 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1310 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnum).eent_eff_name; }
1311 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnum).eent_eff_name; }
1312 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnum).eent_eff_name; }
1316 if(flag.s == "") flag.s = b; \
1317 precache_sound(flag.s);
1319 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnum))))
1320 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnum))))
1321 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnum))))
1322 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnum))))
1323 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1324 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1325 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1329 precache_model(flag.model);
1332 _setmodel(flag, flag.model); // precision set below
1333 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1334 flag.m_mins = flag.mins; // store these for squash checks
1335 flag.m_maxs = flag.maxs;
1336 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1338 if(autocvar_g_ctf_flag_glowtrails)
1342 case NUM_TEAM_1: flag.glow_color = 251; break;
1343 case NUM_TEAM_2: flag.glow_color = 210; break;
1344 case NUM_TEAM_3: flag.glow_color = 110; break;
1345 case NUM_TEAM_4: flag.glow_color = 145; break;
1346 default: flag.glow_color = 254; break;
1348 flag.glow_size = 25;
1349 flag.glow_trail = 1;
1352 flag.effects |= EF_LOWPRECISION;
1353 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1354 if(autocvar_g_ctf_dynamiclights)
1358 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1359 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1360 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1361 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1362 default: flag.effects |= EF_DIMLIGHT; break;
1367 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1369 flag.dropped_origin = flag.origin;
1370 flag.noalign = true;
1371 set_movetype(flag, MOVETYPE_NONE);
1373 else // drop to floor, automatically find a platform and set that as spawn origin
1375 flag.noalign = false;
1377 set_movetype(flag, MOVETYPE_NONE);
1380 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1388 // NOTE: LEGACY CODE, needs to be re-written!
1390 void havocbot_ctf_calculate_middlepoint()
1394 vector fo = '0 0 0';
1397 f = ctf_worldflaglist;
1402 f = f.ctf_worldflagnext;
1408 havocbot_middlepoint = s / n;
1409 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1411 havocbot_symmetry_axis_m = 0;
1412 havocbot_symmetry_axis_q = 0;
1415 // for symmetrical editing of waypoints
1416 entity f1 = ctf_worldflaglist;
1417 entity f2 = f1.ctf_worldflagnext;
1418 float m = -(f1.origin.y - f2.origin.y) / (max(f1.origin.x - f2.origin.x, FLOAT_EPSILON));
1419 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1420 havocbot_symmetry_axis_m = m;
1421 havocbot_symmetry_axis_q = q;
1423 havocbot_symmetry_origin_order = n;
1427 entity havocbot_ctf_find_flag(entity bot)
1430 f = ctf_worldflaglist;
1433 if (CTF_SAMETEAM(bot, f))
1435 f = f.ctf_worldflagnext;
1440 entity havocbot_ctf_find_enemy_flag(entity bot)
1443 f = ctf_worldflaglist;
1448 if(CTF_DIFFTEAM(bot, f))
1455 else if(!bot.flagcarried)
1459 else if (CTF_DIFFTEAM(bot, f))
1461 f = f.ctf_worldflagnext;
1466 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1473 FOREACH_CLIENT(IS_PLAYER(it), {
1474 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1477 if(vdist(it.origin - org, <, tc_radius))
1486 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1489 head = ctf_worldflaglist;
1492 if (CTF_SAMETEAM(this, head))
1494 head = head.ctf_worldflagnext;
1497 navigation_routerating(this, head, ratingscale, 10000);
1501 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1504 head = ctf_worldflaglist;
1507 if (CTF_SAMETEAM(this, head))
1509 if (this.flagcarried)
1510 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1512 head = head.ctf_worldflagnext; // skip base if it has a different group
1517 head = head.ctf_worldflagnext;
1522 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1525 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1528 head = ctf_worldflaglist;
1533 if(CTF_DIFFTEAM(this, head))
1537 if(this.flagcarried)
1540 else if(!this.flagcarried)
1544 else if(CTF_DIFFTEAM(this, head))
1546 head = head.ctf_worldflagnext;
1550 if (head.ctf_status == FLAG_CARRY)
1552 // adjust rating of our flag carrier depending on his health
1553 head = head.tag_entity;
1554 float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1555 ratingscale += ratingscale * f * 0.1;
1557 navigation_routerating(this, head, ratingscale, 10000);
1561 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1563 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1565 if (!bot_waypoints_for_items)
1567 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1573 head = havocbot_ctf_find_enemy_flag(this);
1578 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1581 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1585 mf = havocbot_ctf_find_flag(this);
1587 if(mf.ctf_status == FLAG_BASE)
1591 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1594 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1597 head = ctf_worldflaglist;
1600 // flag is out in the field
1601 if(head.ctf_status != FLAG_BASE)
1602 if(head.tag_entity==NULL) // dropped
1606 if(vdist(org - head.origin, <, df_radius))
1607 navigation_routerating(this, head, ratingscale, 10000);
1610 navigation_routerating(this, head, ratingscale, 10000);
1613 head = head.ctf_worldflagnext;
1617 void havocbot_ctf_reset_role(entity this)
1619 float cdefense, cmiddle, coffense;
1626 if (this.flagcarried)
1628 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1632 mf = havocbot_ctf_find_flag(this);
1633 ef = havocbot_ctf_find_enemy_flag(this);
1635 // Retrieve stolen flag
1636 if(mf.ctf_status!=FLAG_BASE)
1638 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1642 // If enemy flag is taken go to the middle to intercept pursuers
1643 if(ef.ctf_status!=FLAG_BASE)
1645 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1649 // if there is no one else on the team switch to offense
1651 // don't check if this bot is a player since it isn't true when the bot is added to the server
1652 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1656 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1659 else if (time < CS(this).jointime + 1)
1661 // if bots spawn all at once set good default roles
1664 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1667 else if (count == 2)
1669 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1674 // Evaluate best position to take
1675 // Count mates on middle position
1676 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1678 // Count mates on defense position
1679 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1681 // Count mates on offense position
1682 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1684 if(cdefense<=coffense)
1685 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1686 else if(coffense<=cmiddle)
1687 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1689 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1691 // if bots spawn all at once assign them a more appropriated role after a while
1692 if (time < CS(this).jointime + 1 && count > 2)
1693 this.havocbot_role_timeout = time + 10 + random() * 10;
1696 bool havocbot_ctf_is_basewaypoint(entity item)
1698 if (item.classname != "waypoint")
1701 entity head = ctf_worldflaglist;
1704 if (item == head.bot_basewaypoint)
1706 head = head.ctf_worldflagnext;
1711 void havocbot_role_ctf_carrier(entity this)
1715 havocbot_ctf_reset_role(this);
1719 if (this.flagcarried == NULL)
1721 havocbot_ctf_reset_role(this);
1725 if (navigation_goalrating_timeout(this))
1727 navigation_goalrating_start(this);
1730 entity mf = havocbot_ctf_find_flag(this);
1731 vector base_org = mf.dropped_origin;
1732 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1734 havocbot_goalrating_ctf_enemybase(this, base_rating);
1736 havocbot_goalrating_ctf_ourbase(this, base_rating);
1738 // start collecting items very close to the bot but only inside of own base radius
1739 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1740 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1742 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1744 navigation_goalrating_end(this);
1746 navigation_goalrating_timeout_set(this);
1748 entity goal = this.goalentity;
1749 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1750 this.goalentity_lock_timeout = time + ((this.bot_aimtarg) ? 2 : 3);
1753 this.havocbot_cantfindflag = time + 10;
1754 else if (time > this.havocbot_cantfindflag)
1756 // Can't navigate to my own base, suicide!
1757 // TODO: drop it and wander around
1758 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1764 void havocbot_role_ctf_escort(entity this)
1770 havocbot_ctf_reset_role(this);
1774 if (this.flagcarried)
1776 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1780 // If enemy flag is back on the base switch to previous role
1781 ef = havocbot_ctf_find_enemy_flag(this);
1782 if(ef.ctf_status==FLAG_BASE)
1784 this.havocbot_role = this.havocbot_previous_role;
1785 this.havocbot_role_timeout = 0;
1788 if (ef.ctf_status == FLAG_DROPPED)
1790 navigation_goalrating_timeout_expire(this, 1);
1794 // If the flag carrier reached the base switch to defense
1795 mf = havocbot_ctf_find_flag(this);
1796 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1798 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1802 // Set the role timeout if necessary
1803 if (!this.havocbot_role_timeout)
1805 this.havocbot_role_timeout = time + random() * 30 + 60;
1808 // If nothing happened just switch to previous role
1809 if (time > this.havocbot_role_timeout)
1811 this.havocbot_role = this.havocbot_previous_role;
1812 this.havocbot_role_timeout = 0;
1816 // Chase the flag carrier
1817 if (navigation_goalrating_timeout(this))
1819 navigation_goalrating_start(this);
1822 havocbot_goalrating_ctf_enemyflag(this, 10000);
1823 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1824 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1826 navigation_goalrating_end(this);
1828 navigation_goalrating_timeout_set(this);
1832 void havocbot_role_ctf_offense(entity this)
1839 havocbot_ctf_reset_role(this);
1843 if (this.flagcarried)
1845 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1850 mf = havocbot_ctf_find_flag(this);
1851 ef = havocbot_ctf_find_enemy_flag(this);
1854 if(mf.ctf_status!=FLAG_BASE)
1857 pos = mf.tag_entity.origin;
1861 // Try to get it if closer than the enemy base
1862 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1864 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1869 // Escort flag carrier
1870 if(ef.ctf_status!=FLAG_BASE)
1873 pos = ef.tag_entity.origin;
1877 if(vdist(pos - mf.dropped_origin, >, 700))
1879 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1884 // Set the role timeout if necessary
1885 if (!this.havocbot_role_timeout)
1886 this.havocbot_role_timeout = time + 120;
1888 if (time > this.havocbot_role_timeout)
1890 havocbot_ctf_reset_role(this);
1894 if (navigation_goalrating_timeout(this))
1896 navigation_goalrating_start(this);
1899 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1900 havocbot_goalrating_ctf_enemybase(this, 10000);
1901 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1903 navigation_goalrating_end(this);
1905 navigation_goalrating_timeout_set(this);
1909 // Retriever (temporary role):
1910 void havocbot_role_ctf_retriever(entity this)
1916 havocbot_ctf_reset_role(this);
1920 if (this.flagcarried)
1922 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1926 // If flag is back on the base switch to previous role
1927 mf = havocbot_ctf_find_flag(this);
1928 if(mf.ctf_status==FLAG_BASE)
1930 if (mf.enemy == this) // did this bot return the flag?
1931 navigation_goalrating_timeout_force(this);
1932 havocbot_ctf_reset_role(this);
1936 if (!this.havocbot_role_timeout)
1937 this.havocbot_role_timeout = time + 20;
1939 if (time > this.havocbot_role_timeout)
1941 havocbot_ctf_reset_role(this);
1945 if (navigation_goalrating_timeout(this))
1947 const float RT_RADIUS = 10000;
1949 navigation_goalrating_start(this);
1952 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1953 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1954 havocbot_goalrating_ctf_enemybase(this, 8000);
1955 entity ef = havocbot_ctf_find_enemy_flag(this);
1956 vector enemy_base_org = ef.dropped_origin;
1957 // start collecting items very close to the bot but only inside of enemy base radius
1958 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1959 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1960 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1962 navigation_goalrating_end(this);
1964 navigation_goalrating_timeout_set(this);
1968 void havocbot_role_ctf_middle(entity this)
1974 havocbot_ctf_reset_role(this);
1978 if (this.flagcarried)
1980 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1984 mf = havocbot_ctf_find_flag(this);
1985 if(mf.ctf_status!=FLAG_BASE)
1987 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1991 if (!this.havocbot_role_timeout)
1992 this.havocbot_role_timeout = time + 10;
1994 if (time > this.havocbot_role_timeout)
1996 havocbot_ctf_reset_role(this);
2000 if (navigation_goalrating_timeout(this))
2004 org = havocbot_middlepoint;
2005 org.z = this.origin.z;
2007 navigation_goalrating_start(this);
2010 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
2011 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
2012 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2013 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2014 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2015 havocbot_goalrating_ctf_enemybase(this, 3000);
2017 navigation_goalrating_end(this);
2019 entity goal = this.goalentity;
2020 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
2021 this.goalentity_lock_timeout = time + 2;
2023 navigation_goalrating_timeout_set(this);
2027 void havocbot_role_ctf_defense(entity this)
2033 havocbot_ctf_reset_role(this);
2037 if (this.flagcarried)
2039 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2043 // If own flag was captured
2044 mf = havocbot_ctf_find_flag(this);
2045 if(mf.ctf_status!=FLAG_BASE)
2047 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2051 if (!this.havocbot_role_timeout)
2052 this.havocbot_role_timeout = time + 30;
2054 if (time > this.havocbot_role_timeout)
2056 havocbot_ctf_reset_role(this);
2059 if (navigation_goalrating_timeout(this))
2061 vector org = mf.dropped_origin;
2063 navigation_goalrating_start(this);
2065 // if enemies are closer to our base, go there
2066 entity closestplayer = NULL;
2067 float distance, bestdistance = 10000;
2068 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2069 distance = vlen(org - it.origin);
2070 if(distance<bestdistance)
2073 bestdistance = distance;
2079 if(DIFF_TEAM(closestplayer, this))
2080 if(vdist(org - this.origin, >, 1000))
2081 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2082 havocbot_goalrating_ctf_ourbase(this, 10000);
2084 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2085 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2086 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2087 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2088 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2090 navigation_goalrating_end(this);
2092 navigation_goalrating_timeout_set(this);
2096 void havocbot_role_ctf_setrole(entity bot, int role)
2098 string s = "(null)";
2101 case HAVOCBOT_CTF_ROLE_CARRIER:
2103 bot.havocbot_role = havocbot_role_ctf_carrier;
2104 bot.havocbot_role_timeout = 0;
2105 bot.havocbot_cantfindflag = time + 10;
2106 if (bot.havocbot_previous_role != bot.havocbot_role)
2107 navigation_goalrating_timeout_force(bot);
2109 case HAVOCBOT_CTF_ROLE_DEFENSE:
2111 bot.havocbot_role = havocbot_role_ctf_defense;
2112 bot.havocbot_role_timeout = 0;
2114 case HAVOCBOT_CTF_ROLE_MIDDLE:
2116 bot.havocbot_role = havocbot_role_ctf_middle;
2117 bot.havocbot_role_timeout = 0;
2119 case HAVOCBOT_CTF_ROLE_OFFENSE:
2121 bot.havocbot_role = havocbot_role_ctf_offense;
2122 bot.havocbot_role_timeout = 0;
2124 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2126 bot.havocbot_previous_role = bot.havocbot_role;
2127 bot.havocbot_role = havocbot_role_ctf_retriever;
2128 bot.havocbot_role_timeout = time + 10;
2129 if (bot.havocbot_previous_role != bot.havocbot_role)
2130 navigation_goalrating_timeout_expire(bot, 2);
2132 case HAVOCBOT_CTF_ROLE_ESCORT:
2134 bot.havocbot_previous_role = bot.havocbot_role;
2135 bot.havocbot_role = havocbot_role_ctf_escort;
2136 bot.havocbot_role_timeout = time + 30;
2137 if (bot.havocbot_previous_role != bot.havocbot_role)
2138 navigation_goalrating_timeout_expire(bot, 2);
2141 LOG_TRACE(bot.netname, " switched to ", s);
2149 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2151 entity player = M_ARGV(0, entity);
2153 int t = 0, t2 = 0, t3 = 0;
2154 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)
2156 // initially clear items so they can be set as necessary later.
2157 STAT(CTF_FLAGSTATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2158 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2159 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2160 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2161 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2162 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2164 // scan through all the flags and notify the client about them
2165 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2167 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2168 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2169 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2170 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2171 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; }
2173 switch(flag.ctf_status)
2178 if((flag.owner == player) || (flag.pass_sender == player))
2179 STAT(CTF_FLAGSTATUS, player) |= t; // carrying: player is currently carrying the flag
2181 STAT(CTF_FLAGSTATUS, player) |= t2; // taken: someone else is carrying the flag
2186 STAT(CTF_FLAGSTATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2192 // item for stopping players from capturing the flag too often
2193 if(player.ctf_captureshielded)
2194 STAT(CTF_FLAGSTATUS, player) |= CTF_SHIELDED;
2197 STAT(CTF_FLAGSTATUS, player) |= CTF_STALEMATE;
2199 // update the health of the flag carrier waypointsprite
2200 if(player.wps_flagcarrier)
2201 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);
2204 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2206 entity frag_attacker = M_ARGV(1, entity);
2207 entity frag_target = M_ARGV(2, entity);
2208 float frag_damage = M_ARGV(4, float);
2209 vector frag_force = M_ARGV(6, vector);
2211 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2213 if(frag_target == frag_attacker) // damage done to yourself
2215 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2216 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2218 else // damage done to everyone else
2220 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2221 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2224 M_ARGV(4, float) = frag_damage;
2225 M_ARGV(6, vector) = frag_force;
2227 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2229 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
2230 && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2232 frag_target.wps_helpme_time = time;
2233 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2235 // todo: add notification for when flag carrier needs help?
2239 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2241 entity frag_attacker = M_ARGV(1, entity);
2242 entity frag_target = M_ARGV(2, entity);
2244 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2246 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2247 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2250 if(frag_target.flagcarried)
2252 entity tmp_entity = frag_target.flagcarried;
2253 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2254 tmp_entity.ctf_dropper = NULL;
2258 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2260 M_ARGV(2, float) = 0; // frag score
2261 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2264 void ctf_RemovePlayer(entity player)
2266 if(player.flagcarried)
2267 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2269 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2271 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2272 if(flag.pass_target == player) { flag.pass_target = NULL; }
2273 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2277 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2279 entity player = M_ARGV(0, entity);
2281 ctf_RemovePlayer(player);
2284 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2286 entity player = M_ARGV(0, entity);
2288 ctf_RemovePlayer(player);
2291 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2293 if(!autocvar_g_ctf_leaderboard)
2296 entity player = M_ARGV(0, entity);
2298 if(IS_REAL_CLIENT(player))
2300 int m = min(RANKINGS_CNT, autocvar_g_cts_send_rankings_cnt);
2301 race_send_rankings_cnt(MSG_ONE);
2302 for (int i = 1; i <= m; ++i)
2304 race_SendRankings(i, 0, 0, MSG_ONE);
2309 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2311 if(!autocvar_g_ctf_leaderboard)
2314 entity player = M_ARGV(0, entity);
2316 if(CS(player).cvar_cl_allow_uidtracking == 1 && CS(player).cvar_cl_allow_uid2name == 1)
2318 if (!player.stored_netname)
2319 player.stored_netname = strzone(uid2name(player.crypto_idfp));
2320 if(player.stored_netname != player.netname)
2322 db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
2323 strcpy(player.stored_netname, player.netname);
2328 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2330 entity player = M_ARGV(0, entity);
2332 if(player.flagcarried)
2333 if(!autocvar_g_ctf_portalteleport)
2334 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2337 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2339 if(MUTATOR_RETURNVALUE || game_stopped) return;
2341 entity player = M_ARGV(0, entity);
2343 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2345 // pass the flag to a team mate
2346 if(autocvar_g_ctf_pass)
2348 entity head, closest_target = NULL;
2349 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2351 while(head) // find the closest acceptable target to pass to
2353 if(IS_PLAYER(head) && !IS_DEAD(head))
2354 if(head != player && SAME_TEAM(head, player))
2355 if(!head.speedrunning && !head.vehicle)
2357 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2358 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2359 vector passer_center = CENTER_OR_VIEWOFS(player);
2361 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2363 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2365 if(IS_BOT_CLIENT(head))
2367 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2368 ctf_Handle_Throw(head, player, DROP_PASS);
2372 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2373 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2375 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2378 else if(player.flagcarried && !head.flagcarried)
2382 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2383 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2384 { closest_target = head; }
2386 else { closest_target = head; }
2393 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2396 // throw the flag in front of you
2397 if(autocvar_g_ctf_throw && player.flagcarried)
2399 if(player.throw_count == -1)
2401 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2403 player.throw_prevtime = time;
2404 player.throw_count = 1;
2405 ctf_Handle_Throw(player, NULL, DROP_THROW);
2410 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2416 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2417 else { player.throw_count += 1; }
2418 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2420 player.throw_prevtime = time;
2421 ctf_Handle_Throw(player, NULL, DROP_THROW);
2428 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2430 entity player = M_ARGV(0, entity);
2432 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2434 player.wps_helpme_time = time;
2435 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2437 else // create a normal help me waypointsprite
2439 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2440 WaypointSprite_Ping(player.wps_helpme);
2446 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2448 entity player = M_ARGV(0, entity);
2449 entity veh = M_ARGV(1, entity);
2451 if(player.flagcarried)
2453 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2455 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2459 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2460 setattachment(player.flagcarried, veh, "");
2461 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2462 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2463 //player.flagcarried.angles = '0 0 0';
2469 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2471 entity player = M_ARGV(0, entity);
2473 if(player.flagcarried)
2475 setattachment(player.flagcarried, player, "");
2476 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2477 player.flagcarried.scale = FLAG_SCALE;
2478 player.flagcarried.angles = '0 0 0';
2479 player.flagcarried.nodrawtoclient = NULL;
2484 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2486 entity player = M_ARGV(0, entity);
2488 if(player.flagcarried)
2490 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2491 ctf_RespawnFlag(player.flagcarried);
2496 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2498 entity flag; // temporary entity for the search method
2500 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2502 switch(flag.ctf_status)
2507 // lock the flag, game is over
2508 set_movetype(flag, MOVETYPE_NONE);
2509 flag.takedamage = DAMAGE_NO;
2510 flag.solid = SOLID_NOT;
2511 flag.nextthink = false; // stop thinking
2513 //dprint("stopping the ", flag.netname, " from moving.\n");
2521 // do nothing for these flags
2528 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2530 entity bot = M_ARGV(0, entity);
2532 havocbot_ctf_reset_role(bot);
2536 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2538 M_ARGV(1, string) = "ctf_team";
2541 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2543 entity spectatee = M_ARGV(0, entity);
2544 entity client = M_ARGV(1, entity);
2546 STAT(CTF_FLAGSTATUS, client) = STAT(CTF_FLAGSTATUS, spectatee);
2549 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2551 int record_page = M_ARGV(0, int);
2552 string ret_string = M_ARGV(1, string);
2554 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2556 if (MapInfo_Get_ByID(i))
2558 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2564 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2565 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2569 M_ARGV(1, string) = ret_string;
2572 bool superspec_Spectate(entity this, entity targ); // TODO
2573 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2574 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2576 entity player = M_ARGV(0, entity);
2577 string cmd_name = M_ARGV(1, string);
2578 int cmd_argc = M_ARGV(2, int);
2580 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2582 if(cmd_name == "followfc")
2594 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2595 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2596 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2597 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2601 FOREACH_CLIENT(IS_PLAYER(it), {
2602 if(it.flagcarried && (it.team == _team || _team == 0))
2605 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2606 continue; // already spectating this fc, try another
2607 return superspec_Spectate(player, it);
2612 superspec_msg("", "", player, "No active flag carrier\n", 1);
2617 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2619 entity frag_target = M_ARGV(0, entity);
2621 if(frag_target.flagcarried)
2622 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2625 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2627 entity player = M_ARGV(0, entity);
2628 if(player.flagcarried)
2629 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2637 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2638 CTF flag for team one (Red).
2640 "angle" Angle the flag will point (minus 90 degrees)...
2641 "model" model to use, note this needs red and blue as skins 0 and 1...
2642 "noise" sound played when flag is picked up...
2643 "noise1" sound played when flag is returned by a teammate...
2644 "noise2" sound played when flag is captured...
2645 "noise3" sound played when flag is lost in the field and respawns itself...
2646 "noise4" sound played when flag is dropped by a player...
2647 "noise5" sound played when flag touches the ground... */
2648 spawnfunc(item_flag_team1)
2650 if(!g_ctf) { delete(this); return; }
2652 ctf_FlagSetup(NUM_TEAM_1, this);
2655 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2656 CTF flag for team two (Blue).
2658 "angle" Angle the flag will point (minus 90 degrees)...
2659 "model" model to use, note this needs red and blue as skins 0 and 1...
2660 "noise" sound played when flag is picked up...
2661 "noise1" sound played when flag is returned by a teammate...
2662 "noise2" sound played when flag is captured...
2663 "noise3" sound played when flag is lost in the field and respawns itself...
2664 "noise4" sound played when flag is dropped by a player...
2665 "noise5" sound played when flag touches the ground... */
2666 spawnfunc(item_flag_team2)
2668 if(!g_ctf) { delete(this); return; }
2670 ctf_FlagSetup(NUM_TEAM_2, this);
2673 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2674 CTF flag for team three (Yellow).
2676 "angle" Angle the flag will point (minus 90 degrees)...
2677 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2678 "noise" sound played when flag is picked up...
2679 "noise1" sound played when flag is returned by a teammate...
2680 "noise2" sound played when flag is captured...
2681 "noise3" sound played when flag is lost in the field and respawns itself...
2682 "noise4" sound played when flag is dropped by a player...
2683 "noise5" sound played when flag touches the ground... */
2684 spawnfunc(item_flag_team3)
2686 if(!g_ctf) { delete(this); return; }
2688 ctf_FlagSetup(NUM_TEAM_3, this);
2691 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2692 CTF flag for team four (Pink).
2694 "angle" Angle the flag will point (minus 90 degrees)...
2695 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2696 "noise" sound played when flag is picked up...
2697 "noise1" sound played when flag is returned by a teammate...
2698 "noise2" sound played when flag is captured...
2699 "noise3" sound played when flag is lost in the field and respawns itself...
2700 "noise4" sound played when flag is dropped by a player...
2701 "noise5" sound played when flag touches the ground... */
2702 spawnfunc(item_flag_team4)
2704 if(!g_ctf) { delete(this); return; }
2706 ctf_FlagSetup(NUM_TEAM_4, this);
2709 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2712 "angle" Angle the flag will point (minus 90 degrees)...
2713 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2714 "noise" sound played when flag is picked up...
2715 "noise1" sound played when flag is returned by a teammate...
2716 "noise2" sound played when flag is captured...
2717 "noise3" sound played when flag is lost in the field and respawns itself...
2718 "noise4" sound played when flag is dropped by a player...
2719 "noise5" sound played when flag touches the ground... */
2720 spawnfunc(item_flag_neutral)
2722 if(!g_ctf) { delete(this); return; }
2723 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2725 ctf_FlagSetup(0, this);
2728 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2729 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2730 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.
2732 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2733 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2736 if(!g_ctf) { delete(this); return; }
2738 this.classname = "ctf_team";
2739 this.team = this.cnt + 1;
2742 // compatibility for quake maps
2743 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2744 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2745 spawnfunc(info_player_team1);
2746 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2747 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2748 spawnfunc(info_player_team2);
2749 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2750 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2752 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2753 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2755 // compatibility for wop maps
2756 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2757 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2758 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2759 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2760 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2761 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2769 void ctf_ScoreRules(int teams)
2771 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2772 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2773 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2774 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2775 field(SP_CTF_PICKUPS, "pickups", 0);
2776 field(SP_CTF_FCKILLS, "fckills", 0);
2777 field(SP_CTF_RETURNS, "returns", 0);
2778 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2782 // code from here on is just to support maps that don't have flag and team entities
2783 void ctf_SpawnTeam (string teamname, int teamcolor)
2785 entity this = new_pure(ctf_team);
2786 this.netname = teamname;
2787 this.cnt = teamcolor - 1;
2788 this.spawnfunc_checked = true;
2789 this.team = teamcolor;
2792 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2797 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2799 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2800 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2802 switch(tmp_entity.team)
2804 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2805 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2806 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2807 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2809 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2812 havocbot_ctf_calculate_middlepoint();
2814 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2816 ctf_teams = 0; // so set the default red and blue teams
2817 BITSET_ASSIGN(ctf_teams, BIT(0));
2818 BITSET_ASSIGN(ctf_teams, BIT(1));
2821 //ctf_teams = bound(2, ctf_teams, 4);
2823 // if no teams are found, spawn defaults
2824 if(find(NULL, classname, "ctf_team") == NULL)
2826 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2827 if(ctf_teams & BIT(0))
2828 ctf_SpawnTeam("Red", NUM_TEAM_1);
2829 if(ctf_teams & BIT(1))
2830 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2831 if(ctf_teams & BIT(2))
2832 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2833 if(ctf_teams & BIT(3))
2834 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2837 ctf_ScoreRules(ctf_teams);
2840 void ctf_Initialize()
2842 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2844 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2845 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2846 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2848 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);