3 #include <common/effects/all.qh>
4 #include <common/mapobjects/teleporters.qh>
5 #include <common/mapobjects/triggers.qh>
6 #include <common/vehicles/all.qh>
7 #include <server/command/vote.qh>
8 #include <server/client.qh>
9 #include <server/gamelog.qh>
10 #include <server/intermission.qh>
11 #include <server/damage.qh>
12 #include <server/world.qh>
13 #include <server/items/items.qh>
14 #include <server/race.qh>
15 #include <server/teamplay.qh>
17 #include <lib/warpzone/common.qh>
19 bool autocvar_g_ctf_allow_vehicle_carry;
20 bool autocvar_g_ctf_allow_vehicle_touch;
21 bool autocvar_g_ctf_allow_monster_touch;
22 bool autocvar_g_ctf_throw;
23 float autocvar_g_ctf_throw_angle_max;
24 float autocvar_g_ctf_throw_angle_min;
25 int autocvar_g_ctf_throw_punish_count;
26 float autocvar_g_ctf_throw_punish_delay;
27 float autocvar_g_ctf_throw_punish_time;
28 float autocvar_g_ctf_throw_strengthmultiplier;
29 float autocvar_g_ctf_throw_velocity_forward;
30 float autocvar_g_ctf_throw_velocity_up;
31 float autocvar_g_ctf_drop_velocity_up;
32 float autocvar_g_ctf_drop_velocity_side;
33 bool autocvar_g_ctf_oneflag_reverse;
34 bool autocvar_g_ctf_portalteleport;
35 bool autocvar_g_ctf_pass;
36 float autocvar_g_ctf_pass_arc;
37 float autocvar_g_ctf_pass_arc_max;
38 float autocvar_g_ctf_pass_directional_max;
39 float autocvar_g_ctf_pass_directional_min;
40 float autocvar_g_ctf_pass_radius;
41 float autocvar_g_ctf_pass_wait;
42 bool autocvar_g_ctf_pass_request;
43 float autocvar_g_ctf_pass_turnrate;
44 float autocvar_g_ctf_pass_timelimit;
45 float autocvar_g_ctf_pass_velocity;
46 bool autocvar_g_ctf_dynamiclights;
47 float autocvar_g_ctf_flag_collect_delay;
48 float autocvar_g_ctf_flag_damageforcescale;
49 bool autocvar_g_ctf_flag_dropped_waypoint;
50 bool autocvar_g_ctf_flag_dropped_floatinwater;
51 bool autocvar_g_ctf_flag_glowtrails;
52 int autocvar_g_ctf_flag_health;
53 bool autocvar_g_ctf_flag_return;
54 bool autocvar_g_ctf_flag_return_carrying;
55 float autocvar_g_ctf_flag_return_carried_radius;
56 float autocvar_g_ctf_flag_return_time;
57 bool autocvar_g_ctf_flag_return_when_unreachable;
58 float autocvar_g_ctf_flag_return_damage;
59 float autocvar_g_ctf_flag_return_damage_delay;
60 float autocvar_g_ctf_flag_return_dropped;
61 bool autocvar_g_ctf_flag_waypoint = true;
62 float autocvar_g_ctf_flag_waypoint_maxdistance;
63 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
64 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
65 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
66 float autocvar_g_ctf_flagcarrier_selfforcefactor;
67 float autocvar_g_ctf_flagcarrier_damagefactor;
68 float autocvar_g_ctf_flagcarrier_forcefactor;
69 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
70 bool autocvar_g_ctf_fullbrightflags;
71 bool autocvar_g_ctf_ignore_frags;
72 bool autocvar_g_ctf_score_ignore_fields;
73 int autocvar_g_ctf_score_capture;
74 int autocvar_g_ctf_score_capture_assist;
75 int autocvar_g_ctf_score_kill;
76 int autocvar_g_ctf_score_penalty_drop;
77 int autocvar_g_ctf_score_penalty_returned;
78 int autocvar_g_ctf_score_pickup_base;
79 int autocvar_g_ctf_score_pickup_dropped_early;
80 int autocvar_g_ctf_score_pickup_dropped_late;
81 int autocvar_g_ctf_score_return;
82 float autocvar_g_ctf_shield_force;
83 float autocvar_g_ctf_shield_max_ratio;
84 int autocvar_g_ctf_shield_min_negscore;
85 bool autocvar_g_ctf_stalemate;
86 int autocvar_g_ctf_stalemate_endcondition;
87 float autocvar_g_ctf_stalemate_time;
88 bool autocvar_g_ctf_reverse;
89 float autocvar_g_ctf_dropped_capture_delay;
90 float autocvar_g_ctf_dropped_capture_radius;
92 void ctf_FakeTimeLimit(entity e, float t)
95 WriteByte(MSG_ONE, 3); // svc_updatestat
96 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
98 WriteCoord(MSG_ONE, autocvar_timelimit);
100 WriteCoord(MSG_ONE, (t + 1) / 60);
103 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
105 if(autocvar_sv_eventlog)
106 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != NULL) ? ftos(actor.playerid) : "")));
107 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
110 void ctf_CaptureRecord(entity flag, entity player)
112 float cap_record = ctf_captimerecord;
113 float cap_time = (time - flag.ctf_pickuptime);
114 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
118 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname);
119 else if(!ctf_captimerecord)
120 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_TIME), player.netname, TIME_ENCODE(cap_time));
121 else if(cap_time < cap_record)
122 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));
124 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));
126 // write that shit in the database
127 if(!ctf_oneflag) // but not in 1-flag mode
128 if((!ctf_captimerecord) || (cap_time < cap_record))
130 ctf_captimerecord = cap_time;
131 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
132 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
133 write_recordmarker(player, flag.ctf_pickuptime, cap_time);
136 if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
137 race_setTime(GetMapname(), TIME_ENCODE(cap_time), player.crypto_idfp, player.netname, player, false);
140 bool ctf_Immediate_Return_Allowed(entity flag, entity toucher)
143 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), { ++num_perteam; });
145 // automatically return if there's only 1 player on the team
146 return ((autocvar_g_ctf_flag_return || num_perteam <= 1 || (autocvar_g_ctf_flag_return_carrying && toucher.flagcarried))
150 bool ctf_Return_Customize(entity this, entity client)
152 // only to the carrier
153 return boolean(client == this.owner);
156 void ctf_FlagcarrierWaypoints(entity player)
158 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
159 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, 2 * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
160 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);
161 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
163 if(player.flagcarried && CTF_SAMETEAM(player, player.flagcarried))
165 if(!player.wps_enemyflagcarrier)
167 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, 0, player, wps_enemyflagcarrier, true, RADARICON_FLAG);
168 wp.colormod = WPCOLOR_ENEMYFC(player.team);
169 setcefc(wp, ctf_Stalemate_Customize);
171 if(IS_REAL_CLIENT(player) && !ctf_stalemate)
172 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_VISIBLE);
175 if(!player.wps_flagreturn)
177 entity owp = WaypointSprite_SpawnFixed(WP_FlagReturn, player.flagcarried.ctf_spawnorigin + FLAG_WAYPOINT_OFFSET, player, wps_flagreturn, RADARICON_FLAG);
178 owp.colormod = '0 0.8 0.8';
179 //WaypointSprite_UpdateTeamRadar(player.wps_flagreturn, RADARICON_FLAG, ((player.team) ? colormapPaletteColor(player.team - 1, false) : '1 1 1'));
180 setcefc(owp, ctf_Return_Customize);
185 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
187 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
188 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
189 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
190 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
193 if(current_height) // make sure we can actually do this arcing path
195 targpos = (to + ('0 0 1' * current_height));
196 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
197 if(trace_fraction < 1)
199 //print("normal arc line failed, trying to find new pos...");
200 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
201 targpos = (trace_endpos + eZ * FLAG_PASS_ARC_OFFSET_Z);
202 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
203 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
204 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
207 else { targpos = to; }
209 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
211 vector desired_direction = normalize(targpos - from);
212 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
213 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
216 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
218 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
220 // directional tracing only
222 makevectors(passer_angle);
224 // find the closest point on the enemy to the center of the attack
225 float h; // hypotenuse, which is the distance between attacker to head
226 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
228 h = vlen(head_center - passer_center);
229 a = h * (normalize(head_center - passer_center) * v_forward);
231 vector nearest_on_line = (passer_center + a * v_forward);
232 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
234 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
235 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
237 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
242 else { return true; }
246 // =======================
247 // CaptureShield Functions
248 // =======================
250 bool ctf_CaptureShield_CheckStatus(entity p)
252 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
253 int players_worseeq, players_total;
255 if(ctf_captureshield_max_ratio <= 0)
258 s = GameRules_scoring_add(p, CTF_CAPS, 0);
259 s2 = GameRules_scoring_add(p, CTF_PICKUPS, 0);
260 s3 = GameRules_scoring_add(p, CTF_RETURNS, 0);
261 s4 = GameRules_scoring_add(p, CTF_FCKILLS, 0);
263 sr = ((s - s2) + (s3 + s4));
265 if(sr >= -ctf_captureshield_min_negscore)
268 players_total = players_worseeq = 0;
269 FOREACH_CLIENT(IS_PLAYER(it), {
272 se = GameRules_scoring_add(it, CTF_CAPS, 0);
273 se2 = GameRules_scoring_add(it, CTF_PICKUPS, 0);
274 se3 = GameRules_scoring_add(it, CTF_RETURNS, 0);
275 se4 = GameRules_scoring_add(it, CTF_FCKILLS, 0);
277 ser = ((se - se2) + (se3 + se4));
284 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
285 // use this rule here
287 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
293 void ctf_CaptureShield_Update(entity player, bool wanted_status)
295 bool updated_status = ctf_CaptureShield_CheckStatus(player);
296 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
298 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
299 player.ctf_captureshielded = updated_status;
303 bool ctf_CaptureShield_Customize(entity this, entity client)
305 if(!client.ctf_captureshielded) { return false; }
306 if(CTF_SAMETEAM(this, client)) { return false; }
311 void ctf_CaptureShield_Touch(entity this, entity toucher)
313 if(!toucher.ctf_captureshielded) { return; }
314 if(CTF_SAMETEAM(this, toucher)) { return; }
316 vector mymid = (this.absmin + this.absmax) * 0.5;
317 vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
319 Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
320 if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
323 void ctf_CaptureShield_Spawn(entity flag)
325 entity shield = new(ctf_captureshield);
328 shield.team = flag.team;
329 settouch(shield, ctf_CaptureShield_Touch);
330 setcefc(shield, ctf_CaptureShield_Customize);
331 shield.effects = EF_ADDITIVE;
332 set_movetype(shield, MOVETYPE_NOCLIP);
333 shield.solid = SOLID_TRIGGER;
334 shield.avelocity = '7 0 11';
337 setorigin(shield, flag.origin);
338 setmodel(shield, MDL_CTF_SHIELD);
339 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
343 // ====================
344 // Drop/Pass/Throw Code
345 // ====================
347 void ctf_Handle_Drop(entity flag, entity player, int droptype)
350 player = (player ? player : flag.pass_sender);
353 set_movetype(flag, MOVETYPE_TOSS);
354 flag.takedamage = DAMAGE_YES;
355 flag.angles = '0 0 0';
356 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
357 flag.ctf_droptime = time;
358 flag.ctf_dropper = player;
359 flag.ctf_status = FLAG_DROPPED;
361 // messages and sounds
362 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
363 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
364 ctf_EventLog("dropped", player.team, player);
367 GameRules_scoring_add_team(player, SCORE, -((flag.score_drop) ? flag.score_drop : autocvar_g_ctf_score_penalty_drop));
368 GameRules_scoring_add(player, CTF_DROPS, 1);
371 if(autocvar_g_ctf_flag_dropped_waypoint) {
372 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);
373 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
376 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
378 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_health);
379 WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH));
382 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
384 if(droptype == DROP_PASS)
386 flag.pass_distance = 0;
387 flag.pass_sender = NULL;
388 flag.pass_target = NULL;
392 void ctf_Handle_Retrieve(entity flag, entity player)
394 entity sender = flag.pass_sender;
396 // transfer flag to player
398 flag.owner.flagcarried = flag;
399 GameRules_scoring_vip(player, true);
404 setattachment(flag, player.vehicle, "");
405 setorigin(flag, VEHICLE_FLAG_OFFSET);
406 flag.scale = VEHICLE_FLAG_SCALE;
410 setattachment(flag, player, "");
411 setorigin(flag, FLAG_CARRY_OFFSET);
413 set_movetype(flag, MOVETYPE_NONE);
414 flag.takedamage = DAMAGE_NO;
415 flag.solid = SOLID_NOT;
416 flag.angles = '0 0 0';
417 flag.ctf_status = FLAG_CARRY;
419 // messages and sounds
420 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
421 ctf_EventLog("receive", flag.team, player);
423 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
425 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
426 else if(it == player)
427 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
428 else if(SAME_TEAM(it, sender))
429 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
432 // create new waypoint
433 ctf_FlagcarrierWaypoints(player);
435 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
436 player.throw_antispam = sender.throw_antispam;
438 flag.pass_distance = 0;
439 flag.pass_sender = NULL;
440 flag.pass_target = NULL;
443 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
445 entity flag = player.flagcarried;
446 vector targ_origin, flag_velocity;
448 if(!flag) { return; }
449 if((droptype == DROP_PASS) && !receiver) { return; }
451 if(flag.speedrunning)
453 // ensure old waypoints are removed before resetting the flag
454 WaypointSprite_Kill(player.wps_flagcarrier);
456 if(player.wps_enemyflagcarrier)
457 WaypointSprite_Kill(player.wps_enemyflagcarrier);
459 if(player.wps_flagreturn)
460 WaypointSprite_Kill(player.wps_flagreturn);
461 ctf_RespawnFlag(flag);
466 setattachment(flag, NULL, "");
467 tracebox(player.origin - FLAG_DROP_OFFSET, flag.m_mins, flag.m_maxs, player.origin + FLAG_DROP_OFFSET, MOVE_NOMONSTERS, flag);
468 setorigin(flag, trace_endpos);
469 flag.owner.flagcarried = NULL;
470 GameRules_scoring_vip(flag.owner, false);
472 flag.solid = SOLID_TRIGGER;
473 flag.ctf_dropper = player;
474 flag.ctf_droptime = time;
476 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
483 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
484 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
485 WarpZone_RefSys_Copy(flag, receiver);
486 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
487 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
489 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
490 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
493 set_movetype(flag, MOVETYPE_FLY);
494 flag.takedamage = DAMAGE_NO;
495 flag.pass_sender = player;
496 flag.pass_target = receiver;
497 flag.ctf_status = FLAG_PASSING;
500 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
501 WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
502 ctf_EventLog("pass", flag.team, player);
508 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'));
510 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)));
511 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
512 ctf_Handle_Drop(flag, player, droptype);
513 navigation_dynamicgoal_set(flag, player);
519 flag.velocity = '0 0 0'; // do nothing
526 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);
527 ctf_Handle_Drop(flag, player, droptype);
528 navigation_dynamicgoal_set(flag, player);
533 // kill old waypointsprite
534 WaypointSprite_Ping(player.wps_flagcarrier);
535 WaypointSprite_Kill(player.wps_flagcarrier);
537 if(player.wps_enemyflagcarrier)
538 WaypointSprite_Kill(player.wps_enemyflagcarrier);
540 if(player.wps_flagreturn)
541 WaypointSprite_Kill(player.wps_flagreturn);
544 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
548 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
550 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
558 void nades_GiveBonus(entity player, float score);
560 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
562 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
563 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
564 entity player_team_flag = NULL, tmp_entity;
565 float old_time, new_time;
567 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
568 if(CTF_DIFFTEAM(player, flag)) { return; }
569 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)
571 if (toucher.goalentity == flag.bot_basewaypoint)
572 toucher.goalentity_lock_timeout = 0;
575 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
576 if(SAME_TEAM(tmp_entity, player))
578 player_team_flag = tmp_entity;
582 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
584 player.throw_prevtime = time;
585 player.throw_count = 0;
587 // messages and sounds
588 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
589 ctf_CaptureRecord(enemy_flag, player);
590 _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);
594 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
595 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
601 if(enemy_flag.score_capture || flag.score_capture)
602 pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
603 GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
605 if(enemy_flag.score_team_capture || flag.score_team_capture)
606 capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
607 GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
609 old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
610 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
611 if(!old_time || new_time < old_time)
612 GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
615 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
617 shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
621 if(capturetype == CAPTURE_NORMAL)
623 WaypointSprite_Kill(player.wps_flagcarrier);
624 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
626 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
627 { GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist)); }
630 flag.enemy = toucher;
633 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
634 ctf_RespawnFlag(enemy_flag);
637 void ctf_Handle_Return(entity flag, entity player)
639 // messages and sounds
640 if(IS_MONSTER(player))
642 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
646 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
647 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
649 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
650 ctf_EventLog("return", flag.team, player);
653 if(IS_PLAYER(player))
655 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
656 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
658 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
661 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
665 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
666 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
667 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
671 if(player.flagcarried == flag)
672 WaypointSprite_Kill(player.wps_flagcarrier);
677 ctf_RespawnFlag(flag);
680 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
683 float pickup_dropped_score; // used to calculate dropped pickup score
685 // attach the flag to the player
687 player.flagcarried = flag;
688 GameRules_scoring_vip(player, true);
691 setattachment(flag, player.vehicle, "");
692 setorigin(flag, VEHICLE_FLAG_OFFSET);
693 flag.scale = VEHICLE_FLAG_SCALE;
697 setattachment(flag, player, "");
698 setorigin(flag, FLAG_CARRY_OFFSET);
702 set_movetype(flag, MOVETYPE_NONE);
703 flag.takedamage = DAMAGE_NO;
704 flag.solid = SOLID_NOT;
705 flag.angles = '0 0 0';
706 flag.ctf_status = FLAG_CARRY;
710 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
711 case PICKUP_DROPPED: SetResourceExplicit(flag, RES_HEALTH, flag.max_health); break; // reset health/return timelimit
715 // messages and sounds
716 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
718 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
720 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
721 else if(CTF_DIFFTEAM(player, flag))
722 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
724 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
726 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
729 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); });
732 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
733 if(CTF_SAMETEAM(flag, it))
735 if(SAME_TEAM(player, it))
736 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
738 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);
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(time > tmp_entity.ctf_droptime + 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
961 if(autocvar_g_ctf_flag_dropped_floatinwater)
963 vector midpoint = ((this.absmin + this.absmax) * 0.5);
964 if(pointcontents(midpoint) == CONTENT_WATER)
966 this.velocity = this.velocity * 0.5;
968 if (pointcontents(midpoint + eZ * FLAG_FLOAT_OFFSET_Z) == CONTENT_WATER)
969 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
971 { set_movetype(this, MOVETYPE_FLY); }
973 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
975 if(autocvar_g_ctf_flag_return_dropped)
977 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
979 SetResourceExplicit(this, RES_HEALTH, 0);
980 ctf_CheckFlagReturn(this, RETURN_DROPPED);
984 if(this.ctf_flagdamaged_byworld)
986 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
987 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
990 else if(autocvar_g_ctf_flag_return_time)
992 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
993 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
1001 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
1003 SetResourceExplicit(this, RES_HEALTH, 0);
1004 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
1006 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
1007 ImpulseCommands(this.owner);
1009 if(autocvar_g_ctf_stalemate)
1011 if(time >= wpforenemy_nextthink)
1013 ctf_CheckStalemate();
1014 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1017 if(CTF_SAMETEAM(this, this.owner) && this.team)
1019 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1020 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
1021 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
1022 ctf_Handle_Return(this, this.owner);
1029 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1030 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1031 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1033 if((this.pass_target == NULL)
1034 || (IS_DEAD(this.pass_target))
1035 || (this.pass_target.flagcarried)
1036 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1037 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1038 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1040 // give up, pass failed
1041 ctf_Handle_Drop(this, NULL, DROP_PASS);
1045 // still a viable target, go for it
1046 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1051 default: // this should never happen
1053 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1059 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1062 if(game_stopped) return;
1063 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1065 bool is_not_monster = (!IS_MONSTER(toucher));
1067 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1068 if(ITEM_TOUCH_NEEDKILL())
1070 if(!autocvar_g_ctf_flag_return_damage_delay)
1072 SetResourceExplicit(flag, RES_HEALTH, 0);
1073 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1075 if(!flag.ctf_flagdamaged_byworld) { return; }
1078 // special touch behaviors
1079 if(STAT(FROZEN, toucher)) { return; }
1080 else if(IS_VEHICLE(toucher))
1082 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1083 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1085 return; // do nothing
1087 else if(IS_MONSTER(toucher))
1089 if(!autocvar_g_ctf_allow_monster_touch)
1090 return; // do nothing
1092 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1094 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1096 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1097 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1098 flag.wait = time + FLAG_TOUCHRATE;
1102 else if(IS_DEAD(toucher)) { return; }
1104 switch(flag.ctf_status)
1110 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1111 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1112 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1113 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1115 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1116 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1117 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)
1119 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1120 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1122 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1123 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1129 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1130 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1131 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1132 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1138 LOG_TRACE("Someone touched a flag even though it was being carried?");
1144 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1146 if(DIFF_TEAM(toucher, flag.pass_sender))
1148 if(ctf_Immediate_Return_Allowed(flag, toucher))
1149 ctf_Handle_Return(flag, toucher);
1150 else if(is_not_monster && (!toucher.flagcarried))
1151 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1153 else if(!toucher.flagcarried)
1154 ctf_Handle_Retrieve(flag, toucher);
1161 .float last_respawn;
1162 void ctf_RespawnFlag(entity flag)
1164 flag.watertype = CONTENT_EMPTY; // TODO: it is unclear why this workaround is needed, likely many other potential breakage points!!
1165 // check for flag respawn being called twice in a row
1166 if(flag.last_respawn > time - 0.5)
1167 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1169 flag.last_respawn = time;
1171 // reset the player (if there is one)
1172 if((flag.owner) && (flag.owner.flagcarried == flag))
1174 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1175 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1176 WaypointSprite_Kill(flag.wps_flagcarrier);
1178 flag.owner.flagcarried = NULL;
1179 GameRules_scoring_vip(flag.owner, false);
1181 if(flag.speedrunning)
1182 ctf_FakeTimeLimit(flag.owner, -1);
1185 if((flag.owner) && (flag.owner.vehicle))
1186 flag.scale = FLAG_SCALE;
1188 if(flag.ctf_status == FLAG_DROPPED)
1189 { WaypointSprite_Kill(flag.wps_flagdropped); }
1192 setattachment(flag, NULL, "");
1193 setorigin(flag, flag.ctf_spawnorigin);
1195 //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1196 set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1197 flag.takedamage = DAMAGE_NO;
1198 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1199 flag.solid = SOLID_TRIGGER;
1200 flag.velocity = '0 0 0';
1201 flag.angles = flag.mangle;
1202 flag.flags = FL_ITEM | FL_NOTARGET;
1204 flag.ctf_status = FLAG_BASE;
1206 flag.pass_distance = 0;
1207 flag.pass_sender = NULL;
1208 flag.pass_target = NULL;
1209 flag.ctf_dropper = NULL;
1210 flag.ctf_pickuptime = 0;
1211 flag.ctf_droptime = 0;
1212 flag.ctf_flagdamaged_byworld = false;
1213 navigation_dynamicgoal_unset(flag);
1215 ctf_CheckStalemate();
1218 void ctf_Reset(entity this)
1220 if(this.owner && IS_PLAYER(this.owner))
1221 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1224 ctf_RespawnFlag(this);
1227 bool ctf_FlagBase_Customize(entity this, entity client)
1229 entity e = WaypointSprite_getviewentity(client);
1230 entity wp_owner = this.owner;
1231 entity flag = e.flagcarried;
1232 if(flag && CTF_SAMETEAM(e, flag))
1234 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1239 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1242 waypoint_spawnforitem_force(this, this.origin);
1243 navigation_dynamicgoal_init(this, true);
1249 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1250 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1251 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1252 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1253 default: basename = WP_FlagBaseNeutral; break;
1256 if(autocvar_g_ctf_flag_waypoint)
1258 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1259 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1260 wp.fade_rate = autocvar_g_ctf_flag_waypoint_maxdistance;
1261 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1262 setcefc(wp, ctf_FlagBase_Customize);
1265 // captureshield setup
1266 ctf_CaptureShield_Spawn(this);
1271 void ctf_FlagSetup(int teamnum, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1274 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1275 ctf_worldflaglist = flag;
1277 setattachment(flag, NULL, "");
1279 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnum), Team_ColorName_Upper(teamnum)));
1280 flag.team = teamnum;
1281 flag.classname = "item_flag_team";
1282 flag.target = "###item###"; // for finding the nearest item using findnearest
1283 flag.flags = FL_ITEM | FL_NOTARGET;
1284 IL_PUSH(g_items, flag);
1285 flag.solid = SOLID_TRIGGER;
1286 flag.takedamage = DAMAGE_NO;
1287 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1288 flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1289 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1290 flag.event_damage = ctf_FlagDamage;
1291 flag.pushable = true;
1292 flag.teleportable = TELEPORT_NORMAL;
1293 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1294 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1295 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1296 if(flag.damagedbycontents)
1297 IL_PUSH(g_damagedbycontents, flag);
1298 flag.velocity = '0 0 0';
1299 flag.mangle = flag.angles;
1300 flag.reset = ctf_Reset;
1301 settouch(flag, ctf_FlagTouch);
1302 setthink(flag, ctf_FlagThink);
1303 flag.nextthink = time + FLAG_THINKRATE;
1304 flag.ctf_status = FLAG_BASE;
1306 // crudely force them all to 0
1307 if(autocvar_g_ctf_score_ignore_fields)
1308 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1310 string teamname = Static_Team_ColorName_Lower(teamnum);
1312 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1313 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1314 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1315 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnum).eent_eff_name; }
1316 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnum).eent_eff_name; }
1317 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnum).eent_eff_name; }
1321 if(flag.s == "") flag.s = b; \
1322 precache_sound(flag.s);
1324 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnum))))
1325 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnum))))
1326 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnum))))
1327 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnum))))
1328 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1329 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1330 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1334 precache_model(flag.model);
1337 _setmodel(flag, flag.model); // precision set below
1338 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1339 flag.m_mins = flag.mins; // store these for squash checks
1340 flag.m_maxs = flag.maxs;
1341 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1343 if(autocvar_g_ctf_flag_glowtrails)
1347 case NUM_TEAM_1: flag.glow_color = 251; break;
1348 case NUM_TEAM_2: flag.glow_color = 210; break;
1349 case NUM_TEAM_3: flag.glow_color = 110; break;
1350 case NUM_TEAM_4: flag.glow_color = 145; break;
1351 default: flag.glow_color = 254; break;
1353 flag.glow_size = 25;
1354 flag.glow_trail = 1;
1357 flag.effects |= EF_LOWPRECISION;
1358 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1359 if(autocvar_g_ctf_dynamiclights)
1363 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1364 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1365 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1366 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1367 default: flag.effects |= EF_DIMLIGHT; break;
1372 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1374 flag.dropped_origin = flag.origin;
1375 flag.noalign = true;
1376 set_movetype(flag, MOVETYPE_NONE);
1378 else // drop to floor, automatically find a platform and set that as spawn origin
1380 flag.noalign = false;
1382 set_movetype(flag, MOVETYPE_NONE);
1385 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1393 // NOTE: LEGACY CODE, needs to be re-written!
1395 void havocbot_ctf_calculate_middlepoint()
1399 vector fo = '0 0 0';
1402 f = ctf_worldflaglist;
1407 f = f.ctf_worldflagnext;
1413 havocbot_middlepoint = s / n;
1414 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1416 havocbot_symmetry_axis_m = 0;
1417 havocbot_symmetry_axis_q = 0;
1420 // for symmetrical editing of waypoints
1421 entity f1 = ctf_worldflaglist;
1422 entity f2 = f1.ctf_worldflagnext;
1423 float m = -(f1.origin.y - f2.origin.y) / (max(f1.origin.x - f2.origin.x, FLOAT_EPSILON));
1424 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1425 havocbot_symmetry_axis_m = m;
1426 havocbot_symmetry_axis_q = q;
1428 havocbot_symmetry_origin_order = n;
1432 entity havocbot_ctf_find_flag(entity bot)
1435 f = ctf_worldflaglist;
1438 if (CTF_SAMETEAM(bot, f))
1440 f = f.ctf_worldflagnext;
1445 entity havocbot_ctf_find_enemy_flag(entity bot)
1448 f = ctf_worldflaglist;
1453 if(CTF_DIFFTEAM(bot, f))
1460 else if(!bot.flagcarried)
1464 else if (CTF_DIFFTEAM(bot, f))
1466 f = f.ctf_worldflagnext;
1471 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1478 FOREACH_CLIENT(IS_PLAYER(it), {
1479 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1482 if(vdist(it.origin - org, <, tc_radius))
1491 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1494 head = ctf_worldflaglist;
1497 if (CTF_SAMETEAM(this, head))
1499 head = head.ctf_worldflagnext;
1502 navigation_routerating(this, head, ratingscale, 10000);
1506 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1509 head = ctf_worldflaglist;
1512 if (CTF_SAMETEAM(this, head))
1514 if (this.flagcarried)
1515 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1517 head = head.ctf_worldflagnext; // skip base if it has a different group
1522 head = head.ctf_worldflagnext;
1527 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1530 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1533 head = ctf_worldflaglist;
1538 if(CTF_DIFFTEAM(this, head))
1542 if(this.flagcarried)
1545 else if(!this.flagcarried)
1549 else if(CTF_DIFFTEAM(this, head))
1551 head = head.ctf_worldflagnext;
1555 if (head.ctf_status == FLAG_CARRY)
1557 // adjust rating of our flag carrier depending on his health
1558 head = head.tag_entity;
1559 float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1560 ratingscale += ratingscale * f * 0.1;
1562 navigation_routerating(this, head, ratingscale, 10000);
1566 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1568 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1570 if (!bot_waypoints_for_items)
1572 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1578 head = havocbot_ctf_find_enemy_flag(this);
1583 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1586 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1590 mf = havocbot_ctf_find_flag(this);
1592 if(mf.ctf_status == FLAG_BASE)
1596 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1599 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1602 head = ctf_worldflaglist;
1605 // flag is out in the field
1606 if(head.ctf_status != FLAG_BASE)
1607 if(head.tag_entity==NULL) // dropped
1611 if(vdist(org - head.origin, <, df_radius))
1612 navigation_routerating(this, head, ratingscale, 10000);
1615 navigation_routerating(this, head, ratingscale, 10000);
1618 head = head.ctf_worldflagnext;
1622 void havocbot_ctf_reset_role(entity this)
1624 float cdefense, cmiddle, coffense;
1631 if (this.flagcarried)
1633 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1637 mf = havocbot_ctf_find_flag(this);
1638 ef = havocbot_ctf_find_enemy_flag(this);
1640 // Retrieve stolen flag
1641 if(mf.ctf_status!=FLAG_BASE)
1643 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1647 // If enemy flag is taken go to the middle to intercept pursuers
1648 if(ef.ctf_status!=FLAG_BASE)
1650 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1654 // if there is no one else on the team switch to offense
1656 // don't check if this bot is a player since it isn't true when the bot is added to the server
1657 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1661 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1664 else if (time < CS(this).jointime + 1)
1666 // if bots spawn all at once set good default roles
1669 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1672 else if (count == 2)
1674 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1679 // Evaluate best position to take
1680 // Count mates on middle position
1681 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1683 // Count mates on defense position
1684 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1686 // Count mates on offense position
1687 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1689 if(cdefense<=coffense)
1690 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1691 else if(coffense<=cmiddle)
1692 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1694 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1696 // if bots spawn all at once assign them a more appropriated role after a while
1697 if (time < CS(this).jointime + 1 && count > 2)
1698 this.havocbot_role_timeout = time + 10 + random() * 10;
1701 bool havocbot_ctf_is_basewaypoint(entity item)
1703 if (item.classname != "waypoint")
1706 entity head = ctf_worldflaglist;
1709 if (item == head.bot_basewaypoint)
1711 head = head.ctf_worldflagnext;
1716 void havocbot_role_ctf_carrier(entity this)
1720 havocbot_ctf_reset_role(this);
1724 if (this.flagcarried == NULL)
1726 havocbot_ctf_reset_role(this);
1730 if (navigation_goalrating_timeout(this))
1732 navigation_goalrating_start(this);
1735 entity mf = havocbot_ctf_find_flag(this);
1736 vector base_org = mf.dropped_origin;
1737 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1739 havocbot_goalrating_ctf_enemybase(this, base_rating);
1741 havocbot_goalrating_ctf_ourbase(this, base_rating);
1743 // start collecting items very close to the bot but only inside of own base radius
1744 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1745 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1747 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1749 navigation_goalrating_end(this);
1751 navigation_goalrating_timeout_set(this);
1753 entity goal = this.goalentity;
1754 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1755 this.goalentity_lock_timeout = time + ((this.bot_aimtarg) ? 2 : 3);
1758 this.havocbot_cantfindflag = time + 10;
1759 else if (time > this.havocbot_cantfindflag)
1761 // Can't navigate to my own base, suicide!
1762 // TODO: drop it and wander around
1763 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1769 void havocbot_role_ctf_escort(entity this)
1775 havocbot_ctf_reset_role(this);
1779 if (this.flagcarried)
1781 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1785 // If enemy flag is back on the base switch to previous role
1786 ef = havocbot_ctf_find_enemy_flag(this);
1787 if(ef.ctf_status==FLAG_BASE)
1789 this.havocbot_role = this.havocbot_previous_role;
1790 this.havocbot_role_timeout = 0;
1793 if (ef.ctf_status == FLAG_DROPPED)
1795 navigation_goalrating_timeout_expire(this, 1);
1799 // If the flag carrier reached the base switch to defense
1800 mf = havocbot_ctf_find_flag(this);
1801 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1803 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1807 // Set the role timeout if necessary
1808 if (!this.havocbot_role_timeout)
1810 this.havocbot_role_timeout = time + random() * 30 + 60;
1813 // If nothing happened just switch to previous role
1814 if (time > this.havocbot_role_timeout)
1816 this.havocbot_role = this.havocbot_previous_role;
1817 this.havocbot_role_timeout = 0;
1821 // Chase the flag carrier
1822 if (navigation_goalrating_timeout(this))
1824 navigation_goalrating_start(this);
1827 havocbot_goalrating_ctf_enemyflag(this, 10000);
1828 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1829 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1831 navigation_goalrating_end(this);
1833 navigation_goalrating_timeout_set(this);
1837 void havocbot_role_ctf_offense(entity this)
1844 havocbot_ctf_reset_role(this);
1848 if (this.flagcarried)
1850 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1855 mf = havocbot_ctf_find_flag(this);
1856 ef = havocbot_ctf_find_enemy_flag(this);
1859 if(mf.ctf_status!=FLAG_BASE)
1862 pos = mf.tag_entity.origin;
1866 // Try to get it if closer than the enemy base
1867 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1869 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1874 // Escort flag carrier
1875 if(ef.ctf_status!=FLAG_BASE)
1878 pos = ef.tag_entity.origin;
1882 if(vdist(pos - mf.dropped_origin, >, 700))
1884 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1889 // Set the role timeout if necessary
1890 if (!this.havocbot_role_timeout)
1891 this.havocbot_role_timeout = time + 120;
1893 if (time > this.havocbot_role_timeout)
1895 havocbot_ctf_reset_role(this);
1899 if (navigation_goalrating_timeout(this))
1901 navigation_goalrating_start(this);
1904 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1905 havocbot_goalrating_ctf_enemybase(this, 10000);
1906 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1908 navigation_goalrating_end(this);
1910 navigation_goalrating_timeout_set(this);
1914 // Retriever (temporary role):
1915 void havocbot_role_ctf_retriever(entity this)
1921 havocbot_ctf_reset_role(this);
1925 if (this.flagcarried)
1927 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1931 // If flag is back on the base switch to previous role
1932 mf = havocbot_ctf_find_flag(this);
1933 if(mf.ctf_status==FLAG_BASE)
1935 if (mf.enemy == this) // did this bot return the flag?
1936 navigation_goalrating_timeout_force(this);
1937 havocbot_ctf_reset_role(this);
1941 if (!this.havocbot_role_timeout)
1942 this.havocbot_role_timeout = time + 20;
1944 if (time > this.havocbot_role_timeout)
1946 havocbot_ctf_reset_role(this);
1950 if (navigation_goalrating_timeout(this))
1952 const float RT_RADIUS = 10000;
1954 navigation_goalrating_start(this);
1957 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1958 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1959 havocbot_goalrating_ctf_enemybase(this, 8000);
1960 entity ef = havocbot_ctf_find_enemy_flag(this);
1961 vector enemy_base_org = ef.dropped_origin;
1962 // start collecting items very close to the bot but only inside of enemy base radius
1963 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1964 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1965 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1967 navigation_goalrating_end(this);
1969 navigation_goalrating_timeout_set(this);
1973 void havocbot_role_ctf_middle(entity this)
1979 havocbot_ctf_reset_role(this);
1983 if (this.flagcarried)
1985 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1989 mf = havocbot_ctf_find_flag(this);
1990 if(mf.ctf_status!=FLAG_BASE)
1992 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1996 if (!this.havocbot_role_timeout)
1997 this.havocbot_role_timeout = time + 10;
1999 if (time > this.havocbot_role_timeout)
2001 havocbot_ctf_reset_role(this);
2005 if (navigation_goalrating_timeout(this))
2009 org = havocbot_middlepoint;
2010 org.z = this.origin.z;
2012 navigation_goalrating_start(this);
2015 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
2016 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
2017 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2018 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2019 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2020 havocbot_goalrating_ctf_enemybase(this, 3000);
2022 navigation_goalrating_end(this);
2024 entity goal = this.goalentity;
2025 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
2026 this.goalentity_lock_timeout = time + 2;
2028 navigation_goalrating_timeout_set(this);
2032 void havocbot_role_ctf_defense(entity this)
2038 havocbot_ctf_reset_role(this);
2042 if (this.flagcarried)
2044 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2048 // If own flag was captured
2049 mf = havocbot_ctf_find_flag(this);
2050 if(mf.ctf_status!=FLAG_BASE)
2052 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2056 if (!this.havocbot_role_timeout)
2057 this.havocbot_role_timeout = time + 30;
2059 if (time > this.havocbot_role_timeout)
2061 havocbot_ctf_reset_role(this);
2064 if (navigation_goalrating_timeout(this))
2066 vector org = mf.dropped_origin;
2068 navigation_goalrating_start(this);
2070 // if enemies are closer to our base, go there
2071 entity closestplayer = NULL;
2072 float distance, bestdistance = 10000;
2073 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2074 distance = vlen(org - it.origin);
2075 if(distance<bestdistance)
2078 bestdistance = distance;
2084 if(DIFF_TEAM(closestplayer, this))
2085 if(vdist(org - this.origin, >, 1000))
2086 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2087 havocbot_goalrating_ctf_ourbase(this, 10000);
2089 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2090 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2091 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2092 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2093 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2095 navigation_goalrating_end(this);
2097 navigation_goalrating_timeout_set(this);
2101 void havocbot_role_ctf_setrole(entity bot, int role)
2103 string s = "(null)";
2106 case HAVOCBOT_CTF_ROLE_CARRIER:
2108 bot.havocbot_role = havocbot_role_ctf_carrier;
2109 bot.havocbot_role_timeout = 0;
2110 bot.havocbot_cantfindflag = time + 10;
2111 if (bot.havocbot_previous_role != bot.havocbot_role)
2112 navigation_goalrating_timeout_force(bot);
2114 case HAVOCBOT_CTF_ROLE_DEFENSE:
2116 bot.havocbot_role = havocbot_role_ctf_defense;
2117 bot.havocbot_role_timeout = 0;
2119 case HAVOCBOT_CTF_ROLE_MIDDLE:
2121 bot.havocbot_role = havocbot_role_ctf_middle;
2122 bot.havocbot_role_timeout = 0;
2124 case HAVOCBOT_CTF_ROLE_OFFENSE:
2126 bot.havocbot_role = havocbot_role_ctf_offense;
2127 bot.havocbot_role_timeout = 0;
2129 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2131 bot.havocbot_previous_role = bot.havocbot_role;
2132 bot.havocbot_role = havocbot_role_ctf_retriever;
2133 bot.havocbot_role_timeout = time + 10;
2134 if (bot.havocbot_previous_role != bot.havocbot_role)
2135 navigation_goalrating_timeout_expire(bot, 2);
2137 case HAVOCBOT_CTF_ROLE_ESCORT:
2139 bot.havocbot_previous_role = bot.havocbot_role;
2140 bot.havocbot_role = havocbot_role_ctf_escort;
2141 bot.havocbot_role_timeout = time + 30;
2142 if (bot.havocbot_previous_role != bot.havocbot_role)
2143 navigation_goalrating_timeout_expire(bot, 2);
2146 LOG_TRACE(bot.netname, " switched to ", s);
2154 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2156 entity player = M_ARGV(0, entity);
2158 int t = 0, t2 = 0, t3 = 0;
2159 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)
2161 // initially clear items so they can be set as necessary later.
2162 STAT(CTF_FLAGSTATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2163 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2164 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2165 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2166 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2167 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2169 // scan through all the flags and notify the client about them
2170 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2172 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2173 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2174 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2175 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2176 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; }
2178 switch(flag.ctf_status)
2183 if((flag.owner == player) || (flag.pass_sender == player))
2184 STAT(CTF_FLAGSTATUS, player) |= t; // carrying: player is currently carrying the flag
2186 STAT(CTF_FLAGSTATUS, player) |= t2; // taken: someone else is carrying the flag
2191 STAT(CTF_FLAGSTATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2197 // item for stopping players from capturing the flag too often
2198 if(player.ctf_captureshielded)
2199 STAT(CTF_FLAGSTATUS, player) |= CTF_SHIELDED;
2202 STAT(CTF_FLAGSTATUS, player) |= CTF_STALEMATE;
2204 // update the health of the flag carrier waypointsprite
2205 if(player.wps_flagcarrier)
2206 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);
2209 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in damage.qc
2211 entity frag_attacker = M_ARGV(1, entity);
2212 entity frag_target = M_ARGV(2, entity);
2213 float frag_damage = M_ARGV(4, float);
2214 vector frag_force = M_ARGV(6, vector);
2216 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2218 if(frag_target == frag_attacker) // damage done to yourself
2220 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2221 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2223 else // damage done to everyone else
2225 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2226 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2229 M_ARGV(4, float) = frag_damage;
2230 M_ARGV(6, vector) = frag_force;
2232 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2234 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
2235 && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2237 frag_target.wps_helpme_time = time;
2238 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2240 // todo: add notification for when flag carrier needs help?
2244 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2246 entity frag_attacker = M_ARGV(1, entity);
2247 entity frag_target = M_ARGV(2, entity);
2249 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2251 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2252 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2255 if(frag_target.flagcarried)
2257 entity tmp_entity = frag_target.flagcarried;
2258 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2259 tmp_entity.ctf_dropper = NULL;
2263 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2265 M_ARGV(2, float) = 0; // frag score
2266 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2269 void ctf_RemovePlayer(entity player)
2271 if(player.flagcarried)
2272 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2274 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2276 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2277 if(flag.pass_target == player) { flag.pass_target = NULL; }
2278 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2282 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2284 entity player = M_ARGV(0, entity);
2286 ctf_RemovePlayer(player);
2289 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2291 entity player = M_ARGV(0, entity);
2293 ctf_RemovePlayer(player);
2296 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2298 if(!autocvar_g_ctf_leaderboard)
2301 entity player = M_ARGV(0, entity);
2303 if(IS_REAL_CLIENT(player))
2305 int m = min(RANKINGS_CNT, autocvar_g_cts_send_rankings_cnt);
2306 race_send_rankings_cnt(MSG_ONE);
2307 for (int i = 1; i <= m; ++i)
2309 race_SendRankings(i, 0, 0, MSG_ONE);
2314 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2316 if(!autocvar_g_ctf_leaderboard)
2319 entity player = M_ARGV(0, entity);
2321 if(CS(player).cvar_cl_allow_uidtracking == 1 && CS(player).cvar_cl_allow_uid2name == 1)
2323 if (!player.stored_netname)
2324 player.stored_netname = strzone(uid2name(player.crypto_idfp));
2325 if(player.stored_netname != player.netname)
2327 db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
2328 strcpy(player.stored_netname, player.netname);
2333 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2335 entity player = M_ARGV(0, entity);
2337 if(player.flagcarried)
2338 if(!autocvar_g_ctf_portalteleport)
2339 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2342 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2344 if(MUTATOR_RETURNVALUE || game_stopped) return;
2346 entity player = M_ARGV(0, entity);
2348 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2350 // pass the flag to a team mate
2351 if(autocvar_g_ctf_pass)
2353 entity head, closest_target = NULL;
2354 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2356 while(head) // find the closest acceptable target to pass to
2358 if(IS_PLAYER(head) && !IS_DEAD(head))
2359 if(head != player && SAME_TEAM(head, player))
2360 if(!head.speedrunning && !head.vehicle)
2362 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in damage.qc)
2363 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2364 vector passer_center = CENTER_OR_VIEWOFS(player);
2366 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2368 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2370 if(IS_BOT_CLIENT(head))
2372 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2373 ctf_Handle_Throw(head, player, DROP_PASS);
2377 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2378 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2380 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2383 else if(player.flagcarried && !head.flagcarried)
2387 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2388 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2389 { closest_target = head; }
2391 else { closest_target = head; }
2398 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2401 // throw the flag in front of you
2402 if(autocvar_g_ctf_throw && player.flagcarried)
2404 if(player.throw_count == -1)
2406 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2408 player.throw_prevtime = time;
2409 player.throw_count = 1;
2410 ctf_Handle_Throw(player, NULL, DROP_THROW);
2415 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2421 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2422 else { player.throw_count += 1; }
2423 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2425 player.throw_prevtime = time;
2426 ctf_Handle_Throw(player, NULL, DROP_THROW);
2433 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2435 entity player = M_ARGV(0, entity);
2437 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2439 player.wps_helpme_time = time;
2440 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2442 else // create a normal help me waypointsprite
2444 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2445 WaypointSprite_Ping(player.wps_helpme);
2451 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2453 entity player = M_ARGV(0, entity);
2454 entity veh = M_ARGV(1, entity);
2456 if(player.flagcarried)
2458 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2460 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2464 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2465 setattachment(player.flagcarried, veh, "");
2466 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2467 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2468 //player.flagcarried.angles = '0 0 0';
2474 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2476 entity player = M_ARGV(0, entity);
2478 if(player.flagcarried)
2480 setattachment(player.flagcarried, player, "");
2481 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2482 player.flagcarried.scale = FLAG_SCALE;
2483 player.flagcarried.angles = '0 0 0';
2484 player.flagcarried.nodrawtoclient = NULL;
2489 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2491 entity player = M_ARGV(0, entity);
2493 if(player.flagcarried)
2495 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2496 ctf_RespawnFlag(player.flagcarried);
2501 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2503 entity flag; // temporary entity for the search method
2505 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2507 switch(flag.ctf_status)
2512 // lock the flag, game is over
2513 set_movetype(flag, MOVETYPE_NONE);
2514 flag.takedamage = DAMAGE_NO;
2515 flag.solid = SOLID_NOT;
2516 flag.nextthink = false; // stop thinking
2518 //dprint("stopping the ", flag.netname, " from moving.\n");
2526 // do nothing for these flags
2533 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2535 entity bot = M_ARGV(0, entity);
2537 havocbot_ctf_reset_role(bot);
2541 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2543 M_ARGV(1, string) = "ctf_team";
2546 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2548 entity spectatee = M_ARGV(0, entity);
2549 entity client = M_ARGV(1, entity);
2551 STAT(CTF_FLAGSTATUS, client) = STAT(CTF_FLAGSTATUS, spectatee);
2554 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2556 int record_page = M_ARGV(0, int);
2557 string ret_string = M_ARGV(1, string);
2559 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2561 if (MapInfo_Get_ByID(i))
2563 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2569 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2570 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2574 M_ARGV(1, string) = ret_string;
2577 bool superspec_Spectate(entity this, entity targ); // TODO
2578 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2579 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2581 entity player = M_ARGV(0, entity);
2582 string cmd_name = M_ARGV(1, string);
2583 int cmd_argc = M_ARGV(2, int);
2585 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2587 if(cmd_name == "followfc")
2599 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2600 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2601 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2602 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2606 FOREACH_CLIENT(IS_PLAYER(it), {
2607 if(it.flagcarried && (it.team == _team || _team == 0))
2610 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2611 continue; // already spectating this fc, try another
2612 return superspec_Spectate(player, it);
2617 superspec_msg("", "", player, "No active flag carrier\n", 1);
2622 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2624 entity frag_target = M_ARGV(0, entity);
2626 if(frag_target.flagcarried)
2627 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2630 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2632 entity player = M_ARGV(0, entity);
2633 if(player.flagcarried)
2634 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2642 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2643 CTF flag for team one (Red).
2645 "angle" Angle the flag will point (minus 90 degrees)...
2646 "model" model to use, note this needs red and blue as skins 0 and 1...
2647 "noise" sound played when flag is picked up...
2648 "noise1" sound played when flag is returned by a teammate...
2649 "noise2" sound played when flag is captured...
2650 "noise3" sound played when flag is lost in the field and respawns itself...
2651 "noise4" sound played when flag is dropped by a player...
2652 "noise5" sound played when flag touches the ground... */
2653 spawnfunc(item_flag_team1)
2655 if(!g_ctf) { delete(this); return; }
2657 ctf_FlagSetup(NUM_TEAM_1, this);
2660 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2661 CTF flag for team two (Blue).
2663 "angle" Angle the flag will point (minus 90 degrees)...
2664 "model" model to use, note this needs red and blue as skins 0 and 1...
2665 "noise" sound played when flag is picked up...
2666 "noise1" sound played when flag is returned by a teammate...
2667 "noise2" sound played when flag is captured...
2668 "noise3" sound played when flag is lost in the field and respawns itself...
2669 "noise4" sound played when flag is dropped by a player...
2670 "noise5" sound played when flag touches the ground... */
2671 spawnfunc(item_flag_team2)
2673 if(!g_ctf) { delete(this); return; }
2675 ctf_FlagSetup(NUM_TEAM_2, this);
2678 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2679 CTF flag for team three (Yellow).
2681 "angle" Angle the flag will point (minus 90 degrees)...
2682 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2683 "noise" sound played when flag is picked up...
2684 "noise1" sound played when flag is returned by a teammate...
2685 "noise2" sound played when flag is captured...
2686 "noise3" sound played when flag is lost in the field and respawns itself...
2687 "noise4" sound played when flag is dropped by a player...
2688 "noise5" sound played when flag touches the ground... */
2689 spawnfunc(item_flag_team3)
2691 if(!g_ctf) { delete(this); return; }
2693 ctf_FlagSetup(NUM_TEAM_3, this);
2696 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2697 CTF flag for team four (Pink).
2699 "angle" Angle the flag will point (minus 90 degrees)...
2700 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2701 "noise" sound played when flag is picked up...
2702 "noise1" sound played when flag is returned by a teammate...
2703 "noise2" sound played when flag is captured...
2704 "noise3" sound played when flag is lost in the field and respawns itself...
2705 "noise4" sound played when flag is dropped by a player...
2706 "noise5" sound played when flag touches the ground... */
2707 spawnfunc(item_flag_team4)
2709 if(!g_ctf) { delete(this); return; }
2711 ctf_FlagSetup(NUM_TEAM_4, this);
2714 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2717 "angle" Angle the flag will point (minus 90 degrees)...
2718 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2719 "noise" sound played when flag is picked up...
2720 "noise1" sound played when flag is returned by a teammate...
2721 "noise2" sound played when flag is captured...
2722 "noise3" sound played when flag is lost in the field and respawns itself...
2723 "noise4" sound played when flag is dropped by a player...
2724 "noise5" sound played when flag touches the ground... */
2725 spawnfunc(item_flag_neutral)
2727 if(!g_ctf) { delete(this); return; }
2728 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2730 ctf_FlagSetup(0, this);
2733 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2734 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2735 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.
2737 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2738 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2741 if(!g_ctf) { delete(this); return; }
2743 this.classname = "ctf_team";
2744 this.team = this.cnt + 1;
2747 // compatibility for quake maps
2748 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2749 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2750 spawnfunc(info_player_team1);
2751 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2752 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2753 spawnfunc(info_player_team2);
2754 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2755 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2757 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2758 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2760 // compatibility for wop maps
2761 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2762 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2763 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2764 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2765 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2766 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2774 void ctf_ScoreRules(int teams)
2776 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2777 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2778 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2779 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2780 field(SP_CTF_PICKUPS, "pickups", 0);
2781 field(SP_CTF_FCKILLS, "fckills", 0);
2782 field(SP_CTF_RETURNS, "returns", 0);
2783 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2787 // code from here on is just to support maps that don't have flag and team entities
2788 void ctf_SpawnTeam (string teamname, int teamcolor)
2790 entity this = new_pure(ctf_team);
2791 this.netname = teamname;
2792 this.cnt = teamcolor - 1;
2793 this.spawnfunc_checked = true;
2794 this.team = teamcolor;
2797 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2802 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2804 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2805 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2807 switch(tmp_entity.team)
2809 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2810 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2811 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2812 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2814 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2817 havocbot_ctf_calculate_middlepoint();
2819 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2821 ctf_teams = 0; // so set the default red and blue teams
2822 BITSET_ASSIGN(ctf_teams, BIT(0));
2823 BITSET_ASSIGN(ctf_teams, BIT(1));
2826 //ctf_teams = bound(2, ctf_teams, 4);
2828 // if no teams are found, spawn defaults
2829 if(find(NULL, classname, "ctf_team") == NULL)
2831 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2832 if(ctf_teams & BIT(0))
2833 ctf_SpawnTeam("Red", NUM_TEAM_1);
2834 if(ctf_teams & BIT(1))
2835 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2836 if(ctf_teams & BIT(2))
2837 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2838 if(ctf_teams & BIT(3))
2839 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2842 ctf_ScoreRules(ctf_teams);
2845 void ctf_Initialize()
2847 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2849 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2850 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2851 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2853 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);