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_dropper = player;
360 flag.ctf_status = FLAG_DROPPED;
362 // messages and sounds
363 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
364 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
365 ctf_EventLog("dropped", player.team, player);
368 GameRules_scoring_add_team(player, SCORE, -((flag.score_drop) ? flag.score_drop : autocvar_g_ctf_score_penalty_drop));
369 GameRules_scoring_add(player, CTF_DROPS, 1);
372 if(autocvar_g_ctf_flag_dropped_waypoint) {
373 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);
374 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
377 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
379 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_health);
380 WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH));
383 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
385 if(droptype == DROP_PASS)
387 flag.pass_distance = 0;
388 flag.pass_sender = NULL;
389 flag.pass_target = NULL;
393 void ctf_Handle_Retrieve(entity flag, entity player)
395 entity sender = flag.pass_sender;
397 // transfer flag to player
399 flag.owner.flagcarried = flag;
400 GameRules_scoring_vip(player, true);
405 setattachment(flag, player.vehicle, "");
406 setorigin(flag, VEHICLE_FLAG_OFFSET);
407 flag.scale = VEHICLE_FLAG_SCALE;
411 setattachment(flag, player, "");
412 setorigin(flag, FLAG_CARRY_OFFSET);
414 set_movetype(flag, MOVETYPE_NONE);
415 flag.takedamage = DAMAGE_NO;
416 flag.solid = SOLID_NOT;
417 flag.angles = '0 0 0';
418 flag.ctf_status = FLAG_CARRY;
420 // messages and sounds
421 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
422 ctf_EventLog("receive", flag.team, player);
424 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
426 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
427 else if(it == player)
428 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
429 else if(SAME_TEAM(it, sender))
430 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
433 // create new waypoint
434 ctf_FlagcarrierWaypoints(player);
436 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
437 player.throw_antispam = sender.throw_antispam;
439 flag.pass_distance = 0;
440 flag.pass_sender = NULL;
441 flag.pass_target = NULL;
444 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
446 entity flag = player.flagcarried;
447 vector targ_origin, flag_velocity;
449 if(!flag) { return; }
450 if((droptype == DROP_PASS) && !receiver) { return; }
452 if(flag.speedrunning)
454 // ensure old waypoints are removed before resetting the flag
455 WaypointSprite_Kill(player.wps_flagcarrier);
457 if(player.wps_enemyflagcarrier)
458 WaypointSprite_Kill(player.wps_enemyflagcarrier);
460 if(player.wps_flagreturn)
461 WaypointSprite_Kill(player.wps_flagreturn);
462 ctf_RespawnFlag(flag);
467 setattachment(flag, NULL, "");
468 tracebox(player.origin - FLAG_DROP_OFFSET, flag.m_mins, flag.m_maxs, player.origin + FLAG_DROP_OFFSET, MOVE_NOMONSTERS, flag);
469 setorigin(flag, trace_endpos);
470 flag.owner.flagcarried = NULL;
471 GameRules_scoring_vip(flag.owner, false);
473 flag.solid = SOLID_TRIGGER;
474 flag.ctf_dropper = player;
475 flag.ctf_droptime = time;
477 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
484 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
485 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
486 WarpZone_RefSys_Copy(flag, receiver);
487 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
488 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
490 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
491 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
494 set_movetype(flag, MOVETYPE_FLY);
495 flag.takedamage = DAMAGE_NO;
496 flag.pass_sender = player;
497 flag.pass_target = receiver;
498 flag.ctf_status = FLAG_PASSING;
501 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
502 WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
503 ctf_EventLog("pass", flag.team, player);
509 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'));
511 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)));
512 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
513 ctf_Handle_Drop(flag, player, droptype);
514 navigation_dynamicgoal_set(flag, player);
520 flag.velocity = '0 0 0'; // do nothing
527 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);
528 ctf_Handle_Drop(flag, player, droptype);
529 navigation_dynamicgoal_set(flag, player);
534 // kill old waypointsprite
535 WaypointSprite_Ping(player.wps_flagcarrier);
536 WaypointSprite_Kill(player.wps_flagcarrier);
538 if(player.wps_enemyflagcarrier)
539 WaypointSprite_Kill(player.wps_enemyflagcarrier);
541 if(player.wps_flagreturn)
542 WaypointSprite_Kill(player.wps_flagreturn);
545 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
549 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
551 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
559 void nades_GiveBonus(entity player, float score);
561 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
563 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
564 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
565 entity player_team_flag = NULL, tmp_entity;
566 float old_time, new_time;
568 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
569 if(CTF_DIFFTEAM(player, flag)) { return; }
570 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)
572 if (toucher.goalentity == flag.bot_basewaypoint)
573 toucher.goalentity_lock_timeout = 0;
576 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
577 if(SAME_TEAM(tmp_entity, player))
579 player_team_flag = tmp_entity;
583 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
585 player.throw_prevtime = time;
586 player.throw_count = 0;
588 // messages and sounds
589 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
590 ctf_CaptureRecord(enemy_flag, player);
591 _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);
595 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
596 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
602 if(enemy_flag.score_capture || flag.score_capture)
603 pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
604 GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
606 if(enemy_flag.score_team_capture || flag.score_team_capture)
607 capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
608 GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
610 old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
611 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
612 if(!old_time || new_time < old_time)
613 GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
615 Give_Medal(player, CAPTURE);
618 AnnounceScores(player.team);
621 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
623 shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
627 if(capturetype == CAPTURE_NORMAL)
629 WaypointSprite_Kill(player.wps_flagcarrier);
630 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
632 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
634 GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist));
635 Give_Medal(enemy_flag.ctf_dropper, ASSIST);
639 flag.enemy = toucher;
642 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
643 ctf_RespawnFlag(enemy_flag);
646 void ctf_Handle_Return(entity flag, entity player)
648 // messages and sounds
649 if(IS_MONSTER(player))
651 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
655 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
656 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
658 FOREACH_CLIENT(IS_PLAYER(it), {
659 if(it.team == flag.team)
660 Send_Notification(NOTIF_ONE_ONLY, it, MSG_ANNCE, ANNCE_CTF_RETURN_TEAM);
662 Send_Notification(NOTIF_ONE_ONLY, it, MSG_ANNCE, ANNCE_CTF_RETURN_ENEMY);
664 Send_Notification(NOTIF_ALL_SPEC, NULL, MSG_ANNCE, APP_TEAM_NUM(flag.team, ANNCE_CTF_RETURN));
666 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
667 ctf_EventLog("return", flag.team, player);
670 if(IS_PLAYER(player))
672 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
673 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
675 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
678 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
682 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
683 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
684 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
688 if(player.flagcarried == flag)
689 WaypointSprite_Kill(player.wps_flagcarrier);
694 ctf_RespawnFlag(flag);
697 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
700 float pickup_dropped_score; // used to calculate dropped pickup score
702 // attach the flag to the player
704 player.flagcarried = flag;
705 GameRules_scoring_vip(player, true);
708 setattachment(flag, player.vehicle, "");
709 setorigin(flag, VEHICLE_FLAG_OFFSET);
710 flag.scale = VEHICLE_FLAG_SCALE;
714 setattachment(flag, player, "");
715 setorigin(flag, FLAG_CARRY_OFFSET);
719 set_movetype(flag, MOVETYPE_NONE);
720 flag.takedamage = DAMAGE_NO;
721 flag.solid = SOLID_NOT;
722 flag.angles = '0 0 0';
723 flag.ctf_status = FLAG_CARRY;
727 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
728 case PICKUP_DROPPED: SetResourceExplicit(flag, RES_HEALTH, flag.max_health); break; // reset health/return timelimit
732 // messages and sounds
733 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
734 Send_Notification(NOTIF_ALL_SPEC, NULL, MSG_ANNCE, APP_TEAM_NUM(flag.team, ANNCE_CTF_PICKUP));
737 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
739 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
740 else if(CTF_DIFFTEAM(player, flag))
742 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
743 Send_Notification(NOTIF_ONE_ONLY, player, MSG_ANNCE, ANNCE_CTF_PICKUP_YOU);
746 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
748 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
749 Send_Notification(NOTIF_TEAM_ONLY_EXCEPT, player, MSG_ANNCE, ANNCE_CTF_PICKUP_TEAM);
752 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); });
755 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
756 if(CTF_SAMETEAM(flag, it))
758 if(SAME_TEAM(player, it)) {
759 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
760 Send_Notification(NOTIF_ONE_ONLY, it, MSG_ANNCE, ANNCE_CTF_PICKUP_TEAM);
762 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);
763 Send_Notification(NOTIF_ONE_ONLY, it, MSG_ANNCE, ANNCE_CTF_PICKUP_ENEMY);
768 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
771 GameRules_scoring_add(player, CTF_PICKUPS, 1);
772 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
777 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
778 ctf_EventLog("steal", flag.team, player);
784 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);
785 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);
786 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
787 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
788 ctf_EventLog("pickup", flag.team, player);
796 if(pickuptype == PICKUP_BASE)
798 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
799 if((player.speedrunning) && (ctf_captimerecord))
800 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
804 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
807 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
808 ctf_FlagcarrierWaypoints(player);
809 WaypointSprite_Ping(player.wps_flagcarrier);
813 // ===================
814 // Main Flag Functions
815 // ===================
817 void ctf_CheckFlagReturn(entity flag, int returntype)
819 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
821 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH)); }
823 if((GetResource(flag, RES_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
828 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
830 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
831 case RETURN_SPEEDRUN:
832 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
833 case RETURN_NEEDKILL:
834 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
837 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
839 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
840 ctf_EventLog("returned", flag.team, NULL);
842 ctf_RespawnFlag(flag);
847 bool ctf_Stalemate_Customize(entity this, entity client)
849 // make spectators see what the player would see
850 entity e = WaypointSprite_getviewentity(client);
851 entity wp_owner = this.owner;
854 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
855 if(SAME_TEAM(wp_owner, e)) { return false; }
856 if(!IS_PLAYER(e)) { return false; }
861 void ctf_CheckStalemate()
864 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
867 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
869 // build list of stale flags
870 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
872 if(autocvar_g_ctf_stalemate)
873 if(tmp_entity.ctf_status != FLAG_BASE)
874 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
876 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
877 ctf_staleflaglist = tmp_entity;
879 switch(tmp_entity.team)
881 case NUM_TEAM_1: ++stale_red_flags; break;
882 case NUM_TEAM_2: ++stale_blue_flags; break;
883 case NUM_TEAM_3: ++stale_yellow_flags; break;
884 case NUM_TEAM_4: ++stale_pink_flags; break;
885 default: ++stale_neutral_flags; break;
891 stale_flags = (stale_neutral_flags >= 1);
893 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
895 if(ctf_oneflag && stale_flags == 1)
896 ctf_stalemate = true;
897 else if(stale_flags >= 2)
898 ctf_stalemate = true;
899 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
900 { ctf_stalemate = false; wpforenemy_announced = false; }
901 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
902 { ctf_stalemate = false; wpforenemy_announced = false; }
904 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
907 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
909 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
911 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);
912 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
913 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
917 if (!wpforenemy_announced)
919 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)); });
921 wpforenemy_announced = true;
926 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
928 if(ITEM_DAMAGE_NEEDKILL(deathtype))
930 if(autocvar_g_ctf_flag_return_damage_delay)
931 this.ctf_flagdamaged_byworld = true;
934 SetResourceExplicit(this, RES_HEALTH, 0);
935 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
939 if(autocvar_g_ctf_flag_return_damage)
941 // reduce health and check if it should be returned
942 TakeResource(this, RES_HEALTH, damage);
943 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
948 void ctf_FlagThink(entity this)
953 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
956 if(this == ctf_worldflaglist) // only for the first flag
957 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
960 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
961 LOG_TRACE("wtf the flag got squashed?");
962 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
963 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
964 setsize(this, this.m_mins, this.m_maxs);
968 switch(this.ctf_status)
972 if(autocvar_g_ctf_dropped_capture_radius)
974 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
975 if(tmp_entity.ctf_status == FLAG_DROPPED)
976 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
977 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
978 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
985 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
987 if(autocvar_g_ctf_flag_dropped_floatinwater)
989 vector midpoint = ((this.absmin + this.absmax) * 0.5);
990 if(pointcontents(midpoint) == CONTENT_WATER)
992 this.velocity = this.velocity * 0.5;
994 if (pointcontents(midpoint + eZ * FLAG_FLOAT_OFFSET_Z) == CONTENT_WATER)
995 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
997 { set_movetype(this, MOVETYPE_FLY); }
999 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
1001 if(autocvar_g_ctf_flag_return_dropped)
1003 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
1005 SetResourceExplicit(this, RES_HEALTH, 0);
1006 ctf_CheckFlagReturn(this, RETURN_DROPPED);
1010 if(this.ctf_flagdamaged_byworld)
1012 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
1013 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
1016 else if(autocvar_g_ctf_flag_return_time)
1018 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
1019 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
1027 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
1029 SetResourceExplicit(this, RES_HEALTH, 0);
1030 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
1032 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
1033 ImpulseCommands(this.owner);
1035 if(autocvar_g_ctf_stalemate)
1037 if(time >= wpforenemy_nextthink)
1039 ctf_CheckStalemate();
1040 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1043 if(CTF_SAMETEAM(this, this.owner) && this.team)
1045 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1046 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
1047 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
1048 ctf_Handle_Return(this, this.owner);
1055 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1056 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1057 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1059 if((this.pass_target == NULL)
1060 || (IS_DEAD(this.pass_target))
1061 || (this.pass_target.flagcarried)
1062 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1063 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1064 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1066 // give up, pass failed
1067 ctf_Handle_Drop(this, NULL, DROP_PASS);
1071 // still a viable target, go for it
1072 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1077 default: // this should never happen
1079 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1085 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1088 if(game_stopped) return;
1089 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1091 bool is_not_monster = (!IS_MONSTER(toucher));
1093 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1094 if(ITEM_TOUCH_NEEDKILL())
1096 if(!autocvar_g_ctf_flag_return_damage_delay)
1098 SetResourceExplicit(flag, RES_HEALTH, 0);
1099 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1101 if(!flag.ctf_flagdamaged_byworld) { return; }
1104 // special touch behaviors
1105 if(STAT(FROZEN, toucher)) { return; }
1106 else if(IS_VEHICLE(toucher))
1108 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1109 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1111 return; // do nothing
1113 else if(IS_MONSTER(toucher))
1115 if(!autocvar_g_ctf_allow_monster_touch)
1116 return; // do nothing
1118 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1120 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1122 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1123 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1124 flag.wait = time + FLAG_TOUCHRATE;
1128 else if(IS_DEAD(toucher)) { return; }
1130 switch(flag.ctf_status)
1136 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1137 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1138 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1139 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1141 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1142 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1143 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)
1145 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1146 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1148 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1149 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1155 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1156 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1157 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1158 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1164 LOG_TRACE("Someone touched a flag even though it was being carried?");
1170 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1172 if(DIFF_TEAM(toucher, flag.pass_sender))
1174 if(ctf_Immediate_Return_Allowed(flag, toucher))
1175 ctf_Handle_Return(flag, toucher);
1176 else if(is_not_monster && (!toucher.flagcarried))
1177 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1179 else if(!toucher.flagcarried)
1180 ctf_Handle_Retrieve(flag, toucher);
1187 .float last_respawn;
1188 void ctf_RespawnFlag(entity flag)
1190 flag.watertype = CONTENT_EMPTY; // TODO: it is unclear why this workaround is needed, likely many other potential breakage points!!
1191 // check for flag respawn being called twice in a row
1192 if(flag.last_respawn > time - 0.5)
1193 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1195 flag.last_respawn = time;
1197 // reset the player (if there is one)
1198 if((flag.owner) && (flag.owner.flagcarried == flag))
1200 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1201 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1202 WaypointSprite_Kill(flag.wps_flagcarrier);
1204 flag.owner.flagcarried = NULL;
1205 GameRules_scoring_vip(flag.owner, false);
1207 if(flag.speedrunning)
1208 ctf_FakeTimeLimit(flag.owner, -1);
1211 if((flag.owner) && (flag.owner.vehicle))
1212 flag.scale = FLAG_SCALE;
1214 if(flag.ctf_status == FLAG_DROPPED)
1215 { WaypointSprite_Kill(flag.wps_flagdropped); }
1218 setattachment(flag, NULL, "");
1219 setorigin(flag, flag.ctf_spawnorigin);
1221 //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1222 set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1223 flag.takedamage = DAMAGE_NO;
1224 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1225 flag.solid = SOLID_TRIGGER;
1226 flag.velocity = '0 0 0';
1227 flag.angles = flag.mangle;
1228 flag.flags = FL_ITEM | FL_NOTARGET;
1230 flag.ctf_status = FLAG_BASE;
1232 flag.pass_distance = 0;
1233 flag.pass_sender = NULL;
1234 flag.pass_target = NULL;
1235 flag.ctf_dropper = NULL;
1236 flag.ctf_pickuptime = 0;
1237 flag.ctf_droptime = 0;
1238 flag.ctf_flagdamaged_byworld = false;
1239 navigation_dynamicgoal_unset(flag);
1241 ctf_CheckStalemate();
1244 void ctf_Reset(entity this)
1246 if(this.owner && IS_PLAYER(this.owner))
1247 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1250 ctf_RespawnFlag(this);
1253 bool ctf_FlagBase_Customize(entity this, entity client)
1255 entity e = WaypointSprite_getviewentity(client);
1256 entity wp_owner = this.owner;
1257 entity flag = e.flagcarried;
1258 if(flag && CTF_SAMETEAM(e, flag))
1260 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1265 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1268 waypoint_spawnforitem_force(this, this.origin);
1269 navigation_dynamicgoal_init(this, true);
1275 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1276 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1277 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1278 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1279 default: basename = WP_FlagBaseNeutral; break;
1282 if(autocvar_g_ctf_flag_waypoint)
1284 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1285 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1286 wp.fade_rate = autocvar_g_ctf_flag_waypoint_maxdistance;
1287 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1288 setcefc(wp, ctf_FlagBase_Customize);
1291 // captureshield setup
1292 ctf_CaptureShield_Spawn(this);
1297 void ctf_FlagSetup(int teamnum, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1300 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1301 ctf_worldflaglist = flag;
1303 setattachment(flag, NULL, "");
1305 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnum), Team_ColorName_Upper(teamnum)));
1306 flag.team = teamnum;
1307 flag.classname = "item_flag_team";
1308 flag.target = "###item###"; // for finding the nearest item using findnearest
1309 flag.flags = FL_ITEM | FL_NOTARGET;
1310 IL_PUSH(g_items, flag);
1311 flag.solid = SOLID_TRIGGER;
1312 flag.takedamage = DAMAGE_NO;
1313 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1314 flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1315 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1316 flag.event_damage = ctf_FlagDamage;
1317 flag.pushable = true;
1318 flag.teleportable = TELEPORT_NORMAL;
1319 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1320 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1321 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1322 if(flag.damagedbycontents)
1323 IL_PUSH(g_damagedbycontents, flag);
1324 flag.velocity = '0 0 0';
1325 flag.mangle = flag.angles;
1326 flag.reset = ctf_Reset;
1327 settouch(flag, ctf_FlagTouch);
1328 setthink(flag, ctf_FlagThink);
1329 flag.nextthink = time + FLAG_THINKRATE;
1330 flag.ctf_status = FLAG_BASE;
1332 // crudely force them all to 0
1333 if(autocvar_g_ctf_score_ignore_fields)
1334 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1336 string teamname = Static_Team_ColorName_Lower(teamnum);
1338 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1339 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1340 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1341 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnum).eent_eff_name; }
1342 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnum).eent_eff_name; }
1343 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnum).eent_eff_name; }
1347 if(flag.s == "") flag.s = b; \
1348 precache_sound(flag.s);
1350 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnum))))
1351 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnum))))
1352 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnum))))
1353 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnum))))
1354 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1355 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1356 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1360 precache_model(flag.model);
1363 _setmodel(flag, flag.model); // precision set below
1364 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1365 flag.m_mins = flag.mins; // store these for squash checks
1366 flag.m_maxs = flag.maxs;
1367 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1369 if(autocvar_g_ctf_flag_glowtrails)
1373 case NUM_TEAM_1: flag.glow_color = 251; break;
1374 case NUM_TEAM_2: flag.glow_color = 210; break;
1375 case NUM_TEAM_3: flag.glow_color = 110; break;
1376 case NUM_TEAM_4: flag.glow_color = 145; break;
1377 default: flag.glow_color = 254; break;
1379 flag.glow_size = 25;
1380 flag.glow_trail = 1;
1383 flag.effects |= EF_LOWPRECISION;
1384 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1385 if(autocvar_g_ctf_dynamiclights)
1389 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1390 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1391 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1392 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1393 default: flag.effects |= EF_DIMLIGHT; break;
1398 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1400 flag.dropped_origin = flag.origin;
1401 flag.noalign = true;
1402 set_movetype(flag, MOVETYPE_NONE);
1404 else // drop to floor, automatically find a platform and set that as spawn origin
1406 flag.noalign = false;
1408 set_movetype(flag, MOVETYPE_NONE);
1411 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1419 // NOTE: LEGACY CODE, needs to be re-written!
1421 void havocbot_ctf_calculate_middlepoint()
1425 vector fo = '0 0 0';
1428 f = ctf_worldflaglist;
1433 f = f.ctf_worldflagnext;
1439 havocbot_middlepoint = s / n;
1440 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1442 havocbot_symmetry_axis_m = 0;
1443 havocbot_symmetry_axis_q = 0;
1446 // for symmetrical editing of waypoints
1447 entity f1 = ctf_worldflaglist;
1448 entity f2 = f1.ctf_worldflagnext;
1449 float m = -(f1.origin.y - f2.origin.y) / (max(f1.origin.x - f2.origin.x, FLOAT_EPSILON));
1450 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1451 havocbot_symmetry_axis_m = m;
1452 havocbot_symmetry_axis_q = q;
1454 havocbot_symmetry_origin_order = n;
1458 entity havocbot_ctf_find_flag(entity bot)
1461 f = ctf_worldflaglist;
1464 if (CTF_SAMETEAM(bot, f))
1466 f = f.ctf_worldflagnext;
1471 entity havocbot_ctf_find_enemy_flag(entity bot)
1474 f = ctf_worldflaglist;
1479 if(CTF_DIFFTEAM(bot, f))
1486 else if(!bot.flagcarried)
1490 else if (CTF_DIFFTEAM(bot, f))
1492 f = f.ctf_worldflagnext;
1497 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1504 FOREACH_CLIENT(IS_PLAYER(it), {
1505 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1508 if(vdist(it.origin - org, <, tc_radius))
1517 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1520 head = ctf_worldflaglist;
1523 if (CTF_SAMETEAM(this, head))
1525 head = head.ctf_worldflagnext;
1528 navigation_routerating(this, head, ratingscale, 10000);
1532 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1535 head = ctf_worldflaglist;
1538 if (CTF_SAMETEAM(this, head))
1540 if (this.flagcarried)
1541 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1543 head = head.ctf_worldflagnext; // skip base if it has a different group
1548 head = head.ctf_worldflagnext;
1553 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1556 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1559 head = ctf_worldflaglist;
1564 if(CTF_DIFFTEAM(this, head))
1568 if(this.flagcarried)
1571 else if(!this.flagcarried)
1575 else if(CTF_DIFFTEAM(this, head))
1577 head = head.ctf_worldflagnext;
1581 if (head.ctf_status == FLAG_CARRY)
1583 // adjust rating of our flag carrier depending on his health
1584 head = head.tag_entity;
1585 float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1586 ratingscale += ratingscale * f * 0.1;
1588 navigation_routerating(this, head, ratingscale, 10000);
1592 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1594 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1596 if (!bot_waypoints_for_items)
1598 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1604 head = havocbot_ctf_find_enemy_flag(this);
1609 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1612 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1616 mf = havocbot_ctf_find_flag(this);
1618 if(mf.ctf_status == FLAG_BASE)
1622 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1625 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1628 head = ctf_worldflaglist;
1631 // flag is out in the field
1632 if(head.ctf_status != FLAG_BASE)
1633 if(head.tag_entity==NULL) // dropped
1637 if(vdist(org - head.origin, <, df_radius))
1638 navigation_routerating(this, head, ratingscale, 10000);
1641 navigation_routerating(this, head, ratingscale, 10000);
1644 head = head.ctf_worldflagnext;
1648 void havocbot_ctf_reset_role(entity this)
1650 float cdefense, cmiddle, coffense;
1657 if (this.flagcarried)
1659 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1663 mf = havocbot_ctf_find_flag(this);
1664 ef = havocbot_ctf_find_enemy_flag(this);
1666 // Retrieve stolen flag
1667 if(mf.ctf_status!=FLAG_BASE)
1669 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1673 // If enemy flag is taken go to the middle to intercept pursuers
1674 if(ef.ctf_status!=FLAG_BASE)
1676 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1680 // if there is no one else on the team switch to offense
1682 // don't check if this bot is a player since it isn't true when the bot is added to the server
1683 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1687 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1690 else if (time < CS(this).jointime + 1)
1692 // if bots spawn all at once set good default roles
1695 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1698 else if (count == 2)
1700 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1705 // Evaluate best position to take
1706 // Count mates on middle position
1707 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1709 // Count mates on defense position
1710 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1712 // Count mates on offense position
1713 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1715 if(cdefense<=coffense)
1716 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1717 else if(coffense<=cmiddle)
1718 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1720 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1722 // if bots spawn all at once assign them a more appropriated role after a while
1723 if (time < CS(this).jointime + 1 && count > 2)
1724 this.havocbot_role_timeout = time + 10 + random() * 10;
1727 bool havocbot_ctf_is_basewaypoint(entity item)
1729 if (item.classname != "waypoint")
1732 entity head = ctf_worldflaglist;
1735 if (item == head.bot_basewaypoint)
1737 head = head.ctf_worldflagnext;
1742 void havocbot_role_ctf_carrier(entity this)
1746 havocbot_ctf_reset_role(this);
1750 if (this.flagcarried == NULL)
1752 havocbot_ctf_reset_role(this);
1756 if (navigation_goalrating_timeout(this))
1758 navigation_goalrating_start(this);
1761 entity mf = havocbot_ctf_find_flag(this);
1762 vector base_org = mf.dropped_origin;
1763 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1765 havocbot_goalrating_ctf_enemybase(this, base_rating);
1767 havocbot_goalrating_ctf_ourbase(this, base_rating);
1769 // start collecting items very close to the bot but only inside of own base radius
1770 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1771 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1773 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1775 navigation_goalrating_end(this);
1777 navigation_goalrating_timeout_set(this);
1779 entity goal = this.goalentity;
1780 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1781 this.goalentity_lock_timeout = time + ((this.bot_aimtarg) ? 2 : 3);
1784 this.havocbot_cantfindflag = time + 10;
1785 else if (time > this.havocbot_cantfindflag)
1787 // Can't navigate to my own base, suicide!
1788 // TODO: drop it and wander around
1789 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1795 void havocbot_role_ctf_escort(entity this)
1801 havocbot_ctf_reset_role(this);
1805 if (this.flagcarried)
1807 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1811 // If enemy flag is back on the base switch to previous role
1812 ef = havocbot_ctf_find_enemy_flag(this);
1813 if(ef.ctf_status==FLAG_BASE)
1815 this.havocbot_role = this.havocbot_previous_role;
1816 this.havocbot_role_timeout = 0;
1819 if (ef.ctf_status == FLAG_DROPPED)
1821 navigation_goalrating_timeout_expire(this, 1);
1825 // If the flag carrier reached the base switch to defense
1826 mf = havocbot_ctf_find_flag(this);
1827 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1829 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1833 // Set the role timeout if necessary
1834 if (!this.havocbot_role_timeout)
1836 this.havocbot_role_timeout = time + random() * 30 + 60;
1839 // If nothing happened just switch to previous role
1840 if (time > this.havocbot_role_timeout)
1842 this.havocbot_role = this.havocbot_previous_role;
1843 this.havocbot_role_timeout = 0;
1847 // Chase the flag carrier
1848 if (navigation_goalrating_timeout(this))
1850 navigation_goalrating_start(this);
1853 havocbot_goalrating_ctf_enemyflag(this, 10000);
1854 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1855 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1857 navigation_goalrating_end(this);
1859 navigation_goalrating_timeout_set(this);
1863 void havocbot_role_ctf_offense(entity this)
1870 havocbot_ctf_reset_role(this);
1874 if (this.flagcarried)
1876 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1881 mf = havocbot_ctf_find_flag(this);
1882 ef = havocbot_ctf_find_enemy_flag(this);
1885 if(mf.ctf_status!=FLAG_BASE)
1888 pos = mf.tag_entity.origin;
1892 // Try to get it if closer than the enemy base
1893 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1895 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1900 // Escort flag carrier
1901 if(ef.ctf_status!=FLAG_BASE)
1904 pos = ef.tag_entity.origin;
1908 if(vdist(pos - mf.dropped_origin, >, 700))
1910 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1915 // Set the role timeout if necessary
1916 if (!this.havocbot_role_timeout)
1917 this.havocbot_role_timeout = time + 120;
1919 if (time > this.havocbot_role_timeout)
1921 havocbot_ctf_reset_role(this);
1925 if (navigation_goalrating_timeout(this))
1927 navigation_goalrating_start(this);
1930 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1931 havocbot_goalrating_ctf_enemybase(this, 10000);
1932 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1934 navigation_goalrating_end(this);
1936 navigation_goalrating_timeout_set(this);
1940 // Retriever (temporary role):
1941 void havocbot_role_ctf_retriever(entity this)
1947 havocbot_ctf_reset_role(this);
1951 if (this.flagcarried)
1953 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1957 // If flag is back on the base switch to previous role
1958 mf = havocbot_ctf_find_flag(this);
1959 if(mf.ctf_status==FLAG_BASE)
1961 if (mf.enemy == this) // did this bot return the flag?
1962 navigation_goalrating_timeout_force(this);
1963 havocbot_ctf_reset_role(this);
1967 if (!this.havocbot_role_timeout)
1968 this.havocbot_role_timeout = time + 20;
1970 if (time > this.havocbot_role_timeout)
1972 havocbot_ctf_reset_role(this);
1976 if (navigation_goalrating_timeout(this))
1978 const float RT_RADIUS = 10000;
1980 navigation_goalrating_start(this);
1983 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1984 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1985 havocbot_goalrating_ctf_enemybase(this, 8000);
1986 entity ef = havocbot_ctf_find_enemy_flag(this);
1987 vector enemy_base_org = ef.dropped_origin;
1988 // start collecting items very close to the bot but only inside of enemy base radius
1989 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1990 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1991 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1993 navigation_goalrating_end(this);
1995 navigation_goalrating_timeout_set(this);
1999 void havocbot_role_ctf_middle(entity this)
2005 havocbot_ctf_reset_role(this);
2009 if (this.flagcarried)
2011 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2015 mf = havocbot_ctf_find_flag(this);
2016 if(mf.ctf_status!=FLAG_BASE)
2018 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2022 if (!this.havocbot_role_timeout)
2023 this.havocbot_role_timeout = time + 10;
2025 if (time > this.havocbot_role_timeout)
2027 havocbot_ctf_reset_role(this);
2031 if (navigation_goalrating_timeout(this))
2035 org = havocbot_middlepoint;
2036 org.z = this.origin.z;
2038 navigation_goalrating_start(this);
2041 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
2042 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
2043 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2044 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2045 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2046 havocbot_goalrating_ctf_enemybase(this, 3000);
2048 navigation_goalrating_end(this);
2050 entity goal = this.goalentity;
2051 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
2052 this.goalentity_lock_timeout = time + 2;
2054 navigation_goalrating_timeout_set(this);
2058 void havocbot_role_ctf_defense(entity this)
2064 havocbot_ctf_reset_role(this);
2068 if (this.flagcarried)
2070 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2074 // If own flag was captured
2075 mf = havocbot_ctf_find_flag(this);
2076 if(mf.ctf_status!=FLAG_BASE)
2078 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2082 if (!this.havocbot_role_timeout)
2083 this.havocbot_role_timeout = time + 30;
2085 if (time > this.havocbot_role_timeout)
2087 havocbot_ctf_reset_role(this);
2090 if (navigation_goalrating_timeout(this))
2092 vector org = mf.dropped_origin;
2094 navigation_goalrating_start(this);
2096 // if enemies are closer to our base, go there
2097 entity closestplayer = NULL;
2098 float distance, bestdistance = 10000;
2099 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2100 distance = vlen(org - it.origin);
2101 if(distance<bestdistance)
2104 bestdistance = distance;
2110 if(DIFF_TEAM(closestplayer, this))
2111 if(vdist(org - this.origin, >, 1000))
2112 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2113 havocbot_goalrating_ctf_ourbase(this, 10000);
2115 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2116 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2117 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2118 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2119 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2121 navigation_goalrating_end(this);
2123 navigation_goalrating_timeout_set(this);
2127 void havocbot_role_ctf_setrole(entity bot, int role)
2129 string s = "(null)";
2132 case HAVOCBOT_CTF_ROLE_CARRIER:
2134 bot.havocbot_role = havocbot_role_ctf_carrier;
2135 bot.havocbot_role_timeout = 0;
2136 bot.havocbot_cantfindflag = time + 10;
2137 if (bot.havocbot_previous_role != bot.havocbot_role)
2138 navigation_goalrating_timeout_force(bot);
2140 case HAVOCBOT_CTF_ROLE_DEFENSE:
2142 bot.havocbot_role = havocbot_role_ctf_defense;
2143 bot.havocbot_role_timeout = 0;
2145 case HAVOCBOT_CTF_ROLE_MIDDLE:
2147 bot.havocbot_role = havocbot_role_ctf_middle;
2148 bot.havocbot_role_timeout = 0;
2150 case HAVOCBOT_CTF_ROLE_OFFENSE:
2152 bot.havocbot_role = havocbot_role_ctf_offense;
2153 bot.havocbot_role_timeout = 0;
2155 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2157 bot.havocbot_previous_role = bot.havocbot_role;
2158 bot.havocbot_role = havocbot_role_ctf_retriever;
2159 bot.havocbot_role_timeout = time + 10;
2160 if (bot.havocbot_previous_role != bot.havocbot_role)
2161 navigation_goalrating_timeout_expire(bot, 2);
2163 case HAVOCBOT_CTF_ROLE_ESCORT:
2165 bot.havocbot_previous_role = bot.havocbot_role;
2166 bot.havocbot_role = havocbot_role_ctf_escort;
2167 bot.havocbot_role_timeout = time + 30;
2168 if (bot.havocbot_previous_role != bot.havocbot_role)
2169 navigation_goalrating_timeout_expire(bot, 2);
2172 LOG_TRACE(bot.netname, " switched to ", s);
2180 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2182 entity player = M_ARGV(0, entity);
2184 int t = 0, t2 = 0, t3 = 0;
2185 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)
2187 // initially clear items so they can be set as necessary later.
2188 STAT(OBJECTIVE_STATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2189 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2190 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2191 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2192 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2193 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2195 // scan through all the flags and notify the client about them
2196 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2198 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2199 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2200 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2201 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2202 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; }
2204 switch(flag.ctf_status)
2209 if((flag.owner == player) || (flag.pass_sender == player))
2210 STAT(OBJECTIVE_STATUS, player) |= t; // carrying: player is currently carrying the flag
2212 STAT(OBJECTIVE_STATUS, player) |= t2; // taken: someone else is carrying the flag
2217 STAT(OBJECTIVE_STATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2223 // item for stopping players from capturing the flag too often
2224 if(player.ctf_captureshielded)
2225 STAT(OBJECTIVE_STATUS, player) |= CTF_SHIELDED;
2228 STAT(OBJECTIVE_STATUS, player) |= CTF_STALEMATE;
2230 // update the health of the flag carrier waypointsprite
2231 if(player.wps_flagcarrier)
2232 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);
2235 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in damage.qc
2237 entity frag_attacker = M_ARGV(1, entity);
2238 entity frag_target = M_ARGV(2, entity);
2239 float frag_damage = M_ARGV(4, float);
2240 vector frag_force = M_ARGV(6, vector);
2242 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2244 if(frag_target == frag_attacker) // damage done to yourself
2246 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2247 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2249 else // damage done to everyone else
2251 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2252 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2255 M_ARGV(4, float) = frag_damage;
2256 M_ARGV(6, vector) = frag_force;
2258 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2260 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
2261 && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2263 frag_target.wps_helpme_time = time;
2264 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2266 // todo: add notification for when flag carrier needs help?
2270 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2272 entity frag_attacker = M_ARGV(1, entity);
2273 entity frag_target = M_ARGV(2, entity);
2275 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)))
2277 if(frag_target.flagcarried) {
2278 // Killing an enemy flag carrier
2279 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2280 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2281 } else if(frag_attacker.flagcarried) {
2282 Give_Medal(frag_attacker, DEFENSE);
2285 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2286 if(tmp_entity.ctf_status == FLAG_BASE && CTF_SAMETEAM(tmp_entity, frag_attacker))
2288 if(CTF_IS_NEAR(frag_target, tmp_entity, '1 1 1' * 1500))
2290 Give_Medal(frag_attacker, DEFENSE);
2297 if(frag_target.flagcarried)
2299 entity tmp_entity = frag_target.flagcarried;
2300 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2301 tmp_entity.ctf_dropper = NULL;
2306 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2308 M_ARGV(2, float) = 0; // frag score
2309 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2312 void ctf_RemovePlayer(entity player)
2314 if(player.flagcarried)
2315 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2317 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2319 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2320 if(flag.pass_target == player) { flag.pass_target = NULL; }
2321 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2325 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2327 entity player = M_ARGV(0, entity);
2329 ctf_RemovePlayer(player);
2332 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2334 entity player = M_ARGV(0, entity);
2336 ctf_RemovePlayer(player);
2339 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2341 if(!autocvar_g_ctf_leaderboard)
2344 entity player = M_ARGV(0, entity);
2346 race_SendAll(player, true);
2349 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2351 if(!autocvar_g_ctf_leaderboard)
2354 entity player = M_ARGV(0, entity);
2356 race_checkAndWriteName(player);
2359 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2361 entity player = M_ARGV(0, entity);
2363 if(player.flagcarried)
2364 if(!autocvar_g_ctf_portalteleport)
2365 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2368 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2370 if(MUTATOR_RETURNVALUE || game_stopped) return;
2372 entity player = M_ARGV(0, entity);
2374 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2376 // pass the flag to a team mate
2377 if(autocvar_g_ctf_pass)
2379 entity head, closest_target = NULL;
2380 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2382 while(head) // find the closest acceptable target to pass to
2384 if(IS_PLAYER(head) && !IS_DEAD(head))
2385 if(head != player && SAME_TEAM(head, player))
2386 if(!head.speedrunning && !head.vehicle)
2388 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in damage.qc)
2389 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2390 vector passer_center = CENTER_OR_VIEWOFS(player);
2392 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2394 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2396 if(IS_BOT_CLIENT(head))
2398 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2399 ctf_Handle_Throw(head, player, DROP_PASS);
2403 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2404 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2406 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2409 else if(player.flagcarried && !head.flagcarried)
2413 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2414 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2415 { closest_target = head; }
2417 else { closest_target = head; }
2424 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2427 // throw the flag in front of you
2428 if(autocvar_g_ctf_throw && player.flagcarried)
2430 if(player.throw_count == -1)
2432 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2434 player.throw_prevtime = time;
2435 player.throw_count = 1;
2436 ctf_Handle_Throw(player, NULL, DROP_THROW);
2441 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2447 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2448 else { player.throw_count += 1; }
2449 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2451 player.throw_prevtime = time;
2452 ctf_Handle_Throw(player, NULL, DROP_THROW);
2459 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2461 entity player = M_ARGV(0, entity);
2463 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2465 player.wps_helpme_time = time;
2466 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2468 else // create a normal help me waypointsprite
2470 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2471 WaypointSprite_Ping(player.wps_helpme);
2477 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2479 entity player = M_ARGV(0, entity);
2480 entity veh = M_ARGV(1, entity);
2482 if(player.flagcarried)
2484 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2486 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2490 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2491 setattachment(player.flagcarried, veh, "");
2492 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2493 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2494 //player.flagcarried.angles = '0 0 0';
2500 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2502 entity player = M_ARGV(0, entity);
2504 if(player.flagcarried)
2506 setattachment(player.flagcarried, player, "");
2507 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2508 player.flagcarried.scale = FLAG_SCALE;
2509 player.flagcarried.angles = '0 0 0';
2510 player.flagcarried.nodrawtoclient = NULL;
2515 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2517 entity player = M_ARGV(0, entity);
2519 if(player.flagcarried)
2521 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2522 ctf_RespawnFlag(player.flagcarried);
2527 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2529 entity flag; // temporary entity for the search method
2531 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2533 switch(flag.ctf_status)
2538 // lock the flag, game is over
2539 set_movetype(flag, MOVETYPE_NONE);
2540 flag.takedamage = DAMAGE_NO;
2541 flag.solid = SOLID_NOT;
2542 flag.nextthink = false; // stop thinking
2544 //dprint("stopping the ", flag.netname, " from moving.\n");
2552 // do nothing for these flags
2559 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2561 entity bot = M_ARGV(0, entity);
2563 havocbot_ctf_reset_role(bot);
2567 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2569 M_ARGV(1, string) = "ctf_team";
2572 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2574 int record_page = M_ARGV(0, int);
2575 string ret_string = M_ARGV(1, string);
2577 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2579 if (MapInfo_Get_ByID(i))
2581 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2587 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2588 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2592 M_ARGV(1, string) = ret_string;
2595 bool superspec_Spectate(entity this, entity targ); // TODO
2596 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2597 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2599 entity player = M_ARGV(0, entity);
2600 string cmd_name = M_ARGV(1, string);
2601 int cmd_argc = M_ARGV(2, int);
2603 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2605 if(cmd_name == "followfc")
2617 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2618 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2619 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2620 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2624 FOREACH_CLIENT(IS_PLAYER(it), {
2625 if(it.flagcarried && (it.team == _team || _team == 0))
2628 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2629 continue; // already spectating this fc, try another
2630 return superspec_Spectate(player, it);
2635 superspec_msg("", "", player, "No active flag carrier\n", 1);
2640 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2642 entity frag_target = M_ARGV(0, entity);
2644 if(frag_target.flagcarried)
2645 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2648 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2650 entity player = M_ARGV(0, entity);
2651 if(player.flagcarried)
2652 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2660 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2661 CTF flag for team one (Red).
2663 "angle" Angle the flag will point (minus 90 degrees)...
2664 "model" model to use, note this needs red and blue as skins 0 and 1...
2665 "noise" sound played when flag is picked up...
2666 "noise1" sound played when flag is returned by a teammate...
2667 "noise2" sound played when flag is captured...
2668 "noise3" sound played when flag is lost in the field and respawns itself...
2669 "noise4" sound played when flag is dropped by a player...
2670 "noise5" sound played when flag touches the ground... */
2671 spawnfunc(item_flag_team1)
2673 if(!g_ctf) { delete(this); return; }
2675 ctf_FlagSetup(NUM_TEAM_1, this);
2678 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2679 CTF flag for team two (Blue).
2681 "angle" Angle the flag will point (minus 90 degrees)...
2682 "model" model to use, note this needs red and blue as skins 0 and 1...
2683 "noise" sound played when flag is picked up...
2684 "noise1" sound played when flag is returned by a teammate...
2685 "noise2" sound played when flag is captured...
2686 "noise3" sound played when flag is lost in the field and respawns itself...
2687 "noise4" sound played when flag is dropped by a player...
2688 "noise5" sound played when flag touches the ground... */
2689 spawnfunc(item_flag_team2)
2691 if(!g_ctf) { delete(this); return; }
2693 ctf_FlagSetup(NUM_TEAM_2, this);
2696 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2697 CTF flag for team three (Yellow).
2699 "angle" Angle the flag will point (minus 90 degrees)...
2700 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2701 "noise" sound played when flag is picked up...
2702 "noise1" sound played when flag is returned by a teammate...
2703 "noise2" sound played when flag is captured...
2704 "noise3" sound played when flag is lost in the field and respawns itself...
2705 "noise4" sound played when flag is dropped by a player...
2706 "noise5" sound played when flag touches the ground... */
2707 spawnfunc(item_flag_team3)
2709 if(!g_ctf) { delete(this); return; }
2711 ctf_FlagSetup(NUM_TEAM_3, this);
2714 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2715 CTF flag for team four (Pink).
2717 "angle" Angle the flag will point (minus 90 degrees)...
2718 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2719 "noise" sound played when flag is picked up...
2720 "noise1" sound played when flag is returned by a teammate...
2721 "noise2" sound played when flag is captured...
2722 "noise3" sound played when flag is lost in the field and respawns itself...
2723 "noise4" sound played when flag is dropped by a player...
2724 "noise5" sound played when flag touches the ground... */
2725 spawnfunc(item_flag_team4)
2727 if(!g_ctf) { delete(this); return; }
2729 ctf_FlagSetup(NUM_TEAM_4, this);
2732 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2735 "angle" Angle the flag will point (minus 90 degrees)...
2736 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2737 "noise" sound played when flag is picked up...
2738 "noise1" sound played when flag is returned by a teammate...
2739 "noise2" sound played when flag is captured...
2740 "noise3" sound played when flag is lost in the field and respawns itself...
2741 "noise4" sound played when flag is dropped by a player...
2742 "noise5" sound played when flag touches the ground... */
2743 spawnfunc(item_flag_neutral)
2745 if(!g_ctf) { delete(this); return; }
2746 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2748 ctf_FlagSetup(0, this);
2751 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2752 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2753 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.
2755 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2756 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2759 if(!g_ctf) { delete(this); return; }
2761 this.team = this.cnt + 1;
2764 // compatibility for quake maps
2765 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2766 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2767 spawnfunc(info_player_team1);
2768 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2769 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2770 spawnfunc(info_player_team2);
2771 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2772 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2774 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2775 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2777 // compatibility for wop maps
2778 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2779 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2780 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2781 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2782 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2783 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2791 void ctf_ScoreRules(int teams)
2793 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2794 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2795 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2796 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2797 field(SP_CTF_PICKUPS, "pickups", 0);
2798 field(SP_CTF_FCKILLS, "fckills", 0);
2799 field(SP_CTF_RETURNS, "returns", 0);
2800 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2804 // code from here on is just to support maps that don't have flag and team entities
2805 void ctf_SpawnTeam (string teamname, int teamcolor)
2807 entity this = new_pure(ctf_team);
2808 this.netname = teamname;
2809 this.cnt = teamcolor - 1;
2810 this.spawnfunc_checked = true;
2811 this.team = teamcolor;
2814 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2819 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2821 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2822 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2824 switch(tmp_entity.team)
2826 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2827 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2828 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2829 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2831 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2834 havocbot_ctf_calculate_middlepoint();
2836 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2838 ctf_teams = 0; // so set the default red and blue teams
2839 BITSET_ASSIGN(ctf_teams, BIT(0));
2840 BITSET_ASSIGN(ctf_teams, BIT(1));
2843 //ctf_teams = bound(2, ctf_teams, 4);
2845 // if no teams are found, spawn defaults
2846 if(find(NULL, classname, "ctf_team") == NULL)
2848 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2849 if(ctf_teams & BIT(0))
2850 ctf_SpawnTeam("Red", NUM_TEAM_1);
2851 if(ctf_teams & BIT(1))
2852 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2853 if(ctf_teams & BIT(2))
2854 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2855 if(ctf_teams & BIT(3))
2856 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2859 ctf_ScoreRules(ctf_teams);
2862 void ctf_Initialize()
2864 CTF_FLAG = NEW(Flag);
2865 record_type = CTF_RECORD;
2866 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2868 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2869 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2870 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2872 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);