1 #include "../../common/movetypes/movetypes.qh"
3 void ctf_FakeTimeLimit(entity e, float t)
6 WriteByte(MSG_ONE, 3); // svc_updatestat
7 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
9 WriteCoord(MSG_ONE, autocvar_timelimit);
11 WriteCoord(MSG_ONE, (t + 1) / 60);
14 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
16 if(autocvar_sv_eventlog)
17 GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
20 void ctf_CaptureRecord(entity flag, entity player)
22 float cap_record = ctf_captimerecord;
23 float cap_time = (time - flag.ctf_pickuptime);
24 string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
27 if(!ctf_captimerecord) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_2(flag, CHOICE_CTF_CAPTURE_TIME_), player.netname, (cap_time * 100)); }
28 else if(cap_time < cap_record) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_2(flag, CHOICE_CTF_CAPTURE_BROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
29 else { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_2(flag, CHOICE_CTF_CAPTURE_UNBROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
31 // write that shit in the database
32 if((!ctf_captimerecord) || (cap_time < cap_record))
34 ctf_captimerecord = cap_time;
35 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
36 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
37 write_recordmarker(player, (time - cap_time), cap_time);
41 void ctf_FlagcarrierWaypoints(entity player)
43 WaypointSprite_Spawn("flagcarrier", 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, true, RADARICON_FLAG, WPCOLOR_FLAGCARRIER(player.team));
44 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON) * 2);
45 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
46 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
49 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
51 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
52 float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
53 float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
54 //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
57 if(current_height) // make sure we can actually do this arcing path
59 targpos = (to + ('0 0 1' * current_height));
60 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
61 if(trace_fraction < 1)
63 //print("normal arc line failed, trying to find new pos...");
64 WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
65 targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
66 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
67 if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
68 /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
71 else { targpos = to; }
73 //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
75 vector desired_direction = normalize(targpos - from);
76 if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
77 else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
80 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
82 if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
84 // directional tracing only
86 makevectors(passer_angle);
88 // find the closest point on the enemy to the center of the attack
89 float h; // hypotenuse, which is the distance between attacker to head
90 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
92 h = vlen(head_center - passer_center);
93 a = h * (normalize(head_center - passer_center) * v_forward);
95 vector nearest_on_line = (passer_center + a * v_forward);
96 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
98 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
99 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
101 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
106 else { return true; }
110 // =======================
111 // CaptureShield Functions
112 // =======================
114 bool ctf_CaptureShield_CheckStatus(entity p)
118 float players_worseeq, players_total;
120 if(ctf_captureshield_max_ratio <= 0)
123 s = PlayerScore_Add(p, SP_SCORE, 0);
124 if(s >= -ctf_captureshield_min_negscore)
127 players_total = players_worseeq = 0;
132 se = PlayerScore_Add(e, SP_SCORE, 0);
138 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
139 // use this rule here
141 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
147 void ctf_CaptureShield_Update(entity player, float wanted_status)
149 float updated_status = ctf_CaptureShield_CheckStatus(player);
150 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
152 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
153 player.ctf_captureshielded = updated_status;
157 float ctf_CaptureShield_Customize()
159 if(!other.ctf_captureshielded) { return false; }
160 if(SAME_TEAM(self, other)) { return false; }
165 void ctf_CaptureShield_Touch()
167 if(!other.ctf_captureshielded) { return; }
168 if(SAME_TEAM(self, other)) { return; }
170 vector mymid = (self.absmin + self.absmax) * 0.5;
171 vector othermid = (other.absmin + other.absmax) * 0.5;
173 Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
174 if(IS_REAL_CLIENT(other)) { Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
177 void ctf_CaptureShield_Spawn(entity flag)
179 entity shield = spawn();
182 shield.team = self.team;
183 shield.touch = ctf_CaptureShield_Touch;
184 shield.customizeentityforclient = ctf_CaptureShield_Customize;
185 shield.classname = "ctf_captureshield";
186 shield.effects = EF_ADDITIVE;
187 shield.movetype = MOVETYPE_NOCLIP;
188 shield.solid = SOLID_TRIGGER;
189 shield.avelocity = '7 0 11';
192 setorigin(shield, self.origin);
193 setmodel(shield, "models/ctf/shield.md3");
194 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
198 // ====================
199 // Drop/Pass/Throw Code
200 // ====================
202 void ctf_Handle_Drop(entity flag, entity player, float droptype)
205 player = (player ? player : flag.pass_sender);
208 flag.movetype = MOVETYPE_TOSS;
209 flag.takedamage = DAMAGE_YES;
210 flag.angles = '0 0 0';
211 flag.health = flag.max_flag_health;
212 flag.ctf_droptime = time;
213 flag.ctf_dropper = player;
214 flag.ctf_status = FLAG_DROPPED;
216 // messages and sounds
217 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_LOST_), player.netname);
218 sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
219 ctf_EventLog("dropped", player.team, player);
222 PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);
223 PlayerScore_Add(player, SP_CTF_DROPS, 1);
226 if(autocvar_g_ctf_flag_dropped_waypoint)
227 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));
229 if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
231 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
232 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
235 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
237 if(droptype == DROP_PASS)
239 flag.pass_distance = 0;
240 flag.pass_sender = world;
241 flag.pass_target = world;
245 void ctf_Handle_Retrieve(entity flag, entity player)
247 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
248 entity sender = flag.pass_sender;
250 // transfer flag to player
252 flag.owner.flagcarried = flag;
257 setattachment(flag, player.vehicle, "");
258 setorigin(flag, VEHICLE_FLAG_OFFSET);
259 flag.scale = VEHICLE_FLAG_SCALE;
263 setattachment(flag, player, "");
264 setorigin(flag, FLAG_CARRY_OFFSET);
266 flag.movetype = MOVETYPE_NONE;
267 flag.takedamage = DAMAGE_NO;
268 flag.solid = SOLID_NOT;
269 flag.angles = '0 0 0';
270 flag.ctf_status = FLAG_CARRY;
272 // messages and sounds
273 sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
274 ctf_EventLog("receive", flag.team, player);
276 FOR_EACH_REALPLAYER(tmp_player)
278 if(tmp_player == sender)
279 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PASS_SENT_), player.netname);
280 else if(tmp_player == player)
281 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PASS_RECEIVED_), sender.netname);
282 else if(SAME_TEAM(tmp_player, sender))
283 Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PASS_OTHER_), sender.netname, player.netname);
286 // create new waypoint
287 ctf_FlagcarrierWaypoints(player);
289 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
290 player.throw_antispam = sender.throw_antispam;
292 flag.pass_distance = 0;
293 flag.pass_sender = world;
294 flag.pass_target = world;
297 void ctf_Handle_Throw(entity player, entity receiver, float droptype)
299 entity flag = player.flagcarried;
300 vector targ_origin, flag_velocity;
302 if(!flag) { return; }
303 if((droptype == DROP_PASS) && !receiver) { return; }
305 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
308 setattachment(flag, world, "");
309 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
310 flag.owner.flagcarried = world;
312 flag.solid = SOLID_TRIGGER;
313 flag.ctf_dropper = player;
314 flag.ctf_droptime = time;
316 flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
323 // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
324 // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
325 WarpZone_RefSys_Copy(flag, receiver);
326 WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
327 targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
329 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
330 ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
333 flag.movetype = MOVETYPE_FLY;
334 flag.takedamage = DAMAGE_NO;
335 flag.pass_sender = player;
336 flag.pass_target = receiver;
337 flag.ctf_status = FLAG_PASSING;
340 sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
341 WarpZone_TrailParticles(world, particleeffectnum(flag.passeffect), player.origin, targ_origin);
342 ctf_EventLog("pass", flag.team, player);
348 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'));
350 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)));
351 flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, false);
352 ctf_Handle_Drop(flag, player, droptype);
358 flag.velocity = '0 0 0'; // do nothing
365 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);
366 ctf_Handle_Drop(flag, player, droptype);
371 // kill old waypointsprite
372 WaypointSprite_Ping(player.wps_flagcarrier);
373 WaypointSprite_Kill(player.wps_flagcarrier);
375 if(player.wps_enemyflagcarrier)
376 WaypointSprite_Kill(player.wps_enemyflagcarrier);
379 ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
387 void ctf_Handle_Capture(entity flag, entity toucher, float capturetype)
389 entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
390 entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
391 float old_time, new_time;
393 if (!player) { return; } // without someone to give the reward to, we can't possibly cap
395 nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
397 // messages and sounds
398 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_2(enemy_flag, CENTER_CTF_CAPTURE_));
399 ctf_CaptureRecord(enemy_flag, player);
400 sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTEN_NONE);
404 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
405 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
410 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
411 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
413 old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
414 new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
415 if(!old_time || new_time < old_time)
416 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
419 pointparticles(particleeffectnum(flag.capeffect), flag.origin, '0 0 0', 1);
420 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
423 if(capturetype == CAPTURE_NORMAL)
425 WaypointSprite_Kill(player.wps_flagcarrier);
426 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
428 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
429 { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
433 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
434 ctf_RespawnFlag(enemy_flag);
437 void ctf_Handle_Return(entity flag, entity player)
439 // messages and sounds
440 if(player.flags & FL_MONSTER)
442 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_RETURN_MONSTER_), player.monster_name);
446 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_RETURN_));
447 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_RETURN_), player.netname);
449 sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
450 ctf_EventLog("return", flag.team, player);
453 if(IS_PLAYER(player))
455 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
456 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
458 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
461 TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
465 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
466 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
467 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
471 ctf_RespawnFlag(flag);
474 void ctf_Handle_Pickup(entity flag, entity player, float pickuptype)
477 float pickup_dropped_score; // used to calculate dropped pickup score
479 // attach the flag to the player
481 player.flagcarried = flag;
484 setattachment(flag, player.vehicle, "");
485 setorigin(flag, VEHICLE_FLAG_OFFSET);
486 flag.scale = VEHICLE_FLAG_SCALE;
490 setattachment(flag, player, "");
491 setorigin(flag, FLAG_CARRY_OFFSET);
495 flag.movetype = MOVETYPE_NONE;
496 flag.takedamage = DAMAGE_NO;
497 flag.solid = SOLID_NOT;
498 flag.angles = '0 0 0';
499 flag.ctf_status = FLAG_CARRY;
503 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
504 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
508 // messages and sounds
509 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_PICKUP_), player.netname);
510 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PICKUP_));
511 if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
513 Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, CHOICE_CTF_PICKUP_TEAM, Team_ColorCode(player.team), player.netname);
514 Send_Notification(NOTIF_TEAM, flag, MSG_CHOICE, CHOICE_CTF_PICKUP_ENEMY, Team_ColorCode(player.team), player.netname);
516 sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
519 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
520 nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
525 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
526 ctf_EventLog("steal", flag.team, player);
532 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);
533 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);
534 dprint("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
535 PlayerTeamScore_AddScore(player, pickup_dropped_score);
536 ctf_EventLog("pickup", flag.team, player);
544 if(pickuptype == PICKUP_BASE)
546 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
547 if((player.speedrunning) && (ctf_captimerecord))
548 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
552 pointparticles(particleeffectnum(flag.toucheffect), player.origin, '0 0 0', 1);
555 if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
556 ctf_FlagcarrierWaypoints(player);
557 WaypointSprite_Ping(player.wps_flagcarrier);
561 // ===================
562 // Main Flag Functions
563 // ===================
565 void ctf_CheckFlagReturn(entity flag, float returntype)
567 if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
569 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
571 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
575 case RETURN_DROPPED: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_DROPPED_)); break;
576 case RETURN_DAMAGE: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_DAMAGED_)); break;
577 case RETURN_SPEEDRUN: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_SPEEDRUN_), ctf_captimerecord); break;
578 case RETURN_NEEDKILL: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_NEEDKILL_)); break;
582 { Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_TIMEOUT_)); break; }
584 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
585 ctf_EventLog("returned", flag.team, world);
586 ctf_RespawnFlag(flag);
591 void ctf_CheckStalemate(void)
594 float stale_red_flags = 0, stale_blue_flags = 0;
597 entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
599 // build list of stale flags
600 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
602 if(autocvar_g_ctf_stalemate)
603 if(tmp_entity.ctf_status != FLAG_BASE)
604 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time)
606 tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
607 ctf_staleflaglist = tmp_entity;
609 switch(tmp_entity.team)
611 case NUM_TEAM_1: ++stale_red_flags; break;
612 case NUM_TEAM_2: ++stale_blue_flags; break;
617 if(stale_red_flags && stale_blue_flags)
618 ctf_stalemate = true;
619 else if((!stale_red_flags && !stale_blue_flags) && autocvar_g_ctf_stalemate_endcondition == 2)
620 { ctf_stalemate = false; wpforenemy_announced = false; }
621 else if((!stale_red_flags || !stale_blue_flags) && autocvar_g_ctf_stalemate_endcondition == 1)
622 { ctf_stalemate = false; wpforenemy_announced = false; }
624 // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
627 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
629 if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
630 WaypointSprite_Spawn("enemyflagcarrier", 0, 0, tmp_entity.owner, FLAG_WAYPOINT_OFFSET, world, tmp_entity.team, tmp_entity.owner, wps_enemyflagcarrier, true, RADARICON_FLAG, WPCOLOR_ENEMYFC(tmp_entity.owner.team));
633 if (!wpforenemy_announced)
635 FOR_EACH_REALPLAYER(tmp_entity)
636 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CENTER, ((tmp_entity.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER));
638 wpforenemy_announced = true;
643 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
645 if(ITEM_DAMAGE_NEEDKILL(deathtype))
647 // automatically kill the flag and return it
649 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
652 if(autocvar_g_ctf_flag_return_damage)
654 // reduce health and check if it should be returned
655 self.health = self.health - damage;
656 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
666 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
669 if(self == ctf_worldflaglist) // only for the first flag
670 FOR_EACH_CLIENT(tmp_entity)
671 ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
674 if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
675 dprint("wtf the flag got squashed?\n");
676 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
677 if(!trace_startsolid || self.noalign) // can we resize it without getting stuck?
678 setsize(self, FLAG_MIN, FLAG_MAX); }
680 switch(self.ctf_status) // reset flag angles in case warpzones adjust it
684 self.angles = '0 0 0';
692 switch(self.ctf_status)
696 if(autocvar_g_ctf_dropped_capture_radius)
698 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
699 if(tmp_entity.ctf_status == FLAG_DROPPED)
700 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
701 if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
702 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
709 if(autocvar_g_ctf_flag_dropped_floatinwater)
711 vector midpoint = ((self.absmin + self.absmax) * 0.5);
712 if(pointcontents(midpoint) == CONTENT_WATER)
714 self.velocity = self.velocity * 0.5;
716 if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
717 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
719 { self.movetype = MOVETYPE_FLY; }
721 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
723 if(autocvar_g_ctf_flag_return_dropped)
725 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
728 ctf_CheckFlagReturn(self, RETURN_DROPPED);
732 if(autocvar_g_ctf_flag_return_time)
734 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
735 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
743 if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
746 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
750 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
754 if(autocvar_g_ctf_stalemate)
756 if(time >= wpforenemy_nextthink)
758 ctf_CheckStalemate();
759 wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
767 vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
768 targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
769 WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
771 if((self.pass_target == world)
772 || (self.pass_target.deadflag != DEAD_NO)
773 || (self.pass_target.flagcarried)
774 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
775 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
776 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
778 // give up, pass failed
779 ctf_Handle_Drop(self, world, DROP_PASS);
783 // still a viable target, go for it
784 ctf_CalculatePassVelocity(self, targ_origin, self.origin, true);
789 default: // this should never happen
791 dprint("ctf_FlagThink(): Flag exists with no status?\n");
799 if(gameover) { return; }
800 if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
802 entity toucher = other;
803 float is_not_monster = (!(toucher.flags & FL_MONSTER));
805 // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
806 if(ITEM_TOUCH_NEEDKILL())
809 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
813 // special touch behaviors
814 if(toucher.frozen) { return; }
815 else if(toucher.vehicle_flags & VHF_ISVEHICLE)
817 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
818 toucher = toucher.owner; // the player is actually the vehicle owner, not other
820 return; // do nothing
822 else if(toucher.flags & FL_MONSTER)
824 if(!autocvar_g_ctf_allow_monster_touch)
825 return; // do nothing
827 else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
829 if(time > self.wait) // if we haven't in a while, play a sound/effect
831 pointparticles(particleeffectnum(self.toucheffect), self.origin, '0 0 0', 1);
832 sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTEN_NORM);
833 self.wait = time + FLAG_TOUCHRATE;
837 else if(toucher.deadflag != DEAD_NO) { return; }
839 switch(self.ctf_status)
843 if(SAME_TEAM(toucher, self) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, self) && is_not_monster)
844 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
845 else if(DIFF_TEAM(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
846 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
852 if(SAME_TEAM(toucher, self))
853 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
854 else if(is_not_monster && (!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
855 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
861 dprint("Someone touched a flag even though it was being carried?\n");
867 if((IS_PLAYER(toucher)) && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
869 if(DIFF_TEAM(toucher, self.pass_sender))
870 ctf_Handle_Return(self, toucher);
872 ctf_Handle_Retrieve(self, toucher);
880 void ctf_RespawnFlag(entity flag)
882 // check for flag respawn being called twice in a row
883 if(flag.last_respawn > time - 0.5)
884 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
886 flag.last_respawn = time;
888 // reset the player (if there is one)
889 if((flag.owner) && (flag.owner.flagcarried == flag))
891 if(flag.owner.wps_enemyflagcarrier)
892 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
894 WaypointSprite_Kill(flag.wps_flagcarrier);
896 flag.owner.flagcarried = world;
898 if(flag.speedrunning)
899 ctf_FakeTimeLimit(flag.owner, -1);
902 if((flag.owner) && (flag.owner.vehicle))
903 flag.scale = FLAG_SCALE;
905 if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
906 { WaypointSprite_Kill(flag.wps_flagdropped); }
909 setattachment(flag, world, "");
910 setorigin(flag, flag.ctf_spawnorigin);
912 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
913 flag.takedamage = DAMAGE_NO;
914 flag.health = flag.max_flag_health;
915 flag.solid = SOLID_TRIGGER;
916 flag.velocity = '0 0 0';
917 flag.angles = flag.mangle;
918 flag.flags = FL_ITEM | FL_NOTARGET;
920 flag.ctf_status = FLAG_BASE;
922 flag.pass_distance = 0;
923 flag.pass_sender = world;
924 flag.pass_target = world;
925 flag.ctf_dropper = world;
926 flag.ctf_pickuptime = 0;
927 flag.ctf_droptime = 0;
929 ctf_CheckStalemate();
935 if(IS_PLAYER(self.owner))
936 ctf_Handle_Throw(self.owner, world, DROP_RESET);
938 ctf_RespawnFlag(self);
941 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
944 waypoint_spawnforitem_force(self, self.origin);
945 self.nearestwaypointtimeout = 0; // activate waypointing again
946 self.bot_basewaypoint = self.nearestwaypoint;
949 // move_origin isnt accessible just yet
950 WaypointSprite_SpawnFixed(((self.team == NUM_TEAM_1) ? "redbase" : "bluebase"), self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, false));
951 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, false));
953 // captureshield setup
954 ctf_CaptureShield_Spawn(self);
957 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
960 teamnumber = fabs(teamnumber - bound(0, autocvar_g_ctf_reverse, 1)); // if we were originally 1, this will become 0. If we were originally 0, this will become 1.
961 self = flag; // for later usage with droptofloor()
964 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
965 ctf_worldflaglist = flag;
967 setattachment(flag, world, "");
969 flag.netname = ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag"); // Primarily only used for debugging or when showing nearby item name
970 flag.team = ((teamnumber) ? NUM_TEAM_1 : NUM_TEAM_2); // NUM_TEAM_1: color 4 team (red) - NUM_TEAM_2: color 13 team (blue)
971 flag.items = ((teamnumber) ? IT_KEY2 : IT_KEY1); // IT_KEY2: gold key (redish enough) - IT_KEY1: silver key (bluish enough)
972 flag.classname = "item_flag_team";
973 flag.target = "###item###"; // wut?
974 flag.flags = FL_ITEM | FL_NOTARGET;
975 flag.solid = SOLID_TRIGGER;
976 flag.takedamage = DAMAGE_NO;
977 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
978 flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
979 flag.health = flag.max_flag_health;
980 flag.event_damage = ctf_FlagDamage;
981 flag.pushable = true;
982 flag.teleportable = TELEPORT_NORMAL;
983 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
984 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
985 flag.velocity = '0 0 0';
986 flag.mangle = flag.angles;
987 flag.reset = ctf_Reset;
988 flag.touch = ctf_FlagTouch;
989 flag.think = ctf_FlagThink;
990 flag.nextthink = time + FLAG_THINKRATE;
991 flag.ctf_status = FLAG_BASE;
994 if(flag.model == "") { flag.model = ((teamnumber) ? autocvar_g_ctf_flag_red_model : autocvar_g_ctf_flag_blue_model); }
995 if(!flag.scale) { flag.scale = FLAG_SCALE; }
996 if(!flag.skin) { flag.skin = ((teamnumber) ? autocvar_g_ctf_flag_red_skin : autocvar_g_ctf_flag_blue_skin); }
997 if(flag.toucheffect == "") { flag.toucheffect = ((teamnumber) ? "redflag_touch" : "blueflag_touch"); }
998 if(flag.passeffect == "") { flag.passeffect = ((teamnumber) ? "red_pass" : "blue_pass"); }
999 if(flag.capeffect == "") { flag.capeffect = ((teamnumber) ? "red_cap" : "blue_cap"); }
1002 if(flag.snd_flag_taken == "") { flag.snd_flag_taken = ((teamnumber) ? "ctf/red_taken.wav" : "ctf/blue_taken.wav"); }
1003 if(flag.snd_flag_returned == "") { flag.snd_flag_returned = ((teamnumber) ? "ctf/red_returned.wav" : "ctf/blue_returned.wav"); }
1004 if(flag.snd_flag_capture == "") { flag.snd_flag_capture = ((teamnumber) ? "ctf/red_capture.wav" : "ctf/blue_capture.wav"); } // blue team scores by capturing the red flag
1005 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.
1006 if(flag.snd_flag_dropped == "") { flag.snd_flag_dropped = ((teamnumber) ? "ctf/red_dropped.wav" : "ctf/blue_dropped.wav"); }
1007 if(flag.snd_flag_touch == "") { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
1008 if(flag.snd_flag_pass == "") { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
1011 precache_sound(flag.snd_flag_taken);
1012 precache_sound(flag.snd_flag_returned);
1013 precache_sound(flag.snd_flag_capture);
1014 precache_sound(flag.snd_flag_respawn);
1015 precache_sound(flag.snd_flag_dropped);
1016 precache_sound(flag.snd_flag_touch);
1017 precache_sound(flag.snd_flag_pass);
1018 precache_model(flag.model);
1019 precache_model("models/ctf/shield.md3");
1020 precache_model("models/ctf/shockwavetransring.md3");
1023 setmodel(flag, flag.model); // precision set below
1024 setsize(flag, FLAG_MIN, FLAG_MAX);
1025 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1027 if(autocvar_g_ctf_flag_glowtrails)
1029 flag.glow_color = ((teamnumber) ? 251 : 210); // 251: red - 210: blue
1030 flag.glow_size = 25;
1031 flag.glow_trail = 1;
1034 flag.effects |= EF_LOWPRECISION;
1035 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1036 if(autocvar_g_ctf_dynamiclights) { flag.effects |= ((teamnumber) ? EF_RED : EF_BLUE); }
1039 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1041 flag.dropped_origin = flag.origin;
1042 flag.noalign = true;
1043 flag.movetype = MOVETYPE_NONE;
1045 else // drop to floor, automatically find a platform and set that as spawn origin
1047 flag.noalign = false;
1050 flag.movetype = MOVETYPE_TOSS;
1053 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1061 // NOTE: LEGACY CODE, needs to be re-written!
1063 void havocbot_calculate_middlepoint()
1067 vector fo = '0 0 0';
1070 f = ctf_worldflaglist;
1075 f = f.ctf_worldflagnext;
1079 havocbot_ctf_middlepoint = s * (1.0 / n);
1080 havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint);
1084 entity havocbot_ctf_find_flag(entity bot)
1087 f = ctf_worldflaglist;
1090 if (bot.team == f.team)
1092 f = f.ctf_worldflagnext;
1097 entity havocbot_ctf_find_enemy_flag(entity bot)
1100 f = ctf_worldflaglist;
1103 if (bot.team != f.team)
1105 f = f.ctf_worldflagnext;
1110 float havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1118 FOR_EACH_PLAYER(head)
1120 if(head.team!=bot.team || head.deadflag != DEAD_NO || head == bot)
1123 if(vlen(head.origin - org) < tc_radius)
1130 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1133 head = ctf_worldflaglist;
1136 if (self.team == head.team)
1138 head = head.ctf_worldflagnext;
1141 navigation_routerating(head, ratingscale, 10000);
1144 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1147 head = ctf_worldflaglist;
1150 if (self.team == head.team)
1152 head = head.ctf_worldflagnext;
1157 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1160 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1163 head = ctf_worldflaglist;
1166 if (self.team != head.team)
1168 head = head.ctf_worldflagnext;
1171 navigation_routerating(head, ratingscale, 10000);
1174 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1176 if (!bot_waypoints_for_items)
1178 havocbot_goalrating_ctf_enemyflag(ratingscale);
1184 head = havocbot_ctf_find_enemy_flag(self);
1189 navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1192 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1196 mf = havocbot_ctf_find_flag(self);
1198 if(mf.ctf_status == FLAG_BASE)
1202 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1205 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1208 head = ctf_worldflaglist;
1211 // flag is out in the field
1212 if(head.ctf_status != FLAG_BASE)
1213 if(head.tag_entity==world) // dropped
1217 if(vlen(org-head.origin)<df_radius)
1218 navigation_routerating(head, ratingscale, 10000);
1221 navigation_routerating(head, ratingscale, 10000);
1224 head = head.ctf_worldflagnext;
1228 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1232 head = findchainfloat(bot_pickup, true);
1235 // gather health and armor only
1237 if (head.health || head.armorvalue)
1238 if (vlen(head.origin - org) < sradius)
1240 // get the value of the item
1241 t = head.bot_pickupevalfunc(self, head) * 0.0001;
1243 navigation_routerating(head, t * ratingscale, 500);
1249 void havocbot_ctf_reset_role(entity bot)
1251 float cdefense, cmiddle, coffense;
1252 entity mf, ef, head;
1255 if(bot.deadflag != DEAD_NO)
1258 if(vlen(havocbot_ctf_middlepoint)==0)
1259 havocbot_calculate_middlepoint();
1262 if (bot.flagcarried)
1264 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1268 mf = havocbot_ctf_find_flag(bot);
1269 ef = havocbot_ctf_find_enemy_flag(bot);
1271 // Retrieve stolen flag
1272 if(mf.ctf_status!=FLAG_BASE)
1274 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1278 // If enemy flag is taken go to the middle to intercept pursuers
1279 if(ef.ctf_status!=FLAG_BASE)
1281 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1285 // if there is only me on the team switch to offense
1287 FOR_EACH_PLAYER(head)
1288 if(SAME_TEAM(head, bot))
1293 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1297 // Evaluate best position to take
1298 // Count mates on middle position
1299 cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1301 // Count mates on defense position
1302 cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1304 // Count mates on offense position
1305 coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1307 if(cdefense<=coffense)
1308 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1309 else if(coffense<=cmiddle)
1310 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1312 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1315 void havocbot_role_ctf_carrier()
1317 if(self.deadflag != DEAD_NO)
1319 havocbot_ctf_reset_role(self);
1323 if (self.flagcarried == world)
1325 havocbot_ctf_reset_role(self);
1329 if (self.bot_strategytime < time)
1331 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1333 navigation_goalrating_start();
1334 havocbot_goalrating_ctf_ourbase(50000);
1337 havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1339 navigation_goalrating_end();
1341 if (self.navigation_hasgoals)
1342 self.havocbot_cantfindflag = time + 10;
1343 else if (time > self.havocbot_cantfindflag)
1345 // Can't navigate to my own base, suicide!
1346 // TODO: drop it and wander around
1347 Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1353 void havocbot_role_ctf_escort()
1357 if(self.deadflag != DEAD_NO)
1359 havocbot_ctf_reset_role(self);
1363 if (self.flagcarried)
1365 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1369 // If enemy flag is back on the base switch to previous role
1370 ef = havocbot_ctf_find_enemy_flag(self);
1371 if(ef.ctf_status==FLAG_BASE)
1373 self.havocbot_role = self.havocbot_previous_role;
1374 self.havocbot_role_timeout = 0;
1378 // If the flag carrier reached the base switch to defense
1379 mf = havocbot_ctf_find_flag(self);
1380 if(mf.ctf_status!=FLAG_BASE)
1381 if(vlen(ef.origin - mf.dropped_origin) < 300)
1383 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1387 // Set the role timeout if necessary
1388 if (!self.havocbot_role_timeout)
1390 self.havocbot_role_timeout = time + random() * 30 + 60;
1393 // If nothing happened just switch to previous role
1394 if (time > self.havocbot_role_timeout)
1396 self.havocbot_role = self.havocbot_previous_role;
1397 self.havocbot_role_timeout = 0;
1401 // Chase the flag carrier
1402 if (self.bot_strategytime < time)
1404 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1405 navigation_goalrating_start();
1406 havocbot_goalrating_ctf_enemyflag(30000);
1407 havocbot_goalrating_ctf_ourstolenflag(40000);
1408 havocbot_goalrating_items(10000, self.origin, 10000);
1409 navigation_goalrating_end();
1413 void havocbot_role_ctf_offense()
1418 if(self.deadflag != DEAD_NO)
1420 havocbot_ctf_reset_role(self);
1424 if (self.flagcarried)
1426 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1431 mf = havocbot_ctf_find_flag(self);
1432 ef = havocbot_ctf_find_enemy_flag(self);
1435 if(mf.ctf_status!=FLAG_BASE)
1438 pos = mf.tag_entity.origin;
1442 // Try to get it if closer than the enemy base
1443 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1445 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1450 // Escort flag carrier
1451 if(ef.ctf_status!=FLAG_BASE)
1454 pos = ef.tag_entity.origin;
1458 if(vlen(pos-mf.dropped_origin)>700)
1460 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1465 // About to fail, switch to middlefield
1468 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1472 // Set the role timeout if necessary
1473 if (!self.havocbot_role_timeout)
1474 self.havocbot_role_timeout = time + 120;
1476 if (time > self.havocbot_role_timeout)
1478 havocbot_ctf_reset_role(self);
1482 if (self.bot_strategytime < time)
1484 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1485 navigation_goalrating_start();
1486 havocbot_goalrating_ctf_ourstolenflag(50000);
1487 havocbot_goalrating_ctf_enemybase(20000);
1488 havocbot_goalrating_items(5000, self.origin, 1000);
1489 havocbot_goalrating_items(1000, self.origin, 10000);
1490 navigation_goalrating_end();
1494 // Retriever (temporary role):
1495 void havocbot_role_ctf_retriever()
1499 if(self.deadflag != DEAD_NO)
1501 havocbot_ctf_reset_role(self);
1505 if (self.flagcarried)
1507 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1511 // If flag is back on the base switch to previous role
1512 mf = havocbot_ctf_find_flag(self);
1513 if(mf.ctf_status==FLAG_BASE)
1515 havocbot_ctf_reset_role(self);
1519 if (!self.havocbot_role_timeout)
1520 self.havocbot_role_timeout = time + 20;
1522 if (time > self.havocbot_role_timeout)
1524 havocbot_ctf_reset_role(self);
1528 if (self.bot_strategytime < time)
1533 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1534 navigation_goalrating_start();
1535 havocbot_goalrating_ctf_ourstolenflag(50000);
1536 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1537 havocbot_goalrating_ctf_enemybase(30000);
1538 havocbot_goalrating_items(500, self.origin, rt_radius);
1539 navigation_goalrating_end();
1543 void havocbot_role_ctf_middle()
1547 if(self.deadflag != DEAD_NO)
1549 havocbot_ctf_reset_role(self);
1553 if (self.flagcarried)
1555 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1559 mf = havocbot_ctf_find_flag(self);
1560 if(mf.ctf_status!=FLAG_BASE)
1562 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1566 if (!self.havocbot_role_timeout)
1567 self.havocbot_role_timeout = time + 10;
1569 if (time > self.havocbot_role_timeout)
1571 havocbot_ctf_reset_role(self);
1575 if (self.bot_strategytime < time)
1579 org = havocbot_ctf_middlepoint;
1580 org.z = self.origin.z;
1582 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1583 navigation_goalrating_start();
1584 havocbot_goalrating_ctf_ourstolenflag(50000);
1585 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1586 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1587 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1588 havocbot_goalrating_items(2500, self.origin, 10000);
1589 havocbot_goalrating_ctf_enemybase(2500);
1590 navigation_goalrating_end();
1594 void havocbot_role_ctf_defense()
1598 if(self.deadflag != DEAD_NO)
1600 havocbot_ctf_reset_role(self);
1604 if (self.flagcarried)
1606 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1610 // If own flag was captured
1611 mf = havocbot_ctf_find_flag(self);
1612 if(mf.ctf_status!=FLAG_BASE)
1614 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1618 if (!self.havocbot_role_timeout)
1619 self.havocbot_role_timeout = time + 30;
1621 if (time > self.havocbot_role_timeout)
1623 havocbot_ctf_reset_role(self);
1626 if (self.bot_strategytime < time)
1631 org = mf.dropped_origin;
1632 mp_radius = havocbot_ctf_middlepoint_radius;
1634 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1635 navigation_goalrating_start();
1637 // if enemies are closer to our base, go there
1638 entity head, closestplayer = world;
1639 float distance, bestdistance = 10000;
1640 FOR_EACH_PLAYER(head)
1642 if(head.deadflag!=DEAD_NO)
1645 distance = vlen(org - head.origin);
1646 if(distance<bestdistance)
1648 closestplayer = head;
1649 bestdistance = distance;
1654 if(DIFF_TEAM(closestplayer, self))
1655 if(vlen(org - self.origin)>1000)
1656 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1657 havocbot_goalrating_ctf_ourbase(30000);
1659 havocbot_goalrating_ctf_ourstolenflag(20000);
1660 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1661 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1662 havocbot_goalrating_items(10000, org, mp_radius);
1663 havocbot_goalrating_items(5000, self.origin, 10000);
1664 navigation_goalrating_end();
1668 void havocbot_role_ctf_setrole(entity bot, float role)
1670 dprint(strcat(bot.netname," switched to "));
1673 case HAVOCBOT_CTF_ROLE_CARRIER:
1675 bot.havocbot_role = havocbot_role_ctf_carrier;
1676 bot.havocbot_role_timeout = 0;
1677 bot.havocbot_cantfindflag = time + 10;
1678 bot.bot_strategytime = 0;
1680 case HAVOCBOT_CTF_ROLE_DEFENSE:
1682 bot.havocbot_role = havocbot_role_ctf_defense;
1683 bot.havocbot_role_timeout = 0;
1685 case HAVOCBOT_CTF_ROLE_MIDDLE:
1687 bot.havocbot_role = havocbot_role_ctf_middle;
1688 bot.havocbot_role_timeout = 0;
1690 case HAVOCBOT_CTF_ROLE_OFFENSE:
1692 bot.havocbot_role = havocbot_role_ctf_offense;
1693 bot.havocbot_role_timeout = 0;
1695 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1696 dprint("retriever");
1697 bot.havocbot_previous_role = bot.havocbot_role;
1698 bot.havocbot_role = havocbot_role_ctf_retriever;
1699 bot.havocbot_role_timeout = time + 10;
1700 bot.bot_strategytime = 0;
1702 case HAVOCBOT_CTF_ROLE_ESCORT:
1704 bot.havocbot_previous_role = bot.havocbot_role;
1705 bot.havocbot_role = havocbot_role_ctf_escort;
1706 bot.havocbot_role_timeout = time + 30;
1707 bot.bot_strategytime = 0;
1718 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1722 // initially clear items so they can be set as necessary later.
1723 self.items &= ~(IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST
1724 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST | IT_CTF_SHIELDED);
1726 // scan through all the flags and notify the client about them
1727 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1729 switch(flag.ctf_status)
1734 if((flag.owner == self) || (flag.pass_sender == self))
1735 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
1737 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
1742 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
1748 // item for stopping players from capturing the flag too often
1749 if(self.ctf_captureshielded)
1750 self.items |= IT_CTF_SHIELDED;
1752 // update the health of the flag carrier waypointsprite
1753 if(self.wps_flagcarrier)
1754 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
1759 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1761 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1763 if(frag_target == frag_attacker) // damage done to yourself
1765 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1766 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1768 else // damage done to everyone else
1770 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1771 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1774 else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && DIFF_TEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
1776 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)))
1777 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1779 frag_target.wps_helpme_time = time;
1780 WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1782 // todo: add notification for when flag carrier needs help?
1787 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1789 if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
1791 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1792 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1795 if(frag_target.flagcarried)
1796 { ctf_Handle_Throw(frag_target, world, DROP_NORMAL); }
1801 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1804 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1807 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1809 entity flag; // temporary entity for the search method
1811 if(self.flagcarried)
1812 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1814 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1816 if(flag.pass_sender == self) { flag.pass_sender = world; }
1817 if(flag.pass_target == self) { flag.pass_target = world; }
1818 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
1824 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
1826 if(self.flagcarried)
1827 if(!autocvar_g_ctf_portalteleport)
1828 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1833 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
1835 if(MUTATOR_RETURNVALUE || gameover) { return false; }
1837 entity player = self;
1839 if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1841 // pass the flag to a team mate
1842 if(autocvar_g_ctf_pass)
1844 entity head, closest_target = world;
1845 head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
1847 while(head) // find the closest acceptable target to pass to
1849 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
1850 if(head != player && SAME_TEAM(head, player))
1851 if(!head.speedrunning && !head.vehicle)
1853 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
1854 vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
1855 vector passer_center = CENTER_OR_VIEWOFS(player);
1857 if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
1859 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
1861 if(IS_BOT_CLIENT(head))
1863 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1864 ctf_Handle_Throw(head, player, DROP_PASS);
1868 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
1869 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1871 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
1874 else if(player.flagcarried)
1878 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
1879 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
1880 { closest_target = head; }
1882 else { closest_target = head; }
1889 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
1892 // throw the flag in front of you
1893 if(autocvar_g_ctf_throw && player.flagcarried)
1895 if(player.throw_count == -1)
1897 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
1899 player.throw_prevtime = time;
1900 player.throw_count = 1;
1901 ctf_Handle_Throw(player, world, DROP_THROW);
1906 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
1912 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
1913 else { player.throw_count += 1; }
1914 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
1916 player.throw_prevtime = time;
1917 ctf_Handle_Throw(player, world, DROP_THROW);
1926 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
1928 if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
1930 self.wps_helpme_time = time;
1931 WaypointSprite_HelpMePing(self.wps_flagcarrier);
1933 else // create a normal help me waypointsprite
1935 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');
1936 WaypointSprite_Ping(self.wps_helpme);
1942 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
1944 if(vh_player.flagcarried)
1946 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
1948 ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
1952 setattachment(vh_player.flagcarried, vh_vehicle, "");
1953 setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
1954 vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
1955 //vh_player.flagcarried.angles = '0 0 0';
1963 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
1965 if(vh_player.flagcarried)
1967 setattachment(vh_player.flagcarried, vh_player, "");
1968 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
1969 vh_player.flagcarried.scale = FLAG_SCALE;
1970 vh_player.flagcarried.angles = '0 0 0';
1977 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
1979 if(self.flagcarried)
1981 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN_));
1982 ctf_RespawnFlag(self.flagcarried);
1989 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
1991 entity flag; // temporary entity for the search method
1993 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1995 switch(flag.ctf_status)
2000 // lock the flag, game is over
2001 flag.movetype = MOVETYPE_NONE;
2002 flag.takedamage = DAMAGE_NO;
2003 flag.solid = SOLID_NOT;
2004 flag.nextthink = false; // stop thinking
2006 //dprint("stopping the ", flag.netname, " from moving.\n");
2014 // do nothing for these flags
2023 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
2025 havocbot_ctf_reset_role(self);
2034 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
2035 CTF Starting point for a player in team one (Red).
2036 Keys: "angle" viewing angle when spawning. */
2037 void spawnfunc_info_player_team1()
2039 if(g_assault) { remove(self); return; }
2041 self.team = NUM_TEAM_1; // red
2042 spawnfunc_info_player_deathmatch();
2046 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
2047 CTF Starting point for a player in team two (Blue).
2048 Keys: "angle" viewing angle when spawning. */
2049 void spawnfunc_info_player_team2()
2051 if(g_assault) { remove(self); return; }
2053 self.team = NUM_TEAM_2; // blue
2054 spawnfunc_info_player_deathmatch();
2057 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
2058 CTF Starting point for a player in team three (Yellow).
2059 Keys: "angle" viewing angle when spawning. */
2060 void spawnfunc_info_player_team3()
2062 if(g_assault) { remove(self); return; }
2064 self.team = NUM_TEAM_3; // yellow
2065 spawnfunc_info_player_deathmatch();
2069 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
2070 CTF Starting point for a player in team four (Purple).
2071 Keys: "angle" viewing angle when spawning. */
2072 void spawnfunc_info_player_team4()
2074 if(g_assault) { remove(self); return; }
2076 self.team = NUM_TEAM_4; // purple
2077 spawnfunc_info_player_deathmatch();
2080 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2081 CTF flag for team one (Red).
2083 "angle" Angle the flag will point (minus 90 degrees)...
2084 "model" model to use, note this needs red and blue as skins 0 and 1...
2085 "noise" sound played when flag is picked up...
2086 "noise1" sound played when flag is returned by a teammate...
2087 "noise2" sound played when flag is captured...
2088 "noise3" sound played when flag is lost in the field and respawns itself...
2089 "noise4" sound played when flag is dropped by a player...
2090 "noise5" sound played when flag touches the ground... */
2091 void spawnfunc_item_flag_team1()
2093 if(!g_ctf) { remove(self); return; }
2095 ctf_FlagSetup(1, self); // 1 = red
2098 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2099 CTF flag for team two (Blue).
2101 "angle" Angle the flag will point (minus 90 degrees)...
2102 "model" model to use, note this needs red and blue as skins 0 and 1...
2103 "noise" sound played when flag is picked up...
2104 "noise1" sound played when flag is returned by a teammate...
2105 "noise2" sound played when flag is captured...
2106 "noise3" sound played when flag is lost in the field and respawns itself...
2107 "noise4" sound played when flag is dropped by a player...
2108 "noise5" sound played when flag touches the ground... */
2109 void spawnfunc_item_flag_team2()
2111 if(!g_ctf) { remove(self); return; }
2113 ctf_FlagSetup(0, self); // the 0 is misleading, but -- 0 = blue.
2116 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2117 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2118 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.
2120 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2121 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2122 void spawnfunc_ctf_team()
2124 if(!g_ctf) { remove(self); return; }
2126 self.classname = "ctf_team";
2127 self.team = self.cnt + 1;
2130 // compatibility for quake maps
2131 void spawnfunc_team_CTF_redflag() { spawnfunc_item_flag_team1(); }
2132 void spawnfunc_team_CTF_blueflag() { spawnfunc_item_flag_team2(); }
2133 void spawnfunc_team_CTF_redplayer() { spawnfunc_info_player_team1(); }
2134 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2(); }
2135 void spawnfunc_team_CTF_redspawn() { spawnfunc_info_player_team1(); }
2136 void spawnfunc_team_CTF_bluespawn() { spawnfunc_info_player_team2(); }
2144 void ctf_ScoreRules()
2146 ScoreRules_basics(2, SFL_SORT_PRIO_PRIMARY, 0, true);
2147 ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2148 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2149 ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2150 ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0);
2151 ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0);
2152 ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0);
2153 ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2154 ScoreRules_basics_end();
2157 // code from here on is just to support maps that don't have flag and team entities
2158 void ctf_SpawnTeam (string teamname, float teamcolor)
2163 self.classname = "ctf_team";
2164 self.netname = teamname;
2165 self.cnt = teamcolor;
2167 spawnfunc_ctf_team();
2172 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2174 // if no teams are found, spawn defaults
2175 if(find(world, classname, "ctf_team") == world)
2177 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2178 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2179 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2185 void ctf_Initialize()
2187 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2189 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2190 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2191 ctf_captureshield_force = autocvar_g_ctf_shield_force;
2193 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2197 MUTATOR_DEFINITION(gamemode_ctf)
2199 MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2200 MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2201 MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2202 MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2203 MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2204 MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2205 MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2206 MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2207 MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2208 MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2209 MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2210 MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2211 MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2212 MUTATOR_HOOK(HavocBot_ChooseRole, ctf_BotRoles, CBC_ORDER_ANY);
2216 if(time > 1) // game loads at time 1
2217 error("This is a game type and it cannot be added at runtime.");
2221 MUTATOR_ONROLLBACK_OR_REMOVE
2223 // we actually cannot roll back ctf_Initialize here
2224 // BUT: we don't need to! If this gets called, adding always
2230 print("This is a game type and it cannot be removed at runtime.");