pioneer/src/Ship.cpp

1653 lines
52 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 "Ship.h"
#include "CargoBody.h"
#include "EnumStrings.h"
#include "Frame.h"
#include "Game.h"
#include "GameLog.h"
#include "GameSaveError.h"
#include "HeatGradientPar.h"
#include "HyperspaceCloud.h"
#include "Lang.h"
#include "Missile.h"
#include "NavLights.h"
#include "Planet.h"
#include "Player.h" // <-- Here only for 1 occurence of "Pi::player" in Ship::Explode
#include "Sensors.h"
#include "Sfx.h"
#include "Shields.h"
#include "ShipAICmd.h"
#include "Space.h"
#include "SpaceStation.h"
#include "StringF.h"
#include "WorldView.h"
#include "collider/CollisionContact.h"
#include "graphics/TextureBuilder.h"
#include "lua/LuaEvent.h"
#include "lua/LuaObject.h"
#include "lua/LuaUtils.h"
#include "scenegraph/Animation.h"
#include "scenegraph/MatrixTransform.h"
#include "ship/PlayerShipController.h"
static const float TONS_HULL_PER_SHIELD = 10.f;
HeatGradientParameters_t Ship::s_heatGradientParams;
const float Ship::DEFAULT_SHIELD_COOLDOWN_TIME = 1.0f;
const double Ship::DEFAULT_LIFT_TO_DRAG_RATIO = 0.001;
Ship::Ship(const ShipType::Id &shipId) :
DynamicBody(),
m_controller(0),
m_flightState(FLYING),
m_alertState(ALERT_NONE),
m_landingGearAnimation(nullptr)
{
/*
THIS CODE DOES NOT RUN WHEN LOADING SAVEGAMES!!
*/
AddFeature(Feature::PROPULSION); // add component propulsion
AddFeature(Feature::FIXED_GUNS); // add component fixed guns
Properties().Set("flightState", EnumStrings::GetString("ShipFlightState", m_flightState));
Properties().Set("alertStatus", EnumStrings::GetString("ShipAlertStatus", m_alertState));
SetFuel(1.0);
SetFuelReserve(0.0);
m_lastAlertUpdate = 0.0;
m_lastFiringAlert = 0.0;
m_shipNear = false;
m_shipFiring = false;
m_missileDetected = false;
m_testLanded = false;
m_launchLockTimeout = 0;
m_wheelTransition = 0;
m_wheelState = 0;
m_forceWheelUpdate = false;
m_dockedWith = nullptr;
m_dockedWithPort = 0;
SetShipId(shipId);
ClearAngThrusterState();
ClearLinThrusterState();
m_hyperspace.countdown = 0;
m_hyperspace.now = false;
GetFixedGuns()->Init(this);
m_ecmRecharge = 0;
m_shieldCooldown = 0.0f;
m_curAICmd = 0;
m_aiMessage = AIERROR_NONE;
m_decelerating = false;
InitEquipSet();
SetModel(m_type->modelName.c_str());
// Setting thrusters colors
if (m_type->isGlobalColorDefined) GetModel()->SetThrusterColor(m_type->globalThrusterColor);
for (int i = 0; i < THRUSTER_MAX; i++) {
if (!m_type->isDirectionColorDefined[i]) continue;
vector3f dir;
switch (i) {
case THRUSTER_FORWARD: dir = vector3f(0.0, 0.0, 1.0); break;
case THRUSTER_REVERSE: dir = vector3f(0.0, 0.0, -1.0); break;
case THRUSTER_LEFT: dir = vector3f(1.0, 0.0, 0.0); break;
case THRUSTER_RIGHT: dir = vector3f(-1.0, 0.0, 0.0); break;
case THRUSTER_UP: dir = vector3f(1.0, 0.0, 0.0); break;
case THRUSTER_DOWN: dir = vector3f(-1.0, 0.0, 0.0); break;
}
GetModel()->SetThrusterColor(dir, m_type->directionThrusterColor[i]);
}
SetLabel("UNLABELED_SHIP");
m_skin.SetRandomColors(Pi::rng);
m_skin.SetDecal(m_type->manufacturer);
m_skin.Apply(GetModel());
if (GetModel()->SupportsPatterns())
GetModel()->SetPattern(Pi::rng.Int32(0, GetModel()->GetNumPatterns() - 1));
Init();
SetController(new ShipController());
}
Ship::Ship(const Json &jsonObj, Space *space) :
DynamicBody(jsonObj, space)
{
AddFeature(Feature::PROPULSION); // add component propulsion
AddFeature(Feature::FIXED_GUNS); // add component fixed guns
try {
Json shipObj = jsonObj["ship"];
GetPropulsion()->LoadFromJson(shipObj, space);
SetShipId(shipObj["ship_type_id"]); // XXX handle missing thirdparty ship
GetPropulsion()->SetFuelTankMass(GetShipType()->fuelTankMass);
m_stats.fuel_tank_mass_left = GetPropulsion()->FuelTankMassLeft();
m_skin.LoadFromJson(shipObj);
m_skin.Apply(GetModel());
// needs fixups
m_wheelTransition = shipObj["wheel_transition"];
m_wheelState = shipObj["wheel_state"];
m_forceWheelUpdate = true;
m_launchLockTimeout = shipObj["launch_lock_timeout"];
m_testLanded = shipObj["test_landed"];
m_flightState = shipObj["flight_state"];
m_lastAlertUpdate = 0.0; // alertstate check cache timer
m_shipNear = false; // alertstate check cache value
m_shipFiring = false; // alertstate check cache value
m_missileDetected = false; // alertstate check cache value
m_alertState = shipObj["alert_state"];
Properties().Set("flightState", EnumStrings::GetString("ShipFlightState", m_flightState));
Properties().Set("alertStatus", EnumStrings::GetString("ShipAlertStatus", m_alertState));
m_lastFiringAlert = shipObj["last_firing_alert"];
Json hyperspaceDestObj = shipObj["hyperspace_destination"];
m_hyperspace.dest = SystemPath::FromJson(hyperspaceDestObj);
m_hyperspace.countdown = shipObj["hyperspace_countdown"];
m_hyperspace.duration = 0;
m_hyperspace.sounds.warmup_sound = shipObj.value("hyperspace_warmup_sound", "");
m_hyperspace.sounds.abort_sound = shipObj.value("hyperspace_abort_sound", "");
m_hyperspace.sounds.jump_sound = shipObj.value("hyperspace_jump_sound", "");
GetFixedGuns()->LoadFromJson(shipObj, space);
m_ecmRecharge = shipObj["ecm_recharge"];
SetShipId(shipObj["ship_type_id"]); // XXX handle missing thirdparty ship
m_dockedWithPort = shipObj["docked_with_port"];
m_dockedWithIndex = shipObj["index_for_body_docked_with"];
Init();
m_stats.hull_mass_left = shipObj["hull_mass_left"]; // must be after Init()...
m_stats.shield_mass_left = shipObj["shield_mass_left"];
m_shieldCooldown = shipObj["shield_cooldown"];
m_curAICmd = 0;
m_curAICmd = AICommand::LoadFromJson(shipObj);
m_aiMessage = AIError(shipObj["ai_message"]);
PropertyMap &p = Properties();
p.Set("hullMassLeft", m_stats.hull_mass_left);
p.Set("hullPercent", 100.0f * (m_stats.hull_mass_left / float(m_type->hullMass)));
p.Set("shieldMassLeft", m_stats.shield_mass_left);
p.Set("fuelMassLeft", m_stats.fuel_tank_mass_left);
p.PushLuaTable();
lua_State *l = Lua::manager->GetLuaState();
lua_getfield(l, -1, "equipSet");
m_equipSet = LuaRef(l, -1);
lua_pop(l, 2);
m_controller = 0;
const ShipController::Type ctype = shipObj["controller_type"];
if (ctype == ShipController::PLAYER)
SetController(new PlayerShipController());
else
SetController(new ShipController());
m_controller->LoadFromJson(shipObj);
m_navLights->LoadFromJson(shipObj);
m_shipName = shipObj["name"].get<std::string>();
Properties().Set("shipName", m_shipName);
} catch (Json::type_error &) {
throw SavedGameCorruptException();
}
}
Ship::~Ship()
{
if (m_curAICmd) delete m_curAICmd;
delete m_controller;
}
void Ship::Init()
{
m_invulnerable = false;
m_sensors.reset(new Sensors(this));
m_navLights.reset(new NavLights(GetModel()));
m_navLights->SetEnabled(true);
SetMassDistributionFromModel();
UpdateEquipStats();
m_stats.hull_mass_left = float(m_type->hullMass);
m_stats.shield_mass_left = 0;
PropertyMap &p = Properties();
p.Set("hullMassLeft", m_stats.hull_mass_left);
p.Set("hullPercent", 100.0f * (m_stats.hull_mass_left / float(m_type->hullMass)));
p.Set("shieldMassLeft", m_stats.shield_mass_left);
p.Set("fuelMassLeft", m_stats.fuel_tank_mass_left);
// Init of Propulsion:
GetPropulsion()->Init(this, GetModel(), m_type->fuelTankMass, m_type->effectiveExhaustVelocity, m_type->linThrust, m_type->angThrust, m_type->linAccelerationCap);
p.Set("shipName", m_shipName);
m_hyperspace.now = false; // TODO: move this on next savegame change, maybe
m_hyperspaceCloud = 0;
m_landingGearAnimation = GetModel()->FindAnimation("gear_down");
m_forceWheelUpdate = true;
GetFixedGuns()->InitGuns(GetModel());
// If we've got the tag_landing set then use it for an offset
// otherwise use zero so that it will dock but look clearly incorrect
const SceneGraph::MatrixTransform *mt = GetModel()->FindTagByName("tag_landing");
if (mt) {
m_landingMinOffset = mt->GetTransform().GetTranslate().y;
} else {
m_landingMinOffset = 0.0; // GetAabb().min.y;
}
InitMaterials();
}
void Ship::PostLoadFixup(Space *space)
{
DynamicBody::PostLoadFixup(space);
m_dockedWith = static_cast<SpaceStation *>(space->GetBodyByIndex(m_dockedWithIndex));
if (m_curAICmd) m_curAICmd->PostLoadFixup(space);
m_controller->PostLoadFixup(space);
}
void Ship::SaveToJson(Json &jsonObj, Space *space)
{
DynamicBody::SaveToJson(jsonObj, space);
Json shipObj({}); // Create JSON object to contain ship data.
GetPropulsion()->SaveToJson(shipObj, space);
m_skin.SaveToJson(shipObj);
shipObj["wheel_transition"] = m_wheelTransition;
shipObj["wheel_state"] = m_wheelState;
shipObj["launch_lock_timeout"] = m_launchLockTimeout;
shipObj["test_landed"] = m_testLanded;
shipObj["flight_state"] = int(m_flightState);
shipObj["alert_state"] = int(m_alertState);
shipObj["last_firing_alert"] = m_lastFiringAlert;
// XXX make sure all hyperspace attrs and the cloud get saved
Json hyperspaceDestObj({}); // Create JSON object to contain hyperspace destination data.
m_hyperspace.dest.ToJson(hyperspaceDestObj);
shipObj["hyperspace_destination"] = hyperspaceDestObj; // Add hyperspace destination object to ship object.
shipObj["hyperspace_countdown"] = m_hyperspace.countdown;
shipObj["hyperspace_warmup_sound"] = m_hyperspace.sounds.warmup_sound;
shipObj["hyperspace_abort_sound"] = m_hyperspace.sounds.abort_sound;
shipObj["hyperspace_jump_sound"] = m_hyperspace.sounds.jump_sound;
GetFixedGuns()->SaveToJson(shipObj, space);
shipObj["ecm_recharge"] = m_ecmRecharge;
shipObj["ship_type_id"] = m_type->id;
shipObj["docked_with_port"] = m_dockedWithPort;
shipObj["index_for_body_docked_with"] = space->GetIndexForBody(m_dockedWith);
shipObj["hull_mass_left"] = m_stats.hull_mass_left;
shipObj["shield_mass_left"] = m_stats.shield_mass_left;
shipObj["shield_cooldown"] = m_shieldCooldown;
if (m_curAICmd) m_curAICmd->SaveToJson(shipObj);
shipObj["ai_message"] = int(m_aiMessage);
shipObj["controller_type"] = static_cast<int>(m_controller->GetType());
m_controller->SaveToJson(shipObj, space);
m_navLights->SaveToJson(shipObj);
shipObj["name"] = m_shipName;
jsonObj["ship"] = shipObj; // Add ship object to supplied object.
}
void Ship::InitEquipSet()
{
lua_State *l = Lua::manager->GetLuaState();
PropertyMap &p = Properties();
LUA_DEBUG_START(l);
pi_lua_import(l, "EquipSet");
LuaTable es_class(l, -1);
LuaTable slots = LuaTable(l).LoadMap(GetShipType()->slots.begin(), GetShipType()->slots.end());
m_equipSet = es_class.Call<LuaRef>("New", slots);
p.Set("equipSet", ScopedTable(m_equipSet));
UpdateEquipStats();
{
ScopedTable es(m_equipSet);
int usedCargo = es.CallMethod<int>("OccupiedSpace", "cargo");
int totalCargo = std::min(m_stats.free_capacity + usedCargo, es.CallMethod<int>("SlotSize", "cargo"));
p.Set("usedCargo", usedCargo);
p.Set("totalCargo", totalCargo);
}
lua_pop(l, 2);
LUA_DEBUG_END(l, 0);
}
void Ship::InitMaterials()
{
SceneGraph::Model *pModel = GetModel();
assert(pModel);
const Uint32 numMats = pModel->GetNumMaterials();
for (Uint32 m = 0; m < numMats; m++) {
RefCountedPtr<Graphics::Material> mat = pModel->GetMaterialByIndex(m);
mat->heatGradient = Graphics::TextureBuilder::Decal("textures/heat_gradient.dds").GetOrCreateTexture(Pi::renderer, "model");
mat->specialParameter0 = &s_heatGradientParams;
}
s_heatGradientParams.heatingAmount = 0.0f;
s_heatGradientParams.heatingNormal = vector3f(0.0f, -1.0f, 0.0f);
}
void Ship::SetController(ShipController *c)
{
assert(c != 0);
if (m_controller) delete m_controller;
m_controller = c;
m_controller->m_ship = this;
}
float Ship::GetPercentHull() const
{
return 100.0f * (m_stats.hull_mass_left / float(m_type->hullMass));
}
float Ship::GetPercentShields() const
{
if (m_stats.shield_mass <= 0)
return 100.0f;
else
return 100.0f * (m_stats.shield_mass_left / m_stats.shield_mass);
}
float Ship::GetAtmosphericPressureLimit() const
{
return m_type->atmosphericPressureLimit * std::max(m_stats.atmo_shield_cap, 1); //default to base limit if no shield installed
}
void Ship::SetPercentHull(float p)
{
m_stats.hull_mass_left = 0.01f * Clamp(p, 0.0f, 100.0f) * float(m_type->hullMass);
Properties().Set("hullMassLeft", m_stats.hull_mass_left);
Properties().Set("hullPercent", 100.0f * (m_stats.hull_mass_left / float(m_type->hullMass)));
}
void Ship::UpdateMass()
{
SetMass(((double)m_stats.static_mass + GetPropulsion()->FuelTankMassLeft()) * 1000);
}
template <typename T>
inline int sign(T num)
{
return (num >= 0) - (num < 0);
}
vector3d Ship::CalcAtmosphericForce() const
{
// Data from ship.
const double topCrossSec = GetShipType()->topCrossSection;
const double sideCrossSec = GetShipType()->sideCrossSection;
const double frontCrossSec = GetShipType()->frontCrossSection;
// TODO: vary drag coefficient based on Reynolds number, specifically by
// atmospheric composition (viscosity) and airspeed (mach number).
const double topDragCoeff = GetShipType()->topDragCoeff;
const double sideDragCoeff = GetShipType()->sideDragCoeff;
const double frontDragCoeff = GetShipType()->frontDragCoeff;
const double shipLiftCoeff = GetShipType()->shipLiftCoefficient;
// By converting the velocity into local space, we can apply the drag individually to each component.
const vector3d localVel = GetVelocity() * GetOrient();
// The drag forces applied to the craft, in local space.
// TODO: verify dimensional accuracy and that we're not generating more drag than physically possible.
// TODO: use a different drag constant for each side (front, back, etc).
// This also handles (most of) the lift due to wing deflection.
// Get current atmosphere parameters
double pressure, density;
GetCurrentAtmosphericState(pressure, density);
if (is_zero_exact(density))
return vector3d(0.0);
// Precalculate common part of drag components calculation (rho / 2)
const auto rho_2 = density / 2.;
// Vector, which components are square of local airspeed vector components
const vector3d lv2(localVel * localVel);
const vector3d fAtmosDrag(
-sign(localVel.x) * rho_2 * lv2.x * sideCrossSec * sideDragCoeff,
-sign(localVel.y) * rho_2 * lv2.y * topCrossSec * topDragCoeff,
-sign(localVel.z) * rho_2 * lv2.z * frontCrossSec * frontDragCoeff);
// The amount of lift produced by air pressure differential across the top and bottom of the lifting surfaces.
vector3d fAtmosLift(0.0);
double AoAMultiplier = localVel.NormalizedSafe().y;
// There's no lift produced once the wing hits the stall angle.
if (std::abs(AoAMultiplier) < 0.61) {
// Pioneer simulates non-cambered wings, with equal air displacement on either side of AoA.
// Generated lift peaks at around 20 degrees here, and falls off fully at 35-ish.
// TODO: handle AoA better / more gracefully with an actual angle- and curve-based implementation.
AoAMultiplier = cos((std::abs(AoAMultiplier) - 0.31) * 5.0) * sign(AoAMultiplier);
// TODO: verify dimensional accuracy and that we're not generating more lift than physically possible.
// We scale down the lift contribution because fAtmosDrag handles deflection-based lift.
fAtmosLift.y = CalcAtmosphericDrag(pow(localVel.z, 2), topCrossSec, shipLiftCoeff) * -AoAMultiplier * 0.2;
}
return GetOrient() * (fAtmosDrag + fAtmosLift);
}
//calculates torque to force the spacecraft go nose-first in atmosphere
vector3d Ship::CalcAtmoTorque() const
{
double m_topCrossSec = GetShipType()->topCrossSection;
double m_sideCrossSec = GetShipType()->sideCrossSection;
double m_frontCrossSec = GetShipType()->frontCrossSection;
double m_aeroStabilityMultiplier = GetShipType()->atmoStability;
vector3d forward = GetOrient().VectorZ();
vector3d m_vel = GetVelocity().NormalizedSafe();
vector3d m_torqueDir = -m_vel.Cross(-forward); // <--- This is correct
// TODO: evaluate this function and properly implement based upon ship cross-section.
double m_drag = CalcAtmosphericDrag(GetVelocity().LengthSqr(), m_topCrossSec, DEFAULT_DRAG_COEFF);
vector3d fAtmoTorque = vector3d(0.0);
if (GetVelocity().Length() > 100) { //don't apply torque at minimal speeds
fAtmoTorque = m_drag * m_torqueDir * ((m_topCrossSec + m_sideCrossSec) / (m_frontCrossSec * 4)) * 0.3 * m_aeroStabilityMultiplier * Pi::game->GetInvTimeAccelRate();
}
return fAtmoTorque;
}
bool Ship::OnDamage(Body *attacker, float kgDamage, const CollisionContact &contactData)
{
if (m_invulnerable) {
Sound::BodyMakeNoise(this, "Hull_hit_Small", 0.5f);
return true;
}
if (!IsDead()) {
float dam = kgDamage * 0.001f;
if (m_stats.shield_mass_left > 0.0f) {
if (m_stats.shield_mass_left > dam) {
m_stats.shield_mass_left -= dam;
dam = 0;
} else {
dam -= m_stats.shield_mass_left;
m_stats.shield_mass_left = 0;
}
Properties().Set("shieldMassLeft", m_stats.shield_mass_left);
}
m_shieldCooldown = DEFAULT_SHIELD_COOLDOWN_TIME;
// transform the collision location into the models local space (from world space) and add it as a hit.
matrix4x4d mtx = GetOrient();
mtx.SetTranslate(GetPosition());
const matrix4x4d invmtx = mtx.Inverse();
const vector3d localPos = invmtx * contactData.pos;
GetShields()->AddHit(localPos);
m_stats.hull_mass_left -= dam;
Properties().Set("hullMassLeft", m_stats.hull_mass_left);
Properties().Set("hullPercent", 100.0f * (m_stats.hull_mass_left / float(m_type->hullMass)));
if (m_stats.hull_mass_left < 0) {
if (attacker) {
LuaEvent::Queue("onShipDestroyed", this, attacker);
}
Explode();
} else {
if (Pi::rng.Double() < kgDamage)
SfxManager::Add(this, TYPE_DAMAGE);
if (dam > float(GetShipType()->hullMass / 1000.)) {
if (dam < 0.01 * float(GetShipType()->hullMass))
Sound::BodyMakeNoise(this, "Hull_hit_Small", 1.0f);
else
Sound::BodyMakeNoise(this, "Hull_Hit_Medium", 1.0f);
}
}
}
//Output("Ouch! %s took %.1f kilos of damage from %s! (%.1f t hull left)\n", GetLabel().c_str(), kgDamage, attacker->GetLabel().c_str(), m_stats.hull_mass_left);
return true;
}
bool Ship::OnCollision(Body *b, Uint32 flags, double relVel)
{
// Collision with SpaceStation docking surface is
// completely handled by SpaceStations, you only
// need to return a "true" value in order to trigger
// a bounce in Space::OnCollision
// NOTE: 0x10 is a special flag set on docking surfaces
if (b->IsType(ObjectType::SPACESTATION) && (flags & 0x10)) {
return true;
}
// hitting cargo scoop surface shouldn't do damage
int cargoscoop_cap = 0;
Properties().Get("cargo_scoop_cap", cargoscoop_cap);
if (cargoscoop_cap > 0 && b->IsType(ObjectType::CARGOBODY) && !b->IsDead()) {
LuaRef item = static_cast<CargoBody *>(b)->GetCargoType();
if (LuaObject<Ship>::CallMethod<int>(this, "AddEquip", item) > 0) { // try to add it to the ship cargo.
Pi::game->GetSpace()->KillBody(b);
if (this->IsType(ObjectType::PLAYER))
Pi::game->log->Add(stringf(Lang::CARGO_SCOOP_ACTIVE_1_TONNE_X_COLLECTED, formatarg("item", ScopedTable(item).CallMethod<std::string>("GetName"))));
// XXX SfxManager::Add(this, TYPE_SCOOP);
UpdateEquipStats();
return true;
}
if (this->IsType(ObjectType::PLAYER))
Pi::game->log->Add(Lang::CARGO_SCOOP_ATTEMPTED);
}
if (b->IsType(ObjectType::PLANET)) {
// geoms still enabled when landed
if (m_flightState != FLYING)
return false;
else {
if (GetVelocity().Length() < MAX_LANDING_SPEED) {
m_testLanded = true;
return true;
}
}
}
if (b->IsType(ObjectType::SHIP) ||
b->IsType(ObjectType::PLAYER) ||
b->IsType(ObjectType::SPACESTATION) ||
b->IsType(ObjectType::PLANET) ||
b->IsType(ObjectType::STAR) ||
b->IsType(ObjectType::CARGOBODY)) {
LuaEvent::Queue("onShipCollided", this, b);
}
return DynamicBody::OnCollision(b, flags, relVel);
}
//destroy ship in an explosion
void Ship::Explode()
{
if (m_invulnerable) return;
Pi::game->GetSpace()->KillBody(this);
if (this->GetFrame() == Pi::player->GetFrame()) {
SfxManager::AddExplosion(this);
Sound::BodyMakeNoise(this, "Explosion_1", 1.0f);
}
ClearThrusterState();
}
bool Ship::DoDamage(float kgDamage)
{
PROFILE_SCOPED()
if (m_invulnerable) {
return true;
}
if (!IsDead()) {
float dam = kgDamage * 0.01f;
if (m_stats.shield_mass_left > 0.0f) {
if (m_stats.shield_mass_left > dam) {
m_stats.shield_mass_left -= dam;
dam = 0;
} else {
dam -= m_stats.shield_mass_left;
m_stats.shield_mass_left = 0;
}
Properties().Set("shieldMassLeft", m_stats.shield_mass_left);
}
m_shieldCooldown = DEFAULT_SHIELD_COOLDOWN_TIME;
// create a collision location in the models local space and add it as a hit.
Random rnd;
rnd.seed(time(0));
const vector3d randPos(
rnd.Double() * 2.0 - 1.0,
rnd.Double() * 2.0 - 1.0,
rnd.Double() * 2.0 - 1.0);
GetShields()->AddHit(randPos * (GetPhysRadius() * 0.75));
m_stats.hull_mass_left -= dam;
Properties().Set("hullMassLeft", m_stats.hull_mass_left);
Properties().Set("hullPercent", 100.0f * (m_stats.hull_mass_left / float(m_type->hullMass)));
if (m_stats.hull_mass_left < 0) {
Explode();
} else {
if (Pi::rng.Double() < dam)
SfxManager::Add(this, TYPE_DAMAGE);
}
}
//Output("Ouch! %s took %.1f kilos of damage from %s! (%.1f t hull left)\n", GetLabel().c_str(), kgDamage, attacker->GetLabel().c_str(), m_stats.hull_mass_left);
return true;
}
void Ship::UpdateEquipStats()
{
PropertyMap &p = Properties();
m_stats.used_capacity = 0;
p.Get("mass_cap", m_stats.used_capacity);
m_stats.used_cargo = 0;
m_stats.free_capacity = m_type->capacity - m_stats.used_capacity;
m_stats.static_mass = m_stats.used_capacity + m_type->hullMass;
p.Set("usedCapacity", m_stats.used_capacity);
p.Set("freeCapacity", m_stats.free_capacity);
p.Set("totalMass", m_stats.static_mass);
p.Set("staticMass", m_stats.static_mass);
int shield_cap = 0;
p.Get("shield_cap", shield_cap);
m_stats.shield_mass = TONS_HULL_PER_SHIELD * float(shield_cap);
p.Set("shieldMass", m_stats.shield_mass);
UpdateFuelStats();
UpdateGunsStats();
unsigned int thruster_power_cap = 0;
p.Get("thruster_power_cap", thruster_power_cap);
const double power_mul = m_type->thrusterUpgrades[Clamp(thruster_power_cap, 0U, 3U)];
GetPropulsion()->SetThrustPowerMult(power_mul, m_type->linThrust, m_type->angThrust);
m_stats.hyperspace_range = m_stats.hyperspace_range_max = 0;
p.Set("hyperspaceRange", m_stats.hyperspace_range);
p.Set("maxHyperspaceRange", m_stats.hyperspace_range_max);
p.Get<int>("atmo_shield_cap", m_stats.atmo_shield_cap);
p.Get<int>("radar_cap", m_stats.radar_cap);
p.Get<int>("fuel_scoop_cap", m_stats.fuel_scoop_cap);
p.Get<int>("cargo_life_support_cap", m_stats.cargo_bay_life_support_cap);
p.Get<int>("hull_autorepair_cap", m_stats.hull_autorepair_cap);
}
void Ship::UpdateLuaStats()
{
// This code cannot be in UpdateEquipStats itself because *Equip* needs to be
// called in Init(), which is itself called in the constructor, but we absolutely
// cannot use LuaObject<Ship>::* in a constructor, or else we'd fix the type of the
// object to Ship forever, even though it could very well be a Player.
// Updates at Gen 2019: Indeed, this function has been removed from
// loading because loading is now a ctor: see Body.cpp
UpdateEquipStats();
PropertyMap &p = Properties();
m_stats.hyperspace_range = m_stats.hyperspace_range_max = 0;
int hyperclass = 0;
p.Get<int>("hyperclass_cap", hyperclass);
if (hyperclass) {
std::tie(m_stats.hyperspace_range_max, m_stats.hyperspace_range) =
LuaObject<Ship>::CallMethod<double, double>(this, "GetHyperspaceRange");
}
p.Set("hyperspaceRange", m_stats.hyperspace_range);
p.Set("maxHyperspaceRange", m_stats.hyperspace_range_max);
}
void Ship::UpdateGunsStats()
{
float cooler = 1.0f;
Properties().Get("laser_cooler_cap", cooler);
GetFixedGuns()->SetCoolingBoost(cooler);
for (int num = 0; num < 2; num++) {
std::string prefix(num ? "laser_rear_" : "laser_front_");
int damage = 0;
Properties().Get(prefix + "damage", damage);
if (!damage) {
GetFixedGuns()->UnMountGun(num);
} else {
Properties().PushLuaTable();
LuaTable prop(Lua::manager->GetLuaState(), -1);
const Color c(
prop.Get<float>(prefix + "rgba_r"),
prop.Get<float>(prefix + "rgba_g"),
prop.Get<float>(prefix + "rgba_b"),
prop.Get<float>(prefix + "rgba_a"));
const float heatrate = prop.Get<float>(prefix + "heatrate", 0.01f);
const float coolrate = prop.Get<float>(prefix + "coolrate", 0.01f);
const float lifespan = prop.Get<float>(prefix + "lifespan");
const float width = prop.Get<float>(prefix + "width");
const float length = prop.Get<float>(prefix + "length");
const bool mining = prop.Get<int>(prefix + "mining");
const float speed = prop.Get<float>(prefix + "speed");
const float recharge = prop.Get<float>(prefix + "rechargeTime");
const bool beam = prop.Get<int>(prefix + "beam");
GetFixedGuns()->MountGun(num, recharge, lifespan, damage, length, width, mining, c, speed, beam, heatrate, coolrate);
if (prop.Get<int>(prefix + "dual"))
GetFixedGuns()->IsDual(num, true);
else
GetFixedGuns()->IsDual(num, false);
lua_pop(prop.GetLua(), 1);
}
}
}
void Ship::UpdateFuelStats()
{
m_stats.fuel_tank_mass_left = GetPropulsion()->FuelTankMassLeft();
Properties().Set("fuelMassLeft", m_stats.fuel_tank_mass_left);
UpdateMass();
}
Ship::HyperjumpStatus Ship::CheckHyperjumpCapability() const
{
if (GetFlightState() == HYPERSPACE)
return HYPERJUMP_DRIVE_ACTIVE;
if (GetFlightState() != FLYING && GetFlightState() != JUMPING)
return HYPERJUMP_SAFETY_LOCKOUT;
return HYPERJUMP_OK;
}
Ship::HyperjumpStatus Ship::InitiateHyperjumpTo(const SystemPath &dest, int warmup_time, double duration, const HyperdriveSoundsTable &sounds, LuaRef checks)
{
if (!dest.HasValidSystem() || GetFlightState() != FLYING || warmup_time < 1)
return HYPERJUMP_SAFETY_LOCKOUT;
StarSystem *s = Pi::game->GetSpace()->GetStarSystem().Get();
if (s && s->GetPath().IsSameSystem(dest))
return HYPERJUMP_CURRENT_SYSTEM;
m_hyperspace.dest = dest;
m_hyperspace.countdown = warmup_time;
m_hyperspace.now = false;
m_hyperspace.duration = duration;
m_hyperspace.checks = checks;
m_hyperspace.sounds = sounds;
return Ship::HYPERJUMP_OK;
}
void Ship::AbortHyperjump()
{
m_hyperspace.countdown = 0;
m_hyperspace.now = false;
m_hyperspace.duration = 0;
m_hyperspace.checks = LuaRef();
}
float Ship::GetECMRechargeTime()
{
float ecm_recharge_cap = 0.f;
Properties().Get("ecm_recharge_cap", ecm_recharge_cap);
return ecm_recharge_cap;
}
Ship::ECMResult Ship::UseECM()
{
int ecm_power_cap = 0;
Properties().Get("ecm_power_cap", ecm_power_cap);
if (m_ecmRecharge > 0.0f) return ECM_RECHARGING;
if (ecm_power_cap > 0) {
Sound::BodyMakeNoise(this, "ECM", 1.0f);
m_ecmRecharge = GetECMRechargeTime();
// damage neaby missiles
const float ECM_RADIUS = 4000.0f;
Space::BodyNearList nearby = Pi::game->GetSpace()->GetBodiesMaybeNear(this, ECM_RADIUS);
for (Body *body : nearby) {
if (body->GetFrame() != GetFrame()) continue;
if (!body->IsType(ObjectType::MISSILE)) continue;
double dist = (body->GetPosition() - GetPosition()).Length();
if (dist < ECM_RADIUS) {
// increasing chance of destroying it with proximity
if (Pi::rng.Double() > (dist / ECM_RADIUS)) {
static_cast<Missile *>(body)->ECMAttack(ecm_power_cap);
}
}
}
return ECM_ACTIVATED;
} else
return ECM_NOT_INSTALLED;
}
Missile *Ship::SpawnMissile(ShipType::Id missile_type, int power)
{
if (GetFlightState() != FLYING)
return 0;
Missile *missile = new Missile(missile_type, this, power);
missile->SetOrient(GetOrient());
missile->SetFrame(GetFrame());
const vector3d pos = GetOrient() * vector3d(0, GetAabb().min.y - 10, GetAabb().min.z);
const vector3d vel = -40.0 * GetOrient().VectorZ();
missile->SetPosition(GetPosition() + pos);
missile->SetVelocity(GetVelocity() + vel);
Pi::game->GetSpace()->AddBody(missile);
return missile;
}
void Ship::SetFlightState(Ship::FlightState newState)
{
if (m_flightState == newState) return;
if (IsHyperspaceActive() && (newState != FLYING))
AbortHyperjump();
if (newState == FLYING) {
m_testLanded = false;
if (m_flightState == DOCKING || m_flightState == DOCKED)
onUndock.emit();
m_dockedWith = nullptr;
// lock thrusters on for amount of time needed to push us out of station
static const double MASS_LOCK_REFERENCE(40000.0); // based purely on experimentation
// limit the time to between 2.0 and 20.0 seconds of thrust, the player can override
m_launchLockTimeout = std::min(std::max(2.0, 2.0 * (GetMass() / MASS_LOCK_REFERENCE)), 20.0);
}
if (newState == DOCKED) {
m_launchLockTimeout = 0.0;
ClearLinThrusterState();
ClearAngThrusterState();
}
m_flightState = newState;
Properties().Set("flightState", EnumStrings::GetString("ShipFlightState", m_flightState));
switch (m_flightState) {
case FLYING:
SetMoving(true);
SetColliding(true);
SetStatic(false);
break;
case DOCKING:
SetMoving(false);
SetColliding(false);
SetStatic(false);
break;
case UNDOCKING:
SetMoving(false);
SetColliding(false);
SetStatic(false);
break;
// TODO: set collision index? dynamic stations... use landed for open-air?
case DOCKED:
SetMoving(false);
SetColliding(false);
SetStatic(false);
break;
case LANDED:
SetMoving(false);
SetColliding(true);
SetStatic(true);
break;
case JUMPING:
SetMoving(true);
SetColliding(false);
SetStatic(false);
break;
case HYPERSPACE:
SetMoving(false);
SetColliding(false);
SetStatic(false);
break;
}
}
void Ship::Blastoff()
{
if (m_flightState != LANDED) return;
vector3d up = GetPosition().Normalized();
Frame *f = Frame::GetFrame(GetFrame());
assert(f->GetBody()->IsType(ObjectType::PLANET));
const double planetRadius = 2.0 + static_cast<Planet *>(f->GetBody())->GetTerrainHeight(up);
SetVelocity(vector3d(0, 0, 0));
SetAngVelocity(vector3d(0, 0, 0));
SetFlightState(FLYING);
SetPosition(up * planetRadius - GetAabb().min.y * up);
SetThrusterState(1, 1.0); // thrust upwards
LuaEvent::Queue("onShipTakeOff", this, f->GetBody());
}
void Ship::TestLanded()
{
PROFILE_SCOPED()
m_testLanded = false;
if (m_launchLockTimeout > 0.0f) return;
if (m_wheelState < 1.0f) return;
Frame *f = Frame::GetFrame(GetFrame());
if (f->GetBody()->IsType(ObjectType::PLANET)) {
double speed = GetVelocity().Length();
vector3d up = GetPosition().Normalized();
const double planetRadius = static_cast<Planet *>(f->GetBody())->GetTerrainHeight(up);
if (speed < MAX_LANDING_SPEED) {
// check player is sortof sensibly oriented for landing
if (GetOrient().VectorY().Dot(up) > 0.99) {
// position at zero altitude
SetPosition(up * (planetRadius - GetAabb().min.y));
// position facing in roughly the same direction
vector3d right = up.Cross(GetOrient().VectorZ()).Normalized();
SetOrient(matrix3x3d::FromVectors(right, up));
SetVelocity(vector3d(0, 0, 0));
SetAngVelocity(vector3d(0, 0, 0));
ClearThrusterState();
SetFlightState(LANDED);
Sound::BodyMakeNoise(this, "Rough_Landing", 1.0f);
LuaEvent::Queue("onShipLanded", this, f->GetBody());
onLanded.emit();
}
}
}
}
void Ship::SetLandedOn(Planet *p, float latitude, float longitude)
{
m_wheelTransition = 0;
m_wheelState = 1.0f;
m_forceWheelUpdate = true;
Frame *f_non_rot = Frame::GetFrame(p->GetFrame());
SetFrame(f_non_rot->GetRotFrame());
vector3d up = vector3d(cos(latitude) * sin(longitude), sin(latitude), cos(latitude) * cos(longitude));
const double planetRadius = p->GetTerrainHeight(up);
SetPosition(up * (planetRadius - GetAabb().min.y));
vector3d right = up.Cross(vector3d(0, 0, 1)).Normalized();
SetOrient(matrix3x3d::FromVectors(right, up));
SetVelocity(vector3d(0, 0, 0));
SetAngVelocity(vector3d(0, 0, 0));
ClearThrusterState();
SetFlightState(LANDED);
LuaEvent::Queue("onShipLanded", this, p);
onLanded.emit();
}
void Ship::SetFrame(FrameId fId)
{
DynamicBody::SetFrame(fId);
m_sensors->ResetTrails();
}
void Ship::TimeStepUpdate(const float timeStep)
{
PROFILE_SCOPED()
// If docked, station is responsible for updating position/orient of ship
// but we call this crap anyway and hope it doesn't do anything bad
const vector3d thrust = GetPropulsion()->GetActualLinThrust();
AddRelForce(thrust);
AddRelTorque(GetPropulsion()->GetActualAngThrust());
//apply extra atmospheric flight forces
AddTorque(CalcAtmoTorque());
if (m_landingGearAnimation) {
m_landingGearAnimation->SetProgress(m_wheelState);
if (m_forceWheelUpdate) {
m_landingGearAnimation->Interpolate();
m_forceWheelUpdate = false;
}
}
m_dragCoeff = DynamicBody::DEFAULT_DRAG_COEFF * (1.0 + 0.25 * m_wheelState);
DynamicBody::TimeStepUpdate(timeStep);
// fuel use decreases mass, so do this as the last thing in the frame
UpdateFuel(timeStep);
m_navLights->SetEnabled(m_wheelState > 0.01f);
m_navLights->Update(timeStep);
if (m_sensors.get()) m_sensors->Update(timeStep);
}
void Ship::DoThrusterSounds() const
{
// XXX any ship being the current camera body should emit sounds
// also, ship sounds could be split to internal and external sounds
// XXX sound logic could be part of a bigger class (ship internal sounds)
/* Ship engine noise. less loud inside */
float v_env = (Pi::game->GetWorldView()->shipView->IsExteriorView() ? 1.0f : 0.5f) * Sound::GetSfxVolume();
static Sound::Event sndev;
float volBoth = 0.0f;
volBoth += 0.5f * fabs(GetPropulsion()->GetLinThrusterState().y);
volBoth += 0.5f * fabs(GetPropulsion()->GetLinThrusterState().z);
float targetVol[2] = { volBoth, volBoth };
if (GetPropulsion()->GetLinThrusterState().x > 0.0)
targetVol[0] += 0.5f * float(GetPropulsion()->GetLinThrusterState().x);
else
targetVol[1] += -0.5f * float(GetPropulsion()->GetLinThrusterState().x);
targetVol[0] = v_env * Clamp(targetVol[0], 0.0f, 1.0f);
targetVol[1] = v_env * Clamp(targetVol[1], 0.0f, 1.0f);
float dv_dt[2] = { 4.0f, 4.0f };
if (!sndev.VolumeAnimate(targetVol, dv_dt)) {
sndev.Play("Thruster_large", 0.0f, 0.0f, Sound::OP_REPEAT);
sndev.VolumeAnimate(targetVol, dv_dt);
}
float angthrust = 0.1f * v_env * float(GetPropulsion()->GetAngThrusterState().Length());
static Sound::Event angThrustSnd;
if (!angThrustSnd.VolumeAnimate(angthrust, angthrust, 5.0f, 5.0f)) {
angThrustSnd.Play("Thruster_Small", 0.0f, 0.0f, Sound::OP_REPEAT);
angThrustSnd.VolumeAnimate(angthrust, angthrust, 5.0f, 5.0f);
}
}
// for timestep changes, to stop autopilot overshoot
// either adds half of current accel if decelerating
void Ship::TimeAccelAdjust(const float timeStep)
{
if (!AIIsActive()) return;
#ifdef DEBUG_AUTOPILOT
if (this->IsType(ObjectType::PLAYER))
Output("Time accel adjustment, step = %.1f, decel = %s\n", double(timeStep),
m_decelerating ? "true" : "false");
#endif
vector3d vdiff = double(timeStep) * GetLastForce() * (1.0 / GetMass());
if (!m_decelerating) vdiff = -2.0 * vdiff;
SetVelocity(GetVelocity() + vdiff);
}
double Ship::GetHullTemperature() const
{
// TODO: fix this to calculate appropriate skin friction and heating.
//const double dragCoeff = DynamicBody::DEFAULT_DRAG_COEFF * 1.25;
//const double dragGs = CalcAtmosphericDrag(GetVelocity().LengthSqr(), GetClipRadius(), dragCoeff) / (GetMass() * 9.81);
//return dragGs / 25.0;
// TODO: fix this to properly account for heating due to air friction instead of G-force.
double dragGs = GetAtmosForce().Length() / (GetMass() * 9.81);
return dragGs / (15.0 * (1.0 + m_stats.atmo_shield_cap + (2.0 * (1.0 - m_wheelState))));
}
void Ship::SetAlertState(AlertState as)
{
m_alertState = as;
Properties().Set("alertStatus", EnumStrings::GetString("ShipAlertStatus", as));
}
void Ship::UpdateAlertState()
{
// no alerts if no radar
if (m_stats.radar_cap <= 0) {
// clear existing alert state if there was one
if (GetAlertState() != ALERT_NONE) {
SetAlertState(ALERT_NONE);
LuaEvent::Queue("onShipAlertChanged", this, EnumStrings::GetString("ShipAlertStatus", ALERT_NONE));
}
return;
}
bool ship_is_near = false, ship_is_firing = false, missile_detected = false;
// sanity check: m_lastAlertUpdate should not be in the future.
// reset and re-check if it is.
if (m_lastAlertUpdate > Pi::game->GetTime()) {
m_lastAlertUpdate = 0;
m_shipNear = false;
m_shipFiring = false;
m_missileDetected = false;
}
if (m_lastAlertUpdate + 1.0 <= Pi::game->GetTime()) {
// time to update the list again, once per second should suffice
m_lastAlertUpdate = Pi::game->GetTime();
static const double ALERT_DISTANCE = 100000.0; // 100km
Space::BodyNearList nearbyBodies = Pi::game->GetSpace()->GetBodiesMaybeNear(this, ALERT_DISTANCE);
// handle the results
for (auto i : nearbyBodies) {
if ((i) == this) continue;
if ((i)->IsType(ObjectType::SHIP)) {
// TODO: Here there were a const on Ship*, now it cannot remain because of ship->firing and so, this open a breach...
// A solution is to put a member on ship: true if is firing, false if is not
Ship *ship = static_cast<Ship *>(i);
if (ship->GetShipType()->tag == ShipType::TAG_STATIC_SHIP) continue;
if (ship->GetFlightState() == LANDED || ship->GetFlightState() == DOCKED) continue;
if (GetPositionRelTo(ship).LengthSqr() < ALERT_DISTANCE * ALERT_DISTANCE) {
ship_is_near = true;
Uint32 gunstate = ship->GetFixedGuns()->IsFiring();
if (gunstate) {
ship_is_firing = true;
break;
}
}
} else if ((i)->IsType(ObjectType::MISSILE)) {
Missile *missile = static_cast<Missile *>(i);
if (missile->GetOwner() != this) {
if (GetPositionRelTo(missile).LengthSqr() < ALERT_DISTANCE * ALERT_DISTANCE) {
missile_detected = true;
break;
}
}
}
}
// store
m_shipNear = ship_is_near;
m_shipFiring = ship_is_firing;
m_missileDetected = missile_detected;
} else {
ship_is_near = m_shipNear;
ship_is_firing = m_shipFiring;
missile_detected = m_missileDetected;
}
bool changed = false;
switch (m_alertState) {
case ALERT_NONE:
if (ship_is_near) {
SetAlertState(ALERT_SHIP_NEARBY);
changed = true;
}
if (ship_is_firing) {
m_lastFiringAlert = Pi::game->GetTime();
SetAlertState(ALERT_SHIP_FIRING);
changed = true;
}
if (missile_detected) {
m_lastFiringAlert = Pi::game->GetTime();
SetAlertState(ALERT_MISSILE_DETECTED);
changed = true;
}
break;
case ALERT_SHIP_NEARBY:
if (!ship_is_near && !missile_detected) {
SetAlertState(ALERT_NONE);
changed = true;
} else if (ship_is_firing) {
m_lastFiringAlert = Pi::game->GetTime();
SetAlertState(ALERT_SHIP_FIRING);
changed = true;
} else if (missile_detected) {
m_lastFiringAlert = Pi::game->GetTime();
SetAlertState(ALERT_MISSILE_DETECTED);
changed = true;
}
break;
case ALERT_SHIP_FIRING:
case ALERT_MISSILE_DETECTED:
if (!ship_is_near && !missile_detected) {
SetAlertState(ALERT_NONE);
changed = true;
} else if (ship_is_firing || missile_detected) {
m_lastFiringAlert = Pi::game->GetTime();
} else if (m_lastFiringAlert + 60.0 <= Pi::game->GetTime()) {
SetAlertState(ALERT_SHIP_NEARBY);
changed = true;
}
break;
}
if (changed)
LuaEvent::Queue("onShipAlertChanged", this, EnumStrings::GetString("ShipAlertStatus", GetAlertState()));
}
void Ship::UpdateFuel(const float timeStep)
{
GetPropulsion()->UpdateFuel(timeStep);
UpdateFuelStats();
Properties().Set("fuel", GetFuel() * 100); // XXX to match SetFuelPercent
if (GetPropulsion()->IsFuelStateChanged())
LuaEvent::Queue("onShipFuelChanged", this, EnumStrings::GetString("PropulsionFuelStatus", GetPropulsion()->GetFuelState()));
}
void Ship::StaticUpdate(const float timeStep)
{
PROFILE_SCOPED()
// do player sounds before dead check, so they also turn off
if (IsType(ObjectType::PLAYER)) DoThrusterSounds();
if (IsDead()) return;
if (m_controller) m_controller->StaticUpdate(timeStep);
const double hullTemp = GetHullTemperature();
if (hullTemp > 1.0) {
DoDamage(hullTemp);
}
if (m_flightState == FLYING) {
Frame *frame = Frame::GetFrame(GetFrame());
Body *astro = frame->GetBody();
if (astro && astro->IsType(ObjectType::PLANET)) {
Planet *p = static_cast<Planet *>(astro);
double dist = GetPosition().Length();
double pressure, density;
p->GetAtmosphericState(dist, &pressure, &density);
int atmo_shield_cap = std::max(m_stats.atmo_shield_cap, 1); // needs to have some shielding by default
if (pressure > (m_type->atmosphericPressureLimit * atmo_shield_cap)) {
float damage = float(pressure - m_type->atmosphericPressureLimit);
DoDamage(damage);
}
}
}
UpdateAlertState();
/* FUEL SCOOPING!!!!!!!!! */
if (m_flightState == FLYING && m_stats.fuel_scoop_cap > 0) {
// TODO: this should probably be in Lua instead of in C++
// Needs a reliable way to schedule callbacks at ship creation
Frame *frame = Frame::GetFrame(GetFrame());
Body *astro = frame->GetBody();
if (astro && astro->IsType(ObjectType::PLANET)) {
Planet *p = static_cast<Planet *>(astro);
if (p->GetSystemBody()->IsScoopable()) {
const double dist = GetPosition().Length();
double pressure, density;
p->GetAtmosphericState(dist, &pressure, &density);
const double speed = GetVelocity().Length();
const vector3d vdir = GetVelocity().Normalized();
const vector3d pdir = -GetOrient().VectorZ();
const double dot = vdir.Dot(pdir);
if ((m_stats.free_capacity) && (dot > 0.90) && (speed > 1000.0) && (density > 0.5)) {
const double rate = speed * density * 0.00000333 * double(m_stats.fuel_scoop_cap);
if (Pi::rng.Double() < rate) {
lua_State *l = Lua::manager->GetLuaState();
pi_lua_import(l, "Equipment");
LuaTable hydrogen = LuaTable(l, -1).Sub("cargo").Sub("hydrogen");
LuaObject<Ship>::CallMethod(this, "AddEquip", hydrogen);
UpdateEquipStats();
if (this->IsType(ObjectType::PLAYER)) {
Pi::game->log->Add(stringf(Lang::FUEL_SCOOP_ACTIVE_N_TONNES_H_COLLECTED,
formatarg("quantity", LuaObject<Ship>::CallMethod<int>(this, "CountEquip", hydrogen))));
}
lua_pop(l, 3);
}
}
}
}
}
// Cargo bay life support
// TODO: this should be run in Lua
if (!m_stats.cargo_bay_life_support_cap) {
// Hull is pressure-sealed, it just doesn't provide
// temperature regulation and breathable atmosphere
// kill stuff roughly every 5 seconds
if ((!m_dockedWith) && (5.0 * Pi::rng.Double() < timeStep)) {
std::string t(Pi::rng.Int32(2) ? "live_animals" : "slaves");
lua_State *l = Lua::manager->GetLuaState();
pi_lua_import(l, "Equipment");
LuaTable cargo = LuaTable(l, -1).Sub("cargo");
if (LuaObject<Ship>::CallMethod<int>(this, "RemoveEquip", cargo.Sub(t))) {
LuaObject<Ship>::CallMethod<int>(this, "AddEquip", cargo.Sub("fertilizer"));
if (this->IsType(ObjectType::PLAYER)) {
Pi::game->log->Add(Lang::CARGO_BAY_LIFE_SUPPORT_LOST);
}
lua_pop(l, 4);
} else
lua_pop(l, 3);
}
}
if (m_flightState == FLYING)
m_launchLockTimeout -= timeStep;
if (m_launchLockTimeout < 0) m_launchLockTimeout = 0;
if (m_flightState == JUMPING || m_flightState == HYPERSPACE)
m_launchLockTimeout = 0;
// lasers
FixedGuns *fg = GetFixedGuns();
fg->UpdateGuns(timeStep);
for (int i = 0; i < 2; i++) {
if (fg->Fire(i, this)) {
if (fg->IsBeam(i)) {
float vl, vr;
Sound::CalculateStereo(this, 1.0f, &vl, &vr);
m_beamLaser[i].Play("Beam_laser", vl, vr, Sound::OP_REPEAT);
} else {
Sound::BodyMakeNoise(this, "Pulse_Laser", 1.0f);
}
LuaEvent::Queue("onShipFiring", this);
}
if (fg->IsBeam(i)) {
if (fg->IsFiring(i)) {
float vl, vr;
Sound::CalculateStereo(this, 1.0f, &vl, &vr);
if (!m_beamLaser[i].IsPlaying()) {
m_beamLaser[i].Play("Beam_laser", vl, vr, Sound::OP_REPEAT);
} else {
// update volume
m_beamLaser[i].SetVolume(vl, vr);
}
} else if (!fg->IsFiring(i) && m_beamLaser[i].IsPlaying()) {
m_beamLaser[i].Stop();
}
}
}
if (m_ecmRecharge > 0.0f) {
m_ecmRecharge = std::max(0.0f, m_ecmRecharge - timeStep);
}
if (m_shieldCooldown > 0.0f) {
m_shieldCooldown = std::max(0.0f, m_shieldCooldown - timeStep);
}
if (m_stats.shield_mass_left < m_stats.shield_mass) {
// 250 second recharge
float recharge_rate = 0.004f;
float booster = 1.0f;
Properties().Get("shield_energy_booster_cap", booster);
recharge_rate *= booster;
m_stats.shield_mass_left = Clamp(m_stats.shield_mass_left + m_stats.shield_mass * recharge_rate * timeStep, 0.0f, m_stats.shield_mass);
Properties().Set("shieldMassLeft", m_stats.shield_mass_left);
}
if (m_wheelTransition) {
m_wheelState += m_wheelTransition * 0.3f * timeStep;
m_wheelState = Clamp(m_wheelState, 0.0f, 1.0f);
if (is_equal_exact(m_wheelState, 0.0f) || is_equal_exact(m_wheelState, 1.0f)) {
m_wheelTransition = 0;
// TODO: this really needs to be driven by the animation; work around it by forcing an update for the last frame of the animation
m_forceWheelUpdate = true;
if (m_landingGearAnimation)
GetModel()->SetAnimationActive(GetModel()->FindAnimationIndex(m_landingGearAnimation), false);
}
}
if (m_testLanded) TestLanded();
if (m_stats.hull_autorepair_cap && m_stats.hull_mass_left < float(m_type->hullMass)) {
m_stats.hull_mass_left = std::min(m_stats.hull_mass_left + 0.1f * timeStep, float(m_type->hullMass));
Properties().Set("hullMassLeft", m_stats.hull_mass_left);
Properties().Set("hullPercent", 100.0f * (m_stats.hull_mass_left / float(m_type->hullMass)));
}
// After calling StartHyperspaceTo this Ship must not spawn objects
// holding references to it (eg missiles), as StartHyperspaceTo
// removes the ship from Space::bodies and so the missile will not
// have references to this cleared by NotifyRemoved()
if (m_hyperspace.now) {
m_hyperspace.now = false;
EnterHyperspace();
}
if (m_hyperspace.countdown > 0.0f) {
// Check the Lua function
bool abort = false;
lua_State *l = m_hyperspace.checks.GetLua();
if (l) {
m_hyperspace.checks.PushCopyToStack();
if (lua_isfunction(l, -1)) {
lua_call(l, 0, 1);
abort = !lua_toboolean(l, -1);
lua_pop(l, 1);
}
}
if (abort) {
AbortHyperjump();
} else {
m_hyperspace.countdown = m_hyperspace.countdown - timeStep;
if (!abort && m_hyperspace.countdown <= 0.0f && (is_equal_exact(m_wheelState, 0.0f))) {
m_hyperspace.countdown = 0;
m_hyperspace.now = true;
SetFlightState(JUMPING);
// We have to fire it here, because the event isn't actually fired until
// after the whole physics update, which means the flight state on next
// step would be HYPERSPACE, thus breaking quite a few things.
LuaEvent::Queue("onLeaveSystem", this);
} else if (!(is_equal_exact(m_wheelState, 0.0f)) && this->IsType(ObjectType::PLAYER)) {
AbortHyperjump();
Sound::BodyMakeNoise(this, "Missile_Inbound", 1.0f);
}
}
}
}
void Ship::NotifyRemoved(const Body *const removedBody)
{
if (m_curAICmd) m_curAICmd->OnDeleted(removedBody);
}
bool Ship::Undock()
{
return (m_dockedWith && m_dockedWith->LaunchShip(this, m_dockedWithPort));
}
void Ship::SetDockedWith(SpaceStation *s, int port)
{
if (s) {
m_dockedWith = s;
m_dockedWithPort = port;
m_wheelTransition = 0;
m_wheelState = 1.0f;
m_forceWheelUpdate = true;
// hand position/state responsibility over to station
m_dockedWith->SetDocked(this, port);
onDock.emit();
} else {
Undock();
}
}
void Ship::SetGunState(int idx, int state)
{
if (m_flightState != FLYING)
return;
GetFixedGuns()->SetGunFiringState(idx, state);
}
bool Ship::SetWheelState(bool down)
{
if (m_flightState != FLYING) return false;
if (is_equal_exact(m_wheelState, down ? 1.0f : 0.0f)) return false;
int newWheelTransition = (down ? 1 : -1);
if (newWheelTransition == m_wheelTransition) return false;
m_wheelTransition = newWheelTransition;
if (m_landingGearAnimation)
GetModel()->SetAnimationActive(GetModel()->FindAnimationIndex(m_landingGearAnimation), true);
return true;
}
void Ship::Render(Graphics::Renderer *renderer, const Camera *camera, const vector3d &viewCoords, const matrix4x4d &viewTransform)
{
if (IsDead()) return;
GetPropulsion()->Render(renderer, camera, viewCoords, viewTransform);
s_heatGradientParams.heatingMatrix = matrix3x3f(viewTransform.Inverse().GetOrient());
s_heatGradientParams.heatingNormal = vector3f(GetVelocity().Normalized());
s_heatGradientParams.heatingAmount = Clamp(GetHullTemperature(), 0.0, 1.0);
// This has to be done per-model with a shield and just before it's rendered
const bool shieldsVisible = m_shieldCooldown > 0.01f && m_stats.shield_mass_left > (m_stats.shield_mass / 100.0f);
GetShields()->SetEnabled(shieldsVisible);
GetShields()->Update(m_shieldCooldown, 0.01f * GetPercentShields());
//strncpy(params.pText[0], GetLabel().c_str(), sizeof(params.pText));
RenderModel(renderer, camera, viewCoords, viewTransform);
m_navLights->Render(renderer);
renderer->GetStats().AddToStatCount(Graphics::Stats::STAT_SHIPS, 1);
if (m_ecmRecharge > 0.0f) {
// ECM effect: a cloud of particles for a sparkly effect
vector3f v[100];
for (int i = 0; i < 100; i++) {
const double r1 = Pi::rng.Double() - 0.5;
const double r2 = Pi::rng.Double() - 0.5;
const double r3 = Pi::rng.Double() - 0.5;
v[i] = vector3f(GetPhysRadius() * vector3d(r1, r2, r3).NormalizedSafe());
}
Color c(128, 128, 255, 255);
float totalRechargeTime = GetECMRechargeTime();
if (totalRechargeTime >= 0.0f) {
c.a = (m_ecmRecharge / totalRechargeTime) * 255;
}
SfxManager::ecmParticle->diffuse = c;
matrix4x4f t;
for (int i = 0; i < 12; i++)
t[i] = float(viewTransform[i]);
t[12] = viewCoords.x;
t[13] = viewCoords.y;
t[14] = viewCoords.z;
t[15] = 1.0f;
renderer->SetTransform(t);
renderer->DrawPointSprites(100, v, SfxManager::additiveAlphaState, SfxManager::ecmParticle.get(), 50.f);
}
}
bool Ship::SpawnCargo(CargoBody *c_body) const
{
if (m_flightState != FLYING) return false;
vector3d pos = GetOrient() * vector3d(0, GetAabb().min.y - 5, 0);
c_body->SetFrame(GetFrame());
c_body->SetPosition(GetPosition() + pos);
c_body->SetVelocity(GetVelocity() + GetOrient() * vector3d(0, -10, 0));
Pi::game->GetSpace()->AddBody(c_body);
return true;
}
void Ship::EnterHyperspace()
{
assert(GetFlightState() != Ship::HYPERSPACE);
// Is it still a good idea, with the onLeaveSystem moved elsewhere?
Ship::HyperjumpStatus status = CheckHyperjumpCapability();
if (status != HYPERJUMP_OK && status != HYPERJUMP_INITIATED) {
if (m_flightState == JUMPING)
SetFlightState(FLYING);
return;
}
SetFlightState(Ship::HYPERSPACE);
// virtual call, do class-specific things
OnEnterHyperspace();
}
void Ship::OnEnterHyperspace()
{
Sound::BodyMakeNoise(this, m_hyperspace.sounds.jump_sound.c_str(), 1.f);
m_hyperspaceCloud = new HyperspaceCloud(this, Pi::game->GetTime() + m_hyperspace.duration, false);
m_hyperspaceCloud->SetFrame(GetFrame());
m_hyperspaceCloud->SetPosition(GetPosition());
Space *space = Pi::game->GetSpace();
space->RemoveBody(this);
space->AddBody(m_hyperspaceCloud);
}
void Ship::EnterSystem()
{
PROFILE_SCOPED()
assert(GetFlightState() == Ship::HYPERSPACE);
// virtual call, do class-specific things
OnEnterSystem();
SetFlightState(Ship::FLYING);
LuaEvent::Queue("onEnterSystem", this);
}
void Ship::OnEnterSystem()
{
m_hyperspaceCloud = 0;
}
void Ship::SetShipId(const ShipType::Id &shipId)
{
m_type = &ShipType::types[shipId];
Properties().Set("shipId", shipId);
}
void Ship::SetShipType(const ShipType::Id &shipId)
{
// clear all equipment so that any relevant capability properties (or other data) is wiped
ScopedTable(m_equipSet).CallMethod("Clear", this);
SetShipId(shipId);
SetModel(m_type->modelName.c_str());
m_skin.SetDecal(m_type->manufacturer);
m_skin.Apply(GetModel());
Init();
onFlavourChanged.emit();
if (IsType(ObjectType::PLAYER))
Pi::game->GetWorldView()->shipView->GetCameraController()->Reset();
InitEquipSet();
LuaEvent::Queue("onShipTypeChanged", this);
}
void Ship::SetLabel(const std::string &label)
{
DynamicBody::SetLabel(label);
m_skin.SetLabel(label);
m_skin.Apply(GetModel());
}
void Ship::SetShipName(const std::string &shipName)
{
m_shipName = shipName;
Properties().Set("shipName", shipName);
}
void Ship::SetSkin(const SceneGraph::ModelSkin &skin)
{
m_skin = skin;
m_skin.Apply(GetModel());
}
void Ship::SetPattern(const unsigned int num)
{
GetModel()->SetPattern(num);
}
Uint8 Ship::GetRelations(Body *other) const
{
auto it = m_relationsMap.find(other);
if (it != m_relationsMap.end())
return it->second;
return 50;
}
void Ship::SetRelations(Body *other, Uint8 percent)
{
m_relationsMap[other] = percent;
if (m_sensors.get()) m_sensors->UpdateIFF(other);
}