From: TimePath Date: Mon, 26 Oct 2015 00:15:17 +0000 (+1100) Subject: Merge branch 'master' into TimePath/items X-Git-Tag: xonotic-v0.8.2~1760^2~2 X-Git-Url: https://git.rm.cloudns.org/?a=commitdiff_plain;h=948c08402a7b9b7af36059104937650e343ecefe;p=xonotic%2Fxonotic-data.pk3dir.git Merge branch 'master' into TimePath/items # Conflicts: # qcsrc/server/mutators/gamemode_ctf.qh # qcsrc/server/t_items.qc --- 948c08402a7b9b7af36059104937650e343ecefe diff --cc qcsrc/common/weapons/weapon/devastator.qc index 47ab6460a,b9cc0358b..f5aed7b40 --- a/qcsrc/common/weapons/weapon/devastator.qc +++ b/qcsrc/common/weapons/weapon/devastator.qc @@@ -64,9 -64,11 +64,11 @@@ DEVASTATOR_SETTINGS(WEP_ADD_CVAR, WEP_A #endif #ifdef IMPLEMENTATION #ifdef SVQC -spawnfunc(weapon_devastator) { weapon_defaultspawnfunc(WEP_DEVASTATOR.m_id); } +spawnfunc(weapon_devastator) { weapon_defaultspawnfunc(this, WEP_DEVASTATOR); } spawnfunc(weapon_rocketlauncher) { spawnfunc_weapon_devastator(this); } + .entity lastrocket; + void W_Devastator_Unregister(void) {SELFPARAM(); if(self.realowner && self.realowner.lastrocket == self) diff --cc qcsrc/common/weapons/weapon/porto.qc index 2663d3dd6,790ab53ca..1d54b930c --- a/qcsrc/common/weapons/weapon/porto.qc +++ b/qcsrc/common/weapons/weapon/porto.qc @@@ -43,8 -44,15 +44,15 @@@ PORTO_SETTINGS(WEP_ADD_CVAR, WEP_ADD_PR #ifdef SVQC #include "../../triggers/trigger/jumppads.qh" -spawnfunc(weapon_porto) { weapon_defaultspawnfunc(WEP_PORTO.m_id); } +spawnfunc(weapon_porto) { weapon_defaultspawnfunc(this, WEP_PORTO); } + REGISTER_MUTATOR(porto_ticker, true); + MUTATOR_HOOKFUNCTION(porto_ticker, SV_StartFrame) { + entity e; + FOR_EACH_PLAYER(e) + e.porto_forbidden = max(0, e.porto_forbidden - 1); + } + void W_Porto_Success(void) {SELFPARAM(); if(self.realowner == world) diff --cc qcsrc/server/mutators/mutator/gamemode_ctf.qc index 000000000,73e992c95..924cc55f6 mode 000000,100644..100644 --- a/qcsrc/server/mutators/mutator/gamemode_ctf.qc +++ b/qcsrc/server/mutators/mutator/gamemode_ctf.qc @@@ -1,0 -1,2777 +1,2785 @@@ + #ifndef GAMEMODE_CTF_H + #define GAMEMODE_CTF_H + + #ifndef CSQC + void ctf_Initialize(); + + REGISTER_MUTATOR(ctf, false) + { + ActivateTeamplay(); + SetLimits(autocvar_capturelimit_override, -1, autocvar_captureleadlimit_override, -1); + have_team_spawns = -1; // request team spawns + + MUTATOR_ONADD + { + if (time > 1) // game loads at time 1 + error("This is a game type and it cannot be added at runtime."); + ctf_Initialize(); + } + + MUTATOR_ONROLLBACK_OR_REMOVE + { + // we actually cannot roll back ctf_Initialize here + // BUT: we don't need to! If this gets called, adding always + // succeeds. + } + + MUTATOR_ONREMOVE + { + LOG_INFO("This is a game type and it cannot be removed at runtime."); + return -1; + } + + return 0; + } + #endif + + #ifdef SVQC + // used in cheats.qc + void ctf_RespawnFlag(entity flag); + + // score rule declarations + const int ST_CTF_CAPS = 1; + const int SP_CTF_CAPS = 4; + const int SP_CTF_CAPTIME = 5; + const int SP_CTF_PICKUPS = 6; + const int SP_CTF_DROPS = 7; + const int SP_CTF_FCKILLS = 8; + const int SP_CTF_RETURNS = 9; + ++CLASS(Flag, Pickup) ++ ATTRIB(Flag, m_mins, vector, PL_MIN_CONST + '0 0 -13') ++ ATTRIB(Flag, m_maxs, vector, PL_MAX_CONST + '0 0 -13') ++ENDCLASS(Flag) ++Flag CTF_FLAG; STATIC_INIT(Flag) { CTF_FLAG = NEW(Flag); } ++void ctf_FlagTouch() { SELFPARAM(); ITEM_HANDLE(Pickup, CTF_FLAG, this, other); } ++ + // flag constants // for most of these, there is just one question to be asked: WHYYYYY? -#define FLAG_MIN (PL_MIN_CONST + '0 0 -13') -#define FLAG_MAX (PL_MAX_CONST + '0 0 -13') + + const float FLAG_SCALE = 0.6; + + const float FLAG_THINKRATE = 0.2; + const float FLAG_TOUCHRATE = 0.5; + const float WPFE_THINKRATE = 0.5; + + const vector FLAG_DROP_OFFSET = ('0 0 32'); + const vector FLAG_CARRY_OFFSET = ('-16 0 8'); + #define FLAG_SPAWN_OFFSET ('0 0 1' * (PL_MAX_CONST.z - 13)) + const vector FLAG_WAYPOINT_OFFSET = ('0 0 64'); + const vector FLAG_FLOAT_OFFSET = ('0 0 32'); + const vector FLAG_PASS_ARC_OFFSET = ('0 0 -10'); + + const vector VEHICLE_FLAG_OFFSET = ('0 0 96'); + const float VEHICLE_FLAG_SCALE = 1.0; + + // waypoint colors + #define WPCOLOR_ENEMYFC(t) ((t) ? colormapPaletteColor(t - 1, false) * 0.75 : '1 1 1') + #define WPCOLOR_FLAGCARRIER(t) (WP_FlagCarrier.m_color) + #define WPCOLOR_DROPPEDFLAG(t) ((t) ? ('0.25 0.25 0.25' + colormapPaletteColor(t - 1, false)) * 0.5 : '1 1 1') + + // sounds + #define snd_flag_taken noise + #define snd_flag_returned noise1 + #define snd_flag_capture noise2 + #define snd_flag_respawn noise3 + .string snd_flag_dropped; + .string snd_flag_touch; + .string snd_flag_pass; + + // effects + .string toucheffect; + .string passeffect; + .string capeffect; + + // list of flags on the map + entity ctf_worldflaglist; + .entity ctf_worldflagnext; + .entity ctf_staleflagnext; + + // waypoint sprites + .entity bot_basewaypoint; // flag waypointsprite + .entity wps_helpme; + .entity wps_flagbase; + .entity wps_flagcarrier; + .entity wps_flagdropped; + .entity wps_enemyflagcarrier; + .float wps_helpme_time; + bool wpforenemy_announced; + float wpforenemy_nextthink; + + // statuses + const int FLAG_BASE = 1; + const int FLAG_DROPPED = 2; + const int FLAG_CARRY = 3; + const int FLAG_PASSING = 4; + + const int DROP_NORMAL = 1; + const int DROP_THROW = 2; + const int DROP_PASS = 3; + const int DROP_RESET = 4; + + const int PICKUP_BASE = 1; + const int PICKUP_DROPPED = 2; + + const int CAPTURE_NORMAL = 1; + const int CAPTURE_DROPPED = 2; + + const int RETURN_TIMEOUT = 1; + const int RETURN_DROPPED = 2; + const int RETURN_DAMAGE = 3; + const int RETURN_SPEEDRUN = 4; + const int RETURN_NEEDKILL = 5; + ++void ctf_Handle_Throw(entity player, entity receiver, float droptype); ++ + // flag properties + #define ctf_spawnorigin dropped_origin + bool ctf_stalemate; // indicates that a stalemate is active + float ctf_captimerecord; // record time for capturing the flag + .float ctf_pickuptime; + .float ctf_droptime; + .int ctf_status; // status of the flag (FLAG_BASE, FLAG_DROPPED, FLAG_CARRY declared globally) + .entity ctf_dropper; // don't allow spam of dropping the flag + .int max_flag_health; + .float next_take_time; + .bool ctf_flagdamaged; + int ctf_teams; + + // passing/throwing properties + .float pass_distance; + .entity pass_sender; + .entity pass_target; + .float throw_antispam; + .float throw_prevtime; + .int throw_count; + + // CaptureShield: If the player is too bad to be allowed to capture, shield them from taking the flag. + .bool ctf_captureshielded; // set to 1 if the player is too bad to be allowed to capture + float ctf_captureshield_min_negscore; // punish at -20 points + float ctf_captureshield_max_ratio; // punish at most 30% of each team + float ctf_captureshield_force; // push force of the shield + + // 1 flag ctf + bool ctf_oneflag; // indicates whether or not a neutral flag has been found + + // bot player logic + const int HAVOCBOT_CTF_ROLE_NONE = 0; + const int HAVOCBOT_CTF_ROLE_DEFENSE = 2; + const int HAVOCBOT_CTF_ROLE_MIDDLE = 4; + const int HAVOCBOT_CTF_ROLE_OFFENSE = 8; + const int HAVOCBOT_CTF_ROLE_CARRIER = 16; + const int HAVOCBOT_CTF_ROLE_RETRIEVER = 32; + const int HAVOCBOT_CTF_ROLE_ESCORT = 64; + + .bool havocbot_cantfindflag; + + vector havocbot_ctf_middlepoint; + float havocbot_ctf_middlepoint_radius; + + void havocbot_role_ctf_setrole(entity bot, int role); + + // team checking + #define CTF_SAMETEAM(a,b) ((autocvar_g_ctf_reverse || (ctf_oneflag && autocvar_g_ctf_oneflag_reverse)) ? DIFF_TEAM(a,b) : SAME_TEAM(a,b)) + #define CTF_DIFFTEAM(a,b) ((autocvar_g_ctf_reverse || (ctf_oneflag && autocvar_g_ctf_oneflag_reverse)) ? SAME_TEAM(a,b) : DIFF_TEAM(a,b)) + + // networked flag statuses + .int ctf_flagstatus; + #endif + + const int CTF_RED_FLAG_TAKEN = 1; + const int CTF_RED_FLAG_LOST = 2; + const int CTF_RED_FLAG_CARRYING = 3; + const int CTF_BLUE_FLAG_TAKEN = 4; + const int CTF_BLUE_FLAG_LOST = 8; + const int CTF_BLUE_FLAG_CARRYING = 12; + const int CTF_YELLOW_FLAG_TAKEN = 16; + const int CTF_YELLOW_FLAG_LOST = 32; + const int CTF_YELLOW_FLAG_CARRYING = 48; + const int CTF_PINK_FLAG_TAKEN = 64; + const int CTF_PINK_FLAG_LOST = 128; + const int CTF_PINK_FLAG_CARRYING = 192; + const int CTF_NEUTRAL_FLAG_TAKEN = 256; + const int CTF_NEUTRAL_FLAG_LOST = 512; + const int CTF_NEUTRAL_FLAG_CARRYING = 768; + const int CTF_FLAG_NEUTRAL = 2048; + const int CTF_SHIELDED = 4096; + #endif + + #ifdef IMPLEMENTATION + + #ifdef SVQC + #include "../../../common/vehicles/all.qh" + #include "../../teamplay.qh" + #endif + + #include "../../../lib/warpzone/common.qh" + + bool autocvar_g_ctf_allow_vehicle_carry; + bool autocvar_g_ctf_allow_vehicle_touch; + bool autocvar_g_ctf_allow_monster_touch; + bool autocvar_g_ctf_throw; + float autocvar_g_ctf_throw_angle_max; + float autocvar_g_ctf_throw_angle_min; + int autocvar_g_ctf_throw_punish_count; + float autocvar_g_ctf_throw_punish_delay; + float autocvar_g_ctf_throw_punish_time; + float autocvar_g_ctf_throw_strengthmultiplier; + float autocvar_g_ctf_throw_velocity_forward; + float autocvar_g_ctf_throw_velocity_up; + float autocvar_g_ctf_drop_velocity_up; + float autocvar_g_ctf_drop_velocity_side; + bool autocvar_g_ctf_oneflag_reverse; + bool autocvar_g_ctf_portalteleport; + bool autocvar_g_ctf_pass; + float autocvar_g_ctf_pass_arc; + float autocvar_g_ctf_pass_arc_max; + float autocvar_g_ctf_pass_directional_max; + float autocvar_g_ctf_pass_directional_min; + float autocvar_g_ctf_pass_radius; + float autocvar_g_ctf_pass_wait; + bool autocvar_g_ctf_pass_request; + float autocvar_g_ctf_pass_turnrate; + float autocvar_g_ctf_pass_timelimit; + float autocvar_g_ctf_pass_velocity; + bool autocvar_g_ctf_dynamiclights; + float autocvar_g_ctf_flag_collect_delay; + float autocvar_g_ctf_flag_damageforcescale; + bool autocvar_g_ctf_flag_dropped_waypoint; + bool autocvar_g_ctf_flag_dropped_floatinwater; + bool autocvar_g_ctf_flag_glowtrails; + int autocvar_g_ctf_flag_health; + bool autocvar_g_ctf_flag_return; + float autocvar_g_ctf_flag_return_carried_radius; + float autocvar_g_ctf_flag_return_time; + bool autocvar_g_ctf_flag_return_when_unreachable; + float autocvar_g_ctf_flag_return_damage; + float autocvar_g_ctf_flag_return_damage_delay; + float autocvar_g_ctf_flag_return_dropped; + float autocvar_g_ctf_flagcarrier_auto_helpme_damage; + float autocvar_g_ctf_flagcarrier_auto_helpme_time; + float autocvar_g_ctf_flagcarrier_selfdamagefactor; + float autocvar_g_ctf_flagcarrier_selfforcefactor; + float autocvar_g_ctf_flagcarrier_damagefactor; + float autocvar_g_ctf_flagcarrier_forcefactor; + //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting; + bool autocvar_g_ctf_fullbrightflags; + bool autocvar_g_ctf_ignore_frags; + int autocvar_g_ctf_score_capture; + int autocvar_g_ctf_score_capture_assist; + int autocvar_g_ctf_score_kill; + int autocvar_g_ctf_score_penalty_drop; + int autocvar_g_ctf_score_penalty_returned; + int autocvar_g_ctf_score_pickup_base; + int autocvar_g_ctf_score_pickup_dropped_early; + int autocvar_g_ctf_score_pickup_dropped_late; + int autocvar_g_ctf_score_return; + float autocvar_g_ctf_shield_force; + float autocvar_g_ctf_shield_max_ratio; + int autocvar_g_ctf_shield_min_negscore; + bool autocvar_g_ctf_stalemate; + int autocvar_g_ctf_stalemate_endcondition; + float autocvar_g_ctf_stalemate_time; + bool autocvar_g_ctf_reverse; + float autocvar_g_ctf_dropped_capture_delay; + float autocvar_g_ctf_dropped_capture_radius; + + void ctf_FakeTimeLimit(entity e, float t) + { + msg_entity = e; + WriteByte(MSG_ONE, 3); // svc_updatestat + WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT + if(t < 0) + WriteCoord(MSG_ONE, autocvar_timelimit); + else + WriteCoord(MSG_ONE, (t + 1) / 60); + } + + void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later + { + if(autocvar_sv_eventlog) + GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != world) ? ftos(actor.playerid) : ""))); + //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : ""))); + } + + void ctf_CaptureRecord(entity flag, entity player) + { + float cap_record = ctf_captimerecord; + float cap_time = (time - flag.ctf_pickuptime); + string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname")); + + // notify about shit + if(ctf_oneflag) { Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname); } + else if(!ctf_captimerecord) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_TIME_), player.netname, (cap_time * 100)); } + else if(cap_time < cap_record) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_BROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); } + else { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_UNBROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); } + + // write that shit in the database + if(!ctf_oneflag) // but not in 1-flag mode + if((!ctf_captimerecord) || (cap_time < cap_record)) + { + ctf_captimerecord = cap_time; + db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time)); + db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname); + write_recordmarker(player, (time - cap_time), cap_time); + } + } + + void ctf_FlagcarrierWaypoints(entity player) + { + WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, true, RADARICON_FLAG); + WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2); + WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id)); + WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team)); + } + + void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate) + { + float current_distance = vlen((('1 0 0' * to.x) + ('0 1 0' * to.y)) - (('1 0 0' * from.x) + ('0 1 0' * from.y))); // for the sake of this check, exclude Z axis + float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc))); + float current_height = (initial_height * min(1, (current_distance / flag.pass_distance))); + //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n"); + + vector targpos; + if(current_height) // make sure we can actually do this arcing path + { + targpos = (to + ('0 0 1' * current_height)); + WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag); + if(trace_fraction < 1) + { + //print("normal arc line failed, trying to find new pos..."); + WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag); + targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET); + WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag); + if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ } + /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */ + } + } + else { targpos = to; } + + //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y)); + + vector desired_direction = normalize(targpos - from); + if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); } + else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); } + } + + bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer) + { + if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min) + { + // directional tracing only + float spreadlimit; + makevectors(passer_angle); + + // find the closest point on the enemy to the center of the attack + float h; // hypotenuse, which is the distance between attacker to head + float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin + + h = vlen(head_center - passer_center); + a = h * (normalize(head_center - passer_center) * v_forward); + + vector nearest_on_line = (passer_center + a * v_forward); + float distance_from_line = vlen(nearest_to_passer - nearest_on_line); + + spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1); + spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit); + + if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90)) + { return true; } + else + { return false; } + } + else { return true; } + } + + + // ======================= + // CaptureShield Functions + // ======================= + + bool ctf_CaptureShield_CheckStatus(entity p) + { + int s, s2, s3, s4, se, se2, se3, se4, sr, ser; + entity e; + int players_worseeq, players_total; + + if(ctf_captureshield_max_ratio <= 0) + return false; + + s = PlayerScore_Add(p, SP_CTF_CAPS, 0); + s2 = PlayerScore_Add(p, SP_CTF_PICKUPS, 0); + s3 = PlayerScore_Add(p, SP_CTF_RETURNS, 0); + s4 = PlayerScore_Add(p, SP_CTF_FCKILLS, 0); + + sr = ((s - s2) + (s3 + s4)); + + if(sr >= -ctf_captureshield_min_negscore) + return false; + + players_total = players_worseeq = 0; + FOR_EACH_PLAYER(e) + { + if(DIFF_TEAM(e, p)) + continue; + se = PlayerScore_Add(e, SP_CTF_CAPS, 0); + se2 = PlayerScore_Add(e, SP_CTF_PICKUPS, 0); + se3 = PlayerScore_Add(e, SP_CTF_RETURNS, 0); + se4 = PlayerScore_Add(e, SP_CTF_FCKILLS, 0); + + ser = ((se - se2) + (se3 + se4)); + + if(ser <= sr) + ++players_worseeq; + ++players_total; + } + + // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse + // use this rule here + + if(players_worseeq >= players_total * ctf_captureshield_max_ratio) + return false; + + return true; + } + + void ctf_CaptureShield_Update(entity player, bool wanted_status) + { + bool updated_status = ctf_CaptureShield_CheckStatus(player); + if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only + { + Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE)); + player.ctf_captureshielded = updated_status; + } + } + + bool ctf_CaptureShield_Customize() + {SELFPARAM(); + if(!other.ctf_captureshielded) { return false; } + if(CTF_SAMETEAM(self, other)) { return false; } + + return true; + } + + void ctf_CaptureShield_Touch() + {SELFPARAM(); + if(!other.ctf_captureshielded) { return; } + if(CTF_SAMETEAM(self, other)) { return; } + + vector mymid = (self.absmin + self.absmax) * 0.5; + vector othermid = (other.absmin + other.absmax) * 0.5; + + Damage(other, self, self, 0, DEATH_HURTTRIGGER.m_id, mymid, normalize(othermid - mymid) * ctf_captureshield_force); + if(IS_REAL_CLIENT(other)) { Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); } + } + + void ctf_CaptureShield_Spawn(entity flag) + {SELFPARAM(); + entity shield = spawn(); + + shield.enemy = self; + shield.team = self.team; + shield.touch = ctf_CaptureShield_Touch; + shield.customizeentityforclient = ctf_CaptureShield_Customize; + shield.classname = "ctf_captureshield"; + shield.effects = EF_ADDITIVE; + shield.movetype = MOVETYPE_NOCLIP; + shield.solid = SOLID_TRIGGER; + shield.avelocity = '7 0 11'; + shield.scale = 0.5; + + setorigin(shield, self.origin); + setmodel(shield, MDL_CTF_SHIELD); + setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs); + } + + + // ==================== + // Drop/Pass/Throw Code + // ==================== + + void ctf_Handle_Drop(entity flag, entity player, int droptype) + { + // declarations + player = (player ? player : flag.pass_sender); + + // main + flag.movetype = MOVETYPE_TOSS; + flag.takedamage = DAMAGE_YES; + flag.angles = '0 0 0'; + flag.health = flag.max_flag_health; + flag.ctf_droptime = time; + flag.ctf_dropper = player; + flag.ctf_status = FLAG_DROPPED; + + // messages and sounds + Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_LOST_) : INFO_CTF_LOST_NEUTRAL), player.netname); + _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE); + ctf_EventLog("dropped", player.team, player); + + // scoring + PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop); + PlayerScore_Add(player, SP_CTF_DROPS, 1); + + // waypoints + if(autocvar_g_ctf_flag_dropped_waypoint) { + entity wp = WaypointSprite_Spawn(WP_FlagDropped, 0, 0, flag, FLAG_WAYPOINT_OFFSET, world, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, true, RADARICON_FLAG); + wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team); + } + + if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health)) + { + WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health); + WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); + } + + player.throw_antispam = time + autocvar_g_ctf_pass_wait; + + if(droptype == DROP_PASS) + { + flag.pass_distance = 0; + flag.pass_sender = world; + flag.pass_target = world; + } + } + + void ctf_Handle_Retrieve(entity flag, entity player) + { + entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players + entity sender = flag.pass_sender; + + // transfer flag to player + flag.owner = player; + flag.owner.flagcarried = flag; + + // reset flag + if(player.vehicle) + { + setattachment(flag, player.vehicle, ""); + setorigin(flag, VEHICLE_FLAG_OFFSET); + flag.scale = VEHICLE_FLAG_SCALE; + } + else + { + setattachment(flag, player, ""); + setorigin(flag, FLAG_CARRY_OFFSET); + } + flag.movetype = MOVETYPE_NONE; + flag.takedamage = DAMAGE_NO; + flag.solid = SOLID_NOT; + flag.angles = '0 0 0'; + flag.ctf_status = FLAG_CARRY; + + // messages and sounds + _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM); + ctf_EventLog("receive", flag.team, player); + + FOR_EACH_REALPLAYER(tmp_player) + { + if(tmp_player == sender) + Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_SENT_) : CENTER_CTF_PASS_SENT_NEUTRAL), player.netname); + else if(tmp_player == player) + Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_RECEIVED_) : CENTER_CTF_PASS_RECEIVED_NEUTRAL), sender.netname); + else if(SAME_TEAM(tmp_player, sender)) + Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_OTHER_) : CENTER_CTF_PASS_OTHER_NEUTRAL), sender.netname, player.netname); + } + + // create new waypoint + ctf_FlagcarrierWaypoints(player); + + sender.throw_antispam = time + autocvar_g_ctf_pass_wait; + player.throw_antispam = sender.throw_antispam; + + flag.pass_distance = 0; + flag.pass_sender = world; + flag.pass_target = world; + } + + void ctf_Handle_Throw(entity player, entity receiver, int droptype) + { + entity flag = player.flagcarried; + vector targ_origin, flag_velocity; + + if(!flag) { return; } + if((droptype == DROP_PASS) && !receiver) { return; } + + if(flag.speedrunning) { ctf_RespawnFlag(flag); return; } + + // reset the flag + setattachment(flag, world, ""); + setorigin(flag, player.origin + FLAG_DROP_OFFSET); + flag.owner.flagcarried = world; + flag.owner = world; + flag.solid = SOLID_TRIGGER; + flag.ctf_dropper = player; + flag.ctf_droptime = time; + + flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS + + switch(droptype) + { + case DROP_PASS: + { + // warpzone support: + // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver + // findradius has already put wzn ... wz1 into receiver's warpzone parameters! + WarpZone_RefSys_Copy(flag, receiver); + WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver + targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag + + flag.pass_distance = vlen((('1 0 0' * targ_origin.x) + ('0 1 0' * targ_origin.y)) - (('1 0 0' * player.origin.x) + ('0 1 0' * player.origin.y))); // for the sake of this check, exclude Z axis + ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false); + + // main + flag.movetype = MOVETYPE_FLY; + flag.takedamage = DAMAGE_NO; + flag.pass_sender = player; + flag.pass_target = receiver; + flag.ctf_status = FLAG_PASSING; + + // other + _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM); + WarpZone_TrailParticles(world, _particleeffectnum(flag.passeffect), player.origin, targ_origin); + ctf_EventLog("pass", flag.team, player); + break; + } + + case DROP_THROW: + { + makevectors((player.v_angle.y * '0 1 0') + (bound(autocvar_g_ctf_throw_angle_min, player.v_angle.x, autocvar_g_ctf_throw_angle_max) * '1 0 0')); + + flag_velocity = (('0 0 1' * autocvar_g_ctf_throw_velocity_up) + ((v_forward * autocvar_g_ctf_throw_velocity_forward) * ((player.items & ITEM_Strength.m_itemid) ? autocvar_g_ctf_throw_strengthmultiplier : 1))); + flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, false); + ctf_Handle_Drop(flag, player, droptype); + break; + } + + case DROP_RESET: + { + flag.velocity = '0 0 0'; // do nothing + break; + } + + default: + case DROP_NORMAL: + { + flag.velocity = W_CalculateProjectileVelocity(player.velocity, (('0 0 1' * autocvar_g_ctf_drop_velocity_up) + ((('0 1 0' * crandom()) + ('1 0 0' * crandom())) * autocvar_g_ctf_drop_velocity_side)), false); + ctf_Handle_Drop(flag, player, droptype); + break; + } + } + + // kill old waypointsprite + WaypointSprite_Ping(player.wps_flagcarrier); + WaypointSprite_Kill(player.wps_flagcarrier); + + if(player.wps_enemyflagcarrier) + WaypointSprite_Kill(player.wps_enemyflagcarrier); + + // captureshield + ctf_CaptureShield_Update(player, 0); // shield player from picking up flag + } + + + // ============== + // Event Handlers + // ============== + + void ctf_Handle_Capture(entity flag, entity toucher, int capturetype) + { + entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher); + entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper); + entity player_team_flag = world, tmp_entity; + float old_time, new_time; + + if(!player) { return; } // without someone to give the reward to, we can't possibly cap + if(CTF_DIFFTEAM(player, flag)) { return; } + + if(ctf_oneflag) + for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext) + if(SAME_TEAM(tmp_entity, player)) + { + player_team_flag = tmp_entity; + break; + } + + nades_GiveBonus(player, autocvar_g_nades_bonus_score_high ); + + player.throw_prevtime = time; + player.throw_count = 0; + + // messages and sounds + Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((enemy_flag.team) ? APP_TEAM_ENT_4(enemy_flag, CENTER_CTF_CAPTURE_) : CENTER_CTF_CAPTURE_NEUTRAL)); + ctf_CaptureRecord(enemy_flag, player); + _sound(player, CH_TRIGGER, ((ctf_oneflag) ? player_team_flag.snd_flag_capture : ((DIFF_TEAM(player, flag)) ? enemy_flag.snd_flag_capture : flag.snd_flag_capture)), VOL_BASE, ATTEN_NONE); + + switch(capturetype) + { + case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break; + case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break; + default: break; + } + + // scoring + PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture); + PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1); + + old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0); + new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime); + if(!old_time || new_time < old_time) + PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time); + + // effects + Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1); + //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1); + + // other + if(capturetype == CAPTURE_NORMAL) + { + WaypointSprite_Kill(player.wps_flagcarrier); + if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); } + + if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper)) + { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); } + } + + // reset the flag + player.next_take_time = time + autocvar_g_ctf_flag_collect_delay; + ctf_RespawnFlag(enemy_flag); + } + + void ctf_Handle_Return(entity flag, entity player) + { + // messages and sounds + if(IS_MONSTER(player)) + { + Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_MONSTER_), player.monster_name); + } + else if(flag.team) + { + Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_RETURN_)); + Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_), player.netname); + } + _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE); + ctf_EventLog("return", flag.team, player); + + // scoring + if(IS_PLAYER(player)) + { + PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return + PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns + + nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium); + } + + TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it + + if(flag.ctf_dropper) + { + PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag + ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag + flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time + } + + // other + if(player.flagcarried == flag) + WaypointSprite_Kill(player.wps_flagcarrier); + + // reset the flag + ctf_RespawnFlag(flag); + } + + void ctf_Handle_Pickup(entity flag, entity player, int pickuptype) + { + // declarations + float pickup_dropped_score; // used to calculate dropped pickup score + entity tmp_entity; // temporary entity + + // attach the flag to the player + flag.owner = player; + player.flagcarried = flag; + if(player.vehicle) + { + setattachment(flag, player.vehicle, ""); + setorigin(flag, VEHICLE_FLAG_OFFSET); + flag.scale = VEHICLE_FLAG_SCALE; + } + else + { + setattachment(flag, player, ""); + setorigin(flag, FLAG_CARRY_OFFSET); + } + + // flag setup + flag.movetype = MOVETYPE_NONE; + flag.takedamage = DAMAGE_NO; + flag.solid = SOLID_NOT; + flag.angles = '0 0 0'; + flag.ctf_status = FLAG_CARRY; + + switch(pickuptype) + { + case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs + case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit + default: break; + } + + // messages and sounds + Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_PICKUP_) : INFO_CTF_PICKUP_NEUTRAL), player.netname); + if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); } + if(!flag.team) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL); } + else if(CTF_DIFFTEAM(player, flag)) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PICKUP_)); } + else { Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_TEAM : CENTER_CTF_PICKUP_TEAM_ENEMY), Team_ColorCode(flag.team)); } + + Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, ((flag.team) ? APP_TEAM_ENT_4(flag, CHOICE_CTF_PICKUP_TEAM_) : CHOICE_CTF_PICKUP_TEAM_NEUTRAL), Team_ColorCode(player.team), player.netname); + + if(!flag.team) + FOR_EACH_PLAYER(tmp_entity) + if(tmp_entity != player) + if(DIFF_TEAM(player, tmp_entity)) + Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, CHOICE_CTF_PICKUP_ENEMY_NEUTRAL, Team_ColorCode(player.team), player.netname); + + if(flag.team) + FOR_EACH_PLAYER(tmp_entity) + if(tmp_entity != player) + if(CTF_SAMETEAM(flag, tmp_entity)) + if(SAME_TEAM(player, tmp_entity)) + Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_PICKUP_TEAM_), Team_ColorCode(player.team), player.netname); + else + Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, ((SAME_TEAM(flag, player)) ? CHOICE_CTF_PICKUP_ENEMY_TEAM : CHOICE_CTF_PICKUP_ENEMY), Team_ColorCode(player.team), player.netname); + + _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE); + + // scoring + PlayerScore_Add(player, SP_CTF_PICKUPS, 1); + nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor); + switch(pickuptype) + { + case PICKUP_BASE: + { + PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base); + ctf_EventLog("steal", flag.team, player); + break; + } + + case PICKUP_DROPPED: + { + pickup_dropped_score = (autocvar_g_ctf_flag_return_time ? bound(0, ((flag.ctf_droptime + autocvar_g_ctf_flag_return_time) - time) / autocvar_g_ctf_flag_return_time, 1) : 1); + pickup_dropped_score = floor((autocvar_g_ctf_score_pickup_dropped_late * (1 - pickup_dropped_score) + autocvar_g_ctf_score_pickup_dropped_early * pickup_dropped_score) + 0.5); + LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n"); + PlayerTeamScore_AddScore(player, pickup_dropped_score); + ctf_EventLog("pickup", flag.team, player); + break; + } + + default: break; + } + + // speedrunning + if(pickuptype == PICKUP_BASE) + { + flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record + if((player.speedrunning) && (ctf_captimerecord)) + ctf_FakeTimeLimit(player, time + ctf_captimerecord); + } + + // effects + Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1); + + // waypoints + if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); } + ctf_FlagcarrierWaypoints(player); + WaypointSprite_Ping(player.wps_flagcarrier); + } + + + // =================== + // Main Flag Functions + // =================== + + void ctf_CheckFlagReturn(entity flag, int returntype) + { + if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING)) + { + if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); } + + if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time)) + { + switch(returntype) + { + case RETURN_DROPPED: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DROPPED_) : INFO_CTF_FLAGRETURN_DROPPED_NEUTRAL)); break; + case RETURN_DAMAGE: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DAMAGED_) : INFO_CTF_FLAGRETURN_DAMAGED_NEUTRAL)); break; + case RETURN_SPEEDRUN: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_SPEEDRUN_) : INFO_CTF_FLAGRETURN_SPEEDRUN_NEUTRAL), ctf_captimerecord); break; + case RETURN_NEEDKILL: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_NEEDKILL_) : INFO_CTF_FLAGRETURN_NEEDKILL_NEUTRAL)); break; + + default: + case RETURN_TIMEOUT: + { Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_TIMEOUT_) : INFO_CTF_FLAGRETURN_TIMEOUT_NEUTRAL)); break; } + } + _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE); + ctf_EventLog("returned", flag.team, world); + ctf_RespawnFlag(flag); + } + } + } + + bool ctf_Stalemate_Customize() + {SELFPARAM(); + // make spectators see what the player would see + entity e, wp_owner; + e = WaypointSprite_getviewentity(other); + wp_owner = self.owner; + + // team waypoints + if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; } + if(SAME_TEAM(wp_owner, e)) { return false; } + if(!IS_PLAYER(e)) { return false; } + + return true; + } + + void ctf_CheckStalemate(void) + { + // declarations + int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0; + entity tmp_entity; + + entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs + + // build list of stale flags + for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext) + { + if(autocvar_g_ctf_stalemate) + if(tmp_entity.ctf_status != FLAG_BASE) + if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag + { + tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist + ctf_staleflaglist = tmp_entity; + + switch(tmp_entity.team) + { + case NUM_TEAM_1: ++stale_red_flags; break; + case NUM_TEAM_2: ++stale_blue_flags; break; + case NUM_TEAM_3: ++stale_yellow_flags; break; + case NUM_TEAM_4: ++stale_pink_flags; break; + default: ++stale_neutral_flags; break; + } + } + } + + if(ctf_oneflag) + stale_flags = (stale_neutral_flags >= 1); + else + stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1); + + if(ctf_oneflag && stale_flags == 1) + ctf_stalemate = true; + else if(stale_flags >= 2) + ctf_stalemate = true; + else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2) + { ctf_stalemate = false; wpforenemy_announced = false; } + else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1) + { ctf_stalemate = false; wpforenemy_announced = false; } + + // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary + if(ctf_stalemate) + { + for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext) + { + if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier)) + { + entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, tmp_entity.owner, FLAG_WAYPOINT_OFFSET, world, 0, tmp_entity.owner, wps_enemyflagcarrier, true, RADARICON_FLAG); + wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team); + tmp_entity.owner.wps_enemyflagcarrier.customizeentityforclient = ctf_Stalemate_Customize; + } + } + + if (!wpforenemy_announced) + { + FOR_EACH_REALPLAYER(tmp_entity) + Send_Notification(NOTIF_ONE, tmp_entity, MSG_CENTER, ((tmp_entity.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER)); + + wpforenemy_announced = true; + } + } + } + + void ctf_FlagDamage(entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force) + {SELFPARAM(); + if(ITEM_DAMAGE_NEEDKILL(deathtype)) + { + if(autocvar_g_ctf_flag_return_damage_delay) + { + self.ctf_flagdamaged = true; + } + else + { + self.health = 0; + ctf_CheckFlagReturn(self, RETURN_NEEDKILL); + } + return; + } + if(autocvar_g_ctf_flag_return_damage) + { + // reduce health and check if it should be returned + self.health = self.health - damage; + ctf_CheckFlagReturn(self, RETURN_DAMAGE); + return; + } + } + + void ctf_FlagThink() + {SELFPARAM(); + // declarations + entity tmp_entity; + + self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary. + + // captureshield + if(self == ctf_worldflaglist) // only for the first flag + FOR_EACH_CLIENT(tmp_entity) + ctf_CaptureShield_Update(tmp_entity, 1); // release shield only + + // sanity checks - if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished ++ if(self.mins != CTF_FLAG.m_mins || self.maxs != CTF_FLAG.m_maxs) { // reset the flag boundaries in case it got squished + LOG_TRACE("wtf the flag got squashed?\n"); - tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self); ++ tracebox(self.origin, CTF_FLAG.m_mins, CTF_FLAG.m_maxs, self.origin, MOVE_NOMONSTERS, self); + if(!trace_startsolid || self.noalign) // can we resize it without getting stuck? - setsize(self, FLAG_MIN, FLAG_MAX); } ++ setsize(self, CTF_FLAG.m_mins, CTF_FLAG.m_maxs); } + + switch(self.ctf_status) // reset flag angles in case warpzones adjust it + { + case FLAG_DROPPED: + { + self.angles = '0 0 0'; + break; + } + + default: break; + } + + // main think method + switch(self.ctf_status) + { + case FLAG_BASE: + { + if(autocvar_g_ctf_dropped_capture_radius) + { + for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext) + if(tmp_entity.ctf_status == FLAG_DROPPED) + if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius) + if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay) + ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED); + } + return; + } + + case FLAG_DROPPED: + { + if(autocvar_g_ctf_flag_dropped_floatinwater) + { + vector midpoint = ((self.absmin + self.absmax) * 0.5); + if(pointcontents(midpoint) == CONTENT_WATER) + { + self.velocity = self.velocity * 0.5; + + if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER) + { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; } + else + { self.movetype = MOVETYPE_FLY; } + } + else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; } + } + if(autocvar_g_ctf_flag_return_dropped) + { + if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1)) + { + self.health = 0; + ctf_CheckFlagReturn(self, RETURN_DROPPED); + return; + } + } + if(self.ctf_flagdamaged) + { + self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE); + ctf_CheckFlagReturn(self, RETURN_NEEDKILL); + return; + } + else if(autocvar_g_ctf_flag_return_time) + { + self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE); + ctf_CheckFlagReturn(self, RETURN_TIMEOUT); + return; + } + return; + } + + case FLAG_CARRY: + { + if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord)) + { + self.health = 0; + ctf_CheckFlagReturn(self, RETURN_SPEEDRUN); + + setself(self.owner); + self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set + ImpulseCommands(); + setself(this); + } + if(autocvar_g_ctf_stalemate) + { + if(time >= wpforenemy_nextthink) + { + ctf_CheckStalemate(); + wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check) + } + } + if(CTF_SAMETEAM(self, self.owner) && self.team) + { + if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed + ctf_Handle_Throw(self.owner, world, DROP_THROW); + else if(vlen(self.owner.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_carried_radius) + ctf_Handle_Return(self, self.owner); + } + return; + } + + case FLAG_PASSING: + { + vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5); + targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us) + WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self); + + if((self.pass_target == world) + || (self.pass_target.deadflag != DEAD_NO) + || (self.pass_target.flagcarried) + || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius) + || ((trace_fraction < 1) && (trace_ent != self.pass_target)) + || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit)) + { + // give up, pass failed + ctf_Handle_Drop(self, world, DROP_PASS); + } + else + { + // still a viable target, go for it + ctf_CalculatePassVelocity(self, targ_origin, self.origin, true); + } + return; + } + + default: // this should never happen + { + LOG_TRACE("ctf_FlagThink(): Flag exists with no status?\n"); + return; + } + } + } + -void ctf_FlagTouch() -{SELFPARAM(); ++METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher)) ++{ ++ return = false; + if(gameover) { return; } + if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; } + - entity toucher = other, tmp_entity; - bool is_not_monster = (!IS_MONSTER(toucher)), num_perteam = 0; ++ bool is_not_monster = (!IS_MONSTER(toucher)); + + // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces + if(ITEM_TOUCH_NEEDKILL()) + { + if(!autocvar_g_ctf_flag_return_damage_delay) + { - self.health = 0; - ctf_CheckFlagReturn(self, RETURN_NEEDKILL); ++ flag.health = 0; ++ ctf_CheckFlagReturn(flag, RETURN_NEEDKILL); + } - if(!self.ctf_flagdamaged) { return; } ++ if(!flag.ctf_flagdamaged) { return; } + } + - FOR_EACH_PLAYER(tmp_entity) if(SAME_TEAM(toucher, tmp_entity)) { ++num_perteam; } ++ int num_perteam = 0; ++ entity tmp_entity; FOR_EACH_PLAYER(tmp_entity) if(SAME_TEAM(toucher, tmp_entity)) { ++num_perteam; } + + // special touch behaviors + if(toucher.frozen) { return; } + else if(IS_VEHICLE(toucher)) + { + if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner) + toucher = toucher.owner; // the player is actually the vehicle owner, not other + else + return; // do nothing + } + else if(IS_MONSTER(toucher)) + { + if(!autocvar_g_ctf_allow_monster_touch) + return; // do nothing + } + else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world + { - if(time > self.wait) // if we haven't in a while, play a sound/effect ++ if(time > flag.wait) // if we haven't in a while, play a sound/effect + { - Send_Effect_(self.toucheffect, self.origin, '0 0 0', 1); - _sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTEN_NORM); - self.wait = time + FLAG_TOUCHRATE; ++ Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1); ++ _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM); ++ flag.wait = time + FLAG_TOUCHRATE; + } + return; + } + else if(toucher.deadflag != DEAD_NO) { return; } + - switch(self.ctf_status) ++ switch(flag.ctf_status) + { + case FLAG_BASE: + { + if(ctf_oneflag) + { - if(CTF_SAMETEAM(toucher, self) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster) - ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base - else if(!self.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster) - ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the neutral flag ++ if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster) ++ ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base ++ else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster) ++ ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag + } - else if(CTF_SAMETEAM(toucher, self) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, self) && is_not_monster) - ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base - else if(CTF_DIFFTEAM(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster) - ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag ++ else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster) ++ ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base ++ else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster) ++ ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag + break; + } + + case FLAG_DROPPED: + { - if(CTF_SAMETEAM(toucher, self) && (autocvar_g_ctf_flag_return || num_perteam <= 1) && self.team) // automatically return if there's only 1 player on the team - ctf_Handle_Return(self, toucher); // toucher just returned his own flag - else if(is_not_monster && (!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay))) - ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag ++ if(CTF_SAMETEAM(toucher, flag) && (autocvar_g_ctf_flag_return || num_perteam <= 1) && flag.team) // automatically return if there's only 1 player on the team ++ ctf_Handle_Return(flag, toucher); // toucher just returned his own flag ++ else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay))) ++ ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag + break; + } + + case FLAG_CARRY: + { + LOG_TRACE("Someone touched a flag even though it was being carried?\n"); + break; + } + + case FLAG_PASSING: + { - if((IS_PLAYER(toucher)) && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender)) ++ if((IS_PLAYER(toucher)) && (toucher.deadflag == DEAD_NO) && (toucher != flag.pass_sender)) + { - if(DIFF_TEAM(toucher, self.pass_sender)) - ctf_Handle_Return(self, toucher); ++ if(DIFF_TEAM(toucher, flag.pass_sender)) ++ ctf_Handle_Return(flag, toucher); + else - ctf_Handle_Retrieve(self, toucher); ++ ctf_Handle_Retrieve(flag, toucher); + } + break; + } + } + } + + .float last_respawn; + void ctf_RespawnFlag(entity flag) + { + // check for flag respawn being called twice in a row + if(flag.last_respawn > time - 0.5) + { backtrace("flag respawn called twice quickly! please notify Samual about this..."); } + + flag.last_respawn = time; + + // reset the player (if there is one) + if((flag.owner) && (flag.owner.flagcarried == flag)) + { + WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier); + WaypointSprite_Kill(flag.wps_flagcarrier); + + flag.owner.flagcarried = world; + + if(flag.speedrunning) + ctf_FakeTimeLimit(flag.owner, -1); + } + + if((flag.owner) && (flag.owner.vehicle)) + flag.scale = FLAG_SCALE; + + if(flag.ctf_status == FLAG_DROPPED) + { WaypointSprite_Kill(flag.wps_flagdropped); } + + // reset the flag + setattachment(flag, world, ""); + setorigin(flag, flag.ctf_spawnorigin); + + flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS); + flag.takedamage = DAMAGE_NO; + flag.health = flag.max_flag_health; + flag.solid = SOLID_TRIGGER; + flag.velocity = '0 0 0'; + flag.angles = flag.mangle; + flag.flags = FL_ITEM | FL_NOTARGET; + + flag.ctf_status = FLAG_BASE; + flag.owner = world; + flag.pass_distance = 0; + flag.pass_sender = world; + flag.pass_target = world; + flag.ctf_dropper = world; + flag.ctf_pickuptime = 0; + flag.ctf_droptime = 0; + flag.ctf_flagdamaged = 0; + + ctf_CheckStalemate(); + } + + void ctf_Reset() + {SELFPARAM(); + if(self.owner) + if(IS_PLAYER(self.owner)) + ctf_Handle_Throw(self.owner, world, DROP_RESET); + + ctf_RespawnFlag(self); + } + + void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup() + {SELFPARAM(); + // bot waypoints + waypoint_spawnforitem_force(self, self.origin); + self.nearestwaypointtimeout = 0; // activate waypointing again + self.bot_basewaypoint = self.nearestwaypoint; + + // waypointsprites + entity basename; + switch (self.team) + { + case NUM_TEAM_1: basename = WP_FlagBaseRed; break; + case NUM_TEAM_2: basename = WP_FlagBaseBlue; break; + case NUM_TEAM_3: basename = WP_FlagBaseYellow; break; + case NUM_TEAM_4: basename = WP_FlagBasePink; break; + default: basename = WP_FlagBaseNeutral; break; + } + + entity wp = WaypointSprite_SpawnFixed(basename, self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG); + wp.colormod = ((self.team) ? Team_ColorRGB(self.team) : '1 1 1'); + WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, ((self.team) ? colormapPaletteColor(self.team - 1, false) : '1 1 1')); + + // captureshield setup + ctf_CaptureShield_Spawn(self); + } + + void set_flag_string(entity flag, .string field, string value, string teamname) + { + if(flag.(field) == "") + flag.(field) = strzone(sprintf(value,teamname)); + } + + void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc + {SELFPARAM(); + // declarations + setself(flag); // for later usage with droptofloor() + + // main setup + flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist + ctf_worldflaglist = flag; + + setattachment(flag, world, ""); + + flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber))); + flag.team = teamnumber; + flag.classname = "item_flag_team"; + flag.target = "###item###"; // wut? + flag.flags = FL_ITEM | FL_NOTARGET; + flag.solid = SOLID_TRIGGER; + flag.takedamage = DAMAGE_NO; + flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale; + flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100); + flag.health = flag.max_flag_health; + flag.event_damage = ctf_FlagDamage; + flag.pushable = true; + flag.teleportable = TELEPORT_NORMAL; + flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP; + flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable; + flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable; + flag.velocity = '0 0 0'; + flag.mangle = flag.angles; + flag.reset = ctf_Reset; + flag.touch = ctf_FlagTouch; + flag.think = ctf_FlagThink; + flag.nextthink = time + FLAG_THINKRATE; + flag.ctf_status = FLAG_BASE; + + string teamname = Static_Team_ColorName_Lower(teamnumber); + // appearence + if(!flag.scale) { flag.scale = FLAG_SCALE; } + if(flag.skin == 0) { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); } + if(flag.model == "") { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); } + set_flag_string(flag, toucheffect, "%sflag_touch", teamname); + set_flag_string(flag, passeffect, "%s_pass", teamname); + set_flag_string(flag, capeffect, "%s_cap", teamname); + + // sounds + flag.snd_flag_taken = SND(CTF_TAKEN(teamnumber)); + flag.snd_flag_returned = SND(CTF_RETURNED(teamnumber)); + flag.snd_flag_capture = SND(CTF_CAPTURE(teamnumber)); + flag.snd_flag_dropped = SND(CTF_DROPPED(teamnumber)); + if (flag.snd_flag_respawn == "") flag.snd_flag_respawn = SND(CTF_RESPAWN); // if there is ever a team-based sound for this, update the code to match. + precache_sound(flag.snd_flag_respawn); + if (flag.snd_flag_touch == "") flag.snd_flag_touch = SND(CTF_TOUCH); // again has no team-based sound + precache_sound(flag.snd_flag_touch); + if (flag.snd_flag_pass == "") flag.snd_flag_pass = SND(CTF_PASS); // same story here + precache_sound(flag.snd_flag_pass); + + // precache + precache_model(flag.model); + + // appearence + _setmodel(flag, flag.model); // precision set below - setsize(flag, FLAG_MIN, FLAG_MAX); ++ setsize(flag, CTF_FLAG.m_mins, CTF_FLAG.m_maxs); + setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET)); + + if(autocvar_g_ctf_flag_glowtrails) + { + switch(teamnumber) + { + case NUM_TEAM_1: flag.glow_color = 251; break; + case NUM_TEAM_2: flag.glow_color = 210; break; + case NUM_TEAM_3: flag.glow_color = 110; break; + case NUM_TEAM_4: flag.glow_color = 145; break; + default: flag.glow_color = 254; break; + } + flag.glow_size = 25; + flag.glow_trail = 1; + } + + flag.effects |= EF_LOWPRECISION; + if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; } + if(autocvar_g_ctf_dynamiclights) + { + switch(teamnumber) + { + case NUM_TEAM_1: flag.effects |= EF_RED; break; + case NUM_TEAM_2: flag.effects |= EF_BLUE; break; + case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break; + case NUM_TEAM_4: flag.effects |= EF_RED; break; + default: flag.effects |= EF_DIMLIGHT; break; + } + } + + // flag placement + if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location + { + flag.dropped_origin = flag.origin; + flag.noalign = true; + flag.movetype = MOVETYPE_NONE; + } + else // drop to floor, automatically find a platform and set that as spawn origin + { + flag.noalign = false; + setself(flag); + droptofloor(); + flag.movetype = MOVETYPE_TOSS; + } + + InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION); + } + + + // ================ + // Bot player logic + // ================ + + // NOTE: LEGACY CODE, needs to be re-written! + + void havocbot_calculate_middlepoint() + { + entity f; + vector s = '0 0 0'; + vector fo = '0 0 0'; + float n = 0; + + f = ctf_worldflaglist; + while (f) + { + fo = f.origin; + s = s + fo; + f = f.ctf_worldflagnext; + } + if(!n) + return; + havocbot_ctf_middlepoint = s * (1.0 / n); + havocbot_ctf_middlepoint_radius = vlen(fo - havocbot_ctf_middlepoint); + } + + + entity havocbot_ctf_find_flag(entity bot) + { + entity f; + f = ctf_worldflaglist; + while (f) + { + if (CTF_SAMETEAM(bot, f)) + return f; + f = f.ctf_worldflagnext; + } + return world; + } + + entity havocbot_ctf_find_enemy_flag(entity bot) + { + entity f; + f = ctf_worldflaglist; + while (f) + { + if(ctf_oneflag) + { + if(CTF_DIFFTEAM(bot, f)) + { + if(f.team) + { + if(bot.flagcarried) + return f; + } + else if(!bot.flagcarried) + return f; + } + } + else if (CTF_DIFFTEAM(bot, f)) + return f; + f = f.ctf_worldflagnext; + } + return world; + } + + int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius) + { + if (!teamplay) + return 0; + + int c = 0; + entity head; + + FOR_EACH_PLAYER(head) + { + if(DIFF_TEAM(head, bot) || head.deadflag != DEAD_NO || head == bot) + continue; + + if(vlen(head.origin - org) < tc_radius) + ++c; + } + + return c; + } + + void havocbot_goalrating_ctf_ourflag(float ratingscale) + {SELFPARAM(); + entity head; + head = ctf_worldflaglist; + while (head) + { + if (CTF_SAMETEAM(self, head)) + break; + head = head.ctf_worldflagnext; + } + if (head) + navigation_routerating(head, ratingscale, 10000); + } + + void havocbot_goalrating_ctf_ourbase(float ratingscale) + {SELFPARAM(); + entity head; + head = ctf_worldflaglist; + while (head) + { + if (CTF_SAMETEAM(self, head)) + break; + head = head.ctf_worldflagnext; + } + if (!head) + return; + + navigation_routerating(head.bot_basewaypoint, ratingscale, 10000); + } + + void havocbot_goalrating_ctf_enemyflag(float ratingscale) + {SELFPARAM(); + entity head; + head = ctf_worldflaglist; + while (head) + { + if(ctf_oneflag) + { + if(CTF_DIFFTEAM(self, head)) + { + if(head.team) + { + if(self.flagcarried) + break; + } + else if(!self.flagcarried) + break; + } + } + else if(CTF_DIFFTEAM(self, head)) + break; + head = head.ctf_worldflagnext; + } + if (head) + navigation_routerating(head, ratingscale, 10000); + } + + void havocbot_goalrating_ctf_enemybase(float ratingscale) + {SELFPARAM(); + if (!bot_waypoints_for_items) + { + havocbot_goalrating_ctf_enemyflag(ratingscale); + return; + } + + entity head; + + head = havocbot_ctf_find_enemy_flag(self); + + if (!head) + return; + + navigation_routerating(head.bot_basewaypoint, ratingscale, 10000); + } + + void havocbot_goalrating_ctf_ourstolenflag(float ratingscale) + {SELFPARAM(); + entity mf; + + mf = havocbot_ctf_find_flag(self); + + if(mf.ctf_status == FLAG_BASE) + return; + + if(mf.tag_entity) + navigation_routerating(mf.tag_entity, ratingscale, 10000); + } + + void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius) + { + entity head; + head = ctf_worldflaglist; + while (head) + { + // flag is out in the field + if(head.ctf_status != FLAG_BASE) + if(head.tag_entity==world) // dropped + { + if(df_radius) + { + if(vlen(org-head.origin) 0) + navigation_routerating(head, t * ratingscale, 500); + } + head = head.chain; + } + } + + void havocbot_ctf_reset_role(entity bot) + { + float cdefense, cmiddle, coffense; + entity mf, ef, head; + float c; + + if(bot.deadflag != DEAD_NO) + return; + + if(vlen(havocbot_ctf_middlepoint)==0) + havocbot_calculate_middlepoint(); + + // Check ctf flags + if (bot.flagcarried) + { + havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER); + return; + } + + mf = havocbot_ctf_find_flag(bot); + ef = havocbot_ctf_find_enemy_flag(bot); + + // Retrieve stolen flag + if(mf.ctf_status!=FLAG_BASE) + { + havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER); + return; + } + + // If enemy flag is taken go to the middle to intercept pursuers + if(ef.ctf_status!=FLAG_BASE) + { + havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE); + return; + } + + // if there is only me on the team switch to offense + c = 0; + FOR_EACH_PLAYER(head) + if(SAME_TEAM(head, bot)) + ++c; + + if(c==1) + { + havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE); + return; + } + + // Evaluate best position to take + // Count mates on middle position + cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5); + + // Count mates on defense position + cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5); + + // Count mates on offense position + coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius); + + if(cdefense<=coffense) + havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE); + else if(coffense<=cmiddle) + havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE); + else + havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE); + } + + void havocbot_role_ctf_carrier() + {SELFPARAM(); + if(self.deadflag != DEAD_NO) + { + havocbot_ctf_reset_role(self); + return; + } + + if (self.flagcarried == world) + { + havocbot_ctf_reset_role(self); + return; + } + + if (self.bot_strategytime < time) + { + self.bot_strategytime = time + autocvar_bot_ai_strategyinterval; + + navigation_goalrating_start(); + if(ctf_oneflag) + havocbot_goalrating_ctf_enemybase(50000); + else + havocbot_goalrating_ctf_ourbase(50000); + + if(self.health<100) + havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000); + + navigation_goalrating_end(); + + if (self.navigation_hasgoals) + self.havocbot_cantfindflag = time + 10; + else if (time > self.havocbot_cantfindflag) + { + // Can't navigate to my own base, suicide! + // TODO: drop it and wander around + Damage(self, self, self, 100000, DEATH_KILL.m_id, self.origin, '0 0 0'); + return; + } + } + } + + void havocbot_role_ctf_escort() + {SELFPARAM(); + entity mf, ef; + + if(self.deadflag != DEAD_NO) + { + havocbot_ctf_reset_role(self); + return; + } + + if (self.flagcarried) + { + havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER); + return; + } + + // If enemy flag is back on the base switch to previous role + ef = havocbot_ctf_find_enemy_flag(self); + if(ef.ctf_status==FLAG_BASE) + { + self.havocbot_role = self.havocbot_previous_role; + self.havocbot_role_timeout = 0; + return; + } + + // If the flag carrier reached the base switch to defense + mf = havocbot_ctf_find_flag(self); + if(mf.ctf_status!=FLAG_BASE) + if(vlen(ef.origin - mf.dropped_origin) < 300) + { + havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE); + return; + } + + // Set the role timeout if necessary + if (!self.havocbot_role_timeout) + { + self.havocbot_role_timeout = time + random() * 30 + 60; + } + + // If nothing happened just switch to previous role + if (time > self.havocbot_role_timeout) + { + self.havocbot_role = self.havocbot_previous_role; + self.havocbot_role_timeout = 0; + return; + } + + // Chase the flag carrier + if (self.bot_strategytime < time) + { + self.bot_strategytime = time + autocvar_bot_ai_strategyinterval; + navigation_goalrating_start(); + havocbot_goalrating_ctf_enemyflag(30000); + havocbot_goalrating_ctf_ourstolenflag(40000); + havocbot_goalrating_items(10000, self.origin, 10000); + navigation_goalrating_end(); + } + } + + void havocbot_role_ctf_offense() + {SELFPARAM(); + entity mf, ef; + vector pos; + + if(self.deadflag != DEAD_NO) + { + havocbot_ctf_reset_role(self); + return; + } + + if (self.flagcarried) + { + havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER); + return; + } + + // Check flags + mf = havocbot_ctf_find_flag(self); + ef = havocbot_ctf_find_enemy_flag(self); + + // Own flag stolen + if(mf.ctf_status!=FLAG_BASE) + { + if(mf.tag_entity) + pos = mf.tag_entity.origin; + else + pos = mf.origin; + + // Try to get it if closer than the enemy base + if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos)) + { + havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER); + return; + } + } + + // Escort flag carrier + if(ef.ctf_status!=FLAG_BASE) + { + if(ef.tag_entity) + pos = ef.tag_entity.origin; + else + pos = ef.origin; + + if(vlen(pos-mf.dropped_origin)>700) + { + havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT); + return; + } + } + + // About to fail, switch to middlefield + if(self.health<50) + { + havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE); + return; + } + + // Set the role timeout if necessary + if (!self.havocbot_role_timeout) + self.havocbot_role_timeout = time + 120; + + if (time > self.havocbot_role_timeout) + { + havocbot_ctf_reset_role(self); + return; + } + + if (self.bot_strategytime < time) + { + self.bot_strategytime = time + autocvar_bot_ai_strategyinterval; + navigation_goalrating_start(); + havocbot_goalrating_ctf_ourstolenflag(50000); + havocbot_goalrating_ctf_enemybase(20000); + havocbot_goalrating_items(5000, self.origin, 1000); + havocbot_goalrating_items(1000, self.origin, 10000); + navigation_goalrating_end(); + } + } + + // Retriever (temporary role): + void havocbot_role_ctf_retriever() + {SELFPARAM(); + entity mf; + + if(self.deadflag != DEAD_NO) + { + havocbot_ctf_reset_role(self); + return; + } + + if (self.flagcarried) + { + havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER); + return; + } + + // If flag is back on the base switch to previous role + mf = havocbot_ctf_find_flag(self); + if(mf.ctf_status==FLAG_BASE) + { + havocbot_ctf_reset_role(self); + return; + } + + if (!self.havocbot_role_timeout) + self.havocbot_role_timeout = time + 20; + + if (time > self.havocbot_role_timeout) + { + havocbot_ctf_reset_role(self); + return; + } + + if (self.bot_strategytime < time) + { + float rt_radius; + rt_radius = 10000; + + self.bot_strategytime = time + autocvar_bot_ai_strategyinterval; + navigation_goalrating_start(); + havocbot_goalrating_ctf_ourstolenflag(50000); + havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius); + havocbot_goalrating_ctf_enemybase(30000); + havocbot_goalrating_items(500, self.origin, rt_radius); + navigation_goalrating_end(); + } + } + + void havocbot_role_ctf_middle() + {SELFPARAM(); + entity mf; + + if(self.deadflag != DEAD_NO) + { + havocbot_ctf_reset_role(self); + return; + } + + if (self.flagcarried) + { + havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER); + return; + } + + mf = havocbot_ctf_find_flag(self); + if(mf.ctf_status!=FLAG_BASE) + { + havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER); + return; + } + + if (!self.havocbot_role_timeout) + self.havocbot_role_timeout = time + 10; + + if (time > self.havocbot_role_timeout) + { + havocbot_ctf_reset_role(self); + return; + } + + if (self.bot_strategytime < time) + { + vector org; + + org = havocbot_ctf_middlepoint; + org.z = self.origin.z; + + self.bot_strategytime = time + autocvar_bot_ai_strategyinterval; + navigation_goalrating_start(); + havocbot_goalrating_ctf_ourstolenflag(50000); + havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000); + havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5); + havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5); + havocbot_goalrating_items(2500, self.origin, 10000); + havocbot_goalrating_ctf_enemybase(2500); + navigation_goalrating_end(); + } + } + + void havocbot_role_ctf_defense() + {SELFPARAM(); + entity mf; + + if(self.deadflag != DEAD_NO) + { + havocbot_ctf_reset_role(self); + return; + } + + if (self.flagcarried) + { + havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER); + return; + } + + // If own flag was captured + mf = havocbot_ctf_find_flag(self); + if(mf.ctf_status!=FLAG_BASE) + { + havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER); + return; + } + + if (!self.havocbot_role_timeout) + self.havocbot_role_timeout = time + 30; + + if (time > self.havocbot_role_timeout) + { + havocbot_ctf_reset_role(self); + return; + } + if (self.bot_strategytime < time) + { + float mp_radius; + vector org; + + org = mf.dropped_origin; + mp_radius = havocbot_ctf_middlepoint_radius; + + self.bot_strategytime = time + autocvar_bot_ai_strategyinterval; + navigation_goalrating_start(); + + // if enemies are closer to our base, go there + entity head, closestplayer = world; + float distance, bestdistance = 10000; + FOR_EACH_PLAYER(head) + { + if(head.deadflag!=DEAD_NO) + continue; + + distance = vlen(org - head.origin); + if(distance1000) + if(checkpvs(self.origin,closestplayer)||random()<0.5) + havocbot_goalrating_ctf_ourbase(30000); + + havocbot_goalrating_ctf_ourstolenflag(20000); + havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius); + havocbot_goalrating_enemyplayers(15000, org, mp_radius); + havocbot_goalrating_items(10000, org, mp_radius); + havocbot_goalrating_items(5000, self.origin, 10000); + navigation_goalrating_end(); + } + } + + void havocbot_role_ctf_setrole(entity bot, int role) + { + LOG_TRACE(strcat(bot.netname," switched to ")); + switch(role) + { + case HAVOCBOT_CTF_ROLE_CARRIER: + LOG_TRACE("carrier"); + bot.havocbot_role = havocbot_role_ctf_carrier; + bot.havocbot_role_timeout = 0; + bot.havocbot_cantfindflag = time + 10; + bot.bot_strategytime = 0; + break; + case HAVOCBOT_CTF_ROLE_DEFENSE: + LOG_TRACE("defense"); + bot.havocbot_role = havocbot_role_ctf_defense; + bot.havocbot_role_timeout = 0; + break; + case HAVOCBOT_CTF_ROLE_MIDDLE: + LOG_TRACE("middle"); + bot.havocbot_role = havocbot_role_ctf_middle; + bot.havocbot_role_timeout = 0; + break; + case HAVOCBOT_CTF_ROLE_OFFENSE: + LOG_TRACE("offense"); + bot.havocbot_role = havocbot_role_ctf_offense; + bot.havocbot_role_timeout = 0; + break; + case HAVOCBOT_CTF_ROLE_RETRIEVER: + LOG_TRACE("retriever"); + bot.havocbot_previous_role = bot.havocbot_role; + bot.havocbot_role = havocbot_role_ctf_retriever; + bot.havocbot_role_timeout = time + 10; + bot.bot_strategytime = 0; + break; + case HAVOCBOT_CTF_ROLE_ESCORT: + LOG_TRACE("escort"); + bot.havocbot_previous_role = bot.havocbot_role; + bot.havocbot_role = havocbot_role_ctf_escort; + bot.havocbot_role_timeout = time + 30; + bot.bot_strategytime = 0; + break; + } + LOG_TRACE("\n"); + } + + + // ============== + // Hook Functions + // ============== + + MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink) + {SELFPARAM(); + entity flag; + int t = 0, t2 = 0, t3 = 0; + + // initially clear items so they can be set as necessary later. + self.ctf_flagstatus &= ~(CTF_RED_FLAG_CARRYING | CTF_RED_FLAG_TAKEN | CTF_RED_FLAG_LOST + | CTF_BLUE_FLAG_CARRYING | CTF_BLUE_FLAG_TAKEN | CTF_BLUE_FLAG_LOST + | CTF_YELLOW_FLAG_CARRYING | CTF_YELLOW_FLAG_TAKEN | CTF_YELLOW_FLAG_LOST + | CTF_PINK_FLAG_CARRYING | CTF_PINK_FLAG_TAKEN | CTF_PINK_FLAG_LOST + | CTF_NEUTRAL_FLAG_CARRYING | CTF_NEUTRAL_FLAG_TAKEN | CTF_NEUTRAL_FLAG_LOST + | CTF_FLAG_NEUTRAL | CTF_SHIELDED); + + // scan through all the flags and notify the client about them + for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext) + { + if(flag.team == NUM_TEAM_1) { t = CTF_RED_FLAG_CARRYING; t2 = CTF_RED_FLAG_TAKEN; t3 = CTF_RED_FLAG_LOST; } + if(flag.team == NUM_TEAM_2) { t = CTF_BLUE_FLAG_CARRYING; t2 = CTF_BLUE_FLAG_TAKEN; t3 = CTF_BLUE_FLAG_LOST; } + if(flag.team == NUM_TEAM_3) { t = CTF_YELLOW_FLAG_CARRYING; t2 = CTF_YELLOW_FLAG_TAKEN; t3 = CTF_YELLOW_FLAG_LOST; } + if(flag.team == NUM_TEAM_4) { t = CTF_PINK_FLAG_CARRYING; t2 = CTF_PINK_FLAG_TAKEN; t3 = CTF_PINK_FLAG_LOST; } + if(flag.team == 0) { t = CTF_NEUTRAL_FLAG_CARRYING; t2 = CTF_NEUTRAL_FLAG_TAKEN; t3 = CTF_NEUTRAL_FLAG_LOST; self.ctf_flagstatus |= CTF_FLAG_NEUTRAL; } + + switch(flag.ctf_status) + { + case FLAG_PASSING: + case FLAG_CARRY: + { + if((flag.owner == self) || (flag.pass_sender == self)) + self.ctf_flagstatus |= t; // carrying: self is currently carrying the flag + else + self.ctf_flagstatus |= t2; // taken: someone else is carrying the flag + break; + } + case FLAG_DROPPED: + { + self.ctf_flagstatus |= t3; // lost: the flag is dropped somewhere on the map + break; + } + } + } + + // item for stopping players from capturing the flag too often + if(self.ctf_captureshielded) + self.ctf_flagstatus |= CTF_SHIELDED; + + // update the health of the flag carrier waypointsprite + if(self.wps_flagcarrier) + WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id)); + + return false; + } + + MUTATOR_HOOKFUNCTION(ctf, PlayerDamage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc + { + if(frag_attacker.flagcarried) // if the attacker is a flagcarrier + { + if(frag_target == frag_attacker) // damage done to yourself + { + frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor; + frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor; + } + else // damage done to everyone else + { + frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor; + frag_force *= autocvar_g_ctf_flagcarrier_forcefactor; + } + } + else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier + { + if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id))) + if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time) + { + frag_target.wps_helpme_time = time; + WaypointSprite_HelpMePing(frag_target.wps_flagcarrier); + } + // todo: add notification for when flag carrier needs help? + } + return false; + } + + MUTATOR_HOOKFUNCTION(ctf, PlayerDies) + { + if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried)) + { + PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill); + PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1); + } + + if(frag_target.flagcarried) + { + entity tmp_entity = frag_target.flagcarried; + ctf_Handle_Throw(frag_target, world, DROP_NORMAL); + tmp_entity.ctf_dropper = world; + } + + return false; + } + + MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill) + { + frag_score = 0; + return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true + } + + void ctf_RemovePlayer(entity player) + { + if(player.flagcarried) + { ctf_Handle_Throw(player, world, DROP_NORMAL); } + + for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext) + { + if(flag.pass_sender == player) { flag.pass_sender = world; } + if(flag.pass_target == player) { flag.pass_target = world; } + if(flag.ctf_dropper == player) { flag.ctf_dropper = world; } + } + } + + MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver) + {SELFPARAM(); + ctf_RemovePlayer(self); + return false; + } + + MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect) + {SELFPARAM(); + ctf_RemovePlayer(self); + return false; + } + + MUTATOR_HOOKFUNCTION(ctf, PortalTeleport) + {SELFPARAM(); + if(self.flagcarried) + if(!autocvar_g_ctf_portalteleport) + { ctf_Handle_Throw(self, world, DROP_NORMAL); } + + return false; + } + + MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey) + {SELFPARAM(); + if(MUTATOR_RETURNVALUE || gameover) { return false; } + + entity player = self; + + if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch)) + { + // pass the flag to a team mate + if(autocvar_g_ctf_pass) + { + entity head, closest_target = world; + head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true); + + while(head) // find the closest acceptable target to pass to + { + if(IS_PLAYER(head) && head.deadflag == DEAD_NO) + if(head != player && SAME_TEAM(head, player)) + if(!head.speedrunning && !head.vehicle) + { + // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc) + vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head)); + vector passer_center = CENTER_OR_VIEWOFS(player); + + if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest)) + { + if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried) + { + if(IS_BOT_CLIENT(head)) + { + Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname); + ctf_Handle_Throw(head, player, DROP_PASS); + } + else + { + Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname); + Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname); + } + player.throw_antispam = time + autocvar_g_ctf_pass_wait; + return true; + } + else if(player.flagcarried) + { + if(closest_target) + { + vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target)); + if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center)) + { closest_target = head; } + } + else { closest_target = head; } + } + } + } + head = head.chain; + } + + if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; } + } + + // throw the flag in front of you + if(autocvar_g_ctf_throw && player.flagcarried) + { + if(player.throw_count == -1) + { + if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) + { + player.throw_prevtime = time; + player.throw_count = 1; + ctf_Handle_Throw(player, world, DROP_THROW); + return true; + } + else + { + Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time)); + return false; + } + } + else + { + if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; } + else { player.throw_count += 1; } + if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; } + + player.throw_prevtime = time; + ctf_Handle_Throw(player, world, DROP_THROW); + return true; + } + } + } + + return false; + } + + MUTATOR_HOOKFUNCTION(ctf, HelpMePing) + {SELFPARAM(); + if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification + { + self.wps_helpme_time = time; + WaypointSprite_HelpMePing(self.wps_flagcarrier); + } + else // create a normal help me waypointsprite + { + WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, self, FLAG_WAYPOINT_OFFSET, world, self.team, self, wps_helpme, false, RADARICON_HELPME); + WaypointSprite_Ping(self.wps_helpme); + } + + return true; + } + + MUTATOR_HOOKFUNCTION(ctf, VehicleEnter) + { + if(vh_player.flagcarried) + { + vh_player.flagcarried.nodrawtoclient = vh_player; // hide the flag from the driver + + if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch) + { + ctf_Handle_Throw(vh_player, world, DROP_NORMAL); + } + else + { + setattachment(vh_player.flagcarried, vh_vehicle, ""); + setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET); + vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE; + //vh_player.flagcarried.angles = '0 0 0'; + } + return true; + } + + return false; + } + + MUTATOR_HOOKFUNCTION(ctf, VehicleExit) + { + if(vh_player.flagcarried) + { + setattachment(vh_player.flagcarried, vh_player, ""); + setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET); + vh_player.flagcarried.scale = FLAG_SCALE; + vh_player.flagcarried.angles = '0 0 0'; + vh_player.flagcarried.nodrawtoclient = world; + return true; + } + + return false; + } + + MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun) + {SELFPARAM(); + if(self.flagcarried) + { + Send_Notification(NOTIF_ALL, world, MSG_INFO, ((self.flagcarried.team) ? APP_TEAM_ENT_4(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN_) : INFO_CTF_FLAGRETURN_ABORTRUN_NEUTRAL)); + ctf_RespawnFlag(self.flagcarried); + return true; + } + + return false; + } + + MUTATOR_HOOKFUNCTION(ctf, MatchEnd) + { + entity flag; // temporary entity for the search method + + for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext) + { + switch(flag.ctf_status) + { + case FLAG_DROPPED: + case FLAG_PASSING: + { + // lock the flag, game is over + flag.movetype = MOVETYPE_NONE; + flag.takedamage = DAMAGE_NO; + flag.solid = SOLID_NOT; + flag.nextthink = false; // stop thinking + + //dprint("stopping the ", flag.netname, " from moving.\n"); + break; + } + + default: + case FLAG_BASE: + case FLAG_CARRY: + { + // do nothing for these flags + break; + } + } + } + + return false; + } + + MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole) + {SELFPARAM(); + havocbot_ctf_reset_role(self); + return true; + } + + MUTATOR_HOOKFUNCTION(ctf, GetTeamCount) + { + //ret_float = ctf_teams; + ret_string = "ctf_team"; + return true; + } + + MUTATOR_HOOKFUNCTION(ctf, SpectateCopy) + {SELFPARAM(); + self.ctf_flagstatus = other.ctf_flagstatus; + return false; + } + + MUTATOR_HOOKFUNCTION(ctf, GetRecords) + { + for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i) + { + if (MapInfo_Get_ByID(i)) + { + float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time"))); + + if(!r) + continue; + + // TODO: uid2name + string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname")); + ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n"); + } + } + + return false; + } + + bool superspec_Spectate(entity _player); // TODO + void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO + MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand) + { + if(IS_PLAYER(self) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; } + + if(cmd_name == "followfc") + { + if(!g_ctf) + return true; + + entity _player; + int _team = 0; + bool found = false; + + if(cmd_argc == 2) + { + switch(argv(1)) + { + case "red": _team = NUM_TEAM_1; break; + case "blue": _team = NUM_TEAM_2; break; + case "yellow": if(ctf_teams >= 3) _team = NUM_TEAM_3; break; + case "pink": if(ctf_teams >= 4) _team = NUM_TEAM_4; break; + } + } + + FOR_EACH_PLAYER(_player) + { + if(_player.flagcarried && (_player.team == _team || _team == 0)) + { + found = true; + if(_team == 0 && IS_SPEC(self) && self.enemy == _player) + continue; // already spectating a fc, try to find the other fc + return superspec_Spectate(_player); + } + } + + if(!found) + superspec_msg("", "", self, "No active flag carrier\n", 1); + return true; + } + + return false; + } + + MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems) + { + if(frag_target.flagcarried) + ctf_Handle_Throw(frag_target, world, DROP_THROW); + + return false; + } + + + // ========== + // Spawnfuncs + // ========== + + /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37) + CTF flag for team one (Red). + Keys: + "angle" Angle the flag will point (minus 90 degrees)... + "model" model to use, note this needs red and blue as skins 0 and 1... + "noise" sound played when flag is picked up... + "noise1" sound played when flag is returned by a teammate... + "noise2" sound played when flag is captured... + "noise3" sound played when flag is lost in the field and respawns itself... + "noise4" sound played when flag is dropped by a player... + "noise5" sound played when flag touches the ground... */ + spawnfunc(item_flag_team1) + { + if(!g_ctf) { remove(self); return; } + + ctf_FlagSetup(NUM_TEAM_1, self); + } + + /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37) + CTF flag for team two (Blue). + Keys: + "angle" Angle the flag will point (minus 90 degrees)... + "model" model to use, note this needs red and blue as skins 0 and 1... + "noise" sound played when flag is picked up... + "noise1" sound played when flag is returned by a teammate... + "noise2" sound played when flag is captured... + "noise3" sound played when flag is lost in the field and respawns itself... + "noise4" sound played when flag is dropped by a player... + "noise5" sound played when flag touches the ground... */ + spawnfunc(item_flag_team2) + { + if(!g_ctf) { remove(self); return; } + + ctf_FlagSetup(NUM_TEAM_2, self); + } + + /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37) + CTF flag for team three (Yellow). + Keys: + "angle" Angle the flag will point (minus 90 degrees)... + "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3... + "noise" sound played when flag is picked up... + "noise1" sound played when flag is returned by a teammate... + "noise2" sound played when flag is captured... + "noise3" sound played when flag is lost in the field and respawns itself... + "noise4" sound played when flag is dropped by a player... + "noise5" sound played when flag touches the ground... */ + spawnfunc(item_flag_team3) + { + if(!g_ctf) { remove(self); return; } + + ctf_FlagSetup(NUM_TEAM_3, self); + } + + /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37) + CTF flag for team four (Pink). + Keys: + "angle" Angle the flag will point (minus 90 degrees)... + "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3... + "noise" sound played when flag is picked up... + "noise1" sound played when flag is returned by a teammate... + "noise2" sound played when flag is captured... + "noise3" sound played when flag is lost in the field and respawns itself... + "noise4" sound played when flag is dropped by a player... + "noise5" sound played when flag touches the ground... */ + spawnfunc(item_flag_team4) + { + if(!g_ctf) { remove(self); return; } + + ctf_FlagSetup(NUM_TEAM_4, self); + } + + /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37) + CTF flag (Neutral). + Keys: + "angle" Angle the flag will point (minus 90 degrees)... + "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3... + "noise" sound played when flag is picked up... + "noise1" sound played when flag is returned by a teammate... + "noise2" sound played when flag is captured... + "noise3" sound played when flag is lost in the field and respawns itself... + "noise4" sound played when flag is dropped by a player... + "noise5" sound played when flag touches the ground... */ + spawnfunc(item_flag_neutral) + { + if(!g_ctf) { remove(self); return; } + if(!cvar("g_ctf_oneflag")) { remove(self); return; } + + ctf_FlagSetup(0, self); + } + + /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32) + Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map. + 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. + Keys: + "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)... + "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */ + spawnfunc(ctf_team) + { + if(!g_ctf) { remove(self); return; } + + self.classname = "ctf_team"; + self.team = self.cnt + 1; + } + + // compatibility for quake maps + spawnfunc(team_CTF_redflag) { spawnfunc_item_flag_team1(this); } + spawnfunc(team_CTF_blueflag) { spawnfunc_item_flag_team2(this); } + spawnfunc(info_player_team1); + spawnfunc(team_CTF_redplayer) { spawnfunc_info_player_team1(this); } + spawnfunc(team_CTF_redspawn) { spawnfunc_info_player_team1(this); } + spawnfunc(info_player_team2); + spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this); } + spawnfunc(team_CTF_bluespawn) { spawnfunc_info_player_team2(this); } + + void team_CTF_neutralflag() { SELFPARAM(); spawnfunc_item_flag_neutral(self); } + void team_neutralobelisk() { SELFPARAM(); spawnfunc_item_flag_neutral(self); } + + + // ============== + // Initialization + // ============== + + // scoreboard setup + void ctf_ScoreRules(int teams) + { + CheckAllowedTeams(world); + ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true); + ScoreInfo_SetLabel_TeamScore (ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY); + ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY); + ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME); + ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups", 0); + ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills", 0); + ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns", 0); + ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER); + ScoreRules_basics_end(); + } + + // code from here on is just to support maps that don't have flag and team entities + void ctf_SpawnTeam (string teamname, int teamcolor) + { + entity this = new(ctf_team); + this.netname = teamname; + this.cnt = teamcolor; + this.spawnfunc_checked = true; + WITH(entity, self, this, spawnfunc_ctf_team(this)); + } + + void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up. + { + ctf_teams = 2; + + entity tmp_entity; + for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext) + { + if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); } + if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); } + if(tmp_entity.team == 0) { ctf_oneflag = true; } + } + + ctf_teams = bound(2, ctf_teams, 4); + + // if no teams are found, spawn defaults + if(find(world, classname, "ctf_team") == world) + { + LOG_INFO("No ""ctf_team"" entities found on this map, creating them anyway.\n"); + ctf_SpawnTeam("Red", NUM_TEAM_1 - 1); + ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1); + if(ctf_teams >= 3) + ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1); + if(ctf_teams >= 4) + ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1); + } + + ctf_ScoreRules(ctf_teams); + } + + void ctf_Initialize() + { + ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"))); + + ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore; + ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio; + ctf_captureshield_force = autocvar_g_ctf_shield_force; + + addstat(STAT_CTF_FLAGSTATUS, AS_INT, ctf_flagstatus); + + InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE); + } + + #endif diff --cc qcsrc/server/t_items.qc index 595f3c84e,cb76f97f9..832290f84 --- a/qcsrc/server/t_items.qc +++ b/qcsrc/server/t_items.qc @@@ -1073,28 -1063,32 +1071,29 @@@ void _StartItem(entity this, entity def return; } - if(self.angles != '0 0 0') - self.SendFlags |= ISF_ANGLES; + if(this.angles != '0 0 0') + this.SendFlags |= ISF_ANGLES; - self.reset = Item_Reset; + this.reset = Item_Reset_self; // it's a level item - if(self.spawnflags & 1) - self.noalign = 1; - if (self.noalign > 0) - self.movetype = MOVETYPE_NONE; + if(this.spawnflags & 1) + this.noalign = 1; - if (this.noalign) ++ if (this.noalign > 0) + this.movetype = MOVETYPE_NONE; else - self.movetype = MOVETYPE_TOSS; + this.movetype = MOVETYPE_TOSS; // do item filtering according to game mode and other things - if (!this.noalign) - if (self.noalign <= 0) ++ if (this.noalign <= 0) { // first nudge it off the floor a little bit to avoid math errors - setorigin(self, self.origin + '0 0 1'); + setorigin(this, this.origin + '0 0 1'); // set item size before we spawn a spawnfunc_waypoint - if((itemflags & FL_POWERUP) || self.health || self.armorvalue) - setsize (self, '-16 -16 0', '16 16 48'); - else - setsize (self, '-16 -16 0', '16 16 32'); - self.SendFlags |= ISF_SIZE; + setsize(this, def.m_mins, def.m_maxs); + this.SendFlags |= ISF_SIZE; // note droptofloor returns false if stuck/or would fall too far - WITH(entity, self, this, droptofloor()); - if(!self.noalign) - droptofloor(); - waypoint_spawnforitem(self); ++ if (!this.noalign) ++ WITH(entity, self, this, droptofloor()); + waypoint_spawnforitem(this); } /* @@@ -1128,68 -1123,78 +1127,70 @@@ weaponsInMap |= WepSet_FromWeapon(weaponid); - precache_model (self.model); - precache_sound (self.item_pickupsound); - - if((itemflags & (FL_POWERUP | FL_WEAPON)) || (itemid & (IT_HEALTH | IT_ARMOR | IT_KEY1 | IT_KEY2))) - self.target = "###item###"; // for finding the nearest item using find() + precache_model(this.model); + precache_sound(this.item_pickupsound); - Item_ItemsTime_SetTime(self, 0); - } - - self.bot_pickup = true; - self.bot_pickupevalfunc = pickupevalfunc; - self.bot_pickupbasevalue = pickupbasevalue; - self.mdl = self.model; - self.netname = itemname; - self.touch = Item_Touch; - setmodel(self, MDL_Null); // precision set below - //self.effects |= EF_LOWPRECISION; + if ( def.instanceOfPowerup + || def.instanceOfWeaponPickup + || (def.instanceOfHealth && def != ITEM_HealthSmall) + || (def.instanceOfArmor && def != ITEM_ArmorSmall) + || (itemid & (IT_KEY1 | IT_KEY2)) + ) this.target = "###item###"; // for finding the nearest item using find() - if((itemflags & FL_POWERUP) || self.health || self.armorvalue) - { - self.pos1 = '-16 -16 0'; - self.pos2 = '16 16 48'; - } - else - { - self.pos1 = '-16 -16 0'; - self.pos2 = '16 16 32'; + Item_ItemsTime_SetTime(this, 0); } - setsize (self, self.pos1, self.pos2); - - self.SendFlags |= ISF_SIZE; - - if(!(self.spawnflags & 1024)) - { - if(itemflags & FL_POWERUP) - self.ItemStatus |= ITS_ANIMATE1; - if(self.armorvalue || self.health) - self.ItemStatus |= ITS_ANIMATE2; + this.bot_pickup = true; + this.bot_pickupevalfunc = pickupevalfunc; + this.bot_pickupbasevalue = pickupbasevalue; + this.mdl = this.model ? this.model : strzone(this.item_model_ent.model_str()); + this.netname = itemname; + this.touch = Item_Touch; + setmodel(this, MDL_Null); // precision set below + //this.effects |= EF_LOWPRECISION; + + setsize (this, this.pos1 = def.m_mins, this.pos2 = def.m_maxs); + + this.SendFlags |= ISF_SIZE; + - if(def.instanceOfPowerup) - this.ItemStatus |= ITS_ANIMATE1; - - if(this.armorvalue || this.health) - this.ItemStatus |= ITS_ANIMATE2; ++ if (!(this.spawnflags & 1024)) { ++ if(def.instanceOfPowerup) ++ this.ItemStatus |= ITS_ANIMATE1; ++ ++ if(this.armorvalue || this.health) ++ this.ItemStatus |= ITS_ANIMATE2; + } - if(itemflags & FL_WEAPON) + if(def.instanceOfWeaponPickup) { - if (self.classname != "droppedweapon") // if dropped, colormap is already set up nicely - self.colormap = 1024; // color shirt=0 pants=0 grey + if (this.classname != "droppedweapon") // if dropped, colormap is already set up nicely + this.colormap = 1024; // color shirt=0 pants=0 grey else - self.gravity = 1; - - if(!(self.spawnflags & 1024)) - self.ItemStatus |= ITS_ANIMATE1; - self.ItemStatus |= ISF_COLORMAP; + this.gravity = 1; - - this.ItemStatus |= ITS_ANIMATE1; ++ if (!(this.spawnflags & 1024)) ++ this.ItemStatus |= ITS_ANIMATE1; + this.ItemStatus |= ISF_COLORMAP; } - self.state = 0; - if(self.team) // broken, no idea why. + this.state = 0; + if(this.team) // broken, no idea why. { - if(!self.cnt) - self.cnt = 1; // item probability weight + if(!this.cnt) + this.cnt = 1; // item probability weight - self.effects |= EF_NODRAW; // marker for item team search - InitializeEntity(self, Item_FindTeam, INITPRIO_FINDTARGET); + this.effects |= EF_NODRAW; // marker for item team search + InitializeEntity(this, Item_FindTeam, INITPRIO_FINDTARGET); } else - Item_Reset(); + Item_Reset(this); - Net_LinkEntity(self, !((itemflags & FL_POWERUP) || self.health || self.armorvalue), 0, ItemSend); + Net_LinkEntity(this, !(def.instanceOfPowerup || def.instanceOfHealth || def.instanceOfArmor), 0, ItemSend); // call this hook after everything else has been done - if(MUTATOR_CALLHOOK(Item_Spawn, self)) + if (MUTATOR_CALLHOOK(Item_Spawn, this)) { startitem_failed = true; - remove(self); + remove(this); return; } }