3 #include <common/effects/all.qh>
4 #include <common/mapobjects/teleporters.qh>
5 #include <common/mapobjects/triggers.qh>
6 #include <common/mutators/mutator/powerups/_mod.qh>
7 #include <common/vehicles/all.qh>
8 #include <server/command/vote.qh>
9 #include <server/client.qh>
10 #include <server/gamelog.qh>
11 #include <server/intermission.qh>
12 #include <server/damage.qh>
13 #include <server/world.qh>
14 #include <server/items/items.qh>
15 #include <server/race.qh>
16 #include <server/teamplay.qh>
18 #include <lib/warpzone/common.qh>
20 bool autocvar_g_ctf_allow_vehicle_carry;
21 bool autocvar_g_ctf_allow_vehicle_touch;
22 bool autocvar_g_ctf_allow_monster_touch;
23 bool autocvar_g_ctf_throw;
24 float autocvar_g_ctf_throw_angle_max;
25 float autocvar_g_ctf_throw_angle_min;
26 int autocvar_g_ctf_throw_punish_count;
27 float autocvar_g_ctf_throw_punish_delay;
28 float autocvar_g_ctf_throw_punish_time;
29 float autocvar_g_ctf_throw_strengthmultiplier;
30 float autocvar_g_ctf_throw_velocity_forward;
31 float autocvar_g_ctf_throw_velocity_up;
32 float autocvar_g_ctf_drop_velocity_up;
33 float autocvar_g_ctf_drop_velocity_side;
34 bool autocvar_g_ctf_oneflag_reverse;
35 bool autocvar_g_ctf_portalteleport;
36 bool autocvar_g_ctf_pass;
37 float autocvar_g_ctf_pass_arc;
38 float autocvar_g_ctf_pass_arc_max;
39 float autocvar_g_ctf_pass_directional_max;
40 float autocvar_g_ctf_pass_directional_min;
41 float autocvar_g_ctf_pass_radius;
42 float autocvar_g_ctf_pass_wait;
43 bool autocvar_g_ctf_pass_request;
44 float autocvar_g_ctf_pass_turnrate;
45 float autocvar_g_ctf_pass_timelimit;
46 float autocvar_g_ctf_pass_velocity;
47 bool autocvar_g_ctf_dynamiclights;
48 float autocvar_g_ctf_flag_collect_delay;
49 float autocvar_g_ctf_flag_damageforcescale;
50 bool autocvar_g_ctf_flag_dropped_waypoint;
51 bool autocvar_g_ctf_flag_dropped_floatinwater;
52 bool autocvar_g_ctf_flag_glowtrails;
53 int autocvar_g_ctf_flag_health;
54 bool autocvar_g_ctf_flag_return;
55 bool autocvar_g_ctf_flag_return_carrying;
56 float autocvar_g_ctf_flag_return_carried_radius;
57 float autocvar_g_ctf_flag_return_time;
58 bool autocvar_g_ctf_flag_return_when_unreachable;
59 float autocvar_g_ctf_flag_return_damage;
60 float autocvar_g_ctf_flag_return_damage_delay;
61 float autocvar_g_ctf_flag_return_dropped;
62 bool autocvar_g_ctf_flag_waypoint = true;
63 float autocvar_g_ctf_flag_waypoint_maxdistance;
64 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
65 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
66 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
67 float autocvar_g_ctf_flagcarrier_selfforcefactor;
68 float autocvar_g_ctf_flagcarrier_damagefactor;
69 float autocvar_g_ctf_flagcarrier_forcefactor;
70 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
71 bool autocvar_g_ctf_fullbrightflags;
72 bool autocvar_g_ctf_ignore_frags;
73 bool autocvar_g_ctf_score_ignore_fields;
74 int autocvar_g_ctf_score_capture;
75 int autocvar_g_ctf_score_capture_assist;
76 int autocvar_g_ctf_score_kill;
77 int autocvar_g_ctf_score_penalty_drop;
78 int autocvar_g_ctf_score_penalty_returned;
79 int autocvar_g_ctf_score_pickup_base;
80 int autocvar_g_ctf_score_pickup_dropped_early;
81 int autocvar_g_ctf_score_pickup_dropped_late;
82 int autocvar_g_ctf_score_return;
83 float autocvar_g_ctf_shield_force;
84 float autocvar_g_ctf_shield_max_ratio;
85 int autocvar_g_ctf_shield_min_negscore;
86 bool autocvar_g_ctf_stalemate;
87 int autocvar_g_ctf_stalemate_endcondition;
88 float autocvar_g_ctf_stalemate_time;
89 bool autocvar_g_ctf_reverse;
90 float autocvar_g_ctf_dropped_capture_delay;
91 float autocvar_g_ctf_dropped_capture_radius;
93 void ctf_FakeTimeLimit(entity e, float t)
96 WriteByte(MSG_ONE, 3); // svc_updatestat
97 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
99 WriteCoord(MSG_ONE, autocvar_timelimit);
101 WriteCoord(MSG_ONE, (t + 1) / 60);
104 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
106 if(autocvar_sv_eventlog)
107 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != NULL) ? ftos(actor.playerid) : "")));
108 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
111 void ctf_CaptureRecord(entity flag, entity player)
113 float cap_record = ctf_captimerecord;
114 float cap_time = (time - flag.ctf_pickuptime);
115 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
119 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname);
120 else if(!ctf_captimerecord)
121 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_TIME), player.netname, TIME_ENCODE(cap_time));
122 else if(cap_time < cap_record)
123 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_BROKEN), player.netname, refername, TIME_ENCODE(cap_time), TIME_ENCODE(cap_record));
125 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_UNBROKEN), player.netname, refername, TIME_ENCODE(cap_time), TIME_ENCODE(cap_record));
127 // write that shit in the database
128 if(!ctf_oneflag) // but not in 1-flag mode
129 if((!ctf_captimerecord) || (cap_time < cap_record))
131 ctf_captimerecord = cap_time;
132 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
133 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
134 write_recordmarker(player, flag.ctf_pickuptime, cap_time);
137 if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
138 race_setTime(GetMapname(), TIME_ENCODE(cap_time), player.crypto_idfp, player.netname, player, false);
141 bool ctf_Immediate_Return_Allowed(entity flag, entity toucher)
144 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), { ++num_perteam; });
146 // automatically return if there's only 1 player on the team
147 return ((autocvar_g_ctf_flag_return || num_perteam <= 1 || (autocvar_g_ctf_flag_return_carrying && toucher.flagcarried))
151 bool ctf_Return_Customize(entity this, entity client)
153 // only to the carrier
154 return boolean(client == this.owner);
157 void ctf_FlagcarrierWaypoints(entity player)
159 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
160 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, 2 * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
161 WaypointSprite_UpdateHealth(player.wps_flagcarrier, healtharmor_maxdamage(GetResource(player, RES_HEALTH), GetResource(player, RES_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
162 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
164 if(player.flagcarried && CTF_SAMETEAM(player, player.flagcarried))
166 if(!player.wps_enemyflagcarrier)
168 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, 0, player, wps_enemyflagcarrier, true, RADARICON_FLAG);
169 wp.colormod = WPCOLOR_ENEMYFC(player.team);
170 setcefc(wp, ctf_Stalemate_Customize);
172 if(IS_REAL_CLIENT(player) && !ctf_stalemate)
173 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_VISIBLE);
176 if(!player.wps_flagreturn)
178 entity owp = WaypointSprite_SpawnFixed(WP_FlagReturn, player.flagcarried.ctf_spawnorigin + FLAG_WAYPOINT_OFFSET, player, wps_flagreturn, RADARICON_FLAG);
179 owp.colormod = '0 0.8 0.8';
180 //WaypointSprite_UpdateTeamRadar(player.wps_flagreturn, RADARICON_FLAG, ((player.team) ? colormapPaletteColor(player.team - 1, false) : '1 1 1'));
181 setcefc(owp, ctf_Return_Customize);
186 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
188 float current_distance = vlen((('1 0 0' * to.x) + ('0 1 0' * to.y)) - (('1 0 0' * from.x) + ('0 1 0' * from.y))); // for the sake of this check, exclude Z axis
189 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
190 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
191 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
194 if(current_height) // make sure we can actually do this arcing path
196 targpos = (to + ('0 0 1' * current_height));
197 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
198 if(trace_fraction < 1)
200 //print("normal arc line failed, trying to find new pos...");
201 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
202 targpos = (trace_endpos + eZ * FLAG_PASS_ARC_OFFSET_Z);
203 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
204 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
205 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
208 else { targpos = to; }
210 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
212 vector desired_direction = normalize(targpos - from);
213 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
214 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
217 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
219 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
221 // directional tracing only
223 makevectors(passer_angle);
225 // find the closest point on the enemy to the center of the attack
226 float h; // hypotenuse, which is the distance between attacker to head
227 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
229 h = vlen(head_center - passer_center);
230 a = h * (normalize(head_center - passer_center) * v_forward);
232 vector nearest_on_line = (passer_center + a * v_forward);
233 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
235 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
236 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
238 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
243 else { return true; }
247 // =======================
248 // CaptureShield Functions
249 // =======================
251 bool ctf_CaptureShield_CheckStatus(entity p)
253 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
254 int players_worseeq, players_total;
256 if(ctf_captureshield_max_ratio <= 0)
259 s = GameRules_scoring_add(p, CTF_CAPS, 0);
260 s2 = GameRules_scoring_add(p, CTF_PICKUPS, 0);
261 s3 = GameRules_scoring_add(p, CTF_RETURNS, 0);
262 s4 = GameRules_scoring_add(p, CTF_FCKILLS, 0);
264 sr = ((s - s2) + (s3 + s4));
266 if(sr >= -ctf_captureshield_min_negscore)
269 players_total = players_worseeq = 0;
270 FOREACH_CLIENT(IS_PLAYER(it), {
273 se = GameRules_scoring_add(it, CTF_CAPS, 0);
274 se2 = GameRules_scoring_add(it, CTF_PICKUPS, 0);
275 se3 = GameRules_scoring_add(it, CTF_RETURNS, 0);
276 se4 = GameRules_scoring_add(it, CTF_FCKILLS, 0);
278 ser = ((se - se2) + (se3 + se4));
285 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
286 // use this rule here
288 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
294 void ctf_CaptureShield_Update(entity player, bool wanted_status)
296 bool updated_status = ctf_CaptureShield_CheckStatus(player);
297 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
299 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
300 player.ctf_captureshielded = updated_status;
304 bool ctf_CaptureShield_Customize(entity this, entity client)
306 if(!client.ctf_captureshielded) { return false; }
307 if(CTF_SAMETEAM(this, client)) { return false; }
312 void ctf_CaptureShield_Touch(entity this, entity toucher)
314 if(!toucher.ctf_captureshielded) { return; }
315 if(CTF_SAMETEAM(this, toucher)) { return; }
317 vector mymid = (this.absmin + this.absmax) * 0.5;
318 vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
320 Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
321 if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
324 void ctf_CaptureShield_Spawn(entity flag)
326 entity shield = new(ctf_captureshield);
329 shield.team = flag.team;
330 settouch(shield, ctf_CaptureShield_Touch);
331 setcefc(shield, ctf_CaptureShield_Customize);
332 shield.effects = EF_ADDITIVE;
333 set_movetype(shield, MOVETYPE_NOCLIP);
334 shield.solid = SOLID_TRIGGER;
335 shield.avelocity = '7 0 11';
338 setorigin(shield, flag.origin);
339 setmodel(shield, MDL_CTF_SHIELD);
340 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
344 // ====================
345 // Drop/Pass/Throw Code
346 // ====================
348 void ctf_Handle_Drop(entity flag, entity player, int droptype)
351 player = (player ? player : flag.pass_sender);
354 set_movetype(flag, MOVETYPE_TOSS);
355 flag.takedamage = DAMAGE_YES;
356 flag.angles = '0 0 0';
357 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
358 flag.ctf_droptime = time;
359 flag.ctf_landtime = 0;
360 flag.ctf_dropper = player;
361 flag.ctf_status = FLAG_DROPPED;
363 // messages and sounds
364 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
365 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
366 ctf_EventLog("dropped", player.team, player);
369 GameRules_scoring_add_team(player, SCORE, -((flag.score_drop) ? flag.score_drop : autocvar_g_ctf_score_penalty_drop));
370 GameRules_scoring_add(player, CTF_DROPS, 1);
373 if(autocvar_g_ctf_flag_dropped_waypoint) {
374 entity wp = WaypointSprite_Spawn(WP_FlagDropped, 0, 0, flag, FLAG_WAYPOINT_OFFSET, NULL, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, true, RADARICON_FLAG);
375 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
378 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
380 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_health);
381 WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH));
384 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
386 if(droptype == DROP_PASS)
388 flag.pass_distance = 0;
389 flag.pass_sender = NULL;
390 flag.pass_target = NULL;
394 void ctf_Handle_Retrieve(entity flag, entity player)
396 entity sender = flag.pass_sender;
398 // transfer flag to player
400 flag.owner.flagcarried = flag;
401 GameRules_scoring_vip(player, true);
406 setattachment(flag, player.vehicle, "");
407 setorigin(flag, VEHICLE_FLAG_OFFSET);
408 flag.scale = VEHICLE_FLAG_SCALE;
412 setattachment(flag, player, "");
413 setorigin(flag, FLAG_CARRY_OFFSET);
415 set_movetype(flag, MOVETYPE_NONE);
416 flag.takedamage = DAMAGE_NO;
417 flag.solid = SOLID_NOT;
418 flag.angles = '0 0 0';
419 flag.ctf_status = FLAG_CARRY;
421 // messages and sounds
422 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
423 ctf_EventLog("receive", flag.team, player);
425 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
427 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
428 else if(it == player)
429 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
430 else if(SAME_TEAM(it, sender))
431 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
434 // create new waypoint
435 ctf_FlagcarrierWaypoints(player);
437 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
438 player.throw_antispam = sender.throw_antispam;
440 flag.pass_distance = 0;
441 flag.pass_sender = NULL;
442 flag.pass_target = NULL;
445 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
447 entity flag = player.flagcarried;
448 vector targ_origin, flag_velocity;
450 if(!flag) { return; }
451 if((droptype == DROP_PASS) && !receiver) { return; }
453 if(flag.speedrunning)
455 // ensure old waypoints are removed before resetting the flag
456 WaypointSprite_Kill(player.wps_flagcarrier);
458 if(player.wps_enemyflagcarrier)
459 WaypointSprite_Kill(player.wps_enemyflagcarrier);
461 if(player.wps_flagreturn)
462 WaypointSprite_Kill(player.wps_flagreturn);
463 ctf_RespawnFlag(flag);
468 setattachment(flag, NULL, "");
469 tracebox(player.origin - FLAG_DROP_OFFSET, flag.m_mins, flag.m_maxs, player.origin + FLAG_DROP_OFFSET, MOVE_NOMONSTERS, flag);
470 setorigin(flag, trace_endpos);
471 flag.owner.flagcarried = NULL;
472 GameRules_scoring_vip(flag.owner, false);
474 flag.solid = SOLID_TRIGGER;
475 flag.ctf_dropper = player;
476 flag.ctf_droptime = time;
477 flag.ctf_landtime = 0;
479 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
486 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
487 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
488 WarpZone_RefSys_Copy(flag, receiver);
489 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
490 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
492 flag.pass_distance = vlen((('1 0 0' * targ_origin.x) + ('0 1 0' * targ_origin.y)) - (('1 0 0' * player.origin.x) + ('0 1 0' * player.origin.y))); // for the sake of this check, exclude Z axis
493 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
496 set_movetype(flag, MOVETYPE_FLY);
497 flag.takedamage = DAMAGE_NO;
498 flag.pass_sender = player;
499 flag.pass_target = receiver;
500 flag.ctf_status = FLAG_PASSING;
503 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
504 WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
505 ctf_EventLog("pass", flag.team, player);
511 makevectors((player.v_angle.y * '0 1 0') + (bound(autocvar_g_ctf_throw_angle_min, player.v_angle.x, autocvar_g_ctf_throw_angle_max) * '1 0 0'));
513 flag_velocity = (('0 0 1' * autocvar_g_ctf_throw_velocity_up) + ((v_forward * autocvar_g_ctf_throw_velocity_forward) * ((StatusEffects_active(STATUSEFFECT_Strength, player)) ? autocvar_g_ctf_throw_strengthmultiplier : 1)));
514 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
515 ctf_Handle_Drop(flag, player, droptype);
516 navigation_dynamicgoal_set(flag, player);
522 flag.velocity = '0 0 0'; // do nothing
529 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, (('0 0 1' * autocvar_g_ctf_drop_velocity_up) + ((('0 1 0' * crandom()) + ('1 0 0' * crandom())) * autocvar_g_ctf_drop_velocity_side)), false);
530 ctf_Handle_Drop(flag, player, droptype);
531 navigation_dynamicgoal_set(flag, player);
536 // kill old waypointsprite
537 WaypointSprite_Ping(player.wps_flagcarrier);
538 WaypointSprite_Kill(player.wps_flagcarrier);
540 if(player.wps_enemyflagcarrier)
541 WaypointSprite_Kill(player.wps_enemyflagcarrier);
543 if(player.wps_flagreturn)
544 WaypointSprite_Kill(player.wps_flagreturn);
547 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
551 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
553 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
561 void nades_GiveBonus(entity player, float score);
563 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
565 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
566 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
567 entity player_team_flag = NULL, tmp_entity;
568 float old_time, new_time;
570 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
571 if(CTF_DIFFTEAM(player, flag)) { return; }
572 if((flag.cnt || enemy_flag.cnt) && flag.cnt != enemy_flag.cnt) { return; } // this should catch some edge cases (capturing grouped flag at ungrouped flag disallowed etc)
574 if (toucher.goalentity == flag.bot_basewaypoint)
575 toucher.goalentity_lock_timeout = 0;
578 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
579 if(SAME_TEAM(tmp_entity, player))
581 player_team_flag = tmp_entity;
585 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
587 player.throw_prevtime = time;
588 player.throw_count = 0;
590 // messages and sounds
591 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
592 ctf_CaptureRecord(enemy_flag, player);
593 _sound(player, CH_TRIGGER, ((ctf_oneflag) ? player_team_flag.snd_flag_capture : ((DIFF_TEAM(player, flag)) ? enemy_flag.snd_flag_capture : flag.snd_flag_capture)), VOL_BASE, ATTEN_NONE);
597 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
598 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
604 if(enemy_flag.score_capture || flag.score_capture)
605 pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
606 GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
608 if(enemy_flag.score_team_capture || flag.score_team_capture)
609 capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
610 GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
612 old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
613 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
614 if(!old_time || new_time < old_time)
615 GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
618 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
620 shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
624 if(capturetype == CAPTURE_NORMAL)
626 WaypointSprite_Kill(player.wps_flagcarrier);
627 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
629 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
630 { GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist)); }
633 flag.enemy = toucher;
636 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
637 ctf_RespawnFlag(enemy_flag);
640 void ctf_Handle_Return(entity flag, entity player)
642 // messages and sounds
643 if(IS_MONSTER(player))
645 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
649 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
650 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
652 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
653 ctf_EventLog("return", flag.team, player);
656 if(IS_PLAYER(player))
658 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
659 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
661 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
664 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
668 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
669 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
670 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
674 if(player.flagcarried == flag)
675 WaypointSprite_Kill(player.wps_flagcarrier);
680 ctf_RespawnFlag(flag);
683 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
686 float pickup_dropped_score; // used to calculate dropped pickup score
688 // attach the flag to the player
690 player.flagcarried = flag;
691 GameRules_scoring_vip(player, true);
694 setattachment(flag, player.vehicle, "");
695 setorigin(flag, VEHICLE_FLAG_OFFSET);
696 flag.scale = VEHICLE_FLAG_SCALE;
700 setattachment(flag, player, "");
701 setorigin(flag, FLAG_CARRY_OFFSET);
705 set_movetype(flag, MOVETYPE_NONE);
706 flag.takedamage = DAMAGE_NO;
707 flag.solid = SOLID_NOT;
708 flag.angles = '0 0 0';
709 flag.ctf_status = FLAG_CARRY;
713 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
714 case PICKUP_DROPPED: SetResourceExplicit(flag, RES_HEALTH, flag.max_health); break; // reset health/return timelimit
718 // messages and sounds
719 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
721 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
723 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
724 else if(CTF_DIFFTEAM(player, flag))
725 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
727 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
729 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
732 FOREACH_CLIENT(IS_PLAYER(it) && it != player && DIFF_TEAM(it, player), { Send_Notification(NOTIF_ONE, it, MSG_CHOICE, CHOICE_CTF_PICKUP_ENEMY_NEUTRAL, Team_ColorCode(player.team), player.netname); });
735 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
736 if(CTF_SAMETEAM(flag, it))
737 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, ((SAME_TEAM(flag, player)) ? CHOICE_CTF_PICKUP_ENEMY_TEAM : CHOICE_CTF_PICKUP_ENEMY), Team_ColorCode(player.team), player.netname);
738 else if(DIFF_TEAM(player, it))
739 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_ENEMY_OTHER), Team_ColorCode(player.team), player.netname);
742 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
745 GameRules_scoring_add(player, CTF_PICKUPS, 1);
746 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
751 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
752 ctf_EventLog("steal", flag.team, player);
758 pickup_dropped_score = (autocvar_g_ctf_flag_return_time ? bound(0, ((flag.ctf_droptime + autocvar_g_ctf_flag_return_time) - time) / autocvar_g_ctf_flag_return_time, 1) : 1);
759 pickup_dropped_score = floor((autocvar_g_ctf_score_pickup_dropped_late * (1 - pickup_dropped_score) + autocvar_g_ctf_score_pickup_dropped_early * pickup_dropped_score) + 0.5);
760 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
761 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
762 ctf_EventLog("pickup", flag.team, player);
770 if(pickuptype == PICKUP_BASE)
772 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
773 if((player.speedrunning) && (ctf_captimerecord))
774 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
778 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
781 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
782 ctf_FlagcarrierWaypoints(player);
783 WaypointSprite_Ping(player.wps_flagcarrier);
787 // ===================
788 // Main Flag Functions
789 // ===================
791 void ctf_CheckFlagReturn(entity flag, int returntype)
793 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
795 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH)); }
797 if((GetResource(flag, RES_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
802 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
804 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
805 case RETURN_SPEEDRUN:
806 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
807 case RETURN_NEEDKILL:
808 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
811 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
813 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
814 ctf_EventLog("returned", flag.team, NULL);
816 ctf_RespawnFlag(flag);
821 bool ctf_Stalemate_Customize(entity this, entity client)
823 // make spectators see what the player would see
824 entity e = WaypointSprite_getviewentity(client);
825 entity wp_owner = this.owner;
828 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
829 if(SAME_TEAM(wp_owner, e)) { return false; }
830 if(!IS_PLAYER(e)) { return false; }
835 void ctf_CheckStalemate()
838 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
841 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
843 // build list of stale flags
844 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
846 if(autocvar_g_ctf_stalemate)
847 if(tmp_entity.ctf_status != FLAG_BASE)
848 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
850 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
851 ctf_staleflaglist = tmp_entity;
853 switch(tmp_entity.team)
855 case NUM_TEAM_1: ++stale_red_flags; break;
856 case NUM_TEAM_2: ++stale_blue_flags; break;
857 case NUM_TEAM_3: ++stale_yellow_flags; break;
858 case NUM_TEAM_4: ++stale_pink_flags; break;
859 default: ++stale_neutral_flags; break;
865 stale_flags = (stale_neutral_flags >= 1);
867 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
869 if(ctf_oneflag && stale_flags == 1)
870 ctf_stalemate = true;
871 else if(stale_flags >= 2)
872 ctf_stalemate = true;
873 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
874 { ctf_stalemate = false; wpforenemy_announced = false; }
875 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
876 { ctf_stalemate = false; wpforenemy_announced = false; }
878 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
881 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
883 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
885 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, tmp_entity.owner, FLAG_WAYPOINT_OFFSET, NULL, 0, tmp_entity.owner, wps_enemyflagcarrier, true, RADARICON_FLAG);
886 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
887 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
891 if (!wpforenemy_announced)
893 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), { Send_Notification(NOTIF_ONE, it, MSG_CENTER, ((it.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER)); });
895 wpforenemy_announced = true;
900 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
902 if(ITEM_DAMAGE_NEEDKILL(deathtype))
904 if(autocvar_g_ctf_flag_return_damage_delay)
905 this.ctf_flagdamaged_byworld = true;
908 SetResourceExplicit(this, RES_HEALTH, 0);
909 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
913 if(autocvar_g_ctf_flag_return_damage)
915 // reduce health and check if it should be returned
916 TakeResource(this, RES_HEALTH, damage);
917 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
922 void ctf_FlagThink(entity this)
927 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
930 if(this == ctf_worldflaglist) // only for the first flag
931 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
934 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
935 LOG_TRACE("wtf the flag got squashed?");
936 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
937 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
938 setsize(this, this.m_mins, this.m_maxs);
942 switch(this.ctf_status)
946 if(autocvar_g_ctf_dropped_capture_radius)
948 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
949 if(tmp_entity.ctf_status == FLAG_DROPPED)
950 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
951 if((this.noalign || tmp_entity.ctf_landtime) && time > ((this.noalign) ? tmp_entity.ctf_droptime : tmp_entity.ctf_landtime) + autocvar_g_ctf_dropped_capture_delay)
952 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
959 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
960 if(IS_ONGROUND(this) && !this.ctf_landtime)
961 this.ctf_landtime = time; // landtime is reset when thrown, and we don't want to restart the timer if the flag is pushed
963 if(autocvar_g_ctf_flag_dropped_floatinwater)
965 vector midpoint = ((this.absmin + this.absmax) * 0.5);
966 if(pointcontents(midpoint) == CONTENT_WATER)
968 this.velocity = this.velocity * 0.5;
970 if (pointcontents(midpoint + eZ * FLAG_FLOAT_OFFSET_Z) == CONTENT_WATER)
971 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
973 { set_movetype(this, MOVETYPE_FLY); }
975 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
977 if(autocvar_g_ctf_flag_return_dropped)
979 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
981 SetResourceExplicit(this, RES_HEALTH, 0);
982 ctf_CheckFlagReturn(this, RETURN_DROPPED);
986 if(this.ctf_flagdamaged_byworld)
988 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
989 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
992 else if(autocvar_g_ctf_flag_return_time)
994 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
995 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
1003 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
1005 SetResourceExplicit(this, RES_HEALTH, 0);
1006 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
1008 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
1009 ImpulseCommands(this.owner);
1011 if(autocvar_g_ctf_stalemate)
1013 if(time >= wpforenemy_nextthink)
1015 ctf_CheckStalemate();
1016 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1019 if(CTF_SAMETEAM(this, this.owner) && this.team)
1021 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1022 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
1023 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
1024 ctf_Handle_Return(this, this.owner);
1031 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1032 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1033 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1035 if((this.pass_target == NULL)
1036 || (IS_DEAD(this.pass_target))
1037 || (this.pass_target.flagcarried)
1038 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1039 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1040 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1042 // give up, pass failed
1043 ctf_Handle_Drop(this, NULL, DROP_PASS);
1047 // still a viable target, go for it
1048 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1053 default: // this should never happen
1055 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1061 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1064 if(game_stopped) return;
1065 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1067 bool is_not_monster = (!IS_MONSTER(toucher));
1069 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1070 if(ITEM_TOUCH_NEEDKILL())
1072 if(!autocvar_g_ctf_flag_return_damage_delay)
1074 SetResourceExplicit(flag, RES_HEALTH, 0);
1075 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1077 if(!flag.ctf_flagdamaged_byworld) { return; }
1080 // special touch behaviors
1081 if(STAT(FROZEN, toucher)) { return; }
1082 else if(IS_VEHICLE(toucher))
1084 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1085 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1087 return; // do nothing
1089 else if(IS_MONSTER(toucher))
1091 if(!autocvar_g_ctf_allow_monster_touch)
1092 return; // do nothing
1094 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1096 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1098 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1099 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1100 flag.wait = time + FLAG_TOUCHRATE;
1104 else if(IS_DEAD(toucher)) { return; }
1106 switch(flag.ctf_status)
1112 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1113 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1114 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1115 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1117 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1118 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to their base
1119 else if(CTF_DIFFTEAM(toucher, flag) && (toucher.flagcarried) && CTF_SAMETEAM(toucher.flagcarried, toucher) && (!toucher.ctf_captureshielded) && autocvar_g_ctf_flag_return_carrying && (time > toucher.next_take_time) && is_not_monster)
1121 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1122 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1124 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1125 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1131 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1132 ctf_Handle_Return(flag, toucher); // toucher just returned their own flag
1133 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1134 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1140 LOG_TRACE("Someone touched a flag even though it was being carried?");
1146 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1148 if(DIFF_TEAM(toucher, flag.pass_sender))
1150 if(ctf_Immediate_Return_Allowed(flag, toucher))
1151 ctf_Handle_Return(flag, toucher);
1152 else if(is_not_monster && (!toucher.flagcarried))
1153 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1155 else if(!toucher.flagcarried)
1156 ctf_Handle_Retrieve(flag, toucher);
1163 void ctf_RespawnFlag(entity flag)
1165 flag.watertype = CONTENT_EMPTY; // TODO: it is unclear why this workaround is needed, likely many other potential breakage points!!
1167 // reset the player (if there is one)
1168 if((flag.owner) && (flag.owner.flagcarried == flag))
1170 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1171 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1172 WaypointSprite_Kill(flag.wps_flagcarrier);
1174 flag.owner.flagcarried = NULL;
1175 GameRules_scoring_vip(flag.owner, false);
1177 if(flag.speedrunning)
1178 ctf_FakeTimeLimit(flag.owner, -1);
1181 if((flag.owner) && (flag.owner.vehicle))
1182 flag.scale = FLAG_SCALE;
1184 if(flag.ctf_status == FLAG_DROPPED)
1185 { WaypointSprite_Kill(flag.wps_flagdropped); }
1188 setattachment(flag, NULL, "");
1189 setorigin(flag, flag.ctf_spawnorigin);
1191 //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1192 set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1193 flag.takedamage = DAMAGE_NO;
1194 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1195 flag.solid = SOLID_TRIGGER;
1196 flag.velocity = '0 0 0';
1197 flag.angles = flag.mangle;
1198 flag.flags = FL_ITEM | FL_NOTARGET;
1200 flag.ctf_status = FLAG_BASE;
1202 flag.pass_distance = 0;
1203 flag.pass_sender = NULL;
1204 flag.pass_target = NULL;
1205 flag.ctf_dropper = NULL;
1206 flag.ctf_pickuptime = 0;
1207 flag.ctf_droptime = 0;
1208 flag.ctf_landtime = 0;
1209 flag.ctf_flagdamaged_byworld = false;
1210 navigation_dynamicgoal_unset(flag);
1212 ctf_CheckStalemate();
1215 void ctf_Reset(entity this)
1217 if(this.owner && IS_PLAYER(this.owner))
1218 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1221 ctf_RespawnFlag(this);
1224 bool ctf_FlagBase_Customize(entity this, entity client)
1226 entity e = WaypointSprite_getviewentity(client);
1227 entity wp_owner = this.owner;
1228 entity flag = e.flagcarried;
1229 if(flag && CTF_SAMETEAM(e, flag))
1231 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1236 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1239 waypoint_spawnforitem_force(this, this.origin);
1240 navigation_dynamicgoal_init(this, true);
1246 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1247 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1248 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1249 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1250 default: basename = WP_FlagBaseNeutral; break;
1253 if(autocvar_g_ctf_flag_waypoint)
1255 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1256 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1257 wp.fade_rate = autocvar_g_ctf_flag_waypoint_maxdistance;
1258 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1259 setcefc(wp, ctf_FlagBase_Customize);
1262 // captureshield setup
1263 ctf_CaptureShield_Spawn(this);
1268 void ctf_FlagSetup(int teamnum, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1271 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1272 ctf_worldflaglist = flag;
1274 setattachment(flag, NULL, "");
1276 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnum), Team_ColorName_Upper(teamnum)));
1277 flag.team = teamnum;
1278 flag.classname = "item_flag_team";
1279 flag.target = "###item###"; // for finding the nearest item using findnearest
1280 flag.flags = FL_ITEM | FL_NOTARGET;
1281 IL_PUSH(g_items, flag);
1282 flag.solid = SOLID_TRIGGER;
1283 flag.takedamage = DAMAGE_NO;
1284 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1285 flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1286 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1287 flag.event_damage = ctf_FlagDamage;
1288 flag.pushable = true;
1289 flag.teleportable = TELEPORT_NORMAL;
1290 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1291 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1292 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1293 if(flag.damagedbycontents)
1294 IL_PUSH(g_damagedbycontents, flag);
1295 flag.velocity = '0 0 0';
1296 flag.mangle = flag.angles;
1297 flag.reset = ctf_Reset;
1298 settouch(flag, ctf_FlagTouch);
1299 setthink(flag, ctf_FlagThink);
1300 flag.nextthink = time + FLAG_THINKRATE;
1301 flag.ctf_status = FLAG_BASE;
1303 // set correct team colors
1304 flag.glowmod = Team_ColorRGB(teamnum);
1305 flag.colormap = (teamnum) ? (teamnum - 1) * 0x11 : 0x00;
1306 flag.colormap |= BIT(10); // RENDER_COLORMAPPED
1308 // crudely force them all to 0
1309 if(autocvar_g_ctf_score_ignore_fields)
1310 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1312 string teamname = Static_Team_ColorName_Lower(teamnum);
1314 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1315 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1316 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1317 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnum).eent_eff_name; }
1318 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnum).eent_eff_name; }
1319 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnum).eent_eff_name; }
1323 if(flag.s == "") flag.s = b; \
1324 precache_sound(flag.s);
1326 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnum))))
1327 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnum))))
1328 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnum))))
1329 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnum))))
1330 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1331 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1332 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1336 precache_model(flag.model);
1339 _setmodel(flag, flag.model); // precision set below
1340 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1341 flag.m_mins = flag.mins; // store these for squash checks
1342 flag.m_maxs = flag.maxs;
1343 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1345 if(autocvar_g_ctf_flag_glowtrails)
1349 case NUM_TEAM_1: flag.glow_color = 251; break;
1350 case NUM_TEAM_2: flag.glow_color = 210; break;
1351 case NUM_TEAM_3: flag.glow_color = 110; break;
1352 case NUM_TEAM_4: flag.glow_color = 145; break;
1353 default: flag.glow_color = 254; break;
1355 flag.glow_size = 25;
1356 flag.glow_trail = 1;
1359 flag.effects |= EF_LOWPRECISION;
1360 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1361 if(autocvar_g_ctf_dynamiclights)
1365 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1366 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1367 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1368 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1369 default: flag.effects |= EF_DIMLIGHT; break;
1374 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1376 flag.dropped_origin = flag.origin;
1377 flag.noalign = true;
1378 set_movetype(flag, MOVETYPE_NONE);
1380 else // drop to floor, automatically find a platform and set that as spawn origin
1382 flag.noalign = false;
1384 set_movetype(flag, MOVETYPE_NONE);
1387 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1395 // NOTE: LEGACY CODE, needs to be re-written!
1397 void havocbot_ctf_calculate_middlepoint()
1401 vector fo = '0 0 0';
1404 f = ctf_worldflaglist;
1409 f = f.ctf_worldflagnext;
1415 havocbot_middlepoint = s / n;
1416 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1418 havocbot_symmetry_axis_m = 0;
1419 havocbot_symmetry_axis_q = 0;
1422 // for symmetrical editing of waypoints
1423 entity f1 = ctf_worldflaglist;
1424 entity f2 = f1.ctf_worldflagnext;
1425 float m = -(f1.origin.y - f2.origin.y) / (max(f1.origin.x - f2.origin.x, FLOAT_EPSILON));
1426 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1427 havocbot_symmetry_axis_m = m;
1428 havocbot_symmetry_axis_q = q;
1430 havocbot_symmetry_origin_order = n;
1434 entity havocbot_ctf_find_flag(entity bot)
1437 f = ctf_worldflaglist;
1440 if (CTF_SAMETEAM(bot, f))
1442 f = f.ctf_worldflagnext;
1447 entity havocbot_ctf_find_enemy_flag(entity bot)
1450 f = ctf_worldflaglist;
1455 if(CTF_DIFFTEAM(bot, f))
1462 else if(!bot.flagcarried)
1466 else if (CTF_DIFFTEAM(bot, f))
1468 f = f.ctf_worldflagnext;
1473 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1480 FOREACH_CLIENT(IS_PLAYER(it), {
1481 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1484 if(vdist(it.origin - org, <, tc_radius))
1493 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1496 head = ctf_worldflaglist;
1499 if (CTF_SAMETEAM(this, head))
1501 head = head.ctf_worldflagnext;
1504 navigation_routerating(this, head, ratingscale, 10000);
1508 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1511 head = ctf_worldflaglist;
1514 if (CTF_SAMETEAM(this, head))
1516 if (this.flagcarried)
1517 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1519 head = head.ctf_worldflagnext; // skip base if it has a different group
1524 head = head.ctf_worldflagnext;
1529 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1532 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1535 head = ctf_worldflaglist;
1540 if(CTF_DIFFTEAM(this, head))
1544 if(this.flagcarried)
1547 else if(!this.flagcarried)
1551 else if(CTF_DIFFTEAM(this, head))
1553 head = head.ctf_worldflagnext;
1557 if (head.ctf_status == FLAG_CARRY)
1559 // adjust rating of our flag carrier depending on their health
1560 head = head.tag_entity;
1561 float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1562 ratingscale += ratingscale * f * 0.1;
1564 navigation_routerating(this, head, ratingscale, 10000);
1568 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1570 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1572 if (!bot_waypoints_for_items)
1574 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1580 head = havocbot_ctf_find_enemy_flag(this);
1585 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1588 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1592 mf = havocbot_ctf_find_flag(this);
1594 if(mf.ctf_status == FLAG_BASE)
1598 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1601 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1604 head = ctf_worldflaglist;
1607 // flag is out in the field
1608 if(head.ctf_status != FLAG_BASE)
1609 if(head.tag_entity==NULL) // dropped
1613 if(vdist(org - head.origin, <, df_radius))
1614 navigation_routerating(this, head, ratingscale, 10000);
1617 navigation_routerating(this, head, ratingscale, 10000);
1620 head = head.ctf_worldflagnext;
1624 void havocbot_ctf_reset_role(entity this)
1626 float cdefense, cmiddle, coffense;
1633 if (this.flagcarried)
1635 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1639 mf = havocbot_ctf_find_flag(this);
1640 ef = havocbot_ctf_find_enemy_flag(this);
1642 // Retrieve stolen flag
1643 if(mf.ctf_status!=FLAG_BASE)
1645 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1649 // If enemy flag is taken go to the middle to intercept pursuers
1650 if(ef.ctf_status!=FLAG_BASE)
1652 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1656 // if there is no one else on the team switch to offense
1658 // don't check if this bot is a player since it isn't true when the bot is added to the server
1659 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1663 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1666 else if (time < CS(this).jointime + 1)
1668 // if bots spawn all at once set good default roles
1671 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1674 else if (count == 2)
1676 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1681 // Evaluate best position to take
1682 // Count mates on middle position
1683 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1685 // Count mates on defense position
1686 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1688 // Count mates on offense position
1689 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1691 if(cdefense<=coffense)
1692 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1693 else if(coffense<=cmiddle)
1694 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1696 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1698 // if bots spawn all at once assign them a more appropriated role after a while
1699 if (time < CS(this).jointime + 1 && count > 2)
1700 this.havocbot_role_timeout = time + 10 + random() * 10;
1703 bool havocbot_ctf_is_basewaypoint(entity item)
1705 if (item.classname != "waypoint")
1708 entity head = ctf_worldflaglist;
1711 if (item == head.bot_basewaypoint)
1713 head = head.ctf_worldflagnext;
1718 void havocbot_role_ctf_carrier(entity this)
1722 havocbot_ctf_reset_role(this);
1726 if (this.flagcarried == NULL)
1728 havocbot_ctf_reset_role(this);
1732 if (navigation_goalrating_timeout(this))
1734 navigation_goalrating_start(this);
1737 entity mf = havocbot_ctf_find_flag(this);
1738 vector base_org = mf.dropped_origin;
1739 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1741 havocbot_goalrating_ctf_enemybase(this, base_rating);
1743 havocbot_goalrating_ctf_ourbase(this, base_rating);
1745 // start collecting items very close to the bot but only inside of own base radius
1746 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1747 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1749 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1751 navigation_goalrating_end(this);
1753 navigation_goalrating_timeout_set(this);
1755 entity goal = this.goalentity;
1756 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1757 this.goalentity_lock_timeout = time + ((this.enemy) ? 2 : 3);
1760 this.havocbot_cantfindflag = time + 10;
1761 else if (time > this.havocbot_cantfindflag)
1763 // Can't navigate to my own base, suicide!
1764 // TODO: drop it and wander around
1765 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1771 void havocbot_role_ctf_escort(entity this)
1777 havocbot_ctf_reset_role(this);
1781 if (this.flagcarried)
1783 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1787 // If enemy flag is back on the base switch to previous role
1788 ef = havocbot_ctf_find_enemy_flag(this);
1789 if(ef.ctf_status==FLAG_BASE)
1791 this.havocbot_role = this.havocbot_previous_role;
1792 this.havocbot_role_timeout = 0;
1795 if (ef.ctf_status == FLAG_DROPPED)
1797 navigation_goalrating_timeout_expire(this, 1);
1801 // If the flag carrier reached the base switch to defense
1802 mf = havocbot_ctf_find_flag(this);
1803 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1805 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1809 // Set the role timeout if necessary
1810 if (!this.havocbot_role_timeout)
1812 this.havocbot_role_timeout = time + random() * 30 + 60;
1815 // If nothing happened just switch to previous role
1816 if (time > this.havocbot_role_timeout)
1818 this.havocbot_role = this.havocbot_previous_role;
1819 this.havocbot_role_timeout = 0;
1823 // Chase the flag carrier
1824 if (navigation_goalrating_timeout(this))
1826 navigation_goalrating_start(this);
1829 havocbot_goalrating_ctf_enemyflag(this, 10000);
1830 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1831 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1833 navigation_goalrating_end(this);
1835 navigation_goalrating_timeout_set(this);
1839 void havocbot_role_ctf_offense(entity this)
1846 havocbot_ctf_reset_role(this);
1850 if (this.flagcarried)
1852 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1857 mf = havocbot_ctf_find_flag(this);
1858 ef = havocbot_ctf_find_enemy_flag(this);
1861 if(mf.ctf_status!=FLAG_BASE)
1864 pos = mf.tag_entity.origin;
1868 // Try to get it if closer than the enemy base
1869 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1871 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1876 // Escort flag carrier
1877 if(ef.ctf_status!=FLAG_BASE)
1880 pos = ef.tag_entity.origin;
1884 if(vdist(pos - mf.dropped_origin, >, 700))
1886 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1891 // Set the role timeout if necessary
1892 if (!this.havocbot_role_timeout)
1893 this.havocbot_role_timeout = time + 120;
1895 if (time > this.havocbot_role_timeout)
1897 havocbot_ctf_reset_role(this);
1901 if (navigation_goalrating_timeout(this))
1903 navigation_goalrating_start(this);
1906 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1907 havocbot_goalrating_ctf_enemybase(this, 10000);
1908 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1910 navigation_goalrating_end(this);
1912 navigation_goalrating_timeout_set(this);
1916 // Retriever (temporary role):
1917 void havocbot_role_ctf_retriever(entity this)
1923 havocbot_ctf_reset_role(this);
1927 if (this.flagcarried)
1929 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1933 // If flag is back on the base switch to previous role
1934 mf = havocbot_ctf_find_flag(this);
1935 if(mf.ctf_status==FLAG_BASE)
1937 if (mf.enemy == this) // did this bot return the flag?
1938 navigation_goalrating_timeout_force(this);
1939 havocbot_ctf_reset_role(this);
1943 if (!this.havocbot_role_timeout)
1944 this.havocbot_role_timeout = time + 20;
1946 if (time > this.havocbot_role_timeout)
1948 havocbot_ctf_reset_role(this);
1952 if (navigation_goalrating_timeout(this))
1954 const float RT_RADIUS = 10000;
1956 navigation_goalrating_start(this);
1959 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1960 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1961 havocbot_goalrating_ctf_enemybase(this, 8000);
1962 entity ef = havocbot_ctf_find_enemy_flag(this);
1963 vector enemy_base_org = ef.dropped_origin;
1964 // start collecting items very close to the bot but only inside of enemy base radius
1965 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1966 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1967 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1969 navigation_goalrating_end(this);
1971 navigation_goalrating_timeout_set(this);
1975 void havocbot_role_ctf_middle(entity this)
1981 havocbot_ctf_reset_role(this);
1985 if (this.flagcarried)
1987 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1991 mf = havocbot_ctf_find_flag(this);
1992 if(mf.ctf_status!=FLAG_BASE)
1994 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1998 if (!this.havocbot_role_timeout)
1999 this.havocbot_role_timeout = time + 10;
2001 if (time > this.havocbot_role_timeout)
2003 havocbot_ctf_reset_role(this);
2007 if (navigation_goalrating_timeout(this))
2011 org = havocbot_middlepoint;
2012 org.z = this.origin.z;
2014 navigation_goalrating_start(this);
2017 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
2018 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
2019 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2020 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2021 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2022 havocbot_goalrating_ctf_enemybase(this, 3000);
2024 navigation_goalrating_end(this);
2026 entity goal = this.goalentity;
2027 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
2028 this.goalentity_lock_timeout = time + 2;
2030 navigation_goalrating_timeout_set(this);
2034 void havocbot_role_ctf_defense(entity this)
2040 havocbot_ctf_reset_role(this);
2044 if (this.flagcarried)
2046 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2050 // If own flag was captured
2051 mf = havocbot_ctf_find_flag(this);
2052 if(mf.ctf_status!=FLAG_BASE)
2054 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2058 if (!this.havocbot_role_timeout)
2059 this.havocbot_role_timeout = time + 30;
2061 if (time > this.havocbot_role_timeout)
2063 havocbot_ctf_reset_role(this);
2066 if (navigation_goalrating_timeout(this))
2068 vector org = mf.dropped_origin;
2070 navigation_goalrating_start(this);
2072 // if enemies are closer to our base, go there
2073 entity closestplayer = NULL;
2074 float distance, bestdistance = 10000;
2075 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2076 distance = vlen(org - it.origin);
2077 if(distance<bestdistance)
2080 bestdistance = distance;
2086 if(DIFF_TEAM(closestplayer, this))
2087 if(vdist(org - this.origin, >, 1000))
2088 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2089 havocbot_goalrating_ctf_ourbase(this, 10000);
2091 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2092 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2093 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2094 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2095 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2097 navigation_goalrating_end(this);
2099 navigation_goalrating_timeout_set(this);
2103 void havocbot_role_ctf_setrole(entity bot, int role)
2105 string s = "(null)";
2108 case HAVOCBOT_CTF_ROLE_CARRIER:
2110 bot.havocbot_role = havocbot_role_ctf_carrier;
2111 bot.havocbot_role_timeout = 0;
2112 bot.havocbot_cantfindflag = time + 10;
2113 if (bot.havocbot_previous_role != bot.havocbot_role)
2114 navigation_goalrating_timeout_force(bot);
2116 case HAVOCBOT_CTF_ROLE_DEFENSE:
2118 bot.havocbot_role = havocbot_role_ctf_defense;
2119 bot.havocbot_role_timeout = 0;
2121 case HAVOCBOT_CTF_ROLE_MIDDLE:
2123 bot.havocbot_role = havocbot_role_ctf_middle;
2124 bot.havocbot_role_timeout = 0;
2126 case HAVOCBOT_CTF_ROLE_OFFENSE:
2128 bot.havocbot_role = havocbot_role_ctf_offense;
2129 bot.havocbot_role_timeout = 0;
2131 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2133 bot.havocbot_previous_role = bot.havocbot_role;
2134 bot.havocbot_role = havocbot_role_ctf_retriever;
2135 bot.havocbot_role_timeout = time + 10;
2136 if (bot.havocbot_previous_role != bot.havocbot_role)
2137 navigation_goalrating_timeout_expire(bot, 2);
2139 case HAVOCBOT_CTF_ROLE_ESCORT:
2141 bot.havocbot_previous_role = bot.havocbot_role;
2142 bot.havocbot_role = havocbot_role_ctf_escort;
2143 bot.havocbot_role_timeout = time + 30;
2144 if (bot.havocbot_previous_role != bot.havocbot_role)
2145 navigation_goalrating_timeout_expire(bot, 2);
2148 LOG_TRACE(bot.netname, " switched to ", s);
2156 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2158 entity player = M_ARGV(0, entity);
2160 int t = 0, t2 = 0, t3 = 0;
2161 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)
2163 // initially clear items so they can be set as necessary later.
2164 STAT(OBJECTIVE_STATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2165 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2166 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2167 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2168 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2169 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2171 // scan through all the flags and notify the client about them
2172 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2174 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2175 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2176 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2177 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2178 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; }
2180 switch(flag.ctf_status)
2185 if((flag.owner == player) || (flag.pass_sender == player))
2186 STAT(OBJECTIVE_STATUS, player) |= t; // carrying: player is currently carrying the flag
2188 STAT(OBJECTIVE_STATUS, player) |= t2; // taken: someone else is carrying the flag
2193 STAT(OBJECTIVE_STATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2199 // item for stopping players from capturing the flag too often
2200 if(player.ctf_captureshielded)
2201 STAT(OBJECTIVE_STATUS, player) |= CTF_SHIELDED;
2204 STAT(OBJECTIVE_STATUS, player) |= CTF_STALEMATE;
2206 // update the health of the flag carrier waypointsprite
2207 if(player.wps_flagcarrier)
2208 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);
2211 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in damage.qc
2213 entity frag_attacker = M_ARGV(1, entity);
2214 entity frag_target = M_ARGV(2, entity);
2215 float frag_damage = M_ARGV(4, float);
2216 vector frag_force = M_ARGV(6, vector);
2218 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2220 if(frag_target == frag_attacker) // damage done to yourself
2222 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2223 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2225 else // damage done to everyone else
2227 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2228 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2231 M_ARGV(4, float) = frag_damage;
2232 M_ARGV(6, vector) = frag_force;
2234 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2236 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
2237 && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2239 frag_target.wps_helpme_time = time;
2240 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2242 // todo: add notification for when flag carrier needs help?
2246 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2248 entity frag_attacker = M_ARGV(1, entity);
2249 entity frag_target = M_ARGV(2, entity);
2251 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2253 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2254 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2257 if(frag_target.flagcarried)
2259 entity tmp_entity = frag_target.flagcarried;
2260 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2261 tmp_entity.ctf_dropper = NULL;
2265 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2267 M_ARGV(2, float) = 0; // frag score
2268 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2271 void ctf_RemovePlayer(entity player)
2273 if(player.flagcarried)
2274 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2276 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2278 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2279 if(flag.pass_target == player) { flag.pass_target = NULL; }
2280 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2284 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2286 entity player = M_ARGV(0, entity);
2288 ctf_RemovePlayer(player);
2291 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2293 entity player = M_ARGV(0, entity);
2295 ctf_RemovePlayer(player);
2298 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2300 if(!autocvar_g_ctf_leaderboard)
2303 entity player = M_ARGV(0, entity);
2305 race_SendAll(player, true);
2308 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2310 if(!autocvar_g_ctf_leaderboard)
2313 entity player = M_ARGV(0, entity);
2315 race_checkAndWriteName(player);
2318 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2320 entity player = M_ARGV(0, entity);
2322 if(player.flagcarried)
2323 if(!autocvar_g_ctf_portalteleport)
2324 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2327 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2329 if(MUTATOR_RETURNVALUE || game_stopped) return;
2331 entity player = M_ARGV(0, entity);
2333 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2335 // pass the flag to a team mate
2336 if(autocvar_g_ctf_pass)
2338 entity head, closest_target = NULL;
2339 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2341 while(head) // find the closest acceptable target to pass to
2343 if(IS_PLAYER(head) && !IS_DEAD(head))
2344 if(head != player && SAME_TEAM(head, player))
2345 if(!head.speedrunning && !head.vehicle)
2347 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in damage.qc)
2348 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2349 vector passer_center = CENTER_OR_VIEWOFS(player);
2351 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2353 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2355 if(IS_BOT_CLIENT(head))
2357 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2358 ctf_Handle_Throw(head, player, DROP_PASS);
2362 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2363 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2365 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2368 else if(player.flagcarried && !head.flagcarried)
2372 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2373 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2374 { closest_target = head; }
2376 else { closest_target = head; }
2383 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2386 // throw the flag in front of you
2387 if(autocvar_g_ctf_throw && player.flagcarried)
2389 if(player.throw_count == -1)
2391 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2393 player.throw_prevtime = time;
2394 player.throw_count = 1;
2395 ctf_Handle_Throw(player, NULL, DROP_THROW);
2400 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2406 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2407 else { player.throw_count += 1; }
2408 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2410 player.throw_prevtime = time;
2411 ctf_Handle_Throw(player, NULL, DROP_THROW);
2418 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2420 entity player = M_ARGV(0, entity);
2422 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2424 player.wps_helpme_time = time;
2425 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2427 else // create a normal help me waypointsprite
2429 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2430 WaypointSprite_Ping(player.wps_helpme);
2436 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2438 entity player = M_ARGV(0, entity);
2439 entity veh = M_ARGV(1, entity);
2441 if(player.flagcarried)
2443 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2445 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2449 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2450 setattachment(player.flagcarried, veh, "");
2451 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2452 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2453 //player.flagcarried.angles = '0 0 0';
2459 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2461 entity player = M_ARGV(0, entity);
2463 if(player.flagcarried)
2465 setattachment(player.flagcarried, player, "");
2466 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2467 player.flagcarried.scale = FLAG_SCALE;
2468 player.flagcarried.angles = '0 0 0';
2469 player.flagcarried.nodrawtoclient = NULL;
2474 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2476 entity player = M_ARGV(0, entity);
2478 if(player.flagcarried)
2480 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2481 ctf_RespawnFlag(player.flagcarried);
2486 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2488 entity flag; // temporary entity for the search method
2490 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2492 switch(flag.ctf_status)
2497 // lock the flag, game is over
2498 set_movetype(flag, MOVETYPE_NONE);
2499 flag.takedamage = DAMAGE_NO;
2500 flag.solid = SOLID_NOT;
2501 flag.nextthink = false; // stop thinking
2503 //dprint("stopping the ", flag.netname, " from moving.\n");
2511 // do nothing for these flags
2518 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2520 entity bot = M_ARGV(0, entity);
2522 havocbot_ctf_reset_role(bot);
2526 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2528 M_ARGV(1, string) = "ctf_team";
2531 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2533 int record_page = M_ARGV(0, int);
2534 string ret_string = M_ARGV(1, string);
2536 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2538 if (MapInfo_Get_ByID(i))
2540 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2546 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2547 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2551 M_ARGV(1, string) = ret_string;
2554 bool superspec_Spectate(entity this, entity targ); // TODO
2555 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2556 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2558 entity player = M_ARGV(0, entity);
2559 string cmd_name = M_ARGV(1, string);
2560 int cmd_argc = M_ARGV(2, int);
2562 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2564 if(cmd_name == "followfc")
2576 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2577 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2578 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2579 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2583 FOREACH_CLIENT(IS_PLAYER(it), {
2584 if(it.flagcarried && (it.team == _team || _team == 0))
2587 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2588 continue; // already spectating this fc, try another
2589 return superspec_Spectate(player, it);
2594 superspec_msg("", "", player, "No active flag carrier\n", 1);
2599 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2601 entity frag_target = M_ARGV(0, entity);
2603 if(frag_target.flagcarried)
2604 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2607 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2609 entity player = M_ARGV(0, entity);
2610 if(player.flagcarried)
2611 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2619 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2620 CTF flag for team one (Red).
2622 "angle" Angle the flag will point (minus 90 degrees)...
2623 "model" model to use, note this needs red and blue as skins 0 and 1...
2624 "noise" sound played when flag is picked up...
2625 "noise1" sound played when flag is returned by a teammate...
2626 "noise2" sound played when flag is captured...
2627 "noise3" sound played when flag is lost in the field and respawns itself...
2628 "noise4" sound played when flag is dropped by a player...
2629 "noise5" sound played when flag touches the ground... */
2630 spawnfunc(item_flag_team1)
2632 if(!g_ctf) { delete(this); return; }
2634 ctf_FlagSetup(NUM_TEAM_1, this);
2637 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2638 CTF flag for team two (Blue).
2640 "angle" Angle the flag will point (minus 90 degrees)...
2641 "model" model to use, note this needs red and blue as skins 0 and 1...
2642 "noise" sound played when flag is picked up...
2643 "noise1" sound played when flag is returned by a teammate...
2644 "noise2" sound played when flag is captured...
2645 "noise3" sound played when flag is lost in the field and respawns itself...
2646 "noise4" sound played when flag is dropped by a player...
2647 "noise5" sound played when flag touches the ground... */
2648 spawnfunc(item_flag_team2)
2650 if(!g_ctf) { delete(this); return; }
2652 ctf_FlagSetup(NUM_TEAM_2, this);
2655 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2656 CTF flag for team three (Yellow).
2658 "angle" Angle the flag will point (minus 90 degrees)...
2659 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2660 "noise" sound played when flag is picked up...
2661 "noise1" sound played when flag is returned by a teammate...
2662 "noise2" sound played when flag is captured...
2663 "noise3" sound played when flag is lost in the field and respawns itself...
2664 "noise4" sound played when flag is dropped by a player...
2665 "noise5" sound played when flag touches the ground... */
2666 spawnfunc(item_flag_team3)
2668 if(!g_ctf) { delete(this); return; }
2670 ctf_FlagSetup(NUM_TEAM_3, this);
2673 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2674 CTF flag for team four (Pink).
2676 "angle" Angle the flag will point (minus 90 degrees)...
2677 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2678 "noise" sound played when flag is picked up...
2679 "noise1" sound played when flag is returned by a teammate...
2680 "noise2" sound played when flag is captured...
2681 "noise3" sound played when flag is lost in the field and respawns itself...
2682 "noise4" sound played when flag is dropped by a player...
2683 "noise5" sound played when flag touches the ground... */
2684 spawnfunc(item_flag_team4)
2686 if(!g_ctf) { delete(this); return; }
2688 ctf_FlagSetup(NUM_TEAM_4, this);
2691 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2694 "angle" Angle the flag will point (minus 90 degrees)...
2695 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2696 "noise" sound played when flag is picked up...
2697 "noise1" sound played when flag is returned by a teammate...
2698 "noise2" sound played when flag is captured...
2699 "noise3" sound played when flag is lost in the field and respawns itself...
2700 "noise4" sound played when flag is dropped by a player...
2701 "noise5" sound played when flag touches the ground... */
2702 spawnfunc(item_flag_neutral)
2704 if(!g_ctf) { delete(this); return; }
2705 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2707 ctf_FlagSetup(0, this);
2710 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2711 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2712 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.
2714 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2715 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2718 if(!g_ctf) { delete(this); return; }
2720 this.team = this.cnt + 1;
2723 // compatibility for quake maps
2724 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2725 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2726 spawnfunc(info_player_team1);
2727 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2728 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2729 spawnfunc(info_player_team2);
2730 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2731 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2733 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2734 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2736 // compatibility for wop maps
2737 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2738 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2739 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2740 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2741 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2742 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2750 void ctf_ScoreRules(int teams)
2752 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2753 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2754 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2755 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2756 field(SP_CTF_PICKUPS, "pickups", 0);
2757 field(SP_CTF_FCKILLS, "fckills", 0);
2758 field(SP_CTF_RETURNS, "returns", 0);
2759 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2763 // code from here on is just to support maps that don't have flag and team entities
2764 void ctf_SpawnTeam (string teamname, int teamcolor)
2766 entity this = new_pure(ctf_team);
2767 this.netname = teamname;
2768 this.cnt = teamcolor - 1;
2769 this.spawnfunc_checked = true;
2770 this.team = teamcolor;
2773 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2778 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2780 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2781 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2783 switch(tmp_entity.team)
2785 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2786 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2787 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2788 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2790 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2793 havocbot_ctf_calculate_middlepoint();
2795 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2797 ctf_teams = 0; // so set the default red and blue teams
2798 BITSET_ASSIGN(ctf_teams, BIT(0));
2799 BITSET_ASSIGN(ctf_teams, BIT(1));
2802 //ctf_teams = bound(2, ctf_teams, 4);
2804 // if no teams are found, spawn defaults
2805 if(find(NULL, classname, "ctf_team") == NULL)
2807 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2808 if(ctf_teams & BIT(0))
2809 ctf_SpawnTeam("Red", NUM_TEAM_1);
2810 if(ctf_teams & BIT(1))
2811 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2812 if(ctf_teams & BIT(2))
2813 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2814 if(ctf_teams & BIT(3))
2815 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2818 ctf_ScoreRules(ctf_teams);
2821 void ctf_Initialize()
2823 CTF_FLAG = NEW(Flag);
2824 record_type = CTF_RECORD;
2825 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2827 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2828 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2829 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2831 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);