3 // TODO: split into sv_ctf
5 #include <common/effects/all.qh>
6 #include <common/vehicles/all.qh>
7 #include <server/teamplay.qh>
9 #include <lib/warpzone/common.qh>
11 bool autocvar_g_ctf_allow_vehicle_carry;
12 bool autocvar_g_ctf_allow_vehicle_touch;
13 bool autocvar_g_ctf_allow_monster_touch;
14 bool autocvar_g_ctf_throw;
15 float autocvar_g_ctf_throw_angle_max;
16 float autocvar_g_ctf_throw_angle_min;
17 int autocvar_g_ctf_throw_punish_count;
18 float autocvar_g_ctf_throw_punish_delay;
19 float autocvar_g_ctf_throw_punish_time;
20 float autocvar_g_ctf_throw_strengthmultiplier;
21 float autocvar_g_ctf_throw_velocity_forward;
22 float autocvar_g_ctf_throw_velocity_up;
23 float autocvar_g_ctf_drop_velocity_up;
24 float autocvar_g_ctf_drop_velocity_side;
25 bool autocvar_g_ctf_oneflag_reverse;
26 bool autocvar_g_ctf_portalteleport;
27 bool autocvar_g_ctf_pass;
28 float autocvar_g_ctf_pass_arc;
29 float autocvar_g_ctf_pass_arc_max;
30 float autocvar_g_ctf_pass_directional_max;
31 float autocvar_g_ctf_pass_directional_min;
32 float autocvar_g_ctf_pass_radius;
33 float autocvar_g_ctf_pass_wait;
34 bool autocvar_g_ctf_pass_request;
35 float autocvar_g_ctf_pass_turnrate;
36 float autocvar_g_ctf_pass_timelimit;
37 float autocvar_g_ctf_pass_velocity;
38 bool autocvar_g_ctf_dynamiclights;
39 float autocvar_g_ctf_flag_collect_delay;
40 float autocvar_g_ctf_flag_damageforcescale;
41 bool autocvar_g_ctf_flag_dropped_waypoint;
42 bool autocvar_g_ctf_flag_dropped_floatinwater;
43 bool autocvar_g_ctf_flag_glowtrails;
44 int autocvar_g_ctf_flag_health;
45 bool autocvar_g_ctf_flag_return;
46 bool autocvar_g_ctf_flag_return_carrying;
47 float autocvar_g_ctf_flag_return_carried_radius;
48 float autocvar_g_ctf_flag_return_time;
49 bool autocvar_g_ctf_flag_return_when_unreachable;
50 float autocvar_g_ctf_flag_return_damage;
51 float autocvar_g_ctf_flag_return_damage_delay;
52 float autocvar_g_ctf_flag_return_dropped;
53 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
54 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
55 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
56 float autocvar_g_ctf_flagcarrier_selfforcefactor;
57 float autocvar_g_ctf_flagcarrier_damagefactor;
58 float autocvar_g_ctf_flagcarrier_forcefactor;
59 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
60 bool autocvar_g_ctf_fullbrightflags;
61 bool autocvar_g_ctf_ignore_frags;
62 bool autocvar_g_ctf_score_ignore_fields;
63 int autocvar_g_ctf_score_capture;
64 int autocvar_g_ctf_score_capture_assist;
65 int autocvar_g_ctf_score_kill;
66 int autocvar_g_ctf_score_penalty_drop;
67 int autocvar_g_ctf_score_penalty_returned;
68 int autocvar_g_ctf_score_pickup_base;
69 int autocvar_g_ctf_score_pickup_dropped_early;
70 int autocvar_g_ctf_score_pickup_dropped_late;
71 int autocvar_g_ctf_score_return;
72 float autocvar_g_ctf_shield_force;
73 float autocvar_g_ctf_shield_max_ratio;
74 int autocvar_g_ctf_shield_min_negscore;
75 bool autocvar_g_ctf_stalemate;
76 int autocvar_g_ctf_stalemate_endcondition;
77 float autocvar_g_ctf_stalemate_time;
78 bool autocvar_g_ctf_reverse;
79 float autocvar_g_ctf_dropped_capture_delay;
80 float autocvar_g_ctf_dropped_capture_radius;
82 void ctf_FakeTimeLimit(entity e, float t)
85 WriteByte(MSG_ONE, 3); // svc_updatestat
86 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
88 WriteCoord(MSG_ONE, autocvar_timelimit);
90 WriteCoord(MSG_ONE, (t + 1) / 60);
93 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
95 if(autocvar_sv_eventlog)
96 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != NULL) ? ftos(actor.playerid) : "")));
97 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
100 void ctf_CaptureRecord(entity flag, entity player)
102 float cap_record = ctf_captimerecord;
103 float cap_time = (time - flag.ctf_pickuptime);
104 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
108 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname);
109 else if(!ctf_captimerecord)
110 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_TIME), player.netname, TIME_ENCODE(cap_time));
111 else if(cap_time < cap_record)
112 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_BROKEN), player.netname, refername, TIME_ENCODE(cap_time), TIME_ENCODE(cap_record));
114 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_UNBROKEN), player.netname, refername, TIME_ENCODE(cap_time), TIME_ENCODE(cap_record));
116 // write that shit in the database
117 if(!ctf_oneflag) // but not in 1-flag mode
118 if((!ctf_captimerecord) || (cap_time < cap_record))
120 ctf_captimerecord = cap_time;
121 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
122 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
123 write_recordmarker(player, flag.ctf_pickuptime, cap_time);
126 if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
127 race_setTime(GetMapname(), TIME_ENCODE(cap_time), player.crypto_idfp, player.netname, player, false);
130 bool ctf_Immediate_Return_Allowed(entity flag, entity toucher)
133 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), { ++num_perteam; });
135 // automatically return if there's only 1 player on the team
136 return ((autocvar_g_ctf_flag_return || num_perteam <= 1 || (autocvar_g_ctf_flag_return_carrying && toucher.flagcarried))
140 bool ctf_Return_Customize(entity this, entity client)
142 // only to the carrier
143 return boolean(client == this.owner);
146 void ctf_FlagcarrierWaypoints(entity player)
148 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
149 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
150 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
151 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
153 if(player.flagcarried && CTF_SAMETEAM(player, player.flagcarried))
155 if(!player.wps_enemyflagcarrier)
157 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, 0, player, wps_enemyflagcarrier, true, RADARICON_FLAG);
158 wp.colormod = WPCOLOR_ENEMYFC(player.team);
159 setcefc(wp, ctf_Stalemate_Customize);
161 if(IS_REAL_CLIENT(player) && !ctf_stalemate)
162 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_VISIBLE);
165 if(!player.wps_flagreturn)
167 entity owp = WaypointSprite_SpawnFixed(WP_FlagReturn, player.flagcarried.ctf_spawnorigin + FLAG_WAYPOINT_OFFSET, player, wps_flagreturn, RADARICON_FLAG);
168 owp.colormod = '0 0.8 0.8';
169 //WaypointSprite_UpdateTeamRadar(player.wps_flagreturn, RADARICON_FLAG, ((player.team) ? colormapPaletteColor(player.team - 1, false) : '1 1 1'));
170 setcefc(owp, ctf_Return_Customize);
175 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
177 float current_distance = vlen((('1 0 0' * to.x) + ('0 1 0' * to.y)) - (('1 0 0' * from.x) + ('0 1 0' * from.y))); // for the sake of this check, exclude Z axis
178 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
179 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
180 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
183 if(current_height) // make sure we can actually do this arcing path
185 targpos = (to + ('0 0 1' * current_height));
186 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
187 if(trace_fraction < 1)
189 //print("normal arc line failed, trying to find new pos...");
190 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
191 targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
192 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
193 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
194 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
197 else { targpos = to; }
199 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
201 vector desired_direction = normalize(targpos - from);
202 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
203 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
206 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
208 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
210 // directional tracing only
212 makevectors(passer_angle);
214 // find the closest point on the enemy to the center of the attack
215 float h; // hypotenuse, which is the distance between attacker to head
216 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
218 h = vlen(head_center - passer_center);
219 a = h * (normalize(head_center - passer_center) * v_forward);
221 vector nearest_on_line = (passer_center + a * v_forward);
222 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
224 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
225 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
227 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
232 else { return true; }
236 // =======================
237 // CaptureShield Functions
238 // =======================
240 bool ctf_CaptureShield_CheckStatus(entity p)
242 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
243 int players_worseeq, players_total;
245 if(ctf_captureshield_max_ratio <= 0)
248 s = GameRules_scoring_add(p, CTF_CAPS, 0);
249 s2 = GameRules_scoring_add(p, CTF_PICKUPS, 0);
250 s3 = GameRules_scoring_add(p, CTF_RETURNS, 0);
251 s4 = GameRules_scoring_add(p, CTF_FCKILLS, 0);
253 sr = ((s - s2) + (s3 + s4));
255 if(sr >= -ctf_captureshield_min_negscore)
258 players_total = players_worseeq = 0;
259 FOREACH_CLIENT(IS_PLAYER(it), {
262 se = GameRules_scoring_add(it, CTF_CAPS, 0);
263 se2 = GameRules_scoring_add(it, CTF_PICKUPS, 0);
264 se3 = GameRules_scoring_add(it, CTF_RETURNS, 0);
265 se4 = GameRules_scoring_add(it, CTF_FCKILLS, 0);
267 ser = ((se - se2) + (se3 + se4));
274 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
275 // use this rule here
277 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
283 void ctf_CaptureShield_Update(entity player, bool wanted_status)
285 bool updated_status = ctf_CaptureShield_CheckStatus(player);
286 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
288 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
289 player.ctf_captureshielded = updated_status;
293 bool ctf_CaptureShield_Customize(entity this, entity client)
295 if(!client.ctf_captureshielded) { return false; }
296 if(CTF_SAMETEAM(this, client)) { return false; }
301 void ctf_CaptureShield_Touch(entity this, entity toucher)
303 if(!toucher.ctf_captureshielded) { return; }
304 if(CTF_SAMETEAM(this, toucher)) { return; }
306 vector mymid = (this.absmin + this.absmax) * 0.5;
307 vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
309 Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
310 if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
313 void ctf_CaptureShield_Spawn(entity flag)
315 entity shield = new(ctf_captureshield);
318 shield.team = flag.team;
319 settouch(shield, ctf_CaptureShield_Touch);
320 setcefc(shield, ctf_CaptureShield_Customize);
321 shield.effects = EF_ADDITIVE;
322 set_movetype(shield, MOVETYPE_NOCLIP);
323 shield.solid = SOLID_TRIGGER;
324 shield.avelocity = '7 0 11';
327 setorigin(shield, flag.origin);
328 setmodel(shield, MDL_CTF_SHIELD);
329 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
333 // ====================
334 // Drop/Pass/Throw Code
335 // ====================
337 void ctf_Handle_Drop(entity flag, entity player, int droptype)
340 player = (player ? player : flag.pass_sender);
343 set_movetype(flag, MOVETYPE_TOSS);
344 flag.takedamage = DAMAGE_YES;
345 flag.angles = '0 0 0';
346 flag.health = flag.max_flag_health;
347 flag.ctf_droptime = time;
348 flag.ctf_dropper = player;
349 flag.ctf_status = FLAG_DROPPED;
351 // messages and sounds
352 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
353 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
354 ctf_EventLog("dropped", player.team, player);
357 GameRules_scoring_add_team(player, SCORE, -((flag.score_drop) ? flag.score_drop : autocvar_g_ctf_score_penalty_drop));
358 GameRules_scoring_add(player, CTF_DROPS, 1);
361 if(autocvar_g_ctf_flag_dropped_waypoint) {
362 entity wp = WaypointSprite_Spawn(WP_FlagDropped, 0, 0, flag, FLAG_WAYPOINT_OFFSET, NULL, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, true, RADARICON_FLAG);
363 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
366 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
368 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
369 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
372 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
374 if(droptype == DROP_PASS)
376 flag.pass_distance = 0;
377 flag.pass_sender = NULL;
378 flag.pass_target = NULL;
382 void ctf_Handle_Retrieve(entity flag, entity player)
384 entity sender = flag.pass_sender;
386 // transfer flag to player
388 flag.owner.flagcarried = flag;
389 GameRules_scoring_vip(player, true);
394 setattachment(flag, player.vehicle, "");
395 setorigin(flag, VEHICLE_FLAG_OFFSET);
396 flag.scale = VEHICLE_FLAG_SCALE;
400 setattachment(flag, player, "");
401 setorigin(flag, FLAG_CARRY_OFFSET);
403 set_movetype(flag, MOVETYPE_NONE);
404 flag.takedamage = DAMAGE_NO;
405 flag.solid = SOLID_NOT;
406 flag.angles = '0 0 0';
407 flag.ctf_status = FLAG_CARRY;
409 // messages and sounds
410 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
411 ctf_EventLog("receive", flag.team, player);
413 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
415 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
416 else if(it == player)
417 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
418 else if(SAME_TEAM(it, sender))
419 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
422 // create new waypoint
423 ctf_FlagcarrierWaypoints(player);
425 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
426 player.throw_antispam = sender.throw_antispam;
428 flag.pass_distance = 0;
429 flag.pass_sender = NULL;
430 flag.pass_target = NULL;
433 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
435 entity flag = player.flagcarried;
436 vector targ_origin, flag_velocity;
438 if(!flag) { return; }
439 if((droptype == DROP_PASS) && !receiver) { return; }
441 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
444 setattachment(flag, NULL, "");
445 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
446 flag.owner.flagcarried = NULL;
447 GameRules_scoring_vip(flag.owner, false);
449 flag.solid = SOLID_TRIGGER;
450 flag.ctf_dropper = player;
451 flag.ctf_droptime = time;
452 navigation_dynamicgoal_set(flag);
454 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
461 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
462 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
463 WarpZone_RefSys_Copy(flag, receiver);
464 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
465 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
467 flag.pass_distance = vlen((('1 0 0' * targ_origin.x) + ('0 1 0' * targ_origin.y)) - (('1 0 0' * player.origin.x) + ('0 1 0' * player.origin.y))); // for the sake of this check, exclude Z axis
468 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
471 set_movetype(flag, MOVETYPE_FLY);
472 flag.takedamage = DAMAGE_NO;
473 flag.pass_sender = player;
474 flag.pass_target = receiver;
475 flag.ctf_status = FLAG_PASSING;
478 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
479 WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
480 ctf_EventLog("pass", flag.team, player);
486 makevectors((player.v_angle.y * '0 1 0') + (bound(autocvar_g_ctf_throw_angle_min, player.v_angle.x, autocvar_g_ctf_throw_angle_max) * '1 0 0'));
488 flag_velocity = (('0 0 1' * autocvar_g_ctf_throw_velocity_up) + ((v_forward * autocvar_g_ctf_throw_velocity_forward) * ((player.items & ITEM_Strength.m_itemid) ? autocvar_g_ctf_throw_strengthmultiplier : 1)));
489 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
490 ctf_Handle_Drop(flag, player, droptype);
496 flag.velocity = '0 0 0'; // do nothing
503 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);
504 ctf_Handle_Drop(flag, player, droptype);
509 // kill old waypointsprite
510 WaypointSprite_Ping(player.wps_flagcarrier);
511 WaypointSprite_Kill(player.wps_flagcarrier);
513 if(player.wps_enemyflagcarrier)
514 WaypointSprite_Kill(player.wps_enemyflagcarrier);
516 if(player.wps_flagreturn)
517 WaypointSprite_Kill(player.wps_flagreturn);
520 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
523 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
525 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
532 void nades_GiveBonus(entity player, float score);
534 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
536 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
537 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
538 entity player_team_flag = NULL, tmp_entity;
539 float old_time, new_time;
541 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
542 if(CTF_DIFFTEAM(player, flag)) { return; }
543 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)
545 if (toucher.goalentity == flag.bot_basewaypoint)
546 toucher.goalentity_lock_timeout = 0;
549 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
550 if(SAME_TEAM(tmp_entity, player))
552 player_team_flag = tmp_entity;
556 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
558 player.throw_prevtime = time;
559 player.throw_count = 0;
561 // messages and sounds
562 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
563 ctf_CaptureRecord(enemy_flag, player);
564 _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);
568 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
569 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
575 if(enemy_flag.score_capture || flag.score_capture)
576 pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
577 GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
579 if(enemy_flag.score_team_capture || flag.score_team_capture)
580 capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
581 GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
583 old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
584 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
585 if(!old_time || new_time < old_time)
586 GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
589 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
590 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
593 if(capturetype == CAPTURE_NORMAL)
595 WaypointSprite_Kill(player.wps_flagcarrier);
596 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
598 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
599 { GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist)); }
602 flag.enemy = toucher;
605 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
606 ctf_RespawnFlag(enemy_flag);
609 void ctf_Handle_Return(entity flag, entity player)
611 // messages and sounds
612 if(IS_MONSTER(player))
614 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
618 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
619 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
621 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
622 ctf_EventLog("return", flag.team, player);
625 if(IS_PLAYER(player))
627 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
628 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
630 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
633 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
637 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
638 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
639 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
643 if(player.flagcarried == flag)
644 WaypointSprite_Kill(player.wps_flagcarrier);
649 ctf_RespawnFlag(flag);
652 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
655 float pickup_dropped_score; // used to calculate dropped pickup score
657 // attach the flag to the player
659 player.flagcarried = flag;
660 GameRules_scoring_vip(player, true);
663 setattachment(flag, player.vehicle, "");
664 setorigin(flag, VEHICLE_FLAG_OFFSET);
665 flag.scale = VEHICLE_FLAG_SCALE;
669 setattachment(flag, player, "");
670 setorigin(flag, FLAG_CARRY_OFFSET);
674 set_movetype(flag, MOVETYPE_NONE);
675 flag.takedamage = DAMAGE_NO;
676 flag.solid = SOLID_NOT;
677 flag.angles = '0 0 0';
678 flag.ctf_status = FLAG_CARRY;
682 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
683 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
687 // messages and sounds
688 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
690 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
692 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
693 else if(CTF_DIFFTEAM(player, flag))
694 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
696 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
698 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
701 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); });
704 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
705 if(CTF_SAMETEAM(flag, it))
706 if(SAME_TEAM(player, it))
707 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
709 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);
712 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
715 GameRules_scoring_add(player, CTF_PICKUPS, 1);
716 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
721 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
722 ctf_EventLog("steal", flag.team, player);
728 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);
729 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);
730 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
731 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
732 ctf_EventLog("pickup", flag.team, player);
740 if(pickuptype == PICKUP_BASE)
742 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
743 if((player.speedrunning) && (ctf_captimerecord))
744 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
748 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
751 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
752 ctf_FlagcarrierWaypoints(player);
753 WaypointSprite_Ping(player.wps_flagcarrier);
757 // ===================
758 // Main Flag Functions
759 // ===================
761 void ctf_CheckFlagReturn(entity flag, int returntype)
763 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
765 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
767 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
772 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
774 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
775 case RETURN_SPEEDRUN:
776 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
777 case RETURN_NEEDKILL:
778 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
781 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
783 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
784 ctf_EventLog("returned", flag.team, NULL);
786 ctf_RespawnFlag(flag);
791 bool ctf_Stalemate_Customize(entity this, entity client)
793 // make spectators see what the player would see
794 entity e = WaypointSprite_getviewentity(client);
795 entity wp_owner = this.owner;
798 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
799 if(SAME_TEAM(wp_owner, e)) { return false; }
800 if(!IS_PLAYER(e)) { return false; }
805 void ctf_CheckStalemate()
808 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
811 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
813 // build list of stale flags
814 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
816 if(autocvar_g_ctf_stalemate)
817 if(tmp_entity.ctf_status != FLAG_BASE)
818 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
820 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
821 ctf_staleflaglist = tmp_entity;
823 switch(tmp_entity.team)
825 case NUM_TEAM_1: ++stale_red_flags; break;
826 case NUM_TEAM_2: ++stale_blue_flags; break;
827 case NUM_TEAM_3: ++stale_yellow_flags; break;
828 case NUM_TEAM_4: ++stale_pink_flags; break;
829 default: ++stale_neutral_flags; break;
835 stale_flags = (stale_neutral_flags >= 1);
837 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
839 if(ctf_oneflag && stale_flags == 1)
840 ctf_stalemate = true;
841 else if(stale_flags >= 2)
842 ctf_stalemate = true;
843 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
844 { ctf_stalemate = false; wpforenemy_announced = false; }
845 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
846 { ctf_stalemate = false; wpforenemy_announced = false; }
848 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
851 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
853 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
855 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);
856 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
857 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
861 if (!wpforenemy_announced)
863 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)); });
865 wpforenemy_announced = true;
870 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
872 if(ITEM_DAMAGE_NEEDKILL(deathtype))
874 if(autocvar_g_ctf_flag_return_damage_delay)
875 this.ctf_flagdamaged_byworld = true;
879 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
883 if(autocvar_g_ctf_flag_return_damage)
885 // reduce health and check if it should be returned
886 this.health = this.health - damage;
887 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
892 void ctf_FlagThink(entity this)
897 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
900 if(this == ctf_worldflaglist) // only for the first flag
901 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
904 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
905 LOG_TRACE("wtf the flag got squashed?");
906 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
907 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
908 setsize(this, this.m_mins, this.m_maxs);
912 switch(this.ctf_status)
916 if(autocvar_g_ctf_dropped_capture_radius)
918 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
919 if(tmp_entity.ctf_status == FLAG_DROPPED)
920 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
921 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
922 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
929 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
931 if(autocvar_g_ctf_flag_dropped_floatinwater)
933 vector midpoint = ((this.absmin + this.absmax) * 0.5);
934 if(pointcontents(midpoint) == CONTENT_WATER)
936 this.velocity = this.velocity * 0.5;
938 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
939 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
941 { set_movetype(this, MOVETYPE_FLY); }
943 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
945 if(autocvar_g_ctf_flag_return_dropped)
947 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
950 ctf_CheckFlagReturn(this, RETURN_DROPPED);
954 if(this.ctf_flagdamaged_byworld)
956 this.health -= ((this.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
957 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
960 else if(autocvar_g_ctf_flag_return_time)
962 this.health -= ((this.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
963 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
971 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
974 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
976 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
977 ImpulseCommands(this.owner);
979 if(autocvar_g_ctf_stalemate)
981 if(time >= wpforenemy_nextthink)
983 ctf_CheckStalemate();
984 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
987 if(CTF_SAMETEAM(this, this.owner) && this.team)
989 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
990 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
991 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
992 ctf_Handle_Return(this, this.owner);
999 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1000 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1001 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1003 if((this.pass_target == NULL)
1004 || (IS_DEAD(this.pass_target))
1005 || (this.pass_target.flagcarried)
1006 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1007 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1008 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1010 // give up, pass failed
1011 ctf_Handle_Drop(this, NULL, DROP_PASS);
1015 // still a viable target, go for it
1016 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1021 default: // this should never happen
1023 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1029 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1032 if(game_stopped) return;
1033 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1035 bool is_not_monster = (!IS_MONSTER(toucher));
1037 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1038 if(ITEM_TOUCH_NEEDKILL())
1040 if(!autocvar_g_ctf_flag_return_damage_delay)
1043 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1045 if(!flag.ctf_flagdamaged_byworld) { return; }
1048 // special touch behaviors
1049 if(STAT(FROZEN, toucher)) { return; }
1050 else if(IS_VEHICLE(toucher))
1052 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1053 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1055 return; // do nothing
1057 else if(IS_MONSTER(toucher))
1059 if(!autocvar_g_ctf_allow_monster_touch)
1060 return; // do nothing
1062 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1064 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1066 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1067 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1068 flag.wait = time + FLAG_TOUCHRATE;
1072 else if(IS_DEAD(toucher)) { return; }
1074 switch(flag.ctf_status)
1080 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1081 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1082 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1083 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1085 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1086 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1087 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)
1089 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1090 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1092 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1093 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1099 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1100 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1101 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1102 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1108 LOG_TRACE("Someone touched a flag even though it was being carried?");
1114 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1116 if(DIFF_TEAM(toucher, flag.pass_sender))
1118 if(ctf_Immediate_Return_Allowed(flag, toucher))
1119 ctf_Handle_Return(flag, toucher);
1120 else if(is_not_monster && (!toucher.flagcarried))
1121 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1123 else if(!toucher.flagcarried)
1124 ctf_Handle_Retrieve(flag, toucher);
1131 .float last_respawn;
1132 void ctf_RespawnFlag(entity flag)
1134 // check for flag respawn being called twice in a row
1135 if(flag.last_respawn > time - 0.5)
1136 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1138 flag.last_respawn = time;
1140 // reset the player (if there is one)
1141 if((flag.owner) && (flag.owner.flagcarried == flag))
1143 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1144 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1145 WaypointSprite_Kill(flag.wps_flagcarrier);
1147 flag.owner.flagcarried = NULL;
1148 GameRules_scoring_vip(flag.owner, false);
1150 if(flag.speedrunning)
1151 ctf_FakeTimeLimit(flag.owner, -1);
1154 if((flag.owner) && (flag.owner.vehicle))
1155 flag.scale = FLAG_SCALE;
1157 if(flag.ctf_status == FLAG_DROPPED)
1158 { WaypointSprite_Kill(flag.wps_flagdropped); }
1161 setattachment(flag, NULL, "");
1162 setorigin(flag, flag.ctf_spawnorigin);
1164 set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS));
1165 flag.takedamage = DAMAGE_NO;
1166 flag.health = flag.max_flag_health;
1167 flag.solid = SOLID_TRIGGER;
1168 flag.velocity = '0 0 0';
1169 flag.angles = flag.mangle;
1170 flag.flags = FL_ITEM | FL_NOTARGET;
1172 flag.ctf_status = FLAG_BASE;
1174 flag.pass_distance = 0;
1175 flag.pass_sender = NULL;
1176 flag.pass_target = NULL;
1177 flag.ctf_dropper = NULL;
1178 flag.ctf_pickuptime = 0;
1179 flag.ctf_droptime = 0;
1180 flag.ctf_flagdamaged_byworld = false;
1181 navigation_dynamicgoal_unset(flag);
1183 ctf_CheckStalemate();
1186 void ctf_Reset(entity this)
1188 if(this.owner && IS_PLAYER(this.owner))
1189 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1192 ctf_RespawnFlag(this);
1195 bool ctf_FlagBase_Customize(entity this, entity client)
1197 entity e = WaypointSprite_getviewentity(client);
1198 entity wp_owner = this.owner;
1199 entity flag = e.flagcarried;
1200 if(flag && CTF_SAMETEAM(e, flag))
1202 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1207 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1210 waypoint_spawnforitem_force(this, this.origin);
1211 navigation_dynamicgoal_init(this, true);
1217 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1218 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1219 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1220 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1221 default: basename = WP_FlagBaseNeutral; break;
1224 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1225 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1226 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1227 setcefc(wp, ctf_FlagBase_Customize);
1229 // captureshield setup
1230 ctf_CaptureShield_Spawn(this);
1235 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1238 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1239 ctf_worldflaglist = flag;
1241 setattachment(flag, NULL, "");
1243 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1244 flag.team = teamnumber;
1245 flag.classname = "item_flag_team";
1246 flag.target = "###item###"; // wut?
1247 flag.flags = FL_ITEM | FL_NOTARGET;
1248 IL_PUSH(g_items, flag);
1249 flag.solid = SOLID_TRIGGER;
1250 flag.takedamage = DAMAGE_NO;
1251 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1252 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1253 flag.health = flag.max_flag_health;
1254 flag.event_damage = ctf_FlagDamage;
1255 flag.pushable = true;
1256 flag.teleportable = TELEPORT_NORMAL;
1257 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1258 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1259 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1260 if(flag.damagedbycontents)
1261 IL_PUSH(g_damagedbycontents, flag);
1262 flag.velocity = '0 0 0';
1263 flag.mangle = flag.angles;
1264 flag.reset = ctf_Reset;
1265 settouch(flag, ctf_FlagTouch);
1266 setthink(flag, ctf_FlagThink);
1267 flag.nextthink = time + FLAG_THINKRATE;
1268 flag.ctf_status = FLAG_BASE;
1270 // crudely force them all to 0
1271 if(autocvar_g_ctf_score_ignore_fields)
1272 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1274 string teamname = Static_Team_ColorName_Lower(teamnumber);
1276 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1277 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1278 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1279 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnumber).eent_eff_name; }
1280 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnumber).eent_eff_name; }
1281 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnumber).eent_eff_name; }
1285 if(flag.s == "") flag.s = b; \
1286 precache_sound(flag.s);
1288 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnumber))))
1289 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnumber))))
1290 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnumber))))
1291 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnumber))))
1292 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1293 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1294 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1298 precache_model(flag.model);
1301 _setmodel(flag, flag.model); // precision set below
1302 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1303 flag.m_mins = flag.mins; // store these for squash checks
1304 flag.m_maxs = flag.maxs;
1305 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1307 if(autocvar_g_ctf_flag_glowtrails)
1311 case NUM_TEAM_1: flag.glow_color = 251; break;
1312 case NUM_TEAM_2: flag.glow_color = 210; break;
1313 case NUM_TEAM_3: flag.glow_color = 110; break;
1314 case NUM_TEAM_4: flag.glow_color = 145; break;
1315 default: flag.glow_color = 254; break;
1317 flag.glow_size = 25;
1318 flag.glow_trail = 1;
1321 flag.effects |= EF_LOWPRECISION;
1322 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1323 if(autocvar_g_ctf_dynamiclights)
1327 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1328 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1329 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1330 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1331 default: flag.effects |= EF_DIMLIGHT; break;
1336 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1338 flag.dropped_origin = flag.origin;
1339 flag.noalign = true;
1340 set_movetype(flag, MOVETYPE_NONE);
1342 else // drop to floor, automatically find a platform and set that as spawn origin
1344 flag.noalign = false;
1346 set_movetype(flag, MOVETYPE_NONE);
1349 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1357 // NOTE: LEGACY CODE, needs to be re-written!
1359 void havocbot_ctf_calculate_middlepoint()
1363 vector fo = '0 0 0';
1366 f = ctf_worldflaglist;
1371 f = f.ctf_worldflagnext;
1377 havocbot_middlepoint = s / n;
1378 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1380 havocbot_symmetry_axis_m = 0;
1381 havocbot_symmetry_axis_q = 0;
1384 // for symmetrical editing of waypoints
1385 entity f1 = ctf_worldflaglist;
1386 entity f2 = f1.ctf_worldflagnext;
1387 float m = -(f1.origin.y - f2.origin.y) / (f1.origin.x - f2.origin.x);
1388 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1389 havocbot_symmetry_axis_m = m;
1390 havocbot_symmetry_axis_q = q;
1392 havocbot_symmetry_origin_order = n;
1396 entity havocbot_ctf_find_flag(entity bot)
1399 f = ctf_worldflaglist;
1402 if (CTF_SAMETEAM(bot, f))
1404 f = f.ctf_worldflagnext;
1409 entity havocbot_ctf_find_enemy_flag(entity bot)
1412 f = ctf_worldflaglist;
1417 if(CTF_DIFFTEAM(bot, f))
1424 else if(!bot.flagcarried)
1428 else if (CTF_DIFFTEAM(bot, f))
1430 f = f.ctf_worldflagnext;
1435 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1442 FOREACH_CLIENT(IS_PLAYER(it), {
1443 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1446 if(vdist(it.origin - org, <, tc_radius))
1455 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1458 head = ctf_worldflaglist;
1461 if (CTF_SAMETEAM(this, head))
1463 head = head.ctf_worldflagnext;
1466 navigation_routerating(this, head, ratingscale, 10000);
1470 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1473 head = ctf_worldflaglist;
1476 if (CTF_SAMETEAM(this, head))
1478 if (this.flagcarried)
1479 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1481 head = head.ctf_worldflagnext; // skip base if it has a different group
1486 head = head.ctf_worldflagnext;
1491 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1494 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1497 head = ctf_worldflaglist;
1502 if(CTF_DIFFTEAM(this, head))
1506 if(this.flagcarried)
1509 else if(!this.flagcarried)
1513 else if(CTF_DIFFTEAM(this, head))
1515 head = head.ctf_worldflagnext;
1519 if (head.ctf_status == FLAG_CARRY)
1521 // adjust rating of our flag carrier depending on his health
1522 head = head.tag_entity;
1523 float f = bound(0, (head.health + head.armorvalue) / 100, 2) - 1;
1524 ratingscale += ratingscale * f * 0.1;
1526 navigation_routerating(this, head, ratingscale, 10000);
1530 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1532 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1534 if (!bot_waypoints_for_items)
1536 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1542 head = havocbot_ctf_find_enemy_flag(this);
1547 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1550 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1554 mf = havocbot_ctf_find_flag(this);
1556 if(mf.ctf_status == FLAG_BASE)
1560 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1563 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1566 head = ctf_worldflaglist;
1569 // flag is out in the field
1570 if(head.ctf_status != FLAG_BASE)
1571 if(head.tag_entity==NULL) // dropped
1575 if(vdist(org - head.origin, <, df_radius))
1576 navigation_routerating(this, head, ratingscale, 10000);
1579 navigation_routerating(this, head, ratingscale, 10000);
1582 head = head.ctf_worldflagnext;
1586 void havocbot_ctf_reset_role(entity this)
1588 float cdefense, cmiddle, coffense;
1595 if (this.flagcarried)
1597 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1601 mf = havocbot_ctf_find_flag(this);
1602 ef = havocbot_ctf_find_enemy_flag(this);
1604 // Retrieve stolen flag
1605 if(mf.ctf_status!=FLAG_BASE)
1607 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1611 // If enemy flag is taken go to the middle to intercept pursuers
1612 if(ef.ctf_status!=FLAG_BASE)
1614 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1618 // if there is no one else on the team switch to offense
1620 // don't check if this bot is a player since it isn't true when the bot is added to the server
1621 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1625 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1628 else if (CS(this).jointime < time + 1)
1630 // if bots spawn all at once set good default roles
1633 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1636 else if (count == 2)
1638 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1643 // Evaluate best position to take
1644 // Count mates on middle position
1645 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1647 // Count mates on defense position
1648 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1650 // Count mates on offense position
1651 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1653 if(cdefense<=coffense)
1654 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1655 else if(coffense<=cmiddle)
1656 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1658 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1660 // if bots spawn all at once assign them a more appropriated role after a while
1661 if (CS(this).jointime < time + 1 && count > 2)
1662 this.havocbot_role_timeout = time + 10 + random() * 10;
1665 bool havocbot_ctf_is_basewaypoint(entity item)
1667 if (item.classname != "waypoint")
1670 entity head = ctf_worldflaglist;
1673 if (item == head.bot_basewaypoint)
1675 head = head.ctf_worldflagnext;
1680 void havocbot_role_ctf_carrier(entity this)
1684 havocbot_ctf_reset_role(this);
1688 if (this.flagcarried == NULL)
1690 havocbot_ctf_reset_role(this);
1694 if (navigation_goalrating_timeout(this))
1696 navigation_goalrating_start(this);
1699 entity mf = havocbot_ctf_find_flag(this);
1700 vector base_org = mf.dropped_origin;
1701 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1703 havocbot_goalrating_ctf_enemybase(this, base_rating);
1705 havocbot_goalrating_ctf_ourbase(this, base_rating);
1707 // start collecting items very close to the bot but only inside of own base radius
1708 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1709 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1711 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1713 navigation_goalrating_end(this);
1715 navigation_goalrating_timeout_set(this);
1717 entity goal = this.goalentity;
1718 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1719 this.goalentity_lock_timeout = time + ((this.bot_aimtarg) ? 2 : 3);
1722 this.havocbot_cantfindflag = time + 10;
1723 else if (time > this.havocbot_cantfindflag)
1725 // Can't navigate to my own base, suicide!
1726 // TODO: drop it and wander around
1727 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1733 void havocbot_role_ctf_escort(entity this)
1739 havocbot_ctf_reset_role(this);
1743 if (this.flagcarried)
1745 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1749 // If enemy flag is back on the base switch to previous role
1750 ef = havocbot_ctf_find_enemy_flag(this);
1751 if(ef.ctf_status==FLAG_BASE)
1753 this.havocbot_role = this.havocbot_previous_role;
1754 this.havocbot_role_timeout = 0;
1757 if (ef.ctf_status == FLAG_DROPPED)
1759 navigation_goalrating_timeout_expire(this, 1);
1763 // If the flag carrier reached the base switch to defense
1764 mf = havocbot_ctf_find_flag(this);
1765 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1767 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1771 // Set the role timeout if necessary
1772 if (!this.havocbot_role_timeout)
1774 this.havocbot_role_timeout = time + random() * 30 + 60;
1777 // If nothing happened just switch to previous role
1778 if (time > this.havocbot_role_timeout)
1780 this.havocbot_role = this.havocbot_previous_role;
1781 this.havocbot_role_timeout = 0;
1785 // Chase the flag carrier
1786 if (navigation_goalrating_timeout(this))
1788 navigation_goalrating_start(this);
1791 havocbot_goalrating_ctf_enemyflag(this, 10000);
1792 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1793 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1795 navigation_goalrating_end(this);
1797 navigation_goalrating_timeout_set(this);
1801 void havocbot_role_ctf_offense(entity this)
1808 havocbot_ctf_reset_role(this);
1812 if (this.flagcarried)
1814 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1819 mf = havocbot_ctf_find_flag(this);
1820 ef = havocbot_ctf_find_enemy_flag(this);
1823 if(mf.ctf_status!=FLAG_BASE)
1826 pos = mf.tag_entity.origin;
1830 // Try to get it if closer than the enemy base
1831 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1833 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1838 // Escort flag carrier
1839 if(ef.ctf_status!=FLAG_BASE)
1842 pos = ef.tag_entity.origin;
1846 if(vdist(pos - mf.dropped_origin, >, 700))
1848 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1853 // Set the role timeout if necessary
1854 if (!this.havocbot_role_timeout)
1855 this.havocbot_role_timeout = time + 120;
1857 if (time > this.havocbot_role_timeout)
1859 havocbot_ctf_reset_role(this);
1863 if (navigation_goalrating_timeout(this))
1865 navigation_goalrating_start(this);
1868 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1869 havocbot_goalrating_ctf_enemybase(this, 10000);
1870 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1872 navigation_goalrating_end(this);
1874 navigation_goalrating_timeout_set(this);
1878 // Retriever (temporary role):
1879 void havocbot_role_ctf_retriever(entity this)
1885 havocbot_ctf_reset_role(this);
1889 if (this.flagcarried)
1891 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1895 // If flag is back on the base switch to previous role
1896 mf = havocbot_ctf_find_flag(this);
1897 if(mf.ctf_status==FLAG_BASE)
1899 if (mf.enemy == this) // did this bot return the flag?
1900 navigation_goalrating_timeout_force(this);
1901 havocbot_ctf_reset_role(this);
1905 if (!this.havocbot_role_timeout)
1906 this.havocbot_role_timeout = time + 20;
1908 if (time > this.havocbot_role_timeout)
1910 havocbot_ctf_reset_role(this);
1914 if (navigation_goalrating_timeout(this))
1916 const float RT_RADIUS = 10000;
1918 navigation_goalrating_start(this);
1921 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1922 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1923 havocbot_goalrating_ctf_enemybase(this, 8000);
1924 entity ef = havocbot_ctf_find_enemy_flag(this);
1925 vector enemy_base_org = ef.dropped_origin;
1926 // start collecting items very close to the bot but only inside of enemy base radius
1927 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1928 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1929 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1931 navigation_goalrating_end(this);
1933 navigation_goalrating_timeout_set(this);
1937 void havocbot_role_ctf_middle(entity this)
1943 havocbot_ctf_reset_role(this);
1947 if (this.flagcarried)
1949 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1953 mf = havocbot_ctf_find_flag(this);
1954 if(mf.ctf_status!=FLAG_BASE)
1956 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1960 if (!this.havocbot_role_timeout)
1961 this.havocbot_role_timeout = time + 10;
1963 if (time > this.havocbot_role_timeout)
1965 havocbot_ctf_reset_role(this);
1969 if (navigation_goalrating_timeout(this))
1973 org = havocbot_middlepoint;
1974 org.z = this.origin.z;
1976 navigation_goalrating_start(this);
1979 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
1980 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
1981 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
1982 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
1983 havocbot_goalrating_items(this, 18000, this.origin, 10000);
1984 havocbot_goalrating_ctf_enemybase(this, 3000);
1986 navigation_goalrating_end(this);
1988 entity goal = this.goalentity;
1989 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1990 this.goalentity_lock_timeout = time + 2;
1992 navigation_goalrating_timeout_set(this);
1996 void havocbot_role_ctf_defense(entity this)
2002 havocbot_ctf_reset_role(this);
2006 if (this.flagcarried)
2008 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2012 // If own flag was captured
2013 mf = havocbot_ctf_find_flag(this);
2014 if(mf.ctf_status!=FLAG_BASE)
2016 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2020 if (!this.havocbot_role_timeout)
2021 this.havocbot_role_timeout = time + 30;
2023 if (time > this.havocbot_role_timeout)
2025 havocbot_ctf_reset_role(this);
2028 if (navigation_goalrating_timeout(this))
2030 vector org = mf.dropped_origin;
2032 navigation_goalrating_start(this);
2034 // if enemies are closer to our base, go there
2035 entity closestplayer = NULL;
2036 float distance, bestdistance = 10000;
2037 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2038 distance = vlen(org - it.origin);
2039 if(distance<bestdistance)
2042 bestdistance = distance;
2048 if(DIFF_TEAM(closestplayer, this))
2049 if(vdist(org - this.origin, >, 1000))
2050 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2051 havocbot_goalrating_ctf_ourbase(this, 10000);
2053 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2054 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2055 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2056 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2057 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2059 navigation_goalrating_end(this);
2061 navigation_goalrating_timeout_set(this);
2065 void havocbot_role_ctf_setrole(entity bot, int role)
2067 string s = "(null)";
2070 case HAVOCBOT_CTF_ROLE_CARRIER:
2072 bot.havocbot_role = havocbot_role_ctf_carrier;
2073 bot.havocbot_role_timeout = 0;
2074 bot.havocbot_cantfindflag = time + 10;
2075 if (bot.havocbot_previous_role != bot.havocbot_role)
2076 navigation_goalrating_timeout_force(bot);
2078 case HAVOCBOT_CTF_ROLE_DEFENSE:
2080 bot.havocbot_role = havocbot_role_ctf_defense;
2081 bot.havocbot_role_timeout = 0;
2083 case HAVOCBOT_CTF_ROLE_MIDDLE:
2085 bot.havocbot_role = havocbot_role_ctf_middle;
2086 bot.havocbot_role_timeout = 0;
2088 case HAVOCBOT_CTF_ROLE_OFFENSE:
2090 bot.havocbot_role = havocbot_role_ctf_offense;
2091 bot.havocbot_role_timeout = 0;
2093 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2095 bot.havocbot_previous_role = bot.havocbot_role;
2096 bot.havocbot_role = havocbot_role_ctf_retriever;
2097 bot.havocbot_role_timeout = time + 10;
2098 if (bot.havocbot_previous_role != bot.havocbot_role)
2099 navigation_goalrating_timeout_expire(bot, 2);
2101 case HAVOCBOT_CTF_ROLE_ESCORT:
2103 bot.havocbot_previous_role = bot.havocbot_role;
2104 bot.havocbot_role = havocbot_role_ctf_escort;
2105 bot.havocbot_role_timeout = time + 30;
2106 if (bot.havocbot_previous_role != bot.havocbot_role)
2107 navigation_goalrating_timeout_expire(bot, 2);
2110 LOG_TRACE(bot.netname, " switched to ", s);
2118 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2120 entity player = M_ARGV(0, entity);
2122 int t = 0, t2 = 0, t3 = 0;
2123 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)
2125 // initially clear items so they can be set as necessary later.
2126 STAT(CTF_FLAGSTATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2127 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2128 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2129 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2130 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2131 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2133 // scan through all the flags and notify the client about them
2134 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2136 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2137 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2138 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2139 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2140 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; }
2142 switch(flag.ctf_status)
2147 if((flag.owner == player) || (flag.pass_sender == player))
2148 STAT(CTF_FLAGSTATUS, player) |= t; // carrying: player is currently carrying the flag
2150 STAT(CTF_FLAGSTATUS, player) |= t2; // taken: someone else is carrying the flag
2155 STAT(CTF_FLAGSTATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2161 // item for stopping players from capturing the flag too often
2162 if(player.ctf_captureshielded)
2163 STAT(CTF_FLAGSTATUS, player) |= CTF_SHIELDED;
2166 STAT(CTF_FLAGSTATUS, player) |= CTF_STALEMATE;
2168 // update the health of the flag carrier waypointsprite
2169 if(player.wps_flagcarrier)
2170 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
2173 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2175 entity frag_attacker = M_ARGV(1, entity);
2176 entity frag_target = M_ARGV(2, entity);
2177 float frag_damage = M_ARGV(4, float);
2178 vector frag_force = M_ARGV(6, vector);
2180 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2182 if(frag_target == frag_attacker) // damage done to yourself
2184 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2185 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2187 else // damage done to everyone else
2189 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2190 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2193 M_ARGV(4, float) = frag_damage;
2194 M_ARGV(6, vector) = frag_force;
2196 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2198 if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id)))
2199 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2201 frag_target.wps_helpme_time = time;
2202 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2204 // todo: add notification for when flag carrier needs help?
2208 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2210 entity frag_attacker = M_ARGV(1, entity);
2211 entity frag_target = M_ARGV(2, entity);
2213 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2215 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2216 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2219 if(frag_target.flagcarried)
2221 entity tmp_entity = frag_target.flagcarried;
2222 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2223 tmp_entity.ctf_dropper = NULL;
2227 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2229 M_ARGV(2, float) = 0; // frag score
2230 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2233 void ctf_RemovePlayer(entity player)
2235 if(player.flagcarried)
2236 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2238 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2240 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2241 if(flag.pass_target == player) { flag.pass_target = NULL; }
2242 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2246 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2248 entity player = M_ARGV(0, entity);
2250 ctf_RemovePlayer(player);
2253 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2255 entity player = M_ARGV(0, entity);
2257 ctf_RemovePlayer(player);
2260 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2262 if(!autocvar_g_ctf_leaderboard)
2265 entity player = M_ARGV(0, entity);
2267 if(IS_REAL_CLIENT(player))
2269 int m = min(RANKINGS_CNT, autocvar_g_cts_send_rankings_cnt);
2270 race_send_rankings_cnt(MSG_ONE);
2271 for (int i = 1; i <= m; ++i)
2273 race_SendRankings(i, 0, 0, MSG_ONE);
2278 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2280 if(!autocvar_g_ctf_leaderboard)
2283 entity player = M_ARGV(0, entity);
2285 if(CS(player).cvar_cl_allow_uidtracking == 1 && CS(player).cvar_cl_allow_uid2name == 1)
2287 if (!player.stored_netname)
2288 player.stored_netname = strzone(uid2name(player.crypto_idfp));
2289 if(player.stored_netname != player.netname)
2291 db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
2292 strcpy(player.stored_netname, player.netname);
2297 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2299 entity player = M_ARGV(0, entity);
2301 if(player.flagcarried)
2302 if(!autocvar_g_ctf_portalteleport)
2303 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2306 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2308 if(MUTATOR_RETURNVALUE || game_stopped) return;
2310 entity player = M_ARGV(0, entity);
2312 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2314 // pass the flag to a team mate
2315 if(autocvar_g_ctf_pass)
2317 entity head, closest_target = NULL;
2318 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2320 while(head) // find the closest acceptable target to pass to
2322 if(IS_PLAYER(head) && !IS_DEAD(head))
2323 if(head != player && SAME_TEAM(head, player))
2324 if(!head.speedrunning && !head.vehicle)
2326 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2327 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2328 vector passer_center = CENTER_OR_VIEWOFS(player);
2330 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2332 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2334 if(IS_BOT_CLIENT(head))
2336 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2337 ctf_Handle_Throw(head, player, DROP_PASS);
2341 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2342 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2344 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2347 else if(player.flagcarried && !head.flagcarried)
2351 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2352 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2353 { closest_target = head; }
2355 else { closest_target = head; }
2362 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2365 // throw the flag in front of you
2366 if(autocvar_g_ctf_throw && player.flagcarried)
2368 if(player.throw_count == -1)
2370 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2372 player.throw_prevtime = time;
2373 player.throw_count = 1;
2374 ctf_Handle_Throw(player, NULL, DROP_THROW);
2379 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2385 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2386 else { player.throw_count += 1; }
2387 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2389 player.throw_prevtime = time;
2390 ctf_Handle_Throw(player, NULL, DROP_THROW);
2397 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2399 entity player = M_ARGV(0, entity);
2401 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2403 player.wps_helpme_time = time;
2404 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2406 else // create a normal help me waypointsprite
2408 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2409 WaypointSprite_Ping(player.wps_helpme);
2415 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2417 entity player = M_ARGV(0, entity);
2418 entity veh = M_ARGV(1, entity);
2420 if(player.flagcarried)
2422 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2424 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2428 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2429 setattachment(player.flagcarried, veh, "");
2430 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2431 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2432 //player.flagcarried.angles = '0 0 0';
2438 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2440 entity player = M_ARGV(0, entity);
2442 if(player.flagcarried)
2444 setattachment(player.flagcarried, player, "");
2445 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2446 player.flagcarried.scale = FLAG_SCALE;
2447 player.flagcarried.angles = '0 0 0';
2448 player.flagcarried.nodrawtoclient = NULL;
2453 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2455 entity player = M_ARGV(0, entity);
2457 if(player.flagcarried)
2459 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2460 ctf_RespawnFlag(player.flagcarried);
2465 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2467 entity flag; // temporary entity for the search method
2469 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2471 switch(flag.ctf_status)
2476 // lock the flag, game is over
2477 set_movetype(flag, MOVETYPE_NONE);
2478 flag.takedamage = DAMAGE_NO;
2479 flag.solid = SOLID_NOT;
2480 flag.nextthink = false; // stop thinking
2482 //dprint("stopping the ", flag.netname, " from moving.\n");
2490 // do nothing for these flags
2497 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2499 entity bot = M_ARGV(0, entity);
2501 havocbot_ctf_reset_role(bot);
2505 MUTATOR_HOOKFUNCTION(ctf, CheckAllowedTeams)
2507 //M_ARGV(0, float) = ctf_teams;
2508 M_ARGV(1, string) = "ctf_team";
2512 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2514 entity spectatee = M_ARGV(0, entity);
2515 entity client = M_ARGV(1, entity);
2517 STAT(CTF_FLAGSTATUS, client) = STAT(CTF_FLAGSTATUS, spectatee);
2520 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2522 int record_page = M_ARGV(0, int);
2523 string ret_string = M_ARGV(1, string);
2525 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2527 if (MapInfo_Get_ByID(i))
2529 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2535 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2536 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2540 M_ARGV(1, string) = ret_string;
2543 bool superspec_Spectate(entity this, entity targ); // TODO
2544 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2545 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2547 entity player = M_ARGV(0, entity);
2548 string cmd_name = M_ARGV(1, string);
2549 int cmd_argc = M_ARGV(2, int);
2551 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2553 if(cmd_name == "followfc")
2565 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2566 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2567 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2568 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2572 FOREACH_CLIENT(IS_PLAYER(it), {
2573 if(it.flagcarried && (it.team == _team || _team == 0))
2576 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2577 continue; // already spectating this fc, try another
2578 return superspec_Spectate(player, it);
2583 superspec_msg("", "", player, "No active flag carrier\n", 1);
2588 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2590 entity frag_target = M_ARGV(0, entity);
2592 if(frag_target.flagcarried)
2593 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2601 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2602 CTF flag for team one (Red).
2604 "angle" Angle the flag will point (minus 90 degrees)...
2605 "model" model to use, note this needs red and blue as skins 0 and 1...
2606 "noise" sound played when flag is picked up...
2607 "noise1" sound played when flag is returned by a teammate...
2608 "noise2" sound played when flag is captured...
2609 "noise3" sound played when flag is lost in the field and respawns itself...
2610 "noise4" sound played when flag is dropped by a player...
2611 "noise5" sound played when flag touches the ground... */
2612 spawnfunc(item_flag_team1)
2614 if(!g_ctf) { delete(this); return; }
2616 ctf_FlagSetup(NUM_TEAM_1, this);
2619 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2620 CTF flag for team two (Blue).
2622 "angle" Angle the flag will point (minus 90 degrees)...
2623 "model" model to use, note this needs red and blue as skins 0 and 1...
2624 "noise" sound played when flag is picked up...
2625 "noise1" sound played when flag is returned by a teammate...
2626 "noise2" sound played when flag is captured...
2627 "noise3" sound played when flag is lost in the field and respawns itself...
2628 "noise4" sound played when flag is dropped by a player...
2629 "noise5" sound played when flag touches the ground... */
2630 spawnfunc(item_flag_team2)
2632 if(!g_ctf) { delete(this); return; }
2634 ctf_FlagSetup(NUM_TEAM_2, this);
2637 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2638 CTF flag for team three (Yellow).
2640 "angle" Angle the flag will point (minus 90 degrees)...
2641 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2642 "noise" sound played when flag is picked up...
2643 "noise1" sound played when flag is returned by a teammate...
2644 "noise2" sound played when flag is captured...
2645 "noise3" sound played when flag is lost in the field and respawns itself...
2646 "noise4" sound played when flag is dropped by a player...
2647 "noise5" sound played when flag touches the ground... */
2648 spawnfunc(item_flag_team3)
2650 if(!g_ctf) { delete(this); return; }
2652 ctf_FlagSetup(NUM_TEAM_3, this);
2655 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2656 CTF flag for team four (Pink).
2658 "angle" Angle the flag will point (minus 90 degrees)...
2659 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2660 "noise" sound played when flag is picked up...
2661 "noise1" sound played when flag is returned by a teammate...
2662 "noise2" sound played when flag is captured...
2663 "noise3" sound played when flag is lost in the field and respawns itself...
2664 "noise4" sound played when flag is dropped by a player...
2665 "noise5" sound played when flag touches the ground... */
2666 spawnfunc(item_flag_team4)
2668 if(!g_ctf) { delete(this); return; }
2670 ctf_FlagSetup(NUM_TEAM_4, this);
2673 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2676 "angle" Angle the flag will point (minus 90 degrees)...
2677 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2678 "noise" sound played when flag is picked up...
2679 "noise1" sound played when flag is returned by a teammate...
2680 "noise2" sound played when flag is captured...
2681 "noise3" sound played when flag is lost in the field and respawns itself...
2682 "noise4" sound played when flag is dropped by a player...
2683 "noise5" sound played when flag touches the ground... */
2684 spawnfunc(item_flag_neutral)
2686 if(!g_ctf) { delete(this); return; }
2687 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2689 ctf_FlagSetup(0, this);
2692 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2693 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2694 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.
2696 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2697 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2700 if(!g_ctf) { delete(this); return; }
2702 this.classname = "ctf_team";
2703 this.team = this.cnt + 1;
2706 // compatibility for quake maps
2707 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2708 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2709 spawnfunc(info_player_team1);
2710 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2711 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2712 spawnfunc(info_player_team2);
2713 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2714 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2716 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2717 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2719 // compatibility for wop maps
2720 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2721 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2722 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2723 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2724 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2725 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2733 void ctf_ScoreRules(int teams)
2735 CheckAllowedTeams(NULL);
2736 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2737 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2738 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2739 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2740 field(SP_CTF_PICKUPS, "pickups", 0);
2741 field(SP_CTF_FCKILLS, "fckills", 0);
2742 field(SP_CTF_RETURNS, "returns", 0);
2743 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2747 // code from here on is just to support maps that don't have flag and team entities
2748 void ctf_SpawnTeam (string teamname, int teamcolor)
2750 entity this = new_pure(ctf_team);
2751 this.netname = teamname;
2752 this.cnt = teamcolor - 1;
2753 this.spawnfunc_checked = true;
2754 this.team = teamcolor;
2757 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2762 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2764 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2765 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2767 switch(tmp_entity.team)
2769 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2770 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2771 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2772 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2774 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2777 havocbot_ctf_calculate_middlepoint();
2779 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2781 ctf_teams = 0; // so set the default red and blue teams
2782 BITSET_ASSIGN(ctf_teams, BIT(0));
2783 BITSET_ASSIGN(ctf_teams, BIT(1));
2786 //ctf_teams = bound(2, ctf_teams, 4);
2788 // if no teams are found, spawn defaults
2789 if(find(NULL, classname, "ctf_team") == NULL)
2791 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2792 if(ctf_teams & BIT(0))
2793 ctf_SpawnTeam("Red", NUM_TEAM_1);
2794 if(ctf_teams & BIT(1))
2795 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2796 if(ctf_teams & BIT(2))
2797 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2798 if(ctf_teams & BIT(3))
2799 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2802 ctf_ScoreRules(ctf_teams);
2805 void ctf_Initialize()
2807 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2809 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2810 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2811 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2813 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);