3 #include <common/effects/all.qh>
4 #include <common/vehicles/all.qh>
5 #include <server/teamplay.qh>
7 #include <lib/warpzone/common.qh>
9 bool autocvar_g_ctf_allow_vehicle_carry;
10 bool autocvar_g_ctf_allow_vehicle_touch;
11 bool autocvar_g_ctf_allow_monster_touch;
12 bool autocvar_g_ctf_throw;
13 float autocvar_g_ctf_throw_angle_max;
14 float autocvar_g_ctf_throw_angle_min;
15 int autocvar_g_ctf_throw_punish_count;
16 float autocvar_g_ctf_throw_punish_delay;
17 float autocvar_g_ctf_throw_punish_time;
18 float autocvar_g_ctf_throw_strengthmultiplier;
19 float autocvar_g_ctf_throw_velocity_forward;
20 float autocvar_g_ctf_throw_velocity_up;
21 float autocvar_g_ctf_drop_velocity_up;
22 float autocvar_g_ctf_drop_velocity_side;
23 bool autocvar_g_ctf_oneflag_reverse;
24 bool autocvar_g_ctf_portalteleport;
25 bool autocvar_g_ctf_pass;
26 float autocvar_g_ctf_pass_arc;
27 float autocvar_g_ctf_pass_arc_max;
28 float autocvar_g_ctf_pass_directional_max;
29 float autocvar_g_ctf_pass_directional_min;
30 float autocvar_g_ctf_pass_radius;
31 float autocvar_g_ctf_pass_wait;
32 bool autocvar_g_ctf_pass_request;
33 float autocvar_g_ctf_pass_turnrate;
34 float autocvar_g_ctf_pass_timelimit;
35 float autocvar_g_ctf_pass_velocity;
36 bool autocvar_g_ctf_dynamiclights;
37 float autocvar_g_ctf_flag_collect_delay;
38 float autocvar_g_ctf_flag_damageforcescale;
39 bool autocvar_g_ctf_flag_dropped_waypoint;
40 bool autocvar_g_ctf_flag_dropped_floatinwater;
41 bool autocvar_g_ctf_flag_glowtrails;
42 int autocvar_g_ctf_flag_health;
43 bool autocvar_g_ctf_flag_return;
44 bool autocvar_g_ctf_flag_return_carrying;
45 float autocvar_g_ctf_flag_return_carried_radius;
46 float autocvar_g_ctf_flag_return_time;
47 bool autocvar_g_ctf_flag_return_when_unreachable;
48 float autocvar_g_ctf_flag_return_damage;
49 float autocvar_g_ctf_flag_return_damage_delay;
50 float autocvar_g_ctf_flag_return_dropped;
51 bool autocvar_g_ctf_flag_waypoint = true;
52 float autocvar_g_ctf_flag_waypoint_maxdistance;
53 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
54 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
55 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
56 float autocvar_g_ctf_flagcarrier_selfforcefactor;
57 float autocvar_g_ctf_flagcarrier_damagefactor;
58 float autocvar_g_ctf_flagcarrier_forcefactor;
59 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
60 bool autocvar_g_ctf_fullbrightflags;
61 bool autocvar_g_ctf_ignore_frags;
62 bool autocvar_g_ctf_score_ignore_fields;
63 int autocvar_g_ctf_score_capture;
64 int autocvar_g_ctf_score_capture_assist;
65 int autocvar_g_ctf_score_kill;
66 int autocvar_g_ctf_score_penalty_drop;
67 int autocvar_g_ctf_score_penalty_returned;
68 int autocvar_g_ctf_score_pickup_base;
69 int autocvar_g_ctf_score_pickup_dropped_early;
70 int autocvar_g_ctf_score_pickup_dropped_late;
71 int autocvar_g_ctf_score_return;
72 float autocvar_g_ctf_shield_force;
73 float autocvar_g_ctf_shield_max_ratio;
74 int autocvar_g_ctf_shield_min_negscore;
75 bool autocvar_g_ctf_stalemate;
76 int autocvar_g_ctf_stalemate_endcondition;
77 float autocvar_g_ctf_stalemate_time;
78 bool autocvar_g_ctf_reverse;
79 float autocvar_g_ctf_dropped_capture_delay;
80 float autocvar_g_ctf_dropped_capture_radius;
82 void ctf_FakeTimeLimit(entity e, float t)
85 WriteByte(MSG_ONE, 3); // svc_updatestat
86 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
88 WriteCoord(MSG_ONE, autocvar_timelimit);
90 WriteCoord(MSG_ONE, (t + 1) / 60);
93 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
95 if(autocvar_sv_eventlog)
96 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != NULL) ? ftos(actor.playerid) : "")));
97 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
100 void ctf_CaptureRecord(entity flag, entity player)
102 float cap_record = ctf_captimerecord;
103 float cap_time = (time - flag.ctf_pickuptime);
104 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
108 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname);
109 else if(!ctf_captimerecord)
110 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_TIME), player.netname, TIME_ENCODE(cap_time));
111 else if(cap_time < cap_record)
112 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));
114 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));
116 // write that shit in the database
117 if(!ctf_oneflag) // but not in 1-flag mode
118 if((!ctf_captimerecord) || (cap_time < cap_record))
120 ctf_captimerecord = cap_time;
121 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
122 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
123 write_recordmarker(player, flag.ctf_pickuptime, cap_time);
126 if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
127 race_setTime(GetMapname(), TIME_ENCODE(cap_time), player.crypto_idfp, player.netname, player, false);
130 bool ctf_Immediate_Return_Allowed(entity flag, entity toucher)
133 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), { ++num_perteam; });
135 // automatically return if there's only 1 player on the team
136 return ((autocvar_g_ctf_flag_return || num_perteam <= 1 || (autocvar_g_ctf_flag_return_carrying && toucher.flagcarried))
140 bool ctf_Return_Customize(entity this, entity client)
142 // only to the carrier
143 return boolean(client == this.owner);
146 void ctf_FlagcarrierWaypoints(entity player)
148 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
149 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, 2 * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
150 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);
151 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
153 if(player.flagcarried && CTF_SAMETEAM(player, player.flagcarried))
155 if(!player.wps_enemyflagcarrier)
157 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, 0, player, wps_enemyflagcarrier, true, RADARICON_FLAG);
158 wp.colormod = WPCOLOR_ENEMYFC(player.team);
159 setcefc(wp, ctf_Stalemate_Customize);
161 if(IS_REAL_CLIENT(player) && !ctf_stalemate)
162 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_VISIBLE);
165 if(!player.wps_flagreturn)
167 entity owp = WaypointSprite_SpawnFixed(WP_FlagReturn, player.flagcarried.ctf_spawnorigin + FLAG_WAYPOINT_OFFSET, player, wps_flagreturn, RADARICON_FLAG);
168 owp.colormod = '0 0.8 0.8';
169 //WaypointSprite_UpdateTeamRadar(player.wps_flagreturn, RADARICON_FLAG, ((player.team) ? colormapPaletteColor(player.team - 1, false) : '1 1 1'));
170 setcefc(owp, ctf_Return_Customize);
175 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
177 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
178 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
179 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
180 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
183 if(current_height) // make sure we can actually do this arcing path
185 targpos = (to + ('0 0 1' * current_height));
186 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
187 if(trace_fraction < 1)
189 //print("normal arc line failed, trying to find new pos...");
190 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
191 targpos = (trace_endpos + eZ * FLAG_PASS_ARC_OFFSET_Z);
192 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
193 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
194 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
197 else { targpos = to; }
199 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
201 vector desired_direction = normalize(targpos - from);
202 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
203 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
206 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
208 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
210 // directional tracing only
212 makevectors(passer_angle);
214 // find the closest point on the enemy to the center of the attack
215 float h; // hypotenuse, which is the distance between attacker to head
216 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
218 h = vlen(head_center - passer_center);
219 a = h * (normalize(head_center - passer_center) * v_forward);
221 vector nearest_on_line = (passer_center + a * v_forward);
222 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
224 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
225 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
227 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
232 else { return true; }
236 // =======================
237 // CaptureShield Functions
238 // =======================
240 bool ctf_CaptureShield_CheckStatus(entity p)
242 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
243 int players_worseeq, players_total;
245 if(ctf_captureshield_max_ratio <= 0)
248 s = GameRules_scoring_add(p, CTF_CAPS, 0);
249 s2 = GameRules_scoring_add(p, CTF_PICKUPS, 0);
250 s3 = GameRules_scoring_add(p, CTF_RETURNS, 0);
251 s4 = GameRules_scoring_add(p, CTF_FCKILLS, 0);
253 sr = ((s - s2) + (s3 + s4));
255 if(sr >= -ctf_captureshield_min_negscore)
258 players_total = players_worseeq = 0;
259 FOREACH_CLIENT(IS_PLAYER(it), {
262 se = GameRules_scoring_add(it, CTF_CAPS, 0);
263 se2 = GameRules_scoring_add(it, CTF_PICKUPS, 0);
264 se3 = GameRules_scoring_add(it, CTF_RETURNS, 0);
265 se4 = GameRules_scoring_add(it, CTF_FCKILLS, 0);
267 ser = ((se - se2) + (se3 + se4));
274 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
275 // use this rule here
277 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
283 void ctf_CaptureShield_Update(entity player, bool wanted_status)
285 bool updated_status = ctf_CaptureShield_CheckStatus(player);
286 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
288 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
289 player.ctf_captureshielded = updated_status;
293 bool ctf_CaptureShield_Customize(entity this, entity client)
295 if(!client.ctf_captureshielded) { return false; }
296 if(CTF_SAMETEAM(this, client)) { return false; }
301 void ctf_CaptureShield_Touch(entity this, entity toucher)
303 if(!toucher.ctf_captureshielded) { return; }
304 if(CTF_SAMETEAM(this, toucher)) { return; }
306 vector mymid = (this.absmin + this.absmax) * 0.5;
307 vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
309 Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
310 if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
313 void ctf_CaptureShield_Spawn(entity flag)
315 entity shield = new(ctf_captureshield);
318 shield.team = flag.team;
319 settouch(shield, ctf_CaptureShield_Touch);
320 setcefc(shield, ctf_CaptureShield_Customize);
321 shield.effects = EF_ADDITIVE;
322 set_movetype(shield, MOVETYPE_NOCLIP);
323 shield.solid = SOLID_TRIGGER;
324 shield.avelocity = '7 0 11';
327 setorigin(shield, flag.origin);
328 setmodel(shield, MDL_CTF_SHIELD);
329 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
333 // ====================
334 // Drop/Pass/Throw Code
335 // ====================
337 void ctf_Handle_Drop(entity flag, entity player, int droptype)
340 player = (player ? player : flag.pass_sender);
343 set_movetype(flag, MOVETYPE_TOSS);
344 flag.takedamage = DAMAGE_YES;
345 flag.angles = '0 0 0';
346 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
347 flag.ctf_droptime = time;
348 flag.ctf_dropper = player;
349 flag.ctf_status = FLAG_DROPPED;
351 // messages and sounds
352 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
353 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
354 ctf_EventLog("dropped", player.team, player);
357 GameRules_scoring_add_team(player, SCORE, -((flag.score_drop) ? flag.score_drop : autocvar_g_ctf_score_penalty_drop));
358 GameRules_scoring_add(player, CTF_DROPS, 1);
361 if(autocvar_g_ctf_flag_dropped_waypoint) {
362 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);
363 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
366 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
368 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_health);
369 WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH));
372 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
374 if(droptype == DROP_PASS)
376 flag.pass_distance = 0;
377 flag.pass_sender = NULL;
378 flag.pass_target = NULL;
382 void ctf_Handle_Retrieve(entity flag, entity player)
384 entity sender = flag.pass_sender;
386 // transfer flag to player
388 flag.owner.flagcarried = flag;
389 GameRules_scoring_vip(player, true);
394 setattachment(flag, player.vehicle, "");
395 setorigin(flag, VEHICLE_FLAG_OFFSET);
396 flag.scale = VEHICLE_FLAG_SCALE;
400 setattachment(flag, player, "");
401 setorigin(flag, FLAG_CARRY_OFFSET);
403 set_movetype(flag, MOVETYPE_NONE);
404 flag.takedamage = DAMAGE_NO;
405 flag.solid = SOLID_NOT;
406 flag.angles = '0 0 0';
407 flag.ctf_status = FLAG_CARRY;
409 // messages and sounds
410 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
411 ctf_EventLog("receive", flag.team, player);
413 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
415 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
416 else if(it == player)
417 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
418 else if(SAME_TEAM(it, sender))
419 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
422 // create new waypoint
423 ctf_FlagcarrierWaypoints(player);
425 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
426 player.throw_antispam = sender.throw_antispam;
428 flag.pass_distance = 0;
429 flag.pass_sender = NULL;
430 flag.pass_target = NULL;
433 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
435 entity flag = player.flagcarried;
436 vector targ_origin, flag_velocity;
438 if(!flag) { return; }
439 if((droptype == DROP_PASS) && !receiver) { return; }
441 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
444 setattachment(flag, NULL, "");
445 tracebox(player.origin - FLAG_DROP_OFFSET, flag.m_mins, flag.m_maxs, player.origin + FLAG_DROP_OFFSET, MOVE_NOMONSTERS, flag);
446 setorigin(flag, trace_endpos);
447 flag.owner.flagcarried = NULL;
448 GameRules_scoring_vip(flag.owner, false);
450 flag.solid = SOLID_TRIGGER;
451 flag.ctf_dropper = player;
452 flag.ctf_droptime = time;
454 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
461 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
462 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
463 WarpZone_RefSys_Copy(flag, receiver);
464 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
465 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
467 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
468 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
471 set_movetype(flag, MOVETYPE_FLY);
472 flag.takedamage = DAMAGE_NO;
473 flag.pass_sender = player;
474 flag.pass_target = receiver;
475 flag.ctf_status = FLAG_PASSING;
478 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
479 WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
480 ctf_EventLog("pass", flag.team, player);
486 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'));
488 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)));
489 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
490 ctf_Handle_Drop(flag, player, droptype);
491 navigation_dynamicgoal_set(flag, player);
497 flag.velocity = '0 0 0'; // do nothing
504 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);
505 ctf_Handle_Drop(flag, player, droptype);
506 navigation_dynamicgoal_set(flag, player);
511 // kill old waypointsprite
512 WaypointSprite_Ping(player.wps_flagcarrier);
513 WaypointSprite_Kill(player.wps_flagcarrier);
515 if(player.wps_enemyflagcarrier)
516 WaypointSprite_Kill(player.wps_enemyflagcarrier);
518 if(player.wps_flagreturn)
519 WaypointSprite_Kill(player.wps_flagreturn);
522 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
525 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
527 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
534 void nades_GiveBonus(entity player, float score);
536 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
538 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
539 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
540 entity player_team_flag = NULL, tmp_entity;
541 float old_time, new_time;
543 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
544 if(CTF_DIFFTEAM(player, flag)) { return; }
545 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)
547 if (toucher.goalentity == flag.bot_basewaypoint)
548 toucher.goalentity_lock_timeout = 0;
551 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
552 if(SAME_TEAM(tmp_entity, player))
554 player_team_flag = tmp_entity;
558 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
560 player.throw_prevtime = time;
561 player.throw_count = 0;
563 // messages and sounds
564 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
565 ctf_CaptureRecord(enemy_flag, player);
566 _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);
570 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
571 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
577 if(enemy_flag.score_capture || flag.score_capture)
578 pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
579 GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
581 if(enemy_flag.score_team_capture || flag.score_team_capture)
582 capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
583 GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
585 old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
586 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
587 if(!old_time || new_time < old_time)
588 GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
591 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
592 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
595 if(capturetype == CAPTURE_NORMAL)
597 WaypointSprite_Kill(player.wps_flagcarrier);
598 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
600 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
601 { GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist)); }
604 flag.enemy = toucher;
607 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
608 ctf_RespawnFlag(enemy_flag);
611 void ctf_Handle_Return(entity flag, entity player)
613 // messages and sounds
614 if(IS_MONSTER(player))
616 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
620 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
621 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
623 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
624 ctf_EventLog("return", flag.team, player);
627 if(IS_PLAYER(player))
629 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
630 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
632 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
635 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
639 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
640 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
641 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
645 if(player.flagcarried == flag)
646 WaypointSprite_Kill(player.wps_flagcarrier);
651 ctf_RespawnFlag(flag);
654 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
657 float pickup_dropped_score; // used to calculate dropped pickup score
659 // attach the flag to the player
661 player.flagcarried = flag;
662 GameRules_scoring_vip(player, true);
665 setattachment(flag, player.vehicle, "");
666 setorigin(flag, VEHICLE_FLAG_OFFSET);
667 flag.scale = VEHICLE_FLAG_SCALE;
671 setattachment(flag, player, "");
672 setorigin(flag, FLAG_CARRY_OFFSET);
676 set_movetype(flag, MOVETYPE_NONE);
677 flag.takedamage = DAMAGE_NO;
678 flag.solid = SOLID_NOT;
679 flag.angles = '0 0 0';
680 flag.ctf_status = FLAG_CARRY;
684 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
685 case PICKUP_DROPPED: SetResourceExplicit(flag, RES_HEALTH, flag.max_health); break; // reset health/return timelimit
689 // messages and sounds
690 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
692 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
694 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
695 else if(CTF_DIFFTEAM(player, flag))
696 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
698 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
700 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
703 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); });
706 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
707 if(CTF_SAMETEAM(flag, it))
709 if(SAME_TEAM(player, it))
710 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
712 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);
716 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
719 GameRules_scoring_add(player, CTF_PICKUPS, 1);
720 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
725 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
726 ctf_EventLog("steal", flag.team, player);
732 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);
733 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);
734 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
735 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
736 ctf_EventLog("pickup", flag.team, player);
744 if(pickuptype == PICKUP_BASE)
746 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
747 if((player.speedrunning) && (ctf_captimerecord))
748 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
752 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
755 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
756 ctf_FlagcarrierWaypoints(player);
757 WaypointSprite_Ping(player.wps_flagcarrier);
761 // ===================
762 // Main Flag Functions
763 // ===================
765 void ctf_CheckFlagReturn(entity flag, int returntype)
767 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
769 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH)); }
771 if((GetResource(flag, RES_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
776 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
778 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
779 case RETURN_SPEEDRUN:
780 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
781 case RETURN_NEEDKILL:
782 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
785 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
787 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
788 ctf_EventLog("returned", flag.team, NULL);
790 ctf_RespawnFlag(flag);
795 bool ctf_Stalemate_Customize(entity this, entity client)
797 // make spectators see what the player would see
798 entity e = WaypointSprite_getviewentity(client);
799 entity wp_owner = this.owner;
802 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
803 if(SAME_TEAM(wp_owner, e)) { return false; }
804 if(!IS_PLAYER(e)) { return false; }
809 void ctf_CheckStalemate()
812 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
815 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
817 // build list of stale flags
818 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
820 if(autocvar_g_ctf_stalemate)
821 if(tmp_entity.ctf_status != FLAG_BASE)
822 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
824 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
825 ctf_staleflaglist = tmp_entity;
827 switch(tmp_entity.team)
829 case NUM_TEAM_1: ++stale_red_flags; break;
830 case NUM_TEAM_2: ++stale_blue_flags; break;
831 case NUM_TEAM_3: ++stale_yellow_flags; break;
832 case NUM_TEAM_4: ++stale_pink_flags; break;
833 default: ++stale_neutral_flags; break;
839 stale_flags = (stale_neutral_flags >= 1);
841 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
843 if(ctf_oneflag && stale_flags == 1)
844 ctf_stalemate = true;
845 else if(stale_flags >= 2)
846 ctf_stalemate = true;
847 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
848 { ctf_stalemate = false; wpforenemy_announced = false; }
849 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
850 { ctf_stalemate = false; wpforenemy_announced = false; }
852 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
855 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
857 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
859 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);
860 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
861 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
865 if (!wpforenemy_announced)
867 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)); });
869 wpforenemy_announced = true;
874 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
876 if(ITEM_DAMAGE_NEEDKILL(deathtype))
878 if(autocvar_g_ctf_flag_return_damage_delay)
879 this.ctf_flagdamaged_byworld = true;
882 SetResourceExplicit(this, RES_HEALTH, 0);
883 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
887 if(autocvar_g_ctf_flag_return_damage)
889 // reduce health and check if it should be returned
890 TakeResource(this, RES_HEALTH, damage);
891 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
896 void ctf_FlagThink(entity this)
901 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
904 if(this == ctf_worldflaglist) // only for the first flag
905 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
908 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
909 LOG_TRACE("wtf the flag got squashed?");
910 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
911 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
912 setsize(this, this.m_mins, this.m_maxs);
916 switch(this.ctf_status)
920 if(autocvar_g_ctf_dropped_capture_radius)
922 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
923 if(tmp_entity.ctf_status == FLAG_DROPPED)
924 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
925 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
926 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
933 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
935 if(autocvar_g_ctf_flag_dropped_floatinwater)
937 vector midpoint = ((this.absmin + this.absmax) * 0.5);
938 if(pointcontents(midpoint) == CONTENT_WATER)
940 this.velocity = this.velocity * 0.5;
942 if (pointcontents(midpoint + eZ * FLAG_FLOAT_OFFSET_Z) == CONTENT_WATER)
943 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
945 { set_movetype(this, MOVETYPE_FLY); }
947 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
949 if(autocvar_g_ctf_flag_return_dropped)
951 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
953 SetResourceExplicit(this, RES_HEALTH, 0);
954 ctf_CheckFlagReturn(this, RETURN_DROPPED);
958 if(this.ctf_flagdamaged_byworld)
960 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
961 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
964 else if(autocvar_g_ctf_flag_return_time)
966 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
967 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
975 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
977 SetResourceExplicit(this, RES_HEALTH, 0);
978 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
980 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
981 ImpulseCommands(this.owner);
983 if(autocvar_g_ctf_stalemate)
985 if(time >= wpforenemy_nextthink)
987 ctf_CheckStalemate();
988 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
991 if(CTF_SAMETEAM(this, this.owner) && this.team)
993 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
994 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
995 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
996 ctf_Handle_Return(this, this.owner);
1003 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1004 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1005 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1007 if((this.pass_target == NULL)
1008 || (IS_DEAD(this.pass_target))
1009 || (this.pass_target.flagcarried)
1010 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1011 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1012 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1014 // give up, pass failed
1015 ctf_Handle_Drop(this, NULL, DROP_PASS);
1019 // still a viable target, go for it
1020 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1025 default: // this should never happen
1027 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1033 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1036 if(game_stopped) return;
1037 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1039 bool is_not_monster = (!IS_MONSTER(toucher));
1041 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1042 if(ITEM_TOUCH_NEEDKILL())
1044 if(!autocvar_g_ctf_flag_return_damage_delay)
1046 SetResourceExplicit(flag, RES_HEALTH, 0);
1047 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1049 if(!flag.ctf_flagdamaged_byworld) { return; }
1052 // special touch behaviors
1053 if(STAT(FROZEN, toucher)) { return; }
1054 else if(IS_VEHICLE(toucher))
1056 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1057 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1059 return; // do nothing
1061 else if(IS_MONSTER(toucher))
1063 if(!autocvar_g_ctf_allow_monster_touch)
1064 return; // do nothing
1066 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1068 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1070 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1071 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1072 flag.wait = time + FLAG_TOUCHRATE;
1076 else if(IS_DEAD(toucher)) { return; }
1078 switch(flag.ctf_status)
1084 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1085 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1086 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1087 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1089 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1090 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1091 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)
1093 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1094 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1096 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1097 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1103 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1104 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1105 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1106 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1112 LOG_TRACE("Someone touched a flag even though it was being carried?");
1118 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1120 if(DIFF_TEAM(toucher, flag.pass_sender))
1122 if(ctf_Immediate_Return_Allowed(flag, toucher))
1123 ctf_Handle_Return(flag, toucher);
1124 else if(is_not_monster && (!toucher.flagcarried))
1125 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1127 else if(!toucher.flagcarried)
1128 ctf_Handle_Retrieve(flag, toucher);
1135 .float last_respawn;
1136 void ctf_RespawnFlag(entity flag)
1138 // check for flag respawn being called twice in a row
1139 if(flag.last_respawn > time - 0.5)
1140 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1142 flag.last_respawn = time;
1144 // reset the player (if there is one)
1145 if((flag.owner) && (flag.owner.flagcarried == flag))
1147 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1148 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1149 WaypointSprite_Kill(flag.wps_flagcarrier);
1151 flag.owner.flagcarried = NULL;
1152 GameRules_scoring_vip(flag.owner, false);
1154 if(flag.speedrunning)
1155 ctf_FakeTimeLimit(flag.owner, -1);
1158 if((flag.owner) && (flag.owner.vehicle))
1159 flag.scale = FLAG_SCALE;
1161 if(flag.ctf_status == FLAG_DROPPED)
1162 { WaypointSprite_Kill(flag.wps_flagdropped); }
1165 setattachment(flag, NULL, "");
1166 setorigin(flag, flag.ctf_spawnorigin);
1168 //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1169 set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1170 flag.takedamage = DAMAGE_NO;
1171 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1172 flag.solid = SOLID_TRIGGER;
1173 flag.velocity = '0 0 0';
1174 flag.angles = flag.mangle;
1175 flag.flags = FL_ITEM | FL_NOTARGET;
1177 flag.ctf_status = FLAG_BASE;
1179 flag.pass_distance = 0;
1180 flag.pass_sender = NULL;
1181 flag.pass_target = NULL;
1182 flag.ctf_dropper = NULL;
1183 flag.ctf_pickuptime = 0;
1184 flag.ctf_droptime = 0;
1185 flag.ctf_flagdamaged_byworld = false;
1186 navigation_dynamicgoal_unset(flag);
1188 ctf_CheckStalemate();
1191 void ctf_Reset(entity this)
1193 if(this.owner && IS_PLAYER(this.owner))
1194 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1197 ctf_RespawnFlag(this);
1200 bool ctf_FlagBase_Customize(entity this, entity client)
1202 entity e = WaypointSprite_getviewentity(client);
1203 entity wp_owner = this.owner;
1204 entity flag = e.flagcarried;
1205 if(flag && CTF_SAMETEAM(e, flag))
1207 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1212 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1215 waypoint_spawnforitem_force(this, this.origin);
1216 navigation_dynamicgoal_init(this, true);
1222 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1223 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1224 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1225 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1226 default: basename = WP_FlagBaseNeutral; break;
1229 if(autocvar_g_ctf_flag_waypoint)
1231 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1232 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1233 wp.fade_rate = autocvar_g_ctf_flag_waypoint_maxdistance;
1234 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1235 setcefc(wp, ctf_FlagBase_Customize);
1238 // captureshield setup
1239 ctf_CaptureShield_Spawn(this);
1244 void ctf_FlagSetup(int teamnum, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1247 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1248 ctf_worldflaglist = flag;
1250 setattachment(flag, NULL, "");
1252 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnum), Team_ColorName_Upper(teamnum)));
1253 flag.team = teamnum;
1254 flag.classname = "item_flag_team";
1255 flag.target = "###item###"; // for finding the nearest item using findnearest
1256 flag.flags = FL_ITEM | FL_NOTARGET;
1257 IL_PUSH(g_items, flag);
1258 flag.solid = SOLID_TRIGGER;
1259 flag.takedamage = DAMAGE_NO;
1260 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1261 flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1262 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1263 flag.event_damage = ctf_FlagDamage;
1264 flag.pushable = true;
1265 flag.teleportable = TELEPORT_NORMAL;
1266 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1267 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1268 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1269 if(flag.damagedbycontents)
1270 IL_PUSH(g_damagedbycontents, flag);
1271 flag.velocity = '0 0 0';
1272 flag.mangle = flag.angles;
1273 flag.reset = ctf_Reset;
1274 settouch(flag, ctf_FlagTouch);
1275 setthink(flag, ctf_FlagThink);
1276 flag.nextthink = time + FLAG_THINKRATE;
1277 flag.ctf_status = FLAG_BASE;
1279 // crudely force them all to 0
1280 if(autocvar_g_ctf_score_ignore_fields)
1281 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1283 string teamname = Static_Team_ColorName_Lower(teamnum);
1285 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1286 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1287 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1288 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnum).eent_eff_name; }
1289 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnum).eent_eff_name; }
1290 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnum).eent_eff_name; }
1294 if(flag.s == "") flag.s = b; \
1295 precache_sound(flag.s);
1297 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnum))))
1298 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnum))))
1299 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnum))))
1300 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnum))))
1301 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1302 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1303 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1307 precache_model(flag.model);
1310 _setmodel(flag, flag.model); // precision set below
1311 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1312 flag.m_mins = flag.mins; // store these for squash checks
1313 flag.m_maxs = flag.maxs;
1314 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1316 if(autocvar_g_ctf_flag_glowtrails)
1320 case NUM_TEAM_1: flag.glow_color = 251; break;
1321 case NUM_TEAM_2: flag.glow_color = 210; break;
1322 case NUM_TEAM_3: flag.glow_color = 110; break;
1323 case NUM_TEAM_4: flag.glow_color = 145; break;
1324 default: flag.glow_color = 254; break;
1326 flag.glow_size = 25;
1327 flag.glow_trail = 1;
1330 flag.effects |= EF_LOWPRECISION;
1331 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1332 if(autocvar_g_ctf_dynamiclights)
1336 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1337 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1338 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1339 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1340 default: flag.effects |= EF_DIMLIGHT; break;
1345 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1347 flag.dropped_origin = flag.origin;
1348 flag.noalign = true;
1349 set_movetype(flag, MOVETYPE_NONE);
1351 else // drop to floor, automatically find a platform and set that as spawn origin
1353 flag.noalign = false;
1355 set_movetype(flag, MOVETYPE_NONE);
1358 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1366 // NOTE: LEGACY CODE, needs to be re-written!
1368 void havocbot_ctf_calculate_middlepoint()
1372 vector fo = '0 0 0';
1375 f = ctf_worldflaglist;
1380 f = f.ctf_worldflagnext;
1386 havocbot_middlepoint = s / n;
1387 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1389 havocbot_symmetry_axis_m = 0;
1390 havocbot_symmetry_axis_q = 0;
1393 // for symmetrical editing of waypoints
1394 entity f1 = ctf_worldflaglist;
1395 entity f2 = f1.ctf_worldflagnext;
1396 float m = -(f1.origin.y - f2.origin.y) / (max(f1.origin.x - f2.origin.x, FLOAT_EPSILON));
1397 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1398 havocbot_symmetry_axis_m = m;
1399 havocbot_symmetry_axis_q = q;
1401 havocbot_symmetry_origin_order = n;
1405 entity havocbot_ctf_find_flag(entity bot)
1408 f = ctf_worldflaglist;
1411 if (CTF_SAMETEAM(bot, f))
1413 f = f.ctf_worldflagnext;
1418 entity havocbot_ctf_find_enemy_flag(entity bot)
1421 f = ctf_worldflaglist;
1426 if(CTF_DIFFTEAM(bot, f))
1433 else if(!bot.flagcarried)
1437 else if (CTF_DIFFTEAM(bot, f))
1439 f = f.ctf_worldflagnext;
1444 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1451 FOREACH_CLIENT(IS_PLAYER(it), {
1452 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1455 if(vdist(it.origin - org, <, tc_radius))
1464 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1467 head = ctf_worldflaglist;
1470 if (CTF_SAMETEAM(this, head))
1472 head = head.ctf_worldflagnext;
1475 navigation_routerating(this, head, ratingscale, 10000);
1479 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1482 head = ctf_worldflaglist;
1485 if (CTF_SAMETEAM(this, head))
1487 if (this.flagcarried)
1488 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1490 head = head.ctf_worldflagnext; // skip base if it has a different group
1495 head = head.ctf_worldflagnext;
1500 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1503 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1506 head = ctf_worldflaglist;
1511 if(CTF_DIFFTEAM(this, head))
1515 if(this.flagcarried)
1518 else if(!this.flagcarried)
1522 else if(CTF_DIFFTEAM(this, head))
1524 head = head.ctf_worldflagnext;
1528 if (head.ctf_status == FLAG_CARRY)
1530 // adjust rating of our flag carrier depending on his health
1531 head = head.tag_entity;
1532 float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1533 ratingscale += ratingscale * f * 0.1;
1535 navigation_routerating(this, head, ratingscale, 10000);
1539 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1541 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1543 if (!bot_waypoints_for_items)
1545 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1551 head = havocbot_ctf_find_enemy_flag(this);
1556 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1559 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1563 mf = havocbot_ctf_find_flag(this);
1565 if(mf.ctf_status == FLAG_BASE)
1569 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1572 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1575 head = ctf_worldflaglist;
1578 // flag is out in the field
1579 if(head.ctf_status != FLAG_BASE)
1580 if(head.tag_entity==NULL) // dropped
1584 if(vdist(org - head.origin, <, df_radius))
1585 navigation_routerating(this, head, ratingscale, 10000);
1588 navigation_routerating(this, head, ratingscale, 10000);
1591 head = head.ctf_worldflagnext;
1595 void havocbot_ctf_reset_role(entity this)
1597 float cdefense, cmiddle, coffense;
1604 if (this.flagcarried)
1606 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1610 mf = havocbot_ctf_find_flag(this);
1611 ef = havocbot_ctf_find_enemy_flag(this);
1613 // Retrieve stolen flag
1614 if(mf.ctf_status!=FLAG_BASE)
1616 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1620 // If enemy flag is taken go to the middle to intercept pursuers
1621 if(ef.ctf_status!=FLAG_BASE)
1623 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1627 // if there is no one else on the team switch to offense
1629 // don't check if this bot is a player since it isn't true when the bot is added to the server
1630 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1634 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1637 else if (time < CS(this).jointime + 1)
1639 // if bots spawn all at once set good default roles
1642 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1645 else if (count == 2)
1647 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1652 // Evaluate best position to take
1653 // Count mates on middle position
1654 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1656 // Count mates on defense position
1657 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1659 // Count mates on offense position
1660 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1662 if(cdefense<=coffense)
1663 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1664 else if(coffense<=cmiddle)
1665 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1667 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1669 // if bots spawn all at once assign them a more appropriated role after a while
1670 if (time < CS(this).jointime + 1 && count > 2)
1671 this.havocbot_role_timeout = time + 10 + random() * 10;
1674 bool havocbot_ctf_is_basewaypoint(entity item)
1676 if (item.classname != "waypoint")
1679 entity head = ctf_worldflaglist;
1682 if (item == head.bot_basewaypoint)
1684 head = head.ctf_worldflagnext;
1689 void havocbot_role_ctf_carrier(entity this)
1693 havocbot_ctf_reset_role(this);
1697 if (this.flagcarried == NULL)
1699 havocbot_ctf_reset_role(this);
1703 if (navigation_goalrating_timeout(this))
1705 navigation_goalrating_start(this);
1708 entity mf = havocbot_ctf_find_flag(this);
1709 vector base_org = mf.dropped_origin;
1710 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1712 havocbot_goalrating_ctf_enemybase(this, base_rating);
1714 havocbot_goalrating_ctf_ourbase(this, base_rating);
1716 // start collecting items very close to the bot but only inside of own base radius
1717 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1718 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1720 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1722 navigation_goalrating_end(this);
1724 navigation_goalrating_timeout_set(this);
1726 entity goal = this.goalentity;
1727 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1728 this.goalentity_lock_timeout = time + ((this.bot_aimtarg) ? 2 : 3);
1731 this.havocbot_cantfindflag = time + 10;
1732 else if (time > this.havocbot_cantfindflag)
1734 // Can't navigate to my own base, suicide!
1735 // TODO: drop it and wander around
1736 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1742 void havocbot_role_ctf_escort(entity this)
1748 havocbot_ctf_reset_role(this);
1752 if (this.flagcarried)
1754 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1758 // If enemy flag is back on the base switch to previous role
1759 ef = havocbot_ctf_find_enemy_flag(this);
1760 if(ef.ctf_status==FLAG_BASE)
1762 this.havocbot_role = this.havocbot_previous_role;
1763 this.havocbot_role_timeout = 0;
1766 if (ef.ctf_status == FLAG_DROPPED)
1768 navigation_goalrating_timeout_expire(this, 1);
1772 // If the flag carrier reached the base switch to defense
1773 mf = havocbot_ctf_find_flag(this);
1774 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1776 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1780 // Set the role timeout if necessary
1781 if (!this.havocbot_role_timeout)
1783 this.havocbot_role_timeout = time + random() * 30 + 60;
1786 // If nothing happened just switch to previous role
1787 if (time > this.havocbot_role_timeout)
1789 this.havocbot_role = this.havocbot_previous_role;
1790 this.havocbot_role_timeout = 0;
1794 // Chase the flag carrier
1795 if (navigation_goalrating_timeout(this))
1797 navigation_goalrating_start(this);
1800 havocbot_goalrating_ctf_enemyflag(this, 10000);
1801 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1802 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1804 navigation_goalrating_end(this);
1806 navigation_goalrating_timeout_set(this);
1810 void havocbot_role_ctf_offense(entity this)
1817 havocbot_ctf_reset_role(this);
1821 if (this.flagcarried)
1823 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1828 mf = havocbot_ctf_find_flag(this);
1829 ef = havocbot_ctf_find_enemy_flag(this);
1832 if(mf.ctf_status!=FLAG_BASE)
1835 pos = mf.tag_entity.origin;
1839 // Try to get it if closer than the enemy base
1840 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1842 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1847 // Escort flag carrier
1848 if(ef.ctf_status!=FLAG_BASE)
1851 pos = ef.tag_entity.origin;
1855 if(vdist(pos - mf.dropped_origin, >, 700))
1857 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1862 // Set the role timeout if necessary
1863 if (!this.havocbot_role_timeout)
1864 this.havocbot_role_timeout = time + 120;
1866 if (time > this.havocbot_role_timeout)
1868 havocbot_ctf_reset_role(this);
1872 if (navigation_goalrating_timeout(this))
1874 navigation_goalrating_start(this);
1877 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1878 havocbot_goalrating_ctf_enemybase(this, 10000);
1879 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1881 navigation_goalrating_end(this);
1883 navigation_goalrating_timeout_set(this);
1887 // Retriever (temporary role):
1888 void havocbot_role_ctf_retriever(entity this)
1894 havocbot_ctf_reset_role(this);
1898 if (this.flagcarried)
1900 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1904 // If flag is back on the base switch to previous role
1905 mf = havocbot_ctf_find_flag(this);
1906 if(mf.ctf_status==FLAG_BASE)
1908 if (mf.enemy == this) // did this bot return the flag?
1909 navigation_goalrating_timeout_force(this);
1910 havocbot_ctf_reset_role(this);
1914 if (!this.havocbot_role_timeout)
1915 this.havocbot_role_timeout = time + 20;
1917 if (time > this.havocbot_role_timeout)
1919 havocbot_ctf_reset_role(this);
1923 if (navigation_goalrating_timeout(this))
1925 const float RT_RADIUS = 10000;
1927 navigation_goalrating_start(this);
1930 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1931 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1932 havocbot_goalrating_ctf_enemybase(this, 8000);
1933 entity ef = havocbot_ctf_find_enemy_flag(this);
1934 vector enemy_base_org = ef.dropped_origin;
1935 // start collecting items very close to the bot but only inside of enemy base radius
1936 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1937 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1938 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1940 navigation_goalrating_end(this);
1942 navigation_goalrating_timeout_set(this);
1946 void havocbot_role_ctf_middle(entity this)
1952 havocbot_ctf_reset_role(this);
1956 if (this.flagcarried)
1958 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1962 mf = havocbot_ctf_find_flag(this);
1963 if(mf.ctf_status!=FLAG_BASE)
1965 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1969 if (!this.havocbot_role_timeout)
1970 this.havocbot_role_timeout = time + 10;
1972 if (time > this.havocbot_role_timeout)
1974 havocbot_ctf_reset_role(this);
1978 if (navigation_goalrating_timeout(this))
1982 org = havocbot_middlepoint;
1983 org.z = this.origin.z;
1985 navigation_goalrating_start(this);
1988 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
1989 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
1990 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
1991 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
1992 havocbot_goalrating_items(this, 18000, this.origin, 10000);
1993 havocbot_goalrating_ctf_enemybase(this, 3000);
1995 navigation_goalrating_end(this);
1997 entity goal = this.goalentity;
1998 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1999 this.goalentity_lock_timeout = time + 2;
2001 navigation_goalrating_timeout_set(this);
2005 void havocbot_role_ctf_defense(entity this)
2011 havocbot_ctf_reset_role(this);
2015 if (this.flagcarried)
2017 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2021 // If own flag was captured
2022 mf = havocbot_ctf_find_flag(this);
2023 if(mf.ctf_status!=FLAG_BASE)
2025 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2029 if (!this.havocbot_role_timeout)
2030 this.havocbot_role_timeout = time + 30;
2032 if (time > this.havocbot_role_timeout)
2034 havocbot_ctf_reset_role(this);
2037 if (navigation_goalrating_timeout(this))
2039 vector org = mf.dropped_origin;
2041 navigation_goalrating_start(this);
2043 // if enemies are closer to our base, go there
2044 entity closestplayer = NULL;
2045 float distance, bestdistance = 10000;
2046 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2047 distance = vlen(org - it.origin);
2048 if(distance<bestdistance)
2051 bestdistance = distance;
2057 if(DIFF_TEAM(closestplayer, this))
2058 if(vdist(org - this.origin, >, 1000))
2059 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2060 havocbot_goalrating_ctf_ourbase(this, 10000);
2062 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2063 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2064 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2065 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2066 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2068 navigation_goalrating_end(this);
2070 navigation_goalrating_timeout_set(this);
2074 void havocbot_role_ctf_setrole(entity bot, int role)
2076 string s = "(null)";
2079 case HAVOCBOT_CTF_ROLE_CARRIER:
2081 bot.havocbot_role = havocbot_role_ctf_carrier;
2082 bot.havocbot_role_timeout = 0;
2083 bot.havocbot_cantfindflag = time + 10;
2084 if (bot.havocbot_previous_role != bot.havocbot_role)
2085 navigation_goalrating_timeout_force(bot);
2087 case HAVOCBOT_CTF_ROLE_DEFENSE:
2089 bot.havocbot_role = havocbot_role_ctf_defense;
2090 bot.havocbot_role_timeout = 0;
2092 case HAVOCBOT_CTF_ROLE_MIDDLE:
2094 bot.havocbot_role = havocbot_role_ctf_middle;
2095 bot.havocbot_role_timeout = 0;
2097 case HAVOCBOT_CTF_ROLE_OFFENSE:
2099 bot.havocbot_role = havocbot_role_ctf_offense;
2100 bot.havocbot_role_timeout = 0;
2102 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2104 bot.havocbot_previous_role = bot.havocbot_role;
2105 bot.havocbot_role = havocbot_role_ctf_retriever;
2106 bot.havocbot_role_timeout = time + 10;
2107 if (bot.havocbot_previous_role != bot.havocbot_role)
2108 navigation_goalrating_timeout_expire(bot, 2);
2110 case HAVOCBOT_CTF_ROLE_ESCORT:
2112 bot.havocbot_previous_role = bot.havocbot_role;
2113 bot.havocbot_role = havocbot_role_ctf_escort;
2114 bot.havocbot_role_timeout = time + 30;
2115 if (bot.havocbot_previous_role != bot.havocbot_role)
2116 navigation_goalrating_timeout_expire(bot, 2);
2119 LOG_TRACE(bot.netname, " switched to ", s);
2127 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2129 entity player = M_ARGV(0, entity);
2131 int t = 0, t2 = 0, t3 = 0;
2132 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)
2134 // initially clear items so they can be set as necessary later.
2135 STAT(CTF_FLAGSTATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2136 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2137 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2138 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2139 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2140 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2142 // scan through all the flags and notify the client about them
2143 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2145 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2146 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2147 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2148 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2149 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; }
2151 switch(flag.ctf_status)
2156 if((flag.owner == player) || (flag.pass_sender == player))
2157 STAT(CTF_FLAGSTATUS, player) |= t; // carrying: player is currently carrying the flag
2159 STAT(CTF_FLAGSTATUS, player) |= t2; // taken: someone else is carrying the flag
2164 STAT(CTF_FLAGSTATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2170 // item for stopping players from capturing the flag too often
2171 if(player.ctf_captureshielded)
2172 STAT(CTF_FLAGSTATUS, player) |= CTF_SHIELDED;
2175 STAT(CTF_FLAGSTATUS, player) |= CTF_STALEMATE;
2177 // update the health of the flag carrier waypointsprite
2178 if(player.wps_flagcarrier)
2179 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);
2182 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2184 entity frag_attacker = M_ARGV(1, entity);
2185 entity frag_target = M_ARGV(2, entity);
2186 float frag_damage = M_ARGV(4, float);
2187 vector frag_force = M_ARGV(6, vector);
2189 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2191 if(frag_target == frag_attacker) // damage done to yourself
2193 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2194 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2196 else // damage done to everyone else
2198 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2199 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2202 M_ARGV(4, float) = frag_damage;
2203 M_ARGV(6, vector) = frag_force;
2205 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2207 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
2208 && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2210 frag_target.wps_helpme_time = time;
2211 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2213 // todo: add notification for when flag carrier needs help?
2217 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2219 entity frag_attacker = M_ARGV(1, entity);
2220 entity frag_target = M_ARGV(2, entity);
2222 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2224 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2225 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2228 if(frag_target.flagcarried)
2230 entity tmp_entity = frag_target.flagcarried;
2231 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2232 tmp_entity.ctf_dropper = NULL;
2236 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2238 M_ARGV(2, float) = 0; // frag score
2239 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2242 void ctf_RemovePlayer(entity player)
2244 if(player.flagcarried)
2245 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2247 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2249 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2250 if(flag.pass_target == player) { flag.pass_target = NULL; }
2251 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2255 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2257 entity player = M_ARGV(0, entity);
2259 ctf_RemovePlayer(player);
2262 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2264 entity player = M_ARGV(0, entity);
2266 ctf_RemovePlayer(player);
2269 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2271 if(!autocvar_g_ctf_leaderboard)
2274 entity player = M_ARGV(0, entity);
2276 if(IS_REAL_CLIENT(player))
2278 int m = min(RANKINGS_CNT, autocvar_g_cts_send_rankings_cnt);
2279 race_send_rankings_cnt(MSG_ONE);
2280 for (int i = 1; i <= m; ++i)
2282 race_SendRankings(i, 0, 0, MSG_ONE);
2287 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2289 if(!autocvar_g_ctf_leaderboard)
2292 entity player = M_ARGV(0, entity);
2294 if(CS(player).cvar_cl_allow_uidtracking == 1 && CS(player).cvar_cl_allow_uid2name == 1)
2296 if (!player.stored_netname)
2297 player.stored_netname = strzone(uid2name(player.crypto_idfp));
2298 if(player.stored_netname != player.netname)
2300 db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
2301 strcpy(player.stored_netname, player.netname);
2306 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2308 entity player = M_ARGV(0, entity);
2310 if(player.flagcarried)
2311 if(!autocvar_g_ctf_portalteleport)
2312 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2315 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2317 if(MUTATOR_RETURNVALUE || game_stopped) return;
2319 entity player = M_ARGV(0, entity);
2321 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2323 // pass the flag to a team mate
2324 if(autocvar_g_ctf_pass)
2326 entity head, closest_target = NULL;
2327 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2329 while(head) // find the closest acceptable target to pass to
2331 if(IS_PLAYER(head) && !IS_DEAD(head))
2332 if(head != player && SAME_TEAM(head, player))
2333 if(!head.speedrunning && !head.vehicle)
2335 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2336 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2337 vector passer_center = CENTER_OR_VIEWOFS(player);
2339 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2341 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2343 if(IS_BOT_CLIENT(head))
2345 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2346 ctf_Handle_Throw(head, player, DROP_PASS);
2350 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2351 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2353 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2356 else if(player.flagcarried && !head.flagcarried)
2360 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2361 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2362 { closest_target = head; }
2364 else { closest_target = head; }
2371 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2374 // throw the flag in front of you
2375 if(autocvar_g_ctf_throw && player.flagcarried)
2377 if(player.throw_count == -1)
2379 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2381 player.throw_prevtime = time;
2382 player.throw_count = 1;
2383 ctf_Handle_Throw(player, NULL, DROP_THROW);
2388 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2394 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2395 else { player.throw_count += 1; }
2396 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2398 player.throw_prevtime = time;
2399 ctf_Handle_Throw(player, NULL, DROP_THROW);
2406 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2408 entity player = M_ARGV(0, entity);
2410 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2412 player.wps_helpme_time = time;
2413 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2415 else // create a normal help me waypointsprite
2417 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2418 WaypointSprite_Ping(player.wps_helpme);
2424 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2426 entity player = M_ARGV(0, entity);
2427 entity veh = M_ARGV(1, entity);
2429 if(player.flagcarried)
2431 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2433 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2437 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2438 setattachment(player.flagcarried, veh, "");
2439 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2440 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2441 //player.flagcarried.angles = '0 0 0';
2447 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2449 entity player = M_ARGV(0, entity);
2451 if(player.flagcarried)
2453 setattachment(player.flagcarried, player, "");
2454 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2455 player.flagcarried.scale = FLAG_SCALE;
2456 player.flagcarried.angles = '0 0 0';
2457 player.flagcarried.nodrawtoclient = NULL;
2462 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2464 entity player = M_ARGV(0, entity);
2466 if(player.flagcarried)
2468 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2469 ctf_RespawnFlag(player.flagcarried);
2474 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2476 entity flag; // temporary entity for the search method
2478 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2480 switch(flag.ctf_status)
2485 // lock the flag, game is over
2486 set_movetype(flag, MOVETYPE_NONE);
2487 flag.takedamage = DAMAGE_NO;
2488 flag.solid = SOLID_NOT;
2489 flag.nextthink = false; // stop thinking
2491 //dprint("stopping the ", flag.netname, " from moving.\n");
2499 // do nothing for these flags
2506 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2508 entity bot = M_ARGV(0, entity);
2510 havocbot_ctf_reset_role(bot);
2514 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2516 M_ARGV(1, string) = "ctf_team";
2519 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2521 entity spectatee = M_ARGV(0, entity);
2522 entity client = M_ARGV(1, entity);
2524 STAT(CTF_FLAGSTATUS, client) = STAT(CTF_FLAGSTATUS, spectatee);
2527 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2529 int record_page = M_ARGV(0, int);
2530 string ret_string = M_ARGV(1, string);
2532 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2534 if (MapInfo_Get_ByID(i))
2536 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2542 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2543 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2547 M_ARGV(1, string) = ret_string;
2550 bool superspec_Spectate(entity this, entity targ); // TODO
2551 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2552 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2554 entity player = M_ARGV(0, entity);
2555 string cmd_name = M_ARGV(1, string);
2556 int cmd_argc = M_ARGV(2, int);
2558 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2560 if(cmd_name == "followfc")
2572 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2573 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2574 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2575 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2579 FOREACH_CLIENT(IS_PLAYER(it), {
2580 if(it.flagcarried && (it.team == _team || _team == 0))
2583 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2584 continue; // already spectating this fc, try another
2585 return superspec_Spectate(player, it);
2590 superspec_msg("", "", player, "No active flag carrier\n", 1);
2595 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2597 entity frag_target = M_ARGV(0, entity);
2599 if(frag_target.flagcarried)
2600 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2603 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2605 entity player = M_ARGV(0, entity);
2606 if(player.flagcarried)
2607 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2615 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2616 CTF flag for team one (Red).
2618 "angle" Angle the flag will point (minus 90 degrees)...
2619 "model" model to use, note this needs red and blue as skins 0 and 1...
2620 "noise" sound played when flag is picked up...
2621 "noise1" sound played when flag is returned by a teammate...
2622 "noise2" sound played when flag is captured...
2623 "noise3" sound played when flag is lost in the field and respawns itself...
2624 "noise4" sound played when flag is dropped by a player...
2625 "noise5" sound played when flag touches the ground... */
2626 spawnfunc(item_flag_team1)
2628 if(!g_ctf) { delete(this); return; }
2630 ctf_FlagSetup(NUM_TEAM_1, this);
2633 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2634 CTF flag for team two (Blue).
2636 "angle" Angle the flag will point (minus 90 degrees)...
2637 "model" model to use, note this needs red and blue as skins 0 and 1...
2638 "noise" sound played when flag is picked up...
2639 "noise1" sound played when flag is returned by a teammate...
2640 "noise2" sound played when flag is captured...
2641 "noise3" sound played when flag is lost in the field and respawns itself...
2642 "noise4" sound played when flag is dropped by a player...
2643 "noise5" sound played when flag touches the ground... */
2644 spawnfunc(item_flag_team2)
2646 if(!g_ctf) { delete(this); return; }
2648 ctf_FlagSetup(NUM_TEAM_2, this);
2651 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2652 CTF flag for team three (Yellow).
2654 "angle" Angle the flag will point (minus 90 degrees)...
2655 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2656 "noise" sound played when flag is picked up...
2657 "noise1" sound played when flag is returned by a teammate...
2658 "noise2" sound played when flag is captured...
2659 "noise3" sound played when flag is lost in the field and respawns itself...
2660 "noise4" sound played when flag is dropped by a player...
2661 "noise5" sound played when flag touches the ground... */
2662 spawnfunc(item_flag_team3)
2664 if(!g_ctf) { delete(this); return; }
2666 ctf_FlagSetup(NUM_TEAM_3, this);
2669 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2670 CTF flag for team four (Pink).
2672 "angle" Angle the flag will point (minus 90 degrees)...
2673 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2674 "noise" sound played when flag is picked up...
2675 "noise1" sound played when flag is returned by a teammate...
2676 "noise2" sound played when flag is captured...
2677 "noise3" sound played when flag is lost in the field and respawns itself...
2678 "noise4" sound played when flag is dropped by a player...
2679 "noise5" sound played when flag touches the ground... */
2680 spawnfunc(item_flag_team4)
2682 if(!g_ctf) { delete(this); return; }
2684 ctf_FlagSetup(NUM_TEAM_4, this);
2687 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2690 "angle" Angle the flag will point (minus 90 degrees)...
2691 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2692 "noise" sound played when flag is picked up...
2693 "noise1" sound played when flag is returned by a teammate...
2694 "noise2" sound played when flag is captured...
2695 "noise3" sound played when flag is lost in the field and respawns itself...
2696 "noise4" sound played when flag is dropped by a player...
2697 "noise5" sound played when flag touches the ground... */
2698 spawnfunc(item_flag_neutral)
2700 if(!g_ctf) { delete(this); return; }
2701 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2703 ctf_FlagSetup(0, this);
2706 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2707 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2708 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.
2710 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2711 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2714 if(!g_ctf) { delete(this); return; }
2716 this.classname = "ctf_team";
2717 this.team = this.cnt + 1;
2720 // compatibility for quake maps
2721 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2722 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2723 spawnfunc(info_player_team1);
2724 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2725 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2726 spawnfunc(info_player_team2);
2727 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2728 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2730 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2731 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2733 // compatibility for wop maps
2734 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2735 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2736 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2737 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2738 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2739 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2747 void ctf_ScoreRules(int teams)
2749 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2750 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2751 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2752 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2753 field(SP_CTF_PICKUPS, "pickups", 0);
2754 field(SP_CTF_FCKILLS, "fckills", 0);
2755 field(SP_CTF_RETURNS, "returns", 0);
2756 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2760 // code from here on is just to support maps that don't have flag and team entities
2761 void ctf_SpawnTeam (string teamname, int teamcolor)
2763 entity this = new_pure(ctf_team);
2764 this.netname = teamname;
2765 this.cnt = teamcolor - 1;
2766 this.spawnfunc_checked = true;
2767 this.team = teamcolor;
2770 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2775 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2777 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2778 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2780 switch(tmp_entity.team)
2782 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2783 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2784 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2785 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2787 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2790 havocbot_ctf_calculate_middlepoint();
2792 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2794 ctf_teams = 0; // so set the default red and blue teams
2795 BITSET_ASSIGN(ctf_teams, BIT(0));
2796 BITSET_ASSIGN(ctf_teams, BIT(1));
2799 //ctf_teams = bound(2, ctf_teams, 4);
2801 // if no teams are found, spawn defaults
2802 if(find(NULL, classname, "ctf_team") == NULL)
2804 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2805 if(ctf_teams & BIT(0))
2806 ctf_SpawnTeam("Red", NUM_TEAM_1);
2807 if(ctf_teams & BIT(1))
2808 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2809 if(ctf_teams & BIT(2))
2810 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2811 if(ctf_teams & BIT(3))
2812 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2815 ctf_ScoreRules(ctf_teams);
2818 void ctf_Initialize()
2820 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2822 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2823 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2824 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2826 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);