1 // ================================================================
2 // Official capture the flag game mode coding, reworked by Samual
3 // Last updated: September, 2012
4 // ================================================================
6 void ctf_FakeTimeLimit(entity e, float t)
9 WriteByte(MSG_ONE, 3); // svc_updatestat
10 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
12 WriteCoord(MSG_ONE, autocvar_timelimit);
14 WriteCoord(MSG_ONE, (t + 1) / 60);
17 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
19 if(autocvar_sv_eventlog)
20 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != world) ? ftos(actor.playerid) : "")));
21 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
24 void ctf_CaptureRecord(entity flag, entity player)
26 float cap_record = ctf_captimerecord;
27 float cap_time = (time - flag.ctf_pickuptime);
28 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
31 if(ctf_oneflag) { Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname); }
32 else if(!ctf_captimerecord) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_TIME_), player.netname, (cap_time * 100)); }
33 else if(cap_time < cap_record) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_BROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
34 else { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_UNBROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
36 // write that shit in the database
37 if(!ctf_oneflag) // but not in 1-flag mode
38 if((!ctf_captimerecord) || (cap_time < cap_record))
40 ctf_captimerecord = cap_time;
41 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
42 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
43 write_recordmarker(player, (time - cap_time), cap_time);
47 void ctf_FlagcarrierWaypoints(entity player)
49 WaypointSprite_Spawn("flagcarrier", 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, true, RADARICON_FLAG, WPCOLOR_FLAGCARRIER(player.team));
50 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON) * 2);
51 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
52 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
55 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
57 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
58 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
59 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
60 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
63 if(current_height) // make sure we can actually do this arcing path
65 targpos = (to + ('0 0 1' * current_height));
66 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
67 if(trace_fraction < 1)
69 //print("normal arc line failed, trying to find new pos...");
70 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
71 targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
72 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
73 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
74 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
77 else { targpos = to; }
79 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
81 vector desired_direction = normalize(targpos - from);
82 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
83 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
86 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
88 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
90 // directional tracing only
92 makevectors(passer_angle);
94 // find the closest point on the enemy to the center of the attack
95 float ang; // angle between shotdir and h
96 float h; // hypotenuse, which is the distance between attacker to head
97 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
99 h = vlen(head_center - passer_center);
100 ang = acos(dotproduct(normalize(head_center - passer_center), v_forward));
103 vector nearest_on_line = (passer_center + a * v_forward);
104 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
106 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
107 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
109 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
114 else { return true; }
118 // =======================
119 // CaptureShield Functions
120 // =======================
122 bool ctf_CaptureShield_CheckStatus(entity p)
124 float s, s2, s3, s4, se, se2, se3, se4, sr, ser;
126 float players_worseeq, players_total;
128 if(ctf_captureshield_max_ratio <= 0)
131 s = PlayerScore_Add(p, SP_CTF_CAPS, 0);
132 s2 = PlayerScore_Add(p, SP_CTF_PICKUPS, 0);
133 s3 = PlayerScore_Add(p, SP_CTF_RETURNS, 0);
134 s4 = PlayerScore_Add(p, SP_CTF_FCKILLS, 0);
136 sr = ((s - s2) + (s3 + s4));
138 if(sr >= -ctf_captureshield_min_negscore)
141 players_total = players_worseeq = 0;
146 se = PlayerScore_Add(e, SP_CTF_CAPS, 0);
147 se2 = PlayerScore_Add(e, SP_CTF_PICKUPS, 0);
148 se3 = PlayerScore_Add(e, SP_CTF_RETURNS, 0);
149 se4 = PlayerScore_Add(e, SP_CTF_FCKILLS, 0);
151 ser = ((se - se2) + (se3 + se4));
158 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
159 // use this rule here
161 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
167 void ctf_CaptureShield_Update(entity player, bool wanted_status)
169 bool updated_status = ctf_CaptureShield_CheckStatus(player);
170 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
172 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
173 player.ctf_captureshielded = updated_status;
177 bool ctf_CaptureShield_Customize()
179 if(self.enemy.active != ACTIVE_ACTIVE) { return true; }
180 if(!other.ctf_captureshielded) { return false; }
181 if(CTF_SAMETEAM(self, other)) { return false; }
186 void ctf_CaptureShield_Touch()
188 if(self.enemy.active != ACTIVE_ACTIVE)
190 vector mymid = (self.absmin + self.absmax) * 0.5;
191 vector othermid = (other.absmin + other.absmax) * 0.5;
193 Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
194 if(IS_REAL_CLIENT(other)) { Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_INACTIVE); }
199 if(!other.ctf_captureshielded) { return; }
200 if(CTF_SAMETEAM(self, other)) { return; }
202 vector mymid = (self.absmin + self.absmax) * 0.5;
203 vector othermid = (other.absmin + other.absmax) * 0.5;
205 Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
206 if(IS_REAL_CLIENT(other)) { Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
209 void ctf_CaptureShield_Spawn(entity flag)
211 entity shield = spawn();
214 shield.team = self.team;
215 shield.touch = ctf_CaptureShield_Touch;
216 shield.customizeentityforclient = ctf_CaptureShield_Customize;
217 shield.classname = "ctf_captureshield";
218 shield.effects = EF_ADDITIVE;
219 shield.movetype = MOVETYPE_NOCLIP;
220 shield.solid = SOLID_TRIGGER;
221 shield.avelocity = '7 0 11';
224 setorigin(shield, self.origin);
225 setmodel(shield, "models/ctf/shield.md3");
226 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
230 // ====================
231 // Drop/Pass/Throw Code
232 // ====================
234 void ctf_Handle_Drop(entity flag, entity player, int droptype)
237 player = (player ? player : flag.pass_sender);
240 flag.movetype = MOVETYPE_TOSS;
241 flag.takedamage = DAMAGE_YES;
242 flag.angles = '0 0 0';
243 flag.health = flag.max_flag_health;
244 flag.ctf_droptime = time;
245 flag.ctf_dropper = player;
246 flag.ctf_status = FLAG_DROPPED;
248 // messages and sounds
249 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_LOST_) : INFO_CTF_LOST_NEUTRAL), player.netname);
250 sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
251 ctf_EventLog("dropped", player.team, player);
254 PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);
255 PlayerScore_Add(player, SP_CTF_DROPS, 1);
258 if(autocvar_g_ctf_flag_dropped_waypoint)
259 WaypointSprite_Spawn("flagdropped", 0, 0, flag, FLAG_WAYPOINT_OFFSET, world, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, true, RADARICON_FLAG, WPCOLOR_DROPPEDFLAG(flag.team));
261 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
263 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
264 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
267 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
269 if(droptype == DROP_PASS)
271 flag.pass_distance = 0;
272 flag.pass_sender = world;
273 flag.pass_target = world;
277 void ctf_Handle_Retrieve(entity flag, entity player)
279 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
280 entity sender = flag.pass_sender;
282 // transfer flag to player
284 flag.owner.flagcarried = flag;
289 setattachment(flag, player.vehicle, "");
290 setorigin(flag, VEHICLE_FLAG_OFFSET);
291 flag.scale = VEHICLE_FLAG_SCALE;
295 setattachment(flag, player, "");
296 setorigin(flag, FLAG_CARRY_OFFSET);
298 flag.movetype = MOVETYPE_NONE;
299 flag.takedamage = DAMAGE_NO;
300 flag.solid = SOLID_NOT;
301 flag.angles = '0 0 0';
302 flag.ctf_status = FLAG_CARRY;
304 // messages and sounds
305 sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
306 ctf_EventLog("receive", flag.team, player);
308 FOR_EACH_REALPLAYER(tmp_player)
310 if(tmp_player == sender)
311 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_SENT_) : CENTER_CTF_PASS_SENT_NEUTRAL), player.netname);
312 else if(tmp_player == player)
313 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_RECEIVED_) : CENTER_CTF_PASS_RECEIVED_NEUTRAL), sender.netname);
314 else if(SAME_TEAM(tmp_player, sender))
315 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_OTHER_) : CENTER_CTF_PASS_OTHER_NEUTRAL), sender.netname, player.netname);
318 // create new waypoint
319 ctf_FlagcarrierWaypoints(player);
321 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
322 player.throw_antispam = sender.throw_antispam;
324 flag.pass_distance = 0;
325 flag.pass_sender = world;
326 flag.pass_target = world;
329 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
331 entity flag = player.flagcarried;
332 vector targ_origin, flag_velocity;
334 if(!flag) { return; }
335 if((droptype == DROP_PASS) && !receiver) { return; }
337 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
340 setattachment(flag, world, "");
341 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
342 flag.owner.flagcarried = world;
344 flag.solid = SOLID_TRIGGER;
345 flag.ctf_dropper = player;
346 flag.ctf_droptime = time;
348 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
355 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
356 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
357 WarpZone_RefSys_Copy(flag, receiver);
358 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
359 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
361 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
362 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
365 flag.movetype = MOVETYPE_FLY;
366 flag.takedamage = DAMAGE_NO;
367 flag.pass_sender = player;
368 flag.pass_target = receiver;
369 flag.ctf_status = FLAG_PASSING;
372 sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
373 WarpZone_TrailParticles(world, particleeffectnum(flag.passeffect), player.origin, targ_origin);
374 ctf_EventLog("pass", flag.team, player);
380 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'));
382 flag_velocity = (('0 0 1' * autocvar_g_ctf_throw_velocity_up) + ((v_forward * autocvar_g_ctf_throw_velocity_forward) * ((player.items & IT_STRENGTH) ? autocvar_g_ctf_throw_strengthmultiplier : 1)));
383 flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, false);
384 ctf_Handle_Drop(flag, player, droptype);
390 flag.velocity = '0 0 0'; // do nothing
397 flag.velocity = W_CalculateProjectileVelocity(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);
398 ctf_Handle_Drop(flag, player, droptype);
403 // kill old waypointsprite
404 WaypointSprite_Ping(player.wps_flagcarrier);
405 WaypointSprite_Kill(player.wps_flagcarrier);
407 if(player.wps_enemyflagcarrier)
408 WaypointSprite_Kill(player.wps_enemyflagcarrier);
411 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
419 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
421 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
422 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
423 entity player_team_flag = world, tmp_entity;
424 float old_time, new_time;
426 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
427 if(CTF_DIFFTEAM(player, flag)) { return; }
430 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
431 if(SAME_TEAM(tmp_entity, player))
433 player_team_flag = tmp_entity;
437 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
439 player.throw_prevtime = time;
440 player.throw_count = 0;
442 // messages and sounds
443 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((enemy_flag.team) ? APP_TEAM_ENT_4(enemy_flag, CENTER_CTF_CAPTURE_) : CENTER_CTF_CAPTURE_NEUTRAL));
444 ctf_CaptureRecord(enemy_flag, player);
445 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);
449 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
450 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
455 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
456 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
458 old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
459 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
460 if(!old_time || new_time < old_time)
461 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
464 pointparticles(particleeffectnum(flag.capeffect), flag.origin, '0 0 0', 1);
465 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
468 if(capturetype == CAPTURE_NORMAL)
470 WaypointSprite_Kill(player.wps_flagcarrier);
471 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
473 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
474 { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
478 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
479 ctf_RespawnFlag(enemy_flag);
482 void ctf_Handle_Return(entity flag, entity player)
484 // messages and sounds
485 if(player.flags & FL_MONSTER)
487 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_MONSTER_), player.monster_name);
491 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_RETURN_));
492 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_), player.netname);
494 sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
495 ctf_EventLog("return", flag.team, player);
498 if(IS_PLAYER(player))
500 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
501 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
503 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
506 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
510 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
511 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
512 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
516 if(player.flagcarried == flag)
517 WaypointSprite_Kill(player.wps_flagcarrier);
520 ctf_RespawnFlag(flag);
523 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
526 float pickup_dropped_score; // used to calculate dropped pickup score
527 entity tmp_entity; // temporary entity
529 // attach the flag to the player
531 player.flagcarried = flag;
534 setattachment(flag, player.vehicle, "");
535 setorigin(flag, VEHICLE_FLAG_OFFSET);
536 flag.scale = VEHICLE_FLAG_SCALE;
540 setattachment(flag, player, "");
541 setorigin(flag, FLAG_CARRY_OFFSET);
545 flag.movetype = MOVETYPE_NONE;
546 flag.takedamage = DAMAGE_NO;
547 flag.solid = SOLID_NOT;
548 flag.angles = '0 0 0';
549 flag.ctf_status = FLAG_CARRY;
553 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
554 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
558 // messages and sounds
559 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_PICKUP_) : INFO_CTF_PICKUP_NEUTRAL), player.netname);
560 if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
561 if(!flag.team) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL); }
562 else if(CTF_DIFFTEAM(player, flag)) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PICKUP_)); }
563 else { Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_TEAM : CENTER_CTF_PICKUP_TEAM_ENEMY), Team_ColorCode(flag.team)); }
565 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, ((flag.team) ? APP_TEAM_ENT_4(flag, CHOICE_CTF_PICKUP_TEAM_) : CHOICE_CTF_PICKUP_TEAM_NEUTRAL), Team_ColorCode(player.team), player.netname);
568 FOR_EACH_PLAYER(tmp_entity)
569 if(tmp_entity != player)
570 if(DIFF_TEAM(player, tmp_entity))
571 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, CHOICE_CTF_PICKUP_ENEMY_NEUTRAL, Team_ColorCode(player.team), player.netname);
574 FOR_EACH_PLAYER(tmp_entity)
575 if(tmp_entity != player)
576 if(CTF_SAMETEAM(flag, tmp_entity))
577 if(SAME_TEAM(player, tmp_entity))
578 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_PICKUP_TEAM_), Team_ColorCode(player.team), player.netname);
580 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, ((SAME_TEAM(flag, player)) ? CHOICE_CTF_PICKUP_ENEMY_TEAM : CHOICE_CTF_PICKUP_ENEMY), Team_ColorCode(player.team), player.netname);
582 sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
585 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
586 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
591 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
592 ctf_EventLog("steal", flag.team, player);
598 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);
599 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);
600 dprint("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
601 PlayerTeamScore_AddScore(player, pickup_dropped_score);
602 ctf_EventLog("pickup", flag.team, player);
610 if(pickuptype == PICKUP_BASE)
612 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
613 if((player.speedrunning) && (ctf_captimerecord))
614 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
618 pointparticles(particleeffectnum(flag.toucheffect), player.origin, '0 0 0', 1);
621 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
622 ctf_FlagcarrierWaypoints(player);
623 WaypointSprite_Ping(player.wps_flagcarrier);
627 // ===================
628 // Main Flag Functions
629 // ===================
631 void ctf_CheckFlagReturn(entity flag, int returntype)
633 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
635 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
637 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
641 case RETURN_DROPPED: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DROPPED_) : INFO_CTF_FLAGRETURN_DROPPED_NEUTRAL)); break;
642 case RETURN_DAMAGE: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DAMAGED_) : INFO_CTF_FLAGRETURN_DAMAGED_NEUTRAL)); break;
643 case RETURN_SPEEDRUN: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_SPEEDRUN_) : INFO_CTF_FLAGRETURN_SPEEDRUN_NEUTRAL), ctf_captimerecord); break;
644 case RETURN_NEEDKILL: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_NEEDKILL_) : INFO_CTF_FLAGRETURN_NEEDKILL_NEUTRAL)); break;
648 { Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_TIMEOUT_) : INFO_CTF_FLAGRETURN_TIMEOUT_NEUTRAL)); break; }
650 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
651 ctf_EventLog("returned", flag.team, world);
652 ctf_RespawnFlag(flag);
657 bool ctf_Stalemate_Customize()
659 // make spectators see what the player would see
661 e = WaypointSprite_getviewentity(other);
662 wp_owner = self.owner;
665 if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
666 if(SAME_TEAM(wp_owner, e)) { return false; }
667 if(!IS_PLAYER(e)) { return false; }
672 void ctf_CheckStalemate(void)
675 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
678 entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
680 // build list of stale flags
681 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
683 if(autocvar_g_ctf_stalemate)
684 if(tmp_entity.ctf_status != FLAG_BASE)
685 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
687 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
688 ctf_staleflaglist = tmp_entity;
690 switch(tmp_entity.team)
692 case NUM_TEAM_1: ++stale_red_flags; break;
693 case NUM_TEAM_2: ++stale_blue_flags; break;
694 case NUM_TEAM_3: ++stale_yellow_flags; break;
695 case NUM_TEAM_4: ++stale_pink_flags; break;
696 default: ++stale_neutral_flags; break;
702 stale_flags = (stale_neutral_flags >= 1);
704 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
706 if(ctf_oneflag && stale_flags == 1)
707 ctf_stalemate = true;
708 else if(stale_flags == ctf_teams)
709 ctf_stalemate = true;
710 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
711 { ctf_stalemate = false; wpforenemy_announced = false; }
712 else if(stale_flags < ctf_teams && autocvar_g_ctf_stalemate_endcondition == 1)
713 { ctf_stalemate = false; wpforenemy_announced = false; }
715 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
718 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
720 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
722 WaypointSprite_Spawn(((ctf_oneflag) ? "flagcarrier" : "enemyflagcarrier"), 0, 0, tmp_entity.owner, FLAG_WAYPOINT_OFFSET, world, 0, tmp_entity.owner, wps_enemyflagcarrier, true, RADARICON_FLAG, WPCOLOR_ENEMYFC(tmp_entity.owner.team));
723 tmp_entity.owner.wps_enemyflagcarrier.customizeentityforclient = ctf_Stalemate_Customize;
727 if (!wpforenemy_announced)
729 FOR_EACH_REALPLAYER(tmp_entity)
730 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CENTER, ((tmp_entity.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER));
732 wpforenemy_announced = true;
737 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
739 if(ITEM_DAMAGE_NEEDKILL(deathtype))
741 if(autocvar_g_ctf_flag_return_damage_delay)
743 self.ctf_flagdamaged = true;
748 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
752 if(autocvar_g_ctf_flag_return_damage)
754 // reduce health and check if it should be returned
755 self.health = self.health - damage;
756 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
766 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
769 if(self == ctf_worldflaglist) // only for the first flag
770 FOR_EACH_CLIENT(tmp_entity)
771 ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
774 if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
775 dprint("wtf the flag got squashed?\n");
776 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
777 if(!trace_startsolid || self.noalign) // can we resize it without getting stuck?
778 setsize(self, FLAG_MIN, FLAG_MAX); }
780 switch(self.ctf_status) // reset flag angles in case warpzones adjust it
784 self.angles = '0 0 0';
792 switch(self.ctf_status)
796 if(autocvar_g_ctf_dropped_capture_radius)
798 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
799 if(tmp_entity.ctf_status == FLAG_DROPPED)
800 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
801 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
802 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
809 if(autocvar_g_ctf_flag_dropped_floatinwater)
811 vector midpoint = ((self.absmin + self.absmax) * 0.5);
812 if(pointcontents(midpoint) == CONTENT_WATER)
814 self.velocity = self.velocity * 0.5;
816 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
817 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
819 { self.movetype = MOVETYPE_FLY; }
821 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
823 if(autocvar_g_ctf_flag_return_dropped)
825 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
828 ctf_CheckFlagReturn(self, RETURN_DROPPED);
832 if(self.ctf_flagdamaged)
834 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
835 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
838 else if(autocvar_g_ctf_flag_return_time)
840 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
841 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
849 if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
852 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
856 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
860 if(autocvar_g_ctf_stalemate)
862 if(time >= wpforenemy_nextthink)
864 ctf_CheckStalemate();
865 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
868 if(CTF_SAMETEAM(self, self.owner) && self.team)
870 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
871 ctf_Handle_Throw(self.owner, world, DROP_THROW);
872 else if(vlen(self.owner.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_carried_radius)
873 ctf_Handle_Return(self, self.owner);
880 vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
881 targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
882 WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
884 if((self.pass_target == world)
885 || (self.pass_target.deadflag != DEAD_NO)
886 || (self.pass_target.flagcarried)
887 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
888 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
889 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
891 // give up, pass failed
892 ctf_Handle_Drop(self, world, DROP_PASS);
896 // still a viable target, go for it
897 ctf_CalculatePassVelocity(self, targ_origin, self.origin, true);
902 default: // this should never happen
904 dprint("ctf_FlagThink(): Flag exists with no status?\n");
912 if(gameover) { return; }
913 if(self.active != ACTIVE_ACTIVE) { return; }
914 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
916 entity toucher = other, tmp_entity;
917 bool is_not_monster = (!(toucher.flags & FL_MONSTER)), num_perteam = 0;
919 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
920 if(ITEM_TOUCH_NEEDKILL())
922 if(!autocvar_g_ctf_flag_return_damage_delay)
925 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
927 if(!self.ctf_flagdamaged) { return; }
930 FOR_EACH_PLAYER(tmp_entity) if(SAME_TEAM(toucher, tmp_entity)) { ++num_perteam; }
932 // special touch behaviors
933 if(toucher.frozen) { return; }
934 else if(toucher.vehicle_flags & VHF_ISVEHICLE)
936 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
937 toucher = toucher.owner; // the player is actually the vehicle owner, not other
939 return; // do nothing
941 else if(toucher.flags & FL_MONSTER)
943 if(!autocvar_g_ctf_allow_monster_touch)
944 return; // do nothing
946 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
948 if(time > self.wait) // if we haven't in a while, play a sound/effect
950 pointparticles(particleeffectnum(self.toucheffect), self.origin, '0 0 0', 1);
951 sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTEN_NORM);
952 self.wait = time + FLAG_TOUCHRATE;
956 else if(toucher.deadflag != DEAD_NO) { return; }
958 switch(self.ctf_status)
964 if(CTF_SAMETEAM(toucher, self) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
965 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
966 else if(!self.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
967 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the neutral flag
969 else if(CTF_SAMETEAM(toucher, self) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, self) && is_not_monster)
970 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
971 else if(CTF_DIFFTEAM(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
972 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
978 if(CTF_SAMETEAM(toucher, self) && (autocvar_g_ctf_flag_return || num_perteam <= 1) && self.team) // automatically return if there's only 1 player on the team
979 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
980 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
981 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
987 dprint("Someone touched a flag even though it was being carried?\n");
993 if((IS_PLAYER(toucher)) && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
995 if(DIFF_TEAM(toucher, self.pass_sender))
996 ctf_Handle_Return(self, toucher);
998 ctf_Handle_Retrieve(self, toucher);
1005 .float last_respawn;
1006 void ctf_RespawnFlag(entity flag)
1008 // check for flag respawn being called twice in a row
1009 if(flag.last_respawn > time - 0.5)
1010 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1012 flag.last_respawn = time;
1014 // reset the player (if there is one)
1015 if((flag.owner) && (flag.owner.flagcarried == flag))
1017 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1018 WaypointSprite_Kill(flag.wps_flagcarrier);
1020 flag.owner.flagcarried = world;
1022 if(flag.speedrunning)
1023 ctf_FakeTimeLimit(flag.owner, -1);
1026 if((flag.owner) && (flag.owner.vehicle))
1027 flag.scale = FLAG_SCALE;
1029 if(flag.ctf_status == FLAG_DROPPED)
1030 { WaypointSprite_Kill(flag.wps_flagdropped); }
1033 setattachment(flag, world, "");
1034 setorigin(flag, flag.ctf_spawnorigin);
1036 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
1037 flag.takedamage = DAMAGE_NO;
1038 flag.health = flag.max_flag_health;
1039 flag.solid = SOLID_TRIGGER;
1040 flag.velocity = '0 0 0';
1041 flag.angles = flag.mangle;
1042 flag.flags = FL_ITEM | FL_NOTARGET;
1044 flag.ctf_status = FLAG_BASE;
1046 flag.pass_distance = 0;
1047 flag.pass_sender = world;
1048 flag.pass_target = world;
1049 flag.ctf_dropper = world;
1050 flag.ctf_pickuptime = 0;
1051 flag.ctf_droptime = 0;
1052 flag.ctf_flagdamaged = 0;
1054 ctf_CheckStalemate();
1060 if(IS_PLAYER(self.owner))
1061 ctf_Handle_Throw(self.owner, world, DROP_RESET);
1063 ctf_RespawnFlag(self);
1068 if(self.ctf_status != FLAG_BASE) { return; }
1070 self.active = ((self.active) ? ACTIVE_NOT : ACTIVE_ACTIVE);
1072 if(self.active == ACTIVE_ACTIVE)
1073 WaypointSprite_Ping(self.wps_flagbase);
1076 bool ctf_FlagWaypoint_Customize()
1078 if(self.owner.active != ACTIVE_ACTIVE) { return false; }
1082 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
1085 waypoint_spawnforitem_force(self, self.origin);
1086 self.nearestwaypointtimeout = 0; // activate waypointing again
1087 self.bot_basewaypoint = self.nearestwaypoint;
1090 string basename = "base";
1094 case NUM_TEAM_1: basename = "redbase"; break;
1095 case NUM_TEAM_2: basename = "bluebase"; break;
1096 case NUM_TEAM_3: basename = "yellowbase"; break;
1097 case NUM_TEAM_4: basename = "pinkbase"; break;
1098 default: basename = "neutralbase"; break;
1101 WaypointSprite_SpawnFixed(basename, self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, ((self.team) ? Team_ColorRGB(self.team) : '1 1 1'));
1102 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, ((self.team) ? colormapPaletteColor(self.team - 1, false) : '1 1 1'));
1103 self.wps_flagbase.customizeentityforclient = ctf_FlagWaypoint_Customize;
1105 // captureshield setup
1106 ctf_CaptureShield_Spawn(self);
1109 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1112 self = flag; // for later usage with droptofloor()
1115 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1116 ctf_worldflaglist = flag;
1118 setattachment(flag, world, "");
1120 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1121 flag.team = teamnumber;
1122 flag.classname = "item_flag_team";
1123 flag.target = "###item###"; // wut?
1124 flag.flags = FL_ITEM | FL_NOTARGET;
1125 flag.solid = SOLID_TRIGGER;
1126 flag.takedamage = DAMAGE_NO;
1127 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1128 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1129 flag.health = flag.max_flag_health;
1130 flag.event_damage = ctf_FlagDamage;
1131 flag.pushable = true;
1132 flag.teleportable = TELEPORT_NORMAL;
1133 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP;
1134 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1135 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1136 flag.velocity = '0 0 0';
1137 flag.mangle = flag.angles;
1138 flag.reset = ctf_Reset;
1140 flag.touch = ctf_FlagTouch;
1141 flag.think = ctf_FlagThink;
1142 flag.nextthink = time + FLAG_THINKRATE;
1143 flag.ctf_status = FLAG_BASE;
1146 if(flag.model == "") { flag.model = ((teamnumber == NUM_TEAM_1) ? autocvar_g_ctf_flag_red_model : ((teamnumber == NUM_TEAM_2) ? autocvar_g_ctf_flag_blue_model : ((teamnumber == NUM_TEAM_3) ? autocvar_g_ctf_flag_yellow_model : ((teamnumber == NUM_TEAM_4) ? autocvar_g_ctf_flag_pink_model : autocvar_g_ctf_flag_neutral_model)))); }
1147 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1148 if(!flag.skin) { flag.skin = ((teamnumber == NUM_TEAM_1) ? autocvar_g_ctf_flag_red_skin : ((teamnumber == NUM_TEAM_2) ? autocvar_g_ctf_flag_blue_skin : ((teamnumber == NUM_TEAM_3) ? autocvar_g_ctf_flag_yellow_skin : ((teamnumber == NUM_TEAM_4) ? autocvar_g_ctf_flag_pink_skin : autocvar_g_ctf_flag_neutral_skin)))); }
1149 if(flag.toucheffect == "") { flag.toucheffect = ((teamnumber == NUM_TEAM_1) ? "redflag_touch" : ((teamnumber == NUM_TEAM_2) ? "blueflag_touch" : ((teamnumber == NUM_TEAM_3) ? "yellowflag_touch" : ((teamnumber == NUM_TEAM_4) ? "pinkflag_touch" : "neutralflag_touch")))); }
1150 if(flag.passeffect == "") { flag.passeffect = ((teamnumber == NUM_TEAM_1) ? "red_pass" : ((teamnumber == NUM_TEAM_2) ? "blue_pass" : ((teamnumber == NUM_TEAM_3) ? "yellow_pass" : ((teamnumber == NUM_TEAM_4) ? "pink_pass" : "neutral_pass")))); }
1151 if(flag.capeffect == "") { flag.capeffect = ((teamnumber == NUM_TEAM_1) ? "red_cap" : ((teamnumber == NUM_TEAM_2) ? "blue_cap" : ((teamnumber == NUM_TEAM_3) ? "yellow_cap" : ((teamnumber == NUM_TEAM_4) ? "pink_cap" : "")))); } // neutral flag cant be capped itself
1152 if(!flag.active) { flag.active = ACTIVE_ACTIVE; }
1155 if(flag.snd_flag_taken == "") { flag.snd_flag_taken = ((teamnumber == NUM_TEAM_1) ? "ctf/red_taken.wav" : ((teamnumber == NUM_TEAM_2) ? "ctf/blue_taken.wav" : ((teamnumber == NUM_TEAM_3) ? "ctf/yellow_taken.wav" : ((teamnumber == NUM_TEAM_4) ? "ctf/pink_taken.wav" : "ctf/neutral_taken.wav")))); }
1156 if(flag.snd_flag_returned == "") { flag.snd_flag_returned = ((teamnumber == NUM_TEAM_1) ? "ctf/red_returned.wav" : ((teamnumber == NUM_TEAM_2) ? "ctf/blue_returned.wav" : ((teamnumber == NUM_TEAM_3) ? "ctf/yellow_returned.wav" : ((teamnumber == NUM_TEAM_4) ? "ctf/pink_returned.wav" : "")))); } // neutral flag can't be returned by players
1157 if(flag.snd_flag_capture == "") { flag.snd_flag_capture = ((teamnumber == NUM_TEAM_1) ? "ctf/red_capture.wav" : ((teamnumber == NUM_TEAM_2) ? "ctf/blue_capture.wav" : ((teamnumber == NUM_TEAM_3) ? "ctf/yellow_capture.wav" : ((teamnumber == NUM_TEAM_4) ? "ctf/pink_capture.wav" : "")))); } // again can't be captured
1158 if(flag.snd_flag_respawn == "") { flag.snd_flag_respawn = "ctf/flag_respawn.wav"; } // if there is ever a team-based sound for this, update the code to match.
1159 if(flag.snd_flag_dropped == "") { flag.snd_flag_dropped = ((teamnumber == NUM_TEAM_1) ? "ctf/red_dropped.wav" : ((teamnumber == NUM_TEAM_2) ? "ctf/blue_dropped.wav" : ((teamnumber == NUM_TEAM_3) ? "ctf/yellow_dropped.wav" : ((teamnumber == NUM_TEAM_4) ? "ctf/pink_dropped.wav" : "ctf/neutral_dropped.wav")))); }
1160 if(flag.snd_flag_touch == "") { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
1161 if(flag.snd_flag_pass == "") { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
1164 precache_sound(flag.snd_flag_taken);
1165 precache_sound(flag.snd_flag_returned);
1166 precache_sound(flag.snd_flag_capture);
1167 precache_sound(flag.snd_flag_respawn);
1168 precache_sound(flag.snd_flag_dropped);
1169 precache_sound(flag.snd_flag_touch);
1170 precache_sound(flag.snd_flag_pass);
1171 precache_model(flag.model);
1172 precache_model("models/ctf/shield.md3");
1173 precache_model("models/ctf/shockwavetransring.md3");
1176 setmodel(flag, flag.model); // precision set below
1177 setsize(flag, FLAG_MIN, FLAG_MAX);
1178 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1180 if(autocvar_g_ctf_flag_glowtrails)
1182 flag.glow_color = ((teamnumber == NUM_TEAM_1) ? 251 : ((teamnumber == NUM_TEAM_2) ? 210 : ((teamnumber == NUM_TEAM_3) ? 110 : ((teamnumber == NUM_TEAM_4) ? 145 : 254))));
1183 flag.glow_size = 25;
1184 flag.glow_trail = 1;
1187 flag.effects |= EF_LOWPRECISION;
1188 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1189 if(autocvar_g_ctf_dynamiclights)
1193 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1194 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1195 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1196 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1197 default: flag.effects |= EF_DIMLIGHT; break;
1202 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1204 flag.dropped_origin = flag.origin;
1205 flag.noalign = true;
1206 flag.movetype = MOVETYPE_NONE;
1208 else // drop to floor, automatically find a platform and set that as spawn origin
1210 flag.noalign = false;
1213 flag.movetype = MOVETYPE_TOSS;
1216 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1224 // NOTE: LEGACY CODE, needs to be re-written!
1226 void havocbot_calculate_middlepoint()
1230 vector fo = '0 0 0';
1233 f = ctf_worldflaglist;
1238 f = f.ctf_worldflagnext;
1242 havocbot_ctf_middlepoint = s * (1.0 / n);
1243 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1247 entity havocbot_ctf_find_flag(entity bot)
1250 f = ctf_worldflaglist;
1253 if (CTF_SAMETEAM(bot, f))
1255 f = f.ctf_worldflagnext;
1260 entity havocbot_ctf_find_enemy_flag(entity bot)
1263 f = ctf_worldflaglist;
1268 if(CTF_DIFFTEAM(bot, f))
1275 else if(!bot.flagcarried)
1279 else if (CTF_DIFFTEAM(bot, f))
1281 f = f.ctf_worldflagnext;
1286 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1294 FOR_EACH_PLAYER(head)
1296 if(DIFF_TEAM(head, bot) || head.deadflag != DEAD_NO || head == bot)
1299 if(vlen(head.origin - org) < tc_radius)
1306 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1309 head = ctf_worldflaglist;
1312 if (CTF_SAMETEAM(self, head))
1314 head = head.ctf_worldflagnext;
1317 navigation_routerating(head, ratingscale, 10000);
1320 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1323 head = ctf_worldflaglist;
1326 if (CTF_SAMETEAM(self, head))
1328 head = head.ctf_worldflagnext;
1333 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1336 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1339 head = ctf_worldflaglist;
1344 if(CTF_DIFFTEAM(self, head))
1348 if(self.flagcarried)
1351 else if(!self.flagcarried)
1355 else if(CTF_DIFFTEAM(self, head))
1357 head = head.ctf_worldflagnext;
1360 navigation_routerating(head, ratingscale, 10000);
1363 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1365 if (!bot_waypoints_for_items)
1367 havocbot_goalrating_ctf_enemyflag(ratingscale);
1373 head = havocbot_ctf_find_enemy_flag(self);
1378 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1381 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1385 mf = havocbot_ctf_find_flag(self);
1387 if(mf.ctf_status == FLAG_BASE)
1391 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1394 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1397 head = ctf_worldflaglist;
1400 // flag is out in the field
1401 if(head.ctf_status != FLAG_BASE)
1402 if(head.tag_entity==world) // dropped
1406 if(vlen(org-head.origin)<df_radius)
1407 navigation_routerating(head, ratingscale, 10000);
1410 navigation_routerating(head, ratingscale, 10000);
1413 head = head.ctf_worldflagnext;
1417 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1421 head = findchainfloat(bot_pickup, true);
1424 // gather health and armor only
1426 if (head.health || head.armorvalue)
1427 if (vlen(head.origin - org) < sradius)
1429 // get the value of the item
1430 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1432 navigation_routerating(head, t * ratingscale, 500);
1438 void havocbot_ctf_reset_role(entity bot)
1440 float cdefense, cmiddle, coffense;
1441 entity mf, ef, head;
1444 if(bot.deadflag != DEAD_NO)
1447 if(vlen(havocbot_ctf_middlepoint)==0)
1448 havocbot_calculate_middlepoint();
1451 if (bot.flagcarried)
1453 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1457 mf = havocbot_ctf_find_flag(bot);
1458 ef = havocbot_ctf_find_enemy_flag(bot);
1460 // Retrieve stolen flag
1461 if(mf.ctf_status!=FLAG_BASE)
1463 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1467 // If enemy flag is taken go to the middle to intercept pursuers
1468 if(ef.ctf_status!=FLAG_BASE)
1470 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1474 // if there is only me on the team switch to offense
1476 FOR_EACH_PLAYER(head)
1477 if(SAME_TEAM(head, bot))
1482 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1486 // Evaluate best position to take
1487 // Count mates on middle position
1488 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1490 // Count mates on defense position
1491 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1493 // Count mates on offense position
1494 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1496 if(cdefense<=coffense)
1497 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1498 else if(coffense<=cmiddle)
1499 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1501 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1504 void havocbot_role_ctf_carrier()
1506 if(self.deadflag != DEAD_NO)
1508 havocbot_ctf_reset_role(self);
1512 if (self.flagcarried == world)
1514 havocbot_ctf_reset_role(self);
1518 if (self.bot_strategytime < time)
1520 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1522 navigation_goalrating_start();
1524 havocbot_goalrating_ctf_enemybase(50000);
1526 havocbot_goalrating_ctf_ourbase(50000);
1529 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1531 navigation_goalrating_end();
1533 if (self.navigation_hasgoals)
1534 self.havocbot_cantfindflag = time + 10;
1535 else if (time > self.havocbot_cantfindflag)
1537 // Can't navigate to my own base, suicide!
1538 // TODO: drop it and wander around
1539 Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1545 void havocbot_role_ctf_escort()
1549 if(self.deadflag != DEAD_NO)
1551 havocbot_ctf_reset_role(self);
1555 if (self.flagcarried)
1557 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1561 // If enemy flag is back on the base switch to previous role
1562 ef = havocbot_ctf_find_enemy_flag(self);
1563 if(ef.ctf_status==FLAG_BASE)
1565 self.havocbot_role = self.havocbot_previous_role;
1566 self.havocbot_role_timeout = 0;
1570 // If the flag carrier reached the base switch to defense
1571 mf = havocbot_ctf_find_flag(self);
1572 if(mf.ctf_status!=FLAG_BASE)
1573 if(vlen(ef.origin - mf.dropped_origin) < 300)
1575 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1579 // Set the role timeout if necessary
1580 if (!self.havocbot_role_timeout)
1582 self.havocbot_role_timeout = time + random() * 30 + 60;
1585 // If nothing happened just switch to previous role
1586 if (time > self.havocbot_role_timeout)
1588 self.havocbot_role = self.havocbot_previous_role;
1589 self.havocbot_role_timeout = 0;
1593 // Chase the flag carrier
1594 if (self.bot_strategytime < time)
1596 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1597 navigation_goalrating_start();
1598 havocbot_goalrating_ctf_enemyflag(30000);
1599 havocbot_goalrating_ctf_ourstolenflag(40000);
1600 havocbot_goalrating_items(10000, self.origin, 10000);
1601 navigation_goalrating_end();
1605 void havocbot_role_ctf_offense()
1610 if(self.deadflag != DEAD_NO)
1612 havocbot_ctf_reset_role(self);
1616 if (self.flagcarried)
1618 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1623 mf = havocbot_ctf_find_flag(self);
1624 ef = havocbot_ctf_find_enemy_flag(self);
1627 if(mf.ctf_status!=FLAG_BASE)
1630 pos = mf.tag_entity.origin;
1634 // Try to get it if closer than the enemy base
1635 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1637 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1642 // Escort flag carrier
1643 if(ef.ctf_status!=FLAG_BASE)
1646 pos = ef.tag_entity.origin;
1650 if(vlen(pos-mf.dropped_origin)>700)
1652 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1657 // About to fail, switch to middlefield
1660 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1664 // Set the role timeout if necessary
1665 if (!self.havocbot_role_timeout)
1666 self.havocbot_role_timeout = time + 120;
1668 if (time > self.havocbot_role_timeout)
1670 havocbot_ctf_reset_role(self);
1674 if (self.bot_strategytime < time)
1676 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1677 navigation_goalrating_start();
1678 havocbot_goalrating_ctf_ourstolenflag(50000);
1679 havocbot_goalrating_ctf_enemybase(20000);
1680 havocbot_goalrating_items(5000, self.origin, 1000);
1681 havocbot_goalrating_items(1000, self.origin, 10000);
1682 navigation_goalrating_end();
1686 // Retriever (temporary role):
1687 void havocbot_role_ctf_retriever()
1691 if(self.deadflag != DEAD_NO)
1693 havocbot_ctf_reset_role(self);
1697 if (self.flagcarried)
1699 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1703 // If flag is back on the base switch to previous role
1704 mf = havocbot_ctf_find_flag(self);
1705 if(mf.ctf_status==FLAG_BASE)
1707 havocbot_ctf_reset_role(self);
1711 if (!self.havocbot_role_timeout)
1712 self.havocbot_role_timeout = time + 20;
1714 if (time > self.havocbot_role_timeout)
1716 havocbot_ctf_reset_role(self);
1720 if (self.bot_strategytime < time)
1725 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1726 navigation_goalrating_start();
1727 havocbot_goalrating_ctf_ourstolenflag(50000);
1728 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1729 havocbot_goalrating_ctf_enemybase(30000);
1730 havocbot_goalrating_items(500, self.origin, rt_radius);
1731 navigation_goalrating_end();
1735 void havocbot_role_ctf_middle()
1739 if(self.deadflag != DEAD_NO)
1741 havocbot_ctf_reset_role(self);
1745 if (self.flagcarried)
1747 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1751 mf = havocbot_ctf_find_flag(self);
1752 if(mf.ctf_status!=FLAG_BASE)
1754 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1758 if (!self.havocbot_role_timeout)
1759 self.havocbot_role_timeout = time + 10;
1761 if (time > self.havocbot_role_timeout)
1763 havocbot_ctf_reset_role(self);
1767 if (self.bot_strategytime < time)
1771 org = havocbot_ctf_middlepoint;
1772 org.z = self.origin.z;
1774 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1775 navigation_goalrating_start();
1776 havocbot_goalrating_ctf_ourstolenflag(50000);
1777 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1778 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1779 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1780 havocbot_goalrating_items(2500, self.origin, 10000);
1781 havocbot_goalrating_ctf_enemybase(2500);
1782 navigation_goalrating_end();
1786 void havocbot_role_ctf_defense()
1790 if(self.deadflag != DEAD_NO)
1792 havocbot_ctf_reset_role(self);
1796 if (self.flagcarried)
1798 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1802 // If own flag was captured
1803 mf = havocbot_ctf_find_flag(self);
1804 if(mf.ctf_status!=FLAG_BASE)
1806 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1810 if (!self.havocbot_role_timeout)
1811 self.havocbot_role_timeout = time + 30;
1813 if (time > self.havocbot_role_timeout)
1815 havocbot_ctf_reset_role(self);
1818 if (self.bot_strategytime < time)
1823 org = mf.dropped_origin;
1824 mp_radius = havocbot_ctf_middlepoint_radius;
1826 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1827 navigation_goalrating_start();
1829 // if enemies are closer to our base, go there
1830 entity head, closestplayer = world;
1831 float distance, bestdistance = 10000;
1832 FOR_EACH_PLAYER(head)
1834 if(head.deadflag!=DEAD_NO)
1837 distance = vlen(org - head.origin);
1838 if(distance<bestdistance)
1840 closestplayer = head;
1841 bestdistance = distance;
1846 if(DIFF_TEAM(closestplayer, self))
1847 if(vlen(org - self.origin)>1000)
1848 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1849 havocbot_goalrating_ctf_ourbase(30000);
1851 havocbot_goalrating_ctf_ourstolenflag(20000);
1852 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1853 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1854 havocbot_goalrating_items(10000, org, mp_radius);
1855 havocbot_goalrating_items(5000, self.origin, 10000);
1856 navigation_goalrating_end();
1860 void havocbot_role_ctf_setrole(entity bot, float role)
1862 dprint(strcat(bot.netname," switched to "));
1865 case HAVOCBOT_CTF_ROLE_CARRIER:
1867 bot.havocbot_role = havocbot_role_ctf_carrier;
1868 bot.havocbot_role_timeout = 0;
1869 bot.havocbot_cantfindflag = time + 10;
1870 bot.bot_strategytime = 0;
1872 case HAVOCBOT_CTF_ROLE_DEFENSE:
1874 bot.havocbot_role = havocbot_role_ctf_defense;
1875 bot.havocbot_role_timeout = 0;
1877 case HAVOCBOT_CTF_ROLE_MIDDLE:
1879 bot.havocbot_role = havocbot_role_ctf_middle;
1880 bot.havocbot_role_timeout = 0;
1882 case HAVOCBOT_CTF_ROLE_OFFENSE:
1884 bot.havocbot_role = havocbot_role_ctf_offense;
1885 bot.havocbot_role_timeout = 0;
1887 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1888 dprint("retriever");
1889 bot.havocbot_previous_role = bot.havocbot_role;
1890 bot.havocbot_role = havocbot_role_ctf_retriever;
1891 bot.havocbot_role_timeout = time + 10;
1892 bot.bot_strategytime = 0;
1894 case HAVOCBOT_CTF_ROLE_ESCORT:
1896 bot.havocbot_previous_role = bot.havocbot_role;
1897 bot.havocbot_role = havocbot_role_ctf_escort;
1898 bot.havocbot_role_timeout = time + 30;
1899 bot.bot_strategytime = 0;
1910 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1913 int t = 0, t2 = 0, t3 = 0;
1915 // initially clear items so they can be set as necessary later.
1916 self.ctf_flagstatus &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
1917 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
1918 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
1919 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
1920 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
1921 | CTF_FLAG_NEUTRAL | CTF_SHIELDED);
1923 // scan through all the flags and notify the client about them
1924 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1926 if(flag.team == NUM_TEAM_1) { t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
1927 if(flag.team == NUM_TEAM_2) { t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
1928 if(flag.team == NUM_TEAM_3) { t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
1929 if(flag.team == NUM_TEAM_4) { t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
1930 if(flag.team == 0) { t = CTF_NEUTRAL_FLAG_CARRYING; t2 = CTF_NEUTRAL_FLAG_TAKEN; t3 = CTF_NEUTRAL_FLAG_LOST; self.ctf_flagstatus |= CTF_FLAG_NEUTRAL; }
1932 switch(flag.ctf_status)
1937 if((flag.owner == self) || (flag.pass_sender == self))
1938 self.ctf_flagstatus |= t; // carrying: self is currently carrying the flag
1940 self.ctf_flagstatus |= t2; // taken: someone else is carrying the flag
1945 self.ctf_flagstatus |= t3; // lost: the flag is dropped somewhere on the map
1951 // item for stopping players from capturing the flag too often
1952 if(self.ctf_captureshielded)
1953 self.ctf_flagstatus |= CTF_SHIELDED;
1955 // update the health of the flag carrier waypointsprite
1956 if(self.wps_flagcarrier)
1957 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
1962 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1964 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1966 if(frag_target == frag_attacker) // damage done to yourself
1968 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1969 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1971 else // damage done to everyone else
1973 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1974 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1977 else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
1979 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)))
1980 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1982 frag_target.wps_helpme_time = time;
1983 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1985 // todo: add notification for when flag carrier needs help?
1990 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1992 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
1994 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1995 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1998 if(frag_target.flagcarried)
2000 entity tmp_entity = frag_target.flagcarried;
2001 ctf_Handle_Throw(frag_target, world, DROP_NORMAL);
2002 tmp_entity.ctf_dropper = world;
2008 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
2011 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2014 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
2016 entity flag; // temporary entity for the search method
2018 if(self.flagcarried)
2019 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2021 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2023 if(flag.pass_sender == self) { flag.pass_sender = world; }
2024 if(flag.pass_target == self) { flag.pass_target = world; }
2025 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
2031 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
2033 if(self.flagcarried)
2034 if(!autocvar_g_ctf_portalteleport)
2035 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2040 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
2042 if(MUTATOR_RETURNVALUE || gameover) { return false; }
2044 entity player = self;
2046 if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2048 // pass the flag to a team mate
2049 if(autocvar_g_ctf_pass)
2051 entity head, closest_target = world;
2052 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2054 while(head) // find the closest acceptable target to pass to
2056 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
2057 if(head != player && SAME_TEAM(head, player))
2058 if(!head.speedrunning && !head.vehicle)
2060 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2061 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2062 vector passer_center = CENTER_OR_VIEWOFS(player);
2064 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2066 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2068 if(IS_BOT_CLIENT(head))
2070 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2071 ctf_Handle_Throw(head, player, DROP_PASS);
2075 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2076 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2078 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2081 else if(player.flagcarried)
2085 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2086 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
2087 { closest_target = head; }
2089 else { closest_target = head; }
2096 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2099 // throw the flag in front of you
2100 if(autocvar_g_ctf_throw && player.flagcarried)
2102 if(player.throw_count == -1)
2104 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2106 player.throw_prevtime = time;
2107 player.throw_count = 1;
2108 ctf_Handle_Throw(player, world, DROP_THROW);
2113 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2119 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2120 else { player.throw_count += 1; }
2121 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2123 player.throw_prevtime = time;
2124 ctf_Handle_Throw(player, world, DROP_THROW);
2133 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
2135 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2137 self.wps_helpme_time = time;
2138 WaypointSprite_HelpMePing(self.wps_flagcarrier);
2140 else // create a normal help me waypointsprite
2142 WaypointSprite_Spawn("helpme", waypointsprite_deployed_lifetime, waypointsprite_limitedrange, self, FLAG_WAYPOINT_OFFSET, world, self.team, self, wps_helpme, false, RADARICON_HELPME, '1 0.5 0');
2143 WaypointSprite_Ping(self.wps_helpme);
2149 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
2151 if(vh_player.flagcarried)
2153 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2155 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
2159 setattachment(vh_player.flagcarried, vh_vehicle, "");
2160 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
2161 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2162 //vh_player.flagcarried.angles = '0 0 0';
2170 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
2172 if(vh_player.flagcarried)
2174 setattachment(vh_player.flagcarried, vh_player, "");
2175 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2176 vh_player.flagcarried.scale = FLAG_SCALE;
2177 vh_player.flagcarried.angles = '0 0 0';
2184 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
2186 if(self.flagcarried)
2188 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((self.flagcarried.team) ? APP_TEAM_ENT_4(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN_) : INFO_CTF_FLAGRETURN_ABORTRUN_NEUTRAL));
2189 ctf_RespawnFlag(self.flagcarried);
2196 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
2198 entity flag; // temporary entity for the search method
2200 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2202 switch(flag.ctf_status)
2207 // lock the flag, game is over
2208 flag.movetype = MOVETYPE_NONE;
2209 flag.takedamage = DAMAGE_NO;
2210 flag.solid = SOLID_NOT;
2211 flag.nextthink = false; // stop thinking
2213 //dprint("stopping the ", flag.netname, " from moving.\n");
2221 // do nothing for these flags
2230 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
2232 havocbot_ctf_reset_role(self);
2236 MUTATOR_HOOKFUNCTION(ctf_GetTeamCount)
2238 //ret_float = ctf_teams;
2239 ret_string = "ctf_team";
2243 MUTATOR_HOOKFUNCTION(ctf_SpectateCopy)
2245 self.ctf_flagstatus = other.ctf_flagstatus;
2254 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
2255 CTF Starting point for a player in team one (Red).
2256 Keys: "angle" viewing angle when spawning. */
2257 void spawnfunc_info_player_team1()
2259 if(g_assault) { remove(self); return; }
2261 self.team = NUM_TEAM_1; // red
2262 spawnfunc_info_player_deathmatch();
2266 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
2267 CTF Starting point for a player in team two (Blue).
2268 Keys: "angle" viewing angle when spawning. */
2269 void spawnfunc_info_player_team2()
2271 if(g_assault) { remove(self); return; }
2273 self.team = NUM_TEAM_2; // blue
2274 spawnfunc_info_player_deathmatch();
2277 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
2278 CTF Starting point for a player in team three (Yellow).
2279 Keys: "angle" viewing angle when spawning. */
2280 void spawnfunc_info_player_team3()
2282 if(g_assault) { remove(self); return; }
2284 self.team = NUM_TEAM_3; // yellow
2285 spawnfunc_info_player_deathmatch();
2289 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
2290 CTF Starting point for a player in team four (Purple).
2291 Keys: "angle" viewing angle when spawning. */
2292 void spawnfunc_info_player_team4()
2294 if(g_assault) { remove(self); return; }
2296 self.team = NUM_TEAM_4; // purple
2297 spawnfunc_info_player_deathmatch();
2300 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2301 CTF flag for team one (Red).
2303 "angle" Angle the flag will point (minus 90 degrees)...
2304 "model" model to use, note this needs red and blue as skins 0 and 1...
2305 "noise" sound played when flag is picked up...
2306 "noise1" sound played when flag is returned by a teammate...
2307 "noise2" sound played when flag is captured...
2308 "noise3" sound played when flag is lost in the field and respawns itself...
2309 "noise4" sound played when flag is dropped by a player...
2310 "noise5" sound played when flag touches the ground... */
2311 void spawnfunc_item_flag_team1()
2313 if(!g_ctf) { remove(self); return; }
2315 ctf_FlagSetup(NUM_TEAM_1, self);
2318 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2319 CTF flag for team two (Blue).
2321 "angle" Angle the flag will point (minus 90 degrees)...
2322 "model" model to use, note this needs red and blue as skins 0 and 1...
2323 "noise" sound played when flag is picked up...
2324 "noise1" sound played when flag is returned by a teammate...
2325 "noise2" sound played when flag is captured...
2326 "noise3" sound played when flag is lost in the field and respawns itself...
2327 "noise4" sound played when flag is dropped by a player...
2328 "noise5" sound played when flag touches the ground... */
2329 void spawnfunc_item_flag_team2()
2331 if(!g_ctf) { remove(self); return; }
2333 ctf_FlagSetup(NUM_TEAM_2, self);
2336 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2337 CTF flag for team three (Yellow).
2339 "angle" Angle the flag will point (minus 90 degrees)...
2340 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2341 "noise" sound played when flag is picked up...
2342 "noise1" sound played when flag is returned by a teammate...
2343 "noise2" sound played when flag is captured...
2344 "noise3" sound played when flag is lost in the field and respawns itself...
2345 "noise4" sound played when flag is dropped by a player...
2346 "noise5" sound played when flag touches the ground... */
2347 void spawnfunc_item_flag_team3()
2349 if(!g_ctf) { remove(self); return; }
2351 ctf_FlagSetup(NUM_TEAM_3, self);
2354 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2355 CTF flag for team four (Pink).
2357 "angle" Angle the flag will point (minus 90 degrees)...
2358 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2359 "noise" sound played when flag is picked up...
2360 "noise1" sound played when flag is returned by a teammate...
2361 "noise2" sound played when flag is captured...
2362 "noise3" sound played when flag is lost in the field and respawns itself...
2363 "noise4" sound played when flag is dropped by a player...
2364 "noise5" sound played when flag touches the ground... */
2365 void spawnfunc_item_flag_team4()
2367 if(!g_ctf) { remove(self); return; }
2369 ctf_FlagSetup(NUM_TEAM_4, self);
2372 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2375 "angle" Angle the flag will point (minus 90 degrees)...
2376 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2377 "noise" sound played when flag is picked up...
2378 "noise1" sound played when flag is returned by a teammate...
2379 "noise2" sound played when flag is captured...
2380 "noise3" sound played when flag is lost in the field and respawns itself...
2381 "noise4" sound played when flag is dropped by a player...
2382 "noise5" sound played when flag touches the ground... */
2383 void spawnfunc_item_flag_neutral()
2385 if(!g_ctf) { remove(self); return; }
2386 if(!cvar("g_ctf_oneflag")) { remove(self); return; }
2388 ctf_FlagSetup(0, self);
2391 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2392 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2393 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.
2395 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2396 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2397 void spawnfunc_ctf_team()
2399 if(!g_ctf) { remove(self); return; }
2401 self.classname = "ctf_team";
2402 self.team = self.cnt + 1;
2405 // compatibility for quake maps
2406 void spawnfunc_team_CTF_redflag() { spawnfunc_item_flag_team1(); }
2407 void spawnfunc_team_CTF_blueflag() { spawnfunc_item_flag_team2(); }
2408 void spawnfunc_team_CTF_redplayer() { spawnfunc_info_player_team1(); }
2409 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2(); }
2410 void spawnfunc_team_CTF_redspawn() { spawnfunc_info_player_team1(); }
2411 void spawnfunc_team_CTF_bluespawn() { spawnfunc_info_player_team2(); }
2419 void ctf_ScoreRules(float teams)
2421 CheckAllowedTeams(world);
2422 ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
2423 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2424 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2425 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2426 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2427 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2428 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2429 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2430 ScoreRules_basics_end();
2433 // code from here on is just to support maps that don't have flag and team entities
2434 void ctf_SpawnTeam (string teamname, float teamcolor)
2439 self.classname = "ctf_team";
2440 self.netname = teamname;
2441 self.cnt = teamcolor;
2443 spawnfunc_ctf_team();
2448 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2453 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2455 if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2456 if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2457 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2460 ctf_teams = bound(2, ctf_teams, 4);
2462 // if no teams are found, spawn defaults
2463 if(find(world, classname, "ctf_team") == world)
2465 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2466 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2467 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2469 ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2471 ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2474 ctf_ScoreRules(ctf_teams);
2477 void ctf_Initialize()
2479 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2481 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2482 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2483 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2485 addstat(STAT_CTF_FLAGSTATUS, AS_INT, ctf_flagstatus);
2487 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2491 MUTATOR_DEFINITION(gamemode_ctf)
2493 MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2494 MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2495 MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2496 MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2497 MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2498 MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2499 MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2500 MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2501 MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2502 MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2503 MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2504 MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2505 MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2506 MUTATOR_HOOK(HavocBot_ChooseRole, ctf_BotRoles, CBC_ORDER_ANY);
2507 MUTATOR_HOOK(GetTeamCount, ctf_GetTeamCount, CBC_ORDER_ANY);
2508 MUTATOR_HOOK(SpectateCopy, ctf_SpectateCopy, CBC_ORDER_ANY);
2512 if(time > 1) // game loads at time 1
2513 error("This is a game type and it cannot be added at runtime.");
2517 MUTATOR_ONROLLBACK_OR_REMOVE
2519 // we actually cannot roll back ctf_Initialize here
2520 // BUT: we don't need to! If this gets called, adding always
2526 print("This is a game type and it cannot be removed at runtime.");