1 #include "gamemode_ctf.qh"
7 #include "../../common/vehicles/all.qh"
10 #include "../../warpzonelib/common.qh"
12 void ctf_FakeTimeLimit(entity e, float t)
15 WriteByte(MSG_ONE, 3); // svc_updatestat
16 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
18 WriteCoord(MSG_ONE, autocvar_timelimit);
20 WriteCoord(MSG_ONE, (t + 1) / 60);
23 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
25 if(autocvar_sv_eventlog)
26 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != world) ? ftos(actor.playerid) : "")));
27 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
30 void ctf_CaptureRecord(entity flag, entity player)
32 float cap_record = ctf_captimerecord;
33 float cap_time = (time - flag.ctf_pickuptime);
34 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
37 if(ctf_oneflag) { Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname); }
38 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)); }
39 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)); }
40 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)); }
42 // write that shit in the database
43 if(!ctf_oneflag) // but not in 1-flag mode
44 if((!ctf_captimerecord) || (cap_time < cap_record))
46 ctf_captimerecord = cap_time;
47 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
48 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
49 write_recordmarker(player, (time - cap_time), cap_time);
53 void ctf_FlagcarrierWaypoints(entity player)
55 WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
56 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON) * 2);
57 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
58 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
61 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
63 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
64 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
65 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
66 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
69 if(current_height) // make sure we can actually do this arcing path
71 targpos = (to + ('0 0 1' * current_height));
72 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
73 if(trace_fraction < 1)
75 //print("normal arc line failed, trying to find new pos...");
76 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
77 targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
78 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
79 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
80 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
83 else { targpos = to; }
85 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
87 vector desired_direction = normalize(targpos - from);
88 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
89 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
92 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
94 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
96 // directional tracing only
98 makevectors(passer_angle);
100 // find the closest point on the enemy to the center of the attack
101 float h; // hypotenuse, which is the distance between attacker to head
102 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
104 h = vlen(head_center - passer_center);
105 a = h * (normalize(head_center - passer_center) * v_forward);
107 vector nearest_on_line = (passer_center + a * v_forward);
108 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
110 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
111 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
113 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
118 else { return true; }
122 // =======================
123 // CaptureShield Functions
124 // =======================
126 bool ctf_CaptureShield_CheckStatus(entity p)
128 int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
130 int players_worseeq, players_total;
132 if(ctf_captureshield_max_ratio <= 0)
135 s = PlayerScore_Add(p, SP_CTF_CAPS, 0);
136 s2 = PlayerScore_Add(p, SP_CTF_PICKUPS, 0);
137 s3 = PlayerScore_Add(p, SP_CTF_RETURNS, 0);
138 s4 = PlayerScore_Add(p, SP_CTF_FCKILLS, 0);
140 sr = ((s - s2) + (s3 + s4));
142 if(sr >= -ctf_captureshield_min_negscore)
145 players_total = players_worseeq = 0;
150 se = PlayerScore_Add(e, SP_CTF_CAPS, 0);
151 se2 = PlayerScore_Add(e, SP_CTF_PICKUPS, 0);
152 se3 = PlayerScore_Add(e, SP_CTF_RETURNS, 0);
153 se4 = PlayerScore_Add(e, SP_CTF_FCKILLS, 0);
155 ser = ((se - se2) + (se3 + se4));
162 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
163 // use this rule here
165 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
171 void ctf_CaptureShield_Update(entity player, bool wanted_status)
173 bool updated_status = ctf_CaptureShield_CheckStatus(player);
174 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
176 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
177 player.ctf_captureshielded = updated_status;
181 bool ctf_CaptureShield_Customize()
183 if(!other.ctf_captureshielded) { return false; }
184 if(CTF_SAMETEAM(self, other)) { return false; }
189 void ctf_CaptureShield_Touch()
191 if(!other.ctf_captureshielded) { return; }
192 if(CTF_SAMETEAM(self, other)) { return; }
194 vector mymid = (self.absmin + self.absmax) * 0.5;
195 vector othermid = (other.absmin + other.absmax) * 0.5;
197 Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
198 if(IS_REAL_CLIENT(other)) { Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
201 void ctf_CaptureShield_Spawn(entity flag)
203 entity shield = spawn();
206 shield.team = self.team;
207 shield.touch = ctf_CaptureShield_Touch;
208 shield.customizeentityforclient = ctf_CaptureShield_Customize;
209 shield.classname = "ctf_captureshield";
210 shield.effects = EF_ADDITIVE;
211 shield.movetype = MOVETYPE_NOCLIP;
212 shield.solid = SOLID_TRIGGER;
213 shield.avelocity = '7 0 11';
216 setorigin(shield, self.origin);
217 setmodel(shield, "models/ctf/shield.md3");
218 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
222 // ====================
223 // Drop/Pass/Throw Code
224 // ====================
226 void ctf_Handle_Drop(entity flag, entity player, int droptype)
229 player = (player ? player : flag.pass_sender);
232 flag.movetype = MOVETYPE_TOSS;
233 flag.takedamage = DAMAGE_YES;
234 flag.angles = '0 0 0';
235 flag.health = flag.max_flag_health;
236 flag.ctf_droptime = time;
237 flag.ctf_dropper = player;
238 flag.ctf_status = FLAG_DROPPED;
240 // messages and sounds
241 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_LOST_) : INFO_CTF_LOST_NEUTRAL), player.netname);
242 sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
243 ctf_EventLog("dropped", player.team, player);
246 PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);
247 PlayerScore_Add(player, SP_CTF_DROPS, 1);
250 if(autocvar_g_ctf_flag_dropped_waypoint) {
251 entity wp = WaypointSprite_Spawn(WP_FlagDropped, 0, 0, flag, FLAG_WAYPOINT_OFFSET, world, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, true, RADARICON_FLAG);
252 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
255 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
257 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
258 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
261 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
263 if(droptype == DROP_PASS)
265 flag.pass_distance = 0;
266 flag.pass_sender = world;
267 flag.pass_target = world;
271 void ctf_Handle_Retrieve(entity flag, entity player)
273 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
274 entity sender = flag.pass_sender;
276 // transfer flag to player
278 flag.owner.flagcarried = flag;
283 setattachment(flag, player.vehicle, "");
284 setorigin(flag, VEHICLE_FLAG_OFFSET);
285 flag.scale = VEHICLE_FLAG_SCALE;
289 setattachment(flag, player, "");
290 setorigin(flag, FLAG_CARRY_OFFSET);
292 flag.movetype = MOVETYPE_NONE;
293 flag.takedamage = DAMAGE_NO;
294 flag.solid = SOLID_NOT;
295 flag.angles = '0 0 0';
296 flag.ctf_status = FLAG_CARRY;
298 // messages and sounds
299 sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
300 ctf_EventLog("receive", flag.team, player);
302 FOR_EACH_REALPLAYER(tmp_player)
304 if(tmp_player == sender)
305 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);
306 else if(tmp_player == player)
307 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);
308 else if(SAME_TEAM(tmp_player, sender))
309 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);
312 // create new waypoint
313 ctf_FlagcarrierWaypoints(player);
315 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
316 player.throw_antispam = sender.throw_antispam;
318 flag.pass_distance = 0;
319 flag.pass_sender = world;
320 flag.pass_target = world;
323 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
325 entity flag = player.flagcarried;
326 vector targ_origin, flag_velocity;
328 if(!flag) { return; }
329 if((droptype == DROP_PASS) && !receiver) { return; }
331 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
334 setattachment(flag, world, "");
335 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
336 flag.owner.flagcarried = world;
338 flag.solid = SOLID_TRIGGER;
339 flag.ctf_dropper = player;
340 flag.ctf_droptime = time;
342 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
349 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
350 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
351 WarpZone_RefSys_Copy(flag, receiver);
352 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
353 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
355 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
356 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
359 flag.movetype = MOVETYPE_FLY;
360 flag.takedamage = DAMAGE_NO;
361 flag.pass_sender = player;
362 flag.pass_target = receiver;
363 flag.ctf_status = FLAG_PASSING;
366 sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
367 WarpZone_TrailParticles(world, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
368 ctf_EventLog("pass", flag.team, player);
374 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'));
376 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)));
377 flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, false);
378 ctf_Handle_Drop(flag, player, droptype);
384 flag.velocity = '0 0 0'; // do nothing
391 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);
392 ctf_Handle_Drop(flag, player, droptype);
397 // kill old waypointsprite
398 WaypointSprite_Ping(player.wps_flagcarrier);
399 WaypointSprite_Kill(player.wps_flagcarrier);
401 if(player.wps_enemyflagcarrier)
402 WaypointSprite_Kill(player.wps_enemyflagcarrier);
405 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
413 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
415 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
416 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
417 entity player_team_flag = world, tmp_entity;
418 float old_time, new_time;
420 if(!player) { return; } // without someone to give the reward to, we can't possibly cap
421 if(CTF_DIFFTEAM(player, flag)) { return; }
424 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
425 if(SAME_TEAM(tmp_entity, player))
427 player_team_flag = tmp_entity;
431 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
433 player.throw_prevtime = time;
434 player.throw_count = 0;
436 // messages and sounds
437 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((enemy_flag.team) ? APP_TEAM_ENT_4(enemy_flag, CENTER_CTF_CAPTURE_) : CENTER_CTF_CAPTURE_NEUTRAL));
438 ctf_CaptureRecord(enemy_flag, player);
439 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);
443 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
444 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
449 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
450 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
452 old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
453 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
454 if(!old_time || new_time < old_time)
455 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
458 Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
459 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
462 if(capturetype == CAPTURE_NORMAL)
464 WaypointSprite_Kill(player.wps_flagcarrier);
465 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
467 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
468 { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
472 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
473 ctf_RespawnFlag(enemy_flag);
476 void ctf_Handle_Return(entity flag, entity player)
478 // messages and sounds
479 if(IS_MONSTER(player))
481 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_MONSTER_), player.monster_name);
485 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_RETURN_));
486 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_), player.netname);
488 sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
489 ctf_EventLog("return", flag.team, player);
492 if(IS_PLAYER(player))
494 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
495 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
497 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
500 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
504 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
505 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
506 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
510 if(player.flagcarried == flag)
511 WaypointSprite_Kill(player.wps_flagcarrier);
514 ctf_RespawnFlag(flag);
517 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
520 float pickup_dropped_score; // used to calculate dropped pickup score
521 entity tmp_entity; // temporary entity
523 // attach the flag to the player
525 player.flagcarried = flag;
528 setattachment(flag, player.vehicle, "");
529 setorigin(flag, VEHICLE_FLAG_OFFSET);
530 flag.scale = VEHICLE_FLAG_SCALE;
534 setattachment(flag, player, "");
535 setorigin(flag, FLAG_CARRY_OFFSET);
539 flag.movetype = MOVETYPE_NONE;
540 flag.takedamage = DAMAGE_NO;
541 flag.solid = SOLID_NOT;
542 flag.angles = '0 0 0';
543 flag.ctf_status = FLAG_CARRY;
547 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
548 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
552 // messages and sounds
553 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_PICKUP_) : INFO_CTF_PICKUP_NEUTRAL), player.netname);
554 if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
555 if(!flag.team) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL); }
556 else if(CTF_DIFFTEAM(player, flag)) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PICKUP_)); }
557 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)); }
559 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);
562 FOR_EACH_PLAYER(tmp_entity)
563 if(tmp_entity != player)
564 if(DIFF_TEAM(player, tmp_entity))
565 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, CHOICE_CTF_PICKUP_ENEMY_NEUTRAL, Team_ColorCode(player.team), player.netname);
568 FOR_EACH_PLAYER(tmp_entity)
569 if(tmp_entity != player)
570 if(CTF_SAMETEAM(flag, tmp_entity))
571 if(SAME_TEAM(player, tmp_entity))
572 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_PICKUP_TEAM_), Team_ColorCode(player.team), player.netname);
574 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);
576 sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
579 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
580 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
585 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
586 ctf_EventLog("steal", flag.team, player);
592 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);
593 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);
594 LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
595 PlayerTeamScore_AddScore(player, pickup_dropped_score);
596 ctf_EventLog("pickup", flag.team, player);
604 if(pickuptype == PICKUP_BASE)
606 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
607 if((player.speedrunning) && (ctf_captimerecord))
608 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
612 Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
615 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
616 ctf_FlagcarrierWaypoints(player);
617 WaypointSprite_Ping(player.wps_flagcarrier);
621 // ===================
622 // Main Flag Functions
623 // ===================
625 void ctf_CheckFlagReturn(entity flag, int returntype)
627 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
629 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
631 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
635 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;
636 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;
637 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;
638 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;
642 { Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_TIMEOUT_) : INFO_CTF_FLAGRETURN_TIMEOUT_NEUTRAL)); break; }
644 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
645 ctf_EventLog("returned", flag.team, world);
646 ctf_RespawnFlag(flag);
651 bool ctf_Stalemate_Customize()
653 // make spectators see what the player would see
655 e = WaypointSprite_getviewentity(other);
656 wp_owner = self.owner;
659 if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
660 if(SAME_TEAM(wp_owner, e)) { return false; }
661 if(!IS_PLAYER(e)) { return false; }
666 void ctf_CheckStalemate(void)
669 int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
672 entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
674 // build list of stale flags
675 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
677 if(autocvar_g_ctf_stalemate)
678 if(tmp_entity.ctf_status != FLAG_BASE)
679 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
681 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
682 ctf_staleflaglist = tmp_entity;
684 switch(tmp_entity.team)
686 case NUM_TEAM_1: ++stale_red_flags; break;
687 case NUM_TEAM_2: ++stale_blue_flags; break;
688 case NUM_TEAM_3: ++stale_yellow_flags; break;
689 case NUM_TEAM_4: ++stale_pink_flags; break;
690 default: ++stale_neutral_flags; break;
696 stale_flags = (stale_neutral_flags >= 1);
698 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
700 if(ctf_oneflag && stale_flags == 1)
701 ctf_stalemate = true;
702 else if(stale_flags >= 2)
703 ctf_stalemate = true;
704 else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
705 { ctf_stalemate = false; wpforenemy_announced = false; }
706 else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
707 { ctf_stalemate = false; wpforenemy_announced = false; }
709 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
712 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
714 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
716 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, tmp_entity.owner, FLAG_WAYPOINT_OFFSET, world, 0, tmp_entity.owner, wps_enemyflagcarrier, true, RADARICON_FLAG);
717 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
718 tmp_entity.owner.wps_enemyflagcarrier.customizeentityforclient = ctf_Stalemate_Customize;
722 if (!wpforenemy_announced)
724 FOR_EACH_REALPLAYER(tmp_entity)
725 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CENTER, ((tmp_entity.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER));
727 wpforenemy_announced = true;
732 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
734 if(ITEM_DAMAGE_NEEDKILL(deathtype))
736 if(autocvar_g_ctf_flag_return_damage_delay)
738 self.ctf_flagdamaged = true;
743 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
747 if(autocvar_g_ctf_flag_return_damage)
749 // reduce health and check if it should be returned
750 self.health = self.health - damage;
751 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
761 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
764 if(self == ctf_worldflaglist) // only for the first flag
765 FOR_EACH_CLIENT(tmp_entity)
766 ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
769 if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
770 LOG_TRACE("wtf the flag got squashed?\n");
771 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
772 if(!trace_startsolid || self.noalign) // can we resize it without getting stuck?
773 setsize(self, FLAG_MIN, FLAG_MAX); }
775 switch(self.ctf_status) // reset flag angles in case warpzones adjust it
779 self.angles = '0 0 0';
787 switch(self.ctf_status)
791 if(autocvar_g_ctf_dropped_capture_radius)
793 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
794 if(tmp_entity.ctf_status == FLAG_DROPPED)
795 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
796 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
797 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
804 if(autocvar_g_ctf_flag_dropped_floatinwater)
806 vector midpoint = ((self.absmin + self.absmax) * 0.5);
807 if(pointcontents(midpoint) == CONTENT_WATER)
809 self.velocity = self.velocity * 0.5;
811 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
812 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
814 { self.movetype = MOVETYPE_FLY; }
816 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
818 if(autocvar_g_ctf_flag_return_dropped)
820 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
823 ctf_CheckFlagReturn(self, RETURN_DROPPED);
827 if(self.ctf_flagdamaged)
829 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
830 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
833 else if(autocvar_g_ctf_flag_return_time)
835 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
836 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
844 if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
847 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
851 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
855 if(autocvar_g_ctf_stalemate)
857 if(time >= wpforenemy_nextthink)
859 ctf_CheckStalemate();
860 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
863 if(CTF_SAMETEAM(self, self.owner) && self.team)
865 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
866 ctf_Handle_Throw(self.owner, world, DROP_THROW);
867 else if(vlen(self.owner.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_carried_radius)
868 ctf_Handle_Return(self, self.owner);
875 vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
876 targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
877 WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
879 if((self.pass_target == world)
880 || (self.pass_target.deadflag != DEAD_NO)
881 || (self.pass_target.flagcarried)
882 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
883 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
884 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
886 // give up, pass failed
887 ctf_Handle_Drop(self, world, DROP_PASS);
891 // still a viable target, go for it
892 ctf_CalculatePassVelocity(self, targ_origin, self.origin, true);
897 default: // this should never happen
899 LOG_TRACE("ctf_FlagThink(): Flag exists with no status?\n");
907 if(gameover) { return; }
908 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
910 entity toucher = other, tmp_entity;
911 bool is_not_monster = (!IS_MONSTER(toucher)), num_perteam = 0;
913 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
914 if(ITEM_TOUCH_NEEDKILL())
916 if(!autocvar_g_ctf_flag_return_damage_delay)
919 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
921 if(!self.ctf_flagdamaged) { return; }
924 FOR_EACH_PLAYER(tmp_entity) if(SAME_TEAM(toucher, tmp_entity)) { ++num_perteam; }
926 // special touch behaviors
927 if(toucher.frozen) { return; }
928 else if(IS_VEHICLE(toucher))
930 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
931 toucher = toucher.owner; // the player is actually the vehicle owner, not other
933 return; // do nothing
935 else if(IS_MONSTER(toucher))
937 if(!autocvar_g_ctf_allow_monster_touch)
938 return; // do nothing
940 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
942 if(time > self.wait) // if we haven't in a while, play a sound/effect
944 Send_Effect_(self.toucheffect, self.origin, '0 0 0', 1);
945 sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTEN_NORM);
946 self.wait = time + FLAG_TOUCHRATE;
950 else if(toucher.deadflag != DEAD_NO) { return; }
952 switch(self.ctf_status)
958 if(CTF_SAMETEAM(toucher, self) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
959 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
960 else if(!self.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
961 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the neutral flag
963 else if(CTF_SAMETEAM(toucher, self) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, self) && is_not_monster)
964 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
965 else if(CTF_DIFFTEAM(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
966 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
972 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
973 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
974 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
975 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
981 LOG_TRACE("Someone touched a flag even though it was being carried?\n");
987 if((IS_PLAYER(toucher)) && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
989 if(DIFF_TEAM(toucher, self.pass_sender))
990 ctf_Handle_Return(self, toucher);
992 ctf_Handle_Retrieve(self, toucher);
1000 void ctf_RespawnFlag(entity flag)
1002 // check for flag respawn being called twice in a row
1003 if(flag.last_respawn > time - 0.5)
1004 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1006 flag.last_respawn = time;
1008 // reset the player (if there is one)
1009 if((flag.owner) && (flag.owner.flagcarried == flag))
1011 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1012 WaypointSprite_Kill(flag.wps_flagcarrier);
1014 flag.owner.flagcarried = world;
1016 if(flag.speedrunning)
1017 ctf_FakeTimeLimit(flag.owner, -1);
1020 if((flag.owner) && (flag.owner.vehicle))
1021 flag.scale = FLAG_SCALE;
1023 if(flag.ctf_status == FLAG_DROPPED)
1024 { WaypointSprite_Kill(flag.wps_flagdropped); }
1027 setattachment(flag, world, "");
1028 setorigin(flag, flag.ctf_spawnorigin);
1030 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
1031 flag.takedamage = DAMAGE_NO;
1032 flag.health = flag.max_flag_health;
1033 flag.solid = SOLID_TRIGGER;
1034 flag.velocity = '0 0 0';
1035 flag.angles = flag.mangle;
1036 flag.flags = FL_ITEM | FL_NOTARGET;
1038 flag.ctf_status = FLAG_BASE;
1040 flag.pass_distance = 0;
1041 flag.pass_sender = world;
1042 flag.pass_target = world;
1043 flag.ctf_dropper = world;
1044 flag.ctf_pickuptime = 0;
1045 flag.ctf_droptime = 0;
1046 flag.ctf_flagdamaged = 0;
1048 ctf_CheckStalemate();
1054 if(IS_PLAYER(self.owner))
1055 ctf_Handle_Throw(self.owner, world, DROP_RESET);
1057 ctf_RespawnFlag(self);
1060 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
1063 waypoint_spawnforitem_force(self, self.origin);
1064 self.nearestwaypointtimeout = 0; // activate waypointing again
1065 self.bot_basewaypoint = self.nearestwaypoint;
1071 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1072 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1073 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1074 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1075 default: basename = WP_FlagBaseNeutral; break;
1078 entity wp = WaypointSprite_SpawnFixed(basename, self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG);
1079 wp.colormod = ((self.team) ? Team_ColorRGB(self.team) : '1 1 1');
1080 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, ((self.team) ? colormapPaletteColor(self.team - 1, false) : '1 1 1'));
1082 // captureshield setup
1083 ctf_CaptureShield_Spawn(self);
1086 void set_flag_string(entity flag, .string field, string value, string teamname)
1088 if(flag.field == "")
1089 flag.field = strzone(sprintf(value,teamname));
1092 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1095 string teamname = Static_Team_ColorName_Lower(teamnumber);
1096 setself(flag); // for later usage with droptofloor()
1099 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1100 ctf_worldflaglist = flag;
1102 setattachment(flag, world, "");
1104 flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1105 flag.team = teamnumber;
1106 flag.classname = "item_flag_team";
1107 flag.target = "###item###"; // wut?
1108 flag.flags = FL_ITEM | FL_NOTARGET;
1109 flag.solid = SOLID_TRIGGER;
1110 flag.takedamage = DAMAGE_NO;
1111 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1112 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1113 flag.health = flag.max_flag_health;
1114 flag.event_damage = ctf_FlagDamage;
1115 flag.pushable = true;
1116 flag.teleportable = TELEPORT_NORMAL;
1117 flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP;
1118 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1119 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1120 flag.velocity = '0 0 0';
1121 flag.mangle = flag.angles;
1122 flag.reset = ctf_Reset;
1123 flag.touch = ctf_FlagTouch;
1124 flag.think = ctf_FlagThink;
1125 flag.nextthink = time + FLAG_THINKRATE;
1126 flag.ctf_status = FLAG_BASE;
1129 if(!flag.scale) { flag.scale = FLAG_SCALE; }
1130 if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1131 if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1132 set_flag_string(flag, toucheffect, "%sflag_touch", teamname);
1133 set_flag_string(flag, passeffect, "%s_pass", teamname);
1134 set_flag_string(flag, capeffect, "%s_cap", teamname);
1137 set_flag_string(flag, snd_flag_taken, "ctf/%s_taken.wav", teamname);
1138 set_flag_string(flag, snd_flag_returned, "ctf/%s_returned.wav", teamname);
1139 set_flag_string(flag, snd_flag_capture, "ctf/%s_capture.wav", teamname);
1140 set_flag_string(flag, snd_flag_dropped, "ctf/%s_dropped.wav", teamname);
1141 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.
1142 if(flag.snd_flag_touch == "") { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
1143 if(flag.snd_flag_pass == "") { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
1146 precache_sound(flag.snd_flag_taken);
1147 precache_sound(flag.snd_flag_returned);
1148 precache_sound(flag.snd_flag_capture);
1149 precache_sound(flag.snd_flag_respawn);
1150 precache_sound(flag.snd_flag_dropped);
1151 precache_sound(flag.snd_flag_touch);
1152 precache_sound(flag.snd_flag_pass);
1153 precache_model(flag.model);
1154 precache_model("models/ctf/shield.md3");
1155 precache_model("models/ctf/shockwavetransring.md3");
1158 setmodel(flag, flag.model); // precision set below
1159 setsize(flag, FLAG_MIN, FLAG_MAX);
1160 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1162 if(autocvar_g_ctf_flag_glowtrails)
1166 case NUM_TEAM_1: flag.glow_color = 251; break;
1167 case NUM_TEAM_2: flag.glow_color = 210; break;
1168 case NUM_TEAM_3: flag.glow_color = 110; break;
1169 case NUM_TEAM_4: flag.glow_color = 145; break;
1170 default: flag.glow_color = 254; break;
1172 flag.glow_size = 25;
1173 flag.glow_trail = 1;
1176 flag.effects |= EF_LOWPRECISION;
1177 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1178 if(autocvar_g_ctf_dynamiclights)
1182 case NUM_TEAM_1: flag.effects |= EF_RED; break;
1183 case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1184 case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1185 case NUM_TEAM_4: flag.effects |= EF_RED; break;
1186 default: flag.effects |= EF_DIMLIGHT; break;
1191 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1193 flag.dropped_origin = flag.origin;
1194 flag.noalign = true;
1195 flag.movetype = MOVETYPE_NONE;
1197 else // drop to floor, automatically find a platform and set that as spawn origin
1199 flag.noalign = false;
1202 flag.movetype = MOVETYPE_TOSS;
1205 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1213 // NOTE: LEGACY CODE, needs to be re-written!
1215 void havocbot_calculate_middlepoint()
1219 vector fo = '0 0 0';
1222 f = ctf_worldflaglist;
1227 f = f.ctf_worldflagnext;
1231 havocbot_ctf_middlepoint = s * (1.0 / n);
1232 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1236 entity havocbot_ctf_find_flag(entity bot)
1239 f = ctf_worldflaglist;
1242 if (CTF_SAMETEAM(bot, f))
1244 f = f.ctf_worldflagnext;
1249 entity havocbot_ctf_find_enemy_flag(entity bot)
1252 f = ctf_worldflaglist;
1257 if(CTF_DIFFTEAM(bot, f))
1264 else if(!bot.flagcarried)
1268 else if (CTF_DIFFTEAM(bot, f))
1270 f = f.ctf_worldflagnext;
1275 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1283 FOR_EACH_PLAYER(head)
1285 if(DIFF_TEAM(head, bot) || head.deadflag != DEAD_NO || head == bot)
1288 if(vlen(head.origin - org) < tc_radius)
1295 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1298 head = ctf_worldflaglist;
1301 if (CTF_SAMETEAM(self, head))
1303 head = head.ctf_worldflagnext;
1306 navigation_routerating(head, ratingscale, 10000);
1309 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1312 head = ctf_worldflaglist;
1315 if (CTF_SAMETEAM(self, head))
1317 head = head.ctf_worldflagnext;
1322 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1325 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1328 head = ctf_worldflaglist;
1333 if(CTF_DIFFTEAM(self, head))
1337 if(self.flagcarried)
1340 else if(!self.flagcarried)
1344 else if(CTF_DIFFTEAM(self, head))
1346 head = head.ctf_worldflagnext;
1349 navigation_routerating(head, ratingscale, 10000);
1352 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1354 if (!bot_waypoints_for_items)
1356 havocbot_goalrating_ctf_enemyflag(ratingscale);
1362 head = havocbot_ctf_find_enemy_flag(self);
1367 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1370 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1374 mf = havocbot_ctf_find_flag(self);
1376 if(mf.ctf_status == FLAG_BASE)
1380 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1383 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1386 head = ctf_worldflaglist;
1389 // flag is out in the field
1390 if(head.ctf_status != FLAG_BASE)
1391 if(head.tag_entity==world) // dropped
1395 if(vlen(org-head.origin)<df_radius)
1396 navigation_routerating(head, ratingscale, 10000);
1399 navigation_routerating(head, ratingscale, 10000);
1402 head = head.ctf_worldflagnext;
1406 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1410 head = findchainfloat(bot_pickup, true);
1413 // gather health and armor only
1415 if (head.health || head.armorvalue)
1416 if (vlen(head.origin - org) < sradius)
1418 // get the value of the item
1419 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1421 navigation_routerating(head, t * ratingscale, 500);
1427 void havocbot_ctf_reset_role(entity bot)
1429 float cdefense, cmiddle, coffense;
1430 entity mf, ef, head;
1433 if(bot.deadflag != DEAD_NO)
1436 if(vlen(havocbot_ctf_middlepoint)==0)
1437 havocbot_calculate_middlepoint();
1440 if (bot.flagcarried)
1442 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1446 mf = havocbot_ctf_find_flag(bot);
1447 ef = havocbot_ctf_find_enemy_flag(bot);
1449 // Retrieve stolen flag
1450 if(mf.ctf_status!=FLAG_BASE)
1452 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1456 // If enemy flag is taken go to the middle to intercept pursuers
1457 if(ef.ctf_status!=FLAG_BASE)
1459 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1463 // if there is only me on the team switch to offense
1465 FOR_EACH_PLAYER(head)
1466 if(SAME_TEAM(head, bot))
1471 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1475 // Evaluate best position to take
1476 // Count mates on middle position
1477 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1479 // Count mates on defense position
1480 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1482 // Count mates on offense position
1483 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1485 if(cdefense<=coffense)
1486 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1487 else if(coffense<=cmiddle)
1488 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1490 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1493 void havocbot_role_ctf_carrier()
1495 if(self.deadflag != DEAD_NO)
1497 havocbot_ctf_reset_role(self);
1501 if (self.flagcarried == world)
1503 havocbot_ctf_reset_role(self);
1507 if (self.bot_strategytime < time)
1509 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1511 navigation_goalrating_start();
1513 havocbot_goalrating_ctf_enemybase(50000);
1515 havocbot_goalrating_ctf_ourbase(50000);
1518 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1520 navigation_goalrating_end();
1522 if (self.navigation_hasgoals)
1523 self.havocbot_cantfindflag = time + 10;
1524 else if (time > self.havocbot_cantfindflag)
1526 // Can't navigate to my own base, suicide!
1527 // TODO: drop it and wander around
1528 Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1534 void havocbot_role_ctf_escort()
1538 if(self.deadflag != DEAD_NO)
1540 havocbot_ctf_reset_role(self);
1544 if (self.flagcarried)
1546 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1550 // If enemy flag is back on the base switch to previous role
1551 ef = havocbot_ctf_find_enemy_flag(self);
1552 if(ef.ctf_status==FLAG_BASE)
1554 self.havocbot_role = self.havocbot_previous_role;
1555 self.havocbot_role_timeout = 0;
1559 // If the flag carrier reached the base switch to defense
1560 mf = havocbot_ctf_find_flag(self);
1561 if(mf.ctf_status!=FLAG_BASE)
1562 if(vlen(ef.origin - mf.dropped_origin) < 300)
1564 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1568 // Set the role timeout if necessary
1569 if (!self.havocbot_role_timeout)
1571 self.havocbot_role_timeout = time + random() * 30 + 60;
1574 // If nothing happened just switch to previous role
1575 if (time > self.havocbot_role_timeout)
1577 self.havocbot_role = self.havocbot_previous_role;
1578 self.havocbot_role_timeout = 0;
1582 // Chase the flag carrier
1583 if (self.bot_strategytime < time)
1585 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1586 navigation_goalrating_start();
1587 havocbot_goalrating_ctf_enemyflag(30000);
1588 havocbot_goalrating_ctf_ourstolenflag(40000);
1589 havocbot_goalrating_items(10000, self.origin, 10000);
1590 navigation_goalrating_end();
1594 void havocbot_role_ctf_offense()
1599 if(self.deadflag != DEAD_NO)
1601 havocbot_ctf_reset_role(self);
1605 if (self.flagcarried)
1607 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1612 mf = havocbot_ctf_find_flag(self);
1613 ef = havocbot_ctf_find_enemy_flag(self);
1616 if(mf.ctf_status!=FLAG_BASE)
1619 pos = mf.tag_entity.origin;
1623 // Try to get it if closer than the enemy base
1624 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1626 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1631 // Escort flag carrier
1632 if(ef.ctf_status!=FLAG_BASE)
1635 pos = ef.tag_entity.origin;
1639 if(vlen(pos-mf.dropped_origin)>700)
1641 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1646 // About to fail, switch to middlefield
1649 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1653 // Set the role timeout if necessary
1654 if (!self.havocbot_role_timeout)
1655 self.havocbot_role_timeout = time + 120;
1657 if (time > self.havocbot_role_timeout)
1659 havocbot_ctf_reset_role(self);
1663 if (self.bot_strategytime < time)
1665 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1666 navigation_goalrating_start();
1667 havocbot_goalrating_ctf_ourstolenflag(50000);
1668 havocbot_goalrating_ctf_enemybase(20000);
1669 havocbot_goalrating_items(5000, self.origin, 1000);
1670 havocbot_goalrating_items(1000, self.origin, 10000);
1671 navigation_goalrating_end();
1675 // Retriever (temporary role):
1676 void havocbot_role_ctf_retriever()
1680 if(self.deadflag != DEAD_NO)
1682 havocbot_ctf_reset_role(self);
1686 if (self.flagcarried)
1688 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1692 // If flag is back on the base switch to previous role
1693 mf = havocbot_ctf_find_flag(self);
1694 if(mf.ctf_status==FLAG_BASE)
1696 havocbot_ctf_reset_role(self);
1700 if (!self.havocbot_role_timeout)
1701 self.havocbot_role_timeout = time + 20;
1703 if (time > self.havocbot_role_timeout)
1705 havocbot_ctf_reset_role(self);
1709 if (self.bot_strategytime < time)
1714 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1715 navigation_goalrating_start();
1716 havocbot_goalrating_ctf_ourstolenflag(50000);
1717 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1718 havocbot_goalrating_ctf_enemybase(30000);
1719 havocbot_goalrating_items(500, self.origin, rt_radius);
1720 navigation_goalrating_end();
1724 void havocbot_role_ctf_middle()
1728 if(self.deadflag != DEAD_NO)
1730 havocbot_ctf_reset_role(self);
1734 if (self.flagcarried)
1736 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1740 mf = havocbot_ctf_find_flag(self);
1741 if(mf.ctf_status!=FLAG_BASE)
1743 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1747 if (!self.havocbot_role_timeout)
1748 self.havocbot_role_timeout = time + 10;
1750 if (time > self.havocbot_role_timeout)
1752 havocbot_ctf_reset_role(self);
1756 if (self.bot_strategytime < time)
1760 org = havocbot_ctf_middlepoint;
1761 org.z = self.origin.z;
1763 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1764 navigation_goalrating_start();
1765 havocbot_goalrating_ctf_ourstolenflag(50000);
1766 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1767 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1768 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1769 havocbot_goalrating_items(2500, self.origin, 10000);
1770 havocbot_goalrating_ctf_enemybase(2500);
1771 navigation_goalrating_end();
1775 void havocbot_role_ctf_defense()
1779 if(self.deadflag != DEAD_NO)
1781 havocbot_ctf_reset_role(self);
1785 if (self.flagcarried)
1787 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1791 // If own flag was captured
1792 mf = havocbot_ctf_find_flag(self);
1793 if(mf.ctf_status!=FLAG_BASE)
1795 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1799 if (!self.havocbot_role_timeout)
1800 self.havocbot_role_timeout = time + 30;
1802 if (time > self.havocbot_role_timeout)
1804 havocbot_ctf_reset_role(self);
1807 if (self.bot_strategytime < time)
1812 org = mf.dropped_origin;
1813 mp_radius = havocbot_ctf_middlepoint_radius;
1815 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1816 navigation_goalrating_start();
1818 // if enemies are closer to our base, go there
1819 entity head, closestplayer = world;
1820 float distance, bestdistance = 10000;
1821 FOR_EACH_PLAYER(head)
1823 if(head.deadflag!=DEAD_NO)
1826 distance = vlen(org - head.origin);
1827 if(distance<bestdistance)
1829 closestplayer = head;
1830 bestdistance = distance;
1835 if(DIFF_TEAM(closestplayer, self))
1836 if(vlen(org - self.origin)>1000)
1837 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1838 havocbot_goalrating_ctf_ourbase(30000);
1840 havocbot_goalrating_ctf_ourstolenflag(20000);
1841 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1842 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1843 havocbot_goalrating_items(10000, org, mp_radius);
1844 havocbot_goalrating_items(5000, self.origin, 10000);
1845 navigation_goalrating_end();
1849 void havocbot_role_ctf_setrole(entity bot, int role)
1851 LOG_TRACE(strcat(bot.netname," switched to "));
1854 case HAVOCBOT_CTF_ROLE_CARRIER:
1855 LOG_TRACE("carrier");
1856 bot.havocbot_role = havocbot_role_ctf_carrier;
1857 bot.havocbot_role_timeout = 0;
1858 bot.havocbot_cantfindflag = time + 10;
1859 bot.bot_strategytime = 0;
1861 case HAVOCBOT_CTF_ROLE_DEFENSE:
1862 LOG_TRACE("defense");
1863 bot.havocbot_role = havocbot_role_ctf_defense;
1864 bot.havocbot_role_timeout = 0;
1866 case HAVOCBOT_CTF_ROLE_MIDDLE:
1867 LOG_TRACE("middle");
1868 bot.havocbot_role = havocbot_role_ctf_middle;
1869 bot.havocbot_role_timeout = 0;
1871 case HAVOCBOT_CTF_ROLE_OFFENSE:
1872 LOG_TRACE("offense");
1873 bot.havocbot_role = havocbot_role_ctf_offense;
1874 bot.havocbot_role_timeout = 0;
1876 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1877 LOG_TRACE("retriever");
1878 bot.havocbot_previous_role = bot.havocbot_role;
1879 bot.havocbot_role = havocbot_role_ctf_retriever;
1880 bot.havocbot_role_timeout = time + 10;
1881 bot.bot_strategytime = 0;
1883 case HAVOCBOT_CTF_ROLE_ESCORT:
1884 LOG_TRACE("escort");
1885 bot.havocbot_previous_role = bot.havocbot_role;
1886 bot.havocbot_role = havocbot_role_ctf_escort;
1887 bot.havocbot_role_timeout = time + 30;
1888 bot.bot_strategytime = 0;
1899 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1902 int t = 0, t2 = 0, t3 = 0;
1904 // initially clear items so they can be set as necessary later.
1905 self.ctf_flagstatus &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST
1906 | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST
1907 | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST
1908 | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST
1909 | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST
1910 | CTF_FLAG_NEUTRAL | CTF_SHIELDED);
1912 // scan through all the flags and notify the client about them
1913 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1915 if(flag.team == NUM_TEAM_1) { t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; }
1916 if(flag.team == NUM_TEAM_2) { t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; }
1917 if(flag.team == NUM_TEAM_3) { t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; }
1918 if(flag.team == NUM_TEAM_4) { t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; }
1919 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; }
1921 switch(flag.ctf_status)
1926 if((flag.owner == self) || (flag.pass_sender == self))
1927 self.ctf_flagstatus |= t; // carrying: self is currently carrying the flag
1929 self.ctf_flagstatus |= t2; // taken: someone else is carrying the flag
1934 self.ctf_flagstatus |= t3; // lost: the flag is dropped somewhere on the map
1940 // item for stopping players from capturing the flag too often
1941 if(self.ctf_captureshielded)
1942 self.ctf_flagstatus |= CTF_SHIELDED;
1944 // update the health of the flag carrier waypointsprite
1945 if(self.wps_flagcarrier)
1946 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
1951 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1953 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1955 if(frag_target == frag_attacker) // damage done to yourself
1957 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1958 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1960 else // damage done to everyone else
1962 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1963 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1966 else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
1968 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)))
1969 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1971 frag_target.wps_helpme_time = time;
1972 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1974 // todo: add notification for when flag carrier needs help?
1979 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1981 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
1983 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1984 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1987 if(frag_target.flagcarried)
1989 entity tmp_entity = frag_target.flagcarried;
1990 ctf_Handle_Throw(frag_target, world, DROP_NORMAL);
1991 tmp_entity.ctf_dropper = world;
1997 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
2000 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2003 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
2005 entity flag; // temporary entity for the search method
2007 if(self.flagcarried)
2008 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2010 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2012 if(flag.pass_sender == self) { flag.pass_sender = world; }
2013 if(flag.pass_target == self) { flag.pass_target = world; }
2014 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
2020 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
2022 if(self.flagcarried)
2023 if(!autocvar_g_ctf_portalteleport)
2024 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2029 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
2031 if(MUTATOR_RETURNVALUE || gameover) { return false; }
2033 entity player = self;
2035 if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2037 // pass the flag to a team mate
2038 if(autocvar_g_ctf_pass)
2040 entity head, closest_target = world;
2041 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2043 while(head) // find the closest acceptable target to pass to
2045 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
2046 if(head != player && SAME_TEAM(head, player))
2047 if(!head.speedrunning && !head.vehicle)
2049 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2050 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2051 vector passer_center = CENTER_OR_VIEWOFS(player);
2053 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2055 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2057 if(IS_BOT_CLIENT(head))
2059 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2060 ctf_Handle_Throw(head, player, DROP_PASS);
2064 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2065 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2067 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2070 else if(player.flagcarried)
2074 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2075 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
2076 { closest_target = head; }
2078 else { closest_target = head; }
2085 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2088 // throw the flag in front of you
2089 if(autocvar_g_ctf_throw && player.flagcarried)
2091 if(player.throw_count == -1)
2093 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2095 player.throw_prevtime = time;
2096 player.throw_count = 1;
2097 ctf_Handle_Throw(player, world, DROP_THROW);
2102 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2108 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2109 else { player.throw_count += 1; }
2110 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2112 player.throw_prevtime = time;
2113 ctf_Handle_Throw(player, world, DROP_THROW);
2122 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
2124 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2126 self.wps_helpme_time = time;
2127 WaypointSprite_HelpMePing(self.wps_flagcarrier);
2129 else // create a normal help me waypointsprite
2131 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, self, FLAG_WAYPOINT_OFFSET, world, self.team, self, wps_helpme, false, RADARICON_HELPME);
2132 WaypointSprite_Ping(self.wps_helpme);
2138 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
2140 if(vh_player.flagcarried)
2142 vh_player.flagcarried.nodrawtoclient = vh_player; // hide the flag from the driver
2144 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2146 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
2150 setattachment(vh_player.flagcarried, vh_vehicle, "");
2151 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
2152 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2153 //vh_player.flagcarried.angles = '0 0 0';
2161 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
2163 if(vh_player.flagcarried)
2165 setattachment(vh_player.flagcarried, vh_player, "");
2166 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2167 vh_player.flagcarried.scale = FLAG_SCALE;
2168 vh_player.flagcarried.angles = '0 0 0';
2169 vh_player.flagcarried.nodrawtoclient = world;
2176 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
2178 if(self.flagcarried)
2180 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));
2181 ctf_RespawnFlag(self.flagcarried);
2188 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
2190 entity flag; // temporary entity for the search method
2192 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2194 switch(flag.ctf_status)
2199 // lock the flag, game is over
2200 flag.movetype = MOVETYPE_NONE;
2201 flag.takedamage = DAMAGE_NO;
2202 flag.solid = SOLID_NOT;
2203 flag.nextthink = false; // stop thinking
2205 //dprint("stopping the ", flag.netname, " from moving.\n");
2213 // do nothing for these flags
2222 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
2224 havocbot_ctf_reset_role(self);
2228 MUTATOR_HOOKFUNCTION(ctf_GetTeamCount)
2230 //ret_float = ctf_teams;
2231 ret_string = "ctf_team";
2235 MUTATOR_HOOKFUNCTION(ctf_SpectateCopy)
2237 self.ctf_flagstatus = other.ctf_flagstatus;
2246 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2247 CTF flag for team one (Red).
2249 "angle" Angle the flag will point (minus 90 degrees)...
2250 "model" model to use, note this needs red and blue as skins 0 and 1...
2251 "noise" sound played when flag is picked up...
2252 "noise1" sound played when flag is returned by a teammate...
2253 "noise2" sound played when flag is captured...
2254 "noise3" sound played when flag is lost in the field and respawns itself...
2255 "noise4" sound played when flag is dropped by a player...
2256 "noise5" sound played when flag touches the ground... */
2257 void spawnfunc_item_flag_team1()
2259 if(!g_ctf) { remove(self); return; }
2261 ctf_FlagSetup(NUM_TEAM_1, self);
2264 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2265 CTF flag for team two (Blue).
2267 "angle" Angle the flag will point (minus 90 degrees)...
2268 "model" model to use, note this needs red and blue as skins 0 and 1...
2269 "noise" sound played when flag is picked up...
2270 "noise1" sound played when flag is returned by a teammate...
2271 "noise2" sound played when flag is captured...
2272 "noise3" sound played when flag is lost in the field and respawns itself...
2273 "noise4" sound played when flag is dropped by a player...
2274 "noise5" sound played when flag touches the ground... */
2275 void spawnfunc_item_flag_team2()
2277 if(!g_ctf) { remove(self); return; }
2279 ctf_FlagSetup(NUM_TEAM_2, self);
2282 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2283 CTF flag for team three (Yellow).
2285 "angle" Angle the flag will point (minus 90 degrees)...
2286 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2287 "noise" sound played when flag is picked up...
2288 "noise1" sound played when flag is returned by a teammate...
2289 "noise2" sound played when flag is captured...
2290 "noise3" sound played when flag is lost in the field and respawns itself...
2291 "noise4" sound played when flag is dropped by a player...
2292 "noise5" sound played when flag touches the ground... */
2293 void spawnfunc_item_flag_team3()
2295 if(!g_ctf) { remove(self); return; }
2297 ctf_FlagSetup(NUM_TEAM_3, self);
2300 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2301 CTF flag for team four (Pink).
2303 "angle" Angle the flag will point (minus 90 degrees)...
2304 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
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_team4()
2313 if(!g_ctf) { remove(self); return; }
2315 ctf_FlagSetup(NUM_TEAM_4, self);
2318 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2321 "angle" Angle the flag will point (minus 90 degrees)...
2322 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
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_neutral()
2331 if(!g_ctf) { remove(self); return; }
2332 if(!cvar("g_ctf_oneflag")) { remove(self); return; }
2334 ctf_FlagSetup(0, self);
2337 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2338 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2339 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.
2341 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2342 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2343 void spawnfunc_ctf_team()
2345 if(!g_ctf) { remove(self); return; }
2347 self.classname = "ctf_team";
2348 self.team = self.cnt + 1;
2351 // compatibility for quake maps
2352 void spawnfunc_team_CTF_redflag() { spawnfunc_item_flag_team1(); }
2353 void spawnfunc_team_CTF_blueflag() { spawnfunc_item_flag_team2(); }
2354 void spawnfunc_team_CTF_redplayer() { spawnfunc_info_player_team1(); }
2355 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2(); }
2356 void spawnfunc_team_CTF_redspawn() { spawnfunc_info_player_team1(); }
2357 void spawnfunc_team_CTF_bluespawn() { spawnfunc_info_player_team2(); }
2359 void team_CTF_neutralflag() { spawnfunc_item_flag_neutral(); }
2360 void team_neutralobelisk() { spawnfunc_item_flag_neutral(); }
2368 void ctf_ScoreRules(int teams)
2370 CheckAllowedTeams(world);
2371 ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
2372 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2373 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2374 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2375 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2376 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2377 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2378 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2379 ScoreRules_basics_end();
2382 // code from here on is just to support maps that don't have flag and team entities
2383 void ctf_SpawnTeam (string teamname, int teamcolor)
2385 entity oldself = self;
2387 self.classname = "ctf_team";
2388 self.netname = teamname;
2389 self.cnt = teamcolor;
2391 spawnfunc_ctf_team();
2396 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2401 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2403 if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2404 if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2405 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2408 ctf_teams = bound(2, ctf_teams, 4);
2410 // if no teams are found, spawn defaults
2411 if(find(world, classname, "ctf_team") == world)
2413 LOG_INFO("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2414 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2415 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2417 ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2419 ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2422 ctf_ScoreRules(ctf_teams);
2425 void ctf_Initialize()
2427 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2429 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2430 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2431 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2433 addstat(STAT_CTF_FLAGSTATUS, AS_INT, ctf_flagstatus);
2435 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2439 MUTATOR_DEFINITION(gamemode_ctf)
2441 MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2442 MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2443 MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2444 MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2445 MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2446 MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2447 MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2448 MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2449 MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2450 MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2451 MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2452 MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2453 MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2454 MUTATOR_HOOK(HavocBot_ChooseRole, ctf_BotRoles, CBC_ORDER_ANY);
2455 MUTATOR_HOOK(GetTeamCount, ctf_GetTeamCount, CBC_ORDER_ANY);
2456 MUTATOR_HOOK(SpectateCopy, ctf_SpectateCopy, CBC_ORDER_ANY);
2460 if(time > 1) // game loads at time 1
2461 error("This is a game type and it cannot be added at runtime.");
2465 MUTATOR_ONROLLBACK_OR_REMOVE
2467 // we actually cannot roll back ctf_Initialize here
2468 // BUT: we don't need to! If this gets called, adding always
2474 LOG_INFO("This is a game type and it cannot be removed at runtime.");