mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
* avoid undefined behavior from UNREACHABLE on reachable code paths
UNREACHABLE expands to __assume(false) / __builtin_unreachable() in
release builds, which is undefined behavior if the code is ever actually
reached. Many UNREACHABLE sites only guard against unexpected-but-
possible runtime states (values from tables, mission/save files, network
packets, scripting, or user input), so reaching them should degrade
gracefully rather than invoke UB.
Replace UNREACHABLE with Assertion(false, ...) at all such sites. It
still pops an error in debug builds, but in release it is a harmless
no-op that simply falls through to the following code. Sites that are
genuinely unreachable by local invariants (e.g. a switch over a bounded
random number) are left as UNREACHABLE.
A few converted sites had no safe fall-through -- an uninitialized read,
null dereference, or null call would have followed -- so they also get a
minimal recovery: initialize the value, guard the dereference, return
early, or skip the offending item.
Where the converted site was a simple guard of the form
if (x) { Assertion(false, ...); return; }
hoist the check onto the invariant instead:
Assertion(!x, ...);
if (x) { return; }
This states the expectation directly and shows the offending value in
the debug popup, while keeping the same graceful runtime degradation.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* addendum
After consultation with Lafiel, use UNREACHABLE in situations that are effectively impossible even if not provably impossible. UNREACHABLE = "this shouldn't ever happen"; Assertion = "if this ever happens, it's a coding error". Also fix a few other sites and change a couple to Warnings.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
17182 lines
576 KiB
C++
17182 lines
576 KiB
C++
/*
|
|
* Copyright (C) Volition, Inc. 1999. All rights reserved.
|
|
*
|
|
* All source code herein is the property of Volition, Inc. You may not sell
|
|
* or otherwise commercially exploit the source or things you created based on the
|
|
* source.
|
|
*
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
* @file
|
|
* Contains the actual AI code that does interesting stuff to objects.
|
|
*
|
|
* The code in Ai.cpp is just for bookkeeping, allocating AI slots and linking them to ships.
|
|
*/
|
|
|
|
|
|
#include "ai/ai.h"
|
|
#include "ai/ai_profiles.h"
|
|
#include "ai/aibig.h"
|
|
#include "ai/aigoals.h"
|
|
#include "ai/aiinternal.h"
|
|
#include "ai/ailua.h"
|
|
#include "asteroid/asteroid.h"
|
|
#include "autopilot/autopilot.h"
|
|
#include "cmeasure/cmeasure.h"
|
|
#include "debris/debris.h"
|
|
#include "debugconsole/console.h"
|
|
#include "freespace.h"
|
|
#include "gamesequence/gamesequence.h"
|
|
#include "gamesnd/gamesnd.h"
|
|
#include "globalincs/linklist.h"
|
|
#include "hud/hud.h"
|
|
#include "hud/hudets.h"
|
|
#include "hud/hudlock.h"
|
|
#include "iff_defs/iff_defs.h"
|
|
#include "io/joy_ff.h"
|
|
#include "io/timer.h"
|
|
#include "localization/localize.h"
|
|
#include "math/fvi.h"
|
|
#include "math/staticrand.h"
|
|
#include "mission/missiongoals.h"
|
|
#include "mission/missionlog.h"
|
|
#include "mission/missionmessage.h"
|
|
#include "mission/missionparse.h"
|
|
#include "mission/missiontraining.h"
|
|
#include "model/model.h"
|
|
#include "network/multi.h"
|
|
#include "network/multimsgs.h"
|
|
#include "network/multi_turret_manager.h"
|
|
#include "network/multiutil.h"
|
|
#include "object/deadobjectdock.h"
|
|
#include "object/objcollide.h"
|
|
#include "object/object.h"
|
|
#include "object/objectdock.h"
|
|
#include "object/objectshield.h"
|
|
#include "object/waypoint.h"
|
|
#include "parse/parselo.h"
|
|
#include "physics/physics.h"
|
|
#include "playerman/player.h"
|
|
#include "render/3d.h"
|
|
#include "scripting/api/objs/waypoint.h"
|
|
#include "scripting/api/objs/wing.h"
|
|
#include "scripting/scripting.h"
|
|
#include "scripting/global_hooks.h"
|
|
#include "ship/afterburner.h"
|
|
#include "ship/awacs.h"
|
|
#include "ship/ship.h"
|
|
#include "ship/shipfx.h"
|
|
#include "ship/shiphit.h"
|
|
#include "ship/subsysdamage.h"
|
|
#include "utils/Random.h"
|
|
#include "weapon/beam.h"
|
|
#include "weapon/flak.h"
|
|
#include "weapon/swarm.h"
|
|
#include "weapon/weapon.h"
|
|
#include <map>
|
|
#include <climits>
|
|
|
|
|
|
#ifdef _MSC_VER
|
|
#pragma optimize("", off)
|
|
#pragma auto_inline(off)
|
|
#endif
|
|
|
|
#define UNINITIALIZED_VALUE -99999.9f
|
|
|
|
#define INSTRUCTOR_SHIP_NAME NOX("instructor")
|
|
|
|
#define AICODE_SMALL_MAGNITUDE 0.001f // cosider a vector NULL if mag is less than this
|
|
|
|
#define NEXT_REARM_TIMESTAMP (60*1000) // Ships will re-request rearm, typically, after this long.
|
|
|
|
#define CIRCLE_STRAFE_MAX_DIST 300.0f //Maximum distance for circle strafe behavior.
|
|
|
|
// AIM_CHASE submode defines
|
|
// SM_STEALTH_FIND
|
|
#define SM_SF_AHEAD 0
|
|
#define SM_SF_BEHIND 1
|
|
#define SM_SF_BAIL 2
|
|
|
|
// SM_STEALTH_SWEEP
|
|
#define SM_SS_SET_GOAL -1
|
|
#define SM_SS_BOX0 0
|
|
#define SM_SS_LR 1
|
|
#define SM_SS_UL 2
|
|
#define SM_SS_BOX1 3
|
|
#define SM_SS_UR 4
|
|
#define SM_SS_LL 5
|
|
#define SM_SS_BOX2 6
|
|
#define SM_SS_DONE 7
|
|
|
|
//XSTR:OFF
|
|
|
|
const char *Mode_text[MAX_AI_BEHAVIORS] = {
|
|
"CHASE",
|
|
"EVADE",
|
|
"GET_BEHIND",
|
|
"CHASE_LONG",
|
|
"SQUIGGLE",
|
|
"GUARD",
|
|
"AVOID",
|
|
"WAYPOINTS",
|
|
"DOCK",
|
|
"NONE",
|
|
"BIGSHIP",
|
|
"PATH",
|
|
"BE_REARMED",
|
|
"SAFETY",
|
|
"EV_WEAPON",
|
|
"STRAFE",
|
|
"PLAY_DEAD",
|
|
"BAY_EMERGE",
|
|
"BAY_DEPART",
|
|
"SENTRYGUN",
|
|
"WARP_OUT",
|
|
"FLY_TO_SHIP",
|
|
"LUA_AI"
|
|
};
|
|
|
|
// Submode text is only valid for CHASE mode.
|
|
const char *Submode_text[] = {
|
|
"undefined",
|
|
"CONT_TURN",
|
|
"ATTACK ",
|
|
"E_SQUIG ",
|
|
"E_BRAKE ",
|
|
"EVADE ",
|
|
"SUP_ATTAK",
|
|
"AVOID ",
|
|
"BEHIND ",
|
|
"GET_AWAY ",
|
|
"E_WEAPON ",
|
|
"FLY_AWAY ",
|
|
"ATK_4EVER",
|
|
"STLTH_FND",
|
|
"STLTH_SWP",
|
|
"BIG_APPR",
|
|
"BIG_CIRC",
|
|
"BIG_PARL",
|
|
"GLIDE_ATK",
|
|
"CIR_STRAFE"
|
|
};
|
|
|
|
const char *Strafe_submode_text[5] = {
|
|
"ATTACK",
|
|
"AVOID",
|
|
"RETREAT1",
|
|
"RETREAT2",
|
|
"POSITION"
|
|
};
|
|
//XSTR:ON
|
|
|
|
// few forward decs i needed - kazan
|
|
object * get_wing_leader(int wingnum);
|
|
int get_wing_index(object *objp, int wingnum);
|
|
|
|
control_info AI_ci;
|
|
|
|
object *Pl_objp;
|
|
object *En_objp;
|
|
|
|
#define REARM_SOUND_DELAY (3*F1_0) // Amount of time to delay rearm/repair after mode start
|
|
#define REARM_BREAKOFF_DELAY (3*F1_0) // Amount of time to wait after fully rearmed to breakoff.
|
|
|
|
// Select guard options are now exposed to modders --wookieejedi
|
|
#define MIN_DIST_TO_WAYPOINT_GOAL 5.0f
|
|
#define MAX_GUARD_DIST 250.0f
|
|
|
|
#define MAX_EVADE_TIME (15 * 1000) // Max time to evade a weapon.
|
|
|
|
// defines for repair ship stuff.
|
|
#define MAX_REPAIR_SPEED 25.0f
|
|
#define MAX_UNDOCK_ABORT_SPEED 2.0f
|
|
|
|
// defines for EMP effect stuff
|
|
#define MAX_EMP_INACCURACY 50.0f
|
|
|
|
// defines for stealth
|
|
#define MAX_STEALTH_INACCURACY 50.0f // at max view dist
|
|
#define STEALTH_MAX_VIEW_DIST 400 // dist at which 1) stealth no longer visible 2) firing inaccuracy is greatest
|
|
#define STEALTH_VIEW_CONE_DOT 0.707 // (half angle of 45 degrees)
|
|
|
|
ai_class *Ai_classes = NULL;
|
|
int Ai_firing_enabled = 1;
|
|
int Num_ai_classes;
|
|
int Num_alloced_ai_classes;
|
|
|
|
int AI_FrameCount = 0;
|
|
int AI_watch_object = 0; // Debugging, object to spew debug info for.
|
|
int Mission_all_attack = 0; // !0 means all teams attack all teams.
|
|
|
|
// Constant for flag, Name of flag
|
|
ai_flag_name Ai_flag_names[] = {
|
|
{AI::AI_Flags::No_dynamic, "no-dynamic", },
|
|
{AI::AI_Flags::Free_afterburner_use, "free-afterburner-use", },
|
|
{AI::AI_Flags::Waypoints_no_formation, "waypoints-no-formation" },
|
|
};
|
|
|
|
ai_flag_description Ai_flag_descriptions[] = {
|
|
{AI::AI_Flags::No_dynamic, "Will stop allowing the AI to pursue dynamic goals (eg: chasing ships it was not ordered to)."},
|
|
{AI::AI_Flags::Free_afterburner_use, "Will allow AI to use afterburners when attacking a big ship, flying to a target position, guarding a ship, and flying in formation."},
|
|
{AI::AI_Flags::Waypoints_no_formation, "Ship will not form up with its wingmates while running waypoints with them." },
|
|
{AI::AI_Flags::Kamikaze, "Ship will attack big ships by colliding with them and exploding."},
|
|
};
|
|
|
|
extern const int Num_ai_flag_names = sizeof(Ai_flag_names) / sizeof(ai_flag_name);
|
|
extern const size_t Num_ai_flag_descriptions = sizeof(Ai_flag_descriptions) / sizeof(ai_flag_description);
|
|
|
|
const char *Skill_level_names(int level, int translate)
|
|
{
|
|
const char *str = NULL;
|
|
|
|
#if NUM_SKILL_LEVELS != 5
|
|
#error Number of skill levels is wrong!
|
|
#endif
|
|
|
|
if(translate){
|
|
switch( level ) {
|
|
case 0:
|
|
str = XSTR("Very Easy", 469);
|
|
break;
|
|
case 1:
|
|
str = XSTR("Easy", 470);
|
|
break;
|
|
case 2:
|
|
str = XSTR("Medium", 471);
|
|
break;
|
|
case 3:
|
|
str = XSTR("Hard", 472);
|
|
break;
|
|
case 4:
|
|
str = XSTR("Insane", 473);
|
|
break;
|
|
default:
|
|
UNREACHABLE("Unhandled difficulty level of '%d' was passed to Skill_level_names().", level);
|
|
}
|
|
} else {
|
|
switch( level ) {
|
|
case 0:
|
|
str = NOX("Very Easy");
|
|
break;
|
|
case 1:
|
|
str = NOX("Easy");
|
|
break;
|
|
case 2:
|
|
str = NOX("Medium");
|
|
break;
|
|
case 3:
|
|
str = NOX("Hard");
|
|
break;
|
|
case 4:
|
|
str = NOX("Insane");
|
|
break;
|
|
default:
|
|
UNREACHABLE("An invalid difficulty level of '%d' was passed to Skill_level_names().", level);
|
|
}
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
#define DELAY_TARGET_TIME (12*1000) // time in milliseconds until a ship can target a new enemy after an order.
|
|
|
|
pnode Path_points[MAX_PATH_POINTS];
|
|
pnode *Ppfp; // Free pointer in path points.
|
|
|
|
float AI_frametime;
|
|
|
|
char** Ai_class_names = NULL;
|
|
|
|
//used for good-primary-time
|
|
typedef struct {
|
|
object_ship_wing_point_team subject;
|
|
object_ship_wing_point_team target;
|
|
int weapon_index;
|
|
} primary_fire_info;
|
|
|
|
static SCP_vector<primary_fire_info> Preferred_primary_info;
|
|
|
|
typedef struct {
|
|
int team;
|
|
int weapon_index;
|
|
int max_fire_count;
|
|
int ship_registry_index;
|
|
} huge_fire_info;
|
|
|
|
static SCP_vector<huge_fire_info> Ai_huge_fire_info;
|
|
|
|
int Ai_last_arrive_path; // index of ship_bay path used by last arrival from a fighter bay
|
|
|
|
// forward declarations
|
|
int ai_return_path_num_from_dockbay(object *dockee_objp, int dockbay_index);
|
|
void create_model_exit_path(object *pl_objp, object *mobjp, int path_num, int count=1);
|
|
void copy_xlate_model_path_points(object *objp, model_path *mp, int dir, int count, int path_num, pnode *pnp, int randomize_pnt=-1);
|
|
void ai_cleanup_rearm_mode(object *objp, bool deathroll);
|
|
void ai_cleanup_dock_mode_subjective(object *objp);
|
|
void ai_cleanup_dock_mode_objective(object *objp);
|
|
|
|
// The object that is declared to be the leader of the group formation for
|
|
// 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'
|
|
* Once the timestamp is no longer valid, then rearming is not a "good time" (may or may not rearm)
|
|
*
|
|
* @param team Team (friendly, hostile, neutral)
|
|
* @param time Time to rearm
|
|
*/
|
|
void ai_set_good_rearm_time(int team, int time)
|
|
{
|
|
Assert( time >= 0 );
|
|
|
|
Iff_info[team].ai_good_rearm_timestamp = _timestamp(time * MILLISECONDS_PER_SECOND);
|
|
Iff_info[team].ai_bad_rearm_timestamp = TIMESTAMP::invalid();
|
|
}
|
|
|
|
/**
|
|
* Sets the timestamp used to tell is it is a bad time for this team to rearm.
|
|
* Ends a 'good rearm time'
|
|
* Once the timestamp is no longer valid, then rearming is not a "bad time" (may or may not rearm)
|
|
*
|
|
* @param team Team (friendly, hostile, neutral)
|
|
* @param time Time to rearm
|
|
*/
|
|
void ai_set_bad_rearm_time(int team, int time)
|
|
{
|
|
Assert(time >= 0);
|
|
|
|
Iff_info[team].ai_bad_rearm_timestamp = _timestamp(time * MILLISECONDS_PER_SECOND);
|
|
Iff_info[team].ai_good_rearm_timestamp = TIMESTAMP::invalid();
|
|
}
|
|
|
|
/**
|
|
* "safe" is currently defined by the mission designer using the good/bad time to rearm sexpressions. This status is currently team based.
|
|
*
|
|
* @return true(1) or false(0) if it is "safe" for the given object to rearm.
|
|
* @todo This function could be easily expended to further the definition of "safe"
|
|
*/
|
|
bool ai_good_time_to_rearm(object *objp)
|
|
{
|
|
int team;
|
|
|
|
Assert(objp->type == OBJ_SHIP);
|
|
team = Ships[objp->instance].team;
|
|
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_good_rearm_time_bug])
|
|
return Iff_info[team].ai_good_rearm_timestamp.isValid() && !timestamp_elapsed(Iff_info[team].ai_good_rearm_timestamp);
|
|
else
|
|
return Iff_info[team].ai_good_rearm_timestamp.isValid();
|
|
}
|
|
|
|
/**
|
|
* "unsafe" is currently defined by the mission designer using the good/bad time to rearm sexpressions. This status is currently team based.
|
|
*
|
|
* @return true(1) or false(0) if it is "unsafe" for the given object to rearm.
|
|
*/
|
|
bool ai_bad_time_to_rearm(object* objp)
|
|
{
|
|
int team;
|
|
|
|
Assert(objp->type == OBJ_SHIP);
|
|
team = Ships[objp->instance].team;
|
|
|
|
return Iff_info[team].ai_bad_rearm_timestamp.isValid() && !timestamp_elapsed(Iff_info[team].ai_bad_rearm_timestamp);
|
|
}
|
|
|
|
/**
|
|
* Entry point from sexpression code to set internal data for use by AI code.
|
|
*/
|
|
void ai_good_secondary_time( int team, int weapon_index, int max_fire_count, const char *shipname )
|
|
{
|
|
Assert(shipname != nullptr);
|
|
huge_fire_info new_info;
|
|
|
|
new_info.weapon_index = weapon_index;
|
|
new_info.team = team;
|
|
new_info.max_fire_count = max_fire_count;
|
|
new_info.ship_registry_index = ship_registry_get_index(shipname);
|
|
|
|
Ai_huge_fire_info.push_back(new_info);
|
|
}
|
|
|
|
/**
|
|
* Called internally to the AI code to tell whether or not weapon_num can be fired
|
|
* from firer_objp at target_objp. This function will resolve the team for the firer.
|
|
*
|
|
* @return -1 when conditions don't allow firer to fire weapon_num on target_objp
|
|
* @return >=0 when conditions allow firer to fire. Return value is max number of weapon_nums which can be fired on target_objp
|
|
*/
|
|
int is_preferred_weapon(int weapon_num, object *firer_objp, object *target_objp)
|
|
{
|
|
int firer_team, target_signature;
|
|
ship *firer_ship;
|
|
SCP_vector<huge_fire_info>::iterator hfi;
|
|
|
|
Assert( firer_objp->type == OBJ_SHIP );
|
|
firer_ship = &Ships[firer_objp->instance];
|
|
firer_team = firer_ship->team;
|
|
|
|
// get target object's signature and try to find it in the list.
|
|
target_signature = target_objp->signature;
|
|
for (hfi = Ai_huge_fire_info.begin(); hfi != Ai_huge_fire_info.end(); ++hfi)
|
|
{
|
|
if ( hfi->weapon_index == -1 )
|
|
continue;
|
|
if ( hfi->ship_registry_index == -1 )
|
|
continue;
|
|
|
|
auto ship_entry = &Ship_registry[hfi->ship_registry_index];
|
|
if (!ship_entry->has_objp())
|
|
continue;
|
|
|
|
int signature = ship_entry->objp()->signature;
|
|
|
|
// sigatures, weapon_index, and team must match
|
|
if ( (signature == target_signature) && (hfi->weapon_index == weapon_num) && (hfi->team == firer_team) )
|
|
break;
|
|
}
|
|
|
|
// return -1 if not found
|
|
if ( hfi == Ai_huge_fire_info.end() )
|
|
return -1;
|
|
|
|
// otherwise, we can return the max number of weapons we can fire against target_objps
|
|
return hfi->max_fire_count;
|
|
}
|
|
|
|
/**
|
|
* Clear out secondary firing information between levels
|
|
*/
|
|
void ai_init_secondary_info()
|
|
{
|
|
Ai_huge_fire_info.clear();
|
|
}
|
|
|
|
/**
|
|
* Garbage collect the ::Path_points buffer.
|
|
*
|
|
* Scans all objects, looking for used Path_points records.
|
|
* Compresses Path_points buffer, updating aip->path_start and aip->path_cur indices.
|
|
* Updates Ppfp to point to first free record.
|
|
* This function is fairly fast. Its worst-case running time is proportional to
|
|
* 3*MAX_PATH_POINTS + MAX_OBJECTS
|
|
*
|
|
* @todo Things to do to optimize this function:
|
|
* 1. if (t != 0) xlt++; can be replaced by xlt += t; assuming t can only be 0 or 1.
|
|
* 2. When pp_xlate is getting stuffed the first time, note highest index and use that
|
|
* instead of MAX_PATH_POINTS in following two for loops.
|
|
*/
|
|
void garbage_collect_path_points()
|
|
{
|
|
int i;
|
|
int pp_xlate[MAX_PATH_POINTS];
|
|
object *A;
|
|
ship_obj *so;
|
|
|
|
// Scan all objects and create Path_points xlate table.
|
|
for (i=0; i<MAX_PATH_POINTS; i++)
|
|
pp_xlate[i] = 0;
|
|
|
|
// in pp_xlate, mark all used Path_point records
|
|
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
|
|
A = &Objects[so->objnum];
|
|
if (A->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
ship *shipp = &Ships[A->instance];
|
|
if (shipp->ai_index != -1) {
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
|
|
if ((aip->path_length > 0) && (aip->path_start > -1)) {
|
|
|
|
for (i=aip->path_start; i<aip->path_start + aip->path_length; i++) {
|
|
Assert(pp_xlate[i] == 0); // If this is not 0, then two paths use this point!
|
|
pp_xlate[i] = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now, stuff xlate index in pp_xlate. This is the number to translate any path_start
|
|
// or path_cur index to.
|
|
int xlt = 0;
|
|
for (i=0; i<MAX_PATH_POINTS; i++) {
|
|
int t = pp_xlate[i];
|
|
|
|
pp_xlate[i] = xlt;
|
|
if (t != 0)
|
|
xlt++;
|
|
}
|
|
|
|
// Update global Path_points free pointer.
|
|
Ppfp = &Path_points[xlt];
|
|
|
|
// Now, using pp_xlate, fixup all aip->path_cur and aip->path_start indices
|
|
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
|
|
A = &Objects[so->objnum];
|
|
if (A->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
ship *shipp = &Ships[A->instance];
|
|
if (shipp->ai_index != -1) {
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
|
|
if ((aip->path_length > 0) && (aip->path_start > -1)) {
|
|
Assert(aip->path_start < MAX_PATH_POINTS);
|
|
aip->path_start = pp_xlate[aip->path_start];
|
|
|
|
Assert((aip->path_cur >= 0) && (aip->path_cur < MAX_PATH_POINTS));
|
|
aip->path_cur = pp_xlate[aip->path_cur];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now, compress the buffer.
|
|
for (i=0; i<MAX_PATH_POINTS; i++)
|
|
if (i != pp_xlate[i])
|
|
Path_points[pp_xlate[i]] = Path_points[i];
|
|
|
|
}
|
|
|
|
void reset_ai_path_points()
|
|
{
|
|
memset(Path_points, 0, sizeof(Path_points)); // optional: zero out for safety
|
|
Ppfp = Path_points;
|
|
|
|
// Also clear any AI path state
|
|
for (int i = 0; i < MAX_SHIPS; ++i) {
|
|
Ai_info[i].path_start = -1;
|
|
Ai_info[i].path_cur = -1;
|
|
Ai_info[i].path_length = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hash two values together, return result.
|
|
* Hash function: curval shifted right circular by one, newval xored in.
|
|
*/
|
|
int hash(unsigned int curval, int newval)
|
|
{
|
|
int addval = curval & 1;
|
|
|
|
curval >>= 1;
|
|
if (addval)
|
|
curval |= 0x80000000;
|
|
curval ^= newval;
|
|
|
|
return curval;
|
|
}
|
|
|
|
/**
|
|
* Hash some information in an object together.
|
|
* On 2/20/97, the information is position and orientation.
|
|
*/
|
|
int create_object_hash(object *objp)
|
|
{
|
|
int *ip;
|
|
unsigned int hashval = 0;
|
|
int i;
|
|
|
|
ip = (int *) &objp->orient;
|
|
|
|
for (i=0; i<9; i++) {
|
|
hashval = hash(hashval, *ip);
|
|
ip++;
|
|
}
|
|
|
|
ip = (int *) &objp->pos;
|
|
|
|
for (i=0; i<3; i++) {
|
|
hashval = hash(hashval, *ip);
|
|
ip++;
|
|
}
|
|
|
|
return hashval;
|
|
}
|
|
|
|
void free_ai_stuff()
|
|
{
|
|
if(Ai_classes != NULL)
|
|
vm_free(Ai_classes);
|
|
|
|
if(Ai_class_names != NULL)
|
|
vm_free(Ai_class_names);
|
|
}
|
|
|
|
int ai_get_autoscale_index(int absolute_index)
|
|
{
|
|
int index = 0;
|
|
|
|
// getting the total number of autoscale classes uses the same logic but with the size in place of absolute_index
|
|
if (absolute_index < 0)
|
|
absolute_index = Num_ai_classes;
|
|
|
|
for (int i = 0; i < absolute_index; i++)
|
|
{
|
|
if (Ai_classes[i].ai_class_autoscale)
|
|
index++;
|
|
}
|
|
|
|
// sanity check
|
|
Assertion(index > 0 || absolute_index != Num_ai_classes, "When called for autoscaling calculations, there must be at least one autoscaling AI class!");
|
|
|
|
return index;
|
|
}
|
|
|
|
int ai_maybe_autoscale(int absolute_index)
|
|
{
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Adjusted_AI_class_autoscale])
|
|
return ai_get_autoscale_index(absolute_index);
|
|
else
|
|
return absolute_index;
|
|
}
|
|
|
|
/**
|
|
* Initialize an AI class's nonrequired values to defaults
|
|
* Boolean overrides are unset, others initialized to FLT_MIN or INT_MIN (used as the "not set" state)
|
|
*/
|
|
void init_ai_class(ai_class *aicp)
|
|
{
|
|
|
|
// AI Class main attributes
|
|
for (int i = 0; i < NUM_SKILL_LEVELS; i++)
|
|
{
|
|
aicp->ai_cmeasure_fire_chance[i] = FLT_MIN;
|
|
aicp->ai_in_range_time[i] = FLT_MIN;
|
|
aicp->ai_link_ammo_levels_maybe[i] = FLT_MIN;
|
|
aicp->ai_link_ammo_levels_always[i] = FLT_MIN;
|
|
aicp->ai_primary_ammo_burst_mult[i] = FLT_MIN;
|
|
aicp->ai_link_energy_levels_maybe[i] = FLT_MIN;
|
|
aicp->ai_link_energy_levels_always[i] = FLT_MIN;
|
|
aicp->ai_predict_position_delay[i] = INT_MIN;
|
|
aicp->ai_shield_manage_delay[i] = FLT_MIN;
|
|
aicp->ai_ship_fire_delay_scale_friendly[i] = FLT_MIN;
|
|
aicp->ai_ship_fire_delay_scale_hostile[i] = FLT_MIN;
|
|
aicp->ai_ship_fire_secondary_delay_scale_friendly[i] = FLT_MIN;
|
|
aicp->ai_ship_fire_secondary_delay_scale_hostile[i] = FLT_MIN;
|
|
aicp->ai_turn_time_scale[i] = FLT_MIN;
|
|
aicp->ai_glide_attack_percent[i] = FLT_MIN;
|
|
aicp->ai_circle_strafe_percent[i] = FLT_MIN;
|
|
aicp->ai_glide_strafe_percent[i] = FLT_MIN;
|
|
aicp->ai_random_sidethrust_percent[i] = FLT_MIN;
|
|
aicp->ai_stalemate_time_thresh[i] = FLT_MIN;
|
|
aicp->ai_stalemate_dist_thresh[i] = FLT_MIN;
|
|
aicp->ai_chance_to_use_missiles_on_plr[i] = INT_MIN;
|
|
aicp->ai_max_aim_update_delay[i] = FLT_MIN;
|
|
aicp->ai_turret_max_aim_update_delay[i] = FLT_MIN;
|
|
// Set the lowest values here so the table can be modular
|
|
aicp->ai_accuracy[i] = FLT_MIN;
|
|
aicp->ai_evasion[i] = FLT_MIN;
|
|
aicp->ai_courage[i] = FLT_MIN;
|
|
aicp->ai_patience[i] = FLT_MIN;
|
|
}
|
|
aicp->ai_profile_flags.reset();
|
|
aicp->ai_profile_flags_set.reset();
|
|
|
|
//AI Class autoscale overrides
|
|
//INT_MIN and FLT_MIN represent the "not set" state
|
|
for (int i = 0; i < NUM_SKILL_LEVELS; i++)
|
|
{
|
|
aicp->ai_aburn_use_factor[i] = INT_MIN;
|
|
aicp->ai_shockwave_evade_chance[i] = FLT_MIN;
|
|
aicp->ai_get_away_chance[i] = FLT_MIN;
|
|
aicp->ai_secondary_range_mult[i] = FLT_MIN;
|
|
}
|
|
aicp->ai_class_autoscale = true; //Retail behavior is to do the stupid autoscaling
|
|
}
|
|
|
|
void set_aic_flag(ai_class *aicp, const char *name, AI::Profile_Flags flag)
|
|
{
|
|
auto flags = &(aicp->ai_profile_flags);
|
|
auto set = &(aicp->ai_profile_flags_set);
|
|
|
|
if (optional_string(name))
|
|
{
|
|
bool val;
|
|
stuff_boolean(&val);
|
|
|
|
flags->set(flag, val);
|
|
|
|
set->set(flag);
|
|
}
|
|
else
|
|
set->remove(flag);
|
|
}
|
|
|
|
static ai_class* get_ai_class_pointer(const char* class_name)
|
|
{
|
|
for (int i = 0; i < Num_ai_classes; i++) {
|
|
if (!stricmp(class_name, Ai_classes[i].name)) {
|
|
return &Ai_classes[i];
|
|
}
|
|
}
|
|
|
|
// Didn't find anything.
|
|
return nullptr;
|
|
}
|
|
|
|
static bool class_added_in_tbm = false;
|
|
|
|
void parse_ai_class()
|
|
{
|
|
ai_class *aicp;
|
|
bool create_if_not_found = true;
|
|
char thisName[NAME_LENGTH];
|
|
|
|
required_string("$Name:");
|
|
stuff_string(thisName, F_NAME, NAME_LENGTH);
|
|
|
|
if (optional_string("+nocreate")) {
|
|
if (!Parsing_modular_table) {
|
|
Warning(LOCATION, "+nocreate flag used for ai class in non-modular table\n");
|
|
}
|
|
create_if_not_found = false;
|
|
}
|
|
|
|
// Does this class exist already?
|
|
// If so, load this new info into it
|
|
aicp = get_ai_class_pointer(thisName);
|
|
if (aicp != nullptr) {
|
|
if (!Parsing_modular_table) {
|
|
error_display(1, "Error: AI Class %s already exists. All class names must be unique.", thisName);
|
|
}
|
|
} else {
|
|
// Don't create class if it has +nocreate and is in a modular table.
|
|
if (!create_if_not_found && Parsing_modular_table) {
|
|
return;
|
|
}
|
|
|
|
// Setup a new ai class
|
|
aicp = &Ai_classes[Num_ai_classes];
|
|
strcpy(aicp->name, thisName);
|
|
|
|
init_ai_class(aicp);
|
|
Ai_class_names[Num_ai_classes] = aicp->name;
|
|
|
|
Num_ai_classes++;
|
|
|
|
if (Parsing_modular_table) {
|
|
mprintf(("New Ai Class '%s' found in modular table\n", aicp->name));
|
|
class_added_in_tbm = true;
|
|
}
|
|
|
|
}
|
|
|
|
if (optional_string("$accuracy:"))
|
|
parse_float_list(aicp->ai_accuracy, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$evasion:"))
|
|
parse_float_list(aicp->ai_evasion, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$courage:"))
|
|
parse_float_list(aicp->ai_courage, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$patience:"))
|
|
parse_float_list(aicp->ai_patience, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$Afterburner Use Factor:"))
|
|
parse_int_list(aicp->ai_aburn_use_factor, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$Shockwave Evade Chances Per Second:"))
|
|
parse_float_list(aicp->ai_shockwave_evade_chance, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$Get Away Chance:"))
|
|
parse_float_list(aicp->ai_get_away_chance, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$Secondary Range Multiplier:"))
|
|
parse_float_list(aicp->ai_secondary_range_mult, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$Autoscale by AI Class Index:"))
|
|
stuff_boolean(&aicp->ai_class_autoscale);
|
|
|
|
//Parse optional values for stuff imported from ai_profiles
|
|
if (optional_string("$AI Countermeasure Firing Chance:"))
|
|
parse_float_list(aicp->ai_cmeasure_fire_chance, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$AI In Range Time:"))
|
|
parse_float_list(aicp->ai_in_range_time, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$AI Always Links Ammo Weapons:"))
|
|
parse_float_list(aicp->ai_link_ammo_levels_always, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$AI Maybe Links Ammo Weapons:"))
|
|
parse_float_list(aicp->ai_link_ammo_levels_maybe, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$Primary Ammo Burst Multiplier:"))
|
|
parse_float_list(aicp->ai_primary_ammo_burst_mult, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$AI Always Links Energy Weapons:"))
|
|
parse_float_list(aicp->ai_link_energy_levels_always, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$AI Maybe Links Energy Weapons:"))
|
|
parse_float_list(aicp->ai_link_energy_levels_maybe, NUM_SKILL_LEVELS);
|
|
|
|
// represented in fractions of F1_0
|
|
if (optional_string("$Predict Position Delay:")) {
|
|
int iLoop;
|
|
float temp_list[NUM_SKILL_LEVELS];
|
|
parse_float_list(temp_list, NUM_SKILL_LEVELS);
|
|
for (iLoop = 0; iLoop < NUM_SKILL_LEVELS; iLoop++)
|
|
aicp->ai_predict_position_delay[iLoop] = fl2f(temp_list[iLoop]);
|
|
}
|
|
|
|
if (optional_string("$AI Shield Manage Delay:") || optional_string("$AI Shield Manage Delays:"))
|
|
parse_float_list(aicp->ai_shield_manage_delay, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$Friendly AI Fire Delay Scale:"))
|
|
parse_float_list(aicp->ai_ship_fire_delay_scale_friendly, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$Hostile AI Fire Delay Scale:"))
|
|
parse_float_list(aicp->ai_ship_fire_delay_scale_hostile, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$Friendly AI Secondary Fire Delay Scale:"))
|
|
parse_float_list(aicp->ai_ship_fire_secondary_delay_scale_friendly, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$Hostile AI Secondary Fire Delay Scale:"))
|
|
parse_float_list(aicp->ai_ship_fire_secondary_delay_scale_hostile, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$AI Turn Time Scale:"))
|
|
parse_float_list(aicp->ai_turn_time_scale, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$Glide Attack Percent:")) {
|
|
parse_float_list(aicp->ai_glide_attack_percent, NUM_SKILL_LEVELS);
|
|
for (int i = 0; i < NUM_SKILL_LEVELS; i++) {
|
|
if (aicp->ai_glide_attack_percent[i] < 0.0f || aicp->ai_glide_attack_percent[i] > 100.0f) {
|
|
aicp->ai_glide_attack_percent[i] = 0.0f;
|
|
Warning(LOCATION, "$Glide Attack Percent should be between 0 and 100.0 (read %f). Setting to 0.", aicp->ai_glide_attack_percent[i]);
|
|
}
|
|
aicp->ai_glide_attack_percent[i] /= 100.0;
|
|
}
|
|
|
|
}
|
|
|
|
if (optional_string("$Circle Strafe Percent:")) {
|
|
parse_float_list(aicp->ai_circle_strafe_percent, NUM_SKILL_LEVELS);
|
|
for (int i = 0; i < NUM_SKILL_LEVELS; i++) {
|
|
if (aicp->ai_circle_strafe_percent[i] < 0.0f || aicp->ai_circle_strafe_percent[i] > 100.0f) {
|
|
aicp->ai_circle_strafe_percent[i] = 0.0f;
|
|
Warning(LOCATION, "$Circle Strafe Percent should be between 0 and 100.0 (read %f). Setting to 0.", aicp->ai_circle_strafe_percent[i]);
|
|
}
|
|
aicp->ai_circle_strafe_percent[i] /= 100.0;
|
|
}
|
|
}
|
|
|
|
if (optional_string("$Glide Strafe Percent:")) {
|
|
parse_float_list(aicp->ai_glide_strafe_percent, NUM_SKILL_LEVELS);
|
|
for (int i = 0; i < NUM_SKILL_LEVELS; i++) {
|
|
if (aicp->ai_glide_strafe_percent[i] < 0.0f || aicp->ai_glide_strafe_percent[i] > 100.0f) {
|
|
aicp->ai_glide_strafe_percent[i] = 0.0f;
|
|
Warning(LOCATION, "$Glide Strafe Percent should be between 0 and 100.0 (read %f). Setting to 0.", aicp->ai_glide_strafe_percent[i]);
|
|
}
|
|
aicp->ai_glide_strafe_percent[i] /= 100.0;
|
|
}
|
|
}
|
|
|
|
if (optional_string("$Random Sidethrust Percent:")) {
|
|
parse_float_list(aicp->ai_random_sidethrust_percent, NUM_SKILL_LEVELS);
|
|
for (int i = 0; i < NUM_SKILL_LEVELS; i++) {
|
|
if (aicp->ai_random_sidethrust_percent[i] < 0.0f || aicp->ai_random_sidethrust_percent[i] > 100.0f) {
|
|
aicp->ai_random_sidethrust_percent[i] = 0.0f;
|
|
Warning(LOCATION, "$Random Sidethrust Percent should be between 0 and 100.0 (read %f). Setting to 0.", aicp->ai_random_sidethrust_percent[i]);
|
|
}
|
|
aicp->ai_random_sidethrust_percent[i] /= 100.0;
|
|
}
|
|
}
|
|
|
|
if (optional_string("$Stalemate Time Threshold:"))
|
|
parse_float_list(aicp->ai_stalemate_time_thresh, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$Stalemate Distance Threshold:"))
|
|
parse_float_list(aicp->ai_stalemate_dist_thresh, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$Chance AI Has to Fire Missiles at Player:"))
|
|
parse_int_list(aicp->ai_chance_to_use_missiles_on_plr, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$Max Aim Update Delay:"))
|
|
parse_float_list(aicp->ai_max_aim_update_delay, NUM_SKILL_LEVELS);
|
|
|
|
if (optional_string("$Turret Max Aim Update Delay:"))
|
|
parse_float_list(aicp->ai_turret_max_aim_update_delay, NUM_SKILL_LEVELS);
|
|
|
|
set_aic_flag(aicp, "$big ships can attack beam turrets on untargeted ships:", AI::Profile_Flags::Big_ships_can_attack_beam_turrets_on_untargeted_ships);
|
|
|
|
set_aic_flag(aicp, "$smart primary weapon selection:", AI::Profile_Flags::Smart_primary_weapon_selection);
|
|
|
|
set_aic_flag(aicp, "$smart secondary weapon selection:", AI::Profile_Flags::Smart_secondary_weapon_selection);
|
|
|
|
set_aic_flag(aicp, "$smart shield management:", AI::Profile_Flags::Smart_shield_management);
|
|
|
|
set_aic_flag(aicp, "$smart afterburner management:", AI::Profile_Flags::Smart_afterburner_management);
|
|
|
|
set_aic_flag(aicp, "$free afterburner use:", AI::Profile_Flags::Free_afterburner_use);
|
|
|
|
set_aic_flag(aicp, "$allow rapid secondary dumbfire:", AI::Profile_Flags::Allow_rapid_secondary_dumbfire);
|
|
|
|
set_aic_flag(aicp, "$huge turret weapons ignore bombs:", AI::Profile_Flags::Huge_turret_weapons_ignore_bombs);
|
|
|
|
set_aic_flag(aicp, "$don't insert random turret fire delay:", AI::Profile_Flags::Dont_insert_random_turret_fire_delay);
|
|
|
|
set_aic_flag(aicp, "$prevent turrets targeting too distant bombs:", AI::Profile_Flags::Prevent_targeting_bombs_beyond_range);
|
|
|
|
set_aic_flag(aicp, "$smart subsystem targeting for turrets:", AI::Profile_Flags::Smart_subsystem_targeting_for_turrets);
|
|
|
|
set_aic_flag(aicp, "$allow turrets target weapons freely:", AI::Profile_Flags::Allow_turrets_target_weapons_freely);
|
|
|
|
set_aic_flag(aicp, "$allow vertical dodge:", AI::Profile_Flags::Allow_vertical_dodge);
|
|
|
|
set_aic_flag(aicp, "$no extra collision avoidance vs player:", AI::Profile_Flags::No_special_player_avoid);
|
|
|
|
set_aic_flag(aicp, "$all ships manage shields:", AI::Profile_Flags::All_ships_manage_shields);
|
|
|
|
set_aic_flag(aicp, "$ai can slow down when attacking big ships:", AI::Profile_Flags::Ai_can_slow_down_attacking_big_ships);
|
|
|
|
set_aic_flag(aicp, "$use actual primary range:", AI::Profile_Flags::Use_actual_primary_range);
|
|
|
|
set_aic_flag(aicp, "$firing requires exact los:", AI::Profile_Flags::Require_exact_los);
|
|
|
|
set_aic_flag(aicp, "$AI balances shields instead of directs when attacked:", AI::Profile_Flags::AI_balances_shields_when_attacked);
|
|
}
|
|
|
|
void reset_ai_class_names()
|
|
{
|
|
ai_class *aicp;
|
|
|
|
for (int i = 0; i < Num_ai_classes; i++) {
|
|
aicp = &Ai_classes[i];
|
|
|
|
Ai_class_names[i] = aicp->name;
|
|
}
|
|
}
|
|
|
|
#define AI_CLASS_INCREMENT 10
|
|
void parse_aitbl(const char* filename)
|
|
{
|
|
try {
|
|
read_file_text(filename, CF_TYPE_TABLES);
|
|
reset_parse();
|
|
|
|
required_string("#AI Classes");
|
|
|
|
while (required_string_either("#End", "$Name:")) {
|
|
|
|
parse_ai_class();
|
|
|
|
if(Num_ai_classes >= Num_alloced_ai_classes)
|
|
{
|
|
Num_alloced_ai_classes += AI_CLASS_INCREMENT;
|
|
Ai_classes = (ai_class*) vm_realloc(Ai_classes, Num_alloced_ai_classes * sizeof(ai_class));
|
|
|
|
// Ai_class_names doesn't realloc all that well so we have to do it the hard way.
|
|
// Luckily, it's contents can be easily replaced so we don't have to save anything.
|
|
vm_free(Ai_class_names);
|
|
Ai_class_names = (char **) vm_malloc(Num_alloced_ai_classes * sizeof(char*));
|
|
reset_ai_class_names();
|
|
}
|
|
}
|
|
|
|
}
|
|
catch (const parse::ParseException& e)
|
|
{
|
|
mprintf(("TABLES: Unable to parse '%s'! Error message = %s.\n", filename, e.what()));
|
|
return;
|
|
}
|
|
}
|
|
|
|
LOCAL int ai_inited = 0;
|
|
|
|
//========================= BOOK-KEEPING FUNCTIONS =======================
|
|
|
|
/**
|
|
* Called once at game start-up
|
|
*/
|
|
void ai_init()
|
|
{
|
|
if ( !ai_inited ) {
|
|
// Do the first time initialization stuff here
|
|
free_ai_stuff();
|
|
|
|
Num_ai_classes = 0;
|
|
Num_alloced_ai_classes = AI_CLASS_INCREMENT;
|
|
Ai_classes = (ai_class*)vm_malloc(Num_alloced_ai_classes * sizeof(ai_class));
|
|
Ai_class_names = (char**)vm_malloc(Num_alloced_ai_classes * sizeof(char*));
|
|
|
|
parse_aitbl("ai.tbl");
|
|
|
|
parse_modular_table("*-aic.tbm", parse_aitbl);
|
|
|
|
if (class_added_in_tbm) {
|
|
mprintf(("One or more Ai Classes were added in a modular table. This may affect "
|
|
"ai autoscaling. If that's not desired then use a tbl instead of a tbm!\n"));
|
|
}
|
|
|
|
// autoscaling is meaningless if only one AI class has it, and will produce either ineffectual or
|
|
// divide-by-zero math expressions if $use adjusted AI class autoscale: is enabled
|
|
int num_autoscaled_classes = 0;
|
|
int autoscaled_class_index = -1;
|
|
for (int i = 0; i < Num_ai_classes; ++i) {
|
|
if (Ai_classes[i].ai_class_autoscale) {
|
|
num_autoscaled_classes++;
|
|
autoscaled_class_index = i;
|
|
}
|
|
}
|
|
if (num_autoscaled_classes == 1) {
|
|
Warning(LOCATION, "Autoscaling is meaningless when only one AI class has autoscaling enabled. Turning off autoscaling for %s.", Ai_classes[autoscaled_class_index].name);
|
|
Ai_classes[autoscaled_class_index].ai_class_autoscale = false;
|
|
}
|
|
|
|
atexit(free_ai_stuff);
|
|
|
|
ai_inited = 1;
|
|
}
|
|
|
|
init_semirand();
|
|
|
|
ai_level_init();
|
|
}
|
|
|
|
/**
|
|
* This inits the AI. You should be able to call this between
|
|
* levels to reset everything.
|
|
*/
|
|
void ai_level_init()
|
|
{
|
|
int i;
|
|
|
|
// Do the stuff to reset all ai stuff here
|
|
for (i=0; i<MAX_AI_INFO ; i++) {
|
|
Ai_info[i].shipnum = -1;
|
|
}
|
|
|
|
Ai_goal_signature = 0;
|
|
|
|
for (i = 0; i < (int)Iff_info.size(); i++) {
|
|
Iff_info[i].ai_good_rearm_timestamp = TIMESTAMP::invalid();
|
|
Iff_info[i].ai_bad_rearm_timestamp = TIMESTAMP::invalid();
|
|
}
|
|
|
|
// clear out the stuff needed for AI firing powerful secondary weapons
|
|
ai_init_secondary_info();
|
|
|
|
Ai_last_arrive_path=0;
|
|
|
|
// clear out the preferred primaries so that it doesn't persist between missions or between mission reloads
|
|
Preferred_primary_info.clear();
|
|
}
|
|
|
|
// BEGIN STEALTH
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Check if object is a stealth ship
|
|
*/
|
|
bool is_object_stealth_ship(const object *objp)
|
|
{
|
|
if (objp->type == OBJ_SHIP) {
|
|
if (Ships[objp->instance].flags[Ship::Ship_Flags::Stealth]) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// not stealth ship
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Init necessary AI info for new stealth target
|
|
*/
|
|
void init_ai_stealth_info(ai_info *aip, object *stealth_objp)
|
|
{
|
|
Assert(is_object_stealth_ship(stealth_objp));
|
|
|
|
// set necessary ai info for new stealth target
|
|
aip->stealth_last_pos = stealth_objp->pos;
|
|
aip->stealth_velocity = stealth_objp->phys_info.vel;
|
|
aip->stealth_last_visible_stamp = timestamp();
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Check whether Pl_objp can see a stealth ship object
|
|
#define STEALTH_NOT_IN_FRUSTUM 0
|
|
#define STEALTH_IN_FRUSTUM 1
|
|
#define STEALTH_FULLY_TARGETABLE 2
|
|
|
|
/**
|
|
* Return dist scaler based on skill level
|
|
*/
|
|
float get_skill_stealth_dist_scaler()
|
|
{
|
|
switch (Game_skill_level) {
|
|
case 0: // very easy
|
|
return 0.65f;
|
|
|
|
case 1: // easy
|
|
return 0.9f;
|
|
|
|
case 2: // medium
|
|
return 1.0f;
|
|
|
|
case 3: // hard
|
|
return 1.1f;
|
|
|
|
case 4: // insane
|
|
return 1.3f;
|
|
|
|
default:
|
|
UNREACHABLE("An invalid difficulty level of %d was passed to get_skill_stealth_dist_scaler().", Game_skill_level);
|
|
|
|
}
|
|
|
|
return 1.0f;
|
|
}
|
|
|
|
/**
|
|
* Return multiplier on dot based on skill level
|
|
*/
|
|
float get_skill_stealth_dot_scaler()
|
|
{
|
|
switch (Game_skill_level) {
|
|
case 0: // very easy
|
|
return 1.3f;
|
|
|
|
case 1: // easy
|
|
return 1.1f;
|
|
|
|
case 2: // medium
|
|
return 1.0f;
|
|
|
|
case 3: // hard
|
|
return 0.9f;
|
|
|
|
case 4: // insane
|
|
return 0.7f;
|
|
|
|
default:
|
|
UNREACHABLE("An invalid difficulty level of %d was passed to get_skill_stealth_dot_scaler().", Game_skill_level);
|
|
}
|
|
|
|
return 1.0f;
|
|
}
|
|
|
|
int ai_is_stealth_visible(const object *viewer_objp, const object *stealth_objp)
|
|
{
|
|
ship *shipp;
|
|
vec3d vec_to_stealth;
|
|
float dot_to_stealth, dist_to_stealth, max_stealth_dist;
|
|
|
|
Assert(stealth_objp->type == OBJ_SHIP);
|
|
shipp = &Ships[stealth_objp->instance];
|
|
Assert(viewer_objp->type == OBJ_SHIP);
|
|
|
|
// check if stealth ship
|
|
Assert(shipp->flags[Ship::Ship_Flags::Stealth]);
|
|
|
|
// check if in neb and below awac level for visible
|
|
if ( !ship_is_visible_by_team(stealth_objp, &Ships[viewer_objp->instance]) ) {
|
|
vm_vec_sub(&vec_to_stealth, &stealth_objp->pos, &viewer_objp->pos);
|
|
dist_to_stealth = vm_vec_mag_quick(&vec_to_stealth);
|
|
dot_to_stealth = vm_vec_dot(&viewer_objp->orient.vec.fvec, &vec_to_stealth) / dist_to_stealth;
|
|
|
|
// get max dist at which stealth is visible
|
|
max_stealth_dist = get_skill_stealth_dist_scaler() * STEALTH_MAX_VIEW_DIST;
|
|
|
|
// now check if within view frustum
|
|
float needed_dot_to_stealth;
|
|
if (dist_to_stealth < 100) {
|
|
needed_dot_to_stealth = 0.0f;
|
|
} else {
|
|
needed_dot_to_stealth = get_skill_stealth_dot_scaler() * float(STEALTH_VIEW_CONE_DOT) * (dist_to_stealth / max_stealth_dist);
|
|
}
|
|
if (dot_to_stealth > needed_dot_to_stealth) {
|
|
if (dist_to_stealth < max_stealth_dist) {
|
|
return STEALTH_IN_FRUSTUM;
|
|
}
|
|
}
|
|
|
|
// not within frustum
|
|
return STEALTH_NOT_IN_FRUSTUM;
|
|
}
|
|
|
|
// visible by awacs level
|
|
return STEALTH_FULLY_TARGETABLE;
|
|
}
|
|
|
|
// END STEALTH
|
|
|
|
/**
|
|
* Compute dot product of direction vector and forward vector.
|
|
* Direction vector is vector from one object to other object.
|
|
* Forward vector is the forward vector of the ship.
|
|
* If from_dot == NULL, don't fill it in.
|
|
*/
|
|
float compute_dots(const object *objp, const object *other_objp, float *to_dot, float *from_dot)
|
|
{
|
|
vec3d v2o;
|
|
float dist;
|
|
|
|
dist = vm_vec_normalized_dir(&v2o, &other_objp->pos, &objp->pos);
|
|
|
|
*to_dot = vm_vec_dot(&objp->orient.vec.fvec, &v2o);
|
|
|
|
if (from_dot != NULL)
|
|
*from_dot = - vm_vec_dot(&other_objp->orient.vec.fvec, &v2o);
|
|
|
|
return dist;
|
|
}
|
|
|
|
/**
|
|
* Update estimated stealth info
|
|
*
|
|
* This is a "cheat" update. Error increases with time not seen, true distance away, dot to enemy
|
|
* this is done only if we can not see the stealth target
|
|
* need to infer its position either by weapon fire pos or last know pos
|
|
*/
|
|
void update_ai_stealth_info_with_error(ai_info *aip)
|
|
{
|
|
object *stealth_objp;
|
|
|
|
// make sure I am targeting a stealth ship
|
|
Assert( is_object_stealth_ship(&Objects[aip->target_objnum]) );
|
|
stealth_objp = &Objects[aip->target_objnum];
|
|
|
|
// if update is due to weapon fire, get exact stealth position
|
|
aip->stealth_last_pos = stealth_objp->pos;
|
|
aip->stealth_velocity = stealth_objp->phys_info.vel;
|
|
aip->stealth_last_visible_stamp = timestamp();
|
|
}
|
|
|
|
/**
|
|
* Update danger_weapon_objnum and signature in ai_info to say this weapon is to be avoided.
|
|
*/
|
|
void ai_update_danger_weapon(int attacked_objnum, int weapon_objnum)
|
|
{
|
|
object *objp, *weapon_objp;
|
|
ai_info *aip;
|
|
float old_dist, new_dist;
|
|
float old_dot, new_dot;
|
|
object *old_weapon_objp = nullptr;
|
|
|
|
// any object number less than 0 is invalid
|
|
if ((attacked_objnum < 0) || (weapon_objnum < 0)) {
|
|
return;
|
|
}
|
|
|
|
objp = &Objects[attacked_objnum];
|
|
|
|
// AL 2-24-98: If this isn't a ship, we don't need to worry about updating weapon_objnum (ie it would be
|
|
// an asteroid or bomb).
|
|
if ( objp->type != OBJ_SHIP ) {
|
|
return;
|
|
}
|
|
|
|
weapon_objp = &Objects[weapon_objnum];
|
|
|
|
aip = &Ai_info[Ships[objp->instance].ai_index];
|
|
|
|
// if my target is a stealth ship and is not visible
|
|
if (aip->target_objnum >= 0) {
|
|
if ( is_object_stealth_ship(&Objects[aip->target_objnum]) ) {
|
|
if ( ai_is_stealth_visible(objp, &Objects[aip->target_objnum]) == STEALTH_NOT_IN_FRUSTUM ) {
|
|
// and the weapon is coming from that stealth ship
|
|
if (weapon_objp->parent == aip->target_objnum) {
|
|
// update my position estimate for stealth ship
|
|
update_ai_stealth_info_with_error(aip);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
weapon_info* wip = &Weapon_info[Weapons[weapon_objp->instance].weapon_info_index];
|
|
if (wip->wi_flags[Weapon::Info_Flags::No_evasion])
|
|
return;
|
|
|
|
if (aip->danger_weapon_objnum != -1) {
|
|
old_weapon_objp = &Objects[aip->danger_weapon_objnum];
|
|
if ((old_weapon_objp->type == OBJ_WEAPON) && (old_weapon_objp->signature == aip->danger_weapon_signature)) {
|
|
;
|
|
} else {
|
|
aip->danger_weapon_objnum = -1;
|
|
}
|
|
}
|
|
|
|
new_dist = compute_dots(weapon_objp, objp, &new_dot, nullptr); // NOLINT(readability-suspicious-call-argument)
|
|
|
|
if (aip->danger_weapon_objnum == -1) {
|
|
if (new_dist < 1500.0f) {
|
|
if (new_dot > 0.5f) {
|
|
aip->danger_weapon_objnum = weapon_objnum;
|
|
aip->danger_weapon_signature = weapon_objp->signature;
|
|
}
|
|
}
|
|
} else {
|
|
Assert(old_weapon_objp != nullptr);
|
|
old_dist = compute_dots(old_weapon_objp, objp, &old_dot, nullptr); // NOLINT(readability-suspicious-call-argument)
|
|
|
|
if (old_dot < 0.5f) {
|
|
aip->danger_weapon_objnum = -1;
|
|
old_dist = 9999.9f;
|
|
}
|
|
|
|
if ((new_dot > 0.5f) && (new_dot > old_dot-0.01f)) {
|
|
if (new_dist < old_dist) {
|
|
aip->danger_weapon_objnum = weapon_objnum;
|
|
aip->danger_weapon_signature = weapon_objp->signature;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Asteroth - Manipulates retail inputs to produce the outputs that would match retail behavior
|
|
// vel and acc_limit should already be sent in with retail values
|
|
// A more in-depth explanation can be found in the #2740 PR description
|
|
void ai_compensate_for_retail_turning(vec3d* vel_limit, vec3d* acc_limit, float rotdamp, bool is_weapon) {
|
|
|
|
if (is_weapon) {
|
|
// Missiles accelerated instantly technically, but this should do.
|
|
*acc_limit = *vel_limit * 1000.f;
|
|
// Approximately what you'd get at 60fps
|
|
*vel_limit *= 0.128f;
|
|
return;
|
|
}
|
|
|
|
// If we're a ship instead things get a bit hairier
|
|
// This is the polynomial approximation of the 'combination' effects of
|
|
// angular_move and physics_sim_rot
|
|
for (int i = 0; i < 3; i++) {
|
|
float& vel = vel_limit->a1d[i];
|
|
float& acc = acc_limit->a1d[i];
|
|
|
|
float v1 = 2 * acc * rotdamp;
|
|
float v2 = 2 * vel;
|
|
if (v1 <= v2) {
|
|
vel = v1;
|
|
}
|
|
else {
|
|
vel = v2;
|
|
if (v1 == 0)
|
|
acc = 1000.f;
|
|
else
|
|
acc = acc * (2 - v2 / v1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If rvec != NULL, use it to match bank by calling vm_angular_move_matrix.
|
|
// (rvec defaults to NULL)
|
|
// optionally specified turnrate_mod contains multipliers for each component of the turnrate (RATE, so 0.5 = slower)
|
|
void ai_turn_towards_vector(const vec3d* dest, object* objp, const vec3d* slide_vec, const vec3d* rel_pos, float bank_override, int flags, const vec3d* rvec, const vec3d* turnrate_mod)
|
|
{
|
|
matrix curr_orient;
|
|
vec3d vel_in, vel_out, desired_fvec, src;
|
|
float delta_time;
|
|
physics_info *pip;
|
|
float bank;
|
|
|
|
Assertion(objp->type == OBJ_SHIP || objp->type == OBJ_WEAPON, "ai_turn_towards_vector called on a non-ship non-weapon object!");
|
|
|
|
// Don't allow a ship to turn if it has no engine strength.
|
|
// AL 3-12-98: objp may not always be a ship!
|
|
if ( (objp->type == OBJ_SHIP) && !(flags & AITTV_VIA_SEXP) ) {
|
|
if (!Ships[objp->instance].flags[Ship::Ship_Flags::Maneuver_despite_engines] && ship_subsystems_blown(&Ships[objp->instance], SUBSYSTEM_ENGINE))
|
|
return;
|
|
}
|
|
|
|
// Don't allow a ship to turn if it's prevented from turning.
|
|
if ( objp->flags[Object::Object_Flags::Dont_change_orientation, Object::Object_Flags::Immobile] ) {
|
|
return;
|
|
}
|
|
|
|
pip = &objp->phys_info;
|
|
|
|
vel_in = pip->rotvel;
|
|
curr_orient = objp->orient;
|
|
delta_time = flFrametime;
|
|
|
|
vec3d vel_limit, acc_limit;
|
|
vm_vec_zero(&vel_limit);
|
|
|
|
// get the turn rate if we have Use_axial_turnrate_differences
|
|
if (objp->type == OBJ_SHIP && The_mission.ai_profile->flags[AI::Profile_Flags::Use_axial_turnrate_differences]) {
|
|
vel_limit = Ship_info[Ships[objp->instance].ship_info_index].max_rotvel;
|
|
|
|
} else { // else get the turn time
|
|
float turn_time = 0.f;
|
|
if (objp->type == OBJ_WEAPON) {
|
|
turn_time = Weapon_info[Weapons[objp->instance].weapon_info_index].turn_time;
|
|
}
|
|
else if (objp->type == OBJ_SHIP) {
|
|
turn_time = Ship_info[Ships[objp->instance].ship_info_index].srotation_time;
|
|
}
|
|
|
|
//and then turn it into turnrate
|
|
if (turn_time > 0.0f)
|
|
{
|
|
vel_limit.xyz.x = PI2 / turn_time;
|
|
vel_limit.xyz.y = PI2 / turn_time;
|
|
vel_limit.xyz.z = PI2 / turn_time;
|
|
}
|
|
}
|
|
|
|
// maybe modify the ship's turnrate if caller wants
|
|
if (turnrate_mod) {
|
|
vel_limit.xyz.x *= turnrate_mod->xyz.x;
|
|
vel_limit.xyz.y *= turnrate_mod->xyz.y;
|
|
vel_limit.xyz.z *= turnrate_mod->xyz.z;
|
|
}
|
|
|
|
|
|
// Scale turnrate based on skill level and team.
|
|
if (!(flags & AITTV_FAST) && !(flags & AITTV_VIA_SEXP)) {
|
|
if (objp->type == OBJ_SHIP) {
|
|
if (iff_x_attacks_y(Player_ship->team, Ships[objp->instance].team)) {
|
|
if (Ai_info[Ships[objp->instance].ai_index].ai_turn_time_scale != 0.f) {
|
|
vm_vec_scale(&vel_limit, 1/Ai_info[Ships[objp->instance].ai_index].ai_turn_time_scale);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
acc_limit = ai_get_acc_limit(&vel_limit, objp);
|
|
|
|
// for formation flying
|
|
if ((flags & AITTV_SLOW_BANK_ACCEL)) {
|
|
acc_limit.xyz.z *= 0.2f;
|
|
}
|
|
|
|
// finally, maybe override the limits based on some ship flags
|
|
if (objp->type == OBJ_SHIP) {
|
|
ship_info* sip = &Ship_info[Ships[objp->instance].ship_info_index];
|
|
if (sip->flags[Ship::Info_Flags::Dont_clamp_max_velocity])
|
|
vel_limit = vm_vec_new(200.0f, 200.0f, 200.0f); // 200 rad/s should be enough to get anywhere within a 1/60th of a second
|
|
if (sip->flags[Ship::Info_Flags::Instantaneous_acceleration])
|
|
acc_limit = vm_vec_new(12000.0f, 12000.0f, 12000.0f); // enough to get to 200 rad/s in 1/60th of a second
|
|
}
|
|
|
|
src = objp->pos;
|
|
|
|
if (rel_pos != NULL) {
|
|
vec3d gun_point;
|
|
vm_vec_unrotate(&gun_point, rel_pos, &objp->orient);
|
|
vm_vec_add2(&src, &gun_point);
|
|
}
|
|
|
|
vm_vec_normalized_dir(&desired_fvec, dest, &src);
|
|
|
|
// Since ship isn't necessarily moving in the direction it's pointing, sometimes it's better
|
|
// to be moving towards goal rather than just pointing. So, if slide_vec is !NULL, try to
|
|
// make ship move towards goal, not point at goal.
|
|
if (slide_vec != NULL) {
|
|
vm_vec_add2(&desired_fvec, slide_vec);
|
|
vm_vec_normalize(&desired_fvec);
|
|
}
|
|
|
|
// Should be more general case here. Currently, anything that is not a weapon will bank when it turns.
|
|
// Goober5000 - don't bank if sexp or ship says not to
|
|
if (flags & AITTV_FORCE_DELTA_BANK)
|
|
bank = bank_override;
|
|
else if ( (objp->type == OBJ_WEAPON) || (flags & AITTV_IGNORE_BANK ) )
|
|
bank = 0.0f;
|
|
else if (objp->type == OBJ_SHIP && Ship_info[Ships[objp->instance].ship_info_index].flags[Ship::Info_Flags::Dont_bank_when_turning])
|
|
bank = 0.0f;
|
|
else if ((bank_override) && (iff_x_attacks_y(Ships[objp->instance].team, Player_ship->team))) { // Theoretically, this will only happen for Shivans.
|
|
bank = bank_override;
|
|
} else {
|
|
bank = vm_vec_dot(&curr_orient.vec.rvec, &objp->last_orient.vec.rvec);
|
|
bank = 200.0f * (1.0f - bank) * pip->delta_bank_const;
|
|
if (vm_vec_dot(&objp->last_orient.vec.fvec, &objp->orient.vec.rvec) < 0.0f)
|
|
bank = -bank;
|
|
}
|
|
bank *= vel_limit.xyz.z;
|
|
|
|
|
|
matrix out_orient;
|
|
|
|
if (rvec != NULL) {
|
|
matrix goal_orient;
|
|
|
|
vm_vector_2_matrix_norm(&goal_orient, &desired_fvec, nullptr, rvec);
|
|
vm_angular_move_matrix(&goal_orient, &curr_orient, &vel_in, delta_time,
|
|
&out_orient, &vel_out, &vel_limit, &acc_limit, The_mission.ai_profile->flags[AI::Profile_Flags::No_turning_directional_bias]);
|
|
} else {
|
|
vm_angular_move_forward_vec(&desired_fvec, &curr_orient, &vel_in, delta_time, bank,
|
|
&out_orient, &vel_out, &vel_limit, &acc_limit, The_mission.ai_profile->flags[AI::Profile_Flags::No_turning_directional_bias]);
|
|
}
|
|
|
|
if (Framerate_independent_turning) {
|
|
if (IS_MAT_NULL(&pip->ai_desired_orient)) {
|
|
pip->ai_desired_orient = out_orient;
|
|
pip->desired_rotvel = vel_out;
|
|
} // if ai_desired_orient is NOT null, that means a SEXP is trying to move the ship right now, and the AI should defer to it
|
|
}
|
|
else {
|
|
objp->orient = out_orient;
|
|
pip->rotvel = vel_out;
|
|
}
|
|
|
|
}
|
|
|
|
//vel_limit can be modified if the object is a weapon and retail behaviour is enabled
|
|
vec3d ai_get_acc_limit(vec3d* vel_limit, const object* objp) {
|
|
// Set rate at which ship can accelerate to its rotational velocity.
|
|
// For now, weapons just go much faster.
|
|
vec3d acc_limit = *vel_limit;
|
|
if (objp->type == OBJ_WEAPON)
|
|
acc_limit *= 8.0f;
|
|
|
|
const physics_info* pip = &objp->phys_info;
|
|
|
|
// do _proper_ handling of acc_limit and vel_limit if this flag is set
|
|
if (Framerate_independent_turning) {
|
|
// handle modifications to rotdamp (that could affect acc_limit and vel_limit)
|
|
// handled in its entirety in physics_sim_rot
|
|
float rotdamp = pip->rotdamp;
|
|
float shock_fraction_time_left = 0.f;
|
|
if (pip->flags & PF_IN_SHOCKWAVE) {
|
|
shock_fraction_time_left = timestamp_until(pip->shockwave_decay) / (float)SW_BLAST_DURATION;
|
|
if (shock_fraction_time_left > 0)
|
|
rotdamp = pip->rotdamp + pip->rotdamp * (SW_ROT_FACTOR - 1) * shock_fraction_time_left;
|
|
}
|
|
|
|
if (Ai_respect_tabled_turntime_rotdamp) {
|
|
// We're assuming the turn rate here is accurate so leave it as is
|
|
// but we'll use the ship's rotdamp to modify acc_limit
|
|
// (this is the polynomial approximation of the exponential effect rotdamp has on players)
|
|
if (rotdamp == 0.f)
|
|
acc_limit *= 1000.f;
|
|
else
|
|
acc_limit = *vel_limit * (0.5f / rotdamp);
|
|
}
|
|
else {
|
|
// else, calculate the retail-friendly vel and acc values
|
|
ai_compensate_for_retail_turning(vel_limit, &acc_limit, rotdamp, objp->type == OBJ_WEAPON);
|
|
|
|
// handle missile turning accel (which is bundled into rotdamp)
|
|
if (objp->type == OBJ_WEAPON && rotdamp != 0.f) {
|
|
acc_limit = *vel_limit * (0.5f / rotdamp);
|
|
}
|
|
}
|
|
}
|
|
|
|
return acc_limit;
|
|
}
|
|
|
|
|
|
// Set aip->target_objnum to objnum
|
|
// Update aip->previous_target_objnum.
|
|
// If new target (objnum) is different than old target, reset target_time.
|
|
int set_target_objnum(ai_info *aip, int objnum)
|
|
{
|
|
if ((aip != Player_ai) && (!timestamp_elapsed(aip->ok_to_target_timestamp))) {
|
|
return aip->target_objnum;
|
|
}
|
|
|
|
if (aip->target_objnum == objnum) {
|
|
aip->previous_target_objnum = aip->target_objnum;
|
|
} else {
|
|
aip->previous_target_objnum = aip->target_objnum;
|
|
|
|
// ignore this assert if a multiplayer observer
|
|
if((Game_mode & GM_MULTIPLAYER) && (aip == Player_ai) && (Player_obj->type == OBJ_OBSERVER)){
|
|
} else {
|
|
Assert(objnum != Ships[aip->shipnum].objnum); // make sure not targeting self
|
|
}
|
|
|
|
// if stealth target, init ai_info for stealth
|
|
if ( (objnum >= 0) && is_object_stealth_ship(&Objects[objnum]) ) {
|
|
init_ai_stealth_info(aip, &Objects[objnum]);
|
|
}
|
|
|
|
aip->target_objnum = objnum;
|
|
aip->target_time = 0.0f;
|
|
aip->time_enemy_near = 0.0f; //SUSHI: Reset the "time near" counter whenever the target is changed
|
|
aip->last_hit_target_time = Missiontime; //Also, the "last hit target" time should be reset when the target changes
|
|
aip->target_signature = (objnum >= 0) ? Objects[objnum].signature : -1;
|
|
// clear targeted subsystem
|
|
set_targeted_subsys(aip, NULL, -1);
|
|
}
|
|
|
|
return aip->target_objnum;
|
|
}
|
|
|
|
int ai_select_primary_weapon(object *objp, object *other_objp, Weapon::Info_Flags flags);
|
|
|
|
/**
|
|
* Make new_subsys the targeted subsystem of ship *aip.
|
|
*/
|
|
ship_subsys *set_targeted_subsys(ai_info *aip, ship_subsys *new_subsys, int parent_objnum)
|
|
{
|
|
Assert(aip != NULL);
|
|
|
|
aip->last_subsys_target = aip->targeted_subsys;
|
|
aip->targeted_subsys = new_subsys;
|
|
aip->targeted_subsys_parent = parent_objnum;
|
|
|
|
if ( new_subsys ) {
|
|
// Make new_subsys target
|
|
if (new_subsys->system_info->type == SUBSYSTEM_ENGINE) {
|
|
if ( aip != Player_ai ) {
|
|
Assert( aip->shipnum >= 0 );
|
|
ai_select_primary_weapon(&Objects[Ships[aip->shipnum].objnum], &Objects[parent_objnum], Weapon::Info_Flags::Puncture);
|
|
ship_primary_changed(&Ships[aip->shipnum]); // AL: maybe send multiplayer information when AI ship changes primaries
|
|
}
|
|
}
|
|
|
|
if ( aip == Player_ai ) {
|
|
if (aip->last_subsys_target != aip->targeted_subsys) {
|
|
hud_lock_reset(0.5f);
|
|
}
|
|
}
|
|
|
|
} else {
|
|
// Cleanup any subsys path information if it exists
|
|
ai_big_subsys_path_cleanup(aip);
|
|
}
|
|
|
|
return aip->targeted_subsys;
|
|
}
|
|
|
|
/**
|
|
* Called to init the data for single ai object.
|
|
*
|
|
* At this point, the ship and the object and the ai_info are are correctly
|
|
* linked together. Ai_info[ai_index].shipnum is the only valid field
|
|
* in ai_info.
|
|
*
|
|
* This is called right when the object is parsed, so you can't assume much
|
|
* has been initialized. For example, wings, waypoints, goals are probably
|
|
* not yet loaded. --MK, 10/8/96
|
|
*/
|
|
void ai_object_init(object * obj, int ai_index)
|
|
{
|
|
ai_info *aip;
|
|
Assert(ai_index >= 0 && ai_index < MAX_AI_INFO);
|
|
|
|
aip = &Ai_info[ai_index];
|
|
aip->ai_class = Ship_info[Ships[obj->instance].ship_info_index].ai_class;
|
|
aip->mode = AIM_NONE;
|
|
}
|
|
|
|
/**
|
|
* If *aip is docked, set max acceleration to A->mass/(A->mass + B->mass) where A is *aip and B is dock object
|
|
*/
|
|
void adjust_accel_for_docking(ai_info *aip)
|
|
{
|
|
object *objp = &Objects[Ships[aip->shipnum].objnum];
|
|
|
|
if (object_is_docked(objp))
|
|
{
|
|
float ratio = objp->phys_info.mass / dock_calc_total_docked_mass(objp);
|
|
|
|
// put cap on how much ship can slow down
|
|
if ( (ratio < 0.8f) && !(The_mission.ai_profile->flags[AI::Profile_Flags::No_min_dock_speed_cap]) ) {
|
|
ratio = 0.8f;
|
|
}
|
|
|
|
// make sure we at least some velocity
|
|
if (ratio < 0.1f) {
|
|
ratio = 0.1f;
|
|
}
|
|
|
|
if (AI_ci.forward > ratio) {
|
|
AI_ci.forward = ratio;
|
|
}
|
|
}
|
|
}
|
|
|
|
void accelerate_ship(ai_info *aip, float accel)
|
|
{
|
|
aip->prev_accel = accel;
|
|
AI_ci.forward = accel;
|
|
adjust_accel_for_docking(aip);
|
|
}
|
|
|
|
void change_acceleration(ai_info *aip, float delta_accel)
|
|
{
|
|
float new_accel;
|
|
|
|
if (delta_accel < 0.0f) {
|
|
if (aip->prev_accel > 0.0f)
|
|
aip->prev_accel = 0.0f;
|
|
} else if (aip->prev_accel < 0.0f)
|
|
aip->prev_accel = 0.0f;
|
|
|
|
new_accel = aip->prev_accel + delta_accel * flFrametime;
|
|
|
|
if (new_accel > 1.0f)
|
|
new_accel = 1.0f;
|
|
else if (new_accel < -1.0f)
|
|
new_accel = -1.0f;
|
|
|
|
aip->prev_accel = new_accel;
|
|
|
|
AI_ci.forward = new_accel;
|
|
adjust_accel_for_docking(aip);
|
|
}
|
|
|
|
void set_accel_for_target_speed(object *objp, float tspeed)
|
|
{
|
|
float max_speed;
|
|
ai_info *aip;
|
|
|
|
aip = &Ai_info[Ships[objp->instance].ai_index];
|
|
|
|
max_speed = objp->phys_info.max_vel.xyz.z;
|
|
|
|
if (max_speed > 0.0f) {
|
|
AI_ci.forward = tspeed/max_speed;
|
|
} else {
|
|
AI_ci.forward = 0.0f;
|
|
}
|
|
|
|
aip->prev_accel = AI_ci.forward;
|
|
|
|
adjust_accel_for_docking(aip);
|
|
}
|
|
|
|
/**
|
|
* Stuff perim_point with a point on the perimeter of the sphere defined by object *objp
|
|
* on the vector from the center of *objp through the point *vp.
|
|
*/
|
|
void project_point_to_perimeter(vec3d *perim_point, vec3d *pos, float radius, vec3d *vp)
|
|
{
|
|
vec3d v1;
|
|
float mag;
|
|
|
|
vm_vec_sub(&v1, vp, pos);
|
|
mag = vm_vec_mag(&v1);
|
|
|
|
if (mag == 0.0f) {
|
|
Warning(LOCATION, "projectable point is at center of sphere.");
|
|
vm_vec_make(&v1, 0.0f, radius, 0.0f);
|
|
} else {
|
|
vm_vec_normalize(&v1);
|
|
vm_vec_scale(&v1, 1.1f * radius + 10.0f);
|
|
}
|
|
|
|
vm_vec_add2(&v1, pos);
|
|
*perim_point = v1;
|
|
}
|
|
|
|
/**
|
|
* Stuff tan1 with tangent point on sphere.
|
|
*
|
|
* @param tan1 is point nearer to *p1
|
|
* @param *p0 is point through which tangents pass.
|
|
* @param *centerp is center of sphere.
|
|
* @param *p1 is another point in space to define the plane in which tan1, tan2 reside.
|
|
* @param radius is the radius of the sphere.
|
|
*
|
|
* Note, this is a very approximate function just for AI.
|
|
* Note also: On 12/26/96, p1 is used to define the plane perpendicular to that which
|
|
* contains the tangent point.
|
|
*/
|
|
void get_tangent_point(vec3d *tan1, vec3d *p0, vec3d *centerp, vec3d *p1, float radius)
|
|
{
|
|
vec3d dest_vec, v2c, perp_vec, temp_vec, v2;
|
|
float dist, ratio;
|
|
|
|
// Detect condition of point inside sphere.
|
|
if (vm_vec_dist(p0, centerp) < radius)
|
|
project_point_to_perimeter(tan1, centerp, radius, p0);
|
|
else {
|
|
vm_vec_normalized_dir(&v2c, centerp, p0);
|
|
|
|
// Compute perpendicular vector using p0, centerp, p1
|
|
vm_vec_normal(&temp_vec, p0, centerp, p1);
|
|
vm_vec_sub(&v2, centerp, p0);
|
|
vm_vec_cross(&perp_vec, &temp_vec, &v2);
|
|
|
|
vm_vec_normalize(&perp_vec);
|
|
|
|
dist = vm_vec_dist_quick(p0, centerp);
|
|
ratio = dist / radius;
|
|
|
|
if (ratio < 2.0f)
|
|
vm_vec_scale_add(&dest_vec, &perp_vec, &v2c, ratio-1.0f);
|
|
else
|
|
vm_vec_scale_add(&dest_vec, &v2c, &perp_vec, (1.0f + 1.0f/ratio));
|
|
|
|
vm_vec_scale_add(tan1, p0, &dest_vec, dist + radius);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given an object and a point, turn towards the point, resulting in
|
|
* approach behavior. Optional argument target_orient to attempt to match bank with
|
|
*/
|
|
void turn_towards_point(object *objp, vec3d *point, vec3d *slide_vec, float bank_override, matrix* target_orient, int flags)
|
|
{
|
|
vec3d* rvec = nullptr;
|
|
vec3d goal_rvec;
|
|
|
|
if (target_orient != nullptr) {
|
|
rvec = &goal_rvec;
|
|
vec3d goal_fvec;
|
|
|
|
vm_vec_normalized_dir(&goal_fvec,point, &objp->pos);
|
|
vm_match_bank(rvec, &goal_fvec, target_orient);
|
|
}
|
|
|
|
ai_turn_towards_vector(point, objp, slide_vec, nullptr, bank_override, flags, rvec);
|
|
}
|
|
|
|
/**
|
|
* Given an object and a point, turn away from the point, resulting in avoidance behavior.
|
|
* Note: Turn away at full speed, not scaled down by skill level.
|
|
*/
|
|
void turn_away_from_point(object *objp, vec3d *point, float bank_override)
|
|
{
|
|
vec3d opposite_point;
|
|
|
|
vm_vec_sub(&opposite_point, &objp->pos, point);
|
|
vm_vec_add2(&opposite_point, &objp->pos);
|
|
|
|
ai_turn_towards_vector(&opposite_point, objp, nullptr, nullptr, bank_override, AITTV_FAST);
|
|
}
|
|
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Given an object and a point, turn tangent to the point, resulting in
|
|
// a circling behavior.
|
|
// Make object *objp turn around the point *point with a radius of radius.
|
|
// Note that this isn't the same as following a circle of radius radius with
|
|
// center *point, but it should be adequate.
|
|
// Note that if you want to circle an object without hitting it, you should use
|
|
// about twice that object's radius for radius, else you'll certainly bump into it.
|
|
// Return dot product to goal point.
|
|
float turn_towards_tangent(object *objp, vec3d *point, float radius)
|
|
{
|
|
vec3d vec_to_point;
|
|
vec3d goal_point;
|
|
vec3d perp_point; // point radius away from *point on vector to objp->pos
|
|
vec3d up_vec, perp_vec;
|
|
vec3d v2g;
|
|
|
|
vm_vec_normalized_dir(&vec_to_point, point, &objp->pos);
|
|
vm_vec_cross(&up_vec, &vec_to_point, &objp->orient.vec.fvec);
|
|
vm_vec_cross(&perp_vec, &vec_to_point, &up_vec);
|
|
|
|
vm_vec_scale_add(&perp_point, point, &vec_to_point, -radius);
|
|
if (vm_vec_dot(&objp->orient.vec.fvec, &perp_vec) > 0.0f) {
|
|
vm_vec_scale_add(&goal_point, &perp_point, &perp_vec, radius);
|
|
} else {
|
|
vm_vec_scale_add(&goal_point, &perp_point, &perp_vec, -radius);
|
|
}
|
|
|
|
turn_towards_point(objp, &goal_point, NULL, 0.0f);
|
|
|
|
vm_vec_normalized_dir(&v2g, &goal_point, &objp->pos);
|
|
return vm_vec_dot(&objp->orient.vec.fvec, &v2g);
|
|
}
|
|
|
|
float turn_toward_tangent_with_axis(object *objp, object *center_objp, float radius)
|
|
{
|
|
vec3d r_vec, theta_vec;
|
|
vec3d center_vec, vec_on_cylinder, sph_r_vec;
|
|
float center_obj_z;
|
|
|
|
// find closest z of center objp
|
|
vm_vec_sub(&sph_r_vec, &objp->pos, ¢er_objp->pos);
|
|
center_obj_z = vm_vec_dot(&sph_r_vec, ¢er_objp->orient.vec.fvec);
|
|
|
|
// find pt on axis with closest z
|
|
vm_vec_scale_add(¢er_vec, ¢er_objp->pos, ¢er_objp->orient.vec.fvec, center_obj_z);
|
|
|
|
// get r_vec
|
|
vm_vec_sub(&r_vec, &objp->pos, ¢er_vec);
|
|
|
|
Assert( (vm_vec_dot(&r_vec, ¢er_objp->orient.vec.fvec) < 0.0001));
|
|
|
|
// get theta vec - perp to r_vec and z_vec
|
|
vm_vec_cross(&theta_vec, ¢er_objp->orient.vec.fvec, &r_vec);
|
|
|
|
#ifndef NDEBUG
|
|
float mag;
|
|
mag = vm_vec_normalize(&theta_vec);
|
|
Assert(mag > 0.9999 && mag < 1.0001);
|
|
#endif
|
|
|
|
vec3d temp;
|
|
vm_vec_cross(&temp, &r_vec, &theta_vec);
|
|
|
|
#ifndef NDEBUG
|
|
float dot;
|
|
dot = vm_vec_dot(&temp, ¢er_objp->orient.vec.fvec);
|
|
Assert( dot >0.9999 && dot < 1.0001);
|
|
#endif
|
|
|
|
// find pt on clylinder with closest z
|
|
vm_vec_scale_add(&vec_on_cylinder, ¢er_vec, &r_vec, radius);
|
|
|
|
vec3d goal_pt, v2g;
|
|
vm_vec_scale_add(&goal_pt, &vec_on_cylinder, &theta_vec, radius);
|
|
|
|
turn_towards_point(objp, &goal_pt, NULL, 0.0f);
|
|
|
|
vm_vec_normalized_dir(&v2g, &goal_pt, &objp->pos);
|
|
return vm_vec_dot(&objp->orient.vec.fvec, &v2g);
|
|
}
|
|
|
|
/**
|
|
* Returns a point radius units away from *point that *objp should turn towards to orbit *point
|
|
*/
|
|
void get_tangent_point(vec3d *goal_point, object *objp, vec3d *point, float radius)
|
|
{
|
|
vec3d vec_to_point;
|
|
vec3d perp_point; // point radius away from *point on vector to objp->pos
|
|
vec3d up_vec, perp_vec;
|
|
|
|
vm_vec_normalized_dir(&vec_to_point, point, &objp->pos);
|
|
vm_vec_cross(&up_vec, &vec_to_point, &objp->orient.vec.fvec);
|
|
|
|
while (IS_VEC_NULL(&up_vec)) {
|
|
vec3d rnd_vec;
|
|
|
|
vm_vec_rand_vec_quick(&rnd_vec);
|
|
vm_vec_add2(&rnd_vec, &objp->orient.vec.fvec);
|
|
vm_vec_cross(&up_vec, &vec_to_point, &rnd_vec);
|
|
}
|
|
|
|
vm_vec_cross(&perp_vec, &vec_to_point, &up_vec);
|
|
vm_vec_normalize(&perp_vec);
|
|
|
|
vm_vec_scale_add(&perp_point, point, &vec_to_point, -radius);
|
|
|
|
if (vm_vec_dot(&objp->orient.vec.fvec, &perp_vec) > 0.0f) {
|
|
vm_vec_scale_add(goal_point, &perp_point, &perp_vec, radius);
|
|
} else {
|
|
vm_vec_scale_add(goal_point, &perp_point, &perp_vec, -radius);
|
|
}
|
|
}
|
|
|
|
int Player_attacking_enabled = 1;
|
|
|
|
/**
|
|
* Determine whether an object is targetable within a nebula
|
|
*/
|
|
bool object_is_targetable(const object *target, const ship *viewer)
|
|
{
|
|
// if target is ship, check if visible by team
|
|
if (target->type == OBJ_SHIP)
|
|
{
|
|
if (ship_is_visible_by_team(target, viewer)) {
|
|
return 1;
|
|
}
|
|
|
|
// for AI partially targetable works as fully targetable, except for stealth ship
|
|
if (Ships[target->instance].flags[Ship::Ship_Flags::Stealth]) {
|
|
// if not team targetable, check if within frustum
|
|
if ( ai_is_stealth_visible(&Objects[viewer->objnum], target) == STEALTH_IN_FRUSTUM ) {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// if not fully targetable by team, check awacs level with viewer
|
|
// allow targeting even if only only partially targetable to player
|
|
float radar_return = awacs_get_level(target, viewer);
|
|
if ( radar_return > 0.4 ) {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return number of enemies attacking object objnum
|
|
*/
|
|
int num_enemies_attacking(int objnum)
|
|
{
|
|
object *objp;
|
|
ship *sp;
|
|
ship_subsys *ssp;
|
|
ship_obj *so;
|
|
int count;
|
|
|
|
count = 0;
|
|
|
|
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
|
|
objp = &Objects[so->objnum];
|
|
if (objp->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
Assert(objp->instance != -1);
|
|
sp = &Ships[objp->instance];
|
|
|
|
if (Ai_info[sp->ai_index].target_objnum == objnum)
|
|
count++;
|
|
|
|
// consider turrets that may be attacking objnum (but only turrets on SIF_BIG_SHIP ships)
|
|
if ( Ship_info[sp->ship_info_index].is_big_ship() ) {
|
|
|
|
// loop through all the subsystems, check if turret has objnum as a target
|
|
ssp = GET_FIRST(&sp->subsys_list);
|
|
while ( ssp != END_OF_LIST( &sp->subsys_list ) ) {
|
|
|
|
if ( ssp->system_info->type == SUBSYSTEM_TURRET ) {
|
|
if ( (ssp->turret_enemy_objnum == objnum) && (ssp->current_hits > 0) ) {
|
|
count++;
|
|
}
|
|
}
|
|
ssp = GET_NEXT( ssp );
|
|
} // end while
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Scan all the ships in *objp's wing. Return the lowest maximum speed of a ship in the wing.
|
|
*
|
|
* Current maximum speed (based on energy settings) is objp->phys_info.max_vel.xyz.z
|
|
*/
|
|
float get_wing_lowest_max_speed(object *objp)
|
|
{
|
|
ship *shipp;
|
|
float lowest_max_speed;
|
|
int wingnum;
|
|
object *o;
|
|
ship_obj *so;
|
|
|
|
Assert(objp->type == OBJ_SHIP);
|
|
Assert((objp->instance >= 0) && (objp->instance < MAX_OBJECTS));
|
|
shipp = &Ships[objp->instance];
|
|
wingnum = shipp->wingnum;
|
|
|
|
lowest_max_speed = objp->phys_info.max_vel.xyz.z;
|
|
|
|
if ( wingnum == -1 )
|
|
return lowest_max_speed;
|
|
|
|
Assert(wingnum >= 0);
|
|
|
|
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
|
|
o = &Objects[so->objnum];
|
|
if (o->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
ship *oshipp = &Ships[o->instance];
|
|
ai_info *oaip = &Ai_info[oshipp->ai_index];
|
|
|
|
if ((oaip->mode == AIM_WAYPOINTS) && (oshipp->wingnum == wingnum)) {
|
|
// Note: If a ship in the wing has a super low max speed, probably its engines are disabled. So, fly along and
|
|
// ignore the poor guy.
|
|
float cur_max = o->phys_info.max_vel.xyz.z;
|
|
|
|
if (object_is_docked(o)) {
|
|
cur_max *= o->phys_info.mass / dock_calc_total_docked_mass(o);
|
|
}
|
|
|
|
if ((o->phys_info.max_vel.xyz.z > 5.0f) && (cur_max < lowest_max_speed)) {
|
|
lowest_max_speed = cur_max;
|
|
}
|
|
}
|
|
}
|
|
|
|
return lowest_max_speed;
|
|
}
|
|
|
|
/**
|
|
* Scan all the ships in *objp's wing. Return the lowest average speed with afterburner of a ship in the wing.
|
|
*/
|
|
float get_wing_lowest_av_ab_speed(object *objp)
|
|
{
|
|
ship *shipp;
|
|
ai_info *aip;
|
|
float lowest_max_av_ab_speed;
|
|
float recharge_scale;
|
|
int wingnum;
|
|
object *o;
|
|
ship_obj *so;
|
|
ship_info *sip;
|
|
|
|
Assert(objp->type == OBJ_SHIP);
|
|
Assert((objp->instance >= 0) && (objp->instance < MAX_OBJECTS));
|
|
shipp = &Ships[objp->instance];
|
|
Assert((shipp->ai_index >= 0) && (shipp->ai_index < MAX_AI_INFO));
|
|
aip = &Ai_info[shipp->ai_index];
|
|
sip = &Ship_info[shipp->ship_info_index];
|
|
|
|
wingnum = shipp->wingnum;
|
|
|
|
if (((shipp->flags[Ship::Ship_Flags::Afterburner_locked]) || !(sip->flags[Ship::Info_Flags::Afterburner])) || (objp->phys_info.max_vel.xyz.z < 5.0f) ||
|
|
(objp->phys_info.afterburner_max_vel.xyz.z <= objp->phys_info.max_vel.xyz.z) || !(aip->ai_flags[AI::AI_Flags::Free_afterburner_use]
|
|
|| aip->ai_profile_flags[AI::Profile_Flags::Free_afterburner_use])) {
|
|
lowest_max_av_ab_speed = objp->phys_info.max_vel.xyz.z;
|
|
}
|
|
else
|
|
{
|
|
recharge_scale = ets_power_factor(objp) * Energy_levels[shipp->engine_recharge_index] * 2.0f * The_mission.ai_profile->afterburner_recharge_scale[Game_skill_level];
|
|
recharge_scale = sip->afterburner_recover_rate * recharge_scale / (sip->afterburner_burn_rate + sip->afterburner_recover_rate * recharge_scale);
|
|
lowest_max_av_ab_speed = recharge_scale * (objp->phys_info.afterburner_max_vel.xyz.z - objp->phys_info.max_vel.xyz.z) + objp->phys_info.max_vel.xyz.z;
|
|
}
|
|
|
|
|
|
if ( wingnum == -1 )
|
|
return lowest_max_av_ab_speed;
|
|
|
|
Assert(wingnum >= 0);
|
|
|
|
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
|
|
o = &Objects[so->objnum];
|
|
if (o->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
ship *oshipp = &Ships[o->instance];
|
|
ai_info *oaip = &Ai_info[oshipp->ai_index];
|
|
ship_info *osip = &Ship_info[oshipp->ship_info_index];
|
|
|
|
if ((oaip->mode == AIM_WAYPOINTS) && (oshipp->wingnum == wingnum) && (oaip->ai_flags[AI::AI_Flags::Formation_object, AI::AI_Flags::Formation_wing])) {
|
|
|
|
float cur_max;
|
|
if ((oshipp->flags[Ship::Ship_Flags::Afterburner_locked]) || !(osip->flags[Ship::Info_Flags::Afterburner]) || (o->phys_info.afterburner_max_vel.xyz.z <= o->phys_info.max_vel.xyz.z) || !(oaip->ai_flags[AI::AI_Flags::Free_afterburner_use] || oaip->ai_profile_flags[AI::Profile_Flags::Free_afterburner_use])) {
|
|
cur_max = o->phys_info.max_vel.xyz.z;
|
|
}
|
|
else
|
|
{
|
|
recharge_scale = ets_power_factor(o) * Energy_levels[oshipp->engine_recharge_index] * 2.0f * The_mission.ai_profile->afterburner_recharge_scale[Game_skill_level];
|
|
recharge_scale = osip->afterburner_recover_rate * recharge_scale / (osip->afterburner_burn_rate + osip->afterburner_recover_rate * recharge_scale);
|
|
cur_max = recharge_scale * (o->phys_info.afterburner_max_vel.xyz.z - o->phys_info.max_vel.xyz.z) + o->phys_info.max_vel.xyz.z;
|
|
}
|
|
|
|
if (object_is_docked(o)) {
|
|
cur_max *= o->phys_info.mass / dock_calc_total_docked_mass(o);
|
|
}
|
|
// Note: If a ship in the wing has a super low max speed, probably its engines are disabled. So, fly along and
|
|
// ignore the poor guy.
|
|
if ((o->phys_info.max_vel.xyz.z > 5.0f) && (cur_max < lowest_max_av_ab_speed)) {
|
|
lowest_max_av_ab_speed = cur_max;
|
|
}
|
|
}
|
|
}
|
|
|
|
return lowest_max_av_ab_speed;
|
|
}
|
|
|
|
/**
|
|
* Determine if object objnum is supposed to be ignored by object with ai_info *aip.
|
|
* @return TRUE if objnum is aip->ignore_objnum (and signatures match) or objnum is in ignore wing
|
|
* @return FALSE otherwise
|
|
*/
|
|
int is_ignore_object_sub(int *ignore_objnum, int *ignore_signature, int objnum)
|
|
{
|
|
// Should never happen. I thought I removed this behavior! -- MK, 5/17/98
|
|
Assertion(((*ignore_objnum <= UNUSED_OBJNUM) || (*ignore_objnum >= 0)), "Unexpected value for ignore_objnum %d. This is a coder error, please report.", *ignore_objnum);
|
|
// whether this is an invalid OBJNUM or the official UNUSED_OBJNUM, we should just return here.
|
|
// As a note, if it was set to UNUSED_OBJNUM, it would be trying to ignore a wing and not an object. (at least according to previously written comments)
|
|
if (*ignore_objnum < 0) {
|
|
return 0;
|
|
}
|
|
|
|
// see if this object became invalid
|
|
if (Objects[*ignore_objnum].signature != *ignore_signature)
|
|
{
|
|
// reset
|
|
*ignore_objnum = UNUSED_OBJNUM;
|
|
}
|
|
// objects and signatures match
|
|
else if (*ignore_objnum == objnum)
|
|
{
|
|
// found it
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Goober5000
|
|
int find_ignore_new_object_index(ai_info *aip, int objnum)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < MAX_IGNORE_NEW_OBJECTS; i++)
|
|
{
|
|
if (is_ignore_object_sub(&aip->ignore_new_objnums[i], &aip->ignore_new_signatures[i], objnum))
|
|
return i;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
// Goober5000
|
|
int is_ignore_object(ai_info *aip, int objnum, int just_the_original = 0)
|
|
{
|
|
// check original (retail) ignore
|
|
if (is_ignore_object_sub(&aip->ignore_objnum, &aip->ignore_signature, objnum))
|
|
return 1;
|
|
|
|
// check new ignore
|
|
if (!just_the_original)
|
|
{
|
|
if (find_ignore_new_object_index(aip, objnum) >= 0)
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
|
|
typedef struct eval_nearest_objnum {
|
|
int objnum;
|
|
object *trial_objp;
|
|
int enemy_team_mask;
|
|
int enemy_ship_info_index;
|
|
int enemy_class_type;
|
|
int enemy_wing;
|
|
float range;
|
|
int max_attackers;
|
|
int nearest_objnum;
|
|
float nearest_dist;
|
|
int check_danger_weapon_objnum;
|
|
} eval_nearest_objnum;
|
|
|
|
|
|
void evaluate_object_as_nearest_objnum(eval_nearest_objnum *eno)
|
|
{
|
|
ai_info *aip;
|
|
ship_subsys *attacking_subsystem;
|
|
ship *shipp = &Ships[eno->trial_objp->instance];
|
|
|
|
aip = &Ai_info[Ships[Objects[eno->objnum].instance].ai_index];
|
|
|
|
attacking_subsystem = aip->targeted_subsys;
|
|
|
|
if ((attacking_subsystem != NULL) || !(eno->trial_objp->flags[Object::Object_Flags::Protected])) {
|
|
if ( OBJ_INDEX(eno->trial_objp) != eno->objnum ) {
|
|
#ifndef NDEBUG
|
|
if (!Player_attacking_enabled && (eno->trial_objp == Player_obj))
|
|
return;
|
|
#endif
|
|
// If only supposed to attack ship in a specific wing, don't attack other ships.
|
|
if ((eno->enemy_wing != -1) && (shipp->wingnum != eno->enemy_wing))
|
|
return;
|
|
|
|
// If only supposed to attack ships of a certain ship class, don't attack other ships.
|
|
if ((eno->enemy_ship_info_index >= 0) && (shipp->ship_info_index != eno->enemy_ship_info_index))
|
|
return;
|
|
|
|
// If only supposed to attack ships of a certain ship type, don't attack other ships.
|
|
if ((eno->enemy_class_type >= 0) && (Ship_info[shipp->ship_info_index].class_type != eno->enemy_class_type))
|
|
return;
|
|
|
|
// Don't keep firing at a ship that is in its death throes.
|
|
if (shipp->flags[Ship::Ship_Flags::Dying])
|
|
return;
|
|
|
|
if (is_ignore_object(aip, OBJ_INDEX(eno->trial_objp)))
|
|
return;
|
|
|
|
if (eno->trial_objp->flags[Object::Object_Flags::Protected])
|
|
return;
|
|
|
|
if (shipp->is_arriving())
|
|
return;
|
|
|
|
ship_info *sip = &Ship_info[shipp->ship_info_index];
|
|
|
|
if (sip->flags[Ship::Info_Flags::No_ship_type] || sip->flags[Ship::Info_Flags::Navbuoy])
|
|
return;
|
|
|
|
if (iff_matches_mask(shipp->team, eno->enemy_team_mask)) {
|
|
float dist;
|
|
int num_attacking;
|
|
|
|
// Allow targeting of stealth in nebula by his firing at me
|
|
// This is done for a specific ship, not generally.
|
|
if ( !eno->check_danger_weapon_objnum ) {
|
|
// check if can be targeted if inside nebula
|
|
if ( !object_is_targetable(eno->trial_objp, &Ships[Objects[eno->objnum].instance]) ) {
|
|
// check if stealth ship is visible, but not "targetable"
|
|
if ( !((shipp->flags[Ship::Ship_Flags::Stealth]) && ai_is_stealth_visible(&Objects[eno->objnum], eno->trial_objp)) ) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// if objnum is BIG or HUGE, find distance to bbox
|
|
if (sip->is_big_or_huge()) {
|
|
vec3d box_pt;
|
|
// check if inside bbox
|
|
int inside = get_nearest_bbox_point(eno->trial_objp, &Objects[eno->objnum].pos, &box_pt);
|
|
if (inside) {
|
|
dist = 10.0f;
|
|
// on the box
|
|
} else {
|
|
dist = vm_vec_dist_quick(&Objects[eno->objnum].pos, &box_pt);
|
|
}
|
|
} else {
|
|
dist = vm_vec_dist_quick(&Objects[eno->objnum].pos, &eno->trial_objp->pos);
|
|
}
|
|
|
|
// Make it more likely that fighters (or bombers) will be picked as an enemy by scaling up distance for other types.
|
|
if (Ship_info[shipp->ship_info_index].is_fighter_bomber()) {
|
|
dist = dist * 0.5f;
|
|
}
|
|
|
|
num_attacking = num_enemies_attacking(OBJ_INDEX(eno->trial_objp));
|
|
|
|
if (!sip->is_big_or_huge() && num_attacking < eno->max_attackers) {
|
|
dist *= (float)(num_attacking + 2) / 2.0f; // prevents lots of ships from attacking same target
|
|
}
|
|
|
|
if ((sip->is_big_or_huge()) || (num_attacking < eno->max_attackers)) {
|
|
|
|
if (eno->trial_objp->flags[Object::Object_Flags::Player_ship]){
|
|
dist *= 1.0f + (NUM_SKILL_LEVELS - Game_skill_level - 1)/NUM_SKILL_LEVELS; // Favor attacking non-players based on skill level.
|
|
}
|
|
|
|
if (dist < eno->nearest_dist) {
|
|
eno->nearest_dist = dist;
|
|
eno->nearest_objnum = OBJ_INDEX(eno->trial_objp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Given an object and an enemy team, return the index of the nearest enemy object.
|
|
* Unless aip->targeted_subsys != NULL, don't allow to attack objects with OF_PROTECTED bit set.
|
|
*
|
|
* @param objnum Object number
|
|
* @param enemy_team_mask Mask to apply to enemy team
|
|
* @param enemy_wing Enemy wing chosen
|
|
* @param range Ship must be within range "range".
|
|
* @param max_attackers Don't attack a ship that already has at least max_attackers attacking it.
|
|
* @param ship_info_index If >=0, the enemy object must be of the specified ship class
|
|
* @param class_type If >=0, the enemy object must be of the specified ship type
|
|
*/
|
|
int get_nearest_objnum(int objnum, int enemy_team_mask, int enemy_wing, float range, int max_attackers, int ship_info_index, int class_type)
|
|
{
|
|
object *danger_weapon_objp;
|
|
ai_info *aip;
|
|
ship_obj *so;
|
|
|
|
// initialize eno struct
|
|
eval_nearest_objnum eno;
|
|
eno.enemy_team_mask = enemy_team_mask;
|
|
eno.enemy_ship_info_index = ship_info_index;
|
|
eno.enemy_class_type = class_type;
|
|
eno.enemy_wing = enemy_wing;
|
|
eno.max_attackers = max_attackers;
|
|
eno.objnum = objnum;
|
|
eno.range = range;
|
|
eno.nearest_dist = range;
|
|
eno.nearest_objnum = -1;
|
|
eno.check_danger_weapon_objnum = 0;
|
|
|
|
// go through the list of all ships and evaluate as potential targets
|
|
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;
|
|
|
|
eno.trial_objp = &Objects[so->objnum];
|
|
evaluate_object_as_nearest_objnum(&eno);
|
|
}
|
|
|
|
// check if danger_weapon_objnum has will show a stealth ship
|
|
aip = &Ai_info[Ships[Objects[objnum].instance].ai_index];
|
|
if (aip->danger_weapon_objnum >= 0) {
|
|
danger_weapon_objp = &Objects[aip->danger_weapon_objnum];
|
|
// validate weapon
|
|
if (danger_weapon_objp->signature == aip->danger_weapon_signature) {
|
|
Assert(danger_weapon_objp->type == OBJ_WEAPON);
|
|
// check if parent is a ship
|
|
if (danger_weapon_objp->parent >= 0) {
|
|
if ( is_object_stealth_ship(&Objects[danger_weapon_objp->parent]) ) {
|
|
// check if stealthy
|
|
if ( ai_is_stealth_visible(&Objects[objnum], &Objects[danger_weapon_objp->parent]) != STEALTH_FULLY_TARGETABLE ) {
|
|
// check if weapon is laser
|
|
if (Weapon_info[Weapons[danger_weapon_objp->instance].weapon_info_index].subtype == WP_LASER) {
|
|
// check stealth ship by its laser fire
|
|
eno.check_danger_weapon_objnum = 1;
|
|
eno.trial_objp = &Objects[danger_weapon_objp->parent];
|
|
evaluate_object_as_nearest_objnum(&eno);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If only looking for target in certain wing and couldn't find anything in
|
|
// that wing, look for any object.
|
|
if ((eno.nearest_objnum == -1) && (enemy_wing != -1)) {
|
|
return get_nearest_objnum(objnum, enemy_team_mask, -1, range, max_attackers, ship_info_index, class_type);
|
|
}
|
|
|
|
return eno.nearest_objnum;
|
|
}
|
|
|
|
/**
|
|
* Given an object and an enemy team, return the index of the nearest enemy object.
|
|
*
|
|
* Unlike find_enemy or find_nearest_objnum, this doesn't care about things like the protected flag or number of enemies attacking.
|
|
* It is used to find the nearest enemy to determine things like whether to rearm.
|
|
*/
|
|
int find_nearby_threat(int objnum, int enemy_team_mask, float range, int *count)
|
|
{
|
|
int nearest_objnum;
|
|
float nearest_dist;
|
|
object *objp;
|
|
ship_obj *so;
|
|
|
|
nearest_objnum = -1;
|
|
nearest_dist = range;
|
|
|
|
*count = 0;
|
|
|
|
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
|
|
objp = &Objects[so->objnum];
|
|
if (objp->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
if ( OBJ_INDEX(objp) != objnum ) {
|
|
if (Ships[objp->instance].flags[Ship::Ship_Flags::Dying])
|
|
continue;
|
|
|
|
if (Ship_info[Ships[objp->instance].ship_info_index].flags[Ship::Info_Flags::No_ship_type] || Ship_info[Ships[objp->instance].ship_info_index].flags[Ship::Info_Flags::Navbuoy])
|
|
continue;
|
|
|
|
if (iff_matches_mask(Ships[objp->instance].team, enemy_team_mask)) {
|
|
float dist;
|
|
|
|
dist = vm_vec_dist_quick(&Objects[objnum].pos, &objp->pos) - objp->radius*0.75f;
|
|
|
|
if (dist < range) {
|
|
(*count)++;
|
|
|
|
if (dist < nearest_dist) {
|
|
nearest_dist = dist;
|
|
nearest_objnum = OBJ_INDEX(objp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nearest_objnum;
|
|
}
|
|
|
|
/**
|
|
* Number of live turrets with target_objnum
|
|
*/
|
|
int num_turrets_attacking(const object *turret_parent, int target_objnum)
|
|
{
|
|
ship_subsys *ss;
|
|
ship *shipp;
|
|
int count = 0;
|
|
shipp = &Ships[turret_parent->instance];
|
|
|
|
Assert(turret_parent->type == OBJ_SHIP);
|
|
|
|
for (ss=GET_FIRST(&shipp->subsys_list); ss!=END_OF_LIST(&shipp->subsys_list); ss=GET_NEXT(ss)) {
|
|
// check if subsys is alive
|
|
if (ss->current_hits <= 0.0f) {
|
|
continue;
|
|
}
|
|
|
|
// check if it's a turret
|
|
if (ss->system_info->type != SUBSYSTEM_TURRET) {
|
|
continue;
|
|
}
|
|
|
|
// if the turret is locked
|
|
if(ss->weapons.flags[Ship::Weapon_Flags::Turret_Lock]){
|
|
continue;
|
|
}
|
|
|
|
// check if turret is targeting target_objnum
|
|
if (ss->turret_enemy_objnum == target_objnum) {
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Return timestamp until a ship can find an enemy.
|
|
*/
|
|
int get_enemy_timestamp()
|
|
{
|
|
return (NUM_SKILL_LEVELS - Game_skill_level) * Random::next(500, 999);
|
|
}
|
|
|
|
/**
|
|
* Return objnum if enemy found, else return -1;
|
|
*
|
|
* @param objnum Object number
|
|
* @param range Range within which to look
|
|
* @param max_attackers Don't attack a ship that already has at least max_attackers attacking it.
|
|
* @param ship_info_index If specified, restrict the search to enemies with this ship class
|
|
* @param class_type If specified, restrict the search to enemies with this ship type
|
|
*/
|
|
int find_enemy(int objnum, float range, int max_attackers, int ship_info_index, int class_type)
|
|
{
|
|
int enemy_team_mask;
|
|
|
|
if (objnum < 0)
|
|
return -1;
|
|
|
|
enemy_team_mask = iff_get_attackee_mask(obj_team(&Objects[objnum]));
|
|
|
|
// if target_objnum != -1, use that as goal.
|
|
ai_info* aip = &Ai_info[Ships[Objects[objnum].instance].ai_index];
|
|
if (timestamp_elapsed(aip->choose_enemy_timestamp)) {
|
|
aip->choose_enemy_timestamp = timestamp(get_enemy_timestamp());
|
|
|
|
if (aip->target_objnum != -1) {
|
|
int target_objnum = aip->target_objnum;
|
|
|
|
// DKA don't undo object as target in nebula missions.
|
|
// This could cause attack on ship on fringe on nebula to stop if attackee moves out of nebula range. (BAD)
|
|
if (Objects[target_objnum].signature == aip->target_signature) {
|
|
ship* target_shipp = (Objects[target_objnum].type == OBJ_SHIP) ? &Ships[Objects[target_objnum].instance] : nullptr;
|
|
|
|
if (target_shipp && iff_matches_mask(target_shipp->team, enemy_team_mask)) {
|
|
if (ship_info_index < 0 || ship_info_index == target_shipp->ship_info_index) {
|
|
if (class_type < 0 || (target_shipp->ship_info_index >= 0 &&
|
|
class_type == Ship_info[target_shipp->ship_info_index].class_type)) {
|
|
if (!(Objects[target_objnum].flags[Object::Object_Flags::Protected])) {
|
|
return target_objnum;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
aip->target_objnum = -1;
|
|
aip->target_signature = -1;
|
|
}
|
|
}
|
|
|
|
return get_nearest_objnum(objnum, enemy_team_mask, aip->enemy_wing, range, max_attackers, ship_info_index, class_type);
|
|
} else {
|
|
aip->target_objnum = -1;
|
|
aip->target_signature = -1;
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If issued an order to a ship that's awaiting repair, abort that process.
|
|
* However, do not abort process for an object that is currently being repaired -- let it finish.
|
|
*/
|
|
void ai_set_goal_abort_support_call(object *objp, ai_info *aip)
|
|
{
|
|
if (aip->ai_flags[AI::AI_Flags::Awaiting_repair]) {
|
|
object *repair_obj;
|
|
|
|
if (aip->support_ship_objnum == -1) {
|
|
repair_obj = nullptr;
|
|
} else {
|
|
Assertion(aip->support_ship_objnum >= 0, "%s has a nonsense support_ship_objnum of %d, please report!", Ships[aip->shipnum].ship_name, aip->support_ship_objnum);
|
|
repair_obj = &Objects[aip->support_ship_objnum];
|
|
}
|
|
ai_do_objects_repairing_stuff( objp, repair_obj, REPAIR_INFO_ABORT );
|
|
}
|
|
aip->next_rearm_request_timestamp = timestamp(NEXT_REARM_TIMESTAMP); // Might request again after 30 seconds.
|
|
}
|
|
|
|
void force_avoid_player_check(object *objp, ai_info *aip)
|
|
{
|
|
if (Ships[objp->instance].team == Player_ship->team){
|
|
aip->avoid_check_timestamp = timestamp(0); // Force a check for collision next frame.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set *attacked as object to attack for object *attacker
|
|
*
|
|
* If attacked == NULL, then attack any enemy object.
|
|
* Attack point *rel_pos on object. This is for supporting attacking subsystems.
|
|
*/
|
|
void ai_attack_object(object* attacker, object* attacked, int ship_info_index, int class_type)
|
|
{
|
|
int temp;
|
|
ai_info* aip;
|
|
|
|
Assert(attacker != nullptr);
|
|
Assert(attacker->instance >= 0);
|
|
Assert(attacker->type == OBJ_SHIP);
|
|
Assert(Ships[attacker->instance].ai_index != -1);
|
|
// Bogus! Who tried to get me to attack myself!
|
|
if (attacker == attacked) {
|
|
Warning(LOCATION, "%s was told to attack itself! This may be an error in the mission file. If that checks out, please report to a coder!", Ships[attacker->instance].ship_name);
|
|
return;
|
|
}
|
|
|
|
if (attacker == nullptr || attacker->instance == -1) {
|
|
return;
|
|
}
|
|
|
|
aip = &Ai_info[Ships[attacker->instance].ai_index];
|
|
force_avoid_player_check(attacker, aip);
|
|
|
|
aip->ok_to_target_timestamp = timestamp(0); // Guarantee we can target.
|
|
|
|
// Only set to chase if a fighter or bomber, otherwise just return.
|
|
if (!(Ship_info[Ships[attacker->instance].ship_info_index].is_small_ship()) && (attacked != NULL)) {
|
|
nprintf(("AI", "AI ship %s is large ship ordered to attack %s\n", Ships[attacker->instance].ship_name, Ships[attacked->instance].ship_name));
|
|
}
|
|
|
|
// This is how "engage enemy" gets processed
|
|
if (attacked == nullptr) {
|
|
aip->choose_enemy_timestamp = timestamp(0);
|
|
// nebula safe
|
|
set_target_objnum(aip, find_enemy(OBJ_INDEX(attacker), 99999.9f, 4, ship_info_index, class_type));
|
|
} else {
|
|
// check if we can see attacked in nebula
|
|
if (aip->target_objnum != OBJ_INDEX(attacked)) {
|
|
aip->aspect_locked_time = 0.0f;
|
|
}
|
|
set_target_objnum(aip, OBJ_INDEX(attacked));
|
|
}
|
|
|
|
// don't abort a support call if you're disabled!
|
|
if (!ship_subsystems_blown(&Ships[attacker->instance], SUBSYSTEM_ENGINE))
|
|
ai_set_goal_abort_support_call(attacker, aip);
|
|
|
|
aip->ok_to_target_timestamp = timestamp(DELAY_TARGET_TIME); // No dynamic targeting for 7 seconds.
|
|
|
|
// Goober5000
|
|
temp = find_ignore_new_object_index(aip, aip->target_objnum);
|
|
if (temp >= 0)
|
|
{
|
|
aip->ignore_new_objnums[temp] = UNUSED_OBJNUM;
|
|
}
|
|
else if (is_ignore_object(aip, aip->target_objnum, 1))
|
|
{
|
|
aip->ignore_objnum = UNUSED_OBJNUM;
|
|
}
|
|
|
|
aip->mode = AIM_CHASE;
|
|
aip->submode = SM_ATTACK; // AL 12-15-97: need to set submode? I got an assert() where submode was bogus
|
|
aip->submode_start_time = Missiontime; // for AIM_CHASE... it may have been not set correctly here
|
|
|
|
set_targeted_subsys(aip, nullptr , -1);
|
|
if (aip->target_objnum != -1) {
|
|
Objects[aip->target_objnum].flags.remove(Object::Object_Flags::Protected); // If ship had been protected, unprotect it.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set *attacked as object to attack for object *attacker
|
|
* Attack point *rel_pos on object. This is for supporting attacking subsystems.
|
|
*/
|
|
void ai_attack_wing(object *attacker, int wingnum)
|
|
{
|
|
ai_info *aip;
|
|
|
|
Assert(attacker != NULL);
|
|
Assert(attacker->instance != -1);
|
|
Assert(Ships[attacker->instance].ai_index != -1);
|
|
|
|
aip = &Ai_info[Ships[attacker->instance].ai_index];
|
|
|
|
aip->enemy_wing = wingnum;
|
|
aip->mode = AIM_CHASE;
|
|
aip->submode = SM_ATTACK; // AL 12-15-97: need to set submode? I got an assert() where submode was bogus
|
|
aip->submode_start_time = Missiontime; // for AIM_CHASE... it may have been not set correctly here
|
|
|
|
aip->ok_to_target_timestamp = timestamp(0); // Guarantee we can target.
|
|
|
|
int count = Wings[wingnum].current_count;
|
|
if (count > 0) {
|
|
int index;
|
|
|
|
index = (int) (frand() * count);
|
|
|
|
if (index >= count)
|
|
index = 0;
|
|
|
|
set_target_objnum(aip, Ships[Wings[wingnum].ship_index[index]].objnum);
|
|
|
|
ai_set_goal_abort_support_call(attacker, aip);
|
|
aip->ok_to_target_timestamp = timestamp(DELAY_TARGET_TIME); // No dynamic targeting for 7 seconds.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set *evaded as object for *evader to evade.
|
|
*/
|
|
void ai_evade_object(object *evader, object *evaded)
|
|
{
|
|
ai_info *aip;
|
|
|
|
Assert(evader != NULL);
|
|
Assert(evaded != NULL);
|
|
Assert(evader->instance != -1);
|
|
Assert(Ships[evader->instance].ai_index != -1);
|
|
|
|
Assertion(evaded != evader, "Ship %s is trying to evade itself. Please report to the SCP!", Ships[evader->instance].ship_name); // Bogus! Who tried to get me to evade myself! Trace out and fix!
|
|
if (evaded == evader) {
|
|
return;
|
|
}
|
|
|
|
aip = &Ai_info[Ships[evader->instance].ai_index];
|
|
|
|
set_target_objnum(aip, OBJ_INDEX(evaded));
|
|
aip->mode = AIM_EVADE;
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns total number of ignored objects.
|
|
*
|
|
* @param aip AI info
|
|
* @param force Means we forget the oldest object
|
|
*/
|
|
int compact_ignore_new_objects(ai_info *aip, int force = 0)
|
|
{
|
|
if (force)
|
|
aip->ignore_new_objnums[0] = UNUSED_OBJNUM;
|
|
|
|
for (int current_index = 0; current_index < MAX_IGNORE_NEW_OBJECTS; current_index++)
|
|
{
|
|
int next_occupied_index = -1;
|
|
|
|
// skip occupied slots
|
|
if (aip->ignore_new_objnums[current_index] != UNUSED_OBJNUM)
|
|
{
|
|
// prune invalid objects
|
|
if (Objects[aip->ignore_new_objnums[current_index]].signature != aip->ignore_new_signatures[current_index])
|
|
aip->ignore_new_objnums[current_index] = UNUSED_OBJNUM;
|
|
else
|
|
continue;
|
|
}
|
|
|
|
// find an index to move downward
|
|
for (int i = current_index + 1; i < MAX_IGNORE_NEW_OBJECTS; i++)
|
|
{
|
|
// skip empty slots
|
|
if (aip->ignore_new_objnums[i] == UNUSED_OBJNUM)
|
|
continue;
|
|
|
|
// found one
|
|
next_occupied_index = i;
|
|
break;
|
|
}
|
|
|
|
// are all higher slots empty?
|
|
if (next_occupied_index < 0)
|
|
return current_index;
|
|
|
|
// move the occupied slot down to this one
|
|
aip->ignore_new_objnums[current_index] = aip->ignore_new_objnums[next_occupied_index];
|
|
aip->ignore_new_signatures[current_index] = aip->ignore_new_signatures[next_occupied_index];
|
|
|
|
// empty the occupied slot
|
|
aip->ignore_new_objnums[next_occupied_index] = UNUSED_OBJNUM;
|
|
aip->ignore_new_signatures[next_occupied_index] = -1;
|
|
}
|
|
|
|
// all slots are occupied
|
|
return MAX_IGNORE_NEW_OBJECTS;
|
|
}
|
|
|
|
/**
|
|
* Ignore some object without changing mode.
|
|
*/
|
|
void ai_ignore_object(object *ignorer, object *ignored, int ignore_new)
|
|
{
|
|
ai_info *aip;
|
|
|
|
Assert(ignorer != NULL);
|
|
Assert(ignored != NULL);
|
|
Assert(ignorer->instance != -1);
|
|
Assert(Ships[ignorer->instance].ai_index != -1);
|
|
Assert(ignorer != ignored);
|
|
|
|
aip = &Ai_info[Ships[ignorer->instance].ai_index];
|
|
|
|
// Goober5000 - new behavior
|
|
if (ignore_new)
|
|
{
|
|
int num_objects;
|
|
|
|
// compact the array
|
|
num_objects = compact_ignore_new_objects(aip);
|
|
|
|
// make sure we're not adding a duplicate
|
|
if (find_ignore_new_object_index(aip, OBJ_INDEX(ignored)) >= 0)
|
|
return;
|
|
|
|
// if we can't add a new one; "forget" one
|
|
if (num_objects >= MAX_IGNORE_NEW_OBJECTS)
|
|
num_objects = compact_ignore_new_objects(aip, 1);
|
|
|
|
// add it
|
|
aip->ignore_new_objnums[num_objects] = OBJ_INDEX(ignored);
|
|
aip->ignore_new_signatures[num_objects] = ignored->signature;
|
|
}
|
|
// retail behavior
|
|
else
|
|
{
|
|
aip->ignore_objnum = OBJ_INDEX(ignored);
|
|
aip->ignore_signature = ignored->signature;
|
|
aip->ai_flags.remove(AI::AI_Flags::Temporary_ignore);
|
|
ignored->flags.set(Object::Object_Flags::Protected); // set protected bit of ignored ship.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ignore some object without changing mode.
|
|
*/
|
|
void ai_ignore_wing(object *ignorer, int wingnum)
|
|
{
|
|
ai_info *aip;
|
|
|
|
Assert(ignorer != NULL);
|
|
Assert(ignorer->instance != -1);
|
|
Assert(Ships[ignorer->instance].ai_index != -1);
|
|
Assert((wingnum >= 0) && (wingnum < MAX_WINGS));
|
|
|
|
aip = &Ai_info[Ships[ignorer->instance].ai_index];
|
|
|
|
aip->ignore_objnum = -(wingnum +1);
|
|
aip->ai_flags.remove(AI::AI_Flags::Temporary_ignore);
|
|
}
|
|
|
|
|
|
/**
|
|
* Add a path point in the global buffer Path_points.
|
|
*
|
|
* If modify_index == -1, then create a new point.
|
|
* If a new point is created (ie, modify_index == -1), then Ppfp is updated.
|
|
*
|
|
* @param pos Position in vector space
|
|
* @param path_num Path numbers
|
|
* @param path_index Index into path
|
|
* @param modify_index Index in Path_points at which to store path point.
|
|
*/
|
|
void add_path_point(vec3d *pos, int path_num, int path_index, int modify_index)
|
|
{
|
|
pnode *pnp;
|
|
|
|
if (modify_index == -1) {
|
|
Assert(Ppfp-Path_points < MAX_PATH_POINTS-1);
|
|
pnp = Ppfp;
|
|
Ppfp++;
|
|
} else {
|
|
Assert((modify_index >= 0) && (modify_index < MAX_PATH_POINTS-1));
|
|
pnp = &Path_points[modify_index];
|
|
}
|
|
|
|
pnp->pos = *pos;
|
|
pnp->path_num = path_num;
|
|
pnp->path_index = path_index;
|
|
}
|
|
|
|
/**
|
|
* Given two points on a sphere, the center of the sphere and the radius, return a
|
|
* point on the vector through the midpoint of the chord on the sphere.
|
|
*/
|
|
void bisect_chord(vec3d *p0, vec3d *p1, vec3d *centerp, float radius)
|
|
{
|
|
vec3d tvec;
|
|
vec3d new_pnt;
|
|
|
|
vm_vec_add(&tvec, p0, p1);
|
|
vm_vec_sub2(&tvec, centerp);
|
|
vm_vec_sub2(&tvec, centerp);
|
|
if (vm_vec_mag_quick(&tvec) < 0.1f) {
|
|
vm_vec_sub(&tvec, p0, p1);
|
|
if (fl_abs(tvec.xyz.x) <= fl_abs(tvec.xyz.z)){
|
|
tvec.xyz.x = -tvec.xyz.z;
|
|
} else {
|
|
tvec.xyz.y = -tvec.xyz.x;
|
|
}
|
|
}
|
|
|
|
vm_vec_normalize(&tvec);
|
|
vm_vec_scale(&tvec, radius);
|
|
vm_vec_add(&new_pnt, centerp, &tvec);
|
|
|
|
add_path_point(&new_pnt, -1, -1, -1);
|
|
}
|
|
|
|
/**
|
|
* Create a path from the current position to a goal position.
|
|
*
|
|
* The current position is in the current object and the goal position is in the goal object.
|
|
* It is ok to intersect the current object, but not the goal object.
|
|
* This function is useful for creating a path to an initial point near a large object.
|
|
*
|
|
* @param curpos Current position in vector space
|
|
* @param goalpos Goal position in vector space
|
|
* @param curobjp Current object pointer
|
|
* @param goalobjp Goal object pointer
|
|
* @param subsys_path Optional param (default 0), indicates this is a path to a subsystem
|
|
*/
|
|
void create_path_to_point(vec3d *curpos, vec3d *goalpos, object *curobjp, object *goalobjp, int subsys_path)
|
|
{
|
|
// If can't cast vector to goalpos, then create an intermediate point.
|
|
if (pp_collide(curpos, goalpos, goalobjp, curobjp->radius)) {
|
|
vec3d tan1;
|
|
float radius;
|
|
|
|
// If this is a path to a subsystem, use SUBSYS_PATH_DIST as the radius for the object you are
|
|
// trying to avoid. This is needed since subsystem paths extend out to SUBSYS_PATH_DIST, and we
|
|
// want ships to reach their path destination without flying to points that sit on the radius of
|
|
// a small ship
|
|
radius = goalobjp->radius;
|
|
if (subsys_path) {
|
|
if ( SUBSYS_PATH_DIST > goalobjp->radius ) {
|
|
radius = SUBSYS_PATH_DIST;
|
|
}
|
|
}
|
|
|
|
// The intermediate point is at the intersection of:
|
|
// tangent to *goalobjp sphere at point *goalpos
|
|
// tangent to *goalobjp sphere through point *curpos in plane defined by *curpos, *goalpos, goalobjp->pos
|
|
// Note, there are two tangents through *curpos, unless *curpos is on the
|
|
// sphere. The tangent that causes the nearer intersection (to *goalpos) is chosen.
|
|
get_tangent_point(&tan1, curpos, &goalobjp->pos, goalpos, radius);
|
|
|
|
// If we can't reach tan1 from curpos, insert a new point.
|
|
if (pp_collide(&tan1, curpos, goalobjp, curobjp->radius))
|
|
bisect_chord(curpos, &tan1, &goalobjp->pos, radius);
|
|
|
|
add_path_point(&tan1, -1, -1, -1);
|
|
|
|
// If we can't reach goalpos from tan1, insert a new point.
|
|
if (pp_collide(goalpos, &tan1, goalobjp, curobjp->radius))
|
|
bisect_chord(goalpos, &tan1, &goalobjp->pos, radius);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Given an object and a model path, globalize the points on the model and copy into the global path list.
|
|
* If pnp != NULL, then modify, in place, the path points. This is used to create new globalized points when the base object has moved.
|
|
*
|
|
* @param objp Object pointer
|
|
* @param mp Model path
|
|
* @param dir Directory type
|
|
* @param count Count of items
|
|
* @param path_num Path number
|
|
* @param pnp Node on a path
|
|
* @param randomize_pnt Optional parameter (default value -1), add random vector in sphere to this path point
|
|
*/
|
|
void copy_xlate_model_path_points(object *objp, model_path *mp, int dir, int count, int path_num, pnode *pnp, int randomize_pnt)
|
|
{
|
|
int i;
|
|
vec3d v1;
|
|
int pp_index; // index in Path_points at which to store point, if this is a modify-in-place (pnp ! NULL)
|
|
int start_index, finish_index;
|
|
vec3d submodel_offset, local_vert;
|
|
bool moving_submodel;
|
|
|
|
// Initialize pp_index.
|
|
// If pnp == NULL, that means we're creating new points. If not NULL, then modify in place.
|
|
if (pnp == NULL)
|
|
pp_index = -1; // This tells add_path_point to create a new point.
|
|
else
|
|
pp_index = 0; // pp_index will get assigned to index in Path_points to reuse.
|
|
|
|
if (dir == 1) {
|
|
start_index = 0;
|
|
finish_index = MIN(count, mp->nverts);
|
|
} else {
|
|
Assert(dir == -1); // direction must be up by 1 or down by 1 and it's neither!
|
|
start_index = mp->nverts-1;
|
|
finish_index = MAX(-1, mp->nverts-1-count);
|
|
}
|
|
|
|
auto pmi = model_get_instance(Ships[objp->instance].model_instance_num);
|
|
auto pm = model_get(pmi->model_num);
|
|
|
|
// Goober5000 - check for moving submodels
|
|
if ((mp->parent_submodel >= 0) && ((pm->submodel[mp->parent_submodel].rotation_type >= 0) || (pm->submodel[mp->parent_submodel].translation_type >= 0)))
|
|
{
|
|
moving_submodel = true;
|
|
|
|
model_find_submodel_offset(&submodel_offset, pm, mp->parent_submodel);
|
|
}
|
|
else
|
|
{
|
|
moving_submodel = false;
|
|
}
|
|
|
|
int offset = 0;
|
|
for (i=start_index; i != finish_index; i += dir)
|
|
{
|
|
// Globalize the point.
|
|
// Goober5000 - handle moving submodels
|
|
if (moving_submodel)
|
|
{
|
|
// movement... find location of point like with docking code and spark generation
|
|
vm_vec_sub(&local_vert, &mp->verts[i].pos, &submodel_offset);
|
|
model_instance_local_to_global_point(&v1, &local_vert, pm, pmi, mp->parent_submodel, &objp->orient, &objp->pos);
|
|
}
|
|
else
|
|
{
|
|
// no movement... calculate as in original code
|
|
vm_vec_unrotate(&v1, &mp->verts[i].pos, &objp->orient);
|
|
vm_vec_add2(&v1, &objp->pos);
|
|
}
|
|
|
|
if ( randomize_pnt == i ) {
|
|
vec3d v_rand;
|
|
static_randvec(OBJ_INDEX(objp), &v_rand);
|
|
vm_vec_scale(&v_rand, 30.0f);
|
|
vm_vec_add2(&v1, &v_rand);
|
|
}
|
|
|
|
if (pp_index != -1)
|
|
pp_index = (int)(pnp-Path_points) + offset;
|
|
|
|
add_path_point(&v1, path_num, i, pp_index);
|
|
offset++;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* For pl_objp, create a path along path path_num into mobjp.
|
|
* The tricky part of this problem is creating the entry to the first point on the predefined path.
|
|
* The points on this entry path are based on the location of Pl_objp relative to the start of the path.
|
|
*
|
|
* @param pl_objp Player object
|
|
* @param mobjp Model object
|
|
* @param path_num Number of path
|
|
* @param subsys_path Optional param (default 0), indicating this is a path to a subsystem
|
|
*/
|
|
void create_model_path(object *pl_objp, object *mobjp, int path_num, int subsys_path)
|
|
{
|
|
ship *shipp = &Ships[pl_objp->instance];
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
|
|
ship_info *osip = &Ship_info[Ships[mobjp->instance].ship_info_index];
|
|
polymodel *pm = model_get(Ship_info[Ships[mobjp->instance].ship_info_index].model_num);
|
|
int num_points;
|
|
model_path *mp;
|
|
pnode *ppfp_start = Ppfp;
|
|
vec3d gp0;
|
|
|
|
Assert(path_num >= 0);
|
|
|
|
// Do garbage collection if necessary.
|
|
if (Ppfp-Path_points + 64 > MAX_PATH_POINTS) {
|
|
garbage_collect_path_points();
|
|
ppfp_start = Ppfp;
|
|
}
|
|
|
|
aip->path_start = (int)(Ppfp - Path_points);
|
|
Assert(path_num < pm->n_paths);
|
|
|
|
mp = &pm->paths[path_num];
|
|
num_points = mp->nverts;
|
|
|
|
Assert(Ppfp-Path_points + num_points + 4 < MAX_PATH_POINTS);
|
|
|
|
vm_vec_unrotate(&gp0, &mp->verts[0].pos, &mobjp->orient);
|
|
vm_vec_add2(&gp0, &mobjp->pos);
|
|
|
|
if (pp_collide(&pl_objp->pos, &gp0, mobjp, pl_objp->radius)) {
|
|
vec3d perim_point1;
|
|
vec3d perim_point2;
|
|
|
|
perim_point2 = pl_objp->pos;
|
|
|
|
// If object that wants to dock is inside bounding sphere of object it wants to dock with, make it fly out.
|
|
// Assume it can fly "straight" out to the bounding sphere.
|
|
if (vm_vec_dist_quick(&pl_objp->pos, &mobjp->pos) < mobjp->radius) {
|
|
project_point_to_perimeter(&perim_point2, &mobjp->pos, mobjp->radius, &pl_objp->pos);
|
|
add_path_point(&perim_point2, path_num, -1, -1);
|
|
}
|
|
|
|
// If last point on pre-defined path is inside bounding sphere, create a new point on the surface of the sphere.
|
|
if (vm_vec_dist_quick(&mobjp->pos, &gp0) < mobjp->radius) {
|
|
project_point_to_perimeter(&perim_point1, &mobjp->pos, mobjp->radius, &gp0);
|
|
create_path_to_point(&perim_point2, &perim_point1, pl_objp, mobjp, subsys_path);
|
|
add_path_point(&perim_point1, path_num, -1, -1);
|
|
} else { // The predefined path extends outside the sphere. Create path to that point.
|
|
create_path_to_point(&perim_point2, &gp0, pl_objp, mobjp, subsys_path);
|
|
}
|
|
}
|
|
|
|
// AL 12-31-97: If following a subsystem path, add random vector to second last path point
|
|
if ( subsys_path ) {
|
|
copy_xlate_model_path_points(mobjp, mp, 1, mp->nverts, path_num, NULL, mp->nverts-2);
|
|
} else {
|
|
copy_xlate_model_path_points(mobjp, mp, 1, mp->nverts, path_num, NULL);
|
|
}
|
|
|
|
aip->path_cur = aip->path_start;
|
|
aip->path_dir = PD_FORWARD;
|
|
aip->path_objnum = OBJ_INDEX(mobjp);
|
|
aip->mp_index = path_num;
|
|
aip->path_length = (int)(Ppfp - ppfp_start);
|
|
aip->path_next_check_time = timestamp(1);
|
|
|
|
aip->path_goal_obj_hash = create_object_hash(&Objects[aip->path_objnum]);
|
|
|
|
aip->path_next_create_time = timestamp(1000); // OK to try to create one second later
|
|
aip->path_create_pos = pl_objp->pos;
|
|
aip->path_create_orient = pl_objp->orient;
|
|
|
|
//Get path departure orientation from ships.tbl if it exists, otherwise zero it
|
|
SCP_string pathName(mp->name);
|
|
if (osip->pathMetadata.find(pathName) != osip->pathMetadata.end() && !IS_VEC_NULL(&osip->pathMetadata[pathName].departure_rvec))
|
|
vm_vec_copy_normalize(&aip->path_depart_rvec, &osip->pathMetadata[pathName].departure_rvec);
|
|
else if (aip->ai_profile_flags[AI::Profile_Flags::Fighterbay_departures_use_carrier_orient])
|
|
aip->path_depart_rvec = vmd_x_vector; // this will be later rotated into the carrier's frame of reference
|
|
else
|
|
vm_vec_zero(&aip->path_depart_rvec);
|
|
|
|
aip->ai_flags.remove(AI::AI_Flags::Use_exit_path); // ensure this flag is cleared
|
|
}
|
|
|
|
/**
|
|
* For pl_objp, create a path along path path_num into mobjp.
|
|
*
|
|
* The tricky part of this problem is creating the entry to the first point on the predefined path.
|
|
* The points on this entry path are based on the location of pl_objp relative to the start of the path.
|
|
*/
|
|
void create_model_exit_path(object *pl_objp, object *mobjp, int path_num, int count)
|
|
{
|
|
ship *shipp = &Ships[pl_objp->instance];
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
|
|
polymodel *pm = model_get(Ship_info[Ships[mobjp->instance].ship_info_index].model_num);
|
|
int num_points;
|
|
model_path *mp;
|
|
pnode *ppfp_start = Ppfp;
|
|
|
|
Assert(path_num >= 0);
|
|
|
|
// Do garbage collection if necessary.
|
|
if (Ppfp-Path_points + 64 > MAX_PATH_POINTS) {
|
|
garbage_collect_path_points();
|
|
ppfp_start = Ppfp;
|
|
}
|
|
|
|
aip->path_start = (int)(Ppfp - Path_points);
|
|
Assert(path_num < pm->n_paths);
|
|
|
|
mp = &pm->paths[path_num];
|
|
num_points = mp->nverts;
|
|
|
|
Assert(Ppfp-Path_points + num_points + 4 < MAX_PATH_POINTS);
|
|
|
|
copy_xlate_model_path_points(mobjp, mp, -1, count, path_num, NULL);
|
|
|
|
aip->path_cur = aip->path_start;
|
|
aip->path_dir = PD_FORWARD;
|
|
aip->path_objnum = OBJ_INDEX(mobjp);
|
|
aip->mp_index = path_num;
|
|
aip->path_length = (int)(Ppfp - ppfp_start);
|
|
aip->path_next_check_time = timestamp(1);
|
|
|
|
aip->ai_flags.set(AI::AI_Flags::Use_exit_path); // mark as exit path, referenced in maybe
|
|
}
|
|
|
|
/**
|
|
* Return true if the vector from curpos to goalpos intersects with any ship other than the ignore objects.
|
|
* Calls pp_collide()
|
|
*/
|
|
int pp_collide_any(vec3d *curpos, vec3d *goalpos, float radius, object *ignore_objp1, object *ignore_objp2, int big_only_flag)
|
|
{
|
|
ship_obj *so;
|
|
|
|
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
|
|
object *objp = &Objects[so->objnum];
|
|
if (objp->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
if (big_only_flag) {
|
|
if (!Ship_info[Ships[objp->instance].ship_info_index].is_big_or_huge())
|
|
continue;
|
|
}
|
|
|
|
if ((objp != ignore_objp1) && (objp != ignore_objp2)) {
|
|
if (pp_collide(curpos, goalpos, objp, radius))
|
|
return OBJ_INDEX(objp);
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
// Used to create docking paths and other pre-defined paths through ships.
|
|
// Creates a path in absolute space.
|
|
// Create a path into the object objnum.
|
|
//
|
|
// input:
|
|
// pl_objp: object that will use the path
|
|
// objnum: Object to find path to.
|
|
// path_num: model path index to use
|
|
// exit_flag: true means this is an exit path in the model
|
|
// subsys_path: optional param (default 0) that indicates this is a path to a subsystem
|
|
// Exit:
|
|
// ai_info struct in Pl_objp gets stuffed with information to enable Pl_objp to fly the path.
|
|
void ai_find_path(object *pl_objp, int objnum, int path_num, int exit_flag, int subsys_path)
|
|
{
|
|
ai_info *aip = &Ai_info[Ships[pl_objp->instance].ai_index];
|
|
|
|
Assert(path_num >= 0);
|
|
|
|
// This is test code, find an object with paths.
|
|
if (objnum != -1) {
|
|
object *objp = &Objects[objnum];
|
|
|
|
if (objp->type == OBJ_SHIP) {
|
|
polymodel *pm;
|
|
|
|
ship *shipp = &Ships[objp->instance];
|
|
pm = model_get(Ship_info[shipp->ship_info_index].model_num);
|
|
if(path_num >= pm->n_paths)
|
|
Error(LOCATION,"ai_find_path tring to find a path (%d) that doesn't exist, on ship %s", path_num, shipp->ship_name);
|
|
|
|
aip->goal_objnum = objnum;
|
|
aip->goal_signature = objp->signature;
|
|
if (exit_flag)
|
|
create_model_exit_path(pl_objp, objp, path_num);
|
|
else
|
|
create_model_path(pl_objp, objp, path_num, subsys_path);
|
|
return;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
extern int vector_object_collision(vec3d *start_pos, vec3d *end_pos, object *objp, float radius_scale);
|
|
|
|
// Maybe make *objp avoid a player object.
|
|
// For now, 4/6/98, only check Player_obj.
|
|
// If player collision would occur, set AIF_AVOIDING_SMALL_SHIP bit in ai_flags.
|
|
// Set aip->avoid_goal_point
|
|
int maybe_avoid_player(object *objp, vec3d *goal_pos)
|
|
{
|
|
ai_info *aip;
|
|
vec3d cur_pos, new_goal_pos;
|
|
object *player_objp;
|
|
vec3d n_vec_to_goal, n_vec_to_player;
|
|
|
|
aip = &Ai_info[Ships[objp->instance].ai_index];
|
|
|
|
if (!timestamp_elapsed(aip->avoid_check_timestamp))
|
|
return 0;
|
|
|
|
player_objp = Player_obj;
|
|
|
|
float speed_time;
|
|
|
|
// How far two ships could be apart and still collide within one second.
|
|
speed_time = player_objp->phys_info.speed + objp->phys_info.speed;
|
|
|
|
float obj_obj_dist;
|
|
|
|
obj_obj_dist = vm_vec_dist_quick(&player_objp->pos, &objp->pos);
|
|
|
|
if (obj_obj_dist > speed_time*2.0f)
|
|
return 0;
|
|
|
|
cur_pos = objp->pos;
|
|
|
|
new_goal_pos = *goal_pos;
|
|
|
|
float dist = vm_vec_normalized_dir(&n_vec_to_goal, goal_pos, &objp->pos);
|
|
vm_vec_normalized_dir(&n_vec_to_player, &player_objp->pos, &objp->pos);
|
|
|
|
if (dist > speed_time*2.0f) {
|
|
vm_vec_scale_add(&new_goal_pos, &objp->pos, &n_vec_to_goal, 200.0f);
|
|
}
|
|
|
|
if (vector_object_collision(&objp->pos, &new_goal_pos, player_objp, 1.5f)) {
|
|
aip->ai_flags.set(AI::AI_Flags::Avoiding_small_ship);
|
|
|
|
vec3d avoid_vec;
|
|
|
|
vm_vec_sub(&avoid_vec, &n_vec_to_goal, &n_vec_to_player);
|
|
if (vm_vec_mag_quick(&avoid_vec) < 0.01f) {
|
|
vm_vec_copy_scale(&avoid_vec, &objp->orient.vec.rvec, frand()-0.5f);
|
|
vm_vec_scale_add2(&avoid_vec, &objp->orient.vec.uvec, frand()-0.5f);
|
|
vm_vec_normalize(&avoid_vec);
|
|
} else {
|
|
vec3d tvec1;
|
|
vm_vec_normalize(&avoid_vec);
|
|
vm_vec_cross(&tvec1, &n_vec_to_goal, &avoid_vec);
|
|
vm_vec_cross(&avoid_vec, &tvec1, &n_vec_to_player);
|
|
}
|
|
|
|
// Now, avoid_vec is a vector perpendicular to the vector to the player and the direction *objp
|
|
// should fly in to avoid the player while still approaching its goal.
|
|
vm_vec_scale_add(&aip->avoid_goal_point, &player_objp->pos, &avoid_vec, 400.0f);
|
|
|
|
aip->avoid_check_timestamp = timestamp(1000);
|
|
|
|
return 1;
|
|
} else {
|
|
aip->ai_flags.remove(AI::AI_Flags::Avoiding_small_ship);
|
|
aip->avoid_check_timestamp = timestamp((int) (obj_obj_dist/200.0f) + 500);
|
|
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Make object *still_objp enter AIM_STILL mode.
|
|
// Make it point at view_pos.
|
|
void ai_stay_still(object *still_objp, vec3d *view_pos)
|
|
{
|
|
ship *shipp;
|
|
ai_info *aip;
|
|
|
|
Assert(still_objp->type == OBJ_SHIP);
|
|
Assert((still_objp->instance >= 0) && (still_objp->instance < MAX_OBJECTS));
|
|
|
|
shipp = &Ships[still_objp->instance];
|
|
Assert((shipp->ai_index >= 0) && (shipp->ai_index < MAX_AI_INFO));
|
|
|
|
aip = &Ai_info[shipp->ai_index];
|
|
|
|
aip->mode = AIM_STILL;
|
|
|
|
// If view_pos not NULL, point at that point. Else, point at a point directly in front of ship. Ie, don't turn.
|
|
if (view_pos != NULL)
|
|
aip->goal_point = *view_pos;
|
|
else
|
|
vm_vec_scale_add(&aip->goal_point, &still_objp->pos, &still_objp->orient.vec.fvec, 100.0f);
|
|
}
|
|
|
|
// code which is called from ai_dock_with_object and ai_dock to set flags and apprioriate variable
|
|
// when two objects have completed docking. used because we can dock object initially at misison load
|
|
// time (meaning that ai_dock() might never get called). docker has docked with dockee (i.e. docker
|
|
// would be a freighter and dockee would be a cargo).
|
|
void ai_do_objects_docked_stuff(object *docker, int docker_point, object *dockee, int dockee_point, bool update_clients)
|
|
{
|
|
Assert((docker != NULL) && (dockee != NULL));
|
|
|
|
// make sure they're not already docked!
|
|
if (dock_check_find_direct_docked_object(docker, dockee))
|
|
{
|
|
Warning(LOCATION, "Call to ai_do_objects_docked_stuff when objects are already docked! Trace out and fix!\n");
|
|
return;
|
|
}
|
|
|
|
// link the two objects
|
|
dock_dock_objects(docker, docker_point, dockee, dockee_point);
|
|
|
|
if (docker->type == OBJ_SHIP && dockee->type == OBJ_SHIP)
|
|
{
|
|
// maybe set support ship info
|
|
if ((Ship_info[Ships[docker->instance].ship_info_index].flags[Ship::Info_Flags::Support])
|
|
|| (Ship_info[Ships[dockee->instance].ship_info_index].flags[Ship::Info_Flags::Support]))
|
|
{
|
|
ai_info *docker_aip = &Ai_info[Ships[docker->instance].ai_index];
|
|
ai_info *dockee_aip = &Ai_info[Ships[dockee->instance].ai_index];
|
|
|
|
#ifndef NDEBUG
|
|
// support ship can only dock with one thing at a time
|
|
if (Ship_info[Ships[docker->instance].ship_info_index].flags[Ship::Info_Flags::Support])
|
|
Assert(docker->dock_list->next == NULL);
|
|
else
|
|
Assert(dockee->dock_list->next == NULL);
|
|
#endif
|
|
|
|
// set stuff for both objects
|
|
docker_aip->support_ship_objnum = OBJ_INDEX(dockee);
|
|
dockee_aip->support_ship_objnum = OBJ_INDEX(docker);
|
|
docker_aip->support_ship_signature = dockee->signature;
|
|
dockee_aip->support_ship_signature = docker->signature;
|
|
}
|
|
}
|
|
|
|
// add multiplayer hook here to deal with docked objects.
|
|
if ( MULTIPLAYER_MASTER && update_clients)
|
|
send_ai_info_update_packet( docker, AI_UPDATE_DOCK, dockee );
|
|
}
|
|
|
|
// code which is called when objects become undocked. Equivalent of above function.
|
|
// Goober5000 - dockee must always be non-NULL
|
|
void ai_do_objects_undocked_stuff( object *docker, object *dockee )
|
|
{
|
|
Assert((docker != NULL) && (dockee != NULL));
|
|
|
|
// make sure they're not already undocked!
|
|
if (!dock_check_find_direct_docked_object(docker, dockee))
|
|
{
|
|
Warning(LOCATION, "Call to ai_do_objects_undocked_stuff when objects are already undocked! Trace out and fix!\n");
|
|
return;
|
|
}
|
|
|
|
// add multiplayer hook here to deal with undocked objects. Do it before we
|
|
// do anything else. We don't need to send info for both objects, since multi
|
|
// only supports one docked object
|
|
if ( MULTIPLAYER_MASTER )
|
|
send_ai_info_update_packet( docker, AI_UPDATE_UNDOCK, dockee );
|
|
|
|
if (docker->type == OBJ_SHIP && dockee->type == OBJ_SHIP)
|
|
{
|
|
ai_info *docker_aip = &Ai_info[Ships[docker->instance].ai_index];
|
|
ai_info *dockee_aip = &Ai_info[Ships[dockee->instance].ai_index];
|
|
|
|
// clear stuff for both objects
|
|
docker_aip->ai_flags.remove(AI::AI_Flags::Being_repaired);
|
|
dockee_aip->ai_flags.remove(AI::AI_Flags::Being_repaired);
|
|
docker_aip->support_ship_objnum = -1;
|
|
dockee_aip->support_ship_objnum = -1;
|
|
docker_aip->support_ship_signature = -1;
|
|
dockee_aip->support_ship_signature = -1;
|
|
// clear out points for docker
|
|
docker_aip->path_start = -1;
|
|
}
|
|
|
|
// unlink the two objects
|
|
dock_undock_objects(docker, dockee);
|
|
}
|
|
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Interface from goals code to AI.
|
|
// Cause *docker to dock with *dockee.
|
|
// priority is priority of goal from goals code.
|
|
// dock_type is:
|
|
// AIDO_DOCK set goal of docking
|
|
// AIDO_DOCK_NOW immediately dock, used for ships that need to be docked at mission start
|
|
// AIDO_UNDOCK set goal of undocking
|
|
void ai_dock_with_object(object *docker, int docker_index, object *dockee, int dockee_index, int dock_type)
|
|
{
|
|
Assert(docker != NULL);
|
|
Assert(dockee != NULL);
|
|
Assert(docker->type == OBJ_SHIP);
|
|
Assert(dockee->type == OBJ_SHIP);
|
|
Assert(docker->instance >= 0);
|
|
Assert(dockee->instance >= 0);
|
|
Assert(Ships[docker->instance].ai_index >= 0);
|
|
Assert(Ships[dockee->instance].ai_index >= 0);
|
|
Assert(docker_index >= 0);
|
|
Assert(dockee_index >= 0);
|
|
|
|
ai_info *aip = &Ai_info[Ships[docker->instance].ai_index];
|
|
|
|
aip->goal_objnum = OBJ_INDEX(dockee);
|
|
aip->goal_signature = dockee->signature;
|
|
|
|
aip->mode = AIM_DOCK;
|
|
|
|
switch (dock_type) {
|
|
case AIDO_DOCK:
|
|
aip->submode = AIS_DOCK_0;
|
|
aip->submode_start_time = Missiontime;
|
|
break;
|
|
case AIDO_DOCK_NOW:
|
|
aip->submode = AIS_DOCK_4A;
|
|
aip->submode_start_time = Missiontime;
|
|
break;
|
|
case AIDO_UNDOCK:
|
|
aip->submode = AIS_UNDOCK_0;
|
|
aip->submode_start_time = Missiontime;
|
|
break;
|
|
default:
|
|
UNREACHABLE("Unknown dock type in ai_dock_with_object of %d. Please report to the SCP!", dock_type); // Bogus dock_type.
|
|
}
|
|
|
|
// Goober5000 - we no longer need to set dock_path_index because it's easier to grab the path from the dockpoint
|
|
// a debug check would be a good thing here, though
|
|
#ifndef NDEBUG
|
|
if (dock_type == AIDO_UNDOCK)
|
|
{
|
|
polymodel *pm = model_get(Ship_info[Ships[dockee->instance].ship_info_index].model_num);
|
|
Assert( pm->docking_bays[dockee_index].num_spline_paths > 0 );
|
|
}
|
|
#endif
|
|
|
|
// dock instantly
|
|
if (dock_type == AIDO_DOCK_NOW)
|
|
{
|
|
// set model animations correctly
|
|
// (fortunately, this function is called AFTER model_anim_set_initial_states in the sea of ship creation
|
|
// functions, which is necessary for model animations to start from t=0 at the correct positions)
|
|
ship *shipp = &Ships[docker->instance];
|
|
ship *goal_shipp = &Ships[dockee->instance];
|
|
|
|
ship_info* sip = &Ship_info[shipp->ship_info_index];
|
|
ship_info* goal_sip = &Ship_info[goal_shipp->ship_info_index];
|
|
|
|
polymodel_instance* shipp_pmi = model_get_instance(shipp->model_instance_num);
|
|
polymodel_instance* goal_shipp_pmi = model_get_instance(goal_shipp->model_instance_num);
|
|
|
|
(sip->animations.getAll(shipp_pmi, animation::ModelAnimationTriggerType::Docking_Stage1, docker_index)
|
|
+ sip->animations.getAll(shipp_pmi, animation::ModelAnimationTriggerType::Docking_Stage2, docker_index)
|
|
+ sip->animations.getAll(shipp_pmi, animation::ModelAnimationTriggerType::Docking_Stage3, docker_index)
|
|
+ sip->animations.getAll(shipp_pmi, animation::ModelAnimationTriggerType::Docked, docker_index)).start(animation::ModelAnimationDirection::FWD, true, true);
|
|
(goal_sip->animations.getAll(goal_shipp_pmi, animation::ModelAnimationTriggerType::Docking_Stage1, dockee_index)
|
|
+ goal_sip->animations.getAll(goal_shipp_pmi, animation::ModelAnimationTriggerType::Docking_Stage2, dockee_index)
|
|
+ goal_sip->animations.getAll(goal_shipp_pmi, animation::ModelAnimationTriggerType::Docking_Stage3, dockee_index)
|
|
+ goal_sip->animations.getAll(goal_shipp_pmi, animation::ModelAnimationTriggerType::Docked, dockee_index)).start(animation::ModelAnimationDirection::FWD, true, true);
|
|
|
|
dock_orient_and_approach(docker, docker_index, dockee, dockee_index, DOA_DOCK_STAY);
|
|
ai_do_objects_docked_stuff( docker, docker_index, dockee, dockee_index, false );
|
|
}
|
|
// pick a path to use to start docking
|
|
else
|
|
{
|
|
int path_num = ai_return_path_num_from_dockbay(dockee, dockee_index);
|
|
|
|
// make sure we have a path
|
|
if (path_num < 0)
|
|
{
|
|
Warning(LOCATION, "Cannot find a dock path for ship %s, dock index %d. Aborting dock.\n", Ships[dockee->instance].ship_name, dockee_index);
|
|
ai_mission_goal_complete(aip);
|
|
return;
|
|
}
|
|
|
|
ai_find_path(docker, OBJ_INDEX(dockee), path_num, 0);
|
|
}
|
|
}
|
|
|
|
|
|
// Cause a ship to fly its waypoints.
|
|
// flags tells:
|
|
// WPF_REPEAT Set -> repeat waypoints.
|
|
// WPF_BACKTRACK Go in reverse.
|
|
void ai_start_waypoints(object *objp, int wp_list_index, int wp_flags, int start_index, bool force)
|
|
{
|
|
auto aip = &Ai_info[Ships[objp->instance].ai_index];
|
|
|
|
if (Waypoint_lists.empty())
|
|
{
|
|
Warning(LOCATION, "No waypoint lists in mission!");
|
|
aip->mode = AIM_NONE;
|
|
return;
|
|
}
|
|
|
|
auto wp_list = find_waypoint_list_at_index(wp_list_index);
|
|
if (!wp_list)
|
|
{
|
|
Warning(LOCATION, "wp_list_index %d must be a valid waypoint list index!", wp_list_index);
|
|
wp_list_index = 0;
|
|
wp_list = find_waypoint_list_at_index(wp_list_index);
|
|
}
|
|
|
|
if (wp_list->get_waypoints().empty())
|
|
{
|
|
Warning(LOCATION, "Waypoint list '%s' is empty!", wp_list->get_name());
|
|
aip->mode = AIM_NONE;
|
|
return;
|
|
}
|
|
|
|
auto wp = find_waypoint_at_index(wp_list, start_index);
|
|
if (!wp)
|
|
{
|
|
Warning(LOCATION, "Starting index for %s on '%s' is out of range!", Ships[objp->instance].ship_name, wp_list->get_name());
|
|
start_index = (wp_flags & WPF_BACKTRACK) ? (int)wp_list->get_waypoints().size() - 1 : 0;
|
|
}
|
|
|
|
if ( (aip->mode == AIM_WAYPOINTS) && (aip->wp_list_index == wp_list_index) && !force )
|
|
{
|
|
if (aip->wp_index == INVALID_WAYPOINT_POSITION)
|
|
{
|
|
Warning(LOCATION, "aip->wp_index should have been assigned already!");
|
|
aip->wp_index = start_index;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (The_mission.flags[Mission::Mission_Flags::Use_ap_cinematics] && AutoPilotEngaged)
|
|
{
|
|
aip->ai_flags.remove(AI::AI_Flags::Formation_wing);
|
|
}
|
|
else
|
|
{
|
|
aip->ai_flags.set(AI::AI_Flags::Formation_wing);
|
|
}
|
|
aip->ai_flags.remove(AI::AI_Flags::Formation_object);
|
|
|
|
aip->wp_list_index = wp_list_index;
|
|
aip->wp_index = start_index;
|
|
aip->wp_flags = wp_flags;
|
|
aip->mode = AIM_WAYPOINTS;
|
|
aip->submode_parm0 = start_index; // keep track of the start index because we need it for Fix_ai_path_order_bug
|
|
|
|
Assert(aip->active_goal != AI_ACTIVE_GOAL_DYNAMIC);
|
|
}
|
|
|
|
/**
|
|
* Make *objp stay within dist units of *other_objp
|
|
*/
|
|
void ai_do_stay_near(object *objp, object *other_objp, float dist, int additional_data)
|
|
{
|
|
ai_info *aip;
|
|
|
|
Assert(objp != other_objp); // Bogus! Told to stay near self.
|
|
Assert(objp->type == OBJ_SHIP);
|
|
Assert((objp->instance >= 0) && (objp->instance < MAX_SHIPS));
|
|
|
|
aip = &Ai_info[Ships[objp->instance].ai_index];
|
|
|
|
aip->mode = AIM_STAY_NEAR;
|
|
aip->submode = -1;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->submode_float0 = dist;
|
|
aip->submode_parm0 = additional_data;
|
|
aip->goal_objnum = OBJ_INDEX(other_objp);
|
|
aip->goal_signature = other_objp->signature;
|
|
}
|
|
|
|
// Goober5000 - enter safety mode (used by support ships, now possibly by fighters too)
|
|
void ai_do_safety(object *objp)
|
|
{
|
|
ai_info *aip;
|
|
|
|
Assert(objp->type == OBJ_SHIP);
|
|
Assert((objp->instance >= 0) && (objp->instance < MAX_SHIPS));
|
|
|
|
aip = &Ai_info[Ships[objp->instance].ai_index];
|
|
|
|
aip->mode = AIM_SAFETY;
|
|
aip->submode = AISS_1;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
|
|
/**
|
|
* Make object *objp form on wing of object *goal_objp
|
|
*/
|
|
void ai_form_on_wing(object *objp, object *goal_objp)
|
|
{
|
|
ai_info *aip;
|
|
ship *shipp;
|
|
ship_info *sip;
|
|
|
|
// objp == goal_objp sometimes in multiplayer when someone leaves a game -- make a simple
|
|
// out for this case.
|
|
if ( Game_mode & GM_MULTIPLAYER ) {
|
|
if ( objp == goal_objp ) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
Assert(objp != goal_objp); // Bogus! Told to form on own's wing!
|
|
|
|
shipp = &Ships[objp->instance];
|
|
sip = &Ship_info[shipp->ship_info_index];
|
|
|
|
// Only fighters or bombers allowed to form on wing.
|
|
if (!(sip->is_fighter_bomber())) {
|
|
nprintf(("AI", "Warning: Ship %s tried to form on player's wing, but not fighter or bomber.\n", shipp->ship_name));
|
|
return;
|
|
}
|
|
|
|
|
|
aip = &Ai_info[Ships[objp->instance].ai_index];
|
|
|
|
aip->ai_flags.remove(AI::AI_Flags::Formation_wing);
|
|
aip->ai_flags.set(AI::AI_Flags::Formation_object);
|
|
|
|
// if this goal is going to stick around for a while, change the mode
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Do_not_clear_goals_when_running_form_on_wing])
|
|
{
|
|
aip->mode = AIM_NONE;
|
|
aip->submode = -1;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
|
|
aip->goal_objnum = OBJ_INDEX(goal_objp);
|
|
|
|
ai_formation_object_recalculate_slotnums(aip->goal_objnum);
|
|
|
|
ai_set_goal_abort_support_call(objp, aip);
|
|
aip->ok_to_target_timestamp = timestamp(DELAY_TARGET_TIME*4); // Super extra long time until can target another ship.
|
|
|
|
}
|
|
|
|
/**
|
|
* Redistributes slotnums to all ships flying off a given formation object
|
|
* In the case this is needed because it has left the field or died, its objnum is needed to explicitly exclude it
|
|
*/
|
|
void ai_formation_object_recalculate_slotnums(int form_objnum, int exiting_objnum)
|
|
{
|
|
Assertion(form_objnum >= 0, "Invalid object number in ai_formation_object_recalculate_slotnums!");
|
|
int slotnum = 1; // Note: Slot #0 means leader, which isn't someone who was told to form-on-wing.
|
|
|
|
for (ship_obj* ship = GET_FIRST(&Ship_obj_list); ship != END_OF_LIST(&Ship_obj_list); ship = GET_NEXT(ship) ) {
|
|
if (Objects[ship->objnum].flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
ai_info* aip = &Ai_info[Ships[Objects[ship->objnum].instance].ai_index];
|
|
|
|
if (ship->objnum == exiting_objnum)
|
|
continue;
|
|
|
|
if (aip->ai_flags[AI::AI_Flags::Formation_object]) {
|
|
if (aip->goal_objnum == form_objnum) {
|
|
aip->form_obj_slotnum = slotnum;
|
|
slotnum++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#define BIGNUM 100000.0f
|
|
|
|
int Debug_k = 0;
|
|
|
|
/**
|
|
* Given an attacker's position and a target's position and velocity, compute the time of
|
|
* intersection of a weapon fired by the attacker with speed weapon_speed.
|
|
*
|
|
* Return this value. Return value of 0.0f means no collision is possible.
|
|
*/
|
|
float compute_collision_time(const vec3d *targpos, const vec3d *targvel, const vec3d *attackpos, float weapon_speed)
|
|
{
|
|
vec3d vec_to_target;
|
|
float pos_dot_vel;
|
|
float vel_sqr;
|
|
float discrim;
|
|
|
|
vm_vec_sub(&vec_to_target, targpos, attackpos);
|
|
pos_dot_vel = vm_vec_dot(&vec_to_target, targvel);
|
|
vel_sqr = vm_vec_dot(targvel, targvel) - weapon_speed*weapon_speed;
|
|
discrim = pos_dot_vel*pos_dot_vel - vel_sqr*vm_vec_dot(&vec_to_target, &vec_to_target);
|
|
|
|
if (discrim > 0.0f) {
|
|
float t1, t2, t_solve;
|
|
|
|
t1 = (-pos_dot_vel + fl_sqrt(discrim)) / vel_sqr;
|
|
t2 = (-pos_dot_vel - fl_sqrt(discrim)) / vel_sqr;
|
|
|
|
t_solve = BIGNUM;
|
|
|
|
if (t1 > 0.0f)
|
|
t_solve = t1;
|
|
if ((t2 > 0.0f) && (t2 < t_solve))
|
|
t_solve = t2;
|
|
|
|
if (t_solve < BIGNUM-1.0f) {
|
|
return t_solve + Debug_k * flFrametime;
|
|
}
|
|
}
|
|
|
|
return 0.0f;
|
|
}
|
|
|
|
|
|
// --------------------------------------------------------------------------
|
|
// If far away, use player's speed.
|
|
// If in between, lerp between player and laser speed
|
|
// If close, use laser speed.
|
|
// Want to know how much time it will take to get to the enemy.
|
|
// This function doesn't account for the fact that by the time the player
|
|
// (or his laser) gets to the current enemy position, the enemy will have moved.
|
|
// This is dealt with in polish_predicted_enemy_pos.
|
|
float compute_time_to_enemy(float dist_to_enemy, object *pobjp)
|
|
{
|
|
float time_to_enemy;
|
|
float pl_speed = pobjp->phys_info.speed;
|
|
float max_laser_distance, max_laser_speed;
|
|
int bank_num, weapon_num;
|
|
ship *shipp = &Ships[pobjp->instance];
|
|
|
|
bank_num = shipp->weapons.current_primary_bank;
|
|
weapon_num = shipp->weapons.primary_bank_weapons[bank_num];
|
|
max_laser_speed = Weapon_info[weapon_num].max_speed;
|
|
max_laser_distance = MIN((max_laser_speed * Weapon_info[weapon_num].lifetime), Weapon_info[weapon_num].weapon_range);
|
|
|
|
// If pretty far away, use player's speed to predict position, else
|
|
// use laser's speed because when close, we care more about hitting
|
|
// with a laser than about causing ship:ship rendezvous.
|
|
if (dist_to_enemy > 1.5 * max_laser_distance) {
|
|
if (pl_speed > 0.0f)
|
|
time_to_enemy = dist_to_enemy/pl_speed;
|
|
else
|
|
time_to_enemy = 1.0f;
|
|
} else if (dist_to_enemy > 1.1*max_laser_distance) {
|
|
if (pl_speed > 0.1f) {
|
|
float scale;
|
|
|
|
scale = (float) ((dist_to_enemy - max_laser_distance) / max_laser_distance);
|
|
|
|
time_to_enemy = (float) (dist_to_enemy/(pl_speed * scale + max_laser_speed * (1.0f - scale)));
|
|
} else
|
|
time_to_enemy = 2.0f;
|
|
} else
|
|
time_to_enemy = (float) (dist_to_enemy/max_laser_speed);
|
|
|
|
return time_to_enemy + flFrametime;
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
void ai_set_positions(object *pl_objp, object *en_objp, ai_info *aip, vec3d *player_pos, vec3d *enemy_pos)
|
|
{
|
|
*player_pos = pl_objp->pos;
|
|
|
|
if (aip->next_predict_pos_time > Missiontime) {
|
|
*enemy_pos = aip->last_predicted_enemy_pos;
|
|
} else {
|
|
*enemy_pos = en_objp->pos;
|
|
|
|
aip->next_predict_pos_time = Missiontime + aip->ai_predict_position_delay;
|
|
aip->last_predicted_enemy_pos = *enemy_pos;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
/**
|
|
* Updates AI aim automatically based on targeted enemy.
|
|
*
|
|
* @param aip Pointer to AI info
|
|
*/
|
|
void ai_update_aim(ai_info *aip)
|
|
{
|
|
if (Missiontime >= aip->next_aim_pos_time)
|
|
{
|
|
aip->last_aim_enemy_pos = En_objp->pos;
|
|
aip->last_aim_enemy_vel = En_objp->phys_info.vel;
|
|
aip->next_aim_pos_time = Missiontime + fl2f(frand_range(0.0f, aip->ai_max_aim_update_delay));
|
|
}
|
|
else
|
|
{
|
|
//Update the position based on the velocity (assume no velocity vector change)
|
|
vm_vec_scale_add2(&aip->last_aim_enemy_pos, &aip->last_aim_enemy_vel, flFrametime);
|
|
}
|
|
}
|
|
|
|
// returns if the currently active goal trying to afterburn hard
|
|
bool ai_willing_to_afterburn_hard(ai_info* aip) {
|
|
return aip->active_goal == AI_ACTIVE_GOAL_DYNAMIC ?
|
|
The_mission.ai_profile->flags[AI::Profile_Flags::Dynamic_goals_afterburn_hard] :
|
|
(aip->active_goal > -1 && aip->goals[aip->active_goal].flags[AI::Goal_Flags::Afterburn_hard]);
|
|
}
|
|
|
|
// pedal to the metal!!1!
|
|
void ai_afterburn_hard(object* objp, ai_info* aip) {
|
|
ship* shipp = &Ships[objp->instance];
|
|
|
|
if (!(objp->phys_info.flags & PF_AFTERBURNER_ON)) {
|
|
if (shipp->afterburner_fuel > Ship_info[shipp->ship_info_index].afterburner_fuel_capacity * 0.3) {
|
|
afterburners_start(Pl_objp);
|
|
aip->afterburner_stop_time = Missiontime + 5 * F1_0;
|
|
}
|
|
}
|
|
else {
|
|
if (shipp->afterburner_fuel > 0.0f) {
|
|
afterburners_start(Pl_objp);
|
|
aip->afterburner_stop_time = Missiontime + 5 * F1_0;
|
|
}
|
|
}
|
|
|
|
accelerate_ship(aip, 1.0f);
|
|
};
|
|
|
|
// Given an ai_info struct, by reading current goal and path information,
|
|
// extract base path information and return in pmp and pmpv.
|
|
// Return true if found, else return false.
|
|
// false means the current point is not on the original path.
|
|
int get_base_path_info(int path_cur, int goal_objnum, model_path **pmp, mp_vert **pmpv)
|
|
{
|
|
pnode *pn = &Path_points[path_cur];
|
|
ship *shipp = &Ships[Objects[goal_objnum].instance];
|
|
polymodel *pm = model_get(Ship_info[shipp->ship_info_index].model_num);
|
|
|
|
*pmpv = NULL;
|
|
*pmp = NULL;
|
|
|
|
if (pn->path_num != -1) {
|
|
*pmp = &pm->paths[pn->path_num];
|
|
if (pn->path_index != -1)
|
|
*pmpv = &(*pmp)->verts[pn->path_index];
|
|
else
|
|
return 0;
|
|
} else
|
|
return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
// Modify, in place, the points in a global model path.
|
|
// Only modify those points that are defined in the model path. Don't modify the
|
|
// leadin points, such as those that are necessary to get the model on the path.
|
|
void modify_model_path_points(object *objp)
|
|
{
|
|
ai_info *aip = &Ai_info[Ships[objp->instance].ai_index];
|
|
object *mobjp = &Objects[aip->path_objnum];
|
|
polymodel *pm = model_get(Ship_info[Ships[mobjp->instance].ship_info_index].model_num);
|
|
pnode *pnp;
|
|
int path_num, dir;
|
|
|
|
Assert((aip->path_start >= 0) && (aip->path_start < MAX_PATH_POINTS));
|
|
|
|
pnp = &Path_points[aip->path_start];
|
|
while ((pnp->path_index == -1) && (pnp-Path_points - aip->path_start < aip->path_length))
|
|
pnp++;
|
|
|
|
path_num = pnp->path_num;
|
|
Assert((path_num >= 0) && (path_num < pm->n_paths));
|
|
|
|
Assert(pnp->path_index != -1); // If this is -1, that means we never found the model path points
|
|
|
|
dir = 1;
|
|
if ( aip->ai_flags[AI::AI_Flags::Use_exit_path] ) {
|
|
dir = -1;
|
|
}
|
|
|
|
copy_xlate_model_path_points(mobjp, &pm->paths[path_num], dir, pm->paths[path_num].nverts, path_num, pnp);
|
|
}
|
|
|
|
// Return an indication of the distance between two matrices.
|
|
// This is the sum of the distances of their dot products from 1.0f.
|
|
float ai_matrix_dist(matrix *mat1, matrix *mat2)
|
|
{
|
|
float t;
|
|
|
|
t = 1.0f - vm_vec_dot(&mat1->vec.fvec, &mat2->vec.fvec);
|
|
t += 1.0f - vm_vec_dot(&mat1->vec.uvec, &mat2->vec.uvec);
|
|
t += 1.0f - vm_vec_dot(&mat1->vec.rvec, &mat2->vec.rvec);
|
|
|
|
return t;
|
|
}
|
|
|
|
|
|
// Paths are created in absolute space, so a moving object needs to have model paths within it recreated.
|
|
// This uses the hash functions which means the slightest movement will cause a recreate, though the timestamp
|
|
// prevents this from happening too often.
|
|
// force_recreate_flag TRUE means to recreate regardless of timestamp.
|
|
// Returns TRUE if path recreated.
|
|
float maybe_recreate_path(object *objp, ai_info *aip, int force_recreate_flag, int override_hash = 0)
|
|
{
|
|
int hashval;
|
|
|
|
Assert(&Ai_info[Ships[objp->instance].ai_index] == aip);
|
|
|
|
if ((aip->mode == AIM_BAY_EMERGE) || (aip->mode == AIM_BAY_DEPART))
|
|
if ((OBJ_INDEX(objp) % 4) == (Framecount % 4))
|
|
force_recreate_flag = 1;
|
|
|
|
// If no path, that means we don't need one.
|
|
if (aip->path_start == -1)
|
|
return 0.0f;
|
|
|
|
// AL 11-12-97: If AIF_USE_STATIC_PATH is set, don't try to recreate. This is needed when ships
|
|
// emerge from fighter bays. We don't need to recreate the path.. and in case the
|
|
// parent ship dies, we still want to be able to continue on the path
|
|
if ( aip->ai_flags[AI::AI_Flags::Use_static_path] )
|
|
return 0.0f;
|
|
|
|
if (force_recreate_flag || timestamp_elapsed(aip->path_next_create_time)) {
|
|
object *path_objp;
|
|
|
|
path_objp = &Objects[aip->path_objnum];
|
|
hashval = create_object_hash(path_objp);
|
|
|
|
if (override_hash || (hashval != aip->path_goal_obj_hash)) {
|
|
float dist;
|
|
|
|
dist = vm_vec_dist_quick(&path_objp->pos, &aip->path_create_pos);
|
|
dist += ai_matrix_dist(&path_objp->orient, &aip->path_create_orient) * 25.0f;
|
|
|
|
if (force_recreate_flag || (dist > 2.0f)) {
|
|
aip->path_next_create_time = timestamp(1000); // Update again in as little as 1000 milliseconds, ie 1 second.
|
|
aip->path_goal_obj_hash = hashval;
|
|
modify_model_path_points(objp);
|
|
|
|
aip->path_create_pos = path_objp->pos;
|
|
aip->path_create_orient = path_objp->orient;
|
|
|
|
//Get path departure orientation from ships.tbl if it exists, otherwise zero it
|
|
ship_info *osip = &Ship_info[Ships[path_objp->instance].ship_info_index];
|
|
model_path *mp = &model_get(osip->model_num)->paths[aip->mp_index];
|
|
SCP_string pathName(mp->name);
|
|
if (osip->pathMetadata.find(pathName) != osip->pathMetadata.end() && !IS_VEC_NULL(&osip->pathMetadata[pathName].departure_rvec))
|
|
vm_vec_copy_normalize(&aip->path_depart_rvec, &osip->pathMetadata[pathName].departure_rvec);
|
|
else if (aip->ai_profile_flags[AI::Profile_Flags::Fighterbay_departures_use_carrier_orient])
|
|
aip->path_depart_rvec = vmd_x_vector; // this will be later rotated into the carrier's frame of reference
|
|
else
|
|
vm_vec_zero(&aip->path_depart_rvec);
|
|
|
|
return dist;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0.0f;
|
|
}
|
|
|
|
/**
|
|
* Set acceleration for ai_dock().
|
|
*/
|
|
void set_accel_for_docking(object *objp, ai_info *aip, float dot, float dot_to_next, float dist_to_next, float dist_to_goal, ship_info *sip, float max_allowed_speed, object *gobjp)
|
|
{
|
|
float prev_dot_to_goal = aip->prev_dot_to_goal;
|
|
|
|
aip->prev_dot_to_goal = dot;
|
|
|
|
if (objp->phys_info.speed < 0.0f) {
|
|
accelerate_ship(aip, 1.0f/32.0f);
|
|
} else if ((prev_dot_to_goal-dot) > 0.01) {
|
|
if (prev_dot_to_goal > dot + 0.05f) {
|
|
accelerate_ship(aip, 0.0f);
|
|
} else {
|
|
change_acceleration(aip, -1.0f); // -1.0f means subtract off flFrametime from acceleration value in 0.0..1.0
|
|
}
|
|
} else if (max_allowed_speed <= 0.0f && ai_willing_to_afterburn_hard(aip) && dist_to_goal > 1000.0f && dot > 0.99f) {
|
|
// BOOK IT
|
|
ai_afterburn_hard(Pl_objp, aip);
|
|
} else {
|
|
float max_bay_speed = sip->max_speed;
|
|
|
|
// Maybe gradually ramp up/down the speed of a ship flying a fighterbay path
|
|
if (aip->mode == AIM_BAY_EMERGE || (aip->mode == AIM_BAY_DEPART && aip->path_cur != aip->path_start)) {
|
|
ship_info *gsip = &Ship_info[Ships[gobjp->instance].ship_info_index];
|
|
polymodel *pm = model_get(gsip->model_num);
|
|
SCP_string pathName(pm->paths[Path_points[aip->path_start].path_num].name);
|
|
float speed_mult = FLT_MIN;
|
|
|
|
if (aip->mode == AIM_BAY_EMERGE) { // Arriving
|
|
if (gsip->pathMetadata.find(pathName) != gsip->pathMetadata.end()) {
|
|
speed_mult = gsip->pathMetadata[pathName].arrive_speed_mult;
|
|
}
|
|
|
|
if (speed_mult == FLT_MIN) {
|
|
speed_mult = The_mission.ai_profile->bay_arrive_speed_mult;
|
|
}
|
|
} else { // Departing
|
|
if (gsip->pathMetadata.find(pathName) != gsip->pathMetadata.end()) {
|
|
speed_mult = gsip->pathMetadata[pathName].depart_speed_mult;
|
|
}
|
|
|
|
if (speed_mult == FLT_MIN) {
|
|
speed_mult = The_mission.ai_profile->bay_depart_speed_mult;
|
|
}
|
|
}
|
|
|
|
if (speed_mult != FLT_MIN && speed_mult != 1.0f) {
|
|
// We use the distance between the first and last point on the path here; it's not accurate
|
|
// if the path is not straight, but should be good enough usually; can be changed if necessary.
|
|
float total_path_length = vm_vec_dist_quick(&Path_points[aip->path_start].pos, &Path_points[aip->path_start + aip->path_length - 1].pos);
|
|
float dist_to_end;
|
|
|
|
if (aip->mode == AIM_BAY_EMERGE) { // Arriving
|
|
dist_to_end = vm_vec_dist_quick(&Pl_objp->pos, &Path_points[aip->path_start].pos);
|
|
} else { // Departing
|
|
dist_to_end = vm_vec_dist_quick(&Pl_objp->pos, &Path_points[aip->path_start + aip->path_length - 1].pos);
|
|
}
|
|
|
|
// Calculate max speed, but respect the waypoint speed cap if it's lower
|
|
max_bay_speed = sip->max_speed * (speed_mult + (1.0f - speed_mult) * (dist_to_end / total_path_length));
|
|
}
|
|
}
|
|
|
|
// Cap speed to max_allowed_speed, if it's set
|
|
if (max_allowed_speed <= 0)
|
|
max_allowed_speed = max_bay_speed;
|
|
else
|
|
max_allowed_speed = MIN(max_allowed_speed, max_bay_speed);
|
|
|
|
if ((aip->mode == AIM_DOCK) && (dist_to_next < 150.0f) && (aip->path_start + aip->path_length - 2 == aip->path_cur)) {
|
|
set_accel_for_target_speed(objp, max_allowed_speed * MAX(dist_to_next/500.0f, 1.0f));
|
|
} else if ((dot_to_next >= dot * .9) || (dist_to_next > 100.0f)) {
|
|
if (dist_to_goal > 200.0f) {
|
|
set_accel_for_target_speed(objp, max_allowed_speed * (dot + 1.0f) / 2.0f);
|
|
}
|
|
else {
|
|
float xdot;
|
|
|
|
xdot = (dot_to_next + dot)/2.0f;
|
|
if (xdot < 0.0f)
|
|
xdot = 0.0f;
|
|
|
|
// AL: if following a path not in dock mode, move full speed
|
|
if (( aip->mode != AIM_DOCK ) && (dot > 0.9f)) {
|
|
set_accel_for_target_speed(objp, max_allowed_speed*dot*dot*dot);
|
|
} else {
|
|
if ((aip->path_cur - aip->path_start < aip->path_length-2) && (dist_to_goal < 2*objp->radius)) {
|
|
set_accel_for_target_speed(objp, dist_to_goal/8.0f + 2.0f);
|
|
} else {
|
|
set_accel_for_target_speed(objp, max_allowed_speed * (2*xdot + 0.25f)/4.0f);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
float xdot;
|
|
|
|
xdot = MAX(dot_to_next, 0.1f);
|
|
if ( aip->mode != AIM_DOCK ) {
|
|
set_accel_for_target_speed(objp, max_allowed_speed);
|
|
} else {
|
|
float speed;
|
|
if ((aip->path_cur - aip->path_start < aip->path_length-2) && (dist_to_goal < 2*objp->radius)) {
|
|
speed = dist_to_goal/8.0f + 2.0f;
|
|
} else if (dist_to_goal < 4*objp->radius + 50.0f) {
|
|
speed = dist_to_goal/4.0f + 4.0f;
|
|
} else {
|
|
speed = max_allowed_speed * (3*xdot + 1.0f)/4.0f;
|
|
}
|
|
if (aip->mode == AIM_DOCK) {
|
|
speed = speed * 2.0f + 1.0f;
|
|
if (aip->goal_objnum != -1) {
|
|
speed += Objects[aip->goal_objnum].phys_info.speed;
|
|
}
|
|
}
|
|
|
|
set_accel_for_target_speed(objp, speed);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Follow a path associated with a large object, such as a capital ship.
|
|
// The points defined on the path are in the object's reference frame.
|
|
// The object of interest is goal_objnum.
|
|
// The paths are defined in the model. The path of interest is wp_list.
|
|
// The next goal point in the path is wp_index.
|
|
// wp_flags contain special information specific to the path.
|
|
|
|
// The path vertices are defined by model_path structs:
|
|
// typedef struct model_path {
|
|
// char name[MAX_NAME_LEN]; // name of the subsystem. Probably displayed on HUD
|
|
// int nverts;
|
|
// vec3d *verts;
|
|
// } model_path;
|
|
|
|
// The polymodel struct for the object contains the following:
|
|
// int n_paths;
|
|
// model_path *paths;
|
|
|
|
// Returns distance to goal point.
|
|
float ai_path_0()
|
|
{
|
|
int num_points;
|
|
float dot, dist_to_goal, dist_to_next, dot_to_next;
|
|
ship *shipp = &Ships[Pl_objp->instance];
|
|
ship_info *sip = &Ship_info[shipp->ship_info_index];
|
|
ai_info *aip;
|
|
vec3d nvel_vec;
|
|
float mag;
|
|
vec3d temp_vec, *slop_vec;
|
|
object *gobjp;
|
|
vec3d *cvp, *nvp, next_vec, gcvp, gnvp; // current and next vertices in global coordinates.
|
|
|
|
aip = &Ai_info[Ships[Pl_objp->instance].ai_index];
|
|
|
|
Assert(aip->goal_objnum != -1);
|
|
Assert(Objects[aip->goal_objnum].type == OBJ_SHIP);
|
|
|
|
gobjp = &Objects[aip->goal_objnum];
|
|
|
|
if (aip->path_start == -1) {
|
|
Assert(aip->goal_objnum >= 0 && aip->goal_objnum < MAX_OBJECTS);
|
|
int path_num;
|
|
Assert(aip->active_goal >= 0);
|
|
ai_goal *aigp = &aip->goals[aip->active_goal];
|
|
Assert(aigp->flags[AI::Goal_Flags::Dockee_index_valid] && aigp->flags[AI::Goal_Flags::Docker_index_valid]);
|
|
|
|
path_num = ai_return_path_num_from_dockbay(&Objects[aip->goal_objnum], aigp->dockee.index);
|
|
ai_find_path(Pl_objp, aip->goal_objnum, path_num, 0);
|
|
}
|
|
|
|
maybe_recreate_path(Pl_objp, aip, 0);
|
|
|
|
num_points = aip->path_length;
|
|
|
|
// Set cvp and nvp as pointers to current and next vertices of interest on path.
|
|
cvp = &Path_points[aip->path_cur].pos;
|
|
if ((aip->path_cur + aip->path_dir - aip->path_start < num_points) || (aip->path_cur + aip->path_dir < aip->path_start))
|
|
nvp = &Path_points[aip->path_cur + aip->path_dir].pos;
|
|
else {
|
|
// If this is 0, then path length must be 1 which means we have no direction!
|
|
Assert((aip->path_cur - aip->path_dir >= aip->path_start) && (aip->path_cur - aip->path_dir - aip->path_start < num_points));
|
|
// Cleanup for above Assert() which we hit too near release. -- MK, 5/24/98.
|
|
if (aip->path_cur - aip->path_dir - aip->path_start >= num_points) {
|
|
if (aip->path_dir == 1)
|
|
aip->path_cur = aip->path_start;
|
|
else
|
|
aip->path_cur = aip->path_start + num_points - 1;
|
|
}
|
|
|
|
vec3d delvec;
|
|
vm_vec_sub(&delvec, cvp, &Path_points[aip->path_cur - aip->path_dir].pos);
|
|
vm_vec_normalize(&delvec);
|
|
vm_vec_scale_add(&next_vec, cvp, &delvec, 10.0f);
|
|
nvp = &next_vec;
|
|
}
|
|
|
|
// See if can reach next point (as opposed to current point)
|
|
// However, don't do this if docking and next point is last point.
|
|
// That is, we don't want to pursue the last point under control of the
|
|
// path code. In docking, this is a special hack.
|
|
if ((aip->mode != AIM_DOCK) || ((aip->path_cur-aip->path_start) < num_points - 2)) {
|
|
if ((aip->path_cur + aip->path_dir > aip->path_start) && (aip->path_cur + aip->path_dir < aip->path_start + num_points-2)) {
|
|
if ( timestamp_elapsed(aip->path_next_check_time)) {
|
|
aip->path_next_check_time = timestamp( 3000 );
|
|
if (!pp_collide(&Pl_objp->pos, nvp, gobjp, 1.1f * Pl_objp->radius)) {
|
|
cvp = nvp;
|
|
aip->path_cur += aip->path_dir;
|
|
nvp = &Path_points[aip->path_cur].pos;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
gcvp = *cvp;
|
|
gnvp = *nvp;
|
|
|
|
dist_to_goal = vm_vec_dist_quick(&Pl_objp->pos, &gcvp);
|
|
dist_to_next = vm_vec_dist_quick(&Pl_objp->pos, &gnvp);
|
|
// Can't use fvec, need to use velocity vector because we aren't necessarily
|
|
// moving in the direction we're facing.
|
|
|
|
if ( vm_vec_mag_quick(&Pl_objp->phys_info.vel) < AICODE_SMALL_MAGNITUDE ) {
|
|
mag = 0.0f;
|
|
vm_vec_zero(&nvel_vec);
|
|
} else
|
|
mag = vm_vec_copy_normalize(&nvel_vec, &Pl_objp->phys_info.vel);
|
|
|
|
// If moving not-very-slowly and sliding, then try to slide at goal, rather than
|
|
// point at goal.
|
|
slop_vec = NULL;
|
|
if (mag < 1.0f)
|
|
nvel_vec = Pl_objp->orient.vec.fvec;
|
|
else if (mag > 5.0f) {
|
|
float nv_dot;
|
|
nv_dot = vm_vec_dot(&Pl_objp->orient.vec.fvec, &nvel_vec);
|
|
if ((nv_dot > 0.5f) && (nv_dot < 0.97f)) {
|
|
slop_vec = &temp_vec;
|
|
vm_vec_sub(slop_vec, &nvel_vec, &Pl_objp->orient.vec.fvec);
|
|
}
|
|
}
|
|
|
|
if (dist_to_goal > 0.1f)
|
|
ai_turn_towards_vector(&gcvp, Pl_objp, slop_vec, nullptr, 0.0f, 0);
|
|
|
|
// Code to control speed is MUCH less forgiving in path following than in waypoint
|
|
// following. Must be very close to path or might hit objects.
|
|
dot = vm_vec_dot_to_point(&nvel_vec, &Pl_objp->pos, &gcvp);
|
|
dot_to_next = vm_vec_dot_to_point(&nvel_vec, &Pl_objp->pos, &gnvp);
|
|
|
|
set_accel_for_docking(Pl_objp, aip, dot, dot_to_next, dist_to_next, dist_to_goal, sip, 0, gobjp);
|
|
aip->prev_dot_to_goal = dot;
|
|
|
|
// If moving at a non-tiny velocity, detect attaining path point by its being close to
|
|
// line between previous and current object location.
|
|
if ((dist_to_goal < MIN_DIST_TO_WAYPOINT_GOAL) || (vm_vec_dist_quick(&Pl_objp->last_pos, &Pl_objp->pos) > 0.1f)) {
|
|
vec3d nearest_point;
|
|
float r, min_dist_to_goal;
|
|
|
|
r = find_nearest_point_on_line(&nearest_point, &Pl_objp->last_pos, &Pl_objp->pos, &gcvp);
|
|
|
|
// Set min_dist_to_goal = how close must be to waypoint to pick next one.
|
|
// If docking and this is the second last waypoint, must be very close.
|
|
if ((aip->mode == AIM_DOCK) && (aip->path_cur >= aip->path_length-2))
|
|
min_dist_to_goal = MIN_DIST_TO_WAYPOINT_GOAL;
|
|
else
|
|
min_dist_to_goal = MIN_DIST_TO_WAYPOINT_GOAL + Pl_objp->radius;
|
|
|
|
if ( (vm_vec_dist_quick(&Pl_objp->pos, &gcvp) < min_dist_to_goal) ||
|
|
(((r >= 0.0f) && (r <= 1.0f)) && (vm_vec_dist_quick(&nearest_point, &gcvp) < (MIN_DIST_TO_WAYPOINT_GOAL + Pl_objp->radius)))) {
|
|
aip->path_cur += aip->path_dir;
|
|
if (((aip->path_cur - aip->path_start) > (num_points+1)) || (aip->path_cur < aip->path_start)) {
|
|
Assert(aip->mode != AIM_DOCK); // If docking, should never get this far, getting to last point handled outside ai_path()
|
|
aip->path_dir = -aip->path_dir;
|
|
}
|
|
}
|
|
}
|
|
|
|
return dist_to_goal;
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Alternate version of ai_path
|
|
// 1.
|
|
float ai_path_1_or_2(bool allow_path_shortcut)
|
|
{
|
|
int num_points;
|
|
float dot, dist_to_goal, dist_to_next, dot_to_next;
|
|
ship *shipp = &Ships[Pl_objp->instance];
|
|
ship_info *sip = &Ship_info[shipp->ship_info_index];
|
|
ai_info *aip;
|
|
vec3d nvel_vec;
|
|
float mag;
|
|
object *gobjp;
|
|
vec3d *pvp, *cvp, *nvp, next_vec, gpvp, gcvp, gnvp; // previous, current and next vertices in global coordinates.
|
|
|
|
aip = &Ai_info[Ships[Pl_objp->instance].ai_index];
|
|
|
|
Assert(aip->goal_objnum != -1);
|
|
Assert(Objects[aip->goal_objnum].type == OBJ_SHIP);
|
|
|
|
gobjp = &Objects[aip->goal_objnum];
|
|
|
|
if (aip->path_start == -1) {
|
|
Assert(aip->goal_objnum >= 0 && aip->goal_objnum < MAX_OBJECTS);
|
|
int path_num;
|
|
Assert(aip->active_goal >= 0);
|
|
ai_goal *aigp = &aip->goals[aip->active_goal];
|
|
Assert(aigp->flags[AI::Goal_Flags::Dockee_index_valid] && aigp->flags[AI::Goal_Flags::Docker_index_valid]);
|
|
|
|
path_num = ai_return_path_num_from_dockbay(&Objects[aip->goal_objnum], aigp->dockee.index);
|
|
ai_find_path(Pl_objp, aip->goal_objnum, path_num, 0);
|
|
}
|
|
|
|
maybe_recreate_path(Pl_objp, aip, 0);
|
|
|
|
num_points = aip->path_length;
|
|
|
|
// Set cvp and nvp as pointers to current and next vertices of interest on path.
|
|
cvp = &Path_points[aip->path_cur].pos;
|
|
if ((aip->path_cur + aip->path_dir - aip->path_start < num_points) || (aip->path_cur + aip->path_dir < aip->path_start))
|
|
nvp = &Path_points[aip->path_cur + aip->path_dir].pos;
|
|
else {
|
|
// If this is 0, then path length must be 1 which means we have no direction!
|
|
Assert((aip->path_cur - aip->path_dir >= aip->path_start) && (aip->path_cur - aip->path_dir - aip->path_start < num_points));
|
|
// Cleanup for above Assert() which we hit too near release. -- MK, 5/24/98.
|
|
if (aip->path_cur - aip->path_dir - aip->path_start >= num_points) {
|
|
if (aip->path_dir == 1)
|
|
aip->path_cur = aip->path_start;
|
|
else
|
|
aip->path_cur = aip->path_start + num_points - 1;
|
|
}
|
|
|
|
vec3d delvec;
|
|
vm_vec_sub(&delvec, cvp, &Path_points[aip->path_cur - aip->path_dir].pos);
|
|
vm_vec_normalize(&delvec);
|
|
vm_vec_scale_add(&next_vec, cvp, &delvec, 10.0f);
|
|
nvp = &next_vec;
|
|
}
|
|
|
|
if (allow_path_shortcut) {
|
|
// See if can reach next point (as opposed to current point)
|
|
// However, don't do this if docking and next point is last point.
|
|
if ((aip->mode != AIM_DOCK) || ((aip->path_cur - aip->path_start) < num_points - 2)) {
|
|
if ((aip->path_cur + aip->path_dir > aip->path_start) &&
|
|
(aip->path_cur + aip->path_dir < aip->path_start + num_points - 2)) {
|
|
if (timestamp_elapsed(aip->path_next_check_time)) {
|
|
aip->path_next_check_time = timestamp(3000);
|
|
if (!pp_collide(&Pl_objp->pos, nvp, gobjp, 1.1f * Pl_objp->radius)) {
|
|
cvp = nvp;
|
|
aip->path_cur += aip->path_dir;
|
|
nvp = &Path_points[aip->path_cur].pos;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set pvp to the previous vertex of interest on the path (if there is one)
|
|
int prev_point = aip->path_cur - aip->path_dir;
|
|
if (prev_point >= aip->path_start && prev_point <= aip->path_start + num_points)
|
|
pvp = &Path_points[prev_point].pos;
|
|
else
|
|
pvp = NULL;
|
|
|
|
gpvp = (pvp != NULL) ? *pvp : *cvp;
|
|
gcvp = *cvp;
|
|
gnvp = *nvp;
|
|
|
|
dist_to_goal = vm_vec_dist_quick(&Pl_objp->pos, &gcvp);
|
|
dist_to_next = vm_vec_dist_quick(&Pl_objp->pos, &gnvp);
|
|
|
|
// Can't use fvec, need to use velocity vector because we aren't necessarily
|
|
// moving in the direction we're facing.
|
|
if ( vm_vec_mag_quick(&Pl_objp->phys_info.vel) < AICODE_SMALL_MAGNITUDE ) {
|
|
mag = 0.0f;
|
|
vm_vec_zero(&nvel_vec);
|
|
} else
|
|
mag = vm_vec_copy_normalize(&nvel_vec, &Pl_objp->phys_info.vel);
|
|
|
|
if (mag < 1.0f)
|
|
nvel_vec = Pl_objp->orient.vec.fvec;
|
|
|
|
// For departures, optionally rotate so the ship is pointing the correct direction
|
|
// (so you can always have "wheels-down" landings)
|
|
vec3d rvec;
|
|
vec3d *prvec = NULL;
|
|
if ( aip->mode == AIM_BAY_DEPART && !IS_VEC_NULL(&aip->path_depart_rvec) ) {
|
|
vm_vec_unrotate(&rvec, &aip->path_depart_rvec, &Objects[aip->path_objnum].orient);
|
|
prvec = &rvec;
|
|
}
|
|
|
|
// Turn toward a point between where we are on the path and the goal. This helps keep the ship flying
|
|
// "down the line"
|
|
if (dist_to_goal > 0.1f) {
|
|
//Get nearest point on line between current and previous path points
|
|
vec3d closest_point;
|
|
float r = find_nearest_point_on_line(&closest_point, &gpvp, &gcvp, &Pl_objp->pos);
|
|
|
|
//Turn towards the 1/3 point between the closest point on the line and the current goal point
|
|
//(unless we are already past the current goal)
|
|
if (r <= 1.0f) {
|
|
vec3d midpoint;
|
|
vm_vec_avg3(&midpoint, &gcvp, &closest_point, &closest_point);
|
|
ai_turn_towards_vector(&midpoint, Pl_objp, nullptr, nullptr, 0.0f, 0, prvec);
|
|
}
|
|
else {
|
|
ai_turn_towards_vector(&gcvp, Pl_objp, nullptr, nullptr, 0.0f, 0, prvec);
|
|
}
|
|
}
|
|
|
|
// Code to control speed is MUCH less forgiving in path following than in waypoint
|
|
// following. Must be very close to path or might hit objects.
|
|
dot = vm_vec_dot_to_point(&nvel_vec, &Pl_objp->pos, &gcvp);
|
|
dot_to_next = vm_vec_dot_to_point(&nvel_vec, &Pl_objp->pos, &gnvp);
|
|
|
|
// This path mode respects the "cap-waypoint-speed" SEXP
|
|
float max_allowed_speed = 0.0f;
|
|
if ( aip->waypoint_speed_cap > 0) {
|
|
max_allowed_speed = (float) aip->waypoint_speed_cap;
|
|
}
|
|
|
|
set_accel_for_docking(Pl_objp, aip, dot, dot_to_next, dist_to_next, dist_to_goal, sip, max_allowed_speed, gobjp);
|
|
aip->prev_dot_to_goal = dot;
|
|
|
|
// If moving at a non-tiny velocity, detect attaining path point by its being close to
|
|
// line between previous and current object location.
|
|
if ((dist_to_goal < MIN_DIST_TO_WAYPOINT_GOAL) || (vm_vec_dist_quick(&Pl_objp->last_pos, &Pl_objp->pos) > 0.1f)) {
|
|
vec3d nearest_point;
|
|
float r, min_dist_to_goal;
|
|
|
|
r = find_nearest_point_on_line(&nearest_point, &Pl_objp->last_pos, &Pl_objp->pos, &gcvp);
|
|
|
|
// Set min_dist_to_goal = how close must be to waypoint to pick next one.
|
|
// If docking and this is the second last waypoint, must be very close.
|
|
if ((aip->mode == AIM_DOCK) && (aip->path_cur - aip->path_start >= aip->path_length-1)) {
|
|
min_dist_to_goal = MIN_DIST_TO_WAYPOINT_GOAL;
|
|
}
|
|
// If this is not the last waypoint, include a term to compensate for inertia
|
|
else if (aip->path_cur - aip->path_start < aip->path_length - (aip->mode == AIM_DOCK ? 0 : 1)) {
|
|
vec3d cp_rel;
|
|
vm_vec_sub(&cp_rel, &gcvp, &Pl_objp->pos);
|
|
float goal_mag = 0.0f;
|
|
if (vm_vec_mag(&cp_rel) > 0.0f) {
|
|
vm_vec_normalize(&cp_rel);
|
|
goal_mag = vm_vec_dot(&Pl_objp->phys_info.vel, &cp_rel);
|
|
}
|
|
min_dist_to_goal = MIN_DIST_TO_WAYPOINT_GOAL + Pl_objp->radius + (goal_mag * sip->damp);
|
|
}
|
|
else {
|
|
min_dist_to_goal = MIN_DIST_TO_WAYPOINT_GOAL + Pl_objp->radius;
|
|
}
|
|
|
|
if ( (vm_vec_dist_quick(&Pl_objp->pos, &gcvp) < min_dist_to_goal) ||
|
|
(((r >= 0.0f) && (r <= 1.0f)) && (vm_vec_dist_quick(&nearest_point, &gcvp) < (MIN_DIST_TO_WAYPOINT_GOAL + Pl_objp->radius)))) {
|
|
aip->path_cur += aip->path_dir;
|
|
if (((aip->path_cur - aip->path_start) > (num_points+1)) || (aip->path_cur < aip->path_start)) {
|
|
Assert(aip->mode != AIM_DOCK); // If docking, should never get this far, getting to last point handled outside ai_path()
|
|
aip->path_dir = -aip->path_dir;
|
|
}
|
|
}
|
|
}
|
|
|
|
return dist_to_goal;
|
|
}
|
|
|
|
float ai_path()
|
|
{
|
|
switch (The_mission.ai_profile->ai_path_mode)
|
|
{
|
|
case AI_PATH_MODE_NORMAL:
|
|
return ai_path_0();
|
|
break;
|
|
case AI_PATH_MODE_ALT1:
|
|
return ai_path_1_or_2(false);
|
|
break;
|
|
case AI_PATH_MODE_ALT2:
|
|
return ai_path_1_or_2(true);
|
|
break;
|
|
default:
|
|
Error(LOCATION, "Invalid path mode found: %d\n", The_mission.ai_profile->ai_path_mode);
|
|
return ai_path_0();
|
|
}
|
|
}
|
|
|
|
// NOTE: this is not the same thing as CLAMP()! CLAMP() adjusts the value, while this adjusts the bounds.
|
|
void update_min_max(float val, float *min, float *max)
|
|
{
|
|
if (val < *min)
|
|
*min = val;
|
|
else if (val > *max)
|
|
*max = val;
|
|
}
|
|
|
|
// Stuff bounding box of all enemy objects within "range" units of object *my_objp.
|
|
// Stuff ni min_vec and max_vec.
|
|
// Return value: Number of enemy objects in bounding box.
|
|
int get_enemy_team_range(object *my_objp, float range, int enemy_team_mask, vec3d *min_vec, vec3d *max_vec)
|
|
{
|
|
object *objp;
|
|
ship_obj *so;
|
|
int count = 0;
|
|
|
|
for (so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so)) {
|
|
objp = &Objects[so->objnum];
|
|
if (objp->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
if (iff_matches_mask(Ships[objp->instance].team, enemy_team_mask)) {
|
|
ship_info* sip = &Ship_info[Ships[objp->instance].ship_info_index];
|
|
if (sip->is_fighter_bomber() || sip->flags[Ship::Info_Flags::Cruiser] || sip->flags[Ship::Info_Flags::Capital] || sip->flags[Ship::Info_Flags::Supercap] || sip->flags[Ship::Info_Flags::Drydock] || sip->flags[Ship::Info_Flags::Corvette] || sip->flags[Ship::Info_Flags::Awacs] || sip->flags[Ship::Info_Flags::Gas_miner])
|
|
if (vm_vec_dist_quick(&my_objp->pos, &objp->pos) < range) {
|
|
if (count == 0) {
|
|
*min_vec = objp->pos;
|
|
*max_vec = objp->pos;
|
|
} else {
|
|
update_min_max(objp->pos.xyz.x, &min_vec->xyz.x, &max_vec->xyz.x);
|
|
update_min_max(objp->pos.xyz.y, &min_vec->xyz.y, &max_vec->xyz.y);
|
|
update_min_max(objp->pos.xyz.z, &min_vec->xyz.z, &max_vec->xyz.z);
|
|
}
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
// Pick a relatively safe spot for objp to fly to.
|
|
// Problem:
|
|
// Finds a spot away from any enemy within a bounding box.
|
|
// Doesn't verify that "safe spot" is not near some other enemy.
|
|
void ai_safety_pick_spot(object *objp)
|
|
{
|
|
int objnum;
|
|
int enemy_team_mask;
|
|
vec3d min_vec, max_vec;
|
|
vec3d fvec_from_center, center;
|
|
vec3d goal_pos;
|
|
|
|
objnum = OBJ_INDEX(objp);
|
|
|
|
enemy_team_mask = iff_get_attacker_mask(obj_team(&Objects[objnum]));
|
|
|
|
if (get_enemy_team_range(objp, 1000.0f, enemy_team_mask, &min_vec, &max_vec)) {
|
|
vm_vec_avg(¢er, &min_vec, &max_vec);
|
|
vm_vec_normalized_dir(&fvec_from_center, &objp->pos, ¢er);
|
|
|
|
vm_vec_scale_add(&goal_pos, ¢er, &fvec_from_center, 2000.0f);
|
|
} else
|
|
vm_vec_scale_add(&goal_pos, &objp->pos, &objp->orient.vec.fvec, 100.0f);
|
|
|
|
Ai_info[Ships[objp->instance].ai_index].goal_point = goal_pos;
|
|
}
|
|
|
|
// Fly to desired safe point.
|
|
// Returns distance to that point.
|
|
float ai_safety_goto_spot(object *objp)
|
|
{
|
|
float dot, dist;
|
|
ai_info *aip;
|
|
vec3d vec_to_goal;
|
|
ship_info *sip;
|
|
float dot_val;
|
|
|
|
sip = &Ship_info[Ships[objp->instance].ship_info_index];
|
|
|
|
aip = &Ai_info[Ships[objp->instance].ai_index];
|
|
dist = vm_vec_normalized_dir(&vec_to_goal, &aip->goal_point, &objp->pos);
|
|
dot = vm_vec_dot(&vec_to_goal, &objp->orient.vec.fvec);
|
|
|
|
dot_val = (1.1f + dot) / 2.0f;
|
|
if (dist > 200.0f) {
|
|
set_accel_for_target_speed(objp, sip->max_speed * dot_val);
|
|
} else
|
|
set_accel_for_target_speed(objp, sip->max_speed * dot_val * (dist/200.0f + 0.2f));
|
|
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_keep_safe_distance]) {
|
|
// hey, let's try actually aiming toward our goal!
|
|
turn_towards_point(objp, &aip->goal_point, nullptr, 0.0f);
|
|
}
|
|
|
|
return dist;
|
|
}
|
|
|
|
void ai_safety_circle_spot(object *objp)
|
|
{
|
|
vec3d goal_point;
|
|
ship_info *sip;
|
|
float dot;
|
|
|
|
sip = &Ship_info[Ships[objp->instance].ship_info_index];
|
|
|
|
goal_point = Ai_info[Ships[objp->instance].ai_index].goal_point;
|
|
dot = turn_towards_tangent(objp, &goal_point, 250.0f); // Increased from 50 to 250 to make circling not look so wacky.
|
|
|
|
set_accel_for_target_speed(objp, 0.5f * (1.0f + dot) * sip->max_speed/4.0f);
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
void ai_safety()
|
|
{
|
|
ai_info *aip;
|
|
|
|
aip = &Ai_info[Ships[Pl_objp->instance].ai_index];
|
|
|
|
switch (aip->submode) {
|
|
case AISS_1:
|
|
ai_safety_pick_spot(Pl_objp);
|
|
aip->submode = AISS_2;
|
|
aip->submode_start_time = Missiontime;
|
|
break;
|
|
case AISS_2:
|
|
if (ai_safety_goto_spot(Pl_objp) < 25.0f) {
|
|
aip->submode = AISS_3;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
break;
|
|
case AISS_3:
|
|
ai_safety_circle_spot(Pl_objp);
|
|
break;
|
|
case AISS_1a: // Pick a safe point because we just got whacked!
|
|
default:
|
|
UNREACHABLE("Bad AI submode for ai_safety mode of %d. Please report to the SCP!", aip->submode); // Illegal submode for ai_safety();
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
// --------------------------------------------------------------------------
|
|
// make Pl_objp fly to its target's position
|
|
// optionally returns true if Pl_objp (pl_done_p) has reached the target position (can be NULL)
|
|
// optionally returns true if Pl_objp's (pl_treat_as_ship_p) fly to order was issued to him directly,
|
|
// or if the order was issued to his wing (returns false) (can be NULL, and result is only
|
|
// valid if Pl_done_p was not NULL and was set true by function.
|
|
void ai_fly_to_target_position(const vec3d* target_pos, bool* pl_done_p=NULL, bool* pl_treat_as_ship_p=NULL)
|
|
{
|
|
float dot, dist_to_goal;
|
|
ship *shipp = &Ships[Pl_objp->instance];
|
|
ship_info *sip = &Ship_info[shipp->ship_info_index];
|
|
ai_info *aip;
|
|
vec3d nvel_vec;
|
|
float mag;
|
|
float prev_dot_to_goal;
|
|
vec3d temp_vec;
|
|
vec3d *slop_vec;
|
|
int j;
|
|
bool ab_allowed = true;
|
|
|
|
Assert( target_pos != NULL );
|
|
|
|
if ( pl_done_p != NULL ) {
|
|
// initialize this to false now incase we leave early so that that
|
|
// the caller of this function doesn't get any funny ideas about
|
|
// the status of the waypoint
|
|
*pl_done_p = false;
|
|
}
|
|
|
|
aip = &Ai_info[shipp->ai_index];
|
|
|
|
/* I shouldn't be flying to position for what ever called me any more.
|
|
Set mode to none so that default dynamic behaviour gets started up again. */
|
|
if ( (aip->active_goal == AI_ACTIVE_GOAL_NONE) || (aip->active_goal == AI_ACTIVE_GOAL_DYNAMIC) ) {
|
|
aip->mode = AIM_NONE;
|
|
}
|
|
|
|
dist_to_goal = vm_vec_dist_quick(&Pl_objp->pos, target_pos);
|
|
|
|
// Can't use fvec, need to use velocity vector because we aren't necessarily
|
|
// moving in the direction we're facing.
|
|
// AL 23-3-98: Account for very small velocities by checking result of vm_vec_mag().
|
|
// If we don't vm_vec_copy_normalize() will think it is normalizing a null vector.
|
|
if ( vm_vec_mag_quick(&Pl_objp->phys_info.vel) < AICODE_SMALL_MAGNITUDE ) {
|
|
mag = 0.0f;
|
|
vm_vec_zero(&nvel_vec);
|
|
} else {
|
|
mag = vm_vec_copy_normalize(&nvel_vec, &Pl_objp->phys_info.vel);
|
|
}
|
|
|
|
// If moving not-very-slowly and sliding, then try to slide at goal, rather than
|
|
// point at goal.
|
|
slop_vec = NULL;
|
|
if (mag < 1.0f) {
|
|
nvel_vec = Pl_objp->orient.vec.fvec;
|
|
} else if (mag > 5.0f) {
|
|
float nv_dot;
|
|
nv_dot = vm_vec_dot(&Pl_objp->orient.vec.fvec, &nvel_vec);
|
|
if ((nv_dot > 0.5f) && (nv_dot < 0.97f)) {
|
|
slop_vec = &temp_vec;
|
|
vm_vec_sub(slop_vec, &nvel_vec, &Pl_objp->orient.vec.fvec);
|
|
}
|
|
}
|
|
|
|
// If a wing leader, take turns more slowly, based on size of wing.
|
|
int scale;
|
|
|
|
if (shipp->wingnum >= 0) {
|
|
scale = Wings[shipp->wingnum].current_count;
|
|
scale = (int) ((scale+1)/2);
|
|
} else {
|
|
scale = 1;
|
|
}
|
|
|
|
// ----------------------------------------------
|
|
// if in autopilot mode make sure to not collide
|
|
// and "keep reasonable distance"
|
|
// this needs to be done for ALL SHIPS not just capships STOP CHANGING THIS
|
|
// ----------------------------------------------
|
|
|
|
vec3d goal_point;
|
|
|
|
bool carry_flag = ((shipp->flags[Ship::Ship_Flags::Navpoint_carry]) || ((shipp->wingnum >= 0) && (Wings[shipp->wingnum].flags[Ship::Wing_Flags::Nav_carry])));
|
|
|
|
float waypoint_turnrate = 1 / (3.0f * scale);
|
|
|
|
// for retail compatability reasons, have to take into account rotdamp so that ships with less than 1 rotdamp have increased waypoint turnrate
|
|
// please see PR #2740 and #3494 for more information
|
|
if (Pl_objp->phys_info.rotdamp < 1.0f )
|
|
// start increasing the turnrate to 2x at 0.5 rotdamp and 3x at 0
|
|
waypoint_turnrate *= (3 - 2 * Pl_objp->phys_info.rotdamp);
|
|
|
|
vec3d turnrate_mod;
|
|
vm_vec_make(&turnrate_mod, waypoint_turnrate, waypoint_turnrate, waypoint_turnrate);
|
|
|
|
if (AutoPilotEngaged)
|
|
ab_allowed = false;
|
|
|
|
if (AutoPilotEngaged
|
|
&& timestamp_elapsed(LockAPConv)
|
|
&& carry_flag
|
|
&& ((The_mission.flags[Mission::Mission_Flags::Use_ap_cinematics]) || (Pl_objp != Autopilot_flight_leader)) )
|
|
{
|
|
vec3d perp;
|
|
Assertion( Autopilot_flight_leader != NULL, "When under autopilot there must be a flight leader" );
|
|
// snap wings into formation
|
|
if (The_mission.flags[Mission::Mission_Flags::Use_ap_cinematics]) {
|
|
if (shipp->wingnum != -1) {
|
|
int wing_index = get_wing_index(Pl_objp, shipp->wingnum);
|
|
object *wing_leader = get_wing_leader(shipp->wingnum);
|
|
|
|
if (wing_leader != Pl_objp) {
|
|
// not wing leader.. get my position relative to wing leader
|
|
get_absolute_wing_pos(&goal_point, wing_leader, shipp->wingnum, wing_index, aip->ai_flags[AI::AI_Flags::Formation_object], true);
|
|
} else {
|
|
// Am wing leader.. get the wings position relative to the flight leader
|
|
j = 1+int( (float)floor(double(autopilot_wings[shipp->wingnum]-1)/2.0) );
|
|
|
|
switch (autopilot_wings[shipp->wingnum] % 2) {
|
|
case 1: // back-left
|
|
vm_vec_copy_normalize(&perp, &Autopilot_flight_leader->orient.vec.rvec);
|
|
vm_vec_scale(&perp, -166.0f*j); // 166m is supposedly the optimal range according to tolwyn
|
|
vm_vec_add(&goal_point, &Autopilot_flight_leader->pos, &perp);
|
|
break;
|
|
|
|
default: //back-right
|
|
case 0:
|
|
vm_vec_copy_normalize(&perp, &Autopilot_flight_leader->orient.vec.rvec);
|
|
vm_vec_scale(&perp, 166.0f*j);
|
|
vm_vec_add(&goal_point, &Autopilot_flight_leader->pos, &perp);
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
Pl_objp->pos = goal_point;
|
|
}
|
|
|
|
vm_vec_normalized_dir(&perp, Navs[CurrentNav].GetPosition(), &Autopilot_flight_leader->pos);
|
|
vm_vector_2_matrix_norm(&Pl_objp->orient, &perp, nullptr, nullptr);
|
|
} else {
|
|
vm_vec_scale_add(&perp, &Pl_objp->pos, &Autopilot_flight_leader->phys_info.vel, 1000.0f);
|
|
ai_turn_towards_vector(&perp, Pl_objp, slop_vec, nullptr, 0.0f, 0, nullptr, &turnrate_mod);
|
|
}
|
|
} else {
|
|
if (dist_to_goal > 0.1f) {
|
|
ai_turn_towards_vector(target_pos, Pl_objp, slop_vec, nullptr, 0.0f, 0, nullptr, &turnrate_mod);
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------
|
|
|
|
prev_dot_to_goal = aip->prev_dot_to_goal;
|
|
dot = vm_vec_dot_to_point(&nvel_vec, &Pl_objp->pos, target_pos);
|
|
aip->prev_dot_to_goal = dot;
|
|
|
|
if (Pl_objp->phys_info.speed < 0.0f) {
|
|
accelerate_ship(aip, 1.0f/32);
|
|
ab_allowed = false;
|
|
} else if (prev_dot_to_goal > dot+0.01f) {
|
|
// We are further from pointing at our goal this frame than last frame, so slow down.
|
|
set_accel_for_target_speed(Pl_objp, Pl_objp->phys_info.speed * 0.95f);
|
|
ab_allowed = false;
|
|
} else if (dist_to_goal < 100.0f) {
|
|
float slew_dot = vm_vec_dot(&Pl_objp->orient.vec.fvec, &nvel_vec);
|
|
if (fl_abs(slew_dot) < 0.9f) {
|
|
accelerate_ship(aip, 0.0f);
|
|
} else if (dot < 0.88f + 0.1f*(100.0f - dist_to_goal)/100.0f) {
|
|
accelerate_ship(aip, 0.0f);
|
|
} else {
|
|
accelerate_ship(aip, 0.5f * dot * dot);
|
|
}
|
|
ab_allowed = false;
|
|
} else {
|
|
float dot1;
|
|
if (dist_to_goal < 250.0f) {
|
|
dot1 = dot*dot*dot; // Very important to be pointing towards goal when nearby. Note, cubing preserves sign.
|
|
} else {
|
|
if (dot > 0.0f) {
|
|
dot1 = dot*dot;
|
|
} else {
|
|
dot1 = dot;
|
|
}
|
|
}
|
|
|
|
if (dist_to_goal > 100.0f + Pl_objp->radius * 2) {
|
|
if (dot < 0.2f) {
|
|
dot1 = 0.2f;
|
|
}
|
|
}
|
|
|
|
if (sip->is_small_ship()) {
|
|
set_accel_for_target_speed(Pl_objp, dot1 * dist_to_goal/5.0f);
|
|
} else {
|
|
set_accel_for_target_speed(Pl_objp, dot1 * dist_to_goal/10.0f);
|
|
}
|
|
if (dot1 < 0.8f)
|
|
ab_allowed = false;
|
|
}
|
|
|
|
if (!(sip->flags[Ship::Info_Flags::Afterburner]) || (shipp->flags[Ship::Ship_Flags::Afterburner_locked]) ||
|
|
!(aip->ai_flags[AI::AI_Flags::Free_afterburner_use] ||
|
|
aip->ai_profile_flags[AI::Profile_Flags::Free_afterburner_use] ||
|
|
ai_willing_to_afterburn_hard(aip)))
|
|
{
|
|
ab_allowed = false;
|
|
}
|
|
|
|
// Make sure not travelling too fast for someone to keep up.
|
|
float max_allowed_speed = 9999.9f;
|
|
float max_allowed_ab_speed = 10000.1f;
|
|
float self_ab_speed = 10000.0f;
|
|
|
|
if (shipp->wingnum != -1 && !ai_willing_to_afterburn_hard(aip)) {
|
|
max_allowed_speed = 0.9f * get_wing_lowest_max_speed(Pl_objp);
|
|
max_allowed_ab_speed = 0.95f * get_wing_lowest_av_ab_speed(Pl_objp);
|
|
if (ab_allowed) {
|
|
float self_ab_scale = ets_power_factor(Pl_objp) * Energy_levels[shipp->engine_recharge_index] * 2.0f * The_mission.ai_profile->afterburner_recharge_scale[Game_skill_level];
|
|
self_ab_scale = sip->afterburner_recover_rate * self_ab_scale / (sip->afterburner_burn_rate + sip->afterburner_recover_rate * self_ab_scale);
|
|
self_ab_speed = 0.95f * (self_ab_scale * (Pl_objp->phys_info.afterburner_max_vel.xyz.z - Pl_objp->phys_info.max_vel.xyz.z) + Pl_objp->phys_info.max_vel.xyz.z);
|
|
}
|
|
}
|
|
|
|
// check if waypoint speed cap is set and adjust max speed
|
|
if (aip->waypoint_speed_cap > 0) {
|
|
max_allowed_speed = (float) aip->waypoint_speed_cap;
|
|
max_allowed_ab_speed = max_allowed_speed;
|
|
}
|
|
|
|
if ((self_ab_speed < 5.0f) || (max_allowed_ab_speed < 5.0f)){ // || ((shipp->wingnum != -1) && (dist_to_goal / max_allowed_ab_speed < 5.0f))){
|
|
ab_allowed = false;
|
|
} else if (max_allowed_ab_speed < Pl_objp->phys_info.max_vel.xyz.z) {
|
|
if (max_allowed_speed < max_allowed_ab_speed) {
|
|
max_allowed_speed = max_allowed_ab_speed;
|
|
} else {
|
|
ab_allowed = false;
|
|
}
|
|
}
|
|
|
|
|
|
if (ab_allowed) {
|
|
if (self_ab_speed <= (1.001 * max_allowed_ab_speed)){
|
|
if (!(Pl_objp->phys_info.flags & PF_AFTERBURNER_ON )) {
|
|
float percent_left = 100.0f * shipp->afterburner_fuel / sip->afterburner_fuel_capacity;
|
|
if (percent_left > 30.0f) {
|
|
afterburners_start(Pl_objp);
|
|
aip->afterburner_stop_time = Missiontime + 5 * F1_0;
|
|
}
|
|
}
|
|
} else {
|
|
float switch_value;
|
|
if ((self_ab_speed - max_allowed_ab_speed) > (0.2f * max_allowed_ab_speed)) {
|
|
switch_value = 0.2f * max_allowed_ab_speed;
|
|
} else {
|
|
switch_value = self_ab_speed - max_allowed_ab_speed;
|
|
}
|
|
if (!(Pl_objp->phys_info.flags & PF_AFTERBURNER_ON )) {
|
|
float percent_left = 100.0f * shipp->afterburner_fuel / sip->afterburner_fuel_capacity;
|
|
if ((Pl_objp->phys_info.speed < (max_allowed_ab_speed - switch_value)) && (percent_left > 30.0f)){
|
|
afterburners_start(Pl_objp);
|
|
aip->afterburner_stop_time = Missiontime + 5 * F1_0;
|
|
}
|
|
} else {
|
|
if (Pl_objp->phys_info.speed > (max_allowed_ab_speed + 0.9 * switch_value)){
|
|
afterburners_stop(Pl_objp);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!(Pl_objp->phys_info.flags & PF_AFTERBURNER_ON )) {
|
|
if (aip->prev_accel * Pl_objp->phys_info.max_vel.xyz.z > max_allowed_ab_speed) {
|
|
accelerate_ship(aip, max_allowed_ab_speed / Pl_objp->phys_info.max_vel.xyz.z);
|
|
}
|
|
}
|
|
} else {
|
|
if (Pl_objp->phys_info.flags & PF_AFTERBURNER_ON ) {
|
|
afterburners_stop(Pl_objp);
|
|
}
|
|
if (aip->prev_accel * Pl_objp->phys_info.max_vel.xyz.z > max_allowed_speed) {
|
|
accelerate_ship(aip, max_allowed_speed / Pl_objp->phys_info.max_vel.xyz.z);
|
|
}
|
|
}
|
|
|
|
float dist_to_cover_this_frame = (Pl_objp->phys_info.speed * flFrametime);
|
|
|
|
// Retail's completion distance of sqrt(radius) is only ~32m for a capital ship with a 1000m radius --
|
|
// far smaller than such a ship's turning circle -- and retail also skipped the completion check
|
|
// entirely unless the ship covered 0.1m in a single frame, a framerate-dependent gate that excludes
|
|
// exactly the slow speeds at which a big ship could aim precisely. Together these could trap a
|
|
// capital ship in an endless overshoot-turnaround-miss loop at its final waypoint. With the flag
|
|
// set, big ships complete a waypoint anywhere within their own radius of it, and the check always runs.
|
|
bool fix_waypoint_completion = The_mission.ai_profile->flags[AI::Profile_Flags::Fix_big_ship_waypoint_completion];
|
|
float waypoint_tolerance = MIN_DIST_TO_WAYPOINT_GOAL + ((fix_waypoint_completion && sip->is_big_or_huge()) ? Pl_objp->radius : fl_sqrt(Pl_objp->radius));
|
|
|
|
if ( fix_waypoint_completion || (dist_to_goal < MIN_DIST_TO_WAYPOINT_GOAL) || dist_to_cover_this_frame > 0.1f ) {
|
|
vec3d nearest_point;
|
|
float r;
|
|
|
|
r = find_nearest_point_on_line(&nearest_point, &Pl_objp->last_pos, &Pl_objp->pos, target_pos);
|
|
|
|
if ( (dist_to_goal < (waypoint_tolerance + dist_to_cover_this_frame))
|
|
|| (((r >= 0.0f) && (r <= 1.0f)) && (vm_vec_dist_quick(&nearest_point, target_pos) < waypoint_tolerance)))
|
|
{
|
|
int treat_as_ship;
|
|
|
|
// we must be careful when dealing with wings. A ship in a wing might be completing
|
|
// a waypoint for for the entire wing, or it might be completing a goal for itself. If
|
|
// for itself and in a wing, treat the completion as we would a ship
|
|
treat_as_ship = 1;
|
|
if ( shipp->wingnum != -1 ) {
|
|
// protect array access from invalid indexes
|
|
if ((aip->active_goal >= 0) && (aip->active_goal < MAX_AI_GOALS)) {
|
|
auto type = aip->goals[aip->active_goal].type;
|
|
if ( (type == ai_goal_type::EVENT_WING) || (type == ai_goal_type::PLAYER_WING) ) {
|
|
treat_as_ship = 0;
|
|
} else {
|
|
treat_as_ship = 1;
|
|
}
|
|
}
|
|
}
|
|
// setup out parameters
|
|
if ( pl_done_p != NULL ) {
|
|
*pl_done_p = true;
|
|
if ( pl_treat_as_ship_p != NULL ) {
|
|
if ( treat_as_ship ) {
|
|
*pl_treat_as_ship_p = true;
|
|
} else {
|
|
*pl_treat_as_ship_p = false;
|
|
}
|
|
}
|
|
} else {
|
|
Assertion( pl_treat_as_ship_p != NULL,
|
|
"pl_done_p cannot be NULL while pl_treat_as_ship_p is not NULL" );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// make Pl_objp fly waypoints.
|
|
void ai_waypoints()
|
|
{
|
|
ai_info *aip = &Ai_info[Ships[Pl_objp->instance].ai_index];
|
|
|
|
// sanity checking for stuff that should never happen
|
|
Assertion(aip->wp_index != INVALID_WAYPOINT_POSITION, "Waypoints should have been started already!");
|
|
if (aip->wp_index == INVALID_WAYPOINT_POSITION) {
|
|
ai_start_waypoints(Pl_objp, (aip->wp_list_index < 0) ? 0 : aip->wp_list_index, WPF_REPEAT, 0);
|
|
}
|
|
|
|
auto wp_list = find_waypoint_list_at_index(aip->wp_list_index);
|
|
Assert(wp_list && !wp_list->get_waypoints().empty()); // What? Is this zero? Probably never got initialized!
|
|
|
|
bool done, treat_as_ship;
|
|
ai_fly_to_target_position(wp_list->get_waypoints()[aip->wp_index].get_pos(), &done, &treat_as_ship);
|
|
|
|
if ( done ) {
|
|
bool reached_end;
|
|
// go on to next waypoint in path
|
|
if (aip->wp_flags & WPF_BACKTRACK) {
|
|
--aip->wp_index;
|
|
reached_end = (aip->wp_index == -1);
|
|
} else {
|
|
++aip->wp_index;
|
|
reached_end = (aip->wp_index == static_cast<int>(wp_list->get_waypoints().size()));
|
|
}
|
|
|
|
if (reached_end) {
|
|
// have reached the last waypoint. Do I repeat?
|
|
if ( aip->wp_flags & WPF_REPEAT ) {
|
|
// go back to the start.
|
|
if (aip->wp_flags & WPF_BACKTRACK) {
|
|
aip->wp_index = static_cast<int>(wp_list->get_waypoints().size()) - 1;
|
|
} else {
|
|
aip->wp_index = 0;
|
|
}
|
|
} else {
|
|
// stay on the last waypoint.
|
|
if (aip->wp_flags & WPF_BACKTRACK) {
|
|
++aip->wp_index;
|
|
} else {
|
|
--aip->wp_index;
|
|
}
|
|
|
|
// Log a message that the wing or ship reached his waypoint and
|
|
// remove the goal from the AI goals of the ship pr wing, respectively.
|
|
// Whether we should treat this as a ship or a wing is determined by
|
|
// ai_fly_to_target_position when it marks the AI directive as complete
|
|
if ( treat_as_ship ) {
|
|
ai_mission_goal_complete( aip ); // this call should reset the AI mode
|
|
mission_log_add_entry( LOG_WAYPOINTS_DONE, Ships[Pl_objp->instance].ship_name, wp_list->get_name(), -1 );
|
|
} else {
|
|
ai_mission_wing_goal_complete( Ships[Pl_objp->instance].wingnum, &(aip->goals[aip->active_goal]) );
|
|
mission_log_add_entry( LOG_WAYPOINTS_DONE, Wings[Ships[Pl_objp->instance].wingnum].name, wp_list->get_name(), -1 );
|
|
}
|
|
// adds scripting hook for 'On Waypoints Done' --wookieejedi
|
|
if (scripting::hooks::OnWaypointsDone->isActive()) {
|
|
scripting::hooks::OnWaypointsDone->run(scripting::hooks::ShipSourceConditions{ &Ships[Pl_objp->instance] },
|
|
scripting::hook_param_list(
|
|
scripting::hook_param("Ship", 'o', Pl_objp),
|
|
scripting::hook_param("Wing", 'o', scripting::api::l_Wing.Set(Ships[Pl_objp->instance].wingnum)),
|
|
scripting::hook_param("Waypointlist", 'o', scripting::api::l_WaypointList.Set(scripting::api::waypointlist_h(wp_list)))
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Make Pl_objp avoid En_objp
|
|
// Not like evading. This is for avoiding a collision!
|
|
// Note, use sliding if available.
|
|
void avoid_ship()
|
|
{
|
|
// To avoid an object, turn towards right or left vector until facing away from object.
|
|
// To choose right vs. left, pick one that is further from center of avoid object.
|
|
// Keep turning away from until pointing away from ship.
|
|
// Stay in avoid mode until at least 3 enemy ship radii away.
|
|
|
|
// Speed setting:
|
|
// If inside sphere, zero speed and turn towards outside.
|
|
// If outside sphere, inside 2x sphere, set speed percent of max to:
|
|
// max(away_dot, (dist-rad)/rad)
|
|
// where away_dot is dot(Pl_objp->fvec, vec_En_objp_to_Pl_objp)
|
|
|
|
vec3d vec_to_enemy;
|
|
float away_dot;
|
|
float dist;
|
|
ship *shipp = &Ships[Pl_objp->instance];
|
|
ship_info *sip = &Ship_info[shipp->ship_info_index];
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
vec3d player_pos, enemy_pos;
|
|
|
|
// if we're avoiding a stealth ship, then we know where he is, update with no error
|
|
if ( is_object_stealth_ship(En_objp) ) {
|
|
update_ai_stealth_info_with_error(aip/*, 1*/);
|
|
}
|
|
|
|
ai_set_positions(Pl_objp, En_objp, aip, &player_pos, &enemy_pos);
|
|
vm_vec_sub(&vec_to_enemy, &enemy_pos, &Pl_objp->pos);
|
|
|
|
dist = vm_vec_normalize(&vec_to_enemy);
|
|
away_dot = -vm_vec_dot(&Pl_objp->orient.vec.fvec, &vec_to_enemy);
|
|
|
|
if ((sip->max_vel.xyz.x > 0.0f) || (sip->max_vel.xyz.y > 0.0f)) {
|
|
if (vm_vec_dot(&Pl_objp->orient.vec.rvec, &vec_to_enemy) > 0.0f) {
|
|
AI_ci.sideways = -1.0f;
|
|
} else {
|
|
AI_ci.sideways = 1.0f;
|
|
}
|
|
if (vm_vec_dot(&Pl_objp->orient.vec.uvec, &vec_to_enemy) > 0.0f) {
|
|
AI_ci.vertical = -1.0f;
|
|
} else {
|
|
AI_ci.vertical = 1.0f;
|
|
}
|
|
}
|
|
|
|
// If in front of enemy, turn away from it.
|
|
// If behind enemy, try to get fully behind it.
|
|
if (away_dot < 0.0f) {
|
|
turn_away_from_point(Pl_objp, &enemy_pos, 1.0f);
|
|
} else {
|
|
vec3d goal_pos;
|
|
|
|
vm_vec_scale_add(&goal_pos, &En_objp->pos, &En_objp->orient.vec.fvec, -100.0f);
|
|
turn_towards_point(Pl_objp, &goal_pos, NULL, 1.0f);
|
|
}
|
|
|
|
// Set speed.
|
|
float radsum = Pl_objp->radius + En_objp->radius;
|
|
|
|
if (dist < radsum)
|
|
accelerate_ship(aip, MAX(away_dot, 0.2f));
|
|
else if (dist < 2*radsum)
|
|
accelerate_ship(aip, MAX(away_dot, (dist - radsum) / radsum)+0.2f);
|
|
else
|
|
accelerate_ship(aip, 1.0f);
|
|
|
|
}
|
|
|
|
// Maybe it's time to resume the previous AI mode in aip->previous_mode.
|
|
// Each type of previous_mode has its own criteria on when to resume.
|
|
// Return true if previous mode was resumed.
|
|
int maybe_resume_previous_mode(object *objp, ai_info *aip)
|
|
{
|
|
// Only (maybe) resume previous goal if current goal is dynamic.
|
|
if (aip->active_goal != AI_ACTIVE_GOAL_DYNAMIC)
|
|
return 0;
|
|
|
|
if (aip->mode == AIM_EVADE_WEAPON) {
|
|
if (timestamp_elapsed(aip->mode_time) || (((aip->nearest_locked_object == -1) || (Objects[aip->nearest_locked_object].type != OBJ_WEAPON)) && (aip->danger_weapon_objnum == -1))) {
|
|
Assert(aip->previous_mode != AIM_EVADE_WEAPON);
|
|
aip->mode = aip->previous_mode;
|
|
aip->submode = aip->previous_submode;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->active_goal = AI_ACTIVE_GOAL_NONE;
|
|
aip->mode_time = -1; // Means do forever.
|
|
return 1;
|
|
}
|
|
} else if ( aip->previous_mode == AIM_GUARD) {
|
|
if ((aip->guard_objnum != -1) && (aip->guard_signature == Objects[aip->guard_objnum].signature)) {
|
|
object *guard_objp;
|
|
float dist;
|
|
|
|
guard_objp = &Objects[aip->guard_objnum];
|
|
dist = vm_vec_dist_quick(&guard_objp->pos, &objp->pos);
|
|
|
|
// 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 ((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)) {
|
|
Assert(aip->previous_mode == AIM_GUARD);
|
|
aip->mode = aip->previous_mode;
|
|
aip->submode = AIS_GUARD_PATROL;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->active_goal = AI_ACTIVE_GOAL_NONE;
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
// Call this function if you want something to happen on average every N quarters of a second.
|
|
// The truth value returned by this function will be the same for any given quarter second interval.
|
|
// The value "num" is only passed in to get asynchronous behavior for different objects.
|
|
// modulus == 1 will always return true.
|
|
// modulus == 2 will return true half the time.
|
|
// modulus == 16 will return true for one quarter second interval every four seconds.
|
|
int static_rand_timed(int num, int modulus)
|
|
{
|
|
if (modulus < 2)
|
|
return 1;
|
|
else {
|
|
int t;
|
|
|
|
t = Missiontime >> 18; // Get time in quarters of a second (SUSHI: this comment is wrong! It's actually every 4 seconds!)
|
|
t += num;
|
|
|
|
return !(t % modulus);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maybe fire afterburner based on AI class
|
|
*/
|
|
int ai_maybe_fire_afterburner(object *objp, ai_info *aip)
|
|
{
|
|
// bail if the ship doesn't even have an afterburner
|
|
if (!(Ship_info[Ships[objp->instance].ship_info_index].flags[Ship::Info_Flags::Afterburner])) {
|
|
return 0;
|
|
}
|
|
if (aip->ai_aburn_use_factor == INT_MIN && ai_maybe_autoscale(aip->ai_class) == 0) {
|
|
return 0; // Lowest [autoscaled] level never aburners away (unless ai_aburn_use_factor is specified)
|
|
}
|
|
else {
|
|
// Maybe don't afterburner because of a potential collision with the player.
|
|
// If not multiplayer, near player and player in front, probably don't afterburner.
|
|
if (!(Game_mode & GM_MULTIPLAYER)) {
|
|
if (Ships[objp->instance].team == Player_ship->team) {
|
|
float dist;
|
|
|
|
dist = vm_vec_dist_quick(&objp->pos, &Player_obj->pos) - Player_obj->radius - objp->radius;
|
|
if (dist < 150.0f) {
|
|
vec3d v2p;
|
|
float dot;
|
|
|
|
vm_vec_normalized_dir(&v2p, &Player_obj->pos, &objp->pos);
|
|
dot = vm_vec_dot(&v2p, &objp->orient.vec.fvec);
|
|
|
|
if (dot > 0.0f) {
|
|
if (dot * dist > 50.0f)
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//If ai_aburn_use_factor is not specified, calculate a number based on the AI class. Otherwise, use that value.
|
|
if (aip->ai_aburn_use_factor == INT_MIN)
|
|
{
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Adjusted_AI_class_autoscale])
|
|
{
|
|
// Highest two levels always aburner away
|
|
if (ai_get_autoscale_index(aip->ai_class) >= ai_get_autoscale_index(Num_ai_classes) - 2)
|
|
return 1;
|
|
|
|
return static_rand_timed(OBJ_INDEX(objp), ai_get_autoscale_index(Num_ai_classes) - ai_get_autoscale_index(aip->ai_class));
|
|
}
|
|
else
|
|
{
|
|
// Highest two levels always aburner away
|
|
if (aip->ai_class >= Num_ai_classes-2)
|
|
return 1;
|
|
|
|
return static_rand_timed(OBJ_INDEX(objp), Num_ai_classes - aip->ai_class);
|
|
}
|
|
}
|
|
else
|
|
return static_rand_timed(OBJ_INDEX(objp), aip->ai_aburn_use_factor);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maybe engage afterburner after being hit by an object.
|
|
*/
|
|
void maybe_afterburner_after_ship_hit(object *objp, ai_info *aip, object *en_objp)
|
|
{
|
|
// Only do if facing a little away.
|
|
if (en_objp != NULL) {
|
|
vec3d v2e;
|
|
|
|
vm_vec_normalized_dir(&v2e, &en_objp->pos, &objp->pos);
|
|
if (vm_vec_dot(&v2e, &objp->orient.vec.fvec) > -0.5f)
|
|
return;
|
|
}
|
|
|
|
if (!( objp->phys_info.flags & PF_AFTERBURNER_ON )) {
|
|
if (ai_maybe_fire_afterburner(objp, aip)) {
|
|
afterburners_start(objp);
|
|
aip->afterburner_stop_time = Missiontime + F1_0/2;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Is an instructor if name begins INSTRUCTOR_SHIP_NAME else not.
|
|
* @return true if object *objp is an instructor.
|
|
*/
|
|
int is_instructor(object *objp)
|
|
{
|
|
return !strnicmp(Ships[objp->instance].ship_name, INSTRUCTOR_SHIP_NAME, strlen(INSTRUCTOR_SHIP_NAME));
|
|
}
|
|
|
|
// Evade the weapon aip->danger_weapon_objnum
|
|
// If it's not valid, do a quick out.
|
|
// Evade by accelerating hard.
|
|
// If necessary, turn hard left or hard right.
|
|
void evade_weapon()
|
|
{
|
|
object *weapon_objp = NULL;
|
|
object *unlocked_weapon_objp = NULL, *locked_weapon_objp = NULL;
|
|
vec3d weapon_pos, player_pos, goal_point;
|
|
vec3d vec_from_enemy;
|
|
float dot_from_enemy, dot_to_enemy;
|
|
float dist;
|
|
ship *shipp = &Ships[Pl_objp->instance];
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
|
|
if (is_instructor(Pl_objp))
|
|
return;
|
|
|
|
// Make sure we're actually being attacked.
|
|
// Favor locked objects.
|
|
if (aip->nearest_locked_object != -1) {
|
|
if (Objects[aip->nearest_locked_object].type == OBJ_WEAPON)
|
|
locked_weapon_objp = &Objects[aip->nearest_locked_object];
|
|
}
|
|
|
|
if (aip->danger_weapon_objnum != -1) {
|
|
if (Objects[aip->danger_weapon_objnum].signature == aip->danger_weapon_signature)
|
|
unlocked_weapon_objp = &Objects[aip->danger_weapon_objnum];
|
|
else
|
|
aip->danger_weapon_objnum = -1; // Signatures don't match, so no longer endangered.
|
|
}
|
|
|
|
if (locked_weapon_objp != NULL) {
|
|
if (unlocked_weapon_objp != NULL) {
|
|
if (vm_vec_dist_quick(&locked_weapon_objp->pos, &Pl_objp->pos) < 1.5f * vm_vec_dist_quick(&unlocked_weapon_objp->pos, &Pl_objp->pos))
|
|
weapon_objp = locked_weapon_objp;
|
|
else
|
|
weapon_objp = unlocked_weapon_objp;
|
|
} else
|
|
weapon_objp = locked_weapon_objp;
|
|
} else if (unlocked_weapon_objp != NULL)
|
|
weapon_objp = unlocked_weapon_objp;
|
|
else {
|
|
if (aip->mode == AIM_EVADE_WEAPON)
|
|
maybe_resume_previous_mode(Pl_objp, aip);
|
|
return;
|
|
}
|
|
|
|
Assert(weapon_objp != NULL);
|
|
|
|
if (weapon_objp->type != OBJ_WEAPON) {
|
|
if (aip->mode == AIM_EVADE_WEAPON)
|
|
maybe_resume_previous_mode(Pl_objp, aip);
|
|
return;
|
|
}
|
|
|
|
weapon_pos = weapon_objp->pos;
|
|
player_pos = Pl_objp->pos;
|
|
|
|
// Make speed based on skill level, varying at highest skill level, which is harder to hit.
|
|
accelerate_ship(aip, 1.0f);
|
|
|
|
dist = vm_vec_normalized_dir(&vec_from_enemy, &player_pos, &weapon_pos);
|
|
|
|
dot_to_enemy = -vm_vec_dot(&Pl_objp->orient.vec.fvec, &vec_from_enemy);
|
|
dot_from_enemy = vm_vec_dot(&weapon_objp->orient.vec.fvec, &vec_from_enemy);
|
|
|
|
// If shot is incoming...
|
|
if (dot_from_enemy < 0.3f) {
|
|
if (weapon_objp == unlocked_weapon_objp)
|
|
aip->danger_weapon_objnum = -1;
|
|
return;
|
|
} else if (dot_from_enemy > 0.7f) {
|
|
bool should_afterburn = dist < 200.0f;
|
|
int aburn_time = F1_0 / 2;
|
|
|
|
// within 200m bad, within 1.5 seconds good
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Improved_missile_avoidance]) {
|
|
should_afterburn = dist < weapon_objp->phys_info.speed * 1.5f;
|
|
aburn_time = F1_0 + F1_0 / 2;
|
|
}
|
|
|
|
if (should_afterburn) {
|
|
if (!( Pl_objp->phys_info.flags & PF_AFTERBURNER_ON )) {
|
|
if (ai_maybe_fire_afterburner(Pl_objp, aip)) {
|
|
afterburners_start(Pl_objp);
|
|
aip->afterburner_stop_time = Missiontime + aburn_time;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fancy (read: effective) dodging
|
|
// Try to go perpindicular to the incoming missile, and pick the quickest direction to get there
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Improved_missile_avoidance]) {
|
|
vec3d avoid_point;
|
|
vm_project_point_onto_plane(&avoid_point, &vec_from_enemy, &Pl_objp->orient.vec.fvec, &vmd_zero_vector);
|
|
vm_vec_normalize(&avoid_point);
|
|
if (vm_vec_dot(&Pl_objp->orient.vec.fvec, &vec_from_enemy) > 0.f)
|
|
vm_vec_negate(&avoid_point);
|
|
vm_vec_scale_add(&avoid_point, &Pl_objp->pos, &avoid_point, 200.0f);
|
|
|
|
turn_towards_point(Pl_objp, &avoid_point, nullptr, 0.0f);
|
|
}
|
|
// If we're sort of pointing towards it...
|
|
else if ((dot_to_enemy < -0.5f) || (dot_to_enemy > 0.5f)) {
|
|
float rdot;
|
|
float udot;
|
|
|
|
// Turn hard left or right, depending on which gets out of way quicker.
|
|
//SUSHI: Also possibly turn up or down.
|
|
rdot = vm_vec_dot(&Pl_objp->orient.vec.rvec, &vec_from_enemy);
|
|
udot = vm_vec_dot(&Pl_objp->orient.vec.uvec, &vec_from_enemy);
|
|
|
|
if (aip->ai_profile_flags[AI::Profile_Flags::Allow_vertical_dodge] && fl_abs(udot) > fl_abs(rdot))
|
|
{
|
|
if ((udot < -0.5f) || (udot > 0.5f))
|
|
vm_vec_scale_add(&goal_point, &Pl_objp->pos, &Pl_objp->orient.vec.uvec, -200.0f);
|
|
else
|
|
vm_vec_scale_add(&goal_point, &Pl_objp->pos, &Pl_objp->orient.vec.uvec, 200.0f);
|
|
}
|
|
else
|
|
{
|
|
if ((rdot < -0.5f) || (rdot > 0.5f))
|
|
vm_vec_scale_add(&goal_point, &Pl_objp->pos, &Pl_objp->orient.vec.rvec, -200.0f);
|
|
else
|
|
vm_vec_scale_add(&goal_point, &Pl_objp->pos, &Pl_objp->orient.vec.rvec, 200.0f);
|
|
}
|
|
|
|
turn_towards_point(Pl_objp, &goal_point, NULL, 0.0f);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Use sliding and backwards moving to face enemy.
|
|
// (Coded 2/20/98. Works fine, but it's hard to see how to integrate it into the AI system.
|
|
// Typically ships are moving so fast that a little sliding isn't enough to gain an advantage.
|
|
// It's currently used to avoid collisions and could be used to evade weapon fire, but the latter
|
|
// would be frustrating, I think.
|
|
// This function is currently not called.)
|
|
void slide_face_ship()
|
|
{
|
|
ship_info *sip;
|
|
|
|
sip = &Ship_info[Ships[Pl_objp->instance].ship_info_index];
|
|
|
|
// If can't slide, return.
|
|
if ((sip->max_vel.xyz.x == 0.0f) && (sip->max_vel.xyz.y == 0.0f))
|
|
return;
|
|
|
|
vec3d goal_pos;
|
|
float dot_from_enemy;
|
|
vec3d vec_from_enemy, vec_to_goal;
|
|
float dist;
|
|
float up, right;
|
|
ai_info *aip;
|
|
|
|
aip = &Ai_info[Ships[Pl_objp->instance].ai_index];
|
|
|
|
dist = vm_vec_normalized_dir(&vec_from_enemy, &Pl_objp->pos, &En_objp->pos);
|
|
|
|
ai_turn_towards_vector(&En_objp->pos, Pl_objp, nullptr, nullptr, 0.0f, 0);
|
|
|
|
dot_from_enemy = vm_vec_dot(&vec_from_enemy, &En_objp->orient.vec.fvec);
|
|
|
|
if (vm_vec_dot(&vec_from_enemy, &En_objp->orient.vec.rvec) > 0.0f)
|
|
right = 1.0f;
|
|
else
|
|
right = -1.0f;
|
|
|
|
if (vm_vec_dot(&vec_from_enemy, &En_objp->orient.vec.uvec) > 0.0f)
|
|
up = 1.0f;
|
|
else
|
|
up = -1.0f;
|
|
|
|
vm_vec_scale_add(&goal_pos, &En_objp->pos, &En_objp->orient.vec.rvec, right * 200.0f);
|
|
vm_vec_scale_add(&goal_pos, &En_objp->pos, &En_objp->orient.vec.uvec, up * 200.0f);
|
|
|
|
vm_vec_normalized_dir(&vec_to_goal, &goal_pos, &Pl_objp->pos);
|
|
|
|
if (vm_vec_dot(&vec_to_goal, &Pl_objp->orient.vec.rvec) > 0.0f)
|
|
AI_ci.sideways = 1.0f;
|
|
else
|
|
AI_ci.sideways = -1.0f;
|
|
|
|
if (vm_vec_dot(&vec_to_goal, &Pl_objp->orient.vec.uvec) > 0.0f)
|
|
AI_ci.vertical = 1.0f;
|
|
else
|
|
AI_ci.vertical = -1.0f;
|
|
|
|
if (dist < 200.0f) {
|
|
if (dot_from_enemy < 0.7f)
|
|
accelerate_ship(aip, -1.0f);
|
|
else
|
|
accelerate_ship(aip, dot_from_enemy + 0.5f);
|
|
} else {
|
|
if (dot_from_enemy < 0.7f) {
|
|
accelerate_ship(aip, 0.2f);
|
|
} else {
|
|
accelerate_ship(aip, 1.0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
// General code for handling one ship evading another.
|
|
// Problem: This code is also used for avoiding an impending collision.
|
|
// In such a case, it is not good to go to max speed, which is often good
|
|
// for a certain kind of evasion.
|
|
void evade_ship()
|
|
{
|
|
vec3d player_pos, enemy_pos, goal_point;
|
|
vec3d vec_from_enemy;
|
|
float dot_from_enemy;
|
|
float dist;
|
|
ship *shipp = &Ships[Pl_objp->instance];
|
|
ship_info *sip = &Ship_info[shipp->ship_info_index];
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
float bank_override = 0.0f;
|
|
|
|
ai_set_positions(Pl_objp, En_objp, aip, &player_pos, &enemy_pos);
|
|
|
|
// Make speed based on skill level, varying at highest skill level, which is harder to hit.
|
|
if (Game_skill_level == NUM_SKILL_LEVELS-1) {
|
|
int rand_int;
|
|
float accel_val;
|
|
|
|
rand_int = static_rand(OBJ_INDEX(Pl_objp));
|
|
accel_val = (float) (((Missiontime^rand_int) >> 14) & 0x0f)/32.0f + 0.5f;
|
|
accelerate_ship(aip, accel_val);
|
|
} else
|
|
accelerate_ship(aip, (float) (Game_skill_level+2) / (NUM_SKILL_LEVELS+1));
|
|
|
|
if ((Missiontime - aip->submode_start_time > F1_0/2) && (sip->afterburner_fuel_capacity > 0.0f)) {
|
|
float percent_left = 100.0f * shipp->afterburner_fuel / sip->afterburner_fuel_capacity;
|
|
if (percent_left > 30.0f + ((OBJ_INDEX(Pl_objp)) & 0x0f)) {
|
|
afterburners_start(Pl_objp);
|
|
|
|
if (aip->ai_profile_flags[AI::Profile_Flags::Smart_afterburner_management]) {
|
|
aip->afterburner_stop_time = (fix) (Missiontime + F1_0 + static_randf(OBJ_INDEX(Pl_objp)) * F1_0 / 4);
|
|
} else {
|
|
aip->afterburner_stop_time = Missiontime + F1_0 + static_rand(OBJ_INDEX(Pl_objp))/4;
|
|
}
|
|
}
|
|
}
|
|
|
|
vm_vec_sub(&vec_from_enemy, &player_pos, &enemy_pos);
|
|
|
|
dist = vm_vec_normalize(&vec_from_enemy);
|
|
dot_from_enemy = vm_vec_dot(&En_objp->orient.vec.fvec, &vec_from_enemy);
|
|
|
|
if (dist > 250.0f) {
|
|
vec3d gp1, gp2;
|
|
// If far away from enemy, circle, going to nearer of point far off left or right wing
|
|
vm_vec_scale_add(&gp1, &enemy_pos, &En_objp->orient.vec.rvec, 250.0f);
|
|
vm_vec_scale_add(&gp2, &enemy_pos, &En_objp->orient.vec.rvec, -250.0f);
|
|
if (vm_vec_dist_quick(&gp1, &Pl_objp->pos) < vm_vec_dist_quick(&gp2, &Pl_objp->pos))
|
|
goal_point = gp1;
|
|
else
|
|
goal_point = gp2;
|
|
} else if (dot_from_enemy < 0.1f) {
|
|
// If already close to behind, goal is to get completely behind.
|
|
vm_vec_scale_add(&goal_point, &enemy_pos, &En_objp->orient.vec.fvec, -1000.0f);
|
|
} else if (dot_from_enemy > 0.9f) {
|
|
// If enemy pointing almost right at self, and self pointing close to enemy, turn away from
|
|
vec3d vec_to_enemy;
|
|
float dot_to_enemy;
|
|
|
|
vm_vec_sub(&vec_to_enemy, &enemy_pos, &player_pos);
|
|
|
|
vm_vec_normalize(&vec_to_enemy);
|
|
dot_to_enemy = vm_vec_dot(&Pl_objp->orient.vec.fvec, &vec_to_enemy);
|
|
if (dot_to_enemy > 0.75f) {
|
|
// Used to go to En_objp's right vector, but due to banking while turning, that
|
|
// caused flying in an odd spiral.
|
|
vm_vec_scale_add(&goal_point, &enemy_pos, &Pl_objp->orient.vec.rvec, 1000.0f);
|
|
if (dist < 100.0f)
|
|
bank_override = 1.0f;
|
|
} else {
|
|
bank_override = 1.0f; // In enemy's sights, not pointing at him, twirl away.
|
|
goto evade_ship_l1;
|
|
}
|
|
} else {
|
|
evade_ship_l1: ;
|
|
if (aip->ai_evasion > Random::next()*Random::INV_F_MAX_VALUE*100.0f) {
|
|
int temp;
|
|
float scale;
|
|
float psrandval; // some value close to zero to choose whether to turn right or left.
|
|
|
|
psrandval = (float) (((Missiontime >> 14) & 0x0f) - 8); // Value between -8 and 7
|
|
psrandval = psrandval/16.0f; // Value between -1/2 and 1/2 (approx)
|
|
|
|
// If not close to behind, turn towards his right or left vector, whichever won't cross his path.
|
|
if (vm_vec_dot(&vec_from_enemy, &En_objp->orient.vec.rvec) > psrandval) {
|
|
scale = 1000.0f;
|
|
} else {
|
|
scale = -1000.0f;
|
|
}
|
|
|
|
vm_vec_scale_add(&goal_point, &enemy_pos, &En_objp->orient.vec.rvec, scale);
|
|
|
|
temp = ((Missiontime >> 16) & 0x07);
|
|
temp = ((temp * (temp+1)) % 16)/2 - 4;
|
|
if ((psrandval == 0) && (temp == 0))
|
|
temp = 3;
|
|
|
|
scale = 200.0f * temp;
|
|
|
|
vm_vec_scale_add2(&goal_point, &En_objp->orient.vec.uvec, scale);
|
|
} else {
|
|
// No evasion this frame, but continue with previous turn.
|
|
// Reason: If you don't, you lose rotational momentum. Turning every other frame,
|
|
// and not in between results in a very slow turn because of loss of momentum.
|
|
if ((aip->prev_goal_point.xyz.x != 0.0f) || (aip->prev_goal_point.xyz.y != 0.0f) || (aip->prev_goal_point.xyz.z != 0.0f))
|
|
goal_point = aip->prev_goal_point;
|
|
else
|
|
vm_vec_scale_add(&goal_point, &enemy_pos, &En_objp->orient.vec.rvec, 100.0f);
|
|
}
|
|
}
|
|
|
|
turn_towards_point(Pl_objp, &goal_point, NULL, bank_override);
|
|
|
|
aip->prev_goal_point = goal_point;
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Fly in a manner making it difficult for opponent to attack.
|
|
void ai_evade()
|
|
{
|
|
evade_ship();
|
|
}
|
|
|
|
|
|
float G_collision_time;
|
|
vec3d G_predicted_pos, G_fire_pos;
|
|
|
|
|
|
/*
|
|
* plieblang
|
|
* Used by the good-primary-time sexp to store the info supplied from there
|
|
* weapon_idx indexes into Weapon_info if the primary preference is being set
|
|
*/
|
|
void ai_set_or_clear_preferred_primary_weapon(bool set_it, const object_ship_wing_point_team *subject, const object_ship_wing_point_team *target, int weapon_idx)
|
|
{
|
|
// force the ai to reselect a primary instead of possibly waiting the normal elapsed time
|
|
for (auto so : list_range(&Ship_obj_list))
|
|
{
|
|
auto subject_objp = &Objects[so->objnum];
|
|
if (subject_objp->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
auto subject_shipp = &Ships[subject_objp->instance];
|
|
if (!subject->matches(subject_shipp))
|
|
continue;
|
|
|
|
auto subject_aip = &Ai_info[subject_shipp->ai_index];
|
|
if (subject_aip->target_objnum < 0)
|
|
continue;
|
|
|
|
auto target_objp = &Objects[subject_aip->target_objnum];
|
|
if (target_objp->type != OBJ_SHIP || !target->matches(&Ships[target_objp->instance]))
|
|
continue;
|
|
|
|
// for AIs that match this subject-target pair, select a new primary immediately
|
|
subject_aip->primary_select_timestamp = timestamp(1);
|
|
}
|
|
|
|
if (set_it)
|
|
{
|
|
for (auto& ppi : Preferred_primary_info)
|
|
{
|
|
if (ppi.subject == *subject && ppi.target == *target)
|
|
{
|
|
ppi.weapon_index = weapon_idx;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// not found, so add a new entry
|
|
Preferred_primary_info.push_back({ *subject, *target, weapon_idx });
|
|
}
|
|
else
|
|
{
|
|
Preferred_primary_info.erase(
|
|
std::remove_if(Preferred_primary_info.begin(), Preferred_primary_info.end(), [&](const primary_fire_info& ppi) {
|
|
return ppi.subject == *subject && ppi.target == *target;
|
|
}), Preferred_primary_info.end()
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
//old version of this fuction, this will be useful for playing old missions and not having the new primary
|
|
//selection code throw off the balance of the mission.
|
|
// If:
|
|
// flags & Weapon::Info_Flags::Puncture
|
|
// Then Select a Puncture weapon.
|
|
// Else
|
|
// Select Any ol' weapon.
|
|
// Returns primary_bank index.
|
|
static int ai_select_primary_weapon_OLD(const object *objp, Weapon::Info_Flags flags)
|
|
{
|
|
ship *shipp = &Ships[objp->instance];
|
|
ship_weapon *swp = &shipp->weapons;
|
|
|
|
Assert( shipp->ship_info_index >= 0 && shipp->ship_info_index < ship_info_size());
|
|
|
|
if (flags == Weapon::Info_Flags::Puncture) {
|
|
if (swp->current_primary_bank >= 0) {
|
|
int bank_index;
|
|
|
|
bank_index = swp->current_primary_bank;
|
|
|
|
if (Weapon_info[swp->primary_bank_weapons[bank_index]].wi_flags[Weapon::Info_Flags::Puncture]) {
|
|
return swp->current_primary_bank;
|
|
}
|
|
}
|
|
for (int i=0; i<swp->num_primary_banks; i++) {
|
|
int weapon_info_index;
|
|
|
|
weapon_info_index = swp->primary_bank_weapons[i];
|
|
|
|
if (weapon_info_index > -1){
|
|
if (Weapon_info[weapon_info_index].wi_flags[Weapon::Info_Flags::Puncture]) {
|
|
swp->current_primary_bank = i;
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
|
|
// AL 26-3-98: If we couldn't find a puncture weapon, pick first available weapon if one isn't active
|
|
if ( swp->current_primary_bank < 0 ) {
|
|
if ( swp->num_primary_banks > 0 ) {
|
|
swp->current_primary_bank = 0;
|
|
}
|
|
}
|
|
|
|
} else { // Don't need to be using a puncture weapon.
|
|
if (swp->current_primary_bank >= 0) {
|
|
if (!(Weapon_info[swp->primary_bank_weapons[swp->current_primary_bank]].wi_flags[Weapon::Info_Flags::Puncture])){
|
|
return swp->current_primary_bank;
|
|
}
|
|
}
|
|
for (int i=0; i<swp->num_primary_banks; i++) {
|
|
if (swp->primary_bank_weapons[i] > -1) {
|
|
if (!(Weapon_info[swp->primary_bank_weapons[i]].wi_flags[Weapon::Info_Flags::Puncture])) {
|
|
swp->current_primary_bank = i;
|
|
nprintf(("AI", "%i: Ship %s selecting weapon %s\n", Framecount, Ships[objp->instance].ship_name, Weapon_info[swp->primary_bank_weapons[i]].name));
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
// Wasn't able to find a non-puncture weapon. Stick with what we have.
|
|
}
|
|
|
|
Assert( swp->current_primary_bank != -1 ); // get Alan or Allender
|
|
|
|
return swp->current_primary_bank;
|
|
}
|
|
|
|
// If:
|
|
// flags == Weapon::Info_Flags::Puncture
|
|
// Then Select a Puncture weapon.
|
|
// Else
|
|
// Select Any ol' weapon.
|
|
// Returns primary_bank index.
|
|
/**
|
|
* Etc. Etc. This is like the 4th rewrite of the code here. Special thanks to Bobboau
|
|
* for finding the get_shield_strength function.
|
|
*
|
|
* The AI will now intelligently choose the best weapon to use based on the overall shield
|
|
* status of the target.
|
|
*/
|
|
int ai_select_primary_weapon(object *objp, object *other_objp, Weapon::Info_Flags flags)
|
|
{
|
|
// Pointer Set Up
|
|
ship *shipp = &Ships[objp->instance];
|
|
ship *other_shipp = nullptr;
|
|
ship_weapon *swp = &shipp->weapons;
|
|
|
|
// Debugging
|
|
if (other_objp==NULL)
|
|
{
|
|
// this can be NULL in the case of a target death and attacker weapon
|
|
// change. using notification message instead of a fault
|
|
mprintf(("'other_objpp == NULL' in ai_select_primary_weapon()\n"));
|
|
return -1;
|
|
}
|
|
|
|
bool other_is_ship = (other_objp->type == OBJ_SHIP);
|
|
|
|
if (other_is_ship)
|
|
{
|
|
other_shipp = &Ships[other_objp->instance];
|
|
|
|
//if the good-primary-time sexp has been used, return that weapon immediately
|
|
//if smart primary weapon selection isn't set, turning this override behavior off might not do anything
|
|
//however this seems acceptable
|
|
for (const auto &ppi : Preferred_primary_info)
|
|
{
|
|
if (ppi.subject.matches(shipp) && ppi.target.matches(other_shipp))
|
|
{
|
|
int weapon_idx = ppi.weapon_index;
|
|
|
|
//figure out what bank the weapon belongs to
|
|
int bank_num = -1;
|
|
for (int i = 0; i < MAX_SHIP_PRIMARY_BANKS; i++) {
|
|
if (weapon_idx == swp->primary_bank_weapons[i]) {
|
|
bank_num = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
//if it's a valid bank, set it and return
|
|
if (bank_num != -1) {
|
|
//forcing a ship to use a certain primary should prevent it from linking weapons
|
|
shipp->flags.set(Ship::Ship_Flags::Force_primary_unlinking);
|
|
|
|
swp->current_primary_bank = bank_num;
|
|
return bank_num;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// we're not prioritizing a specific primary weapon
|
|
shipp->flags.remove(Ship::Ship_Flags::Force_primary_unlinking);
|
|
|
|
|
|
//not using the new AI, use the old version of this function instead.
|
|
if (!(Ai_info[shipp->ai_index].ai_profile_flags[AI::Profile_Flags::Smart_primary_weapon_selection]))
|
|
{
|
|
return ai_select_primary_weapon_OLD(objp, flags);
|
|
}
|
|
|
|
Assert( shipp->ship_info_index >= 0 && shipp->ship_info_index < ship_info_size());
|
|
|
|
//made it so it only selects puncture weapons if the active goal is to disable something -Bobboau
|
|
if ((flags == Weapon::Info_Flags::Puncture) && ai_goal_is_disable_or_disarm(Ai_info[shipp->ai_index].goals[0].ai_mode))
|
|
{
|
|
if (swp->current_primary_bank >= 0)
|
|
{
|
|
int bank_index = swp->current_primary_bank;
|
|
|
|
if (Weapon_info[swp->primary_bank_weapons[bank_index]].wi_flags[Weapon::Info_Flags::Puncture])
|
|
{
|
|
return swp->current_primary_bank;
|
|
}
|
|
}
|
|
for (int i=0; i<swp->num_primary_banks; i++)
|
|
{
|
|
int weapon_info_index = swp->primary_bank_weapons[i];
|
|
|
|
if (weapon_info_index >= 0)
|
|
{
|
|
if (Weapon_info[weapon_info_index].wi_flags[Weapon::Info_Flags::Puncture])
|
|
{
|
|
swp->current_primary_bank = i;
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
float enemy_remaining_shield = get_shield_pct(other_objp);
|
|
|
|
if ( other_is_ship )
|
|
{
|
|
ship_info* other_sip = &Ship_info[other_shipp->ship_info_index];
|
|
|
|
if (other_sip->class_type >= 0 && Ship_types[other_sip->class_type].flags[Ship::Type_Info_Flags::Targeted_by_huge_Ignored_by_small_only]) {
|
|
//Check if we have a capital+ weapon on board
|
|
for (int i = 0; i < swp->num_primary_banks; i++)
|
|
{
|
|
if (swp->primary_bank_weapons[i] >= 0) // Make sure there is a weapon in the bank
|
|
{
|
|
auto wip = &Weapon_info[swp->primary_bank_weapons[i]];
|
|
if (wip->wi_flags[Weapon::Info_Flags::Capital_plus])
|
|
{
|
|
// handle shielded big/huge ships (non FS-verse)
|
|
// only pick capital+ if the target has no shields; or the weapon is an ok shield-breaker anyway
|
|
if (enemy_remaining_shield <= 0.05f || (wip->shield_factor * 1.33f > wip->armor_factor))
|
|
{
|
|
swp->current_primary_bank = i;
|
|
nprintf(("AI", "%i: Ship %s selecting weapon %s (capital+) vs target %s\n", Framecount, shipp->ship_name, wip->name, other_shipp->ship_name));
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Is the target shielded by say only 5%?
|
|
if (enemy_remaining_shield <= 0.05f)
|
|
{
|
|
// Then it is safe to start using a heavy hull damage weapon such as the maxim
|
|
float i_hullfactor_prev = 0; // Previous weapon bank hull factor (this is the safe way to do it)
|
|
int i_hullfactor_prev_bank = -1; // Bank which gave us this hull factor
|
|
|
|
// Find the weapon with the highest hull * damage / fire delay factor
|
|
for (int i = 0; i < swp->num_primary_banks; i++)
|
|
{
|
|
if (swp->primary_bank_weapons[i] >= 0) // Make sure there is a weapon in the bank
|
|
{
|
|
auto wip = &Weapon_info[swp->primary_bank_weapons[i]];
|
|
if ((((wip->armor_factor) * (wip->damage)) / wip->fire_wait) > i_hullfactor_prev)
|
|
{
|
|
if ( !(wip->wi_flags[Weapon::Info_Flags::Capital_plus]) )
|
|
{
|
|
// This weapon is the new candidate
|
|
i_hullfactor_prev = ( ((wip->armor_factor) * (wip->damage)) / wip->fire_wait );
|
|
i_hullfactor_prev_bank = i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (i_hullfactor_prev_bank == -1) // In the unlikely instance we don't find at least 1 candidate weapon
|
|
{
|
|
i_hullfactor_prev_bank = 0; // Just switch to the first one
|
|
}
|
|
swp->current_primary_bank = i_hullfactor_prev_bank; // Select the best weapon
|
|
nprintf(("AI", "%i: Ship %s selecting weapon %s (no shields) vs target %s\n", Framecount, shipp->ship_name, Weapon_info[swp->primary_bank_weapons[i_hullfactor_prev_bank]].name, (other_is_ship ? other_shipp->ship_name : "non-ship") ));
|
|
return i_hullfactor_prev_bank; // Return
|
|
}
|
|
|
|
//if the shields are above lets say 10% definitely use a pierceing weapon if there are any-Bobboau
|
|
if (enemy_remaining_shield >= 0.10f)
|
|
{
|
|
if (swp->current_primary_bank >= 0)
|
|
{
|
|
auto wip = &Weapon_info[swp->primary_bank_weapons[swp->current_primary_bank]];
|
|
|
|
if ((wip->wi_flags[Weapon::Info_Flags::Pierce_shields]) && !(wip->wi_flags[Weapon::Info_Flags::Capital_plus]))
|
|
{
|
|
nprintf(("AI", "%i: Ship %s selecting weapon %s (>10%% shields)\n", Framecount, shipp->ship_name, wip->name));
|
|
return swp->current_primary_bank;
|
|
}
|
|
}
|
|
for (int i=0; i<swp->num_primary_banks; i++)
|
|
{
|
|
if (swp->primary_bank_weapons[i] >= 0)
|
|
{
|
|
auto wip = &Weapon_info[swp->primary_bank_weapons[i]];
|
|
if ((wip->wi_flags[Weapon::Info_Flags::Pierce_shields]) && !(wip->wi_flags[Weapon::Info_Flags::Capital_plus]))
|
|
{
|
|
swp->current_primary_bank = i;
|
|
nprintf(("AI", "%i: Ship %s selecting weapon %s (>10%% shields)\n", Framecount, shipp->ship_name, wip->name));
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Is the target shielded by less then 50%?
|
|
if (enemy_remaining_shield <= 0.50f)
|
|
{
|
|
// Should be using best balanced shield/hull gun
|
|
float i_balancedfactor_prev = 0; // Previous weapon bank "balanced" factor (this is the safe way to do it)
|
|
int i_balancedfactor_prev_bank = -1; // Bank which gave us this "balanced" factor
|
|
// Find the weapon with the highest average hull and shield * damage / fire delay factor
|
|
for (int i = 0; i < swp->num_primary_banks; i++)
|
|
{
|
|
if (swp->primary_bank_weapons[i] >= 0) // Make sure there is a weapon in the bank
|
|
{
|
|
auto wip = &Weapon_info[swp->primary_bank_weapons[i]];
|
|
if ((((wip->armor_factor + wip->shield_factor) * wip->damage) / wip->fire_wait) > i_balancedfactor_prev)
|
|
{
|
|
if ( !(wip->wi_flags[Weapon::Info_Flags::Capital_plus]) )
|
|
{
|
|
// This weapon is the new candidate
|
|
i_balancedfactor_prev = ((((wip->armor_factor + wip->shield_factor) * wip->damage) / wip->fire_wait));
|
|
i_balancedfactor_prev_bank = i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (i_balancedfactor_prev_bank == -1) // In the unlikely instance we don't find at least 1 candidate weapon
|
|
{
|
|
i_balancedfactor_prev_bank = 0; // Just switch to the first one
|
|
}
|
|
swp->current_primary_bank = i_balancedfactor_prev_bank; // Select the best weapon
|
|
nprintf(("AI", "%i: Ship %s selecting weapon %s (<50%% shields)\n", Framecount, shipp->ship_name, Weapon_info[swp->primary_bank_weapons[i_balancedfactor_prev_bank]].name));
|
|
return i_balancedfactor_prev_bank; // Return
|
|
}
|
|
else
|
|
{
|
|
// Should be using best shield destroying gun
|
|
float i_shieldfactor_prev = 0; // Previous weapon bank shield factor (this is the safe way to do it)
|
|
int i_shieldfactor_prev_bank = -1; // Bank which gave us this shield factor
|
|
// Find the weapon with the highest average hull and shield * damage / fire delay factor
|
|
for (int i = 0; i < swp->num_primary_banks; i++)
|
|
{
|
|
if (swp->primary_bank_weapons[i] >= 0) // Make sure there is a weapon in the bank
|
|
{
|
|
auto wip = &Weapon_info[swp->primary_bank_weapons[i]];
|
|
if ((((wip->shield_factor) * wip->damage) / wip->fire_wait) > i_shieldfactor_prev)
|
|
{
|
|
if ( !(wip->wi_flags[Weapon::Info_Flags::Capital_plus]) )
|
|
{
|
|
// This weapon is the new candidate
|
|
i_shieldfactor_prev = ( ((wip->shield_factor) * (wip->damage)) / wip->fire_wait );
|
|
i_shieldfactor_prev_bank = i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (i_shieldfactor_prev_bank == -1) // In the unlikely instance we don't find at least 1 candidate weapon
|
|
{
|
|
i_shieldfactor_prev_bank = 0; // Just switch to the first one
|
|
}
|
|
swp->current_primary_bank = i_shieldfactor_prev_bank; // Select the best weapon
|
|
nprintf(("AI", "%i: Ship %s selecting weapon %s (>50%% shields)\n", Framecount, shipp->ship_name, Weapon_info[swp->primary_bank_weapons[i_shieldfactor_prev_bank]].name));
|
|
return i_shieldfactor_prev_bank; // Return
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maybe link primary weapons.
|
|
*/
|
|
void set_primary_weapon_linkage(object *objp)
|
|
{
|
|
ship *shipp;
|
|
ship_info *sip;
|
|
ai_info *aip;
|
|
ship_weapon *swp;
|
|
weapon_info *wip;
|
|
|
|
shipp = &Ships[objp->instance];
|
|
|
|
if (Num_weapons > (int) (MAX_WEAPONS * 0.75f)) {
|
|
if (shipp->flags[Ship::Ship_Flags::Primary_linked]) {
|
|
nprintf(("AI", "Frame %i, ship %s: Unlinking primaries.\n", Framecount, shipp->ship_name));
|
|
shipp->flags.remove(Ship::Ship_Flags::Primary_linked);
|
|
}
|
|
return; // If low on slots, don't link.
|
|
}
|
|
|
|
sip = &Ship_info[shipp->ship_info_index];
|
|
aip = &Ai_info[shipp->ai_index];
|
|
swp = &shipp->weapons;
|
|
|
|
shipp->flags.remove(Ship::Ship_Flags::Primary_linked);
|
|
|
|
//bail if it's been included in a good-primary-time
|
|
if (shipp->flags[Ship::Ship_Flags::Force_primary_unlinking]) {
|
|
return;
|
|
}
|
|
|
|
// AL: ensure target is a ship!
|
|
if ( (aip->target_objnum != -1) && (Objects[aip->target_objnum].type == OBJ_SHIP) ) {
|
|
// If trying to destroy a big ship (i.e., not disable/disarm), always unleash all weapons
|
|
if ( Ship_info[Ships[Objects[aip->target_objnum].instance].ship_info_index].is_big_ship() ) {
|
|
if ( aip->targeted_subsys == nullptr ) {
|
|
if (!sip->flags[Ship::Info_Flags::No_primary_linking] ) {
|
|
shipp->flags.set(Ship::Ship_Flags::Primary_linked);
|
|
}
|
|
shipp->flags.set(Ship::Ship_Flags::Secondary_dual_fire);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// AL 2-11-98: If ship has a disarm or disable goal, don't link unless both weapons are
|
|
// puncture weapons
|
|
if ( (aip->active_goal != AI_ACTIVE_GOAL_NONE) && (aip->active_goal != AI_ACTIVE_GOAL_DYNAMIC) )
|
|
{
|
|
if ( ai_goal_is_disable_or_disarm(aip->goals[aip->active_goal].ai_mode) )
|
|
{
|
|
// only continue if both primaries are puncture weapons
|
|
if ( swp->num_primary_banks == 2 ) {
|
|
if ( !(Weapon_info[swp->primary_bank_weapons[0]].wi_flags[Weapon::Info_Flags::Puncture]) )
|
|
return;
|
|
if ( !(Weapon_info[swp->primary_bank_weapons[1]].wi_flags[Weapon::Info_Flags::Puncture]) )
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Don't want all ships always linking weapons at start, so asynchronize.
|
|
if (!(The_mission.ai_profile->flags[AI::Profile_Flags::Allow_primary_link_at_start]))
|
|
{
|
|
if (Missiontime < i2f(30))
|
|
return;
|
|
else if (Missiontime < i2f(120))
|
|
{
|
|
int r = static_rand((Missiontime >> 17) ^ OBJ_INDEX(objp));
|
|
if ( (r&3) != 0)
|
|
return;
|
|
}
|
|
}
|
|
|
|
// get energy level
|
|
float energy;
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_linked_primary_bug]) {
|
|
energy = shipp->weapon_energy / sip->max_weapon_reserve * 100.0f;
|
|
} else {
|
|
energy = shipp->weapon_energy;
|
|
}
|
|
|
|
// make linking decision based on weapon energy
|
|
// but only if primary linking is allowed --wookieejedi
|
|
if (!sip->flags[Ship::Info_Flags::No_primary_linking]) {
|
|
if (energy > aip->ai_link_energy_levels_always) {
|
|
shipp->flags.set(Ship::Ship_Flags::Primary_linked);
|
|
} else if (energy > aip->ai_link_energy_levels_maybe) {
|
|
if (objp->hull_strength < shipp->ship_max_hull_strength / 3.0f) {
|
|
shipp->flags.set(Ship::Ship_Flags::Primary_linked);
|
|
}
|
|
}
|
|
}
|
|
|
|
// also check ballistics - Goober5000
|
|
int total_ammo = 0;
|
|
int current_ammo = 0;
|
|
bool all_ballistic = true;
|
|
|
|
// count ammo, and do not continue unless all weapons are ballistic
|
|
for (int i = 0; i < swp->num_primary_banks; i++)
|
|
{
|
|
wip = &Weapon_info[swp->primary_bank_weapons[i]];
|
|
|
|
if (wip->wi_flags[Weapon::Info_Flags::Ballistic])
|
|
{
|
|
total_ammo += swp->primary_bank_start_ammo[i];
|
|
current_ammo += swp->primary_bank_ammo[i];
|
|
}
|
|
else
|
|
{
|
|
all_ballistic = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (all_ballistic)
|
|
{
|
|
Assert(total_ammo); // Goober5000: div-0 check
|
|
float ammo_pct = float (current_ammo) / float (total_ammo) * 100.0f;
|
|
|
|
// link according to defined levels
|
|
if (ammo_pct > aip->ai_link_ammo_levels_always)
|
|
{
|
|
shipp->flags.set(Ship::Ship_Flags::Primary_linked);
|
|
}
|
|
else if (ammo_pct > aip->ai_link_ammo_levels_maybe)
|
|
{
|
|
if (objp->hull_strength < shipp->ship_max_hull_strength / 3.0f)
|
|
{
|
|
shipp->flags.set(Ship::Ship_Flags::Primary_linked);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const int MULTILOCK_CHECK_INTERVAL = 250; // every quarter of a second
|
|
|
|
// This function returns if the ship could theoretically have fully locked all available targets
|
|
// given the current time it's been locking on its primary target
|
|
// If so, it cheats a little bit and assumes that's what it has done, and fills
|
|
// the ai's missile_locks_firing vector, and to be fired at those targets
|
|
bool ai_do_multilock(ai_info* aip, weapon_info* wip) {
|
|
|
|
if (!timestamp_elapsed(aip->multilock_check_timestamp))
|
|
return false;
|
|
|
|
object* primary_target = &Objects[aip->target_objnum];
|
|
|
|
struct multilock_target {
|
|
object* objp;
|
|
ship_subsys* subsys;
|
|
float dot;
|
|
};
|
|
|
|
// First we accrue all available targets into this vector which we will later sort by dot
|
|
SCP_vector<multilock_target> multilock_targets;
|
|
|
|
if (wip->target_restrict == LR_CURRENT_TARGET)
|
|
multilock_targets.push_back(multilock_target{ primary_target, aip->targeted_subsys, 1.0f });
|
|
else if (wip->target_restrict == LR_CURRENT_TARGET_SUBSYS) {
|
|
|
|
if (primary_target->type != OBJ_SHIP)
|
|
multilock_targets.push_back(multilock_target{ primary_target, nullptr, 1.0f });
|
|
else {
|
|
ship* sp = &Ships[primary_target->instance];
|
|
ship_subsys* ss;
|
|
|
|
if (Ship_info[sp->ship_info_index].is_big_or_huge()) {
|
|
for (ss = GET_FIRST(&sp->subsys_list); ss != END_OF_LIST(&sp->subsys_list); ss = GET_NEXT(ss)) {
|
|
float dot;
|
|
|
|
if (!weapon_multilock_can_lock_on_subsys(Pl_objp, primary_target, ss, wip, &dot))
|
|
continue;
|
|
|
|
// this will guarantee the ship will send always send some missiles at its actual target
|
|
if (aip->targeted_subsys == ss)
|
|
dot = 1.0f;
|
|
|
|
multilock_targets.push_back(multilock_target{ primary_target, ss, dot });
|
|
}
|
|
} else
|
|
multilock_targets.push_back(multilock_target{ primary_target, nullptr, 1.0f });
|
|
}
|
|
} else { // any target
|
|
for (ship_obj* so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so)) {
|
|
object* target_objp = &Objects[so->objnum];
|
|
if (target_objp->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
ship* target_ship = &Ships[target_objp->instance];
|
|
float dot;
|
|
|
|
if (!weapon_multilock_can_lock_on_target(Pl_objp, target_objp, wip, &dot))
|
|
continue;
|
|
|
|
if (Ship_info[target_ship->ship_info_index].is_big_or_huge()) {
|
|
// add ALL the valid subsystems as possible targets
|
|
// dont check range or lock fov, the sub function will do that for subsystems
|
|
ship_subsys* ss;
|
|
for (ss = GET_FIRST(&target_ship->subsys_list); ss != END_OF_LIST(&target_ship->subsys_list); ss = GET_NEXT(ss)) {
|
|
|
|
if (!weapon_multilock_can_lock_on_subsys(Pl_objp, target_objp, ss, wip, &dot))
|
|
continue;
|
|
|
|
// this will guarantee the ship will send always send some missiles at its actual target
|
|
if (aip->targeted_subsys == ss)
|
|
dot = 1.0f;
|
|
|
|
multilock_targets.push_back(multilock_target{ target_objp, ss, dot });
|
|
}
|
|
} else {
|
|
// just a small target, now we check range and fov
|
|
if (!weapon_secondary_world_pos_in_range(Player_obj, wip, &target_objp->pos))
|
|
continue;
|
|
|
|
if (dot < wip->lock_fov)
|
|
continue;
|
|
|
|
// this will guarantee the ship will send always send some missiles at its actual target
|
|
if (primary_target == target_objp)
|
|
dot = 1.0f;
|
|
|
|
multilock_targets.push_back(multilock_target{ target_objp, nullptr, dot });
|
|
}
|
|
}
|
|
}
|
|
|
|
// before any sorting let's first determine if we're maximally locked
|
|
int allocatable_missiles = MIN(weapon_get_max_missile_seekers(wip), (int)multilock_targets.size() * wip->max_seekers_per_target);
|
|
float missiles_allocated = wip->max_seeking * (aip->aspect_locked_time / wip->min_lock_time);
|
|
|
|
// we could still lock more missiles! wait for it (or couldn't find any targets??)
|
|
if (missiles_allocated < allocatable_missiles || allocatable_missiles == 0) {
|
|
aip->multilock_check_timestamp = timestamp(MULTILOCK_CHECK_INTERVAL);
|
|
return false;
|
|
}
|
|
|
|
// we're fully locked! sort by dot and let them fly!
|
|
std::sort(multilock_targets.begin(), multilock_targets.end(), [](multilock_target &a, multilock_target &b)
|
|
{ return a.dot < b.dot; });
|
|
|
|
aip->ai_missile_locks_firing.clear();
|
|
int missiles = weapon_get_max_missile_seekers(wip);
|
|
int remaining_seekers_per_target = wip->max_seekers_per_target;
|
|
for (size_t i = multilock_targets.size() - 1;; i--) {
|
|
std::pair<int, ship_subsys*> lock;
|
|
|
|
lock.first = OBJ_INDEX(multilock_targets.at(i).objp);
|
|
lock.second = multilock_targets.at(i).subsys;
|
|
aip->ai_missile_locks_firing.push_back(lock);
|
|
missiles--;
|
|
|
|
// out of missiles, we're done
|
|
if (missiles == 0)
|
|
break;
|
|
|
|
// still more missiles, but exhausted our targets, time to loop over them all again
|
|
// and start double-locking triple-locking etc
|
|
if (i == 0) {
|
|
remaining_seekers_per_target--;
|
|
// exhausted our seekers per target, we're done
|
|
if (remaining_seekers_per_target == 0)
|
|
break;
|
|
|
|
// reset i and go through the whole list again
|
|
i = multilock_targets.size();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Fire the current primary weapon.
|
|
// *objp is the object to fire from.
|
|
//I changed this to return a true false flag on weather
|
|
//it did or did not fire the weapon sorry if this screws you up-Bobboau
|
|
int ai_fire_primary_weapon(object *objp)
|
|
{
|
|
ship *shipp = &Ships[objp->instance];
|
|
ship_weapon *swp = &shipp->weapons;
|
|
ship_info *enemy_sip;
|
|
ai_info *aip;
|
|
object *enemy_objp;
|
|
|
|
Assert( shipp->ship_info_index >= 0 && shipp->ship_info_index < ship_info_size());
|
|
|
|
aip = &Ai_info[shipp->ai_index];
|
|
|
|
// If low on slots, fire a little less often.
|
|
if (Num_weapons > (int) (0.9f * MAX_WEAPONS)) {
|
|
if (frand() > 0.5f) {
|
|
nprintf(("AI", "Frame %i, %s not fire.\n", Framecount, shipp->ship_name));
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
if (!Ai_firing_enabled){
|
|
return 0;
|
|
}
|
|
|
|
if (aip->target_objnum != -1){
|
|
enemy_objp = &Objects[aip->target_objnum];
|
|
enemy_sip = (enemy_objp->type == OBJ_SHIP) ? &Ship_info[Ships[enemy_objp->instance].ship_info_index] : NULL;
|
|
} else {
|
|
enemy_objp = NULL;
|
|
enemy_sip = NULL;
|
|
}
|
|
|
|
//plieblang - added check for size of Preferred_primaries to force reevaluation if good-primary-time has been used in the meantime
|
|
if ( (swp->current_primary_bank < 0) || (swp->current_primary_bank >= swp->num_primary_banks) || timestamp_elapsed(aip->primary_select_timestamp)) {
|
|
Weapon::Info_Flags flags = Weapon::Info_Flags::NUM_VALUES;
|
|
if ( aip->targeted_subsys != NULL ) {
|
|
flags = Weapon::Info_Flags::Puncture;
|
|
}
|
|
ai_select_primary_weapon(objp, enemy_objp, flags);
|
|
ship_primary_changed(shipp); // AL: maybe send multiplayer information when AI ship changes primaries
|
|
aip->primary_select_timestamp = timestamp(5 * MILLISECONDS_PER_SECOND); // Maybe change primary weapon five seconds from now.
|
|
}
|
|
|
|
//We can only check LoS if we have a target defined
|
|
weapon_info* wip = &Weapon_info[swp->primary_bank_weapons[swp->current_primary_bank]];
|
|
if (aip->target_objnum != -1 && (The_mission.ai_profile->flags[AI::Profile_Flags::Require_exact_los] || wip->wi_flags[Weapon::Info_Flags::Require_exact_los])) {
|
|
//Check if th AI has Line of Sight and is allowed to fire
|
|
if (!check_los(OBJ_INDEX(objp), aip->target_objnum, The_mission.ai_profile->los_min_detection_radius, swp->current_primary_bank, -1, nullptr)) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// If pointing nearly at predicted collision point of target, bash orientation to be perfectly pointing.
|
|
float dot;
|
|
vec3d v2t;
|
|
|
|
bool condition;
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_ramming_stationary_targets_bug]) {
|
|
// fixed by Mantis 3147 - Avoid bashing orientation if trying to avoid a collision.
|
|
condition = !(vm_vec_mag_quick(&G_predicted_pos) < AICODE_SMALL_MAGNITUDE) && (aip->submode != SM_AVOID);
|
|
} else {
|
|
// retail behavior
|
|
condition = !(vm_vec_mag_quick(&G_predicted_pos) < AICODE_SMALL_MAGNITUDE);
|
|
}
|
|
|
|
if ( condition ) {
|
|
if ( vm_vec_cmp(&G_predicted_pos, &G_fire_pos) ) {
|
|
vm_vec_normalized_dir(&v2t, &G_predicted_pos, &G_fire_pos);
|
|
dot = vm_vec_dot(&v2t, &objp->orient.vec.fvec);
|
|
if (dot > .998629534f){ // if within 3.0 degrees of desired heading, bash
|
|
vm_vector_2_matrix_norm(&objp->orient, &v2t, &objp->orient.vec.uvec, nullptr);
|
|
}
|
|
}
|
|
}
|
|
|
|
//SUSHI: Burst-fire for ballistic primaries.
|
|
if (The_mission.ai_profile->primary_ammo_burst_mult[Game_skill_level] > 0 && //Make sure we are using burst fire
|
|
enemy_objp != NULL && enemy_sip != NULL && //We need a target, obviously
|
|
(enemy_objp->phys_info.speed >= 1.0f) && //Only burst for moving ships
|
|
(enemy_sip->is_small_ship() || enemy_sip->flags[Ship::Info_Flags::Transport]) && //Only burst for small ships (transports count)
|
|
swp->primary_bank_start_ammo[swp->current_primary_bank] > 0 && //Prevent div by 0
|
|
Weapon_info[swp->primary_bank_weapons[swp->current_primary_bank]].wi_flags[Weapon::Info_Flags::Ballistic]) //Current weapon must be ballistic
|
|
{
|
|
float percentAmmoLeft = ((float)swp->primary_bank_ammo[swp->current_primary_bank] / (float)swp->primary_bank_start_ammo[swp->current_primary_bank]);
|
|
float distToTarget = vm_vec_dist(&enemy_objp->pos, &objp->pos);
|
|
float weaponRange = Weapon_info[swp->primary_bank_weapons[swp->current_primary_bank]].weapon_range;
|
|
float distanceFactor = 1.0f - distToTarget/weaponRange;
|
|
vec3d vecToTarget;
|
|
vm_vec_normalized_dir(&vecToTarget, &enemy_objp->pos, &objp->pos);
|
|
float dotToTarget = vm_vec_dot(&vecToTarget, &objp->orient.vec.fvec);
|
|
//This makes the dot a tiny bit more impactful (otherwise nearly always over 0.98 or so)
|
|
//square twice = raise to the 4th power
|
|
dotToTarget *= dotToTarget;
|
|
dotToTarget *= dotToTarget;
|
|
float fof_spread_cooldown_factor = 1.0f - swp->primary_bank_fof_cooldown[swp->current_primary_bank];
|
|
|
|
//Combine factors
|
|
float burstFireProb = ((0.6f * percentAmmoLeft) + (0.4f * distanceFactor)) * dotToTarget * aip->ai_primary_ammo_burst_mult * fof_spread_cooldown_factor;
|
|
|
|
//Possibly change values every half-second
|
|
if (static_randf((Missiontime + static_rand(aip->shipnum)) >> 15) > burstFireProb)
|
|
return 0;
|
|
}
|
|
|
|
// Make sure not firing at a protected ship unless firing at a live subsystem.
|
|
// Note: This happens every time the ship tries to fire, perhaps every frame.
|
|
// Should be wrapped in a timestamp, same one that enables it to fire, but that is complicated
|
|
// by multiple banks it can fire from.
|
|
if (aip->target_objnum != -1) {
|
|
object *tobjp = &Objects[aip->target_objnum];
|
|
if (tobjp->flags[Object::Object_Flags::Protected]) {
|
|
if (aip->targeted_subsys != NULL) {
|
|
int type;
|
|
|
|
type = aip->targeted_subsys->system_info->type;
|
|
if (ship_subsystems_blown(&Ships[tobjp->instance], type)) {
|
|
aip->target_objnum = -1;
|
|
return 0;
|
|
}
|
|
} else {
|
|
aip->target_objnum = -1;
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If enemy is protected, not firing a puncture weapon and enemy's hull is low, don't fire.
|
|
if ((enemy_objp != NULL) && (enemy_objp->flags[Object::Object_Flags::Protected])) {
|
|
// AL: 3-6-98: Check if current_primary_bank is valid
|
|
if ((enemy_objp->hull_strength < 750.0f) &&
|
|
((aip->targeted_subsys == NULL) || (enemy_objp->hull_strength < aip->targeted_subsys->current_hits + 50.0f)) &&
|
|
(swp->current_primary_bank >= 0) ) {
|
|
if (!(Weapon_info[swp->primary_bank_weapons[swp->current_primary_bank]].wi_flags[Weapon::Info_Flags::Puncture])) {
|
|
swp->next_primary_fire_stamp[swp->current_primary_bank] = timestamp(1000);
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
set_primary_weapon_linkage(objp);
|
|
|
|
ship_fire_primary(objp);
|
|
|
|
return 1;//if it got down to here then it tryed to fire
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Return number of nearby enemy fighters.
|
|
// threshold is the distance within which a ship is considered near.
|
|
//
|
|
// input: enemy_team_mask => teams that are considered as an enemy
|
|
// pos => world position to measure ship distances from
|
|
// threshold => max distance from pos to be considered "near"
|
|
//
|
|
// exit: number of ships within threshold units of pos
|
|
int num_nearby_fighters(int enemy_team_mask, const vec3d *pos, float threshold)
|
|
{
|
|
ship_obj *so;
|
|
object *ship_objp;
|
|
int count = 0;
|
|
|
|
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
|
|
|
|
ship_objp = &Objects[so->objnum];
|
|
if (ship_objp->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
if (iff_matches_mask(Ships[ship_objp->instance].team, enemy_team_mask)) {
|
|
if (Ship_info[Ships[ship_objp->instance].ship_info_index].is_fighter_bomber()) {
|
|
if (vm_vec_dist_quick(pos, &ship_objp->pos) < threshold)
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Select secondary weapon to fire.
|
|
// Currently, 1/16/98:
|
|
// If 0 secondary weapons available, return -1
|
|
// If 1 available, use it.
|
|
// If 2 or more, if the current weapon is one of them, stick with it, otherwise choose a random one.
|
|
// priority1 and priority2 are Weapon_info[] bitmasks such as WIF_HOMING_ASPECT. If any weapon has any bit in priority1
|
|
// set, that weapon will be selected. If not, apply to priority2. If neither, return -1, meaning no weapon selected.
|
|
// Note, priorityX have default values of -1, meaning if not set, they will match any weapon.
|
|
// Return value:
|
|
// true if an appropriate weapon was found and switched to, false if no valid weapons, and should not fire
|
|
// Should do this:
|
|
// Favor aspect seekers when attacking small ships faraway.
|
|
// Favor rapid fire dumbfire when attacking a large ship.
|
|
// Ignore heat seekers because we're not sure how they'll work.
|
|
bool ai_select_secondary_weapon(object *objp, ship_weapon *swp, flagset<Weapon::Info_Flags>* priority1 = NULL, flagset<Weapon::Info_Flags>* priority2 = NULL)
|
|
{
|
|
int num_weapon_types;
|
|
int weapon_id_list[MAX_WEAPON_TYPES], weapon_bank_list[MAX_WEAPON_TYPES];
|
|
int i;
|
|
flagset<Weapon::Info_Flags> ignore_mask, ignore_mask_without_huge, prio1, prio2;
|
|
int initial_bank;
|
|
ai_info *aip = &Ai_info[Ships[objp->instance].ai_index];
|
|
bool rval = false;
|
|
|
|
initial_bank = swp->current_secondary_bank;
|
|
|
|
if (priority1 != NULL)
|
|
prio1 = *priority1;
|
|
if (priority2 != NULL)
|
|
prio2 = *priority2;
|
|
|
|
|
|
// set up ignore masks
|
|
ignore_mask.reset();
|
|
|
|
// Ignore bombs unless one of the priorities asks for them to be selected.
|
|
if (!(prio1[Weapon::Info_Flags::Huge] || prio2[Weapon::Info_Flags::Huge])) {
|
|
ignore_mask.set(Weapon::Info_Flags::Huge);
|
|
}
|
|
|
|
// Ignore capital+ unless one of the priorities asks for it.
|
|
if (!(prio1[Weapon::Info_Flags::Capital_plus] || prio2[Weapon::Info_Flags::Capital_plus])) {
|
|
ignore_mask.set(Weapon::Info_Flags::Capital_plus);
|
|
}
|
|
|
|
// Ignore bomber+ unless one of the priorities asks for them to be selected
|
|
if (!(prio1[Weapon::Info_Flags::Bomber_plus] || prio2[Weapon::Info_Flags::Bomber_plus])) {
|
|
ignore_mask.set(Weapon::Info_Flags::Bomber_plus);
|
|
}
|
|
|
|
// Ignore mines in normal combat... they must be explicitly prioritized via SEXP
|
|
if (!(prio1[Weapon::Info_Flags::Mine] || prio2[Weapon::Info_Flags::Mine])) {
|
|
ignore_mask.set(Weapon::Info_Flags::Mine);
|
|
}
|
|
|
|
#ifndef NDEBUG
|
|
for (i=0; i<MAX_WEAPON_TYPES; i++) {
|
|
weapon_id_list[i] = -1;
|
|
weapon_bank_list[i] = -1;
|
|
}
|
|
#endif
|
|
|
|
// Stuff weapon_bank_list with bank index of available weapons.
|
|
num_weapon_types = get_available_secondary_weapons(objp, weapon_id_list, weapon_bank_list);
|
|
|
|
// If there are no valid choices, bail
|
|
if (num_weapon_types == 0)
|
|
return false;
|
|
|
|
// Ignore homing weapons if we didn't specify a flag - for priority 1
|
|
if ((aip->ai_profile_flags[AI::Profile_Flags::Smart_secondary_weapon_selection]) && (prio1.none_set())) {
|
|
ignore_mask.set(Weapon::Info_Flags::Homing_aspect).set(Weapon::Info_Flags::Homing_heat).set(Weapon::Info_Flags::Homing_javelin);
|
|
}
|
|
|
|
ignore_mask_without_huge = ignore_mask;
|
|
ignore_mask_without_huge.remove(Weapon::Info_Flags::Huge).remove(Weapon::Info_Flags::Capital_plus);
|
|
|
|
int priority2_index = -1;
|
|
|
|
for (i=0; i<num_weapon_types; i++) {
|
|
auto wi_flags = Weapon_info[swp->secondary_bank_weapons[weapon_bank_list[i]]].wi_flags;
|
|
auto ignore_mask_to_use = ((aip->ai_profile_flags[AI::Profile_Flags::Smart_secondary_weapon_selection]) && (wi_flags[Weapon::Info_Flags::Bomber_plus])) ? ignore_mask_without_huge : ignore_mask;
|
|
|
|
if (!(wi_flags & ignore_mask_to_use).any_set()) { // Maybe bombs are illegal.
|
|
if ((wi_flags & prio1).any_set()) {
|
|
swp->current_secondary_bank = weapon_bank_list[i]; // Found first priority, return it.
|
|
rval = true;
|
|
break;
|
|
} else if ((wi_flags & prio2).any_set())
|
|
priority2_index = weapon_bank_list[i]; // Found second priority, but might still find first priority.
|
|
}
|
|
}
|
|
|
|
// Ignore homing weapons if we didn't specify a flag - for priority 2
|
|
if ((aip->ai_profile_flags[AI::Profile_Flags::Smart_secondary_weapon_selection]) && (prio2.none_set())) {
|
|
ignore_mask.set(Weapon::Info_Flags::Homing_aspect).set(Weapon::Info_Flags::Homing_heat).set(Weapon::Info_Flags::Homing_javelin);
|
|
}
|
|
|
|
// If didn't find anything above, then pick any secondary weapon.
|
|
if (i == num_weapon_types) {
|
|
if (priority2_index == -1) {
|
|
for (i=0; i<num_weapon_types; i++) {
|
|
auto wi_flags = Weapon_info[swp->secondary_bank_weapons[weapon_bank_list[i]]].wi_flags;
|
|
auto ignore_mask_to_use = ((aip->ai_profile_flags[AI::Profile_Flags::Smart_secondary_weapon_selection]) && (wi_flags[Weapon::Info_Flags::Bomber_plus])) ? (ignore_mask - Weapon::Info_Flags::Huge) : ignore_mask;
|
|
|
|
if (!(wi_flags & ignore_mask_to_use).any_set()) { // Maybe bombs are illegal.
|
|
if (ship_secondary_has_ammo(swp, weapon_bank_list[i])) {
|
|
priority2_index = weapon_bank_list[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// if we have a valid priority2, use that
|
|
if (priority2_index >= 0) {
|
|
swp->current_secondary_bank = priority2_index;
|
|
rval = true;
|
|
}
|
|
}
|
|
|
|
Assertion(swp->current_secondary_bank >= 0, "ai_select_secondary_weapon assigned a -1 secondary bank to %s", Ships[objp->instance].ship_name);
|
|
// if we got an invalid bank somehow, just put it back and bail
|
|
if (swp->current_secondary_bank < 0) {
|
|
swp->current_secondary_bank = initial_bank;
|
|
return false;
|
|
}
|
|
|
|
// If switched banks, force reacquisition of aspect lock.
|
|
if (swp->current_secondary_bank != initial_bank) {
|
|
aip->aspect_locked_time = 0.0f;
|
|
aip->current_target_is_locked = 0;
|
|
}
|
|
|
|
if (swp->current_secondary_bank >= 0 && swp->current_secondary_bank < MAX_SHIP_SECONDARY_BANKS)
|
|
{
|
|
weapon_info *wip=&Weapon_info[swp->secondary_bank_weapons[swp->current_secondary_bank]];
|
|
|
|
// phreak -- rapid dumbfire? let it rip!
|
|
if ((aip->ai_profile_flags[AI::Profile_Flags::Allow_rapid_secondary_dumbfire]) && !(wip->is_homing()) && (wip->fire_wait < .5f))
|
|
{
|
|
aip->ai_flags.set(AI::AI_Flags::Unload_secondaries);
|
|
}
|
|
}
|
|
|
|
ship_secondary_changed(&Ships[objp->instance]); // AL: let multiplayer know if secondary bank has changed
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* @return number of objects homing on object *target_objp
|
|
*/
|
|
int compute_num_homing_objects(const object *target_objp)
|
|
{
|
|
int count = 0;
|
|
|
|
for (auto mop: list_range(&Missile_obj_list)) {
|
|
auto objp = &Objects[mop->objnum];
|
|
if (objp->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
if (objp->type == OBJ_WEAPON) {
|
|
if (Weapon_info[Weapons[objp->instance].weapon_info_index].is_homing()) {
|
|
if (Weapons[objp->instance].homing_object == target_objp) {
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
// Object *firing_objp just fired weapon weapon_index (index in Weapon_info).
|
|
// If it's a shockwave weapon, tell your team about it!
|
|
void ai_maybe_announce_shockwave_weapon(object *firing_objp, int weapon_index)
|
|
{
|
|
if ((firing_objp->type == OBJ_SHIP) && (Weapon_info[weapon_index].shockwave.speed > 0.0f)) {
|
|
ship_obj *so;
|
|
int firing_ship_team;
|
|
|
|
firing_ship_team = Ships[firing_objp->instance].team;
|
|
|
|
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
|
|
object *A = &Objects[so->objnum];
|
|
if (A->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
Assert(A->type == OBJ_SHIP);
|
|
|
|
if (Ships[A->instance].team == firing_ship_team) {
|
|
ai_info *aip = &Ai_info[Ships[A->instance].ai_index];
|
|
|
|
// AL 1-5-98: only avoid shockwave if not docked or repairing
|
|
if ( !object_is_docked(A) && !(aip->ai_flags[AI::AI_Flags::Repairing, AI::AI_Flags::Being_repaired]) ) {
|
|
aip->ai_flags.set(AI::AI_Flags::Avoid_shockwave_weapon);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return total payload of all incoming missiles.
|
|
*/
|
|
float compute_incoming_payload(object *target_objp)
|
|
{
|
|
missile_obj *mo;
|
|
float payload = 0.0f;
|
|
|
|
for ( mo = GET_NEXT(&Missile_obj_list); mo != END_OF_LIST(&Missile_obj_list); mo = GET_NEXT(mo) ) {
|
|
auto objp = &Objects[mo->objnum];
|
|
if (objp->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
Assert(objp->type == OBJ_WEAPON);
|
|
if (Weapons[objp->instance].homing_object == target_objp) {
|
|
payload += Weapon_info[Weapons[objp->instance].weapon_info_index].damage;
|
|
}
|
|
}
|
|
|
|
return payload;
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Return true if OK for *aip to fire its current weapon at its current target.
|
|
// Only reason this function returns false is:
|
|
// weapon is a homer
|
|
// targeted at player
|
|
// OR: player has too many homers targeted at him
|
|
// Missiontime in that dead zone in which can't fire at this player
|
|
// OR: secondary los flag is set but no line of sight is availabe
|
|
// Note: If player is attacking a ship, that ship is allowed to fire at player. Otherwise, we get in a situation in which
|
|
// player is attacking a large ship, but that large ship is not defending itself with missiles.
|
|
bool check_ok_to_fire(int objnum, int target_objnum, const weapon_info *wip, int secondary_bank, const vec3d *firing_pos_global)
|
|
{
|
|
int num_homers = 0;
|
|
|
|
if (target_objnum >= 0) {
|
|
object *tobjp = &Objects[target_objnum];
|
|
|
|
// AL 3-4-98: Ensure objp target is a ship first
|
|
if ( tobjp->type == OBJ_SHIP ) {
|
|
if (Ship_info[Ships[tobjp->instance].ship_info_index].is_small_ship()) {
|
|
num_homers = compute_num_homing_objects(&Objects[target_objnum]);
|
|
}
|
|
}
|
|
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Require_exact_los] || wip->wi_flags[Weapon::Info_Flags::Require_exact_los]) {
|
|
//Check if th AI has Line of Sight and is allowed to fire
|
|
if (!check_los(objnum, target_objnum, The_mission.ai_profile->los_min_detection_radius, -1, secondary_bank, firing_pos_global)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// If player, maybe fire based on Skill_level and number of incoming weapons.
|
|
// If non-player, maybe fire based on payload of incoming weapons.
|
|
if (wip->is_homing()) {
|
|
if ((target_objnum > -1) && (tobjp->flags[Object::Object_Flags::Player_ship])) {
|
|
if (Ai_info[Ships[tobjp->instance].ai_index].target_objnum != objnum) {
|
|
// Don't allow AI ships to fire at player for fixed periods of time based on skill level.
|
|
// With 5 skill levels, at Very Easy, they fire in 1/7 of every 10 second interval.
|
|
// At Easy, 2/7...at Expert, 5/7
|
|
int t = ((Missiontime /(65536*10)) ^ target_objnum ^ 0x01) % (NUM_SKILL_LEVELS+2);
|
|
if (t > Ai_info[Ships[tobjp->instance].ai_index].ai_chance_to_use_missiles_on_plr) {
|
|
return false;
|
|
}
|
|
}
|
|
int swarmers = 0;
|
|
if (wip->wi_flags[Weapon::Info_Flags::Swarm])
|
|
swarmers = 2; // Note, always want to be able to fire swarmers if no currently incident homers.
|
|
if (The_mission.ai_profile->max_allowed_player_homers[Game_skill_level] < num_homers + swarmers) {
|
|
return false;
|
|
}
|
|
} else if (num_homers > 3) {
|
|
float incoming_payload;
|
|
|
|
incoming_payload = compute_incoming_payload(&Objects[target_objnum]);
|
|
|
|
if (incoming_payload > tobjp->hull_strength) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Returns true if *aip has a line of sight to its current target.
|
|
// threshold defines the minimum radius for an object to be considered relevant for LoS
|
|
bool check_los(int objnum, int target_objnum, float threshold, int primary_bank, int secondary_bank, const vec3d *firing_pos_global) {
|
|
|
|
//Do both checks over an XOR-Check for more detailed messages
|
|
int sources = (primary_bank != -1 ? 1 : 0) + (secondary_bank != -1 ? 1 : 0) + (firing_pos_global != nullptr ? 1 : 0);
|
|
Assertion(sources >= 1, "Cannot check line of sight from a model with neither a primar bank, nor secondary bank nor a turret set as a source.");
|
|
Assertion(sources <= 1, "Cannot check line of sight from a model with more than one source set.");
|
|
|
|
vec3d start;
|
|
|
|
object* firing_ship = &Objects[objnum];
|
|
|
|
if (firing_pos_global != nullptr) {
|
|
start = *firing_pos_global;
|
|
} else {
|
|
bool is_primary = secondary_bank == -1;
|
|
ship *shipp = &Ships[firing_ship->instance];
|
|
ship_weapon *swp = &shipp->weapons;
|
|
weapon_info *wip = &Weapon_info[is_primary ? swp->primary_bank_weapons[primary_bank] : swp->secondary_bank_weapons[secondary_bank]];
|
|
polymodel* pm = model_get(Ship_info[shipp->ship_info_index].model_num);
|
|
|
|
vec3d pnt = is_primary ? pm->gun_banks[primary_bank].pnt[swp->primary_next_slot[primary_bank]] : pm->missile_banks[secondary_bank].pnt[swp->secondary_next_slot[secondary_bank]];
|
|
vec3d firing_point;
|
|
|
|
//Following Section taken from ship.cpp ship_fire_secondary()
|
|
polymodel* weapon_model = nullptr;
|
|
if (wip->external_model_num >= 0) {
|
|
weapon_model = model_get(wip->external_model_num);
|
|
}
|
|
|
|
if (weapon_model && weapon_model->n_guns) {
|
|
int external_bank = is_primary ? primary_bank : secondary_bank + MAX_SHIP_PRIMARY_BANKS;
|
|
if (wip->wi_flags[Weapon::Info_Flags::External_weapon_fp]) {
|
|
if ((weapon_model->n_guns <= swp->external_model_fp_counter[external_bank]) || (swp->external_model_fp_counter[external_bank] < 0))
|
|
swp->external_model_fp_counter[external_bank] = 0;
|
|
vm_vec_add2(&pnt, &weapon_model->gun_banks[0].pnt[swp->external_model_fp_counter[external_bank]]);
|
|
swp->external_model_fp_counter[external_bank]++;
|
|
}
|
|
else {
|
|
// make it use the 0 index slot
|
|
vm_vec_add2(&pnt, &weapon_model->gun_banks[0].pnt[0]);
|
|
}
|
|
}
|
|
vm_vec_unrotate(&firing_point, &pnt, &firing_ship->orient);
|
|
vm_vec_add(&start, &firing_point, &firing_ship->pos);
|
|
}
|
|
|
|
vec3d end = Objects[target_objnum].pos;
|
|
|
|
//Don't collision check against ourselves or our target
|
|
return test_line_of_sight(&start, &end, {objnum, target_objnum}, threshold);
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Fire a secondary weapon.
|
|
// Maybe choose to fire a different one.
|
|
int ai_fire_secondary_weapon(object *objp)
|
|
{
|
|
ship_weapon *swp;
|
|
ship *shipp;
|
|
int current_bank;
|
|
int rval = 0;
|
|
|
|
#ifndef NDEBUG
|
|
if (!Ai_firing_enabled)
|
|
return rval;
|
|
#endif
|
|
|
|
Assert( objp != NULL );
|
|
Assert(objp->type == OBJ_SHIP);
|
|
shipp = &Ships[objp->instance];
|
|
swp = &shipp->weapons;
|
|
|
|
Assert( shipp->ship_info_index >= 0 && shipp->ship_info_index < ship_info_size());
|
|
|
|
// Select secondary weapon.
|
|
current_bank = swp->current_secondary_bank;
|
|
|
|
if (current_bank == -1) {
|
|
return rval;
|
|
}
|
|
|
|
Assert(current_bank < shipp->weapons.num_secondary_banks);
|
|
|
|
weapon_info *wip = &Weapon_info[shipp->weapons.secondary_bank_weapons[current_bank]];
|
|
|
|
if ((wip->is_locked_homing()) && (!Ai_info[shipp->ai_index].current_target_is_locked)) {
|
|
swp->next_secondary_fire_stamp[current_bank] = timestamp(250);
|
|
} else if ((wip->wi_flags[Weapon::Info_Flags::Bomb]) || (vm_vec_dist_quick(&objp->pos, &En_objp->pos) > 50.0f)) {
|
|
// This might look dumb, firing a bomb even if closer than 50 meters, but the reason is, if you're carrying
|
|
// bombs, delivering them is probably more important than surviving.
|
|
ai_info *aip;
|
|
|
|
aip = &Ai_info[shipp->ai_index];
|
|
|
|
// Note, maybe don't fire if firing at player and any homers yet fired.
|
|
// Decreasing chance to fire the more homers are incoming on player.
|
|
if (check_ok_to_fire(OBJ_INDEX(objp), aip->target_objnum, wip, current_bank, nullptr)) {
|
|
|
|
if (wip->multi_lock && !ai_do_multilock(aip, wip))
|
|
return rval;
|
|
|
|
if (ship_fire_secondary(objp)) {
|
|
rval = 1;
|
|
swp->next_secondary_fire_stamp[current_bank] = timestamp(500);
|
|
|
|
if (wip->multi_lock && wip->launch_reset_locks) {
|
|
aip->aspect_locked_time = 0.0f;
|
|
aip->current_target_is_locked = 0;
|
|
}
|
|
}
|
|
|
|
} else {
|
|
swp->next_secondary_fire_stamp[current_bank] = timestamp(500);
|
|
}
|
|
}
|
|
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* Return true if it looks like obj1, if continuing to move along current vector, will collide with obj2.
|
|
*/
|
|
int might_collide_with_ship(object *obj1, object *obj2, float dot_to_enemy, float dist_to_enemy, float duration)
|
|
{
|
|
if (obj1->phys_info.speed * duration + 2*(obj1->radius + obj2->radius) > dist_to_enemy)
|
|
if (dot_to_enemy > 0.8f - 2*(obj1->radius + obj2->radius)/dist_to_enemy)
|
|
return objects_will_collide(obj1, obj2, duration, 2.0f);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Return true if ship *objp firing a laser believes it will hit a teammate.
|
|
*/
|
|
int might_hit_teammate(object *firing_objp)
|
|
{
|
|
int team;
|
|
object *objp;
|
|
ship_obj *so;
|
|
|
|
team = Ships[firing_objp->instance].team;
|
|
|
|
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
|
|
objp = &Objects[so->objnum];
|
|
if (objp->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
if (Ships[objp->instance].team == team) {
|
|
float dist, dot;
|
|
vec3d vec_to_objp;
|
|
|
|
vm_vec_sub(&vec_to_objp, &firing_objp->pos, &objp->pos);
|
|
dist = vm_vec_mag_quick(&vec_to_objp);
|
|
dot = vm_vec_dot(&firing_objp->orient.vec.fvec, &vec_to_objp)/dist;
|
|
if (might_collide_with_ship(firing_objp, objp, dot, dist, 2.0f))
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void render_all_ship_bay_paths(object *objp)
|
|
{
|
|
int i,j,clr;
|
|
polymodel *pm;
|
|
model_path *mp;
|
|
|
|
pm = model_get(Ship_info[Ships[objp->instance].ship_info_index].model_num);
|
|
vec3d global_path_point;
|
|
vertex v, prev_vertex;
|
|
|
|
if ( pm->ship_bay == NULL )
|
|
return;
|
|
|
|
memset(&v, 0, sizeof(v));
|
|
|
|
for ( i = 0; i < pm->ship_bay->num_paths; i++ ) {
|
|
mp = &pm->paths[pm->ship_bay->path_indexes[i]];
|
|
|
|
for ( j = 0; j < mp->nverts; j++ ) {
|
|
vm_vec_unrotate(&global_path_point, &mp->verts[j].pos, &objp->orient);
|
|
vm_vec_add2(&global_path_point, &objp->pos);
|
|
g3_rotate_vertex(&v, &global_path_point);
|
|
clr = 255 - j*50;
|
|
if ( clr < 50 )
|
|
clr = 100;
|
|
gr_set_color(0, clr, 0);
|
|
|
|
if ( j == mp->nverts-1 ) {
|
|
gr_set_color(255, 0, 0);
|
|
}
|
|
|
|
g3_draw_sphere( &v, 1.5f);
|
|
|
|
if ( j > 0 )
|
|
g3_draw_line(&v, &prev_vertex);
|
|
|
|
prev_vertex = v;
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Debug function to show all path points associated with an object
|
|
*/
|
|
void render_all_subsys_paths(object *objp)
|
|
{
|
|
int i,j,clr;
|
|
polymodel *pm;
|
|
model_path *mp;
|
|
|
|
pm = model_get(Ship_info[Ships[objp->instance].ship_info_index].model_num);
|
|
vec3d global_path_point;
|
|
vertex v, prev_vertex;
|
|
|
|
if ( pm->ship_bay == NULL )
|
|
return;
|
|
|
|
memset(&v, 0, sizeof(v));
|
|
|
|
for ( i = 0; i < pm->n_paths; i++ ) {
|
|
mp = &pm->paths[i];
|
|
for ( j = 0; j < mp->nverts; j++ ) {
|
|
vm_vec_unrotate(&global_path_point, &mp->verts[j].pos, &objp->orient);
|
|
vm_vec_add2(&global_path_point, &objp->pos);
|
|
g3_rotate_vertex(&v, &global_path_point);
|
|
clr = 255 - j*50;
|
|
if ( clr < 50 )
|
|
clr = 100;
|
|
gr_set_color(0, clr, 0);
|
|
|
|
if ( j == mp->nverts-1 ) {
|
|
gr_set_color(255, 0, 0);
|
|
}
|
|
|
|
g3_draw_sphere( &v, 1.5f);
|
|
|
|
if ( j > 0 )
|
|
g3_draw_line(&v, &prev_vertex);
|
|
|
|
prev_vertex = v;
|
|
}
|
|
}
|
|
}
|
|
|
|
void render_path_points(object *objp)
|
|
{
|
|
ship *shipp = &Ships[objp->instance];
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
object *dobjp;
|
|
|
|
render_all_subsys_paths(objp);
|
|
render_all_ship_bay_paths(objp);
|
|
|
|
if (aip->goal_objnum < 0)
|
|
return;
|
|
|
|
dobjp = &Objects[aip->goal_objnum];
|
|
vec3d dock_point, global_dock_point;
|
|
vertex v;
|
|
|
|
auto pmi = model_get_instance(Ships[dobjp->instance].model_instance_num);
|
|
auto pm = model_get(pmi->model_num);
|
|
|
|
if (pm->n_docks) {
|
|
dock_point = pm->docking_bays[0].pnt[0];
|
|
model_instance_local_to_global_point(&global_dock_point, &dock_point, pm, pmi, 0, &dobjp->orient, &dobjp->pos );
|
|
g3_rotate_vertex(&v, &global_dock_point);
|
|
gr_set_color(255, 255, 255);
|
|
g3_draw_sphere( &v, 1.5f);
|
|
}
|
|
|
|
if (aip->path_start != -1) {
|
|
vertex prev_vertex;
|
|
pnode *pp = &Path_points[aip->path_start];
|
|
int num_points = aip->path_length;
|
|
int i;
|
|
|
|
for (i=0; i<num_points; i++) {
|
|
vertex v0;
|
|
|
|
memset(&v0, 0, sizeof(v0));
|
|
|
|
g3_rotate_vertex( &v0, &pp->pos );
|
|
|
|
gr_set_color(0, 128, 96);
|
|
if (i != 0)
|
|
g3_draw_line(&v0, &prev_vertex);
|
|
|
|
if (pp-Path_points == aip->path_cur)
|
|
gr_set_color(255,255,0);
|
|
|
|
g3_draw_sphere( &v0, 4.5f);
|
|
|
|
prev_vertex = v0;
|
|
|
|
pp++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the distance that the current AI weapon will travel
|
|
*/
|
|
float ai_get_weapon_dist(ship_weapon *swp)
|
|
{
|
|
int bank_num, weapon_num;
|
|
|
|
bank_num = swp->current_primary_bank;
|
|
weapon_num = swp->primary_bank_weapons[bank_num];
|
|
|
|
// If weapon_num is illegal, return a reasonable value. A valid weapon
|
|
// will get selected when this ship tries to fire.
|
|
if (weapon_num == -1) {
|
|
return 1000.0f;
|
|
}
|
|
|
|
Assertion(weapon_num >= 0, "A ship's weapon has a nonsense index of %d, but the ship's name is not accessible from this function. Please report!", weapon_num);
|
|
|
|
return MIN((Weapon_info[weapon_num].max_speed * Weapon_info[weapon_num].lifetime), Weapon_info[weapon_num].weapon_range);
|
|
}
|
|
|
|
float ai_get_weapon_speed(const ship_weapon *swp)
|
|
{
|
|
int bank_num, weapon_num;
|
|
|
|
bank_num = swp->current_primary_bank;
|
|
if (bank_num < 0)
|
|
return 100.0f;
|
|
|
|
weapon_num = swp->primary_bank_weapons[bank_num];
|
|
|
|
if (weapon_num == -1) {
|
|
return 100.0f;
|
|
}
|
|
|
|
Assertion(weapon_num >= 0, "A ship's weapon has a nonsense index of %d, but the ship's name is not accessible from this function. Please report!", weapon_num);
|
|
|
|
return Weapon_info[weapon_num].max_speed;
|
|
}
|
|
|
|
weapon_info* ai_get_weapon(ship_weapon *swp)
|
|
{
|
|
int bank_num, weapon_num;
|
|
|
|
bank_num = swp->current_primary_bank;
|
|
if (bank_num < 0)
|
|
return nullptr;
|
|
|
|
weapon_num = swp->primary_bank_weapons[bank_num];
|
|
|
|
if (weapon_num == -1) {
|
|
return nullptr;
|
|
}
|
|
|
|
Assertion(weapon_num >= 0, "A ship's weapon has a nonsense index of %d, but the ship's name is not accessible from this function. Please report!", weapon_num);
|
|
|
|
return &Weapon_info[weapon_num];
|
|
}
|
|
|
|
// Compute the predicted position of a ship to be fired upon from a turret.
|
|
// This is based on position of firing gun, enemy object, weapon speed and skill level constraints.
|
|
// Return value in *predicted_enemy_pos.
|
|
// Also, stuff globals G_predicted_pos, G_collision_time and G_fire_pos.
|
|
// *pobjp object firing the weapon
|
|
// *eobjp object being fired upon
|
|
void set_predicted_enemy_pos_turret(vec3d *predicted_enemy_pos, const vec3d *gun_pos, const object *pobjp, const vec3d *enemy_pos, const vec3d *enemy_vel, float weapon_speed, float time_enemy_in_range)
|
|
{
|
|
ship *shipp = &Ships[pobjp->instance];
|
|
float range_time;
|
|
|
|
if (weapon_speed < 1.0f)
|
|
weapon_speed = 1.0f;
|
|
|
|
range_time = 2.0f;
|
|
|
|
// Make it take longer for enemies to get player's allies in range based on skill level.
|
|
if (iff_x_attacks_y(Ships[pobjp->instance].team, Player_ship->team))
|
|
range_time += Ai_info[shipp->ai_index].ai_in_range_time;
|
|
|
|
if (time_enemy_in_range < range_time) {
|
|
float dist;
|
|
|
|
dist = vm_vec_dist_quick(&pobjp->pos, enemy_pos);
|
|
vm_vec_scale_add(predicted_enemy_pos, enemy_pos, enemy_vel, time_enemy_in_range * dist/weapon_speed);
|
|
} else {
|
|
float collision_time, scale;
|
|
vec3d rand_vec;
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
|
|
collision_time = compute_collision_time(enemy_pos, enemy_vel, gun_pos, weapon_speed);
|
|
|
|
if (collision_time == 0.0f){
|
|
collision_time = 100.0f;
|
|
}
|
|
|
|
vm_vec_scale_add(predicted_enemy_pos, enemy_pos, enemy_vel, collision_time);
|
|
if (time_enemy_in_range > 2*range_time){
|
|
scale = (1.0f - aip->ai_accuracy) * 4.0f;
|
|
} else {
|
|
scale = (1.0f - aip->ai_accuracy) * 4.0f * (1.0f + 4.0f * (1.0f - time_enemy_in_range/(2*range_time)));
|
|
}
|
|
|
|
static_randvec(((OBJ_INDEX(pobjp)) ^ (Missiontime >> 16)) & 7, &rand_vec);
|
|
|
|
vm_vec_scale_add2(predicted_enemy_pos, &rand_vec, scale);
|
|
G_collision_time = collision_time;
|
|
G_fire_pos = *gun_pos;
|
|
}
|
|
|
|
G_predicted_pos = *predicted_enemy_pos;
|
|
}
|
|
|
|
// Compute the predicted position of a ship to be fired upon.
|
|
// This is based on current position of firing object, enemy object, relative position of gun on firing object,
|
|
// weapon speed and skill level constraints.
|
|
// Return value in *predicted_enemy_pos.
|
|
// Also, stuff globals G_predicted_pos, G_collision_time and G_fire_pos.
|
|
//SUSHI: Modified to take in a position and accel value instead of reading it directly from the enemy object
|
|
void set_predicted_enemy_pos(vec3d *predicted_enemy_pos, object *pobjp, vec3d *enemy_pos, vec3d *enemy_vel, ai_info *aip)
|
|
{
|
|
float weapon_speed, range_time;
|
|
ship *shipp = &Ships[pobjp->instance];
|
|
weapon_info *wip;
|
|
vec3d target_moving_direction;
|
|
|
|
Assert( enemy_pos != NULL );
|
|
Assert( enemy_vel != NULL );
|
|
|
|
wip = ai_get_weapon(&shipp->weapons);
|
|
target_moving_direction = *enemy_vel;
|
|
|
|
if (wip != NULL && The_mission.ai_profile->flags[AI::Profile_Flags::Use_additive_weapon_velocity])
|
|
vm_vec_scale_sub2(&target_moving_direction, &pobjp->phys_info.vel, wip->vel_inherit_amount);
|
|
|
|
if (wip != NULL)
|
|
weapon_speed = wip->max_speed;
|
|
else
|
|
weapon_speed = 100.0f;
|
|
|
|
weapon_speed = MAX(weapon_speed, 1.0f); // set not less than 1
|
|
|
|
range_time = 2.0f;
|
|
|
|
// Make it take longer for enemies to get player's allies in range based on skill level.
|
|
// but don't bias team v. team missions
|
|
if ( !(MULTI_TEAM) )
|
|
{
|
|
if (iff_x_attacks_y(shipp->team, Player_ship->team))
|
|
range_time += aip->ai_in_range_time;
|
|
}
|
|
|
|
if (aip->time_enemy_in_range < range_time) {
|
|
float dist;
|
|
|
|
dist = vm_vec_dist_quick(&pobjp->pos, enemy_pos);
|
|
vm_vec_scale_add(predicted_enemy_pos, enemy_pos, &target_moving_direction, aip->time_enemy_in_range * dist/weapon_speed);
|
|
} else {
|
|
float collision_time;
|
|
vec3d gun_pos, pnt;
|
|
polymodel *pm = model_get(Ship_info[shipp->ship_info_index].model_num);
|
|
|
|
// Compute position of gun in absolute space and use that as fire position
|
|
// ...unless we want to just use the ship center
|
|
if(pm->gun_banks != NULL && !(The_mission.ai_profile->flags[AI::Profile_Flags::Ai_aims_from_ship_center])){
|
|
pnt = pm->gun_banks[0].pnt[0];
|
|
} else {
|
|
//Use the convergence offset, if there is one
|
|
vm_vec_copy_scale(&pnt, &Ship_info[shipp->ship_info_index].convergence_offset, 1.0f);
|
|
}
|
|
vm_vec_unrotate(&gun_pos, &pnt, &pobjp->orient);
|
|
vm_vec_add2(&gun_pos, &pobjp->pos);
|
|
|
|
collision_time = compute_collision_time(enemy_pos, &target_moving_direction, &gun_pos, weapon_speed);
|
|
|
|
if (collision_time == 0.0f) {
|
|
collision_time = 100.0f;
|
|
}
|
|
|
|
vm_vec_scale_add(predicted_enemy_pos, enemy_pos, &target_moving_direction, collision_time);
|
|
|
|
// set globals
|
|
G_collision_time = collision_time;
|
|
G_fire_pos = gun_pos;
|
|
}
|
|
|
|
// Now add error terms (1) regular aim (2) EMP (3) stealth
|
|
float scale = 0.0f;
|
|
vec3d rand_vec;
|
|
|
|
// regular skill level error in aim
|
|
if (aip->time_enemy_in_range > 2*range_time) {
|
|
scale = (1.0f - aip->ai_accuracy) * 4.0f;
|
|
} else {
|
|
scale = (1.0f - aip->ai_accuracy) * 4.0f * (1.0f + 4.0f * (1.0f - aip->time_enemy_in_range/(2*range_time)));
|
|
}
|
|
|
|
// if this ship is under the effect of an EMP blast, throw his aim off a bit
|
|
if (shipp->emp_intensity > 0.0f) {
|
|
// never go lower than 1/2 of the EMP effect max, otherwise things aren't noticeable
|
|
scale += (MAX_EMP_INACCURACY * (shipp->emp_intensity < 0.5f ? 0.5f : shipp->emp_intensity));
|
|
mprintf(("AI miss scale factor (EMP) %f\n",scale));
|
|
}
|
|
|
|
// if stealthy ship, throw his aim off, more when farther away and when dot is small
|
|
if ( aip->ai_flags[AI::AI_Flags::Stealth_pursuit] ) {
|
|
float dist = vm_vec_dist_quick(&pobjp->pos, enemy_pos);
|
|
vec3d temp;
|
|
vm_vec_sub(&temp, enemy_pos, &pobjp->pos);
|
|
vm_vec_normalize_quick(&temp);
|
|
float dot = vm_vec_dot(&temp, &pobjp->orient.vec.fvec);
|
|
float st_err = 3.0f * (1.4f - dot) * (1.0f + dist / (get_skill_stealth_dist_scaler() * STEALTH_MAX_VIEW_DIST)) * (1 - aip->ai_accuracy);
|
|
scale += st_err;
|
|
}
|
|
|
|
// get a random vector that changes slowly over time (1x / sec)
|
|
static_randvec(((OBJ_INDEX(pobjp)) ^ (Missiontime >> 16)) & 7, &rand_vec);
|
|
|
|
vm_vec_scale_add2(predicted_enemy_pos, &rand_vec, scale);
|
|
|
|
// set global
|
|
G_predicted_pos = *predicted_enemy_pos;
|
|
}
|
|
|
|
/**
|
|
* Handler of submode for Chase. Go into a continuous turn for awhile.
|
|
*/
|
|
void ai_chase_ct()
|
|
{
|
|
vec3d tvec;
|
|
ai_info *aip;
|
|
|
|
Assert(Ships[Pl_objp->instance].ai_index >= 0);
|
|
aip = &Ai_info[Ships[Pl_objp->instance].ai_index];
|
|
|
|
// Make a continuous turn towards any combination of possibly negated
|
|
// up and right vectors.
|
|
tvec = Pl_objp->pos;
|
|
|
|
if (aip->submode_parm0 & 0x01)
|
|
vm_vec_add2(&tvec, &Pl_objp->orient.vec.rvec);
|
|
if (aip->submode_parm0 & 0x02)
|
|
vm_vec_sub2(&tvec, &Pl_objp->orient.vec.rvec);
|
|
if (aip->submode_parm0 & 0x04)
|
|
vm_vec_add2(&tvec, &Pl_objp->orient.vec.uvec);
|
|
if (aip->submode_parm0 & 0x08)
|
|
vm_vec_sub2(&tvec, &Pl_objp->orient.vec.uvec);
|
|
|
|
// Detect degenerate cases that cause tvec to be same as player pos.
|
|
if (vm_vec_dist_quick(&tvec, &Pl_objp->pos) < 0.1f) {
|
|
aip->submode_parm0 &= 0x05;
|
|
if (aip->submode_parm0 == 0)
|
|
aip->submode_parm0 = 1;
|
|
vm_vec_add2(&tvec, &Pl_objp->orient.vec.rvec);
|
|
}
|
|
|
|
ai_turn_towards_vector(&tvec, Pl_objp, nullptr, nullptr, 0.0f, 0);
|
|
accelerate_ship(aip, 1.0f);
|
|
}
|
|
|
|
/**
|
|
* ATTACK submode handler for chase mode.
|
|
*/
|
|
static void ai_chase_eb(ai_info *aip, vec3d *predicted_enemy_pos)
|
|
{
|
|
vec3d _pep;
|
|
float dot_to_enemy, dot_from_enemy;
|
|
|
|
compute_dots(Pl_objp, En_objp, &dot_to_enemy, &dot_from_enemy);
|
|
|
|
// If we're trying to slow down to get behind, then point to turn towards is different.
|
|
_pep = *predicted_enemy_pos;
|
|
if ((dot_to_enemy > dot_from_enemy + 0.1f) || (dot_to_enemy > 0.9f))
|
|
vm_vec_scale_add(&_pep, &Pl_objp->pos, &En_objp->orient.vec.fvec, 100.0f);
|
|
|
|
ai_turn_towards_vector(&_pep, Pl_objp, nullptr, nullptr, 0.0f, 0);
|
|
|
|
accelerate_ship(aip, 0.0f);
|
|
}
|
|
|
|
// Return time until weapon_objp might hit ship_objp.
|
|
// Assumes ship_objp is not moving.
|
|
// Returns negative time if not going to hit.
|
|
// This is a very approximate function, but is pretty fast.
|
|
float ai_endangered_time(const object *ship_objp, const object *weapon_objp)
|
|
{
|
|
float to_dot, from_dot, dist;
|
|
|
|
dist = compute_dots(ship_objp, weapon_objp, &to_dot, &from_dot);
|
|
|
|
// Note, this is bogus. It assumes only the weapon is moving.
|
|
// Only proceed if weapon sort of pointing at object and object pointing towards or away from weapon
|
|
// (Ie, if object moving at right angle to weapon, just continue for now...)
|
|
if (weapon_objp->phys_info.speed < 1.0f)
|
|
return dist + 1.0f;
|
|
else if ((from_dot > 0.1f) && (dist/(from_dot*from_dot) < 48*ship_objp->radius)) //: don't require them to see it, they have instruments!: && (fl_abs(to_dot) > 0.5f))
|
|
return dist / weapon_objp->phys_info.speed;
|
|
else
|
|
return -1.0f;
|
|
}
|
|
|
|
// Return time until danger weapon could hit this ai object.
|
|
// Return negative time if not endangered.
|
|
float ai_endangered_by_weapon(ai_info *aip)
|
|
{
|
|
object *weapon_objp;
|
|
|
|
if (aip->danger_weapon_objnum == -1) {
|
|
return -1.0f;
|
|
}
|
|
|
|
Assertion(aip->danger_weapon_objnum >= 0, "%s has a nonsense danger_weapon_objnum of %d. Please report!", Ships[aip->shipnum].ship_name, aip->danger_weapon_objnum);\
|
|
|
|
weapon_objp = &Objects[aip->danger_weapon_objnum];
|
|
|
|
if (weapon_objp->signature != aip->danger_weapon_signature) {
|
|
aip->danger_weapon_objnum = -1;
|
|
return -1.0f;
|
|
}
|
|
|
|
return ai_endangered_time(&Objects[Ships[aip->shipnum].objnum], weapon_objp);
|
|
}
|
|
|
|
// Return true if this ship is near full strength.
|
|
// Goober5000: simplified and accounted for shields being 0
|
|
int ai_near_full_strength(object *objp)
|
|
{
|
|
return (get_hull_pct(objp) > 0.9f) || (get_shield_pct(objp) > 0.8f);
|
|
}
|
|
|
|
void do_random_sidethrust(ai_info *aip, ship_info *sip)
|
|
{
|
|
//Sidethrust vector is initially based on the velocity vector representing the ship's current sideways motion.
|
|
vec2d side_vec;
|
|
//Length to hold circle strafe is random (1-3) + slide accel time, changes every 4 seconds
|
|
int strafeHoldDirAmount = (int)(sip->slide_accel) + static_rand_range((Missiontime + static_rand(aip->shipnum)) >> 18, 1, 3);
|
|
//Get a random float using some of the more significant chunks of the missiontime as a seed (>>16 means it changes every second)
|
|
//This means that we get the same random values for a little bit.
|
|
//Using static_rand(shipnum) as a crude hash function to make sure that the seed is different for each ship and direction
|
|
//The *2 ensures that y and x stay separate.
|
|
if (strafeHoldDirAmount > 0) { //This may look unnecessary, but we're apparently still getting div by zero errors on Macs here.
|
|
side_vec.x = static_randf_range((((Missiontime + static_rand(aip->shipnum)) >> 16) / strafeHoldDirAmount) , -1.0f, 1.0f);
|
|
side_vec.y = static_randf_range((((Missiontime + static_rand(aip->shipnum)) >> 16) / strafeHoldDirAmount) * 2, -1.0f, 1.0f);
|
|
} else {
|
|
Warning(LOCATION, "Division by zero in do_random_sidethrust averted. Please tell a coder.\n");
|
|
side_vec.x = 1.0f;
|
|
side_vec.y = 1.0f;
|
|
}
|
|
//Scale it up so that the longest dimension is length 1.0. This ensures we are always getting as much use out of sidethrust as possible.
|
|
vm_vec_boxscale(&side_vec, 1.0f);
|
|
|
|
AI_ci.sideways = side_vec.x;
|
|
AI_ci.vertical = side_vec.y;
|
|
}
|
|
|
|
const float AI_DEFAULT_ATTACK_APPROACH_DIST = 200.0f;
|
|
|
|
/**
|
|
* Set acceleration while in attack mode.
|
|
*/
|
|
void attack_set_accel(ai_info *aip, ship_info *sip, float dist_to_enemy, float dot_to_enemy, float dot_from_enemy)
|
|
{
|
|
float speed_ratio;
|
|
|
|
if (En_objp->phys_info.speed > 1.0f)
|
|
speed_ratio = Pl_objp->phys_info.speed/En_objp->phys_info.speed;
|
|
else
|
|
speed_ratio = 5.0f;
|
|
|
|
// Sometimes, told to attack slowly. Allows to get in more hits.
|
|
if (aip->ai_flags[AI::AI_Flags::Attack_slowly]) {
|
|
if ((dist_to_enemy > 200.0f) && (dist_to_enemy < 800.0f)) {
|
|
if ((dot_from_enemy < 0.9f) || ai_near_full_strength(Pl_objp)) {
|
|
accelerate_ship(aip, MAX(1.0f - (dist_to_enemy-200.0f)/600.0f, 0.1f));
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
aip->ai_flags.remove(AI::AI_Flags::Attack_slowly);
|
|
}
|
|
|
|
//Glide attack: we turn on glide to maintain current vector while aiming at the enemy
|
|
//The aiming part should already be taken care of.
|
|
if (aip->submode == AIS_CHASE_GLIDEATTACK) {
|
|
Pl_objp->phys_info.flags |= PF_GLIDING;
|
|
accelerate_ship(aip, 0.0f);
|
|
return;
|
|
}
|
|
|
|
//Circle Strafe: We try to maintain a constant distance from the target while using sidethrust to move in a circle
|
|
//around the target
|
|
if (aip->submode == AIS_CHASE_CIRCLESTRAFE) {
|
|
//If glide is available, use it (smooths things out a bit)
|
|
if (sip->can_glide == true)
|
|
Pl_objp->phys_info.flags |= PF_GLIDING;
|
|
|
|
//Try to maintain a distance between 50% and 75% of maximum circle strafe distance
|
|
if (dist_to_enemy <= CIRCLE_STRAFE_MAX_DIST * .5)
|
|
accelerate_ship(aip, -1.0f);
|
|
else if (dist_to_enemy >= CIRCLE_STRAFE_MAX_DIST * 0.75)
|
|
accelerate_ship(aip, 1.0f);
|
|
else
|
|
accelerate_ship(aip, 0.0f);
|
|
|
|
do_random_sidethrust(aip, sip);
|
|
return;
|
|
}
|
|
|
|
// assume the original retail value of a 200m range
|
|
float optimal_range = AI_DEFAULT_ATTACK_APPROACH_DIST;
|
|
ship_weapon* weapons = &Ships[Pl_objp->instance].weapons;
|
|
weapon_info* wip = nullptr;
|
|
ship* shipp = &Ships[Pl_objp->instance];
|
|
|
|
// see if we can get a better one
|
|
if (weapons->num_primary_banks >= 1 && weapons->current_primary_bank >= 0 && !shipp->flags[Ship::Ship_Flags::Primaries_locked]) {
|
|
wip = &Weapon_info[weapons->primary_bank_weapons[weapons->current_primary_bank]];
|
|
} else if (weapons->num_secondary_banks >= 1 && weapons->current_secondary_bank >= 0 && !shipp->flags[Ship::Ship_Flags::Secondaries_locked]) {
|
|
wip = &Weapon_info[weapons->secondary_bank_weapons[weapons->current_secondary_bank]];
|
|
}
|
|
|
|
if (wip != nullptr && wip->optimum_range > 0)
|
|
optimal_range = wip->optimum_range;
|
|
|
|
// Standoff for mines: keep at least 1.2x the target mine's proximity radius
|
|
// so we shoot it from outside its detonation zone.
|
|
if (En_objp->type == OBJ_WEAPON) {
|
|
weapon_info *target_wip = &Weapon_info[Weapons[En_objp->instance].weapon_info_index];
|
|
if (target_wip->is_mine() && target_wip->proximity_radius > 0.0f) {
|
|
float standoff = target_wip->proximity_radius * 1.2f;
|
|
// Don't stand off farther than our weapon can actually reach, or we'd hold
|
|
// position outside firing range and never destroy the mine. If the weapon
|
|
// can't outrange the detonation zone, close in and accept the risk.
|
|
if (wip != nullptr)
|
|
standoff = MIN(standoff, MIN(wip->max_speed * wip->lifetime, wip->weapon_range));
|
|
optimal_range = MAX(optimal_range, standoff);
|
|
}
|
|
}
|
|
|
|
if (dist_to_enemy > optimal_range + vm_vec_mag_quick(&En_objp->phys_info.vel) * dot_from_enemy + Pl_objp->phys_info.speed * speed_ratio) {
|
|
if (dist_to_enemy > optimal_range + 600.0f) {
|
|
if (ai_willing_to_afterburn_hard(aip)) {
|
|
ai_afterburn_hard(Pl_objp, aip);
|
|
} else if (ai_maybe_fire_afterburner(Pl_objp, aip)) {
|
|
if (!( Pl_objp->phys_info.flags & PF_AFTERBURNER_ON )) {
|
|
float percent_left;
|
|
ship_info *sip_local;
|
|
|
|
sip_local = &Ship_info[shipp->ship_info_index];
|
|
|
|
if (sip_local->afterburner_fuel_capacity > 0.0f) {
|
|
percent_left = 100.0f * shipp->afterburner_fuel / sip_local->afterburner_fuel_capacity;
|
|
if (percent_left > 30.0f + ((OBJ_INDEX(Pl_objp)) & 0x0f)) {
|
|
afterburners_start(Pl_objp);
|
|
if (aip->ai_profile_flags[AI::Profile_Flags::Smart_afterburner_management]) {
|
|
float max_ab_vel;
|
|
float time_to_exhaust_25pct_fuel;
|
|
float time_to_fly_75pct_of_distance;
|
|
float ab_time;
|
|
|
|
// Max afterburner speed - make sure we don't devide by 0 later
|
|
max_ab_vel = sip_local->afterburner_max_vel.xyz.z > 0.0f ? sip_local->afterburner_max_vel.xyz.z : sip_local->max_vel.xyz.z;
|
|
max_ab_vel = max_ab_vel > 0.0f ? max_ab_vel : 0.0001f;
|
|
|
|
// Time to exhaust 25% of the remaining fuel
|
|
time_to_exhaust_25pct_fuel = shipp->afterburner_fuel * 0.25f / sip_local->afterburner_burn_rate;
|
|
|
|
// Time to fly 75% of the distance to the target
|
|
time_to_fly_75pct_of_distance = dist_to_enemy * 0.75f / max_ab_vel;
|
|
|
|
// Get minimum
|
|
ab_time = MIN(time_to_exhaust_25pct_fuel, time_to_fly_75pct_of_distance);
|
|
|
|
aip->afterburner_stop_time = (fix) (Missiontime + F1_0 * ab_time);
|
|
} else {
|
|
aip->afterburner_stop_time = Missiontime + F1_0 + static_rand(OBJ_INDEX(Pl_objp))/4;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
accelerate_ship(aip, 1.0f);
|
|
} else if ((Missiontime - aip->last_hit_time > F1_0*7)
|
|
&& (En_objp->phys_info.speed < 10.0f)
|
|
&& (dist_to_enemy > 25.0f)
|
|
&& (dot_to_enemy > 0.8f)
|
|
&& (dot_from_enemy < 0.8f)) {
|
|
accelerate_ship(aip, 0.0f); // No one attacking us, so don't need to move.
|
|
} else if ((dot_from_enemy < 0.25f) && (dot_to_enemy > 0.5f)) {
|
|
set_accel_for_target_speed(Pl_objp, En_objp->phys_info.speed);
|
|
} else if (Pl_objp->phys_info.speed < 15.0f) {
|
|
accelerate_ship(aip, 1.0f);
|
|
} else if (Pl_objp->phys_info.speed > En_objp->phys_info.speed - 1.0f) {
|
|
if (dot_from_enemy > 0.75f)
|
|
accelerate_ship(aip, 1.0f);
|
|
else
|
|
set_accel_for_target_speed(Pl_objp, En_objp->phys_info.speed*0.75f + 3.0f);
|
|
} else {
|
|
change_acceleration(aip, 0.5f);
|
|
}
|
|
}
|
|
|
|
// Pl_objp (aip) tries to get behind En_objp.
|
|
// New on 2/21/98: If this ship can move backwards and slide, maybe do that to get behind.
|
|
static void get_behind_ship(ai_info *aip)
|
|
{
|
|
vec3d new_pos;
|
|
float dot;
|
|
vec3d vec_from_enemy;
|
|
|
|
vm_vec_normalized_dir(&vec_from_enemy, &Pl_objp->pos, &En_objp->pos);
|
|
|
|
vm_vec_scale_add(&new_pos, &En_objp->pos, &En_objp->orient.vec.fvec, -100.0f); // Pick point 100 units behind.
|
|
ai_turn_towards_vector(&new_pos, Pl_objp, nullptr, nullptr, 0.0f, 0);
|
|
|
|
dot = vm_vec_dot(&vec_from_enemy, &En_objp->orient.vec.fvec);
|
|
|
|
if (ai_willing_to_afterburn_hard(aip) && dot > 0.9f && vm_vec_dist(&Pl_objp->pos, &En_objp->pos) > 1000.0f) {
|
|
ai_afterburn_hard(Pl_objp, aip);
|
|
} else if (dot > 0.25f) {
|
|
accelerate_ship(aip, 1.0f);
|
|
} else {
|
|
accelerate_ship(aip, (dot + 1.0f)/2.0f);
|
|
}
|
|
}
|
|
|
|
int avoid_player(object *objp, vec3d *goal_pos)
|
|
{
|
|
maybe_avoid_player(Pl_objp, goal_pos);
|
|
ai_info *aip = &Ai_info[Ships[objp->instance].ai_index];
|
|
|
|
if (aip->ai_flags[AI::AI_Flags::Avoiding_small_ship]) {
|
|
ai_turn_towards_vector(&aip->avoid_goal_point, objp, nullptr, nullptr, 0.0f, 0);
|
|
accelerate_ship(aip, 0.5f);
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Determine if a cylinder of width radius from p0 to p1 will collide with big_objp.
|
|
// If so, stuff *collision_point.
|
|
int will_collide_pp(vec3d *p0, vec3d *p1, float radius, object *big_objp, vec3d *collision_point)
|
|
{
|
|
polymodel *pm = model_get(Ship_info[Ships[big_objp->instance].ship_info_index].model_num);
|
|
|
|
mc_info mc;
|
|
mc.model_instance_num = -1;
|
|
mc.model_num = pm->id; // Fill in the model to check
|
|
mc.orient = &big_objp->orient; // The object's orient
|
|
mc.pos = &big_objp->pos; // The object's position
|
|
mc.p0 = p0; // Point 1 of ray to check
|
|
mc.p1 = p1;
|
|
mc.flags = MC_CHECK_MODEL | MC_CHECK_SPHERELINE | MC_SUBMODEL; // flags
|
|
|
|
mc.radius = radius;
|
|
|
|
// Only check the 2nd lowest hull object
|
|
mc.submodel_num = pm->detail[0];
|
|
model_collide(&mc);
|
|
|
|
if (mc.num_hits)
|
|
*collision_point = mc.hit_point_world;
|
|
|
|
return mc.num_hits;
|
|
}
|
|
|
|
// Return true/false if *objp will collide with *big_objp
|
|
// Stuff distance in *distance to collision point if *objp will collide with *big_objp within delta_time seconds.
|
|
// Global collision point stuffed in *collision_point
|
|
int will_collide_with_big_ship(object *objp, vec3d *goal_point, object *big_objp, vec3d *collision_point, float delta_time)
|
|
{
|
|
float radius;
|
|
vec3d end_pos;
|
|
|
|
radius = big_objp->radius + delta_time * objp->phys_info.speed;
|
|
|
|
if (vm_vec_dist_quick(&big_objp->pos, &objp->pos) > radius) {
|
|
return 0;
|
|
}
|
|
|
|
if (goal_point == NULL) {
|
|
vm_vec_scale_add(&end_pos, &objp->pos, &objp->phys_info.vel, delta_time); // Point 2 of ray to check
|
|
} else {
|
|
end_pos = *goal_point;
|
|
}
|
|
|
|
return will_collide_pp(&objp->pos, &end_pos, objp->radius, big_objp, collision_point);
|
|
}
|
|
|
|
// Return true if *objp is expected to collide with a large ship.
|
|
// Stuff global collision point in *collision_point.
|
|
// If *goal_point is not NULL, use that as the point towards which *objp will be flying. Don't use *objp velocity
|
|
// *ignore_objp will typically be the target this ship is pursuing, either to attack or guard. We don't want to avoid it.
|
|
int will_collide_with_big_ship_all(object *objp, object *ignore_objp, vec3d *goal_point, vec3d *collision_point, float *distance, float delta_time)
|
|
{
|
|
ship_obj *so;
|
|
object *big_objp;
|
|
int collision_obj_index = -1;
|
|
float min_dist = 999999.9f;
|
|
|
|
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
|
|
big_objp = &Objects[so->objnum];
|
|
if (big_objp->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
if (big_objp == ignore_objp)
|
|
continue;
|
|
|
|
if (Ship_info[Ships[big_objp->instance].ship_info_index].is_big_or_huge()) {
|
|
vec3d cur_collision_point;
|
|
float cur_dist;
|
|
|
|
if (will_collide_with_big_ship(objp, goal_point, big_objp, &cur_collision_point, delta_time)) {
|
|
|
|
cur_dist = vm_vec_dist(&cur_collision_point, &objp->pos);
|
|
|
|
if (cur_dist < min_dist) {
|
|
min_dist = cur_dist;
|
|
*collision_point = cur_collision_point;
|
|
collision_obj_index = OBJ_INDEX(big_objp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
*distance = min_dist;
|
|
return collision_obj_index;
|
|
|
|
}
|
|
|
|
typedef struct {
|
|
float dist;
|
|
int collide;
|
|
vec3d pos;
|
|
} sgoal;
|
|
|
|
// Pick a point for *objp to fly towards to avoid a collision with *big_objp at *collision_point
|
|
// Return result in *avoid_pos
|
|
void mabs_pick_goal_point(object *objp, object *big_objp, vec3d *collision_point, vec3d *avoid_pos)
|
|
{
|
|
matrix mat1;
|
|
sgoal goals[4];
|
|
vec3d v2b;
|
|
|
|
vm_vec_normalized_dir(&v2b, collision_point, &objp->pos);
|
|
vm_vector_2_matrix_norm(&mat1, &v2b, nullptr, nullptr);
|
|
|
|
int found = 0;
|
|
|
|
// Try various scales, in 0.5f, 0.75f, 1.0f, 1.25f.
|
|
// First try 0.5f to see if we can find a point that near the center of the target ship, which presumably
|
|
// means less of a turn.
|
|
// Try going as far as 1.25f * radius.
|
|
float s;
|
|
for (s=0.5f; s<1.3f; s += 0.25f) {
|
|
int i;
|
|
for (i=0; i<4; i++) {
|
|
vec3d p = big_objp->pos;
|
|
float ku = big_objp->radius*s + objp->radius * (OBJ_INDEX(objp) % 4)/4; // This objp->radius stuff to prevent ships from glomming together at one point
|
|
float kr = big_objp->radius*s + objp->radius * ((OBJ_INDEX(objp) % 4) ^ 2)/4;
|
|
if (i&1)
|
|
ku = -ku;
|
|
if (i&2)
|
|
kr = -kr;
|
|
vm_vec_scale_add2(&p, &mat1.vec.uvec, ku);
|
|
vm_vec_scale_add2(&p, &mat1.vec.rvec, kr);
|
|
goals[i].pos = p;
|
|
goals[i].dist = vm_vec_dist_quick(&objp->pos, &p);
|
|
goals[i].collide = will_collide_pp(&objp->pos, &p, objp->radius, big_objp, collision_point);
|
|
if (!goals[i].collide)
|
|
found = 1;
|
|
}
|
|
|
|
// If we found a point that doesn't collide, find the nearest one and make that the *avoid_pos.
|
|
if (found) {
|
|
float min_dist = 9999999.9f;
|
|
int min_index = -1;
|
|
|
|
for (i=0; i<4; i++) {
|
|
if (!goals[i].collide && (goals[i].dist < min_dist)) {
|
|
min_dist = goals[i].dist;
|
|
min_index = i;
|
|
}
|
|
}
|
|
|
|
Assert(i != -1);
|
|
if (i != -1) {
|
|
*avoid_pos = goals[min_index].pos;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Drat. We tried and tried and could not find a point that did not cause a collision.
|
|
// Get this dump pilot far away from the problem ship.
|
|
vec3d away_vec;
|
|
vm_vec_normalized_dir(&away_vec, &objp->pos, collision_point);
|
|
vm_vec_scale_add(avoid_pos, &objp->pos, &away_vec, big_objp->radius*1.5f);
|
|
|
|
}
|
|
|
|
/**
|
|
* Return true if a large ship is being ignored.
|
|
*/
|
|
bool maybe_avoid_big_ship(object *objp, object *ignore_objp, ai_info *aip, vec3d *goal_point, float delta_time, float time_scale = 1.f)
|
|
{
|
|
if (timestamp_elapsed(aip->avoid_check_timestamp)) {
|
|
float distance;
|
|
vec3d collision_point;
|
|
int ship_num;
|
|
int next_check_time;
|
|
if ((ship_num = will_collide_with_big_ship_all(Pl_objp, ignore_objp, goal_point, &collision_point, &distance, delta_time)) != -1) {
|
|
aip->ai_flags.set(AI::AI_Flags::Avoiding_big_ship);
|
|
mabs_pick_goal_point(objp, &Objects[ship_num], &collision_point, &aip->avoid_goal_point);
|
|
float dist = vm_vec_dist_quick(&aip->avoid_goal_point, &objp->pos);
|
|
next_check_time = fl2i(2000.0f + MIN(1000.0f, (dist * 2.0f)) * time_scale); // Delay until check again is based on distance to avoid point.
|
|
aip->avoid_check_timestamp = timestamp(next_check_time);
|
|
aip->avoid_ship_num = ship_num;
|
|
} else {
|
|
aip->ai_flags.remove(AI::AI_Flags::Avoiding_big_ship);
|
|
aip->ai_flags.remove(AI::AI_Flags::Avoiding_small_ship);
|
|
aip->avoid_ship_num = -1;
|
|
next_check_time = (int) (1500 * time_scale);
|
|
aip->avoid_check_timestamp = timestamp(next_check_time);
|
|
}
|
|
}
|
|
|
|
if (aip->ai_flags[AI::AI_Flags::Avoiding_big_ship]) {
|
|
vec3d v2g;
|
|
|
|
ai_turn_towards_vector(&aip->avoid_goal_point, Pl_objp, nullptr, nullptr, 0.0f, 0);
|
|
vm_vec_normalized_dir(&v2g, &aip->avoid_goal_point, &Pl_objp->pos);
|
|
float dot = vm_vec_dot(&objp->orient.vec.fvec, &v2g);
|
|
float d2 = (1.0f + dot) * (1.0f + dot);
|
|
accelerate_ship(aip, d2/4.0f);
|
|
|
|
if (ai_willing_to_afterburn_hard(aip) && dot > 0.99f)
|
|
ai_afterburn_hard(Pl_objp, aip);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return true if small ship and it will likely collide with large ship
|
|
* developed by Asteroth
|
|
*/
|
|
bool better_collision_avoidance_triggered(bool flag_to_check, float avoidance_aggression, object* pl_objp, object* ignore_objp)
|
|
{
|
|
ship* shipp = &Ships[pl_objp->instance];
|
|
ship_info* sip = &Ship_info[shipp->ship_info_index];
|
|
|
|
if ((flag_to_check) && sip->is_small_ship()) {
|
|
vec3d collide_vec = pl_objp->phys_info.vel * (avoidance_aggression / (PI2 / sip->srotation_time));
|
|
float radius_contribution = (pl_objp->phys_info.speed + pl_objp->radius) / pl_objp->phys_info.speed;
|
|
collide_vec *= radius_contribution;
|
|
|
|
collide_vec += pl_objp->pos;
|
|
return (maybe_avoid_big_ship(pl_objp, ignore_objp, &Ai_info[shipp->ai_index], &collide_vec, 0.f, 0.1f));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Set desired right vector for ships flying towards another ship.
|
|
* Since this is governed only by vector to target, it causes ships to align bank and look less chaotic.
|
|
*/
|
|
void compute_desired_rvec(vec3d *rvec, const vec3d *goal_pos, const vec3d *cur_pos)
|
|
{
|
|
vec3d v2e;
|
|
|
|
vm_vec_normalized_dir(&v2e, goal_pos, cur_pos);
|
|
rvec->xyz.x = v2e.xyz.z;
|
|
rvec->xyz.y = 0.0f;
|
|
rvec->xyz.z = -v2e.xyz.x;
|
|
if (vm_vec_mag_squared(rvec) < 0.001f)
|
|
rvec->xyz.y = 1.0f;
|
|
|
|
vm_vec_normalize_safe(rvec); // since messing with the bank will make it un-normalized
|
|
}
|
|
|
|
/**
|
|
* Handler for stealth find submode of Chase.
|
|
*/
|
|
void ai_stealth_find()
|
|
{
|
|
ai_info *aip;
|
|
ship_info *sip;
|
|
|
|
vec3d new_pos, vec_to_enemy;
|
|
float dist_to_enemy, dot_to_enemy, dot_from_enemy;
|
|
|
|
Assert(Ships[Pl_objp->instance].ship_info_index >= 0);
|
|
sip = &Ship_info[Ships[Pl_objp->instance].ship_info_index];
|
|
Assert(Ships[Pl_objp->instance].ai_index >= 0);
|
|
aip = &Ai_info[Ships[Pl_objp->instance].ai_index];
|
|
|
|
// get time since last seen
|
|
int delta_time = (timestamp() - aip->stealth_last_visible_stamp);
|
|
|
|
// if delta_time is really big, i'm real confused, start sweep
|
|
if (delta_time > 10000) {
|
|
aip->submode_parm0 = SM_SF_BAIL;
|
|
}
|
|
|
|
// guestimate new position
|
|
vm_vec_scale_add(&new_pos, &aip->stealth_last_pos, &aip->stealth_velocity, (delta_time * 0.001f));
|
|
|
|
// if I think he's behind me, go to the goal point
|
|
if ( aip->submode_parm0 == SM_SF_BEHIND ) {
|
|
new_pos = aip->goal_point;
|
|
}
|
|
|
|
// check for collision with big ships
|
|
if (maybe_avoid_big_ship(Pl_objp, En_objp, aip, &new_pos, 10.0f)) {
|
|
// reset ai submode to chase
|
|
return;
|
|
}
|
|
|
|
// if dist is near max and dot is close to 1, accel, afterburn
|
|
vm_vec_sub(&vec_to_enemy, &new_pos, &Pl_objp->pos);
|
|
dist_to_enemy = vm_vec_normalize_quick(&vec_to_enemy);
|
|
dot_to_enemy = vm_vec_dot(&vec_to_enemy, &Pl_objp->orient.vec.fvec);
|
|
|
|
// if i think i should see him ahead and i don't, set goal pos and turn around, but only if I haven't seen him for a while
|
|
if ( (delta_time > 800) && (aip->submode_parm0 == SM_SF_AHEAD) && (dot_to_enemy > .94) && (dist_to_enemy < get_skill_stealth_dist_scaler()*STEALTH_MAX_VIEW_DIST + 50) ) {
|
|
// do turn around)
|
|
vm_vec_scale_add(&aip->goal_point, &Pl_objp->pos, &Pl_objp->orient.vec.fvec, -300.0f);
|
|
aip->submode_parm0 = SM_SF_BEHIND;
|
|
vm_vec_sub(&vec_to_enemy, &new_pos, &Pl_objp->pos);
|
|
dist_to_enemy = vm_vec_normalize_quick(&vec_to_enemy);
|
|
dot_to_enemy = vm_vec_dot(&vec_to_enemy, &Pl_objp->orient.vec.fvec);
|
|
}
|
|
|
|
if ( (dist_to_enemy > get_skill_stealth_dist_scaler()*STEALTH_MAX_VIEW_DIST) && (dot_to_enemy > 0.94f) ) { // 20 degree half angle
|
|
// accelerate ship
|
|
accelerate_ship(aip, 1.0f);
|
|
|
|
// engage afterburner
|
|
if (!( Pl_objp->phys_info.flags & PF_AFTERBURNER_ON )) {
|
|
if (ai_maybe_fire_afterburner(Pl_objp, aip)) {
|
|
afterburners_start(Pl_objp);
|
|
aip->afterburner_stop_time = Missiontime + 3*F1_0/2;
|
|
}
|
|
}
|
|
|
|
ai_turn_towards_vector(&new_pos, Pl_objp, nullptr, nullptr, 0.0f, 0);
|
|
return;
|
|
}
|
|
|
|
// If enemy more than 500 meters away, all ships flying there will tend to match bank.
|
|
// They do this by using their vector to their target to compute their right vector and causing ai_turn_towards_vector
|
|
// to interpolate a matrix rather than just a vector.
|
|
if (dist_to_enemy > 500.0f) {
|
|
vec3d rvec;
|
|
compute_desired_rvec(&rvec, &new_pos, &Pl_objp->pos);
|
|
ai_turn_towards_vector(&new_pos, Pl_objp, nullptr, nullptr, 0.0f, 0, &rvec);
|
|
} else {
|
|
ai_turn_towards_vector(&new_pos, Pl_objp, nullptr, nullptr, 0.0f, 0);
|
|
}
|
|
|
|
dot_from_enemy = -vm_vec_dot(&vec_to_enemy, &En_objp->orient.vec.fvec);
|
|
|
|
attack_set_accel(aip, sip, dist_to_enemy, dot_to_enemy, dot_from_enemy);
|
|
}
|
|
|
|
/**
|
|
* Try to find stealth ship by sweeping an area
|
|
*/
|
|
void ai_stealth_sweep()
|
|
{
|
|
ai_info *aip;
|
|
|
|
Assert(Ships[Pl_objp->instance].ai_index >= 0);
|
|
aip = &Ai_info[Ships[Pl_objp->instance].ai_index];
|
|
|
|
vec3d goal_pt;
|
|
vec3d forward, right, up;
|
|
int lost_time;
|
|
|
|
// time since stealth last seen
|
|
lost_time = (timestamp() - aip->stealth_last_visible_stamp);
|
|
|
|
// determine which pt to fly to in sweep by keeping track of parm0
|
|
if (aip->submode_parm0 == SM_SS_SET_GOAL) {
|
|
|
|
// don't make goal pt more than 2k from current pos
|
|
vm_vec_scale_add(&goal_pt, &aip->stealth_last_pos, &aip->stealth_velocity, (0.001f * lost_time));
|
|
|
|
// make box size based on speed of stealth and expected time to intercept (keep box in range 200-500)
|
|
float box_size = vm_vec_mag_quick(&aip->stealth_velocity) * (0.001f * lost_time);
|
|
box_size = MIN(200.0f, box_size);
|
|
box_size = MAX(500.0f, box_size);
|
|
aip->stealth_sweep_box_size = box_size;
|
|
|
|
aip->goal_point = goal_pt;
|
|
aip->submode_parm0 = SM_SS_BOX0;
|
|
}
|
|
|
|
// GET UP, RIGHT, FORWARD FOR BOX based on stealth ship's velocity
|
|
// if velocity changes in stealth mode, then ship is *seen*, and falls out of sweep mode
|
|
// if stealth has no velocity make a velocity
|
|
if ( vm_vec_mag_quick(&aip->stealth_velocity) < 1 ) {
|
|
vm_vec_rand_vec_quick(&aip->stealth_velocity);
|
|
}
|
|
|
|
// get "right" vector for box
|
|
vm_vec_cross(&right, &aip->stealth_velocity, &vmd_y_vector);
|
|
|
|
if ( vm_vec_mag_quick(&right) < 0.01 ) {
|
|
vm_vec_cross(&right, &aip->stealth_velocity, &vmd_z_vector);
|
|
}
|
|
|
|
vm_vec_normalize_quick(&right);
|
|
|
|
// get "forward" for box
|
|
vm_vec_copy_normalize_quick(&forward, &aip->stealth_velocity);
|
|
|
|
// get "up" for box
|
|
vm_vec_cross(&up, &forward, &right);
|
|
|
|
// lost far away ahead (do box)
|
|
switch(aip->submode_parm0) {
|
|
case SM_SS_BOX0:
|
|
goal_pt = aip->goal_point;
|
|
break;
|
|
|
|
// pt1 -U +R
|
|
case SM_SS_LR:
|
|
vm_vec_scale_add(&goal_pt, &aip->goal_point, &up, -aip->stealth_sweep_box_size);
|
|
vm_vec_scale_add2(&goal_pt, &right, aip->stealth_sweep_box_size);
|
|
vm_vec_scale_add2(&goal_pt, &forward, 0.5f*aip->stealth_sweep_box_size);
|
|
break;
|
|
|
|
// pt2 +U -R
|
|
case SM_SS_UL:
|
|
vm_vec_scale_add(&goal_pt, &aip->goal_point, &up, aip->stealth_sweep_box_size);
|
|
vm_vec_scale_add2(&goal_pt, &right, -aip->stealth_sweep_box_size);
|
|
vm_vec_scale_add2(&goal_pt, &forward, 0.5f*aip->stealth_sweep_box_size);
|
|
break;
|
|
|
|
// pt3 back
|
|
case SM_SS_BOX1:
|
|
goal_pt = aip->goal_point;
|
|
break;
|
|
|
|
// pt4 +U +R
|
|
case SM_SS_UR:
|
|
vm_vec_scale_add(&goal_pt, &aip->goal_point, &up, aip->stealth_sweep_box_size);
|
|
vm_vec_scale_add2(&goal_pt, &right, aip->stealth_sweep_box_size);
|
|
vm_vec_scale_add2(&goal_pt, &forward, 0.5f*aip->stealth_sweep_box_size);
|
|
break;
|
|
|
|
// pt5 -U -R
|
|
case SM_SS_LL:
|
|
vm_vec_scale_add(&goal_pt, &aip->goal_point, &up, -aip->stealth_sweep_box_size);
|
|
vm_vec_scale_add2(&goal_pt, &right, -aip->stealth_sweep_box_size);
|
|
vm_vec_scale_add2(&goal_pt, &forward, 0.5f*aip->stealth_sweep_box_size);
|
|
break;
|
|
|
|
// pt6 back
|
|
case SM_SS_BOX2:
|
|
goal_pt = aip->goal_point;
|
|
break;
|
|
|
|
default:
|
|
UNREACHABLE("Bad submode in ai_steath_sweep of %d. Please report to the SCP!", aip->submode_parm0);
|
|
break;
|
|
}
|
|
|
|
// when close to goal_pt, update next goal pt
|
|
float dist_to_goal = vm_vec_dist(&goal_pt, &Pl_objp->pos);
|
|
if (dist_to_goal < 15) {
|
|
aip->submode_parm0++;
|
|
}
|
|
|
|
// check for collision with big ship
|
|
if (maybe_avoid_big_ship(Pl_objp, En_objp, aip, &goal_pt, 10.0f)) {
|
|
// skip to the next pt on box
|
|
aip->submode_parm0++;
|
|
return;
|
|
}
|
|
|
|
ai_turn_towards_vector(&goal_pt, Pl_objp, nullptr, nullptr, 0.0f, 0);
|
|
|
|
float dot = 1.0f;
|
|
if (dist_to_goal < 100) {
|
|
vec3d vec_to_goal;
|
|
vm_vec_normalized_dir(&vec_to_goal, &goal_pt, &Pl_objp->pos);
|
|
dot = vm_vec_dot(&vec_to_goal, &Pl_objp->orient.vec.fvec);
|
|
}
|
|
|
|
accelerate_ship(aip, 0.8f*dot);
|
|
}
|
|
|
|
/**
|
|
* ATTACK submode handler for chase mode.
|
|
*/
|
|
void ai_chase_attack(ai_info *aip, ship_info *sip, vec3d *predicted_enemy_pos, float dist_to_enemy, int modelnum)
|
|
{
|
|
int start_bank;
|
|
float dot_to_enemy, dot_from_enemy;
|
|
float bank_override = 0.0f;
|
|
|
|
if (!(aip->ai_profile_flags[AI::Profile_Flags::No_special_player_avoid]) && avoid_player(Pl_objp, predicted_enemy_pos))
|
|
return;
|
|
|
|
compute_dots(Pl_objp, En_objp, &dot_to_enemy, &dot_from_enemy);
|
|
|
|
polymodel *po = model_get( modelnum );
|
|
|
|
vec3d *rel_pos;
|
|
float scale;
|
|
vec3d randvec;
|
|
vec3d new_pos;
|
|
|
|
start_bank = Ships[aip->shipnum].weapons.current_primary_bank;
|
|
if (po->n_guns && start_bank != -1 && !(The_mission.ai_profile->flags[AI::Profile_Flags::Ai_aims_from_ship_center])) {
|
|
rel_pos = &po->gun_banks[start_bank].pnt[0];
|
|
} else
|
|
rel_pos = NULL;
|
|
|
|
// If ship moving slowly relative to its size, then don't attack its center point.
|
|
// How far from center we attack is based on speed, size and distance to enemy
|
|
if (En_objp->radius > En_objp->phys_info.speed) {
|
|
static_randvec(OBJ_INDEX(Pl_objp), &randvec);
|
|
scale = dist_to_enemy/(dist_to_enemy + En_objp->radius) * En_objp->radius;
|
|
scale *= 0.5f * En_objp->radius/(En_objp->phys_info.speed + En_objp->radius); // scale downward by 1/2 to 1/4
|
|
vm_vec_scale_add(&new_pos, predicted_enemy_pos, &randvec, scale);
|
|
} else
|
|
new_pos = *predicted_enemy_pos;
|
|
|
|
//SUSHI: Don't change bank while circle strafing or glide attacking
|
|
if (dist_to_enemy < 250.0f && dot_from_enemy > 0.7f && aip->submode != AIS_CHASE_CIRCLESTRAFE && aip->submode != AIS_CHASE_GLIDEATTACK) {
|
|
bank_override = 1.0f;
|
|
}
|
|
|
|
// If enemy more than 500 meters away, all ships flying there will tend to match bank.
|
|
// They do this by using their vector to their target to compute their right vector and causing ai_turn_towards_vector
|
|
// to interpolate a matrix rather than just a vector.
|
|
if (dist_to_enemy > 500.0f) {
|
|
vec3d rvec;
|
|
compute_desired_rvec(&rvec, predicted_enemy_pos, &Pl_objp->pos);
|
|
ai_turn_towards_vector(&new_pos, Pl_objp, nullptr, rel_pos, bank_override, 0, &rvec);
|
|
} else {
|
|
ai_turn_towards_vector(&new_pos, Pl_objp, nullptr, rel_pos, bank_override, 0);
|
|
}
|
|
|
|
attack_set_accel(aip, sip, dist_to_enemy, dot_to_enemy, dot_from_enemy);
|
|
}
|
|
|
|
// EVADE_SQUIGGLE submode handler for chase mode.
|
|
// Changed by MK on 5/5/97.
|
|
// Used to evade towards a point off the right or up vector.
|
|
// Now, evade straight away to try to get far away.
|
|
// The squiggling should protect against laser fire.
|
|
void ai_chase_es(ai_info *aip)
|
|
{
|
|
vec3d tvec;
|
|
fix timeslice;
|
|
fix scale;
|
|
|
|
tvec = Pl_objp->pos;
|
|
|
|
timeslice = (Missiontime >> 16) & 0x0f;
|
|
scale = ((Missiontime >> 16) & 0x0f) << 14;
|
|
|
|
if (timeslice & 0x01)
|
|
vm_vec_scale_add2(&tvec, &Pl_objp->orient.vec.rvec, f2fl(scale ^ 0x10000));
|
|
if (timeslice & 0x02)
|
|
vm_vec_scale_sub2(&tvec, &Pl_objp->orient.vec.rvec, f2fl(scale));
|
|
if (timeslice & 0x04)
|
|
vm_vec_scale_add2(&tvec, &Pl_objp->orient.vec.uvec, f2fl(scale ^ 0x10000));
|
|
if (timeslice & 0x08)
|
|
vm_vec_scale_sub2(&tvec, &Pl_objp->orient.vec.uvec, f2fl(scale));
|
|
|
|
while (vm_vec_dist_quick(&tvec, &Pl_objp->pos) < 0.1f) {
|
|
tvec.xyz.x += frand();
|
|
tvec.xyz.y += frand();
|
|
}
|
|
|
|
ai_turn_towards_vector(&tvec, Pl_objp, nullptr, nullptr, 1.0f, 0);
|
|
accelerate_ship(aip, 1.0f);
|
|
}
|
|
|
|
/**
|
|
* Trying to get away from opponent.
|
|
*/
|
|
void ai_chase_ga(ai_info *aip, ship_info *sip)
|
|
{
|
|
// If not near end of this submode, evade squiggly. If near end, just fly straight for a bit
|
|
vec3d tvec;
|
|
vec3d vec_from_enemy;
|
|
|
|
if (En_objp != NULL) {
|
|
vm_vec_normalized_dir(&vec_from_enemy, &Pl_objp->pos, &En_objp->pos);
|
|
} else
|
|
vec_from_enemy = Pl_objp->orient.vec.fvec;
|
|
|
|
static_randvec(Missiontime >> 15, &tvec);
|
|
vm_vec_scale(&tvec, 100.0f);
|
|
vm_vec_scale_add2(&tvec, &vec_from_enemy, 300.0f);
|
|
vm_vec_add2(&tvec, &Pl_objp->pos);
|
|
|
|
ai_turn_towards_vector(&tvec, Pl_objp, nullptr, nullptr, 1.0f, 0);
|
|
|
|
accelerate_ship(aip, 2.0f);
|
|
|
|
if (ai_maybe_fire_afterburner(Pl_objp, aip)) {
|
|
if (!(Pl_objp->phys_info.flags & PF_AFTERBURNER_ON )) {
|
|
float percent_left = 100.0f * Ships[Pl_objp->instance].afterburner_fuel / sip->afterburner_fuel_capacity;
|
|
if (percent_left > 30.0f + ((OBJ_INDEX(Pl_objp)) & 0x0f)) {
|
|
afterburners_start(Pl_objp);
|
|
aip->afterburner_stop_time = Missiontime + 3*F1_0/2;
|
|
}
|
|
afterburners_start(Pl_objp);
|
|
aip->afterburner_stop_time = Missiontime + 3*F1_0/2;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Make object *objp attack subsystem with ID = subnum.
|
|
// Return true if found a subsystem to attack, else return false.
|
|
// Note, can fail if subsystem exists, but has no hits.
|
|
int ai_set_attack_subsystem(object *objp, int subnum)
|
|
{
|
|
int temp;
|
|
ship *shipp, *attacker_shipp;
|
|
ai_info *aip;
|
|
ship_subsys *ssp;
|
|
object *attacked_objp;
|
|
|
|
Assert(objp->type == OBJ_SHIP);
|
|
Assert(objp->instance >= 0);
|
|
|
|
attacker_shipp = &Ships[objp->instance];
|
|
Assert(attacker_shipp->ai_index >= 0);
|
|
|
|
aip = &Ai_info[attacker_shipp->ai_index];
|
|
|
|
// MWA -- 2/27/98. Due to AL's changes, target_objnum is now not always valid (at least sometimes
|
|
// in terms of goals). So, bail if we don't have a valid target.
|
|
if ( aip->target_objnum == -1 )
|
|
return 0;
|
|
|
|
Assertion(aip->target_objnum >= 0, "The target_objnum for %s has become a nonsense value of %d. Please report!",attacker_shipp->ship_name ,aip->target_objnum);
|
|
|
|
attacked_objp = &Objects[aip->target_objnum];
|
|
shipp = &Ships[attacked_objp->instance]; // need to get our target's ship pointer!!!
|
|
|
|
// When subnum is >= 0, it indicates a specific subsystem. Otherwise it is a subsystem type. See ai_submode
|
|
if (subnum >= 0)
|
|
ssp = ship_get_indexed_subsys(shipp, subnum);
|
|
else
|
|
ssp = ship_find_first_subsys(shipp, -subnum, &objp->pos);
|
|
if (ssp == NULL)
|
|
return 0;
|
|
|
|
set_targeted_subsys(aip, ssp, aip->target_objnum);
|
|
|
|
if (aip->ignore_objnum == aip->target_objnum)
|
|
aip->ignore_objnum = UNUSED_OBJNUM;
|
|
|
|
// Goober5000
|
|
temp = find_ignore_new_object_index(aip, aip->target_objnum);
|
|
if (temp >= 0)
|
|
{
|
|
aip->ignore_new_objnums[temp] = UNUSED_OBJNUM;
|
|
}
|
|
else if (is_ignore_object(aip, aip->target_objnum, 1))
|
|
{
|
|
aip->ignore_objnum = UNUSED_OBJNUM;
|
|
}
|
|
|
|
// -- Done at caller in ai_process_mission_orders -- attacked_objp->flags .add(Object::Object_Flags::Protected);
|
|
|
|
ai_set_goal_abort_support_call(objp, aip);
|
|
aip->ok_to_target_timestamp = timestamp(DELAY_TARGET_TIME);
|
|
|
|
return 1;
|
|
}
|
|
|
|
void ai_set_guard_vec(object *objp, object *guard_objp)
|
|
{
|
|
ai_info *aip;
|
|
float radius;
|
|
|
|
aip = &Ai_info[Ships[objp->instance].ai_index];
|
|
|
|
// Handle case of bogus call in which ship is told to guard self.
|
|
Assert(objp != guard_objp);
|
|
if (objp == guard_objp) {
|
|
vm_vec_rand_vec_quick(&aip->guard_vec);
|
|
vm_vec_scale(&aip->guard_vec, 100.0f);
|
|
return;
|
|
}
|
|
|
|
// check if guard_objp is BIG
|
|
radius = 5.0f * (objp->radius + guard_objp->radius) + 50.0f;
|
|
if (radius > 300.0f) {
|
|
radius = guard_objp->radius * 1.25f;
|
|
}
|
|
|
|
vm_vec_sub(&aip->guard_vec, &objp->pos, &guard_objp->pos);
|
|
|
|
if (vm_vec_mag(&aip->guard_vec) > 3.0f*radius) {
|
|
// Far away, don't just use vector to object, causes clustering of guard ships.
|
|
vec3d tvec, rvec;
|
|
float mag;
|
|
mag = vm_vec_copy_normalize(&tvec, &aip->guard_vec);
|
|
vm_vec_rand_vec_quick(&rvec);
|
|
vm_vec_scale_add2(&tvec, &rvec, 0.5f);
|
|
vm_vec_copy_scale(&aip->guard_vec, &tvec, mag);
|
|
}
|
|
|
|
vm_vec_normalize_quick(&aip->guard_vec);
|
|
vm_vec_scale(&aip->guard_vec, radius);
|
|
}
|
|
|
|
// Make object *objp guard object *other_objp.
|
|
// To be called from the goals code.
|
|
void ai_set_guard_wing(object *objp, int wingnum)
|
|
{
|
|
ship *shipp;
|
|
ai_info *aip;
|
|
int leader_objnum, leader_shipnum;
|
|
|
|
Assert(wingnum >= 0);
|
|
|
|
Assert(objp->type == OBJ_SHIP);
|
|
Assert(objp->instance >= 0);
|
|
|
|
// shouldn't set the ai mode for the player
|
|
if ( objp == Player_obj ) {
|
|
return;
|
|
}
|
|
|
|
shipp = &Ships[objp->instance];
|
|
|
|
Assert(shipp->ai_index >= 0);
|
|
|
|
aip = &Ai_info[shipp->ai_index];
|
|
force_avoid_player_check(objp, aip);
|
|
|
|
ai_set_goal_abort_support_call(objp, aip);
|
|
aip->ok_to_target_timestamp = timestamp(DELAY_TARGET_TIME);
|
|
|
|
// This function is called whenever a guarded ship is destroyed, so this code
|
|
// prevents a ship from trying to guard a non-existent wing.
|
|
if (Wings[wingnum].current_count < 1) {
|
|
aip->guard_objnum = -1;
|
|
aip->guard_wingnum = -1;
|
|
aip->mode = AIM_NONE;
|
|
} else {
|
|
leader_shipnum = Wings[wingnum].ship_index[0];
|
|
leader_objnum = Ships[leader_shipnum].objnum;
|
|
|
|
Assert((leader_objnum >= 0) && (leader_objnum < MAX_OBJECTS));
|
|
|
|
if (leader_objnum == OBJ_INDEX(objp)) {
|
|
return;
|
|
}
|
|
|
|
aip->guard_wingnum = wingnum;
|
|
aip->guard_objnum = leader_objnum;
|
|
aip->guard_signature = Objects[leader_objnum].signature;
|
|
aip->mode = AIM_GUARD;
|
|
aip->submode = AIS_GUARD_STATIC;
|
|
aip->submode_start_time = Missiontime;
|
|
|
|
ai_set_guard_vec(objp, &Objects[leader_objnum]);
|
|
}
|
|
}
|
|
|
|
// Make objp guard other_objp
|
|
// If other_objp is a member of a wing, objp will guard that whole wing
|
|
// UNLESS objp is also a member of the wing!
|
|
void ai_set_guard_object(object *objp, object *other_objp)
|
|
{
|
|
ship *shipp;
|
|
ai_info *aip;
|
|
int other_objnum;
|
|
|
|
Assert(objp->type == OBJ_SHIP);
|
|
Assert(objp->instance >= 0);
|
|
Assert(objp != other_objp);
|
|
|
|
shipp = &Ships[objp->instance];
|
|
|
|
Assert(shipp->ai_index >= 0);
|
|
|
|
aip = &Ai_info[shipp->ai_index];
|
|
aip->avoid_check_timestamp = timestamp(1);
|
|
|
|
// If ship to guard is in a wing, guard that whole wing, unless the appropriate flag has been set
|
|
auto other_shipp = &Ships[other_objp->instance];
|
|
if ((other_shipp->wingnum != -1) && (other_shipp->wingnum != shipp->wingnum) && !(The_mission.ai_profile->flags[AI::Profile_Flags::Ai_guards_specific_ship_in_wing])) {
|
|
ai_set_guard_wing(objp, other_shipp->wingnum);
|
|
} else {
|
|
|
|
other_objnum = OBJ_INDEX(other_objp);
|
|
|
|
aip->guard_objnum = other_objnum;
|
|
aip->guard_signature = other_objp->signature;
|
|
aip->guard_wingnum = -1;
|
|
|
|
aip->mode = AIM_GUARD;
|
|
aip->submode = AIS_GUARD_STATIC;
|
|
aip->submode_start_time = Missiontime;
|
|
|
|
Assert(other_objnum >= 0); // Hmm, bogus object and we need its position for guard_vec.
|
|
|
|
ai_set_guard_vec(objp, &Objects[other_objnum]);
|
|
|
|
ai_set_goal_abort_support_call(objp, aip);
|
|
aip->ok_to_target_timestamp = timestamp(DELAY_TARGET_TIME);
|
|
}
|
|
}
|
|
|
|
// Update the aspect_locked_time field based on whether enemy is in view cone.
|
|
// Also set/clear AIF_SEEK_LOCK.
|
|
void update_aspect_lock_information(ai_info *aip, vec3d *vec_to_enemy, float dist_to_enemy, float enemy_radius)
|
|
{
|
|
float dot_to_enemy;
|
|
int num_weapon_types;
|
|
int weapon_id_list[MAX_WEAPON_TYPES], weapon_bank_list[MAX_WEAPON_TYPES];
|
|
ship *shipp;
|
|
ship *tshpp;
|
|
ship_weapon *swp;
|
|
weapon_info *wip;
|
|
object *tobjp = &Objects[aip->target_objnum];
|
|
|
|
shipp = &Ships[aip->shipnum];
|
|
tshpp = NULL;
|
|
swp = &shipp->weapons;
|
|
|
|
object *aiobjp = &Objects[shipp->objnum];
|
|
|
|
// AL 3-7-98: This probably should never happen, but check to ensure that current_secondary_bank is valid
|
|
if ( (swp->current_secondary_bank < 0) || (swp->current_secondary_bank >= swp->num_secondary_banks) ) {
|
|
return;
|
|
}
|
|
|
|
if (tobjp->type == OBJ_SHIP) {
|
|
tshpp = &Ships[tobjp->instance];
|
|
}
|
|
|
|
num_weapon_types = get_available_secondary_weapons(Pl_objp, weapon_id_list, weapon_bank_list);
|
|
|
|
wip = &Weapon_info[swp->secondary_bank_weapons[swp->current_secondary_bank]];
|
|
|
|
if (num_weapon_types && (wip->is_locked_homing()) && !(shipp->flags[Ship::Ship_Flags::No_secondary_lockon])) {
|
|
aip->ai_flags.set(AI::AI_Flags::Seek_lock, dist_to_enemy > 300.0f - MIN(enemy_radius, 100.0f));
|
|
|
|
// Update locking information for aspect seeking missiles.
|
|
aip->current_target_is_locked = 0;
|
|
dot_to_enemy = vm_vec_dot(vec_to_enemy, &aiobjp->orient.vec.fvec);
|
|
|
|
float needed_dot = 0.9f - 0.5f * enemy_radius/(dist_to_enemy + enemy_radius); // Replaced MIN_TRACKABLE_DOT with 0.9f
|
|
if (dot_to_enemy > needed_dot &&
|
|
(wip->wi_flags[Weapon::Info_Flags::Homing_aspect] ||
|
|
(wip->wi_flags[Weapon::Info_Flags::Homing_javelin] &&
|
|
(tshpp == NULL ||
|
|
ship_get_closest_subsys_in_sight(tshpp, SUBSYSTEM_ENGINE, &aiobjp->pos))))) {
|
|
aip->aspect_locked_time += flFrametime;
|
|
if (aip->aspect_locked_time >= wip->min_lock_time) {
|
|
aip->current_target_is_locked = 1;
|
|
}
|
|
} else {
|
|
if (aip->aspect_locked_time > wip->min_lock_time)
|
|
aip->aspect_locked_time = wip->min_lock_time;
|
|
|
|
aip->aspect_locked_time -= flFrametime*2;
|
|
if (aip->aspect_locked_time < 0.0f)
|
|
aip->aspect_locked_time = 0.0f;
|
|
}
|
|
|
|
} else {
|
|
aip->current_target_is_locked = 0;
|
|
aip->aspect_locked_time = 0.0f; // Used to be this, why?: wip->min_lock_time;
|
|
aip->ai_flags.remove(AI::AI_Flags::Seek_lock);
|
|
}
|
|
|
|
}
|
|
|
|
// We're in chase mode and we've recently collided with our target.
|
|
// Fly away from it!
|
|
void ai_chase_fly_away(object *objp, ai_info *aip)
|
|
{
|
|
int abort_flag = 0;
|
|
|
|
if (aip->ai_flags[AI::AI_Flags::Target_collision]) {
|
|
aip->ai_flags.remove(AI::AI_Flags::Target_collision); // Don't process this hit again next frame.
|
|
aip->submode = SM_FLY_AWAY; // Focus on avoiding target
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
|
|
if ((aip->target_objnum == -1) || (Objects[aip->target_objnum].signature != aip->target_signature)) {
|
|
abort_flag = 1;
|
|
}
|
|
|
|
if (abort_flag || (Missiontime > aip->submode_start_time + F1_0)) {
|
|
aip->last_attack_time = Missiontime;
|
|
aip->submode = SM_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
} else {
|
|
vec3d v2e;
|
|
float dot;
|
|
|
|
Assertion(aip->target_objnum >= 0, "This ship's target objnum is nonsense. It is %d instead of a positive number or -1.", aip->target_objnum);
|
|
|
|
vm_vec_normalized_dir(&v2e, &Objects[aip->target_objnum].pos, &objp->pos);
|
|
|
|
dot = vm_vec_dot(&objp->orient.vec.fvec, &v2e);
|
|
if (dot < 0.0f)
|
|
accelerate_ship(aip, 1.0f);
|
|
else
|
|
accelerate_ship(aip, 1.0f - dot);
|
|
turn_away_from_point(objp, &Objects[aip->target_objnum].pos, 0.0f);
|
|
}
|
|
}
|
|
|
|
// Return bank index of favored secondary weapon.
|
|
// Return -1 if nothing favored.
|
|
// "favored" means SEXPs have specified the weapon as being good to fire at en_objp.
|
|
int has_preferred_secondary(object *objp, object *en_objp, ship_weapon *swp)
|
|
{
|
|
int i;
|
|
|
|
for (i=0; i<swp->num_secondary_banks; i++) {
|
|
if (swp->secondary_bank_capacity[i] > 0) {
|
|
if (ship_secondary_has_ammo(swp, i)) {
|
|
if (is_preferred_weapon(swp->secondary_bank_weapons[i], objp, en_objp) != -1){
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
// Choose which secondary weapon to fire.
|
|
// Note, this is not like ai_select_secondary_weapon(). "choose" means make a choice.
|
|
// "select" means execute an order. Get it?
|
|
// This function calls ai_select_secondary_weapon() with the characteristics it should search for.
|
|
// return true if an appropriate weapon was found and switched to, false if no valid weapons, and should not fire
|
|
bool ai_choose_secondary_weapon(object *objp, ai_info *aip, object *en_objp)
|
|
{
|
|
float subsystem_strength = 0.0f;
|
|
bool is_big_ship;
|
|
flagset<Weapon::Info_Flags> wif_priority1, wif_priority2;
|
|
ship_weapon *swp;
|
|
ship_info *esip;
|
|
|
|
if ( en_objp->type == OBJ_SHIP ) {
|
|
esip = &Ship_info[Ships[en_objp->instance].ship_info_index];
|
|
} else {
|
|
esip = NULL;
|
|
}
|
|
|
|
swp = &Ships[objp->instance].weapons;
|
|
|
|
// AL 3-5-98: do a quick out if the ship has no secondaries
|
|
if ( swp->num_secondary_banks <= 0 ) {
|
|
swp->current_secondary_bank = -1;
|
|
return false;
|
|
}
|
|
|
|
int preferred_secondary = has_preferred_secondary(objp, en_objp, swp);
|
|
|
|
if (preferred_secondary != -1) {
|
|
if (swp->current_secondary_bank != preferred_secondary) {
|
|
aip->current_target_is_locked = 0;
|
|
aip->aspect_locked_time = 0.0f;
|
|
swp->current_secondary_bank = preferred_secondary;
|
|
}
|
|
aip->ai_flags.set(AI::AI_Flags::Unload_secondaries);
|
|
return true;
|
|
} else {
|
|
aip->ai_flags.remove(AI::AI_Flags::Unload_secondaries);
|
|
if (aip->targeted_subsys) {
|
|
subsystem_strength = aip->targeted_subsys->current_hits;
|
|
}
|
|
|
|
if ( esip ) {
|
|
is_big_ship = esip->class_type >= 0 && Ship_types[esip->class_type].flags[Ship::Type_Info_Flags::Targeted_by_huge_Ignored_by_small_only];
|
|
} else {
|
|
is_big_ship = false;
|
|
}
|
|
|
|
if (is_big_ship)
|
|
{
|
|
wif_priority1.set(Weapon::Info_Flags::Huge).set(Weapon::Info_Flags::Capital_plus);
|
|
if (aip->ai_profile_flags[AI::Profile_Flags::Smart_secondary_weapon_selection]) {
|
|
wif_priority2.set(Weapon::Info_Flags::Bomber_plus);
|
|
}
|
|
else {
|
|
wif_priority2.set(Weapon::Info_Flags::Homing_aspect).set(Weapon::Info_Flags::Homing_heat).set(Weapon::Info_Flags::Homing_javelin);
|
|
}
|
|
}
|
|
else if ( (esip != NULL) && (esip->flags[Ship::Info_Flags::Bomber]) )
|
|
{
|
|
wif_priority1.set(Weapon::Info_Flags::Bomber_plus);
|
|
wif_priority2.set(Weapon::Info_Flags::Homing_aspect).set(Weapon::Info_Flags::Homing_heat).set(Weapon::Info_Flags::Homing_javelin);
|
|
}
|
|
else if (subsystem_strength > 100.0f)
|
|
{
|
|
wif_priority1.set(Weapon::Info_Flags::Puncture);
|
|
wif_priority2.set(Weapon::Info_Flags::Homing_aspect).set(Weapon::Info_Flags::Homing_heat).set(Weapon::Info_Flags::Homing_javelin);
|
|
}
|
|
else if ((aip->ai_profile_flags[AI::Profile_Flags::Smart_secondary_weapon_selection]) && (en_objp->type == OBJ_ASTEROID)) //prefer dumbfires if its an asteroid
|
|
{
|
|
wif_priority1.reset();
|
|
wif_priority2.reset();
|
|
}
|
|
else
|
|
{
|
|
wif_priority1.set(Weapon::Info_Flags::Homing_aspect).set(Weapon::Info_Flags::Homing_heat).set(Weapon::Info_Flags::Homing_javelin);
|
|
wif_priority2.reset();
|
|
}
|
|
|
|
return ai_select_secondary_weapon(objp, swp, &wif_priority1, &wif_priority2);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return time, in seconds, at which this ship can next fire its current secondary weapon.
|
|
*/
|
|
float set_secondary_fire_delay(ai_info *aip, ship *shipp, weapon_info *swip, bool burst)
|
|
{
|
|
float t;
|
|
if (burst) {
|
|
t = swip->burst_delay;
|
|
} else {
|
|
t = swip->fire_wait; // Base delay for this weapon.
|
|
}
|
|
if (shipp->team == Player_ship->team) {
|
|
t *= aip->ai_ship_fire_secondary_delay_scale_friendly;
|
|
} else {
|
|
t *= aip->ai_ship_fire_secondary_delay_scale_hostile;
|
|
}
|
|
|
|
if (aip->ai_class_autoscale)
|
|
{
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Adjusted_AI_class_autoscale])
|
|
t += (ai_get_autoscale_index(Num_ai_classes) - ai_get_autoscale_index(aip->ai_class) + 1) * 0.5f;
|
|
else
|
|
t += (Num_ai_classes - aip->ai_class + 1) * 0.5f;
|
|
}
|
|
|
|
t *= frand_range(0.8f, 1.2f);
|
|
|
|
// For the missiles that fire fairly quickly, occasionally add an additional substantial delay.
|
|
if (t < 5.0f)
|
|
if (frand() < 0.5f)
|
|
t = t * 2.0f + 2.0f;
|
|
|
|
return t;
|
|
}
|
|
|
|
|
|
void ai_chase_big_approach_set_goal(vec3d *goal_pos, object *attack_objp, object *target_objp, float *accel)
|
|
{
|
|
float dist_to_goal;
|
|
|
|
// head straight toward him and maybe circle later
|
|
vm_vec_avg(goal_pos, &attack_objp->pos, &target_objp->pos);
|
|
|
|
// get distance to goal
|
|
dist_to_goal = vm_vec_dist(goal_pos, &attack_objp->pos);
|
|
|
|
// set accel
|
|
if (dist_to_goal > 400.0f) {
|
|
*accel = 1.0f;
|
|
} else {
|
|
*accel = dist_to_goal/400.0f;
|
|
}
|
|
}
|
|
|
|
void ai_chase_big_circle_set_goal(vec3d *goal_pos, object *attack_objp, object *target_objp, float *accel)
|
|
{
|
|
get_tangent_point(goal_pos, attack_objp, &target_objp->pos, attack_objp->radius + target_objp->radius + 100.0f);
|
|
|
|
*accel = 1.0f;
|
|
}
|
|
|
|
/**
|
|
* Get the current and desired horizontal separations between target
|
|
*/
|
|
void ai_chase_big_get_separations(object *attack_objp, object *target_objp, vec3d *horz_vec_to_target, float *desired_separation, float *cur_separation)
|
|
{
|
|
float temp, r_target, r_attacker;
|
|
float perp_dist;
|
|
vec3d vec_to_target;
|
|
|
|
// get parameters of ships (as cylinders - radius and height)
|
|
polymodel *pm = model_get(Ship_info[Ships[attack_objp->instance].ship_info_index].model_num);
|
|
|
|
// get radius of attacker (for rotations about forward)
|
|
temp = MAX(pm->maxs.xyz.x, pm->maxs.xyz.y);
|
|
r_attacker = MAX(-pm->mins.xyz.x, -pm->mins.xyz.y);
|
|
r_attacker = MAX(temp, r_attacker);
|
|
|
|
// get radius of target (for rotations about forward)
|
|
temp = MAX(pm->maxs.xyz.x, pm->maxs.xyz.y);
|
|
r_target = MAX(-pm->mins.xyz.x, -pm->mins.xyz.y);
|
|
r_target = MAX(temp, r_target);
|
|
|
|
// find separation between cylinders [if parallel]
|
|
vm_vec_sub(&vec_to_target, &attack_objp->pos, &target_objp->pos);
|
|
|
|
// find the distance between centers along forward direction of ships
|
|
perp_dist = vm_vec_dot(&vec_to_target, &target_objp->orient.vec.fvec);
|
|
|
|
// subtract off perp component to get "horizontal" separation vector between cylinders [ASSUMING parallel]
|
|
vm_vec_scale_add(horz_vec_to_target, &vec_to_target, &target_objp->orient.vec.fvec, -perp_dist);
|
|
*cur_separation = vm_vec_mag_quick(horz_vec_to_target);
|
|
|
|
// choose "optimal" separation of 1000 + r_target + r_attacker
|
|
*desired_separation = 1000 + r_target + r_attacker;
|
|
}
|
|
|
|
void ai_chase_big_parallel_set_goal(vec3d *goal_pos, object *attack_objp, object *target_objp, float *accel)
|
|
{
|
|
int opposing;
|
|
float temp, r_target, r_attacker;
|
|
float separation, optimal_separation;
|
|
vec3d horz_vec_to_target;
|
|
|
|
// get parameters of ships (as cylinders - radius and height)
|
|
polymodel *pm = model_get(Ship_info[Ships[attack_objp->instance].ship_info_index].model_num);
|
|
|
|
// get radius of attacker (for rotations about forward)
|
|
temp = MAX(pm->maxs.xyz.x, pm->maxs.xyz.y);
|
|
r_attacker = MAX(-pm->mins.xyz.x, -pm->mins.xyz.y);
|
|
r_attacker = MAX(temp, r_attacker);
|
|
|
|
// get radius of target (for rotations about forward)
|
|
temp = MAX(pm->maxs.xyz.x, pm->maxs.xyz.y);
|
|
r_target = MAX(-pm->mins.xyz.x, -pm->mins.xyz.y);
|
|
r_target = MAX(temp, r_target);
|
|
|
|
// are we opposing (only when other ship is not moving)
|
|
opposing = ( vm_vec_dot(&attack_objp->orient.vec.fvec, &target_objp->orient.vec.fvec) < 0 );
|
|
|
|
ai_chase_big_get_separations(attack_objp, target_objp, &horz_vec_to_target, &optimal_separation, &separation);
|
|
|
|
// choose dist (2000) so that we don't bash
|
|
float dist = 2000;
|
|
if (opposing) {
|
|
dist = - dist;
|
|
}
|
|
|
|
// set the goal pos as dist forward from target along target forward
|
|
vm_vec_scale_add(goal_pos, &target_objp->pos, &target_objp->orient.vec.fvec, dist);
|
|
// then add horizontal separation
|
|
vm_vec_scale_add2(goal_pos, &horz_vec_to_target, optimal_separation/separation);
|
|
|
|
// find the distance between centers along forward direction of ships
|
|
vec3d vec_to_target;
|
|
vm_vec_sub(&vec_to_target, &target_objp->pos, &attack_objp->pos);
|
|
float perp_dist = vm_vec_dot(&vec_to_target, &target_objp->orient.vec.fvec);
|
|
|
|
float match_accel = 0.0f;
|
|
float length_scale = attack_objp->radius;
|
|
|
|
if (Ship_info[Ships[attack_objp->instance].ship_info_index].max_vel.xyz.z != 0.0f) {
|
|
match_accel = target_objp->phys_info.vel.xyz.z / Ship_info[Ships[attack_objp->instance].ship_info_index].max_vel.xyz.z;
|
|
}
|
|
|
|
// if we're heading toward enemy ship, we want to keep going if we're ahead
|
|
if (opposing) {
|
|
perp_dist = -perp_dist;
|
|
}
|
|
|
|
if (perp_dist > 0) {
|
|
// falling behind, so speed up
|
|
*accel = match_accel + (1.0f - match_accel) / length_scale * (perp_dist);
|
|
} else {
|
|
// up in front, so slow down
|
|
*accel = match_accel - match_accel / length_scale * -perp_dist;
|
|
*accel = MAX(0.0f, *accel);
|
|
}
|
|
|
|
}
|
|
|
|
// Return *goal_pos for one cruiser to attack another (big ship).
|
|
// Choose point fairly nearby that is not occupied by another cruiser.
|
|
void ai_cruiser_chase_set_goal_pos(vec3d *goal_pos, object *pl_objp, object *en_objp)
|
|
{
|
|
ai_info *aip;
|
|
|
|
aip = &Ai_info[Ships[pl_objp->instance].ai_index];
|
|
float accel;
|
|
|
|
switch (aip->submode) {
|
|
case SM_BIG_APPROACH:
|
|
// do approach stuff;
|
|
ai_chase_big_approach_set_goal(goal_pos, pl_objp, en_objp, &accel);
|
|
break;
|
|
|
|
case SM_BIG_CIRCLE:
|
|
// do circle stuff
|
|
ai_chase_big_circle_set_goal(goal_pos, pl_objp, en_objp, &accel);
|
|
break;
|
|
|
|
case SM_BIG_PARALLEL:
|
|
// do parallel stuff
|
|
ai_chase_big_parallel_set_goal(goal_pos, pl_objp, en_objp, &accel);
|
|
break;
|
|
}
|
|
}
|
|
|
|
int maybe_hack_cruiser_chase_abort()
|
|
{
|
|
ship *shipp = &Ships[Pl_objp->instance];
|
|
ship *eshipp = &Ships[En_objp->instance];
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
|
|
// mission sm3-08, sathanos chasing collosus
|
|
if ( stricmp(Mission_filename, "sm3-08.fs2") == 0 ) {
|
|
if (( stricmp(eshipp->ship_name, "colossus") == 0 ) || ( stricmp(shipp->ship_name, "colossus") == 0 )) {
|
|
// Changed so all big ships attacking the Colossus will not do the chase code.
|
|
// Did this so Beast wouldn't swerve away from Colossus. -- MK, 9/14/99
|
|
ai_clear_ship_goals( aip );
|
|
aip->mode = AIM_NONE;
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Make a big ship pursue another big ship.
|
|
// (Note, called "ai_cruiser_chase" because we already have ai_chase_big() which means fighter chases big ship.
|
|
void ai_cruiser_chase()
|
|
{
|
|
ship *shipp = &Ships[Pl_objp->instance];
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
|
|
Assertion(En_objp->type == OBJ_SHIP, "En_objp is of type %d which is illegal in ai_cruiser_chase. Please report to the SCP!", En_objp->type);
|
|
if (En_objp->type != OBJ_SHIP) {
|
|
return;
|
|
}
|
|
|
|
Assertion(En_objp->instance > -1, "En_objp has a bad instance of %d which is illegal in ai_cruiser_chase. Please report to the SCP!", En_objp->instance);
|
|
if (En_objp->instance < 0) {
|
|
return;
|
|
}
|
|
|
|
vec3d goal_pos;
|
|
|
|
// kamikaze - ram and explode
|
|
if (aip->ai_flags[AI::AI_Flags::Kamikaze]) {
|
|
// point toward the subsystem if we're targeting one; otherwise point toward the center
|
|
if (aip->targeted_subsys) {
|
|
get_subsystem_pos(&goal_pos, En_objp, aip->targeted_subsys);
|
|
} else {
|
|
goal_pos = En_objp->pos;
|
|
}
|
|
|
|
ai_turn_towards_vector(&goal_pos, Pl_objp, nullptr, nullptr, 0.0f, 0);
|
|
accelerate_ship(aip, 1.0f);
|
|
|
|
float dot = vm_vec_dot_to_point(&Pl_objp->orient.vec.fvec, &Pl_objp->pos, &goal_pos);
|
|
if (ai_willing_to_afterburn_hard(aip) && dot > 0.99f)
|
|
ai_afterburn_hard(Pl_objp, aip);
|
|
}
|
|
|
|
// really track down and chase
|
|
else {
|
|
// check valid submode
|
|
Assert( (aip->submode == SM_ATTACK) || (aip->submode == SM_BIG_APPROACH) || (aip->submode == SM_BIG_CIRCLE) || (aip->submode == SM_BIG_PARALLEL) );
|
|
|
|
// just entering, approach enemy ship
|
|
if (aip->submode == SM_ATTACK) {
|
|
aip->submode = SM_BIG_APPROACH;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
|
|
// desired accel
|
|
float accel = 0.0f;
|
|
vec3d *rvecp = NULL;
|
|
|
|
switch (aip->submode) {
|
|
case SM_BIG_APPROACH:
|
|
// do approach stuff;
|
|
ai_chase_big_approach_set_goal(&goal_pos, Pl_objp, En_objp, &accel);
|
|
// maybe set rvec
|
|
break;
|
|
|
|
case SM_BIG_CIRCLE:
|
|
// do circle stuff
|
|
ai_chase_big_circle_set_goal(&goal_pos, Pl_objp, En_objp, &accel);
|
|
// maybe set rvec
|
|
break;
|
|
|
|
case SM_BIG_PARALLEL:
|
|
// do parallel stuff
|
|
ai_chase_big_parallel_set_goal(&goal_pos, Pl_objp, En_objp, &accel);
|
|
//maybe set rvec
|
|
break;
|
|
}
|
|
|
|
// now move as desired
|
|
ai_turn_towards_vector(&goal_pos, Pl_objp, nullptr, nullptr, 0.0f, 0, rvecp);
|
|
accelerate_ship(aip, accel);
|
|
|
|
// maybe switch to new mode
|
|
vec3d vec_to_enemy;
|
|
float dist_to_enemy;
|
|
int moving = (En_objp->phys_info.vel.xyz.z > 0.5f);
|
|
vm_vec_sub(&vec_to_enemy, &En_objp->pos, &Pl_objp->pos);
|
|
dist_to_enemy = vm_vec_mag_quick(&vec_to_enemy);
|
|
|
|
switch (aip->submode) {
|
|
case SM_BIG_APPROACH:
|
|
if ( dist_to_enemy < (Pl_objp->radius + En_objp->radius)*1.25f + 200.0f ) {
|
|
// moving
|
|
if (moving) {
|
|
// if within 90 degrees of en forward, go into parallel, otherwise circle
|
|
if ( vm_vec_dot(&En_objp->orient.vec.fvec, &Pl_objp->orient.vec.fvec) > 0 ) {
|
|
aip->submode = SM_BIG_PARALLEL;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
}
|
|
|
|
// otherwise cirle
|
|
if ( !maybe_hack_cruiser_chase_abort() ) {
|
|
aip->submode = SM_BIG_CIRCLE;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SM_BIG_CIRCLE:
|
|
// moving
|
|
if (moving) {
|
|
vec3d temp;
|
|
float desired_sep, cur_sep;
|
|
// we're behind the enemy ship
|
|
if (vm_vec_dot(&vec_to_enemy, &En_objp->orient.vec.fvec) > 0) {
|
|
// and we're turning toward the enemy
|
|
if (vm_vec_dot(&En_objp->orient.vec.fvec, &Pl_objp->orient.vec.fvec) > 0) {
|
|
// get separation
|
|
ai_chase_big_get_separations(Pl_objp, En_objp, &temp, &desired_sep, &cur_sep);
|
|
// and the separation is > 0.9 desired
|
|
if (cur_sep > (0.9f * desired_sep)) {
|
|
aip->submode = SM_BIG_PARALLEL;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// still
|
|
vec3d temp;
|
|
float desired_sep, cur_sep;
|
|
// we're behind the enemy ship
|
|
if (vm_vec_dot(&vec_to_enemy, &En_objp->orient.vec.fvec) > 0) {
|
|
// and we're turning toward the enemy
|
|
if (vm_vec_dot(&En_objp->orient.vec.fvec, &Pl_objp->orient.vec.fvec) > 0) {
|
|
// get separation
|
|
ai_chase_big_get_separations(Pl_objp, En_objp, &temp, &desired_sep, &cur_sep);
|
|
// and the separation is > 0.9 desired
|
|
if (cur_sep > (0.9f * desired_sep)) {
|
|
aip->submode = SM_BIG_PARALLEL;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
}
|
|
}
|
|
// in front of ship
|
|
else {
|
|
// and we're turning toward the enemy
|
|
if (vm_vec_dot(&En_objp->orient.vec.fvec, &Pl_objp->orient.vec.fvec) < 0) {
|
|
// get separation
|
|
ai_chase_big_get_separations(Pl_objp, En_objp, &temp, &desired_sep, &cur_sep);
|
|
// and the separation is > 0.9 desired
|
|
if (cur_sep > (0.9f * desired_sep)) {
|
|
aip->submode = SM_BIG_PARALLEL;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SM_BIG_PARALLEL:
|
|
// we're opposing
|
|
if ( vm_vec_dot(&Pl_objp->orient.vec.fvec, &En_objp->orient.vec.fvec) < 0 ) {
|
|
// and the other ship is moving
|
|
if (moving) {
|
|
// and we no longer overlap
|
|
if ( dist_to_enemy > (0.75 * (En_objp->radius + Pl_objp->radius)) ) {
|
|
aip->submode = SM_BIG_APPROACH;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
constexpr int DEFAULT_WEAPON_RANGE = 700;
|
|
|
|
/**
|
|
* Make object Pl_objp chase object En_objp
|
|
*/
|
|
void ai_chase()
|
|
{
|
|
float dist_to_enemy;
|
|
float dot_to_enemy, dot_from_enemy, real_dot_to_enemy;
|
|
vec3d player_pos, enemy_pos, predicted_enemy_pos, real_vec_to_enemy, predicted_vec_to_enemy;
|
|
ship *shipp = &Ships[Pl_objp->instance];
|
|
ship_info *sip = &Ship_info[shipp->ship_info_index];
|
|
ship_info *enemy_sip = NULL;
|
|
ship_weapon *swp = &shipp->weapons;
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
flagset<Ship::Info_Flags> enemy_sip_flags;
|
|
flagset<Ship::Ship_Flags> enemy_shipp_flags;
|
|
int has_fired = -1;
|
|
|
|
Assertion(aip->mode == AIM_CHASE, "ai_chase was called but the ship was not in the chase ai mode. It is in %d instead. This is a fundamental problem and needs to be reported to the SCP, right away!", aip->mode);
|
|
|
|
// by default we try to chase anything
|
|
bool go_after_it = true;
|
|
|
|
if ( (sip->class_type > -1) && (En_objp->type == OBJ_SHIP) )
|
|
{
|
|
// default to not chasing for ships
|
|
go_after_it = false;
|
|
ship_info *esip = &Ship_info[Ships[En_objp->instance].ship_info_index];
|
|
if (esip->class_type > -1)
|
|
{
|
|
ship_type_info *stp = &Ship_types[sip->class_type];
|
|
size_t ap_size = stp->ai_actively_pursues.size();
|
|
for(size_t i = 0; i < ap_size; i++)
|
|
{
|
|
if(stp->ai_actively_pursues[i] == esip->class_type) {
|
|
go_after_it = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// if there was no class type then assume we can go after it ...
|
|
go_after_it = true;
|
|
// ... but also log this in debug so it doesn't go unchecked (NOTE that this can completely flood a debug log!)
|
|
mprintf(("AI-WARNING: No class_type specified for '%s', assuming that it's ok to chase!\n", esip->name));
|
|
}
|
|
}
|
|
|
|
//WMC - Guess we do need this
|
|
if (!go_after_it) {
|
|
aip->mode = AIM_NONE;
|
|
return;
|
|
}
|
|
|
|
if (sip->class_type > -1 && (Ship_types[sip->class_type].flags[Ship::Type_Info_Flags::AI_attempt_broadside])) {
|
|
ai_cruiser_chase();
|
|
return;
|
|
}
|
|
|
|
Assert( En_objp != NULL );
|
|
|
|
if ( En_objp->type == OBJ_SHIP ) {
|
|
enemy_sip_flags = Ship_info[Ships[En_objp->instance].ship_info_index].flags;
|
|
enemy_sip = &Ship_info[Ships[En_objp->instance].ship_info_index];
|
|
enemy_shipp_flags = Ships[En_objp->instance].flags;
|
|
} else {
|
|
enemy_sip_flags.reset();
|
|
enemy_shipp_flags.reset();
|
|
}
|
|
|
|
// If collided with target_objnum last frame, avoid that ship.
|
|
// This should prevent the embarrassing behavior of ships getting stuck on each other
|
|
// as if they were magnetically attracted. -- MK, 11/13/97.
|
|
if ((aip->ai_flags[AI::AI_Flags::Target_collision]) || (aip->submode == SM_FLY_AWAY)) {
|
|
ai_chase_fly_away(Pl_objp, aip);
|
|
return;
|
|
}
|
|
|
|
if (better_collision_avoidance_triggered(
|
|
The_mission.ai_profile->flags[AI::Profile_Flags::Better_combat_collision_avoidance],
|
|
The_mission.ai_profile->better_collision_avoid_aggression_combat,
|
|
Pl_objp,
|
|
The_mission.ai_profile->flags[AI::Profile_Flags::Better_combat_collision_avoid_includes_target] ? nullptr : En_objp)) {
|
|
return;
|
|
}
|
|
|
|
if (enemy_sip_flags.any_set()) {
|
|
if (enemy_sip->is_big_or_huge()) {
|
|
ai_big_chase();
|
|
return;
|
|
}
|
|
}
|
|
|
|
ai_set_positions(Pl_objp, En_objp, aip, &player_pos, &enemy_pos);
|
|
dist_to_enemy = vm_vec_dist_quick(&player_pos, &enemy_pos);
|
|
vm_vec_sub(&real_vec_to_enemy, &enemy_pos, &player_pos);
|
|
|
|
//Enemy position for the purpose of aiming is already calculated differently, do it explicitly here
|
|
ai_update_aim(aip);
|
|
|
|
vm_vec_normalize(&real_vec_to_enemy);
|
|
|
|
real_dot_to_enemy = vm_vec_dot(&real_vec_to_enemy, &Pl_objp->orient.vec.fvec);
|
|
|
|
int is_stealthy_ship = 0;
|
|
if ( (enemy_sip_flags.any_set() ) && (enemy_shipp_flags[Ship::Ship_Flags::Stealth]) ) {
|
|
if ( ai_is_stealth_visible(Pl_objp, En_objp) != STEALTH_FULLY_TARGETABLE ) {
|
|
is_stealthy_ship = 1;
|
|
}
|
|
}
|
|
|
|
// Can only acquire lock on a target that isn't hidden from sensors
|
|
if ( En_objp->type == OBJ_SHIP && !(Ships[En_objp->instance].flags[Ship::Ship_Flags::Hidden_from_sensors]) && !is_stealthy_ship ) {
|
|
update_aspect_lock_information(aip, &real_vec_to_enemy, dist_to_enemy, En_objp->radius);
|
|
} else {
|
|
aip->current_target_is_locked = 0;
|
|
aip->ai_flags.remove(AI::AI_Flags::Seek_lock);
|
|
}
|
|
|
|
// only ever false for failed ballistic trajectories, to prevent firing, true for everything else
|
|
bool has_valid_ballistic_trajectory = true;
|
|
|
|
// If seeking lock, try to point directly at ship, else predict position so lasers can hit it.
|
|
// If just acquired target, or target is not in reasonable cone, don't refine believed enemy position.
|
|
if ((real_dot_to_enemy < 0.25f) || (aip->target_time < 1.0f)) {
|
|
predicted_enemy_pos = enemy_pos;
|
|
} else if (aip->ai_flags[AI::AI_Flags::Seek_lock] && !(The_mission.ai_profile->flags[AI::Profile_Flags::Ignore_aspect_when_leading]) ) {
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_ramming_stationary_targets_bug]) {
|
|
// fixed by Mantis 3147 - Bash orientation if aspect seekers are equipped.
|
|
set_predicted_enemy_pos(&predicted_enemy_pos, Pl_objp, &aip->last_aim_enemy_pos, &aip->last_aim_enemy_vel, aip); // Set G_fire_pos
|
|
predicted_enemy_pos = enemy_pos;
|
|
G_predicted_pos = predicted_enemy_pos;
|
|
} else {
|
|
// retail behavior
|
|
predicted_enemy_pos = enemy_pos;
|
|
}
|
|
} else {
|
|
// Set predicted_enemy_pos.
|
|
// See if attacking a subsystem.
|
|
weapon_info* wip = ai_get_weapon(&shipp->weapons);
|
|
if (wip) {
|
|
bool ballistic = !IS_VEC_NULL(&The_mission.gravity) && wip->gravity_const != 0.0f;
|
|
if (aip->targeted_subsys != NULL && get_shield_pct(En_objp) < HULL_DAMAGE_THRESHOLD_PERCENT) {
|
|
Assert(En_objp->type == OBJ_SHIP);
|
|
vec3d target_pos;
|
|
get_subsystem_pos(&target_pos, En_objp, aip->targeted_subsys);
|
|
|
|
if (!ballistic)
|
|
predicted_enemy_pos = target_pos;
|
|
else {
|
|
vec3d gravity_vec = The_mission.gravity * wip->gravity_const;
|
|
has_valid_ballistic_trajectory =
|
|
physics_lead_ballistic_trajectory(&Pl_objp->pos, &target_pos, &aip->last_aim_enemy_vel, wip->max_speed, &gravity_vec, &predicted_enemy_pos);
|
|
|
|
if (has_valid_ballistic_trajectory)
|
|
predicted_enemy_pos = Pl_objp->pos + predicted_enemy_pos * vm_vec_dist(&Pl_objp->pos, &target_pos);
|
|
else
|
|
predicted_enemy_pos = target_pos;
|
|
}
|
|
|
|
predicted_vec_to_enemy = real_vec_to_enemy;
|
|
} else {
|
|
if (!ballistic)
|
|
set_predicted_enemy_pos(&predicted_enemy_pos, Pl_objp, &aip->last_aim_enemy_pos, &aip->last_aim_enemy_vel, aip);
|
|
else {
|
|
vec3d gravity_vec = The_mission.gravity * wip->gravity_const;
|
|
has_valid_ballistic_trajectory =
|
|
physics_lead_ballistic_trajectory(&Pl_objp->pos, &aip->last_aim_enemy_pos, &aip->last_aim_enemy_vel, wip->max_speed, &gravity_vec, &predicted_enemy_pos);
|
|
|
|
if (has_valid_ballistic_trajectory)
|
|
predicted_enemy_pos = Pl_objp->pos + predicted_enemy_pos * vm_vec_dist(&Pl_objp->pos, &aip->last_aim_enemy_pos);
|
|
else
|
|
set_predicted_enemy_pos(&predicted_enemy_pos, Pl_objp, &aip->last_aim_enemy_pos, &aip->last_aim_enemy_vel, aip);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
vm_vec_sub(&predicted_vec_to_enemy, &predicted_enemy_pos, &player_pos);
|
|
|
|
vm_vec_normalize(&predicted_vec_to_enemy);
|
|
|
|
dot_to_enemy = vm_vec_dot(&Pl_objp->orient.vec.fvec, &predicted_vec_to_enemy);
|
|
dot_from_enemy= - vm_vec_dot(&En_objp->orient.vec.fvec, &real_vec_to_enemy);
|
|
|
|
// Set turn and acceleration based on submode.
|
|
switch (aip->submode) {
|
|
case SM_CONTINUOUS_TURN:
|
|
ai_chase_ct();
|
|
break;
|
|
|
|
case SM_STEALTH_FIND:
|
|
ai_stealth_find();
|
|
break;
|
|
|
|
case SM_STEALTH_SWEEP:
|
|
ai_stealth_sweep();
|
|
break;
|
|
|
|
case SM_ATTACK:
|
|
case SM_SUPER_ATTACK:
|
|
case SM_ATTACK_FOREVER:
|
|
case AIS_CHASE_GLIDEATTACK:
|
|
case AIS_CHASE_CIRCLESTRAFE:
|
|
if (vm_vec_dist_quick(&Pl_objp->pos, &predicted_enemy_pos) > 100.0f + En_objp->radius * 2.0f) {
|
|
if (maybe_avoid_big_ship(Pl_objp, En_objp, aip, &predicted_enemy_pos, 10.0f))
|
|
return;
|
|
}
|
|
|
|
ai_chase_attack(aip, sip, &predicted_enemy_pos, dist_to_enemy, sip->model_num);
|
|
break;
|
|
|
|
case SM_EVADE_SQUIGGLE:
|
|
ai_chase_es(aip);
|
|
break;
|
|
|
|
case SM_EVADE_BRAKE:
|
|
ai_chase_eb(aip, &predicted_enemy_pos);
|
|
break;
|
|
|
|
case SM_EVADE:
|
|
evade_ship();
|
|
break;
|
|
|
|
case SM_AVOID:
|
|
avoid_ship();
|
|
break;
|
|
|
|
case SM_GET_BEHIND:
|
|
get_behind_ship(aip);
|
|
break;
|
|
|
|
case SM_GET_AWAY: // Used to get away from opponent to prevent endless circling.
|
|
ai_chase_ga(aip, sip);
|
|
break;
|
|
|
|
case SM_EVADE_WEAPON:
|
|
evade_weapon();
|
|
break;
|
|
|
|
default:
|
|
aip->last_attack_time = Missiontime;
|
|
aip->submode = SM_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
|
|
//Maybe apply random sidethrust, depending on the current submode
|
|
//The following are valid targets for random sidethrust (circle strafe uses it too, but that is handled separately)
|
|
if (aip->submode == SM_ATTACK ||
|
|
aip->submode == SM_SUPER_ATTACK ||
|
|
aip->submode == SM_EVADE_SQUIGGLE ||
|
|
aip->submode == SM_EVADE ||
|
|
aip->submode == SM_GET_AWAY ||
|
|
aip->submode == AIS_CHASE_GLIDEATTACK)
|
|
{
|
|
|
|
float current_weapon_range;
|
|
|
|
// check that we have a valid index or not.
|
|
if (swp->num_primary_banks >= 1 && swp->current_primary_bank >= 0 && swp->primary_bank_weapons[swp->current_primary_bank] >= 0) {
|
|
current_weapon_range = Weapon_info[swp->primary_bank_weapons[swp->current_primary_bank]].weapon_range;
|
|
} else {
|
|
// if we didn't have a valid index (very rare) use this default weapon range which is close to average weapon range
|
|
current_weapon_range = DEFAULT_WEAPON_RANGE;
|
|
}
|
|
|
|
//Re-roll for random sidethrust every 2 seconds
|
|
//Also, only do this if we've recently been hit or are in current primary weapon range of target
|
|
if (static_randf((Missiontime + static_rand(aip->shipnum)) >> 17) < aip->ai_random_sidethrust_percent &&
|
|
((Missiontime - aip->last_hit_time < i2f(5) && aip->hitter_objnum >= 0) || dist_to_enemy < current_weapon_range))
|
|
{
|
|
do_random_sidethrust(aip, sip);
|
|
}
|
|
}
|
|
|
|
// Maybe choose a new submode.
|
|
if ( (aip->submode != SM_AVOID) && (aip->submode != SM_ATTACK_FOREVER) ) {
|
|
// If a very long time since attacked, attack no matter what!
|
|
if (Missiontime - aip->last_attack_time > i2f(6)) {
|
|
if ( (aip->submode != SM_SUPER_ATTACK) && (aip->submode != SM_GET_AWAY) && !(aip->ai_flags[AI::AI_Flags::Stealth_pursuit]) ) {
|
|
aip->submode = SM_SUPER_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->last_attack_time = Missiontime;
|
|
}
|
|
}
|
|
|
|
//SUSHI: Alternate stalemate dection method: if nobody has hit each other for a while
|
|
//(and we've been near that whole time), shake things up somehow
|
|
//Only do this if stalemate time threshold > 0
|
|
if (aip->ai_stalemate_time_thresh > 0.0f &&
|
|
Missiontime - aip->last_hit_target_time > fl2f(aip->ai_stalemate_time_thresh) &&
|
|
Missiontime - aip->last_hit_time > fl2f(aip->ai_stalemate_time_thresh) &&
|
|
aip->time_enemy_near > aip->ai_stalemate_time_thresh &&
|
|
(dot_to_enemy < 0.95f - 0.5f * En_objp->radius/MAX(1.0f, En_objp->radius + dist_to_enemy)))
|
|
{
|
|
//Every second, evaluate whether or not to break stalemate. The more patient the ship, the less likely this is.
|
|
if (static_randf((Missiontime + static_rand(aip->shipnum)) >> 16) > (aip->ai_patience * .01))
|
|
{
|
|
if ((sip->can_glide == true) && (frand() < aip->ai_glide_attack_percent)) {
|
|
//Maybe use glide attack
|
|
aip->submode = AIS_CHASE_GLIDEATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->last_hit_target_time = Missiontime;
|
|
aip->time_enemy_near = 0.0f;
|
|
} else {
|
|
//Otherwise, try to get away
|
|
aip->submode = SM_GET_AWAY;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->last_hit_target_time = Missiontime;
|
|
aip->time_enemy_near = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If a collision is expected, pull out!
|
|
// If enemy is pointing away and moving a bit, don't worry about collision detection.
|
|
if ((dot_from_enemy > 0.5f) || (En_objp->phys_info.speed < 10.0f)) {
|
|
//If we're in circle strafe mode, don't worry about colliding with the target
|
|
if (aip->submode != AIS_CHASE_CIRCLESTRAFE && might_collide_with_ship(Pl_objp, En_objp, dot_to_enemy, dist_to_enemy, 4.0f)) {
|
|
if ((Missiontime - aip->last_hit_time > F1_0*4) && (dist_to_enemy < Pl_objp->radius*2 + En_objp->radius*2)) {
|
|
accelerate_ship(aip, -1.0f);
|
|
} else {
|
|
aip->submode = SM_AVOID;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
switch (aip->submode) {
|
|
case SM_CONTINUOUS_TURN:
|
|
if (Missiontime - aip->submode_start_time > i2f(3)) {
|
|
aip->last_attack_time = Missiontime;
|
|
aip->submode = SM_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
break;
|
|
|
|
case SM_ATTACK:
|
|
// if target is stealth and stealth not visible, then enter stealth find mode
|
|
if ( (aip->ai_flags[AI::AI_Flags::Stealth_pursuit]) && (ai_is_stealth_visible(Pl_objp, En_objp) == STEALTH_NOT_IN_FRUSTUM) ) {
|
|
aip->submode = SM_STEALTH_FIND;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->submode_parm0 = SM_SF_AHEAD;
|
|
} else if (dist_to_enemy < CIRCLE_STRAFE_MAX_DIST + En_objp->radius &&
|
|
(En_objp->phys_info.speed < MAX(sip->max_vel.xyz.x, sip->max_vel.xyz.y) * 1.5f) &&
|
|
(static_randf((Missiontime + static_rand(aip->shipnum)) >> 19) < aip->ai_circle_strafe_percent)) {
|
|
aip->submode = AIS_CHASE_CIRCLESTRAFE;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->last_attack_time = Missiontime;
|
|
} else if (ai_near_full_strength(Pl_objp) && (Missiontime - aip->last_hit_target_time > i2f(3)) && (dist_to_enemy < 500.0f) && (dot_to_enemy < 0.5f)) {
|
|
aip->submode = SM_SUPER_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->last_attack_time = Missiontime;
|
|
} else if ((Missiontime - aip->last_hit_target_time > i2f(6)) &&
|
|
(dist_to_enemy < 500.0f) && (dot_to_enemy < 0.2f) &&
|
|
(frand() < (float) Game_skill_level/NUM_SKILL_LEVELS)) {
|
|
aip->submode = SM_GET_AWAY;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->last_hit_target_time = Missiontime;
|
|
} else if (enemy_sip != NULL && (enemy_sip->is_small_ship())
|
|
&& (dot_to_enemy < dot_from_enemy)
|
|
&& (En_objp->phys_info.speed > 15.0f)
|
|
&& (dist_to_enemy < 200.0f)
|
|
&& (dist_to_enemy > 50.0f)
|
|
&& (dot_to_enemy < 0.1f)
|
|
&& (Missiontime - aip->submode_start_time > i2f(2))) {
|
|
aip->submode = SM_EVADE_BRAKE;
|
|
aip->submode_start_time = Missiontime;
|
|
} else if ((dot_to_enemy > 0.2f) && (dot_from_enemy > -0.2f) && (dot_from_enemy < 0.1f)) {
|
|
aip->submode = SM_GET_BEHIND;
|
|
aip->submode_start_time = Missiontime;
|
|
} else if ( enemy_sip != NULL && (enemy_sip->is_small_ship()) && (dist_to_enemy < 150.0f) && (dot_from_enemy > dot_to_enemy + 0.5f + aip->ai_courage*.002)) {
|
|
float get_away_chance = (aip->ai_get_away_chance == FLT_MIN)
|
|
? (float)(ai_maybe_autoscale(aip->ai_class) + Game_skill_level)/(ai_maybe_autoscale(Num_ai_classes) + NUM_SKILL_LEVELS)
|
|
: aip->ai_get_away_chance;
|
|
if ((Missiontime - aip->last_hit_target_time > i2f(5)) && (frand() < get_away_chance)) {
|
|
aip->submode = SM_GET_AWAY;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->last_hit_target_time = Missiontime;
|
|
} else {
|
|
aip->submode = SM_EVADE_SQUIGGLE;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
} else if ( enemy_sip != NULL && (enemy_sip->is_small_ship()) && (Missiontime - aip->submode_start_time > F1_0 * 2)) {
|
|
if ((dot_to_enemy < 0.8f) && (dot_from_enemy > dot_to_enemy)) {
|
|
if (frand() > 0.5f) {
|
|
aip->submode = SM_CONTINUOUS_TURN;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->submode_parm0 = Random::next() & 0x0f;
|
|
} else {
|
|
aip->submode = SM_EVADE;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
} else {
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
}
|
|
|
|
aip->last_attack_time = Missiontime;
|
|
|
|
break;
|
|
|
|
case SM_EVADE_SQUIGGLE:
|
|
if ((Missiontime - aip->submode_start_time > i2f(5)) || (dist_to_enemy > 300.0f)) {
|
|
if ((dist_to_enemy < 100.0f) && (dot_to_enemy < 0.0f) && (dot_from_enemy > 0.5f)) {
|
|
aip->submode = SM_EVADE_BRAKE;
|
|
} else if ((Pl_objp->phys_info.speed >= Pl_objp->phys_info.max_vel.xyz.z / 2.0f) && (sip->can_glide == true) && (frand() < aip->ai_glide_attack_percent)) {
|
|
aip->last_attack_time = Missiontime;
|
|
aip->submode = AIS_CHASE_GLIDEATTACK;
|
|
} else {
|
|
aip->last_attack_time = Missiontime;
|
|
aip->submode = SM_ATTACK;
|
|
}
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
break;
|
|
|
|
case SM_EVADE_BRAKE:
|
|
if ((dist_to_enemy < 15.0f) || (En_objp->phys_info.speed < 10.0f)) {
|
|
aip->submode = SM_AVOID;
|
|
aip->submode_start_time = Missiontime;
|
|
} else if ((dot_to_enemy > 0.9f) || ((dot_from_enemy > 0.9f) && (Missiontime - aip->submode_start_time > i2f(1)))) {
|
|
aip->last_attack_time = Missiontime;
|
|
aip->submode = SM_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
} else if (Missiontime - aip->submode_start_time > i2f(4)) {
|
|
aip->last_attack_time = Missiontime;
|
|
aip->submode = SM_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
break;
|
|
|
|
case SM_EVADE:
|
|
// Modified by MK on 5/5/97 to keep trying to regain attack mode. It's what a human would do.
|
|
if ((dot_to_enemy < 0.2f) && (dot_from_enemy < 0.8f) && (dist_to_enemy < 100.0f) && (En_objp->phys_info.speed > 15.0f)) {
|
|
aip->last_attack_time = Missiontime;
|
|
aip->submode = SM_EVADE_BRAKE;
|
|
aip->submode_start_time = Missiontime;
|
|
} else if (((dot_to_enemy > dot_from_enemy - 0.1f)
|
|
&& (Missiontime > aip->submode_start_time + i2f(1)))
|
|
|| (dist_to_enemy > 150.0f + 2*(Pl_objp->radius + En_objp->radius))) {
|
|
aip->last_attack_time = Missiontime;
|
|
aip->submode = SM_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
} else if (Missiontime - aip->submode_start_time > i2f(2))
|
|
if (dot_from_enemy > 0.8f) {
|
|
aip->submode = SM_EVADE_SQUIGGLE;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
|
|
break;
|
|
|
|
case SM_SUPER_ATTACK:
|
|
// if stealth and invisible, enter stealth find mode
|
|
if ( (aip->ai_flags[AI::AI_Flags::Stealth_pursuit]) && (ai_is_stealth_visible(Pl_objp, En_objp) == STEALTH_NOT_IN_FRUSTUM) ) {
|
|
aip->submode = SM_STEALTH_FIND;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->submode_parm0 = SM_SF_AHEAD;
|
|
} else if (dist_to_enemy < CIRCLE_STRAFE_MAX_DIST + En_objp->radius &&
|
|
(En_objp->phys_info.speed < MAX(sip->max_vel.xyz.x, sip->max_vel.xyz.y) * 1.5f) &&
|
|
(static_randf((Missiontime + static_rand(aip->shipnum)) >> 19) < aip->ai_circle_strafe_percent)) {
|
|
aip->submode = AIS_CHASE_CIRCLESTRAFE;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->last_attack_time = Missiontime;
|
|
} else if ((dist_to_enemy < 100.0f) && (dot_to_enemy < 0.8f) && (enemy_sip != NULL) && (enemy_sip->is_small_ship()) && (Missiontime - aip->submode_start_time > i2f(5) )) {
|
|
aip->ai_flags.remove(AI::AI_Flags::Attack_slowly); // Just in case, clear here.
|
|
|
|
float get_away_chance = (aip->ai_get_away_chance == FLT_MIN)
|
|
? (float)(ai_maybe_autoscale(aip->ai_class) + Game_skill_level)/(ai_maybe_autoscale(Num_ai_classes) + NUM_SKILL_LEVELS)
|
|
: aip->ai_get_away_chance;
|
|
|
|
switch (Random::next(5)) {
|
|
case 0:
|
|
aip->submode = SM_CONTINUOUS_TURN;
|
|
aip->submode_start_time = Missiontime;
|
|
break;
|
|
case 1:
|
|
aip->submode_start_time = Missiontime; // Stay in super attack mode
|
|
break;
|
|
case 2:
|
|
case 3:
|
|
if (frand() < (float) 0.5f * get_away_chance) {
|
|
aip->submode = SM_GET_AWAY;
|
|
aip->submode_start_time = Missiontime;
|
|
} else {
|
|
aip->submode = SM_EVADE;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
break;
|
|
case 4:
|
|
if (dot_from_enemy + (NUM_SKILL_LEVELS - Game_skill_level) * 0.1f > dot_to_enemy) { // Less likely to GET_AWAY at lower skill levels.
|
|
aip->submode = SM_EVADE;
|
|
aip->submode_start_time = Missiontime;
|
|
} else {
|
|
aip->submode = SM_GET_AWAY;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
break;
|
|
default:
|
|
UNREACHABLE("Somehow we changed a call to get a random number in ai_chase so it's not 0-4, please report to the SCP!"); // Impossible!
|
|
}
|
|
}
|
|
|
|
aip->last_attack_time = Missiontime;
|
|
|
|
break;
|
|
|
|
case SM_AVOID:
|
|
if ((dot_to_enemy > -0.2f) && (dist_to_enemy / (dot_to_enemy + 0.3f) < 100.0f)) {
|
|
aip->submode_start_time = Missiontime;
|
|
} else if (Missiontime - aip->submode_start_time > i2f(1)/2) {
|
|
if (might_collide_with_ship(Pl_objp, En_objp, dot_to_enemy, dist_to_enemy, 3.0f)) {
|
|
aip->submode_start_time = Missiontime;
|
|
} else {
|
|
aip->submode = SM_GET_BEHIND;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case SM_GET_BEHIND:
|
|
if ((dot_from_enemy < -0.7f) || (Missiontime - aip->submode_start_time > i2f(2))) {
|
|
aip->submode = SM_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->last_attack_time = Missiontime;
|
|
}
|
|
break;
|
|
|
|
case SM_GET_AWAY:
|
|
if (Missiontime - aip->submode_start_time > i2f(2)) {
|
|
float rand_dist;
|
|
|
|
rand_dist = ((Missiontime >> 17) & 0x03) * 100.0f + 200.0f; // Some value in 200..500
|
|
if ((Missiontime - aip->submode_start_time > i2f(5)) || (dist_to_enemy > rand_dist) || (dot_from_enemy < 0.4f)) {
|
|
//Sometimes use a glide attack instead (if we can)
|
|
if ((sip->can_glide == true) && (frand() < aip->ai_glide_attack_percent)) {
|
|
aip->submode = AIS_CHASE_GLIDEATTACK;
|
|
}
|
|
else {
|
|
aip->ai_flags.set(AI::AI_Flags::Attack_slowly);
|
|
aip->submode = SM_ATTACK;
|
|
}
|
|
|
|
aip->submode_start_time = Missiontime;
|
|
aip->time_enemy_in_range = 2.0f; // Cheat. Presumably if they were running away from you, they were monitoring you!
|
|
aip->last_attack_time = Missiontime;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SM_EVADE_WEAPON:
|
|
if (aip->danger_weapon_objnum == -1) {
|
|
aip->submode = SM_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->last_attack_time = Missiontime;
|
|
}
|
|
break;
|
|
|
|
// Either change to SM_ATTACK or AIM_FIND_STEALTH
|
|
case SM_STEALTH_FIND:
|
|
// if time > 5 sec change mode to sweep
|
|
if ( !(aip->ai_flags[AI::AI_Flags::Stealth_pursuit]) || (ai_is_stealth_visible(Pl_objp, En_objp) == STEALTH_IN_FRUSTUM) ) {
|
|
aip->submode = SM_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->last_attack_time = Missiontime;
|
|
// sweep if I can't find in 5 sec or bail from find
|
|
} else if ( ((Missiontime - aip->submode_start_time) > i2f(5)) || (aip->submode_parm0 == SM_SF_BAIL) ) {
|
|
// begin sweep mode
|
|
aip->submode = SM_STEALTH_SWEEP;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->last_attack_time = Missiontime;
|
|
aip->submode_parm0 = SM_SS_SET_GOAL;
|
|
}
|
|
break;
|
|
|
|
case SM_STEALTH_SWEEP:
|
|
if ( !(aip->ai_flags[AI::AI_Flags::Stealth_pursuit]) || (ai_is_stealth_visible(Pl_objp, En_objp) == STEALTH_IN_FRUSTUM) ) {
|
|
aip->submode = SM_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->last_attack_time = Missiontime;
|
|
} else if ( (timestamp() - aip->stealth_last_visible_stamp) < 5000 ) {
|
|
// go back to find mode
|
|
aip->submode = SM_STEALTH_FIND;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->submode_parm0 = SM_SF_AHEAD;
|
|
} else if ( aip->submode_parm0 == SM_SS_DONE ) {
|
|
// set target objnum = -1
|
|
set_target_objnum(aip, -1);
|
|
|
|
// set submode to attack
|
|
aip->submode = SM_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->last_attack_time = Missiontime;
|
|
}
|
|
break;
|
|
|
|
case SM_ATTACK_FOREVER: // Engines blown, just attack.
|
|
break;
|
|
|
|
case AIS_CHASE_GLIDEATTACK:
|
|
//Glide attack lasts at least as long as it takes to turn around and fire for a couple seconds.
|
|
if (Missiontime - aip->submode_start_time > i2f((int)(sip->rotation_time.xyz.x) + 4 + static_rand_range(Missiontime >> 19, 0, 3))) {
|
|
aip->submode = SM_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->last_attack_time = Missiontime;
|
|
}
|
|
aip->last_attack_time = Missiontime;
|
|
break;
|
|
|
|
case AIS_CHASE_CIRCLESTRAFE:
|
|
//Break out of circle strafe if the target is too far, moving too fast, or we've been doing this for a while
|
|
if ((dist_to_enemy > CIRCLE_STRAFE_MAX_DIST + En_objp->radius) ||
|
|
(Missiontime - aip->submode_start_time > i2f(8)) ||
|
|
(En_objp->phys_info.speed > MAX(sip->max_vel.xyz.x, sip->max_vel.xyz.y) * 1.5)) {
|
|
aip->submode = SM_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->last_attack_time = Missiontime;
|
|
}
|
|
aip->last_attack_time = Missiontime;
|
|
break;
|
|
|
|
default:
|
|
aip->submode = SM_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->last_attack_time = Missiontime;
|
|
}
|
|
|
|
|
|
//Update time enemy near
|
|
//Ignore for stealth ships that can't be seen
|
|
//If we're trying to get away, recent counter
|
|
//Only do this if stalemate distance threshold > 0
|
|
if (aip->ai_stalemate_dist_thresh > 0.0f &&
|
|
dist_to_enemy < aip->ai_stalemate_dist_thresh &&
|
|
aip->submode != SM_GET_AWAY && aip->submode != AIS_CHASE_GLIDEATTACK && aip->submode != SM_FLY_AWAY &&
|
|
(!(aip->ai_flags[AI::AI_Flags::Stealth_pursuit]) || (ai_is_stealth_visible(Pl_objp, En_objp) == STEALTH_IN_FRUSTUM)))
|
|
{
|
|
aip->time_enemy_near += flFrametime;
|
|
}
|
|
else
|
|
{
|
|
aip->time_enemy_near *= (1.0f - flFrametime);
|
|
if (aip->time_enemy_near < 0.0f)
|
|
aip->time_enemy_near = 0.0f;
|
|
}
|
|
|
|
// Maybe fire primary weapon and update time_enemy_in_range
|
|
if (aip->mode != AIM_EVADE) {
|
|
if (dot_to_enemy > 0.95f - 0.5f * En_objp->radius/MAX(1.0f, En_objp->radius + dist_to_enemy)) {
|
|
aip->time_enemy_in_range += flFrametime;
|
|
|
|
// Chance of hitting ship is based on dot product of firing ship's forward vector with vector to ship
|
|
// and also the size of the target relative to distance to target.
|
|
if (dot_to_enemy > std::max(0.5f, 0.90f + aip->ai_accuracy / 10.0f - En_objp->radius / std::max(1.0f, dist_to_enemy))) {
|
|
|
|
ship *temp_shipp;
|
|
ship_weapon *tswp;
|
|
|
|
temp_shipp = &Ships[Pl_objp->instance];
|
|
tswp = &temp_shipp->weapons;
|
|
// if this is a ballistic weapon has_valid_ballistic_trajectory will stop us from firing if we dont have a trajectory
|
|
// will allow in all other cases
|
|
if ( tswp->num_primary_banks > 0 && has_valid_ballistic_trajectory) {
|
|
float scale;
|
|
Assert(tswp->current_primary_bank < tswp->num_primary_banks);
|
|
weapon_info *pwip = &Weapon_info[tswp->primary_bank_weapons[tswp->current_primary_bank]];
|
|
|
|
// Less likely to fire if far away and moving.
|
|
scale = pwip->max_speed/(En_objp->phys_info.speed + pwip->max_speed);
|
|
if (scale > 0.6f)
|
|
scale = (scale - 0.6f) * 1.5f;
|
|
else
|
|
scale = 0.0f;
|
|
float range_min = 0.0f;
|
|
float range_max = pwip->max_speed * (1.0f + scale);
|
|
if (aip->ai_profile_flags[AI::Profile_Flags::Use_actual_primary_range]) {
|
|
range_max = std::min({range_max, pwip->max_speed * pwip->lifetime, pwip->weapon_range});
|
|
range_min = pwip->weapon_min_range;
|
|
}
|
|
if ((dist_to_enemy < range_max) && (dist_to_enemy >= range_min)) {
|
|
if(ai_fire_primary_weapon(Pl_objp) == 1){
|
|
has_fired = 1;
|
|
}else{
|
|
has_fired = -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( tswp->num_secondary_banks > 0) {
|
|
|
|
// Don't fire secondaries at a protected ship.
|
|
if (!(En_objp->flags[Object::Object_Flags::Protected])) {
|
|
bool valid_secondary = ai_choose_secondary_weapon(Pl_objp, aip, En_objp);
|
|
int current_bank = tswp->current_secondary_bank;
|
|
|
|
if (current_bank > -1 && valid_secondary) {
|
|
weapon_info *swip = &Weapon_info[tswp->secondary_bank_weapons[current_bank]];
|
|
if (aip->ai_flags[AI::AI_Flags::Unload_secondaries]) {
|
|
if (timestamp_until(swp->next_secondary_fire_stamp[current_bank]) > swip->fire_wait*1000.0f) {
|
|
swp->next_secondary_fire_stamp[current_bank] = timestamp((int) (swip->fire_wait*1000.0f));
|
|
}
|
|
}
|
|
|
|
if (timestamp_elapsed(swp->next_secondary_fire_stamp[current_bank]) && weapon_target_satisfies_lock_restrictions(swip, En_objp)) {
|
|
if (current_bank >= 0) {
|
|
float firing_range;
|
|
|
|
if (swip->wi_flags[Weapon::Info_Flags::Local_ssm])
|
|
firing_range=swip->lssm_lock_range; //that should be enough
|
|
else if (swip->wi_flags[Weapon::Info_Flags::Bomb])
|
|
firing_range = MIN((swip->max_speed * swip->lifetime * 0.75f), swip->weapon_range);
|
|
else
|
|
{
|
|
float secondary_range_mult;
|
|
|
|
// Cyborg: need to check for division by zero, coverity 1523419
|
|
if (aip->ai_secondary_range_mult == FLT_MIN && (ai_maybe_autoscale(Num_ai_classes) > 1))
|
|
secondary_range_mult = (float)(Game_skill_level + 1 + (3 * ai_maybe_autoscale(aip->ai_class)/(ai_maybe_autoscale(Num_ai_classes) - 1)))/NUM_SKILL_LEVELS;
|
|
else if (aip->ai_secondary_range_mult == FLT_MIN)
|
|
secondary_range_mult = (float)(Game_skill_level + 2)/NUM_SKILL_LEVELS; // default to using the default retail class Captain out of the seven there are total when we don't have enough autoscale classes or just one regular class. (This simplifies to 2 + Game_skill_level / Num of skill levels)
|
|
else
|
|
secondary_range_mult = aip->ai_secondary_range_mult;
|
|
|
|
firing_range = MIN((swip->max_speed * swip->lifetime * secondary_range_mult), swip->weapon_range);
|
|
}
|
|
|
|
|
|
// reduce firing range in nebula
|
|
extern int Nebula_sec_range;
|
|
if ((The_mission.flags[Mission::Mission_Flags::Fullneb]) && Nebula_sec_range) {
|
|
firing_range *= 0.8f;
|
|
}
|
|
|
|
// If firing a spawn weapon, distance doesn't matter.
|
|
int spawn_fire = 0;
|
|
|
|
if (swip->wi_flags[Weapon::Info_Flags::Spawn]) {
|
|
int count;
|
|
|
|
count = num_nearby_fighters(iff_get_attackee_mask(obj_team(Pl_objp)), &Pl_objp->pos, 1000.0f);
|
|
|
|
if (count > 3)
|
|
spawn_fire = 1;
|
|
else if (count >= 1) {
|
|
float hull_percent = Pl_objp->hull_strength/temp_shipp->ship_max_hull_strength;
|
|
|
|
if (hull_percent < 0.01f)
|
|
hull_percent = 0.01f;
|
|
|
|
if (frand() < 0.25f/(30.0f*hull_percent) * count) // With timestamp below, this means could fire in 30 seconds if one enemy.
|
|
spawn_fire = 1;
|
|
}
|
|
}
|
|
|
|
if (spawn_fire || (dist_to_enemy < firing_range)) {
|
|
if (ai_fire_secondary_weapon(Pl_objp)) {
|
|
// Only if weapon was fired do we specify time until next fire. If not fired, done in ai_fire_secondary...
|
|
float t;
|
|
int current_bank_adjusted = MAX_SHIP_PRIMARY_BANKS + current_bank;
|
|
|
|
if ((aip->ai_flags[AI::AI_Flags::Unload_secondaries]) || (swip->burst_flags[Weapon::Burst_Flags::Fast_firing])) {
|
|
if (swip->burst_shots > swp->burst_counter[current_bank_adjusted]) {
|
|
t = swip->burst_delay;
|
|
swp->burst_counter[current_bank_adjusted]++;
|
|
} else {
|
|
t = swip->fire_wait;
|
|
if ((swip->burst_shots > 0) && (swip->burst_flags[Weapon::Burst_Flags::Random_length])) {
|
|
swp->burst_counter[current_bank_adjusted] = Random::next(swip->burst_shots);
|
|
} else {
|
|
swp->burst_counter[current_bank_adjusted] = 0;
|
|
}
|
|
swp->burst_seed[current_bank_adjusted] = Random::next();
|
|
}
|
|
} else {
|
|
if (swip->burst_shots > swp->burst_counter[current_bank_adjusted]) {
|
|
t = set_secondary_fire_delay(aip, temp_shipp, swip, true);
|
|
swp->burst_counter[current_bank_adjusted]++;
|
|
} else {
|
|
t = set_secondary_fire_delay(aip, temp_shipp, swip, false);
|
|
if ((swip->burst_shots > 0) && (swip->burst_flags[Weapon::Burst_Flags::Random_length])) {
|
|
swp->burst_counter[current_bank_adjusted] = Random::next(swip->burst_shots);
|
|
} else {
|
|
swp->burst_counter[current_bank_adjusted] = 0;
|
|
}
|
|
swp->burst_seed[current_bank_adjusted] = Random::next();
|
|
}
|
|
}
|
|
swp->next_secondary_fire_stamp[current_bank] = timestamp((int) (t*1000.0f));
|
|
}
|
|
} else {
|
|
swp->next_secondary_fire_stamp[current_bank] = timestamp(250);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (flFrametime < 1.0f)
|
|
aip->time_enemy_in_range *= (1.0f - flFrametime);
|
|
else
|
|
aip->time_enemy_in_range = 0;
|
|
}
|
|
} else {
|
|
if (flFrametime < 1.0f)
|
|
aip->time_enemy_in_range *= (1.0f - flFrametime);
|
|
else
|
|
aip->time_enemy_in_range = 0;
|
|
}
|
|
|
|
if(has_fired == -1){
|
|
ship_stop_fire_primary(Pl_objp);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Make the object *objp move so that the point *start moves towards the point *finish.
|
|
* @return distance.
|
|
*/
|
|
float dock_move_towards_point(object *objp, vec3d *start, vec3d *finish, float speed_scale, float other_obj_speed = 0.0f, rotating_dockpoint_info *rdinfo = NULL)
|
|
{
|
|
physics_info *pi = &objp->phys_info;
|
|
float dist; // dist to goal
|
|
vec3d v2g; // vector to goal
|
|
|
|
dist = vm_vec_dist_quick(start, finish);
|
|
if (dist > 0.0f) {
|
|
float speed;
|
|
|
|
dist = vm_vec_normalized_dir(&v2g, finish, start);
|
|
speed = fl_sqrt(dist) * speed_scale;
|
|
|
|
// Goober5000 - if we're on a rotating submodel and we're rotating with it, adjust velocity for rotation
|
|
if (rdinfo && (rdinfo->submodel >= 0))
|
|
{
|
|
speed *= 1.25f;
|
|
|
|
switch (rdinfo->dock_mode)
|
|
{
|
|
case DOA_APPROACH:
|
|
// ramp submodel's linear velocity according to distance
|
|
speed += (rdinfo->submodel_r * rdinfo->submodel_w) / (dist);
|
|
break;
|
|
|
|
case DOA_DOCK:
|
|
case DOA_UNDOCK_1:
|
|
// use docker's linear velocity and don't ramp
|
|
speed += (vm_vec_dist(&rdinfo->submodel_pos, &objp->pos) * rdinfo->submodel_w);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (other_obj_speed < MAX_REPAIR_SPEED*0.75f)
|
|
speed += other_obj_speed;
|
|
else
|
|
speed += MAX_REPAIR_SPEED*0.75f;
|
|
|
|
vm_vec_copy_scale(&pi->desired_vel, &v2g, speed);
|
|
} else
|
|
vm_vec_zero(&pi->desired_vel);
|
|
|
|
return dist;
|
|
}
|
|
|
|
// Set the orientation in the global reference frame for an object to attain
|
|
// to dock with another object. Resultant global matrix returned in dom.
|
|
// Revised by Goober5000
|
|
void set_goal_dock_orient(matrix *dom, matrix *docker_dock_orient, matrix *docker_orient, matrix *dockee_dock_orient, matrix *dockee_orient)
|
|
{
|
|
vec3d fvec, uvec;
|
|
matrix m1, m2, m3;
|
|
|
|
// Compute the global orientation of the dockee's docking bay.
|
|
|
|
// get the rotated (local) fvec
|
|
vm_vec_rotate(&fvec, &dockee_dock_orient->vec.fvec, dockee_orient);
|
|
vm_vec_negate(&fvec);
|
|
|
|
// get the rotated (local) uvec
|
|
vm_vec_rotate(&uvec, &dockee_dock_orient->vec.uvec, dockee_orient);
|
|
|
|
// create a rotation matrix
|
|
vm_vector_2_matrix(&m1, &fvec, &uvec, nullptr); // we need to normalize the input vectors here to prevent floating point errors from compounding
|
|
|
|
// get the global orientation
|
|
vm_matrix_x_matrix(&m3, dockee_orient, &m1);
|
|
|
|
// Compute the matrix given by the docker's docking bay.
|
|
|
|
// get the rotated (local) fvec
|
|
vm_vec_rotate(&fvec, &docker_dock_orient->vec.fvec, docker_orient);
|
|
|
|
// get the rotated (local) uvec
|
|
vm_vec_rotate(&uvec, &docker_dock_orient->vec.uvec, docker_orient);
|
|
|
|
// create a rotation matrix
|
|
vm_vector_2_matrix(&m2, &fvec, &uvec, nullptr); // we need to normalize the input vectors here to prevent floating point errors from compounding
|
|
|
|
// Pre-multiply the orientation of the source object (docker_orient) by the transpose
|
|
// of the docking bay's orientation, ie unrotate the source object's matrix.
|
|
vm_transpose(&m2);
|
|
vm_matrix_x_matrix(dom, &m3, &m2);
|
|
}
|
|
|
|
// Goober5000
|
|
// Return the moving submodel on which is mounted the specified dockpoint, or -1 for none.
|
|
int find_parent_moving_submodel(polymodel *pm, int dock_index)
|
|
{
|
|
Assertion(pm != nullptr, "pm cannot be null!");
|
|
Assertion(dock_index >= 0 && dock_index < pm->n_docks, "for model %s, dock_index %d must be >= 0 and < %d!", pm->filename, dock_index, pm->n_docks);
|
|
int path_num, submodel;
|
|
|
|
// if this is explicitly defined, then we're done
|
|
if (pm->docking_bays[dock_index].parent_submodel >= 0)
|
|
{
|
|
return pm->docking_bays[dock_index].parent_submodel;
|
|
}
|
|
|
|
// otherwise infer the submodel using the path...
|
|
|
|
// make sure we have a spline path to check against before going any further
|
|
if (pm->docking_bays[dock_index].num_spline_paths <= 0)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
// find a path for this dockpoint (c.f. ai_return_path_num_from_dockbay)
|
|
path_num = pm->docking_bays[dock_index].splines[0];
|
|
|
|
// path must exist
|
|
if ((path_num >= 0) && (path_num < pm->n_paths))
|
|
{
|
|
// find the submodel for the path for this dockpoint
|
|
submodel = pm->paths[path_num].parent_submodel;
|
|
|
|
// submodel must exist and must move
|
|
if ((submodel >= 0) && (submodel < pm->n_models) && ((pm->submodel[submodel].rotation_type >= 0) || (pm->submodel[submodel].translation_type >= 0)))
|
|
{
|
|
return submodel;
|
|
}
|
|
}
|
|
|
|
// if path doesn't exist or the submodel doesn't exist or the submodel doesn't move
|
|
return -1;
|
|
}
|
|
|
|
// Goober5000
|
|
void find_adjusted_dockpoint_info(vec3d *global_dock_point, matrix *global_dock_orient, object *objp, polymodel *pm, int submodel, int dock_index)
|
|
{
|
|
Assertion(global_dock_point != nullptr && global_dock_orient != nullptr && objp != nullptr && pm != nullptr, "arguments cannot be null!");
|
|
Assertion(dock_index >= 0 && dock_index < pm->n_docks, "for model %s, dock_index %d must be >= 0 and < %d!", pm->filename, dock_index, pm->n_docks);
|
|
|
|
vec3d fvec, uvec;
|
|
|
|
// are we basing this off a moving submodel?
|
|
if (submodel >= 0)
|
|
{
|
|
vec3d submodel_offset;
|
|
vec3d local_p0, local_p1;
|
|
|
|
auto shipp = &Ships[objp->instance];
|
|
auto pmi = model_get_instance(shipp->model_instance_num);
|
|
Assert(pmi->model_num == pm->id);
|
|
|
|
// calculate the dockpoint locations relative to the submodel in its original position
|
|
model_find_submodel_offset(&submodel_offset, pm, submodel);
|
|
vm_vec_sub(&local_p0, &pm->docking_bays[dock_index].pnt[0], &submodel_offset);
|
|
vm_vec_sub(&local_p1, &pm->docking_bays[dock_index].pnt[1], &submodel_offset);
|
|
|
|
// find the dynamic positions of the dockpoints
|
|
vec3d global_p0, global_p1;
|
|
model_instance_local_to_global_point(&global_p0, &local_p0, pm, pmi, submodel, &objp->orient, &objp->pos);
|
|
model_instance_local_to_global_point(&global_p1, &local_p1, pm, pmi, submodel, &objp->orient, &objp->pos);
|
|
vm_vec_avg(global_dock_point, &global_p0, &global_p1);
|
|
vm_vec_normalized_dir(&uvec, &global_p1, &global_p0);
|
|
|
|
// find the normal of the first dockpoint
|
|
model_instance_local_to_global_dir(&fvec, &pm->docking_bays[dock_index].norm[0], pm, pmi, submodel, &objp->orient);
|
|
|
|
vm_vector_2_matrix_norm(global_dock_orient, &fvec, &uvec);
|
|
}
|
|
// use the static dockpoints
|
|
else
|
|
{
|
|
vec3d local_point, local_uvec, local_fvec;
|
|
vm_vec_avg(&local_point, &pm->docking_bays[dock_index].pnt[0], &pm->docking_bays[dock_index].pnt[1]);
|
|
vm_vec_normalized_dir(&local_uvec, &pm->docking_bays[dock_index].pnt[1], &pm->docking_bays[dock_index].pnt[0]);
|
|
local_fvec = pm->docking_bays[dock_index].norm[0];
|
|
|
|
vm_vec_unrotate(global_dock_point, &local_point, &objp->orient);
|
|
vm_vec_unrotate(&uvec, &local_uvec, &objp->orient);
|
|
vm_vec_unrotate(&fvec, &local_fvec, &objp->orient);
|
|
|
|
vm_vec_add2(global_dock_point, &objp->pos);
|
|
vm_vector_2_matrix_norm(global_dock_orient, &fvec, &uvec);
|
|
}
|
|
}
|
|
|
|
|
|
// Make docker_objp dock with dockee_objp
|
|
// Returns distance to goal, defined as distance between corresponding dock points, plus 10.0f * rotational velocity vector (DOA_DOCK only)
|
|
// DOA_APPROACH means approach point aip->path_cur
|
|
// DOA_DOCK means dock
|
|
// DOA_UNDOCK_1 means undock, moving to point nearest dock bay
|
|
// DOA_UNDOCK_2 means undock, moving to point nearest dock bay and facing away from ship
|
|
// DOA_UNDOCK_3 means undock, moving directly away from ship
|
|
// DOA_DOCK_STAY means rigidly maintain position in dock bay.
|
|
float dock_orient_and_approach(object *docker_objp, int docker_index, object *dockee_objp, int dockee_index, int dock_mode, rotating_dockpoint_info *rdinfo)
|
|
{
|
|
ship_info *sip0, *sip1;
|
|
polymodel *pm0, *pm1;
|
|
ai_info *aip;
|
|
matrix dom, out_orient;
|
|
vec3d docker_point, dockee_point;
|
|
float fdist = UNINITIALIZED_VALUE;
|
|
|
|
aip = &Ai_info[Ships[docker_objp->instance].ai_index];
|
|
|
|
docker_objp->phys_info.linear_thrust.xyz.z = 0.0f; // Kill thrust so we don't have a sputtering thruster.
|
|
|
|
// Goober5000 - moved out here to save calculations
|
|
if (dock_mode != DOA_DOCK_STAY)
|
|
if (!Ships[docker_objp->instance].flags[Ship::Ship_Flags::Maneuver_despite_engines] && ship_subsystems_blown(&Ships[docker_objp->instance], SUBSYSTEM_ENGINE))
|
|
return 9999.9f;
|
|
|
|
// If dockee has moved much, then path will be recreated.
|
|
// Might need to change state if moved too far.
|
|
if ((dock_mode != DOA_DOCK_STAY) && (dock_mode != DOA_DOCK)) {
|
|
maybe_recreate_path(docker_objp, aip, 0);
|
|
}
|
|
|
|
sip0 = &Ship_info[Ships[docker_objp->instance].ship_info_index];
|
|
sip1 = &Ship_info[Ships[dockee_objp->instance].ship_info_index];
|
|
pm0 = model_get( sip0->model_num );
|
|
pm1 = model_get( sip1->model_num );
|
|
|
|
Assert( docker_index >= 0 );
|
|
Assert( dockee_index >= 0 );
|
|
|
|
Assert(pm0->docking_bays[docker_index].num_slots == 2);
|
|
Assert(pm1->docking_bays[dockee_index].num_slots == 2);
|
|
|
|
|
|
// Goober5000 - check if we're attached to a moving submodel
|
|
int dockee_moving_submodel = find_parent_moving_submodel(pm1, dockee_index);
|
|
|
|
matrix docker_dock_orient, dockee_dock_orient;
|
|
// Goober5000 - move docking points with submodels if necessary, for both docker and dockee
|
|
find_adjusted_dockpoint_info(&docker_point, &docker_dock_orient, docker_objp, pm0, -1, docker_index);
|
|
find_adjusted_dockpoint_info(&dockee_point, &dockee_dock_orient, dockee_objp, pm1, dockee_moving_submodel, dockee_index);
|
|
|
|
// Goober5000
|
|
vec3d submodel_pos = ZERO_VECTOR;
|
|
float submodel_radius = 0.0f;
|
|
float submodel_omega = 0.0f;
|
|
if ((dockee_moving_submodel >= 0) && (dock_mode != DOA_DOCK_STAY))
|
|
{
|
|
vec3d submodel_offset;
|
|
|
|
// get submodel center
|
|
model_find_submodel_offset(&submodel_offset, pm1, dockee_moving_submodel);
|
|
vm_vec_add(&submodel_pos, &dockee_objp->pos, &submodel_offset);
|
|
|
|
polymodel_instance *pmi1 = model_get_instance(Ships[dockee_objp->instance].model_instance_num);
|
|
|
|
// get angular velocity of dockpoint
|
|
submodel_omega = pmi1->submodel[dockee_moving_submodel].current_turn_rate;
|
|
|
|
// get radius to dockpoint
|
|
submodel_radius = vm_vec_dist(&submodel_pos, &dockee_point);
|
|
}
|
|
|
|
// Goober5000
|
|
rotating_dockpoint_info rdinfo_buf;
|
|
if (rdinfo == NULL)
|
|
rdinfo = &rdinfo_buf;
|
|
|
|
rdinfo->docker_point = docker_point;
|
|
rdinfo->dockee_point = dockee_point;
|
|
rdinfo->dock_mode = dock_mode;
|
|
rdinfo->submodel = dockee_moving_submodel;
|
|
rdinfo->submodel_pos = submodel_pos;
|
|
rdinfo->submodel_r = submodel_radius;
|
|
rdinfo->submodel_w = submodel_omega;
|
|
|
|
|
|
float speed_scale = 1.0f;
|
|
if (sip0->flags[Ship::Info_Flags::Support]) {
|
|
speed_scale = 3.0f;
|
|
}
|
|
|
|
switch (dock_mode) {
|
|
case DOA_APPROACH:
|
|
{
|
|
vec3d *goal_point;
|
|
|
|
// Compute the desired global orientation matrix for the docker's station.
|
|
// That is, the normal vector of the docking station must be the same as the
|
|
// forward vector and the vector between its two points must be the uvec.
|
|
set_goal_dock_orient(&dom, &docker_dock_orient, &docker_objp->orient, &dockee_dock_orient, &dockee_objp->orient);
|
|
|
|
// Compute new orientation matrix and update rotational velocity.
|
|
vec3d omega_in, omega_out, vel_limit, acc_limit;
|
|
float tdist, mdist, ss1;
|
|
|
|
omega_in = docker_objp->phys_info.rotvel;
|
|
vel_limit = docker_objp->phys_info.max_rotvel;
|
|
vm_vec_copy_scale(&acc_limit, &vel_limit, 0.3f);
|
|
|
|
if (sip0->flags[Ship::Info_Flags::Support])
|
|
vm_vec_scale(&acc_limit, 2.0f);
|
|
|
|
if(Framerate_independent_turning)
|
|
ai_compensate_for_retail_turning(&vel_limit, &acc_limit, docker_objp->phys_info.rotdamp, false);
|
|
|
|
// true at end of line prevent overshoot
|
|
vm_angular_move_matrix(&dom, &docker_objp->orient, &omega_in, flFrametime, &out_orient, &omega_out, &vel_limit, &acc_limit, false, true);
|
|
|
|
if (Framerate_independent_turning) {
|
|
docker_objp->phys_info.desired_rotvel = omega_out;
|
|
docker_objp->phys_info.ai_desired_orient = out_orient;
|
|
}
|
|
else {
|
|
docker_objp->phys_info.rotvel = omega_out;
|
|
docker_objp->orient = out_orient;
|
|
}
|
|
|
|
// Translate towards goal and note distance to goal.
|
|
goal_point = &Path_points[aip->path_cur].pos;
|
|
mdist = ai_matrix_dist(&docker_objp->orient, &dom);
|
|
tdist = vm_vec_dist_quick(&docker_objp->pos, goal_point);
|
|
|
|
// If translation is badly lagging rotation, speed up translation.
|
|
if (mdist > 0.1f) {
|
|
ss1 = tdist/(10.0f * mdist);
|
|
if (ss1 > 2.0f)
|
|
ss1 = 2.0f;
|
|
} else
|
|
ss1 = 2.0f;
|
|
|
|
// if we're docking to a moving submodel, speed up translation
|
|
if (dockee_moving_submodel >= 0)
|
|
ss1 = 2.0f;
|
|
|
|
speed_scale *= 1.0f + ss1;
|
|
|
|
fdist = dock_move_towards_point(docker_objp, &docker_objp->pos, goal_point, speed_scale, dockee_objp->phys_info.speed, rdinfo);
|
|
|
|
// Note, we're interested in distance from goal, so if we're still turning, bash that into return value.
|
|
fdist += 2.0f * mdist;
|
|
|
|
break;
|
|
}
|
|
case DOA_DOCK:
|
|
case DOA_DOCK_STAY:
|
|
{
|
|
// Compute the desired global orientation matrix for the docker's station.
|
|
// That is, the normal vector of the docking station must be the same as the
|
|
// forward vector and the vector between its two points must be the uvec.
|
|
set_goal_dock_orient(&dom, &docker_dock_orient, &docker_objp->orient, &dockee_dock_orient, &dockee_objp->orient);
|
|
|
|
// Compute distance between dock bay points.
|
|
if (dock_mode == DOA_DOCK) {
|
|
vec3d omega_in, omega_out, vel_limit, acc_limit;
|
|
|
|
// Compute new orientation matrix and update rotational velocity.
|
|
omega_in = docker_objp->phys_info.rotvel;
|
|
vel_limit = docker_objp->phys_info.max_rotvel;
|
|
vm_vec_copy_scale(&acc_limit, &vel_limit, 0.3f);
|
|
|
|
if (sip0->flags[Ship::Info_Flags::Support])
|
|
vm_vec_scale(&acc_limit, 2.0f);
|
|
|
|
if(Framerate_independent_turning)
|
|
ai_compensate_for_retail_turning(&vel_limit, &acc_limit, docker_objp->phys_info.rotdamp, false);
|
|
|
|
vm_angular_move_matrix(&dom, &docker_objp->orient, &omega_in, flFrametime, &out_orient, &omega_out, &vel_limit, &acc_limit, false);
|
|
|
|
if (Framerate_independent_turning) {
|
|
docker_objp->phys_info.desired_rotvel = omega_out;
|
|
docker_objp->phys_info.ai_desired_orient = out_orient;
|
|
}
|
|
else {
|
|
docker_objp->phys_info.rotvel = omega_out;
|
|
docker_objp->orient = out_orient;
|
|
}
|
|
|
|
fdist = dock_move_towards_point(docker_objp, &docker_point, &dockee_point, speed_scale, dockee_objp->phys_info.speed, rdinfo);
|
|
|
|
// Note, we're interested in distance from goal, so if we're still turning, bash that into return value.
|
|
fdist += 10.0f * vm_vec_mag_quick(&omega_out);
|
|
} else {
|
|
Assert(dock_mode == DOA_DOCK_STAY);
|
|
matrix temp, m_offset;
|
|
vec3d origin_docker_point, adjusted_docker_point, v_offset;
|
|
|
|
// find out the rotation matrix that will get us from the old to the new rotation
|
|
// (see Mantis #2787)
|
|
vm_copy_transpose(&temp, &docker_objp->orient);
|
|
vm_matrix_x_matrix(&m_offset, &temp, &dom);
|
|
|
|
// now find out the new docker point after being adjusted for the new orientation
|
|
vm_vec_sub(&origin_docker_point, &docker_point, &docker_objp->pos);
|
|
vm_vec_rotate(&adjusted_docker_point, &origin_docker_point, &m_offset);
|
|
vm_vec_add2(&adjusted_docker_point, &docker_objp->pos);
|
|
|
|
// find the vector that will get us from the old to the new position
|
|
vm_vec_sub(&v_offset, &dockee_point, &adjusted_docker_point);
|
|
|
|
// now set the new rotation and move to the new position
|
|
docker_objp->orient = dom;
|
|
vm_vec_add2(&docker_objp->pos, &v_offset);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case DOA_UNDOCK_1:
|
|
{
|
|
// Undocking.
|
|
// Move to point on dock path nearest to dock station.
|
|
Assert(aip->path_length >= 2);
|
|
fdist = dock_move_towards_point(docker_objp, &docker_objp->pos, &Path_points[aip->path_start + aip->path_length-2].pos, speed_scale, 0.0f, rdinfo);
|
|
|
|
break;
|
|
}
|
|
case DOA_UNDOCK_2:
|
|
{
|
|
// Undocking.
|
|
// Move to point on dock path nearest to dock station and orient away from big ship.
|
|
int desired_index;
|
|
|
|
Assert(aip->path_length >= 2);
|
|
|
|
desired_index = aip->path_length-2;
|
|
fdist = dock_move_towards_point(docker_objp, &docker_objp->pos, &Path_points[aip->path_start + desired_index].pos, speed_scale, 0.0f, rdinfo);
|
|
|
|
break;
|
|
}
|
|
case DOA_UNDOCK_3:
|
|
{
|
|
vec3d goal_point;
|
|
float dist, goal_dist;
|
|
vec3d away_vec;
|
|
|
|
goal_dist = docker_objp->radius + dockee_objp->radius + 25.0f;
|
|
|
|
dist = vm_vec_normalized_dir(&away_vec, &docker_objp->pos, &dockee_objp->pos);
|
|
vm_vec_scale_add(&goal_point, &dockee_objp->pos, &away_vec, goal_dist);
|
|
if (vm_vec_dist_quick(&goal_point, &dockee_objp->pos) < vm_vec_dist_quick(&docker_objp->pos, &dockee_objp->pos))
|
|
fdist = 0.0f;
|
|
else
|
|
{
|
|
float dot, accel;
|
|
ai_turn_towards_vector(&goal_point, docker_objp, nullptr, nullptr, 0.0f, 0);
|
|
|
|
dot = vm_vec_dot(&docker_objp->orient.vec.fvec, &away_vec);
|
|
accel = 0.1f;
|
|
if (dot > accel)
|
|
accel = dot;
|
|
if (dist > goal_dist/2)
|
|
accel *= 1.2f - 0.5f*goal_dist/dist;
|
|
|
|
accelerate_ship(aip, accel);
|
|
fdist = vm_vec_dist_quick(&docker_objp->pos, &goal_point);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return fdist;
|
|
}
|
|
|
|
/**
|
|
* Given an object number, return the number of ships attacking it.
|
|
*/
|
|
int num_ships_attacking(int target_objnum)
|
|
{
|
|
object *attacking_objp;
|
|
ai_info *attacking_aip;
|
|
ship_obj *so;
|
|
int count = 0;
|
|
int target_team = obj_team(&Objects[target_objnum]);
|
|
|
|
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) )
|
|
{
|
|
attacking_objp = &Objects[so->objnum];
|
|
if (attacking_objp->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
if (attacking_objp->instance < 0)
|
|
continue;
|
|
|
|
attacking_aip = &Ai_info[Ships[attacking_objp->instance].ai_index];
|
|
|
|
// don't count instructor
|
|
if (is_training_mission() && is_instructor(attacking_objp))
|
|
continue; // Goober5000 10/06/2005 changed from break
|
|
|
|
if (iff_x_attacks_y(Ships[attacking_objp->instance].team, target_team))
|
|
{
|
|
if (attacking_aip->target_objnum == target_objnum)
|
|
{
|
|
if ( ((Game_mode & GM_MULTIPLAYER) && (attacking_objp->flags[Object::Object_Flags::Player_ship]))
|
|
|| (attacking_aip->mode == AIM_CHASE) )
|
|
{
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
// For all objects attacking object #objnum, remove the one that is farthest away.
|
|
// Do this by resuming previous behavior, if any. If not, set target_objnum to -1.
|
|
void remove_farthest_attacker(int objnum)
|
|
{
|
|
object *objp, *objp2, *farthest_objp;
|
|
ship_obj *so;
|
|
float farthest_dist;
|
|
|
|
objp2 = &Objects[objnum];
|
|
|
|
farthest_dist = 9999999.9f;
|
|
farthest_objp = NULL;
|
|
|
|
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
|
|
objp = &Objects[so->objnum];
|
|
if (objp->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
if ( !(objp->flags[Object::Object_Flags::Player_ship])) {
|
|
if (objp->instance != -1) {
|
|
ai_info *aip2;
|
|
|
|
aip2 = &Ai_info[Ships[objp->instance].ai_index];
|
|
|
|
if ((aip2->mode == AIM_CHASE) && (aip2->target_objnum == objnum)) {
|
|
if (iff_x_attacks_y(Ships[objp->instance].team, Ships[objp2->instance].team)) {
|
|
float dist;
|
|
|
|
dist = vm_vec_dist_quick(&objp->pos, &objp2->pos);
|
|
if (dist < farthest_dist) {
|
|
farthest_dist = dist;
|
|
farthest_objp = objp;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (farthest_objp != NULL) {
|
|
ai_info *aip;
|
|
Assert(farthest_objp->type == OBJ_SHIP);
|
|
Assert((farthest_objp->instance > -1) && (farthest_objp->instance < MAX_SHIPS));
|
|
Assert(Ships[farthest_objp->instance].ai_index > -1);
|
|
|
|
aip = &Ai_info[Ships[farthest_objp->instance].ai_index];
|
|
|
|
if (!maybe_resume_previous_mode(Pl_objp, aip))
|
|
{
|
|
// If already ignoring something under player's orders, don't ignore current target.
|
|
if ((aip->ignore_objnum == UNUSED_OBJNUM) || (aip->ai_flags[AI::AI_Flags::Temporary_ignore]))
|
|
{
|
|
aip->ignore_objnum = aip->target_objnum;
|
|
aip->ignore_signature = Objects[aip->target_objnum].signature;
|
|
aip->ai_flags.set(AI::AI_Flags::Temporary_ignore);
|
|
aip->ignore_expire_timestamp = timestamp(Random::next(20, 29) * 1000); // OK to attack again in 20 to 29 seconds.
|
|
}
|
|
aip->target_objnum = -1;
|
|
ai_do_default_behavior(farthest_objp);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Maybe limit the number of attackers on attack_objnum. For now, only limit attackers
|
|
// in attacked_objnum is the player
|
|
// input: attacked_objnum => object index for ship we want to limit attacks on
|
|
//
|
|
// exit: 1 => num attackers exceeds maximum, abort
|
|
// 0 => removed the farthest attacker
|
|
// -1 => nothing was done
|
|
int ai_maybe_limit_attackers(int attacked_objnum)
|
|
{
|
|
int rval=-1;
|
|
|
|
if ( Objects[attacked_objnum].flags[Object::Object_Flags::Player_ship]) {
|
|
int num_attacking;
|
|
num_attacking = num_ships_attacking(attacked_objnum);
|
|
|
|
if (num_attacking == The_mission.ai_profile->max_attackers[Game_skill_level]) {
|
|
remove_farthest_attacker(attacked_objnum);
|
|
rval=0;
|
|
} else if (num_attacking > The_mission.ai_profile->max_attackers[Game_skill_level]) {
|
|
rval=1;
|
|
}
|
|
}
|
|
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* Object being guarded by object *guard_objp was hit by object *hitter_objp
|
|
*/
|
|
void guard_object_was_hit(object *guard_objp, object *hitter_objp)
|
|
{
|
|
int hitter_objnum;
|
|
ai_info *aip;
|
|
|
|
aip = &Ai_info[Ships[guard_objp->instance].ai_index];
|
|
|
|
if (guard_objp == hitter_objp) {
|
|
return;
|
|
}
|
|
|
|
if (guard_objp->type == OBJ_GHOST || hitter_objp->type == OBJ_GHOST)
|
|
return;
|
|
|
|
if (aip->ai_flags[AI::AI_Flags::No_dynamic]) // Not allowed to pursue dynamic goals. So, why are we guarding?
|
|
return;
|
|
|
|
Assert( (hitter_objp->type == OBJ_SHIP) || (hitter_objp->type == OBJ_ASTEROID) || (hitter_objp->type == OBJ_WEAPON) );
|
|
|
|
hitter_objnum = OBJ_INDEX(hitter_objp);
|
|
|
|
if ( hitter_objp->type == OBJ_SHIP ) {
|
|
// If the hitter object is the ignore object, don't attack it.
|
|
if (is_ignore_object(aip, OBJ_INDEX(hitter_objp)))
|
|
return;
|
|
|
|
// If the hitter is protected, ignore it
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Guards_ignore_protected_attackers] &&
|
|
hitter_objp->flags[Object::Object_Flags::Protected])
|
|
return;
|
|
|
|
// If hitter is on same team as me, don't attack him.
|
|
if (Ships[guard_objp->instance].team == Ships[hitter_objp->instance].team)
|
|
return;
|
|
|
|
// limit the number of ships attacking hitter_objnum (for now, only if hitter_objnum is player)
|
|
if ( ai_maybe_limit_attackers(hitter_objnum) == 1 ) {
|
|
return;
|
|
}
|
|
|
|
// don't attack if you can't see it
|
|
if ( awacs_get_level(hitter_objp, &Ships[aip->shipnum], true) < 1.0f ) {
|
|
// if it's stealthed and visible, but not targetable, ok to attack.
|
|
if ( is_object_stealth_ship(hitter_objp) ) {
|
|
if ( ai_is_stealth_visible(guard_objp, hitter_objp) != STEALTH_IN_FRUSTUM ) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (aip->target_objnum == -1) {
|
|
aip->ok_to_target_timestamp = timestamp(0);
|
|
}
|
|
|
|
if ((aip->submode == AIS_GUARD_PATROL) || (aip->submode == AIS_GUARD_STATIC)) {
|
|
|
|
if ( hitter_objp->type == OBJ_SHIP ) {
|
|
if (!(Ship_info[Ships[guard_objp->instance].ship_info_index].is_small_ship())) {
|
|
return;
|
|
}
|
|
|
|
// limit the number of ships attacking hitter_objnum (for now, only if hitter_objnum is player)
|
|
if ( ai_maybe_limit_attackers(hitter_objnum) == 1 ) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (aip->target_objnum != hitter_objnum) {
|
|
aip->aspect_locked_time = 0.0f;
|
|
}
|
|
|
|
aip->ok_to_target_timestamp = timestamp(0);
|
|
|
|
set_target_objnum(aip, hitter_objnum);
|
|
aip->previous_mode = AIM_GUARD;
|
|
aip->previous_submode = aip->submode;
|
|
aip->mode = AIM_CHASE;
|
|
aip->submode = SM_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->active_goal = AI_ACTIVE_GOAL_DYNAMIC;
|
|
} else if (aip->previous_mode == AIM_GUARD) {
|
|
if (aip->target_objnum == -1) {
|
|
|
|
if ( hitter_objp->type == OBJ_SHIP ) {
|
|
// limit the number of ships attacking hitter_objnum (for now, only if hitter_objnum is player)
|
|
if ( ai_maybe_limit_attackers(hitter_objnum) == 1 ) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
set_target_objnum(aip, hitter_objnum);
|
|
aip->mode = AIM_CHASE;
|
|
aip->submode = SM_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->active_goal = AI_ACTIVE_GOAL_DYNAMIC;
|
|
} else {
|
|
// This section used to be a lot simpler, but some "invalid" object types were probably getting into num_ships_attacking
|
|
// below, so we have to manually return for some object types and make sure to assert for other object types
|
|
// that are symptomatic of a larger problem with the engine.
|
|
|
|
// This just checks to see that the target was a valid type. If it is not one of these, something really
|
|
// could be drastically wrong in the engine.
|
|
Assertion(Objects[aip->target_objnum].type == OBJ_SHIP || Objects[aip->target_objnum].type == OBJ_WEAPON || Objects[aip->target_objnum].type == OBJ_DEBRIS || Objects[aip->target_objnum].type == OBJ_ASTEROID || Objects[aip->target_objnum].type == OBJ_WAYPOINT,
|
|
"This function just discovered that %s has an invalid target object type of %d. This is bad. Please report!",
|
|
Ships[aip->shipnum].ship_name, Objects[aip->target_objnum].type);
|
|
|
|
if (!(Objects[aip->target_objnum].type == OBJ_SHIP || Objects[aip->target_objnum].type == OBJ_WEAPON || Objects[aip->target_objnum].type == OBJ_ASTEROID || Objects[aip->target_objnum].type == OBJ_DEBRIS)){
|
|
// it's probably a valid target, but retail would not count those cases, so just return.
|
|
return;
|
|
}
|
|
|
|
int num_attacking_cur = num_ships_attacking(aip->target_objnum);
|
|
|
|
if (num_attacking_cur > 1) {
|
|
int num_attacking_new = num_ships_attacking(hitter_objnum);
|
|
|
|
if (num_attacking_new < num_attacking_cur) {
|
|
|
|
if ( hitter_objp->type == OBJ_SHIP ) {
|
|
// limit the number of ships attacking hitter_objnum (for now, only if hitter_objnum is player)
|
|
if ( ai_maybe_limit_attackers(hitter_objnum) == 1 ) {
|
|
return;
|
|
}
|
|
}
|
|
set_target_objnum(aip, OBJ_INDEX(hitter_objp));
|
|
aip->mode = AIM_CHASE;
|
|
aip->submode = SM_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->active_goal = AI_ACTIVE_GOAL_DYNAMIC;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ship object *hit_objp was hit by ship object *hitter_objp.
|
|
// See if anyone is guarding hit_objp and, if so, do something useful.
|
|
void maybe_update_guard_object(object *hit_objp, object *hitter_objp)
|
|
{
|
|
object *objp;
|
|
ship_obj *so;
|
|
|
|
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
|
|
objp = &Objects[so->objnum];
|
|
if (objp->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
if (objp->instance != -1) {
|
|
ai_info *aip;
|
|
aip = &Ai_info[Ships[objp->instance].ai_index];
|
|
|
|
if ((aip->mode == AIM_GUARD) || (aip->active_goal == AI_ACTIVE_GOAL_DYNAMIC)) {
|
|
// Only attack ship if allowed to
|
|
ship *eshipp = &Ships[hitter_objp->instance];
|
|
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]))) {
|
|
if (aip->guard_objnum == OBJ_INDEX(hit_objp)) {
|
|
guard_object_was_hit(objp, hitter_objp);
|
|
} else if ((aip->guard_wingnum != -1) && (aip->guard_wingnum == Ships[hit_objp->instance].wingnum)) {
|
|
guard_object_was_hit(objp, hitter_objp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find the closest hostile mine threatening 'against_objp', ranked by distance from 'from_objp'.
|
|
// If 'against_objp' is null, defaults to 'from_objp' (i.e. find mines threatening the searcher itself).
|
|
// imminent_only: also requires the mine to be within proximity_radius * 1.5 of against_objp;
|
|
// used to preempt a current target when the mine is about to detonate.
|
|
static object *ai_find_nearby_mine_threat(object *from_objp, object *against_objp = nullptr, bool imminent_only = false)
|
|
{
|
|
if (against_objp == nullptr)
|
|
against_objp = from_objp;
|
|
|
|
object *closest_mine = nullptr;
|
|
float closest_dist_from = std::numeric_limits<float>::max();
|
|
|
|
for (const auto *mo : list_range(&Missile_obj_list)) {
|
|
Assert(mo->objnum >= 0 && mo->objnum < MAX_OBJECTS);
|
|
object *mine_objp = &Objects[mo->objnum];
|
|
if (mine_objp->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
weapon *wp = &Weapons[mine_objp->instance];
|
|
weapon_info *wip = &Weapon_info[wp->weapon_info_index];
|
|
|
|
if (!wip->is_mine())
|
|
continue;
|
|
|
|
// Only engage destructible mines
|
|
if (!wip->wi_flags[Weapon::Info_Flags::Fighter_Interceptable])
|
|
continue;
|
|
|
|
// Only engage mines hostile to the threatened ship
|
|
if (!iff_x_attacks_y(wp->team, obj_team(against_objp)))
|
|
continue;
|
|
|
|
float dist_against = vm_vec_dist(&mine_objp->pos, &against_objp->pos);
|
|
|
|
// Mine must be within its own targetable range of the threatened ship
|
|
if (dist_against > wip->mine_targetable_range)
|
|
continue;
|
|
|
|
// Imminent gate: only consider mines about to detonate on the threatened ship.
|
|
// proximity_radius is always > 0 for mines (parser enforces).
|
|
if (imminent_only && dist_against > wip->proximity_radius * 1.5f)
|
|
continue;
|
|
|
|
float dist_from = (from_objp == against_objp) ? dist_against : vm_vec_dist(&mine_objp->pos, &from_objp->pos);
|
|
if (dist_from < closest_dist_from) {
|
|
closest_dist_from = dist_from;
|
|
closest_mine = mine_objp;
|
|
}
|
|
}
|
|
|
|
return closest_mine;
|
|
}
|
|
|
|
// Scan the missile list for a threat to guarded_objp and, if one is found, target it with guarding_objp.
|
|
// Bombs homing on guarded_objp take priority; if none are found, fall back to the closest hostile mine
|
|
// threatening guarded_objp (mines are stationary so they can't home, hence the lower priority).
|
|
// Returns 1 if a threat was found and targeted, otherwise 0.
|
|
int ai_guard_find_nearby_bomb(object *guarding_objp, object *guarded_objp)
|
|
{
|
|
missile_obj *mo;
|
|
object *bomb_objp, *closest_bomb_objp=NULL;
|
|
float dist, dist_to_guarding_obj,closest_dist_to_guarding_obj=999999.0f;
|
|
weapon *wp;
|
|
weapon_info *wip;
|
|
|
|
for ( mo = GET_NEXT(&Missile_obj_list); mo != END_OF_LIST(&Missile_obj_list); mo = GET_NEXT(mo) ) {
|
|
Assert(mo->objnum >= 0 && mo->objnum < MAX_OBJECTS);
|
|
bomb_objp = &Objects[mo->objnum];
|
|
if (bomb_objp->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
wp = &Weapons[bomb_objp->instance];
|
|
wip = &Weapon_info[wp->weapon_info_index];
|
|
|
|
if ( !((wip->wi_flags[Weapon::Info_Flags::Bomb]) || (wip->wi_flags[Weapon::Info_Flags::Fighter_Interceptable])) ) {
|
|
continue;
|
|
}
|
|
|
|
if ( wp->homing_object != guarded_objp ) {
|
|
continue;
|
|
}
|
|
|
|
if (wp->lssm_stage==3){
|
|
continue;
|
|
}
|
|
|
|
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)) {
|
|
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;
|
|
closest_bomb_objp = bomb_objp;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( closest_bomb_objp ) {
|
|
guard_object_was_hit(guarding_objp, closest_bomb_objp);
|
|
return 1;
|
|
}
|
|
|
|
// No homing threat found... check for mines threatening the guarded ship.
|
|
// Mines are stationary so they won't home; handle them as a lower priority threat.
|
|
if (object *closest_mine_objp = ai_find_nearby_mine_threat(guarding_objp, guarded_objp)) {
|
|
guard_object_was_hit(guarding_objp, closest_mine_objp);
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Scan enemy ships and see if one is near enough to guard object to be pursued.
|
|
*/
|
|
void ai_guard_find_nearby_ship(object *guarding_objp, object *guarded_objp)
|
|
{
|
|
ship *guarding_shipp = &Ships[guarding_objp->instance];
|
|
ai_info *guarding_aip = &Ai_info[guarding_shipp->ai_index];
|
|
ship_obj *so;
|
|
object *enemy_objp;
|
|
float dist;
|
|
|
|
for (so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so))
|
|
{
|
|
enemy_objp = &Objects[so->objnum];
|
|
if (enemy_objp->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
if (enemy_objp->instance < 0)
|
|
continue;
|
|
|
|
ship *eshipp = &Ships[enemy_objp->instance];
|
|
|
|
if (iff_x_attacks_y(guarding_shipp->team, eshipp->team))
|
|
{
|
|
// Don't attack a cargo container or other harmless ships
|
|
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))
|
|
{
|
|
guard_object_was_hit(guarding_objp, enemy_objp);
|
|
} 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Scan for nearby asteroids. Favor asteroids which have their collide_objnum set to that of the
|
|
// guarded ship. Also, favor asteroids that are closer to the guarding ship, since it looks cooler
|
|
// when a ship blows up an asteroid then goes after the pieces that break off.
|
|
void ai_guard_find_nearby_asteroid(object *guarding_objp, object *guarded_objp)
|
|
{
|
|
float dist;
|
|
|
|
object *closest_asteroid_objp=NULL, *danger_asteroid_objp=NULL, *asteroid_objp;
|
|
float dist_to_self, closest_danger_asteroid_dist=999999.0f, closest_asteroid_dist=999999.0f;
|
|
|
|
for (auto aop: list_range(&Asteroid_obj_list)) {
|
|
asteroid_objp = &Objects[aop->objnum];
|
|
if (asteroid_objp->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
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)) {
|
|
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 ) {
|
|
danger_asteroid_objp=asteroid_objp;
|
|
closest_danger_asteroid_dist=dist_to_self;
|
|
}
|
|
}
|
|
if ( dist_to_self < closest_asteroid_dist ) {
|
|
// only attack if moving slower than own max speed
|
|
if ( vm_vec_mag_quick(&asteroid_objp->phys_info.vel) < guarding_objp->phys_info.max_vel.xyz.z ) {
|
|
closest_asteroid_dist = dist_to_self;
|
|
closest_asteroid_objp = asteroid_objp;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( danger_asteroid_objp ) {
|
|
guard_object_was_hit(guarding_objp, danger_asteroid_objp);
|
|
} else if ( closest_asteroid_objp ) {
|
|
guard_object_was_hit(guarding_objp, closest_asteroid_objp);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Scan potential harmful objects and see if one is near enough to guard object to be pursued.
|
|
*/
|
|
void ai_guard_find_nearby_object()
|
|
{
|
|
ship *shipp = &Ships[Pl_objp->instance];
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
object *guardobjp;
|
|
int bomb_found=0;
|
|
|
|
guardobjp = &Objects[aip->guard_objnum];
|
|
|
|
// highest priority is a bomb fired on guarded ship
|
|
bomb_found = ai_guard_find_nearby_bomb(Pl_objp, guardobjp);
|
|
|
|
if ( !bomb_found ) {
|
|
// check for ships if there are no bombs fired at guarded ship
|
|
ai_guard_find_nearby_ship(Pl_objp, guardobjp);
|
|
|
|
// if not attacking anything, go for asteroid close to guarded ship
|
|
if ( (aip->target_objnum == -1) && asteroid_count() ) {
|
|
ai_guard_find_nearby_asteroid(Pl_objp, guardobjp);
|
|
}
|
|
|
|
// if still not attacking anything and flag is set, engage mines near the guarding ship itself
|
|
if (aip->target_objnum == -1 && The_mission.ai_profile->flags[AI::Profile_Flags::Ships_intercept_mines]) {
|
|
object *mine_objp = ai_find_nearby_mine_threat(Pl_objp);
|
|
if (mine_objp)
|
|
guard_object_was_hit(Pl_objp, mine_objp);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets closest point on extended axis of cylinder, r_vec, and radius of cylinder
|
|
* @return z of axis_point in cyl_objp reference frame
|
|
*/
|
|
float get_cylinder_points(object *other_objp, object *cyl_objp, vec3d *axis_pt, vec3d *r_vec, float *radius)
|
|
{
|
|
Assert(other_objp->type == OBJ_SHIP);
|
|
Assert(cyl_objp->type == OBJ_SHIP);
|
|
|
|
// get radius of cylinder
|
|
polymodel *pm = model_get(Ship_info[Ships[cyl_objp->instance].ship_info_index].model_num);
|
|
float tempx, tempy;
|
|
tempx = MAX(-pm->mins.xyz.x, pm->maxs.xyz.x);
|
|
tempy = MAX(-pm->mins.xyz.y, pm->maxs.xyz.y);
|
|
*radius = MAX(tempx, tempy);
|
|
|
|
// get vec from cylinder to other_obj
|
|
vec3d r_sph;
|
|
vm_vec_sub(&r_sph, &other_objp->pos, &cyl_objp->pos);
|
|
|
|
// get point on axis and on cylinder
|
|
// extended_cylinder_z is along extended cylinder
|
|
// cylinder_z is capped within cylinder
|
|
float extended_cylinder_z = vm_vec_dot(&r_sph, &cyl_objp->orient.vec.fvec);
|
|
|
|
// get pt on axis of extended cylinder
|
|
vm_vec_scale_add(axis_pt, &cyl_objp->pos, &cyl_objp->orient.vec.fvec, extended_cylinder_z);
|
|
|
|
// get r_vec (pos - axis_pt) normalized
|
|
vm_vec_normalized_dir(r_vec, &other_objp->pos, axis_pt);
|
|
|
|
return extended_cylinder_z;
|
|
}
|
|
|
|
// handler for guard behavior when guarding BIG ships
|
|
// When someone has attacked guarded ship, then attack that ship.
|
|
// To attack another ship, switch out of guard mode into chase mode.
|
|
void ai_big_guard()
|
|
{
|
|
|
|
ship *shipp = &Ships[Pl_objp->instance];
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
object *guard_objp;
|
|
|
|
// sanity checks already done in ai_guard()
|
|
guard_objp = &Objects[aip->guard_objnum];
|
|
|
|
switch (aip->submode) {
|
|
case AIS_GUARD_STATIC:
|
|
case AIS_GUARD_PATROL:
|
|
{
|
|
vec3d axis_pt, r_vec, theta_vec;
|
|
float radius, extended_z;
|
|
|
|
// get random [0 to 1] based on OBJNUM
|
|
float objval = static_randf(OBJ_INDEX(Pl_objp));
|
|
|
|
// get position relative to cylinder of guard_objp
|
|
extended_z = get_cylinder_points(Pl_objp, guard_objp, &axis_pt, &r_vec, &radius);
|
|
vm_vec_cross(&theta_vec, &guard_objp->orient.vec.fvec, &r_vec);
|
|
|
|
// half ships circle each way
|
|
if (objval > 0.5f) {
|
|
vm_vec_negate(&theta_vec);
|
|
}
|
|
|
|
float min_guard_dist = radius + Pl_objp->radius + 50.0f;
|
|
float desired_guard_dist = min_guard_dist + 0.5f * ((1.0f + objval) * MAX_GUARD_DIST);
|
|
float max_guard_dist = min_guard_dist + 1.0f * ((1.0f + objval) * MAX_GUARD_DIST);
|
|
|
|
// get z extents
|
|
float min_z, max_z, length;
|
|
polymodel *pm = model_get(Ship_info[Ships[guard_objp->instance].ship_info_index].model_num);
|
|
min_z = pm->mins.xyz.z;
|
|
max_z = pm->maxs.xyz.z;
|
|
length = max_z - min_z;
|
|
|
|
// get desired z
|
|
// how often to choose new desired_z
|
|
// 1*(64) sec < 2000, 2*(64) < 2-4000 3*(64) > 4-8000, etc (Missiontime >> 22 is 64 sec intervals)
|
|
int time_choose = int(floor(log(length * 0.001f) / log(2.0f)));
|
|
float desired_z = min_z + length * static_randf( (OBJ_INDEX(Pl_objp)) ^ (Missiontime >> (22 + time_choose)) );
|
|
|
|
// get r from guard_ship
|
|
float cur_guard_rad = vm_vec_dist(&Pl_objp->pos, &axis_pt);
|
|
|
|
// is ship within extents of cylinder of ship it is guarding
|
|
int inside = (extended_z > min_z) && (extended_z < min_z + length);
|
|
|
|
vec3d goal_pt;
|
|
// maybe go into orbit mode
|
|
if (cur_guard_rad < max_guard_dist) {
|
|
if ( cur_guard_rad > min_guard_dist ) {
|
|
if (inside) {
|
|
// orbit
|
|
vm_vec_scale_add(&goal_pt, &axis_pt, &r_vec, desired_guard_dist);
|
|
vm_vec_scale_add2(&goal_pt, &theta_vec, desired_guard_dist);
|
|
} else {
|
|
// move to where I can orbit
|
|
if (extended_z < min_z) {
|
|
vm_vec_scale_add(&goal_pt, &guard_objp->pos, &guard_objp->orient.vec.fvec, min_z);
|
|
} else {
|
|
vm_vec_scale_add(&goal_pt, &guard_objp->pos, &guard_objp->orient.vec.fvec, max_z);
|
|
}
|
|
vm_vec_scale_add2(&goal_pt, &r_vec, desired_guard_dist);
|
|
vm_vec_scale_add2(&goal_pt, &theta_vec, desired_guard_dist);
|
|
}
|
|
} else {
|
|
// too close for orbit mode
|
|
if (inside) {
|
|
// inside (fly straight out and return circle)
|
|
vm_vec_scale_add(&goal_pt, &axis_pt, &r_vec, max_guard_dist);
|
|
} else {
|
|
// outside (fly to edge and circle)
|
|
if (extended_z < min_z) {
|
|
vm_vec_scale_add(&goal_pt, &guard_objp->pos, &guard_objp->orient.vec.fvec, min_z);
|
|
} else {
|
|
vm_vec_scale_add(&goal_pt, &guard_objp->pos, &guard_objp->orient.vec.fvec, max_z);
|
|
}
|
|
vm_vec_scale_add2(&goal_pt, &r_vec, max_guard_dist);
|
|
vm_vec_scale_add2(&goal_pt, &theta_vec, desired_guard_dist);
|
|
}
|
|
}
|
|
|
|
// make sure we have a forward velocity worth calculating (values too low can produce NaN in goal_pt) - taylor
|
|
if (Pl_objp->phys_info.fspeed > 0.0001f) {
|
|
// modify goal_pt to take account moving guard objp
|
|
float dist = vm_vec_dist_quick(&Pl_objp->pos, &goal_pt);
|
|
float time = dist / Pl_objp->phys_info.fspeed;
|
|
vm_vec_scale_add2(&goal_pt, &guard_objp->phys_info.vel, time);
|
|
|
|
// now modify to move to desired z (at a max of 20 m/s)
|
|
float delta_z = desired_z - extended_z;
|
|
float v_z = delta_z * 0.2f;
|
|
if (v_z < -20) {
|
|
v_z = -20.0f;
|
|
} else if (v_z > 20) {
|
|
v_z = 20.0f;
|
|
}
|
|
|
|
vm_vec_scale_add2(&goal_pt, &guard_objp->orient.vec.fvec, v_z*time);
|
|
}
|
|
|
|
} else {
|
|
// cast vector to center of guard_ship adjusted by desired_z
|
|
float delta_z = desired_z - extended_z;
|
|
vm_vec_scale_add(&goal_pt, &guard_objp->pos, &guard_objp->orient.vec.fvec, delta_z);
|
|
}
|
|
|
|
// try not to bump into things along the way
|
|
if ( (cur_guard_rad > max_guard_dist) || (extended_z < min_z) || (extended_z > max_z) ) {
|
|
if (maybe_avoid_big_ship(Pl_objp, guard_objp, aip, &goal_pt, 5.0f)) {
|
|
if (Pl_objp->phys_info.flags & PF_AFTERBURNER_ON) {
|
|
afterburners_stop(Pl_objp);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (avoid_player(Pl_objp, &goal_pt)) {
|
|
if (Pl_objp->phys_info.flags & PF_AFTERBURNER_ON) {
|
|
afterburners_stop(Pl_objp);
|
|
}
|
|
return;
|
|
}
|
|
} else {
|
|
if (maybe_avoid_big_ship(Pl_objp, guard_objp, aip, &goal_pt, 5.0f)) {
|
|
if (Pl_objp->phys_info.flags & PF_AFTERBURNER_ON) {
|
|
afterburners_stop(Pl_objp);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// got the point, now let's go there
|
|
ai_turn_towards_vector(&goal_pt, Pl_objp, nullptr, nullptr, 0.0f, 0);
|
|
accelerate_ship(aip, The_mission.ai_profile->guard_big_orbit_max_speed_percent);
|
|
|
|
|
|
bool free_ab_use = aip->ai_flags[AI::AI_Flags::Free_afterburner_use] || aip->ai_profile_flags[AI::Profile_Flags::Free_afterburner_use];
|
|
if (!(shipp->flags[Ship::Ship_Flags::Afterburner_locked]) &&
|
|
(free_ab_use || ai_willing_to_afterburn_hard(aip)) &&
|
|
(cur_guard_rad > 1.1f * max_guard_dist))
|
|
{
|
|
vec3d v2g;
|
|
float dot_to_goal_point;
|
|
|
|
vm_vec_normalized_dir(&v2g, &goal_pt, &Pl_objp->pos);
|
|
dot_to_goal_point = vm_vec_dot(&v2g, &Pl_objp->orient.vec.fvec);
|
|
|
|
if (dot_to_goal_point > 0.75f) {
|
|
if (ai_willing_to_afterburn_hard(aip))
|
|
ai_afterburn_hard(Pl_objp, aip);
|
|
else if (ai_maybe_fire_afterburner(Pl_objp, aip)) {
|
|
afterburners_start(Pl_objp);
|
|
aip->afterburner_stop_time = Missiontime + 3 * F1_0;
|
|
}
|
|
}
|
|
} else if (Pl_objp->phys_info.flags & PF_AFTERBURNER_ON) {
|
|
afterburners_stop(Pl_objp);
|
|
}
|
|
|
|
// Periodically, scan for a nearby ship to attack.
|
|
if (((AI_FrameCount ^ (OBJ_INDEX(Pl_objp))) & 0x07) == 0) {
|
|
ai_guard_find_nearby_object();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case AIS_GUARD_ATTACK:
|
|
// The guarded ship has been attacked. Do something useful!
|
|
ai_chase();
|
|
break;
|
|
|
|
default:
|
|
UNREACHABLE("Illegal submode of %d for guard mode in ai_big_guard. Please report to the SCP!", aip->submode);
|
|
// AL 06/03/97 comment out Int3() to allow milestone to get out the door
|
|
aip->submode = AIS_GUARD_PATROL;
|
|
aip->submode_start_time = Missiontime;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Main handler for guard behavior.
|
|
// When someone has attacked guarded ship, then attack that ship.
|
|
// To attack another ship, switch out of guard mode into chase mode.
|
|
void ai_guard()
|
|
{
|
|
ship *shipp = &Ships[Pl_objp->instance];
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
object *guard_objp;
|
|
float dist_to_guardobj;
|
|
vec3d vec_to_guardobj;
|
|
|
|
if (aip->guard_objnum == -1) {
|
|
aip->mode = AIM_NONE;
|
|
return;
|
|
}
|
|
|
|
Assert(aip->guard_objnum != -1);
|
|
|
|
guard_objp = &Objects[aip->guard_objnum];
|
|
|
|
Assertion(guard_objp != Pl_objp, "The ship %s has been found to be guarding itself in ai_guard. Please report to the SCP!", shipp->ship_name); // This seems illegal. Why is a ship guarding itself?
|
|
if (guard_objp == Pl_objp) {
|
|
aip->guard_objnum = -1;
|
|
return;
|
|
}
|
|
|
|
// check that I have someone to guard
|
|
if (guard_objp->instance == -1) {
|
|
return;
|
|
}
|
|
|
|
// Not sure whether this should be impossible, or a reasonable cleanup condition.
|
|
// For now (3/31/97), it's getting trapped by an Assert, so clean it up.
|
|
if (guard_objp->type != OBJ_SHIP) {
|
|
aip->guard_objnum = -1;
|
|
return;
|
|
}
|
|
|
|
if (better_collision_avoidance_triggered(
|
|
The_mission.ai_profile->flags[AI::Profile_Flags::Better_guard_collision_avoidance],
|
|
The_mission.ai_profile->better_collision_avoid_aggression_guard,
|
|
Pl_objp,
|
|
En_objp)) {
|
|
return;
|
|
}
|
|
|
|
// handler for guard object with BIG radius
|
|
if (guard_objp->radius > The_mission.ai_profile->guard_big_orbit_above_target_radius) {
|
|
ai_big_guard();
|
|
return;
|
|
}
|
|
|
|
float objval;
|
|
vec3d goal_point;
|
|
vec3d rel_vec;
|
|
float dist_to_goal_point, dot_to_goal_point, accel_scale;
|
|
vec3d v2g, rvec;
|
|
|
|
// get random [0 to 1] based on OBJNUM
|
|
objval = static_randf(OBJ_INDEX(Pl_objp));
|
|
|
|
switch (aip->submode) {
|
|
case AIS_GUARD_STATIC:
|
|
case AIS_GUARD_PATROL:
|
|
// Stay near ship
|
|
dist_to_guardobj = vm_vec_normalized_dir(&vec_to_guardobj, &guard_objp->pos, &Pl_objp->pos);
|
|
|
|
rel_vec = aip->guard_vec;
|
|
vm_vec_add(&goal_point, &guard_objp->pos, &rel_vec);
|
|
|
|
vm_vec_normalized_dir(&v2g, &goal_point, &Pl_objp->pos);
|
|
dist_to_goal_point = vm_vec_dist_quick(&goal_point, &Pl_objp->pos);
|
|
dot_to_goal_point = vm_vec_dot(&v2g, &Pl_objp->orient.vec.fvec);
|
|
accel_scale = (1.0f + dot_to_goal_point)/2.0f;
|
|
|
|
// If far away, get closer
|
|
if (dist_to_goal_point > MAX_GUARD_DIST + 1.5 * (Pl_objp->radius + guard_objp->radius)) {
|
|
if (maybe_avoid_big_ship(Pl_objp, guard_objp, aip, &goal_point, 5.0f)) {
|
|
if (Pl_objp->phys_info.flags & PF_AFTERBURNER_ON) {
|
|
afterburners_stop(Pl_objp);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (avoid_player(Pl_objp, &goal_point)) {
|
|
if (Pl_objp->phys_info.flags & PF_AFTERBURNER_ON) {
|
|
afterburners_stop(Pl_objp);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// quite far away, so try to go straight to
|
|
compute_desired_rvec(&rvec, &goal_point, &Pl_objp->pos);
|
|
ai_turn_towards_vector(&goal_point, Pl_objp, nullptr, nullptr, 0.0f, 0, &rvec);
|
|
|
|
bool free_ab_use = aip->ai_flags[AI::AI_Flags::Free_afterburner_use] || aip->ai_profile_flags[AI::Profile_Flags::Free_afterburner_use];
|
|
if (!(shipp->flags[Ship::Ship_Flags::Afterburner_locked]) &&
|
|
(free_ab_use || ai_willing_to_afterburn_hard(aip)) &&
|
|
(accel_scale * (0.25f + dist_to_goal_point/700.0f) > 0.8f))
|
|
{
|
|
if (ai_willing_to_afterburn_hard(aip))
|
|
ai_afterburn_hard(Pl_objp, aip);
|
|
else if (ai_maybe_fire_afterburner(Pl_objp, aip)) {
|
|
afterburners_start(Pl_objp);
|
|
aip->afterburner_stop_time = Missiontime + 3*F1_0;
|
|
}
|
|
accelerate_ship(aip, 1.0f);
|
|
} else {
|
|
if (Pl_objp->phys_info.flags & PF_AFTERBURNER_ON) {
|
|
afterburners_stop(Pl_objp);
|
|
}
|
|
accelerate_ship(aip, accel_scale * (0.25f + dist_to_goal_point/700.0f));
|
|
}
|
|
|
|
} else {
|
|
if (maybe_avoid_big_ship(Pl_objp, guard_objp, aip, &goal_point, 2.0f)) {
|
|
if (Pl_objp->phys_info.flags & PF_AFTERBURNER_ON) {
|
|
afterburners_stop(Pl_objp);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// get max of guard_objp (1) normal speed (2) dock speed
|
|
float speed = guard_objp->phys_info.speed;
|
|
|
|
if (guard_objp->type == OBJ_SHIP) {
|
|
if (object_is_docked(guard_objp)) {
|
|
speed = dock_calc_docked_speed(guard_objp);
|
|
}
|
|
}
|
|
|
|
// Deal with guarding a small object.
|
|
// If going to guard_vec might cause a collision with guarded object, pick a new guard point.
|
|
if (vm_vec_dot(&v2g, &vec_to_guardobj) > 0.8f) {
|
|
if (dist_to_guardobj < dist_to_goal_point) {
|
|
ai_set_guard_vec(Pl_objp, guard_objp); // OK to return here.
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (speed > 10.0f) {
|
|
// If goal ship is moving more than a tiny bit, don't orbit it, get near it.
|
|
if (vm_vec_dist_quick(&goal_point, &Pl_objp->pos) > 40.0f) {
|
|
if (vm_vec_dot(&Pl_objp->orient.vec.fvec, &v2g) < 0.0f) {
|
|
// Just slow down, don't turn.
|
|
set_accel_for_target_speed(Pl_objp, guard_objp->phys_info.speed - dist_to_goal_point/10.0f);
|
|
} else {
|
|
// Goal point is in front.
|
|
// If close to goal point, don't change direction, just change speed.
|
|
if (dist_to_goal_point > Pl_objp->radius + 10.0f) {
|
|
turn_towards_point(Pl_objp, &goal_point, nullptr, 0.0f);
|
|
}
|
|
|
|
set_accel_for_target_speed(Pl_objp, guard_objp->phys_info.speed + (dist_to_goal_point-40.0f)/20.0f);
|
|
}
|
|
} else {
|
|
if (dot_to_goal_point > 0.8f) {
|
|
turn_towards_point(Pl_objp, &goal_point, nullptr, 0.0f);
|
|
set_accel_for_target_speed(Pl_objp, guard_objp->phys_info.speed + dist_to_goal_point*0.1f);
|
|
} else {
|
|
set_accel_for_target_speed(Pl_objp, guard_objp->phys_info.speed - dist_to_goal_point*0.1f - 1.0f);
|
|
}
|
|
}
|
|
// consider guard object STILL
|
|
} else if (guard_objp->radius < 50.0f) {
|
|
if (dist_to_goal_point > 15.0f) {
|
|
turn_towards_point(Pl_objp, &goal_point, nullptr, 0.0f);
|
|
set_accel_for_target_speed(Pl_objp, (dist_to_goal_point-10.0f)/2.0f);
|
|
} else if (Pl_objp->phys_info.speed < 1.0f) {
|
|
if (aip->ai_profile_flags[AI::Profile_Flags::Align_to_target_when_guarding_still]) {
|
|
vec3d goal_vec = guard_objp->orient.vec.fvec + Pl_objp->pos;
|
|
turn_towards_point(Pl_objp, &goal_vec, nullptr, 0.0f, &guard_objp->orient);
|
|
} else {
|
|
turn_away_from_point(Pl_objp, &guard_objp->pos, 0.0f);
|
|
}
|
|
}
|
|
// It's a big ship
|
|
} else if (dist_to_guardobj > MAX_GUARD_DIST + Pl_objp->radius + guard_objp->radius) {
|
|
// Orbiting ship, too far away
|
|
float dot = turn_towards_tangent(Pl_objp, &guard_objp->pos, (1.0f + objval/2) * guard_objp->radius);
|
|
accelerate_ship(aip, (1.0f + dot)/2.0f);
|
|
} else if (dist_to_guardobj < Pl_objp->radius + guard_objp->radius) {
|
|
// Orbiting ship, got too close
|
|
turn_away_from_point(Pl_objp, &guard_objp->pos, 0.0f);
|
|
if ((dist_to_guardobj > guard_objp->radius + Pl_objp->radius + 50.0f) && (guard_objp->phys_info.speed > Pl_objp->phys_info.speed - 1.0f))
|
|
change_acceleration(aip, 0.25f);
|
|
else
|
|
accelerate_ship(aip, 0.5f + objval/4.0f);
|
|
} else {
|
|
// Orbiting ship, about the right distance away.
|
|
float dot = turn_towards_tangent(Pl_objp, &guard_objp->pos, (1.5f + objval/2.0f)*guard_objp->radius);
|
|
if ((dist_to_guardobj > guard_objp->radius + Pl_objp->radius + 50.0f) && (guard_objp->phys_info.speed > Pl_objp->phys_info.speed - 1.0f))
|
|
set_accel_for_target_speed(Pl_objp, (0.5f * (1.0f + dot)) * (guard_objp->phys_info.speed + (dist_to_guardobj - guard_objp->radius - Pl_objp->radius)/10.0f));
|
|
else
|
|
accelerate_ship(aip, 0.5f * (1.0f + dot) * (0.3f + objval/3.0f));
|
|
}
|
|
if (Pl_objp->phys_info.flags & PF_AFTERBURNER_ON) {
|
|
afterburners_stop(Pl_objp);
|
|
}
|
|
}
|
|
|
|
// Periodically, scan for a nearby ship to attack.
|
|
if (((AI_FrameCount ^ (OBJ_INDEX(Pl_objp))) & 0x07) == 0) {
|
|
ai_guard_find_nearby_object();
|
|
}
|
|
break;
|
|
|
|
case AIS_GUARD_ATTACK:
|
|
// The guarded ship has been attacked. Do something useful!
|
|
ai_chase();
|
|
|
|
break;
|
|
default:
|
|
UNREACHABLE("Illegal ai submode of %d for guard mode in ai_guard for ship %s. Pleae report to the SCP!", aip->submode, shipp->ship_name);
|
|
// AL 06/03/97 comment out Int3() to allow milestone to get out the door
|
|
aip->submode = AIS_GUARD_PATROL;
|
|
aip->submode_start_time = Missiontime;
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
// function to clean up ai flags, variables, and other interesting information
|
|
// for a ship that was getting repaired. The how parameter is useful for multiplayer
|
|
// only in that it tells us why the repaired ship is being cleaned up.
|
|
void ai_do_objects_repairing_stuff( object *repaired_objp, object *repair_objp, int how )
|
|
{
|
|
ai_info *aip, *repair_aip;
|
|
int stamp = -1;
|
|
|
|
int p_index;
|
|
int p_team;
|
|
|
|
p_index = -1;
|
|
p_team = -1;
|
|
|
|
// repaired_objp should not be null, but repair_objp will be null when a support ship is just warping in
|
|
Assert(repaired_objp != NULL);
|
|
|
|
Assert( repaired_objp->type == OBJ_SHIP);
|
|
aip = &Ai_info[Ships[repaired_objp->instance].ai_index];
|
|
|
|
if(Game_mode & GM_MULTIPLAYER){
|
|
p_index = multi_find_player_by_object(repaired_objp);
|
|
if (p_index >= 0) {
|
|
p_team = Net_players[p_index].p_info.team;
|
|
}
|
|
} else {
|
|
if(repaired_objp == Player_obj){
|
|
p_index = Player_num;
|
|
}
|
|
}
|
|
|
|
switch( how ) {
|
|
case REPAIR_INFO_BEGIN:
|
|
aip->ai_flags.set(AI::AI_Flags::Being_repaired);
|
|
aip->ai_flags.remove(AI::AI_Flags::Awaiting_repair);
|
|
stamp = timestamp(-1);
|
|
|
|
if (repair_objp != nullptr && scripting::hooks::OnSupportRearmStarted->isActive()) {
|
|
scripting::hooks::OnSupportRearmStarted->run(
|
|
scripting::hook_param_list(scripting::hook_param("Support Ship", 'o', repair_objp),
|
|
scripting::hook_param("Target Ship", 'o', repaired_objp)));
|
|
}
|
|
|
|
// if this is a player ship, then subtract the repair penalty from this player's score
|
|
if ( repaired_objp->flags[Object::Object_Flags::Player_ship] ) {
|
|
if ( !(Game_mode & GM_MULTIPLAYER) ) {
|
|
Player->stats.m_score -= The_mission.ai_profile->repair_penalty[Game_skill_level]; // subtract the penalty
|
|
}
|
|
}
|
|
break;
|
|
|
|
case REPAIR_INFO_BROKEN:
|
|
aip->ai_flags.remove(AI::AI_Flags::Being_repaired);
|
|
aip->ai_flags.set(AI::AI_Flags::Awaiting_repair);
|
|
stamp = timestamp((int) ((30 + 10*frand()) * 1000));
|
|
break;
|
|
|
|
case REPAIR_INFO_END:
|
|
// when only awaiting repair, and the repair is ended, then set support to -1.
|
|
if ( aip->ai_flags[AI::AI_Flags::Awaiting_repair] ){
|
|
aip->support_ship_objnum = -1;
|
|
}
|
|
aip->ai_flags.remove(AI::AI_Flags::Being_repaired);
|
|
aip->ai_flags.remove(AI::AI_Flags::Awaiting_repair);
|
|
stamp = timestamp((int) ((30 + 10*frand()) * 1000));
|
|
break;
|
|
|
|
case REPAIR_INFO_QUEUE:
|
|
aip->ai_flags.set(AI::AI_Flags::Awaiting_repair);
|
|
if ( aip == Player_ai ){
|
|
hud_support_view_start();
|
|
}
|
|
stamp = timestamp(-1);
|
|
break;
|
|
|
|
case REPAIR_INFO_ABORT:
|
|
case REPAIR_INFO_KILLED:
|
|
// undock if necessary (we may be just waiting for a repair in which case we aren't docked)
|
|
if ((repair_objp != NULL) && dock_check_find_direct_docked_object(repair_objp, repaired_objp))
|
|
{
|
|
ai_do_objects_undocked_stuff(repair_objp, repaired_objp);
|
|
}
|
|
|
|
// 5/4/98 -- MWA -- Need to set support objnum to -1 to let code know this guy who was getting
|
|
// repaired (or queued for repair), isn't really going to be docked with anyone anymore.
|
|
aip->support_ship_objnum = -1;
|
|
aip->ai_flags.remove(AI::AI_Flags::Being_repaired);
|
|
aip->ai_flags.remove(AI::AI_Flags::Awaiting_repair);
|
|
|
|
if (repair_objp != NULL) {
|
|
repair_aip = &Ai_info[Ships[repair_objp->instance].ai_index];
|
|
repair_aip->ai_flags.remove(AI::AI_Flags::Being_repaired);
|
|
repair_aip->ai_flags.remove(AI::AI_Flags::Awaiting_repair);
|
|
}
|
|
|
|
if ( p_index >= 0 ) {
|
|
hud_support_view_abort();
|
|
|
|
// send appropriate messages here
|
|
if (Game_mode & GM_NORMAL || MULTIPLAYER_MASTER) {
|
|
if ( how == REPAIR_INFO_KILLED ){
|
|
message_send_builtin(MESSAGE_SUPPORT_KILLED, nullptr, nullptr, p_index, p_team);
|
|
} else {
|
|
if ( repair_objp ){
|
|
message_send_builtin(MESSAGE_REPAIR_ABORTED, &Ships[repair_objp->instance], nullptr, p_index, p_team);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// add log entry if this is a player
|
|
if ( repaired_objp->flags[Object::Object_Flags::Player_ship] ){
|
|
mission_log_add_entry(LOG_PLAYER_ABORTED_REARM, Ships[repaired_objp->instance].ship_name, NULL);
|
|
}
|
|
|
|
if (repair_objp != nullptr && scripting::hooks::OnSupportRearmFinished->isActive()) {
|
|
scripting::hooks::OnSupportRearmFinished->run(
|
|
scripting::hook_param_list(scripting::hook_param("Support Ship", 'o', repair_objp),
|
|
scripting::hook_param("Target Ship", 'o', repaired_objp)));
|
|
}
|
|
|
|
stamp = timestamp((int) ((30 + 10*frand()) * 1000));
|
|
break;
|
|
|
|
case REPAIR_INFO_COMPLETE:
|
|
// clear the being repaired flag -- and
|
|
if ( p_index >= 0 ) {
|
|
Assert( repair_objp );
|
|
|
|
hud_support_view_stop();
|
|
|
|
if (Game_mode & GM_NORMAL || MULTIPLAYER_MASTER) {
|
|
message_send_builtin(MESSAGE_REPAIR_DONE, &Ships[repair_objp->instance], nullptr, p_index, p_team);
|
|
}
|
|
}
|
|
stamp = timestamp((int) ((30 + 10*frand()) * 1000));
|
|
break;
|
|
|
|
case REPAIR_INFO_ONWAY:
|
|
// need to set the signature so that clients in multiplayer games rearm correctly
|
|
Assert( repair_objp );
|
|
aip->support_ship_signature = repair_objp->signature;
|
|
aip->support_ship_objnum = OBJ_INDEX(repair_objp);
|
|
stamp = timestamp(-1);
|
|
break;
|
|
|
|
default:
|
|
UNREACHABLE("Illegal repair info type of %d in ai_do_objects_repairing_stuff. Pleae report to the SCP!", how);
|
|
}
|
|
|
|
// repair_objp might be NULL if we are cleaning up this mode because of the support ship
|
|
// getting killed.
|
|
if ( repair_objp ) {
|
|
Ai_info[Ships[repair_objp->instance].ai_index].warp_out_timestamp = stamp;
|
|
aip = &Ai_info[Ships[repair_objp->instance].ai_index];
|
|
|
|
switch ( how ) {
|
|
case REPAIR_INFO_ONWAY:
|
|
Assert( repaired_objp != NULL );
|
|
aip->goal_objnum = OBJ_INDEX(repaired_objp);
|
|
aip->ai_flags.set(AI::AI_Flags::Repairing);
|
|
break;
|
|
|
|
case REPAIR_INFO_BROKEN:
|
|
break;
|
|
|
|
case REPAIR_INFO_END:
|
|
case REPAIR_INFO_ABORT:
|
|
case REPAIR_INFO_KILLED:
|
|
if ( how == REPAIR_INFO_ABORT )
|
|
aip->goal_objnum = -1;
|
|
|
|
aip->ai_flags.remove(AI::AI_Flags::Repairing);
|
|
break;
|
|
|
|
case REPAIR_INFO_QUEUE:
|
|
ai_add_rearm_goal( repaired_objp, repair_objp );
|
|
break;
|
|
|
|
case REPAIR_INFO_BEGIN:
|
|
case REPAIR_INFO_COMPLETE:
|
|
break;
|
|
|
|
default:
|
|
UNREACHABLE("Illegal repair info type of %d in ai_do_objects_repairing_stuff. Pleae report to the SCP!", how);
|
|
}
|
|
}
|
|
|
|
multi_maybe_send_repair_info( repaired_objp, repair_objp, how );
|
|
}
|
|
|
|
// Goober5000 - helper function that is also called from ai_dock()
|
|
void ai_get_dock_goal_indexes(object *objp, ai_info *aip, ai_goal *aigp, object *goal_objp, int &docker_index, int &dockee_index)
|
|
{
|
|
// get the indexes
|
|
switch (aip->submode)
|
|
{
|
|
case AIS_DOCK_1:
|
|
case AIS_DOCK_2:
|
|
case AIS_DOCK_3:
|
|
Warning(LOCATION, "Normally dock indexes should be calculated for only AIS_DOCK_0 and AIS_UNDOCK_0. Trace out and debug.");
|
|
FALLTHROUGH;
|
|
case AIS_DOCK_0:
|
|
{
|
|
// get them from the active goal
|
|
Assert(aigp != NULL);
|
|
Assert(aigp->flags[AI::Goal_Flags::Dockee_index_valid] && aigp->flags[AI::Goal_Flags::Docker_index_valid]);
|
|
docker_index = aigp->docker.index;
|
|
dockee_index = aigp->dockee.index;
|
|
Assert(docker_index >= 0);
|
|
Assert(dockee_index >= 0);
|
|
break;
|
|
}
|
|
|
|
case AIS_DOCK_4:
|
|
case AIS_DOCK_4A:
|
|
case AIS_UNDOCK_1:
|
|
case AIS_UNDOCK_2:
|
|
Warning(LOCATION, "Normally dock indexes should be calculated for only AIS_DOCK_0 and AIS_UNDOCK_0. Trace out and debug.");
|
|
FALLTHROUGH;
|
|
case AIS_UNDOCK_0:
|
|
{
|
|
// get them from the guy I'm docked to
|
|
Assert(goal_objp != NULL);
|
|
docker_index = dock_find_dockpoint_used_by_object(objp, goal_objp);
|
|
dockee_index = dock_find_dockpoint_used_by_object(goal_objp, objp);
|
|
Assert(docker_index >= 0);
|
|
Assert(dockee_index >= 0);
|
|
break;
|
|
}
|
|
|
|
case AIS_UNDOCK_3:
|
|
case AIS_UNDOCK_4:
|
|
{
|
|
Warning(LOCATION, "Normally dock indexes should be calculated for only AIS_DOCK_0 and AIS_UNDOCK_0. Additionally, dock indexes can't always be determined for AIS_UNDOCK_3 or AIS_UNDOCK_4. Trace out and debug.");
|
|
docker_index = -1;
|
|
dockee_index = -1;
|
|
break;
|
|
}
|
|
|
|
default:
|
|
{
|
|
Error(LOCATION, "Unknown docking submode!");
|
|
docker_index = -1;
|
|
dockee_index = -1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ai_dock_do_animations(ship* shipp, int from_dockstage, int to_dockstage, int dock_index)
|
|
{
|
|
if (shipp == nullptr)
|
|
return;
|
|
|
|
//ordered DOCK / UNDOCK in order of "docked more".
|
|
const int dockstages_ordered[] = { AIS_DOCK_0, AIS_DOCK_1, AIS_DOCK_2, AIS_DOCK_3, AIS_DOCK_4, AIS_UNDOCK_4, AIS_UNDOCK_3, AIS_UNDOCK_2, AIS_UNDOCK_1, AIS_UNDOCK_0 };
|
|
|
|
//AIS_DOCK_4A behaves identical to AIS_DOCK_4 here
|
|
if (from_dockstage == AIS_DOCK_4A)
|
|
from_dockstage = AIS_DOCK_4;
|
|
if (to_dockstage == AIS_DOCK_4A)
|
|
to_dockstage = AIS_DOCK_4;
|
|
|
|
constexpr size_t num_dockstages = sizeof(dockstages_ordered) / sizeof(dockstages_ordered[0]);
|
|
size_t currentIdx = num_dockstages, targetIdx = num_dockstages;
|
|
|
|
|
|
for (size_t idx = 0; idx < num_dockstages; idx++) {
|
|
if (from_dockstage == dockstages_ordered[idx])
|
|
currentIdx = idx;
|
|
if (to_dockstage == dockstages_ordered[idx])
|
|
targetIdx = idx;
|
|
}
|
|
|
|
Assertion(currentIdx != num_dockstages, "Couldn't find current dockstage %d for docking animations.", from_dockstage);
|
|
Assertion(targetIdx != num_dockstages, "Couldn't find target dockstage %d for docking animations.", to_dockstage);
|
|
Assertion(targetIdx != currentIdx, "Cannot perform docking animations when not actually switching dock stages.");
|
|
Assertion((currentIdx < 5 && targetIdx < 5) || (currentIdx >= 5 && targetIdx >= 5), "Cannot perform animations from a AIS_DOCK to AIS_UNDOCK stage!");
|
|
|
|
bool isDocking = currentIdx < targetIdx;
|
|
animation::ModelAnimationDirection direction = isDocking ? animation::ModelAnimationDirection::FWD : animation::ModelAnimationDirection::RWD;
|
|
|
|
const animation::ModelAnimationSet& animations = Ship_info[shipp->ship_info_index].animations;
|
|
polymodel_instance* pmi = model_get_instance(shipp->model_instance_num);
|
|
|
|
SCP_string dock_name = model_get_dock_name(Ship_info[shipp->ship_info_index].model_num, dock_index);
|
|
|
|
for (size_t i = currentIdx; i != targetIdx; i += isDocking ? 1 : -1) {
|
|
switch (dockstages_ordered[i + (isDocking ? 1 : 0)]) {
|
|
case AIS_DOCK_1:
|
|
case AIS_UNDOCK_3:
|
|
//Catches new-table animations with dock port name
|
|
(animations.get(pmi, animation::ModelAnimationTriggerType::Docking_Stage1, dock_name) +
|
|
//Catches new-table animations with no name and no subtype, as well as legacy-table animations with no subtype
|
|
animations.get(pmi, animation::ModelAnimationTriggerType::Docking_Stage1, "") +
|
|
//Catches new-table animations with subtype, as well as legacy-table animations with subtype
|
|
animations.getAll(pmi, animation::ModelAnimationTriggerType::Docking_Stage1, dock_index, true)).start(direction);
|
|
break;
|
|
case AIS_DOCK_2:
|
|
case AIS_UNDOCK_2:
|
|
(animations.get(pmi, animation::ModelAnimationTriggerType::Docking_Stage2, dock_name) +
|
|
animations.get(pmi, animation::ModelAnimationTriggerType::Docking_Stage2, "") +
|
|
animations.getAll(pmi, animation::ModelAnimationTriggerType::Docking_Stage2, dock_index, true)).start(direction);
|
|
break;
|
|
case AIS_DOCK_3:
|
|
case AIS_UNDOCK_1:
|
|
(animations.get(pmi, animation::ModelAnimationTriggerType::Docking_Stage3, dock_name) +
|
|
animations.get(pmi, animation::ModelAnimationTriggerType::Docking_Stage3, "") +
|
|
animations.getAll(pmi, animation::ModelAnimationTriggerType::Docking_Stage3, dock_index, true)).start(direction);
|
|
break;
|
|
case AIS_DOCK_4:
|
|
case AIS_UNDOCK_0:
|
|
(animations.get(pmi, animation::ModelAnimationTriggerType::Docked, dock_name) +
|
|
animations.get(pmi, animation::ModelAnimationTriggerType::Docked, "") +
|
|
animations.getAll(pmi, animation::ModelAnimationTriggerType::Docked, dock_index, true)).start(direction);
|
|
break;
|
|
case AIS_DOCK_0:
|
|
case AIS_UNDOCK_4:
|
|
default:
|
|
//No animation here
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Goober5000 - clean up my own dock mode
|
|
void ai_cleanup_dock_mode_subjective(object *objp)
|
|
{
|
|
ship *shipp = &Ships[objp->instance];
|
|
|
|
// get ai of object
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
|
|
// if the object is in dock mode, force them to near last stage
|
|
if ( (aip->mode == AIM_DOCK) && (aip->submode < AIS_UNDOCK_3) )
|
|
{
|
|
// get the object being acted upon
|
|
object *goal_objp;
|
|
ship *goal_shipp;
|
|
if (aip->goal_objnum >= 0)
|
|
{
|
|
goal_objp = &Objects[aip->goal_objnum];
|
|
Assert(goal_objp->type == OBJ_SHIP);
|
|
goal_shipp = &Ships[goal_objp->instance];
|
|
}
|
|
else
|
|
{
|
|
goal_objp = nullptr;
|
|
goal_shipp = nullptr;
|
|
}
|
|
|
|
// get the indexes from the saved parameters
|
|
int docker_index = aip->submode_parm0;
|
|
int dockee_index = aip->submode_parm1;
|
|
|
|
// undo all the appropriate triggers
|
|
switch (aip->submode)
|
|
{
|
|
case AIS_UNDOCK_0:
|
|
case AIS_UNDOCK_1:
|
|
case AIS_UNDOCK_2:
|
|
ai_dock_do_animations(shipp, aip->submode, AIS_UNDOCK_3, docker_index);
|
|
ai_dock_do_animations(goal_shipp, aip->submode, AIS_UNDOCK_3, dockee_index);
|
|
break;
|
|
case AIS_DOCK_4:
|
|
case AIS_DOCK_4A:
|
|
case AIS_DOCK_3:
|
|
case AIS_DOCK_2:
|
|
ai_dock_do_animations(shipp, aip->submode, AIS_DOCK_1, docker_index);
|
|
ai_dock_do_animations(goal_shipp, aip->submode, AIS_DOCK_1, dockee_index);
|
|
break;
|
|
}
|
|
|
|
aip->submode = AIS_UNDOCK_3;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
}
|
|
|
|
// Goober5000 - clean up the dock mode of everybody around me
|
|
// (This function should ONLY need to be called from a ship doing a deathroll.
|
|
// It ensures that any ship docking or undocking with it will finish gracefully.)
|
|
void ai_cleanup_dock_mode_objective(object *objp)
|
|
{
|
|
// process all directly docked objects
|
|
for (dock_instance *ptr = objp->dock_list; ptr != NULL; ptr = ptr->next)
|
|
ai_cleanup_dock_mode_subjective(ptr->docked_objp);
|
|
}
|
|
|
|
// Goober5000
|
|
// (This function should ONLY need to be called from a ship doing a deathroll. [Addendum - or ai_ship_destroy().]
|
|
// It ensures that any support ship stuff will be wrapped up gracefully.)
|
|
void ai_cleanup_rearm_mode(object *objp, bool deathroll)
|
|
{
|
|
ai_info *aip = &Ai_info[Ships[objp->instance].ai_index];
|
|
|
|
auto how = deathroll ? REPAIR_INFO_ABORT : REPAIR_INFO_END;
|
|
|
|
if (aip->ai_flags[AI::AI_Flags::Repairing]) {
|
|
Assert( aip->goal_objnum != -1 );
|
|
ai_do_objects_repairing_stuff( &Objects[aip->goal_objnum], objp, deathroll ? REPAIR_INFO_KILLED : REPAIR_INFO_END);
|
|
} else if ( aip->ai_flags[AI::AI_Flags::Being_repaired] ) {
|
|
// MWA/Goober5000 -- note that we have to use support object here instead of goal_objnum.
|
|
Assert( aip->support_ship_objnum != -1 );
|
|
ai_do_objects_repairing_stuff( objp, &Objects[aip->support_ship_objnum], how );
|
|
} else if ( aip->ai_flags[AI::AI_Flags::Awaiting_repair] ) {
|
|
// MWA/Goober5000 -- note that we have to use support object here instead of goal_objnum.
|
|
// MWA -- 3/38/98 Check to see if this guy is queued for a support ship, or there is already
|
|
// one in the mission
|
|
if ( mission_is_repair_scheduled(objp) ) {
|
|
mission_remove_scheduled_repair( objp ); // this function will notify multiplayer clients.
|
|
} else {
|
|
if ( aip->support_ship_objnum != -1 )
|
|
ai_do_objects_repairing_stuff( objp, &Objects[aip->support_ship_objnum], how );
|
|
else
|
|
ai_do_objects_repairing_stuff( objp, NULL, how );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Make Pl_objp point at aip->goal_point.
|
|
void ai_still()
|
|
{
|
|
ship *shipp;
|
|
ai_info *aip;
|
|
|
|
Assert(Pl_objp->type == OBJ_SHIP);
|
|
Assert((Pl_objp->instance >= 0) && (Pl_objp->instance < MAX_OBJECTS));
|
|
|
|
shipp = &Ships[Pl_objp->instance];
|
|
Assert((shipp->ai_index >= 0) && (shipp->ai_index < MAX_AI_INFO));
|
|
|
|
aip = &Ai_info[shipp->ai_index];
|
|
|
|
turn_towards_point(Pl_objp, &aip->goal_point, NULL, 0.0f);
|
|
}
|
|
|
|
// Make *Pl_objp stay near another ship.
|
|
void ai_stay_near()
|
|
{
|
|
auto aip = &Ai_info[Ships[Pl_objp->instance].ai_index];
|
|
int goal_objnum = aip->goal_objnum;
|
|
|
|
if ((goal_objnum < 0) || (Objects[goal_objnum].type != OBJ_SHIP) || (Objects[goal_objnum].signature != aip->goal_signature)) {
|
|
aip->mode = AIM_NONE;
|
|
}
|
|
// "escort" behavior
|
|
else if (aip->submode_parm0 != 0) {
|
|
vec3d rand_vec, center_to_goal, center_to_objp;
|
|
auto goal_objp = &Objects[goal_objnum];
|
|
float max_dist = std::max(aip->submode_float0, 1.0f);
|
|
|
|
// Figure out a goal point to fly to
|
|
// Make not all ships pursue same point.
|
|
// Calculate the seed from both shipnums so the same Pl_objp doesn't always choose the same spot
|
|
static_randvec((goal_objp->instance * MAX_SHIPS) + (Pl_objp->instance), &rand_vec);
|
|
aip->goal_point = goal_objp->pos + max_dist * rand_vec;
|
|
|
|
// Make sure point is on the near side of goal_objp (relative to Pl_objp's position).
|
|
center_to_goal = aip->goal_point - goal_objp->pos;
|
|
center_to_objp = Pl_objp->pos - goal_objp->pos;
|
|
if (vm_vec_dot(¢er_to_goal, ¢er_to_objp) < 0.0f) {
|
|
vm_vec_negate(&rand_vec);
|
|
aip->goal_point = goal_objp->pos + max_dist * rand_vec;
|
|
}
|
|
|
|
// Fly to the point
|
|
auto dist = vm_vec_dist_quick(&goal_objp->pos, &Pl_objp->pos);
|
|
if (dist > max_dist) {
|
|
turn_towards_point(Pl_objp, &aip->goal_point, nullptr, 0.0f);
|
|
accelerate_ship(aip, dist / max_dist - 0.8f);
|
|
|
|
float dot = vm_vec_dot_to_point(&Pl_objp->orient.vec.fvec, &Pl_objp->pos, &aip->goal_point);
|
|
if (ai_willing_to_afterburn_hard(aip) && dot > 0.99f && dist - max_dist > 700.0f)
|
|
ai_afterburn_hard(Pl_objp, aip);
|
|
}
|
|
}
|
|
// standard behavior
|
|
else {
|
|
vec3d rand_vec, goal_pos, vec_to_goal;
|
|
auto goal_objp = &Objects[goal_objnum];
|
|
float max_dist = std::max(aip->submode_float0, 1.0f);
|
|
|
|
// Make not all ships pursue same point.
|
|
// Calculate the seed from both shipnums so the same Pl_objp doesn't always choose the same spot
|
|
static_randvec((goal_objp->instance * MAX_SHIPS) + (Pl_objp->instance), &rand_vec);
|
|
|
|
// Make sure point is in front hemisphere (relative to Pl_objp's position.
|
|
vm_vec_sub(&vec_to_goal, &goal_objp->pos, &Pl_objp->pos);
|
|
if (vm_vec_dot(&rand_vec, &vec_to_goal) > 1.0f) {
|
|
vm_vec_negate(&rand_vec);
|
|
}
|
|
|
|
// Scale the random vector by an amount proportional to the distance from Pl_objp to the true goal.
|
|
float dist = vm_vec_dist_quick(&goal_objp->pos, &Pl_objp->pos);
|
|
float scale = dist - max_dist/2;
|
|
if (scale < 0.0f)
|
|
scale = 0.0f;
|
|
vm_vec_scale_add(&goal_pos, &goal_objp->pos, &rand_vec, scale);
|
|
|
|
// Fly to the point
|
|
if (dist > max_dist) {
|
|
turn_towards_point(Pl_objp, &goal_pos, nullptr, 0.0f);
|
|
accelerate_ship(aip, dist / max_dist - 0.8f);
|
|
|
|
float dot = vm_vec_dot_to_point(&Pl_objp->orient.vec.fvec, &Pl_objp->pos, &goal_pos);
|
|
if (ai_willing_to_afterburn_hard(aip) && dot > 0.99f && dist - max_dist > 700.0f)
|
|
ai_afterburn_hard(Pl_objp, aip);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Warn player if dock path is obstructed.
|
|
int maybe_dock_obstructed(object *cur_objp, object *goal_objp, int big_only_flag)
|
|
{
|
|
vec3d *goalpos, *curpos;
|
|
float radius;
|
|
ai_info *aip;
|
|
int collide_objnum;
|
|
|
|
aip = &Ai_info[Ships[cur_objp->instance].ai_index];
|
|
|
|
Ai_info[Ships[goal_objp->instance].ai_index].ai_flags.remove(AI::AI_Flags::Repair_obstructed);
|
|
|
|
if (goal_objp != Player_obj)
|
|
return -1;
|
|
|
|
curpos = &cur_objp->pos;
|
|
radius = cur_objp->radius;
|
|
goalpos = &Path_points[aip->path_cur].pos;
|
|
collide_objnum = pp_collide_any(curpos, goalpos, radius, cur_objp, goal_objp, big_only_flag);
|
|
|
|
if (collide_objnum != -1)
|
|
Ai_info[Ships[goal_objp->instance].ai_index].ai_flags.set(AI::AI_Flags::Repair_obstructed);
|
|
|
|
return collide_objnum;
|
|
}
|
|
|
|
|
|
// Docking behavior.
|
|
// Approach a ship, follow path to docking platform, approach platform; after awhile,
|
|
// undock.
|
|
void ai_dock()
|
|
{
|
|
ship *shipp = &Ships[Pl_objp->instance];
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
|
|
// Make sure we still have a dock goal.
|
|
// Make sure the object we're supposed to dock with or undock from still exists.
|
|
if ( ((aip->active_goal < 0) && (aip->submode != AIS_DOCK_4A))
|
|
|| (aip->goal_objnum < 0)
|
|
|| (Objects[aip->goal_objnum].signature != aip->goal_signature) )
|
|
{
|
|
ai_cleanup_dock_mode_subjective(Pl_objp);
|
|
}
|
|
|
|
ship_info *sip = &Ship_info[shipp->ship_info_index];
|
|
|
|
// we need to keep the dock indexes stored in the submode because the goal may become invalid at any point
|
|
// (when we first dock or first undock, we'll calculate and overwrite these for the first time)
|
|
int docker_index = aip->submode_parm0;
|
|
int dockee_index = aip->submode_parm1;
|
|
|
|
// get the active goal
|
|
ai_goal *aigp;
|
|
if (aip->active_goal >= 0)
|
|
aigp = &aip->goals[aip->active_goal];
|
|
else
|
|
aigp = NULL;
|
|
|
|
// get the object being acted upon
|
|
object *goal_objp;
|
|
ship *goal_shipp;
|
|
if (aip->goal_objnum >= 0)
|
|
{
|
|
goal_objp = &Objects[aip->goal_objnum];
|
|
Assert(goal_objp->type == OBJ_SHIP);
|
|
goal_shipp = &Ships[goal_objp->instance];
|
|
}
|
|
else
|
|
{
|
|
goal_objp = NULL;
|
|
goal_shipp = NULL;
|
|
}
|
|
|
|
|
|
// For docking submodes (ie, not undocking), follow path. Once at second last
|
|
// point on path (point just before point on dock platform), orient into position.
|
|
//
|
|
// For undocking, first mode pushes docked ship straight back from docking point
|
|
// second mode turns ship and moves to point on docking radius
|
|
switch (aip->submode)
|
|
{
|
|
|
|
// This mode means to find the path to the docking point.
|
|
case AIS_DOCK_0:
|
|
{
|
|
// save the dock indexes we're currently using
|
|
ai_get_dock_goal_indexes(Pl_objp, aip, aigp, goal_objp, docker_index, dockee_index);
|
|
aip->submode_parm0 = docker_index;
|
|
aip->submode_parm1 = dockee_index;
|
|
|
|
ai_path();
|
|
if (aip->path_length < 4)
|
|
{
|
|
Assert(goal_objp != nullptr);
|
|
ship_info *goal_sip = &Ship_info[goal_shipp->ship_info_index];
|
|
char *goal_ship_class_name = goal_sip->name;
|
|
char *goal_dock_path_name = model_get(goal_sip->model_num)->paths[aip->mp_index].name;
|
|
|
|
mprintf(("Ship class %s has only %i points on dock path \"%s\". Recommended minimum number of points is 4. "\
|
|
"Docking along that path will look strange. You may wish to edit the model.\n", goal_ship_class_name, aip->path_length, goal_dock_path_name));
|
|
}
|
|
|
|
aip->submode = AIS_DOCK_1;
|
|
aip->submode_start_time = Missiontime;
|
|
ai_dock_do_animations(shipp, AIS_DOCK_0, AIS_DOCK_1, docker_index);
|
|
ai_dock_do_animations(goal_shipp, AIS_DOCK_0, AIS_DOCK_1, dockee_index);
|
|
|
|
aip->path_start = -1;
|
|
break;
|
|
}
|
|
|
|
// This mode means to follow the path until just before the end.
|
|
case AIS_DOCK_1:
|
|
{
|
|
int r;
|
|
|
|
if ((r = maybe_dock_obstructed(Pl_objp, goal_objp, 1)) != -1) {
|
|
int r1;
|
|
if ((r1 = maybe_avoid_big_ship(Pl_objp, goal_objp, aip, &goal_objp->pos, 7.0f)) != 0) {
|
|
nprintf(("AI", "Support ship %s avoiding large ship %s\n", Ships[Pl_objp->instance].ship_name, Ships[Objects[r1].instance].ship_name));
|
|
break;
|
|
}
|
|
} //else {
|
|
{
|
|
ai_path();
|
|
|
|
if (aip->path_cur-aip->path_start >= aip->path_length-1) { // If got this far, advance no matter what.
|
|
aip->submode = AIS_DOCK_2;
|
|
aip->submode_start_time = Missiontime;
|
|
|
|
ai_dock_do_animations(shipp, AIS_DOCK_1, AIS_DOCK_2, docker_index);
|
|
ai_dock_do_animations(goal_shipp, AIS_DOCK_1, AIS_DOCK_2, dockee_index);
|
|
|
|
aip->path_cur--;
|
|
Assert(aip->path_cur-aip->path_start >= 0);
|
|
} else if (aip->path_cur-aip->path_start >= aip->path_length-2) {
|
|
if (Pl_objp->phys_info.speed > goal_objp->phys_info.speed + 1.5f) {
|
|
set_accel_for_target_speed(Pl_objp, goal_objp->phys_info.speed);
|
|
} else {
|
|
aip->submode = AIS_DOCK_2;
|
|
aip->submode_start_time = Missiontime;
|
|
|
|
ai_dock_do_animations(shipp, AIS_DOCK_1, AIS_DOCK_2, docker_index);
|
|
ai_dock_do_animations(goal_shipp, AIS_DOCK_1, AIS_DOCK_2, dockee_index);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
// This mode means to drag oneself right to the second last point on the path.
|
|
// Path code allows it to overshoot.
|
|
case AIS_DOCK_2:
|
|
{
|
|
float dist;
|
|
int r;
|
|
|
|
if ((r = maybe_dock_obstructed(Pl_objp, goal_objp, 0)) != -1) {
|
|
nprintf(("AI", "Dock 2: Obstructed by %s\n", Ships[Objects[r].instance].ship_name));
|
|
accelerate_ship(aip, 0.0f);
|
|
|
|
aip->submode = AIS_DOCK_1;
|
|
aip->submode_start_time = Missiontime;
|
|
|
|
ai_dock_do_animations(shipp, AIS_DOCK_2, AIS_DOCK_1, docker_index);
|
|
ai_dock_do_animations(goal_shipp, AIS_DOCK_2, AIS_DOCK_1, dockee_index);
|
|
} else {
|
|
dist = dock_orient_and_approach(Pl_objp, docker_index, goal_objp, dockee_index, DOA_APPROACH);
|
|
Assert(dist != UNINITIALIZED_VALUE);
|
|
|
|
float tolerance;
|
|
if (goal_objp->flags[Object::Object_Flags::Player_ship])
|
|
tolerance = 6*flFrametime + 1.0f;
|
|
else
|
|
tolerance = 4*flFrametime + 0.5f;
|
|
|
|
if ( dist < tolerance) {
|
|
aip->submode = AIS_DOCK_3;
|
|
aip->submode_start_time = Missiontime;
|
|
|
|
ai_dock_do_animations(shipp, AIS_DOCK_2, AIS_DOCK_3, docker_index);
|
|
ai_dock_do_animations(goal_shipp, AIS_DOCK_2, AIS_DOCK_3, dockee_index);
|
|
|
|
aip->path_cur++;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case AIS_DOCK_3:
|
|
{
|
|
Assert(aip->goal_objnum != -1);
|
|
int r;
|
|
|
|
if ((r = maybe_dock_obstructed(Pl_objp, goal_objp,0)) != -1) {
|
|
nprintf(("AI", "Dock 1: Obstructed by %s\n", Ships[Objects[r].instance].ship_name));
|
|
accelerate_ship(aip, 0.0f);
|
|
|
|
aip->submode = AIS_DOCK_2;
|
|
aip->submode_start_time = Missiontime;
|
|
|
|
ai_dock_do_animations(shipp, AIS_DOCK_3, AIS_DOCK_2, docker_index);
|
|
ai_dock_do_animations(goal_shipp, AIS_DOCK_3, AIS_DOCK_2, dockee_index);
|
|
} else {
|
|
rotating_dockpoint_info rdinfo;
|
|
|
|
float dist = dock_orient_and_approach(Pl_objp, docker_index, goal_objp, dockee_index, DOA_DOCK, &rdinfo);
|
|
Assert(dist != UNINITIALIZED_VALUE);
|
|
|
|
float tolerance = 2*flFrametime * (1.0f + fl_sqrt(goal_objp->phys_info.speed));
|
|
|
|
// Goober5000
|
|
if (rdinfo.submodel >= 0)
|
|
{
|
|
tolerance += 4*flFrametime * (rdinfo.submodel_r * rdinfo.submodel_w);
|
|
}
|
|
|
|
if (dist < tolerance)
|
|
{
|
|
// - Removed by MK on 11/7/97, causes errors for ships docked at mission start: maybe_recreate_path(Pl_objp, aip, 1);
|
|
dist = dock_orient_and_approach(Pl_objp, docker_index, goal_objp, dockee_index, DOA_DOCK);
|
|
Assert(dist != UNINITIALIZED_VALUE);
|
|
|
|
physics_ship_init(Pl_objp);
|
|
|
|
ai_do_objects_docked_stuff( Pl_objp, docker_index, goal_objp, dockee_index );
|
|
|
|
if (aip->submode == AIS_DOCK_3) {
|
|
// Play a ship docking attach sound
|
|
snd_play_3d( gamesnd_get_game_sound(GameSounds::DOCK_ATTACH), &Pl_objp->pos, &View_position );
|
|
|
|
// start the dock animation
|
|
ai_dock_do_animations(shipp, AIS_DOCK_3, AIS_DOCK_4, docker_index);
|
|
ai_dock_do_animations(goal_shipp, AIS_DOCK_3, AIS_DOCK_4, dockee_index);
|
|
|
|
if ((Pl_objp == Player_obj) || (goal_objp == Player_obj))
|
|
joy_ff_docked(); // shake player's joystick a little
|
|
}
|
|
|
|
// If this ship is repairing another ship...
|
|
if (aip->ai_flags[AI::AI_Flags::Repairing]) {
|
|
aip->submode = AIS_DOCK_4; // Special rearming only dock mode.
|
|
aip->submode_start_time = Missiontime;
|
|
} else {
|
|
aip->submode = AIS_DOCK_4A;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Yes, we just sit here. We wait for further orders. No, it's not a bug.
|
|
case AIS_DOCK_4A:
|
|
{
|
|
if (aigp == NULL) { // Can happen for initially docked ships.
|
|
// this now "just sits here" for docked ships
|
|
ai_do_default_behavior( &Objects[Ships[aip->shipnum].objnum] ); // do the default behavior
|
|
} else {
|
|
mission_log_add_entry(LOG_SHIP_DOCKED, shipp->ship_name, goal_shipp->ship_name);
|
|
|
|
if (aigp->ai_mode == AI_GOAL_DOCK) {
|
|
ai_mission_goal_complete( aip ); // Note, this calls ai_do_default_behavior().
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case AIS_DOCK_4:
|
|
{
|
|
// This mode is only for rearming/repairing.
|
|
// The ship that is performing the rearm enters this mode after it docks.
|
|
Assert((aip->goal_objnum >= -1) && (aip->goal_objnum < MAX_OBJECTS));
|
|
|
|
float dist = dock_orient_and_approach(Pl_objp, docker_index, goal_objp, dockee_index, DOA_DOCK);
|
|
Assert(dist != UNINITIALIZED_VALUE);
|
|
|
|
ai_info *goal_aip = &Ai_info[goal_shipp->ai_index];
|
|
|
|
// Goober5000 - moved from call_doa
|
|
// Abort if the ship being repaired exceeds my max speed
|
|
if (goal_objp->phys_info.speed > MAX_REPAIR_SPEED)
|
|
{
|
|
// call the ai_abort rearm request code
|
|
ai_abort_rearm_request( Pl_objp );
|
|
// Goober5000 - add missionlog for support ships, per Mantis #2999
|
|
mission_log_add_entry(LOG_SHIP_UNDOCKED, shipp->ship_name, goal_shipp->ship_name);
|
|
}
|
|
// Make sure repair has not broken off.
|
|
else if (dist > 5.0f) // Oops, too far away!
|
|
{
|
|
if ( goal_aip->ai_flags[AI::AI_Flags::Being_repaired] ) {
|
|
ai_do_objects_repairing_stuff( goal_objp, Pl_objp, REPAIR_INFO_BROKEN);
|
|
// Goober5000 - add missionlog for support ships, per Mantis #2999
|
|
mission_log_add_entry(LOG_SHIP_UNDOCKED, shipp->ship_name, goal_shipp->ship_name);
|
|
}
|
|
|
|
if (dist > Pl_objp->radius*2 + goal_objp->radius*2) {
|
|
// Got real far away from goal, so move back a couple modes and try again.
|
|
aip->submode = AIS_DOCK_2;
|
|
aip->submode_start_time = Missiontime;
|
|
|
|
ai_dock_do_animations(shipp, AIS_DOCK_4, AIS_DOCK_2, docker_index);
|
|
ai_dock_do_animations(goal_shipp, AIS_DOCK_4, AIS_DOCK_2, dockee_index);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( goal_aip->ai_flags[AI::AI_Flags::Awaiting_repair] ) {
|
|
ai_do_objects_repairing_stuff( goal_objp, Pl_objp, REPAIR_INFO_BEGIN );
|
|
// Goober5000 - add missionlog for support ships, per Mantis #2999
|
|
mission_log_add_entry(LOG_SHIP_DOCKED, shipp->ship_name, goal_shipp->ship_name);
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case AIS_UNDOCK_0:
|
|
{
|
|
// First stage of undocking.
|
|
int path_num;
|
|
|
|
// If this is the first frame for this submode, play the animation and set the timestamp
|
|
if (aip->mode_time < 0)
|
|
{
|
|
// save the dock indexes we're currently using
|
|
ai_get_dock_goal_indexes(Pl_objp, aip, aigp, goal_objp, docker_index, dockee_index);
|
|
aip->submode_parm0 = docker_index;
|
|
aip->submode_parm1 = dockee_index;
|
|
|
|
// start the detach animation (opposite of the dock animation)
|
|
Assert(goal_objp != nullptr);
|
|
|
|
ai_dock_do_animations(shipp, AIS_UNDOCK_0, AIS_UNDOCK_1, docker_index);
|
|
ai_dock_do_animations(goal_shipp, AIS_UNDOCK_0, AIS_UNDOCK_1, dockee_index);
|
|
|
|
// calculate time until animations elapse
|
|
int time = sip->animations.getAll(model_get_instance(shipp->model_instance_num), animation::ModelAnimationTriggerType::Docked, docker_index).getTime();
|
|
|
|
if (goal_shipp != nullptr) {
|
|
ship_info* goal_sip = &Ship_info[goal_shipp->ship_info_index];
|
|
int time2 = goal_sip->animations.getAll(model_get_instance(goal_shipp->model_instance_num), animation::ModelAnimationTriggerType::Docked, dockee_index).getTime();
|
|
time = MAX(time, time2);
|
|
}
|
|
aip->mode_time = timestamp(time);
|
|
}
|
|
|
|
// if not enough time has passed, just wait
|
|
if (!timestamp_elapsed(aip->mode_time))
|
|
break;
|
|
|
|
// clear timestamp
|
|
aip->mode_time = -1;
|
|
|
|
// set up the path points for the undocking procedure
|
|
path_num = ai_return_path_num_from_dockbay(goal_objp, dockee_index);
|
|
Assert(path_num >= 0);
|
|
ai_find_path(Pl_objp, OBJ_INDEX(goal_objp), path_num, 0);
|
|
|
|
// Play a ship docking detach sound
|
|
snd_play_3d( gamesnd_get_game_sound(GameSounds::DOCK_DETACH), &Pl_objp->pos, &View_position );
|
|
|
|
aip->submode = AIS_UNDOCK_1;
|
|
aip->submode_start_time = Missiontime;
|
|
break;
|
|
}
|
|
|
|
case AIS_UNDOCK_1:
|
|
{
|
|
// Using thrusters, exit from dock station to nearest next dock path point.
|
|
float dist;
|
|
rotating_dockpoint_info rdinfo;
|
|
|
|
// Waiting for one second to elapse to let detach sound effect play out.
|
|
if (Missiontime - aip->submode_start_time < REARM_BREAKOFF_DELAY)
|
|
break;
|
|
|
|
// play the depart sound, but only once, since this mode is called multiple times per frame
|
|
if ( !(aigp->flags[AI::Goal_Flags::Depart_sound_played]))
|
|
{
|
|
snd_play_3d( gamesnd_get_game_sound(GameSounds::DOCK_DEPART), &Pl_objp->pos, &View_position );
|
|
aigp->flags.set(AI::Goal_Flags::Depart_sound_played);
|
|
}
|
|
|
|
dist = dock_orient_and_approach(Pl_objp, docker_index, goal_objp, dockee_index, DOA_UNDOCK_1, &rdinfo);
|
|
Assert(dist != UNINITIALIZED_VALUE);
|
|
|
|
float dist_to_dock;
|
|
|
|
// Goober5000 - if via submodel, calc distance to point, not center
|
|
if (rdinfo.submodel >= 0)
|
|
dist_to_dock = vm_vec_dist_quick(&Pl_objp->pos, &rdinfo.dockee_point);
|
|
else
|
|
dist_to_dock = vm_vec_dist_quick(&Pl_objp->pos, &goal_objp->pos);
|
|
|
|
// Move to within 0.1 units of second last point on path before orienting, or just plain far away from docked-to ship.
|
|
// This allows undock to complete if first ship flies away.
|
|
if ((dist < 2*flFrametime) || (dist_to_dock > 2*Pl_objp->radius)) {
|
|
aip->submode = AIS_UNDOCK_2;
|
|
aip->submode_start_time = Missiontime;
|
|
|
|
ai_dock_do_animations(shipp, AIS_UNDOCK_1, AIS_UNDOCK_2, docker_index);
|
|
ai_dock_do_animations(goal_shipp, AIS_UNDOCK_1, AIS_UNDOCK_2, dockee_index);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case AIS_UNDOCK_2:
|
|
{
|
|
float dist;
|
|
|
|
// get pointer to docked object's aip to reset flags, etc
|
|
Assert( aip->goal_objnum != -1 );
|
|
|
|
// Second stage of undocking.
|
|
dist = dock_orient_and_approach(Pl_objp, docker_index, goal_objp, dockee_index, DOA_UNDOCK_2);
|
|
Assert(dist != UNINITIALIZED_VALUE);
|
|
|
|
// If at goal point, or quite far away from dock object
|
|
// NOTE: the speed check has an etra 5 thousandths added on to account for some floating point error
|
|
if ((dist < 2.0f) || (vm_vec_dist_quick(&Pl_objp->pos, &goal_objp->pos) > (Pl_objp->radius + goal_objp->radius)*2) || ((goal_objp->phys_info.speed + 0.005f) > MAX_UNDOCK_ABORT_SPEED) ) {
|
|
// reset the dock flags. If rearm/repair, reset rearm repair flags for those ships as well.
|
|
if ( sip->flags[Ship::Info_Flags::Support] ) {
|
|
ai_do_objects_repairing_stuff( &Objects[aip->support_ship_objnum], Pl_objp, REPAIR_INFO_END );
|
|
}
|
|
|
|
// clear out dock stuff for both objects.
|
|
ai_do_objects_undocked_stuff( Pl_objp, goal_objp );
|
|
physics_ship_init(Pl_objp);
|
|
|
|
aip->submode = AIS_UNDOCK_3;
|
|
aip->submode_start_time = Missiontime;
|
|
|
|
ai_dock_do_animations(shipp, AIS_UNDOCK_2, AIS_UNDOCK_3, docker_index);
|
|
ai_dock_do_animations(goal_shipp, AIS_UNDOCK_2, AIS_UNDOCK_3, dockee_index);
|
|
|
|
// don't add undock log entries for support ships.
|
|
// Goober5000 - deliberately contravening Volition's above comment - add missionlog for support ships, per Mantis #2999
|
|
mission_log_add_entry(LOG_SHIP_UNDOCKED, shipp->ship_name, goal_shipp->ship_name);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case AIS_UNDOCK_3:
|
|
{
|
|
if (goal_objp == NULL)
|
|
{
|
|
// this might happen when a goal is cancelled before docking has finished
|
|
aip->submode = AIS_UNDOCK_4;
|
|
aip->submode_start_time = Missiontime;
|
|
|
|
ai_dock_do_animations(shipp, AIS_UNDOCK_3, AIS_UNDOCK_4, docker_index);
|
|
ai_dock_do_animations(goal_shipp, AIS_UNDOCK_3, AIS_UNDOCK_4, dockee_index);
|
|
}
|
|
else
|
|
{
|
|
float dist = dock_orient_and_approach(Pl_objp, docker_index, goal_objp, dockee_index, DOA_UNDOCK_3);
|
|
Assert(dist != UNINITIALIZED_VALUE);
|
|
|
|
if (dist < Pl_objp->radius/2 + 5.0f) {
|
|
aip->submode = AIS_UNDOCK_4;
|
|
aip->submode_start_time = Missiontime;
|
|
|
|
ai_dock_do_animations(shipp, AIS_UNDOCK_3, AIS_UNDOCK_4, docker_index);
|
|
ai_dock_do_animations(goal_shipp, AIS_UNDOCK_3, AIS_UNDOCK_4, dockee_index);
|
|
}
|
|
|
|
// possible that this flag hasn't been cleared yet. When aborting a rearm, this submode might
|
|
// be entered directly.
|
|
if ( (sip->flags[Ship::Info_Flags::Support]) && (aip->ai_flags[AI::AI_Flags::Repairing]) ) {
|
|
ai_do_objects_repairing_stuff( goal_objp, Pl_objp, REPAIR_INFO_ABORT );
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case AIS_UNDOCK_4:
|
|
{
|
|
|
|
aip->mode = AIM_NONE;
|
|
|
|
// only call mission goal complete if this was indeed an undock goal
|
|
if ( aip->active_goal >= 0 ) {
|
|
if ( aigp->ai_mode == AI_GOAL_UNDOCK )
|
|
ai_mission_goal_complete( aip ); // this call should reset the AI mode
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
{
|
|
UNREACHABLE("Illegal dock submode of %d in ai_dock. Pleae report to the SCP!", aip->submode);
|
|
}
|
|
|
|
} // end of switch statement
|
|
}
|
|
|
|
#ifndef NDEBUG
|
|
|
|
#define MAX_AI_DEBUG_RENDER_STUFF 100
|
|
typedef struct ai_render_stuff {
|
|
ship_subsys *ss;
|
|
int parent_objnum;
|
|
} ai_render_stuff;
|
|
|
|
ai_render_stuff AI_debug_render_stuff[MAX_AI_DEBUG_RENDER_STUFF];
|
|
|
|
int Num_AI_debug_render_stuff = 0;
|
|
|
|
void ai_debug_render_stuff()
|
|
{
|
|
|
|
vertex vert1, vert2;
|
|
vec3d gpos2;
|
|
int i;
|
|
|
|
for (i=0; i<Num_AI_debug_render_stuff; i++) {
|
|
ship_subsys *ss;
|
|
int parent_objnum;
|
|
vec3d gpos, gvec;
|
|
model_subsystem *tp;
|
|
|
|
ss = AI_debug_render_stuff[i].ss;
|
|
tp = ss->system_info;
|
|
|
|
parent_objnum = AI_debug_render_stuff[i].parent_objnum;
|
|
|
|
ship_get_global_turret_info(&Objects[parent_objnum], tp, &gpos, &gvec);
|
|
g3_rotate_vertex(&vert1, &gpos);
|
|
vm_vec_scale_add(&gpos2, &gpos, &gvec, 20.0f);
|
|
g3_rotate_vertex(&vert2, &gpos2);
|
|
gr_set_color(0, 0, 255);
|
|
g3_draw_sphere(&vert1, 2.0f);
|
|
gr_set_color(255, 0, 255);
|
|
g3_draw_sphere(&vert2, 2.0f);
|
|
g3_draw_line(&vert1, &vert2);
|
|
}
|
|
|
|
Num_AI_debug_render_stuff = 0;
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Process whatever AI behavior is associated with subobjects of object objnum.
|
|
// Deal with engines disabled.
|
|
void ai_process_subobjects(int objnum)
|
|
{
|
|
ship_subsys *pss;
|
|
object *objp = &Objects[objnum];
|
|
ship *shipp = &Ships[objp->instance];
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
ship_info *sip = &Ship_info[shipp->ship_info_index];
|
|
|
|
bool in_lab = gameseq_get_state() == GS_STATE_LAB;
|
|
|
|
// non-player ships that are playing dead do not process subsystems or turrets unless we're in the lab
|
|
if (gameseq_get_state() != GS_STATE_LAB) {
|
|
if ((!(objp->flags[Object::Object_Flags::Player_ship]) || Player_use_ai) && aip->mode == AIM_PLAY_DEAD)
|
|
return;
|
|
}
|
|
|
|
polymodel_instance *pmi = model_get_instance(shipp->model_instance_num);
|
|
polymodel *pm = model_get(pmi->model_num);
|
|
|
|
for ( pss = GET_FIRST(&shipp->subsys_list); pss !=END_OF_LIST(&shipp->subsys_list); pss = GET_NEXT(pss) ) {
|
|
auto psub = pss->system_info;
|
|
|
|
// Don't process destroyed objects (but allow subobjects with hitpoints disabled -nuke)
|
|
if (pss->max_hits > 0 && pss->current_hits <= 0.0f) {
|
|
continue;
|
|
}
|
|
|
|
switch (psub->type) {
|
|
case SUBSYSTEM_TURRET:
|
|
{ // brace so that we can declare a variable in a case statement.
|
|
|
|
// Don't process multipart turrets if we can't rotate.
|
|
if ((psub->subobj_num != psub->turret_gun_sobj) && (psub->turret_gun_sobj >= 0) && shipp->flags[Ship::Ship_Flags::Subsystem_movement_locked])
|
|
break;
|
|
|
|
int old_target = pss->turret_enemy_objnum;
|
|
|
|
// Don't process a turret for a ship being repaired, if the support ship is close
|
|
// (previously in ship_evaluate_ai (previously in ship_process_post))
|
|
// Cyborg -- Unfortunately Ai info is not reliable and should just not be accessed here.
|
|
// It will have no real effect on gameplay, since the server decides when turrets fire
|
|
if (!MULTIPLAYER_CLIENT && (aip->ai_flags[AI::AI_Flags::Being_repaired, AI::AI_Flags::Awaiting_repair]))
|
|
{
|
|
if (aip->support_ship_objnum >= 0)
|
|
{
|
|
if (vm_vec_dist_quick(&objp->pos, &Objects[aip->support_ship_objnum].pos) < (objp->radius + Objects[aip->support_ship_objnum].radius) * 1.25f)
|
|
break;
|
|
}
|
|
}
|
|
|
|
// we need to update client data here.
|
|
if (MULTIPLAYER_CLIENT) {
|
|
turret_packet network_turret_data;
|
|
|
|
if (Multi_Turret_Manager.get_latest_packet(objp->net_signature, ship_get_subsys_index(pss), &network_turret_data)) {
|
|
pss->info_from_server_stamp = network_turret_data.time;
|
|
pss->turret_enemy_objnum = network_turret_data.target_objnum;
|
|
pss->turret_enemy_sig = (pss->turret_enemy_objnum > -1) ? Objects[pss->turret_enemy_objnum].signature : 0;
|
|
|
|
// if the remote instance had a nullptr, first will be false. second holds the actual angle.
|
|
if (network_turret_data.angle1.first && pss->submodel_instance_1 != nullptr){
|
|
pss->submodel_instance_1->cur_angle = network_turret_data.angle1.second;
|
|
}
|
|
|
|
if (network_turret_data.angle2.first && pss->submodel_instance_2 != nullptr){
|
|
pss->submodel_instance_2->cur_angle = network_turret_data.angle2.second;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// handle ending animations
|
|
if ( (pss->turret_animation_position == MA_POS_READY) && timestamp_elapsed(pss->turret_animation_done_time) ) {
|
|
//For legacy animations using subtype for turret number
|
|
bool started = (Ship_info[shipp->ship_info_index].animations.getAll(model_get_instance(shipp->model_instance_num), animation::ModelAnimationTriggerType::TurretFiring, pss->system_info->subobj_num, true)
|
|
//For modern animations using proper triggered-by-subsys name
|
|
+ Ship_info[shipp->ship_info_index].animations.get(model_get_instance(shipp->model_instance_num), animation::ModelAnimationTriggerType::TurretFiring, animation::anim_name_from_subsys(pss->system_info)))
|
|
.start(animation::ModelAnimationDirection::RWD);
|
|
|
|
if (started) {
|
|
pss->turret_animation_position = MA_POS_NOT_SET;
|
|
}
|
|
}
|
|
|
|
if ( psub->turret_num_firing_points > 0 )
|
|
{
|
|
if (!in_lab) {
|
|
ai_turret_execute_behavior(shipp, pss);
|
|
}
|
|
} else {
|
|
Warning( LOCATION, "Turret %s on ship %s has no firing points assigned to it.\nThis needs to be fixed in the model.\n", psub->name, shipp->ship_name );
|
|
}
|
|
|
|
// If we're not using FIT, the AI stuff will happen after the object and its submodels move,
|
|
// so in that case be sure to keep the turret submodels up to date
|
|
if (!Framerate_independent_turning) {
|
|
if (psub->subobj_num >= 0) {
|
|
model_replicate_submodel_instance(pm, pmi, psub->subobj_num, pss->flags);
|
|
}
|
|
|
|
if ((psub->subobj_num != psub->turret_gun_sobj) && (psub->turret_gun_sobj >= 0)) {
|
|
model_replicate_submodel_instance(pm, pmi, psub->turret_gun_sobj, pss->flags);
|
|
}
|
|
}
|
|
|
|
// For multi, if there's a new target for this turret on the server, send an update to clients.
|
|
if (MULTIPLAYER_MASTER && old_target != pss->turret_enemy_objnum){
|
|
Multi_Turret_Manager.add_packet_to_queue(objp->net_signature, ship_get_subsys_index(pss));
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
case SUBSYSTEM_ENGINE:
|
|
case SUBSYSTEM_NAVIGATION:
|
|
case SUBSYSTEM_COMMUNICATION:
|
|
case SUBSYSTEM_WEAPONS:
|
|
case SUBSYSTEM_SENSORS:
|
|
case SUBSYSTEM_UNKNOWN:
|
|
break;
|
|
|
|
case SUBSYSTEM_RADAR:
|
|
case SUBSYSTEM_SOLAR:
|
|
case SUBSYSTEM_GAS_COLLECT:
|
|
case SUBSYSTEM_ACTIVATION:
|
|
break;
|
|
default:
|
|
Error(LOCATION, "Illegal subsystem type %d.\n", psub->type);
|
|
}
|
|
|
|
// NOTE: Subsystem submodels are no longer rotated here. See ship_move_subsystems().
|
|
}
|
|
|
|
// Lab doesn't need to do anything else here
|
|
if (in_lab) {
|
|
return;
|
|
}
|
|
|
|
// Clients must not make actual AI changes! So bail here.
|
|
if (MULTIPLAYER_CLIENT) {
|
|
return;
|
|
}
|
|
|
|
// Deal with a ship with blown out engines.
|
|
if (ship_subsystems_blown(shipp, SUBSYSTEM_ENGINE)) {
|
|
// Karajorma - if Player_use_ai is ever fixed to work on multiplayer it should be checked that any player ships
|
|
// aren't under AI control here
|
|
if ((!(objp->flags[Object::Object_Flags::Player_ship])) && (sip->is_fighter_bomber()) && !(shipp->flags[Ship::Ship_Flags::Dying])) {
|
|
// Goober5000 - don't do anything if docked
|
|
if (!object_is_docked(objp)) {
|
|
// AL: Only attack forever if not trying to depart to a docking bay. Need to have this in, since
|
|
// a ship may get repaired... and it should still try to depart. Since docking bay departures
|
|
// are not handled as goals, we don't want to leave the AIM_BAY_DEPART mode.
|
|
if (aip->mode != AIM_BAY_DEPART) {
|
|
ai_attack_object(objp, nullptr); // Regardless of current mode, enter attack mode.
|
|
aip->submode = SM_ATTACK_FOREVER; // Never leave attack submode, don't avoid, evade, etc.
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Given an object and the wing it's in, return its index in the wing list.
|
|
// This defines its location in the wing formation.
|
|
// If the object can't be found in the wing, return -1.
|
|
// *objp object of interest
|
|
// wingnum the wing *objp is in
|
|
int get_wing_index(object *objp, int wingnum)
|
|
{
|
|
wing *wingp;
|
|
int i;
|
|
|
|
Assert((wingnum >= 0) && (wingnum < MAX_WINGS));
|
|
|
|
wingp = &Wings[wingnum];
|
|
|
|
for (i=wingp->current_count-1; i>=0; i--)
|
|
if ( objp->instance == wingp->ship_index[i] )
|
|
break;
|
|
|
|
return i; // Note, returns -1 if string not found.
|
|
}
|
|
|
|
// Given a wing, return a pointer to the object of its leader.
|
|
// Asserts if object not found.
|
|
// Currently, the wing leader is defined as the first object in the wing.
|
|
// wingnum Wing number in Wings array.
|
|
// If wing leader is disabled, swap it with another ship.
|
|
object *get_wing_leader(int wingnum)
|
|
{
|
|
wing *wingp;
|
|
int ship_num = 0;
|
|
|
|
if (wingnum < 0) {
|
|
return NULL;
|
|
}
|
|
|
|
Assert( wingnum < MAX_WINGS );
|
|
|
|
wingp = &Wings[wingnum];
|
|
|
|
Assert(wingp->current_count != 0); // Make sure there is a leader
|
|
|
|
ship_num = wingp->ship_index[0];
|
|
|
|
// If this ship is disabled, try another ship in the wing.
|
|
int n = 0;
|
|
while (!Ships[ship_num].flags[Ship::Ship_Flags::Maneuver_despite_engines] && ship_subsystems_blown(&Ships[ship_num], SUBSYSTEM_ENGINE)) {
|
|
n++;
|
|
|
|
if (n >= wingp->current_count) {
|
|
break;
|
|
}
|
|
|
|
ship_num = wingp->ship_index[n];
|
|
}
|
|
|
|
if ( (n != 0) && (n != wingp->current_count) ) {
|
|
int t = wingp->ship_index[0];
|
|
wingp->ship_index[0] = wingp->ship_index[n];
|
|
wingp->ship_index[n] = t;
|
|
}
|
|
|
|
return &Objects[Ships[ship_num].objnum];
|
|
}
|
|
|
|
#define DEFAULT_WING_X_DELTA 1.0f
|
|
#define DEFAULT_WING_Y_DELTA 0.25f
|
|
#define DEFAULT_WING_Z_DELTA 0.75f
|
|
#define DEFAULT_WING_MAG (fl_sqrt(DEFAULT_WING_X_DELTA*DEFAULT_WING_X_DELTA + DEFAULT_WING_Y_DELTA*DEFAULT_WING_Y_DELTA + DEFAULT_WING_Z_DELTA*DEFAULT_WING_Z_DELTA))
|
|
// next constant is higher that MAX_SHIPS_IN_WINGS to deal with forming on player's wing
|
|
#define MAX_FORMATION_ROWS 4
|
|
|
|
// Given a position in a wing, return the desired location of the ship relative to the leader
|
|
// *_delta_vec OUTPUT. delta vector based on wing_index
|
|
// wing_index position in wing.
|
|
void get_wing_delta(vec3d *_delta_vec, int wing_index)
|
|
{
|
|
int wi0;
|
|
|
|
Assert(wing_index >= 0);
|
|
|
|
int k, row, column;
|
|
|
|
int bank = wing_index / (MAX_FORMATION_ROWS*(MAX_FORMATION_ROWS+1)/2);
|
|
wi0 = wing_index % (MAX_FORMATION_ROWS * (MAX_FORMATION_ROWS+1)/2);
|
|
|
|
k = 0;
|
|
for (row=1; row<MAX_FORMATION_ROWS+1; row++) {
|
|
k += row;
|
|
if (wi0 < k)
|
|
break;
|
|
}
|
|
|
|
row--;
|
|
column = wi0 - k + row + 1;
|
|
|
|
_delta_vec->xyz.x = ((float) column - (float) row/2.0f) * DEFAULT_WING_X_DELTA/DEFAULT_WING_MAG;
|
|
_delta_vec->xyz.y = ((float)row + (float)bank*2.25f) * DEFAULT_WING_Y_DELTA/DEFAULT_WING_MAG;
|
|
_delta_vec->xyz.z = - ((float)row + 0.5f * (float) bank) * DEFAULT_WING_Z_DELTA/DEFAULT_WING_MAG;
|
|
|
|
}
|
|
|
|
/**
|
|
* Compute the largest radius of a ship in a *objp's wing.
|
|
*/
|
|
float gwlr_1(object *objp, int wingnum)
|
|
{
|
|
float max_radius;
|
|
object *o;
|
|
ship_obj *so;
|
|
|
|
Assert(wingnum >= 0);
|
|
|
|
max_radius = objp->radius;
|
|
|
|
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
|
|
o = &Objects[so->objnum];
|
|
if (o->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
if (Ships[o->instance].wingnum == wingnum)
|
|
if (o->radius > max_radius)
|
|
max_radius = o->radius;
|
|
}
|
|
|
|
return max_radius;
|
|
}
|
|
|
|
/**
|
|
* Compute the largest radius of a ship forming on *objp's wing.
|
|
*/
|
|
static float gwlr_object_1(object *objp)
|
|
{
|
|
float max_radius;
|
|
object *o;
|
|
ship_obj *so;
|
|
|
|
max_radius = objp->radius;
|
|
|
|
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
|
|
o = &Objects[so->objnum];
|
|
if (o->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
if (Ai_info[Ships[o->instance].ai_index].goal_objnum == OBJ_INDEX(objp))
|
|
if (o->radius > max_radius)
|
|
max_radius = o->radius;
|
|
}
|
|
|
|
return max_radius;
|
|
}
|
|
|
|
/**
|
|
* For the wing that *objp is part of, return the largest ship radius in that wing.
|
|
*/
|
|
float get_wing_largest_radius(object *objp, bool formation_object_flag)
|
|
{
|
|
ship *shipp;
|
|
|
|
Assert(objp->type == OBJ_SHIP || objp->type == OBJ_START);
|
|
Assert((objp->instance >= 0) && (objp->instance < MAX_OBJECTS));
|
|
shipp = &Ships[objp->instance];
|
|
|
|
if (formation_object_flag) {
|
|
return gwlr_object_1(objp);
|
|
} else {
|
|
return gwlr_1(objp, shipp->wingnum);
|
|
}
|
|
}
|
|
|
|
float Wing_y_scale = 2.0f;
|
|
float Wing_scale = 1.0f;
|
|
DCF(wing_y_scale, "Adjusts the wing formation scale along the Y axis (Default is 2.0)")
|
|
{
|
|
dc_stuff_float(&Wing_y_scale);
|
|
}
|
|
|
|
DCF(wing_scale, "Adjusts the wing formation scale. (Default is 1.0f)")
|
|
{
|
|
dc_stuff_float(&Wing_scale);
|
|
}
|
|
|
|
/**
|
|
* Given an object to fly off of, a wing and a position in the wing formation, return the desired absolute location to fly to.
|
|
* @return result in *result_pos.
|
|
*/
|
|
void get_absolute_wing_pos(vec3d *result_pos, object *leader_objp, int wingnum, int wing_index, bool formation_object_flag, bool autopilot)
|
|
{
|
|
vec3d wing_delta, rotated_wing_delta;
|
|
float wing_spread_size;
|
|
int formation_index = Wings[wingnum].formation;
|
|
|
|
if (wing_index == 0)
|
|
wing_delta = vmd_zero_vector;
|
|
else if (!Wing_formations.empty() && (formation_index >= 0) && (wing_index < MAX_SHIPS_PER_WING)) {
|
|
Assertion((int)Wing_formations.size() > formation_index, "%s wing's formation_index %d is higher than the Wing_formations size. Something went very wrong, go tell a coder!", Wings[wingnum].name, formation_index);
|
|
wing_delta = Wing_formations[formation_index].positions[wing_index - 1]; // custom desired location in leader's reference frame
|
|
}
|
|
else
|
|
get_wing_delta(&wing_delta, wing_index); // default desired location in leader's reference frame
|
|
|
|
wing_spread_size = MAX(50.0f, 3.0f * get_wing_largest_radius(leader_objp, formation_object_flag) + 15.0f);
|
|
|
|
// if not autopilot apply debug modifications to spread size and also y scale (2x default) unless it's a custom position
|
|
if ((leader_objp->flags[Object::Object_Flags::Player_ship] || leader_objp->type == OBJ_START) && !autopilot) {
|
|
if(formation_index == -1)
|
|
wing_delta.xyz.y *= Wing_y_scale;
|
|
wing_spread_size *= Wing_scale;
|
|
}
|
|
|
|
float scale_factor = autopilot ? 1.5f : (1.0f + leader_objp->phys_info.speed / 70.0f);
|
|
vm_vec_scale(&wing_delta, wing_spread_size * scale_factor * Wings[wingnum].formation_scale);
|
|
|
|
vm_vec_unrotate(&rotated_wing_delta, &wing_delta, &leader_objp->orient); // Rotate into leader's reference.
|
|
vm_vec_add(result_pos, &leader_objp->pos, &rotated_wing_delta); // goal_point is absolute 3-space point.
|
|
}
|
|
|
|
#ifndef NDEBUG
|
|
int Debug_render_wing_phantoms;
|
|
|
|
void render_wing_phantoms(object *objp)
|
|
{
|
|
int i;
|
|
ship *shipp;
|
|
int wingnum;
|
|
int wing_index; // Index in wing struct, defines 3-space location in wing.
|
|
vec3d goal_point;
|
|
|
|
Assert(objp->type == OBJ_SHIP);
|
|
Assert((objp->instance >= 0) && (objp->instance < MAX_SHIPS));
|
|
|
|
shipp = &Ships[objp->instance];
|
|
wingnum = shipp->wingnum;
|
|
|
|
if (wingnum == -1)
|
|
return;
|
|
|
|
wing_index = get_wing_index(objp, wingnum);
|
|
|
|
// If this ship is NOT the leader, abort.
|
|
if (wing_index != 0)
|
|
return;
|
|
|
|
for (i=0; i<32; i++)
|
|
if (Debug_render_wing_phantoms & (1 << i)) {
|
|
get_absolute_wing_pos(&goal_point, objp, wingnum, i, false);
|
|
|
|
vertex vert;
|
|
gr_set_color(255, 0, 128);
|
|
g3_rotate_vertex(&vert, &goal_point);
|
|
g3_draw_sphere(&vert, 2.0f);
|
|
}
|
|
|
|
Debug_render_wing_phantoms = 0;
|
|
|
|
}
|
|
|
|
void render_wing_phantoms_all()
|
|
{
|
|
object *objp;
|
|
ship_obj *so;
|
|
|
|
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
|
|
ship *shipp;
|
|
int wingnum;
|
|
int wing_index; // Index in wing struct, defines 3-space location in wing.
|
|
|
|
objp = &Objects[so->objnum];
|
|
if (objp->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
Assert((objp->instance >= 0) && (objp->instance < MAX_SHIPS));
|
|
shipp = &Ships[objp->instance];
|
|
wingnum = shipp->wingnum;
|
|
if (wingnum == -1)
|
|
continue;
|
|
|
|
wing_index = get_wing_index(objp, wingnum);
|
|
|
|
// If this ship is NOT the leader, abort.
|
|
if (wing_index != 0)
|
|
continue;
|
|
|
|
render_wing_phantoms(objp);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
float Leader_chaos = 0.0f;
|
|
int Chaos_frame = -1;
|
|
|
|
// Return true if objp is flying in an erratic manner
|
|
// Only true if objp is a player
|
|
int formation_is_leader_chaotic(object *objp)
|
|
{
|
|
if (Game_mode & GM_MULTIPLAYER)
|
|
return 0;
|
|
|
|
if (objp != Player_obj)
|
|
return 0;
|
|
|
|
if (Framecount != Chaos_frame) {
|
|
float speed_scale;
|
|
float fdot, udot;
|
|
|
|
speed_scale = 3.0f + objp->phys_info.speed * 0.1f;
|
|
|
|
fdot = 5.0f * (1.0f - vm_vec_dot(&objp->orient.vec.fvec, &objp->last_orient.vec.fvec)) * flFrametime;
|
|
udot = 8.0f * (1.0f - vm_vec_dot(&objp->orient.vec.uvec, &objp->last_orient.vec.uvec)) * flFrametime;
|
|
|
|
Leader_chaos += fdot * speed_scale + udot * speed_scale;
|
|
|
|
Leader_chaos *= (1.0f - flFrametime*0.2f);
|
|
|
|
CLAMP(Leader_chaos, 0.0f, 1.7f);
|
|
|
|
Chaos_frame = Framecount;
|
|
}
|
|
|
|
return (Leader_chaos > 1.0f);
|
|
}
|
|
|
|
void ai_most_massive_object_of_its_wing_of_all_docked_objects_helper(object *objp, dock_function_info *infop)
|
|
{
|
|
// check that I am a ship
|
|
if (objp->type == OBJ_SHIP)
|
|
{
|
|
// check that wings match
|
|
if (Ships[objp->instance].wingnum == infop->parameter_variables.int_value)
|
|
{
|
|
// if this guy has a higher mass, he is now the most massive object
|
|
if (objp->phys_info.mass > infop->maintained_variables.objp_value->phys_info.mass)
|
|
{
|
|
infop->maintained_variables.objp_value = objp;
|
|
}
|
|
// if masses are equal, then check if this guy has a higher signature - if so, he is now the most massive object
|
|
else if (objp->phys_info.mass == infop->maintained_variables.objp_value->phys_info.mass)
|
|
{
|
|
if (objp->signature > infop->maintained_variables.objp_value->signature)
|
|
{
|
|
infop->maintained_variables.objp_value = objp;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fly in formation.
|
|
// Make Pl_objp assume its proper place in formation.
|
|
// If the leader of the wing is doing something stupid, like fighting a battle,
|
|
// then the poor sap wingmates will be in for a "world of hurt"
|
|
// Return TRUE if we need to process this object's normal mode
|
|
int ai_formation()
|
|
{
|
|
object *leader_objp;
|
|
ship *shipp;
|
|
ai_info *aip, *laip;
|
|
int wingnum;
|
|
int wing_index; // Index in wing struct, defines 3-space location in wing.
|
|
vec3d goal_point, future_goal_point_5, future_goal_point_2, future_goal_point_x, future_goal_point_1000x, vec_to_goal, dir_to_goal;
|
|
float dot_to_goal, dist_to_goal, leader_speed;
|
|
|
|
Assert(Pl_objp->type == OBJ_SHIP);
|
|
Assert((Pl_objp->instance >= 0) && (Pl_objp->instance < MAX_SHIPS));
|
|
|
|
shipp = &Ships[Pl_objp->instance];
|
|
|
|
Assert((shipp->ai_index >= 0) && (shipp->ai_index < MAX_AI_INFO));
|
|
|
|
aip = &Ai_info[shipp->ai_index];
|
|
|
|
Assert(!(aip->ai_flags[AI::AI_Flags::Formation_wing] && aip->ai_flags[AI::AI_Flags::Formation_object])); // Make sure not both types of formation flying in effect.
|
|
|
|
// Determine which kind of formation flying.
|
|
// If tracking an object, not in waypoint mode:
|
|
if (aip->ai_flags[AI::AI_Flags::Formation_object]) {
|
|
if ((aip->goal_objnum < 0) || (aip->goal_objnum >= MAX_OBJECTS) || (aip->mode == AIM_BAY_DEPART)) {
|
|
aip->ai_flags.remove(AI::AI_Flags::Formation_object);
|
|
return 1;
|
|
}
|
|
|
|
wing_index = aip->form_obj_slotnum;
|
|
leader_objp = &Objects[aip->goal_objnum];
|
|
} else { // Formation flying in waypoint mode.
|
|
Assert(aip->ai_flags[AI::AI_Flags::Formation_wing]);
|
|
|
|
if ( aip->mode != AIM_WAYPOINTS ) {
|
|
aip->ai_flags.remove(AI::AI_Flags::Formation_wing);
|
|
return 1;
|
|
}
|
|
|
|
wingnum = shipp->wingnum;
|
|
|
|
if (wingnum == -1) {
|
|
return 1;
|
|
}
|
|
|
|
// disable formation flying for any ship in the players wing
|
|
// ... except when using auto-pilot
|
|
if ( !AutoPilotEngaged ) {
|
|
if (wingnum == Ships[Player_obj->instance].wingnum) {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
wing_index = get_wing_index(Pl_objp, wingnum);
|
|
|
|
leader_objp = get_wing_leader(wingnum);
|
|
}
|
|
|
|
// if Pl_objp is docked with a ship in his own wing, only the most massive one
|
|
// in the whole assembly actually flies in formation
|
|
// Goober5000 - this is really stupid code
|
|
if ( object_is_docked(Pl_objp) ) {
|
|
// assume I am the most massive
|
|
dock_function_info dfi;
|
|
dfi.parameter_variables.int_value = shipp->wingnum;
|
|
dfi.maintained_variables.objp_value = Pl_objp;
|
|
|
|
// check docked objects
|
|
dock_evaluate_all_docked_objects(Pl_objp, &dfi, ai_most_massive_object_of_its_wing_of_all_docked_objects_helper);
|
|
|
|
// if I am not the most massive, return
|
|
if (dfi.maintained_variables.objp_value != Pl_objp)
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
Assert(leader_objp != NULL);
|
|
laip = &Ai_info[Ships[leader_objp->instance].ai_index];
|
|
|
|
// Make sure we're really in this wing.
|
|
if (wing_index == -1) {
|
|
return 1;
|
|
}
|
|
|
|
// skip ourselves, if we happen to be the leader
|
|
if (leader_objp == Pl_objp) {
|
|
return 1;
|
|
}
|
|
|
|
if (aip->mode == AIM_WAYPOINTS) {
|
|
// skip if they have the flag
|
|
if (aip->ai_flags[AI::AI_Flags::Waypoints_no_formation] ||
|
|
(shipp->wingnum >= 0 && Wings[shipp->wingnum].flags[Ship::Wing_Flags::Waypoints_no_formation]))
|
|
return 1;
|
|
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_ai_path_order_bug]){
|
|
// skip if wing leader has no waypoint order or a different waypoint list
|
|
// ...or if it's a different start index or direction
|
|
if ( (laip->mode != AIM_WAYPOINTS)
|
|
|| (laip->wp_list_index != aip->wp_list_index)
|
|
|| (laip->submode_parm0 != aip->submode_parm0)
|
|
|| ((laip->wp_flags & WPF_BACKTRACK) != (aip->wp_flags & WPF_BACKTRACK))
|
|
) {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
aip->wp_list_index = laip->wp_list_index;
|
|
aip->wp_index = laip->wp_index;
|
|
aip->wp_flags = laip->wp_flags;
|
|
|
|
auto wp_list = find_waypoint_list_at_index(aip->wp_list_index);
|
|
if (wp_list != nullptr) {
|
|
if (aip->wp_flags & WPF_BACKTRACK) {
|
|
if (aip->wp_index == -1)
|
|
++aip->wp_index;
|
|
} else {
|
|
if (aip->wp_index == static_cast<int>(wp_list->get_waypoints().size()))
|
|
--aip->wp_index;
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifndef NDEBUG
|
|
Debug_render_wing_phantoms |= (1 << wing_index);
|
|
#endif
|
|
|
|
leader_speed = leader_objp->phys_info.speed;
|
|
vec3d leader_vec = leader_objp->phys_info.vel;
|
|
|
|
get_absolute_wing_pos(&goal_point, leader_objp, shipp->wingnum, wing_index, aip->ai_flags[AI::AI_Flags::Formation_object]);
|
|
vm_vec_scale_add(&future_goal_point_5, &goal_point, &leader_vec, 10.0f);
|
|
vm_vec_scale_add(&future_goal_point_2, &goal_point, &leader_vec, 5.0f);
|
|
vm_vec_scale_add(&future_goal_point_x, &goal_point, &leader_objp->orient.vec.fvec, 10.0f); // used when very close to destination
|
|
vm_vec_scale_add(&future_goal_point_1000x, &goal_point, &leader_objp->orient.vec.fvec, 1000.0f); // used when very close to destination
|
|
|
|
// Now, get information telling this object how to turn and accelerate to get to its
|
|
// desired location.
|
|
vm_vec_sub(&vec_to_goal, &goal_point, &Pl_objp->pos);
|
|
if ( vm_vec_mag_quick(&vec_to_goal) < AICODE_SMALL_MAGNITUDE )
|
|
vec_to_goal.xyz.x += 0.1f;
|
|
|
|
vm_vec_copy_normalize(&dir_to_goal, &vec_to_goal);
|
|
dot_to_goal = vm_vec_dot(&dir_to_goal, &Pl_objp->orient.vec.fvec);
|
|
dist_to_goal = vm_vec_dist_quick(&Pl_objp->pos, &goal_point);
|
|
float dist_to_goal_2 = vm_vec_dist_quick(&Pl_objp->pos, &future_goal_point_2);
|
|
|
|
// See how different this ship's bank is relative to wing leader
|
|
// If it's too different, set leader_orient in subsequent calls to turn_towards_point
|
|
// (nullptr will be ignored otherwise)
|
|
float leader_up_dot = vm_vec_dot(&leader_objp->orient.vec.uvec, &Pl_objp->orient.vec.uvec);
|
|
matrix* leader_orient = nullptr;
|
|
if (leader_up_dot < 0.996f) {
|
|
leader_orient = &leader_objp->orient;
|
|
}
|
|
|
|
int chaotic_leader = 0;
|
|
|
|
chaotic_leader = formation_is_leader_chaotic(leader_objp); // Set to 1 if leader is player and flying erratically. Causes ships to not aggressively pursue formation location.
|
|
|
|
ship_info *sip = &Ship_info[shipp->ship_info_index];
|
|
bool ab_allowed = false;
|
|
if ((sip->flags[Ship::Info_Flags::Afterburner]) && !(shipp->flags[Ship::Ship_Flags::Afterburner_locked]) &&
|
|
((aip->ai_flags[AI::AI_Flags::Free_afterburner_use] || aip->ai_profile_flags[AI::Profile_Flags::Free_afterburner_use]) || ai_willing_to_afterburn_hard(aip))) {
|
|
ab_allowed = true;
|
|
} else {
|
|
if (Pl_objp->phys_info.flags & PF_AFTERBURNER_ON)
|
|
afterburners_stop(Pl_objp);
|
|
}
|
|
|
|
float switch_value = 0.0f;
|
|
if (ab_allowed) {
|
|
if ((Pl_objp->phys_info.afterburner_max_vel.xyz.z - leader_speed) > (0.2f * Pl_objp->phys_info.afterburner_max_vel.xyz.z)) {
|
|
switch_value = 0.2f * Pl_objp->phys_info.afterburner_max_vel.xyz.z;
|
|
} else {
|
|
switch_value = Pl_objp->phys_info.afterburner_max_vel.xyz.z - leader_speed;
|
|
}
|
|
}
|
|
|
|
if (dist_to_goal > 500.0f) {
|
|
turn_towards_point(Pl_objp, &goal_point, nullptr, 0.0f, leader_orient, AITTV_SLOW_BANK_ACCEL);
|
|
if (ab_allowed && !(Pl_objp->phys_info.flags & PF_AFTERBURNER_ON) && (dot_to_goal > 0.2f)) {
|
|
float percent_left = 100.0f * shipp->afterburner_fuel / sip->afterburner_fuel_capacity;
|
|
if (percent_left > 30.0f) {
|
|
afterburners_start(Pl_objp);
|
|
aip->afterburner_stop_time = Missiontime + 5 * F1_0;
|
|
}
|
|
}
|
|
accelerate_ship(aip, 1.0f);
|
|
} else if (dist_to_goal > 200.0f) {
|
|
if (dot_to_goal > -0.5f) {
|
|
turn_towards_point(Pl_objp, &goal_point, nullptr, 0.0f, leader_orient, AITTV_SLOW_BANK_ACCEL);
|
|
float range_speed = Pl_objp->phys_info.max_vel.xyz.z - leader_speed;
|
|
if (range_speed > 0.0f) {
|
|
set_accel_for_target_speed(Pl_objp, leader_speed + range_speed * (dist_to_goal+100.0f)/500.0f);
|
|
if (Pl_objp->phys_info.flags & PF_AFTERBURNER_ON)
|
|
afterburners_stop(Pl_objp);
|
|
} else {
|
|
set_accel_for_target_speed(Pl_objp, Pl_objp->phys_info.max_vel.xyz.z);
|
|
if (ab_allowed) {
|
|
if (!(Pl_objp->phys_info.flags & PF_AFTERBURNER_ON) && (dot_to_goal > 0.4f)) {
|
|
float percent_left = 100.0f * shipp->afterburner_fuel / sip->afterburner_fuel_capacity;
|
|
if ((percent_left > 30.0f) && (Pl_objp->phys_info.speed < leader_speed)) {
|
|
afterburners_start(Pl_objp);
|
|
aip->afterburner_stop_time = Missiontime + 5 * F1_0;
|
|
}
|
|
} else {
|
|
if ((Pl_objp->phys_info.speed > leader_speed + 1.5 * switch_value) || (dot_to_goal < 0.2f))
|
|
afterburners_stop(Pl_objp);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
turn_towards_point(Pl_objp, &future_goal_point_5, nullptr, 0.0f, leader_orient, AITTV_SLOW_BANK_ACCEL);
|
|
if (leader_speed > 10.0f)
|
|
set_accel_for_target_speed(Pl_objp, leader_speed *(1.0f + dot_to_goal));
|
|
else
|
|
set_accel_for_target_speed(Pl_objp, 10.0f);
|
|
if (Pl_objp->phys_info.flags & PF_AFTERBURNER_ON)
|
|
afterburners_stop(Pl_objp);
|
|
|
|
}
|
|
} else {
|
|
vec3d v2f2;
|
|
float dot_to_f2;
|
|
|
|
vm_vec_normalized_dir(&v2f2, &future_goal_point_2, &Pl_objp->pos);
|
|
dot_to_f2 = vm_vec_dot(&v2f2, &Pl_objp->orient.vec.fvec);
|
|
|
|
// Leader flying like a maniac. Don't try hard to form on wing.
|
|
if (chaotic_leader) {
|
|
turn_towards_point(Pl_objp, &future_goal_point_2, nullptr, 0.0f, leader_orient, AITTV_SLOW_BANK_ACCEL);
|
|
set_accel_for_target_speed(Pl_objp, MIN(leader_speed*0.8f, 20.0f));
|
|
if (Pl_objp->phys_info.flags & PF_AFTERBURNER_ON)
|
|
afterburners_stop(Pl_objp);
|
|
} else if (dist_to_goal > 75.0f) {
|
|
turn_towards_point(Pl_objp, &future_goal_point_2, nullptr, 0.0f, leader_orient, AITTV_SLOW_BANK_ACCEL);
|
|
float delta_speed;
|
|
float range_speed = Pl_objp->phys_info.max_vel.xyz.z - leader_speed;
|
|
if (range_speed > 0.0f) {
|
|
delta_speed = dist_to_goal_2/500.0f * range_speed;
|
|
if (Pl_objp->phys_info.flags & PF_AFTERBURNER_ON)
|
|
afterburners_stop(Pl_objp);
|
|
} else {
|
|
delta_speed = Pl_objp->phys_info.max_vel.xyz.z - leader_speed;
|
|
if (ab_allowed) {
|
|
if (!(Pl_objp->phys_info.flags & PF_AFTERBURNER_ON) && (dot_to_goal > 0.6f)) {
|
|
float percent_left = 100.0f * shipp->afterburner_fuel / sip->afterburner_fuel_capacity;
|
|
if ((percent_left > 30.0f) && (Pl_objp->phys_info.speed < leader_speed - 0.5 * switch_value)) {
|
|
afterburners_start(Pl_objp);
|
|
aip->afterburner_stop_time = Missiontime + 5 * F1_0;
|
|
}
|
|
} else {
|
|
if ((Pl_objp->phys_info.speed > leader_speed + 0.95 * switch_value) || (dot_to_goal < 0.4f))
|
|
afterburners_stop(Pl_objp);
|
|
}
|
|
}
|
|
}
|
|
if (dot_to_goal < 0.0f) {
|
|
delta_speed = -delta_speed;
|
|
if (-delta_speed > leader_speed/2)
|
|
delta_speed = -leader_speed/2;
|
|
}
|
|
|
|
if (leader_speed < 5.0f)
|
|
if (delta_speed < 5.0f)
|
|
delta_speed = 5.0f;
|
|
|
|
float scale = dot_to_f2;
|
|
if (scale < 0.1f)
|
|
scale = 0.0f;
|
|
else
|
|
scale *= scale;
|
|
|
|
set_accel_for_target_speed(Pl_objp, scale * (leader_speed + delta_speed));
|
|
} else {
|
|
|
|
if (leader_speed < 5.0f) {
|
|
// Leader very slow. If not close to goal point, get very close. Note, keep trying to get close unless
|
|
// moving very slowly, else momentum can carry far away from goal.
|
|
|
|
if ((dist_to_goal > 10.0f) || ((Pl_objp->phys_info.speed > leader_speed + 2.5f) && (dot_to_goal > 0.5f))) {
|
|
turn_towards_point(Pl_objp, &goal_point, nullptr, 0.0f, leader_orient, AITTV_SLOW_BANK_ACCEL);
|
|
set_accel_for_target_speed(Pl_objp, leader_speed + dist_to_goal/10.0f);
|
|
} else {
|
|
if (Pl_objp->phys_info.speed < 0.5f) {
|
|
turn_towards_point(Pl_objp, &future_goal_point_1000x, nullptr, 0.0f, leader_orient, AITTV_SLOW_BANK_ACCEL);
|
|
}
|
|
set_accel_for_target_speed(Pl_objp, leader_speed);
|
|
}
|
|
if (Pl_objp->phys_info.flags & PF_AFTERBURNER_ON)
|
|
afterburners_stop(Pl_objp);
|
|
|
|
} else if (dist_to_goal > 10.0f) {
|
|
float dv;
|
|
|
|
turn_towards_point(Pl_objp, &future_goal_point_2, nullptr, 0.0f, leader_orient, AITTV_SLOW_BANK_ACCEL);
|
|
|
|
if (dist_to_goal > 25.0f) {
|
|
if (dot_to_goal < 0.3f)
|
|
dv = -0.1f;
|
|
else
|
|
dv = dot_to_goal - 0.2f;
|
|
|
|
set_accel_for_target_speed(Pl_objp, leader_speed + dist_to_goal/5.0f * dv);
|
|
} else {
|
|
set_accel_for_target_speed(Pl_objp, leader_speed + 1.5f * dot_to_goal - 1.0f);
|
|
}
|
|
|
|
float range_speed = Pl_objp->phys_info.max_vel.xyz.z - leader_speed;
|
|
if (range_speed > 0.0f) {
|
|
if (Pl_objp->phys_info.flags & PF_AFTERBURNER_ON)
|
|
afterburners_stop(Pl_objp);
|
|
} else {
|
|
if (ab_allowed) {
|
|
if (!(Pl_objp->phys_info.flags & PF_AFTERBURNER_ON) && (dot_to_goal > 0.9f)) {
|
|
float percent_left = 100.0f * shipp->afterburner_fuel / sip->afterburner_fuel_capacity;
|
|
if ((percent_left > 30.0f) && (Pl_objp->phys_info.speed < leader_speed - 0.75 * switch_value)) {
|
|
afterburners_start(Pl_objp);
|
|
aip->afterburner_stop_time = Missiontime + 3 * F1_0;
|
|
}
|
|
} else {
|
|
if ((Pl_objp->phys_info.speed > leader_speed + 0.95 * switch_value) || (dot_to_goal < 0.7f))
|
|
afterburners_stop(Pl_objp);
|
|
}
|
|
}
|
|
}
|
|
|
|
} else {
|
|
if (Pl_objp->phys_info.speed < 0.1f)
|
|
turn_towards_point(Pl_objp, &future_goal_point_1000x, nullptr, 0.0f, leader_orient, AITTV_SLOW_BANK_ACCEL);
|
|
else
|
|
turn_towards_point(Pl_objp, &future_goal_point_x, nullptr, 0.0f, leader_orient, AITTV_SLOW_BANK_ACCEL);
|
|
|
|
// Goober5000 7/5/2006 changed to leader_speed from 0.0f
|
|
set_accel_for_target_speed(Pl_objp, leader_speed);
|
|
|
|
float range_speed = Pl_objp->phys_info.max_vel.xyz.z - leader_speed;
|
|
if (range_speed > 0.0f) {
|
|
if (Pl_objp->phys_info.flags & PF_AFTERBURNER_ON)
|
|
afterburners_stop(Pl_objp);
|
|
} else {
|
|
if (ab_allowed) {
|
|
if (!(Pl_objp->phys_info.flags & PF_AFTERBURNER_ON) && (dot_to_goal > 0.9f)) {
|
|
float percent_left = 100.0f * shipp->afterburner_fuel / sip->afterburner_fuel_capacity;
|
|
if ((percent_left > 30.0f) && (Pl_objp->phys_info.speed < leader_speed - 0.75 * switch_value)) {
|
|
afterburners_start(Pl_objp);
|
|
aip->afterburner_stop_time = Missiontime + 3 * F1_0;
|
|
}
|
|
} else {
|
|
if ((Pl_objp->phys_info.speed > leader_speed + 0.75 * switch_value) || (dot_to_goal < 0.8f))
|
|
afterburners_stop(Pl_objp);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* If object *objp is being repaired, deal with it!
|
|
*/
|
|
void ai_do_repair_frame(object *objp, ai_info *aip, float frametime)
|
|
{
|
|
static bool rearm_eta_found=false;
|
|
|
|
|
|
if (aip->ai_flags[AI::AI_Flags::Being_repaired, AI::AI_Flags::Awaiting_repair]) {
|
|
if (Ships[objp->instance].team == Iff_traitor) {
|
|
ai_abort_rearm_request(objp);
|
|
return;
|
|
}
|
|
|
|
int support_objnum;
|
|
|
|
ai_info *repair_aip;
|
|
|
|
support_objnum = aip->support_ship_objnum;
|
|
|
|
if (support_objnum == -1)
|
|
return;
|
|
|
|
Assertion(support_objnum >= 0, "The support ship objnum is nonsense. It is %d instead of a positive number or -1.", support_objnum);
|
|
|
|
// Curious -- object numbers match, but signatures do not.
|
|
// Must mean original repair ship died and was replaced by current ship.
|
|
Assert(Objects[support_objnum].signature == aip->support_ship_signature);
|
|
|
|
repair_aip = &Ai_info[Ships[Objects[support_objnum].instance].ai_index];
|
|
|
|
if (aip->ai_flags[AI::AI_Flags::Being_repaired]) {
|
|
|
|
// Wait awhile into the mode to synchronize with sound effect.
|
|
if (Missiontime - repair_aip->submode_start_time > REARM_SOUND_DELAY) {
|
|
int repaired;
|
|
|
|
if (!rearm_eta_found && objp==Player_obj && Player_rearm_eta <= 0)
|
|
{
|
|
rearm_eta_found = true;
|
|
Player_rearm_eta = ship_calculate_rearm_duration(objp);
|
|
}
|
|
|
|
repaired = ship_do_rearm_frame( objp, frametime ); // hook to do missile rearming
|
|
|
|
// See if fully repaired. If so, cause process to stop.
|
|
if ( repaired ) {
|
|
// We can repair ships without being docked, so only do this if we are docked
|
|
if (repair_aip->submode == AIS_DOCK_4) {
|
|
repair_aip->submode = AIS_UNDOCK_0;
|
|
repair_aip->submode_start_time = Missiontime;
|
|
// if repairing player object -- tell him done with repair
|
|
if (!MULTIPLAYER_CLIENT) {
|
|
ai_do_objects_repairing_stuff(objp, &Objects[support_objnum], REPAIR_INFO_COMPLETE);
|
|
}
|
|
}
|
|
//If Repairing self
|
|
else if (&Objects[support_objnum] == objp) { // Undocking ends the repair,
|
|
// but since we don'tneed to do that in this case, end it here
|
|
ai_do_objects_repairing_stuff(objp, &Objects[support_objnum], REPAIR_INFO_END);
|
|
if (!MULTIPLAYER_CLIENT) {
|
|
hud_support_view_stop();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (aip->ai_flags[AI::AI_Flags::Awaiting_repair]) {
|
|
// If this ship has been awaiting repair for 90+ seconds, abort.
|
|
if ( !MULTIPLAYER_CLIENT ) {
|
|
if ((Game_mode & GM_MULTIPLAYER) || (objp != Player_obj)) {
|
|
if ((repair_aip->goal_objnum == OBJ_INDEX(objp)) && (timestamp_elapsed(aip->abort_rearm_timestamp))) {
|
|
ai_abort_rearm_request(objp);
|
|
aip->next_rearm_request_timestamp = timestamp(NEXT_REARM_TIMESTAMP);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// AL 11-24-97: If this is the player ship, ensure the repair sound isn't playing. We need to
|
|
// do this check, since this is a looping sound, and may continue on if rearm/repair
|
|
// finishes abnormally once sound begins looping.
|
|
if ( objp == Player_obj ) {
|
|
Player_rearm_eta = 0;
|
|
rearm_eta_found = false;
|
|
player_stop_repair_sound();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Shell around dock_orient_and_approach to detect whether dock process should be aborted.
|
|
// Goober5000 - The child should always keep up with the parent.
|
|
void call_doa(object *child, object *parent)
|
|
{
|
|
Assert(dock_check_find_direct_docked_object(parent, child));
|
|
|
|
// get indexes
|
|
int parent_index = dock_find_dockpoint_used_by_object(parent, child);
|
|
int child_index = dock_find_dockpoint_used_by_object(child, parent);
|
|
|
|
// child keeps up with parent
|
|
dock_orient_and_approach(child, child_index, parent, parent_index, DOA_DOCK_STAY);
|
|
}
|
|
|
|
// Maybe launch a countermeasure.
|
|
// Also, detect a supposed homing missile that no longer exists.
|
|
void ai_maybe_launch_cmeasure(object *objp, ai_info *aip)
|
|
{
|
|
float dist;
|
|
ship_info *sip;
|
|
ship *shipp;
|
|
|
|
shipp = &Ships[objp->instance];
|
|
sip = &Ship_info[shipp->ship_info_index];
|
|
|
|
if (!(sip->is_small_ship() || sip->flags[Ship::Info_Flags::Transport]))
|
|
return;
|
|
|
|
if (object_is_docked(objp))
|
|
return;
|
|
|
|
if (!shipp->cmeasure_count)
|
|
return;
|
|
|
|
if ( !timestamp_elapsed(shipp->cmeasure_fire_stamp) )
|
|
return;
|
|
|
|
// If not on player's team and Skill_level + ai_class is low, never fire a countermeasure. The ship is too dumb.
|
|
//SUSHI: Only bail if autoscale is on...
|
|
if (iff_x_attacks_y(Player_ship->team, shipp->team) && aip->ai_class_autoscale) {
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Adjusted_AI_class_autoscale]) {
|
|
if (Game_skill_level + ai_get_autoscale_index(aip->ai_class) < 4) {
|
|
return;
|
|
}
|
|
} else {
|
|
if (Game_skill_level + aip->ai_class < 4) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ((aip->nearest_locked_object != -1) && (Objects[aip->nearest_locked_object].type == OBJ_WEAPON)) {
|
|
object *weapon_objp;
|
|
|
|
weapon_objp = &Objects[aip->nearest_locked_object];
|
|
|
|
dist = vm_vec_dist(&objp->pos, &weapon_objp->pos);
|
|
|
|
bool in_countermeasure_range = dist < weapon_objp->phys_info.speed * 2.0f;
|
|
weapon_info *cmeasure = &Weapon_info[shipp->current_cmeasure];
|
|
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Improved_missile_avoidance])
|
|
in_countermeasure_range = dist < cmeasure->cm_effective_rad;
|
|
|
|
if ( in_countermeasure_range ) {
|
|
|
|
aip->nearest_locked_distance = dist;
|
|
// Verify that this object is really homing on us.
|
|
|
|
float fire_chance;
|
|
|
|
// For ships on player's team, check if modder wants default constant chance to fire or value from specific AI class profile.
|
|
// For enemies, use value from specific AI class profile (usually increasing chance with higher skill level).
|
|
// Or use unifying behavior if specified. --wookieejedi
|
|
if ( (aip->ai_profile_flags[AI::Profile_Flags::Unify_usage_countermeasure_firechance]) || (shipp->team != Player_ship->team) ) {
|
|
fire_chance = aip->ai_cmeasure_fire_chance;
|
|
} else {
|
|
fire_chance = The_mission.ai_profile->cmeasure_fire_chance[NUM_SKILL_LEVELS/2];
|
|
}
|
|
|
|
// Decrease chance to fire at lower ai class (SUSHI: Only if autoscale is on)
|
|
if (aip->ai_class_autoscale)
|
|
{
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Adjusted_AI_class_autoscale])
|
|
fire_chance *= (float)ai_get_autoscale_index(aip->ai_class) / ai_get_autoscale_index(Num_ai_classes);
|
|
else
|
|
fire_chance *= (float)aip->ai_class / Num_ai_classes;
|
|
}
|
|
|
|
float r = frand();
|
|
if (fire_chance < r) {
|
|
int fail_delay;
|
|
// check if modder wants to use custom fail delay, or default
|
|
if (cmeasure->cmeasure_failure_delay_multiplier_ai >= 0) {
|
|
// use firewait as delay
|
|
fail_delay = static_cast<int> (cmeasure->cmeasure_firewait * cmeasure->cmeasure_failure_delay_multiplier_ai);
|
|
} else {
|
|
// use default to wait firwait + additional delay to decrease chance of firing very soon.
|
|
fail_delay = cmeasure->cmeasure_firewait + static_cast<int> (fire_chance * 2000);
|
|
}
|
|
shipp->cmeasure_fire_stamp = timestamp(fail_delay);
|
|
return;
|
|
}
|
|
|
|
if (weapon_objp->type == OBJ_WEAPON) {
|
|
if (weapon_objp->instance >= 0) {
|
|
ship_launch_countermeasure(objp);
|
|
shipp->cmeasure_fire_stamp = timestamp(static_cast<int> (cmeasure->cmeasure_firewait * cmeasure->cmeasure_success_delay_multiplier_ai));
|
|
return;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
static void ai_preprocess_ignore_objnum(ai_info *aip)
|
|
{
|
|
if (aip->ai_flags[AI::AI_Flags::Temporary_ignore])
|
|
{
|
|
if (timestamp_elapsed(aip->ignore_expire_timestamp))
|
|
aip->ignore_objnum = UNUSED_OBJNUM;
|
|
}
|
|
|
|
if (is_ignore_object(aip, aip->goal_objnum))
|
|
{
|
|
// you can land and launch on a ship you're ignoring!
|
|
if (aip->mode != AIM_BAY_EMERGE && aip->mode != AIM_BAY_DEPART) {
|
|
aip->goal_objnum = -1;
|
|
}
|
|
|
|
// AL 12-11-97: If in STRAFE mode, we need to ensure that target_objnum is also
|
|
// set to -1
|
|
if (aip->mode == AIM_STRAFE)
|
|
aip->target_objnum = -1;
|
|
}
|
|
|
|
if (is_ignore_object(aip, aip->target_objnum))
|
|
aip->target_objnum = -1;
|
|
}
|
|
|
|
void ai_chase_circle(object *objp)
|
|
{
|
|
float dist_to_goal;
|
|
float target_speed;
|
|
vec3d goal_point;
|
|
ship_info *sip;
|
|
ai_info *aip;
|
|
|
|
sip = &Ship_info[Ships[Pl_objp->instance].ship_info_index];
|
|
|
|
target_speed = sip->max_speed/4.0f;
|
|
aip = &Ai_info[Ships[Pl_objp->instance].ai_index];
|
|
|
|
Assert(vm_vec_mag(&aip->goal_point) >= 0.0f); // Supposedly detects bogus vector
|
|
|
|
goal_point = aip->goal_point;
|
|
|
|
if (aip->ignore_objnum == UNUSED_OBJNUM) {
|
|
dist_to_goal = vm_vec_dist_quick(&aip->goal_point, &objp->pos);
|
|
|
|
if (dist_to_goal > 2 * The_mission.ai_profile->attack_any_idle_circle_distance) {
|
|
vec3d vec_to_goal;
|
|
// Too far from circle goal, create a new goal point.
|
|
vm_vec_normalized_dir(&vec_to_goal, &aip->goal_point, &objp->pos);
|
|
vm_vec_scale_add(&aip->goal_point, &objp->pos, &vec_to_goal, The_mission.ai_profile->attack_any_idle_circle_distance);
|
|
}
|
|
|
|
goal_point = aip->goal_point;
|
|
} else if (is_ignore_object(aip, aip->ignore_objnum)) {
|
|
object *ignore_objp = &Objects[aip->ignore_objnum];
|
|
|
|
vec3d tvec1;
|
|
float dist;
|
|
|
|
dist = vm_vec_normalized_dir(&tvec1, &Pl_objp->pos, &ignore_objp->pos);
|
|
|
|
if (dist < ignore_objp->radius*2 + 1500.0f) {
|
|
vm_vec_scale_add(&goal_point, &Pl_objp->pos, &tvec1, ignore_objp->radius*2 + 1400.0f);
|
|
if (dist < ignore_objp->radius*2 + 1300.0f)
|
|
target_speed = sip->max_speed * (1.25f - dist/(ignore_objp->radius*2 + 1500.0f));
|
|
}
|
|
}
|
|
|
|
Assert(vm_vec_mag(&aip->goal_point) >= 0.0f); // Supposedly detects bogus vector
|
|
|
|
turn_towards_tangent(Pl_objp, &goal_point, 10*objp->radius + 200.0f);
|
|
|
|
set_accel_for_target_speed(Pl_objp, target_speed);
|
|
|
|
}
|
|
|
|
#define SHIELD_BALANCE_RATE 0.2f // 0.1f -> takes 10 seconds to equalize shield.
|
|
|
|
/**
|
|
* Transfer shield energy to most recently hit section from others.
|
|
*/
|
|
void ai_transfer_shield(object *objp, int quadrant_num)
|
|
{
|
|
shield_transfer(objp, quadrant_num, (SHIELD_BALANCE_RATE / 2));
|
|
}
|
|
|
|
void ai_balance_shield(object *objp)
|
|
{
|
|
shield_balance(objp, SHIELD_BALANCE_RATE, 0.0f);
|
|
}
|
|
|
|
// Manage the shield for this ship.
|
|
// Try to max out the side that was most recently hit.
|
|
void ai_manage_shield(object *objp, ai_info *aip)
|
|
{
|
|
ship_info *sip;
|
|
|
|
sip = &Ship_info[Ships[objp->instance].ship_info_index];
|
|
|
|
if (timestamp_elapsed(aip->shield_manage_timestamp)) {
|
|
float delay;
|
|
|
|
// Scale time until next manage shield based on Skill_level.
|
|
// Ships on player's team are treated as if Skill_level is average.
|
|
// Or use unifying behavior if specified. --wookieejedi
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Unify_usage_ai_shield_manage_delay] || (iff_x_attacks_y(Player_ship->team, Ships[objp->instance].team)))
|
|
{
|
|
delay = aip->ai_shield_manage_delay;
|
|
}
|
|
else
|
|
{
|
|
delay = The_mission.ai_profile->shield_manage_delay[NUM_SKILL_LEVELS/2];
|
|
}
|
|
|
|
// Scale between 1x and 3x based on ai_class (SUSHI: only if autoscale is on)
|
|
if (aip->ai_class_autoscale)
|
|
{
|
|
int number_of_as_classes = ai_get_autoscale_index(Num_ai_classes);
|
|
|
|
// Cyborg: Make sure that autoscale index is greater than one before dividing, coverity 1523548
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Adjusted_AI_class_autoscale] && number_of_as_classes > 1)
|
|
delay = delay + delay * (float)(3 * (number_of_as_classes - ai_get_autoscale_index(aip->ai_class) - 1) / (number_of_as_classes - 1));
|
|
else {
|
|
static bool autoscale_checked = false;
|
|
|
|
// warn if there's only one autoscale ai class
|
|
if (number_of_as_classes == 1 && !autoscale_checked) {
|
|
mprintf(("Warning! Having only 1 autoscale ai class will disable autoscale features that rely on division."));
|
|
autoscale_checked = true;
|
|
}
|
|
|
|
// Only adjust if we have more than one ai class to avoid div by 0
|
|
if (Num_ai_classes > 1){
|
|
delay = delay + delay * (float)(3 * (Num_ai_classes - aip->ai_class - 1) / (Num_ai_classes - 1));
|
|
}
|
|
}
|
|
}
|
|
|
|
// set timestamp
|
|
aip->shield_manage_timestamp = timestamp((int) (delay * 1000.0f));
|
|
|
|
if (sip->is_small_ship() || (aip->ai_profile_flags[AI::Profile_Flags::All_ships_manage_shields])) {
|
|
if ((Missiontime - aip->last_hit_time < F1_0 * 10) && !((aip->ai_profile_flags[AI::Profile_Flags::AI_balances_shields_when_attacked])))
|
|
ai_transfer_shield(objp, aip->danger_shield_quadrant);
|
|
else
|
|
ai_balance_shield(objp);
|
|
}
|
|
}
|
|
}
|
|
|
|
// See if object *objp should evade an incoming missile.
|
|
// *aip is the ai_info pointer within *objp.
|
|
void ai_maybe_evade_locked_missile(object *objp, ai_info *aip)
|
|
{
|
|
ship *shipp;
|
|
shipp = &Ships[objp->instance];
|
|
|
|
// Only small ships evade an incoming missile. Why would a capital ship try to swerve?
|
|
if (!Ship_info[Ships[objp->instance].ship_info_index].is_small_ship()) {
|
|
return;
|
|
}
|
|
|
|
// don't evade missiles if you're docked
|
|
if (object_is_docked(objp)) {
|
|
return;
|
|
}
|
|
|
|
if (aip->ai_flags[AI::AI_Flags::No_dynamic, AI::AI_Flags::Kamikaze]) { // If not allowed to pursue dynamic objectives, don't evade. Dumb? Maybe change. -- MK, 3/15/98
|
|
return;
|
|
}
|
|
|
|
if (aip->nearest_locked_object != -1) {
|
|
object *missile_objp;
|
|
|
|
missile_objp = &Objects[aip->nearest_locked_object];
|
|
|
|
if (Weapons[missile_objp->instance].homing_object != objp) {
|
|
aip->nearest_locked_object = -1;
|
|
return;
|
|
}
|
|
|
|
if ((missile_objp->type == OBJ_WEAPON) && (Weapon_info[Weapons[missile_objp->instance].weapon_info_index].is_homing())) {
|
|
|
|
vec3d v2m;
|
|
float dist = vm_vec_normalized_dir(&v2m, &objp->pos, &missile_objp->pos);
|
|
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Improved_missile_avoidance]
|
|
&& vm_vec_dot(&v2m, &missile_objp->orient.vec.fvec) < 0.5f ) {
|
|
// don't bother if the missile isn't actually coming towards us
|
|
aip->nearest_locked_object = -1;
|
|
return;
|
|
}
|
|
|
|
// evade missiles 4 seconds away normally, 2 seconds away with the flag (evading too early is a thing!)
|
|
float evade_dist_scalar = The_mission.ai_profile->flags[AI::Profile_Flags::Improved_missile_avoidance] ? 2.0f : 4.0f;
|
|
float evade_distance = evade_dist_scalar * vm_vec_mag_quick(&missile_objp->phys_info.vel);
|
|
if (dist < evade_distance) {
|
|
switch (aip->mode) {
|
|
// If in AIM_STRAFE mode, don't evade if parent of weapon is targeted ship.
|
|
case AIM_STRAFE:
|
|
if ((missile_objp->parent != -1) && (missile_objp->parent == aip->target_objnum)) {
|
|
;
|
|
} else {
|
|
; // Alan -- If you want to handle incoming weapons from someone other than the ship
|
|
// the strafing ship is attacking, do it here.
|
|
}
|
|
break;
|
|
case AIM_CHASE:
|
|
// Don't always go into evade weapon mode. Usually, a countermeasure gets launched.
|
|
// If low on countermeasures, more likely to try to evade. If 8+, never evade due to low cmeasures.
|
|
// Asteroth - If Improved_missile_avoidance, always evade!! Don't assume anything else will save you
|
|
if (((((Missiontime >> 18) ^ OBJ_INDEX(objp)) & 3) == 0) ||
|
|
(objp->phys_info.speed < 40.0f) ||
|
|
(frand() < 1.0f - (float) shipp->cmeasure_count/8.0f ||
|
|
The_mission.ai_profile->flags[AI::Profile_Flags::Improved_missile_avoidance])) {
|
|
if (aip->submode != SM_ATTACK_FOREVER) { // SM_ATTACK_FOREVER means engines blown.
|
|
aip->submode = SM_EVADE_WEAPON;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
}
|
|
break;
|
|
case AIM_DOCK: // Ships in dock mode can evade iif they are not currently repairing or docked.
|
|
if (object_is_docked(objp) || (aip->ai_flags[AI::AI_Flags::Repairing, AI::AI_Flags::Being_repaired]))
|
|
break;
|
|
FALLTHROUGH;
|
|
case AIM_GUARD:
|
|
// If in guard mode and far away from guard object, don't pursue guy that hit me.
|
|
if ((aip->guard_objnum != -1) && (aip->guard_signature == Objects[aip->guard_objnum].signature)) {
|
|
if (vm_vec_dist_quick(&objp->pos, &Objects[aip->guard_objnum].pos) > 500.0f) {
|
|
return;
|
|
}
|
|
}
|
|
FALLTHROUGH;
|
|
case AIM_EVADE:
|
|
case AIM_GET_BEHIND:
|
|
case AIM_STAY_NEAR:
|
|
case AIM_STILL:
|
|
case AIM_AVOID:
|
|
case AIM_WAYPOINTS:
|
|
case AIM_NONE:
|
|
case AIM_BIGSHIP:
|
|
case AIM_PATH:
|
|
case AIM_BE_REARMED:
|
|
case AIM_SAFETY:
|
|
case AIM_BAY_EMERGE:
|
|
case AIM_FLY_TO_SHIP:
|
|
aip->active_goal = AI_ACTIVE_GOAL_DYNAMIC;
|
|
aip->previous_mode = aip->mode;
|
|
aip->previous_submode = aip->submode;
|
|
aip->mode = AIM_EVADE_WEAPON;
|
|
aip->submode = -1;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->mode_time = timestamp(MAX_EVADE_TIME); // Max time to evade.
|
|
break;
|
|
case AIM_EVADE_WEAPON: // Note: We don't want to change mode on another evasion, or previous_mode will get bashed.
|
|
case AIM_PLAY_DEAD:
|
|
case AIM_BAY_DEPART:
|
|
case AIM_SENTRYGUN:
|
|
case AIM_LUA:
|
|
break;
|
|
case AIM_WARP_OUT:
|
|
break;
|
|
default:
|
|
UNREACHABLE("Unknown AI Mode %d! Get a coder!", aip->mode);
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
aip->nearest_locked_object = -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Maybe evade a dumbfire weapon that was fired when Pl_objp was targeted.
|
|
// Have an 80% chance of evading in a second
|
|
void maybe_evade_dumbfire_weapon(ai_info *aip)
|
|
{
|
|
// Only small ships evade an incoming missile. Why would a capital ship try to swerve?
|
|
if (!Ship_info[Ships[Pl_objp->instance].ship_info_index].is_small_ship()) {
|
|
return;
|
|
}
|
|
|
|
// don't evade missiles if you're docked
|
|
if (object_is_docked(Pl_objp)) {
|
|
return;
|
|
}
|
|
|
|
// Make sure in a mode in which we evade dumbfire weapons.
|
|
switch (aip->mode) {
|
|
case AIM_CHASE:
|
|
if (aip->submode == SM_ATTACK_FOREVER) {
|
|
return;
|
|
}
|
|
break;
|
|
case AIM_GUARD:
|
|
// If in guard mode and far away from guard object, don't pursue guy that hit me.
|
|
if ((aip->guard_objnum != -1) && (aip->guard_signature == Objects[aip->guard_objnum].signature)) {
|
|
if (vm_vec_dist_quick(&Objects[Ships[aip->shipnum].objnum].pos, &Objects[aip->guard_objnum].pos) > 500.0f) {
|
|
return;
|
|
}
|
|
}
|
|
case AIM_STILL:
|
|
case AIM_STAY_NEAR:
|
|
case AIM_EVADE:
|
|
case AIM_GET_BEHIND:
|
|
case AIM_AVOID:
|
|
case AIM_PATH:
|
|
case AIM_NONE:
|
|
case AIM_WAYPOINTS:
|
|
case AIM_SAFETY:
|
|
break;
|
|
case AIM_STRAFE:
|
|
case AIM_BIGSHIP:
|
|
case AIM_DOCK:
|
|
case AIM_PLAY_DEAD:
|
|
case AIM_EVADE_WEAPON:
|
|
case AIM_BAY_EMERGE:
|
|
case AIM_BAY_DEPART:
|
|
case AIM_SENTRYGUN:
|
|
case AIM_WARP_OUT:
|
|
case AIM_FLY_TO_SHIP:
|
|
case AIM_LUA:
|
|
return;
|
|
default:
|
|
UNREACHABLE("Unknown AI Mode %d! Get a coder!", aip->mode);
|
|
return;
|
|
}
|
|
|
|
if (is_instructor(&Objects[Ships[aip->shipnum].objnum]))
|
|
return; // Instructor doesn't evade.
|
|
|
|
float t = ai_endangered_by_weapon(aip);
|
|
if ((t > 0.0f) && (t < 1.0f)) {
|
|
// Check if this weapon is from a large ship Pl_objp is attacking... if so, enter strafe mode
|
|
if ( !(aip->ai_flags[AI::AI_Flags::No_dynamic]) ) {
|
|
if ( ai_big_maybe_enter_strafe_mode(Pl_objp, aip->danger_weapon_objnum) ) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
switch (aip->mode) {
|
|
case AIM_CHASE:
|
|
switch (aip->submode) {
|
|
case SM_EVADE:
|
|
case SM_ATTACK_FOREVER:
|
|
case SM_AVOID:
|
|
case SM_GET_AWAY:
|
|
case SM_EVADE_WEAPON:
|
|
break;
|
|
default:
|
|
if (ai_near_full_strength(Pl_objp)) {
|
|
aip->submode = SM_SUPER_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->last_attack_time = Missiontime;
|
|
} else {
|
|
aip->submode = SM_EVADE_WEAPON;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
case AIM_GUARD:
|
|
case AIM_STILL:
|
|
case AIM_STAY_NEAR:
|
|
case AIM_EVADE:
|
|
case AIM_GET_BEHIND:
|
|
case AIM_AVOID:
|
|
case AIM_PATH:
|
|
case AIM_NONE:
|
|
case AIM_WAYPOINTS:
|
|
case AIM_FLY_TO_SHIP:
|
|
case AIM_SAFETY:
|
|
if (!(aip->ai_flags[AI::AI_Flags::No_dynamic, AI::AI_Flags::Kamikaze]) && Ship_info[Ships[aip->shipnum].ship_info_index].is_small_ship()) {
|
|
aip->active_goal = AI_ACTIVE_GOAL_DYNAMIC;
|
|
aip->previous_mode = aip->mode;
|
|
aip->previous_submode = aip->submode;
|
|
aip->mode = AIM_EVADE_WEAPON;
|
|
aip->submode = -1;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->mode_time = timestamp(MAX_EVADE_TIME); // Evade for up to five seconds.
|
|
}
|
|
break;
|
|
case AIM_STRAFE:
|
|
case AIM_BIGSHIP:
|
|
case AIM_DOCK:
|
|
case AIM_PLAY_DEAD:
|
|
case AIM_EVADE_WEAPON:
|
|
case AIM_BAY_EMERGE:
|
|
case AIM_BAY_DEPART:
|
|
case AIM_SENTRYGUN:
|
|
case AIM_LUA:
|
|
break;
|
|
default:
|
|
UNREACHABLE("Unknown AI Mode %d! Get a coder!", aip->mode);
|
|
}
|
|
}
|
|
}
|
|
|
|
// manage the opening and closing of fighter bay related subobject animations
|
|
// input: pl_objp => the object which is/has entered/exited the fighter bay
|
|
// aip => ai info for said object
|
|
// done => true if the object has is finished with the path, or is dead
|
|
void ai_manage_bay_doors(object *pl_objp, ai_info *aip, bool done)
|
|
{
|
|
Assert( pl_objp );
|
|
Assert( pl_objp->instance >= 0 );
|
|
Assert( aip );
|
|
|
|
ship *shipp = &Ships[pl_objp->instance];
|
|
|
|
if (done && !shipp->bay_doors_need_open)
|
|
return;
|
|
|
|
if (done)
|
|
shipp->bay_doors_need_open = false;
|
|
|
|
// if this happens then the parent was probably destroyed/departed, so just skip the rest
|
|
if (shipp->bay_doors_parent_shipnum < 0)
|
|
return;
|
|
|
|
ship *parent_ship = &Ships[shipp->bay_doors_parent_shipnum];
|
|
|
|
// the parent may have already been destroyed/departed, leaving stale bay door state behind
|
|
if (parent_ship->objnum < 0 || parent_ship->model_instance_num < 0)
|
|
return;
|
|
|
|
if (done)
|
|
parent_ship->bay_doors_wanting_open--;
|
|
|
|
// trigger an open if we need it
|
|
if ( (parent_ship->bay_doors_status == MA_POS_NOT_SET) && (parent_ship->bay_doors_wanting_open > 0) ) {
|
|
const auto animList = Ship_info[parent_ship->ship_info_index].animations.getDockBayDoors(model_get_instance(parent_ship->model_instance_num), shipp->bay_doors_launched_from);
|
|
if (animList.start(animation::ModelAnimationDirection::FWD)) {
|
|
parent_ship->bay_doors_status = MA_POS_SET;
|
|
parent_ship->bay_doors_anim_done_time = animList.getTime();
|
|
} else {
|
|
parent_ship->bay_doors_status = MA_POS_READY;
|
|
}
|
|
}
|
|
|
|
// if we are already open, and no longer need to be, then close the doors
|
|
if ( (parent_ship->bay_doors_status == MA_POS_READY) && (parent_ship->bay_doors_wanting_open <= 0) ) {
|
|
Ship_info[parent_ship->ship_info_index].animations.getDockBayDoors(model_get_instance(parent_ship->model_instance_num), shipp->bay_doors_launched_from).start(animation::ModelAnimationDirection::RWD);
|
|
parent_ship->bay_doors_status = MA_POS_NOT_SET;
|
|
}
|
|
|
|
if ( (parent_ship->bay_doors_status == MA_POS_SET) && timestamp_elapsed(parent_ship->bay_doors_anim_done_time) ) {
|
|
parent_ship->bay_doors_status = MA_POS_READY;
|
|
}
|
|
}
|
|
|
|
// determine what path to use when emerging from a fighter bay
|
|
// input: pl_objp => pointer to object for ship that is arriving
|
|
// pos => output parameter, it is the starting world pos for path choosen
|
|
// fvec => output parameter, this is the forward vector that ship has when arriving
|
|
//
|
|
// exit: -1 => path could not be located
|
|
// 0 => success
|
|
int ai_acquire_emerge_path(object *pl_objp, int parent_objnum, int allowed_path_mask)
|
|
{
|
|
int path_index, bay_path;
|
|
pnode *pnp;
|
|
vec3d *next_point;
|
|
|
|
Assertion(pl_objp->instance >= 0, "Arriving ship does not have a valid ship number. Has %d instead. Please report!", pl_objp->instance);
|
|
ship *shipp = &Ships[pl_objp->instance];
|
|
Assertion(shipp->ai_index >= 0, "Arriving ship does not have a valid ai index. Has %d instead. Please report!", shipp->ai_index);
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
|
|
Assertion(parent_objnum > -1, "parent_objnum in ai_acquire_emerge_path was found to be %d, which is an invalid value. Please report to the SCP!", parent_objnum);
|
|
if ( parent_objnum == -1 ) {
|
|
return -1;
|
|
}
|
|
|
|
Assertion(parent_objnum >= 0, "Arriving ship does not have a parent object number. Has %d instead. Please report!", parent_objnum);
|
|
object *parent_objp = &Objects[parent_objnum];
|
|
ship *parent_shipp = &Ships[parent_objp->instance];
|
|
ship_info* parent_sip = &Ship_info[parent_shipp->ship_info_index];
|
|
|
|
polymodel *pm = model_get( parent_sip->model_num );
|
|
const auto& bay = pm->ship_bay;
|
|
|
|
if ( bay == nullptr ) {
|
|
Warning(LOCATION, "Ship %s was set to arrive from fighter bay on object %s, but no fighter bay exists on that ships' model (%s).\n", shipp->ship_name, parent_shipp->ship_name, pm->filename);
|
|
return -1;
|
|
}
|
|
|
|
if ( bay->num_paths <= 0 ) {
|
|
Warning(LOCATION, "Ship %s was set to arrive from fighter bay on object %s, but no fighter bay paths exist on that ships' model (%s).\n", shipp->ship_name, parent_shipp->ship_name, pm->filename);
|
|
return -1;
|
|
}
|
|
|
|
// try to find a bay path that is not taken
|
|
// Goober5000 - choose from among allowed paths
|
|
if (allowed_path_mask != 0)
|
|
{
|
|
int i, num_allowed_paths = 0, allowed_bay_paths[MAX_SHIP_BAY_PATHS];
|
|
|
|
memset(allowed_bay_paths, 0, sizeof(allowed_bay_paths));
|
|
|
|
for (i = 0; i < bay->num_paths; i++)
|
|
{
|
|
if (allowed_path_mask & (1 << i))
|
|
{
|
|
allowed_bay_paths[num_allowed_paths] = i;
|
|
num_allowed_paths++;
|
|
}
|
|
}
|
|
|
|
// Cyborg: to avoid divide by zero and avoid a logic path that should not exist check for 0 allowed paths -- Coverity 1523287
|
|
if (num_allowed_paths == 0) {
|
|
bay_path = Ai_last_arrive_path % bay->num_paths;
|
|
Assertion(false, "Parent_shipp in ai_acquire_emerge_path somehow does not have any allowed bay paths!");
|
|
} else {
|
|
// cycle through the allowed paths
|
|
bay_path = allowed_bay_paths[Ai_last_arrive_path % num_allowed_paths];
|
|
}
|
|
}
|
|
// choose from among all paths
|
|
else
|
|
{
|
|
bay_path = Ai_last_arrive_path % bay->num_paths;
|
|
}
|
|
|
|
Ai_last_arrive_path++;
|
|
|
|
path_index = bay->path_indexes[bay_path];
|
|
|
|
if ( path_index == -1 )
|
|
return -1;
|
|
|
|
// notify parent that I'll want to leave
|
|
parent_shipp->bay_doors_wanting_open++;
|
|
|
|
// keep track of my own status as well
|
|
shipp->bay_doors_need_open = true;
|
|
shipp->bay_doors_launched_from = (ubyte)bay_path;
|
|
shipp->bay_doors_parent_shipnum = parent_objp->instance;
|
|
|
|
// create the path for pl_objp to follow
|
|
create_model_exit_path(pl_objp, parent_objp, path_index, pm->paths[path_index].nverts);
|
|
|
|
// now return to the caller what the starting world pos and starting fvec for the ship will be
|
|
Assert((aip->path_start >= 0) && (aip->path_start < MAX_PATH_POINTS));
|
|
pnp = &Path_points[aip->path_start];
|
|
vec3d pos = pnp->pos;
|
|
|
|
// calc the forward vector using the starting two points of the path
|
|
pnp = &Path_points[aip->path_start+1];
|
|
next_point = &pnp->pos;
|
|
vec3d fvec;
|
|
vm_vec_normalized_dir(&fvec, next_point, &pos);
|
|
|
|
// record the parent objnum, since we'll need it once we're done with following the path
|
|
aip->goal_objnum = parent_objnum;
|
|
aip->goal_signature = parent_objp->signature;
|
|
aip->mode = AIM_BAY_EMERGE;
|
|
aip->submode_start_time = Missiontime;
|
|
|
|
//This is kind of hacky, but allows for hangarbay animations to trigger even if the player is not accompanied by AI.
|
|
//Since it doesn't really keep track of this, it won't ever tell the bay door that the player has fully left for it to close again.
|
|
if (pl_objp->flags[Object::Object_Flags::Player_ship])
|
|
ai_manage_bay_doors(pl_objp, aip, false);
|
|
|
|
//due to the door animations the ship has to remain still until the doors are open
|
|
vec3d vel = ZERO_VECTOR;
|
|
pl_objp->phys_info.vel = vel;
|
|
pl_objp->phys_info.desired_vel = vel;
|
|
pl_objp->phys_info.prev_ramp_vel.xyz.x = 0.0f;
|
|
pl_objp->phys_info.prev_ramp_vel.xyz.y = 0.0f;
|
|
pl_objp->phys_info.prev_ramp_vel.xyz.z = 0.0f;
|
|
pl_objp->phys_info.linear_thrust.xyz.z = 0.0f; // How much the forward thruster is applied. -1 - 1.
|
|
pl_objp->pos = pos;
|
|
|
|
vec3d rvec; vm_vec_zero(&rvec);
|
|
vec3d uvec; vm_vec_zero(&uvec);
|
|
|
|
// use the modder-defined arrival rvec, if applicable
|
|
SCP_string pathName(pm->paths[path_index].name);
|
|
if (parent_sip->pathMetadata.find(pathName) != parent_sip->pathMetadata.end() && !IS_VEC_NULL(&parent_sip->pathMetadata[pathName].arrival_rvec)) {
|
|
vm_vec_copy_normalize(&rvec, &parent_sip->pathMetadata[pathName].arrival_rvec);
|
|
vm_vec_unrotate(&rvec, &rvec, &Objects[aip->path_objnum].orient);
|
|
}
|
|
else if (The_mission.ai_profile->flags[AI::Profile_Flags::Fighterbay_arrivals_use_carrier_orient]) {
|
|
// using only the flag provides a way for modders to use a quick option (though not 100% perfect)
|
|
// also, it is more intuitive that the carrier's uvec is preferable to attempt to match first
|
|
// this avoids unintutive scenarios like rear-facing arrivals ending up side down
|
|
uvec = parent_objp->orient.vec.uvec;
|
|
rvec = parent_objp->orient.vec.rvec;
|
|
}
|
|
|
|
vm_vector_2_matrix_norm(&pl_objp->orient, &fvec, IS_VEC_NULL(&uvec) ? nullptr : &uvec, IS_VEC_NULL(&rvec) ? nullptr : &rvec);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Clean up path data used for emerging from a fighter bay
|
|
*/
|
|
void ai_emerge_bay_path_cleanup(ai_info *aip)
|
|
{
|
|
aip->path_start = -1;
|
|
aip->path_cur = -1;
|
|
aip->path_length = 0;
|
|
aip->mode = AIM_NONE;
|
|
}
|
|
|
|
/**
|
|
* Handler for AIM_BAY_EMERGE
|
|
*/
|
|
void ai_bay_emerge()
|
|
{
|
|
ai_info *aip;
|
|
ship *shipp;
|
|
int parent_died=0;
|
|
|
|
Assert( Pl_objp != NULL );
|
|
Assert( Pl_objp->instance >= 0 );
|
|
|
|
shipp = &Ships[Pl_objp->instance];
|
|
aip = &Ai_info[shipp->ai_index];
|
|
|
|
// if no path to follow, leave this mode
|
|
if ( aip->path_start < 0 ) {
|
|
ai_manage_bay_doors(Pl_objp, aip, true);
|
|
aip->mode = AIM_NONE;
|
|
return;
|
|
}
|
|
|
|
// ensure parent ship is still alive
|
|
if (aip->goal_objnum < 0) {
|
|
parent_died = 1;
|
|
}
|
|
|
|
if ( !parent_died ) {
|
|
if ( Objects[aip->goal_objnum].signature != aip->goal_signature ) {
|
|
parent_died = 1;
|
|
}
|
|
}
|
|
|
|
if ( !parent_died ) {
|
|
Assert(Objects[aip->goal_objnum].type == OBJ_SHIP);
|
|
if ( Ships[Objects[aip->goal_objnum].instance].flags[Ship::Ship_Flags::Dying] ) {
|
|
parent_died = 1;
|
|
}
|
|
}
|
|
|
|
if ( parent_died ) {
|
|
ai_emerge_bay_path_cleanup(aip);
|
|
return;
|
|
}
|
|
|
|
ai_manage_bay_doors(Pl_objp, aip, false);
|
|
|
|
if ( Ships[Objects[aip->goal_objnum].instance].bay_doors_status != MA_POS_READY )
|
|
return;
|
|
|
|
// follow the path to the final point
|
|
ai_path();
|
|
|
|
// New test: must have been in AI_EMERGE mode for at least 10 seconds, and be a minimum distance from the start point
|
|
// This timeout results in AI not completing bay paths that take longer than 10 seconds, so disable with a flag with mods that need it --wookieejedi
|
|
if ( !(aip->ai_profile_flags[AI::Profile_Flags::Disable_bay_emerge_timeout]) &&
|
|
( (Missiontime - aip->submode_start_time) > 10 * F1_0) &&
|
|
( (vm_vec_dist_quick(&Pl_objp->pos, &Objects[aip->goal_objnum].pos) > 0.75f * Objects[aip->goal_objnum].radius) ) ) {
|
|
// erase path
|
|
ai_emerge_bay_path_cleanup(aip);
|
|
// deal with model animations
|
|
ai_manage_bay_doors(Pl_objp, aip, true);
|
|
}
|
|
|
|
// 2-25-99: Need this check to fix an assert for supercap ships... maybe we'll only do this check for supercaps
|
|
if (aip->path_cur > (aip->path_start+aip->path_length-1)) {
|
|
ai_emerge_bay_path_cleanup(aip);
|
|
// deal with model animations
|
|
ai_manage_bay_doors(Pl_objp, aip, true);
|
|
}
|
|
}
|
|
|
|
// Select the closest depart path
|
|
//
|
|
// input: aip => ai info pointer to ship seeking to depart
|
|
// pm => pointer to polymodel for the ship contining the ship bay/depart paths
|
|
//
|
|
// exit: >=0 => ship bay path index for depart path (ie index into sb->paths[])
|
|
// -1 => no path could be found
|
|
//
|
|
// NOTE: this function should only be used for calculating closest depart paths for ai mode
|
|
// AI_BAY_DEPART. It tries to find the closest path that isn't already in use
|
|
int ai_find_closest_depart_path(ai_info *aip, polymodel *pm, int allowed_path_mask)
|
|
{
|
|
int i, j, best_path, best_free_path;
|
|
float dist, min_dist, min_free_dist;
|
|
vec3d *source;
|
|
model_path *mp;
|
|
|
|
const auto& bay = pm->ship_bay;
|
|
|
|
best_free_path = best_path = -1;
|
|
min_free_dist = min_dist = 1e20f;
|
|
Assert(aip->shipnum >= 0);
|
|
source = &Objects[Ships[aip->shipnum].objnum].pos;
|
|
|
|
for ( i = 0; i < bay->num_paths; i++ )
|
|
{
|
|
// Goober5000 - maybe skip this path
|
|
if ((allowed_path_mask != 0) && !(allowed_path_mask & (1 << i)))
|
|
continue;
|
|
|
|
mp = &pm->paths[bay->path_indexes[i]];
|
|
for ( j = 0; j < mp->nverts; j++ )
|
|
{
|
|
dist = vm_vec_dist_squared(source, &mp->verts[j].pos);
|
|
|
|
if (dist < min_dist)
|
|
{
|
|
min_dist = dist;
|
|
best_path = i;
|
|
}
|
|
|
|
// If this is a free path
|
|
if (!(bay->depart_flags & (1 << i)))
|
|
{
|
|
if (dist < min_free_dist)
|
|
{
|
|
min_free_dist = dist;
|
|
best_free_path = i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (best_free_path >= 0)
|
|
{
|
|
return best_free_path;
|
|
}
|
|
|
|
return best_path;
|
|
}
|
|
|
|
// determine what path to use when trying to depart to a fighter bay
|
|
// NOTE: this should be called when AIM_BAY_DEPART mode is set
|
|
//
|
|
// input: pl_objp => pointer to object for ship that is departing
|
|
//
|
|
// exit: -1 => could not find depart path
|
|
// 0 => found depart path
|
|
int ai_acquire_depart_path(object *pl_objp, int parent_objnum, int allowed_path_mask)
|
|
{
|
|
int path_index = -1;
|
|
int ship_bay_path = -1;
|
|
|
|
ship *shipp = &Ships[pl_objp->instance];
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
|
|
if ( parent_objnum < 0 )
|
|
{
|
|
Warning(LOCATION, "In ai_acquire_depart_path(), specified a negative object number for the parent ship! (Departing ship is %s.) Looking for another ship...", shipp->ship_name);
|
|
|
|
// try to locate a capital ship on the same team:
|
|
int shipnum = ship_get_ship_for_departure(shipp->team);
|
|
|
|
if (shipnum >= 0)
|
|
parent_objnum = Ships[shipnum].objnum;
|
|
}
|
|
|
|
aip->path_start = -1;
|
|
|
|
if ( parent_objnum < 0 )
|
|
return -1;
|
|
|
|
object *parent_objp = &Objects[parent_objnum];
|
|
polymodel *pm = model_get(Ship_info[Ships[parent_objp->instance].ship_info_index].model_num );
|
|
const auto& bay = pm->ship_bay;
|
|
|
|
if ( bay == nullptr )
|
|
return -1;
|
|
if ( bay->num_paths <= 0 )
|
|
return -1;
|
|
|
|
// take the closest path we can find
|
|
ship_bay_path = ai_find_closest_depart_path(aip, pm, allowed_path_mask);
|
|
|
|
if ( ship_bay_path < 0 )
|
|
return -1;
|
|
|
|
path_index = bay->path_indexes[ship_bay_path];
|
|
aip->submode_parm0 = ship_bay_path;
|
|
bay->depart_flags |= (1<<ship_bay_path);
|
|
|
|
if ( path_index == -1 ) {
|
|
return -1;
|
|
}
|
|
|
|
// notify parent that I'll want to enter bay
|
|
Ships[parent_objp->instance].bay_doors_wanting_open++;
|
|
|
|
// keep track of my own status as well
|
|
shipp->bay_doors_need_open = true;
|
|
shipp->bay_doors_launched_from = (ubyte)ship_bay_path;
|
|
shipp->bay_doors_parent_shipnum = parent_objp->instance;
|
|
|
|
Assert(path_index < pm->n_paths);
|
|
ai_find_path(pl_objp, parent_objnum, path_index, 0);
|
|
|
|
// Set this flag, so we don't bother recreating the path... we won't need to update the path
|
|
// that has just been created.
|
|
aip->ai_flags.remove(AI::AI_Flags::Use_static_path);
|
|
|
|
aip->goal_objnum = parent_objnum;
|
|
aip->goal_signature = parent_objp->signature;
|
|
aip->mode = AIM_BAY_DEPART;
|
|
|
|
shipp->flags.set(Ship::Ship_Flags::Depart_dockbay);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Handler for AIM_BAY_DEPART
|
|
*/
|
|
void ai_bay_depart()
|
|
{
|
|
auto aip = &Ai_info[Ships[Pl_objp->instance].ai_index];
|
|
|
|
// if no path to follow, leave this mode
|
|
if ( aip->path_start < 0 ) {
|
|
aip->mode = AIM_NONE;
|
|
return;
|
|
}
|
|
|
|
ship* shipp;
|
|
|
|
// check if parent ship valid; if not, abort depart
|
|
if (gameseq_get_state() != GS_STATE_LAB) {
|
|
auto anchor_ship_entry = ship_registry_get(Ships[Pl_objp->instance].departure_anchor);
|
|
if (!anchor_ship_entry ||
|
|
!ship_useful_for_departure(anchor_ship_entry->shipnum, Ships[Pl_objp->instance].departure_path_mask)) {
|
|
mprintf(("Aborting bay departure!\n"));
|
|
aip->mode = AIM_NONE;
|
|
|
|
Ships[Pl_objp->instance].flags.remove(Ship::Ship_Flags::Depart_dockbay);
|
|
return;
|
|
}
|
|
|
|
shipp = anchor_ship_entry->shipp();
|
|
} else {
|
|
shipp = &Ships[Objects[aip->goal_objnum].instance];
|
|
}
|
|
|
|
ai_manage_bay_doors(Pl_objp, aip, false);
|
|
|
|
if ( shipp->bay_doors_status != MA_POS_READY )
|
|
return;
|
|
|
|
// follow the path to the final point
|
|
ai_path();
|
|
|
|
//if we are on the last segment of the path and the bay doors are not open, open them
|
|
//if we are on any other segment of the path, close them
|
|
|
|
// if the final point is reached, let default AI take over
|
|
if ( aip->path_cur >= (aip->path_start+aip->path_length) ) {
|
|
ai_manage_bay_doors(Pl_objp, aip, true);
|
|
|
|
// Volition bay code
|
|
polymodel *pm;
|
|
|
|
pm = model_get(Ship_info[Ships[Objects[aip->goal_objnum].instance].ship_info_index].model_num);
|
|
const auto& bay = pm->ship_bay;
|
|
if ( bay != nullptr ) {
|
|
bay->depart_flags &= ~(1<<aip->submode_parm0);
|
|
}
|
|
|
|
// make ship disappear
|
|
ship_actually_depart(Pl_objp->instance, SHIP_DEPARTED_BAY);
|
|
|
|
// clean up path stuff
|
|
aip->path_start = -1;
|
|
aip->path_cur = -1;
|
|
aip->path_length = 0;
|
|
aip->mode = AIM_NONE;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for AIM_SENTRYGUN. This AI mode is for sentry guns only (ie floating turrets).
|
|
*/
|
|
void ai_sentrygun()
|
|
{
|
|
// Nothing to do here. Turret firing is handled via ai_process_subobjects().
|
|
// If you want the sentry guns to do anything beyond firing their turrets at enemies, add it here!
|
|
}
|
|
|
|
/**
|
|
* Execute behavior given by aip->mode.
|
|
*/
|
|
void ai_execute_behavior(ai_info *aip)
|
|
{
|
|
switch (aip->mode) {
|
|
case AIM_CHASE:
|
|
if (En_objp) {
|
|
ai_chase();
|
|
} else if (aip->submode == SM_EVADE_WEAPON) {
|
|
evade_weapon();
|
|
// maybe reset submode
|
|
if (aip->danger_weapon_objnum == -1) {
|
|
aip->submode = SM_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->last_attack_time = Missiontime;
|
|
}
|
|
} else {
|
|
// Don't circle if this is the instructor.
|
|
ship *shipp = &Ships[aip->shipnum];
|
|
ship_info *sip = &Ship_info[shipp->ship_info_index];
|
|
|
|
if (!is_instructor(&Objects[shipp->objnum])) {
|
|
if (sip->is_big_or_huge()) {
|
|
aip->mode = AIM_NONE;
|
|
} else {
|
|
ai_chase_circle(Pl_objp);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case AIM_EVADE:
|
|
if (En_objp) {
|
|
ai_evade();
|
|
} else {
|
|
vec3d tvec;
|
|
vm_vec_scale_add(&tvec, &Pl_objp->pos, &Pl_objp->orient.vec.rvec, 100.0f);
|
|
turn_towards_point(Pl_objp, &tvec, NULL, 0.0f);
|
|
accelerate_ship(aip, 0.5f);
|
|
}
|
|
break;
|
|
case AIM_STILL:
|
|
ai_still();
|
|
break;
|
|
case AIM_FLY_TO_SHIP: // same code as stay-near
|
|
case AIM_STAY_NEAR:
|
|
ai_stay_near();
|
|
break;
|
|
case AIM_GUARD:
|
|
ai_guard();
|
|
break;
|
|
case AIM_WAYPOINTS:
|
|
ai_waypoints();
|
|
break;
|
|
case AIM_DOCK:
|
|
ai_dock();
|
|
break;
|
|
case AIM_NONE:
|
|
break;
|
|
case AIM_BIGSHIP:
|
|
break;
|
|
case AIM_PATH: {
|
|
int path_num;
|
|
path_num = ai_return_path_num_from_dockbay(&Objects[aip->goal_objnum], 0);
|
|
ai_find_path(Pl_objp, aip->goal_objnum, path_num, 0);
|
|
ai_path();
|
|
break;
|
|
}
|
|
case AIM_SAFETY:
|
|
ai_safety();
|
|
break;
|
|
case AIM_EVADE_WEAPON:
|
|
evade_weapon();
|
|
break;
|
|
case AIM_STRAFE:
|
|
if (En_objp) {
|
|
Assert(En_objp->type == OBJ_SHIP);
|
|
if (!(better_collision_avoidance_triggered(
|
|
The_mission.ai_profile->flags[AI::Profile_Flags::Better_combat_collision_avoidance],
|
|
The_mission.ai_profile->better_collision_avoid_aggression_combat,
|
|
Pl_objp,
|
|
The_mission.ai_profile->flags[AI::Profile_Flags::Better_combat_collision_avoid_includes_target] ? nullptr : En_objp))) {
|
|
ai_big_strafe(); // strafe a big ship
|
|
}
|
|
} else {
|
|
aip->mode = AIM_NONE;
|
|
}
|
|
break;
|
|
case AIM_GET_BEHIND:
|
|
if (En_objp) {
|
|
Assert(En_objp->type == OBJ_SHIP);
|
|
// This mode is not currently implemented, but if it were, it would be called here.
|
|
nprintf(("AI", "AIM_GET_BEHIND called; this mode is not yet implemented\n"));
|
|
} else {
|
|
aip->mode = AIM_NONE;
|
|
}
|
|
break;
|
|
case AIM_BAY_EMERGE:
|
|
ai_bay_emerge();
|
|
break;
|
|
case AIM_BAY_DEPART:
|
|
ai_bay_depart();
|
|
break;
|
|
case AIM_SENTRYGUN:
|
|
ai_sentrygun();
|
|
break;
|
|
case AIM_WARP_OUT:
|
|
break; // Note, handled directly from ai_frame().
|
|
case AIM_LUA:
|
|
ai_lua(aip);
|
|
break;
|
|
default:
|
|
UNREACHABLE("Unknown AI Mode %d! Get a coder!", aip->mode);
|
|
break;
|
|
}
|
|
|
|
if ( Ship_info[Ships[aip->shipnum].ship_info_index].is_flyable() ) {
|
|
maybe_evade_dumbfire_weapon(aip);
|
|
}
|
|
}
|
|
|
|
// Auxiliary function for maybe_request_support.
|
|
// Return 1 if subsystem "type" is worthy of repair, else return 0.
|
|
// Since subsystems cannot be repaired if they are at 0 strength, don't return 1 if subsystem is dead.
|
|
int mrs_subsystem(ship *shipp, int type)
|
|
{
|
|
float t;
|
|
|
|
t = ship_get_subsystem_strength(shipp, type);
|
|
|
|
if (t > 0.0f) {
|
|
return (int) ((1.0f - t) * 3);
|
|
} else {
|
|
return 3;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return number of ships on *objp's team that are currently rearming.
|
|
*/
|
|
int num_allies_rearming(object *objp)
|
|
{
|
|
ship_obj *so;
|
|
int team;
|
|
int count = 0;
|
|
|
|
team = Ships[objp->instance].team;
|
|
|
|
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
|
|
object *A;
|
|
|
|
Assert (so->objnum != -1);
|
|
A = &Objects[so->objnum];
|
|
if (A->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
if (Ships[A->instance].team == team) {
|
|
if (Ai_info[Ships[A->instance].ai_index].ai_flags[AI::AI_Flags::Repairing, AI::AI_Flags::Awaiting_repair]) {
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
|
|
// Maybe ship *objp should request support (rearm/repair).
|
|
// If it does, return TRUE, else return FALSE.
|
|
int maybe_request_support(object *objp)
|
|
{
|
|
ship_info *sip;
|
|
ship *shipp;
|
|
ai_info *aip;
|
|
weapon_info *wip;
|
|
int desire;
|
|
int i;
|
|
float r;
|
|
|
|
Assert(objp->type == OBJ_SHIP);
|
|
shipp = &Ships[objp->instance];
|
|
aip = &Ai_info[shipp->ai_index];
|
|
sip = &Ship_info[shipp->ship_info_index];
|
|
|
|
// Only fighters and bombers request support.
|
|
if (!(sip->is_fighter_bomber()))
|
|
return 0;
|
|
|
|
// A ship that is currently awaiting does not need support!
|
|
if (aip->ai_flags[AI::AI_Flags::Being_repaired, AI::AI_Flags::Awaiting_repair])
|
|
return 0;
|
|
|
|
if (!timestamp_elapsed(aip->next_rearm_request_timestamp))
|
|
return 0;
|
|
|
|
// just do a simple check first, and the more expensive check later
|
|
if (!is_support_allowed(objp, true))
|
|
return 0;
|
|
|
|
// Goober5000 - a ship that is currently docked shouldn't request repair.
|
|
// This is to prevent support ships being summoned for a ship that is getting
|
|
// towed away by other means.
|
|
if (object_is_docked(objp))
|
|
return 0;
|
|
|
|
// Compute a desire value.
|
|
// Desire of 0 means no reason to request support.
|
|
// 1 is slight, 2 more, etc. Maximum is around 20. Anything larger than 3 is pretty strong.
|
|
desire = 0;
|
|
|
|
// Set desire based on hull strength.
|
|
// Note: We no longer repair hull, so this would cause repeated repair requests.
|
|
// Added back in upon mission flag condition - Goober5000
|
|
if (The_mission.flags[Mission::Mission_Flags::Support_repairs_hull])
|
|
{
|
|
desire += 6 - (int) (get_hull_pct(objp) * 6.0f);
|
|
}
|
|
|
|
// Set desire based on key subsystems.
|
|
desire += 2*mrs_subsystem(shipp, SUBSYSTEM_ENGINE); // Note, disabled engine forces repair request, regardless of nearby enemies.
|
|
desire += mrs_subsystem(shipp, SUBSYSTEM_COMMUNICATION);
|
|
desire += mrs_subsystem(shipp, SUBSYSTEM_WEAPONS);
|
|
desire += mrs_subsystem(shipp, SUBSYSTEM_SENSORS);
|
|
|
|
|
|
ship_weapon *swp = &shipp->weapons;
|
|
|
|
// Set desire based on percentage of secondary weapons.
|
|
for (i = 0; i < swp->num_secondary_banks; ++i)
|
|
{
|
|
if (swp->secondary_bank_start_ammo[i] > 0)
|
|
{
|
|
r = (float)swp->secondary_bank_ammo[i] / swp->secondary_bank_start_ammo[i];
|
|
desire += (int)((1.0f - r) * 3.0f);
|
|
}
|
|
}
|
|
|
|
// Set desire based on ballistic weapons - Goober5000
|
|
for (i = 0; i < swp->num_primary_banks; ++i)
|
|
{
|
|
wip = &Weapon_info[swp->primary_bank_weapons[i]];
|
|
|
|
if (wip->wi_flags[Weapon::Info_Flags::Ballistic] && swp->primary_bank_start_ammo[i] > 0)
|
|
{
|
|
r = (float) swp->primary_bank_ammo[i] / swp->primary_bank_start_ammo[i];
|
|
|
|
// cube ammo level for better behavior, and adjust for number of banks
|
|
desire += (int) ((1.0f - r)*(1.0f - r)*(1.0f - r) * (5.0f / swp->num_primary_banks));
|
|
}
|
|
}
|
|
|
|
|
|
// If no reason to repair, don't bother to see if it's safe to repair.
|
|
if (desire == 0){
|
|
return 0;
|
|
}
|
|
|
|
bool try_to_rearm = false;
|
|
|
|
// Compute danger threshold.
|
|
// Balance this with desire and maybe request support.
|
|
if (ai_good_time_to_rearm(objp)) {
|
|
try_to_rearm = true;
|
|
} else if (ai_bad_time_to_rearm(objp)) {
|
|
try_to_rearm = false;
|
|
} else if (num_allies_rearming(objp) < The_mission.ai_profile->max_allies_rearming_threshold) {
|
|
if (desire >= 8) { // guarantees disabled will cause repair request
|
|
try_to_rearm = true;
|
|
} else if (desire >= 3) { // >= 3 means having a single subsystem fully blown will cause repair.
|
|
int count;
|
|
int objnum = find_nearby_threat(OBJ_INDEX(objp), iff_get_attacker_mask(obj_team(objp)), 2000.0f, &count);
|
|
|
|
if ((objnum == -1) || (count < 2) || (vm_vec_dist_quick(&objp->pos, &Objects[objnum].pos) > 3000.0f*count/desire)) {
|
|
try_to_rearm = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (try_to_rearm) {
|
|
// Now do the more thorough check
|
|
if (is_support_allowed(objp)) {
|
|
ai_issue_rearm_request(objp);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void ai_set_mode_warp_out(object *objp, ai_info *aip)
|
|
{
|
|
ai_abort_rearm_request(objp);
|
|
|
|
aip->mode = AIM_WARP_OUT;
|
|
aip->submode = AIS_WARP_1;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
|
|
// Maybe make ship depart (Goober5000 - changed from always warp ship out)
|
|
// Shivan and HoL fighter/bomber warp out if their weapons subsystems have been destroyed.
|
|
void ai_maybe_depart(object *objp)
|
|
{
|
|
ship *shipp;
|
|
ship_info *sip;
|
|
|
|
// don't do anything if in a training mission.
|
|
if ( The_mission.game_type & MISSION_TYPE_TRAINING )
|
|
return;
|
|
|
|
Assert(objp->type == OBJ_SHIP);
|
|
|
|
shipp = &Ships[objp->instance];
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
sip = &Ship_info[shipp->ship_info_index];
|
|
|
|
if (aip->mode == AIM_WARP_OUT || aip->mode == AIM_BAY_DEPART)
|
|
return;
|
|
|
|
// If a support ship with no goals and low hull, depart. Be sure that there are no pending goals
|
|
// in the support ships ai_goal array. Just process this ships goals.
|
|
if (sip->flags[Ship::Info_Flags::Support]) {
|
|
if ( timestamp_elapsed(aip->warp_out_timestamp) ) {
|
|
ai_process_mission_orders( OBJ_INDEX(objp), aip );
|
|
if ( (aip->support_ship_objnum == -1) && (get_hull_pct(objp) < 0.25f) ) {
|
|
if (!shipp->is_departing()) {
|
|
if (!mission_do_departure(objp)) {
|
|
// if departure failed, try again at a later point
|
|
// (timestamp taken from ai_do_objects_repairing_stuff)
|
|
aip->warp_out_timestamp = timestamp((int) ((30 + 10*frand()) * 1000));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// some iffs don't warp out, they'll eventually request support.
|
|
if (Iff_info[shipp->team].flags & IFFF_SUPPORT_ALLOWED)
|
|
return;
|
|
|
|
if (!shipp->is_departing()) {
|
|
if (sip->is_fighter_bomber()) {
|
|
if (aip->warp_out_timestamp == 0) {
|
|
//if (ship_subsystems_blown(shipp, SUBSYSTEM_WEAPONS)) {
|
|
// aip->warp_out_timestamp = timestamp(Random::next(10, 19) * 1000);
|
|
//}
|
|
} else if (timestamp_elapsed(aip->warp_out_timestamp)) {
|
|
mission_do_departure(objp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Warp this ship out.
|
|
*/
|
|
void ai_warp_out(object *objp)
|
|
{
|
|
ship *shipp = &Ships[objp->instance];
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
|
|
// if dying, don't warp out.
|
|
if (shipp->flags[Ship::Ship_Flags::Dying])
|
|
return;
|
|
|
|
// Goober5000 - check for engine or navigation failure
|
|
if (!ship_engine_ok_to_warp(shipp) || !ship_navigation_ok_to_warp(shipp))
|
|
{
|
|
// you shouldn't hit this... if you do, then I need to add a check for it
|
|
// in whatever function initiates a warpout
|
|
Assert (!(shipp->flags[Ship::Ship_Flags::No_subspace_drive]));
|
|
|
|
// flag us as trying to warp so that this function keeps getting called
|
|
// (in other words, if we can't warp just yet, we want to warp at the first
|
|
// opportunity)
|
|
if (aip->mode != AIM_WARP_OUT)
|
|
aip->mode = AIM_WARP_OUT;
|
|
aip->submode = AIS_WARP_1;
|
|
aip->ai_flags.set(AI::AI_Flags::Trying_unsuccessfully_to_warp);
|
|
|
|
return;
|
|
}
|
|
|
|
// Goober5000 - make sure the flag is clear (if it was previously set)
|
|
aip->ai_flags.remove(AI::AI_Flags::Trying_unsuccessfully_to_warp);
|
|
|
|
WarpParams* warp_params = &Warp_params[shipp->warpout_params_index];
|
|
|
|
switch (aip->submode) {
|
|
case AIS_WARP_1:
|
|
aip->force_warp_time = timestamp(10*1000); // Try to avoid a collision for up to ten seconds.
|
|
aip->submode = AIS_WARP_2;
|
|
aip->submode_start_time = Missiontime;
|
|
break;
|
|
case AIS_WARP_2: // Make sure won't collide with any object.
|
|
if (timestamp_elapsed(aip->force_warp_time)
|
|
|| (warp_params->warp_type == WT_SWEEPER)
|
|
|| (warp_params->warp_type == WT_IN_PLACE_ANIM)
|
|
|| (!collide_predict_large_ship(objp, objp->radius * 2.0f + 100.0f))) {
|
|
aip->submode = AIS_WARP_3;
|
|
aip->submode_start_time = Missiontime;
|
|
|
|
// maybe recalculate collision pairs.
|
|
if ((objp->flags[Object::Object_Flags::Collides]) && (ship_get_warpout_speed(objp) > ship_get_max_speed(shipp))) {
|
|
// recalculate collision pairs
|
|
OBJ_RECALC_PAIRS(objp);
|
|
}
|
|
|
|
aip->force_warp_time = timestamp(4*1000); // Try to attain target speed for up to 4 seconds.
|
|
} else {
|
|
vec3d goal_point;
|
|
vm_vec_scale_add(&goal_point, &objp->pos, &objp->orient.vec.uvec, 100.0f);
|
|
turn_towards_point(objp, &goal_point, NULL, 0.0f);
|
|
accelerate_ship(aip, 0.0f);
|
|
}
|
|
break;
|
|
case AIS_WARP_3:
|
|
// Rampup desired_vel in here from current to desired velocity and set PF_USE_VEL. (not sure this is the right flag)
|
|
// See shipfx#572 for sample code.
|
|
float speed, goal_speed;
|
|
goal_speed = ship_get_warpout_speed(objp);
|
|
|
|
// HUGE ships go immediately to AIS_WARP_4
|
|
if (Ship_info[shipp->ship_info_index].is_huge_ship()) {
|
|
aip->submode = AIS_WARP_4;
|
|
aip->submode_start_time = Missiontime;
|
|
break;
|
|
}
|
|
|
|
speed = goal_speed * flFrametime + objp->phys_info.speed * (1.0f - flFrametime);
|
|
vm_vec_copy_scale(&objp->phys_info.vel, &objp->orient.vec.fvec, speed);
|
|
objp->phys_info.desired_vel = objp->phys_info.vel;
|
|
|
|
if (timestamp_elapsed(aip->force_warp_time) || (fl_abs(objp->phys_info.speed - goal_speed) < 2.0f)) {
|
|
aip->submode = AIS_WARP_4;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
break;
|
|
case AIS_WARP_4: {
|
|
// Only lets the ship warp after waiting for the warpout engage time
|
|
if ( (Missiontime / 100) >= (aip->submode_start_time / 100 + warp_params->warpout_engage_time) ) {
|
|
shipfx_warpout_start(objp);
|
|
aip->submode = AIS_WARP_5;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
break;
|
|
}
|
|
case AIS_WARP_5:
|
|
break;
|
|
default:
|
|
UNREACHABLE("Illegal ai submode of %d in ai_warp_out. Pleae report to the SCP!", aip->submode);
|
|
}
|
|
}
|
|
|
|
// Return object index of weapon that could produce a shockwave that should be known about to *objp.
|
|
// Return nearest one.
|
|
static int ai_find_shockwave_weapon(const object *objp)
|
|
{
|
|
missile_obj *mo;
|
|
float nearest_dist = 999999.9f;
|
|
int nearest_index = -1;
|
|
|
|
for ( mo = GET_NEXT(&Missile_obj_list); mo != END_OF_LIST(&Missile_obj_list); mo = GET_NEXT(mo) ) {
|
|
object *A;
|
|
weapon *wp;
|
|
weapon_info *wip;
|
|
|
|
Assert(mo->objnum >= 0 && mo->objnum < MAX_OBJECTS);
|
|
A = &Objects[mo->objnum];
|
|
if (A->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
Assert(A->type == OBJ_WEAPON);
|
|
Assert((A->instance >= 0) && (A->instance < MAX_WEAPONS));
|
|
wp = &Weapons[A->instance];
|
|
wip = &Weapon_info[wp->weapon_info_index];
|
|
Assert( wip->subtype == WP_MISSILE );
|
|
|
|
if (wip->shockwave.speed > 0.0f) {
|
|
float dist;
|
|
|
|
dist = vm_vec_dist_quick(&objp->pos, &A->pos);
|
|
if (dist < nearest_dist) {
|
|
nearest_dist = dist;
|
|
nearest_index = mo->objnum;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nearest_index;
|
|
}
|
|
|
|
#define EVADE_SHOCKWAVE_DAMAGE_THRESHOLD 100.0f
|
|
|
|
// Tell all ships to avoid a big ship that is blowing up.
|
|
// Only avoid if shockwave is fairly large.
|
|
// OK to tell everyone to avoid. If they're too far away, that gets cleaned up in the frame interval.
|
|
void ai_announce_ship_dying(object *dying_objp)
|
|
{
|
|
float damage = ship_get_exp_damage(dying_objp);
|
|
if (damage >= EVADE_SHOCKWAVE_DAMAGE_THRESHOLD) {
|
|
ship_obj *so;
|
|
|
|
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
|
|
object *A = &Objects[so->objnum];
|
|
if (A->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
Assert(A->type == OBJ_SHIP);
|
|
|
|
// Goober5000 - the disallow is now handled uniformly in the ai_avoid_shockwave function
|
|
/*if (Ship_info[Ships[A->instance].ship_info_index].flags & (SIF_SMALL_SHIP | SIF_FREIGHTER | SIF_TRANSPORT))*/ {
|
|
ai_info *aip = &Ai_info[Ships[A->instance].ai_index];
|
|
|
|
// AL 1-5-98: only avoid shockwave if not docked or repairing
|
|
if ( !object_is_docked(A) && !(aip->ai_flags[AI::AI_Flags::Repairing, AI::AI_Flags::Being_repaired]) ) {
|
|
aip->ai_flags.set(AI::AI_Flags::Avoid_shockwave_ship);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Return object index of weapon that could produce a shockwave that should be known about to *objp.
|
|
// Return nearest one.
|
|
static int ai_find_shockwave_ship(object *objp)
|
|
{
|
|
ship_obj *so;
|
|
float nearest_dist = 999999.9f;
|
|
int nearest_index = -1;
|
|
|
|
for ( so = GET_NEXT(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
|
|
object *A;
|
|
ship *shipp;
|
|
|
|
Assert(so->objnum >= 0 && so->objnum < MAX_OBJECTS);
|
|
A = &Objects[so->objnum];
|
|
if (A->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
Assert(A->type == OBJ_SHIP);
|
|
Assert((A->instance >= 0) && (A->instance < MAX_SHIPS));
|
|
shipp = &Ships[A->instance];
|
|
// Only look at objects in the process of dying.
|
|
if (shipp->flags[Ship::Ship_Flags::Dying]) {
|
|
float damage;
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_avoid_shockwave_bugs])
|
|
damage = ship_get_exp_damage(A); // A is the object that is actually exploding!
|
|
else
|
|
damage = ship_get_exp_damage(objp);
|
|
|
|
if (damage >= EVADE_SHOCKWAVE_DAMAGE_THRESHOLD) { // Only evade quite large blasts
|
|
float dist;
|
|
|
|
dist = vm_vec_dist_quick(&objp->pos, &A->pos);
|
|
if (dist < nearest_dist) {
|
|
nearest_dist = dist;
|
|
nearest_index = so->objnum;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nearest_index;
|
|
|
|
}
|
|
|
|
int aas_1(object *objp, ai_info *aip, vec3d *safe_pos)
|
|
{
|
|
bool fix_bugs = The_mission.ai_profile->flags[AI::Profile_Flags::Fix_avoid_shockwave_bugs];
|
|
|
|
// MAKE SURE safe_pos DOES NOT TAKE US TOWARD THE A SHIP WE'RE ATTACKING.
|
|
if (aip->ai_flags[AI::AI_Flags::Avoid_shockwave_weapon]) {
|
|
// If we don't currently know of a weapon to avoid, try to find one.
|
|
// If we can't find one, then clear the bit so we don't keep coming here.
|
|
if (aip->shockwave_object == -1) {
|
|
int shockwave_weapon = ai_find_shockwave_weapon(objp);
|
|
if (shockwave_weapon == -1) {
|
|
aip->ai_flags.remove(AI::AI_Flags::Avoid_shockwave_weapon);
|
|
return 0;
|
|
} else {
|
|
aip->shockwave_object = shockwave_weapon;
|
|
}
|
|
}
|
|
|
|
// OK, we have reason to believe we should avoid aip->shockwave_object.
|
|
Assert(aip->shockwave_object > -1);
|
|
object *weapon_objp = &Objects[aip->shockwave_object];
|
|
if (weapon_objp->type != OBJ_WEAPON) {
|
|
aip->ai_flags.remove(AI::AI_Flags::Avoid_shockwave_weapon);
|
|
aip->shockwave_object = -1;
|
|
return 0;
|
|
}
|
|
|
|
weapon *weaponp = &Weapons[weapon_objp->instance];
|
|
weapon_info *wip = &Weapon_info[weaponp->weapon_info_index];
|
|
object *target_ship_obj = NULL;
|
|
|
|
if (wip->shockwave.speed == 0.0f) {
|
|
aip->ai_flags.remove(AI::AI_Flags::Avoid_shockwave_weapon);
|
|
aip->shockwave_object = -1;
|
|
return 0;
|
|
}
|
|
|
|
float danger_dist;
|
|
vec3d expected_pos = vmd_zero_vector; // Position at which we expect the weapon to detonate.
|
|
int pos_set = 0;
|
|
|
|
danger_dist = wip->shockwave.outer_rad;
|
|
// Set predicted position of detonation.
|
|
// If an aspect locked missile, assume it will detonate at the homing position.
|
|
// If not, which is not possible in a default FreeSpace weapon, then predict it will detonate at some
|
|
// time in the future, this time based on max lifetime and life left.
|
|
if (wip->is_locked_homing()) {
|
|
expected_pos = weaponp->homing_pos;
|
|
if (weaponp->homing_object && weaponp->homing_object->type == OBJ_SHIP) {
|
|
target_ship_obj = weaponp->homing_object;
|
|
}
|
|
pos_set = 1;
|
|
if (IS_VEC_NULL(&weaponp->homing_pos)) {
|
|
pos_set = 0;
|
|
if (weaponp->target_num != -1) {
|
|
if (Objects[weaponp->target_num].type == OBJ_SHIP) {
|
|
target_ship_obj = &Objects[weaponp->target_num];
|
|
expected_pos = target_ship_obj->pos;
|
|
pos_set = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pos_set) {
|
|
// if the object is not a small ship, a surface impact is likely to be some distance away from the ship center, so refine the position
|
|
if (fix_bugs && target_ship_obj && !Ship_info[Ships[target_ship_obj->instance].ship_info_index].is_small_ship()) {
|
|
// set up cache if needed
|
|
if (weaponp->homing_cache_ptr == nullptr) {
|
|
weaponp->homing_cache_ptr.reset(new homing_cache_info{ TIMESTAMP::immediate(), expected_pos, false, false });
|
|
}
|
|
|
|
// only refine the position every so often though
|
|
if (!weaponp->homing_cache_ptr->skip_future_refinements && timestamp_elapsed(weaponp->homing_cache_ptr->next_update)) {
|
|
mc_info mc;
|
|
mc.model_instance_num = Ships[target_ship_obj->instance].model_instance_num;
|
|
mc.model_num = Ship_info[Ships[target_ship_obj->instance].ship_info_index].model_num;
|
|
mc.orient = &target_ship_obj->orient;
|
|
mc.pos = &target_ship_obj->pos;
|
|
mc.p0 = &weapon_objp->pos; // Point 1 of ray to check
|
|
mc.p1 = &expected_pos; // Point 2 of ray to check
|
|
mc.flags = MC_CHECK_MODEL;
|
|
|
|
model_collide(&mc);
|
|
if (mc.num_hits > 0) {
|
|
// if the refined point was sufficiently close to the homing point, skip future refinements
|
|
// (refining the point only matters if the surface impact really is far away from the homing position,
|
|
// e.g. if a missile is homing on the center of a large ship or large subsystem)
|
|
float refinement_threshold = std::min(10.0f, wip->shockwave.inner_rad * 0.1f);
|
|
if (vm_vec_dist_squared(&expected_pos, &mc.hit_point_world) < refinement_threshold * refinement_threshold) {
|
|
weaponp->homing_cache_ptr->skip_future_refinements = true;
|
|
weaponp->homing_cache_ptr->valid = false;
|
|
} else {
|
|
weaponp->homing_cache_ptr->expected_pos = mc.hit_point_world;
|
|
weaponp->homing_cache_ptr->valid = true;
|
|
}
|
|
} else {
|
|
weaponp->homing_cache_ptr->valid = false;
|
|
}
|
|
|
|
weaponp->homing_cache_ptr->next_update = _timestamp(500);
|
|
}
|
|
|
|
if (weaponp->homing_cache_ptr->valid) {
|
|
expected_pos = weaponp->homing_cache_ptr->expected_pos;
|
|
}
|
|
}
|
|
} else {
|
|
float time_scale;
|
|
|
|
if (wip->lifetime - weaponp->lifeleft > 5.0f) {
|
|
time_scale = 1.0f;
|
|
} else {
|
|
time_scale = weaponp->lifeleft/2.0f;
|
|
}
|
|
|
|
vm_vec_scale_add(&expected_pos, &weapon_objp->pos, &weapon_objp->orient.vec.fvec, time_scale);
|
|
}
|
|
|
|
// See if too far away to care about shockwave.
|
|
if (vm_vec_dist_quick(&objp->pos, &expected_pos) > danger_dist*2.0f) {
|
|
// Volition commented this out, but based on testing it looks like removing the flag makes more sense.
|
|
// Also, the corresponding flag is removed in the distance check for ship shockwaves.
|
|
if (fix_bugs) {
|
|
aip->ai_flags.remove(AI::AI_Flags::Avoid_shockwave_weapon);
|
|
aip->shockwave_object = -1;
|
|
}
|
|
//aip->ai_flags &= ~AIF_AVOID_SHOCKWAVE_WEAPON;
|
|
return 0;
|
|
} else {
|
|
// try to find a safe position
|
|
vec3d vec_from_exp;
|
|
float dir = 1.0f;
|
|
vm_vec_sub(&vec_from_exp, &objp->pos, &expected_pos);
|
|
float dot = vm_vec_dot(&vec_from_exp, &weapon_objp->orient.vec.fvec);
|
|
if (dot > -30) {
|
|
// if we're already on the other side of the explosion, don't try to fly behind it
|
|
dir = -1.0f;
|
|
}
|
|
|
|
// Fly towards a point behind the weapon.
|
|
vm_vec_scale_add(safe_pos, &weapon_objp->pos, &weapon_objp->orient.vec.fvec, -50000.0f*dir);
|
|
|
|
return 1;
|
|
}
|
|
} else if (aip->ai_flags[AI::AI_Flags::Avoid_shockwave_ship]) {
|
|
if (aip->shockwave_object == -1) {
|
|
int shockwave_ship = ai_find_shockwave_ship(objp);
|
|
if (shockwave_ship == -1) {
|
|
aip->ai_flags.remove(AI::AI_Flags::Avoid_shockwave_ship);
|
|
return 0;
|
|
} else {
|
|
aip->shockwave_object = shockwave_ship;
|
|
}
|
|
}
|
|
|
|
Assert(aip->shockwave_object > -1);
|
|
object *ship_objp = &Objects[aip->shockwave_object];
|
|
if (ship_objp == objp) {
|
|
aip->shockwave_object = -1; // this one was already present in retail
|
|
return 0;
|
|
}
|
|
|
|
if (ship_objp->type != OBJ_SHIP) {
|
|
aip->ai_flags.remove(AI::AI_Flags::Avoid_shockwave_ship);
|
|
if (fix_bugs) aip->shockwave_object = -1;
|
|
return 0;
|
|
}
|
|
|
|
// Optimize note! Don't really have to normalize. We only need a point away from the blowing-up ship.
|
|
vec3d safe_vec;
|
|
|
|
vm_vec_normalized_dir(&safe_vec, &objp->pos, &ship_objp->pos);
|
|
vm_vec_scale_add(safe_pos, &ship_objp->pos, &safe_vec, 50000.0f); // Fly away from the ship.
|
|
|
|
float outer_rad = ship_get_exp_outer_rad(ship_objp);
|
|
|
|
if (vm_vec_dist_quick(&objp->pos, &ship_objp->pos) > outer_rad*1.5f) {
|
|
aip->ai_flags.remove(AI::AI_Flags::Avoid_shockwave_ship);
|
|
if (fix_bugs) aip->shockwave_object = -1;
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
|
|
} else {
|
|
Assertion(false, "aas_1 has been passed an invalid object type of %d, please report to the SCP!", objp->type);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Make object *objp avoid the nearest dangerous shockwave-producing weapon.
|
|
// If it looks like there is no valid shockwave-producing weapon then clear the AIF_AVOID_SHOCKWAVE_WEAPON bit in ai_flags and return.
|
|
// Return 1 if avoiding a shockwave, else return 0.
|
|
int ai_avoid_shockwave(object *objp, ai_info *aip)
|
|
{
|
|
vec3d safe_pos;
|
|
bool fix_bugs = The_mission.ai_profile->flags[AI::Profile_Flags::Fix_avoid_shockwave_bugs];
|
|
|
|
// BIG|HUGE do not respond to shockwaves
|
|
// Goober5000 - let's treat shockwave response the same way whether from weapon or ship
|
|
if (!Ship_info[Ships[objp->instance].ship_info_index].avoids_shockwaves()) {
|
|
// don't come here again
|
|
aip->ai_flags.remove(AI::AI_Flags::Avoid_shockwave_ship);
|
|
aip->ai_flags.remove(AI::AI_Flags::Avoid_shockwave_weapon);
|
|
if (fix_bugs) aip->shockwave_object = -1;
|
|
return 0;
|
|
}
|
|
|
|
// Don't try to evade a homing weapon until it starts homing
|
|
if (fix_bugs && aip->ai_flags[AI::AI_Flags::Avoid_shockwave_weapon] && !(aip->ai_flags[AI::AI_Flags::Avoid_shockwave_started])) {
|
|
// since the "don't all react right away" timer should only start when the weapon begins homing, we need the actual weapon now...
|
|
// so choose the weapon ahead of time using the same technique aas_1() would normally use later
|
|
if (aip->shockwave_object == -1) {
|
|
aip->shockwave_object = ai_find_shockwave_weapon(objp);
|
|
if (aip->shockwave_object == -1) {
|
|
aip->ai_flags.remove(AI::AI_Flags::Avoid_shockwave_weapon);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// if this is a homing weapon that isn't homing, don't react yet
|
|
// (this might be a should-be-dead weapon, in which case it will be pruned with an object type check inside aas_1(),
|
|
// so to minimize changes to the existing logic, don't prune it here)
|
|
if (!Objects[aip->shockwave_object].flags[Object::Object_Flags::Should_be_dead]) {
|
|
auto wp = &Weapons[Objects[aip->shockwave_object].instance];
|
|
if (Weapon_info[wp->weapon_info_index].is_homing() && IS_VEC_NULL(&wp->homing_pos)) {
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Don't all react right away.
|
|
if (!(aip->ai_flags[AI::AI_Flags::Avoid_shockwave_started])) {
|
|
float evadeChance = (aip->ai_shockwave_evade_chance == FLT_MIN)
|
|
? ((float) ai_maybe_autoscale(aip->ai_class)/4.0f + 0.25f)
|
|
: aip->ai_shockwave_evade_chance;
|
|
if (!rand_chance(flFrametime, evadeChance)) // Chance to avoid in 1 second is 0.25 + ai_class/4
|
|
return 0;
|
|
}
|
|
|
|
if (!aas_1(objp, aip, &safe_pos)) {
|
|
aip->ai_flags.set(AI::AI_Flags::Avoid_shockwave_started);
|
|
return 0;
|
|
}
|
|
|
|
aip->ai_flags.set(AI::AI_Flags::Avoid_shockwave_started);
|
|
|
|
// OK, evade the shockwave!
|
|
turn_towards_point(objp, &safe_pos, NULL, 0.0f);
|
|
vec3d vec_to_safe_pos;
|
|
float dot_to_goal;
|
|
|
|
vm_vec_normalized_dir(&vec_to_safe_pos, &safe_pos, &objp->pos);
|
|
|
|
dot_to_goal = vm_vec_dot(&objp->orient.vec.fvec, &vec_to_safe_pos);
|
|
if (dot_to_goal < -0.5f)
|
|
accelerate_ship(aip, 0.3f);
|
|
else {
|
|
accelerate_ship(aip, 1.0f + dot_to_goal);
|
|
if (dot_to_goal > 0.2f) {
|
|
if (!(objp->phys_info.flags & PF_AFTERBURNER_ON )) {
|
|
afterburners_start(objp);
|
|
aip->afterburner_stop_time = Missiontime + 2*F1_0;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
// Awaiting repair. Be useful.
|
|
// Probably fly towards incoming repair ship.
|
|
// Return true if this ship is close to being repaired, else return false.
|
|
int ai_await_repair_frame(object *objp, ai_info *aip)
|
|
{
|
|
if (!(aip->ai_flags[AI::AI_Flags::Being_repaired, AI::AI_Flags::Awaiting_repair]))
|
|
return 0;
|
|
|
|
if (aip->support_ship_objnum == -1)
|
|
return 0;
|
|
|
|
ship *shipp;
|
|
ship_info *sip;
|
|
|
|
shipp = &Ships[Objects[aip->support_ship_objnum].instance];
|
|
sip = &Ship_info[shipp->ship_info_index];
|
|
|
|
aip->ai_flags.remove(AI::AI_Flags::Formation_object); // Prevents endless rotation.
|
|
|
|
if (!(sip->flags[Ship::Info_Flags::Support]))
|
|
return 0;
|
|
|
|
vec3d goal_point;
|
|
object *repair_objp;
|
|
|
|
repair_objp = &Objects[aip->support_ship_objnum];
|
|
|
|
if (Ships[repair_objp->instance].team == Iff_traitor) {
|
|
ai_abort_rearm_request(repair_objp);
|
|
return 0;
|
|
}
|
|
|
|
vm_vec_scale_add(&goal_point, &repair_objp->pos, &repair_objp->orient.vec.uvec, -50.0f); // Fly towards point below repair ship.
|
|
|
|
vec3d vtr;
|
|
float dist = vm_vec_normalized_dir(&vtr, &goal_point, &objp->pos);
|
|
float dot = vm_vec_dot(&vtr, &objp->orient.vec.fvec);
|
|
|
|
if (dist > 200.0f) {
|
|
accelerate_ship(aip, (0.9f + dot) * dist/1500.0f);
|
|
turn_towards_point(objp, &goal_point, NULL, 0.0f);
|
|
} else {
|
|
accelerate_ship(aip, 0.0f);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
// Maybe cause this ship to self-destruct.
|
|
// Currently, any small ship (SIF_SMALL_SHIP) that has been disabled will self-destruct after awhile.
|
|
// Maybe should only do this if they are preventing their wing from re-entering.
|
|
void ai_maybe_self_destruct(object *objp, ai_info *aip)
|
|
{
|
|
Assertion(objp->type == OBJ_SHIP, "ai_maybe_self_destruct() can only be called with objects that are ships!");
|
|
ship *shipp = &Ships[objp->instance];
|
|
|
|
// Some IFFs can be repaired, so no self-destruct.
|
|
// In multiplayer, just don't self-destruct. I figured there would be a problem. -- MK, 3/19/98.
|
|
if ((Iff_info[shipp->team].flags & IFFF_SUPPORT_ALLOWED) || (Game_mode & GM_MULTIPLAYER))
|
|
return;
|
|
|
|
// Small ships in a wing blow themselves up after awhile if engine or weapons system has been destroyed.
|
|
// Reason: Don't want them to prevent a re-emergence of the wing.
|
|
// Note: Don't blow up if not in a wing for two reasons: One, won't affect re-emergence of waves and (1) disable the Dragon
|
|
// mission would be broken.
|
|
// Also, don't blow up the ship if it has a ship flag preventing this - Goober5000
|
|
if ((Ship_info[shipp->ship_info_index].is_small_ship()) && (shipp->wingnum >= 0) && !(shipp->flags[Ship::Ship_Flags::No_disabled_self_destruct])) {
|
|
if ((ship_subsystems_blown(shipp, SUBSYSTEM_ENGINE)) ||
|
|
(ship_subsystems_blown(shipp, SUBSYSTEM_WEAPONS))) {
|
|
if (aip->self_destruct_timestamp < 0)
|
|
aip->self_destruct_timestamp = timestamp(90 * 1000); // seconds until self-destruct
|
|
} else {
|
|
aip->self_destruct_timestamp = -1;
|
|
}
|
|
|
|
if (aip->self_destruct_timestamp < 0) {
|
|
return;
|
|
}
|
|
|
|
if (timestamp_elapsed(aip->self_destruct_timestamp)) {
|
|
ship_apply_local_damage( objp, objp, &objp->pos, objp->hull_strength*flFrametime + 1.0f, -1, MISS_SHIELDS);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine if pl_objp needs a new target, called from ai_frame()
|
|
*/
|
|
int ai_need_new_target(object *pl_objp, int target_objnum)
|
|
{
|
|
object *objp;
|
|
|
|
if ( target_objnum < 0 ) {
|
|
return 1;
|
|
}
|
|
|
|
objp = &Objects[target_objnum];
|
|
|
|
if ( (objp->type != OBJ_SHIP) && (objp->type != OBJ_ASTEROID) && (objp->type != OBJ_WEAPON) ) {
|
|
return 1;
|
|
}
|
|
|
|
if ( objp->type == OBJ_SHIP ) {
|
|
if ( Ships[objp->instance].flags[Ship::Ship_Flags::Dying] ) {
|
|
return 1;
|
|
} else if (Ships[objp->instance].team == Ships[pl_objp->instance].team) {
|
|
// Goober5000 - targeting the same team is allowed it if attacks itself
|
|
if (iff_x_attacks_y(Ships[pl_objp->instance].team, Ships[pl_objp->instance].team)) {
|
|
return 0;
|
|
}
|
|
|
|
// Goober5000 - targeting the same team is allowed if pl_objp is going bonkers
|
|
ai_info *pl_aip = &Ai_info[Ships[pl_objp->instance].ai_index];
|
|
if (pl_aip->active_goal != AI_ACTIVE_GOAL_NONE && pl_aip->active_goal != AI_ACTIVE_GOAL_DYNAMIC) {
|
|
if (pl_aip->goals[pl_aip->active_goal].flags[AI::Goal_Flags::Target_own_team]) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* If *objp is recovering from a collision with a big ship, handle it.
|
|
* @return true if recovering.
|
|
*/
|
|
int maybe_big_ship_collide_recover_frame(object *objp, ai_info *aip)
|
|
{
|
|
float dot, dist;
|
|
vec3d v2g;
|
|
|
|
bool better_collision_avoid_and_fighting = (The_mission.ai_profile->flags[AI::Profile_Flags::Better_combat_collision_avoidance] && aip->mode == AIM_CHASE)
|
|
|| (The_mission.ai_profile->flags[AI::Profile_Flags::Better_guard_collision_avoidance] && aip->mode == AIM_GUARD);
|
|
|
|
// if this guy's in battle he doesn't have the time to spend up to 35 seconds recovering!
|
|
int phase1_time = better_collision_avoid_and_fighting ? 2 : 5;
|
|
int phase2_time = 30;
|
|
|
|
if (aip->ai_flags[AI::AI_Flags::Big_ship_collide_recover_1]) {
|
|
vec3d global_recover_pos = aip->big_recover_1_direction + objp->pos;
|
|
ai_turn_towards_vector(&global_recover_pos, objp, nullptr, nullptr, 0.0f, 0, nullptr);
|
|
dist = vm_vec_normalized_dir(&v2g, &global_recover_pos, &objp->pos);
|
|
dot = vm_vec_dot(&objp->orient.vec.fvec, &v2g);
|
|
accelerate_ship(aip, dot);
|
|
|
|
// If close to desired point, or long enough since entered this mode, continue to next mode.
|
|
if ((timestamp_until(aip->big_recover_timestamp) < -phase1_time * 1000) || (dist < (0.5f + flFrametime) * objp->phys_info.speed)) {
|
|
aip->ai_flags.remove(AI::AI_Flags::Big_ship_collide_recover_1);
|
|
if (better_collision_avoid_and_fighting) // if true, bug out here 2 seconds should be enough
|
|
aip->ai_flags.remove(AI::AI_Flags::Target_collision);
|
|
else
|
|
aip->ai_flags.set(AI::AI_Flags::Big_ship_collide_recover_2);
|
|
}
|
|
|
|
return 1;
|
|
|
|
} else if (aip->ai_flags[AI::AI_Flags::Big_ship_collide_recover_2]) {
|
|
ai_turn_towards_vector(&aip->big_recover_2_pos, objp, nullptr, nullptr, 0.0f, 0, nullptr);
|
|
dist = vm_vec_normalized_dir(&v2g, &aip->big_recover_2_pos, &objp->pos);
|
|
dot = vm_vec_dot(&objp->orient.vec.fvec, &v2g);
|
|
accelerate_ship(aip, dot);
|
|
|
|
// If close to desired point, or 30+ seconds since started avoiding collision, done avoiding.
|
|
// TODO: below comment (this never gets tripped because this is like one of the first things AI do, so it won't be following any dynamic goals yet)
|
|
// also done avoiding if you're using better collision avoid and youve starting fighting
|
|
if ((timestamp_until(aip->big_recover_timestamp) < -phase2_time * 1000) || (dist < (0.5f + flFrametime) * objp->phys_info.speed) || better_collision_avoid_and_fighting) {
|
|
aip->ai_flags.remove(AI::AI_Flags::Big_ship_collide_recover_2);
|
|
aip->ai_flags.remove(AI::AI_Flags::Target_collision);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
if (aip->ai_flags[AI::AI_Flags::Target_collision]) {
|
|
aip->ai_flags.remove(AI::AI_Flags::Target_collision);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void validate_mode_submode(ai_info *aip)
|
|
{
|
|
switch (aip->mode) {
|
|
case AIM_CHASE:
|
|
// check valid submode
|
|
switch (aip->submode) {
|
|
case SM_CONTINUOUS_TURN:
|
|
case SM_ATTACK:
|
|
case SM_EVADE_SQUIGGLE:
|
|
case SM_EVADE_BRAKE:
|
|
case SM_EVADE:
|
|
case SM_SUPER_ATTACK:
|
|
case SM_AVOID:
|
|
case SM_GET_BEHIND:
|
|
case SM_GET_AWAY:
|
|
case SM_EVADE_WEAPON:
|
|
case SM_FLY_AWAY:
|
|
case SM_ATTACK_FOREVER:
|
|
break;
|
|
default:
|
|
UNREACHABLE("validate_mode_submode found an illegal submode for AIM_CHASE of %d. Please report to the SCP!", aip->submode);
|
|
}
|
|
break;
|
|
|
|
case AIM_STRAFE:
|
|
// check valid submode
|
|
switch(aip->submode) {
|
|
case AIS_STRAFE_ATTACK:
|
|
case AIS_STRAFE_AVOID:
|
|
case AIS_STRAFE_RETREAT1:
|
|
case AIS_STRAFE_RETREAT2:
|
|
case AIS_STRAFE_POSITION:
|
|
break;
|
|
default:
|
|
UNREACHABLE("validate_mode_submode found an illegal submode for AIM_STRAFE of %d. Please report to the SCP!", aip->submode);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process AI object "objnum".
|
|
*/
|
|
void ai_frame(int objnum)
|
|
{
|
|
// Cyborg - can you believe this was added in *2022*??
|
|
Assertion (Objects[objnum].type == OBJ_SHIP, "Ai_frame was called for a non-ship of type %d. Please report!", Objects[objnum].type);
|
|
|
|
if (Objects[objnum].type != OBJ_SHIP){
|
|
return;
|
|
}
|
|
|
|
ship *shipp = &Ships[Objects[objnum].instance];
|
|
ai_info *aip = &Ai_info[shipp->ai_index];
|
|
int target_objnum;
|
|
|
|
Assert((aip->mode != AIM_WAYPOINTS) || (aip->active_goal != AI_ACTIVE_GOAL_DYNAMIC));
|
|
|
|
// Set globals defining the current object and its enemy object.
|
|
Pl_objp = &Objects[objnum];
|
|
|
|
//Default to glide OFF
|
|
Pl_objp->phys_info.flags &= ~PF_GLIDING;
|
|
|
|
// turn off trigger down
|
|
shipp->weapons.flags.remove(Ship::Weapon_Flags::Primary_trigger_down);
|
|
|
|
// warping out?
|
|
if ((aip->mode == AIM_WARP_OUT) || (aip->ai_flags[AI::AI_Flags::Trying_unsuccessfully_to_warp]))
|
|
{
|
|
ai_warp_out(Pl_objp);
|
|
|
|
// Goober5000 - either we were never trying unsuccessfully, or we were but now
|
|
// we're successful... in either case, since we're actually warping we simply return
|
|
if (!(aip->ai_flags[AI::AI_Flags::Trying_unsuccessfully_to_warp]))
|
|
return;
|
|
}
|
|
|
|
ai_maybe_self_destruct(Pl_objp, aip);
|
|
ai_process_mission_orders( objnum, aip );
|
|
|
|
// Avoid a shockwave, if necessary. If a shockwave and rearming, stop rearming.
|
|
if (aip->mode != AIM_PLAY_DEAD && aip->ai_flags[AI::AI_Flags::Avoid_shockwave_ship, AI::AI_Flags::Avoid_shockwave_weapon]) {
|
|
if (ai_avoid_shockwave(Pl_objp, aip)) {
|
|
aip->ai_flags.remove(AI::AI_Flags::Big_ship_collide_recover_1);
|
|
aip->ai_flags.remove(AI::AI_Flags::Big_ship_collide_recover_2);
|
|
if (aip->ai_flags[AI::AI_Flags::Being_repaired, AI::AI_Flags::Awaiting_repair])
|
|
ai_abort_rearm_request(Pl_objp);
|
|
return;
|
|
}
|
|
} else {
|
|
aip->ai_flags.remove(AI::AI_Flags::Avoid_shockwave_started);
|
|
}
|
|
|
|
// moved call to ai_do_repair frame here from below because of the subsequent if statement returning
|
|
// if the ship is getting repaired
|
|
// If waiting to be repaired, just stop and sit.
|
|
ai_do_repair_frame(Pl_objp, aip, flFrametime);
|
|
if ((aip->ai_flags[AI::AI_Flags::Awaiting_repair]) || (aip->ai_flags[AI::AI_Flags::Being_repaired])) {
|
|
if (ai_await_repair_frame(Pl_objp, aip))
|
|
return;
|
|
}
|
|
|
|
if (aip->mode == AIM_PLAY_DEAD)
|
|
return;
|
|
|
|
// If recovering from a collision with a big ship, don't continue.
|
|
if (maybe_big_ship_collide_recover_frame(Pl_objp, aip))
|
|
return;
|
|
|
|
ai_preprocess_ignore_objnum(aip);
|
|
target_objnum = set_target_objnum(aip, aip->target_objnum);
|
|
|
|
Assert(objnum != target_objnum);
|
|
|
|
ai_manage_shield(Pl_objp, aip);
|
|
|
|
if ( maybe_request_support(Pl_objp) ) {
|
|
if ( Ships[Pl_objp->instance].flags[Ship::Ship_Flags::From_player_wing] ) {
|
|
ship_maybe_tell_about_rearm(shipp);
|
|
}
|
|
}
|
|
else {
|
|
ship_maybe_tell_about_low_ammo(shipp);
|
|
}
|
|
|
|
ai_maybe_depart(Pl_objp);
|
|
|
|
// Imminent-mine override: even if we have a current target, swap to a mine
|
|
// that is about to detonate on us (within proximity_radius * 1.5).
|
|
// Skip departing ships (AIM_WARP_OUT/AIM_BAY_DEPART): ai_maybe_depart() ran just above and may
|
|
// have committed this ship to leaving, so we must not divert it back to a mine here.
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Ships_intercept_mines]
|
|
&& aip->mode != AIM_GUARD && aip->mode != AIM_EVADE_WEAPON
|
|
&& aip->mode != AIM_WARP_OUT && aip->mode != AIM_BAY_DEPART
|
|
&& Ship_info[shipp->ship_info_index].class_type > -1
|
|
&& Ship_types[Ship_info[shipp->ship_info_index].class_type].flags[Ship::Type_Info_Flags::AI_auto_attacks]) {
|
|
object *imminent_mine = ai_find_nearby_mine_threat(Pl_objp, nullptr, true);
|
|
if (imminent_mine != nullptr && (target_objnum < 0 || &Objects[target_objnum] != imminent_mine)) {
|
|
aip->aspect_locked_time = 0.0f;
|
|
target_objnum = set_target_objnum(aip, OBJ_INDEX(imminent_mine));
|
|
}
|
|
}
|
|
|
|
// Find an enemy if don't already have one.
|
|
En_objp = NULL;
|
|
if ( ai_need_new_target(Pl_objp, target_objnum) ) {
|
|
if ((aip->mode != AIM_EVADE_WEAPON) && (aip->active_goal == AI_ACTIVE_GOAL_DYNAMIC)) {
|
|
aip->resume_goal_time = -1;
|
|
aip->active_goal = AI_ACTIVE_GOAL_NONE;
|
|
} else if (aip->resume_goal_time == -1) {
|
|
// AL 12-9-97: Don't allow cargo and navbuoys to set their aip->target_objnum
|
|
if ( Ship_info[shipp->ship_info_index].class_type > -1 && (Ship_types[Ship_info[shipp->ship_info_index].class_type].flags[Ship::Type_Info_Flags::AI_auto_attacks]) ) {
|
|
target_objnum = find_enemy(objnum, MAX_ENEMY_DISTANCE, The_mission.ai_profile->max_attackers[Game_skill_level]); // Attack up to 2.5K units away.
|
|
if (target_objnum != -1) {
|
|
if (aip->target_objnum != target_objnum)
|
|
aip->aspect_locked_time = 0.0f;
|
|
target_objnum = set_target_objnum(aip, target_objnum);
|
|
|
|
if (target_objnum >= 0)
|
|
{
|
|
En_objp = &Objects[target_objnum];
|
|
}
|
|
} else if (aip->mode != AIM_GUARD && The_mission.ai_profile->flags[AI::Profile_Flags::Ships_intercept_mines]) {
|
|
// No enemy found so check for nearby hostile mines within their targetable range
|
|
object *mine_objp = ai_find_nearby_mine_threat(Pl_objp);
|
|
if (mine_objp) {
|
|
target_objnum = set_target_objnum(aip, OBJ_INDEX(mine_objp));
|
|
if (target_objnum >= 0)
|
|
En_objp = &Objects[target_objnum];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (target_objnum >= 0) {
|
|
En_objp = &Objects[target_objnum];
|
|
}
|
|
|
|
// set base stealth info each frame
|
|
aip->ai_flags.remove(AI::AI_Flags::Stealth_pursuit);
|
|
if (En_objp && En_objp->type == OBJ_SHIP) {
|
|
if (Ships[En_objp->instance].flags[Ship::Ship_Flags::Stealth]) {
|
|
int stealth_state = ai_is_stealth_visible(Pl_objp, En_objp);
|
|
float dist = vm_vec_dist_quick(&En_objp->pos, &Pl_objp->pos);
|
|
|
|
if (stealth_state != STEALTH_FULLY_TARGETABLE) {
|
|
aip->ai_flags.set(AI::AI_Flags::Stealth_pursuit);
|
|
}
|
|
|
|
if ( (stealth_state == STEALTH_FULLY_TARGETABLE) || (stealth_state == STEALTH_IN_FRUSTUM) ) {
|
|
aip->stealth_last_visible_stamp = timestamp();
|
|
aip->stealth_last_cheat_visible_stamp = aip->stealth_last_visible_stamp;
|
|
aip->stealth_last_pos = En_objp->pos;
|
|
aip->stealth_velocity = En_objp->phys_info.vel;
|
|
} else if (dist < 100) {
|
|
// get cheat timestamp
|
|
aip->stealth_last_cheat_visible_stamp = timestamp();
|
|
|
|
// set approximate pos and vel, with increasing error as time from last_visible_stamp increases
|
|
update_ai_stealth_info_with_error(aip/*, 0*/);
|
|
}
|
|
}
|
|
}
|
|
|
|
// AL 12-10-97: ensure that cargo and navbuoys aip->target_objnum is always -1.
|
|
if ( Ship_info[shipp->ship_info_index].class_type > -1 && !(Ship_types[Ship_info[shipp->ship_info_index].class_type].flags[Ship::Type_Info_Flags::AI_auto_attacks])) {
|
|
aip->target_objnum = -1;
|
|
}
|
|
|
|
if ((En_objp != NULL) && (En_objp->pos.xyz.x == Pl_objp->pos.xyz.x) && (En_objp->pos.xyz.y == Pl_objp->pos.xyz.y) && (En_objp->pos.xyz.z == Pl_objp->pos.xyz.z)) {
|
|
mprintf(("Warning: Object and its enemy have same position. Object #%d\n", OBJ_INDEX(Pl_objp)));
|
|
En_objp = NULL;
|
|
}
|
|
|
|
if (aip->mode == AIM_CHASE) {
|
|
if (En_objp == NULL) {
|
|
aip->active_goal = -1;
|
|
}
|
|
}
|
|
|
|
// If there is a goal to resume and enough time has elapsed, resume the goal.
|
|
if ((aip->resume_goal_time > 0) && (aip->resume_goal_time < Missiontime)) {
|
|
aip->active_goal = AI_ACTIVE_GOAL_NONE;
|
|
aip->resume_goal_time = -1;
|
|
target_objnum = find_enemy(objnum, 2000.0f, The_mission.ai_profile->max_attackers[Game_skill_level]);
|
|
if (target_objnum != -1) {
|
|
if (aip->target_objnum != target_objnum) {
|
|
aip->aspect_locked_time = 0.0f;
|
|
}
|
|
set_target_objnum(aip, target_objnum);
|
|
}
|
|
}
|
|
|
|
// check if targeted subsystem has been destroyed, if so, move onto another subsystem
|
|
// if trying to disable or disarm the target
|
|
if ((En_objp != NULL) && ( aip->targeted_subsys != NULL )) {
|
|
Assert(En_objp->type == OBJ_SHIP);
|
|
if ( aip->targeted_subsys->current_hits <= 0.0f ) {
|
|
int subsys_type;
|
|
|
|
if ( aip->goals[0].ai_mode == AI_GOAL_DISABLE_SHIP || aip->goals[0].ai_mode == AI_GOAL_DISABLE_SHIP_TACTICAL ) {
|
|
subsys_type = SUBSYSTEM_ENGINE;
|
|
} else if ( aip->goals[0].ai_mode == AI_GOAL_DISARM_SHIP || aip->goals[0].ai_mode == AI_GOAL_DISARM_SHIP_TACTICAL ) {
|
|
subsys_type = SUBSYSTEM_TURRET;
|
|
} else {
|
|
subsys_type = -1;
|
|
}
|
|
|
|
if ( subsys_type != -1 ) {
|
|
ship_subsys *new_subsys;
|
|
new_subsys = ship_return_next_subsys(&Ships[En_objp->instance], subsys_type, &Pl_objp->pos);
|
|
if ( new_subsys != NULL ) {
|
|
set_targeted_subsys(aip, new_subsys, aip->target_objnum);
|
|
} else {
|
|
// AL 12-16-97: no more subsystems to attack... reset targeting info
|
|
aip->target_objnum = -1;
|
|
set_targeted_subsys(aip, NULL, -1);
|
|
}
|
|
} else {
|
|
// targeted subsys is destroyed, so stop attacking it
|
|
set_targeted_subsys(aip, NULL, -1);
|
|
}
|
|
}
|
|
}
|
|
|
|
ai_maybe_launch_cmeasure(Pl_objp, aip);
|
|
ai_maybe_evade_locked_missile(Pl_objp, aip);
|
|
|
|
aip->target_time += flFrametime;
|
|
|
|
int in_formation = 0;
|
|
if (aip->ai_flags[AI::AI_Flags::Formation_object, AI::AI_Flags::Formation_wing]) {
|
|
in_formation = !ai_formation();
|
|
}
|
|
|
|
if ( !in_formation ) {
|
|
ai_execute_behavior(aip);
|
|
}
|
|
|
|
ai_process_subobjects(objnum);
|
|
maybe_resume_previous_mode(Pl_objp, aip);
|
|
|
|
if (Pl_objp->phys_info.flags & PF_AFTERBURNER_ON ) {
|
|
if (Missiontime > aip->afterburner_stop_time) {
|
|
afterburners_stop(Pl_objp);
|
|
}
|
|
}
|
|
}
|
|
|
|
// set and removes any maneuver flags for ai maneuver overrides
|
|
static void ai_control_info_flags_upkeep(ai_info* aip, ship_info* sip, physics_info* pi)
|
|
{
|
|
if (aip->ai_override_flags.none_set())
|
|
return;
|
|
|
|
bool no_lat = false;
|
|
bool no_rot = false;
|
|
|
|
if (!aip->ai_override_flags[AI::Maneuver_Override_Flags::Lateral_never_expire] && timestamp_elapsed(aip->ai_override_lat_timestamp))
|
|
{
|
|
aip->ai_override_flags.remove(AI::Maneuver_Override_Flags::Sideways);
|
|
aip->ai_override_flags.remove(AI::Maneuver_Override_Flags::Up);
|
|
aip->ai_override_flags.remove(AI::Maneuver_Override_Flags::Forward);
|
|
aip->ai_override_flags.remove(AI::Maneuver_Override_Flags::Full_lat);
|
|
no_lat = true;
|
|
}
|
|
|
|
if (!aip->ai_override_flags[AI::Maneuver_Override_Flags::Rotational_never_expire] && timestamp_elapsed(aip->ai_override_rot_timestamp))
|
|
{
|
|
aip->ai_override_flags.remove(AI::Maneuver_Override_Flags::Heading);
|
|
aip->ai_override_flags.remove(AI::Maneuver_Override_Flags::Roll);
|
|
aip->ai_override_flags.remove(AI::Maneuver_Override_Flags::Pitch);
|
|
aip->ai_override_flags.remove(AI::Maneuver_Override_Flags::Full_rot);
|
|
no_rot = true;
|
|
}
|
|
|
|
if (no_rot && no_lat) {
|
|
aip->ai_override_flags.reset();
|
|
}
|
|
else
|
|
{
|
|
if (aip->ai_override_flags[AI::Maneuver_Override_Flags::Dont_bank_when_turning] || sip->flags[Ship::Info_Flags::Dont_bank_when_turning])
|
|
AI_ci.control_flags |= CIF_DONT_BANK_WHEN_TURNING;
|
|
if (aip->ai_override_flags[AI::Maneuver_Override_Flags::Dont_clamp_max_velocity] || sip->flags[Ship::Info_Flags::Dont_clamp_max_velocity])
|
|
AI_ci.control_flags |= CIF_DONT_CLAMP_MAX_VELOCITY;
|
|
if (aip->ai_override_flags[AI::Maneuver_Override_Flags::Instantaneous_acceleration] || sip->flags[Ship::Info_Flags::Instantaneous_acceleration])
|
|
AI_ci.control_flags |= CIF_INSTANTANEOUS_ACCELERATION;
|
|
}
|
|
|
|
// set physics flag according to whether we are instantaneously accelerating
|
|
if (AI_ci.control_flags & CIF_INSTANTANEOUS_ACCELERATION)
|
|
pi->flags |= PF_MANEUVER_NO_DAMP;
|
|
else
|
|
pi->flags &= ~PF_MANEUVER_NO_DAMP;
|
|
}
|
|
|
|
// applies any rotational or lateral maneuver values to the AI's CI
|
|
static void ai_control_info_apply(ai_info *aip)
|
|
{
|
|
if (aip->ai_override_flags.none_set())
|
|
return;
|
|
|
|
bool lateral_maneuver = false;
|
|
bool rotational_maneuver = false;
|
|
|
|
if (aip->ai_override_flags[AI::Maneuver_Override_Flags::Lateral_never_expire] || !timestamp_elapsed(aip->ai_override_lat_timestamp))
|
|
lateral_maneuver = true;
|
|
|
|
if (aip->ai_override_flags[AI::Maneuver_Override_Flags::Rotational_never_expire] || timestamp_elapsed(aip->ai_override_rot_timestamp))
|
|
rotational_maneuver = true;
|
|
|
|
if (lateral_maneuver || rotational_maneuver) {
|
|
if (aip->ai_override_flags[AI::Maneuver_Override_Flags::Full_rot])
|
|
{
|
|
AI_ci.pitch = aip->ai_override_ci.pitch;
|
|
AI_ci.heading = aip->ai_override_ci.heading;
|
|
AI_ci.bank = aip->ai_override_ci.bank;
|
|
}
|
|
else
|
|
{
|
|
if (aip->ai_override_flags[AI::Maneuver_Override_Flags::Pitch])
|
|
AI_ci.pitch = aip->ai_override_ci.pitch;
|
|
if (aip->ai_override_flags[AI::Maneuver_Override_Flags::Heading])
|
|
AI_ci.heading = aip->ai_override_ci.heading;
|
|
if (aip->ai_override_flags[AI::Maneuver_Override_Flags::Roll])
|
|
AI_ci.bank = aip->ai_override_ci.bank;
|
|
}
|
|
|
|
if (aip->ai_override_flags[AI::Maneuver_Override_Flags::Full_lat])
|
|
{
|
|
AI_ci.vertical = aip->ai_override_ci.vertical;
|
|
AI_ci.sideways = aip->ai_override_ci.sideways;
|
|
AI_ci.forward = aip->ai_override_ci.forward;
|
|
}
|
|
else
|
|
{
|
|
if (aip->ai_override_flags[AI::Maneuver_Override_Flags::Up])
|
|
AI_ci.vertical = aip->ai_override_ci.vertical;
|
|
if (aip->ai_override_flags[AI::Maneuver_Override_Flags::Sideways])
|
|
AI_ci.sideways = aip->ai_override_ci.sideways;
|
|
if (aip->ai_override_flags[AI::Maneuver_Override_Flags::Forward])
|
|
AI_ci.forward = aip->ai_override_ci.forward;
|
|
}
|
|
}
|
|
}
|
|
|
|
int Last_ai_obj = -1;
|
|
|
|
void ai_process( object * obj, int ai_index, float frametime )
|
|
{
|
|
if (obj->flags[Object::Object_Flags::Should_be_dead])
|
|
return;
|
|
|
|
Assert(obj->type == OBJ_SHIP);
|
|
auto shipp = &Ships[obj->instance];
|
|
auto sip = &Ship_info[shipp->ship_info_index];
|
|
|
|
// return if ship is dead, unless it's a big ship...then its turrets still fire, like I was quoted in a magazine. -- MK, 5/15/98.
|
|
if ((shipp->flags[Ship::Ship_Flags::Dying] ) && !(sip->is_big_or_huge())) {
|
|
return;
|
|
}
|
|
|
|
bool rfc = true; // Assume will be Reading Flying Controls.
|
|
|
|
Assert( ai_index >= 0 );
|
|
auto aip = &Ai_info[shipp->ai_index];
|
|
|
|
AI_frametime = frametime;
|
|
if (OBJ_INDEX(obj) <= Last_ai_obj) {
|
|
AI_FrameCount++;
|
|
}
|
|
|
|
memset( &AI_ci, 0, sizeof(AI_ci) );
|
|
|
|
AI_ci.pitch = 0.0f;
|
|
AI_ci.bank = 0.0f;
|
|
AI_ci.heading = 0.0f;
|
|
|
|
ai_frame(OBJ_INDEX(obj));
|
|
|
|
// In certain circumstances, the AI says don't fly in the normal way.
|
|
// One circumstance is in docking and undocking, when the ship is moving
|
|
// under thruster control.
|
|
switch (aip->mode) {
|
|
case AIM_DOCK:
|
|
if ((aip->submode >= AIS_DOCK_2) && (aip->submode != AIS_UNDOCK_3))
|
|
rfc = false;
|
|
break;
|
|
case AIM_WARP_OUT:
|
|
if (aip->submode >= AIS_WARP_3)
|
|
rfc = false;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// this is sufficient because non-big ships were already weeded out by dying above
|
|
if (shipp->flags[Ship::Ship_Flags::Dying] && sip->flags[Ship::Info_Flags::Large_ship_deathroll])
|
|
rfc = false;
|
|
|
|
// regardless if we're acting on them, upkeep maneuver flags
|
|
ai_control_info_flags_upkeep(aip, sip, &obj->phys_info);
|
|
|
|
if (rfc) {
|
|
// Wanderer - sexp based override goes here - only if rfc is valid though
|
|
ai_control_info_apply(aip);
|
|
physics_read_flying_controls( &obj->orient, &obj->phys_info, &AI_ci, frametime);
|
|
}
|
|
|
|
Last_ai_obj = OBJ_INDEX(obj);
|
|
}
|
|
|
|
/**
|
|
* Initialize ::ai_info struct of object objnum.
|
|
*/
|
|
void init_ai_object(int objnum)
|
|
{
|
|
int i, ship_index, ai_index, ship_type;
|
|
ai_info *aip;
|
|
object *objp;
|
|
vec3d near_vec; // A vector nearby and mainly in front of this object.
|
|
|
|
objp = &Objects[objnum];
|
|
ship_index = objp->instance;
|
|
ai_index = Ships[ship_index].ai_index;
|
|
Assert((ai_index >= 0) && (ai_index < MAX_AI_INFO));
|
|
|
|
aip = &Ai_info[ai_index];
|
|
|
|
ship_type = Ships[ship_index].ship_info_index;
|
|
|
|
vm_vec_scale_add(&near_vec, &objp->pos, &objp->orient.vec.fvec, 100.0f);
|
|
vm_vec_scale_add2(&near_vec, &objp->orient.vec.rvec, 10.0f);
|
|
|
|
// Things that shouldn't have to get initialized, but initialize them just in case!
|
|
aip->ai_flags.reset();
|
|
aip->previous_mode = AIM_NONE;
|
|
aip->mode_time = -1;
|
|
aip->target_objnum = -1;
|
|
aip->target_signature = -1;
|
|
aip->previous_target_objnum = -1;
|
|
aip->target_time = 0.0f;
|
|
aip->enemy_wing = -1;
|
|
aip->attacker_objnum = -1;
|
|
aip->goal_objnum = -1;
|
|
aip->goal_signature = -1;
|
|
aip->guard_objnum = -1;
|
|
aip->guard_signature = -1;
|
|
aip->guard_wingnum = -1;
|
|
aip->submode = 0;
|
|
aip->previous_submode = 0;
|
|
aip->best_dot_to_enemy = -1.0f;
|
|
aip->best_dot_from_enemy = -1.0f;
|
|
aip->best_dot_to_time = 0;
|
|
aip->best_dot_from_time = 0;
|
|
aip->submode_start_time = 0;
|
|
aip->submode_parm0 = 0;
|
|
aip->submode_parm1 = 0;
|
|
aip->submode_float0 = 0.0f;
|
|
aip->active_goal = -1;
|
|
aip->goal_check_time = timestamp(0);
|
|
aip->last_predicted_enemy_pos = near_vec;
|
|
aip->prev_goal_point = near_vec;
|
|
aip->goal_point = near_vec;
|
|
aip->time_enemy_in_range = 0.0f;
|
|
aip->time_enemy_near = 0.0f;
|
|
aip->last_attack_time = 0;
|
|
aip->last_hit_time = 0;
|
|
aip->danger_shield_quadrant = 0;
|
|
aip->hitter_objnum = -1;
|
|
aip->hitter_signature = -1;
|
|
aip->resume_goal_time = -1;
|
|
aip->prev_accel = 0.0f;
|
|
aip->prev_dot_to_goal = 0.0f;
|
|
|
|
aip->ignore_objnum = UNUSED_OBJNUM;
|
|
aip->ignore_signature = -1;
|
|
|
|
// Goober5000
|
|
for (i = 0; i < MAX_IGNORE_NEW_OBJECTS; i++)
|
|
{
|
|
aip->ignore_new_objnums[i] = UNUSED_OBJNUM;
|
|
aip->ignore_new_signatures[i] = -1;
|
|
}
|
|
|
|
//Init stuff from AI class and AI profiles
|
|
init_aip_from_class_and_profile(aip, &Ai_classes[Ship_info[ship_type].ai_class], The_mission.ai_profile);
|
|
|
|
aip->wp_list_index = -1;
|
|
aip->wp_index = INVALID_WAYPOINT_POSITION;
|
|
|
|
aip->attacker_objnum = -1;
|
|
aip->goal_signature = -1;
|
|
|
|
aip->last_predicted_enemy_pos.xyz.x = 0.0f; // Says this value needs to be recomputed!
|
|
aip->time_enemy_in_range = 0.0f;
|
|
aip->time_enemy_near = 0.0f;
|
|
|
|
aip->resume_goal_time = -1; // Say there is no goal to resume.
|
|
|
|
aip->active_goal = -1;
|
|
aip->path_start = -1;
|
|
aip->path_goal_dist = -1;
|
|
aip->path_length = 0;
|
|
aip->path_subsystem_next_check = 1;
|
|
|
|
aip->support_ship_objnum = -1;
|
|
aip->support_ship_signature = -1;
|
|
|
|
aip->danger_weapon_objnum = -1;
|
|
aip->danger_weapon_signature = -1;
|
|
|
|
aip->last_hit_target_time = Missiontime;
|
|
aip->last_hit_time = Missiontime;
|
|
|
|
aip->nearest_locked_object = -1;
|
|
aip->nearest_locked_distance = 99999.0f;
|
|
|
|
aip->targeted_subsys = NULL;
|
|
aip->last_subsys_target = NULL;
|
|
aip->targeted_subsys_parent = -1;
|
|
|
|
// The next two fields are used to time the rearming to allow useful sound effects for missile rearming
|
|
aip->rearm_first_missile = TRUE; // flag to indicate that next missile to load is the first missile
|
|
aip->rearm_first_ballistic_primary = TRUE; // flag to indicate that next ballistic to load is the first ballistic
|
|
aip->rearm_release_delay = 0; // timestamp to delay the separation of docked ships after rearm
|
|
|
|
aip->next_predict_pos_time = 0;
|
|
aip->next_aim_pos_time = 0;
|
|
|
|
aip->next_dynamic_path_check_time = timestamp(0);
|
|
vm_vec_zero(&aip->last_dynamic_path_goal);
|
|
|
|
aip->afterburner_stop_time = 0;
|
|
aip->last_objsig_hit = -1; // object signature of the ship most recently hit by aip
|
|
|
|
aip->path_next_create_time = timestamp(1);
|
|
aip->path_create_pos = Objects[objnum].pos;
|
|
aip->path_create_orient = Objects[objnum].orient;
|
|
vm_vec_zero(&aip->path_depart_rvec);
|
|
|
|
aip->ignore_expire_timestamp = timestamp(1);
|
|
aip->warp_out_timestamp = 0;
|
|
aip->next_rearm_request_timestamp = timestamp(1);
|
|
aip->primary_select_timestamp = timestamp(1);
|
|
aip->secondary_select_timestamp = timestamp(1);
|
|
aip->scan_for_enemy_timestamp = timestamp(1);
|
|
|
|
aip->choose_enemy_timestamp = timestamp(3*(NUM_SKILL_LEVELS-Game_skill_level) * ((static_rand_alt() % 500) + 500));
|
|
|
|
aip->shockwave_object = -1;
|
|
aip->shield_manage_timestamp = timestamp(1);
|
|
aip->self_destruct_timestamp = -1; // This is a flag that we have not yet set this.
|
|
aip->ok_to_target_timestamp = timestamp(1);
|
|
aip->pick_big_attack_point_timestamp = timestamp(1);
|
|
vm_vec_zero(&aip->big_attack_point);
|
|
|
|
aip->avoid_check_timestamp = timestamp(1);
|
|
|
|
aip->abort_rearm_timestamp = -1;
|
|
|
|
// artillery stuff
|
|
aip->artillery_objnum = -1;
|
|
aip->artillery_sig = -1;
|
|
|
|
// waypoint speed cap
|
|
aip->waypoint_speed_cap = -1;
|
|
|
|
// set lethality to enemy team
|
|
aip->lethality = 0.0f;
|
|
aip->ai_override_flags.reset();
|
|
memset(&aip->ai_override_ci,0,sizeof(control_info));
|
|
|
|
aip->form_obj_slotnum = -1;
|
|
|
|
aip->multilock_check_timestamp = timestamp(1);
|
|
aip->ai_missile_locks_firing.clear();
|
|
}
|
|
|
|
void init_ai_system()
|
|
{
|
|
Ppfp = Path_points;
|
|
}
|
|
|
|
//Sets the ai_info stuff based on what is in the ai class and the current ai profile
|
|
//Stuff in the ai class will override what is in the ai profile, but only if it is set.
|
|
//Unset per-difficulty-level values are marked with FLT_MIN or INT_MIN
|
|
//Which flags are set is handled by using two flag ints: one with the flag values (TRUE/FALSE), one that
|
|
//just says which flags are set.
|
|
void init_aip_from_class_and_profile(ai_info *aip, ai_class *aicp, ai_profile_t *profile)
|
|
{
|
|
// since we use it so much in this function, sanity check the value for Game_skill_level
|
|
if (Game_skill_level < 0 || Game_skill_level >= NUM_SKILL_LEVELS) {
|
|
Warning(LOCATION, "Invalid skill level %i! Valid range 0 to %i. Resetting to default.", Game_skill_level, NUM_SKILL_LEVELS);
|
|
Game_skill_level = game_get_default_skill_level();
|
|
}
|
|
|
|
//ai_class-only stuff
|
|
aip->ai_courage = aicp->ai_courage[Game_skill_level];
|
|
aip->ai_patience = aicp->ai_patience[Game_skill_level];
|
|
aip->ai_evasion = aicp->ai_evasion[Game_skill_level];
|
|
aip->ai_accuracy = aicp->ai_accuracy[Game_skill_level];
|
|
|
|
aip->ai_aburn_use_factor = aicp->ai_aburn_use_factor[Game_skill_level];
|
|
aip->ai_shockwave_evade_chance = aicp->ai_shockwave_evade_chance[Game_skill_level];
|
|
aip->ai_get_away_chance = aicp->ai_get_away_chance[Game_skill_level];
|
|
aip->ai_secondary_range_mult = aicp->ai_secondary_range_mult[Game_skill_level];
|
|
aip->ai_class_autoscale = aicp->ai_class_autoscale;
|
|
|
|
//Apply overrides from ai class to ai profiles values
|
|
//Only override values which were explicitly set in the AI class
|
|
aip->ai_cmeasure_fire_chance = (aicp->ai_cmeasure_fire_chance[Game_skill_level] == FLT_MIN) ?
|
|
profile->cmeasure_fire_chance[Game_skill_level] : aicp->ai_cmeasure_fire_chance[Game_skill_level];
|
|
aip->ai_in_range_time = (aicp->ai_in_range_time[Game_skill_level] == FLT_MIN) ?
|
|
profile->in_range_time[Game_skill_level] : aicp->ai_in_range_time[Game_skill_level];
|
|
aip->ai_link_ammo_levels_maybe = (aicp->ai_link_ammo_levels_maybe[Game_skill_level] == FLT_MIN) ?
|
|
profile->link_ammo_levels_maybe[Game_skill_level] : aicp->ai_link_ammo_levels_maybe[Game_skill_level];
|
|
aip->ai_link_ammo_levels_always = (aicp->ai_link_ammo_levels_always[Game_skill_level] == FLT_MIN) ?
|
|
profile->link_ammo_levels_always[Game_skill_level] : aicp->ai_link_ammo_levels_always[Game_skill_level];
|
|
aip->ai_primary_ammo_burst_mult = (aicp->ai_primary_ammo_burst_mult[Game_skill_level] == FLT_MIN) ?
|
|
profile->primary_ammo_burst_mult[Game_skill_level] : aicp->ai_primary_ammo_burst_mult[Game_skill_level];
|
|
aip->ai_link_energy_levels_maybe = (aicp->ai_link_energy_levels_maybe[Game_skill_level] == FLT_MIN) ?
|
|
profile->link_energy_levels_maybe[Game_skill_level] : aicp->ai_link_energy_levels_maybe[Game_skill_level];
|
|
aip->ai_link_energy_levels_always = (aicp->ai_link_energy_levels_always[Game_skill_level] == FLT_MIN) ?
|
|
profile->link_energy_levels_always[Game_skill_level] : aicp->ai_link_energy_levels_always[Game_skill_level];
|
|
aip->ai_predict_position_delay = (aicp->ai_predict_position_delay[Game_skill_level] == INT_MIN) ?
|
|
profile->predict_position_delay[Game_skill_level] : aicp->ai_predict_position_delay[Game_skill_level];
|
|
aip->ai_shield_manage_delay = (aicp->ai_shield_manage_delay[Game_skill_level] == FLT_MIN) ?
|
|
profile->shield_manage_delay[Game_skill_level] : aicp->ai_shield_manage_delay[Game_skill_level];
|
|
aip->ai_ship_fire_delay_scale_friendly = (aicp->ai_ship_fire_delay_scale_friendly[Game_skill_level] == FLT_MIN) ?
|
|
profile->ship_fire_delay_scale_friendly[Game_skill_level] : aicp->ai_ship_fire_delay_scale_friendly[Game_skill_level];
|
|
aip->ai_ship_fire_delay_scale_hostile = (aicp->ai_ship_fire_delay_scale_hostile[Game_skill_level] == FLT_MIN) ?
|
|
profile->ship_fire_delay_scale_hostile[Game_skill_level] : aicp->ai_ship_fire_delay_scale_hostile[Game_skill_level];
|
|
aip->ai_ship_fire_secondary_delay_scale_friendly = (aicp->ai_ship_fire_secondary_delay_scale_friendly[Game_skill_level] == FLT_MIN) ?
|
|
profile->ship_fire_secondary_delay_scale_friendly[Game_skill_level] : aicp->ai_ship_fire_secondary_delay_scale_friendly[Game_skill_level];
|
|
aip->ai_ship_fire_secondary_delay_scale_hostile = (aicp->ai_ship_fire_secondary_delay_scale_hostile[Game_skill_level] == FLT_MIN) ?
|
|
profile->ship_fire_secondary_delay_scale_hostile[Game_skill_level] : aicp->ai_ship_fire_secondary_delay_scale_hostile[Game_skill_level];
|
|
aip->ai_turn_time_scale = (aicp->ai_turn_time_scale[Game_skill_level] == FLT_MIN) ?
|
|
profile->turn_time_scale[Game_skill_level] : aicp->ai_turn_time_scale[Game_skill_level];
|
|
aip->ai_glide_attack_percent = (aicp->ai_glide_attack_percent[Game_skill_level] == FLT_MIN) ?
|
|
profile->glide_attack_percent[Game_skill_level] : aicp->ai_glide_attack_percent[Game_skill_level];
|
|
aip->ai_circle_strafe_percent = (aicp->ai_circle_strafe_percent[Game_skill_level] == FLT_MIN) ?
|
|
profile->circle_strafe_percent[Game_skill_level] : aicp->ai_circle_strafe_percent[Game_skill_level];
|
|
aip->ai_glide_strafe_percent = (aicp->ai_glide_strafe_percent[Game_skill_level] == FLT_MIN) ?
|
|
profile->glide_strafe_percent[Game_skill_level] : aicp->ai_glide_strafe_percent[Game_skill_level];
|
|
aip->ai_random_sidethrust_percent = (aicp->ai_random_sidethrust_percent[Game_skill_level] == FLT_MIN) ?
|
|
profile->random_sidethrust_percent[Game_skill_level] : aicp->ai_random_sidethrust_percent[Game_skill_level];
|
|
aip->ai_stalemate_time_thresh = (aicp->ai_stalemate_time_thresh[Game_skill_level] == FLT_MIN) ?
|
|
profile->stalemate_time_thresh[Game_skill_level] : aicp->ai_stalemate_time_thresh[Game_skill_level];
|
|
aip->ai_stalemate_dist_thresh = (aicp->ai_stalemate_dist_thresh[Game_skill_level] == FLT_MIN) ?
|
|
profile->stalemate_dist_thresh[Game_skill_level] : aicp->ai_stalemate_dist_thresh[Game_skill_level];
|
|
aip->ai_chance_to_use_missiles_on_plr = (aicp->ai_chance_to_use_missiles_on_plr[Game_skill_level] == INT_MIN) ?
|
|
profile->chance_to_use_missiles_on_plr[Game_skill_level] : aicp->ai_chance_to_use_missiles_on_plr[Game_skill_level];
|
|
aip->ai_max_aim_update_delay = (aicp->ai_max_aim_update_delay[Game_skill_level] == FLT_MIN) ?
|
|
profile->max_aim_update_delay[Game_skill_level] : aicp->ai_max_aim_update_delay[Game_skill_level];
|
|
aip->ai_turret_max_aim_update_delay = (aicp->ai_turret_max_aim_update_delay[Game_skill_level] == FLT_MIN) ?
|
|
profile->turret_max_aim_update_delay[Game_skill_level] : aicp->ai_turret_max_aim_update_delay[Game_skill_level];
|
|
|
|
//Combine AI profile and AI class flags
|
|
aip->ai_profile_flags = profile->flags | (aicp->ai_profile_flags & aicp->ai_profile_flags_set);
|
|
}
|
|
|
|
void ai_do_default_behavior(object *obj)
|
|
{
|
|
Assertion(obj != nullptr, "Ai_do_default_behavior() was given a bad information. Specifically obj is a nullptr. This is a coder error, please report!");
|
|
Assertion((obj->type == OBJ_SHIP) && (obj->instance > -1) && (obj->instance < MAX_SHIPS), "This function that requires an object to be a ship was passed an invalid ship. Please report\n\nObject type: %d\nInstance #: %d", obj->type, obj->instance);
|
|
Assertion(Ships[obj->instance].ai_index >= 0, "A ship has been found to have an invalid ai_index of %d. This should have been assigned and is coder mistake, please report!", Ships[obj->instance].ai_index);
|
|
|
|
ai_info *aip = &Ai_info[Ships[obj->instance].ai_index];
|
|
ship_info* sip = &Ship_info[Ships[obj->instance].ship_info_index];
|
|
|
|
// default behavior in most cases (especially if we're docked) is to just stay put
|
|
aip->mode = AIM_NONE;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->active_goal = AI_ACTIVE_GOAL_NONE;
|
|
|
|
// if we're not docked, we may modify the behavior a bit
|
|
if (!object_is_docked(obj))
|
|
{
|
|
// fighters automatically chase things
|
|
if (!is_instructor(obj) && (sip->is_fighter_bomber()))
|
|
{
|
|
int enemy_objnum = find_enemy(OBJ_INDEX(obj), 1000.0f, The_mission.ai_profile->max_attackers[Game_skill_level]);
|
|
set_target_objnum(aip, enemy_objnum);
|
|
aip->mode = AIM_CHASE;
|
|
aip->submode = SM_ATTACK;
|
|
}
|
|
// support ships automatically keep a safe distance
|
|
else if (sip->flags[Ship::Info_Flags::Support])
|
|
{
|
|
aip->mode = AIM_SAFETY;
|
|
aip->submode = AISS_1;
|
|
aip->ai_flags.remove(AI::AI_Flags::Repairing);
|
|
}
|
|
// sentry guns... do their thing
|
|
else if (sip->flags[Ship::Info_Flags::Sentrygun])
|
|
{
|
|
aip->mode = AIM_SENTRYGUN;
|
|
}
|
|
}
|
|
}
|
|
|
|
#define FRIENDLY_DAMAGE_THRESHOLD 50.0f // Display a message at this threshold. Note, this gets scaled by Skill_level
|
|
|
|
// send the given message from objp. called from the maybe_process_friendly_hit
|
|
// code below when a message must get send to the player when he fires on friendlies
|
|
void process_friendly_hit_message( int message, object *objp )
|
|
{
|
|
int index;
|
|
|
|
// no traitor in multiplayer
|
|
if(Game_mode & GM_MULTIPLAYER){
|
|
return;
|
|
}
|
|
|
|
// don't send this message if a player ship was hit.
|
|
if ( objp->flags[Object::Object_Flags::Player_ship] ){
|
|
return;
|
|
}
|
|
|
|
// check if objp is a fighter/bomber -- if not, then find a new ship to send the message
|
|
index = objp->instance;
|
|
if ( !Ship_info[Ships[objp->instance].ship_info_index].is_fighter_bomber() ){
|
|
index = -1;
|
|
}
|
|
|
|
// If the ship can't send messages pick someone else
|
|
if (Ships[objp->instance].flags[Ship::Ship_Flags::No_builtin_messages]) {
|
|
index = -1;
|
|
}
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Check_comms_for_non_player_ships] && hud_communications_state(&Ships[objp->instance]) <= COMM_DAMAGED) {
|
|
index = -1;
|
|
}
|
|
|
|
// Karajorma - pick a random ship to send Command messages if command is silenced.
|
|
if (index < 0 && (The_mission.flags[Mission::Mission_Flags::No_builtin_command]) ) {
|
|
index = ship_get_random_player_wing_ship( SHIP_GET_UNSILENCED );
|
|
}
|
|
|
|
if ( index >= 0 )
|
|
{
|
|
message_send_builtin(message, &Ships[index], nullptr, -1, -1);
|
|
} else {
|
|
message_send_builtin(message, nullptr, nullptr, -1, -1);
|
|
}
|
|
}
|
|
|
|
extern void ship_set_subsystem_strength( ship *shipp, int type, float strength );
|
|
|
|
/**
|
|
* Object *objp_weapon, fired by *objp_hitter, hit object *objp_ship.
|
|
*/
|
|
void maybe_process_friendly_hit(object *objp_hitter, object *objp_hit, object *objp_weapon)
|
|
{
|
|
// no turning traitor in multiplayer
|
|
if ( Game_mode & GM_MULTIPLAYER ) {
|
|
return;
|
|
}
|
|
|
|
// ditto if mission says no traitors allowed
|
|
if (The_mission.flags[Mission::Mission_Flags::No_traitor]) {
|
|
return;
|
|
}
|
|
|
|
// Cyborg -- I had to switch around the logic here because it was potentially
|
|
// sending non-ships to obj_team, which would trigger an assert
|
|
if (objp_hitter == Player_obj){
|
|
|
|
// Cyborg - So check if this is a ship first. Probably true, but you never know, with FSO
|
|
// (especially in multi)
|
|
if ( objp_hitter->type != OBJ_SHIP ) {
|
|
return;
|
|
}
|
|
|
|
// And assert that obj_hit is a ship as well.
|
|
Assert(objp_hit->type == OBJ_SHIP);
|
|
|
|
if (obj_team(objp_hitter) == obj_team(objp_hit)) {
|
|
|
|
Assert((objp_weapon->type == OBJ_WEAPON) || (objp_weapon->type == OBJ_BEAM)); //beam added for traitor detection - FUBAR
|
|
|
|
ship *shipp_hitter = &Ships[objp_hitter->instance];
|
|
ship *shipp_hit = &Ships[objp_hit->instance];
|
|
|
|
if (shipp_hitter->team != shipp_hit->team) {
|
|
return;
|
|
}
|
|
|
|
// get the player
|
|
player *pp = &Players[Player_num];
|
|
|
|
// wacky stuff here
|
|
if (pp->friendly_hits != 0) {
|
|
float time_since_last_hit = f2fl(Missiontime - pp->friendly_last_hit_time);
|
|
if ((time_since_last_hit >= 0.0f) && (time_since_last_hit < 10000.0f)) {
|
|
if (time_since_last_hit > 60.0f) {
|
|
pp->friendly_hits = 0;
|
|
pp->friendly_damage = 0.0f;
|
|
} else if (time_since_last_hit > 2.0f) {
|
|
pp->friendly_hits -= (int) time_since_last_hit/2;
|
|
pp->friendly_damage -= time_since_last_hit;
|
|
}
|
|
|
|
if (pp->friendly_damage < 0.0f) {
|
|
pp->friendly_damage = 0.0f;
|
|
}
|
|
|
|
if (pp->friendly_hits < 0) {
|
|
pp->friendly_hits = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
float damage; // Damage done by weapon. Gets scaled down based on size of ship.
|
|
|
|
if (objp_weapon->type == OBJ_BEAM) // added beam for traitor detection -FUBAR
|
|
damage = beam_get_ship_damage(&Beams[objp_weapon->instance], objp_hit);
|
|
else
|
|
damage = Weapon_info[Weapons[objp_weapon->instance].weapon_info_index].damage;
|
|
|
|
// wacky stuff here
|
|
ship_info *sip = &Ship_info[Ships[objp_hit->instance].ship_info_index];
|
|
if (shipp_hit->ship_max_hull_strength > 1000.0f) {
|
|
float factor = shipp_hit->ship_max_hull_strength / 1000.0f;
|
|
factor = MIN(100.0f, factor);
|
|
damage /= factor;
|
|
}
|
|
|
|
// Don't penalize much at all for hitting cargo
|
|
if (sip->class_type > -1) {
|
|
damage *= Ship_types[sip->class_type].ff_multiplier;
|
|
}
|
|
|
|
// Hit ship, but not targeting it, so it's not so heinous, maybe an accident.
|
|
if (Ai_info[shipp_hitter->ai_index].target_objnum != OBJ_INDEX(objp_hit)) {
|
|
damage /= 5.0f;
|
|
}
|
|
|
|
pp->friendly_last_hit_time = Missiontime;
|
|
pp->friendly_hits++;
|
|
|
|
// cap damage and number of hits done this frame
|
|
float accredited_damage = MIN(MAX_BURST_DAMAGE, pp->damage_this_burst + damage) - pp->damage_this_burst;
|
|
pp->friendly_damage += accredited_damage;
|
|
pp->damage_this_burst += accredited_damage;
|
|
|
|
// Done with adjustments to damage. Evaluate based on current friendly_damage
|
|
nprintf(("AI", "Friendly damage: %.1f, threshold: %.1f, inc damage: %.1f, max burst: %.1f\n", pp->friendly_damage, FRIENDLY_DAMAGE_THRESHOLD * (1.0f + (float) (NUM_SKILL_LEVELS + 1 - Game_skill_level)/3.0f), pp->damage_this_burst, MAX_BURST_DAMAGE ));
|
|
|
|
if (is_instructor(objp_hit)) {
|
|
// it's not nice to hit your instructor
|
|
if (pp->friendly_damage > FRIENDLY_DAMAGE_THRESHOLD) {
|
|
message_send_builtin(MESSAGE_INSTRUCTOR_ATTACK, nullptr, nullptr, -1, -1);
|
|
pp->last_warning_message_time = Missiontime;
|
|
ship_set_subsystem_strength( Player_ship, SUBSYSTEM_WEAPONS, 0.0f);
|
|
|
|
training_fail();
|
|
|
|
// Instructor leaves.
|
|
mission_do_departure(objp_hit);
|
|
gameseq_post_event( GS_EVENT_PLAYER_WARPOUT_START_FORCED ); // Force player to warp out.
|
|
} else if (Missiontime - pp->last_warning_message_time > F1_0*4) {
|
|
// warning every 4 sec
|
|
// use NULL as the message sender here since it is the Terran Command persona
|
|
message_send_builtin(MESSAGE_INSTRUCTOR_HIT, nullptr, nullptr, -1, -1);
|
|
pp->last_warning_message_time = Missiontime;
|
|
}
|
|
|
|
// not nice to hit your friends
|
|
} else if (pp->friendly_damage > FRIENDLY_DAMAGE_THRESHOLD * (1.0f + (float) (NUM_SKILL_LEVELS + 1 - Game_skill_level)/3.0f)) {
|
|
process_friendly_hit_message( MESSAGE_HAMMER_SWINE, objp_hit );
|
|
mission_goal_fail_all();
|
|
ai_abort_rearm_request( Player_obj );
|
|
|
|
Player_ship->team = Iff_traitor;
|
|
|
|
} else if ((damage > frand()) && (Missiontime - pp->last_warning_message_time > F1_0*4) && (pp->friendly_damage > FRIENDLY_DAMAGE_THRESHOLD)) {
|
|
// no closer than 4 sec intervals
|
|
// Note: (damage > frand()) added on 12/9/97 by MK. Since damage is now scaled down for big ships, we could get too
|
|
// many warnings. Kind of tedious. frand() returns a value in 0..1, so this won't affect legit hits.
|
|
process_friendly_hit_message( MESSAGE_OOPS, objp_hit );
|
|
pp->last_warning_message_time = Missiontime;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maybe make ship with ai_info *aip attack hitter_objnum as a dynamic goal
|
|
*/
|
|
void maybe_set_dynamic_chase(ai_info *aip, int hitter_objnum)
|
|
{
|
|
Assert(Ship_info[Ships[aip->shipnum].ship_info_index].is_fighter_bomber());
|
|
|
|
// limit the number of ships attacking hitter_objnum (for now, only if hitter_objnum is player)
|
|
if ( ai_maybe_limit_attackers(hitter_objnum) == 1 ) {
|
|
return;
|
|
}
|
|
|
|
// only set as target if can be targeted.
|
|
if (awacs_get_level(&Objects[hitter_objnum], &Ships[aip->shipnum], true) < 1.0f) {
|
|
return;
|
|
}
|
|
|
|
if (aip->target_objnum != hitter_objnum)
|
|
aip->aspect_locked_time = 0.0f;
|
|
set_target_objnum(aip, hitter_objnum);
|
|
aip->resume_goal_time = Missiontime + i2f(20); // Only chase up to 20 seconds.
|
|
aip->active_goal = AI_ACTIVE_GOAL_DYNAMIC;
|
|
|
|
set_targeted_subsys(aip, NULL, -1); // Say not attacking any particular subsystem.
|
|
|
|
aip->previous_submode = aip->mode;
|
|
aip->mode = AIM_CHASE;
|
|
aip->submode = SM_ATTACK;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
|
|
|
|
// Return true if *objp has armed an aspect seeking bomb.
|
|
// This function written so a ship with an important bomb to fire will willingly take hits in the face to fire its bomb.
|
|
int firing_aspect_seeking_bomb(object *objp)
|
|
{
|
|
ship *shipp;
|
|
int bank_index;
|
|
ship_weapon *swp;
|
|
|
|
shipp = &Ships[objp->instance];
|
|
|
|
swp = &shipp->weapons;
|
|
|
|
bank_index = swp->current_secondary_bank;
|
|
|
|
if (bank_index != -1) {
|
|
if (swp->secondary_bank_weapons[bank_index] > 0) {
|
|
if (ship_secondary_has_ammo(swp, bank_index)) {
|
|
if (Weapon_info[swp->secondary_bank_weapons[bank_index]].wi_flags[Weapon::Info_Flags::Bomb]) {
|
|
if (Weapon_info[swp->secondary_bank_weapons[bank_index]].wi_flags[Weapon::Info_Flags::Homing_aspect]) {
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// *objp collided with big ship *big_objp at some global point.
|
|
// Make it fly away from the collision point.
|
|
// collision_normal is NULL, when a collision is imminent and we just want to bug out.
|
|
void big_ship_collide_recover_start(const object *objp, const object *big_objp, const vec3d *collision_normal)
|
|
{
|
|
ai_info *aip;
|
|
|
|
Assert(objp->type == OBJ_SHIP);
|
|
|
|
aip = &Ai_info[Ships[objp->instance].ai_index];
|
|
|
|
if (!timestamp_elapsed(aip->big_recover_timestamp) && (aip->ai_flags[AI::AI_Flags::Big_ship_collide_recover_1]))
|
|
return;
|
|
|
|
if (collision_normal) {
|
|
aip->big_recover_timestamp = timestamp(2000);
|
|
aip->big_collision_normal = *collision_normal;
|
|
} else {
|
|
aip->big_recover_timestamp = timestamp(500);
|
|
}
|
|
|
|
aip->ai_flags.remove(AI::AI_Flags::Big_ship_collide_recover_2);
|
|
aip->ai_flags.set(AI::AI_Flags::Big_ship_collide_recover_1);
|
|
|
|
// big_recover_1_direction is 100 m out along normal
|
|
if (collision_normal) {
|
|
aip->big_recover_1_direction = *collision_normal * 100.f;
|
|
} else {
|
|
aip->big_recover_1_direction = objp->orient.vec.fvec * -100.0f;
|
|
}
|
|
|
|
vec3d global_recover_pos_1 = aip->big_recover_1_direction + objp->pos;
|
|
// go out 200 m from box closest box point
|
|
get_world_closest_box_point_with_delta(&aip->big_recover_2_pos, big_objp, &global_recover_pos_1, nullptr, 300.0f);
|
|
|
|
accelerate_ship(aip, 0.0f);
|
|
}
|
|
|
|
float max_lethality = 0.0f;
|
|
|
|
void ai_update_lethality(const object *pship_obj, const object *other_obj, float damage)
|
|
{
|
|
// Goober5000 - stop any trickle-down errors from ship_do_damage
|
|
Assert(other_obj);
|
|
if (!other_obj)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Assert(pship_obj); // Goober5000
|
|
Assert(pship_obj->type == OBJ_SHIP);
|
|
Assert(other_obj->type == OBJ_WEAPON || other_obj->type == OBJ_SHOCKWAVE);
|
|
int dont_count = FALSE;
|
|
|
|
int parent = other_obj->parent;
|
|
if (Objects[parent].type == OBJ_SHIP) {
|
|
if (Objects[parent].signature == other_obj->parent_sig) {
|
|
|
|
// check damage done to enemy team
|
|
if (iff_x_attacks_y(Ships[pship_obj->instance].team, Ships[Objects[parent].instance].team)) {
|
|
|
|
// other is weapon
|
|
if (other_obj->type == OBJ_WEAPON) {
|
|
weapon *wp = &Weapons[other_obj->instance];
|
|
weapon_info *wif = &Weapon_info[wp->weapon_info_index];
|
|
|
|
// if parent is BIG|HUGE, don't count beam
|
|
if ( Ship_info[Ships[Objects[parent].instance].ship_info_index].is_big_or_huge() ) {
|
|
if (wif->wi_flags[Weapon::Info_Flags::Beam]) {
|
|
dont_count = TRUE;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!dont_count) {
|
|
float lethality = 0.025f * damage; // 2 cyclops (@2000) put you at 100 lethality
|
|
|
|
// increase lethality weapon's parent ship
|
|
ai_info *aip = &Ai_info[Ships[Objects[parent].instance].ai_index];
|
|
aip->lethality += lethality;
|
|
aip->lethality = MIN(110.0f, aip->lethality);
|
|
// if you hit, don't be less than 0
|
|
aip->lethality = MAX(0.0f, aip->lethality);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Object *objp_ship was hit by either weapon *objp_weapon or collided into by ship hit_objp
|
|
*/
|
|
void ai_ship_hit(object *objp_ship, object *hit_objp, const vec3d *hit_normal)
|
|
{
|
|
int hitter_objnum = -2;
|
|
object *objp_hitter = nullptr;
|
|
ship *shipp;
|
|
ai_info *aip, *hitter_aip;
|
|
|
|
shipp = &Ships[objp_ship->instance];
|
|
aip = &Ai_info[shipp->ai_index];
|
|
|
|
if (objp_ship->flags[Object::Object_Flags::Player_ship]) {
|
|
if (The_mission.ai_profile->flags[AI::Profile_Flags::Reset_last_hit_target_time_for_player_hits]) {
|
|
//SUSHI: So that hitting a player ship actually resets the last_hit_target_time counter for whoever hit the player.
|
|
//This is all copypasted from code below
|
|
// Added OBJ_BEAM for traitor detection - FUBAR
|
|
if ((hit_objp->type == OBJ_WEAPON) || (hit_objp->type == OBJ_BEAM)) {
|
|
hitter_objnum = hit_objp->parent;
|
|
Assert((hitter_objnum < MAX_OBJECTS));
|
|
if (hitter_objnum == -1) {
|
|
return; // Possible SSM, bail while we still can.
|
|
}
|
|
objp_hitter = &Objects[hitter_objnum];
|
|
} else if (hit_objp->type == OBJ_SHIP) {
|
|
objp_hitter = hit_objp;
|
|
} else {
|
|
Assertion(false, "ai_ship_hit has been passed an invalid object type of %d, please report to the SCP!", hit_objp->type);
|
|
return;
|
|
}
|
|
Assert(objp_hitter != nullptr);
|
|
hitter_aip = &Ai_info[Ships[objp_hitter->instance].ai_index];
|
|
hitter_aip->last_hit_target_time = Missiontime;
|
|
|
|
aip->last_hit_time = Missiontime;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ((aip->mode == AIM_WARP_OUT) || (aip->mode == AIM_PLAY_DEAD))
|
|
return;
|
|
|
|
if (hit_objp->type == OBJ_SHIP) {
|
|
// If the object that this ship collided with is a big ship
|
|
if (Ship_info[Ships[hit_objp->instance].ship_info_index].is_big_or_huge()) {
|
|
// And the current object is a small ship
|
|
if (Ship_info[Ships[objp_ship->instance].ship_info_index].is_small_ship()) {
|
|
// Recover from hitting a big ship. Note, if two big ships collide, they just pound away at each other. Oh well. Recovery looks dumb and it's very late.
|
|
big_ship_collide_recover_start(objp_ship, hit_objp, hit_normal); // NOLINT(readability-suspicious-call-argument)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Added OBJ_BEAM for traitor detection - FUBAR
|
|
if ((hit_objp->type == OBJ_WEAPON) || (hit_objp->type == OBJ_BEAM)) {
|
|
if (hit_objp->parent < 0) {
|
|
return;
|
|
}
|
|
if (hit_objp->parent_sig != Objects[hit_objp->parent].signature) {
|
|
return;
|
|
}
|
|
|
|
weapon_info* wip = hit_objp->type == OBJ_WEAPON ? &Weapon_info[Weapons[hit_objp->instance].weapon_info_index] :
|
|
&Weapon_info[Beams[hit_objp->instance].weapon_info_index];
|
|
if (wip->wi_flags[Weapon::Info_Flags::Heals])
|
|
return;
|
|
|
|
hitter_objnum = hit_objp->parent;
|
|
Assertion((hitter_objnum >= 0) && (hitter_objnum < MAX_OBJECTS), "hitter_objnum in this function is an invalid index of %d. This can cause random behavior, and is a coder mistake. Please report!", hitter_objnum);
|
|
objp_hitter = &Objects[hitter_objnum];
|
|
|
|
// Only work through hits by objects that are still in the game
|
|
if(objp_hitter->type != OBJ_SHIP && objp_hitter->type != OBJ_WEAPON && objp_hitter->type != OBJ_ASTEROID && objp_hitter->type != OBJ_BEAM && objp_hitter->type != OBJ_DEBRIS) {
|
|
// if the object was not in the game anymore, the only valid type should be OBJ_GHOST here.
|
|
// OBJ_GHOST is a dead player. The player's weapons can do damage after they are dead. Having that get here is acceptable
|
|
// OBJ_NONE should be excluded above when it checks that the instance number matches.
|
|
Assertion(objp_hitter->type == OBJ_GHOST, "FSO's AI code is passing an invalid object type of %d to a function when trying to decide how to respond to something hitting a ship. This is a bug in the code, please report!", objp_hitter->type);
|
|
return;
|
|
}
|
|
|
|
// Hit by a protected ship, don't attack it.
|
|
if (objp_hitter->flags[Object::Object_Flags::Protected]) {
|
|
if (!object_is_docked(objp_ship)) {
|
|
if ((Ship_info[shipp->ship_info_index].is_fighter_bomber()) && (aip->target_objnum == -1)) {
|
|
if (aip->mode == AIM_CHASE) {
|
|
if (aip->submode != SM_EVADE_WEAPON) {
|
|
aip->mode = AIM_CHASE;
|
|
aip->submode = SM_EVADE_WEAPON;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
} else if (aip->mode != AIM_EVADE_WEAPON) {
|
|
aip->active_goal = AI_ACTIVE_GOAL_DYNAMIC;
|
|
aip->previous_mode = aip->mode;
|
|
aip->previous_submode = aip->submode;
|
|
aip->mode = AIM_EVADE_WEAPON;
|
|
aip->submode = -1;
|
|
aip->submode_start_time = Missiontime;
|
|
aip->mode_time = timestamp(MAX_EVADE_TIME); // Evade for up to five seconds.
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
maybe_process_friendly_hit(objp_hitter, objp_ship, hit_objp); // Deal with player's friendly fire.
|
|
|
|
ship* attacker = &Ships[objp_hitter->instance];
|
|
ship_maybe_ask_for_help(shipp, attacker);
|
|
} else if (hit_objp->type == OBJ_SHIP) {
|
|
if (shipp->team == Ships[hit_objp->instance].team) // Don't have AI react to collisions between teammates.
|
|
return;
|
|
objp_hitter = hit_objp;
|
|
hitter_objnum = OBJ_INDEX(hit_objp);
|
|
} else {
|
|
Assertion(false, "ai_ship_hit has been passed an invalid object type of %d, please report to the SCP!", hit_objp->type);
|
|
return;
|
|
}
|
|
|
|
// traitor detection was the only reason for OBJ_BEAM to make it this far.
|
|
// so bail if it wasn't fired by a player.
|
|
if ((hit_objp->type == OBJ_BEAM) && (!(objp_hitter->flags[Object::Object_Flags::Player_ship])))
|
|
return;
|
|
|
|
// Collided into a protected ship, don't attack it.
|
|
if (hit_objp->flags[Object::Object_Flags::Protected])
|
|
return;
|
|
|
|
Assert(objp_hitter != nullptr);
|
|
hitter_aip = &Ai_info[Ships[objp_hitter->instance].ai_index];
|
|
hitter_aip->last_hit_target_time = Missiontime;
|
|
|
|
// store the object signature of objp_ship into ai_info, since we want to track the last ship hit by 'hitter_objnum'
|
|
hitter_aip->last_objsig_hit = objp_ship->signature;
|
|
|
|
aip->last_hit_time = Missiontime;
|
|
|
|
if (aip->ai_flags[AI::AI_Flags::No_dynamic, AI::AI_Flags::Kamikaze]) // If not allowed to pursue dynamic objectives, don't evade. Dumb? Maybe change. -- MK, 3/15/98
|
|
return;
|
|
|
|
// If this ship is awaiting repair, abort!
|
|
if (aip->ai_flags[AI::AI_Flags::Being_repaired, AI::AI_Flags::Awaiting_repair]) {
|
|
if (get_hull_pct(objp_ship) < 0.3f) {
|
|
// Note, only abort if hull below a certain level.
|
|
aip->next_rearm_request_timestamp = timestamp(NEXT_REARM_TIMESTAMP/2); // Might request again after 15 seconds.
|
|
if ( !(objp_ship->flags[Object::Object_Flags::Player_ship]) ) // mwa -- don't abort rearm for a player
|
|
ai_abort_rearm_request(objp_ship);
|
|
}
|
|
}
|
|
|
|
// If firing a bomb, ignore enemy fire so we can gain lock drop the bomb.
|
|
// Only ignore fire if aspect_locked_time > 0.5f, as this means we're in range.
|
|
if (firing_aspect_seeking_bomb(objp_ship)) {
|
|
if ((aip->ai_flags[AI::AI_Flags::Seek_lock]) && (aip->aspect_locked_time > 0.1f))
|
|
return;
|
|
}
|
|
|
|
// If in AIM_STRAFE mode and got hit by target, maybe attack turret if appropriate
|
|
if (aip->mode == AIM_STRAFE) {
|
|
Assert(hitter_objnum != -2);
|
|
if (aip->target_objnum == hitter_objnum) {
|
|
if ( hit_objp->type == OBJ_WEAPON ) {
|
|
ai_big_strafe_maybe_attack_turret(objp_ship, hit_objp);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ((objp_ship == Player_obj) && !Player_use_ai) // Goober5000 - allow for exception
|
|
return; // We don't do AI for the player.
|
|
|
|
maybe_update_guard_object(objp_ship, objp_hitter);
|
|
|
|
// Big ships don't go any further.
|
|
if (!(Ship_info[shipp->ship_info_index].is_small_ship()))
|
|
return;
|
|
|
|
// If the hitter object is the ignore object, don't attack it.
|
|
ship_info *sip = &Ship_info[shipp->ship_info_index];
|
|
if (is_ignore_object(aip, OBJ_INDEX(objp_hitter)) && (sip->is_fighter_bomber())) {
|
|
if (!object_is_docked(objp_ship)) {
|
|
if (aip->mode == AIM_NONE) {
|
|
aip->mode = AIM_CHASE; // This will cause the ship to move, if not attack.
|
|
aip->submode = SM_EVADE;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Maybe abort based on mode.
|
|
switch (aip->mode) {
|
|
case AIM_CHASE:
|
|
if (aip->submode == SM_ATTACK_FOREVER)
|
|
return;
|
|
|
|
if ( (hit_objp->type == OBJ_WEAPON) && !(aip->ai_flags[AI::AI_Flags::No_dynamic]) ) {
|
|
if ( ai_big_maybe_enter_strafe_mode(objp_ship, OBJ_INDEX(hit_objp)) )
|
|
return;
|
|
}
|
|
break;
|
|
case AIM_GUARD:
|
|
// If in guard mode and far away from guard object, don't pursue guy that hit me.
|
|
if ((aip->guard_objnum != -1) && (aip->guard_signature == Objects[aip->guard_objnum].signature)) {
|
|
if (vm_vec_dist_quick(&objp_ship->pos, &Objects[aip->guard_objnum].pos) > 500.0f) {
|
|
return;
|
|
}
|
|
}
|
|
case AIM_STILL:
|
|
case AIM_STAY_NEAR:
|
|
// Note: Dealt with above, at very top. case AIM_PLAY_DEAD:
|
|
case AIM_STRAFE:
|
|
break;
|
|
case AIM_EVADE_WEAPON:
|
|
case AIM_EVADE:
|
|
case AIM_GET_BEHIND:
|
|
case AIM_AVOID:
|
|
case AIM_DOCK:
|
|
case AIM_BIGSHIP:
|
|
case AIM_PATH:
|
|
case AIM_NONE:
|
|
case AIM_BAY_DEPART:
|
|
case AIM_SENTRYGUN:
|
|
return;
|
|
case AIM_BAY_EMERGE:
|
|
// If just leaving the docking bay, don't react to enemy fire... just keep flying away from docking bay
|
|
if ( (Missiontime - aip->submode_start_time) < 5*F1_0 ) {
|
|
return;
|
|
}
|
|
break;
|
|
case AIM_FLY_TO_SHIP:
|
|
case AIM_WAYPOINTS:
|
|
if (sip->is_fighter_bomber())
|
|
break;
|
|
return;
|
|
case AIM_SAFETY:
|
|
if ((aip->submode != AISS_1) || (Missiontime - aip->submode_start_time > i2f(1))) {
|
|
aip->submode = AISS_1;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
return;
|
|
case AIM_WARP_OUT:
|
|
return;
|
|
case AIM_LUA:
|
|
return;
|
|
default:
|
|
UNREACHABLE("Unknown AI Mode %d! Get a coder!", aip->mode);
|
|
}
|
|
|
|
if (timestamp_elapsed(aip->ok_to_target_timestamp)) {
|
|
aip->ai_flags.remove(AI::AI_Flags::Formation_object); // If flying in formation, bug out!
|
|
aip->ai_flags.remove(AI::AI_Flags::Formation_wing);
|
|
}
|
|
|
|
aip->hitter_objnum = hitter_objnum;
|
|
aip->hitter_signature = Objects[hitter_objnum].signature;
|
|
|
|
// If the hitter is not on the same team as the hittee, do some stuff.
|
|
if (iff_x_attacks_y(shipp->team, Ships[objp_hitter->instance].team)) {
|
|
|
|
if ((hitter_objnum != aip->target_objnum) && (sip->is_fighter_bomber())) {
|
|
maybe_set_dynamic_chase(aip, hitter_objnum);
|
|
maybe_afterburner_after_ship_hit(objp_ship, aip, &Objects[hitter_objnum]);
|
|
} else {
|
|
if ((aip->mode == AIM_CHASE) && ai_near_full_strength(objp_ship)) {
|
|
switch (aip->submode) {
|
|
case SM_ATTACK:
|
|
case SM_SUPER_ATTACK:
|
|
case SM_GET_AWAY:
|
|
break;
|
|
default:
|
|
if (sip->is_fighter_bomber()) {
|
|
maybe_set_dynamic_chase(aip, hitter_objnum);
|
|
}
|
|
maybe_afterburner_after_ship_hit(objp_ship, aip, &Objects[hitter_objnum]);
|
|
break;
|
|
}
|
|
} else if (aip->mode == AIM_CHASE) {
|
|
switch (aip->submode) {
|
|
case SM_ATTACK:
|
|
aip->submode = SM_EVADE;
|
|
aip->submode_start_time = Missiontime;
|
|
break;
|
|
case SM_SUPER_ATTACK:
|
|
if (Missiontime - aip->submode_start_time > i2f(1)) {
|
|
aip->submode = SM_EVADE;
|
|
aip->submode_start_time = Missiontime;
|
|
}
|
|
break;
|
|
case SM_EVADE_BRAKE:
|
|
break;
|
|
case SM_EVADE_SQUIGGLE:
|
|
aip->submode = SM_EVADE;
|
|
aip->submode_start_time = Missiontime;
|
|
break;
|
|
default:
|
|
if (sip->is_fighter_bomber()) {
|
|
maybe_set_dynamic_chase(aip, hitter_objnum);
|
|
maybe_afterburner_after_ship_hit(objp_ship, aip, &Objects[hitter_objnum]);
|
|
}
|
|
|
|
break;
|
|
}
|
|
} else {
|
|
// AL 3-15-98: Prevent escape pods from entering chase mode
|
|
if (sip->is_fighter_bomber()) {
|
|
maybe_set_dynamic_chase(aip, hitter_objnum);
|
|
}
|
|
maybe_afterburner_after_ship_hit(objp_ship, aip, &Objects[hitter_objnum]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ship shipnum has been destroyed.
|
|
// Cleanup.
|
|
void ai_ship_destroy(int shipnum)
|
|
{
|
|
Assert((shipnum >= 0) && (shipnum < MAX_SHIPS));
|
|
auto dead_shipp = &Ships[shipnum];
|
|
Assert((dead_shipp->objnum >= 0) && (dead_shipp->objnum < MAX_OBJECTS));
|
|
auto dead_objp = &Objects[dead_shipp->objnum];
|
|
Assert((dead_shipp->ai_index >= 0) && (dead_shipp->ai_index < MAX_AI_INFO));
|
|
auto dead_aip = &Ai_info[dead_shipp->ai_index];
|
|
|
|
// if I was getting repaired, or awaiting repair, or performing repair, then clean up the repair mode.
|
|
ai_cleanup_rearm_mode(dead_objp, false);
|
|
|
|
// clear bay door animations
|
|
ai_manage_bay_doors(dead_objp, dead_aip, true);
|
|
|
|
// For all objects that had this ship as a target, wipe it out, forcing find of a new enemy.
|
|
for ( auto 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 other_objp = &Objects[so->objnum];
|
|
Assert((other_objp->instance >= 0) && (other_objp->instance < MAX_SHIPS));
|
|
auto other_shipp = &Ships[other_objp->instance];
|
|
Assert((other_shipp->ai_index >= 0) && (other_shipp->ai_index < MAX_AI_INFO));
|
|
auto other_aip = &Ai_info[other_shipp->ai_index];
|
|
|
|
if (other_aip->target_objnum == dead_shipp->objnum) {
|
|
set_target_objnum(other_aip, -1);
|
|
// If this ship had a dynamic goal of chasing the dead ship, clear the dynamic goal.
|
|
if (other_aip->resume_goal_time != -1)
|
|
other_aip->active_goal = AI_ACTIVE_GOAL_NONE;
|
|
}
|
|
|
|
if (other_aip->goal_objnum == dead_shipp->objnum) {
|
|
other_aip->goal_objnum = -1;
|
|
other_aip->goal_signature = -1;
|
|
}
|
|
|
|
if (other_aip->guard_objnum == dead_shipp->objnum) {
|
|
other_aip->guard_objnum = -1;
|
|
other_aip->guard_signature = -1;
|
|
}
|
|
|
|
if ((other_aip->guard_wingnum != -1) && (other_aip->guard_wingnum == dead_shipp->wingnum)) {
|
|
if (other_aip->guard_wingnum != other_shipp->wingnum)
|
|
ai_set_guard_wing(other_objp, other_aip->guard_wingnum);
|
|
}
|
|
|
|
if (other_aip->hitter_objnum == dead_shipp->objnum)
|
|
other_aip->hitter_objnum = -1;
|
|
}
|
|
|
|
if (dead_aip->ai_flags[AI::AI_Flags::Formation_object] && dead_aip->goal_objnum >= 0)
|
|
ai_formation_object_recalculate_slotnums(dead_aip->goal_objnum, dead_shipp->objnum);
|
|
}
|
|
|
|
/**
|
|
* Do stuff at start of deathroll.
|
|
*/
|
|
void ai_deathroll_start(object *dying_objp)
|
|
{
|
|
// make sure this is a ship
|
|
Assert(dying_objp->type == OBJ_SHIP);
|
|
|
|
// mark objects we are docked with so we can do damage and separate during death roll
|
|
for (dock_instance *ptr = dying_objp->dock_list; ptr != NULL; ptr = ptr->next)
|
|
{
|
|
object *docked_objp = ptr->docked_objp;
|
|
int docker_index = ptr->dockpoint_used;
|
|
int dockee_index = dock_find_dockpoint_used_by_object(docked_objp, dying_objp);
|
|
|
|
dock_dead_dock_objects(dying_objp, docker_index, docked_objp, dockee_index);
|
|
}
|
|
|
|
// clean up any rearm-related stuff
|
|
ai_cleanup_rearm_mode(dying_objp, true);
|
|
|
|
// clean up anybody docking or undocking to me
|
|
ai_cleanup_dock_mode_objective(dying_objp);
|
|
|
|
// Undock from every object directly docked to dying_objp. We can't just iterate through the list because
|
|
// we're undocking the objects while we iterate over them and the pointers get seriously messed up.
|
|
// So we just repeatedly remove the first object until the dying object is no longer docked to anything.
|
|
while (object_is_docked(dying_objp))
|
|
{
|
|
object *docked_objp = dock_get_first_docked_object(dying_objp);
|
|
|
|
// undock these objects
|
|
ai_do_objects_undocked_stuff(dying_objp, docked_objp);
|
|
}
|
|
|
|
// clear my ai mode
|
|
Ai_info[Ships[dying_objp->instance].ai_index].mode = AIM_NONE;
|
|
}
|
|
|
|
// Object *requester_objp tells rearm ship to abort rearm.
|
|
// Returns true if it succeeded, else false.
|
|
// To succeed means you were previously rearming.
|
|
int ai_abort_rearm_request(object *requester_objp)
|
|
{
|
|
ship *requester_shipp;
|
|
ai_info *requester_aip;
|
|
|
|
Assert(requester_objp->type == OBJ_SHIP);
|
|
if(requester_objp->type != OBJ_SHIP){
|
|
return 0;
|
|
}
|
|
Assert((requester_objp->instance >= 0) && (requester_objp->instance < MAX_SHIPS));
|
|
if((requester_objp->instance < 0) || (requester_objp->instance >= MAX_SHIPS)){
|
|
return 0;
|
|
}
|
|
requester_shipp = &Ships[requester_objp->instance];
|
|
Assert((requester_shipp->ai_index >= 0) && (requester_shipp->ai_index < MAX_AI_INFO));
|
|
if((requester_shipp->ai_index < 0) || (requester_shipp->ai_index >= MAX_AI_INFO)){
|
|
return 0;
|
|
}
|
|
requester_aip = &Ai_info[requester_shipp->ai_index];
|
|
|
|
if (requester_aip->ai_flags[AI::AI_Flags::Being_repaired, AI::AI_Flags::Awaiting_repair]){
|
|
|
|
// support objnum is always valid once a rearm repair has been requested. It points to the
|
|
// ship that is coming to repair me.
|
|
if (requester_aip->support_ship_objnum != -1) {
|
|
object *repair_objp;
|
|
ai_info *repair_aip;
|
|
|
|
repair_objp = &Objects[requester_aip->support_ship_objnum];
|
|
repair_aip = &Ai_info[Ships[repair_objp->instance].ai_index];
|
|
|
|
// Make sure signatures match. This prevents nasty bugs in which an object
|
|
// that was repairing another is destroyed and is replaced by another ship
|
|
// before this code comes around.
|
|
if (repair_objp->signature == requester_aip->support_ship_signature) {
|
|
|
|
Assert( repair_objp->type == OBJ_SHIP );
|
|
|
|
// if support ship is in the process of undocking, don't do anything.
|
|
if ( repair_aip->submode < AIS_UNDOCK_0 ) {
|
|
ai_do_objects_repairing_stuff( requester_objp, repair_objp, REPAIR_INFO_ABORT );
|
|
|
|
if ( repair_aip->submode == AIS_DOCK_4 )
|
|
{
|
|
repair_aip->submode = AIS_UNDOCK_0;
|
|
repair_aip->submode_start_time = Missiontime;
|
|
}
|
|
else
|
|
{
|
|
// unwind all the support ship docking operations
|
|
ai_cleanup_dock_mode_subjective(repair_objp);
|
|
}
|
|
} else {
|
|
nprintf(("AI", "Not aborting rearm since already undocking\n"));
|
|
}
|
|
}
|
|
} else {
|
|
// setting these flags is the safe things to do. There may not be a corresponding repair
|
|
// ship for this guys since a repair ship may be currently repairing someone else.
|
|
ai_do_objects_repairing_stuff( requester_objp, NULL, REPAIR_INFO_ABORT );
|
|
|
|
// try and remove this guy from an arriving support ship.
|
|
mission_remove_scheduled_repair(requester_objp);
|
|
}
|
|
|
|
return 1;
|
|
} else if ( requester_aip->ai_flags[AI::AI_Flags::Repairing] ) {
|
|
// a support ship can request to abort when he is told to do something else (like warp out).
|
|
// see if this support ships goal_objnum is valid. If so, then issue this ai_abort comment
|
|
// for the ship that he is enroute to repair
|
|
if ( requester_aip->goal_objnum != -1 ) {
|
|
int val;
|
|
|
|
val = ai_abort_rearm_request( &Objects[requester_aip->goal_objnum] );
|
|
return val;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// function which gets called from ai-issue_rearm_request and from code in missionparse.cpp
|
|
// to actually issue the rearm goal (support_obj to rearm requester_obj);
|
|
void ai_add_rearm_goal( object *requester_objp, object *support_objp )
|
|
{
|
|
ship *support_shipp, *requester_shipp;
|
|
ai_info *support_aip, *requester_aip;
|
|
|
|
support_shipp = &Ships[support_objp->instance];
|
|
requester_shipp = &Ships[requester_objp->instance];
|
|
requester_aip = &Ai_info[requester_shipp->ai_index];
|
|
|
|
Assert( support_shipp->ai_index != -1 );
|
|
support_aip = &Ai_info[support_shipp->ai_index];
|
|
|
|
// if the requester is a player object, issue the order as the squadmate messaging code does. Doing so
|
|
// ensures that the player get a higher priority!
|
|
requester_aip->ai_flags.set(AI::AI_Flags::Awaiting_repair); // Tell that I'm awaiting repair.
|
|
if ( requester_objp->flags[Object::Object_Flags::Player_ship] )
|
|
ai_add_ship_goal_player( ai_goal_type::PLAYER_SHIP, AI_GOAL_REARM_REPAIR, -1, requester_shipp->ship_name, support_aip );
|
|
else
|
|
ai_add_goal_ship_internal( support_aip, AI_GOAL_REARM_REPAIR, requester_shipp->ship_name, -1, -1 );
|
|
|
|
}
|
|
|
|
// Object *requester_objp requests rearming.
|
|
// Returns objnum of ship coming to repair requester on success
|
|
// Success means you found someone to rearm you and you weren't previously rearming.
|
|
int ai_issue_rearm_request(object *requester_objp)
|
|
{
|
|
object *objp = NULL;
|
|
ship *requester_shipp;
|
|
ai_info *requester_aip;
|
|
|
|
Assert(requester_objp->type == OBJ_SHIP);
|
|
Assert((requester_objp->instance >= 0) && (requester_objp->instance < MAX_SHIPS));
|
|
requester_shipp = &Ships[requester_objp->instance];
|
|
|
|
Assert((requester_shipp->ai_index >= 0) && (requester_shipp->ai_index < MAX_AI_INFO));
|
|
requester_aip = &Ai_info[requester_shipp->ai_index];
|
|
|
|
// these should have already been caught by the time we get here!
|
|
Assert(!(requester_aip->ai_flags[AI::AI_Flags::Awaiting_repair]));
|
|
Assert(is_support_allowed(requester_objp));
|
|
|
|
requester_aip->next_rearm_request_timestamp = timestamp(NEXT_REARM_TIMESTAMP); // Might request again after this much time.
|
|
|
|
// call ship_find_repair_ship to get a support ship. If none is found, then we will warp one in. This
|
|
// function will return the next available ship which can repair requester
|
|
int result = ship_find_repair_ship( requester_objp, &objp );
|
|
|
|
// we are able to call in a ship, or a ship is warping in
|
|
// (Arriving_support_ship may be non-NULL in either of these cases, but mission_bring_in_support_ship has a check for that)
|
|
if (result == 0 || result == 2) {
|
|
ai_do_objects_repairing_stuff( requester_objp, NULL, REPAIR_INFO_QUEUE );
|
|
mission_bring_in_support_ship( requester_objp );
|
|
return -1;
|
|
}
|
|
// we are able to service a request
|
|
else if (result == 1 || result == 3) {
|
|
Assert(objp != NULL);
|
|
ai_do_objects_repairing_stuff( requester_objp, objp, REPAIR_INFO_QUEUE );
|
|
return OBJ_INDEX(objp);
|
|
}
|
|
// we aren't able to do anything!
|
|
else {
|
|
Assert(result == 4);
|
|
UNREACHABLE("This case should have already been caught by the is_support_allowed precheck!");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make objp rearm and repair goal_objp
|
|
*/
|
|
void ai_rearm_repair( object *objp, int docker_index, object *goal_objp, int dockee_index )
|
|
{
|
|
ai_info *aip, *goal_aip;
|
|
|
|
aip = &Ai_info[Ships[objp->instance].ai_index];
|
|
aip->goal_objnum = OBJ_INDEX(goal_objp);
|
|
|
|
ai_dock_with_object(objp, docker_index, goal_objp, dockee_index, AIDO_DOCK);
|
|
aip->ai_flags.set(AI::AI_Flags::Repairing); // Tell that repair guy is busy trying to repair someone.
|
|
|
|
goal_aip = &Ai_info[Ships[goal_objp->instance].ai_index];
|
|
|
|
goal_aip->support_ship_objnum = OBJ_INDEX(objp); // Tell which object is coming to repair.
|
|
goal_aip->support_ship_signature = objp->signature;
|
|
|
|
ai_do_objects_repairing_stuff( goal_objp, objp, REPAIR_INFO_ONWAY );
|
|
|
|
goal_aip->abort_rearm_timestamp = timestamp(NEXT_REARM_TIMESTAMP*3/2);
|
|
}
|
|
|
|
// Given a dockee object and the index of the dockbay for that object (ie the dockbay index
|
|
// into polymodel->dockbays[] for the model associated with the object), return the index
|
|
// of a path_num associated with than dockbay (this is an index into polymodel->paths[])
|
|
int ai_return_path_num_from_dockbay(object *dockee_objp, int dockbay_index)
|
|
{
|
|
Assertion(dockbay_index > -1 && dockee_objp != nullptr, "ai_return_path_num_from_dockbay has been passed invalid docking info: %s dockbay index of %d, please report to the SCP!", dockee_objp == nullptr ? "null pointer dockee object and " : "", dockbay_index);
|
|
if ( dockbay_index < 0 || dockee_objp == nullptr ) {
|
|
return -1;
|
|
}
|
|
|
|
if ( dockee_objp->type == OBJ_SHIP ) {
|
|
int path_num;
|
|
polymodel *pm;
|
|
|
|
pm = model_get(Ship_info[Ships[dockee_objp->instance].ship_info_index].model_num );
|
|
|
|
Assert(pm->n_docks > dockbay_index);
|
|
Assert(pm->docking_bays[dockbay_index].num_spline_paths > 0);
|
|
Assert(pm->docking_bays[dockbay_index].splines != NULL);
|
|
|
|
if(pm->n_docks <= dockbay_index){
|
|
return -1;
|
|
}
|
|
if(pm->docking_bays[dockbay_index].num_spline_paths <= 0){
|
|
return -1;
|
|
}
|
|
if(pm->docking_bays[dockbay_index].splines == NULL){
|
|
return -1;
|
|
}
|
|
|
|
// We only need to return one path for the dockbay, so return the first
|
|
path_num = pm->docking_bays[dockbay_index].splines[0];
|
|
return path_num;
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Actually go ahead and fire the synaptics.
|
|
*/
|
|
static void cheat_fire_synaptic(object *objp, ship *shipp)
|
|
{
|
|
ship_weapon *swp;
|
|
swp = &shipp->weapons;
|
|
int current_bank = swp->current_secondary_bank;
|
|
flagset<Weapon::Info_Flags> flags;
|
|
flags += Weapon::Info_Flags::Spawn;
|
|
ai_select_secondary_weapon(objp, swp, &flags, NULL);
|
|
if (timestamp_elapsed(swp->next_secondary_fire_stamp[current_bank])) {
|
|
if (ship_fire_secondary(objp)) {
|
|
nprintf(("AI", "ship %s cheat fired synaptic!\n", shipp->ship_name));
|
|
swp->next_secondary_fire_stamp[current_bank] = timestamp(2500);
|
|
}
|
|
}
|
|
}
|
|
|
|
// For the subspace mission (sm3-09a)
|
|
// for delta wing
|
|
// if they're sufficiently far into the mission
|
|
// if they're near one or more enemies
|
|
// every so often
|
|
// fire a synaptic if they have one.
|
|
void maybe_cheat_fire_synaptic(object *objp)
|
|
{
|
|
// Only do in subspace missions.
|
|
if (!(The_mission.flags[Mission::Mission_Flags::Subspace]))
|
|
return;
|
|
|
|
// Only do in sm3-09a
|
|
if (!stricmp(Game_current_mission_filename, "sm3-09a"))
|
|
{
|
|
ship *shipp;
|
|
int wing_index, time;
|
|
|
|
Assertion(objp->type == OBJ_SHIP, "This ai function requires a ship, but was called with a non-ship object type of %d. Please report!", objp->type);
|
|
|
|
if (objp->type != OBJ_SHIP) {
|
|
return;
|
|
}
|
|
|
|
shipp = &Ships[objp->instance];
|
|
|
|
if (!(strnicmp(shipp->ship_name, NOX("delta"), 5)))
|
|
{
|
|
wing_index = shipp->ship_name[6] - '1';
|
|
|
|
if ((wing_index >= 0) && (wing_index < MAX_SHIPS_PER_WING))
|
|
{
|
|
time = Missiontime >> 16; // Convert to seconds.
|
|
time -= 2*60; // Subtract off two minutes.
|
|
|
|
if (time > 0)
|
|
{
|
|
int modulus = 17 + wing_index*3;
|
|
|
|
if ((time % modulus) < 2)
|
|
{
|
|
int count = num_nearby_fighters(iff_get_attackee_mask(obj_team(objp)), &objp->pos, 1500.0f);
|
|
|
|
if (count > 0)
|
|
cheat_fire_synaptic(objp, shipp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool test_line_of_sight(const vec3d* from, const vec3d* to, SCP_unordered_set<int>&& excluded_object_ids, float threshold, bool test_for_shields, bool test_for_hull, float* first_intersect_dist, object** first_intersect_obj)
|
|
{
|
|
bool collides = false;
|
|
|
|
for (object* objp = GET_FIRST(&obj_used_list); objp != END_OF_LIST(&obj_used_list); objp = GET_NEXT(objp)) {
|
|
if (objp->flags[Object::Object_Flags::Should_be_dead])
|
|
continue;
|
|
|
|
//Don't collision check against excluded objects
|
|
if (excluded_object_ids.contains(OBJ_INDEX(objp)))
|
|
continue;
|
|
|
|
int model_num = 0;
|
|
int model_instance_num = 0;
|
|
|
|
//Only collision check against other pieces of Debris, Asteroids or Ships
|
|
char type = objp->type;
|
|
if (type == OBJ_DEBRIS) {
|
|
model_num = Debris[objp->instance].model_num;
|
|
model_instance_num = -1;
|
|
}
|
|
else if (type == OBJ_ASTEROID) {
|
|
model_num = Asteroid_info[Asteroids[objp->instance].asteroid_type].subtypes[Asteroids[objp->instance].asteroid_subtype].model_number;
|
|
model_instance_num = Asteroids[objp->instance].model_instance_num;
|
|
}
|
|
else if (type == OBJ_SHIP) {
|
|
model_num = Ship_info[Ships[objp->instance].ship_info_index].model_num;
|
|
model_instance_num = Ships[objp->instance].model_instance_num;
|
|
}
|
|
else
|
|
continue;
|
|
|
|
//Early Out Model too small
|
|
|
|
float radius = objp->radius;
|
|
if (radius < threshold)
|
|
continue;
|
|
|
|
//Asteroth's implementation
|
|
// if objp is inside start or end then we have to check the model
|
|
if (vm_vec_dist(from, &objp->pos) > objp->radius && vm_vec_dist(to, &objp->pos) > objp->radius) {
|
|
// now check that objp is in between start and end
|
|
vec3d start2end = *to - *from;
|
|
vec3d end2start = *from - *to;
|
|
vec3d start2objp = objp->pos - *from;
|
|
vec3d end2objp = objp->pos - *to;
|
|
|
|
// if objp and end are in opposite directions from start, then early out
|
|
if (vm_vec_dot(&start2end, &start2objp) < 0.0f)
|
|
continue;
|
|
|
|
// if objp and start are in opposite directions from end, then early out
|
|
if (vm_vec_dot(&end2start, &end2objp) < 0.0f)
|
|
continue;
|
|
|
|
// finally check if objp is too close to the path
|
|
if (vm_vec_mag(&start2objp) > (vm_vec_mag(&start2end) + objp->radius))
|
|
continue; // adding objp->radius is somewhat of an overestimate but thats ok
|
|
}
|
|
|
|
mc_info hull_check;
|
|
hull_check.model_instance_num = model_instance_num;
|
|
hull_check.model_num = model_num;
|
|
hull_check.orient = &objp->orient;
|
|
hull_check.pos = &objp->pos;
|
|
hull_check.p0 = from;
|
|
hull_check.p1 = to;
|
|
|
|
if (test_for_hull) {
|
|
hull_check.flags = MC_CHECK_MODEL;
|
|
|
|
if (model_collide(&hull_check)) {
|
|
float dist = vm_vec_dist(&hull_check.hit_point_world, from);
|
|
if (first_intersect_dist == nullptr) {
|
|
return false;
|
|
}
|
|
else {
|
|
//If we need to find the first intersect distance, we need to keep searching if there might be an intersect earlier. Also, always record the first dist found
|
|
if (!collides || *first_intersect_dist > dist) {
|
|
*first_intersect_dist = dist;
|
|
if (first_intersect_obj != nullptr)
|
|
*first_intersect_obj = objp;
|
|
}
|
|
collides = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (test_for_shields) {
|
|
hull_check.flags = MC_CHECK_SHIELD;
|
|
|
|
if (model_collide(&hull_check)) {
|
|
float dist = vm_vec_dist(&hull_check.hit_point_world, from);
|
|
if (first_intersect_dist == nullptr) {
|
|
return false;
|
|
}
|
|
else {
|
|
//If we need to find the first intersect distance, we need to keep searching if there might be an intersect earlier
|
|
if (!collides || *first_intersect_dist > dist) {
|
|
*first_intersect_dist = dist;
|
|
if (first_intersect_obj != nullptr)
|
|
*first_intersect_obj = objp;
|
|
}
|
|
collides = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return !collides;
|
|
}
|