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_FlagcarrierWaypoints(entity player)
65 WaypointSprite_Spawn("flagcarrier", 0, 0, player, '0 0 64', world, player.team, player, wps_flagcarrier, FALSE, RADARICON_FLAG, '1 1 0');
66 WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
67 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
68 WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, '1 1 0');
72 // =======================
73 // CaptureShield Functions
74 // =======================
76 float ctf_CaptureShield_CheckStatus(entity p)
80 float players_worseeq, players_total;
82 if(ctf_captureshield_max_ratio <= 0)
85 s = PlayerScore_Add(p, SP_SCORE, 0);
86 if(s >= -ctf_captureshield_min_negscore)
89 players_total = players_worseeq = 0;
94 se = PlayerScore_Add(e, SP_SCORE, 0);
100 // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
101 // use this rule here
103 if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
109 void ctf_CaptureShield_Update(entity player, float wanted_status)
111 float updated_status = ctf_CaptureShield_CheckStatus(player);
112 if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
114 if(updated_status) // TODO csqc notifier for this // Samual: How?
115 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);
117 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);
119 player.ctf_captureshielded = updated_status;
123 float ctf_CaptureShield_Customize()
125 if not(other.ctf_captureshielded) { return FALSE; }
126 if(self.team == other.team) { return FALSE; }
131 void ctf_CaptureShield_Touch()
133 if not(other.ctf_captureshielded) { return; }
134 if(self.team == other.team) { return; }
136 vector mymid = (self.absmin + self.absmax) * 0.5;
137 vector othermid = (other.absmin + other.absmax) * 0.5;
139 Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
140 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);
143 void ctf_CaptureShield_Spawn(entity flag)
145 entity shield = spawn();
148 shield.team = self.team;
149 shield.touch = ctf_CaptureShield_Touch;
150 shield.customizeentityforclient = ctf_CaptureShield_Customize;
151 shield.classname = "ctf_captureshield";
152 shield.effects = EF_ADDITIVE;
153 shield.movetype = MOVETYPE_NOCLIP;
154 shield.solid = SOLID_TRIGGER;
155 shield.avelocity = '7 0 11';
158 setorigin(shield, self.origin);
159 setmodel(shield, "models/ctf/shield.md3");
160 setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
163 // ====================
164 // Drop/Pass/Throw Code
165 // ====================
167 void ctf_Handle_Failed_Pass(entity flag)
169 entity sender = flag.pass_sender;
171 flag.movetype = MOVETYPE_TOSS;
172 flag.takedamage = DAMAGE_YES;
173 flag.health = flag.max_flag_health;
174 flag.ctf_droptime = time;
175 flag.ctf_dropper = sender;
176 flag.ctf_status = FLAG_DROPPED;
178 // messages and sounds
179 Send_KillNotification(sender.netname, flag.netname, "", INFO_LOSTFLAG, MSG_INFO);
180 sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTN_NONE);
181 ctf_EventLog("dropped", sender.team, sender);
184 PlayerTeamScore_AddScore(sender, -ctf_ReadScore("penalty_drop"));
185 PlayerScore_Add(sender, SP_CTF_DROPS, 1);
188 if(autocvar_g_ctf_flag_dropped_waypoint)
189 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'));
191 if(autocvar_g_ctf_flag_returntime || (autocvar_g_ctf_flag_take_damage && autocvar_g_ctf_flag_health))
193 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
194 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
197 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
199 flag.pass_sender = world;
200 flag.pass_target = world;
203 void ctf_Handle_Retrieve(entity flag, entity player)
205 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
206 entity sender = flag.pass_sender;
208 // transfer flag to player
209 flag.ctf_carrier = player;
211 flag.owner.flagcarried = flag;
214 setattachment(flag, player, "");
215 setorigin(flag, FLAG_CARRY_OFFSET);
216 flag.movetype = MOVETYPE_NONE;
217 flag.takedamage = DAMAGE_NO;
218 flag.solid = SOLID_NOT;
219 flag.ctf_carrier = player;
220 flag.ctf_status = FLAG_CARRY;
222 // messages and sounds
223 sound(player, CH_TRIGGER, "keepaway/respawn.wav", VOL_BASE, ATTN_NORM);
224 ctf_EventLog("recieve", flag.team, player);
225 FOR_EACH_PLAYER(tmp_player)
226 if(tmp_player == sender)
227 centerprint(tmp_player, strcat("You passed the ", flag.netname, " to ", player.netname));
228 else if(tmp_player == player)
229 centerprint(tmp_player, strcat("You recieved the ", flag.netname, " from ", sender.netname));
230 else if(tmp_player.team == sender.team)
231 centerprint(tmp_player, strcat(sender.netname, " passed the ", flag.netname, " to ", player.netname));
233 // create new waypoint
234 ctf_FlagcarrierWaypoints(player);
236 sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
237 player.throw_antispam = sender.throw_antispam;
239 flag.pass_sender = world;
240 flag.pass_target = world;
243 void ctf_Handle_Throw(entity player, entity reciever, float droptype)
245 entity flag = player.flagcarried;
247 if(!flag) { return; }
248 if((droptype == DROPTYPE_PASS) && !reciever) { return; }
250 //if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
253 setattachment(flag, world, "");
254 setorigin(flag, player.origin + FLAG_DROP_OFFSET);
255 flag.owner.flagcarried = world;
257 flag.solid = SOLID_TRIGGER;
258 flag.ctf_droptime = time;
264 vector targ_origin = (0.5 * (reciever.absmin + reciever.absmax));
265 flag.velocity = (normalize(targ_origin - player.origin) * autocvar_g_ctf_throw_velocity);
271 makevectors((player.v_angle_y * '0 1 0') + (player.v_angle_x * '0.5 0 0'));
272 flag.velocity = W_CalculateProjectileVelocity(player.velocity, ('0 0 200' + (v_forward * autocvar_g_ctf_throw_velocity)), FALSE);
279 flag.velocity = ('0 0 200' + ('0 100 0' * crandom()) + ('100 0 0' * crandom()));
284 print("flag.velocity = ", vtos(flag.velocity), ".\n");
291 flag.movetype = MOVETYPE_FLY;
292 flag.takedamage = DAMAGE_NO;
293 flag.pass_sender = player;
294 flag.pass_target = reciever;
295 flag.ctf_status = FLAG_PASSING;
298 sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTN_NORM);
299 ctf_EventLog("pass", flag.team, player);
300 te_lightning2(world, reciever.origin, player.origin);
309 flag.movetype = MOVETYPE_TOSS;
310 flag.takedamage = DAMAGE_YES;
311 flag.health = flag.max_flag_health;
312 flag.ctf_dropper = player;
313 flag.ctf_status = FLAG_DROPPED;
315 // messages and sounds
316 Send_KillNotification(player.netname, flag.netname, "", INFO_LOSTFLAG, MSG_INFO);
317 sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTN_NONE);
318 ctf_EventLog("dropped", player.team, player);
321 PlayerTeamScore_AddScore(player, -ctf_ReadScore("penalty_drop"));
322 PlayerScore_Add(player, SP_CTF_DROPS, 1);
325 if(autocvar_g_ctf_flag_dropped_waypoint)
326 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)
328 if(autocvar_g_ctf_flag_returntime || (autocvar_g_ctf_flag_take_damage && autocvar_g_ctf_flag_health))
330 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
331 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
337 // kill old waypointsprite
338 WaypointSprite_Ping(player.wps_flagcarrier);
339 WaypointSprite_Kill(player.wps_flagcarrier);
342 //ctf_CaptureShield_Update(player, 0); // shield only
350 void ctf_Handle_Dropped_Capture(entity flag, entity enemy_flag)
354 entity player = enemy_flag.ctf_dropper;
356 if not(player) { return; } // without someone to give the reward to, we can't possibly cap
358 // messages and sounds
359 Send_KillNotification(player.netname, enemy_flag.netname, ctf_CaptureRecord(enemy_flag, player), INFO_CAPTUREFLAG, MSG_INFO);
360 sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE);
361 ctf_EventLog("droppedcapture", enemy_flag.team, player);
364 PlayerTeamScore_AddScore(player, ctf_ReadScore("score_capture"));
365 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
368 if(autocvar_g_ctf_flag_capture_effects)
370 pointparticles(particleeffectnum((player.team == COLOR_TEAM1) ? "red_ground_quake" : "blue_ground_quake"), flag.origin, '0 0 0', 1);
371 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
374 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
377 ctf_RespawnFlag(enemy_flag);
380 void ctf_Handle_Capture(entity flag, entity player)
382 // messages and sounds
383 Send_KillNotification(player.netname, player.flagcarried.netname, ctf_CaptureRecord(player.flagcarried, player), INFO_CAPTUREFLAG, MSG_INFO);
384 sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE);
385 ctf_EventLog("capture", player.flagcarried.team, player);
388 PlayerTeamScore_AddScore(player, ctf_ReadScore("score_capture"));
389 PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
392 if(autocvar_g_ctf_flag_capture_effects)
394 pointparticles(particleeffectnum((player.team == COLOR_TEAM1) ? "red_ground_quake" : "blue_ground_quake"), flag.origin, '0 0 0', 1);
395 shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
399 WaypointSprite_Kill(player.wps_flagcarrier);
402 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
403 player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
405 ctf_RespawnFlag(player.flagcarried);
408 void ctf_Handle_Return(entity flag, entity player)
410 // messages and sounds
411 //centerprint(player, strcat("You returned ", flag.netname));
412 Send_KillNotification (player.netname, flag.netname, "", INFO_RETURNFLAG, MSG_INFO);
413 sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTN_NONE);
414 ctf_EventLog("return", flag.team, player);
417 PlayerTeamScore_AddScore(player, ctf_ReadScore("score_return")); // reward for return
418 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
420 TeamScore_AddToTeam(((flag.team == COLOR_TEAM1) ? COLOR_TEAM2 : COLOR_TEAM1), ST_SCORE, -ctf_ReadScore("penalty_returned")); // punish the team who was last carrying it
421 FOR_EACH_PLAYER(player) if(player == flag.ctf_dropper) // punish the player who dropped the flag
423 PlayerScore_Add(player, SP_SCORE, -ctf_ReadScore("penalty_returned"));
424 ctf_CaptureShield_Update(player, 0); // shield only
428 ctf_RespawnFlag(flag);
431 void ctf_Handle_Pickup(entity flag, entity player, float pickuptype)
434 entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
435 string verbosename; // holds the name of the player OR no name at all for printing in the centerprints
436 float pickup_dropped_score; // used to calculate dropped pickup score
438 // attach the flag to the player
440 player.flagcarried = flag;
441 setattachment(flag, player, "");
442 setorigin(flag, FLAG_CARRY_OFFSET);
445 flag.movetype = MOVETYPE_NONE;
446 flag.takedamage = DAMAGE_NO;
447 flag.solid = SOLID_NOT;
448 flag.angles = '0 0 0';
449 flag.ctf_carrier = player;
450 flag.ctf_status = FLAG_CARRY;
454 case PICKUPTYPE_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
455 case PICKUPTYPE_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
459 // messages and sounds
460 Send_KillNotification (player.netname, flag.netname, "", INFO_GOTFLAG, MSG_INFO);
461 sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE);
462 verbosename = ((autocvar_g_ctf_flag_pickup_verbosename) ? strcat(Team_ColorCode(player.team), "(^7", player.netname, Team_ColorCode(player.team), ") ") : "");
464 FOR_EACH_PLAYER(tmp_player)
465 if(tmp_player == player)
466 centerprint(tmp_player, strcat("You got the ", flag.netname, "!"));
467 else if(tmp_player.team == player.team)
468 centerprint(tmp_player, strcat("Your ", Team_ColorCode(player.team), "team mate ", verbosename, "^7got the flag! Protect them!"));
469 else if(tmp_player.team == flag.team)
470 centerprint(tmp_player, strcat("The ", Team_ColorCode(player.team), "enemy ", verbosename, "^7got your flag! Retrieve it!"));
474 case PICKUPTYPE_BASE: ctf_EventLog("steal", flag.team, player); break;
475 case PICKUPTYPE_DROPPED: ctf_EventLog("pickup", flag.team, player); break;
480 PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
483 case PICKUPTYPE_BASE:
485 PlayerTeamScore_AddScore(player, ctf_ReadScore("score_pickup_base"));
489 case PICKUPTYPE_DROPPED:
491 pickup_dropped_score = (autocvar_g_ctf_flag_returntime ? bound(0, ((flag.ctf_droptime + autocvar_g_ctf_flag_returntime) - time) / autocvar_g_ctf_flag_returntime, 1) : 1);
492 pickup_dropped_score = floor((ctf_ReadScore("score_pickup_dropped_late") * (1 - pickup_dropped_score) + ctf_ReadScore("score_pickup_dropped_early") * pickup_dropped_score) + 0.5);
493 print("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
494 PlayerTeamScore_AddScore(player, pickup_dropped_score);
502 if(pickuptype == PICKUPTYPE_BASE)
504 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
505 if((player.speedrunning) && (ctf_captimerecord))
506 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
510 if(autocvar_g_ctf_flag_pickup_effects)
511 pointparticles(particleeffectnum("smoke_ring"), 0.5 * (flag.absmin + flag.absmax), '0 0 0', 1);
514 if(pickuptype == PICKUPTYPE_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
515 ctf_FlagcarrierWaypoints(player);
516 WaypointSprite_Ping(player.wps_flagcarrier);
520 // ===================
521 // Main Flag Functions
522 // ===================
524 void ctf_CheckFlagReturn(entity flag)
526 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
528 if((flag.health <= 0) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_returntime))
530 bprint("The ", flag.netname, " has returned to base\n");
531 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTN_NONE);
532 ctf_EventLog("returned", flag.team, world);
533 ctf_RespawnFlag(flag);
537 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
539 if(ITEM_DAMAGE_NEEDKILL(deathtype))
541 // automatically kill the flag and return it
543 ctf_CheckFlagReturn(self);
546 if(autocvar_g_ctf_flag_take_damage)
548 self.health = self.health - damage;
549 ctf_CheckFlagReturn(self);
558 self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
561 if(self == ctf_worldflaglist) // only for the first flag
562 FOR_EACH_CLIENT(tmp_entity)
563 ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
566 if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
567 dprint("wtf the flag got squashed?\n");
568 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
569 if(!trace_startsolid) // can we resize it without getting stuck?
570 setsize(self, FLAG_MIN, FLAG_MAX); }
573 switch(self.ctf_status)
577 if(autocvar_g_ctf_dropped_capture_radius)
579 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
580 if(tmp_entity.ctf_status == FLAG_DROPPED)
581 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
582 ctf_Handle_Dropped_Capture(self, tmp_entity);
589 if(autocvar_g_ctf_flag_returntime)
591 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_returntime) * FLAG_THINKRATE);
592 ctf_CheckFlagReturn(self);
599 if((self.owner) && (self.speedrunning) && (ctf_captimerecord) && (time >= self.ctf_pickuptime + ctf_captimerecord))
601 bprint("The ", self.netname, " became impatient after ", ftos_decimals(ctf_captimerecord, 2), " seconds and returned itself\n");
602 sound(self, CH_TRIGGER, self.snd_flag_respawn, VOL_BASE, ATTN_NONE);
603 ctf_EventLog("returned", self.team, world);
604 ctf_RespawnFlag(tmp_entity);
608 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
617 vector targ_origin = (0.5 * (self.pass_target.absmin + self.pass_target.absmax));
619 traceline(self.origin, targ_origin, MOVE_NOMONSTERS, self);
621 if((self.pass_target.deadflag != DEAD_NO)
622 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
623 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
624 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
626 ctf_Handle_Failed_Pass(self);
628 else // still a viable target, go for it
630 vector desired_direction = normalize(targ_origin - self.origin);
631 vector current_direction = normalize(self.velocity);
633 self.velocity = (normalize(current_direction + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_throw_velocity);
638 default: // this should never happen
640 dprint("ctf_FlagThink(): Flag exists with no status?\n");
648 if(gameover) { return; }
649 if(!self) { return; }
650 if(other.deadflag != DEAD_NO) { return; }
651 if(ITEM_TOUCH_NEEDKILL())
653 // automatically kill the flag and return it
655 ctf_CheckFlagReturn(self);
658 if(other.classname != "player") // The flag just touched an object, most likely the world
660 if(time > self.wait) // if we haven't in a while, play a sound/effect
662 pointparticles(particleeffectnum("kaball_sparks"), self.origin, '0 0 0', 1);
663 sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTN_NORM);
664 self.wait = time + FLAG_TOUCHRATE;
669 switch(self.ctf_status)
673 if((other.team == self.team) && (other.flagcarried) && (other.flagcarried.team != self.team))
674 ctf_Handle_Capture(self, other); // other just captured the enemies flag to his base
675 else if((other.team != self.team) && (!other.flagcarried) && (!other.ctf_captureshielded) && (time > other.next_take_time))
676 ctf_Handle_Pickup(self, other, PICKUPTYPE_BASE); // other just stole the enemies flag
682 if(other.team == self.team)
683 ctf_Handle_Return(self, other); // other just returned his own flag
684 else if((!other.flagcarried) && ((other != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
685 ctf_Handle_Pickup(self, other, PICKUPTYPE_DROPPED); // other just picked up a dropped enemy flag
691 dprint("Someone touched a flag even though it was being carried?\n");
697 if((other.classname == "player") && (other.deadflag == DEAD_NO) && (other != self.pass_sender))
699 if(IsDifferentTeam(other, self.pass_sender))
700 ctf_Handle_Return(self, other);
702 ctf_Handle_Retrieve(self, other);
707 default: // this should never happen
709 dprint("Touch: Flag exists with no status?\n");
715 void ctf_RespawnFlag(entity flag)
717 // reset the player (if there is one)
718 if((flag.owner) && (flag.owner.flagcarried == flag))
720 WaypointSprite_Kill(flag.wps_flagcarrier);
721 flag.owner.flagcarried = world;
723 if(flag.speedrunning)
724 ctf_FakeTimeLimit(flag.owner, -1);
727 if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
728 { WaypointSprite_Kill(flag.wps_flagdropped); }
731 setattachment(flag, world, "");
732 setorigin(flag, flag.ctf_spawnorigin);
734 flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
735 flag.takedamage = DAMAGE_NO;
736 flag.health = flag.max_flag_health;
737 flag.solid = SOLID_TRIGGER;
738 flag.velocity = '0 0 0';
739 flag.angles = flag.mangle;
740 flag.flags = FL_ITEM | FL_NOTARGET;
742 flag.ctf_status = FLAG_BASE;
744 flag.pass_sender = world;
745 flag.pass_target = world;
746 flag.ctf_carrier = world;
747 flag.ctf_dropper = world;
748 flag.ctf_pickuptime = 0;
749 flag.ctf_droptime = 0;
755 if(self.owner.classname == "player")
756 ctf_Handle_Throw(self.owner, world, DROPTYPE_DROP);
758 ctf_RespawnFlag(self);
761 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
764 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.
767 waypoint_spawnforitem_force(self, self.origin);
768 self.nearestwaypointtimeout = 0; // activate waypointing again
769 self.bot_basewaypoint = self.nearestwaypoint;
772 WaypointSprite_SpawnFixed(((teamnumber) ? "redbase" : "bluebase"), self.origin + '0 0 64', self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2) - 1, FALSE));
773 WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2) - 1, FALSE));
775 // captureshield setup
776 ctf_CaptureShield_Spawn(self);
779 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
782 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.
783 self = flag; // for later usage with droptofloor()
786 flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
787 ctf_worldflaglist = flag;
789 setattachment(flag, world, "");
791 flag.netname = ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag");
792 flag.team = ((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2); // COLOR_TEAM1: color 4 team (red) - COLOR_TEAM2: color 13 team (blue)
793 flag.items = ((teamnumber) ? IT_KEY2 : IT_KEY1); // IT_KEY2: gold key (redish enough) - IT_KEY1: silver key (bluish enough)
794 flag.classname = "item_flag_team";
795 flag.target = "###item###"; // wut?
796 flag.flags = FL_ITEM | FL_NOTARGET;
797 flag.solid = SOLID_TRIGGER;
798 flag.takedamage = DAMAGE_NO;
799 flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
800 flag.max_flag_health = ((autocvar_g_ctf_flag_take_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
801 flag.health = flag.max_flag_health;
802 flag.event_damage = ctf_FlagDamage;
803 flag.pushable = TRUE;
804 flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
805 flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
806 flag.velocity = '0 0 0';
807 flag.mangle = flag.angles;
808 flag.reset = ctf_Reset;
809 flag.touch = ctf_FlagTouch;
810 flag.think = ctf_FlagThink;
811 flag.nextthink = time + FLAG_THINKRATE;
812 flag.ctf_status = FLAG_BASE;
814 if(!flag.model) { flag.model = ((teamnumber) ? autocvar_g_ctf_flag_red_model : autocvar_g_ctf_flag_blue_model); }
815 if(!flag.scale) { flag.scale = FLAG_SCALE; }
816 if(!flag.skin) { flag.skin = ((teamnumber) ? autocvar_g_ctf_flag_red_skin : autocvar_g_ctf_flag_blue_skin); }
819 if(!flag.snd_flag_taken) { flag.snd_flag_taken = ((teamnumber) ? "ctf/red_taken.wav" : "ctf/blue_taken.wav"); }
820 if(!flag.snd_flag_returned) { flag.snd_flag_returned = ((teamnumber) ? "ctf/red_returned.wav" : "ctf/blue_returned.wav"); }
821 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
822 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.
823 if(!flag.snd_flag_dropped) { flag.snd_flag_dropped = ((teamnumber) ? "ctf/red_dropped.wav" : "ctf/blue_dropped.wav"); }
824 if(!flag.snd_flag_touch) { flag.snd_flag_touch = "keepaway/touch.wav"; } // again has no team-based sound // FIXME
827 precache_sound(flag.snd_flag_taken);
828 precache_sound(flag.snd_flag_returned);
829 precache_sound(flag.snd_flag_capture);
830 precache_sound(flag.snd_flag_respawn);
831 precache_sound(flag.snd_flag_dropped);
832 precache_sound(flag.snd_flag_touch);
833 precache_model(flag.model);
834 precache_model("models/ctf/shield.md3");
835 precache_model("models/ctf/shockwavetransring.md3");
838 setmodel(flag, flag.model); // precision set below
839 setsize(flag, FLAG_MIN, FLAG_MAX);
840 setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
842 if(autocvar_g_ctf_flag_glowtrails)
844 flag.glow_color = ((teamnumber) ? 251 : 210); // 251: red - 210: blue
849 flag.effects |= EF_LOWPRECISION;
850 if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
851 if(autocvar_g_ctf_dynamiclights) { flag.effects |= ((teamnumber) ? EF_RED : EF_BLUE); }
854 if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
856 flag.dropped_origin = flag.origin;
858 flag.movetype = MOVETYPE_NONE;
860 else // drop to floor, automatically find a platform and set that as spawn origin
862 flag.noalign = FALSE;
865 flag.movetype = MOVETYPE_TOSS;
868 InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
876 MUTATOR_HOOKFUNCTION(ctf_HookedDrop)
878 if(self.flagcarried) { ctf_Handle_Throw(self, world, DROPTYPE_DROP); }
882 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
886 // initially clear items so they can be set as necessary later.
887 self.items &~= (IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST
888 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST | IT_CTF_SHIELDED);
890 // scan through all the flags and notify the client about them
891 for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
893 if(flag.ctf_status == FLAG_CARRY)
894 if(flag.owner == self)
895 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
897 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
898 else if(flag.ctf_status == FLAG_DROPPED)
899 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
902 // item for stopping players from capturing the flag too often
903 if(self.ctf_captureshielded)
904 self.items |= IT_CTF_SHIELDED;
906 // update the health of the flag carrier waypointsprite
907 if(self.wps_flagcarrier)
908 WaypointSprite_UpdateHealth(self.wps_flagcarrier, self.health);
913 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
915 if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
917 if(frag_target == frag_attacker) // damage done to yourself
919 frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
920 frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
922 else // damage done everyone else
924 frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
925 frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
931 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
934 return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
937 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
939 entity player = self;
941 if(time > player.throw_antispam)
943 // pass the flag to a team mate
944 if(autocvar_g_ctf_allow_pass)
946 entity head, closest_target;
947 head = findradius(player.origin, autocvar_g_ctf_pass_radius);
949 while(head) // find the closest acceptable target to pass to
951 if(head.classname == "player" && head.deadflag == DEAD_NO)
952 if(head != player && !IsDifferentTeam(head, player))
953 if(!player.speedrunning && !head.speedrunning)
955 traceline(player.origin, head.origin, MOVE_NOMONSTERS, player);
956 if not((trace_fraction < 1) && (trace_ent != head))
958 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
960 if(clienttype(head) == CLIENTTYPE_BOT)
962 centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname));
963 ctf_Handle_Throw(head, player, DROPTYPE_PASS);
967 centerprint(head, strcat(player.netname, " requests you to pass the ", head.flagcarried.netname));
968 centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname));
970 player.throw_antispam = time + autocvar_g_ctf_pass_wait;
973 else if(player.flagcarried)
975 if(closest_target) { if(vlen(player.origin - head.origin) < vlen(player.origin - closest_target.origin)) { closest_target = head; } }
976 else { closest_target = head; }
983 if(closest_target) { ctf_Handle_Throw(player, closest_target, DROPTYPE_PASS); return 0; }
986 // throw the flag in front of you
987 if(autocvar_g_ctf_allow_drop && player.flagcarried && !player.speedrunning)
988 { ctf_Handle_Throw(player, world, DROPTYPE_THROW); }
998 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
999 CTF Starting point for a player in team one (Red).
1000 Keys: "angle" viewing angle when spawning. */
1001 void spawnfunc_info_player_team1()
1003 if(g_assault) { remove(self); return; }
1005 self.team = COLOR_TEAM1; // red
1006 spawnfunc_info_player_deathmatch();
1010 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
1011 CTF Starting point for a player in team two (Blue).
1012 Keys: "angle" viewing angle when spawning. */
1013 void spawnfunc_info_player_team2()
1015 if(g_assault) { remove(self); return; }
1017 self.team = COLOR_TEAM2; // blue
1018 spawnfunc_info_player_deathmatch();
1021 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
1022 CTF Starting point for a player in team three (Yellow).
1023 Keys: "angle" viewing angle when spawning. */
1024 void spawnfunc_info_player_team3()
1026 if(g_assault) { remove(self); return; }
1028 self.team = COLOR_TEAM3; // yellow
1029 spawnfunc_info_player_deathmatch();
1033 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
1034 CTF Starting point for a player in team four (Purple).
1035 Keys: "angle" viewing angle when spawning. */
1036 void spawnfunc_info_player_team4()
1038 if(g_assault) { remove(self); return; }
1040 self.team = COLOR_TEAM4; // purple
1041 spawnfunc_info_player_deathmatch();
1044 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
1045 CTF flag for team one (Red).
1047 "angle" Angle the flag will point (minus 90 degrees)...
1048 "model" model to use, note this needs red and blue as skins 0 and 1...
1049 "noise" sound played when flag is picked up...
1050 "noise1" sound played when flag is returned by a teammate...
1051 "noise2" sound played when flag is captured...
1052 "noise3" sound played when flag is lost in the field and respawns itself...
1053 "noise4" sound played when flag is dropped by a player...
1054 "noise5" sound played when flag touches the ground... */
1055 void spawnfunc_item_flag_team1()
1057 if(!g_ctf) { remove(self); return; }
1059 ctf_FlagSetup(1, self); // 1 = red
1062 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
1063 CTF flag for team two (Blue).
1065 "angle" Angle the flag will point (minus 90 degrees)...
1066 "model" model to use, note this needs red and blue as skins 0 and 1...
1067 "noise" sound played when flag is picked up...
1068 "noise1" sound played when flag is returned by a teammate...
1069 "noise2" sound played when flag is captured...
1070 "noise3" sound played when flag is lost in the field and respawns itself...
1071 "noise4" sound played when flag is dropped by a player...
1072 "noise5" sound played when flag touches the ground... */
1073 void spawnfunc_item_flag_team2()
1075 if(!g_ctf) { remove(self); return; }
1077 ctf_FlagSetup(0, self); // the 0 is misleading, but -- 0 = blue.
1080 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
1081 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
1082 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.
1084 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
1085 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
1086 void spawnfunc_ctf_team()
1088 if(!g_ctf) { remove(self); return; }
1090 self.classname = "ctf_team";
1091 self.team = self.cnt + 1;
1099 // code from here on is just to support maps that don't have flag and team entities
1100 void ctf_SpawnTeam (string teamname, float teamcolor)
1105 self.classname = "ctf_team";
1106 self.netname = teamname;
1107 self.cnt = teamcolor;
1109 spawnfunc_ctf_team();
1114 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
1116 // if no teams are found, spawn defaults
1117 if(find(world, classname, "ctf_team") == world)
1119 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
1120 ctf_SpawnTeam("Red", COLOR_TEAM1 - 1);
1121 ctf_SpawnTeam("Blue", COLOR_TEAM2 - 1);
1127 void ctf_Initialize()
1129 ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
1131 ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
1132 ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
1133 ctf_captureshield_force = autocvar_g_ctf_shield_force;
1135 InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
1139 MUTATOR_DEFINITION(gamemode_ctf)
1141 MUTATOR_HOOK(MakePlayerObserver, ctf_HookedDrop, CBC_ORDER_ANY);
1142 MUTATOR_HOOK(ClientDisconnect, ctf_HookedDrop, CBC_ORDER_ANY);
1143 MUTATOR_HOOK(PlayerDies, ctf_HookedDrop, CBC_ORDER_ANY);
1144 MUTATOR_HOOK(PortalTeleport, ctf_HookedDrop, CBC_ORDER_ANY);
1145 MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
1146 MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
1147 MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
1148 MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
1152 if(time > 1) // game loads at time 1
1153 error("This is a game type and it cannot be added at runtime.");
1161 error("This is a game type and it cannot be removed at runtime.");