diff --git a/code/ai/aicode.cpp b/code/ai/aicode.cpp index f171da4e701..817ea257ae8 100644 --- a/code/ai/aicode.cpp +++ b/code/ai/aicode.cpp @@ -323,12 +323,17 @@ 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) +static inline float ai_guard_threshold(const object* guarded_objp, float threshold, const ship* guarder_ship) { 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; + for (const auto& range_entry : guarder_ship->max_guard_ranges) { + const int regester = ship_registry_get_index(Ships[guarded_objp->instance].ship_name); + if (regester == range_entry.shipnum) { + const float configured = range_entry.range; + if (configured > 0.0f) { + return configured; + } + } } } @@ -5161,10 +5166,13 @@ 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 > ai_guard_threshold(guard_objp, (MAX_GUARD_DIST + guard_objp->radius) * 6)) + if (dist > ai_guard_threshold(guard_objp, (MAX_GUARD_DIST + guard_objp->radius) * 6, &Ships[aip->shipnum])) { if ((En_objp != NULL) && (En_objp->type == OBJ_SHIP)) { - if (vm_vec_dist_quick(&guard_objp->pos, &En_objp->pos) > ai_guard_threshold(guard_objp, (MAX_GUARD_DIST + guard_objp->radius) * 6)) { + if (vm_vec_dist_quick(&guard_objp->pos, &En_objp->pos) > + ai_guard_threshold(guard_objp, + (MAX_GUARD_DIST + guard_objp->radius) * 6, + &Ships[aip->shipnum])) { Assert(aip->previous_mode == AIM_GUARD); aip->mode = aip->previous_mode; aip->submode = AIS_GUARD_PATROL; @@ -10637,7 +10645,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 < ai_guard_threshold(guarded_objp, (MAX_GUARD_DIST + guarded_objp->radius) * 3)) { + if (dist < ai_guard_threshold(guarded_objp, (MAX_GUARD_DIST + guarded_objp->radius) * 3, &Ships[guarding_objp->instance])) { 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; @@ -10688,10 +10696,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 < ai_guard_threshold(guarded_objp, (MAX_GUARD_DIST + guarded_objp->radius) * 3)) + if (dist < + ai_guard_threshold(guarded_objp, (MAX_GUARD_DIST + guarded_objp->radius) * 3, guarding_shipp)) { guard_object_was_hit(guarding_objp, enemy_objp); - } else if ((dist < ai_guard_threshold(guarded_objp, 3000.0f)) && + } else if ((dist < ai_guard_threshold(guarded_objp, 3000.0f, guarding_shipp)) && (Ai_info[eshipp->ai_index].target_objnum == guarding_aip->guard_objnum)) { guard_object_was_hit(guarding_objp, enemy_objp); @@ -10719,7 +10728,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 < ai_guard_threshold(guarded_objp, (MAX_GUARD_DIST + guarded_objp->radius) * 2)) { + if (dist < ai_guard_threshold(guarded_objp, (MAX_GUARD_DIST + guarded_objp->radius) * 2, &Ships[guarding_objp->instance])) { 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/missioneditor/sexp_tree_model.cpp b/code/missioneditor/sexp_tree_model.cpp index 7b489d765c9..c5207ba8dbd 100644 --- a/code/missioneditor/sexp_tree_model.cpp +++ b/code/missioneditor/sexp_tree_model.cpp @@ -1248,6 +1248,7 @@ bool SexpTreeModel::is_operator_hidden(int op_value) case OP_SET_DEBRIS_FIELD: case OP_NEBULA_TOGGLE_POOF: case OP_NEBULA_FADE_POOF: + case OP_SET_GUARD_RANGE: return true; default: return false; diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index 539fa03406c..38ff21b69fe 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -19537,27 +19537,47 @@ void sexp_ship_guardian_threshold(int node) ship_entry->shipp()->ship_guardian_threshold = threshold; } } - // MjnMixael void sexp_set_guard_range(int node) { int range, n = node; bool is_nan, is_nan_forever; - + auto ship_entry = eval_ship(n); + if (!ship_entry || !ship_entry->has_shipp()) { + return; + } + n = CDR(n); range = eval_num(n, is_nan, is_nan_forever); - if (is_nan || is_nan_forever) + if (is_nan || is_nan_forever) { return; + } + auto true_range = static_cast(range); n = CDR(n); - for (; n != -1; n = CDR(n)) { - auto ship_entry = eval_ship(n); - if (!ship_entry || !ship_entry->has_shipp()) { + object_ship_wing_point_team oswpt; + eval_object_ship_wing_point_team(&oswpt, n); + if (oswpt.type == OSWPT_TYPE_SHIP) { + auto shipp = oswpt.shipp(); + set_guard_range_ship(true_range, ship_entry, shipp); + } else if (oswpt.type == OSWPT_TYPE_WING) { + for (int i = 0; i < oswpt.wingp()->current_count; ++i) { + auto shipp = &Ships[oswpt.wingp()->ship_index[i]]; + set_guard_range_ship(true_range, ship_entry, shipp); + } + } else if (oswpt.type == OSWPT_TYPE_WHOLE_TEAM) { + ship_obj* so; + for (so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so)) { + if (Objects[so->objnum].flags[Object::Object_Flags::Should_be_dead]) + continue; + + auto shipp = &Ships[Objects[so->objnum].instance]; + if (shipp->team == oswpt.team) { + set_guard_range_ship(true_range, ship_entry, shipp); + } + } + } else { 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; } } @@ -28982,7 +29002,6 @@ int eval_sexp(int cur_node, int referenced_node) 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; @@ -32457,9 +32476,11 @@ int query_operator_argument_type(int op_index, int argnum) case OP_SET_GUARD_RANGE: if (argnum == 0) + return OPF_SHIP; + else if (argnum == 1) return OPF_NUMBER; else - return OPF_SHIP; + return OPF_SHIP_WING_WHOLETEAM; case OP_SHIP_SUBSYS_TARGETABLE: case OP_SHIP_SUBSYS_UNTARGETABLE: @@ -40613,12 +40634,14 @@ SCP_vector Sexp_help = { // 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" + "\tLimits the range that selected ships or wings can move when guarding a specific ship\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)." }, + "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" + "Warning: Will not apply to future waves of wings or ships not currently in mission.\r\n\r\n" + "Takes 3 or more arguments...\r\n" + "\t1:\tShip the escorts won't leave the range of if guarding (Ship must be in mission)\r\n" + "\t2:\tGuard range cap in meters (<= 0 disables cap)\r\n" + "\t3+:\tEscort ships and wings that the limit appies too" }, // Goober5000 { OP_SHIP_STEALTHY, "ship-stealthy\r\n" diff --git a/code/scripting/api/objs/ship.cpp b/code/scripting/api/objs/ship.cpp index 537a3f8c150..a979a93fe60 100644 --- a/code/scripting/api/objs/ship.cpp +++ b/code/scripting/api/objs/ship.cpp @@ -1162,22 +1162,39 @@ ADE_VIRTVAR(Orders, l_Ship, "shiporders", "Array of ship orders", "shiporders", return ade_set_args(L, "o", l_ShipOrders.Set(object_h(objh->objp()))); } -ADE_VIRTVAR(MaxGuardRadius, l_Ship, "number", "Sets the max range in meters at which any ships guarding this ship will engage with threats. If the value is <= 0, regular dynamic guard range behavior will resume.", "number", "Max range in meters, or 0 if handle is invalid") +ADE_VIRTVAR(MaxGuardRadius, + l_Ship, + "number", + "Sets the max range in meters at which any ships guarding this ship will engage with threats. If the value is <= " + "0, regular dynamic guard range behavior will resume.", nullptr, + nullptr) { object_h *objh; float new_max_guard_radius = -1; if (!ade_get_args(L, "o|f", l_Ship.GetPtr(&objh), &new_max_guard_radius)) - return ade_set_error(L, "f", 0.0f); + return ADE_RETURN_NIL; if(!objh->isValid()) - return ade_set_error(L, "f", 0.0f); + return ADE_RETURN_NIL; ship *shipp = &Ships[objh->objp()->instance]; + auto ship_entry = ship_registry_get(shipp->ship_name); + if (!ship_entry) + return ADE_RETURN_NIL; + if (ADE_SETTING_VAR) { + // Apply to every ship on this ship's team, mirroring sexp_set_guard_range's + // "all potential guardians" behavior. + for (ship_obj* so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so)) { + if (Objects[so->objnum].flags[Object::Object_Flags::Should_be_dead]) + continue; + + ship* guarding_shipp = &Ships[Objects[so->objnum].instance]; + if (guarding_shipp->team == shipp->team) + set_guard_range_ship(new_max_guard_radius, ship_entry, guarding_shipp); + } + } - if (ADE_SETTING_VAR) - shipp->max_guard_radius = new_max_guard_radius; - - return ade_set_args(L, "f", shipp->max_guard_radius); + return ADE_RETURN_NIL; } ADE_VIRTVAR(WaypointSpeedCap, l_Ship, "number", "Waypoint speed cap", "number", "The limit on the ship's speed for traversing waypoints. -1 indicates no speed cap. 0 will be returned if handle is invalid.") diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index a18ada69b4c..69684460dca 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -7249,7 +7249,7 @@ void ship::clear() ship_max_hull_strength = 0.0f; ship_guardian_threshold = 0; - max_guard_radius = -1.0f; + max_guard_ranges.clear(); ship_name[0] = 0; display_name.clear(); @@ -21755,7 +21755,33 @@ int get_nearest_bbox_point(const object *ship_objp, const vec3d *start, vec3d *b return inside; } - +void set_guard_range_ship(float range, const ship_registry_entry* ship_entry, ship* shipp) +{ + bool done = false; + if (!range <= 0) { + for (auto& exist : shipp->max_guard_ranges) { + if (exist.shipnum == ship_registry_get_index(ship_entry->shipp()->ship_name)) { + if (range > 0) { + exist.range = range; + } else { + exist.range = -1.0f; + } + done = true; + break; + } + } + if (!done) { + auto item = guard_range_entry(range, ship_registry_get_index(ship_entry->shipp()->ship_name)); + shipp->max_guard_ranges.push_back(item); + } + } else { + for (auto& exist : shipp->max_guard_ranges) { + if (exist.shipnum == ship_registry_get_index(ship_entry->shipp()->ship_name)) { + exist.range = -1.0f; + } + } + } +} void ship_set_thruster_info(mst_info *mst, object *obj, ship *shipp, ship_info *sip) { mst->length = obj->phys_info.linear_thrust; diff --git a/code/ship/ship.h b/code/ship/ship.h index 636084b9186..f5eb9bdca50 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -41,7 +41,6 @@ class WarpEffect; // Part of the player died system. extern vec3d Original_vec_to_deader; - // States for player death sequence, stuffed in Player_died_state. #define PDS_NONE 1 #define PDS_DIED 2 @@ -254,7 +253,8 @@ class ArmorType extern SCP_vector Armor_types; -//************************************************************** +void set_guard_range_ship(float range, const ship_registry_entry* ship_entry, ship* shipp); + //************************************************************** //WMC - Damage type handling code typedef struct DamageTypeStruct @@ -336,7 +336,11 @@ typedef struct lock_info { float lock_gauge_time_elapsed; float lock_anim_time_elapsed; } lock_info; - +struct guard_range_entry { + float range; + int shipnum; + guard_range_entry(float _range, int _shipnum) : range(_range), shipnum(_shipnum) {} +}; // structure definition for a linked list of subsystems for a ship. Each subsystem has a pointer // to the static data for the subsystem. The obj_subsystem data is defined and read in the model // code. Other dynamic data (such as current_hits) should remain in this structure. @@ -623,7 +627,9 @@ 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 + + SCP_vector + max_guard_ranges; // Optional clamp for guard engagement/resume ranges; <= 0 means unused char ship_name[NAME_LENGTH]; @@ -877,7 +883,6 @@ struct ai_target_priority { flagset sif_flags; flagset wif_flags; }; - extern SCP_vector Ai_tp_list; void parse_ai_target_priorities();