X-Git-Url: http://git.xonotic.org/?a=blobdiff_plain;f=qcsrc%2Fserver%2Fmutators%2Fgamemode_ctf.qc;h=fbe57ec9361b84ae737da334df5dd3ca5390eeea;hb=52ae1315c1851e595d12ffc13e2407dee0b24e5e;hp=7cfd3a3b719acdab5df3175e74bf1c7367951763;hpb=2f2fd66d1b78236228005a04f5bc193e7ff37251;p=xonotic%2Fxonotic-data.pk3dir.git diff --git a/qcsrc/server/mutators/gamemode_ctf.qc b/qcsrc/server/mutators/gamemode_ctf.qc index 7cfd3a3b7..fbe57ec93 100644 --- a/qcsrc/server/mutators/gamemode_ctf.qc +++ b/qcsrc/server/mutators/gamemode_ctf.qc @@ -20,36 +20,39 @@ void ctf_EventLog(string mode, float flagteam, entity actor) // use an alias for GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : ""))); } -string ctf_CaptureRecord(entity flag, entity player) +void ctf_CaptureRecord(entity flag, entity player) { - float cap_time, cap_record, success; - string cap_message, refername; - - if((autocvar_g_ctf_captimerecord_always) || (player_count - currentbots)) - { - cap_record = ctf_captimerecord; - cap_time = (time - flag.ctf_pickuptime); - - refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname")); - refername = ((refername == player.netname) ? "their" : strcat(refername, "^7's")); - - if(!ctf_captimerecord) - { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds"); success = TRUE; } - else if(cap_time < cap_record) - { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, breaking ", refername, " previous record of ", ftos_decimals(cap_record, 2), " seconds"); success = TRUE; } - else - { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, failing to break ", refername, " record of ", ftos_decimals(cap_record, 2), " seconds"); success = FALSE; } + entity tmp_entity; + float notification, success; + float cap_record = ctf_captimerecord; + float cap_time = (time - flag.ctf_pickuptime); + float f1, f2 = NO_FL_ARG; + string s1, s2 = NO_STR_ARG; + string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname")); + + // figure shit out + if(!ctf_captimerecord) + { notification = APP_TEAM_ENT_2(flag, INFO_CTF_CAPTURE_TIME_); s1 = player.netname; f1 = (cap_time * 100); success = TRUE; } + else if(cap_time < cap_record) + { notification = APP_TEAM_ENT_2(flag, INFO_CTF_CAPTURE_BROKEN_); s1 = player.netname; s2 = refername; f1 = (cap_time * 100); f2 = (cap_record * 100); success = TRUE; } + else + { notification = APP_TEAM_ENT_2(flag, INFO_CTF_CAPTURE_UNBROKEN_); s1 = player.netname; s2 = refername; f1 = (cap_time * 100); f2 = (cap_record * 100); success = FALSE; } - if(success) - { - 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); - } + // notify about shit + FOR_EACH_REALCLIENT(tmp_entity) + { + if not(tmp_entity.CAPTURE_VERBOSE) { notification = APP_TEAM_ENT_2(flag, INFO_CTF_CAPTURE_); s2 = NO_STR_ARG; f1 = f2 = NO_FL_ARG; } + Send_Notification_Legacy_Wrapper(tmp_entity, MSG_ONE, MSG_INFO, notification, s1, s2, f1, f2, NO_FL_ARG); } - - return cap_message; + + // write that shit in the database + if(success) + { + 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) @@ -60,6 +63,68 @@ void ctf_FlagcarrierWaypoints(entity player) 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); } +} + +float 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 ang; // angle between shotdir and h + 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); + ang = acos(dotproduct(normalize(head_center - passer_center), v_forward)); + a = h * cos(ang); + + 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 @@ -104,9 +169,9 @@ void ctf_CaptureShield_Update(entity player, float wanted_status) if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only { if(updated_status) // TODO csqc notifier for this // Samual: How? - Send_CSQC_Centerprint_Generic(player, CPID_CTF_CAPTURESHIELD, "^3You are now ^4shielded^3 from the flag\n^3for ^1too many unsuccessful attempts^3 to capture.\n\n^3Make some defensive scores before trying again.", 5, 0); + Send_Notification_Legacy_Wrapper(player, MSG_ONE, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED, NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); else - Send_CSQC_Centerprint_Generic(player, CPID_CTF_CAPTURESHIELD, "^3You are now free.\n\n^3Feel free to ^1try to capture^3 the flag again\n^3if you think you will succeed.", 5, 0); + Send_Notification_Legacy_Wrapper(player, MSG_ONE, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_FREE, NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); player.ctf_captureshielded = updated_status; } @@ -129,7 +194,7 @@ void ctf_CaptureShield_Touch() vector othermid = (other.absmin + other.absmax) * 0.5; Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force); - Send_CSQC_Centerprint_Generic(other, CPID_CTF_CAPTURESHIELD, "^3You are ^4shielded^3 from the flag\n^3for ^1too many unsuccessful attempts^3 to capture.\n\n^3Get some defensive scores before trying again.", 5, 0); + Send_Notification_Legacy_Wrapper(other, MSG_ONE, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED, NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); } void ctf_CaptureShield_Spawn(entity flag) @@ -165,13 +230,14 @@ void ctf_Handle_Drop(entity flag, entity player, float droptype) // 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_KillNotification(player.netname, flag.netname, "", INFO_LOSTFLAG, MSG_INFO); + Send_Notification_Legacy_Wrapper(world, MSG_BROADCAST, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_LOST_), player.netname, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTN_NONE); ctf_EventLog("dropped", player.team, player); @@ -193,6 +259,7 @@ void ctf_Handle_Drop(entity flag, entity player, float droptype) if(droptype == DROP_PASS) { + flag.pass_distance = 0; flag.pass_sender = world; flag.pass_target = world; } @@ -213,6 +280,7 @@ void ctf_Handle_Retrieve(entity flag, entity player) 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 @@ -222,11 +290,11 @@ void ctf_Handle_Retrieve(entity flag, entity player) FOR_EACH_REALPLAYER(tmp_player) { if(tmp_player == sender) - centerprint(tmp_player, strcat("You passed the ", flag.netname, " to ", player.netname)); + Send_Notification_Legacy_Wrapper(tmp_player, MSG_ONE, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PASS_SENT_), player.netname, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); else if(tmp_player == player) - centerprint(tmp_player, strcat("You received the ", flag.netname, " from ", sender.netname)); + Send_Notification_Legacy_Wrapper(tmp_player, MSG_ONE, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PASS_RECEIVED_), sender.netname, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); else if(!IsDifferentTeam(tmp_player, sender)) - centerprint(tmp_player, strcat(sender.netname, " passed the ", flag.netname, " to ", player.netname)); + Send_Notification_Legacy_Wrapper(tmp_player, MSG_ONE, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PASS_OTHER_), sender.netname, player.netname, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); } // create new waypoint @@ -235,6 +303,7 @@ void ctf_Handle_Retrieve(entity flag, entity 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; } @@ -264,38 +333,16 @@ void ctf_Handle_Throw(entity player, entity receiver, float 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); - targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); - flag.velocity = (normalize(targ_origin - player.origin) * autocvar_g_ctf_pass_velocity); - break; - } - - case DROP_THROW: - { - makevectors((player.v_angle_y * '0 1 0') + (player.v_angle_x * '0.5 0 0')); - flag_velocity = ('0 0 200' + ((v_forward * autocvar_g_ctf_drop_velocity) * ((player.items & IT_STRENGTH) ? autocvar_g_ctf_drop_strengthmultiplier : 1))); - flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, FALSE); - break; - } - - case DROP_RESET: - { - flag.velocity = '0 0 0'; // do nothing - break; - } - - default: - case DROP_NORMAL: - { - flag.velocity = W_CalculateProjectileVelocity(player.velocity, ('0 0 200' + ('0 100 0' * crandom()) + ('100 0 0' * crandom())), FALSE); - break; - } - } - - switch(droptype) - { - case DROP_PASS: - { + 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; @@ -305,21 +352,31 @@ void ctf_Handle_Throw(entity player, entity receiver, float droptype) // other sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTN_NORM); - WarpZone_TrailParticles(world, particleeffectnum(flag.passeffect), targ_origin, player.origin); + WarpZone_TrailParticles(world, particleeffectnum(flag.passeffect), player.origin, targ_origin); ctf_EventLog("pass", flag.team, player); break; } - - case DROP_RESET: + + case DROP_THROW: { - // do nothing + 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 & IT_STRENGTH) ? 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_THROW: 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; } @@ -350,7 +407,7 @@ void ctf_Handle_Capture(entity flag, entity toucher, float capturetype) if not(player) { return; } // without someone to give the reward to, we can't possibly cap // messages and sounds - Send_KillNotification(player.netname, enemy_flag.netname, ctf_CaptureRecord(enemy_flag, player), INFO_CAPTUREFLAG, MSG_INFO); + ctf_CaptureRecord(enemy_flag, player); sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE); switch(capturetype) @@ -391,8 +448,8 @@ void ctf_Handle_Capture(entity flag, entity toucher, float capturetype) void ctf_Handle_Return(entity flag, entity player) { // messages and sounds - //centerprint(player, strcat("You returned the ", flag.netname)); - Send_KillNotification(player.netname, flag.netname, "", INFO_RETURNFLAG, MSG_INFO); + Send_Notification_Legacy_Wrapper(player, MSG_ONE, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_RETURN_), NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); + Send_Notification_Legacy_Wrapper(world, MSG_BROADCAST, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_RETURN_), player.netname, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTN_NONE); ctf_EventLog("return", flag.team, player); @@ -417,7 +474,6 @@ void ctf_Handle_Pickup(entity flag, entity player, float pickuptype) { // declarations entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players - string verbosename; // holds the name of the player OR no name at all for printing in the centerprints float pickup_dropped_score; // used to calculate dropped pickup score // attach the flag to the player @@ -441,25 +497,20 @@ void ctf_Handle_Pickup(entity flag, entity player, float pickuptype) } // messages and sounds - Send_KillNotification (player.netname, flag.netname, "", INFO_GOTFLAG, MSG_INFO); + Send_Notification_Legacy_Wrapper(world, MSG_BROADCAST, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_PICKUP_), player.netname, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE); - verbosename = ((autocvar_g_ctf_flag_pickup_verbosename) ? strcat(Team_ColorCode(player.team), "(^7", player.netname, Team_ColorCode(player.team), ") ") : ""); - + FOR_EACH_REALPLAYER(tmp_player) { if(tmp_player == player) - centerprint(tmp_player, strcat("You got the ", flag.netname, "!")); - else if(!IsDifferentTeam(tmp_player, player)) - centerprint(tmp_player, strcat("Your ", Team_ColorCode(player.team), "team mate ", verbosename, "^7got the flag! Protect them!")); - else if(!IsDifferentTeam(tmp_player, flag)) - centerprint(tmp_player, strcat("The ", Team_ColorCode(player.team), "enemy ", verbosename, "^7got your flag! Retrieve it!")); - } - - switch(pickuptype) - { - case PICKUP_BASE: ctf_EventLog("steal", flag.team, player); break; - case PICKUP_DROPPED: ctf_EventLog("pickup", flag.team, player); break; - default: break; + { + Send_Notification_Legacy_Wrapper(tmp_player, MSG_ONE, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PICKUP_), NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); + if(ctf_stalemate) { Send_Notification_Legacy_Wrapper(player, MSG_ONE, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER, NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); } + } + else if(!IsDifferentTeam(tmp_player, player) && tmp_player != player) + Send_Notification_Legacy_Wrapper(tmp_player, MSG_ONE, MSG_CENTER, (tmp_player.PICKUP_TEAM_VERBOSE ? CENTER_CTF_PICKUP_TEAM_VERBOSE : CENTER_CTF_PICKUP_TEAM), Team_ColorCode(player.team), (tmp_player.PICKUP_TEAM_VERBOSE ? player.netname : NO_STR_ARG), NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); + else if(IsDifferentTeam(tmp_player, player)) + Send_Notification_Legacy_Wrapper(tmp_player, MSG_ONE, MSG_CENTER, (tmp_player.PICKUP_ENEMY_VERBOSE ? CENTER_CTF_PICKUP_ENEMY_VERBOSE : CENTER_CTF_PICKUP_ENEMY), Team_ColorCode(player.team), (tmp_player.PICKUP_ENEMY_VERBOSE ? player.netname : NO_STR_ARG), NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); } // scoring @@ -469,6 +520,7 @@ void ctf_Handle_Pickup(entity flag, entity player, float pickuptype) case PICKUP_BASE: { PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base); + ctf_EventLog("steal", flag.team, player); break; } @@ -478,6 +530,7 @@ void ctf_Handle_Pickup(entity flag, entity player, float pickuptype) 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); dprint("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n"); PlayerTeamScore_AddScore(player, pickup_dropped_score); + ctf_EventLog("pickup", flag.team, player); break; } @@ -508,58 +561,63 @@ void ctf_Handle_Pickup(entity flag, entity player, float pickuptype) void ctf_CheckFlagReturn(entity flag, float returntype) { - 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)) + if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING)) { - switch(returntype) + 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)) { - case RETURN_DROPPED: bprint("The ", flag.netname, " was dropped in the base and returned itself\n"); break; - case RETURN_DAMAGE: bprint("The ", flag.netname, " was destroyed and returned to base\n"); break; - case RETURN_SPEEDRUN: bprint("The ", flag.netname, " became impatient after ", ftos_decimals(ctf_captimerecord, 2), " seconds and returned itself\n"); break; - case RETURN_NEEDKILL: bprint("The ", flag.netname, " fell somewhere it couldn't be reached and returned to base\n"); break; - - default: - case RETURN_TIMEOUT: - { bprint("The ", flag.netname, " has returned to base\n"); break; } + switch(returntype) + { + case RETURN_DROPPED: Send_Notification_Legacy_Wrapper(world, MSG_BROADCAST, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_DROPPED_), NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); break; + case RETURN_DAMAGE: Send_Notification_Legacy_Wrapper(world, MSG_BROADCAST, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_DAMAGED_), NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); break; + case RETURN_SPEEDRUN: Send_Notification_Legacy_Wrapper(world, MSG_BROADCAST, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_SPEEDRUN_), NO_STR_ARG, NO_STR_ARG, ctf_captimerecord, NO_FL_ARG, NO_FL_ARG); break; + case RETURN_NEEDKILL: Send_Notification_Legacy_Wrapper(world, MSG_BROADCAST, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_NEEDKILL_), NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); break; + + default: + case RETURN_TIMEOUT: + { Send_Notification_Legacy_Wrapper(world, MSG_BROADCAST, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_TIMEOUT_), NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); break; } + } + sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTN_NONE); + ctf_EventLog("returned", flag.team, world); + ctf_RespawnFlag(flag); } - sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTN_NONE); - ctf_EventLog("returned", flag.team, world); - ctf_RespawnFlag(flag); } } void ctf_CheckStalemate(void) { // declarations - float stale_red_flags, stale_blue_flags; + float stale_red_flags = 0, stale_blue_flags = 0; entity tmp_entity; - entity ctf_staleflaglist; // reset the list, we need to build the list each time this function runs + 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_flagcarrier_waypointforenemy_stalemate) + if(autocvar_g_ctf_stalemate) if(tmp_entity.ctf_status != FLAG_BASE) - if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_flagcarrier_waypointforenemy_stalemate) + if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time) { tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist ctf_staleflaglist = tmp_entity; switch(tmp_entity.team) { - case COLOR_TEAM1: ++stale_red_flags; break; - case COLOR_TEAM2: ++stale_blue_flags; break; + case FL_TEAM_1: ++stale_red_flags; break; + case FL_TEAM_2: ++stale_blue_flags; break; } } } if(stale_red_flags && stale_blue_flags) ctf_stalemate = TRUE; - else if(!stale_red_flags && !stale_blue_flags) - ctf_stalemate = FALSE; - + else if((!stale_red_flags && !stale_blue_flags) && autocvar_g_ctf_stalemate_endcondition == 2) + { ctf_stalemate = FALSE; wpforenemy_announced = FALSE; } + else if((!stale_red_flags || !stale_blue_flags) && 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) { @@ -573,9 +631,9 @@ void ctf_CheckStalemate(void) { FOR_EACH_REALPLAYER(tmp_entity) if(tmp_entity.flagcarried) - centerprint(tmp_entity, "Stalemate! Enemies can now see you on radar!"); + Send_Notification_Legacy_Wrapper(tmp_entity, MSG_ONE, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER, NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); else - centerprint(tmp_entity, "Stalemate! Flag carriers can now be seen by enemies on radar!"); + Send_Notification_Legacy_Wrapper(tmp_entity, MSG_ONE, MSG_CENTER, CENTER_CTF_STALEMATE_OTHER, NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); wpforenemy_announced = TRUE; } @@ -622,7 +680,6 @@ void ctf_FlagThink() switch(self.ctf_status) // reset flag angles in case warpzones adjust it { case FLAG_DROPPED: - case FLAG_PASSING: { self.angles = '0 0 0'; break; @@ -640,8 +697,9 @@ void ctf_FlagThink() { 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) - ctf_Handle_Capture(self, tmp_entity, CAPTURE_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; } @@ -693,7 +751,7 @@ void ctf_FlagThink() ImpulseCommands(); self = tmp_entity; } - if(autocvar_g_ctf_flagcarrier_waypointforenemy_stalemate) + if(autocvar_g_ctf_stalemate) { if(time >= wpforenemy_nextthink) { @@ -704,28 +762,25 @@ void ctf_FlagThink() return; } - case FLAG_PASSING: // todo make work with warpzones + case FLAG_PASSING: { vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5); - vector old_targ_origin = targ_origin; - targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); + 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); - - print(strcat("self: ", vtos(self.origin), ", old: ", vtos(old_targ_origin), " (", ftos(vlen(self.origin - old_targ_origin)), "qu)"), ", transformed: ", vtos(targ_origin), " (", ftos(vlen(self.origin - targ_origin)), "qu)", ".\n"); - if((self.pass_target.deadflag != DEAD_NO) + if((self.pass_target == world) + || (self.pass_target.deadflag != DEAD_NO) || (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 + else { - vector desired_direction = normalize(targ_origin - self.origin); - vector current_direction = normalize(self.velocity); - - self.velocity = (normalize(current_direction + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); + // still a viable target, go for it + ctf_CalculatePassVelocity(self, targ_origin, self.origin, TRUE); } return; } @@ -812,8 +867,15 @@ void ctf_FlagTouch() } } +.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)) { @@ -845,13 +907,12 @@ void ctf_RespawnFlag(entity flag) 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; - - wpforenemy_announced = FALSE; } void ctf_Reset() @@ -871,7 +932,7 @@ void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf self.bot_basewaypoint = self.nearestwaypoint; // waypointsprites - WaypointSprite_SpawnFixed(((self.team == COLOR_TEAM1) ? "redbase" : "bluebase"), self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE)); + WaypointSprite_SpawnFixed(((self.team == FL_TEAM_1) ? "redbase" : "bluebase"), self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE)); WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE)); // captureshield setup @@ -888,10 +949,10 @@ void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist ctf_worldflaglist = flag; - setattachment(flag, world, ""); + setattachment(flag, world, ""); - flag.netname = ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag"); - flag.team = ((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2); // COLOR_TEAM1: color 4 team (red) - COLOR_TEAM2: color 13 team (blue) + flag.netname = ((teamnumber) ? "^1REPLACETHIS^7" : "^4REPLACETHIS^7"); // ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag"); + flag.team = ((teamnumber) ? FL_TEAM_1 : FL_TEAM_2); // FL_TEAM_1: color 4 team (red) - FL_TEAM_2: color 13 team (blue) flag.items = ((teamnumber) ? IT_KEY2 : IT_KEY1); // IT_KEY2: gold key (redish enough) - IT_KEY1: silver key (bluish enough) flag.classname = "item_flag_team"; flag.target = "###item###"; // wut? @@ -977,6 +1038,663 @@ void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag } +// ================ +// 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 (bot.team == f.team) + return f; + f = f.ctf_worldflagnext; + } + return world; +} + +entity havocbot_ctf_find_enemy_flag(entity bot) +{ + entity f; + f = ctf_worldflaglist; + while (f) + { + if (bot.team != f.team) + return f; + f = f.ctf_worldflagnext; + } + return world; +} + +float havocbot_ctf_teamcount(entity bot, vector org, float tc_radius) +{ + if not(teamplay) + return 0; + + float c = 0; + entity head; + + FOR_EACH_PLAYER(head) + { + if(head.team!=bot.team || head.deadflag != DEAD_NO || head == bot) + continue; + + if(vlen(head.origin - org) < tc_radius) + ++c; + } + + return c; +} + +void havocbot_goalrating_ctf_ourflag(float ratingscale) +{ + entity head; + head = ctf_worldflaglist; + while (head) + { + if (self.team == head.team) + break; + head = head.ctf_worldflagnext; + } + if (head) + navigation_routerating(head, ratingscale, 10000); +} + +void havocbot_goalrating_ctf_ourbase(float ratingscale) +{ + entity head; + head = ctf_worldflaglist; + while (head) + { + if (self.team == head.team) + break; + head = head.ctf_worldflagnext; + } + if not(head) + return; + + navigation_routerating(head.bot_basewaypoint, ratingscale, 10000); +} + +void havocbot_goalrating_ctf_enemyflag(float ratingscale) +{ + entity head; + head = ctf_worldflaglist; + while (head) + { + if (self.team != head.team) + break; + head = head.ctf_worldflagnext; + } + if (head) + navigation_routerating(head, ratingscale, 10000); +} + +void havocbot_goalrating_ctf_enemybase(float ratingscale) +{ + if not(bot_waypoints_for_items) + { + havocbot_goalrating_ctf_enemyflag(ratingscale); + return; + } + + entity head; + + head = havocbot_ctf_find_enemy_flag(self); + + if not(head) + return; + + navigation_routerating(head.bot_basewaypoint, ratingscale, 10000); +} + +void havocbot_goalrating_ctf_ourstolenflag(float ratingscale) +{ + 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(head.team==bot.team) + ++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() +{ + 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(); + 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, self.origin, '0 0 0'); + return; + } + } +} + +void havocbot_role_ctf_escort() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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, float role) +{ + dprint(strcat(bot.netname," switched to ")); + switch(role) + { + case HAVOCBOT_CTF_ROLE_CARRIER: + dprint("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: + dprint("defense"); + bot.havocbot_role = havocbot_role_ctf_defense; + bot.havocbot_role_timeout = 0; + break; + case HAVOCBOT_CTF_ROLE_MIDDLE: + dprint("middle"); + bot.havocbot_role = havocbot_role_ctf_middle; + bot.havocbot_role_timeout = 0; + break; + case HAVOCBOT_CTF_ROLE_OFFENSE: + dprint("offense"); + bot.havocbot_role = havocbot_role_ctf_offense; + bot.havocbot_role_timeout = 0; + break; + case HAVOCBOT_CTF_ROLE_RETRIEVER: + dprint("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: + dprint("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; + } + dprint("\n"); +} + + // ============== // Hook Functions // ============== @@ -1039,8 +1757,12 @@ MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values t } else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && IsDifferentTeam(frag_target, frag_attacker)) // if the target is a flagcarrier { - if(autocvar_g_ctf_flagcarrier_auto_helpme_when_damaged > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent))) - WaypointSprite_HelpMePing(frag_target.wps_flagcarrier); // TODO: only do this if there is a significant loss of health? + if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent))) + 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); + } } return FALSE; } @@ -1067,8 +1789,17 @@ MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill) MUTATOR_HOOKFUNCTION(ctf_RemovePlayer) { + entity flag; // temporary entity for the search method + if(self.flagcarried) { ctf_Handle_Throw(self, world, DROP_NORMAL); } + + for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext) + { + if(flag.pass_sender == self) { flag.pass_sender = world; } + if(flag.pass_target == self) { flag.pass_target = world; } + if(flag.ctf_dropper == self) { flag.ctf_dropper = world; } + } return FALSE; } @@ -1084,7 +1815,7 @@ MUTATOR_HOOKFUNCTION(ctf_PortalTeleport) MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey) { - if(gameover) { return FALSE; } + if(MUTATOR_RETURNVALUE || gameover) { return FALSE; } entity player = self; @@ -1093,38 +1824,46 @@ MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey) // pass the flag to a team mate if(autocvar_g_ctf_pass) { - entity head, closest_target; + 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(head.classname == "player" && head.deadflag == DEAD_NO) if(head != player && !IsDifferentTeam(head, player)) - if(!head.speedrunning && (!head.vehicle || autocvar_g_ctf_allow_vehicle_touch)) + if(!head.speedrunning && !head.vehicle) { - if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried) - { - if(clienttype(head) == CLIENTTYPE_BOT) - { - centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname)); - ctf_Handle_Throw(head, player, DROP_PASS); - } - else - { - centerprint(head, strcat(player.netname, " requests you to pass the ", head.flagcarried.netname)); - centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname)); - } - player.throw_antispam = time + autocvar_g_ctf_pass_wait; - return TRUE; - } - else if(player.flagcarried) + // 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(closest_target) + if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried) + { + if(clienttype(head) == CLIENTTYPE_BOT) + { + Send_Notification_Legacy_Wrapper(player, MSG_ONE, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); + ctf_Handle_Throw(head, player, DROP_PASS); + } + else + { + Send_Notification_Legacy_Wrapper(head, MSG_ONE, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); + Send_Notification_Legacy_Wrapper(player, MSG_ONE, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); + } + player.throw_antispam = time + autocvar_g_ctf_pass_wait; + return TRUE; + } + else if(player.flagcarried) { - if(vlen(player.origin - WarpZone_UnTransformOrigin(head, head.origin)) < vlen(player.origin - WarpZone_UnTransformOrigin(closest_target, closest_target.origin))) - { closest_target = head; } + 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; } } - else { closest_target = head; } } } head = head.chain; @@ -1134,8 +1873,34 @@ MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey) } // throw the flag in front of you - if(autocvar_g_ctf_drop && player.flagcarried) - { ctf_Handle_Throw(player, world, DROP_THROW); return TRUE; } + 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_Legacy_Wrapper(player, MSG_ONE, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, NO_STR_ARG, NO_STR_ARG, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time), NO_FL_ARG, NO_FL_ARG); + 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; @@ -1145,6 +1910,7 @@ MUTATOR_HOOKFUNCTION(ctf_HelpMePing) { 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 @@ -1160,7 +1926,7 @@ MUTATOR_HOOKFUNCTION(ctf_VehicleEnter) { if(vh_player.flagcarried) { - if(!autocvar_g_ctf_flagcarrier_allow_vehicle_carry) + if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch) { ctf_Handle_Throw(vh_player, world, DROP_NORMAL); } @@ -1195,8 +1961,8 @@ MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun) { if(self.flagcarried) { - bprint("The ", self.flagcarried.netname, " was returned to base by its carrier\n"); - ctf_RespawnFlag(self); + Send_Notification_Legacy_Wrapper(world, MSG_BROADCAST, MSG_INFO, APP_TEAM_ENT_2(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN_), NO_STR_ARG, NO_STR_ARG, NO_FL_ARG, NO_FL_ARG, NO_FL_ARG); + ctf_RespawnFlag(self.flagcarried); return TRUE; } @@ -1220,7 +1986,7 @@ MUTATOR_HOOKFUNCTION(ctf_MatchEnd) flag.solid = SOLID_NOT; flag.nextthink = FALSE; // stop thinking - print("stopping the ", flag.netname, " from moving.\n"); + //dprint("stopping the ", flag.netname, " from moving.\n"); break; } @@ -1237,6 +2003,20 @@ MUTATOR_HOOKFUNCTION(ctf_MatchEnd) return FALSE; } +MUTATOR_HOOKFUNCTION(ctf_BotRoles) +{ + havocbot_ctf_reset_role(self); + return TRUE; +} + +MUTATOR_HOOKFUNCTION(ctf_GetCvars) +{ + GetCvars_handleFloat(get_cvars_s, get_cvars_f, CAPTURE_VERBOSE, "notification_ctf_capture_verbose"); + GetCvars_handleFloat(get_cvars_s, get_cvars_f, PICKUP_TEAM_VERBOSE, "notification_ctf_pickup_team_verbose"); + GetCvars_handleFloat(get_cvars_s, get_cvars_f, PICKUP_ENEMY_VERBOSE, "notification_ctf_pickup_enemy_verbose"); + return TRUE; +} + // ========== // Spawnfuncs @@ -1249,7 +2029,7 @@ void spawnfunc_info_player_team1() { if(g_assault) { remove(self); return; } - self.team = COLOR_TEAM1; // red + self.team = FL_TEAM_1; // red spawnfunc_info_player_deathmatch(); } @@ -1261,7 +2041,7 @@ void spawnfunc_info_player_team2() { if(g_assault) { remove(self); return; } - self.team = COLOR_TEAM2; // blue + self.team = FL_TEAM_2; // blue spawnfunc_info_player_deathmatch(); } @@ -1272,7 +2052,7 @@ void spawnfunc_info_player_team3() { if(g_assault) { remove(self); return; } - self.team = COLOR_TEAM3; // yellow + self.team = FL_TEAM_3; // yellow spawnfunc_info_player_deathmatch(); } @@ -1284,7 +2064,7 @@ void spawnfunc_info_player_team4() { if(g_assault) { remove(self); return; } - self.team = COLOR_TEAM4; // purple + self.team = FL_TEAM_4; // purple spawnfunc_info_player_deathmatch(); } @@ -1338,11 +2118,33 @@ void spawnfunc_ctf_team() self.team = self.cnt + 1; } +// compatibility for quake maps +void spawnfunc_team_CTF_redflag() { spawnfunc_item_flag_team1(); } +void spawnfunc_team_CTF_blueflag() { spawnfunc_item_flag_team2(); } +void spawnfunc_team_CTF_redplayer() { spawnfunc_info_player_team1(); } +void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2(); } +void spawnfunc_team_CTF_redspawn() { spawnfunc_info_player_team1(); } +void spawnfunc_team_CTF_bluespawn() { spawnfunc_info_player_team2(); } + // ============== // Initialization // ============== +// scoreboard setup +void ctf_ScoreRules() +{ + ScoreRules_basics(2, 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, float teamcolor) { @@ -1364,11 +2166,11 @@ void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to if(find(world, classname, "ctf_team") == world) { print("No ""ctf_team"" entities found on this map, creating them anyway.\n"); - ctf_SpawnTeam("Red", COLOR_TEAM1 - 1); - ctf_SpawnTeam("Blue", COLOR_TEAM2 - 1); + ctf_SpawnTeam("Red", FL_TEAM_1 - 1); + ctf_SpawnTeam("Blue", FL_TEAM_2 - 1); } - ScoreRules_ctf(); + ctf_ScoreRules(); } void ctf_Initialize() @@ -1398,19 +2200,27 @@ MUTATOR_DEFINITION(gamemode_ctf) MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY); MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY); MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY); + MUTATOR_HOOK(HavocBot_ChooseRule, ctf_BotRoles, CBC_ORDER_ANY); + MUTATOR_HOOK(GetCvars, ctf_GetCvars, CBC_ORDER_ANY); MUTATOR_ONADD { if(time > 1) // game loads at time 1 error("This is a game type and it cannot be added at runtime."); - g_ctf = 1; 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 { - g_ctf = 0; - error("This is a game type and it cannot be removed at runtime."); + print("This is a game type and it cannot be removed at runtime."); + return -1; } return 0;