3 #include <common/effects/all.qh>
4 #include <common/vehicles/all.qh>
5 #include <server/gamelog.qh>
6 #include <server/teamplay.qh>
8 #include <lib/warpzone/common.qh>
10 bool autocvar_g_ctf_allow_vehicle_carry;
11 bool autocvar_g_ctf_allow_vehicle_touch;
12 bool autocvar_g_ctf_allow_monster_touch;
13 bool autocvar_g_ctf_throw;
14 float autocvar_g_ctf_throw_angle_max;
15 float autocvar_g_ctf_throw_angle_min;
16 int autocvar_g_ctf_throw_punish_count;
17 float autocvar_g_ctf_throw_punish_delay;
18 float autocvar_g_ctf_throw_punish_time;
19 float autocvar_g_ctf_throw_strengthmultiplier;
20 float autocvar_g_ctf_throw_velocity_forward;
21 float autocvar_g_ctf_throw_velocity_up;
22 float autocvar_g_ctf_drop_velocity_up;
23 float autocvar_g_ctf_drop_velocity_side;
24 bool autocvar_g_ctf_oneflag_reverse;
25 bool autocvar_g_ctf_portalteleport;
26 bool autocvar_g_ctf_pass;
27 float autocvar_g_ctf_pass_arc;
28 float autocvar_g_ctf_pass_arc_max;
29 float autocvar_g_ctf_pass_directional_max;
30 float autocvar_g_ctf_pass_directional_min;
31 float autocvar_g_ctf_pass_radius;
32 float autocvar_g_ctf_pass_wait;
33 bool autocvar_g_ctf_pass_request;
34 float autocvar_g_ctf_pass_turnrate;
35 float autocvar_g_ctf_pass_timelimit;
36 float autocvar_g_ctf_pass_velocity;
37 bool autocvar_g_ctf_dynamiclights;
38 float autocvar_g_ctf_flag_collect_delay;
39 float autocvar_g_ctf_flag_damageforcescale;
40 bool autocvar_g_ctf_flag_dropped_waypoint;
41 bool autocvar_g_ctf_flag_dropped_floatinwater;
42 bool autocvar_g_ctf_flag_glowtrails;
43 int autocvar_g_ctf_flag_health;
44 bool autocvar_g_ctf_flag_return;
45 bool autocvar_g_ctf_flag_return_carrying;
46 float autocvar_g_ctf_flag_return_carried_radius;
47 float autocvar_g_ctf_flag_return_time;
48 bool autocvar_g_ctf_flag_return_when_unreachable;
49 float autocvar_g_ctf_flag_return_damage;
50 float autocvar_g_ctf_flag_return_damage_delay;
51 float autocvar_g_ctf_flag_return_dropped;
52 bool autocvar_g_ctf_flag_waypoint = true;
53 float autocvar_g_ctf_flag_waypoint_maxdistance;
54 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
55 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
56 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
57 float autocvar_g_ctf_flagcarrier_selfforcefactor;
58 float autocvar_g_ctf_flagcarrier_damagefactor;
59 float autocvar_g_ctf_flagcarrier_forcefactor;
60 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
61 bool autocvar_g_ctf_fullbrightflags;
62 bool autocvar_g_ctf_ignore_frags;
63 bool autocvar_g_ctf_score_ignore_fields;
64 int autocvar_g_ctf_score_capture;
65 int autocvar_g_ctf_score_capture_assist;
66 int autocvar_g_ctf_score_kill;
67 int autocvar_g_ctf_score_penalty_drop;
68 int autocvar_g_ctf_score_penalty_returned;
69 int autocvar_g_ctf_score_pickup_base;
70 int autocvar_g_ctf_score_pickup_dropped_early;
71 int autocvar_g_ctf_score_pickup_dropped_late;
72 int autocvar_g_ctf_score_return;
73 float autocvar_g_ctf_shield_force;
74 float autocvar_g_ctf_shield_max_ratio;
75 int autocvar_g_ctf_shield_min_negscore;
76 bool autocvar_g_ctf_stalemate;
77 int autocvar_g_ctf_stalemate_endcondition;
78 float autocvar_g_ctf_stalemate_time;
79 bool autocvar_g_ctf_reverse;
80 float autocvar_g_ctf_dropped_capture_delay;
81 float autocvar_g_ctf_dropped_capture_radius;
83 void ctf_FakeTimeLimit(entity e, float t)
86 WriteByte(MSG_ONE, 3); // svc_updatestat
87 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
89 WriteCoord(MSG_ONE, autocvar_timelimit);
91 WriteCoord(MSG_ONE, (t + 1) / 60);
94 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
96 if(autocvar_sv_eventlog)
97 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != NULL) ? ftos(actor.playerid) : "")));
98 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
101 void ctf_CaptureRecord(entity flag, entity player)
103 float cap_record = ctf_captimerecord;
104 float cap_time = (time - flag.ctf_pickuptime);
105 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
109 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname);
110 else if(!ctf_captimerecord)
111 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_TIME), player.netname, TIME_ENCODE(cap_time));
112 else if(cap_time < cap_record)
113 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));
115 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));
117 // write that shit in the database
118 if(!ctf_oneflag) // but not in 1-flag mode
119 if((!ctf_captimerecord) || (cap_time < cap_record))
121 ctf_captimerecord = cap_time;
122 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
123 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
124 write_recordmarker(player, flag.ctf_pickuptime, cap_time);
127 if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
128 race_setTime(GetMapname(), TIME_ENCODE(cap_time), player.crypto_idfp, player.netname, player, false);
131 bool ctf_Immediate_Return_Allowed(entity flag, entity toucher)
134 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), { ++num_perteam; });
136 // automatically return if there's only 1 player on the team
137 return ((autocvar_g_ctf_flag_return || num_perteam <= 1 || (autocvar_g_ctf_flag_return_carrying && toucher.flagcarried))
141 bool ctf_Return_Customize(entity this, entity client)
143 // only to the carrier
144 return boolean(client == this.owner);
147 void ctf_FlagcarrierWaypoints(entity player)
149 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
150 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, 2 * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
151 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);
152 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
154 if(player.flagcarried && CTF_SAMETEAM(player, player.flagcarried))
156 if(!player.wps_enemyflagcarrier)
158 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, 0, player, wps_enemyflagcarrier, true, RADARICON_FLAG);
159 wp.colormod = WPCOLOR_ENEMYFC(player.team);
160 setcefc(wp, ctf_Stalemate_Customize);
162 if(IS_REAL_CLIENT(player) && !ctf_stalemate)
163 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_VISIBLE);
166 if(!player.wps_flagreturn)
168 entity owp = WaypointSprite_SpawnFixed(WP_FlagReturn, player.flagcarried.ctf_spawnorigin + FLAG_WAYPOINT_OFFSET, player, wps_flagreturn, RADARICON_FLAG);
169 owp.colormod = '0 0.8 0.8';
170 //WaypointSprite_UpdateTeamRadar(player.wps_flagreturn, RADARICON_FLAG, ((player.team) ? colormapPaletteColor(player.team - 1, false) : '1 1 1'));
171 setcefc(owp, ctf_Return_Customize);
176 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
178 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
179 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
180 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
181 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
184 if(current_height) // make sure we can actually do this arcing path
186 targpos = (to + ('0 0 1' * current_height));
187 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
188 if(trace_fraction < 1)
190 //print("normal arc line failed, trying to find new pos...");
191 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
192 targpos = (trace_endpos + eZ * FLAG_PASS_ARC_OFFSET_Z);
193 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
194 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
195 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
198 else { targpos = to; }
200 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
202 vector desired_direction = normalize(targpos - from);
203 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
204 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
207 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
209 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
211 // directional tracing only
213 makevectors(passer_angle);
215 // find the closest point on the enemy to the center of the attack
216 float h; // hypotenuse, which is the distance between attacker to head
217 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
219 h = vlen(head_center - passer_center);
220 a = h * (normalize(head_center - passer_center) * v_forward);
222 vector nearest_on_line = (passer_center + a * v_forward);
223 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
225 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
226 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
228 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
233 else { return true; }
237 // =======================
238 // CaptureShield Functions
239 // =======================
241 bool ctf_CaptureShield_CheckStatus(entity p)
243 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
244 int players_worseeq, players_total;
246 if(ctf_captureshield_max_ratio <= 0)
249 s = GameRules_scoring_add(p, CTF_CAPS, 0);
250 s2 = GameRules_scoring_add(p, CTF_PICKUPS, 0);
251 s3 = GameRules_scoring_add(p, CTF_RETURNS, 0);
252 s4 = GameRules_scoring_add(p, CTF_FCKILLS, 0);
254 sr = ((s - s2) + (s3 + s4));
256 if(sr >= -ctf_captureshield_min_negscore)
259 players_total = players_worseeq = 0;
260 FOREACH_CLIENT(IS_PLAYER(it), {
263 se = GameRules_scoring_add(it, CTF_CAPS, 0);
264 se2 = GameRules_scoring_add(it, CTF_PICKUPS, 0);
265 se3 = GameRules_scoring_add(it, CTF_RETURNS, 0);
266 se4 = GameRules_scoring_add(it, CTF_FCKILLS, 0);
268 ser = ((se - se2) + (se3 + se4));
275 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
276 // use this rule here
278 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
284 void ctf_CaptureShield_Update(entity player, bool wanted_status)
286 bool updated_status = ctf_CaptureShield_CheckStatus(player);
287 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
289 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
290 player.ctf_captureshielded = updated_status;
294 bool ctf_CaptureShield_Customize(entity this, entity client)
296 if(!client.ctf_captureshielded) { return false; }
297 if(CTF_SAMETEAM(this, client)) { return false; }
302 void ctf_CaptureShield_Touch(entity this, entity toucher)
304 if(!toucher.ctf_captureshielded) { return; }
305 if(CTF_SAMETEAM(this, toucher)) { return; }
307 vector mymid = (this.absmin + this.absmax) * 0.5;
308 vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
310 Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
311 if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
314 void ctf_CaptureShield_Spawn(entity flag)
316 entity shield = new(ctf_captureshield);
319 shield.team = flag.team;
320 settouch(shield, ctf_CaptureShield_Touch);
321 setcefc(shield, ctf_CaptureShield_Customize);
322 shield.effects = EF_ADDITIVE;
323 set_movetype(shield, MOVETYPE_NOCLIP);
324 shield.solid = SOLID_TRIGGER;
325 shield.avelocity = '7 0 11';
328 setorigin(shield, flag.origin);
329 setmodel(shield, MDL_CTF_SHIELD);
330 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
334 // ====================
335 // Drop/Pass/Throw Code
336 // ====================
338 void ctf_Handle_Drop(entity flag, entity player, int droptype)
341 player = (player ? player : flag.pass_sender);
344 set_movetype(flag, MOVETYPE_TOSS);
345 flag.takedamage = DAMAGE_YES;
346 flag.angles = '0 0 0';
347 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
348 flag.ctf_droptime = time;
349 flag.ctf_dropper = player;
350 flag.ctf_status = FLAG_DROPPED;
352 // messages and sounds
353 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
354 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
355 ctf_EventLog("dropped", player.team, player);
358 GameRules_scoring_add_team(player, SCORE, -((flag.score_drop) ? flag.score_drop : autocvar_g_ctf_score_penalty_drop));
359 GameRules_scoring_add(player, CTF_DROPS, 1);
362 if(autocvar_g_ctf_flag_dropped_waypoint) {
363 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);
364 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
367 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
369 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_health);
370 WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH));
373 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
375 if(droptype == DROP_PASS)
377 flag.pass_distance = 0;
378 flag.pass_sender = NULL;
379 flag.pass_target = NULL;
383 void ctf_Handle_Retrieve(entity flag, entity player)
385 entity sender = flag.pass_sender;
387 // transfer flag to player
389 flag.owner.flagcarried = flag;
390 GameRules_scoring_vip(player, true);
395 setattachment(flag, player.vehicle, "");
396 setorigin(flag, VEHICLE_FLAG_OFFSET);
397 flag.scale = VEHICLE_FLAG_SCALE;
401 setattachment(flag, player, "");
402 setorigin(flag, FLAG_CARRY_OFFSET);
404 set_movetype(flag, MOVETYPE_NONE);
405 flag.takedamage = DAMAGE_NO;
406 flag.solid = SOLID_NOT;
407 flag.angles = '0 0 0';
408 flag.ctf_status = FLAG_CARRY;
410 // messages and sounds
411 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
412 ctf_EventLog("receive", flag.team, player);
414 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
416 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
417 else if(it == player)
418 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
419 else if(SAME_TEAM(it, sender))
420 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
423 // create new waypoint
424 ctf_FlagcarrierWaypoints(player);
426 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
427 player.throw_antispam = sender.throw_antispam;
429 flag.pass_distance = 0;
430 flag.pass_sender = NULL;
431 flag.pass_target = NULL;
434 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
436 entity flag = player.flagcarried;
437 vector targ_origin, flag_velocity;
439 if(!flag) { return; }
440 if((droptype == DROP_PASS) && !receiver) { return; }
442 if(flag.speedrunning)
444 // ensure old waypoints are removed before resetting the flag
445 WaypointSprite_Kill(player.wps_flagcarrier);
447 if(player.wps_enemyflagcarrier)
448 WaypointSprite_Kill(player.wps_enemyflagcarrier);
450 if(player.wps_flagreturn)
451 WaypointSprite_Kill(player.wps_flagreturn);
452 ctf_RespawnFlag(flag);
457 setattachment(flag, NULL, "");
458 tracebox(player.origin - FLAG_DROP_OFFSET, flag.m_mins, flag.m_maxs, player.origin + FLAG_DROP_OFFSET, MOVE_NOMONSTERS, flag);
459 setorigin(flag, trace_endpos);
460 flag.owner.flagcarried = NULL;
461 GameRules_scoring_vip(flag.owner, false);
463 flag.solid = SOLID_TRIGGER;
464 flag.ctf_dropper = player;
465 flag.ctf_droptime = time;
467 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
474 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
475 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
476 WarpZone_RefSys_Copy(flag, receiver);
477 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
478 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
480 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
481 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
484 set_movetype(flag, MOVETYPE_FLY);
485 flag.takedamage = DAMAGE_NO;
486 flag.pass_sender = player;
487 flag.pass_target = receiver;
488 flag.ctf_status = FLAG_PASSING;
491 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
492 WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
493 ctf_EventLog("pass", flag.team, player);
499 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'));
501 flag_velocity = (('0 0 1' * autocvar_g_ctf_throw_velocity_up) + ((v_forward * autocvar_g_ctf_throw_velocity_forward) * ((player.items & ITEM_Strength.m_itemid) ? autocvar_g_ctf_throw_strengthmultiplier : 1)));
502 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
503 ctf_Handle_Drop(flag, player, droptype);
504 navigation_dynamicgoal_set(flag, player);
510 flag.velocity = '0 0 0'; // do nothing
517 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);
518 ctf_Handle_Drop(flag, player, droptype);
519 navigation_dynamicgoal_set(flag, player);
524 // kill old waypointsprite
525 WaypointSprite_Ping(player.wps_flagcarrier);
526 WaypointSprite_Kill(player.wps_flagcarrier);
528 if(player.wps_enemyflagcarrier)
529 WaypointSprite_Kill(player.wps_enemyflagcarrier);
531 if(player.wps_flagreturn)
532 WaypointSprite_Kill(player.wps_flagreturn);
535 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
538 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
540 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
547 void nades_GiveBonus(entity player, float score);
549 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
551 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
552 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
553 entity player_team_flag = NULL, tmp_entity;
554 float old_time, new_time;
556 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
557 if(CTF_DIFFTEAM(player, flag)) { return; }
558 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)
560 if (toucher.goalentity == flag.bot_basewaypoint)
561 toucher.goalentity_lock_timeout = 0;
564 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
565 if(SAME_TEAM(tmp_entity, player))
567 player_team_flag = tmp_entity;
571 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
573 player.throw_prevtime = time;
574 player.throw_count = 0;
576 // messages and sounds
577 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
578 ctf_CaptureRecord(enemy_flag, player);
579 _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);
583 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
584 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
590 if(enemy_flag.score_capture || flag.score_capture)
591 pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
592 GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
594 if(enemy_flag.score_team_capture || flag.score_team_capture)
595 capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
596 GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
598 old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
599 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
600 if(!old_time || new_time < old_time)
601 GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
604 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
605 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
608 if(capturetype == CAPTURE_NORMAL)
610 WaypointSprite_Kill(player.wps_flagcarrier);
611 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
613 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
614 { GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist)); }
617 flag.enemy = toucher;
620 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
621 ctf_RespawnFlag(enemy_flag);
624 void ctf_Handle_Return(entity flag, entity player)
626 // messages and sounds
627 if(IS_MONSTER(player))
629 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
633 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
634 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
636 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
637 ctf_EventLog("return", flag.team, player);
640 if(IS_PLAYER(player))
642 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
643 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
645 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
648 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
652 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
653 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
654 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
658 if(player.flagcarried == flag)
659 WaypointSprite_Kill(player.wps_flagcarrier);
664 ctf_RespawnFlag(flag);
667 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
670 float pickup_dropped_score; // used to calculate dropped pickup score
672 // attach the flag to the player
674 player.flagcarried = flag;
675 GameRules_scoring_vip(player, true);
678 setattachment(flag, player.vehicle, "");
679 setorigin(flag, VEHICLE_FLAG_OFFSET);
680 flag.scale = VEHICLE_FLAG_SCALE;
684 setattachment(flag, player, "");
685 setorigin(flag, FLAG_CARRY_OFFSET);
689 set_movetype(flag, MOVETYPE_NONE);
690 flag.takedamage = DAMAGE_NO;
691 flag.solid = SOLID_NOT;
692 flag.angles = '0 0 0';
693 flag.ctf_status = FLAG_CARRY;
697 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
698 case PICKUP_DROPPED: SetResourceExplicit(flag, RES_HEALTH, flag.max_health); break; // reset health/return timelimit
702 // messages and sounds
703 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
705 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
707 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
708 else if(CTF_DIFFTEAM(player, flag))
709 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
711 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
713 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
716 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); });
719 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
720 if(CTF_SAMETEAM(flag, it))
722 if(SAME_TEAM(player, it))
723 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
725 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);
729 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
732 GameRules_scoring_add(player, CTF_PICKUPS, 1);
733 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
738 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
739 ctf_EventLog("steal", flag.team, player);
745 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);
746 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);
747 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
748 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
749 ctf_EventLog("pickup", flag.team, player);
757 if(pickuptype == PICKUP_BASE)
759 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
760 if((player.speedrunning) && (ctf_captimerecord))
761 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
765 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
768 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
769 ctf_FlagcarrierWaypoints(player);
770 WaypointSprite_Ping(player.wps_flagcarrier);
774 // ===================
775 // Main Flag Functions
776 // ===================
778 void ctf_CheckFlagReturn(entity flag, int returntype)
780 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
782 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH)); }
784 if((GetResource(flag, RES_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
789 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
791 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
792 case RETURN_SPEEDRUN:
793 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
794 case RETURN_NEEDKILL:
795 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
798 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
800 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
801 ctf_EventLog("returned", flag.team, NULL);
803 ctf_RespawnFlag(flag);
808 bool ctf_Stalemate_Customize(entity this, entity client)
810 // make spectators see what the player would see
811 entity e = WaypointSprite_getviewentity(client);
812 entity wp_owner = this.owner;
815 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
816 if(SAME_TEAM(wp_owner, e)) { return false; }
817 if(!IS_PLAYER(e)) { return false; }
822 void ctf_CheckStalemate()
825 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
828 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
830 // build list of stale flags
831 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
833 if(autocvar_g_ctf_stalemate)
834 if(tmp_entity.ctf_status != FLAG_BASE)
835 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
837 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
838 ctf_staleflaglist = tmp_entity;
840 switch(tmp_entity.team)
842 case NUM_TEAM_1: ++stale_red_flags; break;
843 case NUM_TEAM_2: ++stale_blue_flags; break;
844 case NUM_TEAM_3: ++stale_yellow_flags; break;
845 case NUM_TEAM_4: ++stale_pink_flags; break;
846 default: ++stale_neutral_flags; break;
852 stale_flags = (stale_neutral_flags >= 1);
854 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
856 if(ctf_oneflag && stale_flags == 1)
857 ctf_stalemate = true;
858 else if(stale_flags >= 2)
859 ctf_stalemate = true;
860 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
861 { ctf_stalemate = false; wpforenemy_announced = false; }
862 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
863 { ctf_stalemate = false; wpforenemy_announced = false; }
865 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
868 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
870 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
872 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);
873 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
874 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
878 if (!wpforenemy_announced)
880 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)); });
882 wpforenemy_announced = true;
887 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
889 if(ITEM_DAMAGE_NEEDKILL(deathtype))
891 if(autocvar_g_ctf_flag_return_damage_delay)
892 this.ctf_flagdamaged_byworld = true;
895 SetResourceExplicit(this, RES_HEALTH, 0);
896 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
900 if(autocvar_g_ctf_flag_return_damage)
902 // reduce health and check if it should be returned
903 TakeResource(this, RES_HEALTH, damage);
904 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
909 void ctf_FlagThink(entity this)
914 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
917 if(this == ctf_worldflaglist) // only for the first flag
918 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
921 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
922 LOG_TRACE("wtf the flag got squashed?");
923 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
924 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
925 setsize(this, this.m_mins, this.m_maxs);
929 switch(this.ctf_status)
933 if(autocvar_g_ctf_dropped_capture_radius)
935 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
936 if(tmp_entity.ctf_status == FLAG_DROPPED)
937 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
938 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
939 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
946 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
948 if(autocvar_g_ctf_flag_dropped_floatinwater)
950 vector midpoint = ((this.absmin + this.absmax) * 0.5);
951 if(pointcontents(midpoint) == CONTENT_WATER)
953 this.velocity = this.velocity * 0.5;
955 if (pointcontents(midpoint + eZ * FLAG_FLOAT_OFFSET_Z) == CONTENT_WATER)
956 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
958 { set_movetype(this, MOVETYPE_FLY); }
960 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
962 if(autocvar_g_ctf_flag_return_dropped)
964 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
966 SetResourceExplicit(this, RES_HEALTH, 0);
967 ctf_CheckFlagReturn(this, RETURN_DROPPED);
971 if(this.ctf_flagdamaged_byworld)
973 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
974 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
977 else if(autocvar_g_ctf_flag_return_time)
979 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
980 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
988 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
990 SetResourceExplicit(this, RES_HEALTH, 0);
991 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
993 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
994 ImpulseCommands(this.owner);
996 if(autocvar_g_ctf_stalemate)
998 if(time >= wpforenemy_nextthink)
1000 ctf_CheckStalemate();
1001 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1004 if(CTF_SAMETEAM(this, this.owner) && this.team)
1006 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1007 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
1008 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
1009 ctf_Handle_Return(this, this.owner);
1016 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1017 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1018 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1020 if((this.pass_target == NULL)
1021 || (IS_DEAD(this.pass_target))
1022 || (this.pass_target.flagcarried)
1023 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1024 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1025 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1027 // give up, pass failed
1028 ctf_Handle_Drop(this, NULL, DROP_PASS);
1032 // still a viable target, go for it
1033 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1038 default: // this should never happen
1040 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1046 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1049 if(game_stopped) return;
1050 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1052 bool is_not_monster = (!IS_MONSTER(toucher));
1054 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1055 if(ITEM_TOUCH_NEEDKILL())
1057 if(!autocvar_g_ctf_flag_return_damage_delay)
1059 SetResourceExplicit(flag, RES_HEALTH, 0);
1060 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1062 if(!flag.ctf_flagdamaged_byworld) { return; }
1065 // special touch behaviors
1066 if(STAT(FROZEN, toucher)) { return; }
1067 else if(IS_VEHICLE(toucher))
1069 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1070 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1072 return; // do nothing
1074 else if(IS_MONSTER(toucher))
1076 if(!autocvar_g_ctf_allow_monster_touch)
1077 return; // do nothing
1079 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1081 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1083 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1084 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1085 flag.wait = time + FLAG_TOUCHRATE;
1089 else if(IS_DEAD(toucher)) { return; }
1091 switch(flag.ctf_status)
1097 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1098 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1099 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1100 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1102 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1103 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1104 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)
1106 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1107 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1109 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1110 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1116 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1117 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1118 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1119 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1125 LOG_TRACE("Someone touched a flag even though it was being carried?");
1131 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1133 if(DIFF_TEAM(toucher, flag.pass_sender))
1135 if(ctf_Immediate_Return_Allowed(flag, toucher))
1136 ctf_Handle_Return(flag, toucher);
1137 else if(is_not_monster && (!toucher.flagcarried))
1138 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1140 else if(!toucher.flagcarried)
1141 ctf_Handle_Retrieve(flag, toucher);
1148 .float last_respawn;
1149 void ctf_RespawnFlag(entity flag)
1151 flag.watertype = CONTENT_EMPTY; // TODO: it is unclear why this workaround is needed, likely many other potential breakage points!!
1152 // check for flag respawn being called twice in a row
1153 if(flag.last_respawn > time - 0.5)
1154 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1156 flag.last_respawn = time;
1158 // reset the player (if there is one)
1159 if((flag.owner) && (flag.owner.flagcarried == flag))
1161 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1162 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1163 WaypointSprite_Kill(flag.wps_flagcarrier);
1165 flag.owner.flagcarried = NULL;
1166 GameRules_scoring_vip(flag.owner, false);
1168 if(flag.speedrunning)
1169 ctf_FakeTimeLimit(flag.owner, -1);
1172 if((flag.owner) && (flag.owner.vehicle))
1173 flag.scale = FLAG_SCALE;
1175 if(flag.ctf_status == FLAG_DROPPED)
1176 { WaypointSprite_Kill(flag.wps_flagdropped); }
1179 setattachment(flag, NULL, "");
1180 setorigin(flag, flag.ctf_spawnorigin);
1182 //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1183 set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1184 flag.takedamage = DAMAGE_NO;
1185 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1186 flag.solid = SOLID_TRIGGER;
1187 flag.velocity = '0 0 0';
1188 flag.angles = flag.mangle;
1189 flag.flags = FL_ITEM | FL_NOTARGET;
1191 flag.ctf_status = FLAG_BASE;
1193 flag.pass_distance = 0;
1194 flag.pass_sender = NULL;
1195 flag.pass_target = NULL;
1196 flag.ctf_dropper = NULL;
1197 flag.ctf_pickuptime = 0;
1198 flag.ctf_droptime = 0;
1199 flag.ctf_flagdamaged_byworld = false;
1200 navigation_dynamicgoal_unset(flag);
1202 ctf_CheckStalemate();
1205 void ctf_Reset(entity this)
1207 if(this.owner && IS_PLAYER(this.owner))
1208 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1211 ctf_RespawnFlag(this);
1214 bool ctf_FlagBase_Customize(entity this, entity client)
1216 entity e = WaypointSprite_getviewentity(client);
1217 entity wp_owner = this.owner;
1218 entity flag = e.flagcarried;
1219 if(flag && CTF_SAMETEAM(e, flag))
1221 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1226 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1229 waypoint_spawnforitem_force(this, this.origin);
1230 navigation_dynamicgoal_init(this, true);
1236 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1237 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1238 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1239 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1240 default: basename = WP_FlagBaseNeutral; break;
1243 if(autocvar_g_ctf_flag_waypoint)
1245 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1246 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1247 wp.fade_rate = autocvar_g_ctf_flag_waypoint_maxdistance;
1248 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1249 setcefc(wp, ctf_FlagBase_Customize);
1252 // captureshield setup
1253 ctf_CaptureShield_Spawn(this);
1258 void ctf_FlagSetup(int teamnum, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1261 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1262 ctf_worldflaglist = flag;
1264 setattachment(flag, NULL, "");
1266 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnum), Team_ColorName_Upper(teamnum)));
1267 flag.team = teamnum;
1268 flag.classname = "item_flag_team";
1269 flag.target = "###item###"; // for finding the nearest item using findnearest
1270 flag.flags = FL_ITEM | FL_NOTARGET;
1271 IL_PUSH(g_items, flag);
1272 flag.solid = SOLID_TRIGGER;
1273 flag.takedamage = DAMAGE_NO;
1274 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1275 flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1276 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1277 flag.event_damage = ctf_FlagDamage;
1278 flag.pushable = true;
1279 flag.teleportable = TELEPORT_NORMAL;
1280 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1281 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1282 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1283 if(flag.damagedbycontents)
1284 IL_PUSH(g_damagedbycontents, flag);
1285 flag.velocity = '0 0 0';
1286 flag.mangle = flag.angles;
1287 flag.reset = ctf_Reset;
1288 settouch(flag, ctf_FlagTouch);
1289 setthink(flag, ctf_FlagThink);
1290 flag.nextthink = time + FLAG_THINKRATE;
1291 flag.ctf_status = FLAG_BASE;
1293 // crudely force them all to 0
1294 if(autocvar_g_ctf_score_ignore_fields)
1295 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1297 string teamname = Static_Team_ColorName_Lower(teamnum);
1299 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1300 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1301 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1302 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnum).eent_eff_name; }
1303 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnum).eent_eff_name; }
1304 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnum).eent_eff_name; }
1308 if(flag.s == "") flag.s = b; \
1309 precache_sound(flag.s);
1311 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnum))))
1312 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnum))))
1313 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnum))))
1314 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnum))))
1315 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1316 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1317 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1321 precache_model(flag.model);
1324 _setmodel(flag, flag.model); // precision set below
1325 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1326 flag.m_mins = flag.mins; // store these for squash checks
1327 flag.m_maxs = flag.maxs;
1328 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1330 if(autocvar_g_ctf_flag_glowtrails)
1334 case NUM_TEAM_1: flag.glow_color = 251; break;
1335 case NUM_TEAM_2: flag.glow_color = 210; break;
1336 case NUM_TEAM_3: flag.glow_color = 110; break;
1337 case NUM_TEAM_4: flag.glow_color = 145; break;
1338 default: flag.glow_color = 254; break;
1340 flag.glow_size = 25;
1341 flag.glow_trail = 1;
1344 flag.effects |= EF_LOWPRECISION;
1345 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1346 if(autocvar_g_ctf_dynamiclights)
1350 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1351 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1352 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1353 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1354 default: flag.effects |= EF_DIMLIGHT; break;
1359 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1361 flag.dropped_origin = flag.origin;
1362 flag.noalign = true;
1363 set_movetype(flag, MOVETYPE_NONE);
1365 else // drop to floor, automatically find a platform and set that as spawn origin
1367 flag.noalign = false;
1369 set_movetype(flag, MOVETYPE_NONE);
1372 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1380 // NOTE: LEGACY CODE, needs to be re-written!
1382 void havocbot_ctf_calculate_middlepoint()
1386 vector fo = '0 0 0';
1389 f = ctf_worldflaglist;
1394 f = f.ctf_worldflagnext;
1400 havocbot_middlepoint = s / n;
1401 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1403 havocbot_symmetry_axis_m = 0;
1404 havocbot_symmetry_axis_q = 0;
1407 // for symmetrical editing of waypoints
1408 entity f1 = ctf_worldflaglist;
1409 entity f2 = f1.ctf_worldflagnext;
1410 float m = -(f1.origin.y - f2.origin.y) / (max(f1.origin.x - f2.origin.x, FLOAT_EPSILON));
1411 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1412 havocbot_symmetry_axis_m = m;
1413 havocbot_symmetry_axis_q = q;
1415 havocbot_symmetry_origin_order = n;
1419 entity havocbot_ctf_find_flag(entity bot)
1422 f = ctf_worldflaglist;
1425 if (CTF_SAMETEAM(bot, f))
1427 f = f.ctf_worldflagnext;
1432 entity havocbot_ctf_find_enemy_flag(entity bot)
1435 f = ctf_worldflaglist;
1440 if(CTF_DIFFTEAM(bot, f))
1447 else if(!bot.flagcarried)
1451 else if (CTF_DIFFTEAM(bot, f))
1453 f = f.ctf_worldflagnext;
1458 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1465 FOREACH_CLIENT(IS_PLAYER(it), {
1466 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1469 if(vdist(it.origin - org, <, tc_radius))
1478 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1481 head = ctf_worldflaglist;
1484 if (CTF_SAMETEAM(this, head))
1486 head = head.ctf_worldflagnext;
1489 navigation_routerating(this, head, ratingscale, 10000);
1493 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1496 head = ctf_worldflaglist;
1499 if (CTF_SAMETEAM(this, head))
1501 if (this.flagcarried)
1502 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1504 head = head.ctf_worldflagnext; // skip base if it has a different group
1509 head = head.ctf_worldflagnext;
1514 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1517 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1520 head = ctf_worldflaglist;
1525 if(CTF_DIFFTEAM(this, head))
1529 if(this.flagcarried)
1532 else if(!this.flagcarried)
1536 else if(CTF_DIFFTEAM(this, head))
1538 head = head.ctf_worldflagnext;
1542 if (head.ctf_status == FLAG_CARRY)
1544 // adjust rating of our flag carrier depending on his health
1545 head = head.tag_entity;
1546 float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1547 ratingscale += ratingscale * f * 0.1;
1549 navigation_routerating(this, head, ratingscale, 10000);
1553 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1555 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1557 if (!bot_waypoints_for_items)
1559 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1565 head = havocbot_ctf_find_enemy_flag(this);
1570 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1573 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1577 mf = havocbot_ctf_find_flag(this);
1579 if(mf.ctf_status == FLAG_BASE)
1583 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1586 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1589 head = ctf_worldflaglist;
1592 // flag is out in the field
1593 if(head.ctf_status != FLAG_BASE)
1594 if(head.tag_entity==NULL) // dropped
1598 if(vdist(org - head.origin, <, df_radius))
1599 navigation_routerating(this, head, ratingscale, 10000);
1602 navigation_routerating(this, head, ratingscale, 10000);
1605 head = head.ctf_worldflagnext;
1609 void havocbot_ctf_reset_role(entity this)
1611 float cdefense, cmiddle, coffense;
1618 if (this.flagcarried)
1620 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1624 mf = havocbot_ctf_find_flag(this);
1625 ef = havocbot_ctf_find_enemy_flag(this);
1627 // Retrieve stolen flag
1628 if(mf.ctf_status!=FLAG_BASE)
1630 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1634 // If enemy flag is taken go to the middle to intercept pursuers
1635 if(ef.ctf_status!=FLAG_BASE)
1637 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1641 // if there is no one else on the team switch to offense
1643 // don't check if this bot is a player since it isn't true when the bot is added to the server
1644 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1648 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1651 else if (time < CS(this).jointime + 1)
1653 // if bots spawn all at once set good default roles
1656 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1659 else if (count == 2)
1661 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1666 // Evaluate best position to take
1667 // Count mates on middle position
1668 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1670 // Count mates on defense position
1671 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1673 // Count mates on offense position
1674 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1676 if(cdefense<=coffense)
1677 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1678 else if(coffense<=cmiddle)
1679 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1681 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1683 // if bots spawn all at once assign them a more appropriated role after a while
1684 if (time < CS(this).jointime + 1 && count > 2)
1685 this.havocbot_role_timeout = time + 10 + random() * 10;
1688 bool havocbot_ctf_is_basewaypoint(entity item)
1690 if (item.classname != "waypoint")
1693 entity head = ctf_worldflaglist;
1696 if (item == head.bot_basewaypoint)
1698 head = head.ctf_worldflagnext;
1703 void havocbot_role_ctf_carrier(entity this)
1707 havocbot_ctf_reset_role(this);
1711 if (this.flagcarried == NULL)
1713 havocbot_ctf_reset_role(this);
1717 if (navigation_goalrating_timeout(this))
1719 navigation_goalrating_start(this);
1722 entity mf = havocbot_ctf_find_flag(this);
1723 vector base_org = mf.dropped_origin;
1724 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1726 havocbot_goalrating_ctf_enemybase(this, base_rating);
1728 havocbot_goalrating_ctf_ourbase(this, base_rating);
1730 // start collecting items very close to the bot but only inside of own base radius
1731 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1732 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1734 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1736 navigation_goalrating_end(this);
1738 navigation_goalrating_timeout_set(this);
1740 entity goal = this.goalentity;
1741 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1742 this.goalentity_lock_timeout = time + ((this.bot_aimtarg) ? 2 : 3);
1745 this.havocbot_cantfindflag = time + 10;
1746 else if (time > this.havocbot_cantfindflag)
1748 // Can't navigate to my own base, suicide!
1749 // TODO: drop it and wander around
1750 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1756 void havocbot_role_ctf_escort(entity this)
1762 havocbot_ctf_reset_role(this);
1766 if (this.flagcarried)
1768 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1772 // If enemy flag is back on the base switch to previous role
1773 ef = havocbot_ctf_find_enemy_flag(this);
1774 if(ef.ctf_status==FLAG_BASE)
1776 this.havocbot_role = this.havocbot_previous_role;
1777 this.havocbot_role_timeout = 0;
1780 if (ef.ctf_status == FLAG_DROPPED)
1782 navigation_goalrating_timeout_expire(this, 1);
1786 // If the flag carrier reached the base switch to defense
1787 mf = havocbot_ctf_find_flag(this);
1788 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1790 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1794 // Set the role timeout if necessary
1795 if (!this.havocbot_role_timeout)
1797 this.havocbot_role_timeout = time + random() * 30 + 60;
1800 // If nothing happened just switch to previous role
1801 if (time > this.havocbot_role_timeout)
1803 this.havocbot_role = this.havocbot_previous_role;
1804 this.havocbot_role_timeout = 0;
1808 // Chase the flag carrier
1809 if (navigation_goalrating_timeout(this))
1811 navigation_goalrating_start(this);
1814 havocbot_goalrating_ctf_enemyflag(this, 10000);
1815 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1816 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1818 navigation_goalrating_end(this);
1820 navigation_goalrating_timeout_set(this);
1824 void havocbot_role_ctf_offense(entity this)
1831 havocbot_ctf_reset_role(this);
1835 if (this.flagcarried)
1837 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1842 mf = havocbot_ctf_find_flag(this);
1843 ef = havocbot_ctf_find_enemy_flag(this);
1846 if(mf.ctf_status!=FLAG_BASE)
1849 pos = mf.tag_entity.origin;
1853 // Try to get it if closer than the enemy base
1854 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1856 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1861 // Escort flag carrier
1862 if(ef.ctf_status!=FLAG_BASE)
1865 pos = ef.tag_entity.origin;
1869 if(vdist(pos - mf.dropped_origin, >, 700))
1871 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1876 // Set the role timeout if necessary
1877 if (!this.havocbot_role_timeout)
1878 this.havocbot_role_timeout = time + 120;
1880 if (time > this.havocbot_role_timeout)
1882 havocbot_ctf_reset_role(this);
1886 if (navigation_goalrating_timeout(this))
1888 navigation_goalrating_start(this);
1891 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1892 havocbot_goalrating_ctf_enemybase(this, 10000);
1893 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1895 navigation_goalrating_end(this);
1897 navigation_goalrating_timeout_set(this);
1901 // Retriever (temporary role):
1902 void havocbot_role_ctf_retriever(entity this)
1908 havocbot_ctf_reset_role(this);
1912 if (this.flagcarried)
1914 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1918 // If flag is back on the base switch to previous role
1919 mf = havocbot_ctf_find_flag(this);
1920 if(mf.ctf_status==FLAG_BASE)
1922 if (mf.enemy == this) // did this bot return the flag?
1923 navigation_goalrating_timeout_force(this);
1924 havocbot_ctf_reset_role(this);
1928 if (!this.havocbot_role_timeout)
1929 this.havocbot_role_timeout = time + 20;
1931 if (time > this.havocbot_role_timeout)
1933 havocbot_ctf_reset_role(this);
1937 if (navigation_goalrating_timeout(this))
1939 const float RT_RADIUS = 10000;
1941 navigation_goalrating_start(this);
1944 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1945 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1946 havocbot_goalrating_ctf_enemybase(this, 8000);
1947 entity ef = havocbot_ctf_find_enemy_flag(this);
1948 vector enemy_base_org = ef.dropped_origin;
1949 // start collecting items very close to the bot but only inside of enemy base radius
1950 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1951 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1952 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1954 navigation_goalrating_end(this);
1956 navigation_goalrating_timeout_set(this);
1960 void havocbot_role_ctf_middle(entity this)
1966 havocbot_ctf_reset_role(this);
1970 if (this.flagcarried)
1972 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1976 mf = havocbot_ctf_find_flag(this);
1977 if(mf.ctf_status!=FLAG_BASE)
1979 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1983 if (!this.havocbot_role_timeout)
1984 this.havocbot_role_timeout = time + 10;
1986 if (time > this.havocbot_role_timeout)
1988 havocbot_ctf_reset_role(this);
1992 if (navigation_goalrating_timeout(this))
1996 org = havocbot_middlepoint;
1997 org.z = this.origin.z;
1999 navigation_goalrating_start(this);
2002 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
2003 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
2004 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2005 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2006 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2007 havocbot_goalrating_ctf_enemybase(this, 3000);
2009 navigation_goalrating_end(this);
2011 entity goal = this.goalentity;
2012 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
2013 this.goalentity_lock_timeout = time + 2;
2015 navigation_goalrating_timeout_set(this);
2019 void havocbot_role_ctf_defense(entity this)
2025 havocbot_ctf_reset_role(this);
2029 if (this.flagcarried)
2031 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2035 // If own flag was captured
2036 mf = havocbot_ctf_find_flag(this);
2037 if(mf.ctf_status!=FLAG_BASE)
2039 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2043 if (!this.havocbot_role_timeout)
2044 this.havocbot_role_timeout = time + 30;
2046 if (time > this.havocbot_role_timeout)
2048 havocbot_ctf_reset_role(this);
2051 if (navigation_goalrating_timeout(this))
2053 vector org = mf.dropped_origin;
2055 navigation_goalrating_start(this);
2057 // if enemies are closer to our base, go there
2058 entity closestplayer = NULL;
2059 float distance, bestdistance = 10000;
2060 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2061 distance = vlen(org - it.origin);
2062 if(distance<bestdistance)
2065 bestdistance = distance;
2071 if(DIFF_TEAM(closestplayer, this))
2072 if(vdist(org - this.origin, >, 1000))
2073 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2074 havocbot_goalrating_ctf_ourbase(this, 10000);
2076 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2077 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2078 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2079 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2080 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2082 navigation_goalrating_end(this);
2084 navigation_goalrating_timeout_set(this);
2088 void havocbot_role_ctf_setrole(entity bot, int role)
2090 string s = "(null)";
2093 case HAVOCBOT_CTF_ROLE_CARRIER:
2095 bot.havocbot_role = havocbot_role_ctf_carrier;
2096 bot.havocbot_role_timeout = 0;
2097 bot.havocbot_cantfindflag = time + 10;
2098 if (bot.havocbot_previous_role != bot.havocbot_role)
2099 navigation_goalrating_timeout_force(bot);
2101 case HAVOCBOT_CTF_ROLE_DEFENSE:
2103 bot.havocbot_role = havocbot_role_ctf_defense;
2104 bot.havocbot_role_timeout = 0;
2106 case HAVOCBOT_CTF_ROLE_MIDDLE:
2108 bot.havocbot_role = havocbot_role_ctf_middle;
2109 bot.havocbot_role_timeout = 0;
2111 case HAVOCBOT_CTF_ROLE_OFFENSE:
2113 bot.havocbot_role = havocbot_role_ctf_offense;
2114 bot.havocbot_role_timeout = 0;
2116 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2118 bot.havocbot_previous_role = bot.havocbot_role;
2119 bot.havocbot_role = havocbot_role_ctf_retriever;
2120 bot.havocbot_role_timeout = time + 10;
2121 if (bot.havocbot_previous_role != bot.havocbot_role)
2122 navigation_goalrating_timeout_expire(bot, 2);
2124 case HAVOCBOT_CTF_ROLE_ESCORT:
2126 bot.havocbot_previous_role = bot.havocbot_role;
2127 bot.havocbot_role = havocbot_role_ctf_escort;
2128 bot.havocbot_role_timeout = time + 30;
2129 if (bot.havocbot_previous_role != bot.havocbot_role)
2130 navigation_goalrating_timeout_expire(bot, 2);
2133 LOG_TRACE(bot.netname, " switched to ", s);
2141 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2143 entity player = M_ARGV(0, entity);
2145 int t = 0, t2 = 0, t3 = 0;
2146 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)
2148 // initially clear items so they can be set as necessary later.
2149 STAT(CTF_FLAGSTATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2150 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2151 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2152 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2153 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2154 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2156 // scan through all the flags and notify the client about them
2157 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2159 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2160 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2161 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2162 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2163 if(flag.team == 0 && !b5) { b5 = true; t = CTF_NEUTRAL_FLAG_CARRYING; t2 = CTF_NEUTRAL_FLAG_TAKEN; t3 = CTF_NEUTRAL_FLAG_LOST; STAT(CTF_FLAGSTATUS, player) |= CTF_FLAG_NEUTRAL; }
2165 switch(flag.ctf_status)
2170 if((flag.owner == player) || (flag.pass_sender == player))
2171 STAT(CTF_FLAGSTATUS, player) |= t; // carrying: player is currently carrying the flag
2173 STAT(CTF_FLAGSTATUS, player) |= t2; // taken: someone else is carrying the flag
2178 STAT(CTF_FLAGSTATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2184 // item for stopping players from capturing the flag too often
2185 if(player.ctf_captureshielded)
2186 STAT(CTF_FLAGSTATUS, player) |= CTF_SHIELDED;
2189 STAT(CTF_FLAGSTATUS, player) |= CTF_STALEMATE;
2191 // update the health of the flag carrier waypointsprite
2192 if(player.wps_flagcarrier)
2193 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);
2196 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2198 entity frag_attacker = M_ARGV(1, entity);
2199 entity frag_target = M_ARGV(2, entity);
2200 float frag_damage = M_ARGV(4, float);
2201 vector frag_force = M_ARGV(6, vector);
2203 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2205 if(frag_target == frag_attacker) // damage done to yourself
2207 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2208 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2210 else // damage done to everyone else
2212 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2213 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2216 M_ARGV(4, float) = frag_damage;
2217 M_ARGV(6, vector) = frag_force;
2219 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2221 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
2222 && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2224 frag_target.wps_helpme_time = time;
2225 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2227 // todo: add notification for when flag carrier needs help?
2231 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2233 entity frag_attacker = M_ARGV(1, entity);
2234 entity frag_target = M_ARGV(2, entity);
2236 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2238 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2239 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2242 if(frag_target.flagcarried)
2244 entity tmp_entity = frag_target.flagcarried;
2245 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2246 tmp_entity.ctf_dropper = NULL;
2250 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2252 M_ARGV(2, float) = 0; // frag score
2253 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2256 void ctf_RemovePlayer(entity player)
2258 if(player.flagcarried)
2259 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2261 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2263 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2264 if(flag.pass_target == player) { flag.pass_target = NULL; }
2265 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2269 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2271 entity player = M_ARGV(0, entity);
2273 ctf_RemovePlayer(player);
2276 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2278 entity player = M_ARGV(0, entity);
2280 ctf_RemovePlayer(player);
2283 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2285 if(!autocvar_g_ctf_leaderboard)
2288 entity player = M_ARGV(0, entity);
2290 if(IS_REAL_CLIENT(player))
2292 int m = min(RANKINGS_CNT, autocvar_g_cts_send_rankings_cnt);
2293 race_send_rankings_cnt(MSG_ONE);
2294 for (int i = 1; i <= m; ++i)
2296 race_SendRankings(i, 0, 0, MSG_ONE);
2301 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2303 if(!autocvar_g_ctf_leaderboard)
2306 entity player = M_ARGV(0, entity);
2308 if(CS(player).cvar_cl_allow_uidtracking == 1 && CS(player).cvar_cl_allow_uid2name == 1)
2310 if (!player.stored_netname)
2311 player.stored_netname = strzone(uid2name(player.crypto_idfp));
2312 if(player.stored_netname != player.netname)
2314 db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
2315 strcpy(player.stored_netname, player.netname);
2320 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2322 entity player = M_ARGV(0, entity);
2324 if(player.flagcarried)
2325 if(!autocvar_g_ctf_portalteleport)
2326 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2329 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2331 if(MUTATOR_RETURNVALUE || game_stopped) return;
2333 entity player = M_ARGV(0, entity);
2335 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2337 // pass the flag to a team mate
2338 if(autocvar_g_ctf_pass)
2340 entity head, closest_target = NULL;
2341 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2343 while(head) // find the closest acceptable target to pass to
2345 if(IS_PLAYER(head) && !IS_DEAD(head))
2346 if(head != player && SAME_TEAM(head, player))
2347 if(!head.speedrunning && !head.vehicle)
2349 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2350 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2351 vector passer_center = CENTER_OR_VIEWOFS(player);
2353 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2355 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2357 if(IS_BOT_CLIENT(head))
2359 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2360 ctf_Handle_Throw(head, player, DROP_PASS);
2364 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2365 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2367 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2370 else if(player.flagcarried && !head.flagcarried)
2374 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2375 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2376 { closest_target = head; }
2378 else { closest_target = head; }
2385 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2388 // throw the flag in front of you
2389 if(autocvar_g_ctf_throw && player.flagcarried)
2391 if(player.throw_count == -1)
2393 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2395 player.throw_prevtime = time;
2396 player.throw_count = 1;
2397 ctf_Handle_Throw(player, NULL, DROP_THROW);
2402 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2408 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2409 else { player.throw_count += 1; }
2410 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2412 player.throw_prevtime = time;
2413 ctf_Handle_Throw(player, NULL, DROP_THROW);
2420 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2422 entity player = M_ARGV(0, entity);
2424 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2426 player.wps_helpme_time = time;
2427 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2429 else // create a normal help me waypointsprite
2431 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2432 WaypointSprite_Ping(player.wps_helpme);
2438 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2440 entity player = M_ARGV(0, entity);
2441 entity veh = M_ARGV(1, entity);
2443 if(player.flagcarried)
2445 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2447 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2451 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2452 setattachment(player.flagcarried, veh, "");
2453 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2454 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2455 //player.flagcarried.angles = '0 0 0';
2461 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2463 entity player = M_ARGV(0, entity);
2465 if(player.flagcarried)
2467 setattachment(player.flagcarried, player, "");
2468 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2469 player.flagcarried.scale = FLAG_SCALE;
2470 player.flagcarried.angles = '0 0 0';
2471 player.flagcarried.nodrawtoclient = NULL;
2476 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2478 entity player = M_ARGV(0, entity);
2480 if(player.flagcarried)
2482 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2483 ctf_RespawnFlag(player.flagcarried);
2488 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2490 entity flag; // temporary entity for the search method
2492 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2494 switch(flag.ctf_status)
2499 // lock the flag, game is over
2500 set_movetype(flag, MOVETYPE_NONE);
2501 flag.takedamage = DAMAGE_NO;
2502 flag.solid = SOLID_NOT;
2503 flag.nextthink = false; // stop thinking
2505 //dprint("stopping the ", flag.netname, " from moving.\n");
2513 // do nothing for these flags
2520 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2522 entity bot = M_ARGV(0, entity);
2524 havocbot_ctf_reset_role(bot);
2528 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2530 M_ARGV(1, string) = "ctf_team";
2533 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2535 entity spectatee = M_ARGV(0, entity);
2536 entity client = M_ARGV(1, entity);
2538 STAT(CTF_FLAGSTATUS, client) = STAT(CTF_FLAGSTATUS, spectatee);
2541 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2543 int record_page = M_ARGV(0, int);
2544 string ret_string = M_ARGV(1, string);
2546 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2548 if (MapInfo_Get_ByID(i))
2550 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2556 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2557 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2561 M_ARGV(1, string) = ret_string;
2564 bool superspec_Spectate(entity this, entity targ); // TODO
2565 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2566 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2568 entity player = M_ARGV(0, entity);
2569 string cmd_name = M_ARGV(1, string);
2570 int cmd_argc = M_ARGV(2, int);
2572 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2574 if(cmd_name == "followfc")
2586 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2587 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2588 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2589 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2593 FOREACH_CLIENT(IS_PLAYER(it), {
2594 if(it.flagcarried && (it.team == _team || _team == 0))
2597 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2598 continue; // already spectating this fc, try another
2599 return superspec_Spectate(player, it);
2604 superspec_msg("", "", player, "No active flag carrier\n", 1);
2609 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2611 entity frag_target = M_ARGV(0, entity);
2613 if(frag_target.flagcarried)
2614 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2617 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2619 entity player = M_ARGV(0, entity);
2620 if(player.flagcarried)
2621 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2629 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2630 CTF flag for team one (Red).
2632 "angle" Angle the flag will point (minus 90 degrees)...
2633 "model" model to use, note this needs red and blue as skins 0 and 1...
2634 "noise" sound played when flag is picked up...
2635 "noise1" sound played when flag is returned by a teammate...
2636 "noise2" sound played when flag is captured...
2637 "noise3" sound played when flag is lost in the field and respawns itself...
2638 "noise4" sound played when flag is dropped by a player...
2639 "noise5" sound played when flag touches the ground... */
2640 spawnfunc(item_flag_team1)
2642 if(!g_ctf) { delete(this); return; }
2644 ctf_FlagSetup(NUM_TEAM_1, this);
2647 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2648 CTF flag for team two (Blue).
2650 "angle" Angle the flag will point (minus 90 degrees)...
2651 "model" model to use, note this needs red and blue as skins 0 and 1...
2652 "noise" sound played when flag is picked up...
2653 "noise1" sound played when flag is returned by a teammate...
2654 "noise2" sound played when flag is captured...
2655 "noise3" sound played when flag is lost in the field and respawns itself...
2656 "noise4" sound played when flag is dropped by a player...
2657 "noise5" sound played when flag touches the ground... */
2658 spawnfunc(item_flag_team2)
2660 if(!g_ctf) { delete(this); return; }
2662 ctf_FlagSetup(NUM_TEAM_2, this);
2665 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2666 CTF flag for team three (Yellow).
2668 "angle" Angle the flag will point (minus 90 degrees)...
2669 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2670 "noise" sound played when flag is picked up...
2671 "noise1" sound played when flag is returned by a teammate...
2672 "noise2" sound played when flag is captured...
2673 "noise3" sound played when flag is lost in the field and respawns itself...
2674 "noise4" sound played when flag is dropped by a player...
2675 "noise5" sound played when flag touches the ground... */
2676 spawnfunc(item_flag_team3)
2678 if(!g_ctf) { delete(this); return; }
2680 ctf_FlagSetup(NUM_TEAM_3, this);
2683 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2684 CTF flag for team four (Pink).
2686 "angle" Angle the flag will point (minus 90 degrees)...
2687 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2688 "noise" sound played when flag is picked up...
2689 "noise1" sound played when flag is returned by a teammate...
2690 "noise2" sound played when flag is captured...
2691 "noise3" sound played when flag is lost in the field and respawns itself...
2692 "noise4" sound played when flag is dropped by a player...
2693 "noise5" sound played when flag touches the ground... */
2694 spawnfunc(item_flag_team4)
2696 if(!g_ctf) { delete(this); return; }
2698 ctf_FlagSetup(NUM_TEAM_4, this);
2701 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2704 "angle" Angle the flag will point (minus 90 degrees)...
2705 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2706 "noise" sound played when flag is picked up...
2707 "noise1" sound played when flag is returned by a teammate...
2708 "noise2" sound played when flag is captured...
2709 "noise3" sound played when flag is lost in the field and respawns itself...
2710 "noise4" sound played when flag is dropped by a player...
2711 "noise5" sound played when flag touches the ground... */
2712 spawnfunc(item_flag_neutral)
2714 if(!g_ctf) { delete(this); return; }
2715 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2717 ctf_FlagSetup(0, this);
2720 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2721 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2722 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.
2724 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2725 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2728 if(!g_ctf) { delete(this); return; }
2730 this.classname = "ctf_team";
2731 this.team = this.cnt + 1;
2734 // compatibility for quake maps
2735 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2736 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2737 spawnfunc(info_player_team1);
2738 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2739 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2740 spawnfunc(info_player_team2);
2741 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2742 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2744 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2745 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2747 // compatibility for wop maps
2748 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2749 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2750 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2751 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2752 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2753 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2761 void ctf_ScoreRules(int teams)
2763 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2764 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2765 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2766 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2767 field(SP_CTF_PICKUPS, "pickups", 0);
2768 field(SP_CTF_FCKILLS, "fckills", 0);
2769 field(SP_CTF_RETURNS, "returns", 0);
2770 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2774 // code from here on is just to support maps that don't have flag and team entities
2775 void ctf_SpawnTeam (string teamname, int teamcolor)
2777 entity this = new_pure(ctf_team);
2778 this.netname = teamname;
2779 this.cnt = teamcolor - 1;
2780 this.spawnfunc_checked = true;
2781 this.team = teamcolor;
2784 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2789 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2791 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2792 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2794 switch(tmp_entity.team)
2796 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2797 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2798 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2799 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2801 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2804 havocbot_ctf_calculate_middlepoint();
2806 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2808 ctf_teams = 0; // so set the default red and blue teams
2809 BITSET_ASSIGN(ctf_teams, BIT(0));
2810 BITSET_ASSIGN(ctf_teams, BIT(1));
2813 //ctf_teams = bound(2, ctf_teams, 4);
2815 // if no teams are found, spawn defaults
2816 if(find(NULL, classname, "ctf_team") == NULL)
2818 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2819 if(ctf_teams & BIT(0))
2820 ctf_SpawnTeam("Red", NUM_TEAM_1);
2821 if(ctf_teams & BIT(1))
2822 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2823 if(ctf_teams & BIT(2))
2824 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2825 if(ctf_teams & BIT(3))
2826 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2829 ctf_ScoreRules(ctf_teams);
2832 void ctf_Initialize()
2834 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2836 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2837 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2838 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2840 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);