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))) : "")));
32 // =======================
33 // CaptureShield Functions
34 // =======================
36 float ctf_CaptureShield_CheckStatus(entity p)
40 float players_worseeq, players_total;
42 if(ctf_captureshield_max_ratio <= 0)
45 s = PlayerScore_Add(p, SP_SCORE, 0);
46 if(s >= -ctf_captureshield_min_negscore)
49 players_total = players_worseeq = 0;
54 se = PlayerScore_Add(e, SP_SCORE, 0);
60 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
63 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
69 void ctf_CaptureShield_Update(entity player, float wanted_status)
71 float updated_status = ctf_CaptureShield_CheckStatus(player);
72 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
74 if(updated_status) // TODO csqc notifier for this // Samual: How?
75 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);
77 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);
79 player.ctf_captureshielded = updated_status;
83 float ctf_CaptureShield_Customize()
85 if not(other.ctf_captureshielded) { return FALSE; }
86 if(self.team == other.team) { return FALSE; }
91 void ctf_CaptureShield_Touch()
93 if not(other.ctf_captureshielded) { return; }
94 if(self.team == other.team) { return; }
96 vector mymid = (self.absmin + self.absmax) * 0.5;
97 vector othermid = (other.absmin + other.absmax) * 0.5;
99 Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
100 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);
103 void ctf_CaptureShield_Spawn(entity flag)
105 entity shield = spawn();
108 shield.team = self.team;
109 shield.touch = ctf_CaptureShield_Touch;
110 shield.customizeentityforclient = ctf_CaptureShield_Customize;
111 shield.classname = "ctf_captureshield";
112 shield.effects = EF_ADDITIVE;
113 shield.movetype = MOVETYPE_NOCLIP;
114 shield.solid = SOLID_TRIGGER;
115 shield.avelocity = '7 0 11';
118 setorigin(shield, self.origin);
119 setmodel(shield, "models/ctf/shield.md3");
120 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
128 void ctf_Handle_Pass(entity player, entity reciever)
130 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
131 entity flag = player.flagcarried;
132 if(!flag) { return; }
133 if(time < flag.wait) { return; } // antispam of passing
136 player.flagcarried = world;
137 WaypointSprite_Ping(player.wps_flagcarrier);
138 WaypointSprite_Kill(player.wps_flagcarrier);
140 // transfer flag to reciever
141 flag.owner = reciever;
142 flag.owner.flagcarried = flag;
143 flag.ctf_pickupper = reciever;
144 setattachment(flag, reciever, "");
145 setorigin(flag, FLAG_CARRY_OFFSET);
147 // messages and sounds
148 sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTN_NORM);
149 ctf_EventLog("pass", flag.team, player);
150 ctf_EventLog("recieve", flag.team, reciever);
151 FOR_EACH_PLAYER(tmp_player)
152 if(tmp_player == player)
153 centerprint(tmp_player, strcat("You passed the ", flag.netname, " to ", reciever.netname));
154 else if(tmp_player == reciever)
155 centerprint(tmp_player, strcat("You recieved the ", flag.netname, " from ", player.netname));
156 else if(tmp_player.team == player.team)
157 centerprint(tmp_player, strcat(player.netname, " passed the ", flag.netname, " to ", reciever.netname));
160 te_lightning2(world, reciever.origin, player.origin);
162 // create new waypoint
163 WaypointSprite_Spawn("flagcarrier", 0, 0, reciever, '0 0 64', world, reciever.team, reciever, wps_flagcarrier, FALSE, RADARICON_FLAG, '1 1 0');
164 WaypointSprite_UpdateMaxHealth(reciever.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
165 WaypointSprite_UpdateHealth(reciever.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(reciever.health, reciever.armorvalue, autocvar_g_balance_armor_blockpercent));
166 WaypointSprite_UpdateTeamRadar(reciever.wps_flagcarrier, RADARICON_FLAGCARRIER, '1 1 0');
168 flag.wait = time + autocvar_g_ctf_pass_wait;
171 void ctf_Handle_Drop(entity player, float droptype)
173 entity flag = player.flagcarried;
174 if(!flag) { return; }
176 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
179 setattachment(flag, world, "");
180 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
181 flag.owner.flagcarried = world;
183 flag.movetype = MOVETYPE_TOSS;
184 flag.solid = SOLID_TRIGGER;
185 flag.takedamage = DAMAGE_YES;
186 flag.health = flag.max_flag_health;
190 case DROPTYPE_THROWN:
192 makevectors((player.v_angle_y * '0 1 0') + (player.v_angle_x * '0.5 0 0'));
193 flag.velocity = W_CalculateProjectileVelocity(player.velocity, ('0 0 200' + (v_forward * autocvar_g_ctf_throw_velocity)), FALSE);
198 case DROPTYPE_NORMAL:
200 flag.velocity = ('0 0 200' + ('0 100 0' * crandom()) + ('100 0 0' * crandom()));
205 flag.ctf_droptime = time;
206 flag.ctf_dropper = player;
207 flag.ctf_status = FLAG_DROPPED;
209 // messages and sounds
210 Send_KillNotification(player.netname, flag.netname, "", INFO_LOSTFLAG, MSG_INFO);
211 sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTN_NONE);
212 ctf_EventLog("dropped", player.team, player);
215 PlayerTeamScore_AddScore(player, -ctf_ReadScore("penalty_drop"));
216 PlayerScore_Add(player, SP_CTF_DROPS, 1);
219 if(autocvar_g_ctf_flag_dropped_waypoint)
220 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)
222 WaypointSprite_Ping(player.wps_flagcarrier);
223 WaypointSprite_Kill(player.wps_flagcarrier);
225 if(autocvar_g_ctf_flag_returntime || (autocvar_g_ctf_flag_take_damage && autocvar_g_ctf_flag_health))
227 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
228 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
232 ctf_CaptureShield_Update(player, 0); // shield only
234 // check if the flag will fall off the map
235 trace_startsolid = FALSE;
236 tracebox(flag.origin, flag.mins, flag.maxs, flag.origin, TRUE, flag);
238 dprint("FLAG FALLTHROUGH will happen SOON\n");
241 void ctf_Handle_Dropped_Capture(entity flag, entity enemy_flag)
244 float cap_time, cap_record, success;
245 string cap_message, refername;
247 entity player = enemy_flag.ctf_dropper;
249 if not(player) { return; } // without someone to give the reward to, we can't possibly cap
252 if((autocvar_g_ctf_captimerecord_always) || (player_count - currentbots)) {
253 cap_record = ctf_captimerecord;
254 cap_time = (time - enemy_flag.ctf_pickuptime);
256 refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
257 refername = ((refername == player.netname) ? "their" : strcat(refername, "^7's"));
259 if(!ctf_captimerecord)
260 { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds"); success = TRUE; }
261 else if(cap_time < cap_record)
262 { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, breaking ", refername, " previous record of ", ftos_decimals(cap_record, 2), " seconds"); success = TRUE; }
264 { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, failing to break ", refername, " record of ", ftos_decimals(cap_record, 2), " seconds"); success = FALSE; }
267 ctf_captimerecord = cap_time;
268 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
269 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
270 write_recordmarker(player, (time - cap_time), cap_time); } }
272 // messages and sounds
273 Send_KillNotification(player.netname, enemy_flag.netname, cap_message, INFO_CAPTUREFLAG, MSG_INFO);
274 sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE); // "ctf/*_capture.wav"
275 ctf_EventLog("droppedcapture", enemy_flag.team, player);
278 PlayerTeamScore_AddScore(player, ctf_ReadScore("score_capture"));
279 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
282 if (autocvar_g_ctf_flag_capture_effects)
284 pointparticles(particleeffectnum((player.team == COLOR_TEAM1) ? "red_ground_quake" : "blue_ground_quake"), flag.origin, '0 0 0', 1);
285 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
289 ctf_RespawnFlag(enemy_flag);
292 void ctf_Handle_Capture(entity flag, entity player)
295 float cap_time, cap_record, success;
296 string cap_message, refername;
299 if((autocvar_g_ctf_captimerecord_always) || (player_count - currentbots)) {
300 cap_record = ctf_captimerecord;
301 cap_time = (time - player.flagcarried.ctf_pickuptime);
303 refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
304 refername = ((refername == player.netname) ? "their" : strcat(refername, "^7's"));
306 if(!ctf_captimerecord)
307 { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds"); success = TRUE; }
308 else if(cap_time < cap_record)
309 { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, breaking ", refername, " previous record of ", ftos_decimals(cap_record, 2), " seconds"); success = TRUE; }
311 { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, failing to break ", refername, " record of ", ftos_decimals(cap_record, 2), " seconds"); success = FALSE; }
314 ctf_captimerecord = cap_time;
315 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
316 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
317 write_recordmarker(player, (time - cap_time), cap_time); } }
319 // messages and sounds
320 Send_KillNotification(player.netname, player.flagcarried.netname, cap_message, INFO_CAPTUREFLAG, MSG_INFO);
321 sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE); // "ctf/*_capture.wav"
322 ctf_EventLog("capture", player.flagcarried.team, player);
325 PlayerTeamScore_AddScore(player, ctf_ReadScore("score_capture"));
326 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
329 if (autocvar_g_ctf_flag_capture_effects)
331 pointparticles(particleeffectnum((player.team == COLOR_TEAM1) ? "red_ground_quake" : "blue_ground_quake"), flag.origin, '0 0 0', 1);
332 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
336 WaypointSprite_Kill(player.wps_flagcarrier);
339 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
341 ctf_RespawnFlag(player.flagcarried);
344 void ctf_Handle_Return(entity flag, entity player)
346 // messages and sounds
347 //centerprint(player, strcat("You returned ", flag.netname));
348 Send_KillNotification (player.netname, flag.netname, "", INFO_RETURNFLAG, MSG_INFO);
349 sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTN_NONE);
350 ctf_EventLog("return", flag.team, player);
353 PlayerTeamScore_AddScore(player, ctf_ReadScore("score_return")); // reward for return
354 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
356 TeamScore_AddToTeam(((flag.team == COLOR_TEAM1) ? COLOR_TEAM2 : COLOR_TEAM1), ST_SCORE, -ctf_ReadScore("penalty_returned")); // punish the team who was last carrying it
357 FOR_EACH_PLAYER(player) if(player == flag.ctf_dropper) // punish the player who dropped the flag
359 PlayerScore_Add(player, SP_SCORE, -ctf_ReadScore("penalty_returned"));
360 ctf_CaptureShield_Update(player, 0); // shield only
364 ctf_RespawnFlag(flag);
367 void ctf_Handle_Pickup_Base(entity flag, entity player)
369 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
370 string verbosename; // holds the name of the player OR no name at all for printing in the centerprints
372 // attach the flag to the player
374 player.flagcarried = flag;
375 setattachment(flag, player, "");
376 setorigin(flag, FLAG_CARRY_OFFSET);
379 flag.movetype = MOVETYPE_NONE;
380 flag.takedamage = DAMAGE_NO;
381 flag.solid = SOLID_NOT;
382 flag.angles = '0 0 0';
383 flag.ctf_pickuptime = time; // used for timing runs
384 flag.ctf_pickupper = player;
385 flag.ctf_status = FLAG_CARRY;
387 // messages and sounds
388 Send_KillNotification (player.netname, flag.netname, "", INFO_GOTFLAG, MSG_INFO);
389 sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE);
390 ctf_EventLog("steal", flag.team, player);
391 verbosename = ((autocvar_g_ctf_flag_pickup_verbosename) ? strcat(Team_ColorCode(player.team), "(^7", player.netname, Team_ColorCode(player.team), ") ") : "");
392 FOR_EACH_PLAYER(tmp_player)
393 if(tmp_player == player)
394 centerprint(tmp_player, strcat("You got the ", flag.netname, "!"));
395 else if(tmp_player.team == player.team)
396 centerprint(tmp_player, strcat("Your ", Team_ColorCode(player.team), "team mate ", verbosename, "^7got the flag! Protect them!"));
397 else if(tmp_player.team == flag.team)
398 centerprint(tmp_player, strcat("The ", Team_ColorCode(player.team), "enemy ", verbosename, "^7got your flag! Retrieve it!"));
401 PlayerTeamScore_AddScore(player, ctf_ReadScore("score_pickup_base"));
402 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
405 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
406 if((player.speedrunning) && (ctf_captimerecord))
407 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
410 if (autocvar_g_ctf_flag_pickup_effects)
412 pointparticles(particleeffectnum("smoke_ring"), 0.5 * (flag.absmin + flag.absmax), '0 0 0', 1);
416 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)
417 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
418 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
419 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, '1 1 0');
420 WaypointSprite_Ping(player.wps_flagcarrier);
423 void ctf_Handle_Pickup_Dropped(entity flag, entity player)
426 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);
427 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
428 string verbosename; // holds the name of the player OR no name at all for printing in the centerprints
430 // attach the flag to the player
432 player.flagcarried = flag;
433 setattachment(flag, player, "");
434 setorigin(flag, FLAG_CARRY_OFFSET);
437 flag.movetype = MOVETYPE_NONE;
438 flag.takedamage = DAMAGE_NO;
439 flag.health = flag.max_flag_health;
440 flag.solid = SOLID_NOT;
441 flag.angles = '0 0 0';
442 //flag.ctf_pickuptime = time; // don't update pickuptime since this isn't a real steal.
443 flag.ctf_pickupper = player;
444 flag.ctf_status = FLAG_CARRY;
446 // messages and sounds
447 Send_KillNotification(player.netname, flag.netname, "", INFO_PICKUPFLAG, MSG_INFO);
448 sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE);
449 ctf_EventLog("pickup", flag.team, player);
450 verbosename = ((autocvar_g_ctf_flag_pickup_verbosename) ? strcat(Team_ColorCode(player.team), "(^7", player.netname, Team_ColorCode(player.team), ") ") : "");
451 FOR_EACH_PLAYER(tmp_player)
452 if(tmp_player == player)
453 centerprint(tmp_player, strcat("You got the ", flag.netname, "!"));
454 else if(tmp_player.team == player.team)
455 centerprint(tmp_player, strcat("Your ", Team_ColorCode(player.team), "team mate ", verbosename, "^7got the flag! Protect them!"));
456 else if(tmp_player.team == flag.team)
457 centerprint(tmp_player, strcat("The ", Team_ColorCode(player.team), "enemy ", verbosename, "^7got your flag! Retrieve it!"));
460 returnscore = floor((ctf_ReadScore("score_pickup_dropped_late") * (1-returnscore) + ctf_ReadScore("score_pickup_dropped_early") * returnscore) + 0.5);
461 print("score is ", ftos(returnscore), "\n");
462 PlayerTeamScore_AddScore(player, returnscore);
463 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
466 if(autocvar_g_ctf_flag_pickup_effects) // field pickup effect
468 pointparticles(particleeffectnum("smoke_ring"), 0.5 * (flag.absmin + flag.absmax), '0 0 0', 1);
472 WaypointSprite_Kill(flag.wps_flagdropped);
473 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)
474 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
475 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
476 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, '1 1 0');
477 WaypointSprite_Ping(player.wps_flagcarrier);
481 // ===================
482 // Main Flag Functions
483 // ===================
485 void ctf_CheckFlagReturn(entity flag)
487 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
489 if((flag.health <= 0) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_returntime))
491 bprint("The ", flag.netname, " has returned to base\n");
492 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTN_NONE);
493 ctf_EventLog("returned", flag.team, world);
494 ctf_RespawnFlag(flag);
498 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
500 if(ITEM_DAMAGE_NEEDKILL(deathtype))
502 // automatically kill the flag and return it
504 ctf_CheckFlagReturn(self);
507 if(autocvar_g_ctf_flag_take_damage)
509 self.health = self.health - damage;
510 ctf_CheckFlagReturn(self);
519 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
522 if(self == ctf_worldflaglist) // only for the first flag
523 FOR_EACH_CLIENT(tmp_entity)
524 ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
527 if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
528 dprint("wtf the flag got squashed?\n");
529 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
530 if(!trace_startsolid) // can we resize it without getting stuck?
531 setsize(self, FLAG_MIN, FLAG_MAX); }
534 switch(self.ctf_status)
538 if(autocvar_g_ctf_dropped_capture_radius)
540 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
541 if(tmp_entity.ctf_status == FLAG_DROPPED)
542 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
543 ctf_Handle_Dropped_Capture(self, tmp_entity);
550 if(autocvar_g_ctf_flag_returntime)
552 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_returntime) * FLAG_THINKRATE);
553 ctf_CheckFlagReturn(self);
560 if((self.owner) && (self.speedrunning) && (ctf_captimerecord) && (time >= self.ctf_pickuptime + ctf_captimerecord))
562 bprint("The ", self.netname, " became impatient after ", ftos_decimals(ctf_captimerecord, 2), " seconds and returned itself\n");
563 sound(self, CH_TRIGGER, self.snd_flag_respawn, VOL_BASE, ATTN_NONE);
564 ctf_EventLog("returned", self.team, world);
565 ctf_RespawnFlag(tmp_entity);
569 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
576 default: // this should never happen
578 dprint("ctf_FlagThink(): Flag exists with no status?\n");
586 if(gameover) { return; }
587 if(!self) { return; }
588 if(other.deadflag != DEAD_NO) { return; }
589 if(ITEM_TOUCH_NEEDKILL())
591 // automatically kill the flag and return it
593 ctf_CheckFlagReturn(self);
596 if(other.classname != "player") // The flag just touched an object, most likely the world
598 if(time > self.wait) // if we haven't in a while, play a sound/effect
600 pointparticles(particleeffectnum("kaball_sparks"), self.origin, '0 0 0', 1);
601 sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTN_NORM);
606 switch(self.ctf_status)
610 if((other.team == self.team) && (other.flagcarried) && (other.flagcarried.team != self.team))
611 ctf_Handle_Capture(self, other); // other just captured the enemies flag to his base
612 else if((other.team != self.team) && (!other.flagcarried) && (!other.ctf_captureshielded))
613 ctf_Handle_Pickup_Base(self, other); // other just stole the enemies flag
619 if(other.team == self.team)
620 ctf_Handle_Return(self, other); // other just returned his own flag
621 else if((!other.flagcarried) && ((other != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
622 ctf_Handle_Pickup_Dropped(self, other); // other just picked up a dropped enemy flag
628 dprint("Someone touched a flag even though it was being carried?\n");
632 default: // this should never happen
634 dprint("Touch: Flag exists with no status?\n");
639 self.wait = time + FLAG_TOUCHRATE;
642 void ctf_RespawnFlag(entity flag)
644 // reset the player (if there is one)
645 if((flag.owner) && (flag.owner.flagcarried == flag))
647 WaypointSprite_Kill(flag.wps_flagcarrier);
648 flag.owner.flagcarried = world;
650 if(flag.speedrunning)
651 ctf_FakeTimeLimit(flag.owner, -1);
654 if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
655 { WaypointSprite_Kill(flag.wps_flagdropped); }
658 setattachment(flag, world, "");
659 setorigin(flag, flag.ctf_spawnorigin); // replace with flag.ctf_spawnorigin
660 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
661 flag.takedamage = DAMAGE_NO;
662 flag.health = flag.max_flag_health;
663 flag.solid = SOLID_TRIGGER;
664 flag.velocity = '0 0 0';
665 flag.angles = flag.mangle;
666 flag.ctf_status = FLAG_BASE;
667 flag.flags = FL_ITEM | FL_NOTARGET;
674 if(self.owner.classname == "player")
675 ctf_Handle_Drop(self.owner, DROPTYPE_NORMAL);
677 ctf_RespawnFlag(self);
680 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
683 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.
686 waypoint_spawnforitem_force(self, self.origin);
687 self.nearestwaypointtimeout = 0; // activate waypointing again
688 self.bot_basewaypoint = self.nearestwaypoint;
691 WaypointSprite_SpawnFixed(((teamnumber) ? "redbase" : "bluebase"), self.origin + '0 0 64', self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2) - 1, FALSE));
692 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2) - 1, FALSE));
694 // captureshield setup
695 ctf_CaptureShield_Spawn(self);
698 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
701 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.
702 self = flag; // for later usage with droptofloor()
705 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
706 ctf_worldflaglist = flag;
708 setattachment(flag, world, "");
710 flag.netname = ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag");
711 flag.team = ((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2); // COLOR_TEAM1: color 4 team (red) - COLOR_TEAM2: color 13 team (blue)
712 flag.items = ((teamnumber) ? IT_KEY2 : IT_KEY1); // IT_KEY2: gold key (redish enough) - IT_KEY1: silver key (bluish enough)
713 flag.classname = "item_flag_team";
714 flag.target = "###item###"; // wut?
715 flag.flags = FL_ITEM | FL_NOTARGET;
716 flag.solid = SOLID_TRIGGER;
717 flag.takedamage = DAMAGE_NO;
718 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
719 flag.max_flag_health = ((autocvar_g_ctf_flag_take_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
720 flag.health = flag.max_flag_health;
721 flag.event_damage = ctf_FlagDamage;
722 flag.pushable = TRUE;
723 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
724 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
725 flag.velocity = '0 0 0';
726 flag.mangle = flag.angles;
727 flag.reset = ctf_Reset;
728 flag.touch = ctf_FlagTouch;
729 flag.think = ctf_FlagThink;
730 flag.nextthink = time + FLAG_THINKRATE;
731 flag.ctf_status = FLAG_BASE;
733 if(!flag.model) { flag.model = ((teamnumber) ? autocvar_g_ctf_flag_red_model : autocvar_g_ctf_flag_blue_model); }
734 if(!flag.scale) { flag.scale = FLAG_SCALE; }
735 if(!flag.skin) { flag.skin = ((teamnumber) ? autocvar_g_ctf_flag_red_skin : autocvar_g_ctf_flag_blue_skin); }
738 if(!flag.snd_flag_taken) { flag.snd_flag_taken = ((teamnumber) ? "ctf/red_taken.wav" : "ctf/blue_taken.wav"); }
739 if(!flag.snd_flag_returned) { flag.snd_flag_returned = ((teamnumber) ? "ctf/red_returned.wav" : "ctf/blue_returned.wav"); }
740 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
741 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.
742 if(!flag.snd_flag_dropped) { flag.snd_flag_dropped = ((teamnumber) ? "ctf/red_dropped.wav" : "ctf/blue_dropped.wav"); }
743 if(!flag.snd_flag_touch) { flag.snd_flag_touch = "keepaway/touch.wav"; } // again has no team-based sound // FIXME
746 precache_sound(flag.snd_flag_taken);
747 precache_sound(flag.snd_flag_returned);
748 precache_sound(flag.snd_flag_capture);
749 precache_sound(flag.snd_flag_respawn);
750 precache_sound(flag.snd_flag_dropped);
751 precache_sound(flag.snd_flag_touch);
752 precache_model(flag.model);
753 precache_model("models/ctf/shield.md3");
754 precache_model("models/ctf/shockwavetransring.md3");
757 setmodel(flag, flag.model); // precision set below
758 setsize(flag, FLAG_MIN, FLAG_MAX);
759 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
761 if(autocvar_g_ctf_flag_glowtrails)
763 flag.glow_color = ((teamnumber) ? 251 : 210); // 251: red - 210: blue
768 flag.effects |= EF_LOWPRECISION;
769 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
770 if(autocvar_g_ctf_dynamiclights) { flag.effects |= ((teamnumber) ? EF_RED : EF_BLUE); }
773 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
775 flag.dropped_origin = flag.origin;
777 flag.movetype = MOVETYPE_NONE;
779 else // drop to floor, automatically find a platform and set that as spawn origin
781 flag.noalign = FALSE;
784 flag.movetype = MOVETYPE_TOSS;
787 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
795 MUTATOR_HOOKFUNCTION(ctf_HookedDrop)
797 if(self.flagcarried) { ctf_Handle_Drop(self, DROPTYPE_NORMAL); }
801 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
805 // initially clear items so they can be set as necessary later.
806 self.items &~= (IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST
807 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST | IT_CTF_SHIELDED);
809 // item for stopping players from capturing the flag too often
810 if(self.ctf_captureshielded)
811 self.items |= IT_CTF_SHIELDED;
813 // scan through all the flags and notify the client about them
814 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
816 if(flag.ctf_status == FLAG_CARRY)
817 if(flag.owner == self)
818 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
820 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
821 else if(flag.ctf_status == FLAG_DROPPED)
822 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
825 if(self.wps_flagcarrier) { WaypointSprite_UpdateHealth(self.wps_flagcarrier, self.health); }
830 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
832 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
834 if(frag_target == frag_attacker) // damage done to yourself
836 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
837 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
839 else // damage done everyone else
841 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
842 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
848 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
850 frag_score = 0; // no frags counted in ctf
851 return (autocvar_g_ctf_ignore_frags); // you deceptive little bugger ;3 This needs to be true in order for this function to even count.
854 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
856 entity player = self;
858 if((player.flagcarried) && !(player.speedrunning))
860 if(autocvar_g_ctf_allow_pass)
862 entity head, closest_target;
863 head = findradius(player.origin, autocvar_g_ctf_pass_radius);
865 while(head) // find the closest acceptable target to pass to
867 if(head.classname == "player" && head.deadflag == DEAD_NO)
868 if(head != player && !IsDifferentTeam(head, player))
872 if(vlen(player.origin - head.origin) < vlen(player.origin - closest_target.origin))
873 closest_target = head;
876 closest_target = head;
881 if(closest_target) { ctf_Handle_Pass(player, closest_target); return 0; }
884 if(autocvar_g_ctf_allow_drop) { ctf_Handle_Drop(player, DROPTYPE_THROWN); }
894 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
895 CTF Starting point for a player in team one (Red).
896 Keys: "angle" viewing angle when spawning. */
897 void spawnfunc_info_player_team1()
899 if(g_assault) { remove(self); return; }
901 self.team = COLOR_TEAM1; // red
902 spawnfunc_info_player_deathmatch();
906 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
907 CTF Starting point for a player in team two (Blue).
908 Keys: "angle" viewing angle when spawning. */
909 void spawnfunc_info_player_team2()
911 if(g_assault) { remove(self); return; }
913 self.team = COLOR_TEAM2; // blue
914 spawnfunc_info_player_deathmatch();
917 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
918 CTF Starting point for a player in team three (Yellow).
919 Keys: "angle" viewing angle when spawning. */
920 void spawnfunc_info_player_team3()
922 if(g_assault) { remove(self); return; }
924 self.team = COLOR_TEAM3; // yellow
925 spawnfunc_info_player_deathmatch();
929 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
930 CTF Starting point for a player in team four (Purple).
931 Keys: "angle" viewing angle when spawning. */
932 void spawnfunc_info_player_team4()
934 if(g_assault) { remove(self); return; }
936 self.team = COLOR_TEAM4; // purple
937 spawnfunc_info_player_deathmatch();
940 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
941 CTF flag for team one (Red).
943 "angle" Angle the flag will point (minus 90 degrees)...
944 "model" model to use, note this needs red and blue as skins 0 and 1...
945 "noise" sound played when flag is picked up...
946 "noise1" sound played when flag is returned by a teammate...
947 "noise2" sound played when flag is captured...
948 "noise3" sound played when flag is lost in the field and respawns itself...
949 "noise4" sound played when flag is dropped by a player...
950 "noise5" sound played when flag touches the ground... */
951 void spawnfunc_item_flag_team1()
953 if(!g_ctf) { remove(self); return; }
955 ctf_FlagSetup(1, self); // 1 = red
958 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
959 CTF flag for team two (Blue).
961 "angle" Angle the flag will point (minus 90 degrees)...
962 "model" model to use, note this needs red and blue as skins 0 and 1...
963 "noise" sound played when flag is picked up...
964 "noise1" sound played when flag is returned by a teammate...
965 "noise2" sound played when flag is captured...
966 "noise3" sound played when flag is lost in the field and respawns itself...
967 "noise4" sound played when flag is dropped by a player...
968 "noise5" sound played when flag touches the ground... */
969 void spawnfunc_item_flag_team2()
971 if(!g_ctf) { remove(self); return; }
973 ctf_FlagSetup(0, self); // the 0 is misleading, but -- 0 = blue.
976 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
977 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
978 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.
980 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
981 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
982 void spawnfunc_ctf_team()
984 if(!g_ctf) { remove(self); return; }
986 self.classname = "ctf_team";
987 self.team = self.cnt + 1;
995 // code from here on is just to support maps that don't have flag and team entities
996 void ctf_SpawnTeam (string teamname, float teamcolor)
1001 self.classname = "ctf_team";
1002 self.netname = teamname;
1003 self.cnt = teamcolor;
1005 spawnfunc_ctf_team();
1010 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
1012 // if no teams are found, spawn defaults
1013 if(find(world, classname, "ctf_team") == world)
1015 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
1016 ctf_SpawnTeam("Red", COLOR_TEAM1 - 1);
1017 ctf_SpawnTeam("Blue", COLOR_TEAM2 - 1);
1023 void ctf_Initialize()
1025 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
1027 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
1028 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
1029 ctf_captureshield_force = autocvar_g_ctf_shield_force;
1031 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
1035 MUTATOR_DEFINITION(gamemode_ctf)
1037 MUTATOR_HOOK(MakePlayerObserver, ctf_HookedDrop, CBC_ORDER_ANY);
1038 MUTATOR_HOOK(ClientDisconnect, ctf_HookedDrop, CBC_ORDER_ANY);
1039 MUTATOR_HOOK(PlayerDies, ctf_HookedDrop, CBC_ORDER_ANY);
1040 MUTATOR_HOOK(PortalTeleport, ctf_HookedDrop, CBC_ORDER_ANY);
1041 MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
1042 MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
1043 MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
1044 MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
1048 if(time > 1) // game loads at time 1
1049 error("This is a game type and it cannot be added at runtime.");
1057 error("This is a game type and it cannot be removed at runtime.");