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; }
135 player.flagcarried = world;
136 WaypointSprite_Ping(player.wps_flagcarrier);
137 WaypointSprite_Kill(player.wps_flagcarrier);
139 // transfer flag to reciever
140 flag.owner = reciever;
141 flag.owner.flagcarried = flag;
142 flag.ctf_pickupid = reciever.playerid;
143 setattachment(flag, reciever, "");
144 setorigin(flag, FLAG_CARRY_OFFSET);
146 // messages and sounds
147 sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTN_NORM);
148 ctf_EventLog("pass", flag.team, player);
149 ctf_EventLog("recieve", flag.team, reciever);
150 FOR_EACH_PLAYER(tmp_player)
151 if(tmp_player == player)
152 centerprint(tmp_player, strcat("You passed the ", flag.netname, " to ", reciever.netname));
153 else if(tmp_player == reciever)
154 centerprint(tmp_player, strcat("You recieved the ", flag.netname, " from ", player.netname));
155 else if(tmp_player.team == player.team)
156 centerprint(tmp_player, strcat(player.netname, " passed the ", flag.netname, " to ", reciever.netname));
159 te_lightning2(world, reciever.origin, player.origin);
161 // create new waypoint
162 WaypointSprite_Spawn("flagcarrier", 0, 0, reciever, '0 0 64', world, reciever.team, reciever, wps_flagcarrier, FALSE, RADARICON_FLAG, '1 1 0');
163 WaypointSprite_UpdateMaxHealth(reciever.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
164 WaypointSprite_UpdateHealth(reciever.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(reciever.health, reciever.armorvalue, autocvar_g_balance_armor_blockpercent));
165 WaypointSprite_UpdateTeamRadar(reciever.wps_flagcarrier, RADARICON_FLAGCARRIER, '1 1 0');
168 void ctf_Handle_Drop(entity player, float droptype)
170 entity flag = player.flagcarried;
171 if(!flag) { return; }
173 if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
176 setattachment(flag, world, "");
177 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
178 flag.owner.flagcarried = world;
180 flag.movetype = MOVETYPE_TOSS;
181 flag.solid = SOLID_TRIGGER;
182 flag.takedamage = DAMAGE_YES;
183 flag.health = flag.max_flag_health;
187 case DROPTYPE_THROWN:
189 makevectors((player.v_angle_y * '0 1 0') + (player.v_angle_x * '0.5 0 0'));
190 flag.velocity = W_CalculateProjectileVelocity(player.velocity, ('0 0 200' + (v_forward * autocvar_g_ctf_throw_velocity)), FALSE);
195 case DROPTYPE_NORMAL:
197 flag.velocity = ('0 0 200' + ('0 100 0' * crandom()) + ('100 0 0' * crandom()));
202 flag.ctf_droptime = time;
203 flag.ctf_dropperid = player.playerid;
204 flag.ctf_status = FLAG_DROPPED;
206 // messages and sounds
207 Send_KillNotification(player.netname, flag.netname, "", INFO_LOSTFLAG, MSG_INFO);
208 sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTN_NONE);
209 ctf_EventLog("dropped", player.team, player);
212 PlayerTeamScore_AddScore(player, -ctf_ReadScore("penalty_drop"));
213 PlayerScore_Add(player, SP_CTF_DROPS, 1);
216 if(autocvar_g_ctf_flag_dropped_waypoint)
217 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)
219 WaypointSprite_Ping(player.wps_flagcarrier);
220 WaypointSprite_Kill(player.wps_flagcarrier);
222 if(autocvar_g_ctf_flag_returntime || (autocvar_g_ctf_flag_take_damage && autocvar_g_ctf_flag_health))
224 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
225 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
229 ctf_CaptureShield_Update(player, 0); // shield only
231 // check if the flag will fall off the map
232 trace_startsolid = FALSE;
233 tracebox(flag.origin, flag.mins, flag.maxs, flag.origin, TRUE, flag);
235 dprint("FLAG FALLTHROUGH will happen SOON\n");
238 void ctf_Handle_Dropped_Capture(entity flag, entity enemy_flag)
242 float cap_time, cap_record, success;
243 string cap_message, refername;
245 entity player = enemy_flag.dropperid;
248 if((autocvar_g_ctf_captimerecord_always) || (player_count - currentbots)) {
249 cap_record = ctf_captimerecord;
250 cap_time = (time - player.flagcarried.ctf_pickuptime);
252 refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
253 refername = ((refername == player.netname) ? "their" : strcat(refername, "^7's"));
255 if(!ctf_captimerecord)
256 { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds"); success = TRUE; }
257 else if(cap_time < cap_record)
258 { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, breaking ", refername, " previous record of ", ftos_decimals(cap_record, 2), " seconds"); success = TRUE; }
260 { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, failing to break ", refername, " record of ", ftos_decimals(cap_record, 2), " seconds"); success = FALSE; }
263 ctf_captimerecord = cap_time;
264 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
265 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
266 write_recordmarker(player, (time - cap_time), cap_time); } }
268 // messages and sounds
269 Send_KillNotification(player.netname, player.flagcarried.netname, cap_message, INFO_CAPTUREFLAG, MSG_INFO);
270 sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE); // "ctf/*_capture.wav"
271 ctf_EventLog("capture", player.flagcarried.team, player);
274 PlayerTeamScore_AddScore(player, ctf_ReadScore("score_capture"));
275 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
278 if (autocvar_g_ctf_flag_capture_effects)
280 pointparticles(particleeffectnum((player.team == COLOR_TEAM1) ? "red_ground_quake" : "blue_ground_quake"), flag.origin, '0 0 0', 1);
281 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
285 WaypointSprite_Kill(player.wps_flagcarrier);
288 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
292 sound(flag, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE);
293 print("dropped capture! :D\n");
295 ctf_RespawnFlag(enemy_flag);
298 void ctf_Handle_Capture(entity flag, entity player)
301 float cap_time, cap_record, success;
302 string cap_message, refername;
305 if((autocvar_g_ctf_captimerecord_always) || (player_count - currentbots)) {
306 cap_record = ctf_captimerecord;
307 cap_time = (time - player.flagcarried.ctf_pickuptime);
309 refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
310 refername = ((refername == player.netname) ? "their" : strcat(refername, "^7's"));
312 if(!ctf_captimerecord)
313 { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds"); success = TRUE; }
314 else if(cap_time < cap_record)
315 { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, breaking ", refername, " previous record of ", ftos_decimals(cap_record, 2), " seconds"); success = TRUE; }
317 { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, failing to break ", refername, " record of ", ftos_decimals(cap_record, 2), " seconds"); success = FALSE; }
320 ctf_captimerecord = cap_time;
321 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
322 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
323 write_recordmarker(player, (time - cap_time), cap_time); } }
325 // messages and sounds
326 Send_KillNotification(player.netname, player.flagcarried.netname, cap_message, INFO_CAPTUREFLAG, MSG_INFO);
327 sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE); // "ctf/*_capture.wav"
328 ctf_EventLog("capture", player.flagcarried.team, player);
331 PlayerTeamScore_AddScore(player, ctf_ReadScore("score_capture"));
332 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
335 if (autocvar_g_ctf_flag_capture_effects)
337 pointparticles(particleeffectnum((player.team == COLOR_TEAM1) ? "red_ground_quake" : "blue_ground_quake"), flag.origin, '0 0 0', 1);
338 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
342 WaypointSprite_Kill(player.wps_flagcarrier);
345 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
347 ctf_RespawnFlag(player.flagcarried);
350 void ctf_Handle_Return(entity flag, entity player)
352 // messages and sounds
353 //centerprint(player, strcat("You returned ", flag.netname));
354 Send_KillNotification (player.netname, flag.netname, "", INFO_RETURNFLAG, MSG_INFO);
355 sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTN_NONE);
356 ctf_EventLog("return", flag.team, player);
359 PlayerTeamScore_AddScore(player, ctf_ReadScore(strcat("score_return", ((player.playerid == flag.playerid) ? "_by_killer" : "")))); // reward for return
360 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
362 TeamScore_AddToTeam(((flag.team == COLOR_TEAM1) ? COLOR_TEAM2 : COLOR_TEAM1), ST_SCORE, -ctf_ReadScore("penalty_returned")); // punish the team who was last carrying it
363 FOR_EACH_PLAYER(player) if(player.playerid == flag.ctf_dropperid) // punish the player who dropped the flag
365 PlayerScore_Add(player, SP_SCORE, -ctf_ReadScore("penalty_returned"));
366 ctf_CaptureShield_Update(player, 0); // shield only
370 ctf_RespawnFlag(flag);
373 void ctf_Handle_Pickup_Base(entity flag, entity player)
375 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
376 string verbosename; // holds the name of the player OR no name at all for printing in the centerprints
378 // attach the flag to the player
380 player.flagcarried = flag;
381 setattachment(flag, player, "");
382 setorigin(flag, FLAG_CARRY_OFFSET);
385 flag.movetype = MOVETYPE_NONE;
386 flag.takedamage = DAMAGE_NO;
387 flag.solid = SOLID_NOT;
388 flag.angles = '0 0 0';
389 flag.ctf_pickuptime = time; // used for timing runs
390 flag.ctf_pickupid = player.playerid;
391 flag.ctf_status = FLAG_CARRY;
393 // messages and sounds
394 Send_KillNotification (player.netname, flag.netname, "", INFO_GOTFLAG, MSG_INFO);
395 sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE);
396 ctf_EventLog("steal", flag.team, player);
397 verbosename = ((autocvar_g_ctf_flag_pickup_verbosename) ? strcat(Team_ColorCode(player.team), "(^7", player.netname, Team_ColorCode(player.team), ") ") : "");
398 FOR_EACH_PLAYER(tmp_player)
399 if(tmp_player == player)
400 centerprint(tmp_player, strcat("You got the ", flag.netname, "!"));
401 else if(tmp_player.team == player.team)
402 centerprint(tmp_player, strcat("Your ", Team_ColorCode(player.team), "team mate ", verbosename, "^7got the flag! Protect them!"));
403 else if(tmp_player.team == flag.team)
404 centerprint(tmp_player, strcat("The ", Team_ColorCode(player.team), "enemy ", verbosename, "^7got your flag! Retrieve it!"));
407 PlayerTeamScore_AddScore(player, ctf_ReadScore("score_pickup_base"));
408 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
411 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
412 if((player.speedrunning) && (ctf_captimerecord))
413 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
416 if (autocvar_g_ctf_flag_pickup_effects)
418 pointparticles(particleeffectnum("smoke_ring"), 0.5 * (flag.absmin + flag.absmax), '0 0 0', 1);
422 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)
423 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
424 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
425 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, '1 1 0');
426 WaypointSprite_Ping(player.wps_flagcarrier);
429 void ctf_Handle_Pickup_Dropped(entity flag, entity player)
432 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);
433 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
434 string verbosename; // holds the name of the player OR no name at all for printing in the centerprints
436 // attach the flag to the player
438 player.flagcarried = flag;
439 setattachment(flag, player, "");
440 setorigin(flag, FLAG_CARRY_OFFSET);
443 flag.movetype = MOVETYPE_NONE;
444 flag.takedamage = DAMAGE_NO;
445 flag.health = flag.max_flag_health;
446 flag.solid = SOLID_NOT;
447 flag.angles = '0 0 0';
448 //flag.ctf_pickuptime = time; // don't update pickuptime since this isn't a real steal.
449 flag.ctf_pickupid = player.playerid;
450 flag.ctf_status = FLAG_CARRY;
452 // messages and sounds
453 Send_KillNotification (player.netname, flag.netname, "", INFO_PICKUPFLAG, MSG_INFO);
454 sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE);
455 ctf_EventLog("pickup", flag.team, player);
456 verbosename = ((autocvar_g_ctf_flag_pickup_verbosename) ? strcat(Team_ColorCode(player.team), "(^7", player.netname, Team_ColorCode(player.team), ") ") : "");
457 FOR_EACH_PLAYER(tmp_player)
458 if(tmp_player == player)
459 centerprint(tmp_player, strcat("You got the ", flag.netname, "!"));
460 else if(tmp_player.team == player.team)
461 centerprint(tmp_player, strcat("Your ", Team_ColorCode(player.team), "team mate ", verbosename, "^7got the flag! Protect them!"));
462 else if(tmp_player.team == flag.team)
463 centerprint(tmp_player, strcat("The ", Team_ColorCode(player.team), "enemy ", verbosename, "^7got your flag! Retrieve it!"));
466 returnscore = floor((ctf_ReadScore("score_pickup_dropped_late") * (1-returnscore) + ctf_ReadScore("score_pickup_dropped_early") * returnscore) + 0.5);
467 print("score is ", ftos(returnscore), "\n");
468 PlayerTeamScore_AddScore(player, returnscore);
469 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
472 if (autocvar_g_ctf_flag_pickup_effects) // field pickup effect
474 pointparticles(particleeffectnum("smoke_ring"), 0.5 * (flag.absmin + flag.absmax), '0 0 0', 1);
478 WaypointSprite_Kill(flag.wps_flagdropped);
479 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)
480 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
481 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
482 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, '1 1 0');
483 WaypointSprite_Ping(player.wps_flagcarrier);
487 // ===================
488 // Main Flag Functions
489 // ===================
491 void ctf_CheckFlagReturn(entity flag)
493 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
495 if((flag.health <= 0) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_returntime))
497 bprint("The ", flag.netname, " has returned to base\n");
498 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTN_NONE);
499 ctf_EventLog("returned", flag.team, world);
500 ctf_RespawnFlag(flag);
504 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
506 if(deathtype == DEATH_HURTTRIGGER || deathtype == DEATH_SLIME || deathtype == DEATH_LAVA)
508 // automatically kill the flag and return it
510 ctf_CheckFlagReturn(self);
513 if(autocvar_g_ctf_flag_take_damage)
515 self.health = self.health - damage;
516 ctf_CheckFlagReturn(self);
525 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
528 if(self == ctf_worldflaglist) // only for the first flag
529 FOR_EACH_CLIENT(tmp_entity)
530 ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
533 if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
534 dprint("wtf the flag got squashed?\n");
535 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
536 if(!trace_startsolid) // can we resize it without getting stuck?
537 setsize(self, FLAG_MIN, FLAG_MAX); }
540 switch(self.ctf_status)
544 if(autocvar_g_ctf_dropped_capture_radius)
546 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
547 if(tmp_entity.ctf_status == FLAG_DROPPED)
548 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
549 ctf_Handle_Dropped_Capture(self, tmp_entity);
556 if(autocvar_g_ctf_flag_returntime)
558 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_returntime) * FLAG_THINKRATE);
559 ctf_CheckFlagReturn(self);
566 if((self.owner) && (self.speedrunning) && (ctf_captimerecord) && (time >= self.ctf_pickuptime + ctf_captimerecord))
568 bprint("The ", self.netname, " became impatient after ", ftos_decimals(ctf_captimerecord, 2), " seconds and returned itself\n");
569 sound(self, CH_TRIGGER, self.snd_flag_respawn, VOL_BASE, ATTN_NONE);
570 ctf_EventLog("returned", self.team, world);
571 ctf_RespawnFlag(tmp_entity);
575 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
582 default: // this should never happen
584 dprint("ctf_FlagThink(): Flag exists with no status?\n");
592 if(gameover) { return; }
593 if(!self) { return; }
594 if(other.deadflag != DEAD_NO) { return; }
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.playerid != self.ctf_dropperid) || (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.");