1 // ================================================================
2 // Official capture the flag game mode coding, reworked by Samual
3 // Last updated: March 30th, 2012
4 // ================================================================
6 float ctf_ReadScore(string parameter) // make this obsolete
8 //if(g_ctf_win_mode != 2)
9 return cvar(strcat("g_ctf_personal", parameter));
11 // return cvar(strcat("g_ctf_flag", parameter));
14 void ctf_FakeTimeLimit(entity e, float t)
17 WriteByte(MSG_ONE, 3); // svc_updatestat
18 WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
20 WriteCoord(MSG_ONE, autocvar_timelimit);
22 WriteCoord(MSG_ONE, (t + 1) / 60);
25 void ctf_EventLog(string mode, float flagteam, entity actor) // use an alias for easy changing and quick editing later
27 if(autocvar_sv_eventlog)
28 GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
31 string ctf_CaptureRecord(entity flag, entity player)
33 float cap_time, cap_record, success;
34 string cap_message, refername;
36 if((autocvar_g_ctf_captimerecord_always) || (player_count - currentbots))
38 cap_record = ctf_captimerecord;
39 cap_time = (time - flag.ctf_pickuptime);
41 refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
42 refername = ((refername == player.netname) ? "their" : strcat(refername, "^7's"));
44 if(!ctf_captimerecord)
45 { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds"); success = TRUE; }
46 else if(cap_time < cap_record)
47 { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, breaking ", refername, " previous record of ", ftos_decimals(cap_record, 2), " seconds"); success = TRUE; }
49 { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, failing to break ", refername, " record of ", ftos_decimals(cap_record, 2), " seconds"); success = FALSE; }
53 ctf_captimerecord = cap_time;
54 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
55 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
56 write_recordmarker(player, (time - cap_time), cap_time);
63 void ctf_AnnounceStolenFlag(entity flag, entity player)
65 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
66 string verbosename; // holds the name of the player OR no name at all for printing in the centerprints
68 verbosename = ((autocvar_g_ctf_flag_pickup_verbosename) ? strcat(Team_ColorCode(player.team), "(^7", player.netname, Team_ColorCode(player.team), ") ") : "");
70 FOR_EACH_PLAYER(tmp_player)
71 if(tmp_player == player)
72 centerprint(tmp_player, strcat("You got the ", flag.netname, "!"));
73 else if(tmp_player.team == player.team)
74 centerprint(tmp_player, strcat("Your ", Team_ColorCode(player.team), "team mate ", verbosename, "^7got the flag! Protect them!"));
75 else if(tmp_player.team == flag.team)
76 centerprint(tmp_player, strcat("The ", Team_ColorCode(player.team), "enemy ", verbosename, "^7got your flag! Retrieve it!"));
80 // =======================
81 // CaptureShield Functions
82 // =======================
84 float ctf_CaptureShield_CheckStatus(entity p)
88 float players_worseeq, players_total;
90 if(ctf_captureshield_max_ratio <= 0)
93 s = PlayerScore_Add(p, SP_SCORE, 0);
94 if(s >= -ctf_captureshield_min_negscore)
97 players_total = players_worseeq = 0;
102 se = PlayerScore_Add(e, SP_SCORE, 0);
108 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
109 // use this rule here
111 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
117 void ctf_CaptureShield_Update(entity player, float wanted_status)
119 float updated_status = ctf_CaptureShield_CheckStatus(player);
120 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
122 if(updated_status) // TODO csqc notifier for this // Samual: How?
123 Send_CSQC_Centerprint_Generic(player, CPID_CTF_CAPTURESHIELD, "^3You are now ^4shielded^3 from the flag\n^3for ^1too many unsuccessful attempts^3 to capture.\n\n^3Make some defensive scores before trying again.", 5, 0);
125 Send_CSQC_Centerprint_Generic(player, CPID_CTF_CAPTURESHIELD, "^3You are now free.\n\n^3Feel free to ^1try to capture^3 the flag again\n^3if you think you will succeed.", 5, 0);
127 player.ctf_captureshielded = updated_status;
131 float ctf_CaptureShield_Customize()
133 if not(other.ctf_captureshielded) { return FALSE; }
134 if(self.team == other.team) { return FALSE; }
139 void ctf_CaptureShield_Touch()
141 if not(other.ctf_captureshielded) { return; }
142 if(self.team == other.team) { return; }
144 vector mymid = (self.absmin + self.absmax) * 0.5;
145 vector othermid = (other.absmin + other.absmax) * 0.5;
147 Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
148 Send_CSQC_Centerprint_Generic(other, CPID_CTF_CAPTURESHIELD, "^3You are ^4shielded^3 from the flag\n^3for ^1too many unsuccessful attempts^3 to capture.\n\n^3Get some defensive scores before trying again.", 5, 0);
151 void ctf_CaptureShield_Spawn(entity flag)
153 entity shield = spawn();
156 shield.team = self.team;
157 shield.touch = ctf_CaptureShield_Touch;
158 shield.customizeentityforclient = ctf_CaptureShield_Customize;
159 shield.classname = "ctf_captureshield";
160 shield.effects = EF_ADDITIVE;
161 shield.movetype = MOVETYPE_NOCLIP;
162 shield.solid = SOLID_TRIGGER;
163 shield.avelocity = '7 0 11';
166 setorigin(shield, self.origin);
167 setmodel(shield, "models/ctf/shield.md3");
168 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
171 // ====================
172 // Drop/Pass/Throw Code
173 // ====================
175 void ctf_Handle_Failed_Pass(entity flag)
177 entity sender = flag.pass_sender;
179 flag.movetype = MOVETYPE_TOSS;
180 flag.takedamage = DAMAGE_YES;
181 flag.health = flag.max_flag_health;
182 flag.ctf_droptime = time;
183 flag.ctf_dropper = sender;
184 flag.ctf_status = FLAG_DROPPED;
186 // messages and sounds
187 Send_KillNotification(sender.netname, flag.netname, "", INFO_LOSTFLAG, MSG_INFO);
188 sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTN_NONE);
189 ctf_EventLog("dropped", sender.team, sender);
192 PlayerTeamScore_AddScore(sender, -ctf_ReadScore("penalty_drop"));
193 PlayerScore_Add(sender, SP_CTF_DROPS, 1);
196 if(autocvar_g_ctf_flag_dropped_waypoint)
197 WaypointSprite_Spawn("flagdropped", 0, 0, flag, '0 0 64', world, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : sender.team), flag, wps_flagdropped, FALSE, RADARICON_FLAG, '0 0.5 0' + ((flag.team == COLOR_TEAM1) ? '0.75 0 0' : '0 0 0.75'));
199 if(autocvar_g_ctf_flag_returntime || (autocvar_g_ctf_flag_take_damage && autocvar_g_ctf_flag_health))
201 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
202 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
205 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
207 flag.pass_sender = world;
208 flag.pass_target = world;
211 void ctf_Handle_Retrieve(entity flag, entity player)
213 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
214 entity sender = flag.pass_sender;
216 // transfer flag to player
217 flag.ctf_carrier = player;
219 flag.owner.flagcarried = flag;
222 setattachment(flag, player, "");
223 setorigin(flag, FLAG_CARRY_OFFSET);
224 flag.movetype = MOVETYPE_NONE;
225 flag.takedamage = DAMAGE_NO;
226 flag.solid = SOLID_NOT;
227 flag.ctf_carrier = player;
228 flag.ctf_status = FLAG_CARRY;
230 // messages and sounds
231 sound(player, CH_TRIGGER, "keepaway/respawn.wav", VOL_BASE, ATTN_NORM);
232 ctf_EventLog("recieve", flag.team, player);
233 FOR_EACH_PLAYER(tmp_player)
234 if(tmp_player == sender)
235 centerprint(tmp_player, strcat("You passed the ", flag.netname, " to ", player.netname));
236 else if(tmp_player == player)
237 centerprint(tmp_player, strcat("You recieved the ", flag.netname, " from ", sender.netname));
238 else if(tmp_player.team == sender.team)
239 centerprint(tmp_player, strcat(sender.netname, " passed the ", flag.netname, " to ", player.netname));
241 // create new waypoint
242 WaypointSprite_Spawn("flagcarrier", 0, 0, player, '0 0 64', world, player.team, player, wps_flagcarrier, FALSE, RADARICON_FLAG, '1 1 0');
243 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
244 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
245 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, '1 1 0');
247 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
248 player.throw_antispam = sender.throw_antispam;
250 flag.pass_sender = world;
251 flag.pass_target = world;
254 void ctf_Handle_Throw(entity player, entity reciever, float droptype)
256 entity flag = player.flagcarried;
258 if(!flag) { return; }
259 if((droptype == DROPTYPE_PASS) && !reciever) { return; }
261 //if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
264 setattachment(flag, world, "");
265 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
266 flag.owner.flagcarried = world;
268 flag.solid = SOLID_TRIGGER;
274 vector targ_origin = (0.5 * (reciever.absmin + reciever.absmax));
275 flag.velocity = W_CalculateProjectileVelocity(player.velocity, ('0 0 200' + (normalize(targ_origin - player.origin) * autocvar_g_ctf_throw_velocity)), FALSE);
281 makevectors((player.v_angle_y * '0 1 0') + (player.v_angle_x * '0.5 0 0'));
282 flag.velocity = W_CalculateProjectileVelocity(player.velocity, ('0 0 200' + (v_forward * autocvar_g_ctf_throw_velocity)), FALSE);
289 flag.velocity = ('0 0 200' + ('0 100 0' * crandom()) + ('100 0 0' * crandom()));
299 flag.movetype = MOVETYPE_FLY;
300 flag.takedamage = DAMAGE_NO;
301 flag.pass_sender = player;
302 flag.pass_target = reciever;
303 flag.ctf_status = FLAG_PASSING;
306 sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTN_NORM);
307 ctf_EventLog("pass", flag.team, player);
308 te_lightning2(world, reciever.origin, player.origin);
317 flag.movetype = MOVETYPE_TOSS;
318 flag.takedamage = DAMAGE_YES;
319 flag.health = flag.max_flag_health;
320 flag.ctf_droptime = time;
321 flag.ctf_dropper = player;
322 flag.ctf_status = FLAG_DROPPED;
324 // messages and sounds
325 Send_KillNotification(player.netname, flag.netname, "", INFO_LOSTFLAG, MSG_INFO);
326 sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTN_NONE);
327 ctf_EventLog("dropped", player.team, player);
330 PlayerTeamScore_AddScore(player, -ctf_ReadScore("penalty_drop"));
331 PlayerScore_Add(player, SP_CTF_DROPS, 1);
334 if(autocvar_g_ctf_flag_dropped_waypoint)
335 WaypointSprite_Spawn("flagdropped", 0, 0, flag, '0 0 64', world, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, FALSE, RADARICON_FLAG, '0 0.5 0' + ((flag.team == COLOR_TEAM1) ? '0.75 0 0' : '0 0 0.75')); // (COLOR_TEAM1 + COLOR_TEAM2 - flag.team)
337 if(autocvar_g_ctf_flag_returntime || (autocvar_g_ctf_flag_take_damage && autocvar_g_ctf_flag_health))
339 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
340 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
346 // kill old waypointsprite
347 WaypointSprite_Ping(player.wps_flagcarrier);
348 WaypointSprite_Kill(player.wps_flagcarrier);
351 //ctf_CaptureShield_Update(player, 0); // shield only
359 void ctf_Handle_Dropped_Capture(entity flag, entity enemy_flag)
363 entity player = enemy_flag.ctf_dropper;
365 if not(player) { return; } // without someone to give the reward to, we can't possibly cap
367 // messages and sounds
368 Send_KillNotification(player.netname, enemy_flag.netname, ctf_CaptureRecord(enemy_flag, player), INFO_CAPTUREFLAG, MSG_INFO);
369 sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE);
370 ctf_EventLog("droppedcapture", enemy_flag.team, player);
373 PlayerTeamScore_AddScore(player, ctf_ReadScore("score_capture"));
374 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
377 if(autocvar_g_ctf_flag_capture_effects)
379 pointparticles(particleeffectnum((player.team == COLOR_TEAM1) ? "red_ground_quake" : "blue_ground_quake"), flag.origin, '0 0 0', 1);
380 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
384 ctf_RespawnFlag(enemy_flag);
387 void ctf_Handle_Capture(entity flag, entity player)
389 // messages and sounds
390 Send_KillNotification(player.netname, player.flagcarried.netname, ctf_CaptureRecord(flag, player), INFO_CAPTUREFLAG, MSG_INFO);
391 sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE);
392 ctf_EventLog("capture", player.flagcarried.team, player);
395 PlayerTeamScore_AddScore(player, ctf_ReadScore("score_capture"));
396 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
399 if(autocvar_g_ctf_flag_capture_effects)
401 pointparticles(particleeffectnum((player.team == COLOR_TEAM1) ? "red_ground_quake" : "blue_ground_quake"), flag.origin, '0 0 0', 1);
402 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
406 WaypointSprite_Kill(player.wps_flagcarrier);
409 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
411 ctf_RespawnFlag(player.flagcarried);
414 void ctf_Handle_Return(entity flag, entity player)
416 // messages and sounds
417 //centerprint(player, strcat("You returned ", flag.netname));
418 Send_KillNotification (player.netname, flag.netname, "", INFO_RETURNFLAG, MSG_INFO);
419 sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTN_NONE);
420 ctf_EventLog("return", flag.team, player);
423 PlayerTeamScore_AddScore(player, ctf_ReadScore("score_return")); // reward for return
424 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
426 TeamScore_AddToTeam(((flag.team == COLOR_TEAM1) ? COLOR_TEAM2 : COLOR_TEAM1), ST_SCORE, -ctf_ReadScore("penalty_returned")); // punish the team who was last carrying it
427 FOR_EACH_PLAYER(player) if(player == flag.ctf_dropper) // punish the player who dropped the flag
429 PlayerScore_Add(player, SP_SCORE, -ctf_ReadScore("penalty_returned"));
430 ctf_CaptureShield_Update(player, 0); // shield only
434 ctf_RespawnFlag(flag);
437 void ctf_Handle_Pickup_Base(entity flag, entity player)
439 // attach the flag to the player
441 player.flagcarried = flag;
442 setattachment(flag, player, "");
443 setorigin(flag, FLAG_CARRY_OFFSET);
446 flag.movetype = MOVETYPE_NONE;
447 flag.takedamage = DAMAGE_NO;
448 flag.solid = SOLID_NOT;
449 flag.angles = '0 0 0';
450 flag.ctf_pickuptime = time; // used for timing runs
451 flag.ctf_carrier = player;
452 flag.ctf_status = FLAG_CARRY;
454 // messages and sounds
455 Send_KillNotification (player.netname, flag.netname, "", INFO_GOTFLAG, MSG_INFO);
456 sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE);
457 ctf_EventLog("steal", flag.team, player);
458 ctf_AnnounceStolenFlag(flag, player);
461 PlayerTeamScore_AddScore(player, ctf_ReadScore("score_pickup_base"));
462 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
465 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
466 if((player.speedrunning) && (ctf_captimerecord))
467 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
470 if (autocvar_g_ctf_flag_pickup_effects)
472 pointparticles(particleeffectnum("smoke_ring"), 0.5 * (flag.absmin + flag.absmax), '0 0 0', 1);
476 WaypointSprite_Spawn("flagcarrier", 0, 0, player, '0 0 64', world, player.team, player, wps_flagcarrier, FALSE, RADARICON_FLAG, '1 1 0'); // (COLOR_TEAM1 + COLOR_TEAM2 - flag.team)
477 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
478 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
479 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, '1 1 0');
480 WaypointSprite_Ping(player.wps_flagcarrier);
483 void ctf_Handle_Pickup_Dropped(entity flag, entity player)
486 float returnscore = (autocvar_g_ctf_flag_returntime ? bound(0, ((flag.ctf_droptime + autocvar_g_ctf_flag_returntime) - time) / autocvar_g_ctf_flag_returntime, 1) : 1);
488 // attach the flag to the player
490 player.flagcarried = flag;
491 setattachment(flag, player, "");
492 setorigin(flag, FLAG_CARRY_OFFSET);
495 flag.movetype = MOVETYPE_NONE;
496 flag.takedamage = DAMAGE_NO;
497 flag.health = flag.max_flag_health;
498 flag.solid = SOLID_NOT;
499 flag.angles = '0 0 0';
500 //flag.ctf_pickuptime = time; // don't update pickuptime since this isn't a real steal.
501 flag.ctf_carrier = player;
502 flag.ctf_status = FLAG_CARRY;
504 // messages and sounds
505 Send_KillNotification(player.netname, flag.netname, "", INFO_PICKUPFLAG, MSG_INFO);
506 sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE);
507 ctf_EventLog("pickup", flag.team, player);
508 ctf_AnnounceStolenFlag(flag, player);
511 returnscore = floor((ctf_ReadScore("score_pickup_dropped_late") * (1-returnscore) + ctf_ReadScore("score_pickup_dropped_early") * returnscore) + 0.5);
512 print("score is ", ftos(returnscore), "\n");
513 PlayerTeamScore_AddScore(player, returnscore);
514 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
517 if(autocvar_g_ctf_flag_pickup_effects) // field pickup effect
519 pointparticles(particleeffectnum("smoke_ring"), 0.5 * (flag.absmin + flag.absmax), '0 0 0', 1);
523 WaypointSprite_Kill(flag.wps_flagdropped);
524 WaypointSprite_Spawn("flagcarrier", 0, 0, player, '0 0 64', world, player.team, player, wps_flagcarrier, FALSE, RADARICON_FLAG, '1 1 0'); // (COLOR_TEAM1 + COLOR_TEAM2 - flag.team)
525 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
526 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
527 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, '1 1 0');
528 WaypointSprite_Ping(player.wps_flagcarrier);
532 // ===================
533 // Main Flag Functions
534 // ===================
536 void ctf_CheckFlagReturn(entity flag)
538 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
540 if((flag.health <= 0) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_returntime))
542 bprint("The ", flag.netname, " has returned to base\n");
543 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTN_NONE);
544 ctf_EventLog("returned", flag.team, world);
545 ctf_RespawnFlag(flag);
549 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
551 if(ITEM_DAMAGE_NEEDKILL(deathtype))
553 // automatically kill the flag and return it
555 ctf_CheckFlagReturn(self);
558 if(autocvar_g_ctf_flag_take_damage)
560 self.health = self.health - damage;
561 ctf_CheckFlagReturn(self);
570 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
573 if(self == ctf_worldflaglist) // only for the first flag
574 FOR_EACH_CLIENT(tmp_entity)
575 ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
578 if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
579 dprint("wtf the flag got squashed?\n");
580 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
581 if(!trace_startsolid) // can we resize it without getting stuck?
582 setsize(self, FLAG_MIN, FLAG_MAX); }
585 switch(self.ctf_status)
589 if(autocvar_g_ctf_dropped_capture_radius)
591 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
592 if(tmp_entity.ctf_status == FLAG_DROPPED)
593 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
594 ctf_Handle_Dropped_Capture(self, tmp_entity);
601 if(autocvar_g_ctf_flag_returntime)
603 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_returntime) * FLAG_THINKRATE);
604 ctf_CheckFlagReturn(self);
611 if((self.owner) && (self.speedrunning) && (ctf_captimerecord) && (time >= self.ctf_pickuptime + ctf_captimerecord))
613 bprint("The ", self.netname, " became impatient after ", ftos_decimals(ctf_captimerecord, 2), " seconds and returned itself\n");
614 sound(self, CH_TRIGGER, self.snd_flag_respawn, VOL_BASE, ATTN_NONE);
615 ctf_EventLog("returned", self.team, world);
616 ctf_RespawnFlag(tmp_entity);
620 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
629 traceline(self.origin, self.pass_target.origin, MOVE_NOMONSTERS, self);
631 vector targ_origin = (0.5 * (self.pass_target.absmin + self.pass_target.absmax));
633 if((self.pass_target.deadflag != DEAD_NO)
634 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
635 || ((trace_fraction < 1) && (trace_ent != self.pass_target)))
637 ctf_Handle_Failed_Pass(self);
639 else // still a viable target, go for it
641 vector desired_direction = normalize(targ_origin - self.origin);
642 vector current_direction = normalize(self.velocity);
644 self.velocity = (normalize(current_direction + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_throw_velocity);
649 default: // this should never happen
651 dprint("ctf_FlagThink(): Flag exists with no status?\n");
659 if(gameover) { return; }
660 if(!self) { return; }
661 if(other.deadflag != DEAD_NO) { return; }
662 if(ITEM_TOUCH_NEEDKILL())
664 // automatically kill the flag and return it
666 ctf_CheckFlagReturn(self);
669 if(other.classname != "player") // The flag just touched an object, most likely the world
671 if(time > self.wait) // if we haven't in a while, play a sound/effect
673 pointparticles(particleeffectnum("kaball_sparks"), self.origin, '0 0 0', 1);
674 sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTN_NORM);
679 switch(self.ctf_status)
683 if((other.team == self.team) && (other.flagcarried) && (other.flagcarried.team != self.team))
684 ctf_Handle_Capture(self, other); // other just captured the enemies flag to his base
685 else if((other.team != self.team) && (!other.flagcarried) && (!other.ctf_captureshielded))
686 ctf_Handle_Pickup_Base(self, other); // other just stole the enemies flag
692 if(other.team == self.team)
693 ctf_Handle_Return(self, other); // other just returned his own flag
694 else if((!other.flagcarried) && ((other != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
695 ctf_Handle_Pickup_Dropped(self, other); // other just picked up a dropped enemy flag
701 dprint("Someone touched a flag even though it was being carried?\n");
707 if((other.classname == "player") && (other.deadflag == DEAD_NO) && (other != self.pass_sender))
709 if(IsDifferentTeam(other, self.pass_sender))
710 ctf_Handle_Return(self, other);
712 ctf_Handle_Retrieve(self, other);
717 default: // this should never happen
719 dprint("Touch: Flag exists with no status?\n");
724 self.wait = time + FLAG_TOUCHRATE;
727 void ctf_RespawnFlag(entity flag)
729 // reset the player (if there is one)
730 if((flag.owner) && (flag.owner.flagcarried == flag))
732 WaypointSprite_Kill(flag.wps_flagcarrier);
733 flag.owner.flagcarried = world;
735 if(flag.speedrunning)
736 ctf_FakeTimeLimit(flag.owner, -1);
739 if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
740 { WaypointSprite_Kill(flag.wps_flagdropped); }
743 setattachment(flag, world, "");
744 setorigin(flag, flag.ctf_spawnorigin);
746 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
747 flag.takedamage = DAMAGE_NO;
748 flag.health = flag.max_flag_health;
749 flag.solid = SOLID_TRIGGER;
750 flag.velocity = '0 0 0';
751 flag.angles = flag.mangle;
752 flag.flags = FL_ITEM | FL_NOTARGET;
754 flag.ctf_status = FLAG_BASE;
756 flag.pass_sender = world;
757 flag.pass_target = world;
758 flag.ctf_carrier = world;
759 flag.ctf_dropper = world;
760 flag.ctf_pickuptime = 0;
761 flag.ctf_droptime = 0;
767 if(self.owner.classname == "player")
768 ctf_Handle_Throw(self.owner, world, DROPTYPE_DROP);
770 ctf_RespawnFlag(self);
773 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
776 float teamnumber = ((self.team == COLOR_TEAM1) ? TRUE : FALSE); // if we were originally 1, this will become 0. If we were originally 0, this will become 1.
779 waypoint_spawnforitem_force(self, self.origin);
780 self.nearestwaypointtimeout = 0; // activate waypointing again
781 self.bot_basewaypoint = self.nearestwaypoint;
784 WaypointSprite_SpawnFixed(((teamnumber) ? "redbase" : "bluebase"), self.origin + '0 0 64', self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2) - 1, FALSE));
785 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2) - 1, FALSE));
787 // captureshield setup
788 ctf_CaptureShield_Spawn(self);
791 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
794 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.
795 self = flag; // for later usage with droptofloor()
798 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
799 ctf_worldflaglist = flag;
801 setattachment(flag, world, "");
803 flag.netname = ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag");
804 flag.team = ((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2); // COLOR_TEAM1: color 4 team (red) - COLOR_TEAM2: color 13 team (blue)
805 flag.items = ((teamnumber) ? IT_KEY2 : IT_KEY1); // IT_KEY2: gold key (redish enough) - IT_KEY1: silver key (bluish enough)
806 flag.classname = "item_flag_team";
807 flag.target = "###item###"; // wut?
808 flag.flags = FL_ITEM | FL_NOTARGET;
809 flag.solid = SOLID_TRIGGER;
810 flag.takedamage = DAMAGE_NO;
811 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
812 flag.max_flag_health = ((autocvar_g_ctf_flag_take_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
813 flag.health = flag.max_flag_health;
814 flag.event_damage = ctf_FlagDamage;
815 flag.pushable = TRUE;
816 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
817 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
818 flag.velocity = '0 0 0';
819 flag.mangle = flag.angles;
820 flag.reset = ctf_Reset;
821 flag.touch = ctf_FlagTouch;
822 flag.think = ctf_FlagThink;
823 flag.nextthink = time + FLAG_THINKRATE;
824 flag.ctf_status = FLAG_BASE;
826 if(!flag.model) { flag.model = ((teamnumber) ? autocvar_g_ctf_flag_red_model : autocvar_g_ctf_flag_blue_model); }
827 if(!flag.scale) { flag.scale = FLAG_SCALE; }
828 if(!flag.skin) { flag.skin = ((teamnumber) ? autocvar_g_ctf_flag_red_skin : autocvar_g_ctf_flag_blue_skin); }
831 if(!flag.snd_flag_taken) { flag.snd_flag_taken = ((teamnumber) ? "ctf/red_taken.wav" : "ctf/blue_taken.wav"); }
832 if(!flag.snd_flag_returned) { flag.snd_flag_returned = ((teamnumber) ? "ctf/red_returned.wav" : "ctf/blue_returned.wav"); }
833 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
834 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.
835 if(!flag.snd_flag_dropped) { flag.snd_flag_dropped = ((teamnumber) ? "ctf/red_dropped.wav" : "ctf/blue_dropped.wav"); }
836 if(!flag.snd_flag_touch) { flag.snd_flag_touch = "keepaway/touch.wav"; } // again has no team-based sound // FIXME
839 precache_sound(flag.snd_flag_taken);
840 precache_sound(flag.snd_flag_returned);
841 precache_sound(flag.snd_flag_capture);
842 precache_sound(flag.snd_flag_respawn);
843 precache_sound(flag.snd_flag_dropped);
844 precache_sound(flag.snd_flag_touch);
845 precache_model(flag.model);
846 precache_model("models/ctf/shield.md3");
847 precache_model("models/ctf/shockwavetransring.md3");
850 setmodel(flag, flag.model); // precision set below
851 setsize(flag, FLAG_MIN, FLAG_MAX);
852 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
854 if(autocvar_g_ctf_flag_glowtrails)
856 flag.glow_color = ((teamnumber) ? 251 : 210); // 251: red - 210: blue
861 flag.effects |= EF_LOWPRECISION;
862 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
863 if(autocvar_g_ctf_dynamiclights) { flag.effects |= ((teamnumber) ? EF_RED : EF_BLUE); }
866 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
868 flag.dropped_origin = flag.origin;
870 flag.movetype = MOVETYPE_NONE;
872 else // drop to floor, automatically find a platform and set that as spawn origin
874 flag.noalign = FALSE;
877 flag.movetype = MOVETYPE_TOSS;
880 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
888 MUTATOR_HOOKFUNCTION(ctf_HookedDrop)
890 if(self.flagcarried) { ctf_Handle_Throw(self, world, DROPTYPE_DROP); }
894 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
898 // initially clear items so they can be set as necessary later.
899 self.items &~= (IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST
900 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST | IT_CTF_SHIELDED);
902 // scan through all the flags and notify the client about them
903 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
905 if(flag.ctf_status == FLAG_CARRY)
906 if(flag.owner == self)
907 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
909 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
910 else if(flag.ctf_status == FLAG_DROPPED)
911 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
914 // item for stopping players from capturing the flag too often
915 if(self.ctf_captureshielded)
916 self.items |= IT_CTF_SHIELDED;
918 // update the health of the flag carrier waypointsprite
919 if(self.wps_flagcarrier)
920 WaypointSprite_UpdateHealth(self.wps_flagcarrier, self.health);
925 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
927 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
929 if(frag_target == frag_attacker) // damage done to yourself
931 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
932 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
934 else // damage done everyone else
936 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
937 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
943 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
946 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
949 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
951 entity player = self;
953 if(time > player.throw_antispam)
955 // pass the flag to a team mate
956 if(autocvar_g_ctf_allow_pass)
958 entity head, closest_target;
959 head = findradius(player.origin, autocvar_g_ctf_pass_radius);
961 while(head) // find the closest acceptable target to pass to
963 if(head.classname == "player" && head.deadflag == DEAD_NO)
964 if(head != player && !IsDifferentTeam(head, player))
965 if(!player.speedrunning && !head.speedrunning)
967 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
969 if(clienttype(head) == CLIENTTYPE_BOT)
971 centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname));
972 ctf_Handle_Throw(head, player, DROPTYPE_PASS);
976 centerprint(head, strcat(player.netname, " requests you to pass the ", head.flagcarried.netname));
977 centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname));
979 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
982 else if(player.flagcarried)
984 if(closest_target) { if(vlen(player.origin - head.origin) < vlen(player.origin - closest_target.origin)) { closest_target = head; } }
985 else { closest_target = head; }
991 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROPTYPE_PASS); return 0; }
994 // throw the flag in front of you
995 if(autocvar_g_ctf_allow_drop && player.flagcarried && !player.speedrunning)
996 { ctf_Handle_Throw(player, world, DROPTYPE_THROW); }
1006 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
1007 CTF Starting point for a player in team one (Red).
1008 Keys: "angle" viewing angle when spawning. */
1009 void spawnfunc_info_player_team1()
1011 if(g_assault) { remove(self); return; }
1013 self.team = COLOR_TEAM1; // red
1014 spawnfunc_info_player_deathmatch();
1018 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
1019 CTF Starting point for a player in team two (Blue).
1020 Keys: "angle" viewing angle when spawning. */
1021 void spawnfunc_info_player_team2()
1023 if(g_assault) { remove(self); return; }
1025 self.team = COLOR_TEAM2; // blue
1026 spawnfunc_info_player_deathmatch();
1029 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
1030 CTF Starting point for a player in team three (Yellow).
1031 Keys: "angle" viewing angle when spawning. */
1032 void spawnfunc_info_player_team3()
1034 if(g_assault) { remove(self); return; }
1036 self.team = COLOR_TEAM3; // yellow
1037 spawnfunc_info_player_deathmatch();
1041 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
1042 CTF Starting point for a player in team four (Purple).
1043 Keys: "angle" viewing angle when spawning. */
1044 void spawnfunc_info_player_team4()
1046 if(g_assault) { remove(self); return; }
1048 self.team = COLOR_TEAM4; // purple
1049 spawnfunc_info_player_deathmatch();
1052 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
1053 CTF flag for team one (Red).
1055 "angle" Angle the flag will point (minus 90 degrees)...
1056 "model" model to use, note this needs red and blue as skins 0 and 1...
1057 "noise" sound played when flag is picked up...
1058 "noise1" sound played when flag is returned by a teammate...
1059 "noise2" sound played when flag is captured...
1060 "noise3" sound played when flag is lost in the field and respawns itself...
1061 "noise4" sound played when flag is dropped by a player...
1062 "noise5" sound played when flag touches the ground... */
1063 void spawnfunc_item_flag_team1()
1065 if(!g_ctf) { remove(self); return; }
1067 ctf_FlagSetup(1, self); // 1 = red
1070 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
1071 CTF flag for team two (Blue).
1073 "angle" Angle the flag will point (minus 90 degrees)...
1074 "model" model to use, note this needs red and blue as skins 0 and 1...
1075 "noise" sound played when flag is picked up...
1076 "noise1" sound played when flag is returned by a teammate...
1077 "noise2" sound played when flag is captured...
1078 "noise3" sound played when flag is lost in the field and respawns itself...
1079 "noise4" sound played when flag is dropped by a player...
1080 "noise5" sound played when flag touches the ground... */
1081 void spawnfunc_item_flag_team2()
1083 if(!g_ctf) { remove(self); return; }
1085 ctf_FlagSetup(0, self); // the 0 is misleading, but -- 0 = blue.
1088 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
1089 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
1090 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.
1092 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
1093 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
1094 void spawnfunc_ctf_team()
1096 if(!g_ctf) { remove(self); return; }
1098 self.classname = "ctf_team";
1099 self.team = self.cnt + 1;
1107 // code from here on is just to support maps that don't have flag and team entities
1108 void ctf_SpawnTeam (string teamname, float teamcolor)
1113 self.classname = "ctf_team";
1114 self.netname = teamname;
1115 self.cnt = teamcolor;
1117 spawnfunc_ctf_team();
1122 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
1124 // if no teams are found, spawn defaults
1125 if(find(world, classname, "ctf_team") == world)
1127 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
1128 ctf_SpawnTeam("Red", COLOR_TEAM1 - 1);
1129 ctf_SpawnTeam("Blue", COLOR_TEAM2 - 1);
1135 void ctf_Initialize()
1137 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
1139 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
1140 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
1141 ctf_captureshield_force = autocvar_g_ctf_shield_force;
1143 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
1147 MUTATOR_DEFINITION(gamemode_ctf)
1149 MUTATOR_HOOK(MakePlayerObserver, ctf_HookedDrop, CBC_ORDER_ANY);
1150 MUTATOR_HOOK(ClientDisconnect, ctf_HookedDrop, CBC_ORDER_ANY);
1151 MUTATOR_HOOK(PlayerDies, ctf_HookedDrop, CBC_ORDER_ANY);
1152 MUTATOR_HOOK(PortalTeleport, ctf_HookedDrop, CBC_ORDER_ANY);
1153 MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
1154 MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
1155 MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
1156 MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
1160 if(time > 1) // game loads at time 1
1161 error("This is a game type and it cannot be added at runtime.");
1169 error("This is a game type and it cannot be removed at runtime.");