From 56b075a958408e6e3296a88da25b56a0e5d30143 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Fri, 27 Feb 2026 13:14:37 -0600 Subject: [PATCH] set-guard-range --- code/ai/aicode.cpp | 26 ++++++++++++++++++------- code/parse/sexp.cpp | 47 +++++++++++++++++++++++++++++++++++++++++++++ code/parse/sexp.h | 1 + code/ship/ship.cpp | 1 + code/ship/ship.h | 1 + 5 files changed, 69 insertions(+), 7 deletions(-) diff --git a/code/ai/aicode.cpp b/code/ai/aicode.cpp index 99e6337d841..6e1b0448af5 100644 --- a/code/ai/aicode.cpp +++ b/code/ai/aicode.cpp @@ -321,6 +321,18 @@ void ai_cleanup_dock_mode_objective(object *objp); // the "autopilot" object *Autopilot_flight_leader = NULL; +static inline float ai_guard_threshold(const object* guarded_objp, float threshold) +{ + if (guarded_objp != nullptr && guarded_objp->type == OBJ_SHIP && guarded_objp->instance >= 0) { + const float configured = Ships[guarded_objp->instance].max_guard_radius; + if (configured > 0.0f) { + return configured; + } + } + + return threshold; +} + /** * Sets the timestamp used to tell is it is a good time for this team to rearm. * Ends a 'bad rearm time' @@ -5115,9 +5127,9 @@ int maybe_resume_previous_mode(object *objp, ai_info *aip) // If guarding ship is far away from guardee and enemy is far away from guardee, // then stop chasing and resume guarding. - if (dist > (MAX_GUARD_DIST + guard_objp->radius) * 6) { + if (dist > ai_guard_threshold(guard_objp, 6.0f)) { if ((En_objp != NULL) && (En_objp->type == OBJ_SHIP)) { - if (vm_vec_dist_quick(&guard_objp->pos, &En_objp->pos) > (MAX_GUARD_DIST + guard_objp->radius) * 6) { + if (vm_vec_dist_quick(&guard_objp->pos, &En_objp->pos) > ai_guard_threshold(guard_objp, 6.0f)) { Assert(aip->previous_mode == AIM_GUARD); aip->mode = aip->previous_mode; aip->submode = AIS_GUARD_PATROL; @@ -10515,7 +10527,7 @@ int ai_guard_find_nearby_bomb(object *guarding_objp, object *guarded_objp) dist = vm_vec_dist_quick(&bomb_objp->pos, &guarded_objp->pos); - if (dist < (MAX_GUARD_DIST + guarded_objp->radius)*3) { + if (dist < ai_guard_threshold(guarded_objp, 3.0f)) { dist_to_guarding_obj = vm_vec_dist_quick(&bomb_objp->pos, &guarding_objp->pos); if ( dist_to_guarding_obj < closest_dist_to_guarding_obj ) { closest_dist_to_guarding_obj = dist_to_guarding_obj; @@ -10559,11 +10571,11 @@ void ai_guard_find_nearby_ship(object *guarding_objp, object *guarded_objp) if (Ship_info[eshipp->ship_info_index].class_type >= 0 && (Ship_types[Ship_info[eshipp->ship_info_index].class_type].flags[Ship::Type_Info_Flags::AI_guards_attack])) { dist = vm_vec_dist_quick(&enemy_objp->pos, &guarded_objp->pos); - if (dist < (MAX_GUARD_DIST + guarded_objp->radius)*3) + if (dist < ai_guard_threshold(guarded_objp, 3.0f)) { guard_object_was_hit(guarding_objp, enemy_objp); - } - else if ((dist < 3000.0f) && (Ai_info[eshipp->ai_index].target_objnum == guarding_aip->guard_objnum)) + } else if ((dist < ai_guard_threshold(guarded_objp, 3000.0f)) && + (Ai_info[eshipp->ai_index].target_objnum == guarding_aip->guard_objnum)) { guard_object_was_hit(guarding_objp, enemy_objp); } @@ -10590,7 +10602,7 @@ void ai_guard_find_nearby_asteroid(object *guarding_objp, object *guarded_objp) if ( asteroid_objp->type == OBJ_ASTEROID ) { // Attack asteroid if near guarded ship dist = vm_vec_dist_quick(&asteroid_objp->pos, &guarded_objp->pos); - if ( dist < (MAX_GUARD_DIST + guarded_objp->radius)*2) { + if (dist < ai_guard_threshold(guarded_objp, 2.0f)) { dist_to_self = vm_vec_dist_quick(&asteroid_objp->pos, &guarding_objp->pos); if ( OBJ_INDEX(guarded_objp) == asteroid_collide_objnum(asteroid_objp) ) { if( dist_to_self < closest_danger_asteroid_dist ) { diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index 7b75b5d4ad4..9c0efd31b87 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -524,6 +524,7 @@ SCP_vector Operators = { { "ship-no-guardian", OP_SHIP_NO_GUARDIAN, 1, INT_MAX, SEXP_ACTION_OPERATOR, }, { "ship-guardian-threshold", OP_SHIP_GUARDIAN_THRESHOLD, 2, INT_MAX, SEXP_ACTION_OPERATOR, }, { "ship-subsys-guardian-threshold", OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD, 3, INT_MAX, SEXP_ACTION_OPERATOR, }, + { "set-guard-range", OP_SET_GUARD_RANGE, 2, INT_MAX, SEXP_ACTION_OPERATOR, }, // MjnMixael { "self-destruct", OP_SELF_DESTRUCT, 1, INT_MAX, SEXP_ACTION_OPERATOR, }, { "destroy-instantly", OP_DESTROY_INSTANTLY, 1, INT_MAX, SEXP_ACTION_OPERATOR, }, // Admiral MS { "destroy-instantly-with-debris", OP_DESTROY_INSTANTLY_WITH_DEBRIS, 1, INT_MAX, SEXP_ACTION_OPERATOR, }, // Asteroth @@ -19467,6 +19468,29 @@ void sexp_ship_guardian_threshold(int node) } } +// MjnMixael +void sexp_set_guard_range(int node) +{ + int range, n = node; + bool is_nan, is_nan_forever; + + range = eval_num(n, is_nan, is_nan_forever); + if (is_nan || is_nan_forever) + return; + n = CDR(n); + + for (; n != -1; n = CDR(n)) { + auto ship_entry = eval_ship(n); + if (!ship_entry || !ship_entry->has_shipp()) { + continue; + } + + // Intentionally no lower bound validation beyond disabling at <= 0. + // Mission authors may choose very small positive values for highly restrictive escort behavior. + ship_entry->shipp()->max_guard_radius = (range > 0) ? static_cast(range) : -1.0f; + } +} + // Goober5000 void sexp_ship_subsys_guardian_threshold(int node) { @@ -28860,6 +28884,11 @@ int eval_sexp(int cur_node, int referenced_node) sexp_val = SEXP_TRUE; break; + case OP_SET_GUARD_RANGE: + sexp_set_guard_range(node); + sexp_val = SEXP_TRUE; + break; + case OP_SHIP_SUBSYS_TARGETABLE: sexp_ship_deal_with_subsystem_flag(cur_node, node, Ship::Subsystem_Flags::Untargetable, true, false); sexp_val = SEXP_TRUE; @@ -31597,6 +31626,7 @@ int query_operator_return_type(int op) case OP_SHIP_NO_GUARDIAN: case OP_SHIP_GUARDIAN_THRESHOLD: case OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD: + case OP_SET_GUARD_RANGE: case OP_SHIP_VANISH: case OP_PROP_VANISH: case OP_DESTROY_INSTANTLY: @@ -32308,6 +32338,12 @@ int query_operator_argument_type(int op, int argnum) else return OPF_SUBSYS_OR_GENERIC; + case OP_SET_GUARD_RANGE: + if (argnum == 0) + return OPF_NUMBER; + else + return OPF_SHIP; + case OP_SHIP_SUBSYS_TARGETABLE: case OP_SHIP_SUBSYS_UNTARGETABLE: if (argnum == 0) @@ -36895,6 +36931,7 @@ int get_category(int op_id) case OP_JUMP_NODE_HIDE_JUMPNODE: case OP_SHIP_GUARDIAN_THRESHOLD: case OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD: + case OP_SET_GUARD_RANGE: case OP_SET_SKYBOX_MODEL: case OP_SHIP_CREATE: case OP_PROP_CREATE: @@ -37252,6 +37289,7 @@ int get_subcategory(int op_id) case OP_ALTER_SHIP_FLAG: case OP_ALTER_WING_FLAG: + case OP_SET_GUARD_RANGE: case OP_PROTECT_SHIP: case OP_UNPROTECT_SHIP: case OP_BEAM_PROTECT_SHIP: @@ -40447,6 +40485,15 @@ SCP_vector Sexp_help = { "\t2:\tShip housing the subsystem(s) (ships must be in-mission).\r\n" "\t3+:\tSubsystems to make unkillable." }, + // MjnMixael + { OP_SET_GUARD_RANGE, "set-guard-range\r\n" + "\tSets the max range in meters at which any ships guarding this ship will engage with threats.\r\n" + "This range will override the default dynamic range behavior for ships obeying a guard order.\r\n" + "If the value is <= 0, regular dynamic guard range behavior will resume. Positive values are used as is with no size validation based on ship class.\r\n\r\n" + "Takes 2 or more arguments...\r\n" + "\t1:\tGuard range cap in meters (<= 0 disables cap).\r\n" + "\t2+:\tShip(s) to apply the cap to (ships must be in-mission)." }, + // Goober5000 { OP_SHIP_STEALTHY, "ship-stealthy\r\n" "\tCauses the ships listed in this sexpression to become stealth ships (i.e. invisible to radar).\r\n\r\n" diff --git a/code/parse/sexp.h b/code/parse/sexp.h index abe08dbfd91..6e78e3de3d7 100644 --- a/code/parse/sexp.h +++ b/code/parse/sexp.h @@ -695,6 +695,7 @@ enum : int { OP_JUMP_NODE_HIDE_JUMPNODE, // WMC OP_SHIP_GUARDIAN_THRESHOLD, // Goober5000 OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD, // Goober5000 + OP_SET_GUARD_RANGE, //MjnMixael OP_SET_SKYBOX_MODEL, // taylor OP_SHIP_CREATE, OP_PROP_CREATE, // MjnMixael diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 0bed76538e3..2437bc0a0a0 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -7184,6 +7184,7 @@ void ship::clear() ship_max_hull_strength = 0.0f; ship_guardian_threshold = 0; + max_guard_radius = -1.0f; ship_name[0] = 0; display_name.clear(); diff --git a/code/ship/ship.h b/code/ship/ship.h index 5c1bc8f0454..a22f5f92918 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -622,6 +622,7 @@ class ship float max_weapon_regen_per_second; // wookieejedi - make this a ship object variable int ship_guardian_threshold; // Goober5000 - now also determines whether ship is guardian'd + float max_guard_radius; // Optional clamp for guard engagement/resume ranges; <= 0 means unused char ship_name[NAME_LENGTH];