diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index 989feb2936..df38a96fe1 100644 --- a/data/libs/EquipType.lua +++ b/data/libs/EquipType.lua @@ -548,6 +548,12 @@ local ThrusterType = utils.inherits(EquipType, "Equipment.ThrusterType") --============================================================================== +---@class Equipment.MissileType : EquipType +---@field missile_stats table +local MissileType = utils.inherits(EquipType, "Equipment.MissileType") + +--============================================================================== + Serializer:RegisterClass("EquipType", EquipType) Serializer:RegisterClass("Equipment.LaserType", LaserType) Serializer:RegisterClass("Equipment.HyperdriveType", HyperdriveType) @@ -555,6 +561,7 @@ Serializer:RegisterClass("Equipment.SensorType", SensorType) Serializer:RegisterClass("Equipment.BodyScannerType", BodyScannerType) Serializer:RegisterClass("Equipment.CabinType", CabinType) Serializer:RegisterClass("Equipment.ThrusterType", ThrusterType) +Serializer:RegisterClass("Equipment.MissileType", MissileType) EquipType:SetupPrototype() LaserType:SetupPrototype() @@ -563,6 +570,7 @@ SensorType:SetupPrototype() BodyScannerType:SetupPrototype() CabinType:SetupPrototype() ThrusterType:SetupPrototype() +MissileType:SetupPrototype() return { laser = laser, @@ -575,4 +583,5 @@ return { BodyScannerType = BodyScannerType, CabinType = CabinType, ThrusterType = ThrusterType, + MissileType = MissileType, } diff --git a/data/libs/Ship.lua b/data/libs/Ship.lua index 56dda8fb5a..995c5fadf4 100644 --- a/data/libs/Ship.lua +++ b/data/libs/Ship.lua @@ -307,11 +307,10 @@ function Ship:FireMissileAt(missile, target) -- FIXME: handle multiple-count missile mounts equipSet:Remove(missile) - local missile_object = self:SpawnMissile(missile.missile_type) + local missile_object = self:SpawnMissile(missile.missile_stats, target) if missile_object then if target then - missile_object:AIKamikaze(target) Event.Queue("onShipFiring", self) end -- Let's keep a safe distance before activating this device, shall we ? diff --git a/data/meta/CoreObject/Ship.meta.lua b/data/meta/CoreObject/Ship.meta.lua index 8e2b14e7da..1dcfb00b60 100644 --- a/data/meta/CoreObject/Ship.meta.lua +++ b/data/meta/CoreObject/Ship.meta.lua @@ -131,3 +131,9 @@ function Ship:IsLanded() end -- Get the starport this ship is docked with, if any ---@return SpaceStation? function Ship:GetDockedWith() end + +-- Spawn a new missile from this ship +---@param stats table Information about the missile to spawn. Must include a shipType: string field +---@param target Body? Optional body to target with the missile +---@return Body? missile The spawned missile if valid +function Ship:SpawnMissile(stats, target) end diff --git a/data/modules/Debug/DebugShip.lua b/data/modules/Debug/DebugShip.lua index 3dad4f90c9..8631a47d55 100644 --- a/data/modules/Debug/DebugShip.lua +++ b/data/modules/Debug/DebugShip.lua @@ -41,17 +41,19 @@ local templateOptions = { } local missileOptions = { - "Guided Missile", - "Unguided Missile", - "Smart Missile", - "Naval Missile" + "S1 Guided Missile", + "S1 Unguided Missile", + "S2 Guided Missile", + "S3 Smart Missile", + "S4 Naval Missile" } local missileTypes = { - "missile_guided", - "missile_unguided", - "missile_smart", - "missile_naval", + "missile.guided_s1", + "missile.unguided_s1", + "missile.guided_s2", + "missile.smart_s3", + "missile.naval_s4", } ---@type HullConfig[] @@ -221,15 +223,21 @@ end function DebugShipTool:onSpawnMissile() - local missile_type = missileTypes[self.missileIdx] + local missile_type = require 'Equipment'.Get(missileTypes[self.missileIdx]) --[[@as Equipment.MissileType?]] - if missile_type ~= "missile_unguided" and not Game.player:GetCombatTarget() then + if not missile_type then + Notification.add(Notification.Type.Error, "No missile equipment {}" % { missileTypes[self.missileIdx] }) + return + end + + if missile_type.missile_stats.guided and not Game.player:GetCombatTarget() then Notification.add(Notification.Type.Error, "Debug: no target for {}" % { missileOptions[self.missileIdx] }) return end - local missile = Game.player:SpawnMissile(missile_type) - missile:AIKamikaze(Game.player:GetCombatTarget()) + local missile = Game.player:SpawnMissile(missile_type.missile_stats, Game.player:GetCombatTarget()) + + if not missile then return end Timer:CallAt(Game.time + 1, function() if missile:exists() then diff --git a/data/modules/Equipment/Weapons.lua b/data/modules/Equipment/Weapons.lua index f08ba8d9c1..6336968e5f 100644 --- a/data/modules/Equipment/Weapons.lua +++ b/data/modules/Equipment/Weapons.lua @@ -7,6 +7,7 @@ local Slot = require 'HullConfig'.Slot local EquipType = EquipTypes.EquipType local LaserType = EquipTypes.LaserType +local MissileType = EquipTypes.MissileType --=============================================== -- Pulse Cannons @@ -199,46 +200,46 @@ Equipment.Register("laser.miningcannon_17mw", LaserType.New { -- Missiles --=============================================== -Equipment.Register("missile.unguided_s1", EquipType.New { +Equipment.Register("missile.unguided_s1", MissileType.New { l10n_key="MISSILE_UNGUIDED", price=30, purchasable=true, tech_level=1, - missile_type="missile_unguided", + missile_stats = { shipType="missile_unguided", guided=false, warheadSize=200.0, fuzeRadius=100.0, effectiveRadius=1000.0, chargeEffectiveness=1.0, ecmResist=5.0 }, volume=0, mass=0.045, slot = { type="missile", size=1, hardpoint=true }, icon_name="equip_missile_unguided" }) -- Approximately equivalent in size to an R60M / AA-8 'Aphid' -Equipment.Register("missile.guided_s1", EquipType.New { +Equipment.Register("missile.guided_s1", MissileType.New { l10n_key="MISSILE_GUIDED", price=45, purchasable=true, tech_level=5, - missile_type="missile_guided", + missile_stats = { shipType="missile_guided", guided=true, warheadSize=125.0, fuzeRadius=30.0, effectiveRadius=800.0, chargeEffectiveness=3.0, ecmResist=1.0 }, volume=0, mass=0.065, slot = { type="missile", size=1, hardpoint=true }, icon_name="equip_missile_guided" }) -- Approximately equivalent in size to an R73 / AA-11 'Archer' -Equipment.Register("missile.guided_s2", EquipType.New { +Equipment.Register("missile.guided_s2", MissileType.New { l10n_key="MISSILE_GUIDED", price=60, purchasable=true, tech_level=5, - missile_type="missile_guided", + missile_stats = { shipType="missile_guided", guided=true, warheadSize=200.0, fuzeRadius=40.0, effectiveRadius=1500.0, chargeEffectiveness=3.5, ecmResist=1.0 }, volume=0, mass=0.145, slot = { type="missile", size=2, hardpoint=true }, icon_name="equip_missile_guided" }) -- Approximately equivalent in size to an R77 / AA-12 'Adder' -Equipment.Register("missile.smart_s3", EquipType.New { +Equipment.Register("missile.smart_s3", MissileType.New { l10n_key="MISSILE_SMART", price=95, purchasable=true, tech_level=9, - missile_type="missile_smart", + missile_stats = { shipType="missile_smart", guided=true, warheadSize=320.0, fuzeRadius=35.0, effectiveRadius=2000.0, chargeEffectiveness=4.0, ecmResist=2.0 }, volume=0, mass=0.5, slot = { type="missile", size=3, hardpoint=true }, icon_name="equip_missile_smart" }) -- TBD -Equipment.Register("missile.naval_s4", EquipType.New { +Equipment.Register("missile.naval_s4", MissileType.New { l10n_key="MISSILE_NAVAL", price=160, purchasable=true, tech_level="MILITARY", - missile_type="missile_naval", + missile_stats = { shipType="missile_naval", guided=true, warheadSize=580.0, fuzeRadius=40.0, effectiveRadius=2000.0, chargeEffectiveness=4.5, ecmResist=3.0 }, volume=0, mass=1, slot = { type="missile", size=4, hardpoint=true }, icon_name="equip_missile_naval" diff --git a/data/pigui/modules/equipment.lua b/data/pigui/modules/equipment.lua index 19171c5bed..e0c0fd4f10 100644 --- a/data/pigui/modules/equipment.lua +++ b/data/pigui/modules/equipment.lua @@ -2,6 +2,7 @@ -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt local Engine = require 'Engine' +local Equipment = require 'Equipment' local Game = require 'Game' local utils = require 'utils' local Event = require 'Event' @@ -72,16 +73,9 @@ local function displayECM(uiPos) return uiPos end -local function getMissileIcon(missile) - if icons[missile.missile_type] then - return icons[missile.missile_type] - else - print("no icon for missile " .. missile.missile_type) - return icons.bullseye - end -end - -local function fireMissile(missile) +---@param player Player +---@param missile Equipment.MissileType +local function fireMissile(player, missile) if not player:GetCombatTarget() then Game.AddCommsLogLine(lc.SELECT_A_TARGET, "", 1) else @@ -90,28 +84,40 @@ local function fireMissile(missile) end local function displayMissiles(uiPos) - player = Game.player - local current_view = Game.CurrentView() + if Game.CurrentView() == "WorldView" then - if current_view == "WorldView" then + local paused = Game.paused + local docked = Game.player:GetDockedWith() + + local missiles = Game.player:GetComponent("EquipSet"):GetInstalledOfType("missile") --[[@as Equipment.MissileType[] ]] + + local groups = utils.automagic() - local missiles = player:GetComponent("EquipSet"):GetInstalledOfType("missile") - local count = {} - local types = {} + for i, missile in ipairs(missiles) do + local group = groups[missile.id] - for i,missile in ipairs(missiles) do - count[missile.missile_type] = (count[missile.missile_type] or 0) + 1 - types[missile.missile_type] = missile + group.count = (group.count or 0) + 1 + group.size = missile.slot.size + group.proto = missile:GetPrototype() + group.index = i end - for t,missile in pairs(types) do - local c = count[t] - local size,clicked = iconEqButton(uiPos, getMissileIcon(missile), true, mainWideIconSize, c, c == 0, mainBackgroundColor, mainForegroundColor, mainHoverColor, mainPressedColor, lec[missile.l10n_key]) + local display = utils.build_array(pairs(groups)) + table.sort(display, function(a, b) return + a.size < b.size or (a.size == b.size and (not a.guided and b.guided)) + end) + + for _, group in ipairs(display) do + local count = tostring(group.count) + + -- TODO: slot size indicators should have a translated string at some point + local tooltip = "{} (S{})" % { group.proto:GetName(), group.size } + local size, clicked = iconEqButton(uiPos, icons[group.proto.icon_name], false, mainIconSize, + count, false, mainBackgroundColor, mainForegroundColor, mainHoverColor, mainPressedColor, tooltip) uiPos.y = uiPos.y + size.y + 10 - if clicked then - print("firing missile " .. t) - fireMissile(missile) + if clicked and not paused and not docked then + fireMissile(Game.player, missiles[group.index]) end end diff --git a/src/Missile.cpp b/src/Missile.cpp index cffe36dd58..0cd21c0a4c 100644 --- a/src/Missile.cpp +++ b/src/Missile.cpp @@ -12,25 +12,25 @@ #include "ShipAICmd.h" #include "Space.h" #include "collider/CollisionContact.h" -#include "core/Log.h" #include "lua/LuaEvent.h" #include "ship/Propulsion.h" -Missile::Missile(const ShipType::Id &shipId, Body *owner, int power) +// For debugging +// #include "core/Log.h" +// #include "EnumStrings.h" + +Missile::Missile(Body *owner, const MissileDef &mdef) { + m_owner = owner; + m_missileStats = mdef; m_propulsion = AddComponent(); - if (power < 0) { - m_power = 0; - if (shipId == ShipType::MISSILE_GUIDED) m_power = 1; - if (shipId == ShipType::MISSILE_SMART) m_power = 2; - if (shipId == ShipType::MISSILE_NAVAL) m_power = 3; - } else - m_power = power; - - m_owner = owner; - m_type = &ShipType::types[shipId]; + m_type = &ShipType::types[mdef.shipType.c_str()]; + Init(); +} +void Missile::Init() +{ SetMass(m_type->hullMass * 1000); SetModel(m_type->modelName.c_str()); @@ -58,7 +58,14 @@ Missile::Missile(const Json &jsonObj, Space *space) : Json missileObj = jsonObj["missile"]; try { - m_type = &ShipType::types[missileObj["ship_type_id"]]; + m_missileStats.shipType = missileObj["ship_type_id"].get(); + m_missileStats.fuzeRadius = missileObj["fuze_radius"]; + m_missileStats.warheadSize = missileObj["warhead_size"]; + m_missileStats.effectiveRadius = missileObj["effective_radius"]; + m_missileStats.chargeEffectiveness = missileObj["shaped_charge"]; + m_missileStats.ecmResist = missileObj["power"]; + + m_type = &ShipType::types[m_missileStats.shipType.c_str()]; SetModel(m_type->modelName.c_str()); m_curAICmd = 0; @@ -66,7 +73,6 @@ Missile::Missile(const Json &jsonObj, Space *space) : m_aiMessage = AIError(missileObj["ai_message"]); m_ownerIndex = missileObj["index_for_body"]; - m_power = missileObj["power"]; m_armed = missileObj["armed"]; } catch (Json::type_error &) { throw SavedGameCorruptException(); @@ -85,7 +91,14 @@ void Missile::SaveToJson(Json &jsonObj, Space *space) missileObj["ai_message"] = int(m_aiMessage); missileObj["index_for_body"] = space->GetIndexForBody(m_owner); - missileObj["power"] = m_power; + + missileObj["ship_type_id"] = m_missileStats.shipType.sv(); + missileObj["fuze_radius"] = m_missileStats.fuzeRadius; + missileObj["warhead_size"] = m_missileStats.warheadSize; + missileObj["effective_radius"] = m_missileStats.effectiveRadius; + missileObj["shaped_charge"] = m_missileStats.chargeEffectiveness; + missileObj["power"] = m_missileStats.ecmResist; + missileObj["armed"] = m_armed; missileObj["ship_type_id"] = m_type->id; @@ -106,7 +119,7 @@ Missile::~Missile() void Missile::ECMAttack(int power_val) { - if (power_val > m_power) { + if (power_val > m_missileStats.ecmResist) { CollisionContact dummy; OnDamage(0, 1.0f, dummy); } @@ -160,7 +173,7 @@ void Missile::TimeStepUpdate(const float timeStep) DynamicBody::TimeStepUpdate(timeStep); m_propulsion->UpdateFuel(timeStep); - const float MISSILE_DETECTION_RADIUS = 100.0f; + const float MISSILE_DETECTION_RADIUS = m_missileStats.fuzeRadius; const float MISSILE_TRIGGER_RADIUS = 10.0f; const Body *target = GetTarget(); @@ -217,16 +230,8 @@ void Missile::Explode() { Pi::game->GetSpace()->KillBody(this); - // how much energy was converted in the explosion? - // defaults to 200kg of TNT - double mjYield = Properties().Get("missile_yield_cap").get_number(4.184 * 200); - // defaults to 2 km, this is sufficient for most explosions - double queryRadius = Properties().Get("missile_explosion_radius_cap").get_number(2000.0); - - // How effective is the blast at hitting a target compared to a omnidirectional warhead? - // defaults to 4x effectiveness, this is sufficient for most anti-ship missile explosions - double chargeShapeScalar = Properties().Get("missile_charge_effect_cap").get_number(4.0); + double queryRadius = m_missileStats.effectiveRadius; CollisionContact dummy; Space::BodyNearList nearby = Pi::game->GetSpace()->GetBodiesMaybeNear(this, queryRadius); @@ -244,19 +249,24 @@ void Missile::Explode() const double crossSectionTarget = calcAreaCircle(targetRadius); double ratioArea = crossSectionTarget / areaSphere; // compute ratio of areas to know how much energy was transfered to target - if (body == GetTarget()) // missiles have shaped-charge warheads to focus the blast towards the target - ratioArea = ratioArea * chargeShapeScalar; // assume the warhead is oriented towards the target correctly + // We assume missiles with shaped-charge warheads to focus the blast towards the target have the warhead oriented correctly + if (body == GetTarget()) + ratioArea = ratioArea * m_missileStats.chargeEffectiveness; - ratioArea = std::min(ratioArea, 1.0); // we must limit received energy to finite amount + ratioArea = std::min(ratioArea, 1.0); // we must limit received energy to finite amount - const double mjReceivedEnergy = ratioArea * mjYield; // compute received energy by blast + // Compute received energy using warhead size expressed as equivalent to kg of TNT + const double mjReceivedEnergy = ratioArea * m_missileStats.warheadSize * 4.184; double kgDamage = mjReceivedEnergy * 16.18033; // received energy back to damage in pioneer "kg" unit, using Phi*10 because we can if (kgDamage < 5.0) continue; // early-out if we're dealing a negligable amount of damage - // Log::Info("Missile impact on {} | {}\n\ttarget.radius={} dist={} sphereArea={} crossSection={} (ratio={}) => received energy {}mj={}kgD\n", - // body->GetLabel(), body->GetType(), targetRadius, dist, areaSphere, crossSectionTarget, ratioArea, mjReceivedEnergy, kgDamage); + /* + Log::Info("Missile impact on {} | {}\n\ttarget.radius={} dist={} sphereArea={} crossSection={} (ratio={}) => received energy {}mj={}kgD\n", + body->GetLabel(), EnumStrings::GetString("PhysicsObjectType", int(body->GetType())), + targetRadius, dist, areaSphere, crossSectionTarget, ratioArea, mjReceivedEnergy, kgDamage); + */ body->OnDamage(m_owner, kgDamage, dummy); if (body->IsType(ObjectType::SHIP)) diff --git a/src/Missile.h b/src/Missile.h index cc54984a56..b99818eb88 100644 --- a/src/Missile.h +++ b/src/Missile.h @@ -9,13 +9,33 @@ class AICommand; +struct MissileDef { + StringName shipType; + + // Distance in meters within which the missile will attempt to explode on a valid target + float fuzeRadius = 50.0; + // How much energy does the warhead have (in kg of TNT) + float warheadSize = 100.0; + // What is the effective radius within which the warhead can meaningfully damage something + float effectiveRadius = 2000.0; + // How effective is the blast at hitting a target compared to a omnidirectional warhead? + // defaults to 4x effectiveness, this is sufficient for most anti-ship missile explosions + // 1.0 = omnidirectional blast + // >1.0 = shaped charge in the direction of the target + float chargeEffectiveness = 4.0; + // How effective is the missile at resisting an ECM system? + float ecmResist = 1.0; +}; + class Missile : public DynamicBody { public: OBJDEF(Missile, DynamicBody, MISSILE); Missile() = delete; - Missile(const ShipType::Id &type, Body *owner, int power = -1); + Missile(Body *owner, const MissileDef &def); Missile(const Json &jsonObj, Space *space); virtual ~Missile(); + + void Init(); void StaticUpdate(const float timeStep) override; void TimeStepUpdate(const float timeStep) override; virtual bool OnCollision(Body *o, Uint32 flags, double relVel) override; @@ -41,12 +61,13 @@ class Missile : public DynamicBody { bool IsValidTarget(const Body *body); AICommand *m_curAICmd; - int m_power; Body *m_owner; bool m_armed; const ShipType *m_type; Propulsion *m_propulsion; + MissileDef m_missileStats; + int m_ownerIndex; // deserialisation }; diff --git a/src/Player.cpp b/src/Player.cpp index 99ffb2bfc7..216f2b77a2 100644 --- a/src/Player.cpp +++ b/src/Player.cpp @@ -125,9 +125,9 @@ bool Player::SetWheelState(bool down) } //XXX all ships should make this sound -Missile *Player::SpawnMissile(ShipType::Id missile_type, int power) +Missile *Player::SpawnMissile(const MissileDef &def, Body *target) { - Missile *m = Ship::SpawnMissile(missile_type, power); + Missile *m = Ship::SpawnMissile(def, target); if (m) Sound::PlaySfx("Missile_launch", 1.0f, 1.0f, 0); return m; diff --git a/src/Player.h b/src/Player.h index a9392c9694..7a7d80a72a 100644 --- a/src/Player.h +++ b/src/Player.h @@ -24,7 +24,7 @@ class Player : public Ship { virtual bool DoDamage(float kgDamage) override final; // overloaded to add "crush" audio virtual bool OnDamage(Body *attacker, float kgDamage, const CollisionContact &contactData) override; virtual bool SetWheelState(bool down) override; // returns success of state change, NOT state itself - virtual Missile *SpawnMissile(ShipType::Id missile_type, int power = -1) override; + virtual Missile *SpawnMissile(const MissileDef &, Body *) override; virtual void SetAlertState(Ship::AlertState as) override; virtual void NotifyRemoved(const Body *const removedBody) override; virtual bool ManualDocking() const override { return !AIIsActive(); } diff --git a/src/Ship.cpp b/src/Ship.cpp index d3c25fb175..9576ca7cf8 100644 --- a/src/Ship.cpp +++ b/src/Ship.cpp @@ -257,6 +257,7 @@ void Ship::PostLoadFixup(Space *space) m_dockedWith = static_cast(space->GetBodyByIndex(m_dockedWithIndex)); if (m_curAICmd) m_curAICmd->PostLoadFixup(space); m_controller->PostLoadFixup(space); + m_gunManager->PostLoadFixup(space); } void Ship::SaveToJson(Json &jsonObj, Space *space) @@ -741,12 +742,12 @@ Ship::ECMResult Ship::UseECM() return ECM_NOT_INSTALLED; } -Missile *Ship::SpawnMissile(ShipType::Id missile_type, int power) +Missile *Ship::SpawnMissile(const MissileDef &missileStats, Body *target) { if (GetFlightState() != FLYING) return 0; - Missile *missile = new Missile(missile_type, this, power); + Missile *missile = new Missile(this, missileStats); missile->SetOrient(GetOrient()); missile->SetFrame(GetFrame()); const vector3d pos = GetOrient() * vector3d(0, GetAabb().min.y - 10, GetAabb().min.z); @@ -754,6 +755,11 @@ Missile *Ship::SpawnMissile(ShipType::Id missile_type, int power) missile->SetPosition(GetPosition() + pos); missile->SetVelocity(GetVelocity() + vel); Pi::game->GetSpace()->AddBody(missile); + + if (target) { + missile->AIKamikaze(target); + } + return missile; } diff --git a/src/Ship.h b/src/Ship.h index 1fb2a207d5..b0b658bdee 100644 --- a/src/Ship.h +++ b/src/Ship.h @@ -21,6 +21,7 @@ class CargoBody; class SpaceStation; class HyperspaceCloud; class Missile; +struct MissileDef; class NavLights; class Planet; class Sensors; @@ -171,7 +172,7 @@ class Ship : public DynamicBody { ECMResult UseECM(); - virtual Missile *SpawnMissile(ShipType::Id missile_type, int power = -1); + virtual Missile *SpawnMissile(const MissileDef &missileStats, Body *target); enum AlertState { // ALERT_NONE, diff --git a/src/ShipType.cpp b/src/ShipType.cpp index 75ef841fb3..d5626b710a 100644 --- a/src/ShipType.cpp +++ b/src/ShipType.cpp @@ -17,10 +17,6 @@ std::vector ShipType::static_ships; std::vector ShipType::missile_ships; const std::string ShipType::POLICE = "kanara"; -const std::string ShipType::MISSILE_GUIDED = "missile_guided"; -const std::string ShipType::MISSILE_NAVAL = "missile_naval"; -const std::string ShipType::MISSILE_SMART = "missile_smart"; -const std::string ShipType::MISSILE_UNGUIDED = "missile_unguided"; float ShipType::GetFuelUseRate() const { diff --git a/src/ShipType.h b/src/ShipType.h index 6300278b85..ee31f98a0d 100644 --- a/src/ShipType.h +++ b/src/ShipType.h @@ -80,10 +80,6 @@ struct ShipType { float GetFuelUseRate() const; static const std::string POLICE; - static const std::string MISSILE_GUIDED; - static const std::string MISSILE_NAVAL; - static const std::string MISSILE_SMART; - static const std::string MISSILE_UNGUIDED; static std::map types; static std::vector player_ships; diff --git a/src/lua/LuaShip.cpp b/src/lua/LuaShip.cpp index fe69a37cd9..32fc67cf97 100644 --- a/src/lua/LuaShip.cpp +++ b/src/lua/LuaShip.cpp @@ -602,16 +602,14 @@ static int l_ship_blast_off(lua_State *l) * * Spawn a missile near the ship. * - * > missile = ship:SpawnMissile(type, target, power) + * > missile = ship:SpawnMissile(stats, target) * * Parameters: * - * shiptype - a string for the missile type. specifying an - * ship that is not a missile will result in a Lua error + * shiptype - A table containing information about the missile type. + * The table must contain a ship type identifier and information about the missile warhead. * - * target - the to fire the missile at - * - * power - the power of the missile. If unspecified, the default power for the + * target - an optional to fire the missile at * * Return: * @@ -628,18 +626,30 @@ static int l_ship_blast_off(lua_State *l) static int l_ship_spawn_missile(lua_State *l) { Ship *s = LuaObject::CheckFromLua(1); + LuaTable stats = LuaTable(l, 2); + Body *target = LuaPull(l, 3, nullptr); + if (s->GetFlightState() == Ship::HYPERSPACE) return luaL_error(l, "Ship:SpawnMissile() cannot be called on a ship in hyperspace"); - ShipType::Id missile_type(luaL_checkstring(l, 2)); - if (missile_type != ShipType::MISSILE_UNGUIDED && - missile_type != ShipType::MISSILE_GUIDED && - missile_type != ShipType::MISSILE_SMART && - missile_type != ShipType::MISSILE_NAVAL) - luaL_error(l, "Ship type '%s' is not a valid missile type", lua_tostring(l, 2)); - int power = (lua_isnone(l, 3)) ? -1 : lua_tointeger(l, 3); + if (stats.Get("shipType", {}).empty()) + return luaL_error(l, "Ship:SpawnMissile() is missing a shipType value in the passed missile table!"); + + MissileDef def = {}; + + def.shipType = stats.Get("shipType"); + def.fuzeRadius = stats.Get("fuzeRadius", def.fuzeRadius); + def.warheadSize = stats.Get("warheadSize", def.warheadSize); + def.effectiveRadius = stats.Get("effectiveRadius", def.effectiveRadius); + def.chargeEffectiveness = stats.Get("chargeEffectiveness", def.chargeEffectiveness); + def.ecmResist = stats.Get("ecmResist", def.ecmResist); + + const ShipType *type = ShipType::Get(def.shipType.c_str()); + if (!type || type->tag != ShipType::TAG_MISSILE) { + return luaL_error(l, "Ship type '%s' is not a valid missile type", def.shipType.c_str()); + } - Missile *missile = s->SpawnMissile(missile_type, power); + Missile *missile = s->SpawnMissile(def, target); if (missile) LuaObject::PushToLua(missile); else diff --git a/src/ship/GunManager.cpp b/src/ship/GunManager.cpp index f12fb46f21..5353f6ccf0 100644 --- a/src/ship/GunManager.cpp +++ b/src/ship/GunManager.cpp @@ -99,22 +99,26 @@ void GunManager::LoadFromJson(const Json &jsonObj, Space *space) const Json &groups = jsonObj["groups"]; for (const auto &group : groups) { m_groups.push_back(group.get()); + } +} - if (group.count("target")) { - - size_t groupIdx = m_groups.size() - 1; - size_t targetIdx = group["target"].get(); +void GunManager::PostLoadFixup(Space *space) +{ + for (size_t groupIdx = 0; groupIdx < m_groups.size(); groupIdx++) { + GroupState &gs = m_groups[groupIdx]; - Body *target = space->GetBodyByIndex(targetIdx); + if (gs.target_idx == 0) + continue; - if (!target) { - Log::Warning("Could not find target body index {} for ship {} (weapon group {})", - targetIdx, m_parent->GetLabel(), groupIdx); - continue; - } + Body *target = space->GetBodyByIndex(gs.target_idx); - SetGroupTarget(groupIdx, target); + if (!target) { + Log::Warning("Could not find target body index {} for ship {} (weapon group {})", + gs.target_idx, m_parent->GetLabel(), groupIdx); + continue; } + + SetGroupTarget(groupIdx, target); } } @@ -705,6 +709,7 @@ void from_json(const Json &obj, GunManager::GroupState &group) } // Target will be fixed up externally + group.target_idx = obj.value("target", 0); group.target = nullptr; group.firing = obj.value("firing", false); group.fireWithoutTargeting = obj.value("fireWithoutTargeting", false); diff --git a/src/ship/GunManager.h b/src/ship/GunManager.h index fe0c54f51b..2b208c49c9 100644 --- a/src/ship/GunManager.h +++ b/src/ship/GunManager.h @@ -121,6 +121,7 @@ class GunManager : public LuaWrappable { struct GroupState { WeaponIndexSet weapons; // Whic weapons are assigned to this group? const Body *target; // The target for this group, if any + uint32_t target_idx; // Group target body index for serialization, used in PostLoadFixup() bool firing; // Is the group currently firing bool fireWithoutTargeting; // Can the group fire without a target/target not in gimbal range? ConnectionTicket onTargetDestroyed; // Unlock the target once it's been destroyed @@ -133,6 +134,8 @@ class GunManager : public LuaWrappable { void SaveToJson(Json &jsonObj, Space *space); void LoadFromJson(const Json &jsonObj, Space *space); + void PostLoadFixup(Space *space); + void StaticUpdate(float deltaTime); // ==========================================