3 #include <common/effects/all.qh>
4 #include <common/mapobjects/teleporters.qh>
5 #include <common/mapobjects/triggers.qh>
6 #include <common/mutators/mutator/powerups/_mod.qh>
7 #include <common/vehicles/all.qh>
8 #include <server/command/vote.qh>
9 #include <server/client.qh>
10 #include <server/gamelog.qh>
11 #include <server/intermission.qh>
12 #include <server/damage.qh>
13 #include <server/world.qh>
14 #include <server/items/items.qh>
15 #include <server/race.qh>
16 #include <server/teamplay.qh>
18 #include <lib/warpzone/common.qh>
20 bool autocvar_g_ctf_allow_vehicle_carry;
21 bool autocvar_g_ctf_allow_vehicle_touch;
22 bool autocvar_g_ctf_allow_monster_touch;
23 bool autocvar_g_ctf_throw;
24 float autocvar_g_ctf_throw_angle_max;
25 float autocvar_g_ctf_throw_angle_min;
26 int autocvar_g_ctf_throw_punish_count;
27 float autocvar_g_ctf_throw_punish_delay;
28 float autocvar_g_ctf_throw_punish_time;
29 float autocvar_g_ctf_throw_strengthmultiplier;
30 float autocvar_g_ctf_throw_velocity_forward;
31 float autocvar_g_ctf_throw_velocity_up;
32 float autocvar_g_ctf_drop_velocity_up;
33 float autocvar_g_ctf_drop_velocity_side;
34 bool autocvar_g_ctf_oneflag_reverse;
35 bool autocvar_g_ctf_portalteleport;
36 bool autocvar_g_ctf_pass;
37 float autocvar_g_ctf_pass_arc;
38 float autocvar_g_ctf_pass_arc_max;
39 float autocvar_g_ctf_pass_directional_max;
40 float autocvar_g_ctf_pass_directional_min;
41 float autocvar_g_ctf_pass_radius;
42 float autocvar_g_ctf_pass_wait;
43 bool autocvar_g_ctf_pass_request;
44 float autocvar_g_ctf_pass_turnrate;
45 float autocvar_g_ctf_pass_timelimit;
46 float autocvar_g_ctf_pass_velocity;
47 bool autocvar_g_ctf_dynamiclights;
48 float autocvar_g_ctf_flag_collect_delay;
49 float autocvar_g_ctf_flag_damageforcescale;
50 bool autocvar_g_ctf_flag_dropped_waypoint;
51 bool autocvar_g_ctf_flag_dropped_floatinwater;
52 bool autocvar_g_ctf_flag_glowtrails;
53 int autocvar_g_ctf_flag_health;
54 bool autocvar_g_ctf_flag_return;
55 bool autocvar_g_ctf_flag_return_carrying;
56 float autocvar_g_ctf_flag_return_carried_radius;
57 float autocvar_g_ctf_flag_return_time;
58 bool autocvar_g_ctf_flag_return_when_unreachable;
59 float autocvar_g_ctf_flag_return_damage;
60 float autocvar_g_ctf_flag_return_damage_delay;
61 float autocvar_g_ctf_flag_return_dropped;
62 bool autocvar_g_ctf_flag_waypoint = true;
63 float autocvar_g_ctf_flag_waypoint_maxdistance;
64 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
65 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
66 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
67 float autocvar_g_ctf_flagcarrier_selfforcefactor;
68 float autocvar_g_ctf_flagcarrier_damagefactor;
69 float autocvar_g_ctf_flagcarrier_forcefactor;
70 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
71 bool autocvar_g_ctf_fullbrightflags;
72 bool autocvar_g_ctf_ignore_frags;
73 bool autocvar_g_ctf_score_ignore_fields;
74 int autocvar_g_ctf_score_capture;
75 int autocvar_g_ctf_score_capture_assist;
76 int autocvar_g_ctf_score_kill;
77 int autocvar_g_ctf_score_penalty_drop;
78 int autocvar_g_ctf_score_penalty_returned;
79 int autocvar_g_ctf_score_pickup_base;
80 int autocvar_g_ctf_score_pickup_dropped_early;
81 int autocvar_g_ctf_score_pickup_dropped_late;
82 int autocvar_g_ctf_score_return;
83 float autocvar_g_ctf_shield_force;
84 float autocvar_g_ctf_shield_max_ratio;
85 int autocvar_g_ctf_shield_min_negscore;
86 bool autocvar_g_ctf_stalemate;
87 int autocvar_g_ctf_stalemate_endcondition;
88 float autocvar_g_ctf_stalemate_time;
89 bool autocvar_g_ctf_reverse;
90 float autocvar_g_ctf_dropped_capture_delay;
91 float autocvar_g_ctf_dropped_capture_radius;
93 void ctf_FakeTimeLimit(entity e, float t)
96 WriteByte(MSG_ONE, 3); // svc_updatestat
97 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
99 WriteCoord(MSG_ONE, autocvar_timelimit);
101 WriteCoord(MSG_ONE, (t + 1) / 60);
104 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
106 if(autocvar_sv_eventlog)
107 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != NULL) ? ftos(actor.playerid) : "")));
108 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
111 void ctf_CaptureRecord(entity flag, entity player)
113 float cap_record = ctf_captimerecord;
114 float cap_time = (time - flag.ctf_pickuptime);
115 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
119 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname);
120 else if(!ctf_captimerecord)
121 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_TIME), player.netname, TIME_ENCODE(cap_time));
122 else if(cap_time < cap_record)
123 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_BROKEN), player.netname, refername, TIME_ENCODE(cap_time), TIME_ENCODE(cap_record));
125 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_UNBROKEN), player.netname, refername, TIME_ENCODE(cap_time), TIME_ENCODE(cap_record));
127 // write that shit in the database
128 if(!ctf_oneflag) // but not in 1-flag mode
129 if((!ctf_captimerecord) || (cap_time < cap_record))
131 ctf_captimerecord = cap_time;
132 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
133 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
134 write_recordmarker(player, flag.ctf_pickuptime, cap_time);
137 if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
138 race_setTime(GetMapname(), TIME_ENCODE(cap_time), player.crypto_idfp, player.netname, player, false);
141 bool ctf_Immediate_Return_Allowed(entity flag, entity toucher)
144 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), { ++num_perteam; });
146 // automatically return if there's only 1 player on the team
147 return ((autocvar_g_ctf_flag_return || num_perteam <= 1 || (autocvar_g_ctf_flag_return_carrying && toucher.flagcarried))
151 bool ctf_Return_Customize(entity this, entity client)
153 // only to the carrier
154 return boolean(client == this.owner);
157 void ctf_FlagcarrierWaypoints(entity player)
159 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
160 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, 2 * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
161 WaypointSprite_UpdateHealth(player.wps_flagcarrier, healtharmor_maxdamage(GetResource(player, RES_HEALTH), GetResource(player, RES_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
162 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
164 if(player.flagcarried && CTF_SAMETEAM(player, player.flagcarried))
166 if(!player.wps_enemyflagcarrier)
168 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, 0, player, wps_enemyflagcarrier, true, RADARICON_FLAG);
169 wp.colormod = WPCOLOR_ENEMYFC(player.team);
170 setcefc(wp, ctf_Stalemate_Customize);
172 if(IS_REAL_CLIENT(player) && !ctf_stalemate)
173 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_VISIBLE);
176 if(!player.wps_flagreturn)
178 entity owp = WaypointSprite_SpawnFixed(WP_FlagReturn, player.flagcarried.ctf_spawnorigin + FLAG_WAYPOINT_OFFSET, player, wps_flagreturn, RADARICON_FLAG);
179 owp.colormod = '0 0.8 0.8';
180 //WaypointSprite_UpdateTeamRadar(player.wps_flagreturn, RADARICON_FLAG, ((player.team) ? colormapPaletteColor(player.team - 1, false) : '1 1 1'));
181 setcefc(owp, ctf_Return_Customize);
186 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
188 float current_distance = vlen((('1 0 0' * to.x) + ('0 1 0' * to.y)) - (('1 0 0' * from.x) + ('0 1 0' * from.y))); // for the sake of this check, exclude Z axis
189 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
190 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
191 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
194 if(current_height) // make sure we can actually do this arcing path
196 targpos = (to + ('0 0 1' * current_height));
197 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
198 if(trace_fraction < 1)
200 //print("normal arc line failed, trying to find new pos...");
201 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
202 targpos = (trace_endpos + eZ * FLAG_PASS_ARC_OFFSET_Z);
203 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
204 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
205 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
208 else { targpos = to; }
210 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
212 vector desired_direction = normalize(targpos - from);
213 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
214 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
217 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
219 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
221 // directional tracing only
223 makevectors(passer_angle);
225 // find the closest point on the enemy to the center of the attack
226 float h; // hypotenuse, which is the distance between attacker to head
227 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
229 h = vlen(head_center - passer_center);
230 a = h * (normalize(head_center - passer_center) * v_forward);
232 vector nearest_on_line = (passer_center + a * v_forward);
233 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
235 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
236 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
238 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
243 else { return true; }
247 // =======================
248 // CaptureShield Functions
249 // =======================
251 bool ctf_CaptureShield_CheckStatus(entity p)
253 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
254 int players_worseeq, players_total;
256 if(ctf_captureshield_max_ratio <= 0)
259 s = GameRules_scoring_add(p, CTF_CAPS, 0);
260 s2 = GameRules_scoring_add(p, CTF_PICKUPS, 0);
261 s3 = GameRules_scoring_add(p, CTF_RETURNS, 0);
262 s4 = GameRules_scoring_add(p, CTF_FCKILLS, 0);
264 sr = ((s - s2) + (s3 + s4));
266 if(sr >= -ctf_captureshield_min_negscore)
269 players_total = players_worseeq = 0;
270 FOREACH_CLIENT(IS_PLAYER(it), {
273 se = GameRules_scoring_add(it, CTF_CAPS, 0);
274 se2 = GameRules_scoring_add(it, CTF_PICKUPS, 0);
275 se3 = GameRules_scoring_add(it, CTF_RETURNS, 0);
276 se4 = GameRules_scoring_add(it, CTF_FCKILLS, 0);
278 ser = ((se - se2) + (se3 + se4));
285 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
286 // use this rule here
288 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
294 void ctf_CaptureShield_Update(entity player, bool wanted_status)
296 bool updated_status = ctf_CaptureShield_CheckStatus(player);
297 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
299 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
300 player.ctf_captureshielded = updated_status;
304 bool ctf_CaptureShield_Customize(entity this, entity client)
306 if(!client.ctf_captureshielded) { return false; }
307 if(CTF_SAMETEAM(this, client)) { return false; }
312 void ctf_CaptureShield_Touch(entity this, entity toucher)
314 if(!toucher.ctf_captureshielded) { return; }
315 if(CTF_SAMETEAM(this, toucher)) { return; }
317 vector mymid = (this.absmin + this.absmax) * 0.5;
318 vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
320 Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
321 if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
324 void ctf_CaptureShield_Spawn(entity flag)
326 entity shield = new(ctf_captureshield);
329 shield.team = flag.team;
330 settouch(shield, ctf_CaptureShield_Touch);
331 setcefc(shield, ctf_CaptureShield_Customize);
332 shield.effects = EF_ADDITIVE;
333 set_movetype(shield, MOVETYPE_NOCLIP);
334 shield.solid = SOLID_TRIGGER;
335 shield.avelocity = '7 0 11';
338 setorigin(shield, flag.origin);
339 setmodel(shield, MDL_CTF_SHIELD);
340 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
344 // ====================
345 // Drop/Pass/Throw Code
346 // ====================
348 void ctf_Handle_Drop(entity flag, entity player, int droptype)
351 player = (player ? player : flag.pass_sender);
354 set_movetype(flag, MOVETYPE_TOSS);
355 flag.takedamage = DAMAGE_YES;
356 flag.angles = '0 0 0';
357 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
358 flag.ctf_droptime = time;
359 flag.ctf_landtime = 0;
360 flag.ctf_dropper = player;
361 flag.ctf_status = FLAG_DROPPED;
363 // messages and sounds
364 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
365 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
366 ctf_EventLog("dropped", player.team, player);
369 GameRules_scoring_add_team(player, SCORE, -((flag.score_drop) ? flag.score_drop : autocvar_g_ctf_score_penalty_drop));
370 GameRules_scoring_add(player, CTF_DROPS, 1);
373 if(autocvar_g_ctf_flag_dropped_waypoint) {
374 entity wp = WaypointSprite_Spawn(WP_FlagDropped, 0, 0, flag, FLAG_WAYPOINT_OFFSET, NULL, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, true, RADARICON_FLAG);
375 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
378 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
380 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_health);
381 WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH));
384 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
386 if(droptype == DROP_PASS)
388 flag.pass_distance = 0;
389 flag.pass_sender = NULL;
390 flag.pass_target = NULL;
394 void ctf_Handle_Retrieve(entity flag, entity player)
396 entity sender = flag.pass_sender;
398 // transfer flag to player
400 flag.owner.flagcarried = flag;
401 GameRules_scoring_vip(player, true);
404 flag.solid = SOLID_NOT; // before setorigin to prevent area grid linking
407 setattachment(flag, player.vehicle, "");
408 setorigin(flag, VEHICLE_FLAG_OFFSET);
409 flag.scale = VEHICLE_FLAG_SCALE;
413 setattachment(flag, player, "");
414 setorigin(flag, FLAG_CARRY_OFFSET);
416 set_movetype(flag, MOVETYPE_NONE);
417 flag.takedamage = DAMAGE_NO;
418 flag.angles = '0 0 0';
419 flag.ctf_status = FLAG_CARRY;
421 // messages and sounds
422 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
423 ctf_EventLog("receive", flag.team, player);
425 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
427 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
428 else if(it == player)
429 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
430 else if(SAME_TEAM(it, sender))
431 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
434 // create new waypoint
435 ctf_FlagcarrierWaypoints(player);
437 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
438 player.throw_antispam = sender.throw_antispam;
440 flag.pass_distance = 0;
441 flag.pass_sender = NULL;
442 flag.pass_target = NULL;
445 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
447 entity flag = player.flagcarried;
448 vector targ_origin, flag_velocity;
450 if(!flag) { return; }
451 if((droptype == DROP_PASS) && !receiver) { return; }
453 if(flag.speedrunning)
455 // ensure old waypoints are removed before resetting the flag
456 WaypointSprite_Kill(player.wps_flagcarrier);
458 if(player.wps_enemyflagcarrier)
459 WaypointSprite_Kill(player.wps_enemyflagcarrier);
461 if(player.wps_flagreturn)
462 WaypointSprite_Kill(player.wps_flagreturn);
463 ctf_RespawnFlag(flag);
468 setattachment(flag, NULL, "");
469 tracebox(player.origin - FLAG_DROP_OFFSET, flag.m_mins, flag.m_maxs, player.origin + FLAG_DROP_OFFSET, MOVE_NOMONSTERS, flag);
470 flag.solid = SOLID_TRIGGER; // before setorigin to ensure area grid linking
471 setorigin(flag, trace_endpos);
472 if (trace_startsolid && !nudgeoutofsolid(flag)) // TODO: trace_allsolid would perform better but isn't 100% reliable yet
474 // the flag's bbox doesn't fit but we can assume the player's current bbox does
475 tracebox(player.origin - FLAG_DROP_OFFSET, player.mins, player.maxs, player.origin + FLAG_DROP_OFFSET, MOVE_NOMONSTERS, flag);
476 flag.origin = trace_endpos;
477 setsize(flag, player.mins, player.maxs); // this allows physics to move the flag somewhere its think func can resize it
479 flag.owner.flagcarried = NULL;
480 GameRules_scoring_vip(flag.owner, false);
482 flag.ctf_dropper = player;
483 flag.ctf_droptime = time;
484 flag.ctf_landtime = 0;
486 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
493 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
494 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
495 WarpZone_RefSys_Copy(flag, receiver);
496 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
497 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
499 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
500 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
503 set_movetype(flag, MOVETYPE_FLY);
504 flag.takedamage = DAMAGE_NO;
505 flag.pass_sender = player;
506 flag.pass_target = receiver;
507 flag.ctf_status = FLAG_PASSING;
510 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
511 WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
512 ctf_EventLog("pass", flag.team, player);
518 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'));
520 flag_velocity = (('0 0 1' * autocvar_g_ctf_throw_velocity_up) + ((v_forward * autocvar_g_ctf_throw_velocity_forward) * ((StatusEffects_active(STATUSEFFECT_Strength, player)) ? autocvar_g_ctf_throw_strengthmultiplier : 1)));
521 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
522 ctf_Handle_Drop(flag, player, droptype);
523 navigation_dynamicgoal_set(flag, player);
529 flag.velocity = '0 0 0'; // do nothing
536 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);
537 ctf_Handle_Drop(flag, player, droptype);
538 navigation_dynamicgoal_set(flag, player);
543 // kill old waypointsprite
544 WaypointSprite_Ping(player.wps_flagcarrier);
545 WaypointSprite_Kill(player.wps_flagcarrier);
547 if(player.wps_enemyflagcarrier)
548 WaypointSprite_Kill(player.wps_enemyflagcarrier);
550 if(player.wps_flagreturn)
551 WaypointSprite_Kill(player.wps_flagreturn);
554 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
558 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
560 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
568 void nades_GiveBonus(entity player, float score);
570 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
572 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
573 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
574 entity player_team_flag = NULL, tmp_entity;
575 float old_time, new_time;
577 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
578 if(CTF_DIFFTEAM(player, flag)) { return; }
579 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)
581 if (toucher.goalentity == flag.bot_basewaypoint)
582 toucher.goalentity_lock_timeout = 0;
585 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
586 if(SAME_TEAM(tmp_entity, player))
588 player_team_flag = tmp_entity;
592 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
594 player.throw_prevtime = time;
595 player.throw_count = 0;
597 // messages and sounds
598 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
599 ctf_CaptureRecord(enemy_flag, player);
600 _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);
604 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
605 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
611 if(enemy_flag.score_capture || flag.score_capture)
612 pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
613 GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
615 if(enemy_flag.score_team_capture || flag.score_team_capture)
616 capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
617 GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
619 old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
620 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
621 if(!old_time || new_time < old_time)
622 GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
625 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
627 shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
631 if(capturetype == CAPTURE_NORMAL)
633 WaypointSprite_Kill(player.wps_flagcarrier);
634 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
636 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
637 { GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist)); }
640 flag.enemy = toucher;
643 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
644 ctf_RespawnFlag(enemy_flag);
647 void ctf_Handle_Return(entity flag, entity player)
649 // messages and sounds
650 if(IS_MONSTER(player))
652 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
656 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
657 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
659 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
660 ctf_EventLog("return", flag.team, player);
663 if(IS_PLAYER(player))
665 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
666 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
668 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
671 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
675 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
676 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
677 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
681 if(player.flagcarried == flag)
682 WaypointSprite_Kill(player.wps_flagcarrier);
687 ctf_RespawnFlag(flag);
690 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
693 float pickup_dropped_score; // used to calculate dropped pickup score
695 // attach the flag to the player
697 player.flagcarried = flag;
698 GameRules_scoring_vip(player, true);
699 flag.solid = SOLID_NOT; // before setorigin to prevent area grid linking
702 setattachment(flag, player.vehicle, "");
703 setorigin(flag, VEHICLE_FLAG_OFFSET);
704 flag.scale = VEHICLE_FLAG_SCALE;
708 setattachment(flag, player, "");
709 setorigin(flag, FLAG_CARRY_OFFSET);
713 set_movetype(flag, MOVETYPE_NONE);
714 flag.takedamage = DAMAGE_NO;
715 flag.angles = '0 0 0';
716 flag.ctf_status = FLAG_CARRY;
720 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
721 case PICKUP_DROPPED: SetResourceExplicit(flag, RES_HEALTH, flag.max_health); break; // reset health/return timelimit
725 // messages and sounds
726 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
728 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
730 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
731 else if(CTF_DIFFTEAM(player, flag))
732 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
734 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
736 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
739 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); });
742 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
743 if(CTF_SAMETEAM(flag, it))
744 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);
745 else if(DIFF_TEAM(player, it))
746 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_ENEMY_OTHER), Team_ColorCode(player.team), player.netname);
749 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
752 GameRules_scoring_add(player, CTF_PICKUPS, 1);
753 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
758 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
759 ctf_EventLog("steal", flag.team, player);
765 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);
766 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);
767 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
768 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
769 ctf_EventLog("pickup", flag.team, player);
777 if(pickuptype == PICKUP_BASE)
779 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
780 if((player.speedrunning) && (ctf_captimerecord))
781 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
785 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
788 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
789 ctf_FlagcarrierWaypoints(player);
790 WaypointSprite_Ping(player.wps_flagcarrier);
794 // ===================
795 // Main Flag Functions
796 // ===================
798 void ctf_CheckFlagReturn(entity flag, int returntype)
800 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
802 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH)); }
804 if((GetResource(flag, RES_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
809 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
811 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
812 case RETURN_SPEEDRUN:
813 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
814 case RETURN_NEEDKILL:
815 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
818 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
820 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
821 ctf_EventLog("returned", flag.team, NULL);
823 ctf_RespawnFlag(flag);
828 bool ctf_Stalemate_Customize(entity this, entity client)
830 // make spectators see what the player would see
831 entity e = WaypointSprite_getviewentity(client);
832 entity wp_owner = this.owner;
835 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
836 if(SAME_TEAM(wp_owner, e)) { return false; }
837 if(!IS_PLAYER(e)) { return false; }
842 void ctf_CheckStalemate()
845 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
848 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
850 // build list of stale flags
851 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
853 if(autocvar_g_ctf_stalemate)
854 if(tmp_entity.ctf_status != FLAG_BASE)
855 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
857 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
858 ctf_staleflaglist = tmp_entity;
860 switch(tmp_entity.team)
862 case NUM_TEAM_1: ++stale_red_flags; break;
863 case NUM_TEAM_2: ++stale_blue_flags; break;
864 case NUM_TEAM_3: ++stale_yellow_flags; break;
865 case NUM_TEAM_4: ++stale_pink_flags; break;
866 default: ++stale_neutral_flags; break;
872 stale_flags = (stale_neutral_flags >= 1);
874 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
876 if(ctf_oneflag && stale_flags == 1)
877 ctf_stalemate = true;
878 else if(stale_flags >= 2)
879 ctf_stalemate = true;
880 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
881 { ctf_stalemate = false; wpforenemy_announced = false; }
882 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
883 { ctf_stalemate = false; wpforenemy_announced = false; }
885 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
888 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
890 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
892 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);
893 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
894 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
898 if (!wpforenemy_announced)
900 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)); });
902 wpforenemy_announced = true;
907 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
909 if(ITEM_DAMAGE_NEEDKILL(deathtype))
911 if(autocvar_g_ctf_flag_return_damage_delay)
912 this.ctf_flagdamaged_byworld = true;
915 SetResourceExplicit(this, RES_HEALTH, 0);
916 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
920 if(autocvar_g_ctf_flag_return_damage)
922 // reduce health and check if it should be returned
923 TakeResource(this, RES_HEALTH, damage);
924 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
929 void ctf_FlagThink(entity this)
934 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
937 if(this == ctf_worldflaglist) // only for the first flag
938 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
941 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
942 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
943 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
944 setsize(this, this.m_mins, this.m_maxs);
948 switch(this.ctf_status)
952 if(autocvar_g_ctf_dropped_capture_radius)
954 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
955 if(tmp_entity.ctf_status == FLAG_DROPPED)
956 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
957 if((this.noalign || tmp_entity.ctf_landtime) && time > ((this.noalign) ? tmp_entity.ctf_droptime : tmp_entity.ctf_landtime) + autocvar_g_ctf_dropped_capture_delay)
958 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
965 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
966 if(IS_ONGROUND(this) && !this.ctf_landtime)
967 this.ctf_landtime = time; // landtime is reset when thrown, and we don't want to restart the timer if the flag is pushed
969 if(autocvar_g_ctf_flag_dropped_floatinwater)
971 vector midpoint = ((this.absmin + this.absmax) * 0.5);
972 if(pointcontents(midpoint) == CONTENT_WATER)
974 this.velocity = this.velocity * 0.5;
976 if (pointcontents(midpoint + eZ * FLAG_FLOAT_OFFSET_Z) == CONTENT_WATER)
977 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
979 { set_movetype(this, MOVETYPE_FLY); }
981 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
983 if(autocvar_g_ctf_flag_return_dropped)
985 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
987 SetResourceExplicit(this, RES_HEALTH, 0);
988 ctf_CheckFlagReturn(this, RETURN_DROPPED);
992 if(this.ctf_flagdamaged_byworld)
994 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
995 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
998 else if(autocvar_g_ctf_flag_return_time)
1000 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
1001 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
1009 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
1011 SetResourceExplicit(this, RES_HEALTH, 0);
1012 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
1014 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
1015 ImpulseCommands(this.owner);
1017 if(autocvar_g_ctf_stalemate)
1019 if(time >= wpforenemy_nextthink)
1021 ctf_CheckStalemate();
1022 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1025 if(CTF_SAMETEAM(this, this.owner) && this.team)
1027 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1028 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
1029 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
1030 ctf_Handle_Return(this, this.owner);
1037 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1038 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1039 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1041 if((this.pass_target == NULL)
1042 || (IS_DEAD(this.pass_target))
1043 || (this.pass_target.flagcarried)
1044 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1045 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1046 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1048 // give up, pass failed
1049 ctf_Handle_Drop(this, NULL, DROP_PASS);
1053 // still a viable target, go for it
1054 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1059 default: // this should never happen
1061 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1067 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1070 if(game_stopped) return;
1071 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1073 bool is_not_monster = (!IS_MONSTER(toucher));
1075 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1076 if(ITEM_TOUCH_NEEDKILL())
1078 if(!autocvar_g_ctf_flag_return_damage_delay)
1080 SetResourceExplicit(flag, RES_HEALTH, 0);
1081 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1083 if(!flag.ctf_flagdamaged_byworld) { return; }
1086 // special touch behaviors
1087 if(STAT(FROZEN, toucher)) { return; }
1088 else if(IS_VEHICLE(toucher))
1090 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1091 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1093 return; // do nothing
1095 else if(IS_MONSTER(toucher))
1097 if(!autocvar_g_ctf_allow_monster_touch)
1098 return; // do nothing
1100 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1102 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1104 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1105 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1106 flag.wait = time + FLAG_TOUCHRATE;
1110 else if(IS_DEAD(toucher)) { return; }
1112 switch(flag.ctf_status)
1118 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1119 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1120 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1121 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1123 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1124 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to their base
1125 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)
1127 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1128 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1130 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1131 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1137 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1138 ctf_Handle_Return(flag, toucher); // toucher just returned their own flag
1139 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1140 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1146 LOG_TRACE("Someone touched a flag even though it was being carried?");
1152 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1154 if(DIFF_TEAM(toucher, flag.pass_sender))
1156 if(ctf_Immediate_Return_Allowed(flag, toucher))
1157 ctf_Handle_Return(flag, toucher);
1158 else if(is_not_monster && (!toucher.flagcarried))
1159 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1161 else if(!toucher.flagcarried)
1162 ctf_Handle_Retrieve(flag, toucher);
1169 void ctf_RespawnFlag(entity flag)
1171 flag.watertype = CONTENT_EMPTY; // TODO: it is unclear why this workaround is needed, likely many other potential breakage points!!
1173 // reset the player (if there is one)
1174 if((flag.owner) && (flag.owner.flagcarried == flag))
1176 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1177 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1178 WaypointSprite_Kill(flag.wps_flagcarrier);
1180 flag.owner.flagcarried = NULL;
1181 GameRules_scoring_vip(flag.owner, false);
1183 if(flag.speedrunning)
1184 ctf_FakeTimeLimit(flag.owner, -1);
1187 if((flag.owner) && (flag.owner.vehicle))
1188 flag.scale = FLAG_SCALE;
1190 if(flag.ctf_status == FLAG_DROPPED)
1191 { WaypointSprite_Kill(flag.wps_flagdropped); }
1194 setattachment(flag, NULL, "");
1195 flag.solid = SOLID_TRIGGER; // before setorigin to ensure area grid linking
1196 setorigin(flag, flag.ctf_spawnorigin);
1198 //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1199 set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1200 flag.takedamage = DAMAGE_NO;
1201 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1202 flag.velocity = '0 0 0';
1203 flag.angles = flag.mangle;
1204 flag.flags = FL_ITEM | FL_NOTARGET;
1206 flag.ctf_status = FLAG_BASE;
1208 flag.pass_distance = 0;
1209 flag.pass_sender = NULL;
1210 flag.pass_target = NULL;
1211 flag.ctf_dropper = NULL;
1212 flag.ctf_pickuptime = 0;
1213 flag.ctf_droptime = 0;
1214 flag.ctf_landtime = 0;
1215 flag.ctf_flagdamaged_byworld = false;
1216 navigation_dynamicgoal_unset(flag);
1218 ctf_CheckStalemate();
1221 void ctf_Reset(entity this)
1223 if(this.owner && IS_PLAYER(this.owner))
1224 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1227 ctf_RespawnFlag(this);
1230 bool ctf_FlagBase_Customize(entity this, entity client)
1232 entity e = WaypointSprite_getviewentity(client);
1233 entity wp_owner = this.owner;
1234 entity flag = e.flagcarried;
1235 if(flag && CTF_SAMETEAM(e, flag))
1237 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1242 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1245 waypoint_spawnforitem_force(this, this.origin);
1246 navigation_dynamicgoal_init(this, true);
1252 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1253 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1254 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1255 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1256 default: basename = WP_FlagBaseNeutral; break;
1259 if(autocvar_g_ctf_flag_waypoint)
1261 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1262 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1263 wp.fade_rate = autocvar_g_ctf_flag_waypoint_maxdistance;
1264 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1265 setcefc(wp, ctf_FlagBase_Customize);
1268 // captureshield setup
1269 ctf_CaptureShield_Spawn(this);
1274 void ctf_FlagSetup(int teamnum, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1277 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1278 ctf_worldflaglist = flag;
1280 setattachment(flag, NULL, "");
1282 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnum), Team_ColorName_Upper(teamnum)));
1283 flag.team = teamnum;
1284 flag.classname = "item_flag_team";
1285 flag.target = "###item###"; // for finding the nearest item using findnearest
1286 flag.flags = FL_ITEM | FL_NOTARGET;
1287 IL_PUSH(g_items, flag);
1288 flag.solid = SOLID_TRIGGER;
1289 flag.takedamage = DAMAGE_NO;
1290 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1291 flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1292 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1293 flag.event_damage = ctf_FlagDamage;
1294 flag.pushable = true;
1295 flag.teleportable = TELEPORT_NORMAL;
1296 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1297 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1298 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1299 if(flag.damagedbycontents)
1300 IL_PUSH(g_damagedbycontents, flag);
1301 flag.velocity = '0 0 0';
1302 flag.mangle = flag.angles;
1303 flag.reset = ctf_Reset;
1304 settouch(flag, ctf_FlagTouch);
1305 setthink(flag, ctf_FlagThink);
1306 flag.nextthink = time + FLAG_THINKRATE;
1307 flag.ctf_status = FLAG_BASE;
1309 // set correct team colors
1310 flag.glowmod = Team_ColorRGB(teamnum);
1311 flag.colormap = (teamnum) ? (teamnum - 1) * 0x11 : 0x00;
1312 flag.colormap |= BIT(10); // RENDER_COLORMAPPED
1314 // crudely force them all to 0
1315 if(autocvar_g_ctf_score_ignore_fields)
1316 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1318 string teamname = Static_Team_ColorName_Lower(teamnum);
1320 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1321 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1322 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1323 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnum).eent_eff_name; }
1324 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnum).eent_eff_name; }
1325 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnum).eent_eff_name; }
1329 if(flag.s == "") flag.s = b; \
1330 precache_sound(flag.s);
1332 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnum))))
1333 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnum))))
1334 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnum))))
1335 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnum))))
1336 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1337 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1338 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1342 precache_model(flag.model);
1345 _setmodel(flag, flag.model); // precision set below
1346 // 0.8.6 with sv_legacy_bbox_expand 1 did this FL_ITEM expansion in DP
1347 setsize(flag, CTF_FLAG.m_mins * flag.scale - '15 15 1', CTF_FLAG.m_maxs * flag.scale + '15 15 1');
1348 flag.m_mins = flag.mins; // store these for squash checks
1349 flag.m_maxs = flag.maxs;
1350 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1352 if(autocvar_g_ctf_flag_glowtrails)
1356 case NUM_TEAM_1: flag.glow_color = 251; break;
1357 case NUM_TEAM_2: flag.glow_color = 210; break;
1358 case NUM_TEAM_3: flag.glow_color = 110; break;
1359 case NUM_TEAM_4: flag.glow_color = 145; break;
1360 default: flag.glow_color = 254; break;
1362 flag.glow_size = 25;
1363 flag.glow_trail = 1;
1366 flag.effects |= EF_LOWPRECISION;
1367 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1368 if(autocvar_g_ctf_dynamiclights)
1372 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1373 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1374 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1375 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1376 default: flag.effects |= EF_DIMLIGHT; break;
1381 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1383 flag.dropped_origin = flag.origin;
1384 flag.noalign = true;
1385 set_movetype(flag, MOVETYPE_NONE);
1387 else // drop to floor, automatically find a platform and set that as spawn origin
1389 flag.noalign = false;
1390 DropToFloor_QC_DelayedInit(flag);
1391 set_movetype(flag, MOVETYPE_NONE);
1394 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1402 // NOTE: LEGACY CODE, needs to be re-written!
1404 void havocbot_ctf_calculate_middlepoint()
1408 vector fo = '0 0 0';
1411 f = ctf_worldflaglist;
1416 f = f.ctf_worldflagnext;
1422 havocbot_middlepoint = s / n;
1423 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1425 havocbot_symmetry_axis_m = 0;
1426 havocbot_symmetry_axis_q = 0;
1429 // for symmetrical editing of waypoints
1430 entity f1 = ctf_worldflaglist;
1431 entity f2 = f1.ctf_worldflagnext;
1432 float m = -(f1.origin.y - f2.origin.y) / (max(f1.origin.x - f2.origin.x, FLOAT_EPSILON));
1433 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1434 havocbot_symmetry_axis_m = m;
1435 havocbot_symmetry_axis_q = q;
1437 havocbot_symmetry_origin_order = n;
1441 entity havocbot_ctf_find_flag(entity bot)
1444 f = ctf_worldflaglist;
1447 if (CTF_SAMETEAM(bot, f))
1449 f = f.ctf_worldflagnext;
1454 entity havocbot_ctf_find_enemy_flag(entity bot)
1457 f = ctf_worldflaglist;
1462 if(CTF_DIFFTEAM(bot, f))
1469 else if(!bot.flagcarried)
1473 else if (CTF_DIFFTEAM(bot, f))
1475 f = f.ctf_worldflagnext;
1480 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1487 FOREACH_CLIENT(IS_PLAYER(it), {
1488 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1491 if(vdist(it.origin - org, <, tc_radius))
1500 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1503 head = ctf_worldflaglist;
1506 if (CTF_SAMETEAM(this, head))
1508 head = head.ctf_worldflagnext;
1511 navigation_routerating(this, head, ratingscale, 10000);
1515 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1518 head = ctf_worldflaglist;
1521 if (CTF_SAMETEAM(this, head))
1523 if (this.flagcarried)
1524 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1526 head = head.ctf_worldflagnext; // skip base if it has a different group
1531 head = head.ctf_worldflagnext;
1536 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1539 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1542 head = ctf_worldflaglist;
1547 if(CTF_DIFFTEAM(this, head))
1551 if(this.flagcarried)
1554 else if(!this.flagcarried)
1558 else if(CTF_DIFFTEAM(this, head))
1560 head = head.ctf_worldflagnext;
1564 if (head.ctf_status == FLAG_CARRY)
1566 // adjust rating of our flag carrier depending on their health
1567 head = head.tag_entity;
1568 float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1569 ratingscale += ratingscale * f * 0.1;
1571 navigation_routerating(this, head, ratingscale, 10000);
1575 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1577 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1579 if (!bot_waypoints_for_items)
1581 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1587 head = havocbot_ctf_find_enemy_flag(this);
1592 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1595 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1599 mf = havocbot_ctf_find_flag(this);
1601 if(mf.ctf_status == FLAG_BASE)
1605 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1608 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1611 head = ctf_worldflaglist;
1614 // flag is out in the field
1615 if(head.ctf_status != FLAG_BASE)
1616 if(head.tag_entity==NULL) // dropped
1620 if(vdist(org - head.origin, <, df_radius))
1621 navigation_routerating(this, head, ratingscale, 10000);
1624 navigation_routerating(this, head, ratingscale, 10000);
1627 head = head.ctf_worldflagnext;
1631 void havocbot_ctf_reset_role(entity this)
1633 float cdefense, cmiddle, coffense;
1640 if (this.flagcarried)
1642 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1646 mf = havocbot_ctf_find_flag(this);
1647 ef = havocbot_ctf_find_enemy_flag(this);
1649 // Retrieve stolen flag
1650 if(mf.ctf_status!=FLAG_BASE)
1652 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1656 // If enemy flag is taken go to the middle to intercept pursuers
1657 if(ef.ctf_status!=FLAG_BASE)
1659 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1663 // if there is no one else on the team switch to offense
1665 // don't check if this bot is a player since it isn't true when the bot is added to the server
1666 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1670 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1673 else if (time < CS(this).jointime + 1)
1675 // if bots spawn all at once set good default roles
1678 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1681 else if (count == 2)
1683 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1688 // Evaluate best position to take
1689 // Count mates on middle position
1690 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1692 // Count mates on defense position
1693 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1695 // Count mates on offense position
1696 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1698 if(cdefense<=coffense)
1699 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1700 else if(coffense<=cmiddle)
1701 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1703 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1705 // if bots spawn all at once assign them a more appropriated role after a while
1706 if (time < CS(this).jointime + 1 && count > 2)
1707 this.havocbot_role_timeout = time + 10 + random() * 10;
1710 bool havocbot_ctf_is_basewaypoint(entity item)
1712 if (item.classname != "waypoint")
1715 entity head = ctf_worldflaglist;
1718 if (item == head.bot_basewaypoint)
1720 head = head.ctf_worldflagnext;
1725 void havocbot_role_ctf_carrier(entity this)
1729 havocbot_ctf_reset_role(this);
1733 if (this.flagcarried == NULL)
1735 havocbot_ctf_reset_role(this);
1739 if (navigation_goalrating_timeout(this))
1741 navigation_goalrating_start(this);
1744 entity mf = havocbot_ctf_find_flag(this);
1745 vector base_org = mf.dropped_origin;
1746 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1748 havocbot_goalrating_ctf_enemybase(this, base_rating);
1750 havocbot_goalrating_ctf_ourbase(this, base_rating);
1752 // start collecting items very close to the bot but only inside of own base radius
1753 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1754 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1756 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1758 navigation_goalrating_end(this);
1760 navigation_goalrating_timeout_set(this);
1762 entity goal = this.goalentity;
1763 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1764 this.goalentity_lock_timeout = time + ((this.enemy) ? 2 : 3);
1767 this.havocbot_cantfindflag = time + 10;
1768 else if (time > this.havocbot_cantfindflag)
1770 // Can't navigate to my own base, suicide!
1771 // TODO: drop it and wander around
1772 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1778 void havocbot_role_ctf_escort(entity this)
1784 havocbot_ctf_reset_role(this);
1788 if (this.flagcarried)
1790 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1794 // If enemy flag is back on the base switch to previous role
1795 ef = havocbot_ctf_find_enemy_flag(this);
1796 if(ef.ctf_status==FLAG_BASE)
1798 this.havocbot_role = this.havocbot_previous_role;
1799 this.havocbot_role_timeout = 0;
1802 if (ef.ctf_status == FLAG_DROPPED)
1804 navigation_goalrating_timeout_expire(this, 1);
1808 // If the flag carrier reached the base switch to defense
1809 mf = havocbot_ctf_find_flag(this);
1810 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1812 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1816 // Set the role timeout if necessary
1817 if (!this.havocbot_role_timeout)
1819 this.havocbot_role_timeout = time + random() * 30 + 60;
1822 // If nothing happened just switch to previous role
1823 if (time > this.havocbot_role_timeout)
1825 this.havocbot_role = this.havocbot_previous_role;
1826 this.havocbot_role_timeout = 0;
1830 // Chase the flag carrier
1831 if (navigation_goalrating_timeout(this))
1833 navigation_goalrating_start(this);
1836 havocbot_goalrating_ctf_enemyflag(this, 10000);
1837 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1838 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1840 navigation_goalrating_end(this);
1842 navigation_goalrating_timeout_set(this);
1846 void havocbot_role_ctf_offense(entity this)
1853 havocbot_ctf_reset_role(this);
1857 if (this.flagcarried)
1859 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1864 mf = havocbot_ctf_find_flag(this);
1865 ef = havocbot_ctf_find_enemy_flag(this);
1868 if(mf.ctf_status!=FLAG_BASE)
1871 pos = mf.tag_entity.origin;
1875 // Try to get it if closer than the enemy base
1876 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1878 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1883 // Escort flag carrier
1884 if(ef.ctf_status!=FLAG_BASE)
1887 pos = ef.tag_entity.origin;
1891 if(vdist(pos - mf.dropped_origin, >, 700))
1893 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1898 // Set the role timeout if necessary
1899 if (!this.havocbot_role_timeout)
1900 this.havocbot_role_timeout = time + 120;
1902 if (time > this.havocbot_role_timeout)
1904 havocbot_ctf_reset_role(this);
1908 if (navigation_goalrating_timeout(this))
1910 navigation_goalrating_start(this);
1913 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1914 havocbot_goalrating_ctf_enemybase(this, 10000);
1915 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1917 navigation_goalrating_end(this);
1919 navigation_goalrating_timeout_set(this);
1923 // Retriever (temporary role):
1924 void havocbot_role_ctf_retriever(entity this)
1930 havocbot_ctf_reset_role(this);
1934 if (this.flagcarried)
1936 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1940 // If flag is back on the base switch to previous role
1941 mf = havocbot_ctf_find_flag(this);
1942 if(mf.ctf_status==FLAG_BASE)
1944 if (mf.enemy == this) // did this bot return the flag?
1945 navigation_goalrating_timeout_force(this);
1946 havocbot_ctf_reset_role(this);
1950 if (!this.havocbot_role_timeout)
1951 this.havocbot_role_timeout = time + 20;
1953 if (time > this.havocbot_role_timeout)
1955 havocbot_ctf_reset_role(this);
1959 if (navigation_goalrating_timeout(this))
1961 const float RT_RADIUS = 10000;
1963 navigation_goalrating_start(this);
1966 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1967 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1968 havocbot_goalrating_ctf_enemybase(this, 8000);
1969 entity ef = havocbot_ctf_find_enemy_flag(this);
1970 vector enemy_base_org = ef.dropped_origin;
1971 // start collecting items very close to the bot but only inside of enemy base radius
1972 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1973 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1974 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1976 navigation_goalrating_end(this);
1978 navigation_goalrating_timeout_set(this);
1982 void havocbot_role_ctf_middle(entity this)
1988 havocbot_ctf_reset_role(this);
1992 if (this.flagcarried)
1994 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1998 mf = havocbot_ctf_find_flag(this);
1999 if(mf.ctf_status!=FLAG_BASE)
2001 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2005 if (!this.havocbot_role_timeout)
2006 this.havocbot_role_timeout = time + 10;
2008 if (time > this.havocbot_role_timeout)
2010 havocbot_ctf_reset_role(this);
2014 if (navigation_goalrating_timeout(this))
2018 org = havocbot_middlepoint;
2019 org.z = this.origin.z;
2021 navigation_goalrating_start(this);
2024 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
2025 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
2026 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2027 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2028 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2029 havocbot_goalrating_ctf_enemybase(this, 3000);
2031 navigation_goalrating_end(this);
2033 entity goal = this.goalentity;
2034 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
2035 this.goalentity_lock_timeout = time + 2;
2037 navigation_goalrating_timeout_set(this);
2041 void havocbot_role_ctf_defense(entity this)
2047 havocbot_ctf_reset_role(this);
2051 if (this.flagcarried)
2053 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2057 // If own flag was captured
2058 mf = havocbot_ctf_find_flag(this);
2059 if(mf.ctf_status!=FLAG_BASE)
2061 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2065 if (!this.havocbot_role_timeout)
2066 this.havocbot_role_timeout = time + 30;
2068 if (time > this.havocbot_role_timeout)
2070 havocbot_ctf_reset_role(this);
2073 if (navigation_goalrating_timeout(this))
2075 vector org = mf.dropped_origin;
2077 navigation_goalrating_start(this);
2079 // if enemies are closer to our base, go there
2080 entity closestplayer = NULL;
2081 float distance, bestdistance = 10000;
2082 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2083 distance = vlen(org - it.origin);
2084 if(distance<bestdistance)
2087 bestdistance = distance;
2093 if(DIFF_TEAM(closestplayer, this))
2094 if(vdist(org - this.origin, >, 1000))
2095 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2096 havocbot_goalrating_ctf_ourbase(this, 10000);
2098 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2099 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2100 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2101 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2102 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2104 navigation_goalrating_end(this);
2106 navigation_goalrating_timeout_set(this);
2110 void havocbot_role_ctf_setrole(entity bot, int role)
2112 string s = "(null)";
2115 case HAVOCBOT_CTF_ROLE_CARRIER:
2117 bot.havocbot_role = havocbot_role_ctf_carrier;
2118 bot.havocbot_role_timeout = 0;
2119 bot.havocbot_cantfindflag = time + 10;
2120 if (bot.havocbot_previous_role != bot.havocbot_role)
2121 navigation_goalrating_timeout_force(bot);
2123 case HAVOCBOT_CTF_ROLE_DEFENSE:
2125 bot.havocbot_role = havocbot_role_ctf_defense;
2126 bot.havocbot_role_timeout = 0;
2128 case HAVOCBOT_CTF_ROLE_MIDDLE:
2130 bot.havocbot_role = havocbot_role_ctf_middle;
2131 bot.havocbot_role_timeout = 0;
2133 case HAVOCBOT_CTF_ROLE_OFFENSE:
2135 bot.havocbot_role = havocbot_role_ctf_offense;
2136 bot.havocbot_role_timeout = 0;
2138 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2140 bot.havocbot_previous_role = bot.havocbot_role;
2141 bot.havocbot_role = havocbot_role_ctf_retriever;
2142 bot.havocbot_role_timeout = time + 10;
2143 if (bot.havocbot_previous_role != bot.havocbot_role)
2144 navigation_goalrating_timeout_expire(bot, 2);
2146 case HAVOCBOT_CTF_ROLE_ESCORT:
2148 bot.havocbot_previous_role = bot.havocbot_role;
2149 bot.havocbot_role = havocbot_role_ctf_escort;
2150 bot.havocbot_role_timeout = time + 30;
2151 if (bot.havocbot_previous_role != bot.havocbot_role)
2152 navigation_goalrating_timeout_expire(bot, 2);
2155 LOG_TRACE(bot.netname, " switched to ", s);
2163 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2165 entity player = M_ARGV(0, entity);
2167 int t = 0, t2 = 0, t3 = 0;
2168 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)
2170 // initially clear items so they can be set as necessary later.
2171 STAT(OBJECTIVE_STATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2172 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2173 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2174 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2175 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2176 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2178 // scan through all the flags and notify the client about them
2179 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2181 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2182 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2183 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2184 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2185 if(flag.team == 0 && !b5) { b5 = true; t = CTF_NEUTRAL_FLAG_CARRYING; t2 = CTF_NEUTRAL_FLAG_TAKEN; t3 = CTF_NEUTRAL_FLAG_LOST; STAT(OBJECTIVE_STATUS, player) |= CTF_FLAG_NEUTRAL; }
2187 switch(flag.ctf_status)
2192 if((flag.owner == player) || (flag.pass_sender == player))
2193 STAT(OBJECTIVE_STATUS, player) |= t; // carrying: player is currently carrying the flag
2195 STAT(OBJECTIVE_STATUS, player) |= t2; // taken: someone else is carrying the flag
2200 STAT(OBJECTIVE_STATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2206 // item for stopping players from capturing the flag too often
2207 if(player.ctf_captureshielded)
2208 STAT(OBJECTIVE_STATUS, player) |= CTF_SHIELDED;
2211 STAT(OBJECTIVE_STATUS, player) |= CTF_STALEMATE;
2213 // update the health of the flag carrier waypointsprite
2214 if(player.wps_flagcarrier)
2215 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);
2218 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in damage.qc
2220 entity frag_attacker = M_ARGV(1, entity);
2221 entity frag_target = M_ARGV(2, entity);
2222 float frag_damage = M_ARGV(4, float);
2223 vector frag_force = M_ARGV(6, vector);
2225 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2227 if(frag_target == frag_attacker) // damage done to yourself
2229 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2230 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2232 else // damage done to everyone else
2234 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2235 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2238 M_ARGV(4, float) = frag_damage;
2239 M_ARGV(6, vector) = frag_force;
2241 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2243 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
2244 && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2246 frag_target.wps_helpme_time = time;
2247 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2249 // todo: add notification for when flag carrier needs help?
2253 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2255 entity frag_attacker = M_ARGV(1, entity);
2256 entity frag_target = M_ARGV(2, entity);
2258 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2260 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2261 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2264 if(frag_target.flagcarried)
2266 entity tmp_entity = frag_target.flagcarried;
2267 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2268 tmp_entity.ctf_dropper = NULL;
2272 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2274 M_ARGV(2, float) = 0; // frag score
2275 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2278 void ctf_RemovePlayer(entity player)
2280 if(player.flagcarried)
2281 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2283 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2285 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2286 if(flag.pass_target == player) { flag.pass_target = NULL; }
2287 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2291 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2293 entity player = M_ARGV(0, entity);
2295 ctf_RemovePlayer(player);
2298 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2300 entity player = M_ARGV(0, entity);
2302 ctf_RemovePlayer(player);
2305 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2307 if(!autocvar_g_ctf_leaderboard)
2310 entity player = M_ARGV(0, entity);
2312 race_SendAll(player, true);
2315 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2317 if(!autocvar_g_ctf_leaderboard)
2320 entity player = M_ARGV(0, entity);
2322 race_checkAndWriteName(player);
2325 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2327 entity player = M_ARGV(0, entity);
2329 if(player.flagcarried)
2330 if(!autocvar_g_ctf_portalteleport)
2331 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2334 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2336 if(MUTATOR_RETURNVALUE || game_stopped) return;
2338 entity player = M_ARGV(0, entity);
2340 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2342 // pass the flag to a team mate
2343 if(autocvar_g_ctf_pass)
2345 entity head, closest_target = NULL;
2346 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2348 while(head) // find the closest acceptable target to pass to
2350 if(IS_PLAYER(head) && !IS_DEAD(head))
2351 if(head != player && SAME_TEAM(head, player))
2352 if(!head.speedrunning && !head.vehicle)
2354 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in damage.qc)
2355 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2356 vector passer_center = CENTER_OR_VIEWOFS(player);
2358 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2360 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2362 if(IS_BOT_CLIENT(head))
2364 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2365 ctf_Handle_Throw(head, player, DROP_PASS);
2369 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2370 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2372 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2375 else if(player.flagcarried && !head.flagcarried)
2379 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2380 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2381 { closest_target = head; }
2383 else { closest_target = head; }
2390 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2393 // throw the flag in front of you
2394 if(autocvar_g_ctf_throw && player.flagcarried)
2396 if(player.throw_count == -1)
2398 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2400 player.throw_prevtime = time;
2401 player.throw_count = 1;
2402 ctf_Handle_Throw(player, NULL, DROP_THROW);
2407 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2413 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2414 else { player.throw_count += 1; }
2415 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2417 player.throw_prevtime = time;
2418 ctf_Handle_Throw(player, NULL, DROP_THROW);
2425 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2427 entity player = M_ARGV(0, entity);
2429 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2431 player.wps_helpme_time = time;
2432 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2434 else // create a normal help me waypointsprite
2436 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2437 WaypointSprite_Ping(player.wps_helpme);
2443 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2445 entity player = M_ARGV(0, entity);
2446 entity veh = M_ARGV(1, entity);
2448 if(player.flagcarried)
2450 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2452 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2456 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2457 setattachment(player.flagcarried, veh, "");
2458 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2459 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2460 //player.flagcarried.angles = '0 0 0';
2466 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2468 entity player = M_ARGV(0, entity);
2470 if(player.flagcarried)
2472 setattachment(player.flagcarried, player, "");
2473 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2474 player.flagcarried.scale = FLAG_SCALE;
2475 player.flagcarried.angles = '0 0 0';
2476 player.flagcarried.nodrawtoclient = NULL;
2481 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2483 entity player = M_ARGV(0, entity);
2485 if(player.flagcarried)
2487 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2488 ctf_RespawnFlag(player.flagcarried);
2493 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2495 entity flag; // temporary entity for the search method
2497 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2499 switch(flag.ctf_status)
2504 // lock the flag, game is over
2505 set_movetype(flag, MOVETYPE_NONE);
2506 flag.takedamage = DAMAGE_NO;
2507 flag.solid = SOLID_NOT;
2508 flag.nextthink = false; // stop thinking
2510 //dprint("stopping the ", flag.netname, " from moving.\n");
2518 // do nothing for these flags
2525 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2527 entity bot = M_ARGV(0, entity);
2529 havocbot_ctf_reset_role(bot);
2533 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2535 M_ARGV(1, string) = "ctf_team";
2538 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2540 int record_page = M_ARGV(0, int);
2541 string ret_string = M_ARGV(1, string);
2543 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2545 if (MapInfo_Get_ByID(i))
2547 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2553 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2554 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2558 M_ARGV(1, string) = ret_string;
2561 bool superspec_Spectate(entity this, entity targ); // TODO
2562 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2563 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2565 entity player = M_ARGV(0, entity);
2566 string cmd_name = M_ARGV(1, string);
2567 int cmd_argc = M_ARGV(2, int);
2569 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2571 if(cmd_name == "followfc")
2583 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2584 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2585 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2586 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2590 FOREACH_CLIENT(IS_PLAYER(it), {
2591 if(it.flagcarried && (it.team == _team || _team == 0))
2594 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2595 continue; // already spectating this fc, try another
2596 return superspec_Spectate(player, it);
2601 superspec_msg("", "", player, "No active flag carrier\n", 1);
2606 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2608 entity frag_target = M_ARGV(0, entity);
2610 if(frag_target.flagcarried)
2611 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2614 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2616 entity player = M_ARGV(0, entity);
2617 if(player.flagcarried)
2618 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2626 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2627 CTF flag for team one (Red).
2629 "angle" Angle the flag will point (minus 90 degrees)...
2630 "model" model to use, note this needs red and blue as skins 0 and 1...
2631 "noise" sound played when flag is picked up...
2632 "noise1" sound played when flag is returned by a teammate...
2633 "noise2" sound played when flag is captured...
2634 "noise3" sound played when flag is lost in the field and respawns itself...
2635 "noise4" sound played when flag is dropped by a player...
2636 "noise5" sound played when flag touches the ground... */
2637 spawnfunc(item_flag_team1)
2639 if(!g_ctf) { delete(this); return; }
2641 ctf_FlagSetup(NUM_TEAM_1, this);
2644 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2645 CTF flag for team two (Blue).
2647 "angle" Angle the flag will point (minus 90 degrees)...
2648 "model" model to use, note this needs red and blue as skins 0 and 1...
2649 "noise" sound played when flag is picked up...
2650 "noise1" sound played when flag is returned by a teammate...
2651 "noise2" sound played when flag is captured...
2652 "noise3" sound played when flag is lost in the field and respawns itself...
2653 "noise4" sound played when flag is dropped by a player...
2654 "noise5" sound played when flag touches the ground... */
2655 spawnfunc(item_flag_team2)
2657 if(!g_ctf) { delete(this); return; }
2659 ctf_FlagSetup(NUM_TEAM_2, this);
2662 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2663 CTF flag for team three (Yellow).
2665 "angle" Angle the flag will point (minus 90 degrees)...
2666 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2667 "noise" sound played when flag is picked up...
2668 "noise1" sound played when flag is returned by a teammate...
2669 "noise2" sound played when flag is captured...
2670 "noise3" sound played when flag is lost in the field and respawns itself...
2671 "noise4" sound played when flag is dropped by a player...
2672 "noise5" sound played when flag touches the ground... */
2673 spawnfunc(item_flag_team3)
2675 if(!g_ctf) { delete(this); return; }
2677 ctf_FlagSetup(NUM_TEAM_3, this);
2680 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2681 CTF flag for team four (Pink).
2683 "angle" Angle the flag will point (minus 90 degrees)...
2684 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2685 "noise" sound played when flag is picked up...
2686 "noise1" sound played when flag is returned by a teammate...
2687 "noise2" sound played when flag is captured...
2688 "noise3" sound played when flag is lost in the field and respawns itself...
2689 "noise4" sound played when flag is dropped by a player...
2690 "noise5" sound played when flag touches the ground... */
2691 spawnfunc(item_flag_team4)
2693 if(!g_ctf) { delete(this); return; }
2695 ctf_FlagSetup(NUM_TEAM_4, this);
2698 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2701 "angle" Angle the flag will point (minus 90 degrees)...
2702 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2703 "noise" sound played when flag is picked up...
2704 "noise1" sound played when flag is returned by a teammate...
2705 "noise2" sound played when flag is captured...
2706 "noise3" sound played when flag is lost in the field and respawns itself...
2707 "noise4" sound played when flag is dropped by a player...
2708 "noise5" sound played when flag touches the ground... */
2709 spawnfunc(item_flag_neutral)
2711 if(!g_ctf) { delete(this); return; }
2712 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2714 ctf_FlagSetup(0, this);
2717 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2718 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2719 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.
2721 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2722 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2725 if(!g_ctf) { delete(this); return; }
2727 this.team = this.cnt + 1;
2730 // compatibility for quake maps
2731 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2732 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2733 spawnfunc(info_player_team1);
2734 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2735 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2736 spawnfunc(info_player_team2);
2737 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2738 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2740 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2741 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2743 // compatibility for wop maps
2744 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2745 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2746 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2747 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2748 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2749 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2757 void ctf_ScoreRules(int teams)
2759 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2760 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2761 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2762 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2763 field(SP_CTF_PICKUPS, "pickups", 0);
2764 field(SP_CTF_FCKILLS, "fckills", 0);
2765 field(SP_CTF_RETURNS, "returns", 0);
2766 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2770 // code from here on is just to support maps that don't have flag and team entities
2771 void ctf_SpawnTeam (string teamname, int teamcolor)
2773 entity this = new_pure(ctf_team);
2774 this.netname = teamname;
2775 this.cnt = teamcolor - 1;
2776 this.spawnfunc_checked = true;
2777 this.team = teamcolor;
2780 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2785 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2787 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2788 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2790 switch(tmp_entity.team)
2792 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2793 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2794 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2795 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2797 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2800 havocbot_ctf_calculate_middlepoint();
2802 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2804 ctf_teams = 0; // so set the default red and blue teams
2805 BITSET_ASSIGN(ctf_teams, BIT(0));
2806 BITSET_ASSIGN(ctf_teams, BIT(1));
2809 //ctf_teams = bound(2, ctf_teams, 4);
2811 // if no teams are found, spawn defaults
2812 if(find(NULL, classname, "ctf_team") == NULL)
2814 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2815 if(ctf_teams & BIT(0))
2816 ctf_SpawnTeam("Red", NUM_TEAM_1);
2817 if(ctf_teams & BIT(1))
2818 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2819 if(ctf_teams & BIT(2))
2820 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2821 if(ctf_teams & BIT(3))
2822 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2825 ctf_ScoreRules(ctf_teams);
2828 void ctf_Initialize()
2830 CTF_FLAG = NEW(Flag);
2831 record_type = CTF_RECORD;
2832 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2834 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2835 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2836 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2838 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);