3 #include <common/effects/all.qh>
4 #include <common/mapobjects/teleporters.qh>
5 #include <common/mapobjects/triggers.qh>
6 #include <common/vehicles/all.qh>
7 #include <server/command/vote.qh>
8 #include <server/client.qh>
9 #include <server/gamelog.qh>
10 #include <server/intermission.qh>
11 #include <server/damage.qh>
12 #include <server/world.qh>
13 #include <server/items/items.qh>
14 #include <server/race.qh>
15 #include <server/teamplay.qh>
17 #include <lib/warpzone/common.qh>
19 bool autocvar_g_ctf_allow_vehicle_carry;
20 bool autocvar_g_ctf_allow_vehicle_touch;
21 bool autocvar_g_ctf_allow_monster_touch;
22 bool autocvar_g_ctf_throw;
23 float autocvar_g_ctf_throw_angle_max;
24 float autocvar_g_ctf_throw_angle_min;
25 int autocvar_g_ctf_throw_punish_count;
26 float autocvar_g_ctf_throw_punish_delay;
27 float autocvar_g_ctf_throw_punish_time;
28 float autocvar_g_ctf_throw_strengthmultiplier;
29 float autocvar_g_ctf_throw_velocity_forward;
30 float autocvar_g_ctf_throw_velocity_up;
31 float autocvar_g_ctf_drop_velocity_up;
32 float autocvar_g_ctf_drop_velocity_side;
33 bool autocvar_g_ctf_oneflag_reverse;
34 bool autocvar_g_ctf_portalteleport;
35 bool autocvar_g_ctf_pass;
36 float autocvar_g_ctf_pass_arc;
37 float autocvar_g_ctf_pass_arc_max;
38 float autocvar_g_ctf_pass_directional_max;
39 float autocvar_g_ctf_pass_directional_min;
40 float autocvar_g_ctf_pass_radius;
41 float autocvar_g_ctf_pass_wait;
42 bool autocvar_g_ctf_pass_request;
43 float autocvar_g_ctf_pass_turnrate;
44 float autocvar_g_ctf_pass_timelimit;
45 float autocvar_g_ctf_pass_velocity;
46 bool autocvar_g_ctf_dynamiclights;
47 float autocvar_g_ctf_flag_collect_delay;
48 float autocvar_g_ctf_flag_damageforcescale;
49 bool autocvar_g_ctf_flag_dropped_waypoint;
50 bool autocvar_g_ctf_flag_dropped_floatinwater;
51 bool autocvar_g_ctf_flag_glowtrails;
52 int autocvar_g_ctf_flag_health;
53 bool autocvar_g_ctf_flag_return;
54 bool autocvar_g_ctf_flag_return_carrying;
55 float autocvar_g_ctf_flag_return_carried_radius;
56 float autocvar_g_ctf_flag_return_time;
57 bool autocvar_g_ctf_flag_return_when_unreachable;
58 float autocvar_g_ctf_flag_return_damage;
59 float autocvar_g_ctf_flag_return_damage_delay;
60 float autocvar_g_ctf_flag_return_dropped;
61 bool autocvar_g_ctf_flag_waypoint = true;
62 float autocvar_g_ctf_flag_waypoint_maxdistance;
63 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
64 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
65 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
66 float autocvar_g_ctf_flagcarrier_selfforcefactor;
67 float autocvar_g_ctf_flagcarrier_damagefactor;
68 float autocvar_g_ctf_flagcarrier_forcefactor;
69 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
70 bool autocvar_g_ctf_fullbrightflags;
71 bool autocvar_g_ctf_ignore_frags;
72 bool autocvar_g_ctf_score_ignore_fields;
73 int autocvar_g_ctf_score_capture;
74 int autocvar_g_ctf_score_capture_assist;
75 int autocvar_g_ctf_score_kill;
76 int autocvar_g_ctf_score_penalty_drop;
77 int autocvar_g_ctf_score_penalty_returned;
78 int autocvar_g_ctf_score_pickup_base;
79 int autocvar_g_ctf_score_pickup_dropped_early;
80 int autocvar_g_ctf_score_pickup_dropped_late;
81 int autocvar_g_ctf_score_return;
82 float autocvar_g_ctf_shield_force;
83 float autocvar_g_ctf_shield_max_ratio;
84 int autocvar_g_ctf_shield_min_negscore;
85 bool autocvar_g_ctf_stalemate;
86 int autocvar_g_ctf_stalemate_endcondition;
87 float autocvar_g_ctf_stalemate_time;
88 bool autocvar_g_ctf_reverse;
89 float autocvar_g_ctf_dropped_capture_delay;
90 float autocvar_g_ctf_dropped_capture_radius;
92 void ctf_FakeTimeLimit(entity e, float t)
95 WriteByte(MSG_ONE, 3); // svc_updatestat
96 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
98 WriteCoord(MSG_ONE, autocvar_timelimit);
100 WriteCoord(MSG_ONE, (t + 1) / 60);
103 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
105 if(autocvar_sv_eventlog)
106 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != NULL) ? ftos(actor.playerid) : "")));
107 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
110 void ctf_CaptureRecord(entity flag, entity player)
112 float cap_record = ctf_captimerecord;
113 float cap_time = (time - flag.ctf_pickuptime);
114 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
118 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname);
119 else if(!ctf_captimerecord)
120 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_TIME), player.netname, TIME_ENCODE(cap_time));
121 else if(cap_time < cap_record)
122 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));
124 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));
126 // write that shit in the database
127 if(!ctf_oneflag) // but not in 1-flag mode
128 if((!ctf_captimerecord) || (cap_time < cap_record))
130 ctf_captimerecord = cap_time;
131 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
132 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
133 write_recordmarker(player, flag.ctf_pickuptime, cap_time);
136 if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
137 race_setTime(GetMapname(), TIME_ENCODE(cap_time), player.crypto_idfp, player.netname, player, false);
140 bool ctf_Immediate_Return_Allowed(entity flag, entity toucher)
143 FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), { ++num_perteam; });
145 // automatically return if there's only 1 player on the team
146 return ((autocvar_g_ctf_flag_return || num_perteam <= 1 || (autocvar_g_ctf_flag_return_carrying && toucher.flagcarried))
150 bool ctf_Return_Customize(entity this, entity client)
152 // only to the carrier
153 return boolean(client == this.owner);
156 void ctf_FlagcarrierWaypoints(entity player)
158 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
159 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, 2 * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
160 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);
161 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
163 if(player.flagcarried && CTF_SAMETEAM(player, player.flagcarried))
165 if(!player.wps_enemyflagcarrier)
167 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, 0, player, wps_enemyflagcarrier, true, RADARICON_FLAG);
168 wp.colormod = WPCOLOR_ENEMYFC(player.team);
169 setcefc(wp, ctf_Stalemate_Customize);
171 if(IS_REAL_CLIENT(player) && !ctf_stalemate)
172 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_VISIBLE);
175 if(!player.wps_flagreturn)
177 entity owp = WaypointSprite_SpawnFixed(WP_FlagReturn, player.flagcarried.ctf_spawnorigin + FLAG_WAYPOINT_OFFSET, player, wps_flagreturn, RADARICON_FLAG);
178 owp.colormod = '0 0.8 0.8';
179 //WaypointSprite_UpdateTeamRadar(player.wps_flagreturn, RADARICON_FLAG, ((player.team) ? colormapPaletteColor(player.team - 1, false) : '1 1 1'));
180 setcefc(owp, ctf_Return_Customize);
185 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
187 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
188 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
189 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
190 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
193 if(current_height) // make sure we can actually do this arcing path
195 targpos = (to + ('0 0 1' * current_height));
196 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
197 if(trace_fraction < 1)
199 //print("normal arc line failed, trying to find new pos...");
200 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
201 targpos = (trace_endpos + eZ * FLAG_PASS_ARC_OFFSET_Z);
202 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
203 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
204 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
207 else { targpos = to; }
209 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
211 vector desired_direction = normalize(targpos - from);
212 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
213 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
216 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
218 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
220 // directional tracing only
222 makevectors(passer_angle);
224 // find the closest point on the enemy to the center of the attack
225 float h; // hypotenuse, which is the distance between attacker to head
226 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
228 h = vlen(head_center - passer_center);
229 a = h * (normalize(head_center - passer_center) * v_forward);
231 vector nearest_on_line = (passer_center + a * v_forward);
232 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
234 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
235 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
237 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
242 else { return true; }
246 // =======================
247 // CaptureShield Functions
248 // =======================
250 bool ctf_CaptureShield_CheckStatus(entity p)
252 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
253 int players_worseeq, players_total;
255 if(ctf_captureshield_max_ratio <= 0)
258 s = GameRules_scoring_add(p, CTF_CAPS, 0);
259 s2 = GameRules_scoring_add(p, CTF_PICKUPS, 0);
260 s3 = GameRules_scoring_add(p, CTF_RETURNS, 0);
261 s4 = GameRules_scoring_add(p, CTF_FCKILLS, 0);
263 sr = ((s - s2) + (s3 + s4));
265 if(sr >= -ctf_captureshield_min_negscore)
268 players_total = players_worseeq = 0;
269 FOREACH_CLIENT(IS_PLAYER(it), {
272 se = GameRules_scoring_add(it, CTF_CAPS, 0);
273 se2 = GameRules_scoring_add(it, CTF_PICKUPS, 0);
274 se3 = GameRules_scoring_add(it, CTF_RETURNS, 0);
275 se4 = GameRules_scoring_add(it, CTF_FCKILLS, 0);
277 ser = ((se - se2) + (se3 + se4));
284 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
285 // use this rule here
287 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
293 void ctf_CaptureShield_Update(entity player, bool wanted_status)
295 bool updated_status = ctf_CaptureShield_CheckStatus(player);
296 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
298 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
299 player.ctf_captureshielded = updated_status;
303 bool ctf_CaptureShield_Customize(entity this, entity client)
305 if(!client.ctf_captureshielded) { return false; }
306 if(CTF_SAMETEAM(this, client)) { return false; }
311 void ctf_CaptureShield_Touch(entity this, entity toucher)
313 if(!toucher.ctf_captureshielded) { return; }
314 if(CTF_SAMETEAM(this, toucher)) { return; }
316 vector mymid = (this.absmin + this.absmax) * 0.5;
317 vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
319 Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
320 if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
323 void ctf_CaptureShield_Spawn(entity flag)
325 entity shield = new(ctf_captureshield);
328 shield.team = flag.team;
329 settouch(shield, ctf_CaptureShield_Touch);
330 setcefc(shield, ctf_CaptureShield_Customize);
331 shield.effects = EF_ADDITIVE;
332 set_movetype(shield, MOVETYPE_NOCLIP);
333 shield.solid = SOLID_TRIGGER;
334 shield.avelocity = '7 0 11';
337 setorigin(shield, flag.origin);
338 setmodel(shield, MDL_CTF_SHIELD);
339 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
343 // ====================
344 // Drop/Pass/Throw Code
345 // ====================
347 void ctf_Handle_Drop(entity flag, entity player, int droptype)
350 player = (player ? player : flag.pass_sender);
353 set_movetype(flag, MOVETYPE_TOSS);
354 flag.takedamage = DAMAGE_YES;
355 flag.angles = '0 0 0';
356 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
357 flag.ctf_droptime = time;
358 flag.ctf_dropper = player;
359 flag.ctf_status = FLAG_DROPPED;
361 // messages and sounds
362 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
363 _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
364 ctf_EventLog("dropped", player.team, player);
367 GameRules_scoring_add_team(player, SCORE, -((flag.score_drop) ? flag.score_drop : autocvar_g_ctf_score_penalty_drop));
368 GameRules_scoring_add(player, CTF_DROPS, 1);
371 if(autocvar_g_ctf_flag_dropped_waypoint) {
372 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);
373 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
376 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
378 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_health);
379 WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH));
382 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
384 if(droptype == DROP_PASS)
386 flag.pass_distance = 0;
387 flag.pass_sender = NULL;
388 flag.pass_target = NULL;
392 void ctf_Handle_Retrieve(entity flag, entity player)
394 entity sender = flag.pass_sender;
396 // transfer flag to player
398 flag.owner.flagcarried = flag;
399 GameRules_scoring_vip(player, true);
404 setattachment(flag, player.vehicle, "");
405 setorigin(flag, VEHICLE_FLAG_OFFSET);
406 flag.scale = VEHICLE_FLAG_SCALE;
410 setattachment(flag, player, "");
411 setorigin(flag, FLAG_CARRY_OFFSET);
413 set_movetype(flag, MOVETYPE_NONE);
414 flag.takedamage = DAMAGE_NO;
415 flag.solid = SOLID_NOT;
416 flag.angles = '0 0 0';
417 flag.ctf_status = FLAG_CARRY;
419 // messages and sounds
420 _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
421 ctf_EventLog("receive", flag.team, player);
423 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
425 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
426 else if(it == player)
427 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
428 else if(SAME_TEAM(it, sender))
429 Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
432 // create new waypoint
433 ctf_FlagcarrierWaypoints(player);
435 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
436 player.throw_antispam = sender.throw_antispam;
438 flag.pass_distance = 0;
439 flag.pass_sender = NULL;
440 flag.pass_target = NULL;
443 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
445 entity flag = player.flagcarried;
446 vector targ_origin, flag_velocity;
448 if(!flag) { return; }
449 if((droptype == DROP_PASS) && !receiver) { return; }
451 if(flag.speedrunning)
453 // ensure old waypoints are removed before resetting the flag
454 WaypointSprite_Kill(player.wps_flagcarrier);
456 if(player.wps_enemyflagcarrier)
457 WaypointSprite_Kill(player.wps_enemyflagcarrier);
459 if(player.wps_flagreturn)
460 WaypointSprite_Kill(player.wps_flagreturn);
461 ctf_RespawnFlag(flag);
466 setattachment(flag, NULL, "");
467 tracebox(player.origin - FLAG_DROP_OFFSET, flag.m_mins, flag.m_maxs, player.origin + FLAG_DROP_OFFSET, MOVE_NOMONSTERS, flag);
468 setorigin(flag, trace_endpos);
469 flag.owner.flagcarried = NULL;
470 GameRules_scoring_vip(flag.owner, false);
472 flag.solid = SOLID_TRIGGER;
473 flag.ctf_dropper = player;
474 flag.ctf_droptime = time;
476 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
483 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
484 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
485 WarpZone_RefSys_Copy(flag, receiver);
486 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
487 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
489 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
490 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
493 set_movetype(flag, MOVETYPE_FLY);
494 flag.takedamage = DAMAGE_NO;
495 flag.pass_sender = player;
496 flag.pass_target = receiver;
497 flag.ctf_status = FLAG_PASSING;
500 _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
501 WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
502 ctf_EventLog("pass", flag.team, player);
508 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'));
510 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)));
511 flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
512 ctf_Handle_Drop(flag, player, droptype);
513 navigation_dynamicgoal_set(flag, player);
519 flag.velocity = '0 0 0'; // do nothing
526 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);
527 ctf_Handle_Drop(flag, player, droptype);
528 navigation_dynamicgoal_set(flag, player);
533 // kill old waypointsprite
534 WaypointSprite_Ping(player.wps_flagcarrier);
535 WaypointSprite_Kill(player.wps_flagcarrier);
537 if(player.wps_enemyflagcarrier)
538 WaypointSprite_Kill(player.wps_enemyflagcarrier);
540 if(player.wps_flagreturn)
541 WaypointSprite_Kill(player.wps_flagreturn);
544 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
548 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
550 return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
558 void nades_GiveBonus(entity player, float score);
560 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
562 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
563 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
564 entity player_team_flag = NULL, tmp_entity;
565 float old_time, new_time;
567 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
568 if(CTF_DIFFTEAM(player, flag)) { return; }
569 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)
571 if (toucher.goalentity == flag.bot_basewaypoint)
572 toucher.goalentity_lock_timeout = 0;
575 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
576 if(SAME_TEAM(tmp_entity, player))
578 player_team_flag = tmp_entity;
582 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
584 player.throw_prevtime = time;
585 player.throw_count = 0;
587 // messages and sounds
588 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
589 ctf_CaptureRecord(enemy_flag, player);
590 _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);
594 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
595 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
601 if(enemy_flag.score_capture || flag.score_capture)
602 pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
603 GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
605 if(enemy_flag.score_team_capture || flag.score_team_capture)
606 capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
607 GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
609 old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
610 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
611 if(!old_time || new_time < old_time)
612 GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
614 Give_Medal(player, CAPTURE);
617 AnnounceScores(player.team);
620 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
622 shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
626 if(capturetype == CAPTURE_NORMAL)
628 WaypointSprite_Kill(player.wps_flagcarrier);
629 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
631 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
633 GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist));
634 Give_Medal(enemy_flag.ctf_dropper, ASSIST);
638 flag.enemy = toucher;
641 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
642 ctf_RespawnFlag(enemy_flag);
645 void ctf_Handle_Return(entity flag, entity player)
647 // messages and sounds
648 if(IS_MONSTER(player))
650 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
654 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
655 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
657 FOREACH_CLIENT(IS_PLAYER(it), {
658 if(it.team == flag.team)
659 Send_Notification(NOTIF_ONE_ONLY, it, MSG_ANNCE, ANNCE_CTF_RETURN_TEAM);
661 Send_Notification(NOTIF_ONE_ONLY, it, MSG_ANNCE, ANNCE_CTF_RETURN_ENEMY);
663 Send_Notification(NOTIF_ALL_SPEC, NULL, MSG_ANNCE, APP_TEAM_NUM(flag.team, ANNCE_CTF_RETURN));
665 _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
666 ctf_EventLog("return", flag.team, player);
669 if(IS_PLAYER(player))
671 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
672 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
674 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
677 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
681 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
682 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
683 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
687 if(player.flagcarried == flag)
688 WaypointSprite_Kill(player.wps_flagcarrier);
693 ctf_RespawnFlag(flag);
696 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
699 float pickup_dropped_score; // used to calculate dropped pickup score
701 // attach the flag to the player
703 player.flagcarried = flag;
704 GameRules_scoring_vip(player, true);
707 setattachment(flag, player.vehicle, "");
708 setorigin(flag, VEHICLE_FLAG_OFFSET);
709 flag.scale = VEHICLE_FLAG_SCALE;
713 setattachment(flag, player, "");
714 setorigin(flag, FLAG_CARRY_OFFSET);
718 set_movetype(flag, MOVETYPE_NONE);
719 flag.takedamage = DAMAGE_NO;
720 flag.solid = SOLID_NOT;
721 flag.angles = '0 0 0';
722 flag.ctf_status = FLAG_CARRY;
726 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
727 case PICKUP_DROPPED: SetResourceExplicit(flag, RES_HEALTH, flag.max_health); break; // reset health/return timelimit
731 // messages and sounds
732 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
733 Send_Notification(NOTIF_ALL_SPEC, NULL, MSG_ANNCE, APP_TEAM_NUM(flag.team, ANNCE_CTF_PICKUP));
736 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
738 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
739 else if(CTF_DIFFTEAM(player, flag))
741 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
742 Send_Notification(NOTIF_ONE_ONLY, player, MSG_ANNCE, ANNCE_CTF_PICKUP_YOU);
745 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
747 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
748 Send_Notification(NOTIF_TEAM_ONLY_EXCEPT, player, MSG_ANNCE, ANNCE_CTF_PICKUP_TEAM);
751 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); });
754 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
755 if(CTF_SAMETEAM(flag, it))
757 if(SAME_TEAM(player, it)) {
758 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
759 Send_Notification(NOTIF_ONE_ONLY, it, MSG_ANNCE, ANNCE_CTF_PICKUP_TEAM);
761 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);
762 Send_Notification(NOTIF_ONE_ONLY, it, MSG_ANNCE, ANNCE_CTF_PICKUP_ENEMY);
767 _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
770 GameRules_scoring_add(player, CTF_PICKUPS, 1);
771 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
776 GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
777 ctf_EventLog("steal", flag.team, player);
783 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);
784 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);
785 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
786 GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
787 ctf_EventLog("pickup", flag.team, player);
795 if(pickuptype == PICKUP_BASE)
797 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
798 if((player.speedrunning) && (ctf_captimerecord))
799 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
803 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
806 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
807 ctf_FlagcarrierWaypoints(player);
808 WaypointSprite_Ping(player.wps_flagcarrier);
812 // ===================
813 // Main Flag Functions
814 // ===================
816 void ctf_CheckFlagReturn(entity flag, int returntype)
818 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
820 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH)); }
822 if((GetResource(flag, RES_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
827 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
829 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
830 case RETURN_SPEEDRUN:
831 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
832 case RETURN_NEEDKILL:
833 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
836 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
838 _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
839 ctf_EventLog("returned", flag.team, NULL);
841 ctf_RespawnFlag(flag);
846 bool ctf_Stalemate_Customize(entity this, entity client)
848 // make spectators see what the player would see
849 entity e = WaypointSprite_getviewentity(client);
850 entity wp_owner = this.owner;
853 //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
854 if(SAME_TEAM(wp_owner, e)) { return false; }
855 if(!IS_PLAYER(e)) { return false; }
860 void ctf_CheckStalemate()
863 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
866 entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
868 // build list of stale flags
869 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
871 if(autocvar_g_ctf_stalemate)
872 if(tmp_entity.ctf_status != FLAG_BASE)
873 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
875 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
876 ctf_staleflaglist = tmp_entity;
878 switch(tmp_entity.team)
880 case NUM_TEAM_1: ++stale_red_flags; break;
881 case NUM_TEAM_2: ++stale_blue_flags; break;
882 case NUM_TEAM_3: ++stale_yellow_flags; break;
883 case NUM_TEAM_4: ++stale_pink_flags; break;
884 default: ++stale_neutral_flags; break;
890 stale_flags = (stale_neutral_flags >= 1);
892 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
894 if(ctf_oneflag && stale_flags == 1)
895 ctf_stalemate = true;
896 else if(stale_flags >= 2)
897 ctf_stalemate = true;
898 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
899 { ctf_stalemate = false; wpforenemy_announced = false; }
900 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
901 { ctf_stalemate = false; wpforenemy_announced = false; }
903 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
906 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
908 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
910 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);
911 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
912 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
916 if (!wpforenemy_announced)
918 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)); });
920 wpforenemy_announced = true;
925 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
927 if(ITEM_DAMAGE_NEEDKILL(deathtype))
929 if(autocvar_g_ctf_flag_return_damage_delay)
930 this.ctf_flagdamaged_byworld = true;
933 SetResourceExplicit(this, RES_HEALTH, 0);
934 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
938 if(autocvar_g_ctf_flag_return_damage)
940 // reduce health and check if it should be returned
941 TakeResource(this, RES_HEALTH, damage);
942 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
947 void ctf_FlagThink(entity this)
952 this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
955 if(this == ctf_worldflaglist) // only for the first flag
956 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
959 if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
960 LOG_TRACE("wtf the flag got squashed?");
961 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
962 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
963 setsize(this, this.m_mins, this.m_maxs);
967 switch(this.ctf_status)
971 if(autocvar_g_ctf_dropped_capture_radius)
973 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
974 if(tmp_entity.ctf_status == FLAG_DROPPED)
975 if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
976 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
977 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
984 this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
986 if(autocvar_g_ctf_flag_dropped_floatinwater)
988 vector midpoint = ((this.absmin + this.absmax) * 0.5);
989 if(pointcontents(midpoint) == CONTENT_WATER)
991 this.velocity = this.velocity * 0.5;
993 if (pointcontents(midpoint + eZ * FLAG_FLOAT_OFFSET_Z) == CONTENT_WATER)
994 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
996 { set_movetype(this, MOVETYPE_FLY); }
998 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
1000 if(autocvar_g_ctf_flag_return_dropped)
1002 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
1004 SetResourceExplicit(this, RES_HEALTH, 0);
1005 ctf_CheckFlagReturn(this, RETURN_DROPPED);
1009 if(this.ctf_flagdamaged_byworld)
1011 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
1012 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
1015 else if(autocvar_g_ctf_flag_return_time)
1017 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
1018 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
1026 if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
1028 SetResourceExplicit(this, RES_HEALTH, 0);
1029 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
1031 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
1032 ImpulseCommands(this.owner);
1034 if(autocvar_g_ctf_stalemate)
1036 if(time >= wpforenemy_nextthink)
1038 ctf_CheckStalemate();
1039 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1042 if(CTF_SAMETEAM(this, this.owner) && this.team)
1044 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1045 ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
1046 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
1047 ctf_Handle_Return(this, this.owner);
1054 vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1055 targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1056 WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1058 if((this.pass_target == NULL)
1059 || (IS_DEAD(this.pass_target))
1060 || (this.pass_target.flagcarried)
1061 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1062 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1063 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1065 // give up, pass failed
1066 ctf_Handle_Drop(this, NULL, DROP_PASS);
1070 // still a viable target, go for it
1071 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1076 default: // this should never happen
1078 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1084 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1087 if(game_stopped) return;
1088 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1090 bool is_not_monster = (!IS_MONSTER(toucher));
1092 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1093 if(ITEM_TOUCH_NEEDKILL())
1095 if(!autocvar_g_ctf_flag_return_damage_delay)
1097 SetResourceExplicit(flag, RES_HEALTH, 0);
1098 ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1100 if(!flag.ctf_flagdamaged_byworld) { return; }
1103 // special touch behaviors
1104 if(STAT(FROZEN, toucher)) { return; }
1105 else if(IS_VEHICLE(toucher))
1107 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1108 toucher = toucher.owner; // the player is actually the vehicle owner, not other
1110 return; // do nothing
1112 else if(IS_MONSTER(toucher))
1114 if(!autocvar_g_ctf_allow_monster_touch)
1115 return; // do nothing
1117 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1119 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1121 Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1122 _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1123 flag.wait = time + FLAG_TOUCHRATE;
1127 else if(IS_DEAD(toucher)) { return; }
1129 switch(flag.ctf_status)
1135 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1136 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1137 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1138 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1140 else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1141 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1142 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)
1144 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1145 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1147 else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1148 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1154 if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1155 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1156 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1157 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1163 LOG_TRACE("Someone touched a flag even though it was being carried?");
1169 if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1171 if(DIFF_TEAM(toucher, flag.pass_sender))
1173 if(ctf_Immediate_Return_Allowed(flag, toucher))
1174 ctf_Handle_Return(flag, toucher);
1175 else if(is_not_monster && (!toucher.flagcarried))
1176 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1178 else if(!toucher.flagcarried)
1179 ctf_Handle_Retrieve(flag, toucher);
1186 .float last_respawn;
1187 void ctf_RespawnFlag(entity flag)
1189 flag.watertype = CONTENT_EMPTY; // TODO: it is unclear why this workaround is needed, likely many other potential breakage points!!
1190 // check for flag respawn being called twice in a row
1191 if(flag.last_respawn > time - 0.5)
1192 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1194 flag.last_respawn = time;
1196 // reset the player (if there is one)
1197 if((flag.owner) && (flag.owner.flagcarried == flag))
1199 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1200 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1201 WaypointSprite_Kill(flag.wps_flagcarrier);
1203 flag.owner.flagcarried = NULL;
1204 GameRules_scoring_vip(flag.owner, false);
1206 if(flag.speedrunning)
1207 ctf_FakeTimeLimit(flag.owner, -1);
1210 if((flag.owner) && (flag.owner.vehicle))
1211 flag.scale = FLAG_SCALE;
1213 if(flag.ctf_status == FLAG_DROPPED)
1214 { WaypointSprite_Kill(flag.wps_flagdropped); }
1217 setattachment(flag, NULL, "");
1218 setorigin(flag, flag.ctf_spawnorigin);
1220 //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1221 set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1222 flag.takedamage = DAMAGE_NO;
1223 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1224 flag.solid = SOLID_TRIGGER;
1225 flag.velocity = '0 0 0';
1226 flag.angles = flag.mangle;
1227 flag.flags = FL_ITEM | FL_NOTARGET;
1229 flag.ctf_status = FLAG_BASE;
1231 flag.pass_distance = 0;
1232 flag.pass_sender = NULL;
1233 flag.pass_target = NULL;
1234 flag.ctf_dropper = NULL;
1235 flag.ctf_pickuptime = 0;
1236 flag.ctf_droptime = 0;
1237 flag.ctf_flagdamaged_byworld = false;
1238 navigation_dynamicgoal_unset(flag);
1240 ctf_CheckStalemate();
1243 void ctf_Reset(entity this)
1245 if(this.owner && IS_PLAYER(this.owner))
1246 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1249 ctf_RespawnFlag(this);
1252 bool ctf_FlagBase_Customize(entity this, entity client)
1254 entity e = WaypointSprite_getviewentity(client);
1255 entity wp_owner = this.owner;
1256 entity flag = e.flagcarried;
1257 if(flag && CTF_SAMETEAM(e, flag))
1259 if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1264 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1267 waypoint_spawnforitem_force(this, this.origin);
1268 navigation_dynamicgoal_init(this, true);
1274 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1275 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1276 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1277 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1278 default: basename = WP_FlagBaseNeutral; break;
1281 if(autocvar_g_ctf_flag_waypoint)
1283 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1284 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1285 wp.fade_rate = autocvar_g_ctf_flag_waypoint_maxdistance;
1286 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1287 setcefc(wp, ctf_FlagBase_Customize);
1290 // captureshield setup
1291 ctf_CaptureShield_Spawn(this);
1296 void ctf_FlagSetup(int teamnum, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1299 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1300 ctf_worldflaglist = flag;
1302 setattachment(flag, NULL, "");
1304 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnum), Team_ColorName_Upper(teamnum)));
1305 flag.team = teamnum;
1306 flag.classname = "item_flag_team";
1307 flag.target = "###item###"; // for finding the nearest item using findnearest
1308 flag.flags = FL_ITEM | FL_NOTARGET;
1309 IL_PUSH(g_items, flag);
1310 flag.solid = SOLID_TRIGGER;
1311 flag.takedamage = DAMAGE_NO;
1312 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1313 flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1314 SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1315 flag.event_damage = ctf_FlagDamage;
1316 flag.pushable = true;
1317 flag.teleportable = TELEPORT_NORMAL;
1318 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1319 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1320 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1321 if(flag.damagedbycontents)
1322 IL_PUSH(g_damagedbycontents, flag);
1323 flag.velocity = '0 0 0';
1324 flag.mangle = flag.angles;
1325 flag.reset = ctf_Reset;
1326 settouch(flag, ctf_FlagTouch);
1327 setthink(flag, ctf_FlagThink);
1328 flag.nextthink = time + FLAG_THINKRATE;
1329 flag.ctf_status = FLAG_BASE;
1331 // crudely force them all to 0
1332 if(autocvar_g_ctf_score_ignore_fields)
1333 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1335 string teamname = Static_Team_ColorName_Lower(teamnum);
1337 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1338 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1339 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1340 if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnum).eent_eff_name; }
1341 if (flag.passeffect == "") { flag.passeffect = EFFECT_PASS(teamnum).eent_eff_name; }
1342 if (flag.capeffect == "") { flag.capeffect = EFFECT_CAP(teamnum).eent_eff_name; }
1346 if(flag.s == "") flag.s = b; \
1347 precache_sound(flag.s);
1349 X(snd_flag_taken, strzone(SND(CTF_TAKEN(teamnum))))
1350 X(snd_flag_returned, strzone(SND(CTF_RETURNED(teamnum))))
1351 X(snd_flag_capture, strzone(SND(CTF_CAPTURE(teamnum))))
1352 X(snd_flag_dropped, strzone(SND(CTF_DROPPED(teamnum))))
1353 X(snd_flag_respawn, strzone(SND(CTF_RESPAWN)))
1354 X(snd_flag_touch, strzone(SND(CTF_TOUCH)))
1355 X(snd_flag_pass, strzone(SND(CTF_PASS)))
1359 precache_model(flag.model);
1362 _setmodel(flag, flag.model); // precision set below
1363 setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1364 flag.m_mins = flag.mins; // store these for squash checks
1365 flag.m_maxs = flag.maxs;
1366 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1368 if(autocvar_g_ctf_flag_glowtrails)
1372 case NUM_TEAM_1: flag.glow_color = 251; break;
1373 case NUM_TEAM_2: flag.glow_color = 210; break;
1374 case NUM_TEAM_3: flag.glow_color = 110; break;
1375 case NUM_TEAM_4: flag.glow_color = 145; break;
1376 default: flag.glow_color = 254; break;
1378 flag.glow_size = 25;
1379 flag.glow_trail = 1;
1382 flag.effects |= EF_LOWPRECISION;
1383 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1384 if(autocvar_g_ctf_dynamiclights)
1388 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1389 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1390 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1391 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1392 default: flag.effects |= EF_DIMLIGHT; break;
1397 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1399 flag.dropped_origin = flag.origin;
1400 flag.noalign = true;
1401 set_movetype(flag, MOVETYPE_NONE);
1403 else // drop to floor, automatically find a platform and set that as spawn origin
1405 flag.noalign = false;
1407 set_movetype(flag, MOVETYPE_NONE);
1410 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1418 // NOTE: LEGACY CODE, needs to be re-written!
1420 void havocbot_ctf_calculate_middlepoint()
1424 vector fo = '0 0 0';
1427 f = ctf_worldflaglist;
1432 f = f.ctf_worldflagnext;
1438 havocbot_middlepoint = s / n;
1439 havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1441 havocbot_symmetry_axis_m = 0;
1442 havocbot_symmetry_axis_q = 0;
1445 // for symmetrical editing of waypoints
1446 entity f1 = ctf_worldflaglist;
1447 entity f2 = f1.ctf_worldflagnext;
1448 float m = -(f1.origin.y - f2.origin.y) / (max(f1.origin.x - f2.origin.x, FLOAT_EPSILON));
1449 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1450 havocbot_symmetry_axis_m = m;
1451 havocbot_symmetry_axis_q = q;
1453 havocbot_symmetry_origin_order = n;
1457 entity havocbot_ctf_find_flag(entity bot)
1460 f = ctf_worldflaglist;
1463 if (CTF_SAMETEAM(bot, f))
1465 f = f.ctf_worldflagnext;
1470 entity havocbot_ctf_find_enemy_flag(entity bot)
1473 f = ctf_worldflaglist;
1478 if(CTF_DIFFTEAM(bot, f))
1485 else if(!bot.flagcarried)
1489 else if (CTF_DIFFTEAM(bot, f))
1491 f = f.ctf_worldflagnext;
1496 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1503 FOREACH_CLIENT(IS_PLAYER(it), {
1504 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1507 if(vdist(it.origin - org, <, tc_radius))
1516 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1519 head = ctf_worldflaglist;
1522 if (CTF_SAMETEAM(this, head))
1524 head = head.ctf_worldflagnext;
1527 navigation_routerating(this, head, ratingscale, 10000);
1531 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1534 head = ctf_worldflaglist;
1537 if (CTF_SAMETEAM(this, head))
1539 if (this.flagcarried)
1540 if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1542 head = head.ctf_worldflagnext; // skip base if it has a different group
1547 head = head.ctf_worldflagnext;
1552 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1555 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1558 head = ctf_worldflaglist;
1563 if(CTF_DIFFTEAM(this, head))
1567 if(this.flagcarried)
1570 else if(!this.flagcarried)
1574 else if(CTF_DIFFTEAM(this, head))
1576 head = head.ctf_worldflagnext;
1580 if (head.ctf_status == FLAG_CARRY)
1582 // adjust rating of our flag carrier depending on his health
1583 head = head.tag_entity;
1584 float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1585 ratingscale += ratingscale * f * 0.1;
1587 navigation_routerating(this, head, ratingscale, 10000);
1591 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1593 // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1595 if (!bot_waypoints_for_items)
1597 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1603 head = havocbot_ctf_find_enemy_flag(this);
1608 navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1611 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1615 mf = havocbot_ctf_find_flag(this);
1617 if(mf.ctf_status == FLAG_BASE)
1621 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1624 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1627 head = ctf_worldflaglist;
1630 // flag is out in the field
1631 if(head.ctf_status != FLAG_BASE)
1632 if(head.tag_entity==NULL) // dropped
1636 if(vdist(org - head.origin, <, df_radius))
1637 navigation_routerating(this, head, ratingscale, 10000);
1640 navigation_routerating(this, head, ratingscale, 10000);
1643 head = head.ctf_worldflagnext;
1647 void havocbot_ctf_reset_role(entity this)
1649 float cdefense, cmiddle, coffense;
1656 if (this.flagcarried)
1658 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1662 mf = havocbot_ctf_find_flag(this);
1663 ef = havocbot_ctf_find_enemy_flag(this);
1665 // Retrieve stolen flag
1666 if(mf.ctf_status!=FLAG_BASE)
1668 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1672 // If enemy flag is taken go to the middle to intercept pursuers
1673 if(ef.ctf_status!=FLAG_BASE)
1675 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1679 // if there is no one else on the team switch to offense
1681 // don't check if this bot is a player since it isn't true when the bot is added to the server
1682 FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1686 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1689 else if (time < CS(this).jointime + 1)
1691 // if bots spawn all at once set good default roles
1694 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1697 else if (count == 2)
1699 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1704 // Evaluate best position to take
1705 // Count mates on middle position
1706 cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1708 // Count mates on defense position
1709 cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1711 // Count mates on offense position
1712 coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1714 if(cdefense<=coffense)
1715 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1716 else if(coffense<=cmiddle)
1717 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1719 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1721 // if bots spawn all at once assign them a more appropriated role after a while
1722 if (time < CS(this).jointime + 1 && count > 2)
1723 this.havocbot_role_timeout = time + 10 + random() * 10;
1726 bool havocbot_ctf_is_basewaypoint(entity item)
1728 if (item.classname != "waypoint")
1731 entity head = ctf_worldflaglist;
1734 if (item == head.bot_basewaypoint)
1736 head = head.ctf_worldflagnext;
1741 void havocbot_role_ctf_carrier(entity this)
1745 havocbot_ctf_reset_role(this);
1749 if (this.flagcarried == NULL)
1751 havocbot_ctf_reset_role(this);
1755 if (navigation_goalrating_timeout(this))
1757 navigation_goalrating_start(this);
1760 entity mf = havocbot_ctf_find_flag(this);
1761 vector base_org = mf.dropped_origin;
1762 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1764 havocbot_goalrating_ctf_enemybase(this, base_rating);
1766 havocbot_goalrating_ctf_ourbase(this, base_rating);
1768 // start collecting items very close to the bot but only inside of own base radius
1769 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1770 havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1772 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1774 navigation_goalrating_end(this);
1776 navigation_goalrating_timeout_set(this);
1778 entity goal = this.goalentity;
1779 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1780 this.goalentity_lock_timeout = time + ((this.bot_aimtarg) ? 2 : 3);
1783 this.havocbot_cantfindflag = time + 10;
1784 else if (time > this.havocbot_cantfindflag)
1786 // Can't navigate to my own base, suicide!
1787 // TODO: drop it and wander around
1788 Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1794 void havocbot_role_ctf_escort(entity this)
1800 havocbot_ctf_reset_role(this);
1804 if (this.flagcarried)
1806 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1810 // If enemy flag is back on the base switch to previous role
1811 ef = havocbot_ctf_find_enemy_flag(this);
1812 if(ef.ctf_status==FLAG_BASE)
1814 this.havocbot_role = this.havocbot_previous_role;
1815 this.havocbot_role_timeout = 0;
1818 if (ef.ctf_status == FLAG_DROPPED)
1820 navigation_goalrating_timeout_expire(this, 1);
1824 // If the flag carrier reached the base switch to defense
1825 mf = havocbot_ctf_find_flag(this);
1826 if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1828 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1832 // Set the role timeout if necessary
1833 if (!this.havocbot_role_timeout)
1835 this.havocbot_role_timeout = time + random() * 30 + 60;
1838 // If nothing happened just switch to previous role
1839 if (time > this.havocbot_role_timeout)
1841 this.havocbot_role = this.havocbot_previous_role;
1842 this.havocbot_role_timeout = 0;
1846 // Chase the flag carrier
1847 if (navigation_goalrating_timeout(this))
1849 navigation_goalrating_start(this);
1852 havocbot_goalrating_ctf_enemyflag(this, 10000);
1853 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1854 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1856 navigation_goalrating_end(this);
1858 navigation_goalrating_timeout_set(this);
1862 void havocbot_role_ctf_offense(entity this)
1869 havocbot_ctf_reset_role(this);
1873 if (this.flagcarried)
1875 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1880 mf = havocbot_ctf_find_flag(this);
1881 ef = havocbot_ctf_find_enemy_flag(this);
1884 if(mf.ctf_status!=FLAG_BASE)
1887 pos = mf.tag_entity.origin;
1891 // Try to get it if closer than the enemy base
1892 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1894 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1899 // Escort flag carrier
1900 if(ef.ctf_status!=FLAG_BASE)
1903 pos = ef.tag_entity.origin;
1907 if(vdist(pos - mf.dropped_origin, >, 700))
1909 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1914 // Set the role timeout if necessary
1915 if (!this.havocbot_role_timeout)
1916 this.havocbot_role_timeout = time + 120;
1918 if (time > this.havocbot_role_timeout)
1920 havocbot_ctf_reset_role(this);
1924 if (navigation_goalrating_timeout(this))
1926 navigation_goalrating_start(this);
1929 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1930 havocbot_goalrating_ctf_enemybase(this, 10000);
1931 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1933 navigation_goalrating_end(this);
1935 navigation_goalrating_timeout_set(this);
1939 // Retriever (temporary role):
1940 void havocbot_role_ctf_retriever(entity this)
1946 havocbot_ctf_reset_role(this);
1950 if (this.flagcarried)
1952 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1956 // If flag is back on the base switch to previous role
1957 mf = havocbot_ctf_find_flag(this);
1958 if(mf.ctf_status==FLAG_BASE)
1960 if (mf.enemy == this) // did this bot return the flag?
1961 navigation_goalrating_timeout_force(this);
1962 havocbot_ctf_reset_role(this);
1966 if (!this.havocbot_role_timeout)
1967 this.havocbot_role_timeout = time + 20;
1969 if (time > this.havocbot_role_timeout)
1971 havocbot_ctf_reset_role(this);
1975 if (navigation_goalrating_timeout(this))
1977 const float RT_RADIUS = 10000;
1979 navigation_goalrating_start(this);
1982 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1983 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1984 havocbot_goalrating_ctf_enemybase(this, 8000);
1985 entity ef = havocbot_ctf_find_enemy_flag(this);
1986 vector enemy_base_org = ef.dropped_origin;
1987 // start collecting items very close to the bot but only inside of enemy base radius
1988 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1989 havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1990 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1992 navigation_goalrating_end(this);
1994 navigation_goalrating_timeout_set(this);
1998 void havocbot_role_ctf_middle(entity this)
2004 havocbot_ctf_reset_role(this);
2008 if (this.flagcarried)
2010 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2014 mf = havocbot_ctf_find_flag(this);
2015 if(mf.ctf_status!=FLAG_BASE)
2017 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2021 if (!this.havocbot_role_timeout)
2022 this.havocbot_role_timeout = time + 10;
2024 if (time > this.havocbot_role_timeout)
2026 havocbot_ctf_reset_role(this);
2030 if (navigation_goalrating_timeout(this))
2034 org = havocbot_middlepoint;
2035 org.z = this.origin.z;
2037 navigation_goalrating_start(this);
2040 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
2041 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
2042 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2043 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2044 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2045 havocbot_goalrating_ctf_enemybase(this, 3000);
2047 navigation_goalrating_end(this);
2049 entity goal = this.goalentity;
2050 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
2051 this.goalentity_lock_timeout = time + 2;
2053 navigation_goalrating_timeout_set(this);
2057 void havocbot_role_ctf_defense(entity this)
2063 havocbot_ctf_reset_role(this);
2067 if (this.flagcarried)
2069 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2073 // If own flag was captured
2074 mf = havocbot_ctf_find_flag(this);
2075 if(mf.ctf_status!=FLAG_BASE)
2077 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2081 if (!this.havocbot_role_timeout)
2082 this.havocbot_role_timeout = time + 30;
2084 if (time > this.havocbot_role_timeout)
2086 havocbot_ctf_reset_role(this);
2089 if (navigation_goalrating_timeout(this))
2091 vector org = mf.dropped_origin;
2093 navigation_goalrating_start(this);
2095 // if enemies are closer to our base, go there
2096 entity closestplayer = NULL;
2097 float distance, bestdistance = 10000;
2098 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2099 distance = vlen(org - it.origin);
2100 if(distance<bestdistance)
2103 bestdistance = distance;
2109 if(DIFF_TEAM(closestplayer, this))
2110 if(vdist(org - this.origin, >, 1000))
2111 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2112 havocbot_goalrating_ctf_ourbase(this, 10000);
2114 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2115 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2116 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2117 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2118 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2120 navigation_goalrating_end(this);
2122 navigation_goalrating_timeout_set(this);
2126 void havocbot_role_ctf_setrole(entity bot, int role)
2128 string s = "(null)";
2131 case HAVOCBOT_CTF_ROLE_CARRIER:
2133 bot.havocbot_role = havocbot_role_ctf_carrier;
2134 bot.havocbot_role_timeout = 0;
2135 bot.havocbot_cantfindflag = time + 10;
2136 if (bot.havocbot_previous_role != bot.havocbot_role)
2137 navigation_goalrating_timeout_force(bot);
2139 case HAVOCBOT_CTF_ROLE_DEFENSE:
2141 bot.havocbot_role = havocbot_role_ctf_defense;
2142 bot.havocbot_role_timeout = 0;
2144 case HAVOCBOT_CTF_ROLE_MIDDLE:
2146 bot.havocbot_role = havocbot_role_ctf_middle;
2147 bot.havocbot_role_timeout = 0;
2149 case HAVOCBOT_CTF_ROLE_OFFENSE:
2151 bot.havocbot_role = havocbot_role_ctf_offense;
2152 bot.havocbot_role_timeout = 0;
2154 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2156 bot.havocbot_previous_role = bot.havocbot_role;
2157 bot.havocbot_role = havocbot_role_ctf_retriever;
2158 bot.havocbot_role_timeout = time + 10;
2159 if (bot.havocbot_previous_role != bot.havocbot_role)
2160 navigation_goalrating_timeout_expire(bot, 2);
2162 case HAVOCBOT_CTF_ROLE_ESCORT:
2164 bot.havocbot_previous_role = bot.havocbot_role;
2165 bot.havocbot_role = havocbot_role_ctf_escort;
2166 bot.havocbot_role_timeout = time + 30;
2167 if (bot.havocbot_previous_role != bot.havocbot_role)
2168 navigation_goalrating_timeout_expire(bot, 2);
2171 LOG_TRACE(bot.netname, " switched to ", s);
2179 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2181 entity player = M_ARGV(0, entity);
2183 int t = 0, t2 = 0, t3 = 0;
2184 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)
2186 // initially clear items so they can be set as necessary later.
2187 STAT(CTF_FLAGSTATUS, player) &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
2188 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
2189 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
2190 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
2191 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
2192 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2194 // scan through all the flags and notify the client about them
2195 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2197 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
2198 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
2199 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
2200 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
2201 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; }
2203 switch(flag.ctf_status)
2208 if((flag.owner == player) || (flag.pass_sender == player))
2209 STAT(CTF_FLAGSTATUS, player) |= t; // carrying: player is currently carrying the flag
2211 STAT(CTF_FLAGSTATUS, player) |= t2; // taken: someone else is carrying the flag
2216 STAT(CTF_FLAGSTATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2222 // item for stopping players from capturing the flag too often
2223 if(player.ctf_captureshielded)
2224 STAT(CTF_FLAGSTATUS, player) |= CTF_SHIELDED;
2227 STAT(CTF_FLAGSTATUS, player) |= CTF_STALEMATE;
2229 // update the health of the flag carrier waypointsprite
2230 if(player.wps_flagcarrier)
2231 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);
2234 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in damage.qc
2236 entity frag_attacker = M_ARGV(1, entity);
2237 entity frag_target = M_ARGV(2, entity);
2238 float frag_damage = M_ARGV(4, float);
2239 vector frag_force = M_ARGV(6, vector);
2241 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2243 if(frag_target == frag_attacker) // damage done to yourself
2245 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2246 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2248 else // damage done to everyone else
2250 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2251 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2254 M_ARGV(4, float) = frag_damage;
2255 M_ARGV(6, vector) = frag_force;
2257 else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2259 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
2260 && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2262 frag_target.wps_helpme_time = time;
2263 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2265 // todo: add notification for when flag carrier needs help?
2269 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2271 entity frag_attacker = M_ARGV(1, entity);
2272 entity frag_target = M_ARGV(2, entity);
2274 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)))
2276 if(frag_target.flagcarried) {
2277 // Killing an enemy flag carrier
2278 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2279 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2280 } else if(frag_attacker.flagcarried) {
2281 Give_Medal(frag_attacker, DEFENSE);
2284 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2285 if(tmp_entity.ctf_status == FLAG_BASE && CTF_SAMETEAM(tmp_entity, frag_attacker))
2287 if(CTF_IS_NEAR(frag_target, tmp_entity, '1 1 1' * 1500))
2289 Give_Medal(frag_attacker, DEFENSE);
2296 if(frag_target.flagcarried)
2298 entity tmp_entity = frag_target.flagcarried;
2299 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2300 tmp_entity.ctf_dropper = NULL;
2305 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2307 M_ARGV(2, float) = 0; // frag score
2308 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2311 void ctf_RemovePlayer(entity player)
2313 if(player.flagcarried)
2314 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2316 for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2318 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2319 if(flag.pass_target == player) { flag.pass_target = NULL; }
2320 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2324 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2326 entity player = M_ARGV(0, entity);
2328 ctf_RemovePlayer(player);
2331 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2333 entity player = M_ARGV(0, entity);
2335 ctf_RemovePlayer(player);
2338 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2340 if(!autocvar_g_ctf_leaderboard)
2343 entity player = M_ARGV(0, entity);
2345 race_SendAll(player, true);
2348 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2350 if(!autocvar_g_ctf_leaderboard)
2353 entity player = M_ARGV(0, entity);
2355 race_checkAndWriteName(player);
2358 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2360 entity player = M_ARGV(0, entity);
2362 if(player.flagcarried)
2363 if(!autocvar_g_ctf_portalteleport)
2364 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2367 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2369 if(MUTATOR_RETURNVALUE || game_stopped) return;
2371 entity player = M_ARGV(0, entity);
2373 if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2375 // pass the flag to a team mate
2376 if(autocvar_g_ctf_pass)
2378 entity head, closest_target = NULL;
2379 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2381 while(head) // find the closest acceptable target to pass to
2383 if(IS_PLAYER(head) && !IS_DEAD(head))
2384 if(head != player && SAME_TEAM(head, player))
2385 if(!head.speedrunning && !head.vehicle)
2387 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in damage.qc)
2388 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2389 vector passer_center = CENTER_OR_VIEWOFS(player);
2391 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2393 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2395 if(IS_BOT_CLIENT(head))
2397 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2398 ctf_Handle_Throw(head, player, DROP_PASS);
2402 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2403 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2405 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2408 else if(player.flagcarried && !head.flagcarried)
2412 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2413 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2414 { closest_target = head; }
2416 else { closest_target = head; }
2423 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2426 // throw the flag in front of you
2427 if(autocvar_g_ctf_throw && player.flagcarried)
2429 if(player.throw_count == -1)
2431 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2433 player.throw_prevtime = time;
2434 player.throw_count = 1;
2435 ctf_Handle_Throw(player, NULL, DROP_THROW);
2440 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2446 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2447 else { player.throw_count += 1; }
2448 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2450 player.throw_prevtime = time;
2451 ctf_Handle_Throw(player, NULL, DROP_THROW);
2458 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2460 entity player = M_ARGV(0, entity);
2462 if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2464 player.wps_helpme_time = time;
2465 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2467 else // create a normal help me waypointsprite
2469 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2470 WaypointSprite_Ping(player.wps_helpme);
2476 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2478 entity player = M_ARGV(0, entity);
2479 entity veh = M_ARGV(1, entity);
2481 if(player.flagcarried)
2483 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2485 ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2489 player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2490 setattachment(player.flagcarried, veh, "");
2491 setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2492 player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2493 //player.flagcarried.angles = '0 0 0';
2499 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2501 entity player = M_ARGV(0, entity);
2503 if(player.flagcarried)
2505 setattachment(player.flagcarried, player, "");
2506 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2507 player.flagcarried.scale = FLAG_SCALE;
2508 player.flagcarried.angles = '0 0 0';
2509 player.flagcarried.nodrawtoclient = NULL;
2514 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2516 entity player = M_ARGV(0, entity);
2518 if(player.flagcarried)
2520 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2521 ctf_RespawnFlag(player.flagcarried);
2526 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2528 entity flag; // temporary entity for the search method
2530 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2532 switch(flag.ctf_status)
2537 // lock the flag, game is over
2538 set_movetype(flag, MOVETYPE_NONE);
2539 flag.takedamage = DAMAGE_NO;
2540 flag.solid = SOLID_NOT;
2541 flag.nextthink = false; // stop thinking
2543 //dprint("stopping the ", flag.netname, " from moving.\n");
2551 // do nothing for these flags
2558 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2560 entity bot = M_ARGV(0, entity);
2562 havocbot_ctf_reset_role(bot);
2566 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2568 M_ARGV(1, string) = "ctf_team";
2571 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2573 entity spectatee = M_ARGV(0, entity);
2574 entity client = M_ARGV(1, entity);
2576 STAT(CTF_FLAGSTATUS, client) = STAT(CTF_FLAGSTATUS, spectatee);
2579 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2581 int record_page = M_ARGV(0, int);
2582 string ret_string = M_ARGV(1, string);
2584 for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2586 if (MapInfo_Get_ByID(i))
2588 float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2594 string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2595 ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2599 M_ARGV(1, string) = ret_string;
2602 bool superspec_Spectate(entity this, entity targ); // TODO
2603 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2604 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2606 entity player = M_ARGV(0, entity);
2607 string cmd_name = M_ARGV(1, string);
2608 int cmd_argc = M_ARGV(2, int);
2610 if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2612 if(cmd_name == "followfc")
2624 case "red": if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2625 case "blue": if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2626 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2627 case "pink": if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2631 FOREACH_CLIENT(IS_PLAYER(it), {
2632 if(it.flagcarried && (it.team == _team || _team == 0))
2635 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2636 continue; // already spectating this fc, try another
2637 return superspec_Spectate(player, it);
2642 superspec_msg("", "", player, "No active flag carrier\n", 1);
2647 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2649 entity frag_target = M_ARGV(0, entity);
2651 if(frag_target.flagcarried)
2652 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2655 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2657 entity player = M_ARGV(0, entity);
2658 if(player.flagcarried)
2659 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2667 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2668 CTF flag for team one (Red).
2670 "angle" Angle the flag will point (minus 90 degrees)...
2671 "model" model to use, note this needs red and blue as skins 0 and 1...
2672 "noise" sound played when flag is picked up...
2673 "noise1" sound played when flag is returned by a teammate...
2674 "noise2" sound played when flag is captured...
2675 "noise3" sound played when flag is lost in the field and respawns itself...
2676 "noise4" sound played when flag is dropped by a player...
2677 "noise5" sound played when flag touches the ground... */
2678 spawnfunc(item_flag_team1)
2680 if(!g_ctf) { delete(this); return; }
2682 ctf_FlagSetup(NUM_TEAM_1, this);
2685 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2686 CTF flag for team two (Blue).
2688 "angle" Angle the flag will point (minus 90 degrees)...
2689 "model" model to use, note this needs red and blue as skins 0 and 1...
2690 "noise" sound played when flag is picked up...
2691 "noise1" sound played when flag is returned by a teammate...
2692 "noise2" sound played when flag is captured...
2693 "noise3" sound played when flag is lost in the field and respawns itself...
2694 "noise4" sound played when flag is dropped by a player...
2695 "noise5" sound played when flag touches the ground... */
2696 spawnfunc(item_flag_team2)
2698 if(!g_ctf) { delete(this); return; }
2700 ctf_FlagSetup(NUM_TEAM_2, this);
2703 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2704 CTF flag for team three (Yellow).
2706 "angle" Angle the flag will point (minus 90 degrees)...
2707 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2708 "noise" sound played when flag is picked up...
2709 "noise1" sound played when flag is returned by a teammate...
2710 "noise2" sound played when flag is captured...
2711 "noise3" sound played when flag is lost in the field and respawns itself...
2712 "noise4" sound played when flag is dropped by a player...
2713 "noise5" sound played when flag touches the ground... */
2714 spawnfunc(item_flag_team3)
2716 if(!g_ctf) { delete(this); return; }
2718 ctf_FlagSetup(NUM_TEAM_3, this);
2721 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2722 CTF flag for team four (Pink).
2724 "angle" Angle the flag will point (minus 90 degrees)...
2725 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2726 "noise" sound played when flag is picked up...
2727 "noise1" sound played when flag is returned by a teammate...
2728 "noise2" sound played when flag is captured...
2729 "noise3" sound played when flag is lost in the field and respawns itself...
2730 "noise4" sound played when flag is dropped by a player...
2731 "noise5" sound played when flag touches the ground... */
2732 spawnfunc(item_flag_team4)
2734 if(!g_ctf) { delete(this); return; }
2736 ctf_FlagSetup(NUM_TEAM_4, this);
2739 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2742 "angle" Angle the flag will point (minus 90 degrees)...
2743 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2744 "noise" sound played when flag is picked up...
2745 "noise1" sound played when flag is returned by a teammate...
2746 "noise2" sound played when flag is captured...
2747 "noise3" sound played when flag is lost in the field and respawns itself...
2748 "noise4" sound played when flag is dropped by a player...
2749 "noise5" sound played when flag touches the ground... */
2750 spawnfunc(item_flag_neutral)
2752 if(!g_ctf) { delete(this); return; }
2753 if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2755 ctf_FlagSetup(0, this);
2758 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2759 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2760 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.
2762 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2763 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2766 if(!g_ctf) { delete(this); return; }
2768 this.team = this.cnt + 1;
2771 // compatibility for quake maps
2772 spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); }
2773 spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); }
2774 spawnfunc(info_player_team1);
2775 spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); }
2776 spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); }
2777 spawnfunc(info_player_team2);
2778 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); }
2779 spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); }
2781 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this); }
2782 spawnfunc(team_neutralobelisk) { spawnfunc_item_flag_neutral(this); }
2784 // compatibility for wop maps
2785 spawnfunc(team_redplayer) { spawnfunc_info_player_team1(this); }
2786 spawnfunc(team_blueplayer) { spawnfunc_info_player_team2(this); }
2787 spawnfunc(team_ctl_redlolly) { spawnfunc_item_flag_team1(this); }
2788 spawnfunc(team_CTL_redlolly) { spawnfunc_item_flag_team1(this); }
2789 spawnfunc(team_ctl_bluelolly) { spawnfunc_item_flag_team2(this); }
2790 spawnfunc(team_CTL_bluelolly) { spawnfunc_item_flag_team2(this); }
2798 void ctf_ScoreRules(int teams)
2800 GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2801 field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2802 field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2803 field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2804 field(SP_CTF_PICKUPS, "pickups", 0);
2805 field(SP_CTF_FCKILLS, "fckills", 0);
2806 field(SP_CTF_RETURNS, "returns", 0);
2807 field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2811 // code from here on is just to support maps that don't have flag and team entities
2812 void ctf_SpawnTeam (string teamname, int teamcolor)
2814 entity this = new_pure(ctf_team);
2815 this.netname = teamname;
2816 this.cnt = teamcolor - 1;
2817 this.spawnfunc_checked = true;
2818 this.team = teamcolor;
2821 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2826 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2828 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2829 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2831 switch(tmp_entity.team)
2833 case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2834 case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2835 case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2836 case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2838 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2841 havocbot_ctf_calculate_middlepoint();
2843 if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2845 ctf_teams = 0; // so set the default red and blue teams
2846 BITSET_ASSIGN(ctf_teams, BIT(0));
2847 BITSET_ASSIGN(ctf_teams, BIT(1));
2850 //ctf_teams = bound(2, ctf_teams, 4);
2852 // if no teams are found, spawn defaults
2853 if(find(NULL, classname, "ctf_team") == NULL)
2855 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2856 if(ctf_teams & BIT(0))
2857 ctf_SpawnTeam("Red", NUM_TEAM_1);
2858 if(ctf_teams & BIT(1))
2859 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2860 if(ctf_teams & BIT(2))
2861 ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2862 if(ctf_teams & BIT(3))
2863 ctf_SpawnTeam("Pink", NUM_TEAM_4);
2866 ctf_ScoreRules(ctf_teams);
2869 void ctf_Initialize()
2871 CTF_FLAG = NEW(Flag);
2872 record_type = CTF_RECORD;
2873 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2875 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2876 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2877 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2879 InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);