1681 lines
56 KiB
C++
1681 lines
56 KiB
C++
// Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details
|
|
// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
|
|
|
|
#include "ShipAICmd.h"
|
|
|
|
#include "Frame.h"
|
|
#include "Game.h"
|
|
#include "Pi.h"
|
|
#include "Planet.h"
|
|
#include "Ship.h"
|
|
#include "Space.h"
|
|
#include "SpaceStation.h"
|
|
#include "perlin.h"
|
|
|
|
static const double VICINITY_MIN = 15000.0;
|
|
static const double VICINITY_MUL = 4.0;
|
|
|
|
AICommand *AICommand::LoadFromJson(const Json &jsonObj)
|
|
{
|
|
// Return 0 if supplied object doesn't contain an "ai_command" object.
|
|
if (!jsonObj.count("ai_command")) return 0;
|
|
|
|
try {
|
|
Json aiCommandObj = jsonObj["ai_command"];
|
|
Json commonAiCommandObj = aiCommandObj["common_ai_command"];
|
|
CmdName name = CmdName(commonAiCommandObj["command_name"]);
|
|
switch (name) {
|
|
case CMD_NONE:
|
|
default: return 0; // No longer need CMD_NONE (see AICommand::SaveToJson notes).
|
|
case CMD_DOCK: return new AICmdDock(aiCommandObj);
|
|
case CMD_FLYTO: return new AICmdFlyTo(aiCommandObj);
|
|
case CMD_FLYAROUND: return new AICmdFlyAround(aiCommandObj);
|
|
case CMD_KILL: return new AICmdKill(aiCommandObj);
|
|
case CMD_KAMIKAZE: return new AICmdKamikaze(aiCommandObj);
|
|
case CMD_HOLDPOSITION: return new AICmdHoldPosition(aiCommandObj);
|
|
case CMD_FORMATION: return new AICmdFormation(aiCommandObj);
|
|
}
|
|
} catch (Json::type_error &) {
|
|
throw SavedGameCorruptException();
|
|
}
|
|
}
|
|
|
|
void AICommand::SaveToJson(Json &jsonObj)
|
|
{
|
|
// AICommand is an abstract base class, so it is guaranteed that the supplied object
|
|
// is the top-level JSON object (always named "ai_command") encoding the specific ai command.
|
|
// The top-level ai command object will contain:
|
|
// (1) the specific ai command data (already created and added in the overriding concrete class function), and
|
|
// (2) the common ai command object (created and added in this base class function).
|
|
// The command name (enum CmdName) and a child ai command (if this ai command has one) are added to the common ai command object.
|
|
// No longer need to save CMD_NONE when a child ai command does not exist (just don't add a child ai command object).
|
|
Space *space = Pi::game->GetSpace();
|
|
Json commonAiCommandObj({}); // Create JSON object to contain common ai command data.
|
|
commonAiCommandObj["command_name"] = m_cmdName;
|
|
commonAiCommandObj["index_for_body"] = space->GetIndexForBody(m_dBody);
|
|
commonAiCommandObj["is_flyto"] = m_is_flyto;
|
|
if (m_child) m_child->SaveToJson(commonAiCommandObj);
|
|
jsonObj["common_ai_command"] = commonAiCommandObj; // Add common ai command object to supplied object.
|
|
}
|
|
|
|
AICommand::AICommand(const Json &jsonObj, CmdName name) :
|
|
m_cmdName(name)
|
|
{
|
|
try {
|
|
Json commonAiCommandObj = jsonObj["common_ai_command"];
|
|
m_dBodyIndex = commonAiCommandObj["index_for_body"];
|
|
m_is_flyto = commonAiCommandObj["is_flyto"];
|
|
|
|
m_child.reset(LoadFromJson(commonAiCommandObj));
|
|
} catch (Json::type_error &) {
|
|
throw SavedGameCorruptException();
|
|
}
|
|
}
|
|
|
|
void AICommand::PostLoadFixup(Space *space)
|
|
{
|
|
// subsystem should be initializated on each inherited AICommand
|
|
m_dBody = static_cast<DynamicBody *>(space->GetBodyByIndex(m_dBodyIndex));
|
|
if (m_child) m_child->PostLoadFixup(space);
|
|
}
|
|
|
|
bool AICommand::ProcessChild()
|
|
{
|
|
if (!m_child) return true; // no child present
|
|
m_child->m_is_flyto = false;
|
|
if (!m_child->TimeStepUpdate()) return false; // child still active
|
|
m_child.reset();
|
|
return true; // child finished
|
|
}
|
|
|
|
/*
|
|
// temporary evasion-test version
|
|
bool AICmdKill::TimeStepUpdate()
|
|
{
|
|
m_timeSinceChange += Pi::game->GetTimeStep();
|
|
if (m_timeSinceChange < m_changeTime) {
|
|
m_ship->AIFaceDirection(m_curDir);
|
|
return false;
|
|
}
|
|
|
|
m_ship->ClearThrusterState();
|
|
|
|
// ok, so now pick new direction
|
|
vector3d targdir = m_target->GetPositionRelTo(m_ship).Normalized();
|
|
vector3d tdir1 = targdir.Cross(vector3d(targdir.z+0.1, targdir.x, targdir.y));
|
|
tdir1 = tdir1.Normalized();
|
|
vector3d tdir2 = targdir.Cross(tdir1);
|
|
|
|
double d1 = Pi::rng.Double() - 0.5;
|
|
double d2 = Pi::rng.Double() - 0.5;
|
|
|
|
m_curDir = (targdir + d1*tdir1 + d2*tdir2).Normalized();
|
|
m_ship->AIFaceDirection(m_curDir);
|
|
|
|
m_ship->SetThrusterState(ShipType::THRUSTER_FORWARD, 0.66); // give player a chance
|
|
switch(Pi::rng.Int32() & 0x3)
|
|
{
|
|
case 0x0: m_ship->SetThrusterState(ShipType::THRUSTER_LEFT, 0.7); break;
|
|
case 0x1: m_ship->SetThrusterState(ShipType::THRUSTER_RIGHT, 0.7); break;
|
|
case 0x2: m_ship->SetThrusterState(ShipType::THRUSTER_UP, 0.7); break;
|
|
case 0x3: m_ship->SetThrusterState(ShipType::THRUSTER_DOWN, 0.7); break;
|
|
}
|
|
|
|
m_timeSinceChange = 0.0f;
|
|
m_changeTime = (float)Pi::rng.Double() * 10.0f;
|
|
return false;
|
|
}
|
|
*/
|
|
|
|
// goals of this command:
|
|
// 1. inflict damage on current target
|
|
// 2. avoid taking damage to self
|
|
// two sub-patterns:
|
|
// 1. point at leaddir, shift sideways, adjust range with front/rear
|
|
// 2. flypast - change angle to target as rapidly as possible
|
|
/*
|
|
bool AICmdKill::TimeStepUpdate()
|
|
{
|
|
if (GetDockedWith()) Undock();
|
|
if (m_ship->GetFlightState() != Ship::FLYING) return false; // wait until active
|
|
|
|
SetGunState(0,0);
|
|
|
|
// do pattern timeout here
|
|
|
|
bool rval = true;
|
|
switch (m_state) {
|
|
case 0: break;
|
|
case 1: rval = PatternKill(); break;
|
|
case 2: rval = PatternShift(); break;
|
|
case 3: rval = PatternEvade(); break; // full evades should be in higher level function
|
|
}
|
|
|
|
// have the following things to pass from higher-level function:
|
|
// 1. whether to evade or counter-evade
|
|
// 2. desired separation (based on relative ship sizes + random)
|
|
|
|
// long term factors:
|
|
// if our angular accel is higher, flypast and close-range combat become more effective
|
|
m_accRatio = (m_target->GetShipType().angThrust * m_ship->GetAngularInertia())
|
|
/ (m_ship->GetShipType().angThrust * m_target->GetAngularInertia());
|
|
|
|
// if their ship is relatively large, want to use longer distances or more evasion
|
|
m_sizeRatio = m_target->GetBoundingRadius() / m_ship->GetBoundingRadius();
|
|
|
|
// if their ship has higher-speed weaponry, want to use closer distances or less evasion
|
|
|
|
// m_wpnSpeedRatio = Equip::types[m_ship->m_equipment.Get(Equip::SLOT_LASERS, 0)]
|
|
|
|
|
|
// Immediate factors:
|
|
// if their laser temperature is high, counter-evade and close range
|
|
|
|
// if our laser temperature is high, full evade and increase range
|
|
|
|
// if outmatched, run away?
|
|
|
|
// if under attack from other ships, may evade randomly
|
|
|
|
// if opponent is not visible, may enter random control mode
|
|
|
|
// if not visible to opponent and close, may attempt to stay in blind spot?
|
|
|
|
|
|
if (rval) { // current pattern complete, pick which to use next
|
|
|
|
|
|
// danger metrics: damage taken, target heading & range,
|
|
// separate danger from target and danger from elsewhere?
|
|
|
|
// *could* check
|
|
}
|
|
}
|
|
*/
|
|
|
|
// assumes approximate target facing and cleared thruster state
|
|
// curdist => current distance from target
|
|
// curspeed => current speed towards target, positive towards
|
|
// reqdist => desired distance from target
|
|
// speedmargin => don't use thrust if within this value of ideal speed
|
|
/*
|
|
double AICmdKill::MaintainDistance(double curdist, double curspeed, double reqdist, double speedmargin)
|
|
{
|
|
// use maximum *deceleration*
|
|
const ShipType &stype = m_ship->GetShipType();
|
|
double rearaccel = stype.linThrust[ShipType::THRUSTER_REVERSE] / m_ship->GetMass();
|
|
// v = sqrt(2as)
|
|
double ispeed = sqrt(2.0 * rearaccel * (curdist - reqdist));
|
|
double vdiff = ispeed - curspeed;
|
|
|
|
if (vdiff*vdiff < speedmargin*speedmargin) return 0;
|
|
if (vdiff > 0.0) return -1.0;
|
|
else return 1.0;
|
|
}
|
|
*/
|
|
|
|
static void LaunchShip(Ship *ship)
|
|
{
|
|
if (ship->GetFlightState() == Ship::LANDED)
|
|
ship->Blastoff();
|
|
else if (ship->GetFlightState() == Ship::DOCKED)
|
|
ship->Undock();
|
|
}
|
|
|
|
void AICmdKamikaze::OnDeleted(const Body *body)
|
|
{
|
|
AICommand::OnDeleted(body);
|
|
if (static_cast<Body *>(m_target) == body) m_target = 0;
|
|
}
|
|
|
|
AICmdKamikaze::AICmdKamikaze(DynamicBody *dBody, Body *target) :
|
|
AICommand(dBody, CMD_KAMIKAZE)
|
|
{
|
|
m_target = target;
|
|
m_prop.Reset(m_dBody->GetPropulsion());
|
|
assert(m_prop != nullptr);
|
|
}
|
|
|
|
AICmdKamikaze::AICmdKamikaze(const Json &jsonObj) :
|
|
AICommand(jsonObj, CMD_KAMIKAZE)
|
|
{
|
|
if (!jsonObj.count("index_for_target")) throw SavedGameCorruptException();
|
|
m_targetIndex = jsonObj["index_for_target"];
|
|
}
|
|
|
|
void AICmdKamikaze::SaveToJson(Json &jsonObj)
|
|
{
|
|
Space *space = Pi::game->GetSpace();
|
|
Json aiCommandObj({}); // Create JSON object to contain ai command data.
|
|
AICommand::SaveToJson(aiCommandObj);
|
|
aiCommandObj["index_for_target"] = space->GetIndexForBody(m_target);
|
|
jsonObj["ai_command"] = aiCommandObj; // Add ai command object to supplied object.
|
|
}
|
|
|
|
void AICmdKamikaze::PostLoadFixup(Space *space)
|
|
{
|
|
AICommand::PostLoadFixup(space);
|
|
m_target = space->GetBodyByIndex(m_targetIndex);
|
|
// Ensure needed sub-system:
|
|
m_prop.Reset(m_dBody->GetPropulsion());
|
|
assert(m_prop != nullptr);
|
|
}
|
|
|
|
bool AICmdKamikaze::TimeStepUpdate()
|
|
{
|
|
if (!m_target || m_target->IsDead()) return true;
|
|
|
|
if (m_dBody->IsType(ObjectType::SHIP)) {
|
|
// "Standard" checks for a ship...
|
|
Ship *ship = static_cast<Ship *>(m_dBody);
|
|
assert(ship != nullptr);
|
|
if (ship->GetFlightState() == Ship::JUMPING) return false;
|
|
if (ship->GetFlightState() == Ship::FLYING)
|
|
ship->SetWheelState(false);
|
|
else {
|
|
LaunchShip(ship);
|
|
return false;
|
|
}
|
|
|
|
ship->SetGunState(0, 0);
|
|
|
|
} else {
|
|
// Missile, for now ;-)
|
|
}
|
|
|
|
const vector3d targetPos = m_target->GetPositionRelTo(m_dBody);
|
|
const vector3d targetDir = targetPos.NormalizedSafe();
|
|
const double dist = targetPos.Length();
|
|
|
|
// Don't come in too fast when we're close, so we don't overshoot by
|
|
// too much if we miss the target.
|
|
|
|
// Aim to collide at a speed which would take us 4s to reverse.
|
|
const double aimCollisionSpeed = m_prop->GetAccelFwd() * 2;
|
|
|
|
// Aim to use 1/4 of our acceleration for braking while closing
|
|
// distance, leaving the rest for course adjustment.
|
|
const double brake = m_prop->GetAccelFwd() / 4;
|
|
|
|
const double aimRelSpeed =
|
|
sqrt(aimCollisionSpeed * aimCollisionSpeed + 2 * dist * brake);
|
|
|
|
const vector3d aimVel = aimRelSpeed * targetDir + m_target->GetVelocityRelTo(m_dBody->GetFrame());
|
|
const vector3d accelDir = (aimVel - m_dBody->GetVelocity()).NormalizedSafe();
|
|
|
|
m_prop->ClearLinThrusterState();
|
|
m_prop->ClearAngThrusterState();
|
|
m_prop->AIFaceDirection(accelDir);
|
|
|
|
m_prop->AIAccelToModelRelativeVelocity(aimVel * m_dBody->GetOrient());
|
|
|
|
return false;
|
|
}
|
|
|
|
void AICmdKill::OnDeleted(const Body *body)
|
|
{
|
|
if (static_cast<Body *>(m_target) == body) m_target = 0;
|
|
AICommand::OnDeleted(body);
|
|
}
|
|
|
|
AICmdKill::AICmdKill(DynamicBody *dBody, Ship *target) :
|
|
AICommand(dBody, CMD_KILL)
|
|
{
|
|
m_target = target;
|
|
m_leadTime = m_evadeTime = m_closeTime = 0.0;
|
|
m_lastVel = m_target->GetVelocity();
|
|
m_prop.Reset(m_dBody->GetPropulsion());
|
|
m_fguns.Reset(m_dBody->GetFixedGuns());
|
|
assert(m_prop != nullptr);
|
|
assert(m_fguns != nullptr);
|
|
}
|
|
|
|
AICmdKill::AICmdKill(const Json &jsonObj) :
|
|
AICommand(jsonObj, CMD_KILL)
|
|
{
|
|
m_targetIndex = jsonObj["index_for_target"];
|
|
}
|
|
|
|
void AICmdKill::SaveToJson(Json &jsonObj)
|
|
{
|
|
Space *space = Pi::game->GetSpace();
|
|
Json aiCommandObj({}); // Create JSON object to contain ai command data.
|
|
AICommand::SaveToJson(aiCommandObj);
|
|
aiCommandObj["index_for_target"] = space->GetIndexForBody(m_target);
|
|
jsonObj["ai_command"] = aiCommandObj; // Add ai command object to supplied object.
|
|
}
|
|
|
|
AICmdKill::~AICmdKill()
|
|
{
|
|
if (m_fguns) m_fguns->SetGunFiringState(0, 0);
|
|
}
|
|
|
|
void AICmdKill::PostLoadFixup(Space *space)
|
|
{
|
|
AICommand::PostLoadFixup(space);
|
|
m_target = static_cast<Ship *>(space->GetBodyByIndex(m_targetIndex));
|
|
m_leadTime = m_evadeTime = m_closeTime = 0.0;
|
|
m_lastVel = m_target->GetVelocity();
|
|
// Ensure needed sub-system:
|
|
m_prop.Reset(m_dBody->GetPropulsion());
|
|
m_fguns.Reset(m_dBody->GetFixedGuns());
|
|
assert(m_prop != nullptr);
|
|
assert(m_fguns != nullptr);
|
|
}
|
|
|
|
bool AICmdKill::TimeStepUpdate()
|
|
{
|
|
if (m_dBody->IsType(ObjectType::SHIP)) {
|
|
Ship *ship = static_cast<Ship *>(m_dBody);
|
|
assert(ship != nullptr);
|
|
if (ship->GetFlightState() == Ship::JUMPING) return false;
|
|
if (ship->GetFlightState() == Ship::FLYING)
|
|
ship->SetWheelState(false);
|
|
else {
|
|
LaunchShip(ship);
|
|
return false;
|
|
}
|
|
} else {
|
|
// Maybe be a drone ;-)
|
|
return false;
|
|
}
|
|
|
|
if (!ProcessChild()) return false;
|
|
if (!m_target || m_target->IsDead()) return true;
|
|
|
|
const matrix3x3d &rot = m_dBody->GetOrient();
|
|
vector3d targpos = m_target->GetPositionRelTo(m_dBody);
|
|
vector3d targvel = m_target->GetVelocityRelTo(m_dBody);
|
|
vector3d targdir = targpos.NormalizedSafe();
|
|
vector3d heading = -rot.VectorZ();
|
|
// Accel will be wrong for a frame on timestep changes, but it doesn't matter
|
|
vector3d targaccel = (m_target->GetVelocity() - m_lastVel) / Pi::game->GetTimeStep();
|
|
m_lastVel = m_target->GetVelocity(); // may need next frame
|
|
vector3d leaddir = m_prop->AIGetLeadDir(m_target, targaccel, m_fguns->GetProjSpeed(0));
|
|
|
|
if (targpos.Length() >= VICINITY_MIN + 1000.0) { // if really far from target, intercept
|
|
// Output("%s started AUTOPILOT\n", m_ship->GetLabel().c_str());
|
|
m_child.reset(new AICmdFlyTo(m_dBody, m_target));
|
|
ProcessChild();
|
|
return false;
|
|
}
|
|
|
|
// turn towards target lead direction, add inaccuracy
|
|
// trigger recheck when angular velocity reaches zero or after certain time
|
|
|
|
if (m_leadTime < Pi::game->GetTime()) {
|
|
double skillShoot = 0.5; // todo: should come from AI stats
|
|
|
|
double headdiff = (leaddir - heading).Length();
|
|
double leaddiff = (leaddir - targdir).Length();
|
|
m_leadTime = Pi::game->GetTime() + headdiff + (1.0 * Pi::rng.Double() * skillShoot);
|
|
|
|
// lead inaccuracy based on diff between heading and leaddir
|
|
vector3d r(Pi::rng.Double() - 0.5, Pi::rng.Double() - 0.5, Pi::rng.Double() - 0.5);
|
|
vector3d newoffset = r * (0.02 + 2.0 * leaddiff + 2.0 * headdiff) * Pi::rng.Double() * skillShoot;
|
|
m_leadOffset = (heading - leaddir); // should be already...
|
|
m_leadDrift = (newoffset - m_leadOffset) / (m_leadTime - Pi::game->GetTime());
|
|
|
|
// Shoot only when close to target
|
|
|
|
double vissize = 1.3 * m_dBody->GetPhysRadius() / targpos.Length();
|
|
vissize += (0.05 + 0.5 * leaddiff) * Pi::rng.Double() * skillShoot;
|
|
if (vissize > headdiff)
|
|
m_fguns->SetGunFiringState(0, 1);
|
|
else
|
|
m_fguns->SetGunFiringState(0, 0);
|
|
float max_fire_dist = m_fguns->GetGunRange(0);
|
|
if (max_fire_dist > 4000) max_fire_dist = 4000;
|
|
max_fire_dist *= max_fire_dist;
|
|
if (targpos.LengthSqr() > max_fire_dist) m_fguns->SetGunFiringState(0, 0); // temp
|
|
}
|
|
m_leadOffset += m_leadDrift * Pi::game->GetTimeStep();
|
|
double leadAV = (leaddir - targdir).Dot((leaddir - heading).NormalizedSafe()); // leaddir angvel
|
|
m_prop->AIFaceDirection((leaddir + m_leadOffset).Normalized(), leadAV);
|
|
|
|
vector3d evadethrust(0, 0, 0);
|
|
if (m_evadeTime < Pi::game->GetTime()) // evasion time!
|
|
{
|
|
double skillEvade = 0.5; // todo: should come from AI stats
|
|
m_evadeTime = Pi::game->GetTime() + Pi::rng.Double(3.0, 10.0) * skillEvade;
|
|
if (heading.Dot(targdir) < 0.7) skillEvade += 0.5; // not in view
|
|
skillEvade += Pi::rng.Double(-0.5, 0.5);
|
|
|
|
vector3d targhead = -m_target->GetOrient().VectorZ() * rot; // obj space
|
|
vector3d targav = m_target->GetAngVelocity();
|
|
|
|
if (skillEvade < 1.6 && targhead.z < 0.0) { // smart chase
|
|
vector3d objvel = targvel * rot; // obj space targvel
|
|
if ((objvel.x * objvel.x + objvel.y * objvel.y) < 10000) {
|
|
evadethrust.x = objvel.x > 0.0 ? 1.0 : -1.0;
|
|
evadethrust.y = objvel.y > 0.0 ? 1.0 : -1.0;
|
|
}
|
|
} else {
|
|
skillEvade += targpos.Length() / 2000; // 0.25 per 500m
|
|
|
|
if (skillEvade < 1.0 && targav.Length() < 0.05) { // smart evade, assumes facing
|
|
evadethrust.x = targhead.x < 0.0 ? 1.0 : -1.0;
|
|
evadethrust.y = targhead.y < 0.0 ? 1.0 : -1.0;
|
|
} else if (skillEvade < 1.3) { // random two-thruster evade
|
|
evadethrust.x = (Pi::rng.Int32() & 8) ? 1.0 : -1.0;
|
|
evadethrust.y = (Pi::rng.Int32() & 4) ? 1.0 : -1.0;
|
|
} else if (skillEvade < 1.6) { // one thruster only
|
|
if (Pi::rng.Int32() & 8)
|
|
evadethrust.x = (Pi::rng.Int32() & 4) ? 1.0 : -1.0;
|
|
else
|
|
evadethrust.y = (Pi::rng.Int32() & 4) ? 1.0 : -1.0;
|
|
}
|
|
// else no evade thrust
|
|
}
|
|
} else
|
|
evadethrust = m_prop->GetLinThrusterState();
|
|
|
|
// todo: some logic behind desired range? pass from higher level
|
|
if (m_closeTime < Pi::game->GetTime()) {
|
|
double skillEvade = 0.5;
|
|
if (heading.Dot(targdir) < 0.7) skillEvade += 0.5; // not in view
|
|
|
|
m_closeTime = Pi::game->GetTime() + skillEvade * Pi::rng.Double(1.0, 5.0);
|
|
|
|
double reqdist = 500.0 + skillEvade * Pi::rng.Double(-500.0, 250);
|
|
double dist = targpos.Length(), ispeed;
|
|
double rearaccel = m_prop->GetAccelRev();
|
|
rearaccel += targaccel.Dot(targdir);
|
|
// v = sqrt(2as), positive => towards
|
|
double as2 = 2.0 * rearaccel * (dist - reqdist);
|
|
if (as2 > 0)
|
|
ispeed = sqrt(as2);
|
|
else
|
|
ispeed = -sqrt(-as2);
|
|
double vdiff = ispeed + targvel.Dot(targdir);
|
|
|
|
if (skillEvade + Pi::rng.Double() > 1.5)
|
|
evadethrust.z = 0.0;
|
|
else if (vdiff * vdiff < 400.0)
|
|
evadethrust.z = 0.0;
|
|
else
|
|
evadethrust.z = (vdiff > 0.0) ? -1.0 : 1.0;
|
|
} else
|
|
evadethrust.z = m_prop->GetLinThrusterState().z;
|
|
m_prop->SetLinThrusterState(evadethrust);
|
|
|
|
return false;
|
|
}
|
|
|
|
//Four modes for evasion vector...
|
|
// 1. target is main threat - consider heading and sweep
|
|
// 2. other ship is main threat - consider laser bolt position
|
|
// 3. no real threat - just reduce velocity vector
|
|
// 4. random
|
|
|
|
// ok, can't really decide what's best.
|
|
// best: evade from heading if low velocity, otherwise evade in direction of angvel
|
|
|
|
// first need to consider whether danger is sufficiently high to prioritise evasion
|
|
// back to the threat metrics thing
|
|
|
|
// ok, threat of target
|
|
// ideally just watch nearby laser bolts
|
|
// take account of:
|
|
// 1. range (both for hit chance and output)
|
|
// 2. laser output (consider recharge) vs armour
|
|
// 3. proximity and speed of lead angle
|
|
|
|
// double range = targpos.Length(), rthreat;
|
|
// if(range < 200.0) rthreat = 1.0;
|
|
// else if(range > maxlrange) rthreat = 0.0;
|
|
// else rthreat = (maxlrange-range) / (maxlrange-200.0);
|
|
// rthreat *= rthreat; // not enough maybe. consider aim, power and evasion time
|
|
|
|
// hmm. could consider heading strictly, like watching laser bolts.
|
|
|
|
// vector3d targld = m_target->AIGetLeadDir(m_ship, vector3d(0,0,0), 0);
|
|
// (-targpos).Normalized().Dot(targld);
|
|
// compare against target's actual heading and this ship's current velocity
|
|
// pure velocity or angular
|
|
|
|
// ok, what were the actual questions here?
|
|
// 1. whether to kill, flypast or hard evade
|
|
// - hard evade is useless except as flypast, delaying tactic, or specific laser bolt dodge
|
|
// - have dafter AIs do it anyway?
|
|
|
|
// kill if winning, basically?
|
|
// against ships with slower turn rate, might want to try to exploit that
|
|
// So what actually matters?
|
|
|
|
// 1. closer range, closing velocity => worth doing a flypast
|
|
|
|
// need fuzzy range-maintenance
|
|
// every time period, hit forward or reverse thruster or neither
|
|
|
|
// actually just use real one except only occasionally and with randomised distances
|
|
//
|
|
|
|
/*
|
|
bool AICmdKill::TimeStepUpdate()
|
|
{
|
|
// do everything in object space
|
|
matrix4x4d rot; GetRotMatrix(rot);
|
|
vector3d targpos = inst.target->GetPositionRelTo(this) * rot;
|
|
vector3d targvel = (inst.lastVel - inst.target->GetVelocity()) * inst.timeStep;
|
|
targvel = (targvel + inst.target->GetVelocityRelativeTo(this)) * rot;
|
|
// TODO: should adjust targpos for gunmount offset
|
|
|
|
// store current target velocity for next frame's accel calc
|
|
inst.lastVel = inst.target->GetVelocity();
|
|
inst.timeStep = timeStep;
|
|
|
|
int laser = Equip::types[m_equipment.Get(Equip::SLOT_LASER, 0)].tableIndex;
|
|
double projspeed = Equip::lasers[laser].speed;
|
|
|
|
vector3d leadpos = targpos + targvel*(targpos.Length()/projspeed);
|
|
leadpos = targpos + targvel*(leadpos.Length()/projspeed); // second order approx
|
|
vector3d leaddir = leadpos.Normalized();
|
|
|
|
AIFaceDirection(rot * leaddir, timeStep);
|
|
|
|
// ok, now work out evasion and range adjustment
|
|
// just generate preferred evasion and range vectors and span accordingly?
|
|
// never mind that, just consider each thruster axis individually?
|
|
|
|
// get representation of approximate angular distance
|
|
// dot product of direction and enemy heading?
|
|
// ideally use enemy angvel arc too - try to stay out of arc and away from heading
|
|
|
|
// so, need three vectors in object space
|
|
// 1. enemy position - targpos
|
|
// 2. enemy heading - take from their rot matrix, transform to obj space
|
|
// 2.5. enemy up vector, not using yet
|
|
// 3. enemy angvel - transform to obj space
|
|
|
|
matrix4x4d erot;
|
|
inst.target->GetRotMatrix(erot);
|
|
vector3d ehead = vector3d(-erot[8], -erot[9], -erot[10]) * rot;
|
|
// vector3d eup = vector3d(erot[4], erot[5], erot[6]) * rot;
|
|
vector3d eav = ((Ship *)inst.target)->GetAngVelocity() * rot;
|
|
|
|
// stupid evade: away from heading
|
|
vector3d evade1, evade2;
|
|
evade1 = (ehead * targpos.Dot(ehead)) - targpos;
|
|
|
|
// smarter evade? away from angular velocity plane
|
|
if (eav.Length() > 0.0) {
|
|
evade2 = eav.Normalized();
|
|
if (targpos.Dot(eav * targpos.Dot(eav)) > 0.0) evade2 *= -1.0;
|
|
}
|
|
else evade2 = evade1;
|
|
|
|
// only do this if on target
|
|
if (leaddir.z < -0.98)
|
|
{
|
|
if (evade1.x > 0.0) m_ship->SetThrusterState(ShipType::THRUSTER_RIGHT, 1.0);
|
|
else m_ship->SetThrusterState(ShipType::THRUSTER_LEFT, 1.0);
|
|
if (evade1.y > 0.0) m_ship->SetThrusterState(ShipType::THRUSTER_UP, 1.0);
|
|
else m_ship->SetThrusterState(ShipType::THRUSTER_DOWN, 1.0);
|
|
|
|
// basic range-maintenance?
|
|
|
|
double relspeed = -targvel.Dot(targpos.Normalized()); // positive => closing
|
|
// use maximum *deceleration*
|
|
const ShipType &stype = GetShipType();
|
|
double rearaccel = stype.linThrust[ShipType::THRUSTER_REVERSE] / GetMass();
|
|
double fwdaccel = stype.linThrust[ShipType::THRUSTER_FORWARD] / GetMass();
|
|
// v = sqrt(2as)
|
|
double idist = 500.0; // temporary
|
|
double ivel = sqrt(2.0 * rearaccel * (targpos.Length() - idist));
|
|
double vdiff = ivel - relspeed;
|
|
|
|
if (vdiff > 0.0) m_ship->SetThrusterState(ShipType::THRUSTER_FORWARD, 1.0);
|
|
else m_ship->SetThrusterState(ShipType::THRUSTER_REVERSE, 1.0);
|
|
|
|
SetGunState(0,1);
|
|
}
|
|
|
|
// Possibly don't need this because angvel never reaches zero on moving target
|
|
// and approximate target angular velocity at leaddir
|
|
// vector3d leaddir2 = (leadpos + targvel*0.01).Normalized();
|
|
// vector3d leadav = leaddir.Cross(leaddir2) * 100.0;
|
|
// does this really give a genuine angvel? Probably
|
|
|
|
// so have target heading and target angvel at that heading
|
|
// can now use modified version of FaceDirection?
|
|
// not really: direction of leaddir and leadangvel may be different
|
|
// so blend two results: thrust to reach leaddir and thrust to attain leadangvel
|
|
// bias towards leadangvel as heading approaches leaddir
|
|
|
|
return false;
|
|
}
|
|
*/
|
|
|
|
static double MaxFeatureRad(Body *body)
|
|
{
|
|
if (!body)
|
|
return 0.0;
|
|
else
|
|
return body->GetPhysRadius();
|
|
}
|
|
|
|
double MaxEffectRad(const Body *body, Propulsion *prop)
|
|
{
|
|
if (!body) return 0.0;
|
|
if (!body->IsType(ObjectType::TERRAINBODY)) {
|
|
if (!body->IsType(ObjectType::SPACESTATION)) return body->GetPhysRadius() + 1000.0;
|
|
return static_cast<const SpaceStation *>(body)->GetStationType()->ParkingDistance() + 1000.0;
|
|
}
|
|
return std::max(body->GetPhysRadius(), sqrt(G * body->GetMass() / prop->GetAccelUp()));
|
|
}
|
|
|
|
// returns acceleration due to gravity at that point
|
|
static double GetGravityAtPos(FrameId targframeId, const vector3d &posoff)
|
|
{
|
|
Frame *targframe = Frame::GetFrame(targframeId);
|
|
Body *body = targframe->GetBody();
|
|
if (!body || body->IsType(ObjectType::SPACESTATION)) return 0;
|
|
double rsqr = posoff.LengthSqr();
|
|
return G * body->GetMass() / rsqr;
|
|
// inverse is: sqrt(G * m1m2 / thrust)
|
|
}
|
|
|
|
// gets position of (target + offset in target's frame) in frame
|
|
static vector3d GetPosInFrame(FrameId frameId, FrameId targetId, const vector3d &offset)
|
|
{
|
|
Frame *target = Frame::GetFrame(targetId);
|
|
return target->GetOrientRelTo(frameId) * offset + target->GetPositionRelTo(frameId);
|
|
}
|
|
|
|
static vector3d GetVelInFrame(FrameId frameId, FrameId targetId, const vector3d &offset)
|
|
{
|
|
vector3d vel = vector3d(0.0);
|
|
Frame *target = Frame::GetFrame(targetId);
|
|
if (targetId != frameId && target->IsRotFrame()) {
|
|
// double ang = Pi::game->GetTimeStep() * target->GetAngSpeed();
|
|
// vector3d newpos = offset * matrix3x3d::RotateYMatrix(ang);
|
|
// vel = (newpos - offset) / Pi::game->GetTimeStep();
|
|
vel = -target->GetStasisVelocity(offset); // stasis velocity not accurate enough
|
|
}
|
|
return target->GetOrientRelTo(frameId) * vel + target->GetVelocityRelTo(frameId);
|
|
}
|
|
|
|
// generates from (0,0,0) to spos, in plane of target
|
|
// formula uses similar triangles
|
|
// shiptarg in ship's frame
|
|
// output in targframe
|
|
static vector3d GenerateTangent(DynamicBody *dBody, FrameId targframeId, const vector3d &shiptarg, double alt)
|
|
{
|
|
vector3d spos = dBody->GetPositionRelTo(targframeId);
|
|
vector3d targ = GetPosInFrame(targframeId, dBody->GetFrame(), shiptarg);
|
|
double a = spos.Length(), b = alt;
|
|
if (b * 1.02 > a) {
|
|
spos *= b * 1.02 / a;
|
|
a = b * 1.02;
|
|
} // fudge if ship gets under radius
|
|
double c = sqrt(a * a - b * b);
|
|
return (spos * b * b) / (a * a) + spos.Cross(targ).Cross(spos).Normalized() * b * c / a;
|
|
}
|
|
|
|
// check whether ship is at risk of colliding with frame body on current path
|
|
// return values:
|
|
//0 - no collision
|
|
//1 - below feature height
|
|
//2 - unsafe escape from effect radius
|
|
//3 - unsafe entry to effect radius
|
|
//4 - probable path intercept
|
|
int CheckCollision(DynamicBody *dBody, const vector3d &pathdir, double pathdist, const vector3d &tpos, double endvel, double r)
|
|
{
|
|
if (!dBody->Have(DynamicBody::PROPULSION)) return 0;
|
|
Propulsion *prop = dBody->GetPropulsion();
|
|
assert(prop != nullptr);
|
|
// ship is in obstructor's frame anyway, so is tpos
|
|
if (pathdist < 100.0) return 0;
|
|
Body *body = Frame::GetFrame(dBody->GetFrame())->GetBody();
|
|
if (!body) return 0;
|
|
vector3d spos = dBody->GetPosition();
|
|
double tlen = tpos.Length(), slen = spos.Length();
|
|
double fr = MaxFeatureRad(body);
|
|
|
|
// if target inside, check if direct entry is safe (30 degree)
|
|
if (tlen < r) {
|
|
double af = (tlen > fr) ? 0.5 * (1 - (tlen - fr) / (r - fr)) : 0.5;
|
|
if (pathdir.Dot(tpos) > -af * tlen)
|
|
if (slen < fr)
|
|
return 1;
|
|
else
|
|
return 3;
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
// if ship inside, check for max feature height and direct escape (30 degree)
|
|
if (slen < r) {
|
|
if (slen < fr) return 1;
|
|
double af = (slen > fr) ? 0.5 * (1 - (slen - fr) / (r - fr)) : 0.5;
|
|
if (pathdir.Dot(spos) < af * slen)
|
|
return 2;
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
// now for the intercept calc
|
|
// find closest point to obstructor
|
|
double tanlen = -spos.Dot(pathdir);
|
|
if (tanlen < 0 || tanlen > pathdist) return 0; // closest point outside path
|
|
|
|
vector3d perpdir = (tanlen * pathdir + spos).Normalized();
|
|
double perpspeed = dBody->GetVelocity().Dot(perpdir);
|
|
double parspeed = dBody->GetVelocity().Dot(pathdir);
|
|
if (parspeed < 0) parspeed = 0; // shouldn't break any important case
|
|
if (perpspeed > 0) perpspeed = 0; // prevent attempts to speculatively fly through planets
|
|
|
|
// find time that dBody will pass through that point
|
|
// get velocity as if accelerating from start or end, pick smallest
|
|
double ivelsqr = endvel * endvel + 2 * prop->GetAccelFwd() * (pathdist - tanlen); // could put endvel in here
|
|
double fvelsqr = parspeed * parspeed + 2 * prop->GetAccelFwd() * tanlen;
|
|
double tanspeed = sqrt(ivelsqr < fvelsqr ? ivelsqr : fvelsqr);
|
|
double time = tanlen / (0.5 * (parspeed + tanspeed)); // actually correct?
|
|
|
|
double dist = spos.Dot(perpdir) + perpspeed * time; // spos.perpdir should be positive
|
|
if (dist < r) return 4;
|
|
return 0;
|
|
}
|
|
|
|
// ok, need thing to step down through bodies and find closest approach
|
|
// modify targpos directly to aim short of dangerous bodies
|
|
static bool ParentSafetyAdjust(DynamicBody *dBody, FrameId targframeId, vector3d &targpos, vector3d &targvel)
|
|
{
|
|
Body *body = nullptr;
|
|
FrameId frameId = Frame::GetFrame(targframeId)->GetNonRotFrame();
|
|
Frame *frame = Frame::GetFrame(frameId);
|
|
while (frame) {
|
|
Frame *bFrame = Frame::GetFrame(dBody->GetFrame());
|
|
if (bFrame->GetNonRotFrame() == frameId) break; // ship in frame, stop
|
|
if (frame->GetBody()) body = frame->GetBody(); // ignore grav points?
|
|
|
|
double sdist = dBody->GetPositionRelTo(frameId).Length();
|
|
if (sdist < frame->GetRadius()) break; // ship inside frame, stop
|
|
|
|
// we should always be inside the root frame, so if we're not inside 'frame'
|
|
// then it must not be the root frame (ie, it must have a parent)
|
|
Frame *parent = Frame::GetFrame(frame->GetParent());
|
|
assert(parent);
|
|
|
|
frameId = parent->GetNonRotFrame();
|
|
frame = Frame::GetFrame(frameId); // check next frame down
|
|
}
|
|
if (!body) return false;
|
|
|
|
// aim for zero velocity at surface of that body
|
|
// still along path to target
|
|
Propulsion *prop = dBody->GetPropulsion();
|
|
if (prop == nullptr) return false;
|
|
vector3d targpos2 = targpos - dBody->GetPosition();
|
|
double targdist = targpos2.Length();
|
|
double bodydist = body->GetPositionRelTo(dBody).Length() - MaxEffectRad(body, prop) * 1.5;
|
|
if (targdist < bodydist) return false;
|
|
targpos -= (targdist - bodydist) * targpos2 / targdist;
|
|
targvel = body->GetVelocityRelTo(dBody->GetFrame());
|
|
return true;
|
|
}
|
|
|
|
// check for collision course with frame body
|
|
// tandir is normal vector from planet to target pos or dir
|
|
static bool CheckSuicide(DynamicBody *dBody, const vector3d &tandir)
|
|
{
|
|
Body *body = Frame::GetFrame(dBody->GetFrame())->GetBody();
|
|
if (dBody->Have(DynamicBody::PROPULSION)) return false;
|
|
Propulsion *prop = dBody->GetPropulsion();
|
|
assert(prop != nullptr);
|
|
if (!body || !body->IsType(ObjectType::TERRAINBODY)) return false;
|
|
|
|
double vel = dBody->GetVelocity().Dot(tandir); // vel towards is negative
|
|
double dist = dBody->GetPosition().Length() - MaxFeatureRad(body);
|
|
if (vel < -1.0 && vel * vel > 2.0 * prop->GetAccelMin() * dist)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
extern double calc_ivel(double dist, double vel, double acc);
|
|
|
|
void AICmdFlyTo::OnDeleted(const Body *body)
|
|
{
|
|
AICommand::OnDeleted(body);
|
|
if (m_target == body) m_target = 0;
|
|
}
|
|
|
|
void AICmdFlyTo::GetStatusText(char *str)
|
|
{
|
|
if (m_child)
|
|
m_child->GetStatusText(str);
|
|
else if (m_target)
|
|
snprintf(str, 255, "Intercept: %s, dist %.1fkm, state %i",
|
|
m_target->GetLabel().c_str(), m_dist, m_state);
|
|
else
|
|
snprintf(str, 255, "FlyTo: %s, dist %.1fkm, endvel %.1fkm/s, state %i",
|
|
Frame::GetFrame(m_targframeId)->GetLabel().c_str(), m_posoff.Length() / 1000.0, m_endvel / 1000.0, m_state);
|
|
}
|
|
|
|
void AICmdFlyTo::PostLoadFixup(Space *space)
|
|
{
|
|
AICommand::PostLoadFixup(space);
|
|
m_target = space->GetBodyByIndex(m_targetIndex);
|
|
m_lockhead = true;
|
|
m_frameId = m_target ? m_target->GetFrame() : FrameId();
|
|
// Ensure needed sub-system:
|
|
m_prop.Reset(m_dBody->GetPropulsion());
|
|
assert(m_prop != nullptr);
|
|
}
|
|
|
|
// Fly to vicinity of body
|
|
AICmdFlyTo::AICmdFlyTo(DynamicBody *dBody, Body *target) :
|
|
AICommand(dBody, CMD_FLYTO)
|
|
{
|
|
m_prop.Reset(dBody->GetPropulsion());
|
|
assert(m_prop != nullptr);
|
|
m_frameId = FrameId::Invalid;
|
|
m_state = -6;
|
|
m_lockhead = true;
|
|
m_endvel = 0;
|
|
m_tangent = false;
|
|
m_is_flyto = true;
|
|
if (!target->IsType(ObjectType::TERRAINBODY))
|
|
m_dist = VICINITY_MIN;
|
|
else
|
|
m_dist = VICINITY_MUL * MaxEffectRad(target, m_prop.Get());
|
|
|
|
if (target->IsType(ObjectType::SPACESTATION) && static_cast<SpaceStation *>(target)->IsGroundStation()) {
|
|
m_posoff = target->GetPosition() + VICINITY_MIN * target->GetOrient().VectorY();
|
|
// m_posoff += 500.0 * target->GetOrient().VectorX();
|
|
m_targframeId = target->GetFrame();
|
|
m_target = nullptr;
|
|
} else {
|
|
m_target = target;
|
|
m_targframeId = FrameId::Invalid;
|
|
}
|
|
|
|
if (dBody->GetPositionRelTo(target).Length() <= VICINITY_MIN) m_targframeId = FrameId::Invalid;
|
|
}
|
|
|
|
// Specified pos, endvel should be > 0
|
|
AICmdFlyTo::AICmdFlyTo(DynamicBody *dBody, FrameId targframe, const vector3d &posoff, double endvel, bool tangent) :
|
|
AICommand(dBody, CMD_FLYTO),
|
|
m_target(nullptr),
|
|
m_targframeId(targframe),
|
|
m_posoff(posoff),
|
|
m_endvel(endvel),
|
|
m_tangent(tangent),
|
|
m_state(-6),
|
|
m_lockhead(true),
|
|
m_frameId(FrameId::Invalid)
|
|
{
|
|
m_prop.Reset(dBody->GetPropulsion());
|
|
assert(m_prop != nullptr);
|
|
}
|
|
|
|
AICmdFlyTo::AICmdFlyTo(const Json &jsonObj) :
|
|
AICommand(jsonObj, CMD_FLYTO)
|
|
{
|
|
try {
|
|
m_targetIndex = jsonObj["index_for_target"];
|
|
m_dist = jsonObj["dist"];
|
|
m_targframeId = jsonObj["target_frame"];
|
|
m_posoff = jsonObj["pos_off"];
|
|
m_endvel = jsonObj["end_vel"];
|
|
m_tangent = jsonObj["tangent"];
|
|
m_state = jsonObj["state"];
|
|
} catch (Json::type_error &) {
|
|
throw SavedGameCorruptException();
|
|
}
|
|
}
|
|
|
|
void AICmdFlyTo::SaveToJson(Json &jsonObj)
|
|
{
|
|
if (m_child) {
|
|
m_child.reset();
|
|
}
|
|
Json aiCommandObj({}); // Create JSON object to contain ai command data.
|
|
AICommand::SaveToJson(aiCommandObj);
|
|
aiCommandObj["index_for_target"] = Pi::game->GetSpace()->GetIndexForBody(m_target);
|
|
aiCommandObj["dist"] = m_dist;
|
|
aiCommandObj["target_frame"] = m_targframeId;
|
|
aiCommandObj["pos_off"] = m_posoff;
|
|
aiCommandObj["end_vel"] = m_endvel;
|
|
aiCommandObj["tangent"] = m_tangent;
|
|
aiCommandObj["state"] = m_state;
|
|
jsonObj["ai_command"] = aiCommandObj; // Add ai command object to supplied object.
|
|
}
|
|
|
|
bool AICmdFlyTo::TimeStepUpdate()
|
|
{
|
|
/* TODO: ship is used ONLY to calls
|
|
* wheels, launch and flightstate, so
|
|
* it is better to split them in a module
|
|
*/
|
|
if (m_dBody->IsType(ObjectType::SHIP)) {
|
|
Ship *ship = static_cast<Ship *>(m_dBody);
|
|
assert(ship != nullptr);
|
|
if (ship->GetFlightState() == Ship::JUMPING) return false;
|
|
// sort out gear, launching
|
|
if (ship->GetFlightState() == Ship::FLYING)
|
|
ship->SetWheelState(false);
|
|
else {
|
|
LaunchShip(ship);
|
|
return false;
|
|
}
|
|
} else {
|
|
// may be an exploration probe ;-)
|
|
return false;
|
|
}
|
|
if (!m_target && !m_targframeId.valid()) return true; // deleted object
|
|
|
|
// generate base target pos (with vicinity adjustment) & vel
|
|
double timestep = Pi::game->GetTimeStep();
|
|
vector3d targpos, targvel;
|
|
if (m_target) {
|
|
targpos = m_target->GetPositionRelTo(m_dBody->GetFrame());
|
|
targpos -= (targpos - m_dBody->GetPosition()).NormalizedSafe() * m_dist;
|
|
targvel = m_target->GetVelocityRelTo(m_dBody->GetFrame());
|
|
} else {
|
|
targpos = GetPosInFrame(m_dBody->GetFrame(), m_targframeId, m_posoff);
|
|
targvel = GetVelInFrame(m_dBody->GetFrame(), m_targframeId, m_posoff);
|
|
}
|
|
FrameId targframeId = m_target ? m_target->GetFrame() : m_targframeId;
|
|
ParentSafetyAdjust(m_dBody, targframeId, targpos, targvel);
|
|
vector3d relpos = targpos - m_dBody->GetPosition();
|
|
vector3d reldir = relpos.NormalizedSafe();
|
|
vector3d relvel = targvel - m_dBody->GetVelocity();
|
|
double targdist = relpos.Length();
|
|
|
|
#ifdef DEBUG_AUTOPILOT
|
|
if (m_ship->IsType(ObjectType::PLAYER))
|
|
Output("Autopilot dist = %.1f, speed = %.1f, zthrust = %.2f, state = %i\n",
|
|
targdist, relvel.Length(), m_ship->GetLinThrusterState().z, m_state);
|
|
#endif
|
|
|
|
// frame switch stuff - clear children/collision state
|
|
if (m_frameId != m_dBody->GetFrame()) {
|
|
if (m_child) {
|
|
m_child.reset();
|
|
}
|
|
if (m_tangent && m_frameId.valid()) return true; // regen tangent on frame switch
|
|
m_reldir = reldir; // for +vel termination condition
|
|
m_frameId = m_dBody->GetFrame();
|
|
}
|
|
|
|
// TODO: collision needs to be processed according to vdiff, not reldir?
|
|
|
|
Body *body = Frame::GetFrame(m_frameId)->GetBody();
|
|
double erad = MaxEffectRad(body, m_prop.Get());
|
|
Frame *targframe = Frame::GetFrame(targframeId);
|
|
if ((m_target && body != m_target) || (targframe && (!m_tangent || body != targframe->GetBody()))) {
|
|
int coll = CheckCollision(m_dBody, reldir, targdist, targpos, m_endvel, erad);
|
|
if (coll == 0) { // no collision
|
|
if (m_child) {
|
|
m_child.reset();
|
|
}
|
|
} else if (coll == 1) { // below feature height, target not below
|
|
double ang = m_prop->AIFaceDirection(m_dBody->GetPosition());
|
|
m_prop->AIMatchVel(ang < 0.05 ? 1000.0 * m_dBody->GetPosition().Normalized() : vector3d(0.0));
|
|
} else { // same thing for 2/3/4
|
|
if (!m_child) m_child.reset(new AICmdFlyAround(m_dBody, Frame::GetFrame(m_frameId)->GetBody(), erad * 1.05, 0.0));
|
|
static_cast<AICmdFlyAround *>(m_child.get())->SetTargPos(targpos);
|
|
ProcessChild();
|
|
}
|
|
if (coll) {
|
|
m_state = -coll;
|
|
return false;
|
|
}
|
|
}
|
|
if (m_state < 0 && m_state > -6 && m_tangent) return true; // bail out
|
|
if (m_state < 0) m_state = targdist > 10000000.0 ? 1 : 0; // still lame
|
|
|
|
double maxdecel = m_state ? m_prop->GetAccelFwd() : m_prop->GetAccelRev();
|
|
double gravdir = -reldir.Dot(m_dBody->GetPosition().Normalized());
|
|
maxdecel -= gravdir * GetGravityAtPos(m_dBody->GetFrame(), m_dBody->GetPosition());
|
|
bool bZeroDecel = false;
|
|
if (maxdecel < 0) {
|
|
maxdecel = 0.0;
|
|
bZeroDecel = true;
|
|
}
|
|
|
|
// target ship acceleration adjustment
|
|
if (m_target && m_target->IsType(ObjectType::SHIP)) {
|
|
Ship *targship = static_cast<Ship *>(m_target);
|
|
matrix3x3d orient = Frame::GetFrame(m_target->GetFrame())->GetOrientRelTo(m_frameId);
|
|
vector3d targaccel = orient * targship->GetLastForce() / m_target->GetMass();
|
|
// fudge: targets accelerating towards you are usually going to flip
|
|
if (targaccel.Dot(reldir) < 0.0 && !targship->IsDecelerating()) targaccel *= 0.5;
|
|
relvel += targaccel * timestep;
|
|
maxdecel += targaccel.Dot(reldir);
|
|
// if we have margin lower than 10%, fly as if 10% anyway
|
|
maxdecel = std::max(maxdecel, 0.1 * m_prop->GetAccelFwd());
|
|
}
|
|
|
|
const double curspeed = -relvel.Dot(reldir);
|
|
const double tt = (bZeroDecel) ? timestep : std::max(sqrt(2.0 * targdist / maxdecel), timestep);
|
|
const vector3d perpvel = relvel + reldir * curspeed;
|
|
double perpspeed = perpvel.Length();
|
|
const vector3d perpdir = (perpspeed > 1e-30) ? perpvel / perpspeed : vector3d(0, 0, 1);
|
|
|
|
double sidefactor = perpspeed / (tt * 0.5);
|
|
if (curspeed > (tt + timestep) * maxdecel || maxdecel < sidefactor) {
|
|
m_prop->AIFaceDirection(relvel);
|
|
m_prop->AIMatchVel(targvel);
|
|
m_state = -5;
|
|
return false;
|
|
} else
|
|
maxdecel = sqrt(maxdecel * maxdecel - sidefactor * sidefactor);
|
|
|
|
// ignore targvel if we could clear with side thrusters in a fraction of minimum time
|
|
// if (perpspeed < tt*0.01*m_ship->GetAccelMin()) perpspeed = 0;
|
|
|
|
// calculate target speed
|
|
double ispeed = (maxdecel < 1e-10) ? 0.0 : calc_ivel(targdist, m_endvel, maxdecel);
|
|
|
|
// cap target speed according to spare fuel remaining
|
|
double fuelspeed = m_prop->GetSpeedReachedWithFuel();
|
|
if (m_target && m_target->IsType(ObjectType::SHIP)) fuelspeed -=
|
|
m_dBody->GetVelocityRelTo(Pi::game->GetSpace()->GetRootFrame()).Length();
|
|
if (ispeed > curspeed && curspeed > 0.9 * fuelspeed) ispeed = curspeed;
|
|
|
|
// Don't exit a frame faster than some fraction of radius
|
|
// double maxframespeed = 0.2 * m_frameId->GetRadius() / timestep;
|
|
// if (m_frameId->GetParent() && ispeed > maxframespeed) ispeed = maxframespeed;
|
|
|
|
// cap perpspeed according to what's needed now
|
|
perpspeed = std::min(perpspeed, 2.0 * sidefactor * timestep);
|
|
|
|
// cap sdiff by thrust...
|
|
double sdiff = ispeed - curspeed;
|
|
double linaccel = sdiff < 0 ?
|
|
std::max(sdiff, -m_prop->GetAccelFwd() * timestep) :
|
|
std::min(sdiff, m_prop->GetAccelFwd() * timestep);
|
|
|
|
// linear thrust application, decel check
|
|
vector3d vdiff = linaccel * reldir + perpspeed * perpdir;
|
|
bool decel = sdiff <= 0;
|
|
// TODO: what is "SetDecelerating"??? => needs to be moved
|
|
m_dBody->SetDecelerating(decel);
|
|
if (decel)
|
|
m_prop->AIChangeVelBy(vdiff * m_dBody->GetOrient());
|
|
else
|
|
m_prop->AIChangeVelDir(vdiff * m_dBody->GetOrient());
|
|
|
|
// work out which way to head
|
|
vector3d head = reldir;
|
|
if (!m_state && sdiff < -1.2 * maxdecel * timestep) m_state = 1;
|
|
// if we're not coasting due to fuel constraints, and we're in the deceleration phase
|
|
// then flip the ship so we can use our main thrusters to decelerate
|
|
if (m_state && !is_zero_exact(sdiff) && sdiff < maxdecel * timestep * 60) head = -head;
|
|
if (!m_state && decel) sidefactor = -sidefactor;
|
|
head = head * maxdecel + perpdir * sidefactor;
|
|
|
|
// face appropriate direction
|
|
if (m_state >= 3) {
|
|
if (Pi::game->GetTimeAccelRate() <= 100.0 && m_is_flyto) {
|
|
vector3d pos;
|
|
if (m_target) {
|
|
pos = m_target->GetPositionRelTo(m_dBody).NormalizedSafe();
|
|
} else {
|
|
pos = -m_dBody->GetPosition().NormalizedSafe();
|
|
}
|
|
double ang = m_prop->AIFaceDirection(pos);
|
|
if (ang > DEG2RAD(5.0) || ang < DEG2RAD(-5.0))
|
|
return false;
|
|
}
|
|
m_prop->AIMatchAngVelObjSpace(vector3d(0.0));
|
|
return true;
|
|
} else
|
|
m_prop->AIFaceDirection(head);
|
|
if (body && body->IsType(ObjectType::PLANET) && m_dBody->GetPosition().LengthSqr() < 2 * erad * erad)
|
|
m_prop->AIFaceUpdir(m_dBody->GetPosition()); // turn bottom thruster towards planet
|
|
|
|
// termination conditions: check
|
|
if (m_state >= 3) return true; // finished last adjustment, hopefully
|
|
if (m_endvel > 0.0) {
|
|
if (reldir.Dot(m_reldir) < 0.9) return true;
|
|
} else if (targdist < 0.5 * m_prop->GetAccelMin() * timestep * timestep)
|
|
m_state = 3;
|
|
return false;
|
|
}
|
|
|
|
void AICmdDock::OnDeleted(const Body *body)
|
|
{
|
|
AICommand::OnDeleted(body);
|
|
if (static_cast<Body *>(m_target) == body) m_target = nullptr;
|
|
}
|
|
|
|
void AICmdDock::GetStatusText(char *str)
|
|
{
|
|
if (m_child)
|
|
m_child->GetStatusText(str);
|
|
else
|
|
snprintf(str, 255, "Dock: target %s, state %i", m_target->GetLabel().c_str(), m_state);
|
|
}
|
|
|
|
void AICmdDock::PostLoadFixup(Space *space)
|
|
{
|
|
AICommand::PostLoadFixup(space);
|
|
m_target = static_cast<SpaceStation *>(space->GetBodyByIndex(m_targetIndex));
|
|
// Ensure needed sub-system:
|
|
m_prop.Reset(m_dBody->GetPropulsion());
|
|
assert(m_prop != nullptr);
|
|
}
|
|
|
|
AICmdDock::AICmdDock(DynamicBody *dBody, SpaceStation *target) :
|
|
AICommand(dBody, CMD_DOCK),
|
|
m_target(target),
|
|
m_state(eDockGetDataStart)
|
|
{
|
|
Ship *ship = nullptr;
|
|
if (!dBody->IsType(ObjectType::SHIP)) return;
|
|
ship = static_cast<Ship *>(dBody);
|
|
assert(ship != nullptr);
|
|
|
|
m_prop.Reset(ship->GetPropulsion());
|
|
assert(m_prop != nullptr);
|
|
|
|
if (target->IsGroundStation()) {
|
|
Frame *frame = Frame::GetFrame(target->GetFrame());
|
|
Body *stationPlanet = frame->GetBody();
|
|
Planet *p = static_cast<Planet *>(stationPlanet);
|
|
|
|
double pressure, density;
|
|
p->GetAtmosphericState(target->GetPositionRelTo(stationPlanet).Length(), &pressure, &density);
|
|
|
|
if (pressure > static_cast<Ship *>(dBody)->GetAtmosphericPressureLimit()) {
|
|
m_dBody->AIMessage(Ship::AIERROR_PRESS_TOO_HIGH);
|
|
m_target = nullptr; // bail out on next timestep call
|
|
return;
|
|
}
|
|
}
|
|
|
|
double grav = GetGravityAtPos(m_target->GetFrame(), m_target->GetPosition());
|
|
if (m_prop->GetAccelUp() < grav) {
|
|
m_dBody->AIMessage(Ship::AIERROR_GRAV_TOO_HIGH);
|
|
m_target = nullptr; // bail out on next timestep call
|
|
}
|
|
}
|
|
|
|
AICmdDock::AICmdDock(const Json &jsonObj) :
|
|
AICommand(jsonObj, CMD_DOCK)
|
|
{
|
|
try {
|
|
m_targetIndex = jsonObj["index_for_target"];
|
|
m_dockpos = jsonObj["dock_pos"];
|
|
m_dockdir = jsonObj["dock_dir"];
|
|
m_dockupdir = jsonObj["dock_up_dir"];
|
|
m_state = EDockingStates(jsonObj["state"]);
|
|
} catch (Json::type_error &) {
|
|
throw SavedGameCorruptException();
|
|
}
|
|
}
|
|
|
|
void AICmdDock::SaveToJson(Json &jsonObj)
|
|
{
|
|
Space *space = Pi::game->GetSpace();
|
|
Json aiCommandObj({}); // Create JSON object to contain ai command data.
|
|
AICommand::SaveToJson(aiCommandObj);
|
|
aiCommandObj["index_for_target"] = space->GetIndexForBody(m_target);
|
|
aiCommandObj["dock_pos"] = m_dockpos;
|
|
aiCommandObj["dock_dir"] = m_dockdir;
|
|
aiCommandObj["dock_up_dir"] = m_dockupdir;
|
|
aiCommandObj["state"] = m_state;
|
|
jsonObj["ai_command"] = aiCommandObj; // Add ai command object to supplied object.
|
|
}
|
|
|
|
// m_state values:
|
|
// 0: get data for docking start pos
|
|
// 1: Fly to docking start pos
|
|
// 2: get data for docking end pos
|
|
// 3: Fly to docking end pos
|
|
|
|
bool AICmdDock::TimeStepUpdate()
|
|
{
|
|
Ship *ship = nullptr;
|
|
if (!ProcessChild()) return false;
|
|
if (!m_target) return true;
|
|
|
|
if (!m_dBody->IsType(ObjectType::SHIP)) return false;
|
|
ship = static_cast<Ship *>(m_dBody);
|
|
assert(ship != nullptr);
|
|
|
|
// finished moving into dock start pos (done by child FlyTo command)
|
|
if (m_state == eDockFlyToStart) IncrementState();
|
|
|
|
// If we're docked with the target, then we're finished!
|
|
if (ship->GetDockedWith() == m_target) {
|
|
ship->ClearThrusterState();
|
|
return true;
|
|
}
|
|
|
|
switch (ship->GetFlightState()) {
|
|
case Ship::UNDOCKING: return false; // allow undock animation to proceed
|
|
case Ship::DOCKED:
|
|
case Ship::LANDED:
|
|
LaunchShip(ship);
|
|
return false;
|
|
case Ship::JUMPING:
|
|
case Ship::HYPERSPACE:
|
|
return false;
|
|
case Ship::FLYING:
|
|
case Ship::DOCKING:
|
|
break;
|
|
}
|
|
|
|
// if we're not close to target, do a flyto first
|
|
double targdist = m_target->GetPositionRelTo(ship).LengthSqr();
|
|
if (targdist > 16000.0 * 16000.0) {
|
|
m_child.reset(new AICmdFlyTo(m_dBody, m_target));
|
|
ProcessChild();
|
|
return false;
|
|
}
|
|
|
|
int port = m_target->GetMyDockingPort(ship);
|
|
if (port == -1) {
|
|
const bool cleared = m_target->GetDockingClearance(ship);
|
|
port = m_target->GetMyDockingPort(ship);
|
|
if (!cleared || (port == -1)) {
|
|
ship->AIMessage(Ship::AIERROR_REFUSED_PERM);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// state 0,2: Get docking data
|
|
if (m_state == eDockGetDataStart || m_state == eDockGetDataEnd || m_state == eDockingComplete) {
|
|
const SpaceStationType *type = m_target->GetStationType();
|
|
SpaceStationType::positionOrient_t dockpos;
|
|
type->GetShipApproachWaypoints(port, (m_state == 0) ? 1 : 2, dockpos);
|
|
if (m_state != eDockGetDataEnd) {
|
|
m_dockpos = dockpos.pos;
|
|
}
|
|
|
|
m_dockdir = dockpos.zaxis.Normalized();
|
|
m_dockupdir = dockpos.yaxis.Normalized(); // don't trust these enough
|
|
if (type->IsOrbitalStation()) {
|
|
m_dockupdir = -m_dockupdir;
|
|
} else if (m_state == eDockingComplete) {
|
|
m_dockpos -= m_dockupdir * (ship->GetLandingPosOffset() + 0.1);
|
|
}
|
|
|
|
if (m_state != eDockGetDataEnd) {
|
|
m_dockpos = m_target->GetOrient() * m_dockpos + m_target->GetPosition();
|
|
}
|
|
IncrementState();
|
|
// should have m_dockpos in target frame, dirs relative to target orient
|
|
}
|
|
|
|
if (m_state == eDockFlyToStart) { // fly to first docking waypoint
|
|
m_child.reset(new AICmdFlyTo(m_dBody, m_target->GetFrame(), m_dockpos, 0.0, false));
|
|
ProcessChild();
|
|
return false;
|
|
}
|
|
|
|
// second docking waypoint
|
|
ship->SetWheelState(true);
|
|
const vector3d targpos = GetPosInFrame(m_dBody->GetFrame(), m_target->GetFrame(), m_dockpos);
|
|
const vector3d relpos = targpos - m_dBody->GetPosition();
|
|
const vector3d reldir = relpos.NormalizedSafe();
|
|
const vector3d relvel = -m_target->GetVelocityRelTo(m_dBody);
|
|
|
|
const double maxdecel = m_prop->GetAccelUp() - GetGravityAtPos(m_target->GetFrame(), m_dockpos);
|
|
const double ispeed = calc_ivel(relpos.Length(), 0.0, maxdecel);
|
|
const vector3d vdiff = ispeed * reldir - relvel;
|
|
m_prop->AIChangeVelDir(vdiff * m_dBody->GetOrient());
|
|
if (vdiff.Dot(reldir) < 0) {
|
|
m_dBody->SetDecelerating(true);
|
|
}
|
|
|
|
// get rotation of station for next frame
|
|
matrix3x3d trot = m_target->GetOrientRelTo(m_dBody->GetFrame());
|
|
double av = m_target->GetAngVelocity().Length();
|
|
double ang = av * Pi::game->GetTimeStep();
|
|
if (ang > 1e-16) {
|
|
vector3d axis = m_target->GetAngVelocity().Normalized();
|
|
trot = trot * matrix3x3d::Rotate(ang, axis);
|
|
}
|
|
double af;
|
|
if (m_target->GetStationType()->IsOrbitalStation()) {
|
|
af = m_prop->AIFaceDirection(trot * m_dockdir);
|
|
} else {
|
|
af = m_prop->AIFaceDirection(m_dBody->GetPosition().Cross(m_dBody->GetOrient().VectorX()));
|
|
}
|
|
if (af < 0.01) {
|
|
af = m_prop->AIFaceUpdir(trot * m_dockupdir, av) - ang;
|
|
}
|
|
if (m_state < eInvalidDockingStage && af < 0.01 && ship->GetWheelState() >= 1.0f) {
|
|
IncrementState();
|
|
}
|
|
|
|
#ifdef DEBUG_AUTOPILOT
|
|
Output("AICmdDock dist = %.1f, speed = %.1f, ythrust = %.2f, state = %i\n",
|
|
sqrt(targdist), relvel.Length(), m_ship->GetLinThrusterState().y, m_state);
|
|
#endif
|
|
|
|
return false;
|
|
}
|
|
|
|
AICmdHoldPosition::AICmdHoldPosition(DynamicBody *dBody) :
|
|
AICommand(dBody, CMD_HOLDPOSITION)
|
|
{
|
|
m_prop.Reset(m_dBody->GetPropulsion());
|
|
assert(m_prop != nullptr);
|
|
}
|
|
|
|
AICmdHoldPosition::AICmdHoldPosition(const Json &jsonObj) :
|
|
AICommand(jsonObj, CMD_HOLDPOSITION)
|
|
{
|
|
// Ensure needed sub-system:
|
|
m_prop.Reset(m_dBody->GetPropulsion());
|
|
assert(m_prop != nullptr);
|
|
}
|
|
|
|
bool AICmdHoldPosition::TimeStepUpdate()
|
|
{
|
|
// XXX perhaps try harder to move back to the original position
|
|
m_prop->AIMatchVel(vector3d(0, 0, 0));
|
|
return false;
|
|
}
|
|
|
|
void AICmdFlyAround::GetStatusText(char *str)
|
|
{
|
|
if (m_child)
|
|
m_child->GetStatusText(str);
|
|
else
|
|
snprintf(str, 255, "FlyAround: alt %.1fkm, vel %.1fkm/s, mode %i",
|
|
m_alt / 1000.0, m_vel / 1000.0, m_targmode);
|
|
}
|
|
|
|
void AICmdFlyAround::PostLoadFixup(Space *space)
|
|
{
|
|
AICommand::PostLoadFixup(space);
|
|
m_obstructor = space->GetBodyByIndex(m_obstructorIndex);
|
|
// Ensure needed sub-system:
|
|
m_prop.Reset(m_dBody->GetPropulsion());
|
|
assert(m_prop != nullptr);
|
|
}
|
|
|
|
void AICmdFlyAround::Setup(Body *obstructor, double alt, double vel, int mode)
|
|
{
|
|
assert(!std::isnan(alt));
|
|
assert(!std::isnan(vel));
|
|
m_obstructor = obstructor;
|
|
m_alt = alt;
|
|
m_vel = vel;
|
|
m_targmode = mode;
|
|
|
|
// push out of effect radius (gravity safety & station parking zones)
|
|
alt = std::max(alt, MaxEffectRad(obstructor, m_prop.Get()));
|
|
|
|
// drag within frame because orbits are impossible otherwise
|
|
// timestep code also doesn't work correctly for ex-frame cases, should probably be fixed
|
|
Frame *obsFrame = Frame::GetFrame(obstructor->GetFrame());
|
|
Frame *nonRot = Frame::GetFrame(obsFrame->GetNonRotFrame());
|
|
alt = std::min(alt, 0.95 * nonRot->GetRadius());
|
|
|
|
// generate suitable velocity if none provided
|
|
double minacc = (mode == 2) ? 0 : m_prop->GetAccelMin();
|
|
double mass = obstructor->IsType(ObjectType::TERRAINBODY) ? obstructor->GetMass() : 0;
|
|
if (vel < 1e-30) m_vel = sqrt(m_alt * 0.8 * minacc + mass * G / m_alt);
|
|
}
|
|
|
|
AICmdFlyAround::AICmdFlyAround(DynamicBody *dBody, Body *obstructor, double relalt, int mode) :
|
|
AICommand(dBody, CMD_FLYAROUND)
|
|
{
|
|
m_prop.Reset(dBody->GetPropulsion());
|
|
assert(m_prop != nullptr);
|
|
|
|
assert(!std::isnan(relalt));
|
|
double alt = relalt * obstructor->GetPhysRadius();
|
|
assert(!std::isnan(alt));
|
|
Setup(obstructor, alt, 0.0, mode);
|
|
}
|
|
|
|
AICmdFlyAround::AICmdFlyAround(DynamicBody *dBody, Body *obstructor, double alt, double vel, int mode) :
|
|
AICommand(dBody, CMD_FLYAROUND)
|
|
{
|
|
m_prop.Reset(dBody->GetPropulsion());
|
|
assert(m_prop != nullptr);
|
|
|
|
assert(!std::isnan(alt));
|
|
Setup(obstructor, alt, vel, mode);
|
|
}
|
|
|
|
AICmdFlyAround::AICmdFlyAround(const Json &jsonObj) :
|
|
AICommand(jsonObj, CMD_FLYAROUND)
|
|
{
|
|
try {
|
|
m_obstructorIndex = jsonObj["index_for_obstructor"];
|
|
m_vel = jsonObj["vel"];
|
|
m_alt = jsonObj["alt"];
|
|
m_targmode = jsonObj["targ_mode"];
|
|
} catch (Json::type_error &) {
|
|
throw SavedGameCorruptException();
|
|
}
|
|
}
|
|
|
|
void AICmdFlyAround::SaveToJson(Json &jsonObj)
|
|
{
|
|
if (m_child) {
|
|
m_child.reset();
|
|
}
|
|
Json aiCommandObj({}); // Create JSON object to contain ai command data.
|
|
AICommand::SaveToJson(aiCommandObj);
|
|
aiCommandObj["index_for_obstructor"] = Pi::game->GetSpace()->GetIndexForBody(m_obstructor);
|
|
aiCommandObj["vel"] = m_vel;
|
|
aiCommandObj["alt"] = m_alt;
|
|
aiCommandObj["targ_mode"] = m_targmode;
|
|
jsonObj["ai_command"] = aiCommandObj; // Add ai command object to supplied object.
|
|
}
|
|
|
|
double AICmdFlyAround::MaxVel(double targdist, double targalt)
|
|
{
|
|
Propulsion *prop = m_dBody->GetPropulsion();
|
|
assert(prop != 0);
|
|
|
|
if (targalt > m_alt) return m_vel;
|
|
double t = sqrt(2.0 * targdist / prop->GetAccelFwd());
|
|
double vmaxprox = prop->GetAccelMin() * t; // limit by target proximity
|
|
double vmaxstep = std::max(m_alt * 0.05, m_alt - targalt);
|
|
vmaxstep /= Pi::game->GetTimeStep(); // limit by distance covered per timestep
|
|
return std::min(m_vel, std::min(vmaxprox, vmaxstep));
|
|
}
|
|
|
|
bool AICmdFlyAround::TimeStepUpdate()
|
|
{
|
|
if (m_dBody->IsType(ObjectType::SHIP)) {
|
|
Ship *ship = nullptr;
|
|
ship = static_cast<Ship *>(m_dBody);
|
|
assert(ship != 0);
|
|
|
|
if (ship->GetFlightState() == Ship::JUMPING) return false;
|
|
if (!ProcessChild()) return false;
|
|
|
|
// Not necessary unless it's a tier 1 AI
|
|
if (ship->GetFlightState() == Ship::FLYING)
|
|
ship->SetWheelState(false);
|
|
else {
|
|
LaunchShip(ship);
|
|
return false;
|
|
}
|
|
|
|
} else {
|
|
// return false;
|
|
}
|
|
|
|
double timestep = Pi::game->GetTimeStep();
|
|
vector3d targpos = (!m_targmode) ? m_targpos :
|
|
m_dBody->GetVelocity().NormalizedSafe() * m_dBody->GetPosition().LengthSqr();
|
|
vector3d obspos = m_obstructor->GetPositionRelTo(m_dBody);
|
|
double obsdist = obspos.Length();
|
|
vector3d obsdir = obspos / obsdist;
|
|
vector3d relpos = targpos - m_dBody->GetPosition();
|
|
|
|
// frame body suicide check, response
|
|
if (CheckSuicide(m_dBody, -obsdir)) {
|
|
m_prop->AIFaceDirection(m_dBody->GetPosition()); // face away from planet
|
|
m_prop->AIMatchVel(vector3d(0.0));
|
|
return false;
|
|
}
|
|
|
|
// if too far away, fly to tangent
|
|
if (obsdist > 1.1 * m_alt) {
|
|
double v;
|
|
FrameId obsframeId = Frame::GetFrame(m_obstructor->GetFrame())->GetNonRotFrame();
|
|
vector3d tangent = GenerateTangent(m_dBody, obsframeId, targpos, m_alt);
|
|
vector3d tpos_obs = GetPosInFrame(obsframeId, m_dBody->GetFrame(), targpos);
|
|
if (m_targmode)
|
|
v = m_vel;
|
|
else if (relpos.LengthSqr() < obsdist + tpos_obs.LengthSqr())
|
|
v = 0.0;
|
|
else
|
|
v = MaxVel((tpos_obs - tangent).Length(), tpos_obs.Length());
|
|
m_child.reset(new AICmdFlyTo(m_dBody, obsframeId, tangent, v, true));
|
|
ProcessChild();
|
|
return false;
|
|
}
|
|
|
|
// limit m_vel by target proximity & distance covered per frame
|
|
double vel = (m_targmode) ? m_vel : MaxVel(relpos.Length(), targpos.Length());
|
|
|
|
// all calculations in ship's frame
|
|
vector3d fwddir = (obsdir.Cross(relpos).Cross(obsdir)).NormalizedSafe();
|
|
vector3d tanvel = vel * fwddir;
|
|
|
|
// max feature avoidance check, response
|
|
if (obsdist < MaxFeatureRad(m_obstructor)) {
|
|
double ang = m_prop->AIFaceDirection(-obsdir);
|
|
m_prop->AIMatchVel(ang < 0.05 ? 1000.0 * -obsdir : vector3d(0.0));
|
|
return false;
|
|
}
|
|
|
|
// calculate target velocity
|
|
double alt = (tanvel * timestep + obspos).Length(); // unnecessary?
|
|
double ivel = calc_ivel(alt - m_alt, 0.0, m_prop->GetAccelMin());
|
|
|
|
vector3d finalvel = tanvel + ivel * obsdir;
|
|
m_prop->AIMatchVel(finalvel);
|
|
m_prop->AIFaceDirection(fwddir);
|
|
m_prop->AIFaceUpdir(-obsdir);
|
|
|
|
// vector3d newhead = GenerateTangent(m_ship, m_obstructor->GetFrame(), fwddir);
|
|
// newhead = GetPosInFrame(m_ship->GetFrame(), m_obstructor->GetFrame(), newhead);
|
|
// m_ship->AIFaceDirection(newhead-m_ship->GetPosition());
|
|
|
|
// termination condition for orbits
|
|
vector3d thrust = m_prop->GetLinThrusterState();
|
|
if (m_targmode >= 2 && thrust.LengthSqr() < 0.01) m_targmode++;
|
|
if (m_targmode == 4) {
|
|
m_prop->SetLinThrusterState(vector3d(0.0));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void AICmdFormation::OnDeleted(const Body *body)
|
|
{
|
|
AICommand::OnDeleted(body);
|
|
if (static_cast<Body *>(m_target) == body) m_target = 0;
|
|
}
|
|
|
|
void AICmdFormation::GetStatusText(char *str)
|
|
{
|
|
if (m_child)
|
|
m_child->GetStatusText(str);
|
|
else
|
|
snprintf(str, 255, "Formation: %s, dist %.1fkm",
|
|
m_target->GetLabel().c_str(), m_posoff.Length() / 1000.0);
|
|
}
|
|
|
|
AICmdFormation::AICmdFormation(DynamicBody *dBody, DynamicBody *target, const vector3d &posoff) :
|
|
AICommand(dBody, CMD_FORMATION),
|
|
m_target(target),
|
|
m_posoff(posoff)
|
|
{
|
|
m_prop.Reset(dBody->GetPropulsion());
|
|
assert(m_prop != nullptr);
|
|
}
|
|
|
|
AICmdFormation::AICmdFormation(const Json &jsonObj) :
|
|
AICommand(jsonObj, CMD_FORMATION)
|
|
{
|
|
try {
|
|
m_targetIndex = jsonObj["index_for_target"];
|
|
m_posoff = jsonObj["pos_off"];
|
|
} catch (Json::type_error &) {
|
|
throw SavedGameCorruptException();
|
|
}
|
|
}
|
|
|
|
void AICmdFormation::SaveToJson(Json &jsonObj)
|
|
{
|
|
if (m_child) {
|
|
m_child.reset();
|
|
}
|
|
Json aiCommandObj({}); // Create JSON object to contain ai command data.
|
|
AICommand::SaveToJson(aiCommandObj);
|
|
aiCommandObj["index_for_target"] = Pi::game->GetSpace()->GetIndexForBody(m_target);
|
|
aiCommandObj["pos_off"] = m_posoff;
|
|
jsonObj["ai_command"] = aiCommandObj; // Add ai command object to supplied object.
|
|
}
|
|
|
|
void AICmdFormation::PostLoadFixup(Space *space)
|
|
{
|
|
AICommand::PostLoadFixup(space);
|
|
m_target = static_cast<Ship *>(space->GetBodyByIndex(m_targetIndex));
|
|
// Ensure needed sub-system:
|
|
m_prop.Reset(m_dBody->GetPropulsion());
|
|
assert(m_prop != nullptr);
|
|
}
|
|
|
|
bool AICmdFormation::TimeStepUpdate()
|
|
{
|
|
if (m_dBody->IsType(ObjectType::SHIP)) {
|
|
Ship *ship = static_cast<Ship *>(m_dBody);
|
|
assert(ship != 0);
|
|
|
|
if (ship->GetFlightState() == Ship::JUMPING) return false;
|
|
if (ship->GetFlightState() == Ship::FLYING)
|
|
ship->SetWheelState(false);
|
|
else {
|
|
LaunchShip(ship);
|
|
return false;
|
|
}
|
|
}
|
|
if (!m_target) return true;
|
|
if (!ProcessChild()) return false; // In case we're doing an intercept
|
|
|
|
// if too far away, do an intercept first
|
|
// TODO: adjust distance cap by timestep so we don't bounce?
|
|
if (m_target->GetPositionRelTo(m_dBody).Length() > 30000.0) {
|
|
m_child.reset(new AICmdFlyTo(m_dBody, m_target));
|
|
ProcessChild();
|
|
return false;
|
|
}
|
|
|
|
matrix3x3d torient = m_target->GetOrientRelTo(m_dBody->GetFrame());
|
|
vector3d relpos = m_target->GetPositionRelTo(m_dBody) + torient * m_posoff;
|
|
vector3d relvel = -m_target->GetVelocityRelTo(m_dBody);
|
|
double targdist = relpos.Length();
|
|
vector3d reldir = (targdist < 1e-16) ? vector3d(1, 0, 0) : relpos / targdist;
|
|
|
|
// adjust for target acceleration
|
|
matrix3x3d forient = Frame::GetFrame(m_target->GetFrame())->GetOrientRelTo(m_dBody->GetFrame());
|
|
vector3d targaccel = forient * m_target->GetLastForce() / m_target->GetMass();
|
|
relvel -= targaccel * Pi::game->GetTimeStep();
|
|
double maxdecel = m_prop->GetAccelFwd() + targaccel.Dot(reldir);
|
|
if (maxdecel < 0.0) maxdecel = 0.0;
|
|
|
|
// linear thrust
|
|
double ispeed = calc_ivel(targdist, 0.0, maxdecel);
|
|
vector3d vdiff = ispeed * reldir - relvel;
|
|
m_prop->AIChangeVelDir(vdiff * m_dBody->GetOrient());
|
|
if (m_target->IsType(ObjectType::SHIP)) {
|
|
Ship *target_ship = static_cast<Ship *>(m_target);
|
|
if (target_ship->IsDecelerating()) m_dBody->SetDecelerating(true);
|
|
} else {
|
|
m_dBody->SetDecelerating(false);
|
|
}
|
|
|
|
m_prop->AIFaceDirection(-torient.VectorZ());
|
|
return false; // never self-terminates
|
|
}
|