From 1994fd9264c64f47d4af77290de17ff1a33e4c6d Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 13 Feb 2023 16:52:17 -0500 Subject: [PATCH 001/119] Provide lua access to the raw shipdef json values - Allows forward-compatibility with mods and other values in ShipDef without requiring C++ changes to load and store the new values --- src/ShipType.cpp | 2 ++ src/ShipType.h | 2 ++ src/lua/LuaShipDef.cpp | 6 ++++++ 3 files changed, 10 insertions(+) diff --git a/src/ShipType.cpp b/src/ShipType.cpp index 3ee626ded4..f90430189a 100644 --- a/src/ShipType.cpp +++ b/src/ShipType.cpp @@ -63,6 +63,8 @@ ShipType::ShipType(const Id &_id, const std::string &path) } id = _id; + definitionPath = path; + name = data.value("name", ""); shipClass = data.value("ship_class", ""); manufacturer = data.value("manufacturer", ""); diff --git a/src/ShipType.h b/src/ShipType.h index 646b92f7a6..9f37731cf2 100644 --- a/src/ShipType.h +++ b/src/ShipType.h @@ -72,6 +72,8 @@ struct ShipType { int hyperdriveClass; int minCrew, maxCrew; // XXX really only for Lua, but needs to be declared in the ship def + + std::string definitionPath; /////// // percentage (ie, 0--100) of tank used per second at full thrust diff --git a/src/lua/LuaShipDef.cpp b/src/lua/LuaShipDef.cpp index ef1dd48eb6..7a46befef3 100644 --- a/src/lua/LuaShipDef.cpp +++ b/src/lua/LuaShipDef.cpp @@ -3,7 +3,9 @@ #include "LuaShipDef.h" #include "EnumStrings.h" +#include "JsonUtils.h" #include "Lua.h" +#include "LuaJson.h" #include "LuaUtils.h" #include "ShipType.h" @@ -313,6 +315,10 @@ void LuaShipDef::Register() lua_setfield(l, -3, "roles"); lua_pop(l, 1); + Json data = JsonUtils::LoadJsonDataFile(st.definitionPath); + LuaJson::PushToLua(l, data); + lua_setfield(l, -2, "raw"); + pi_lua_readonly_table_proxy(l, -1); lua_setfield(l, -3, iter.first.c_str()); lua_pop(l, 1); From 6ba9057dfc6fdfee1a13b7281c3be0f438dd2019 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 13 Feb 2023 14:41:05 -0500 Subject: [PATCH 002/119] Equipment item prototype and instances - All equipment items defined in Lua are treated as prototypes storing shared data. - When adding an equipment item to a ship, create a new instance of it and add the instance to the EquipSet. - This allows tracking per-instance details such as durability or storing specific equipment item instances at a station. --- data/libs/EquipType.lua | 77 ++++++++++++++++++++++++------ data/libs/Ship.lua | 3 +- data/modules/Taxi/Taxi.lua | 12 +++-- data/modules/TradeShips/Trader.lua | 5 +- 4 files changed, 76 insertions(+), 21 deletions(-) diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index 29a02c33ab..a2e76b7148 100644 --- a/data/libs/EquipType.lua +++ b/data/libs/EquipType.lua @@ -39,21 +39,63 @@ function EquipType.New (specs) for i,v in pairs(specs) do obj[i] = v end + if not obj.l10n_resource then obj.l10n_resource = "equipment-core" end - local l = Lang.GetResource(obj.l10n_resource) - obj.volatile = { - description = l:get(obj.l10n_key.."_DESCRIPTION") or "", - name = l[obj.l10n_key] or "" - } + setmetatable(obj, EquipType.meta) + EquipType._createTransient(obj) + if type(obj.slots) ~= "table" then obj.slots = {obj.slots} end return obj end +function EquipType._createTransient(obj) + local l = Lang.GetResource(obj.l10n_resource) + obj.transient = { + description = l:get(obj.l10n_key .. "_DESCRIPTION") or "", + name = l[obj.l10n_key] or "" + } +end + +function EquipType.isProto(inst) + return not rawget(inst, "__proto") +end + +-- Patch an EquipType class to support a prototype-based equipment system +-- `equipProto = EquipType.New({ ... })` to create an equipment prototype +-- `equipInst = equipProto()` to create a new instance based on the created prototype +function EquipType.SetupPrototype(type) + local old = type.New + local un = type.Unserialize + + -- Create a new metatable for instances of the prototype object; + -- delegates serialization to the base class of the proto + function type.New(...) + local inst = old(...) + inst.meta = { __index = inst, class = type.meta.class } + return inst + end + + function type.Unserialize(inst) + inst = un(inst) ---@type any + + -- if we have a "__proto" field we're an instance of the equipment prototype + if rawget(inst, "__proto") then + setmetatable(inst, inst.__proto.meta) + end + + return inst + end + + type.meta.__call = function(equip) + return setmetatable({ __proto = equip }, equip.meta) + end +end + function EquipType:Serialize() local tmp = EquipType.Super().Serialize(self) local ret = {} @@ -63,21 +105,19 @@ function EquipType:Serialize() end end - ret.volatile = nil + ret.transient = nil return ret end function EquipType.Unserialize(data) local obj = EquipType.Super().Unserialize(data) setmetatable(obj, EquipType.meta) - if not obj.l10n_resource then - obj.l10n_resource = "equipment-core" + + -- Only patch the common prototype with runtime transient data + if EquipType.isProto(obj) then + EquipType._createTransient(obj) end - local l = Lang.GetResource(obj.l10n_resource) - obj.volatile = { - description = l:get(obj.l10n_key.."_DESCRIPTION") or "", - name = l[obj.l10n_key] or "" - } + return obj end @@ -127,11 +167,11 @@ function EquipType:IsValidSlot(slot, ship) end function EquipType:GetName() - return self.volatile.name + return self.transient.name end function EquipType:GetDescription() - return self.volatile.description + return self.transient.description end local function __ApplyMassLimit(ship, capabilities, num) @@ -170,6 +210,7 @@ end -- Base type for weapons local LaserType = utils.inherits(EquipType, "LaserType") + function LaserType:Install(ship, num, slot) if num > 1 then num = 1 end -- FIXME: support installing multiple lasers (e.g., in the "cargo" slot?) if LaserType.Super().Install(self, ship, 1, slot) < 1 then return 0 end @@ -337,6 +378,12 @@ Serializer:RegisterClass("HyperdriveType", HyperdriveType) Serializer:RegisterClass("SensorType", SensorType) Serializer:RegisterClass("BodyScannerType", BodyScannerType) +EquipType:SetupPrototype() +LaserType:SetupPrototype() +HyperdriveType:SetupPrototype() +SensorType:SetupPrototype() +BodyScannerType:SetupPrototype() + return { laser = laser, hyperspace = hyperspace, diff --git a/data/libs/Ship.lua b/data/libs/Ship.lua index beecb46763..d6435a51f6 100644 --- a/data/libs/Ship.lua +++ b/data/libs/Ship.lua @@ -159,7 +159,8 @@ end -- experimental -- function Ship:AddEquip(item, count, slot) - local ret = self.equipSet:Add(self, item, count, slot) + assert(not count or count == 1) + local ret = self.equipSet:Add(self, item(), count, slot) if ret > 0 then Event.Queue("onShipEquipmentChanged", self, item) end diff --git a/data/modules/Taxi/Taxi.lua b/data/modules/Taxi/Taxi.lua index 0b03919f19..89eb59d6b5 100644 --- a/data/modules/Taxi/Taxi.lua +++ b/data/modules/Taxi/Taxi.lua @@ -107,14 +107,18 @@ local missions = {} local passengers = 0 local add_passengers = function (group) - Game.player:RemoveEquip(eq.misc.cabin, group) - Game.player:AddEquip(eq.misc.cabin_occupied, group) + for i = 1, group do + Game.player:RemoveEquip(eq.misc.cabin, 1) + Game.player:AddEquip(eq.misc.cabin_occupied, 1) + end passengers = passengers + group end local remove_passengers = function (group) - Game.player:RemoveEquip(eq.misc.cabin_occupied, group) - Game.player:AddEquip(eq.misc.cabin, group) + for i = 1, group do + Game.player:RemoveEquip(eq.misc.cabin_occupied, 1) + Game.player:AddEquip(eq.misc.cabin, 1) + end passengers = passengers - group end diff --git a/data/modules/TradeShips/Trader.lua b/data/modules/TradeShips/Trader.lua index 0d0ca629a0..44bda5820e 100644 --- a/data/modules/TradeShips/Trader.lua +++ b/data/modules/TradeShips/Trader.lua @@ -35,7 +35,10 @@ Trader.addEquip = function (ship) if Engine.rand:Number(1) - 0.1 < lawlessness then local num = math.floor(math.sqrt(ship.freeCapacity / 50)) - ship:CountEquip(e.misc.shield_generator) - if num > 0 then ship:AddEquip(e.misc.shield_generator, num) end + for i = 1, num do + ship:AddEquip(e.misc.shield_generator) + end + if ship_type.equipSlotCapacity.energy_booster > 0 and Engine.rand:Number(1) + 0.5 - size_factor < lawlessness then ship:AddEquip(e.misc.shield_energy_booster) From 5ad02ba0866ce96ba41967a85bf5598443548da6 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 17 Feb 2023 01:36:15 -0500 Subject: [PATCH 003/119] Polyfill equipment mass/volume - Promote old equipment prototypes to use top-level mass/volume properties --- data/libs/EquipType.lua | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index a2e76b7148..c33a63f7eb 100644 --- a/data/libs/EquipType.lua +++ b/data/libs/EquipType.lua @@ -32,8 +32,12 @@ local misc = {} -- author, but who knows ? Some people might find it useful.) -- -- +---@class EquipType +---@field transient table +---@field slots table -- deprecated local EquipType = utils.inherits(nil, "EquipType") +---@return EquipType function EquipType.New (specs) local obj = {} for i,v in pairs(specs) do @@ -50,6 +54,15 @@ function EquipType.New (specs) if type(obj.slots) ~= "table" then obj.slots = {obj.slots} end + + -- fixup old capabilities system to explicitly specified mass/volume + if obj.capabilities and obj.capabilities.mass then + obj.mass = obj.capabilities.mass + obj.volume = obj.mass + + -- obj.capabilities.mass = nil + end + return obj end From 758d7c6ea28f78b6404ede3f247e8f9f15254da1 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 16 Dec 2023 03:55:28 -0500 Subject: [PATCH 004/119] Convert equipment to use new slot system - Split existing items into various internal slots by size - Add select new variants of existing items for testing slot sizes --- data/libs/Equipment.lua | 11 ++ data/modules/Equipment/Hyperdrive.lua | 134 +++++++++++++++ data/modules/Equipment/Internal.lua | 179 ++++++++++++++++++++ data/modules/Equipment/Stats.lua | 112 +++++++++++++ data/modules/Equipment/Utility.lua | 94 +++++++++++ data/modules/Equipment/Weapons.lua | 232 ++++++++++++++++++++++++++ 6 files changed, 762 insertions(+) create mode 100644 data/modules/Equipment/Hyperdrive.lua create mode 100644 data/modules/Equipment/Internal.lua create mode 100644 data/modules/Equipment/Stats.lua create mode 100644 data/modules/Equipment/Utility.lua create mode 100644 data/modules/Equipment/Weapons.lua diff --git a/data/libs/Equipment.lua b/data/libs/Equipment.lua index 2224728077..51eb122b65 100644 --- a/data/libs/Equipment.lua +++ b/data/libs/Equipment.lua @@ -15,6 +15,17 @@ local laser = EquipTypes.laser local hyperspace = EquipTypes.hyperspace local misc = EquipTypes.misc +EquipTypes.new = {} + +function EquipTypes.Get(id) + return EquipTypes.new[id] +end + +function EquipTypes.Register(id, type) + EquipTypes.new[id] = type + type.id = id +end + -- Constants: EquipSlot -- -- Equipment slots. Every equipment item has a corresponding diff --git a/data/modules/Equipment/Hyperdrive.lua b/data/modules/Equipment/Hyperdrive.lua new file mode 100644 index 0000000000..c09474e768 --- /dev/null +++ b/data/modules/Equipment/Hyperdrive.lua @@ -0,0 +1,134 @@ +-- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +local Equipment = require 'Equipment' +local Commodities = require 'Commodities' + +local HyperdriveType = require 'EquipType'.HyperdriveType + +Equipment.Register("hyperspace.hyperdrive_1", HyperdriveType.New { + l10n_key="DRIVE_CLASS1", fuel=Commodities.hydrogen, + slot = { type="hyperdrive", size=1 }, + mass=2, volume=2, capabilities={ hyperclass=1 }, + price=700, purchasable=true, tech_level=3, + icon_name="equip_hyperdrive" +}) +Equipment.Register("hyperspace.hyperdrive_2", HyperdriveType.New { + l10n_key="DRIVE_CLASS2", fuel=Commodities.hydrogen, + slot = { type="hyperdrive", size=2 }, + mass=6, volume=6, capabilities={ hyperclass=2 }, + price=1300, purchasable=true, tech_level=4, + icon_name="equip_hyperdrive" +}) +Equipment.Register("hyperspace.hyperdrive_3", HyperdriveType.New { + l10n_key="DRIVE_CLASS3", fuel=Commodities.hydrogen, + slot = { type="hyperdrive", size=3 }, + mass=11, volume=11, capabilities={ hyperclass=3 }, + price=2500, purchasable=true, tech_level=4, + icon_name="equip_hyperdrive" +}) +Equipment.Register("hyperspace.hyperdrive_4", HyperdriveType.New { + l10n_key="DRIVE_CLASS4", fuel=Commodities.hydrogen, + slot = { type="hyperdrive", size=4 }, + mass=25, volume=25, capabilities={ hyperclass=4 }, + price=5000, purchasable=true, tech_level=5, + icon_name="equip_hyperdrive" +}) +Equipment.Register("hyperspace.hyperdrive_5", HyperdriveType.New { + l10n_key="DRIVE_CLASS5", fuel=Commodities.hydrogen, + slot = { type="hyperdrive", size=5 }, + mass=60, volume=60, capabilities={ hyperclass=5 }, + price=10000, purchasable=true, tech_level=5, + icon_name="equip_hyperdrive" +}) +Equipment.Register("hyperspace.hyperdrive_6", HyperdriveType.New { + l10n_key="DRIVE_CLASS6", fuel=Commodities.hydrogen, + slot = { type="hyperdrive", size=6 }, + mass=130, volume=130, capabilities={ hyperclass=6 }, + price=20000, purchasable=true, tech_level=6, + icon_name="equip_hyperdrive" +}) +Equipment.Register("hyperspace.hyperdrive_7", HyperdriveType.New { + l10n_key="DRIVE_CLASS7", fuel=Commodities.hydrogen, + slot = { type="hyperdrive", size=7 }, + mass=245, volume=245, capabilities={ hyperclass=7 }, + price=30000, purchasable=true, tech_level=8, + icon_name="equip_hyperdrive" +}) +Equipment.Register("hyperspace.hyperdrive_8", HyperdriveType.New { + l10n_key="DRIVE_CLASS8", fuel=Commodities.hydrogen, + slot = { type="hyperdrive", size=8 }, + mass=360, volume=360, capabilities={ hyperclass=8 }, + price=60000, purchasable=true, tech_level=9, + icon_name="equip_hyperdrive" +}) +Equipment.Register("hyperspace.hyperdrive_9", HyperdriveType.New { + l10n_key="DRIVE_CLASS9", fuel=Commodities.hydrogen, + slot = { type="hyperdrive", size=9 }, + mass=540, volume=540, capabilities={ hyperclass=9 }, + price=120000, purchasable=true, tech_level=10, + icon_name="equip_hyperdrive" +}) +Equipment.Register("hyperspace.hyperdrive_mil1", HyperdriveType.New { + l10n_key="DRIVE_MIL1", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, + slot = { type="hyperdrive", size=1 }, + mass=1, volume=1, capabilities={ hyperclass=1 }, + price=23000, purchasable=true, tech_level=10, + icon_name="equip_hyperdrive_mil" +}) +Equipment.Register("hyperspace.hyperdrive_mil2", HyperdriveType.New { + l10n_key="DRIVE_MIL2", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, + slot = { type="hyperdrive", size=2 }, + mass=3, volume=3, capabilities={ hyperclass=2 }, + price=47000, purchasable=true, tech_level="MILITARY", + icon_name="equip_hyperdrive_mil" +}) +Equipment.Register("hyperspace.hyperdrive_mil3", HyperdriveType.New { + l10n_key="DRIVE_MIL3", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, + slot = { type="hyperdrive", size=3 }, + mass=5, volume=5, capabilities={ hyperclass=3 }, + price=85000, purchasable=true, tech_level=11, + icon_name="equip_hyperdrive_mil" +}) +Equipment.Register("hyperspace.hyperdrive_mil4", HyperdriveType.New { + l10n_key="DRIVE_MIL4", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, + slot = { type="hyperdrive", size=4 }, + mass=13, volume=13, capabilities={ hyperclass=4 }, + price=214000, purchasable=true, tech_level=12, + icon_name="equip_hyperdrive_mil" +}) +Equipment.Register("hyperspace.hyperdrive_mil5", HyperdriveType.New { + l10n_key="DRIVE_MIL5", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, + slot = { type="hyperdrive", size=5 }, + mass=29, volume=29, capabilities={ hyperclass=5 }, + price=540000, purchasable=false, tech_level="MILITARY", + icon_name="equip_hyperdrive_mil" +}) +Equipment.Register("hyperspace.hyperdrive_mil6", HyperdriveType.New { + l10n_key="DRIVE_MIL6", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, + slot = { type="hyperdrive", size=6 }, + mass=60, volume=60, capabilities={ hyperclass=6 }, + price=1350000, purchasable=false, tech_level="MILITARY", + icon_name="equip_hyperdrive_mil" +}) +Equipment.Register("hyperspace.hyperdrive_mil7", HyperdriveType.New { + l10n_key="DRIVE_MIL7", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, + slot = { type="hyperdrive", size=7 }, + mass=135, volume=135, capabilities={ hyperclass=7 }, + price=3500000, purchasable=false, tech_level="MILITARY", + icon_name="equip_hyperdrive_mil" +}) +Equipment.Register("hyperspace.hyperdrive_mil8", HyperdriveType.New { + l10n_key="DRIVE_MIL8", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, + slot = { type="hyperdrive", size=8 }, + mass=190, volume=190, capabilities={ hyperclass=8 }, + price=8500000, purchasable=false, tech_level="MILITARY", + icon_name="equip_hyperdrive_mil" +}) +Equipment.Register("hyperspace.hyperdrive_mil9", HyperdriveType.New { + l10n_key="DRIVE_MIL9", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, + slot = { type="hyperdrive", size=9 }, + mass=260, volume=260, capabilities={ hyperclass=9 }, + price=22000000, purchasable=false, tech_level="MILITARY", + icon_name="equip_hyperdrive_mil" +}) diff --git a/data/modules/Equipment/Internal.lua b/data/modules/Equipment/Internal.lua new file mode 100644 index 0000000000..02f213fba9 --- /dev/null +++ b/data/modules/Equipment/Internal.lua @@ -0,0 +1,179 @@ +-- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +local EquipTypes = require 'EquipType' +local Equipment = require 'Equipment' + +local EquipType = EquipTypes.EquipType +local SensorType = EquipTypes.SensorType + +--=============================================== +-- Computer Modules +--=============================================== + +Equipment.Register("misc.autopilot", EquipType.New { + l10n_key="AUTOPILOT", + price=1400, purchasable=true, tech_level=1, + slot = { type="computer", size=1 }, + mass=0.2, volume=0.5, capabilities = { set_speed=1, autopilot=1 }, + icon_name="equip_autopilot" +}) + +Equipment.Register("misc.trade_computer", EquipType.New { + l10n_key="TRADE_COMPUTER", + price=400, purchasable=true, tech_level=9, + slot={ type="computer", size=1 }, + mass=0.2, volume=0.5, capabilities={ trade_computer=1 }, + icon_name="equip_trade_computer" +}) + +--=============================================== +-- Sensors +--=============================================== + +Equipment.Register("sensor.radar", SensorType.New { + l10n_key="RADAR", + price=680, purchasable=true, tech_level=3, + slot = { type="sensor.radar", size=1 }, + mass=1.0, volume=1.0, capabilities = { radar=1 }, + icon_name="equip_radar" +}) + +--=============================================== +-- Shield Generators +--=============================================== + +Equipment.Register("shield.basic_s1", EquipType.New { + l10n_key="SHIELD_GENERATOR", + price=2500, purchasable=true, tech_level=8, + slot = { type="shield", size=1 }, + mass=2, volume=1, capabilities = { shield=1 }, + icon_name="equip_shield_generator" +}) + +Equipment.Register("shield.basic_s2", EquipType.New { + l10n_key="SHIELD_GENERATOR", + price=5500, purchasable=true, tech_level=9, + slot = { type="shield", size=2 }, + mass=4, volume=2.5, capabilities = { shield=2 }, + icon_name="equip_shield_generator" +}) + +--=============================================== +-- Hull Modifications +--=============================================== + +Equipment.Register("hull.atmospheric_shielding", EquipType.New { + l10n_key="ATMOSPHERIC_SHIELDING", + price=200, purchasable=true, tech_level=3, + slot = { type="hull.atmo_shield", size=1 }, + mass=1, volume=2, capabilities = { atmo_shield=9 }, + icon_name="equip_atmo_shield_generator" +}) + +Equipment.Register("hull.heavy_atmospheric_shielding", EquipType.New { + l10n_key="ATMOSPHERIC_SHIELDING_HEAVY", + price=900, purchasable=true, tech_level=5, + slot = { type="hull.atmo_shield", size=3 }, + mass=5, volume=12, capabilities = { atmo_shield=19 }, + icon_name="equip_atmo_shield_generator" +}) + +Equipment.Register("misc.hull_autorepair", EquipType.New { + l10n_key="HULL_AUTOREPAIR", slots="hull_autorepair", + price=16000, purchasable=true, tech_level="MILITARY", + slot = { type="hull.autorepair", size=4 }, + mass=30, volume=40, capabilities={ hull_autorepair=1 }, + icon_name="repairs" +}) + +--=============================================== +-- Thruster Mods +--=============================================== + +Equipment.Register("misc.thrusters_default", EquipType.New { + l10n_key="THRUSTERS_DEFAULT", slots="thruster", + price=1500, purchasable=true, tech_level=2, + slot = { type="thruster", size=1 }, + mass=0, volume=0, capabilities={ thruster_power=0 }, + icon_name="equip_thrusters_basic" +}) + +Equipment.Register("misc.thrusters_basic", EquipType.New { + l10n_key="THRUSTERS_BASIC", slots="thruster", + price=3000, purchasable=true, tech_level=5, + slot = { type="thruster", size=1 }, + mass=0, volume=0, capabilities={ thruster_power=1 }, + icon_name="equip_thrusters_basic" +}) + +Equipment.Register("misc.thrusters_medium", EquipType.New { + l10n_key="THRUSTERS_MEDIUM", slots="thruster", + price=6500, purchasable=true, tech_level=8, + slot = { type="thruster", size=1 }, + mass=0, volume=0, capabilities={ thruster_power=2 }, + icon_name="equip_thrusters_medium" +}) + +Equipment.Register("misc.thrusters_best", EquipType.New { + l10n_key="THRUSTERS_BEST", slots="thruster", + price=14000, purchasable=true, tech_level="MILITARY", + slot = { type="thruster", size=1 }, + mass=0, volume=0, capabilities={ thruster_power=3 }, + icon_name="equip_thrusters_best" +}) + +--=============================================== +-- Scoops +--=============================================== + +Equipment.Register("misc.fuel_scoop", EquipType.New { + l10n_key="FUEL_SCOOP", + price=3500, purchasable=true, tech_level=4, + slot = { type="scoop", size=1, hardpoint=true }, + mass=6, volume=4, capabilities={ fuel_scoop=3 }, + icon_name="equip_fuel_scoop" +}) +Equipment.Register("misc.cargo_scoop", EquipType.New { + l10n_key="CARGO_SCOOP", + price=3900, purchasable=true, tech_level=5, + slot = { type="scoop", size=1, hardpoint=true }, + mass=4, volume=7, capabilities={ cargo_scoop=1 }, + icon_name="equip_cargo_scoop" +}) +Equipment.Register("misc.multi_scoop", EquipType.New { + l10n_key="MULTI_SCOOP", + price=12000, purchasable=true, tech_level=9, + slot = { type="scoop", size=1, hardpoint=true }, + mass=11, volume=9, capabilities={ cargo_scoop=1, fuel_scoop=2 }, + icon_name="equip_multi_scoop" +}) + +--=============================================== +-- Slot-less equipment +--=============================================== + +Equipment.Register("misc.cabin", EquipType.New { + l10n_key="UNOCCUPIED_CABIN", + price=1350, purchasable=true, tech_level=1, + mass=1, volume=5, capabilities={ cabin=1 }, + icon_name="equip_cabin_empty" +}) + +Equipment.Register("misc.laser_cooling_booster", EquipType.New { + l10n_key="LASER_COOLING_BOOSTER", + price=380, purchasable=true, tech_level=8, + mass=1, volume=2, capabilities={ laser_cooler=2 }, +}) + +Equipment.Register("misc.shield_energy_booster", EquipType.New { + l10n_key="SHIELD_ENERGY_BOOSTER", + price=10000, purchasable=true, tech_level=11, + mass=5, volume=8, capabilities={ shield_energy_booster=1 }, +}) + +Equipment.Register("misc.cargo_life_support", EquipType.New { + l10n_key="CARGO_LIFE_SUPPORT", + price=700, purchasable=true, tech_level=2, + mass=1, volume=2, capabilities={ cargo_life_support=1 }, +}) diff --git a/data/modules/Equipment/Stats.lua b/data/modules/Equipment/Stats.lua new file mode 100644 index 0000000000..4c52d85dd3 --- /dev/null +++ b/data/modules/Equipment/Stats.lua @@ -0,0 +1,112 @@ +-- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +local EquipTypes = require 'EquipType' + +local Lang = require 'Lang' + +local utils = require 'utils' + +local lc = Lang.GetResource("core") +local le = Lang.GetResource("equipment-core") + +local ui = require 'pigui' +local icons = ui.theme.icons + +--============================================================================== + +---@class EquipType +local EquipType = EquipTypes.EquipType + +---@alias EquipType.UI.Stats { [1]:string, [2]:ui.Icon, [3]:any, [4]:(fun(v: any):string), [5]:boolean? } + +local format_integrity = function(v) return string.format("%d%%", v * 100) end +local format_mass = function(v) return ui.Format.Mass(v * 1000, 1) end +local format_power = function(v) return string.format("%.1f KW", v) end + +---@return EquipType.UI.Stats[] +function EquipType:GetDetailedStats() + local equipHealth = 1 + local powerDraw = 0 + + return { + { le.EQUIPMENT_INTEGRITY, icons.repairs, equipHealth, format_integrity }, + { le.STAT_VOLUME, icons.square, self.volume, ui.Format.Volume, true }, + { le.STAT_WEIGHT, icons.hull, self.mass, format_mass, true }, + { le.STAT_POWER_DRAW, icons.ecm, powerDraw, format_power, true } + } +end + +---@return table[] +function EquipType:GetItemCardStats() + return { + { icons.square, ui.Format.Volume(self.volume) }, + { icons.hull, format_mass(self.mass) }, + { icons.ecm, format_power(0) }, + { icons.repairs, format_integrity(1) } + } +end + +--============================================================================== + +local LaserType = EquipTypes.LaserType + +local format_rpm = function(v) return string.format("%d RPM", 60 / v) end +local format_speed = function(v) return string.format("%.1f%s", v, lc.UNIT_METERS_PER_SECOND) end + +function LaserType:GetDetailedStats() + local out = self:Super().GetDetailedStats(self) + + table.insert(out, { + le.SHOTS_PER_MINUTE, + icons.comms, -- PLACEHOLDER + self.laser_stats.rechargeTime, + format_rpm, + true -- lower is better + }) + + table.insert(out, { + le.DAMAGE_PER_SHOT, + icons.ecm_advanced, + self.laser_stats.damage, + format_power + }) + + table.insert(out, { + le.PROJECTILE_SPEED, + icons.forward, + self.laser_stats.speed, + format_speed + }) + + return out +end + +--============================================================================== + +local BodyScannerType = EquipTypes.BodyScannerType + +local format_px = function(v) return string.format("%s px", ui.Format.Number(v, 0)) end + +function BodyScannerType:GetDetailedStats() + local out = self:Super().GetDetailedStats(self) + + table.insert(out, { + le.SENSOR_RESOLUTION, + icons.scanner, + self.stats.resolution, + format_px + }) + + table.insert(out, { + le.SENSOR_MIN_ALTITUDE, + icons.altitude, + self.stats.minAltitude, + ui.Format.Distance, + true -- lower is better + }) + + return out +end + +--============================================================================== diff --git a/data/modules/Equipment/Utility.lua b/data/modules/Equipment/Utility.lua new file mode 100644 index 0000000000..83cda367ed --- /dev/null +++ b/data/modules/Equipment/Utility.lua @@ -0,0 +1,94 @@ +-- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +local EquipTypes = require 'EquipType' +local Equipment = require 'Equipment' + +local EquipType = EquipTypes.EquipType +local BodyScannerType = EquipTypes.BodyScannerType + +--=============================================== +-- ECM +--=============================================== + +Equipment.Register("misc.ecm_basic", EquipType.New { + l10n_key="ECM_BASIC", + price=6000, purchasable=true, tech_level=9, + slot = { type="utility.ecm", size=1, hardpoint=true }, + mass=2, volume=3, capabilities={ ecm_power=2, ecm_recharge=5 }, + ecm_type = 'ecm', + hover_message="ECM_HOVER_MESSAGE" +}) + +Equipment.Register("misc.ecm_advanced", EquipType.New { + l10n_key="ECM_ADVANCED", + price=15200, purchasable=true, tech_level="MILITARY", + slot = { type="utility.ecm", size=2, hardpoint=true }, + mass=2, volume=5, capabilities={ ecm_power=3, ecm_recharge=5 }, + ecm_type = 'ecm_advanced', + hover_message="ECM_HOVER_MESSAGE" +}) + +--=============================================== +-- Scanners +--=============================================== + +Equipment.Register("misc.target_scanner", EquipType.New { + l10n_key="TARGET_SCANNER", + price=900, purchasable=true, tech_level=9, + slot = { type="utility.scanner.combat_scanner", size=1, hardpoint=true }, + mass=0.5, volume=0, capabilities={ target_scanner_level=1 }, + icon_name="equip_scanner" +}) + +Equipment.Register("misc.advanced_target_scanner", EquipType.New { + l10n_key="ADVANCED_TARGET_SCANNER", + price=1200, purchasable=true, tech_level="MILITARY", + slot = { type="utility.scanner.combat_scanner", size=2, hardpoint=true }, + mass=1.0, volume=0, capabilities={ target_scanner_level=2 }, + icon_name="equip_scanner" +}) + +Equipment.Register("misc.hypercloud_analyzer", EquipType.New { + l10n_key="HYPERCLOUD_ANALYZER", + price=1500, purchasable=true, tech_level=10, + slot = { type="utility.scanner.hypercloud", size=1, hardpoint=true }, + mass=0.5, volume=0, capabilities={ hypercloud_analyzer=1 }, + icon_name="equip_scanner" +}) + +Equipment.Register("misc.planetscanner", BodyScannerType.New { + l10n_key = 'SURFACE_SCANNER', + price=2950, purchasable=true, tech_level=5, + slot = { type="utility.scanner.planet", size=1, hardpoint=true }, + mass=1, volume=1, capabilities={ sensor=1 }, + stats={ aperture = 50.0, minAltitude = 150, resolution = 768, orbital = false }, + icon_name="equip_planet_scanner" +}) + +Equipment.Register("misc.planetscanner_good", BodyScannerType.New { + l10n_key = 'SURFACE_SCANNER_GOOD', + price=5000, purchasable=true, tech_level=8, + slot = { type="utility.scanner.planet", size=2, hardpoint=true }, + mass=2, volume = 2, capabilities={ sensor=1 }, + stats={ aperture = 65.0, minAltitude = 250, resolution = 1092, orbital = false }, + icon_name="equip_planet_scanner" +}) + +Equipment.Register("misc.orbitscanner", BodyScannerType.New { + l10n_key = 'ORBIT_SCANNER', + price=7500, purchasable=true, tech_level=3, + slot = { type="utility.scanner.planet", size=1, hardpoint=true }, + mass=3, volume=2, capabilities={ sensor=1 }, + stats={ aperture = 4.0, minAltitude = 650000, resolution = 6802, orbital = true }, + icon_name="equip_orbit_scanner" +}) + +Equipment.Register("misc.orbitscanner_good", BodyScannerType.New { + l10n_key = 'ORBIT_SCANNER_GOOD', + price=11000, purchasable=true, tech_level=8, + slot = { type="utility.scanner.planet", size=2, hardpoint=true }, + mass=7, volume=4, capabilities={ sensor=1 }, + stats={ aperture = 2.8, minAltitude = 1750000, resolution = 12375, orbital = true }, + icon_name="equip_orbit_scanner" +}) diff --git a/data/modules/Equipment/Weapons.lua b/data/modules/Equipment/Weapons.lua new file mode 100644 index 0000000000..ebf58cec50 --- /dev/null +++ b/data/modules/Equipment/Weapons.lua @@ -0,0 +1,232 @@ +-- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +local EquipTypes = require 'EquipType' +local Equipment = require 'Equipment' + +local EquipType = EquipTypes.EquipType +local LaserType = EquipTypes.LaserType + +--=============================================== +-- Pulse Cannons +--=============================================== + +Equipment.Register("laser.pulsecannon_1mw", LaserType.New { + l10n_key="PULSECANNON_1MW", + price=600, purchasable=true, tech_level=3, + mass=2, volume=1.5, capabilities = {}, + slot = { type="weapon.energy.pulsecannon", size=1, hardpoint=true }, + laser_stats = { + lifespan=8, speed=1000, damage=1000, rechargeTime=0.25, length=30, + width=5, beam=0, dual=0, mining=0, rgba_r = 255, rgba_g = 51, rgba_b = 51, rgba_a = 255 + }, + icon_name="equip_pulsecannon" +}) + +Equipment.Register("laser.pulsecannon_dual_1mw", LaserType.New { + l10n_key="PULSECANNON_DUAL_1MW", + price=1100, purchasable=true, tech_level=3, + mass=4, volume=2, capabilities = {}, + slot = { type="weapon.energy.pulsecannon", size=2, hardpoint=true }, + laser_stats = { + lifespan=8, speed=1000, damage=1000, rechargeTime=0.25, length=30, + width=5, beam=0, dual=1, mining=0, rgba_r = 255, rgba_g = 51, rgba_b = 51, rgba_a = 255 + }, + icon_name="equip_pulsecannon" +}) + +Equipment.Register("laser.pulsecannon_2mw", LaserType.New { + l10n_key="PULSECANNON_2MW", + price=1000, purchasable=true, tech_level=5, + mass=3, volume=2.5, capabilities = {}, + slot = { type="weapon.energy.pulsecannon", size=2, hardpoint=true }, + laser_stats = { + lifespan=8, speed=1000, damage=2000, rechargeTime=0.25, length=30, + width=5, beam=0, dual=0, mining=0, rgba_r = 255, rgba_g = 127, rgba_b = 51, rgba_a = 255 + }, + icon_name="equip_pulsecannon" +}) + +Equipment.Register("laser.pulsecannon_rapid_2mw", LaserType.New { + l10n_key="PULSECANNON_RAPID_2MW", + price=1800, purchasable=true, tech_level=5, + mass=7, volume=7, capabilities={}, + slot = { type="weapon.energy.pulsecannon", size=2, hardpoint=true }, + laser_stats = { + lifespan=8, speed=1000, damage=2000, rechargeTime=0.13, length=30, + width=5, beam=0, dual=0, mining=0, rgba_r = 255, rgba_g = 127, rgba_b = 51, rgba_a = 255 + }, + icon_name="equip_pulsecannon_rapid" +}) + +Equipment.Register("laser.pulsecannon_4mw", LaserType.New { + l10n_key="PULSECANNON_4MW", + price=2200, purchasable=true, tech_level=6, + mass=10, volume=10, capabilities={}, + slot = { type="weapon.energy.pulsecannon", size=3, hardpoint=true }, + laser_stats = { + lifespan=8, speed=1000, damage=4000, rechargeTime=0.25, length=30, + width=5, beam=0, dual=0, mining=0, rgba_r = 255, rgba_g = 255, rgba_b = 51, rgba_a = 255 + }, + icon_name="equip_pulsecannon" +}) + +Equipment.Register("laser.pulsecannon_10mw", LaserType.New { + l10n_key="PULSECANNON_10MW", + price=4900, purchasable=true, tech_level=7, + mass=30, volume=30, capabilities={}, + slot = { type="weapon.energy.pulsecannon", size=4, hardpoint=true }, + laser_stats = { + lifespan=8, speed=1000, damage=10000, rechargeTime=0.25, length=30, + width=5, beam=0, dual=0, mining=0, rgba_r = 51, rgba_g = 255, rgba_b = 51, rgba_a = 255 + }, + icon_name="equip_pulsecannon" +}) + +Equipment.Register("laser.pulsecannon_20mw", LaserType.New { + l10n_key="PULSECANNON_20MW", + price=12000, purchasable=true, tech_level="MILITARY", + mass=65, volume=65, capabilities={}, + slot = { type="weapon.energy.pulsecannon", size=5, hardpoint=true }, + laser_stats = { + lifespan=8, speed=1000, damage=20000, rechargeTime=0.25, length=30, + width=5, beam=0, dual=0, mining=0, rgba_r = 0.1, rgba_g = 51, rgba_b = 255, rgba_a = 255 + }, + icon_name="equip_pulsecannon" +}) + +--=============================================== +-- Beam Lasers +--=============================================== + +Equipment.Register("laser.beamlaser_1mw", LaserType.New { + l10n_key="BEAMLASER_1MW", + price=2400, purchasable=true, tech_level=4, + mass=3, volume=3, capabilities={}, + slot = { type="weapon.energy.laser", size=1, hardpoint=true }, + laser_stats = { + lifespan=8, speed=1000, damage=1500, rechargeTime=0.25, length=10000, + width=1, beam=1, dual=0, mining=0, rgba_r = 255, rgba_g = 51, rgba_b = 127, rgba_a = 255, + heatrate=0.02, coolrate=0.01 + }, + icon_name="equip_beamlaser" +}) + +Equipment.Register("laser.beamlaser_dual_1mw", LaserType.New { + l10n_key="BEAMLASER_DUAL_1MW", + price=4800, purchasable=true, tech_level=5, + mass=6, volume=6, capabilities={}, + slot = { type="weapon.energy.laser", size=2, hardpoint=true }, + laser_stats = { + lifespan=8, speed=1000, damage=1500, rechargeTime=0.5, length=10000, + width=1, beam=1, dual=1, mining=0, rgba_r = 255, rgba_g = 51, rgba_b = 127, rgba_a = 255, + heatrate=0.02, coolrate=0.01 + }, + icon_name="equip_dual_beamlaser" +}) + +Equipment.Register("laser.beamlaser_2mw", LaserType.New { + l10n_key="BEAMLASER_RAPID_2MW", + price=5600, purchasable=true, tech_level=6, + mass=7, volume=7, capabilities={}, + slot = { type="weapon.energy.laser", size=2, hardpoint=true }, + laser_stats = { + lifespan=8, speed=1000, damage=3000, rechargeTime=0.13, length=20000, + width=1, beam=1, dual=0, mining=0, rgba_r = 255, rgba_g = 192, rgba_b = 192, rgba_a = 255, + heatrate=0.02, coolrate=0.01 + }, + icon_name="equip_beamlaser" +}) + +--=============================================== +-- Plasma Accelerators +--=============================================== + +Equipment.Register("laser.small_plasma_accelerator", LaserType.New { + l10n_key="SMALL_PLASMA_ACCEL", + price=120000, purchasable=true, tech_level=10, + mass=22, volume=22, capabilities={}, + slot = { type="weapon.energy.plasma_acc", size=5, hardpoint=true }, + laser_stats = { + lifespan=8, speed=1000, damage=50000, rechargeTime=0.3, length=42, + width=7, beam=0, dual=0, mining=0, rgba_r = 51, rgba_g = 255, rgba_b = 255, rgba_a = 255 + }, + icon_name="equip_plasma_accelerator" +}) + +Equipment.Register("laser.large_plasma_accelerator", LaserType.New { + l10n_key="LARGE_PLASMA_ACCEL", + price=390000, purchasable=true, tech_level=12, + mass=50, volume=50, capabilities={}, + slot = { type="weapon.energy.plasma_acc", size=6, hardpoint=true }, + laser_stats = { + lifespan=8, speed=1000, damage=100000, rechargeTime=0.3, length=42, + width=7, beam=0, dual=0, mining=0, rgba_r = 127, rgba_g = 255, rgba_b = 255, rgba_a = 255 + }, + icon_name="equip_plasma_accelerator" +}) + +--=============================================== +-- Mining Cannons +--=============================================== + +Equipment.Register("laser.miningcannon_5mw", LaserType.New { + l10n_key="MININGCANNON_5MW", + price=3700, purchasable=true, tech_level=5, + mass=6, volume=6, capabilities={}, + slot = { type="weapon.mining", size=2, hardpoint=true }, + laser_stats = { + lifespan=8, speed=1000, damage=5000, rechargeTime=1.5, length=30, + width=5, beam=0, dual=0, mining=1, rgba_r = 51, rgba_g = 127, rgba_b = 0, rgba_a = 255 + }, + icon_name="equip_mining_laser" +}) + +Equipment.Register("laser.miningcannon_17mw", LaserType.New { + l10n_key="MININGCANNON_17MW", + price=10600, purchasable=true, tech_level=8, + mass=10, volume=10, capabilities={}, + slot = { type="weapon.mining", size=4, hardpoint=true }, + laser_stats = { + lifespan=8, speed=1000, damage=17000, rechargeTime=2, length=30, + width=5, beam=0, dual=0, mining=1, rgba_r = 51, rgba_g = 127, rgba_b = 0, rgba_a = 255 + }, + icon_name="equip_mining_laser" +}) + +--=============================================== +-- Missiles +--=============================================== + +Equipment.Register("missile.unguided_s1", EquipType.New { + l10n_key="MISSILE_UNGUIDED", + price=30, purchasable=true, tech_level=1, + missile_type="missile_unguided", + volume=0, mass=0.1, + slot = { type="missile", size=1, hardpoint=true }, + icon_name="equip_missile_unguided" +}) +Equipment.Register("missile.guided_s2", EquipType.New { + l10n_key="MISSILE_GUIDED", + price=50, purchasable=true, tech_level=5, + missile_type="missile_guided", + volume=0, mass=0.3, + slot = { type="missile", size=2, hardpoint=true }, + icon_name="equip_missile_guided" +}) +Equipment.Register("missile.smart_s3", EquipType.New { + l10n_key="MISSILE_SMART", + price=95, purchasable=true, tech_level=10, + missile_type="missile_smart", + volume=0, mass=0.5, + slot = { type="missile", size=3, hardpoint=true }, + icon_name="equip_missile_smart" +}) +Equipment.Register("missile.naval_s4", EquipType.New { + l10n_key="MISSILE_NAVAL", + price=160, purchasable=true, tech_level="MILITARY", + missile_type="missile_naval", + volume=0, mass=1, + slot = { type="missile", size=4, hardpoint=true }, + icon_name="equip_missile_naval" +}) From 85e5800304144013ce4340e2341398f234bdc7eb Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 22 Dec 2023 13:40:07 -0500 Subject: [PATCH 005/119] Remove old EquipSet implementation - Ship.cpp no longer holds a reference to an EquipSet, uses LuaComponent system instead --- data/libs/EquipSet.lua | 404 ----------------------------------------- data/libs/Ship.lua | 4 + src/Ship.cpp | 30 +-- src/Ship.h | 4 - src/lua/LuaShip.cpp | 8 - 5 files changed, 8 insertions(+), 442 deletions(-) delete mode 100644 data/libs/EquipSet.lua diff --git a/data/libs/EquipSet.lua b/data/libs/EquipSet.lua deleted file mode 100644 index dfa258cf62..0000000000 --- a/data/libs/EquipSet.lua +++ /dev/null @@ -1,404 +0,0 @@ --- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details --- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt - -local utils = require 'utils' -local Serializer = require 'Serializer' --- --- Class: EquipSet --- --- A container for a ship's equipment. - ----@class EquipSet ----@field meta table -local EquipSet = utils.inherits(nil, "EquipSet") - -EquipSet.default = { - cargo=0, - engine=1, - laser_front=1, - laser_rear=0, - missile=0, - ecm=1, - radar=1, - target_scanner=1, - hypercloud=1, - hull_autorepair=1, - energy_booster=1, - atmo_shield=1, - cabin=50, - shield=9999, - scoop=2, - laser_cooler=1, - cargo_life_support=1, - autopilot=1, - trade_computer=1, - sensor = 2, - thruster = 1 -} - -function EquipSet.New (slots) - ---@class EquipSet - local obj = {} - obj.slots = {} - for k, n in pairs(EquipSet.default) do - obj.slots[k] = {__occupied = 0, __limit = n} - end - for k, n in pairs(slots) do - obj.slots[k] = {__occupied = 0, __limit = n} - end - setmetatable(obj, EquipSet.meta) - return obj -end - -local listeners = {} -function EquipSet:AddListener(listener) - listeners[self] = listeners[self] or {} - table.insert(listeners[self], listener) -end - -function EquipSet:CallListener(slot) - if not listeners[self] then return end - - for _, listener in ipairs(listeners[self]) do - listener(slot) - end -end - -function EquipSet:Serialize() - local serialize = { - slots = {} - } - - for k, v in pairs(self.slots) do - serialize.slots[k] = v - end - - return serialize -end - -function EquipSet.Unserialize(data) - setmetatable(data, EquipSet.meta) - return data -end - --- --- Group: Methods --- - --- --- Method: FreeSpace --- --- returns the available space in the given slot. --- --- Parameters: --- --- slot - The slot name. --- --- Return: --- --- free_space - The available space (integer) --- -function EquipSet:FreeSpace (slot) - local s = self.slots[slot] - if not s then - return 0 - end - return s.__limit - s.__occupied -end - -function EquipSet:SlotSize(slot) - local s = self.slots[slot] - if not s then - return 0 - end - return s.__limit -end - --- --- Method: OccupiedSpace --- --- returns the space occupied in the given slot. --- --- Parameters: --- --- slot - The slot name. --- --- Return: --- --- occupied_space - The occupied space (integer) --- -function EquipSet:OccupiedSpace (slot) - local s = self.slots[slot] - if not s then - return 0 - end - return s.__occupied -end - --- --- Method: Count --- --- returns the number of occurrences of the given equipment in the specified slot. --- --- Parameters: --- --- item - The equipment to count. --- --- slots - List of the slots to check. You can also provide a string if it --- is only one slot. If this argument is not provided, all slots --- will be searched. --- --- Return: --- --- free_space - The available space (integer) --- -function EquipSet:Count(item, slots) - local to_check - if type(slots) == "table" then - to_check = {} - for _, s in ipairs(slots) do - table.insert(to_check, self.slots[s]) - end - elseif slots == nil then - to_check = self.slots - else - to_check = {self.slots[slots]} - end - - local count = 0 - for _, slot in pairs(to_check) do - for _, e in pairs(slot) do - if e == item then - count = count + 1 - end - end - end - return count -end - -function EquipSet:__TriggerCallbacks(ship, slot) - ship:UpdateEquipStats() - -- if we reduce the available capacity, we need to update the maximum amount of cargo available - ship:setprop("totalCargo", math.min(self.slots.cargo.__limit, ship.usedCargo+ship.freeCapacity)) - self:CallListener(slot) -end - --- Method: __Remove_NoCheck (PRIVATE) --- --- Remove equipment without checking whether the slot is appropriate nor --- calling the uninstall hooks nor even checking the arguments sanity. --- It DOES check the free place in the slot. --- --- Parameters: --- --- Please refer to the Remove method. --- --- Return: --- --- Please refer to the Remove method. --- -function EquipSet:__Remove_NoCheck (item, num, slot) - local s = self.slots[slot] - if not s or s.__occupied == 0 then - return 0 - end - local removed = 0 - for i = 1,s.__limit do - if removed >= num or s.__occupied <= 0 then - return removed - end - if s[i] == item then - s[i] = nil - removed = removed + 1 - s.__occupied = s.__occupied - 1 - end - end - return removed -end - --- Method: __Add_NoCheck (PRIVATE) --- --- Add equipment without checking whether the slot is appropriate nor --- calling the install hooks nor even checking the arguments sanity. --- It DOES check the free place in the slot. --- --- Parameters: --- --- Please refer to the Add method. --- --- Return: --- --- Please refer to the Add method. --- -function EquipSet:__Add_NoCheck(item, num, slot) - if self:FreeSpace(slot) == 0 then - return 0 - end - local s = self.slots[slot] - local added = 0 - for i = 1,s.__limit do - if added >= num or s.__occupied >= s.__limit then - return added - end - if not s[i] then - s[i] = item - added = added + 1 - s.__occupied = s.__occupied + 1 - end - end - return added -end - --- Method: Add --- --- Add some equipment to the set, filling the specified slot as much as --- possible. --- --- Parameters: --- --- item - the equipment to install --- num - the number of pieces to install. If nil, only one will be installed. --- slot - the slot where to install the equipment. It will be checked against --- the equipment itself, the method will return -1 if the slot isn't --- valid. If nil, the default slot for the equipment will be used. --- --- Return: --- --- installed - the number of pieces actually installed, or -1 if the specified --- slot is not valid. --- -function EquipSet:Add(ship, item, num, slot) - num = num or 1 - if not slot then - slot = item:GetDefaultSlot(ship) - elseif not item:IsValidSlot(slot, ship) then - return -1 - end - assert(slot ~= "cargo", "Cargo slots for equipment are no longer valid") - - local added = self:__Add_NoCheck(item, num, slot) - if added == 0 then - return 0 - end - local postinst_diff = added - item:Install(ship, added, slot) - if postinst_diff > 0 then - self:__Remove_NoCheck(item, postinst_diff, slot) - added = added-postinst_diff - end - if added > 0 then - self:__TriggerCallbacks(ship, slot) - end - return added -end - --- Method: Remove --- --- Remove some equipment from the set. --- --- Parameters: --- --- item - the equipment to remove. --- num - the number of pieces to uninstall. If nil, only one will be removed. --- slot - the slot where to install the equipment. If nil, the default slot --- for the equipment will be used. --- --- Return: --- --- removed - the number of pieces actually removed. --- -function EquipSet:Remove(ship, item, num, slot) - num = num or 1 - if not slot then - slot = item:GetDefaultSlot(ship) - end - assert(slot ~= "cargo", "Cargo slots for equipment are no longer valid") - - local removed = self:__Remove_NoCheck(item, num, slot) - if removed == 0 then - return 0 - end - local postuninstall_diff = removed - item:Uninstall(ship, removed, slot) - if postuninstall_diff > 0 then - self:__Add_NoCheck(item, postuninstall_diff, slot) - removed = removed-postuninstall_diff - end - if removed > 0 then - self:__TriggerCallbacks(ship, slot) - end - return removed -end - -local EquipSet__ClearSlot = function (self, ship, slot) - local s = self.slots[slot] - local item_counts = {} - for k,v in pairs(s) do - if type(k) == 'number' then - item_counts[v] = (item_counts[v] or 0) + 1 - end - end - for item, count in pairs(item_counts) do - local uninstalled = item:Uninstall(ship, count, slot) - -- FIXME support failed uninstalls?? - -- note that failed uninstalls are almost incompatible with Ship::SetShipType - assert(uninstalled == count) - end - self.slots[slot] = {__occupied = 0, __limit = s.__limit} - self:__TriggerCallbacks(ship, slot) - -end - -function EquipSet:Clear(ship, slot_names) - if slot_names == nil then - for k,_ in pairs(self.slots) do - EquipSet__ClearSlot(self, ship, k) - end - - elseif type(slot_names) == 'string' then - EquipSet__ClearSlot(self, ship, slot_names) - - elseif type(slot_names) == 'table' then - for _, s in ipairs(slot_names) do - EquipSet__ClearSlot(self, ship, s) - end - end -end - -function EquipSet:Get(slot, index) - if type(index) == "number" then - return self.slots[slot][index] - end - local ret = {} - for i,v in pairs(self.slots[slot]) do - if type(i) == 'number' then - ret[i] = v - end - end - return ret -end - -function EquipSet:Set(ship, slot_name, index, item) - local slot = self.slots[slot_name] - - if index < 1 or index > slot.__limit then - error("EquipSet:Set(): argument 'index' out of range") - end - - local to_remove = slot[index] - if item == to_remove then return end - - if not to_remove or to_remove:Uninstall(ship, 1, slot_name) == 1 then - if not item or item:Install(ship, 1, slot_name) == 1 then - if not item then - slot.__occupied = slot.__occupied - 1 - elseif not to_remove then - slot.__occupied = slot.__occupied + 1 - end - slot[index] = item - self:__TriggerCallbacks(ship, slot_name) - else -- Rollback the uninstall - if to_remove then to_remove:Install(ship, 1, slot_name) end - end - end -end -Serializer:RegisterClass("EquipSet", EquipSet) -return EquipSet diff --git a/data/libs/Ship.lua b/data/libs/Ship.lua index d6435a51f6..60326e52c5 100644 --- a/data/libs/Ship.lua +++ b/data/libs/Ship.lua @@ -35,6 +35,10 @@ function Ship:Constructor() Event.Queue('onShipCreated', self) end +function Ship:OnShipTypeChanged() + -- immediately update any needed components or properties +end + -- class method function Ship.MakeRandomLabel () local letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" diff --git a/src/Ship.cpp b/src/Ship.cpp index 7264563088..c7b93318a8 100644 --- a/src/Ship.cpp +++ b/src/Ship.cpp @@ -91,8 +91,6 @@ Ship::Ship(const ShipType::Id &shipId) : m_aiMessage = AIERROR_NONE; m_decelerating = false; - InitEquipSet(); - SetModel(m_type->modelName.c_str()); SetupShields(); @@ -191,7 +189,7 @@ Ship::Ship(const Json &jsonObj, Space *space) : p.Set("fuelMassLeft", m_stats.fuel_tank_mass_left); // TODO: object components - m_equipSet.LoadFromJson(shipObj["equipSet"]); + // m_equipSet.LoadFromJson(shipObj["equipSet"]); m_controller = 0; const ShipController::Type ctype = shipObj["controller_type"]; @@ -298,7 +296,7 @@ void Ship::SaveToJson(Json &jsonObj, Space *space) shipObj["hyperspace_jump_sound"] = m_hyperspace.sounds.jump_sound; m_fixedGuns->SaveToJson(shipObj, space); - m_equipSet.SaveToJson(shipObj["equipSet"]); + // m_equipSet.SaveToJson(shipObj["equipSet"]); shipObj["ecm_recharge"] = m_ecmRecharge; shipObj["ship_type_id"] = m_type->id; @@ -322,24 +320,6 @@ void Ship::SaveToJson(Json &jsonObj, Space *space) jsonObj["ship"] = shipObj; // Add ship object to supplied object. } -void Ship::InitEquipSet() -{ - lua_State *l = Lua::manager->GetLuaState(); - - 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("New", slots); - - UpdateEquipStats(); - - lua_pop(l, 2); - LUA_DEBUG_END(l, 0); -} - void Ship::InitMaterials() { SceneGraph::Model *pModel = GetModel(); @@ -1602,9 +1582,6 @@ void Ship::SetShipId(const ShipType::Id &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()); SetupShields(); @@ -1615,7 +1592,8 @@ void Ship::SetShipType(const ShipType::Id &shipId) onFlavourChanged.emit(); if (IsType(ObjectType::PLAYER)) Pi::game->GetWorldView()->shipView->GetCameraController()->Reset(); - InitEquipSet(); + + LuaObject::CallMethod(this, "OnShipTypeChanged"); LuaEvent::Queue("onShipTypeChanged", this); } diff --git a/src/Ship.h b/src/Ship.h index 8f8f205b24..e0ccbff941 100644 --- a/src/Ship.h +++ b/src/Ship.h @@ -137,8 +137,6 @@ class Ship : public DynamicBody { int GetWheelTransition() const { return m_wheelTransition; } bool SpawnCargo(CargoBody *c_body) const; - LuaRef GetEquipSet() const { return m_equipSet; } - virtual bool IsInSpace() const override { return (m_flightState != HYPERSPACE); } void SetHyperspaceDest(const SystemPath &dest) { m_hyperspace.dest = dest; } @@ -274,8 +272,6 @@ class Ship : public DynamicBody { HyperdriveSoundsTable sounds; } m_hyperspace; - LuaRef m_equipSet; - Propulsion *m_propulsion; FixedGuns *m_fixedGuns; Shields *m_shields; diff --git a/src/lua/LuaShip.cpp b/src/lua/LuaShip.cpp index f56bb0eea4..27458e3fd0 100644 --- a/src/lua/LuaShip.cpp +++ b/src/lua/LuaShip.cpp @@ -1659,13 +1659,6 @@ static int l_ship_update_equip_stats(lua_State *l) return 0; } -static int l_ship_attr_equipset(lua_State *l) -{ - Ship *s = LuaObject::CheckFromLua(1); - s->GetEquipSet().PushCopyToStack(); - return 1; -} - template <> const char *LuaObject::s_type = "Ship"; @@ -1756,7 +1749,6 @@ void LuaObject::RegisterClass() }; const luaL_Reg l_attrs[] = { - { "equipSet", l_ship_attr_equipset }, { 0, 0 } }; From d9ccc6be08802dfc0debdb32164bab86d56d8202 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 15 Jan 2024 00:27:29 -0500 Subject: [PATCH 006/119] ItemCard improvements - Unify background drawing across callsites - More efficient and resilient draw cursor manipulation - Better handling of whole-card vs detail tooltips - Reserve space for "highlight bar" drawn in left margin --- data/pigui/libs/item-card.lua | 85 +++++++++++++---------------------- 1 file changed, 31 insertions(+), 54 deletions(-) diff --git a/data/pigui/libs/item-card.lua b/data/pigui/libs/item-card.lua index b202f7b4c6..a846e745b7 100644 --- a/data/pigui/libs/item-card.lua +++ b/data/pigui/libs/item-card.lua @@ -59,33 +59,31 @@ end -- Draw an empty item card background function ItemCard:drawBackground(isSelected) - local lineSpacing = self.lineSpacing local totalHeight = self:calcHeight() -- calculate the background area - local highlightBegin = ui.getCursorScreenPos() local highlightSize = Vector2(ui.getContentRegion().x, totalHeight) - local highlightEnd = highlightBegin + highlightSize ui.dummy(highlightSize) + local tl, br = ui.getItemRect() + + local isHovered = ui.isItemHovered() + local isClicked = ui.isItemClicked(0) - local isHovered = ui.isMouseHoveringRect(highlightBegin, highlightEnd + Vector2(0, lineSpacing.y)) and ui.isWindowHovered() local bgColor = ui.getButtonColor(self.colors, isHovered, isSelected) if self.highlightBar then -- if we're hovered, we want to draw a little bar to the left of the background if isHovered or isSelected then - ui.addRectFilled(highlightBegin - Vector2(self.rounding, 0), highlightBegin + Vector2(0, totalHeight), colors.equipScreenHighlight, 2, 5) + ui.addRectFilled(tl - Vector2(self.rounding, 0), tl + Vector2(0, totalHeight), colors.equipScreenHighlight, 2, 5) end - ui.addRectFilled(highlightBegin, highlightEnd, bgColor, self.rounding, (isHovered or isSelected) and 10 or 0) -- 10 == top-right | bottom-right + ui.addRectFilled(tl, br, bgColor, self.rounding, (isHovered or isSelected) and 10 or 0) -- 10 == top-right | bottom-right else -- otherwise just draw a normal rounded rectangle - ui.addRectFilled(highlightBegin, highlightEnd, bgColor, self.rounding, 0) + ui.addRectFilled(tl, br, bgColor, self.rounding, 0) end - local isClicked = isHovered and ui.isMouseClicked(0) - return isClicked, isHovered, highlightSize end @@ -99,6 +97,10 @@ end function ItemCard:draw(data, isSelected) local lineSpacing = self.lineSpacing + if self.highlightBar then + ui.addCursorPos(Vector2(self.lineSpacing.x, 0)) + end + -- initial sizing setup local textHeight = pionillium.body.size + pionillium.details.size + lineSpacing.y @@ -107,55 +109,33 @@ function ItemCard:draw(data, isSelected) local textWidth = ui.getContentRegion().x - iconSize.x - lineSpacing.x * 3 - local totalHeight = math.max(iconSize.y, textHeight) + lineSpacing.y * 2 - - -- calculate the background area - local highlightBegin = ui.getCursorScreenPos() - local highlightSize = Vector2(ui.getContentRegion().x, totalHeight) - local highlightEnd = highlightBegin + highlightSize - ui.beginGroup() - ui.dummy(highlightSize) - local isHovered = ui.isItemHovered() - local isClicked = ui.isItemClicked(0) + local isClicked, isHovered, highlightSize = self:drawBackground(isSelected) - local bgColor = ui.getButtonColor(self.colors, isHovered, isSelected) - - if self.highlightBar then - -- if we're hovered, we want to draw a little bar to the left of the background - if isHovered or isSelected then - ui.addRectFilled(highlightBegin - Vector2(self.rounding, 0), highlightBegin + Vector2(0, totalHeight), colors.equipScreenHighlight, 2, 5) - end - - ui.addRectFilled(highlightBegin, highlightEnd, bgColor, self.rounding, (isHovered or isSelected) and 10 or 0) -- 10 == top-right | bottom-right - else - -- otherwise just draw a normal rounded rectangle - ui.addRectFilled(highlightBegin, highlightEnd, bgColor, self.rounding, 0) - end + -- constrain rendering inside the frame padding area + local tl, br = ui.getItemRect() + PiGui.PushClipRect(tl + lineSpacing, br - lineSpacing, true) local detailTooltip = nil - PiGui.PushClipRect(highlightBegin + lineSpacing, highlightEnd - lineSpacing, true) - ui.setCursorScreenPos(highlightBegin) + ui.setCursorScreenPos(tl + lineSpacing) ui.withStyleVars({ ItemSpacing = lineSpacing }, function() - -- Set up padding for the top and left sides - ui.addCursorPos(lineSpacing + Vector2(0, iconOffset)) + -- Draw the main icon + -- The icon is offset vertically to center it in the available space if + -- smaller than the height of the text + ui.addIconSimple(tl + lineSpacing + Vector2(0, iconOffset), data.icon, iconSize, colors.white) - -- Draw the main icon and add some spacing next to it - ui.icon(data.icon, iconSize, colors.white) - -- ui.addCursorPos(Vector2(iconHeight + lineSpacing.x, 0)) - ui.sameLine() - ui.addCursorPos(Vector2(0, -iconOffset)) + -- Position the cursor for the title and details + local textLinePos = tl + lineSpacing + Vector2(iconSize.x + lineSpacing.x, 0) + ui.setCursorScreenPos(textLinePos) -- Draw the title line - local pos = ui.getCursorPos() self:drawTitle(data, textWidth, isSelected) -- Set up the details line - pos = pos + Vector2(0, ui.getTextLineHeightWithSpacing()) - ui.setCursorPos(pos) + local pos = textLinePos + Vector2(0, ui.getTextLineHeightWithSpacing()) -- This block draws several small icons with text next to them ui.withFont(pionillium.details, function() @@ -166,7 +146,7 @@ function ItemCard:draw(data, isSelected) -- do all of the text first to generate as few draw commands as possible for i, v in ipairs(data) do local offset = fieldSize * (i - 1) + smIconSize.x + 2 - ui.setCursorPos(pos + Vector2(offset, 1)) -- HACK: force 1-pixel offset here to align baselines + ui.setCursorScreenPos(pos + Vector2(offset, 1)) -- HACK: force 1-pixel offset here to align baselines ui.text(v[2]) if v[3] and ui.isItemHovered() then detailTooltip = v @@ -176,22 +156,15 @@ function ItemCard:draw(data, isSelected) -- Then draw the icons for i, v in ipairs(data) do local offset = fieldSize * (i - 1) - ui.setCursorPos(pos + Vector2(offset, 0)) + ui.setCursorScreenPos(pos + Vector2(offset, 0)) ui.icon(v[1], smIconSize, colors.white) end - - -- ensure we consume the appropriate amount of space if we don't have any details - if #data == 0 then - ui.newLine() - end end) -- Add a bit of spacing after the slot ui.spacing() - if isHovered and not detailTooltip then - self:drawTooltip(data, isSelected) - elseif detailTooltip then + if isHovered and detailTooltip then self:drawDetailTooltip(detailTooltip, detailTooltip[3]) end end) @@ -200,6 +173,10 @@ function ItemCard:draw(data, isSelected) ui.endGroup() + if ui.isItemHovered() and not detailTooltip then + self:drawTooltip(data, isSelected) + end + return isClicked, isHovered, highlightSize end From 2f5d7b8262c4eaa532a3f95d18e495cba59c29d3 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 15 Jan 2024 00:38:15 -0500 Subject: [PATCH 007/119] Equipment: mark equip as persistent, separate CabinType - Function prototypes for installing/removing EquipType items - Separate cabins into their own EquipType subclass - Use the LuaSerializer persist functionality instead of serializing equipment directly --- data/libs/EquipType.lua | 70 ++++++++++++++++++++++++++++- data/libs/Equipment.lua | 26 ++--------- data/modules/Equipment/Internal.lua | 5 ++- 3 files changed, 75 insertions(+), 26 deletions(-) diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index c33a63f7eb..d87c660436 100644 --- a/data/libs/EquipType.lua +++ b/data/libs/EquipType.lua @@ -33,12 +33,22 @@ local misc = {} -- -- ---@class EquipType +---@field id string +---@field mass number +---@field volume number +---@field slot { type: string, size: integer, hardpoint: boolean } | nil +---@field capabilities table? +---@field purchasable boolean +---@field icon_name string? +---@field tech_level integer | "MILITARY" ---@field transient table ---@field slots table -- deprecated +---@field __proto EquipType? local EquipType = utils.inherits(nil, "EquipType") ---@return EquipType function EquipType.New (specs) + ---@class EquipType local obj = {} for i,v in pairs(specs) do obj[i] = v @@ -55,6 +65,19 @@ function EquipType.New (specs) obj.slots = {obj.slots} end + if obj.slot and not obj.slot.hardpoint then + obj.slot.hardpoint = false + end + + if not obj.tech_level then + obj.tech_level = 1 + end + + if not obj.icon_name then + obj.icon_name = "equip_generic" + end + + -- TODO: remove all usage of obj.capabilities, transition to explicit volume for equipment -- fixup old capabilities system to explicitly specified mass/volume if obj.capabilities and obj.capabilities.mass then obj.mass = obj.capabilities.mass @@ -66,6 +89,10 @@ function EquipType.New (specs) return obj end +-- Override this with a function returning an equipment instance appropriate for the passed ship +-- (E.g. for equipment with mass/volume/cost dependent on the specific ship hull) +EquipType.SpecializeForShip = nil ---@type nil | fun(self: self, ship: Ship): EquipType + function EquipType._createTransient(obj) local l = Lang.GetResource(obj.l10n_resource) obj.transient = { @@ -74,10 +101,32 @@ function EquipType._createTransient(obj) } end +---@param ship Ship +---@param slot ShipDef.Slot? +function EquipType:OnInstall(ship, slot) + -- Override this for any custom installation logic needed + -- (e.g. mounting weapons) +end + +---@param ship Ship +---@param slot ShipDef.Slot? +function EquipType:OnRemove(ship, slot) + -- Override this for any custom uninstallation logic needed +end + function EquipType.isProto(inst) return not rawget(inst, "__proto") end +function EquipType:GetPrototype() + return rawget(self, "__proto") or self +end + +---@return EquipType +function EquipType:Instance() + return setmetatable({ __proto = self }, self.meta) +end + -- Patch an EquipType class to support a prototype-based equipment system -- `equipProto = EquipType.New({ ... })` to create an equipment prototype -- `equipInst = equipProto()` to create a new instance based on the created prototype @@ -245,6 +294,9 @@ function LaserType:Uninstall(ship, num, slot) end -- Single drive type, no support for slave drives. +---@class Equipment.HyperdriveType : EquipType +---@field fuel CommodityType +---@field byproduct CommodityType? local HyperdriveType = utils.inherits(EquipType, "HyperdriveType") function HyperdriveType:GetMaximumRange(ship) @@ -385,17 +437,32 @@ local SensorType = utils.inherits(EquipType, "SensorType") -- NOTE: all code related to managing a body scanner is implemented in the ScanManager component local BodyScannerType = utils.inherits(SensorType, "BodyScannerType") +---@class Equipment.CabinType : EquipType +---@field passenger Character? +local CabinType = utils.inherits(EquipType, "Equipment.CabinType") + +function CabinType:OnRemove(ship, slot) + EquipType.OnRemove(self, ship, slot) + + if self.passenger then + logWarning("Removing passenger cabin with passenger onboard!") + ship:setprop("cabin_occupied_cap", ship["cabin_occupied_cap"] - 1) + end +end + Serializer:RegisterClass("LaserType", LaserType) Serializer:RegisterClass("EquipType", EquipType) Serializer:RegisterClass("HyperdriveType", HyperdriveType) Serializer:RegisterClass("SensorType", SensorType) Serializer:RegisterClass("BodyScannerType", BodyScannerType) +Serializer:RegisterClass("Equipment.CabinType", CabinType) EquipType:SetupPrototype() LaserType:SetupPrototype() HyperdriveType:SetupPrototype() SensorType:SetupPrototype() BodyScannerType:SetupPrototype() +CabinType:SetupPrototype() return { laser = laser, @@ -405,5 +472,6 @@ return { LaserType = LaserType, HyperdriveType = HyperdriveType, SensorType = SensorType, - BodyScannerType = BodyScannerType + BodyScannerType = BodyScannerType, + CabinType = CabinType, } diff --git a/data/libs/Equipment.lua b/data/libs/Equipment.lua index 51eb122b65..9682c1e176 100644 --- a/data/libs/Equipment.lua +++ b/data/libs/Equipment.lua @@ -15,6 +15,7 @@ local laser = EquipTypes.laser local hyperspace = EquipTypes.hyperspace local misc = EquipTypes.misc +---@type table EquipTypes.new = {} function EquipTypes.Get(id) @@ -24,6 +25,8 @@ end function EquipTypes.Register(id, type) EquipTypes.new[id] = type type.id = id + + Serializer:RegisterPersistent("Equipment." .. id, type) end -- Constants: EquipSlot @@ -435,27 +438,4 @@ laser.large_plasma_accelerator = LaserType.New({ icon_name="equip_plasma_accelerator" }) -local serialize = function() - local ret = {} - for _,k in ipairs{"laser", "hyperspace", "misc"} do - local tmp = {} - for kk, vv in pairs(EquipTypes[k]) do - tmp[kk] = vv - end - ret[k] = tmp - end - return ret -end - -local unserialize = function (data) - for _,k in ipairs{"laser", "hyperspace", "misc"} do - local tmp = EquipTypes[k] - for kk, vv in pairs(data[k]) do - tmp[kk] = vv - end - end -end - -Serializer:Register("Equipment", serialize, unserialize) - return EquipTypes diff --git a/data/modules/Equipment/Internal.lua b/data/modules/Equipment/Internal.lua index 02f213fba9..b9cbb41661 100644 --- a/data/modules/Equipment/Internal.lua +++ b/data/modules/Equipment/Internal.lua @@ -6,6 +6,7 @@ local Equipment = require 'Equipment' local EquipType = EquipTypes.EquipType local SensorType = EquipTypes.SensorType +local CabinType = EquipTypes.CabinType --=============================================== -- Computer Modules @@ -14,7 +15,7 @@ local SensorType = EquipTypes.SensorType Equipment.Register("misc.autopilot", EquipType.New { l10n_key="AUTOPILOT", price=1400, purchasable=true, tech_level=1, - slot = { type="computer", size=1 }, + slot = { type="computer.autopilot", size=1 }, mass=0.2, volume=0.5, capabilities = { set_speed=1, autopilot=1 }, icon_name="equip_autopilot" }) @@ -153,7 +154,7 @@ Equipment.Register("misc.multi_scoop", EquipType.New { -- Slot-less equipment --=============================================== -Equipment.Register("misc.cabin", EquipType.New { +Equipment.Register("misc.cabin", CabinType.New { l10n_key="UNOCCUPIED_CABIN", price=1350, purchasable=true, tech_level=1, mass=1, volume=5, capabilities={ cabin=1 }, From 02bc14de696ebbe1710d6f629e6f55d54bc54dc7 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 15 Jan 2024 00:39:31 -0500 Subject: [PATCH 008/119] temp: add EquipSetCompat to fill old EquipSet API --- data/libs/EquipSetCompat.lua | 27 +++++++++++++++++++++++++++ src/lua/LuaShipDef.cpp | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 data/libs/EquipSetCompat.lua diff --git a/data/libs/EquipSetCompat.lua b/data/libs/EquipSetCompat.lua new file mode 100644 index 0000000000..4536bd246e --- /dev/null +++ b/data/libs/EquipSetCompat.lua @@ -0,0 +1,27 @@ +local Compat = {} + +Compat.default = { + cargo=0, + engine=1, + laser_front=1, + laser_rear=0, + missile=0, + ecm=1, + radar=1, + target_scanner=1, + hypercloud=1, + hull_autorepair=1, + energy_booster=1, + atmo_shield=1, + cabin=50, + shield=9999, + scoop=2, + laser_cooler=1, + cargo_life_support=1, + autopilot=1, + trade_computer=1, + sensor = 2, + thruster = 1 +} + +return Compat diff --git a/src/lua/LuaShipDef.cpp b/src/lua/LuaShipDef.cpp index 7a46befef3..cfb3b37af9 100644 --- a/src/lua/LuaShipDef.cpp +++ b/src/lua/LuaShipDef.cpp @@ -298,7 +298,7 @@ void LuaShipDef::Register() if (!lua_getmetatable(l, -1)) { lua_newtable(l); } - pi_lua_import(l, "EquipSet"); + pi_lua_import(l, "EquipSetCompat"); luaL_getsubtable(l, -1, "default"); lua_setfield(l, -3, "__index"); lua_pop(l, 1); From 4076518f720cacd724069e643397aee3a3c13c78 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 15 Jan 2024 20:40:01 -0500 Subject: [PATCH 009/119] Fully remove Ship:GetStats() from Lua API - Was originally "removed" in 2013, but some references crept back in. - Prefer using properties defined directly on a Ship object instead --- .../modules/flight-ui/target-scanner.lua | 8 ++-- data/pigui/modules/system-view-ui.lua | 10 +++-- src/lua/LuaShip.cpp | 38 ------------------- 3 files changed, 9 insertions(+), 47 deletions(-) diff --git a/data/pigui/modules/flight-ui/target-scanner.lua b/data/pigui/modules/flight-ui/target-scanner.lua index 637bca25c6..7949a010c6 100644 --- a/data/pigui/modules/flight-ui/target-scanner.lua +++ b/data/pigui/modules/flight-ui/target-scanner.lua @@ -25,9 +25,8 @@ local function displayTargetScannerFor(target, offset) local class = target:GetShipType() local label = target.label local engine = target:GetEquip('engine', 1) - local stats = target:GetStats() - local mass = stats.staticMass - local cargo = stats.usedCargo + local mass = target.staticMass + local cargo = target.usedCargo if engine then engine = engine:GetName() else @@ -81,8 +80,7 @@ local function displayTargetScanner() local arrival = target:IsArrival() local ship = target:GetShip() if ship then - local stats = ship:GetStats() - local mass = stats.staticMass + local mass = ship.staticMass local path,destName = ship:GetHyperspaceDestination() local date = target:GetDueDate() local dueDate = ui.Format.Datetime(date) diff --git a/data/pigui/modules/system-view-ui.lua b/data/pigui/modules/system-view-ui.lua index 0cd8c94d59..bceb547d73 100644 --- a/data/pigui/modules/system-view-ui.lua +++ b/data/pigui/modules/system-view-ui.lua @@ -844,17 +844,19 @@ function Windows.objectInfo:Show() self.data = data elseif obj.ref:IsShip() then -- physical body + ---@cast body Ship -- TODO: the advanced target scanner should add additional data here, -- but we really do not want to hardcode that here. there should be -- some kind of hook that the target scanner can hook into to display -- more info here. -- This is what should be inserted: table.insert(data, { name = luc.SHIP_TYPE, value = body:GetShipType() }) - if player:GetEquipCountOccupied('target_scanner') > 0 or player:GetEquipCountOccupied('advanced_target_scanner') > 0 then - local hd = body:GetEquip("engine", 1) + if (player["target_scanner_level_cap"] or 0) > 0 then + local hd = body:GetInstalledHyperdrive() table.insert(data, { name = luc.HYPERDRIVE, value = hd and hd:GetName() or lc.NO_HYPERDRIVE }) - table.insert(data, { name = luc.MASS, value = Format.MassTonnes(body:GetStats().staticMass) }) - table.insert(data, { name = luc.CARGO, value = Format.MassTonnes(body:GetStats().usedCargo) }) + table.insert(data, { name = luc.MASS, value = Format.MassTonnes(body.staticMass) }) + -- FIXME: this should use a separate cargoMass property + table.insert(data, { name = luc.CARGO, value = Format.MassTonnes(body.usedCargo) }) end else data = {} diff --git a/src/lua/LuaShip.cpp b/src/lua/LuaShip.cpp index 27458e3fd0..13c72e27be 100644 --- a/src/lua/LuaShip.cpp +++ b/src/lua/LuaShip.cpp @@ -1088,43 +1088,6 @@ static int l_ship_get_velocity(lua_State *l) return 1; } -/* Method: GetStats - * - * Return some ship stats. - * - * Returns: - * - * Return a table containing: - * - usedCapacity - * - usedCargo - * - freeCapacity - * - staticMass - * - hullMassLeft - * - hyperspaceRange - * - hyperspaceRangeMax - * - shieldMass - * - shieldMassLeft - * - fuelTankMassLeft - * - */ -static int l_ship_get_stats(lua_State *l) -{ - Ship *s = LuaObject::CheckFromLua(1); - LuaTable t(l, 0, 10); - const shipstats_t &stats = s->GetStats(); - t.Set("usedCapacity", stats.used_capacity); - t.Set("usedCargo", stats.used_cargo); - t.Set("freeCapacity", stats.free_capacity); - t.Set("staticMass", stats.static_mass); - t.Set("hullMassLeft", stats.hull_mass_left); - t.Set("hyperspaceRange", stats.hyperspace_range); - t.Set("hyperspaceRangeMax", stats.hyperspace_range_max); - t.Set("shieldMass", stats.shield_mass); - t.Set("shieldMassLeft", stats.shield_mass_left); - t.Set("fuelTankMassLeft", stats.fuel_tank_mass_left); - return 1; -} - /* * Method: GetPosition * @@ -1731,7 +1694,6 @@ void LuaObject::RegisterClass() { "GetFlightState", l_ship_get_flight_state }, { "GetCruiseSpeed", l_ship_get_cruise_speed }, { "GetFollowTarget", l_ship_get_follow_target }, - { "GetStats", l_ship_get_stats }, { "GetHyperspaceCountdown", l_ship_get_hyperspace_countdown }, { "IsHyperspaceActive", l_ship_is_hyperspace_active }, From b1d9c97e9cece1690586c1108414e067c65eff81 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 15 Jan 2024 21:02:35 -0500 Subject: [PATCH 010/119] Ship: rename usedCapacity -> loadedMass - Amend documentation for C++ set ship properties - Temporary re-definition of freeCapacity before deprecation --- src/Ship.cpp | 15 +++++++-------- src/Ship.h | 3 +-- src/lua/LuaShip.cpp | 36 +++++++++++++++++------------------- 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/src/Ship.cpp b/src/Ship.cpp index c7b93318a8..adc6734e13 100644 --- a/src/Ship.cpp +++ b/src/Ship.cpp @@ -622,18 +622,17 @@ void Ship::UpdateEquipStats() { PropertyMap &p = Properties(); - m_stats.used_capacity = p.Get("mass_cap"); - m_stats.used_cargo = 0; + m_stats.loaded_mass = p.Get("mass_cap"); + m_stats.static_mass = m_stats.loaded_mass + m_type->hullMass; - m_stats.free_capacity = m_type->capacity - m_stats.used_capacity; - m_stats.static_mass = m_stats.used_capacity + m_type->hullMass; + m_stats.free_capacity = m_type->capacity - p.Get("equipVolume").get_integer(); + // m_stats.free_capacity = m_type->capacity - m_stats.loaded_mass; - p.Set("usedCapacity", m_stats.used_capacity); - p.Set("freeCapacity", m_stats.free_capacity); - - p.Set("totalMass", m_stats.static_mass); + p.Set("loadedMass", m_stats.loaded_mass); p.Set("staticMass", m_stats.static_mass); + p.Set("freeCapacity", m_stats.free_capacity); + float shield_cap = p.Get("shield_cap"); m_stats.shield_mass = TONS_HULL_PER_SHIELD * shield_cap; p.Set("shieldMass", m_stats.shield_mass); diff --git a/src/Ship.h b/src/Ship.h index e0ccbff941..8bc8333030 100644 --- a/src/Ship.h +++ b/src/Ship.h @@ -36,7 +36,7 @@ namespace Graphics { } struct shipstats_t { - int used_capacity; + int loaded_mass; int used_cargo; int free_capacity; int static_mass; // cargo, equipment + hull @@ -287,7 +287,6 @@ class Ship : public DynamicBody { void SetupShields(); void EnterHyperspace(); void InitMaterials(); - void InitEquipSet(); bool m_invulnerable; diff --git a/src/lua/LuaShip.cpp b/src/lua/LuaShip.cpp index 13c72e27be..0b9718dc55 100644 --- a/src/lua/LuaShip.cpp +++ b/src/lua/LuaShip.cpp @@ -1858,44 +1858,42 @@ void LuaObject::RegisterClass() * experimental * * - * Attribute: staticMass - * - * Mass of the ship including hull, equipment and cargo, but excluding - * thruster fuel mass. Measured in tonnes. + * Attribute: loadedMass * - * Availability: - * - * November 2013 + * Mass of all contents of the ship, including equipment and cargo, but + * excluding hull and thruster fuel mass. * * Status: * - * experimental - * - * - * Attribute: usedCapacity + * stable * - * Hull capacity used by equipment and cargo. Measured in tonnes. * - * Availability: + * Attribute: staticMass * - * November 2013 + * Mass of the ship including hull, equipment and cargo, but excluding + * thruster fuel mass. Measured in tonnes. * * Status: * - * experimental + * stable * * * Attribute: usedCargo * - * Hull capacity used by cargo only (not equipment). Measured in tonnes. + * Hull capacity used by cargo only (not equipment). Measured in cargo units. * - * Availability: + * Status: * - * November 2013 + * stable + * + * + * Attribute: totalCargo + * + * Hull capacity available for cargo (not equipment). Measured in cargo units. * * Status: * - * experimental + * stable * * * Attribute: freeCapacity From 9024710dc41139ebcdc627bf136926eaaf2b3f76 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 15 Jan 2024 21:05:54 -0500 Subject: [PATCH 011/119] ShipType: capacity is volume, add cargo attribute - ShipType capacity is now a floating-point number defining equipment volume - Added a new cargo value as a migration path for slots.cargo - Add Lua type definition file for LuaShipDef --- data/meta/ShipDef.lua | 43 ++++++++++++++++++++++++++++++++++++++++++ src/ShipType.cpp | 3 ++- src/ShipType.h | 3 ++- src/lua/LuaShipDef.cpp | 1 + 4 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 data/meta/ShipDef.lua diff --git a/data/meta/ShipDef.lua b/data/meta/ShipDef.lua new file mode 100644 index 0000000000..a51d7c7652 --- /dev/null +++ b/data/meta/ShipDef.lua @@ -0,0 +1,43 @@ +-- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +-- This file implements type information about C++ modules for Lua static analysis + +---@meta + +---@class ShipDef +---@field id string +---@field name string +---@field shipClass string +---@field manufacturer string +---@field modelName string +---@field cockpitName string +---@field tag ShipTypeTag +---@field roles string[] +-- +---@field angularThrust number +---@field linearThrust table +---@field linAccelerationCap table +---@field effectiveExhaustVelocity number +---@field thrusterFuelUse number -- deprecated +---@field frontCrossSec number +---@field sideCrossSec number +---@field topCrossSec number +---@field atmosphericPressureLimit number +-- +---@field capacity number +---@field cargo integer +---@field hullMass number +---@field fuelTankMass number +---@field basePrice number +---@field minCrew integer +---@field maxCrew integer +---@field hyperdriveClass integer +---@field equipSlotCapacity table -- deprecated +--- +---@field raw table The entire ShipDef JSON object as a Lua table + +---@type table +local ShipDef = {} + +return ShipDef diff --git a/src/ShipType.cpp b/src/ShipType.cpp index f90430189a..6183b07b14 100644 --- a/src/ShipType.cpp +++ b/src/ShipType.cpp @@ -179,7 +179,8 @@ ShipType::ShipType(const Id &_id, const std::string &path) angThrust = angThrust * 0.5f; hullMass = data.value("hull_mass", 100); - capacity = data.value("capacity", 0); + capacity = data.value("capacity", 0.0); + cargo = data.value("cargo", 0); fuelTankMass = data.value("fuel_tank_mass", 5); for (Json::iterator slot = data["slots"].begin(); slot != data["slots"].end(); ++slot) { diff --git a/src/ShipType.h b/src/ShipType.h index 9f37731cf2..d7ac198a09 100644 --- a/src/ShipType.h +++ b/src/ShipType.h @@ -50,7 +50,8 @@ struct ShipType { bool isDirectionColorDefined[THRUSTER_MAX]; double thrusterUpgrades[4]; double atmosphericPressureLimit; - int capacity; // tonnes + float capacity; // m3 + int cargo; // cargo units ~ m3 int hullMass; float effectiveExhaustVelocity; // velocity at which the propellant escapes the engines int fuelTankMass; //full fuel tank mass, on top of hullMass diff --git a/src/lua/LuaShipDef.cpp b/src/lua/LuaShipDef.cpp index cfb3b37af9..bd641a7b9d 100644 --- a/src/lua/LuaShipDef.cpp +++ b/src/lua/LuaShipDef.cpp @@ -262,6 +262,7 @@ void LuaShipDef::Register() pi_lua_settable(l, "tag", EnumStrings::GetString("ShipTypeTag", st.tag)); pi_lua_settable(l, "angularThrust", st.angThrust); pi_lua_settable(l, "capacity", st.capacity); + pi_lua_settable(l, "cargo", st.cargo); pi_lua_settable(l, "hullMass", st.hullMass); pi_lua_settable(l, "fuelTankMass", st.fuelTankMass); pi_lua_settable(l, "basePrice", st.baseprice); From 5dd80debaaa413a697b51af1c11fb41cbfdfe836 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 18 Jan 2024 02:05:29 -0500 Subject: [PATCH 012/119] Cargo space is separate from equipment volume - Replace the shared capacity metric with separate cargo and equipment volumes. - If needed later, cargo and equipment space could potentially be "converted" into the other. --- data/libs/CargoManager.lua | 19 ++++++------------- data/pigui/modules/new-game-window/ship.lua | 2 +- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/data/libs/CargoManager.lua b/data/libs/CargoManager.lua index 43aaeef3fe..4f2e90b917 100644 --- a/data/libs/CargoManager.lua +++ b/data/libs/CargoManager.lua @@ -31,7 +31,7 @@ function CargoManager:Constructor(ship) if not self.ship:hasprop("totalCargo") then ship:setprop("totalCargo", self:GetTotalSpace()) end - + if not self.ship:hasprop("usedCargo") then ship:setprop("usedCargo", 0) end @@ -63,14 +63,7 @@ end -- -- Returns the available amount of cargo space currently present on the vessel. function CargoManager:GetFreeSpace() - local ship = self.ship - - -- use mass_cap directly here instead of freeCapacity because this can be - -- called before ship:UpdateEquipStats() has been called - local avail_mass = ShipDef[ship.shipId].capacity - (ship.mass_cap or 0) - local cargo_space = ShipDef[ship.shipId].equipSlotCapacity.cargo or 0 - - return math.min(avail_mass, cargo_space - self.usedCargoSpace) + return self:GetTotalSpace() - self.usedCargoSpace end -- Method: GetUsedSpace @@ -84,7 +77,7 @@ end -- -- Returns the maximum amount of cargo that could be stored on the vessel. function CargoManager:GetTotalSpace() - return self:GetFreeSpace() + self.usedCargoSpace + return ShipDef[self.ship.shipId].cargo end -- Method: AddCommodity @@ -103,9 +96,9 @@ end ---@param type CommodityType ---@param count integer function CargoManager:AddCommodity(type, count) - -- TODO: use a cargo volume metric with variable mass instead of fixed 1m^3 == 1t + -- TODO: use a cargo volume metric with variable mass instead of fixed 1t == 1m^3 local required_space = (type.mass or 1) * (count or 1) - + if self:GetFreeSpace() < required_space then return false end @@ -251,7 +244,7 @@ end function CargoManager:Unserialize() setmetatable(self, CargoManager.meta) self.listeners = {} - + return self end diff --git a/data/pigui/modules/new-game-window/ship.lua b/data/pigui/modules/new-game-window/ship.lua index 5392b33de3..a3a8e3f6e0 100644 --- a/data/pigui/modules/new-game-window/ship.lua +++ b/data/pigui/modules/new-game-window/ship.lua @@ -922,7 +922,7 @@ end function ShipSummary:prepareAndValidateParamList() local def = ShipDef[ShipType.value] local usedSlots = ShipEquip.usedSlots - local freeCargo = math.min(def.equipSlotCapacity.cargo, def.capacity - ShipEquip.mass) + local freeCargo = def.cargo self.cargo.valid = true self.equip.valid = true local eq_n_cargo = { valid = true } From 3321ee85df22e009f851355b84b3c4fc022a6b3e Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 19 Jan 2024 15:35:10 -0500 Subject: [PATCH 013/119] Add ShipConfig class - The counterpart to a ShipDef, a ShipConfig is concerned with expressing the slot arrangement and equipment capacity of a specific version of a ship's hull - The default configuration for a ship is loaded from the ship.json file, but additional ship configurations can be supported with minimal effort - Each ShipConfig is comprised of a set of slots with unique identifiers - The default set of slots can be modified or overridden entirely from within the ship.json file by specifying key-value pairs in the equipment_slots object --- data/libs/ShipConfig.lua | 98 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 data/libs/ShipConfig.lua diff --git a/data/libs/ShipConfig.lua b/data/libs/ShipConfig.lua new file mode 100644 index 0000000000..84c57579e0 --- /dev/null +++ b/data/libs/ShipConfig.lua @@ -0,0 +1,98 @@ +-- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +local ShipDef = require 'ShipDef' +local Serializer = require 'Serializer' +local utils = require 'utils' + +-- Class: ShipDef.Slot +-- +-- Generic interface for an equipment-containing slot in a shipdef +-- +-- Represents a constrained potential mounting point for ship equipment +-- either internal or external to a ship. +-- Can contain additional fields for specific slot types. +---@class ShipDef.Slot +---@field clone fun(self, mixin):self +local Slot = utils.proto("ShipDef.Slot") + +Slot.id = "" +Slot.type = "" +Slot.size = 1 +Slot.size_min = nil ---@type number? +Slot.tag = nil ---@type string? +Slot.default = nil ---@type string? +Slot.hardpoint = false +Slot.i18n_key = nil ---@type string? +Slot.i18n_res = "equipment-core" +Slot.count = nil ---@type integer? + +-- Class: ShipDef.Config +-- +-- Represents a specific "ship configuration", being a list of equipment slots +-- and associated data. +-- +-- The default configuration for a ship hull is defined in its JSON shipdef and +-- consumed by Lua as a ship config. +---@class ShipDef.Config +---@field clone fun():self +local ShipConfig = utils.proto("ShipDef.Config") + +ShipConfig.id = "" +ShipConfig.capacity = 0 + +-- Default slot config for a new shipdef +-- Individual shipdefs can redefine slots or remove them by setting the slot to 'false' +---@type table +ShipConfig.slots = { + sensor = Slot:clone { type = "sensor", size = 1 }, + computer_1 = Slot:clone { type = "computer", size = 1 }, + computer_2 = Slot:clone { type = "computer", size = 1 }, + hull_mod = Slot:clone { type = "hull", size = 1 }, + hyperdrive = Slot:clone { type = "hyperdrive", size = 1 }, + thruster = Slot:clone { type = "thruster", size = 1 }, + scoop = Slot:clone { type = "scoop", size = 1, hardpoint = true }, +} + +function ShipConfig:__clone() + self.slots = utils.map_table(self.slots, function(key, slot) + return key, slot:clone() + end) +end + +local function CreateShipConfig(def) + ---@class ShipDef.Config + local newShip = ShipConfig:clone() + Serializer:RegisterPersistent("ShipDef." .. def.id, newShip) + + newShip.id = def.id + newShip.capacity = def.capacity + + table.merge(newShip.slots, def.raw.equipment_slots or {}, function(name, slotDef) + if slotDef == false then return name, nil end + + local newSlot = table.merge(Slot:clone(), slotDef) + + return name, newSlot + end) + + for name, slot in pairs(newShip.slots) do + slot.id = name + end + + -- if def.id == 'coronatrix' then utils.print_r(newShip) end + + return newShip +end + +---@type table +local Config = {} + +for id, def in pairs(ShipDef) do + Config[id] = CreateShipConfig(def) +end + +Serializer:RegisterClass("ShipDef.Config", ShipConfig) +Serializer:RegisterClass("ShipDef.Slot", Slot) + +return Config From ecd690f6447d258aafebceb4fe3fac9174ae14b0 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 19 Jan 2024 15:58:55 -0500 Subject: [PATCH 014/119] New EquipSet implementation - Unlike the previous iteration, EquipSet v2 is concerned with modelling individual equipment items installed in unique slots - It also supports "loose" items which are installed somewhere in the ship's equipment volume without requiring a slot - Equipment volume is now completely separated from cargo volume and equipment cannot be installed in cargo space or vice versa. - Support for trading off equipment volume for cargo volume may be added later if deemed a good idea from a gameplay balance perspective. --- data/libs/EquipSet.lua | 405 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 405 insertions(+) create mode 100644 data/libs/EquipSet.lua diff --git a/data/libs/EquipSet.lua b/data/libs/EquipSet.lua new file mode 100644 index 0000000000..abc2fba538 --- /dev/null +++ b/data/libs/EquipSet.lua @@ -0,0 +1,405 @@ +-- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +local ShipConfig = require 'ShipConfig' +local Serializer = require 'Serializer' + +local utils = require 'utils' + +-- Class: EquipSet +-- +-- EquipSet is responsible for managing all installed ship equipment items. +-- It provides helpers to query installed items of specific types, and +-- centralizes the management of "capability properties" set on the owning ship. + +---@class EquipSet +---@field New fun(ship: Ship): EquipSet +local EquipSet = utils.class("EquipSet") + +local function slotTypeMatches(equipType, slotType) + return string.sub(equipType, 1, #slotType) == slotType +end + +-- Function: CompatibleWithSlot +-- +-- Static helper function to check if the given equipment item is compatible +-- with the given slot object. Validates type and size parameters of the slot. +---@param equip EquipType +---@param slot ShipDef.Slot? +function EquipSet.CompatibleWithSlot(equip, slot) + local equipSlot = equip.slot or false + if not slot then return not equipSlot end + + return equipSlot and + slotTypeMatches(equip.slot.type, slot.type) + and (equip.slot.size <= slot.size) + and (equip.slot.size >= (slot.size_min or slot.size)) +end + +---@param ship Ship +function EquipSet:Constructor(ship) + self.ship = ship + self.config = ShipConfig[ship.shipId] + + -- Stores a mapping of slot id -> equipment item + -- Non-slot equipment is stored in the array portion + self.installed = {} ---@type table|EquipType[] + -- Note: the integer value stored in the cache is NOT the current array + -- index of the given item. It's simply a non-nil integer to indicate the + -- item is not installed in a slot. + self.cache = {} ---@type table + + self.listeners = {} + + -- Initialize ship properties we're responsible for modifying + self.ship:setprop("mass_cap", self.ship["mass_cap"] or 0) + self.ship:setprop("equipVolume", self.ship.equipVolume or 0) + self.ship:setprop("totalVolume", self.ship.totalVolume or self.config.capacity) +end + +-- Function: GetFreeVolume +-- +-- Returns the available volume for mounting equipment +---@return number +function EquipSet:GetFreeVolume() + return self.ship.totalVolume - self.ship.equipVolume +end + +-- Function: GetSlotHandle +-- +-- Return a reference to the slot with the given ID managed by this EquipSet. +-- The returned slot should be considered immutable. +---@param id string +---@return ShipDef.Slot? +function EquipSet:GetSlotHandle(id) + return self.config.slots[id] +end + +-- Function: GetItemInSlot +-- +-- Return the equipment item installed in the given slot, if present. +---@param slot ShipDef.Slot +---@return EquipType? +function EquipSet:GetItemInSlot(slot) + -- The equipment item is not stored in the slot itself to reduce savefile + -- size. While the API would be marginally simpler if so, there would be a + -- significant amount of (de)serialization overhead. + return self.installed[slot.id] +end + +-- Function: GetFreeSlotForEquip +-- +-- Attempts to find an available slot where the passed equipment item could be +-- installed. Does not attempt to find the most optimal slot - the first slot +-- which meets the type and size constraints for the equipment item is returned. +---@param equip EquipType +---@return ShipDef.Slot? +function EquipSet:GetFreeSlotForEquip(equip) + if not equip.slot then return nil end + + local filter = function(_, slot) + return not slot.item + and slot.hardpoint == equip.slot.hardpoint + and self:CanInstallInSlot(slot, equip) + end + + for _, slot in pairs(self.config.slots) do + if filter(_, slot) then + return slot + end + end + + return nil +end + +-- Function: GetAllSlotsOfType +-- +-- Return a list of all slots matching the given filter parameters. +-- If hardpoint is not specified, returns both hardpoint and internal slots. +---@param type string +---@param hardpoint boolean? +---@return ShipDef.Slot[] +function EquipSet:GetAllSlotsOfType(type, hardpoint) + local t = {} + + for _, slot in pairs(self.config.slots) do + local match = (hardpoint == nil or hardpoint == slot.hardpoint) + and slotTypeMatches(slot.type, type) + if match then table.insert(t, slot) end + end + + return t +end + +-- Function: CountInstalledWithFilter +-- +-- Return a count of all installed equipment matching the given filter function +---@param filter fun(equip: EquipType): boolean +---@return integer +function EquipSet:CountInstalledWithFilter(filter) + local count = 0 + + for _, equip in pairs(self.installed) do + if filter(equip) then + count = count + (equip.count or 1) + end + end + + return count +end + +-- Function: GetInstalledWithFilter +-- +-- Return a list of all installed equipment matching the given filter function +---@param filter fun(equip: EquipType): boolean +---@return EquipType[] +function EquipSet:GetInstalledWithFilter(filter) + local out = {} + + for _, equip in pairs(self.installed) do + if filter(equip) then + table.insert(out, equip) + end + end + + return out +end + +-- Function: GetInstalledOfType +-- +-- Return a list of all installed equipment matching the given slot type +---@param type string type filter +---@return EquipType[] +function EquipSet:GetInstalledOfType(type) + local out = {} + + for _, equip in pairs(self.installed) do + if equip.slot and slotTypeMatches(equip.slot.type, type) then + table.insert(out, equip) + end + end + + return out +end + +-- Function: GetInstalledEquipment +-- +-- Returns an array containing all equipment items installed on this ship, +-- including both slot-based equipment and freely-installed equipment. +---@return EquipType[] +function EquipSet:GetInstalledEquipment() + local out = {} + + for _, equip in pairs(self.installed) do + table.insert(out, equip) + end + + return out +end + +-- Function: GetInstalledNonSlot +-- +-- Returns an array containing all non-slot equipment items installed on this +-- ship. Items installed in a specific slot are not returned. +-- +-- Status: +-- +-- experimental +-- +---@return EquipType[] +function EquipSet:GetInstalledNonSlot() + local out = {} + + for i, equip in ipairs(self.installed) do + out[i] = equip + end + + return out +end + +-- Function: CanInstallInSlot +-- +-- Checks if the given equipment item could potentially fit in the passed slot, +-- given the current state of the ship. +-- +-- If there is an item in the current slot, validates the fit as though that +-- item were not currently installed. +-- Returns false if the equipment item is not compatible with slot mounting. +---@param slotHandle ShipDef.Slot +---@param equipment EquipType +function EquipSet:CanInstallInSlot(slotHandle, equipment) + local equipped = self:GetItemInSlot(slotHandle) + local freeVolume = self:GetFreeVolume() + (equipped and equipped.volume or 0) + + return (equipment.slot or false) + and EquipSet.CompatibleWithSlot(equipment, slotHandle) + and (freeVolume >= equipment.volume) +end + +-- Function: CanInstallLoose +-- +-- Checks if the given equipment item can be installed in the free equipment +-- volume of the ship. Returns false if the equipment item requires a slot. +---@param equipment EquipType +function EquipSet:CanInstallLoose(equipment) + return not equipment.slot + and self:GetFreeVolume() >= equipment.volume +end + +-- Function: AddListener +-- +-- Register an event listener function to be notified of changes to this ship's +-- equipment loadout. +function EquipSet:AddListener(fun) + table.insert(self.listeners, fun) +end + +-- Function: RemoveListener +-- +-- Remove a previously-registered event listener function. +function EquipSet:RemoveListener(fun) + utils.remove_elem(self.listeners, fun) +end + +-- Function: Install +-- +-- Install an equipment item in the given slot or in free equipment volume. +-- +-- The passed equipment item must be an equipment item instance, not an +-- equipment item prototype. +-- +-- The passed slot must be a slot returned from caling GetSlotHandle or +-- GetAllSlotsOfType on this EquipSet. +---@param equipment EquipType +---@param slotHandle ShipDef.Slot? +---@return boolean +function EquipSet:Install(equipment, slotHandle) + print("Installing equip {} in slot {}" % { equipment:GetName(), slotHandle }) + + if slotHandle then + if not self:CanInstallInSlot(slotHandle, equipment) then + return false + end + + if self.installed[slotHandle.id] then + return false + end + + self.installed[slotHandle.id] = equipment + else + if not self:CanInstallLoose(equipment) then + return false + end + + table.insert(self.installed, equipment) + end + + self:_InstallInternal(equipment) + equipment:OnInstall(self.ship, slotHandle) + + for _, fun in ipairs(self.listeners) do + fun("install", equipment, slotHandle) + end + + return true +end + +-- Function: Remove +-- +-- Remove a previously-installed equipment item from this ship. +-- +-- The equipment item must be the same item instance that was installed prior; +-- passing an equipment prototype instance will not result in any equipment +-- item being removed. +---@param equipment EquipType +---@return boolean +function EquipSet:Remove(equipment) + local cacheKey = self.cache[equipment] + + if not cacheKey then + return false + end + + local slotHandle = nil + + if type(cacheKey) == "string" then + slotHandle = self:GetSlotHandle(cacheKey) + end + + equipment:OnRemove(self.ship, slotHandle) + self:_RemoveInternal(equipment) + + self.cache[equipment] = nil + + if slotHandle then + self.installed[cacheKey] = nil + else + utils.remove_elem(self.installed, equipment) + end + + for _, fun in ipairs(self.listeners) do + fun("remove", equipment, slotHandle) + end + + return true +end + +-- Update ship properties after installing an equipment item +---@param equipment EquipType +---@private +function EquipSet:_InstallInternal(equipment) + self.ship:setprop("mass_cap", self.ship["mass_cap"] + equipment.mass) + self.ship:setprop("equipVolume", self.ship.equipVolume + equipment.volume) + + if equipment.capabilities then + for k, v in pairs(equipment.capabilities) do + local cap = k .. "_cap" + self.ship:setprop(cap, (self.ship:hasprop(cap) and self.ship[cap] or 0) + v) + end + end + + self.ship:UpdateEquipStats() +end + +-- Update ship properties after removing an equipment item +---@param equipment EquipType +---@private +function EquipSet:_RemoveInternal(equipment) + self.ship:setprop("mass_cap", self.ship["mass_cap"] - equipment.mass) + self.ship:setprop("equipVolume", self.ship.equipVolume - equipment.volume) + + if equipment.capabilities then + for k, v in pairs(equipment.capabilities) do + local cap = k .. "_cap" + self.ship:setprop(cap, self.ship[cap] - v) + end + end + + self.ship:UpdateEquipStats() +end + +-- Remove transient fields from the serialized copy of the EquipSet +function EquipSet:Serialize() + local obj = table.copy(self) + + obj.cache = nil + obj.listeners = nil + + return obj +end + +-- Restore transient fields to the unserialized version of the EquipSet +function EquipSet:Unserialize() + self.cache = {} + self.listeners = {} + + for k, v in pairs(self.installed) do + self.cache[v] = k + end + + return setmetatable(self, EquipSet.meta) +end + +Serializer:RegisterClass("EquipSet", EquipSet) + +return EquipSet From f4e08f976499acb19fe91dd9220e8b9ee0f3caf0 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 19 Jan 2024 16:25:02 -0500 Subject: [PATCH 015/119] EquipType: migrate to EquipSet v2 - Remove old Install, ApplyMassLimit etc. functions; all logic is handled in EquipSet instead - Migrate laser installation code to new OnInstall/OnUninstall callbacks --- data/libs/EquipType.lua | 72 ++++++++++++----------------------------- 1 file changed, 21 insertions(+), 51 deletions(-) diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index d87c660436..0d3b98d212 100644 --- a/data/libs/EquipType.lua +++ b/data/libs/EquipType.lua @@ -4,10 +4,8 @@ local utils = require 'utils' local Serializer = require 'Serializer' local Lang = require 'Lang' -local ShipDef = require 'ShipDef' local Game = package.core['Game'] -local Space = package.core['Space'] local laser = {} local hyperspace = {} @@ -23,7 +21,8 @@ local misc = {} -- the object in a language-agnostic way -- * l10n_resource: where to look up the aforementioned key. If not specified, -- the system assumes "equipment-core" --- * capabilities: a table of string->int, having at least "mass" as a valid key +-- * capabilities: a table of string->number properties to set on the ship object. +-- All keys will be suffixed with _cap for namespacing/convience reasons. -- -- All specs are copied directly within the object (even those I know nothing about), -- but it is a shallow copy. This is particularly important for the capabilities, as @@ -39,6 +38,7 @@ local misc = {} ---@field slot { type: string, size: integer, hardpoint: boolean } | nil ---@field capabilities table? ---@field purchasable boolean +---@field price number ---@field icon_name string? ---@field tech_level integer | "MILITARY" ---@field transient table @@ -77,6 +77,10 @@ function EquipType.New (specs) obj.icon_name = "equip_generic" end + if not obj.purchasable then + obj.price = obj.price or 0 + end + -- TODO: remove all usage of obj.capabilities, transition to explicit volume for equipment -- fixup old capabilities system to explicitly specified mass/volume if obj.capabilities and obj.capabilities.mass then @@ -236,61 +240,27 @@ function EquipType:GetDescription() return self.transient.description end -local function __ApplyMassLimit(ship, capabilities, num) - if num <= 0 then return 0 end - -- we need to use mass_cap directly (not, eg, ship.freeCapacity), - -- because ship.freeCapacity may not have been updated when Install is called - -- (see implementation of EquipSet:Set) - local avail_mass = ShipDef[ship.shipId].capacity - (ship.mass_cap or 0) - local item_mass = capabilities.mass or 0 - if item_mass > 0 then - num = math.min(num, math.floor(avail_mass / item_mass)) - end - return num -end - -local function __ApplyCapabilities(ship, capabilities, num, factor) - if num <= 0 then return 0 end - factor = factor or 1 - for k,v in pairs(capabilities) do - local full_name = k.."_cap" - local prev = (ship:hasprop(full_name) and ship[full_name]) or 0 - ship:setprop(full_name, (factor*v*num)+prev) - end - return num -end - -function EquipType:Install(ship, num, slot) - local caps = self.capabilities - num = __ApplyMassLimit(ship, caps, num) - return __ApplyCapabilities(ship, caps, num, 1) -end - -function EquipType:Uninstall(ship, num, slot) - return __ApplyCapabilities(ship, self.capabilities, num, -1) -end - -- Base type for weapons +---@class EquipType.LaserType : EquipType +---@field laser_stats table local LaserType = utils.inherits(EquipType, "LaserType") -function LaserType:Install(ship, num, slot) - if num > 1 then num = 1 end -- FIXME: support installing multiple lasers (e.g., in the "cargo" slot?) - if LaserType.Super().Install(self, ship, 1, slot) < 1 then return 0 end - local prefix = slot..'_' - for k,v in pairs(self.laser_stats) do - ship:setprop(prefix..k, v) +---@param ship Ship +---@param slot ShipDef.Slot +function LaserType:OnInstall(ship, slot) + for k, v in ipairs(self.laser_stats) do + -- TODO: allow installing more than one laser + ship:setprop('laser_front_' .. k, v) end - return 1 end -function LaserType:Uninstall(ship, num, slot) - if num > 1 then num = 1 end -- FIXME: support uninstalling multiple lasers (e.g., in the "cargo" slot?) - if LaserType.Super().Uninstall(self, ship, 1) < 1 then return 0 end - local prefix = (slot or "laser_front").."_" - for k,v in pairs(self.laser_stats) do - ship:unsetprop(prefix..k) +---@param ship Ship +---@param slot ShipDef.Slot +function LaserType:OnUninstall(ship, slot) + for k, v in ipairs(self.laser_stats) do + -- TODO: allow installing more than one laser + ship:setprop('laser_front_' .. k, nil) end - return 1 end -- Single drive type, no support for slave drives. From 6b360c1f170baee8c221b0a2b5271c27e89eaac5 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 19 Jan 2024 21:39:48 -0500 Subject: [PATCH 016/119] Ship: add GetInstalledHyperdrive, initialize EquipSet - Add a helper utility on Ship which queries the currently installed primary hyperdrive. - Querying for a ship's hyperdrive is by far the most prevalent use of the old GetEquip API - Initialize an EquipSet component on ship objects when they are pushed to Lua, instead of during the Ship constructor - Update c++ uses of GetEquip("engine") -> GetInstalledHyperdrive() --- data/libs/EquipType.lua | 15 ++--- data/libs/Ship.lua | 88 +++++++++--------------------- data/meta/CoreObject/Ship.meta.lua | 11 ++-- src/SectorView.cpp | 2 +- 4 files changed, 39 insertions(+), 77 deletions(-) diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index 0d3b98d212..71c7a133e5 100644 --- a/data/libs/EquipType.lua +++ b/data/libs/EquipType.lua @@ -292,7 +292,7 @@ end -- if the destination is out of range, returns: distance -- if the specified jump is invalid, returns nil function HyperdriveType:CheckJump(ship, source, destination) - if ship:GetEquip('engine', 1) ~= self or source:IsSameSystem(destination) then + if ship:GetInstalledHyperdrive() ~= self or source:IsSameSystem(destination) then return nil end local distance = source:DistanceTo(destination) @@ -355,18 +355,13 @@ local HYPERDRIVE_SOUNDS_MILITARY = { function HyperdriveType:HyperjumpTo(ship, destination) -- First off, check that this is the primary engine. - local engines = ship:GetEquip('engine') - local primary_index = 0 - for i,e in ipairs(engines) do - if e == self then - primary_index = i - break - end - end - if primary_index == 0 then + -- NOTE: this enforces the constraint that only one hyperdrive may be installed on a ship + local engine = ship:GetInstalledHyperdrive() + if engine ~= self then -- wrong ship return "WRONG_SHIP" end + local distance, fuel_use, duration = self:CheckDestination(ship, destination) if not distance then return "OUT_OF_RANGE" diff --git a/data/libs/Ship.lua b/data/libs/Ship.lua index 60326e52c5..e0128fbd8a 100644 --- a/data/libs/Ship.lua +++ b/data/libs/Ship.lua @@ -15,6 +15,7 @@ local CargoManager = require 'CargoManager' local CommodityType = require 'CommodityType' local Character = require 'Character' local Comms = require 'Comms' +local EquipSet = require 'EquipSet' local l = Lang.GetResource("ui-core") @@ -26,6 +27,7 @@ local l = Lang.GetResource("ui-core") function Ship:Constructor() self:SetComponent('CargoManager', CargoManager.New(self)) + self:SetComponent('EquipSet', EquipSet.New(self)) -- Timers cannot be started in ship constructors before Game is fully set, -- so trigger a lazy event to setup gameplay timers. @@ -55,50 +57,21 @@ local CrewRoster = {} -- Group: Methods -- --- Method: GetEquipSlotCapacity +-- Method: GetInstalledHyperdrive -- --- Get the maximum number of a particular type of equipment this ship can --- hold. This is the number of items that can be held, not the mass. --- will take care of ensuring the hull capacity is not exceeded. +-- Return the ship's installed hyperdrive equipment item if present. -- --- > capacity = shiptype:GetEquipSlotCapacity(slot) --- --- Parameters: --- --- slot - a string for the wanted equipment type --- --- Returns: --- --- capacity - the maximum capacity of the equipment slot --- --- Availability: --- --- alpha 10 --- --- Status: --- --- experimental --- -function Ship:GetEquipSlotCapacity(slot) - return self.equipSet:SlotSize(slot) -end - --- Method: GetEquipCountOccupied --- --- Return the number of item in a given slot --- --- > if ship:GetEquipCountOccupied("engine") > 1 then HyperdriveOverLoadAndExplode(ship) end --- --- Availability: --- --- TBA +-- > if ship:GetInstalledHyperdrive() then HyperdriveOverloadAndExplode(ship) end -- -- Status: -- --- experimental +-- stable -- -function Ship:GetEquipCountOccupied(slot) - return self.equipSet:OccupiedSpace(slot) +---@return Equipment.HyperdriveType? hyperdrive +function Ship:GetInstalledHyperdrive() + ---@type Equipment.HyperdriveType[] + local drives = self:GetComponent("EquipSet"):GetInstalledOfType("hyperdrive") + return drives[1] end -- Method: CountEquip @@ -402,7 +375,7 @@ end -- experimental -- Ship.HyperjumpTo = function (self, path, is_legal) - local engine = self:GetEquip("engine", 1) + local engine = self:GetInstalledHyperdrive() local wheels = self:GetWheelState() if not engine then return "NO_DRIVE" @@ -443,7 +416,7 @@ Ship.GetHyperspaceDetails = function (self, source, destination) source = Game.system.path end - local engine = self:GetEquip("engine", 1) + local engine = self:GetInstalledHyperdrive() if not engine then return "NO_DRIVE", 0, 0, 0 elseif source:IsSameSystem(destination) then @@ -467,7 +440,7 @@ Ship.GetHyperspaceDetails = function (self, source, destination) end Ship.GetHyperspaceRange = function (self) - local engine = self:GetEquip("engine", 1) + local engine = self:GetInstalledHyperdrive() if not engine then return 0, 0 end @@ -503,28 +476,19 @@ end -- -- experimental -- -function Ship:FireMissileAt(which_missile, target) - local missile_object = false - if type(which_missile) == "number" then - local missile_equip = self:GetEquip("missile", which_missile) - if missile_equip then - missile_object = self:SpawnMissile(missile_equip.missile_type) - if missile_object ~= nil then - self:SetEquip("missile", which_missile) - end - end - else - for i,m in pairs(self:GetEquip("missile")) do - if (which_missile == m) or (which_missile == "any") then - missile_object = self:SpawnMissile(m.missile_type) - if missile_object ~= nil then - self:SetEquip("missile", i) - break - end - end - end +---@param missile EquipType +function Ship:FireMissileAt(missile, target) + local equipSet = self:GetComponent("EquipSet") + + if missile == "any" then + missile = equipSet:GetInstalledOfType("missile")[1] end + -- FIXME: handle multiple-count missile mounts + equipSet:Remove(missile) + + local missile_object = self:SpawnMissile(missile.missile_type) + if missile_object then if target then missile_object:AIKamikaze(target) @@ -958,7 +922,7 @@ local onEnterSystem = function (ship) end end end - local engine = ship:GetEquip("engine", 1) + local engine = ship:GetInstalledHyperdrive() if engine then engine:OnLeaveHyperspace(ship) end diff --git a/data/meta/CoreObject/Ship.meta.lua b/data/meta/CoreObject/Ship.meta.lua index dc37389aad..ce166e83af 100644 --- a/data/meta/CoreObject/Ship.meta.lua +++ b/data/meta/CoreObject/Ship.meta.lua @@ -13,7 +13,6 @@ --- ---@field shipId string ---@field shipName string ----@field equipSet EquipSet --- ---@field flightState ShipFlightState ---@field alertStatus ShipAlertStatus @@ -29,14 +28,18 @@ --- Remaining fuel mass in tons ---@field fuelMassLeft number --- ----@field usedCapacity number +--- Currently used equipment volume +---@field equipVolume number +--- Total equipment volume +---@field totalVolume number +--- ---@field freeCapacity number --- ---@field usedCargo number ---@field totalCargo number --- ----@field staticMass number ----@field totalMass number +---@field loadedMass number Mass of the equipment and cargo onboard the ship +---@field staticMass number Hull mass + loaded mass --- ---@field hyperspaceRange number ---@field maxHyperspaceRange number diff --git a/src/SectorView.cpp b/src/SectorView.cpp index 08b7691439..74c2186030 100644 --- a/src/SectorView.cpp +++ b/src/SectorView.cpp @@ -385,7 +385,7 @@ const std::string SectorView::AutoRoute(const SystemPath &start, const SystemPat const RefCountedPtr start_sec = m_game.GetGalaxy()->GetSector(start); const RefCountedPtr target_sec = m_game.GetGalaxy()->GetSector(target); - LuaRef try_hdrive = LuaObject::CallMethod(Pi::player, "GetEquip", "engine", 1); + LuaRef try_hdrive = LuaObject::CallMethod(Pi::player, "GetInstalledHyperdrive"); if (try_hdrive.IsNil()) return "NO_DRIVE"; // Get the player's hyperdrive from Lua, later used to calculate the duration between systems From a814ce4ac05fda35895c665330a55f232b167d95 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 20 Jan 2024 07:02:13 -0500 Subject: [PATCH 017/119] Add Passenger helper library - Responsible for querying, embarking, and disembarking passenger objects from equipped cabins - Individual cabins now track which Character objects are embarked inside of them and change icon based on their state - Replaces existing model where empty cabins and occupied cabins are separate equipment added and removed at runtime --- data/libs/Character.lua | 1 + data/libs/EquipType.lua | 12 ++++ data/libs/Passengers.lua | 142 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 data/libs/Passengers.lua diff --git a/data/libs/Character.lua b/data/libs/Character.lua index ebf0e60042..1a44d35489 100644 --- a/data/libs/Character.lua +++ b/data/libs/Character.lua @@ -55,6 +55,7 @@ local Serializer = require 'Serializer' local utils = require 'utils' local Character; +---@class Character Character = { -- diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index 71c7a133e5..61893146e8 100644 --- a/data/libs/EquipType.lua +++ b/data/libs/EquipType.lua @@ -406,6 +406,18 @@ local BodyScannerType = utils.inherits(SensorType, "BodyScannerType") ---@field passenger Character? local CabinType = utils.inherits(EquipType, "Equipment.CabinType") +---@param passenger Character +function CabinType:AddPassenger(passenger) + self.passenger = passenger + self.icon_name = "equip_cabin_occupied" +end + +---@param passenger Character +function CabinType:RemovePassenger(passenger) + self.passenger = nil + self.icon_name = "equip_cabin_empty" +end + function CabinType:OnRemove(ship, slot) EquipType.OnRemove(self, ship, slot) diff --git a/data/libs/Passengers.lua b/data/libs/Passengers.lua new file mode 100644 index 0000000000..e5834e847a --- /dev/null +++ b/data/libs/Passengers.lua @@ -0,0 +1,142 @@ +-- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +local Equipment = require 'Equipment' +local utils = require 'utils' + +-- Module: Passengers +-- +-- Helper utilities for working with passengers onboard a ship +local Passengers = {} + + +-- Function: CountOccupiedCabins +-- +-- Return the number of occupied cabins present on the ship +-- +---@param ship Ship +---@return integer +function Passengers.CountOccupiedCabins(ship) + return ship["cabin_occupied_cap"] or 0 +end + +-- Function: CountFreeCabins +-- +-- Return the number of unoccupied cabins present on the ship +-- +---@param ship Ship +---@return integer +function Passengers.CountFreeCabins(ship) + return (ship["cabin_cap"] or 0) - (ship["cabin_occupied_cap"] or 0) +end + +-- Function: GetOccupiedCabins +-- +-- Return a list of currently occupied cabin equipment items +-- +---@param ship Ship +---@return Equipment.CabinType[] +function Passengers.GetOccupiedCabins(ship) + local equipSet = ship:GetComponent("EquipSet") + local cabin = Equipment.Get("misc.cabin") + + return equipSet:GetInstalledWithFilter(function(equip) + return equip:GetPrototype() == cabin and equip.passenger + end) +end + +-- Function: GetFreeCabins +-- +-- Return a list of currently free passenger cabins +-- +---@param ship Ship +---@return Equipment.CabinType[] +function Passengers.GetFreeCabins(ship) + local equipSet = ship:GetComponent("EquipSet") + local cabin = Equipment.Get("misc.cabin") + + return equipSet:GetInstalledWithFilter(function(equip) + return equip:GetPrototype() == cabin and equip.passenger == nil + end) +end + +-- Function: CheckEmbarked +-- +-- Validate how many of the given list of passengers are present on this ship +-- +---@param ship Ship +---@param passengers Character[] +---@return integer +function Passengers.CheckEmbarked(ship, passengers) + local occupiedCabins = Passengers.GetOccupiedCabins(ship) + local passengerLookup = {} + + for i, cabin in ipairs(occupiedCabins) do + passengerLookup[cabin.passenger] = true + end + + local count = 0 + + for i, passenger in ipairs(passengers) do + if passengerLookup[passenger] then + count = count + 1 + end + end + + return count +end + +-- Function: EmbarkPassenger +-- +-- Load the given passenger onboard the ship, optionally to a specific cabin. +-- +---@param ship Ship +---@param passenger Character +---@param cabin EquipType? +function Passengers.EmbarkPassenger(ship, passenger, cabin) + if not cabin then + cabin = Passengers.GetFreeCabins(ship)[1] + end + + if not cabin or cabin.passenger then + return false + end + + ---@cast cabin Equipment.CabinType + cabin:AddPassenger(passenger) + ship:setprop("cabin_occupied_cap", (ship["cabin_occupied_cap"] or 0) + 1) + + return true +end + +-- Function: DisembarkPassenger +-- +-- Unload the given passenger from the ship. If not specified, the function +-- will attempt to automatically determine which cabin the passenger is +-- embarked in. +-- +---@param ship Ship +---@param passenger Character +---@param cabin EquipType? +function Passengers.DisembarkPassenger(ship, passenger, cabin) + if not cabin then + local cabinProto = Equipment.Get("misc.cabin") + local equipSet = ship:GetComponent("EquipSet") + + cabin = equipSet:GetInstalledWithFilter(function(equip) + return equip:GetPrototype() == cabinProto and equip.passenger == passenger + end)[1] + end + + if not cabin or cabin.passenger ~= passenger then + return false + end + + ---@cast cabin Equipment.CabinType + cabin:RemovePassenger(passenger) + ship:setprop("cabin_occupied_cap", ship["cabin_occupied_cap"] - 1) + + return true +end + +return Passengers From 7246d3278a012f55415bf4d95cb77c813ee27323 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 20 Jan 2024 07:08:14 -0500 Subject: [PATCH 018/119] Add proof-of-concept ShipBuilder utility library - Helper utilities allowing ship loadouts and equipment selection to be written in a generic fashion - Ship loadouts are written as productions of individual rules applied in order which add equipment items to the prospective ship - A finished ship plan is then applied to a newly-spawned ship and equipment items created and installed as appropriate --- data/modules/MissionUtils/ShipBuilder.lua | 268 ++++++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 data/modules/MissionUtils/ShipBuilder.lua diff --git a/data/modules/MissionUtils/ShipBuilder.lua b/data/modules/MissionUtils/ShipBuilder.lua new file mode 100644 index 0000000000..564be6db43 --- /dev/null +++ b/data/modules/MissionUtils/ShipBuilder.lua @@ -0,0 +1,268 @@ +-- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +local Engine = require 'Engine' +local Equipment = require 'Equipment' +local EquipSet = require 'EquipSet' +local ShipDef = require 'ShipDef' +local ShipConfig = require 'ShipConfig' +local Space = require 'Space' +local Ship = require 'Ship' + +local utils = require 'utils' + +local slotTypeMatches = function(slot, filter) + return string.sub(slot, 1, #filter) == filter +end + +-- Class: MissionUtils.ShipBuilder +-- +-- Utilities for spawning and equipping NPC ships to be used in mission modules + +---@class MissionUtils.ShipBuilder +local ShipBuilder = {} + +local hyperdriveRule = { + slot = "hyperdrive", +} + +local randomPulsecannonEasyRule = { + slot = "weapon", + filter = "weapon.energy.pulsecannon", + pick = "random", + maxSize = 3, + limit = 1 +} + +local atmoShieldRule = { + slot = "hull", + filter = "hull.atmo_shield", + limit = 1 +} + +local pirateProduction = { + role = "pirate", + rules = { + hyperdriveRule, + randomPulsecannonEasyRule, + atmoShieldRule, + { + slot = "shield", + limit = 1 + }, + { + slot = "computer", + equip = "misc.autopilot", + limit = 1 + } + } +} + +---@param shipPlan table +---@param shipConfig ShipDef.Config +---@param rule table +---@param rand Rand +function ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, rand) + + local addEquipToPlan = function(equip, slot) + shipPlan.slots[slot.id] = equip + shipPlan.freeVolume = shipPlan.freeVolume - equip.volume + shipPlan.equipMass = shipPlan.equipMass + equip.volume + end + + local matchRuleSlot = function(slot) + return slotTypeMatches(slot.type, rule.slot) + and (not rule.maxSize or slot.size <= rule.maxSize) + and (not rule.minSize or slot.size >= rule.minSize) + end + + -- Get a list of all equipment slots on the ship that match this rule + local slots = utils.to_array(shipConfig.slots, function(slot) + -- Don't install in already-filled slots + return not shipPlan.slots[slot.id] + and matchRuleSlot(slot) + end) + + -- Early-out if we have nowhere to install equipment + if #slots == 0 then return end + + -- Sort the table of slots so we install in the best/biggest slot first + table.sort(slots, function(a, b) return a.size > b.size or (a.size == b.size and a.id < b.id) end) + + -- Track how many items have been installed total + local numInstalled = 0 + + -- Explicitly-specified equipment item, just add it to slots as able + if rule.equip then + + local equip = Equipment.Get(rule.equip) + + for _, slot in ipairs(slots) do + if EquipSet.CompatibleWithSlot(equip, slot) and shipPlan.freeVolume >= equip.volume then + addEquipToPlan(equip, slot) + end + + numInstalled = numInstalled + 1 + + if rule.limit and numInstalled >= rule.limit then + break + end + end + + return + end + + -- Limit equipment according to what will actually fit the ship + -- NOTE: this does not guarantee all slots will be able to be filled in a balanced manner + local maxVolume = rule.balance and shipPlan.freeVolume / #slots or shipPlan.freeVolume + + -- Build a list of all equipment items that could potentially be installed + local filteredEquip = utils.to_array(Equipment.new, function(equip) + return (equip.slot or false) + and matchRuleSlot(equip.slot) + and equip.volume <= maxVolume + and (not rule.filter or slotTypeMatches(equip.slot.type, rule.filter)) + end) + + -- No equipment items can be installed, rule is finished + if #filteredEquip == 0 then + return + end + + -- Iterate over each slot and install items + for _, slot in ipairs(slots) do + + -- Not all equipment items which passed the size/slot type check earlier + -- may be compatible with this specific slot (e.g. if it has a more + -- specific slot type than the rule itself). + local compatible = utils.filter_array(filteredEquip, function(equip) + return EquipSet.CompatibleWithSlot(equip, slot) + and shipPlan.freeVolume >= equip.volume + end) + + -- Nothing fits in this slot, ignore it then + if #compatible > 0 then + + if rule.pick == "random" then + -- Select a random item from the list + local equip = compatible[rand:Integer(1, #compatible)] + addEquipToPlan(equip, slot) + else + -- Sort equipment items by size; heavier items of the same size + -- class first. Assume the largest and heaviest item is the best, + -- since we don't have any markup data to tell otherwise + table.sort(compatible, function(a, b) + return a.slot.size > b.slot.size or (a.slot.size == b.slot.size and a.mass > b.mass) + end) + + addEquipToPlan(compatible[1], slot) + end + + numInstalled = numInstalled + 1 + + if rule.limit and numInstalled > rule.limit then + break + end + + end + + end + +end + +function ShipBuilder.SelectHull(production) + local hullList = {} + + -- TODO: some sort of balance metric should be expressed here + + for id, shipDef in pairs(ShipDef) do + if utils.contains(shipDef.roles, production.role) then + table.insert(hullList, id) + end + end + + if #hullList == 0 then + return nil + end + + local selectedHull = hullList[Engine.rand:Integer(1, #hullList)] + + return ShipConfig[selectedHull] +end + +---@param production table +---@param shipConfig ShipDef.Config +function ShipBuilder.MakePlan(production, shipConfig) + + local shipPlan = { + shipId = shipConfig.id, + equipMass = 0, + freeVolume = shipConfig.capacity, + slots = {} + } + + for _, rule in ipairs(production.rules) do + ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, Engine.rand) + end + + return shipPlan + +end + +---@param ship Ship +---@param shipPlan table +function ShipBuilder.ApplyPlan(ship, shipPlan) + + local equipSet = ship:GetComponent('EquipSet') + + for name, proto in pairs(shipPlan.slots) do + local slot = equipSet:GetSlotHandle(name) + assert(slot) + + equipSet:Install(proto(), slot) + end + + -- TODO: loose equipment + +end + +---@param player Ship +---@param nearDist number? +---@param farDist number? +function ShipBuilder.MakeGenericPirateNear(player, risk, nearDist, farDist) + + local hullConfig = ShipBuilder.SelectHull(pirateProduction) + assert(hullConfig) + + local plan = ShipBuilder.MakePlan(pirateProduction, hullConfig) + assert(plan) + + ---@type Ship + local ship = Space.SpawnShipNear(plan.shipId, player, nearDist or 50, farDist or 100) + ship:SetLabel(Ship.MakeRandomLabel()) + + ShipBuilder.ApplyPlan(ship, plan) + + -- TODO: handle risk/difficulty-based installation of equipment as part of + -- the loadout rules + local equipSet = ship:GetComponent('EquipSet') + + if Engine.rand:Number(2) <= risk then + equipSet:Install(Equipment.Get("misc.laser_cooling_booster")) + end + + if Engine.rand:Number(3) <= risk then + equipSet:Install(Equipment.Get("misc.shield_energy_booster")) + end + + return ship + +end + +require 'Event'.Register("onEnterMainMenu", function() + local plan = ShipBuilder.MakePlan(pirateProduction, ShipConfig['coronatrix']) + + utils.print_r(plan) +end) + +return ShipBuilder From ad787f1d66d1c6b65cbb0f2b21c3af09b834f37b Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 20 Jan 2024 19:38:03 -0500 Subject: [PATCH 019/119] Convert misc. UI code to new EquipSet APIs - Replace most GetEquip() calls with querying ship capabilities instead - Use Ship:GetInstalledHyperdrive() in all cases instead of manually querying for hyperdrive equipment - Rework missile display + launching - Display new equipVolume stat instead of ship mass in StationView --- data/pigui/libs/radial-menu.lua | 50 ++++++++++++------- data/pigui/modules/equipment.lua | 21 +++++--- .../modules/flight-ui/target-scanner.lua | 15 ++++-- data/pigui/modules/hyperjump-planner.lua | 6 +-- data/pigui/modules/info-view/01-ship-info.lua | 38 +++++++------- .../pigui/modules/info-view/03-econ-trade.lua | 11 ++-- data/pigui/modules/radar.lua | 3 +- data/pigui/modules/station-view/01-lobby.lua | 2 +- .../modules/station-view/04-shipMarket.lua | 20 ++++---- data/pigui/views/station-view.lua | 17 +++++-- 10 files changed, 111 insertions(+), 72 deletions(-) diff --git a/data/pigui/libs/radial-menu.lua b/data/pigui/libs/radial-menu.lua index e58adc5276..9271be3217 100644 --- a/data/pigui/libs/radial-menu.lua +++ b/data/pigui/libs/radial-menu.lua @@ -68,18 +68,25 @@ function ui.openRadialMenu(id, target, mouse_button, size, actions, padding, pos end end +local hasAutopilotLevel = function(level) + return (Game.player["autopilot_cap"] or 0) >= level +end + -- TODO: add cloud Lang::SET_HYPERSPACE_TARGET_TO_FOLLOW_THIS_DEPARTURE local radial_menu_actions_station = { - {icon=ui.theme.icons.comms, tooltip=lc.REQUEST_DOCKING_CLEARANCE, + { + icon=ui.theme.icons.comms, tooltip=lc.REQUEST_DOCKING_CLEARANCE, action=function(target) target:RequestDockingClearance(Game.player) -- TODO: play a negative sound if clearance is refused Game.player:SetNavTarget(target) ui.playSfx("OK") - end}, - {icon=ui.theme.icons.autopilot_dock, tooltip=lc.AUTOPILOT_DOCK_WITH_STATION, + end + }, + { + icon=ui.theme.icons.autopilot_dock, tooltip=lc.AUTOPILOT_DOCK_WITH_STATION, action=function(target) - if next(Game.player:GetEquip('autopilot')) ~= nil then + if hasAutopilotLevel(1) then Game.player:SetFlightControlState("CONTROL_AUTOPILOT") Game.player:AIDockWith(target) Game.player:SetNavTarget(target) @@ -87,13 +94,15 @@ local radial_menu_actions_station = { else Game.AddCommsLogLine(lc.NO_AUTOPILOT_INSTALLED) end - end}, + end + }, } local radial_menu_actions_all_bodies = { - {icon=ui.theme.icons.autopilot_fly_to, tooltip=lc.AUTOPILOT_FLY_TO_VICINITY_OF, + { + icon=ui.theme.icons.autopilot_fly_to, tooltip=lc.AUTOPILOT_FLY_TO_VICINITY_OF, action=function(target) - if next(Game.player:GetEquip('autopilot')) ~= nil then + if hasAutopilotLevel(1) then Game.player:SetFlightControlState("CONTROL_AUTOPILOT") Game.player:AIFlyTo(target) Game.player:SetNavTarget(target) @@ -101,13 +110,15 @@ local radial_menu_actions_all_bodies = { else Game.AddCommsLogLine(lc.NO_AUTOPILOT_INSTALLED) end - end}, + end + }, } local radial_menu_actions_systembody = { - {icon=ui.theme.icons.autopilot_low_orbit, tooltip=lc.AUTOPILOT_ENTER_LOW_ORBIT_AROUND, + { + icon=ui.theme.icons.autopilot_low_orbit, tooltip=lc.AUTOPILOT_ENTER_LOW_ORBIT_AROUND, action=function(target) - if next(Game.player:GetEquip('autopilot')) ~= nil then + if hasAutopilotLevel(1) then Game.player:SetFlightControlState("CONTROL_AUTOPILOT") Game.player:AIEnterLowOrbit(target) Game.player:SetNavTarget(target) @@ -115,10 +126,12 @@ local radial_menu_actions_systembody = { else Game.AddCommsLogLine(lc.NO_AUTOPILOT_INSTALLED) end - end}, - {icon=ui.theme.icons.autopilot_medium_orbit, tooltip=lc.AUTOPILOT_ENTER_MEDIUM_ORBIT_AROUND, + end + }, + { + icon=ui.theme.icons.autopilot_medium_orbit, tooltip=lc.AUTOPILOT_ENTER_MEDIUM_ORBIT_AROUND, action=function(target) - if next(Game.player:GetEquip('autopilot')) ~= nil then + if hasAutopilotLevel(1) then Game.player:SetFlightControlState("CONTROL_AUTOPILOT") Game.player:AIEnterMediumOrbit(target) Game.player:SetNavTarget(target) @@ -126,10 +139,12 @@ local radial_menu_actions_systembody = { else Game.AddCommsLogLine(lc.NO_AUTOPILOT_INSTALLED) end - end}, - {icon=ui.theme.icons.autopilot_high_orbit, tooltip=lc.AUTOPILOT_ENTER_HIGH_ORBIT_AROUND, + end + }, + { + icon=ui.theme.icons.autopilot_high_orbit, tooltip=lc.AUTOPILOT_ENTER_HIGH_ORBIT_AROUND, action=function(target) - if next(Game.player:GetEquip('autopilot')) ~= nil then + if hasAutopilotLevel(1) then Game.player:SetFlightControlState("CONTROL_AUTOPILOT") Game.player:AIEnterHighOrbit(target) Game.player:SetNavTarget(target) @@ -137,7 +152,8 @@ local radial_menu_actions_systembody = { else Game.AddCommsLogLine(lc.NO_AUTOPILOT_INSTALLED) end - end}, + end + }, } function ui.openDefaultRadialMenu(id, body) diff --git a/data/pigui/modules/equipment.lua b/data/pigui/modules/equipment.lua index dbf59a4bc7..83691e34a4 100644 --- a/data/pigui/modules/equipment.lua +++ b/data/pigui/modules/equipment.lua @@ -60,7 +60,7 @@ local function displayECM(uiPos) player = Game.player local current_view = Game.CurrentView() if current_view == "world" then - local ecms = player:GetEquip('ecm') + local ecms = player:GetComponent("EquipSet"):GetInstalledOfType("utility.ecm") for i,ecm in ipairs(ecms) do local size, clicked = iconEqButton(uiPos, icons[ecm.ecm_type], false, mainIconSize, "ECM", not player:IsECMReady(), mainBackgroundColor, mainForegroundColor, mainHoverColor, mainPressedColor, lec[ecm.hover_message]) uiPos.y = uiPos.y + size.y + 10 @@ -81,37 +81,42 @@ local function getMissileIcon(missile) end end -local function fireMissile(index) +local function fireMissile(missile) if not player:GetCombatTarget() then Game.AddCommsLogLine(lc.SELECT_A_TARGET, "", 1) else - player:FireMissileAt(index, player:GetCombatTarget()) + player:FireMissileAt(missile, player:GetCombatTarget()) end end local function displayMissiles(uiPos) player = Game.player local current_view = Game.CurrentView() + if current_view == "world" then - local missiles = player:GetEquip('missile') + + local missiles = player:GetComponent("EquipSet"):GetInstalledOfType("missile") local count = {} local types = {} - local index = {} + for i,missile in ipairs(missiles) do count[missile.missile_type] = (count[missile.missile_type] or 0) + 1 types[missile.missile_type] = missile - index[missile.missile_type] = 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]) uiPos.y = uiPos.y + size.y + 10 + if clicked then - print("firing missile " .. t .. ", " .. index[t]) - fireMissile(index[t]) + print("firing missile " .. t) + fireMissile(missile) end end + end + return uiPos end diff --git a/data/pigui/modules/flight-ui/target-scanner.lua b/data/pigui/modules/flight-ui/target-scanner.lua index 7949a010c6..c5047b226e 100644 --- a/data/pigui/modules/flight-ui/target-scanner.lua +++ b/data/pigui/modules/flight-ui/target-scanner.lua @@ -14,17 +14,18 @@ local lc = require 'Lang'.GetResource("core") local lui = require 'Lang'.GetResource("ui-core") -- cache player each frame -local player = nil +local player = nil ---@type Player local gameView = require 'pigui.views.game' local shipInfoLowerBound +---@param target Ship local function displayTargetScannerFor(target, offset) local hull = target:GetHullPercent() local shield = target:GetShieldsPercent() local class = target:GetShipType() local label = target.label - local engine = target:GetEquip('engine', 1) + local engine = target:GetInstalledHyperdrive() local mass = target.staticMass local cargo = target.usedCargo if engine then @@ -62,7 +63,11 @@ end local function displayTargetScanner() local offset = 7 shipInfoLowerBound = Vector2(ui.screenWidth - 30, 1 * ui.gauge_height) - if player:GetEquipCountOccupied('target_scanner') > 0 or player:GetEquipCountOccupied('advanced_target_scanner') > 0 then + + local scanner_level = (player["target_scanner_level_cap"] or 0) + local hypercloud_level = (player["hypercloud_analyzer_cap"] or 0) + + if scanner_level > 0 then -- what is the difference between target_scanner and advanced_target_scanner? local target = player:GetNavTarget() if target and target:IsShip() then @@ -74,8 +79,10 @@ local function displayTargetScanner() end end end - if player:GetEquipCountOccupied('hypercloud') > 0 then + + if hypercloud_level > 0 then local target = player:GetNavTarget() + if target and target:IsHyperspaceCloud() then local arrival = target:IsArrival() local ship = target:GetShip() diff --git a/data/pigui/modules/hyperjump-planner.lua b/data/pigui/modules/hyperjump-planner.lua index 5dcb46c01a..4e50753e0c 100644 --- a/data/pigui/modules/hyperjump-planner.lua +++ b/data/pigui/modules/hyperjump-planner.lua @@ -115,7 +115,7 @@ local function buildJumpRouteList() -- if we are not in the system, then we are in hyperspace, we start building the route from the jump target local start = Game.system and Game.system.path or player:GetHyperspaceDestination() - local drive = table.unpack(player:GetEquip("engine")) or nil + local drive = player:GetInstalledHyperdrive() local fuel_type = drive and drive.fuel or Commodities.hydrogen local cur_fuel = player:GetComponent('CargoManager'):CountCommodity(fuel_type) @@ -307,7 +307,7 @@ function hyperJumpPlanner.display() textIconSize = ui.calcTextSize("H") textIconSize.x = textIconSize.y -- make square end - local drive = table.unpack(Game.player:GetEquip("engine")) or nil + local drive = Game.player:GetInstalledHyperdrive() local fuel_type = drive and drive.fuel or Commodities.hydrogen current_system = Game.system -- will be nil during the hyperjump current_path = Game.system and current_system.path -- will be nil during the hyperjump @@ -321,7 +321,7 @@ function hyperJumpPlanner.setSectorView(sv) end function hyperJumpPlanner.onPlayerCargoChanged(comm, count) - local drive = table.unpack(Game.player:GetEquip("engine")) or nil + local drive = Game.player:GetInstalledHyperdrive() local fuel_type = drive and drive.fuel or Commodities.hydrogen if comm.name == fuel_type.name then diff --git a/data/pigui/modules/info-view/01-ship-info.lua b/data/pigui/modules/info-view/01-ship-info.lua index 08a335edf1..db4ea5d30c 100644 --- a/data/pigui/modules/info-view/01-ship-info.lua +++ b/data/pigui/modules/info-view/01-ship-info.lua @@ -1,33 +1,34 @@ -- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt -local Equipment = require 'Equipment' local Event = require 'Event' local Game = require 'Game' local Lang = require 'Lang' local ShipDef = require 'ShipDef' local InfoView = require 'pigui.views.info-view' +local Passengers = require 'Passengers' local Vector2 = Vector2 local ui = require 'pigui' local l = Lang.GetResource("ui-core") -local fonts = ui.fonts local textTable = require 'pigui.libs.text-table' local equipmentWidget = require 'pigui.libs.ship-equipment'.New("ShipInfo") local function shipStats() local player = Game.player + local equipSet = player:GetComponent("EquipSet") -- Taken directly from ShipInfo.lua. local shipDef = ShipDef[player.shipId] local shipLabel = player:GetLabel() - local hyperdrive = table.unpack(player:GetEquip("engine")) - local frontWeapon = table.unpack(player:GetEquip("laser_front")) - local rearWeapon = table.unpack(player:GetEquip("laser_rear")) - local cabinEmpty = player:CountEquip(Equipment.misc.cabin, "cabin") - local cabinOccupied = player:CountEquip(Equipment.misc.cabin_occupied, "cabin") + local hyperdrive = equipSet:GetInstalledOfType("hyperdrive")[1] + local frontWeapon = equipSet:GetInstalledOfType("weapon")[1] + local rearWeapon = equipSet:GetInstalledOfType("weapon")[2] + local cabinEmpty = Passengers.CountFreeCabins(player) + local cabinOccupied = Passengers.CountOccupiedCabins(player) + local cabinMaximum = shipDef.equipSlotCapacity.cabin hyperdrive = hyperdrive or nil frontWeapon = frontWeapon or nil @@ -39,7 +40,7 @@ local function shipStats() local bwd_acc = player:GetAcceleration("reverse") local up_acc = player:GetAcceleration("up") - local atmo_shield = table.unpack(player:GetEquip("atmo_shield")) or nil + local atmo_shield = equipSet:GetInstalledOfType("hull.atmo_shield")[1] local atmo_shield_cap = 1 if atmo_shield then atmo_shield_cap = atmo_shield.capabilities.atmo_shield @@ -56,12 +57,11 @@ local function shipStats() }) }, false, - { l.WEIGHT_EMPTY..":", string.format("%dt", player.staticMass - player.usedCapacity) }, - { l.CAPACITY_USED..":", string.format("%dt (%dt "..l.FREE..")", player.usedCapacity, player.freeCapacity) }, - { l.CARGO_SPACE..":", string.format("%dt (%dt "..l.MAX..")", player.totalCargo, shipDef.equipSlotCapacity.cargo) }, - { l.CARGO_SPACE_USED..":", string.format("%dt (%dt "..l.FREE..")", player.usedCargo, player.totalCargo - player.usedCargo) }, - { l.FUEL_WEIGHT..":", string.format("%dt (%dt "..l.MAX..")", player.fuelMassLeft, shipDef.fuelTankMass ) }, - { l.ALL_UP_WEIGHT..":", string.format("%dt", mass_with_fuel ) }, + { l.WEIGHT_EMPTY..":", string.format("%dt", player.staticMass - player.loadedMass) }, + { l.CAPACITY_USED..":", string.format("%s (%s "..l.MAX..")", ui.Format.Volume(player.equipVolume), ui.Format.Volume(player.totalVolume) ) }, + { l.CARGO_SPACE_USED..":", string.format("%dcu (%dcu "..l.MAX..")", player.usedCargo, player.totalCargo) }, + { l.FUEL_WEIGHT..":", string.format("%.1ft (%.1ft "..l.MAX..")", player.fuelMassLeft, shipDef.fuelTankMass ) }, + { l.ALL_UP_WEIGHT..":", string.format("%dt", mass_with_fuel ) }, false, { l.FRONT_WEAPON..":", frontWeapon and frontWeapon:GetName() or l.NONE }, { l.REAR_WEAPON..":", rearWeapon and rearWeapon:GetName() or l.NONE }, @@ -76,13 +76,13 @@ local function shipStats() { l.CREW_CABINS..":", shipDef.maxCrew }, { l.UNOCCUPIED_PASSENGER_CABINS..":", cabinEmpty }, { l.OCCUPIED_PASSENGER_CABINS..":", cabinOccupied }, - { l.PASSENGER_CABIN_CAPACITY..":", shipDef.equipSlotCapacity.cabin}, + { l.PASSENGER_CABIN_CAPACITY..":", cabinMaximum }, false, - { l.MISSILE_MOUNTS..":", shipDef.equipSlotCapacity.missile}, - { l.SCOOP_MOUNTS..":", shipDef.equipSlotCapacity.scoop}, + { l.MISSILE_MOUNTS..":", shipDef.equipSlotCapacity.missile}, + { l.SCOOP_MOUNTS..":", shipDef.equipSlotCapacity.scoop}, false, - { l.ATMOSPHERIC_SHIELDING..":", shipDef.equipSlotCapacity.atmo_shield > 0 and l.YES or l.NO }, - { l.ATMO_PRESS_LIMIT..":", string.format("%d atm", shipDef.atmosphericPressureLimit * atmo_shield_cap) }, + { l.ATMOSPHERIC_SHIELDING..":", shipDef.equipSlotCapacity.atmo_shield > 0 and l.YES or l.NO }, + { l.ATMO_PRESS_LIMIT..":", string.format("%d atm", shipDef.atmosphericPressureLimit * atmo_shield_cap) }, }) end diff --git a/data/pigui/modules/info-view/03-econ-trade.lua b/data/pigui/modules/info-view/03-econ-trade.lua index bfdccc72d6..4cc05dd8f9 100644 --- a/data/pigui/modules/info-view/03-econ-trade.lua +++ b/data/pigui/modules/info-view/03-econ-trade.lua @@ -7,7 +7,7 @@ local Lang = require 'Lang' local Game = require 'Game' local ShipDef = require 'ShipDef' local StationView = require 'pigui.views.station-view' -local Format = require 'Format' +local Passengers = require 'Passengers' local Commodities = require 'Commodities' local CommodityType = require 'CommodityType' @@ -175,9 +175,10 @@ end -- Gauge bar for used/free cabins local function gauge_cabins() local player = Game.player - local cabins_total = player:GetEquipCountOccupied("cabin") - local cabins_free = player.cabin_cap or 0 - local cabins_used = cabins_total - cabins_free + local cabins_free = Passengers.CountFreeCabins(player) + local cabins_used = Passengers.CountOccupiedCabins(player) + local cabins_total = cabins_used + cabins_free + gauge_bar(cabins_used, string.format('%%i %s / %i %s', l.USED, cabins_free, l.FREE), 0, cabins_total, icons.personal) end @@ -288,7 +289,7 @@ InfoView:registerView({ end) shipDef = ShipDef[Game.player.shipId] - hyperdrive = table.unpack(Game.player:GetEquip("engine")) or nil + hyperdrive = Game.player:GetInstalledHyperdrive() hyperdrive_fuel = hyperdrive and hyperdrive.fuel or Commodities.hydrogen end, diff --git a/data/pigui/modules/radar.lua b/data/pigui/modules/radar.lua index 56c03abe9a..97a8201e81 100644 --- a/data/pigui/modules/radar.lua +++ b/data/pigui/modules/radar.lua @@ -179,8 +179,9 @@ local click_on_radar = false local function displayRadar() if ui.optionsWindow.isOpen or Game.CurrentView() ~= "world" then return end player = Game.player - local equipped_radar = player:GetEquip("radar") + -- only display if there actually *is* a radar installed + local equipped_radar = player:GetComponent("EquipSet"):GetInstalledOfType("sensor.radar") if #equipped_radar > 0 then local size = ui.reticuleCircleRadius * 0.66 diff --git a/data/pigui/modules/station-view/01-lobby.lua b/data/pigui/modules/station-view/01-lobby.lua index 7fde58cceb..428f821243 100644 --- a/data/pigui/modules/station-view/01-lobby.lua +++ b/data/pigui/modules/station-view/01-lobby.lua @@ -340,7 +340,7 @@ StationView:registerView({ face = PiGuiFace.New(Character.New({ title = l.STATION_MANAGER }, rand), {itemSpacing = widgetSizes.itemSpacing}) end - hyperdrive = table.unpack(Game.player:GetEquip("engine")) or nil + hyperdrive = Game.player:GetInstalledHyperdrive() hyperdrive_fuel = hyperdrive and hyperdrive.fuel or Commodities.hydrogen hyperdriveIcon = PiImage.New("icons/goods/" .. hyperdrive_fuel.icon_name .. ".png") end diff --git a/data/pigui/modules/station-view/04-shipMarket.lua b/data/pigui/modules/station-view/04-shipMarket.lua index 62009f9824..ec1cb398de 100644 --- a/data/pigui/modules/station-view/04-shipMarket.lua +++ b/data/pigui/modules/station-view/04-shipMarket.lua @@ -90,19 +90,19 @@ local function manufacturerIcon (manufacturer) end end - -local tradeInValue = function(shipDef) - local value = shipDef.basePrice * shipSellPriceReduction * Game.player.hullPercent/100 +---@param ship Ship +local tradeInValue = function(ship) + local shipDef = ShipDef[ship.shipId] + local value = shipDef.basePrice * shipSellPriceReduction * ship.hullPercent/100 if shipDef.hyperdriveClass > 0 then value = value - Equipment.hyperspace["hyperdrive_" .. shipDef.hyperdriveClass].price * equipSellPriceReduction end - for _, t in pairs({Equipment.misc, Equipment.hyperspace, Equipment.laser}) do - for _, e in pairs(t) do - local n = Game.player:CountEquip(e) - value = value + n * e.price * equipSellPriceReduction - end + local equipment = ship:GetComponent("EquipSet"):GetInstalledEquipment() + for _, e in ipairs(equipment) do + local n = e.count or 1 + value = value + n * e.price * equipSellPriceReduction end return math.ceil(value) @@ -113,7 +113,7 @@ local function buyShip (mkt, sos) local station = player:GetDockedWith() local def = sos.def - local cost = def.basePrice - tradeInValue(ShipDef[Game.player.shipId]) + local cost = def.basePrice - tradeInValue(Game.player) if math.floor(cost) ~= cost then error("Ship price non-integer value.") end @@ -340,7 +340,7 @@ local tradeMenu = function() ui.withFont(pionillium.heading, function() ui.text(l.PRICE..": "..Format.Money(selectedItem.def.basePrice, false)) ui.sameLine() - ui.text(l.AFTER_TRADE_IN..": "..Format.Money(selectedItem.def.basePrice - tradeInValue(ShipDef[Game.player.shipId]), false)) + ui.text(l.AFTER_TRADE_IN..": "..Format.Money(selectedItem.def.basePrice - tradeInValue(Game.player), false)) end) ui.nextColumn() diff --git a/data/pigui/views/station-view.lua b/data/pigui/views/station-view.lua index 10dca141b1..dc21bdb9e3 100644 --- a/data/pigui/views/station-view.lua +++ b/data/pigui/views/station-view.lua @@ -4,8 +4,10 @@ local Lang = require 'Lang' local Game = require 'Game' local Format = require 'Format' +local Passengers = require 'Passengers' local l = Lang.GetResource("ui-core") + local ui = require 'pigui' local colors = ui.theme.colors local icons = ui.theme.icons @@ -54,13 +56,20 @@ if not stationView then ui.sameLine() local gaugePos = ui.getWindowPos() + ui.getCursorPos() + Vector2(0, ui.getTextLineHeight() / 2) local gaugeWidth = ui.getContentRegion().x - self.style.inventoryPadding.x - self.style.itemSpacing.x - ui.gauge(gaugePos, player.usedCapacity, '', string.format('%%it %s / %it %s', l.USED, player.freeCapacity, l.FREE), 0, player.usedCapacity + player.freeCapacity, icons.market, colors.gaugeEquipmentMarket, '', gaugeWidth, ui.getTextLineHeight()) + + local fmt = "{} {} / {} {}" % { + ui.Format.Volume(player.equipVolume), l.USED, + ui.Format.Volume(player.totalVolume - player.equipVolume), l.FREE + } + ui.gauge(gaugePos, player.equipVolume, '', fmt, 0, player.totalVolume, icons.market, colors.gaugeEquipmentMarket, '', gaugeWidth, ui.getTextLineHeight()) ui.nextColumn() ui.text(l.CABINS .. ': ') ui.sameLine() - local cabins_total = Game.player:GetEquipCountOccupied("cabin") - local cabins_free = player.cabin_cap or 0 - local cabins_used = cabins_total - cabins_free + + local cabins_free = Passengers.CountFreeCabins(player) + local cabins_used = Passengers.CountOccupiedCabins(player) + local cabins_total = cabins_used + cabins_free + gaugePos = ui.getWindowPos() + ui.getCursorPos() + Vector2(0, ui.getTextLineHeight() / 2) gaugeWidth = ui.getContentRegion().x - self.style.inventoryPadding.x - self.style.itemSpacing.x ui.gauge(gaugePos, cabins_used, '', string.format('%%i %s / %i %s', l.USED, cabins_free, l.FREE), 0, cabins_total, icons.personal, colors.gaugeEquipmentMarket, '', gaugeWidth, ui.getTextLineHeight()) From 8c28f529ad81cc649ced3eb6621cab1bb139c4ab Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 3 Feb 2024 02:19:19 -0500 Subject: [PATCH 020/119] First pass updating mission modules to use new APIs - Update mission modules to use Ship:GetInstalledHyperdrive() and Passengers.Embark/Disembark/Count rather than querying the equipment system directly - Additional work required to convert modules to new ship outfitting code --- .../BreakdownServicing/BreakdownServicing.lua | 15 ++- data/modules/Combat/Combat.lua | 3 +- data/modules/Debug/DebugShipSpawn.lua | 29 ++---- data/modules/Debug/DebugShipSpawn.moon | 14 ++- data/modules/Rondel/Rondel.lua | 4 +- data/modules/Scoop/Scoop.lua | 2 +- data/modules/Scout/ScanManager.lua | 9 +- data/modules/SearchRescue/SearchRescue.lua | 97 +++++++++---------- data/modules/SecondHand/SecondHand.lua | 48 ++++----- data/modules/Taxi/Taxi.lua | 70 ++++++------- data/modules/TradeShips/Debug.lua | 38 +++----- data/modules/TradeShips/Trader.lua | 10 +- 12 files changed, 157 insertions(+), 182 deletions(-) diff --git a/data/modules/BreakdownServicing/BreakdownServicing.lua b/data/modules/BreakdownServicing/BreakdownServicing.lua index 6322042260..3965b0e3ff 100644 --- a/data/modules/BreakdownServicing/BreakdownServicing.lua +++ b/data/modules/BreakdownServicing/BreakdownServicing.lua @@ -86,7 +86,7 @@ end local onChat = function (form, ref, option) local ad = ads[ref] - local hyperdrive = Game.player:GetEquip('engine',1) + local hyperdrive = Game.player:GetInstalledHyperdrive() -- Tariff! ad.baseprice is from 2 to 10 local price = ad.baseprice @@ -253,6 +253,7 @@ local savedByCrew = function(ship) return false end +---@param ship Ship local onEnterSystem = function (ship) if ship:IsPlayer() then print(('DEBUG: Jumps since warranty: %d\nWarranty expires: %s'):format(service_history.jumpcount,Format.Date(service_history.lastdate + service_history.service_period))) @@ -260,6 +261,12 @@ local onEnterSystem = function (ship) return -- Don't care about NPC ships end + local engine = ship:GetInstalledHyperdrive() + if not engine then + -- somehow got here without a hyperdrive - were aliens involved? + return + end + -- Jump drive is getting worn and is running down if (service_history.lastdate + service_history.service_period < Game.time) then service_history.jumpcount = service_history.jumpcount + 1 @@ -284,16 +291,14 @@ local onEnterSystem = function (ship) service_history.jumpcount = service_history.jumpcount - fixup else -- Destroy the engine - local engine = ship:GetEquip('engine',1) - if engine.fuel.name == 'military_fuel' then pigui.playSfx("Hyperdrive_Breakdown_Military", 1.0, 1.0) else pigui.playSfx("Hyperdrive_Breakdown", 1.0, 1.0) end - ship:RemoveEquip(engine) - ship:GetComponent('CargoManager'):AddCommodity(Commodities.rubbish, engine.capabilities.mass) + ship:GetComponent('EquipSet'):Remove(engine) + ship:GetComponent('CargoManager'):AddCommodity(Commodities.rubbish, engine.mass) Comms.Message(l.THE_SHIPS_HYPERDRIVE_HAS_BEEN_DESTROYED_BY_A_MALFUNCTION) end diff --git a/data/modules/Combat/Combat.lua b/data/modules/Combat/Combat.lua index 687f351f8b..4183d85647 100644 --- a/data/modules/Combat/Combat.lua +++ b/data/modules/Combat/Combat.lua @@ -133,7 +133,8 @@ local onChat = function (form, ref, option) end elseif option == 5 then - if not Game.player:GetEquip('radar', 1) then + local radar = Game.player:GetComponent("EquipSet"):GetInstalledOfType("sensor.radar")[1] + if not radar then form:SetMessage(l.RADAR_NOT_INSTALLED) form:RemoveNavButton() return diff --git a/data/modules/Debug/DebugShipSpawn.lua b/data/modules/Debug/DebugShipSpawn.lua index 62f1802cd6..f7d26ef70d 100644 --- a/data/modules/Debug/DebugShipSpawn.lua +++ b/data/modules/Debug/DebugShipSpawn.lua @@ -128,34 +128,17 @@ local ship_equip = { } local set_player_ship_type set_player_ship_type = function(shipType) - local item_types = { - Equipment.misc, - Equipment.laser, - Equipment.hyperspace - } - local equip - do - local _accum_0 = { } - local _len_0 = 1 - for _index_0 = 1, #item_types do - local type = item_types[_index_0] - for _, item in pairs(type) do - for i = 1, Game.player:CountEquip(item) do - _accum_0[_len_0] = item - _len_0 = _len_0 + 1 - end - end - end - equip = _accum_0 + local equipSet = Game.player:GetComponent("EquipSet") + local items = equipSet:GetInstalledEquipment() + local returnedMoney = 0 + for i, item in ipairs(items) do + returnedMoney = returnedMoney + item.price end do local _with_0 = Game.player _with_0:SetShipType(shipType) - for _index_0 = 1, #equip do - local item = equip[_index_0] - _with_0:AddEquip(item) - end _with_0:UpdateEquipStats() + _with_0:AddMoney(returnedMoney) return _with_0 end end diff --git a/data/modules/Debug/DebugShipSpawn.moon b/data/modules/Debug/DebugShipSpawn.moon index de7a650aef..88d8345b62 100644 --- a/data/modules/Debug/DebugShipSpawn.moon +++ b/data/modules/Debug/DebugShipSpawn.moon @@ -111,13 +111,21 @@ ship_equip = { } set_player_ship_type = (shipType) -> - item_types = { Equipment.misc, Equipment.laser, Equipment.hyperspace } - equip = [item for type in *item_types for _, item in pairs(type) for i=1, Game.player\CountEquip item] + -- NOTE: this does not preserve installed items as there's no clear A->B + -- mapping for how to handle different slots between ships + -- Instead, refund the purchase price of all installed equipment + + equipSet = Game.player\GetComponent "EquipSet" + items = equipSet\GetInstalledEquipment! + returnedMoney = 0 + + for i, item in ipairs items + returnedMoney += item.price with Game.player \SetShipType shipType - \AddEquip item for item in *equip \UpdateEquipStats! + \AddMoney returnedMoney ship_spawn_debug_window = -> ui.child 'ship_list', Vector2(150, 0), draw_ship_types diff --git a/data/modules/Rondel/Rondel.lua b/data/modules/Rondel/Rondel.lua index fb2054e848..079073c492 100644 --- a/data/modules/Rondel/Rondel.lua +++ b/data/modules/Rondel/Rondel.lua @@ -142,8 +142,8 @@ local onEnterSystem = function (player) if not system.path:IsSameSystem(rondel_syspath) then return end local tolerance = 1 - local hyperdrive = Game.player:GetEquip('engine',1) - if hyperdrive.fuel == Commodities.military_fuel then + local hyperdrive = Game.player:GetInstalledHyperdrive() + if hyperdrive and hyperdrive.fuel == Commodities.military_fuel then tolerance = 0.5 end diff --git a/data/modules/Scoop/Scoop.lua b/data/modules/Scoop/Scoop.lua index 7330be2e96..278d6cb5ff 100644 --- a/data/modules/Scoop/Scoop.lua +++ b/data/modules/Scoop/Scoop.lua @@ -392,7 +392,7 @@ local onChat = function (form, ref, option) form:SetMessage(string.interp(l["HOW_MUCH_TIME_" .. ad.id], { star = ad.star:GetSystemBody().name, date = Format.Date(ad.due) })) elseif option == 3 then - if ad.reward > 0 and player:CountEquip(Equipment.misc.cargo_scoop) == 0 and player:CountEquip(Equipment.misc.multi_scoop) == 0 then + if ad.reward > 0 and (player["cargo_scoop_cap"] or 0) == 0 then form:SetMessage(l.YOU_DO_NOT_HAVE_A_SCOOP) form:RemoveNavButton() return diff --git a/data/modules/Scout/ScanManager.lua b/data/modules/Scout/ScanManager.lua index fbc26d9f45..9b997cd4fb 100644 --- a/data/modules/Scout/ScanManager.lua +++ b/data/modules/Scout/ScanManager.lua @@ -166,8 +166,8 @@ end ---@package -- Register an equipment listener on the player's ship function ScanManager:SetupShipEquipListener() - self.ship.equipSet:AddListener(function(slot) - if slot == "sensor" then + self.ship:GetComponent("EquipSet"):AddListener(function(_, equip, slot) + if equip.slot and equip.slot.type:match("utility.scanner.planet") then self:UpdateSensorEquipInfo() end end) @@ -179,11 +179,12 @@ end -- Scan the ship's equipment and determine its sensor capabilities -- Note: this function completely rebuilds the list of sensors when a sensor equipment item is changed on the ship function ScanManager:UpdateSensorEquipInfo() - local equip = self.ship.equipSet + local equip = self.ship:GetComponent("EquipSet") self.sensors = {} - local sensors = equip:Get("sensor") + local sensors = equip:GetInstalledOfType("utility.scanner.planet") + if #sensors == 0 then self.activeSensor = nil self:ClearActiveScan() diff --git a/data/modules/SearchRescue/SearchRescue.lua b/data/modules/SearchRescue/SearchRescue.lua index 039f257ae1..fdfe16b514 100644 --- a/data/modules/SearchRescue/SearchRescue.lua +++ b/data/modules/SearchRescue/SearchRescue.lua @@ -39,6 +39,7 @@ local Serializer = require 'Serializer' local Character = require 'Character' local Commodities = require 'Commodities' local Equipment = require 'Equipment' +local Passengers = require 'Passengers' local ShipDef = require 'ShipDef' local Ship = require 'Ship' local utils = require 'utils' @@ -407,20 +408,12 @@ end local passengersPresent = function (ship) -- Check if any passengers are present on the ship. - if ship:CountEquip(Equipment.misc.cabin_occupied) > 0 then - return true - else - return false - end + return Passengers.CountOccupiedCabins(ship) > 0 end local passengerSpace = function (ship) -- Check if the ship has space for passengers. - if ship:CountEquip(Equipment.misc.cabin) > 0 then - return true - else - return false - end + return Passengers.CountFreeCabins(ship) > 0 end local cargoPresent = function (ship, item) @@ -454,18 +447,16 @@ local removeCrew = function (ship) return crew_member end -local addPassenger = function (ship) +local addPassenger = function (ship, passenger) -- Add a passenger to the supplied ship. if not passengerSpace(ship) then return end - ship:RemoveEquip(Equipment.misc.cabin, 1) - ship:AddEquip(Equipment.misc.cabin_occupied, 1) + Passengers.EmbarkPassenger(ship, passenger) end -local removePassenger = function (ship) +local removePassenger = function (ship, passenger) -- Remove a passenger from the supplied ship. if not passengersPresent(ship) then return end - ship:RemoveEquip(Equipment.misc.cabin_occupied, 1) - ship:AddEquip(Equipment.misc.cabin, 1) + Passengers.DisembarkPassenger(ship, passenger) end local addCargo = function (ship, item) @@ -478,26 +469,16 @@ local removeCargo = function (ship, item) ship:GetComponent('CargoManager'):RemoveCommodity(item, 1) end -local passEquipmentRequirements = function (requirements) - -- Check if player ship passes equipment requirements for this mission. - if requirements == {} then return true end - for equipment,amount in pairs(requirements) do - if Game.player:CountEquip(equipment) < amount then return false end - end - return true -end - local isQualifiedFor = function(ad) -- Return if player is qualified for this mission. Used for example by ads to determine if they are -- enabled or disabled for selection on the banter board of if certain missions can be accepted. -- TODO: enable reputation based qualifications -- collect equipment requirements per mission flavor - local requirements = {} local empty_cabins = ad.pickup_crew + ad.deliver_crew + ad.pickup_pass + ad.deliver_pass - if empty_cabins > 0 then requirements[Equipment.misc.cabin] = empty_cabins end - if not passEquipmentRequirements(requirements) then return false end - return true + local avail_cabins = Passengers.CountFreeCabins(Game.player) + + return avail_cabins >= empty_cabins end -- extended mission functions @@ -691,7 +672,7 @@ end local createTargetShip = function (mission) -- Create the target ship to be search for. - local ship + local ship ---@type Ship local shipdef = shipdefFromName(mission.shipdef_name) -- set rand with unique ship seed @@ -733,10 +714,10 @@ local createTargetShip = function (mission) for i = 9, 1, -1 do table.insert(drives, Equipment.hyperspace['hyperdrive_'..tostring(i)]) end - table.sort(drives, function (a,b) return a.capabilities.mass < b.capabilities.mass end) + table.sort(drives, function (a,b) return a.mass < b.mass end) for i = #drives, 1, -1 do local test_drive = drives[i] - if shipdef.capacity / 10 > test_drive.capabilities.mass then + if shipdef.capacity / 10 > test_drive.mass then drive = test_drive end end @@ -744,14 +725,22 @@ local createTargetShip = function (mission) if not drive then drive = drives[1] end ship:AddEquip(drive) - -- load passengers - if mission.pickup_pass > 0 then - ship:AddEquip(Equipment.misc.cabin_occupied, mission.pickup_pass) + local equipSet = ship:GetComponent("EquipSet") + local cabinType = Equipment.Get("misc.cabin") + + for i = 1, math.max(mission.deliver_pass, mission.pickup_pass) do + local cabin = cabinType:Instance() + local installed = equipSet:Install(cabin) + assert(installed, "Not enough space in mission ship for all passengers!") + + if mission.return_pass[i] then + Passengers.EmbarkPassenger(ship, mission.return_pass[i]) + end end -- add hydrogen for hyperjumping even for refueling missoins - to reserve the space -- for refueling missions it is removed later - local drive = ship:GetEquip('engine', 1) + local drive = ship:GetInstalledHyperdrive() local hypfuel = drive.capabilities.hyperclass ^ 2 -- fuel for max range ship:GetComponent('CargoManager'):AddCommodity(drive.fuel or Commodities.hydrogen, hypfuel) @@ -937,11 +926,16 @@ local onChat = function (form, ref, option) deliver_pass_check = "NOT", deliver_comm_check = {}, target_destroyed = "NOT", + return_pass = {}, cargo_pass = {}, cargo_comm = {}, searching = false -- makes sure only one search is active for this mission (function "searchForTarget") } + for _ = 1, ad.pickup_pass do + table.insert(mission.return_pass, Character.New()) + end + -- create target ship if in the same systems, otherwise create when jumping there if mission.flavour.loctype ~= "FAR_SPACE" then mission.target = createTargetShip(mission) @@ -951,14 +945,14 @@ local onChat = function (form, ref, option) if ad.deliver_crew > 0 then for _ = 1, ad.deliver_crew do local passenger = Character.New() - addPassenger(Game.player) + addPassenger(Game.player, passenger) table.insert(mission.cargo_pass, passenger) end end if ad.deliver_pass > 0 then for _ = 1, ad.deliver_pass do local passenger = Character.New() - addPassenger(Game.player) + addPassenger(Game.player, passenger) table.insert(mission.cargo_pass, passenger) end end @@ -1111,9 +1105,9 @@ local flyToNearbyStation = function (ship) -- closest system that does have a station. -- check if ship has atmo shield and limit to vacuum starports if not - local vacuum = false - if ship:CountEquip(Equipment.misc.atmospheric_shielding) == 0 then - vacuum = true + local vacuum = true + if (ship["atmo_shield_cap"] or 0) > 1 then + vacuum = false end local nearbysystems @@ -1514,9 +1508,10 @@ local closeMission = function (mission) -- clear player cargo -- TODO: what to do if player got rid of mission commodity cargo in between (sold?) - for _ = 1, arraySize(mission.cargo_pass) do - removePassenger(Game.player) + for i, passenger in ipairs(mission.cargo_pass) do + removePassenger(Game.player, passenger) end + for commodity,_ in pairs(mission.cargo_comm) do for _ = 1, mission.cargo_comm[commodity] do removeCargo(Game.player, commodity) @@ -1560,7 +1555,7 @@ local pickupCrew = function (mission) -- pickup crew else local crew_member = removeCrew(mission.target) - addPassenger(Game.player) + addPassenger(Game.player, crew_member) table.insert(mission.cargo_pass, crew_member) local boardedtxt = string.interp(l.BOARDED_PASSENGER, {name = crew_member.name}) Comms.ImportantMessage(boardedtxt) @@ -1598,9 +1593,9 @@ local pickupPassenger = function (mission) -- pickup passenger else - local passenger = Character.New() - removePassenger(mission.target) - addPassenger(Game.player) + local passenger = table.remove(mission.return_pass) + removePassenger(mission.target, passenger) + addPassenger(Game.player, passenger) table.insert(mission.cargo_pass, passenger) local boardedtxt = string.interp(l.BOARDED_PASSENGER, {name = passenger.name}) Comms.ImportantMessage(boardedtxt) @@ -1673,7 +1668,7 @@ local deliverCrew = function (mission) -- transfer crew else local crew_member = table.remove(mission.cargo_pass, 1) - removePassenger(Game.player) + removePassenger(Game.player, crew_member) addCrew(mission.target, crew_member) mission.crew_num = mission.crew_num + 1 local deliverytxt = string.interp(l.DELIVERED_PASSENGER, {name = crew_member.name}) @@ -1708,8 +1703,8 @@ local deliverPassenger = function (mission) -- transfer passenger else local passenger = table.remove(mission.cargo_pass, 1) - removePassenger(Game.player) - addPassenger(mission.target) + removePassenger(Game.player, passenger) + addPassenger(mission.target, passenger) local deliverytxt = string.interp(l.DELIVERED_PASSENGER, {name = passenger.name}) Comms.ImportantMessage(deliverytxt) @@ -2097,7 +2092,7 @@ local onShipDocked = function (ship, station) ship:SetFuelPercent(100) -- add hydrogen for hyperjumping - local drive = ship:GetEquip('engine', 1) + local drive = ship:GetInstalledHyperdrive() if drive then ---@type CargoManager local cargoMgr = ship:GetComponent('CargoManager') diff --git a/data/modules/SecondHand/SecondHand.lua b/data/modules/SecondHand/SecondHand.lua index 53fde8d899..07ea1d5626 100644 --- a/data/modules/SecondHand/SecondHand.lua +++ b/data/modules/SecondHand/SecondHand.lua @@ -43,28 +43,17 @@ end -- check it fits on the ship, both the slot for that equipment and the -- weight. +---@param e EquipType local canFit = function (e) - -- todo: this is the same code as in equipmentTableWidgets, unify? - local slot - for i=1,#e.slots do - if Game.player:GetEquipFree(e.slots[i]) > 0 then - slot = e.slots[i] - break - end - end - - -- if ship maxed out in any valid slot for e - if not slot then - return false, l2.SHIP_IS_FULLY_EQUIPPED - end + local equipSet = Game.player:GetComponent("EquipSet") - -- if ship too heavy with this equipment - if Game.player.freeCapacity < e.capabilities.mass then - return false, l2.SHIP_IS_FULLY_LADEN + if e.slot then + local slot = equipSet:GetFreeSlotForEquip(e) + return slot ~= nil, slot, l2.SHIP_IS_FULLY_EQUIPPED + else + return equipSet:CanInstallLoose(e), nil, l2.SHIP_IS_FULLY_EQUIPPED end - - return true, "" end @@ -92,23 +81,30 @@ local onChat = function (form, ref, option) form:SetMessage(l.FITTING_IS_INCLUDED_IN_THE_PRICE) form:AddOption(l.REPEAT_OFFER, 0); elseif option == 2 then --- "Buy" + if Engine.rand:Integer(0, 99) == 0 then -- This is a one in a hundred event + form:SetMessage(l["NO_LONGER_AVAILABLE_" .. Engine.rand:Integer(0, max_surprise_index)]) form:RemoveAdvertOnClose() ads[ref] = nil + elseif Game.player:GetMoney() >= ad.price then - local state, message_str = canFit(ad.equipment) - if state then - local buy_message = string.interp(l.HAS_BEEN_FITTED_TO_YOUR_SHIP, - {equipment = ad.equipment:GetName(),}) + + local ok, slot, message_str = canFit(ad.equipment) + if ok then + local buy_message = string.interp(l.HAS_BEEN_FITTED_TO_YOUR_SHIP, { + equipment = ad.equipment:GetName() + }) + form:SetMessage(buy_message) - Game.player:AddEquip(ad.equipment, 1) + Game.player:GetComponent("EquipSet"):Install(ad.equipment, slot) Game.player:AddMoney(-ad.price) form:RemoveAdvertOnClose() ads[ref] = nil else form:SetMessage(message_str) end + else form:SetMessage(l.YOU_DONT_HAVE_ENOUGH_MONEY) end @@ -137,7 +133,11 @@ local makeAdvert = function (station) end -- choose a random ship equipment - local equipment = avail_equipment[Engine.rand:Integer(1,#avail_equipment)] + local equipType = avail_equipment[Engine.rand:Integer(1,#avail_equipment)] + + -- make an instance of the equipment + -- TODO: set equipment integrity/wear, etc. + local equipment = equipType:Instance() -- buy back price in equipment market is 0.8, so make sure the value is higher local reduction = Engine.rand:Number(0.8,0.9) diff --git a/data/modules/Taxi/Taxi.lua b/data/modules/Taxi/Taxi.lua index 89eb59d6b5..f7e3200130 100644 --- a/data/modules/Taxi/Taxi.lua +++ b/data/modules/Taxi/Taxi.lua @@ -4,17 +4,16 @@ local Engine = require 'Engine' local Lang = require 'Lang' local Game = require 'Game' -local Space = require 'Space' local Comms = require 'Comms' local Event = require 'Event' local Mission = require 'Mission' local MissionUtils = require 'modules.MissionUtils' +local Passengers = require 'Passengers' local Format = require 'Format' local Serializer = require 'Serializer' local Character = require 'Character' +local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' local ShipDef = require 'ShipDef' -local Ship = require 'Ship' -local eq = require 'Equipment' local utils = require 'utils' -- Get the language resource @@ -108,16 +107,14 @@ local passengers = 0 local add_passengers = function (group) for i = 1, group do - Game.player:RemoveEquip(eq.misc.cabin, 1) - Game.player:AddEquip(eq.misc.cabin_occupied, 1) + Passengers.EmbarkPassenger(Game.player, group[i]) end passengers = passengers + group end local remove_passengers = function (group) for i = 1, group do - Game.player:RemoveEquip(eq.misc.cabin_occupied, 1) - Game.player:AddEquip(eq.misc.cabin, 1) + Passengers.DisembarkPassenger(Game.player, group[i]) end passengers = passengers - group end @@ -183,18 +180,24 @@ local onChat = function (form, ref, option) form:SetMessage(howmany) elseif option == 3 then - if not Game.player.cabin_cap or Game.player.cabin_cap < ad.group then + if Passengers.CountFreeCabins(Game.player) < ad.group then form:SetMessage(l.YOU_DO_NOT_HAVE_ENOUGH_CABIN_SPACE_ON_YOUR_SHIP) form:RemoveNavButton() return end - add_passengers(ad.group) - form:RemoveAdvertOnClose() ads[ref] = nil + local group = {} + + for i = 1, ad.group do + table.insert(group, Character.New()) + end + + add_passengers(group) + local mission = { type = "Taxi", client = ad.client, @@ -202,12 +205,12 @@ local onChat = function (form, ref, option) location = ad.location, risk = ad.risk, reward = ad.reward, - due = ad.due, - group = ad.group, + due = ad.due, + group = group, flavour = ad.flavour } - table.insert(missions,Mission.New(mission)) + table.insert(missions, Mission.New(mission)) form:SetMessage(l.EXCELLENT) @@ -358,27 +361,7 @@ local onEnterSystem = function (player) ships = ships-1 if Engine.rand:Number(1) <= risk then - local shipdef = shipdefs[Engine.rand:Integer(1,#shipdefs)] - local default_drive = eq.hyperspace['hyperdrive_'..tostring(shipdef.hyperdriveClass)] - - local max_laser_size = shipdef.capacity - default_drive.capabilities.mass - local laserdefs = utils.build_array(utils.filter( - function (k,l) return l:IsValidSlot('laser_front') and l.capabilities.mass <= max_laser_size and l.l10n_key:find("PULSECANNON") end, - pairs(eq.laser) - )) - local laserdef = laserdefs[Engine.rand:Integer(1,#laserdefs)] - - ship = Space.SpawnShipNear(shipdef.id, Game.player, 50, 100) - ship:SetLabel(Ship.MakeRandomLabel()) - ship:AddEquip(default_drive) - ship:AddEquip(laserdef) - ship:AddEquip(eq.misc.shield_generator, math.ceil(risk * 3)) - if Engine.rand:Number(2) <= risk then - ship:AddEquip(eq.misc.laser_cooling_booster) - end - if Engine.rand:Number(3) <= risk then - ship:AddEquip(eq.misc.shield_energy_booster) - end + ship = ShipBuilder.MakeGenericPirateNear(Game.player, risk, 50, 100) ship:AIKill(Game.player) end end @@ -427,17 +410,22 @@ local onShipDocked = function (player, station) end end +---@param player Player local onShipUndocked = function (player, station) if not player:IsPlayer() then return end - local current_passengers = Game.player:GetEquipCountOccupied("cabin")-(Game.player.cabin_cap or 0) - if current_passengers >= passengers then return end -- nothing changed, good for ref,mission in pairs(missions) do - remove_passengers(mission.group) + local numPassengers = Passengers.CheckEmbarked(player, mission.group) + + -- Lost a passenger somewhere along the way + -- maybe we sold the equipment module with them inside? + if numPassengers < #mission.group then + remove_passengers(mission.group) - Comms.ImportantMessage(l.HEY_YOU_ARE_GOING_TO_PAY_FOR_THIS, mission.client.name) - mission:Remove() - missions[ref] = nil + Comms.ImportantMessage(l.HEY_YOU_ARE_GOING_TO_PAY_FOR_THIS, mission.client.name) + mission:Remove() + missions[ref] = nil + end end end @@ -495,7 +483,7 @@ local buildMissionDescription = function(mission) desc.details = { { l.FROM, ui.Format.SystemPath(mission.start) }, { l.TO, ui.Format.SystemPath(mission.location) }, - { l.GROUP_DETAILS, string.interp(flavours[mission.flavour].howmany, {group = mission.group}) }, + { l.GROUP_DETAILS, string.interp(flavours[mission.flavour].howmany, {group = #mission.group}) }, { l.DEADLINE, ui.Format.Date(mission.due) }, { l.DANGER, flavours[mission.flavour].danger }, { l.DISTANCE, dist.." "..lc.UNIT_LY } diff --git a/data/modules/TradeShips/Debug.lua b/data/modules/TradeShips/Debug.lua index 9bc4f3854f..00e7423060 100644 --- a/data/modules/TradeShips/Debug.lua +++ b/data/modules/TradeShips/Debug.lua @@ -281,7 +281,7 @@ debugView.registerTab('debug-trade-ships', { infosize = ui.getCursorScreenPos().y local obj = Game.systemView:GetSelectedObject() if obj.type ~= Engine.GetEnumValue("ProjectableTypes", "NONE") and Core.ships[obj.ref] then - local ship = obj.ref + local ship = obj.ref ---@type Ship local trader = Core.ships[ship] if ui.collapsingHeader("Info:", {"DefaultOpen"}) then @@ -306,33 +306,27 @@ debugView.registerTab('debug-trade-ships', { local equipItems = {} local total_mass = 0 - local equips = { "misc", "hyperspace", "laser" } - for _,t in ipairs(equips) do - for _,et in pairs(e[t]) do - local count = ship:CountEquip(et) - if count > 0 then - local all_mass = count * et.capabilities.mass - table.insert(equipItems, { - name = et:GetName(), - eq_type = t, - count = count, - mass = et.capabilities.mass, - all_mass = all_mass - }) - total_mass = total_mass + all_mass - end - end - end - - ---@type CargoManager + local equipSet = ship:GetComponent('EquipSet') local cargoMgr = ship:GetComponent('CargoManager') + for _, equip in ipairs(equipSet:GetInstalledEquipment()) do + local count = equip.count or 1 + total_mass = total_mass + equip.mass + table.insert(equipItems, { + name = equip:GetName(), + type = equip.slot and equip.slot.type or "equip", + count = count, + mass = equip.mass, + all_mass = equip.mass * count + }) + end + for name, info in pairs(cargoMgr.commodities) do local ct = CommodityType.GetCommodity(name) total_mass = total_mass + ct.mass * info.count table.insert(equipItems, { name = ct:GetName(), - eq_type = "cargo", + type = "cargo", count = info.count, mass = ct.mass, all_mass = ct.mass * info.count @@ -343,7 +337,7 @@ debugView.registerTab('debug-trade-ships', { arrayTable.draw("tradeships_traderequipment", equipItems, ipairs, { { name = "Name", key = "name", string = true }, - { name = "Type", key = "eq_type", string = true }, + { name = "Type", key = "type", string = true }, { name = "Units", key = "count" }, { name = "Unit's mass", key = "mass", fnc = format("%dt") }, { name = "Total", key = "all_mass", fnc = format("%dt") } diff --git a/data/modules/TradeShips/Trader.lua b/data/modules/TradeShips/Trader.lua index 44bda5820e..2239fce7eb 100644 --- a/data/modules/TradeShips/Trader.lua +++ b/data/modules/TradeShips/Trader.lua @@ -33,8 +33,8 @@ Trader.addEquip = function (ship) local size_factor = ship.freeCapacity ^ 2 / 2000000 if Engine.rand:Number(1) - 0.1 < lawlessness then - local num = math.floor(math.sqrt(ship.freeCapacity / 50)) - - ship:CountEquip(e.misc.shield_generator) + local num = math.floor(math.sqrt(ship.freeCapacity / 50)) --[[ - + ship:CountEquip(e.misc.shield_generator) ]] for i = 1, num do ship:AddEquip(e.misc.shield_generator) end @@ -123,7 +123,7 @@ Trader.doOrbit = function (ship) end local getSystem = function (ship) - local max_range = ship:GetEquip('engine', 1):GetMaximumRange(ship) + local max_range = ship:GetInstalledHyperdrive():GetMaximumRange(ship) max_range = math.min(max_range, 30) local min_range = max_range / 2; local systems_in_range = Game.system:GetNearbySystems(min_range) @@ -212,7 +212,7 @@ local function isAtmo(starport) end local function canAtmo(ship) - return ship:CountEquip(e.misc.atmospheric_shielding) ~= 0 + return (ship["atmo_shield_cap"] or 0) > 0 end local THRUSTER_UP = Engine.GetEnumValue('ShipTypeThruster', 'UP') @@ -245,7 +245,7 @@ Trader.getNearestStarport = function(ship, current) end Trader.addFuel = function (ship) - local drive = ship:GetEquip('engine', 1) + local drive = ship:GetInstalledHyperdrive() -- a drive must be installed if not drive then From ddbb95a926fb95fc951ebabe8b15edac63800c6e Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 3 Feb 2024 02:21:58 -0500 Subject: [PATCH 021/119] Ship: remove unused methods, deprecate AddEquip --- data/libs/Ship.lua | 185 ++++----------------------------------------- 1 file changed, 16 insertions(+), 169 deletions(-) diff --git a/data/libs/Ship.lua b/data/libs/Ship.lua index e0128fbd8a..689bc69926 100644 --- a/data/libs/Ship.lua +++ b/data/libs/Ship.lua @@ -74,34 +74,6 @@ function Ship:GetInstalledHyperdrive() return drives[1] end --- Method: CountEquip --- --- Get the number of a given equipment item in a given equipment slot --- --- > count = ship:CountEquip(item, slot) --- --- Parameters: --- --- item - an Equipment type object (e.g., require 'Equipment'.misc.radar) --- --- slot - the slot name (e.g., "radar") --- --- Return: --- --- count - the number of the given item in the slot --- --- Availability: --- --- TBA --- --- Status: --- --- experimental --- -function Ship:CountEquip(item, slot) - return self.equipSet:Count(item, slot) -end - -- -- Method: AddEquip -- @@ -133,157 +105,32 @@ end -- -- Status: -- --- experimental +-- deprecated -- +---@deprecated function Ship:AddEquip(item, count, slot) + -- NOTE: this function only remains to ensure legacy "fire and forget" + -- ship equipment codepaths remain functional. New code should interact + -- directly without EquipSet instead assert(not count or count == 1) - local ret = self.equipSet:Add(self, item(), count, slot) - if ret > 0 then - Event.Queue("onShipEquipmentChanged", self, item) - end - return ret -end + assert(not slot) --- --- Method: GetEquip --- --- Get a list of equipment in a given equipment slot --- --- > equip = ship:GetEquip(slot, index) --- > equiplist = ship:GetEquip(slot) --- --- Parameters: --- --- slot - a slot name string (e.g., "autopilot") --- --- index - optional. The equipment position in the slot to fetch. If --- specified the item at that position in the slot will be returned, --- otherwise a table containing all items in the slot will be --- returned instead. --- --- Return: --- --- equip - when index is specified, an equipment type object for the --- item, or nil --- --- equiplist - when index is not specified, a table which has slot indexes --- as keys and equipment type objects as values. --- WARNING: although slot indexes are integers, this table is --- not guaranteed to contain a contiguous set of entries, so you --- should iterate over it with pairs(), not ipairs(). --- --- Availability: --- --- alpha 10 --- --- Status: --- --- experimental --- -Ship.GetEquip = function (self, slot, index) - return self.equipSet:Get(slot, index) -end - --- --- Method: GetEquipFree --- --- Get the amount of free space in a given equipment slot --- --- > free = ship:GetEquipFree(slot) --- --- Parameters: --- --- slot - a slot name (e.g., "autopilot") --- --- Return: --- --- free - the number of item spaces left in this slot --- --- Availability: --- --- alpha 10 --- --- Status: --- --- experimental --- -Ship.GetEquipFree = function (self, slot) - return self.equipSet:FreeSpace(slot) -end - --- --- Method: SetEquip --- --- Overwrite a single item of equipment in a given equipment slot --- --- > ship:SetEquip(slot, index, equip) --- --- Parameters: --- --- slot - a slot name (e.g., "laser_front") --- --- index - the position to store the item in --- --- item - an equipment type object (e.g., Equipment.laser.large_plasma_accelerator) --- --- Example: --- --- > -- add a laser to the rear laser mount --- > ship:SetEquip("laser_rear", 1, Equipment.laser.pulsecannon_4mw) --- --- Availability: --- --- alpha 10 --- --- Status: --- --- experimental --- -Ship.SetEquip = function (self, slot, index, item) - self.equipSet:Set(self, slot, index, item) - Event.Queue("onShipEquipmentChanged", self) -end + local equipSet = self:GetComponent("EquipSet") + if not item:isProto() then + item = item:Instance() + end --- --- Method: RemoveEquip --- --- Remove one or more of a given equipment type from its appropriate equipment slot --- --- > num_removed = ship:RemoveEquip(item, count, slot) --- --- Parameters: --- --- item - an equipment type object (e.g., Equipment.misc.autopilot) --- --- count - optional. The number of this item to remove. Defaults to 1. --- --- slot - optional. The slot to remove the Equipment in, if other than default. --- --- Return: --- --- num_removed - the number of items removed --- --- Example: --- --- > ship:RemoveEquip(Equipment.hyperspace.hyperdrive_2) --- --- Availability: --- --- alpha 10 --- --- Status: --- --- experimental --- + local slotHandle = item.slot and equipSet:GetFreeSlotForEquip(item) + local ok = equipSet:Install(item, slotHandle) -Ship.RemoveEquip = function (self, item, count, slot) - local ret = self.equipSet:Remove(self, item, count, slot) - if ret > 0 then + if ok then Event.Queue("onShipEquipmentChanged", self, item) end - return ret + + return ok and 1 or 0 end + -- -- Method: IsHyperjumpAllowed -- From e197fce8a72f55fca8d90e091c7c929d165efa9c Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 3 Feb 2024 02:23:23 -0500 Subject: [PATCH 022/119] Move equipment resell price modifier into Economy.lua --- data/libs/Economy.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/data/libs/Economy.lua b/data/libs/Economy.lua index 0df7bca9ce..013ebb1206 100644 --- a/data/libs/Economy.lua +++ b/data/libs/Economy.lua @@ -31,8 +31,13 @@ table.sort(Economies, function(a, b) return a.id < b.id end) -- Percentage modifier applied to buying/selling commodities -- Prevents buying a commodity at a station and immediately reselling it Economy.TradeFeeSplit = 2 + +-- Total trade fee percentage applied to a buy->sell transaction Economy.TotalTradeFees = 2 * Economy.TradeFeeSplit +-- Scalar multiplier applied when reselling "used" equipment items back onto the market +Economy.BaseResellPriceModifier = 0.8 + -- stationMarket is a persistent table of stock information for every station -- the player has visited in their journey local stationMarket = {} From 5c812bc9c20591feb48dd65fc67b330981de0aac Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 3 Feb 2024 02:23:48 -0500 Subject: [PATCH 023/119] Break equip card rendering out into a separate file --- data/pigui/libs/equip-card.lua | 196 +++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 data/pigui/libs/equip-card.lua diff --git a/data/pigui/libs/equip-card.lua b/data/pigui/libs/equip-card.lua new file mode 100644 index 0000000000..bba2083671 --- /dev/null +++ b/data/pigui/libs/equip-card.lua @@ -0,0 +1,196 @@ +-- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +local ItemCard = require 'pigui.libs.item-card' +local Vector2 = Vector2 +local utils = require 'utils' + +local Lang = require 'Lang' +local le = Lang.GetResource("equipment-core") + +local ui = require 'pigui' + +local icons = ui.theme.icons +local colors = ui.theme.colors +local pionillium = ui.fonts.pionillium + +-- +-- ============================================================================= +-- Equipment Item Card +-- ============================================================================= +-- + +-- Class: UI.EquipCard +-- +-- Draw all visuals related to an individual equipment item. +-- This includes the background, highlight, icon, [slot type], +-- name, [size], and up to 4 sub-stats directly on the icon card +-- +-- DrawEquipmentItem* functions take a "data" argument table with the form: +-- icon - icon index to display for the slot +-- name - translated name to display on the slot +-- equip - equipment object being drawn (or none for an empty slot) +-- type* - translated "slot type" name to display +-- size* - string of the form "S1" to display in the slot name for sized slots +-- stats - list of detailed item stats to display in the item tooltip, +-- in the format { label, icon, value, color } +-- [...] - up to 4 { icon, value, tooltip } data items for the stats line + +---@class UI.EquipCard : UI.ItemCard +---@field New fun(): self +local EquipCard = utils.inherits(ItemCard, "UI.EquipCard") + +---@class UI.EquipCard.Data +---@field icon ui.Icon +---@field name string? +---@field count integer? +---@field type string? +---@field size string? +---@field equip EquipType? +---@field stats EquipType.UI.Stats[] | nil + +EquipCard.tooltipStats = true +EquipCard.highlightBar = true +EquipCard.detailFields = 4 +EquipCard.tooltipWrapEm = 18 + +EquipCard.emptyIcon = icons.autopilot_dock + +local tooltipStyle = { + WindowPadding = ui.theme.styles.WindowPadding, + WindowRounding = EquipCard.rounding +} + +---@param data UI.EquipCard.Data +function EquipCard:tooltipContents(data, isSelected) + if data.equip then + ui.withFont(pionillium.heading, function() + ui.text(data.equip:GetName()) + ui.spacing() + end) + + local desc = data.equip:GetDescription() + + if desc and desc ~= "" then + ui.withFont(pionillium.body, function() + + ui.textWrapped(desc) + ui.spacing() + end) + end + end + + if self.tooltipStats and data.stats then + ui.separator() + ui.spacing() + + ui.withFont(pionillium.body, function() + self.drawEquipStats(data) + end) + end +end + +---@param data UI.EquipCard.Data +function EquipCard:drawTooltip(data, isSelected) + if not (self.tooltipStats and data.stats or data.equip) then + return + end + + ui.withStyleVars(tooltipStyle, function() + ui.customTooltip(function() + local wrapWidth = ui.getTextLineHeight() * self.tooltipWrapEm + + ui.pushTextWrapPos(wrapWidth) + + self:tooltipContents(data, isSelected) + + ui.popTextWrapPos() + end) + end) +end + +---@param data UI.EquipCard.Data +function EquipCard.drawEquipStats(data) + ui.beginTable("EquipStats", 2, { "NoSavedSettings" }) + + ui.tableSetupColumn("##name", { "WidthStretch" }) + ui.tableSetupColumn("##value", { "WidthFixed" }) + + ui.pushTextWrapPos(-1) + for i, tooltip in ipairs(data.stats) do + ui.tableNextRow() + + ui.tableSetColumnIndex(0) + ui.text(tooltip[1] .. ":") + + ui.tableSetColumnIndex(1) + ui.icon(tooltip[2], Vector2(ui.getTextLineHeight(), ui.getTextLineHeight()), ui.theme.colors.font) + ui.sameLine(0, ui.getTextLineHeight() / 4.0) + + local value, format = tooltip[3], tooltip[4] + ui.text(format(value)) + end + ui.popTextWrapPos() + + ui.endTable() +end + +local slotColors = { Text = colors.equipScreenBgText } + +---@param data UI.EquipCard.Data +function EquipCard:drawTitle(data, textWidth, isSelected) + local pos = ui.getCursorPos() + + -- Draw the name of what's in the slot + if data.name then + ui.text(data.name) + else + ui.withStyleColors(slotColors, function() + ui.text("[" .. le.EMPTY_SLOT .. "]") + end) + end + + -- If there's a slot count, draw it + if data.count then + ui.sameLine() + ui.withStyleColors(slotColors, function() + ui.text("x{count}" % data) + end) + end + + -- Draw the size and/or type of the slot + local slotType = data.type and data.size and "{type} | {size}" % data + or data.type or data.size + + if slotType then + ui.setCursorPos(pos + Vector2(textWidth - ui.calcTextSize(slotType).x, 0)) + ui.withStyleColors(slotColors, function() + ui.text(slotType) + end) + end +end + +---@param equip EquipType? +---@param compare EquipType? +function EquipCard.getDataForEquip(equip, compare) + ---@type UI.EquipCard.Data + local out = { + icon = equip and icons[equip.icon_name] or EquipCard.emptyIcon + } + + if equip then + out.name = equip:GetName() + out.equip = equip + out.size = equip.slot and ("S" .. equip.slot.size) or nil + + out.stats = equip:GetDetailedStats() + + for i, v in ipairs(equip:GetItemCardStats()) do + out[i] = v + end + end + + return out +end + +return EquipCard From 890dcca37c59616037dc40cb684e4f1c20e36d6a Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 3 Feb 2024 02:31:39 -0500 Subject: [PATCH 024/119] Replace equipment market with equipment outfitter - New equipment outfitter widget provides much richer display of equipment items, confirmation before purchase, and comparison between installed and purchaseable equipment - Can be made forward-compatible with stored equipment items in ship cargo or station storage --- data/pigui/libs/equipment-market.lua | 241 ---------- data/pigui/libs/equipment-outfitter.lua | 528 +++++++++++++++++++++ data/pigui/libs/ship-equipment.lua | 596 ++++++++++-------------- 3 files changed, 787 insertions(+), 578 deletions(-) delete mode 100644 data/pigui/libs/equipment-market.lua create mode 100644 data/pigui/libs/equipment-outfitter.lua diff --git a/data/pigui/libs/equipment-market.lua b/data/pigui/libs/equipment-market.lua deleted file mode 100644 index 0ca85f8350..0000000000 --- a/data/pigui/libs/equipment-market.lua +++ /dev/null @@ -1,241 +0,0 @@ --- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details --- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt - -local Game = require 'Game' -local Lang = require 'Lang' -local utils= require 'utils' - -local ui = require 'pigui' -local ModalWindow = require 'pigui.libs.modal-win' -local TableWidget = require 'pigui.libs.table' - -local l = Lang.GetResource("ui-core") - -local sellPriceReduction = 0.8 - -local defaultFuncs = { - -- can we display this item - canDisplayItem = function (self, e) - return e.purchasable - end, - - -- how much of this item do we have in stock? - getStock = function (self, e) - return Game.player:GetDockedWith():GetEquipmentStock(e) - end, - - -- what do we charge for this item if we are buying - getBuyPrice = function (self, e) - return Game.player:GetDockedWith():GetEquipmentPrice(e) - end, - - -- what do we get for this item if we are selling - getSellPrice = function (self, e) - local basePrice = Game.player:GetDockedWith():GetEquipmentPrice(e) - if basePrice > 0 then - if e:IsValidSlot("cargo") then - return basePrice - else - return sellPriceReduction * basePrice - end - else - return 1.0/sellPriceReduction * basePrice - end - end, - - -- do something when a "buy" button is clicked - -- return true if the buy can proceed - onClickBuy = function (self, e) - return true -- allow buy - end, - - buy = function(self, e) - if not self.funcs.onClickBuy(self, e) then return end - - if self.funcs.getStock(self, e) <= 0 then - return self.funcs.onBuyFailed(self, e, l.ITEM_IS_OUT_OF_STOCK) - end - - local player = Game.player - - -- if this ship model doesn't support fitting of this equip: - if player:GetEquipSlotCapacity(e:GetDefaultSlot(player)) < 1 then - return self.funcs.onBuyFailed(self, e, string.interp(l.NOT_SUPPORTED_ON_THIS_SHIP, {equipment = e:GetName(),})) - end - - -- add to first free slot - local slot - for i=1,#e.slots do - if player:GetEquipFree(e.slots[i]) > 0 then - slot = e.slots[i] - break - end - end - - -- if ship maxed out in any valid slot for e - if not slot then - return self.funcs.onBuyFailed(self, e, l.SHIP_IS_FULLY_EQUIPPED) - end - - -- if ship too heavy to support more - if player.freeCapacity < e.capabilities.mass then - return self.funcs.onBuyFailed(self, e, l.SHIP_IS_FULLY_LADEN) - end - - - local price = self.funcs.getBuyPrice(self, e) - if player:GetMoney() < self.funcs.getBuyPrice(self, e) then - return self.funcs.onBuyFailed(self, e, l.YOU_NOT_ENOUGH_MONEY) - end - - assert(player:AddEquip(e, 1, slot) == 1) - player:AddMoney(-price) - - self.funcs.bought(self, e) - end, - - -- do something when we buy this commodity - bought = function (self, e, tradeamount) - local count = tradeamount or 1 -- default to 1 for e.g. equipment market - Game.player:GetDockedWith():AddEquipmentStock(e, -count) - end, - - onBuyFailed = function (self, e, reason) - self.popup.msg = reason - self.popup:open() - end, - - -- do something when a "sell" button is clicked - -- return true if the buy can proceed - onClickSell = function (self, e) - return true -- allow sell - end, - - sell = function(self, e) - if not self.funcs.onClickSell(self, e) then return end - - local player = Game.player - - -- remove from last free slot (reverse table) - local slot - for i, s in utils.reverse(e.slots) do - if player:CountEquip(e, s) > 0 then - slot = s - break - end - end - - player:RemoveEquip(e, 1, slot) - player:AddMoney(self.funcs.getSellPrice(self, e)) - - self.funcs.sold(self, e) - end, - - -- do something when we sell this items - sold = function (self, e, tradeamount) - local count = tradeamount or 1 -- default to 1 for e.g. equipment market - Game.player:GetDockedWith():AddEquipmentStock(e, count) - end, - - initTable = function(self) - ui.withFont(self.style.headingFont.name, self.style.headingFont.size, function() - ui.text(self.title) - end) - - ui.columns(5, self.id, false) - end, - - renderRow = function(self, item) - - end, - - onMouseOverItem = function(self, item) - - end, - - -- sort items in the market table - sortingFunction = function(e1,e2) - if e1:GetDefaultSlot() == e2:GetDefaultSlot() then - if e1:GetDefaultSlot() == "cargo" then - return e1:GetName() < e2:GetName() -- cargo sorted on translated name - else - if e1:GetDefaultSlot():find("laser") then -- can be laser_front or _back - if e1.l10n_key:find("PULSE") and e2.l10n_key:find("PULSE") or - e1.l10n_key:find("PLASMA") and e2.l10n_key:find("PLASMA") then - return e1.price < e2.price - else - return e1.l10n_key < e2.l10n_key - end - else - return e1.l10n_key < e2.l10n_key - end - end - else - return e1:GetDefaultSlot() < e2:GetDefaultSlot() - end - end -} - -local MarketWidget = { - defaultFuncs = defaultFuncs -} - -function MarketWidget.New(id, title, config) - local self = TableWidget.New(id, title, config) - - self.popup = config.popup or ModalWindow.New('popupMsg' .. id, function() - ui.text(self.popup.msg) - ui.dummy(Vector2((ui.getContentRegion().x - 100) / 2, 0)) - ui.sameLine() - if ui.button("OK", Vector2(100, 0)) then - self.popup:close() - end - end) - - self.items = {} - self.itemTypes = config.itemTypes or {} - self.columnCount = config.columnCount or 0 - - self.funcs.getStock = config.getStock or defaultFuncs.getStock - self.funcs.getBuyPrice = config.getBuyPrice or defaultFuncs.getBuyPrice - self.funcs.getSellPrice = config.getSellPrice or defaultFuncs.getSellPrice - self.funcs.onClickBuy = config.onClickBuy or defaultFuncs.onClickBuy - self.funcs.onClickSell = config.onClickSell or defaultFuncs.onClickSell - self.funcs.buy = config.buy or defaultFuncs.buy - self.funcs.bought = config.bought or defaultFuncs.bought - self.funcs.onBuyFailed = config.onBuyFailed or defaultFuncs.onBuyFailed - self.funcs.sell = config.sell or defaultFuncs.sell - self.funcs.sold = config.sold or defaultFuncs.sold - self.funcs.initTable = config.initTable or defaultFuncs.initTable - self.funcs.canDisplayItem = config.canDisplayItem or defaultFuncs.canDisplayItem - self.funcs.sortingFunction = config.sortingFunction or defaultFuncs.sortingFunction - self.funcs.onMouseOverItem = config.onMouseOverItem or defaultFuncs.onMouseOverItem - - - setmetatable(self, { - __index = MarketWidget, - class = "UI.MarketWidget", - }) - - return self -end - -function MarketWidget:refresh() - self.items = {} - - for i, type in pairs(self.itemTypes) do - for j, item in pairs(type) do - if self.funcs.canDisplayItem(self, item) then - table.insert(self.items, item) - end - end - end - - table.sort(self.items, self.funcs.sortingFunction) -end - -function MarketWidget:render() - TableWidget.render(self) -end - -return MarketWidget diff --git a/data/pigui/libs/equipment-outfitter.lua b/data/pigui/libs/equipment-outfitter.lua new file mode 100644 index 0000000000..8e4c382c40 --- /dev/null +++ b/data/pigui/libs/equipment-outfitter.lua @@ -0,0 +1,528 @@ +-- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +local Game = require 'Game' +local Economy = require 'Economy' +local Equipment = require 'Equipment' +local EquipSet = require 'EquipSet' +local Lang = require 'Lang' +local utils = require 'utils' + +local ui = require 'pigui' + +local pionillium = ui.fonts.pionillium +local colors = ui.theme.colors +local icons = ui.theme.icons + +local Module = require 'pigui.libs.module' +local EquipCard = require 'pigui.libs.equip-card' + +local l = Lang.GetResource("ui-core") + +local compare = function(a, b, invert) + local n = invert and b - a or a - b + if n > 0 then + return colors.compareBetter + elseif n == 0 then + return colors.font + else + return colors.compareWorse + end +end + + +local pigui = require 'Engine'.pigui + +local framePadding = ui.rescaleUI(Vector2(10, 12)) +local function getCustomButtonHeight() + return framePadding.y * 2.0 + pionillium.heading.size * 1.5 +end + +local customButton = function(label, icon, infoText, variant) + local lineHeight = pionillium.heading.size * 1.5 + local height = lineHeight + framePadding.y * 2.0 + local icon_size = Vector2(lineHeight) + local size = Vector2(ui.getContentRegion().x, height) + + local textOffset = framePadding + Vector2(icon_size.x + framePadding.x, pionillium.heading.size * 0.25) + local fontCol = ui.theme.colors.font + + local startPos = ui.getCursorScreenPos() + + variant = variant or ui.theme.buttonColors.dark + local clicked = false + + ui.withButtonColors(variant, function() + pigui.PushStyleVar("FramePadding", framePadding) + pigui.PushStyleVar("FrameRounding", 4) + clicked = pigui.Button("##" .. label, size) + pigui.PopStyleVar(2) + end) + + ui.withFont(pionillium.heading, function() + ui.addIconSimple(startPos + framePadding, icon, icon_size, fontCol) + ui.addText(startPos + textOffset, fontCol, label) + + if infoText then + local calcSize = ui.calcTextSize(infoText) + local infoTextOffset = Vector2(size.x - (framePadding.x + calcSize.x), textOffset.y) + ui.addText(startPos + infoTextOffset, fontCol, infoText) + else + local endOffset = size.x - framePadding.x + local width = ui.getTextLineHeight() / 1.6 + ui.addRectFilled( + startPos + Vector2(endOffset - width, framePadding.y), + startPos + Vector2(endOffset, height - framePadding.y), + variant.normal, 4, 0) + end + end) + + return clicked +end + +--============================================================================= + +local EquipCardAvailable = EquipCard.New() +EquipCardAvailable.tooltipStats = false + +---@class UI.EquipmentOutfitter.EquipData : UI.EquipCard.Data +---@field canInstall boolean +---@field available boolean +---@field price number + +---@class UI.EquipmentOutfitter.EquipCard : UI.EquipCard +local EquipCardUnavailable = EquipCard.New() + +EquipCardUnavailable.tooltipStats = false +EquipCardUnavailable.backgroundColor = ui.theme.styleColors.gray_800 +EquipCardUnavailable.hoveredColor = ui.theme.styleColors.gray_700 +EquipCardUnavailable.selectedColor = ui.theme.styleColors.gray_600 +EquipCardUnavailable.textColor = ui.theme.styleColors.danger_300 + +---@param data UI.EquipmentOutfitter.EquipData +function EquipCardUnavailable:tooltipContents(data, isSelected) + EquipCard.tooltipContents(self, data, isSelected) + + ui.spacing() + + ui.withStyleColors({ Text = self.textColor }, function() + + if not data.canInstall then + ui.textWrapped(l.NOT_SUPPORTED_ON_THIS_SHIP % { equipment = data.name }) + else + ui.textWrapped(l.YOU_NOT_ENOUGH_MONEY) + end + end) +end + +--============================================================================= + +---@class UI.EquipmentOutfitter : UI.Module +---@field New fun(): self +local Outfitter = utils.class("UI.EquipmentOutfitter", Module) + +function Outfitter:Constructor() + Module.Constructor(self) + + self.ship = nil ---@type Ship + self.station = nil ---@type SpaceStation + self.filterSlot = nil ---@type ShipDef.Slot? + self.replaceEquip = nil ---@type EquipType? + + self.equipmentList = {} ---@type UI.EquipmentOutfitter.EquipData[] + self.selectedEquip = nil ---@type UI.EquipmentOutfitter.EquipData? + self.currentEquip = nil ---@type UI.EquipCard.Data? + + self.compare_stats = nil ---@type { label: string, a: EquipType.UI.Stats?, b: EquipType.UI.Stats? }[]? +end + +--================== +-- Helper functions +--================== + +function Outfitter:stationHasTech(level) + level = level == "MILITARY" and 11 or level + return self.station.techLevel >= level +end + +-- Override to support e.g. custom equipment shops +---@return EquipType[] +function Outfitter:getAvailableEquipment() + return utils.filter_table(Equipment.new, function(id, equip) + if self:getStock(equip) <= 0 then + -- FIXME: restore when equipment stocking converted to new system + --return false + end + + return self:canDisplayItem(equip) + end) +end + +---@param e EquipType +function Outfitter:canDisplayItem(e) + if e.SpecializeForShip then + e = e:SpecializeForShip(self.ship) + end + + return e.purchasable and self:stationHasTech(e.tech_level) and EquipSet.CompatibleWithSlot(e, self.filterSlot) +end + +---@param e EquipType +function Outfitter:getStock(e) + e = e.__proto or e + return self.station:GetEquipmentStock(e) +end + +-- Cost of the equipment item if buying +function Outfitter:getBuyPrice(e) + e = e.__proto or e + return self.station:GetEquipmentPrice(e) +end + +-- Money gained from equipment item if selling +function Outfitter:getSellPrice(e) + e = e.__proto or e + return self.station:GetEquipmentPrice(e) * Economy.BaseResellPriceModifier +end + +-- Purchase price of an item less the sale cost of the old item +function Outfitter:getInstallPrice(e) + return self:getBuyPrice(e) - (self.replaceEquip and self:getSellPrice(self.replaceEquip) or 0) +end + +function Outfitter.sortEquip(e1, e2) + if e1.slot then + return e1.slot.size < e2.slot.size + or (e1.slot.size == e2.slot.size and e1:GetName() < e2:GetName()) + else + return e1:GetName() < e2:GetName() + end +end + +function Outfitter:buildEquipmentList() + local equipment = self:getAvailableEquipment() + + ---@type EquipType[] + local equipList = {} + + for _, v in pairs(equipment) do + table.insert(equipList, v) + end + + table.sort(equipList, self.sortEquip) + + local currentProto = self.replaceEquip and self.replaceEquip:GetPrototype() + local equipSet = self.ship:GetComponent("EquipSet") + local money = Game.player:GetMoney() + + self.currentEquip = self.replaceEquip and EquipCard.getDataForEquip(self.replaceEquip) or nil + + self.equipmentList = utils.map_array(equipList, function(equip) + local data = EquipCard.getDataForEquip(equip, self.replaceEquip) + ---@cast data UI.EquipmentOutfitter.EquipData + + data.price = self:getBuyPrice(equip) + + if self.filterSlot then + data.canInstall = equipSet:CanInstallInSlot(self.filterSlot, equip) + else + data.canInstall = equipSet:CanInstallLoose(equip) + end + + data.available = data.canInstall and money >= self:getInstallPrice(equip) + + -- Replace condition widget with price instead + -- trim leading '$' character since we're drawing it with an icon instead + data[#data] = { icons.money, ui.Format.Money(data.price):sub(2, -1) } + + if equip:GetPrototype() == currentProto then + data.type = l.INSTALLED + end + + return data + end) +end + +local emptyStats = {} + +function Outfitter:buildCompareStats() + local a = self.selectedEquip and self.selectedEquip.stats or emptyStats + local b = self.currentEquip and self.currentEquip.stats or emptyStats + + local out = {} + + if a and b then + local s1 = 1 + local s2 = 1 + + -- A bit messy, but essentially inserts the shared prefix of the array + -- followed by the leftover values from A and then the leftover values + -- from B. + while s1 <= #a or s2 <= #b do + local stat_a = a[s1] + local stat_b = b[s2] + + if s1 == s2 and stat_a and stat_b and stat_a[1] == stat_b[1] then + table.insert(out, { label = stat_a[1], a = stat_a, b = stat_b }) + s1 = s1 + 1 + s2 = s2 + 1 + elseif stat_a then + table.insert(out, { label = stat_a[1], a = stat_a, b = nil }) + s1 = s1 + 1 + elseif stat_b then + table.insert(out, { label = stat_b[1], a = nil, b = stat_b}) + s2 = s2 + 1 + end + end + elseif a then + out = utils.map_array(a, function(v) return { label = v[1], a = v, b = nil } end) + elseif b then + out = utils.map_array(b, function(v) return { label = v[1], a = nil, b = v } end) + end + + self.compare_stats = out +end + +--================== +-- Message handlers +--================== + +---@param item UI.EquipmentOutfitter.EquipData +function Outfitter:onSelectItem(item) + self.selectedEquip = item + self:buildCompareStats() +end + +---@param item EquipType +function Outfitter:onBuyItem(item) + self.selectedEquip = nil +end + +---@param item EquipType +function Outfitter:onSellItem(item) + self.selectedEquip = nil +end + +function Outfitter:onClose() + return +end + +function Outfitter:refresh() + self.selectedEquip = nil + + if not self.ship or not self.station then + self.equipmentList = {} + return + end + + self:buildEquipmentList() + self:buildCompareStats() +end + +--================== +-- Render functions +--================== + +function Outfitter:drawEquipmentItem(data, isSelected) + if data.available then + return EquipCardAvailable:draw(data, isSelected) + else + return EquipCardUnavailable:draw(data, isSelected) + end +end + +function Outfitter:drawBuyButton(data) + local icon = icons.autopilot_dock + local price_text = l.PRICE .. ": " .. ui.Format.Money(self:getInstallPrice(data.equip)) + + local variant = data.available and ui.theme.buttonColors.dark or ui.theme.buttonColors.disabled + if customButton(l.BUY_EQUIP % data, icon, price_text, variant) and data.available then + self:message("onBuyItem", data.equip) + end +end + +function Outfitter:drawSellButton(data) + local icon = icons.autopilot_undock_illegal + local price_text = l.PRICE .. ": " .. ui.Format.Money(self:getSellPrice(data.equip)) + + if customButton(l.SELL_EQUIP % data, icon, price_text) then + self:message("onSellItem", data.equip) + end +end + +---@param label string +---@param stat_a EquipType.UI.Stats? +---@param stat_b EquipType.UI.Stats? +function Outfitter:renderCompareRow(label, stat_a, stat_b) + ui.tableNextColumn() + ui.text(label) + + local icon_size = Vector2(ui.getTextLineHeight()) + local color = stat_a and stat_b + and compare(stat_a[3], stat_b[3], stat_a[5]) + or colors.font + + ui.tableNextColumn() + if stat_a then + ui.icon(stat_a[2], icon_size, colors.font) + ui.sameLine() + + local val, format = stat_a[3], stat_a[4] + ui.textColored(color, format(val)) + end + + ui.tableNextColumn() + if stat_b then + ui.icon(stat_b[2], icon_size, colors.font) + ui.sameLine() + + local val, format = stat_b[3], stat_b[4] + ui.text(format(val)) + end +end + +function Outfitter:renderCompareStats() + if self.selectedEquip then + ui.group(function() + ui.text(l.SELECTED .. ":") + + ui.withFont(pionillium.heading, function() + ui.text(self.selectedEquip.name) + end) + end) + end + + if self.currentEquip then + ui.sameLine() + + ui.group(function() + ui.textAligned(l.EQUIPPED .. ":", 1.0) + + ui.withFont(pionillium.heading, function() + ui.textAligned(self.currentEquip.name, 1.0) + end) + end) + end + + ui.separator() + ui.spacing() + + if self.selectedEquip then + ui.textWrapped(self.selectedEquip.equip:GetDescription()) + elseif self.currentEquip then + ui.textWrapped(self.currentEquip.equip:GetDescription()) + end + + ui.spacing() + + if self.compare_stats then + + ui.beginTable("##CompareEquipStats", 3) + + ui.tableSetupColumn("##name", { "WidthStretch" }) + ui.tableSetupColumn("##selected", { "WidthFixed" }) + ui.tableSetupColumn("##current", { "WidthFixed" }) + + for _, row in ipairs(self.compare_stats) do + ui.tableNextRow() + self:renderCompareRow(row.label, row.a, row.b) + end + + ui.endTable() + + end + +end + +function Outfitter:render() + + local spacing = ui.getItemSpacing() + local panelWidth = (ui.getContentRegion().x - spacing.x * 4.0) / 2 + + ui.child("##ListPane", Vector2(panelWidth, 0), function() + + ui.withStyleVars({ FrameRounding = 4 }, function() + if ui.button("<") then + self:message("onClose") + end + end) + + ui.sameLine() + + ui.withFont(pionillium.heading, function() + + ui.text(self.replaceEquip and l.REPLACE_EQUIPMENT_WITH or l.AVAILABLE_FOR_PURCHASE) + end) + + ui.spacing() + + local buttonLineHeight = 0.0 + local spacing = ui.getItemSpacing() + + if self.selectedEquip then + buttonLineHeight = buttonLineHeight + getCustomButtonHeight() + spacing.y + end + + if self.currentEquip then + buttonLineHeight = buttonLineHeight + getCustomButtonHeight() + spacing.y + end + + if buttonLineHeight > 0.0 then + buttonLineHeight = buttonLineHeight + spacing.y * 2.0 + end + + ui.child("##EquipmentList", Vector2(0, -buttonLineHeight), function() + for _, data in ipairs(self.equipmentList) do + local clicked = self:drawEquipmentItem(data, data == self.selectedEquip) + + if clicked then + self:message("onSelectItem", data) + end + end + end) + + if self.selectedEquip or self.currentEquip then + ui.separator() + ui.spacing() + end + + if self.selectedEquip then + self:drawBuyButton(self.selectedEquip) + end + + if self.currentEquip then + self:drawSellButton(self.currentEquip) + end + + end) + + ui.sameLine(0, spacing.x * 4) + + local linePos = ui.getCursorScreenPos() - Vector2(spacing.x * 2, 0) + ui.addLine(linePos, linePos + Vector2(0, ui.getContentRegion().y), ui.theme.styleColors.gray_500, 2.0) + + ui.group(function() + local scannerSize = Vector2(panelWidth, (ui.getContentRegion().y - ui.getItemSpacing().y) / 2.0) + + ui.child("##EquipmentDetails", Vector2(panelWidth, scannerSize.y), function() + + if self.selectedEquip or self.currentEquip then + + ui.withFont(pionillium.body, function() + self:renderCompareStats() + end) + + end + + end) + + self:drawShipSpinner(scannerSize) + end) + +end + +function Outfitter:drawShipSpinner(size) + -- Override this in an owning widget to draw a ship spinner +end + +return Outfitter diff --git a/data/pigui/libs/ship-equipment.lua b/data/pigui/libs/ship-equipment.lua index 25cd9cb97e..513b172131 100644 --- a/data/pigui/libs/ship-equipment.lua +++ b/data/pigui/libs/ship-equipment.lua @@ -1,17 +1,16 @@ -- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt -local Equipment = require 'Equipment' -local Format = require 'Format' local Game = require 'Game' local ShipDef = require 'ShipDef' local ModelSpinner = require 'PiGui.Modules.ModelSpinner' -local EquipMarket = require 'pigui.libs.equipment-market' -local ItemCard = require 'pigui.libs.item-card' -local EquipType = require 'EquipType' +local EquipCard = require 'pigui.libs.equip-card' local Vector2 = Vector2 local utils = require 'utils' +local Module = require 'pigui.libs.module' +local Outfitter = require 'pigui.libs.equipment-outfitter' + local Lang = require 'Lang' local l = Lang.GetResource("ui-core") local le = Lang.GetResource("equipment-core") @@ -26,196 +25,102 @@ local iconSize = Vector2(pionillium.body.size) local equipmentInfoTab ----@class EquipmentWidget ----@field meta table -local EquipmentWidget = utils.inherits(nil, "EquipmentWidget") - --- Slot information for the empty slot + example slot data layout -local emptySlot = { - -- type = "Slot", name = "[EMPTY]" - icon = icons.autopilot_dock, --size = "S1", - { icons.hull, ui.Format.Mass(0, 1), le.EQUIPMENT_WEIGHT }, - -- { icons.repairs, "100%", le.EQUIPMENT_INTEGRITY } - -- { icons.ecm_advanced, "0 KW", "Max Power Draw" }, - -- { icons.temperature, "0 KW", "Operating Heat" }, -} +---@class UI.EquipmentWidget : UI.Module +---@field New fun(id): self +local EquipmentWidget = utils.class("UI.EquipmentWidget", Module) -- Equipment item grouping by underlying slot type -- TODO: significant refactor to slot system to reduce highly-specialized slots local sections = { - { name = le.PROPULSION, slot = "engine", showCapacity = true }, - { name = le.WEAPONS, slot = "laser_front", showCapacity = true }, - { name = le.MISSILES, slot = "missile", showCapacity = true }, - { name = le.SCOOPS, slot = "scoop", showCapacity = true }, - { name = le.SENSORS, showCapacity = true, slots = { - "sensor", "radar", "target_scanner", "hypercloud" - } }, - { name = le.SHIELDS, slot = "shield" }, - { name = le.UTILITY, slots = { - "cabin", "ecm", "hull_autorepair", - "energy_booster", "atmo_shield", - "laser_cooler", "cargo_life_support", - "autopilot", "trade_computer", "thruster" - } } + { name = le.PROPULSION, types = { "engine", "thruster", "hyperdrive" }, showCapacity = true }, + { name = le.WEAPONS, type = "weapon", showCapacity = true }, + { name = le.MISSILES, types = { "pylon", "missile" }, showCapacity = true }, + { name = le.SHIELDS, type = "shield", showCapacity = true }, + { name = le.SENSORS, type = "sensor", showCapacity = true, }, + { name = le.COMPUTER_MODULES, type = "computer", showCapacity = true, }, + { name = le.HULL_MOUNTS, types = { "hull", "utility" }, showCapacity = true }, } -- -- ============================================================================= --- Equipment Item Card +-- Equipment Market Widget -- ============================================================================= -- ----@class UI.EquipCard : UI.ItemCard -local EquipCard = utils.inherits(ItemCard, "UI.EquipCard") +---@param equip EquipType +function EquipmentWidget:onBuyItem(equip) + local player = Game.player -EquipCard.highlightBar = true -EquipCard.detailFields = 4 - 0.3 + if self.selectedEquip then + self:onSellItem(self.selectedEquip) + end -function EquipCard:drawTooltip(data, isSelected) - if data.equip then - local desc = data.equip:GetDescription() - if desc and #desc > 0 then - ui.withStyleVars({ WindowPadding = ui.theme.styles.WindowPadding }, function() - ui.setTooltip(desc) - end) - end + if equip.SpecializeForShip then + equip = equip:SpecializeForShip(self.ship) + else + equip = equip:Instance() end + + player:AddMoney(-self.market:getBuyPrice(equip)) + self.station:AddEquipmentStock(equip:GetPrototype(), -1) + + assert(self.ship:GetComponent("EquipSet"):Install(equip, self.selectedSlot)) + + self.selectedEquip = equip end -function EquipCard:drawTitle(data, textWidth, isSelected) - local pos = ui.getCursorPos() +---@param equip EquipType +function EquipmentWidget:onSellItem(equip) + assert(self.selectedEquip) + assert(self.selectedEquip == equip) - -- Draw the slot type - if data.type then - ui.text(data.type .. ":") - ui.sameLine() - end + local player = Game.player - -- Draw the name of what's in the slot - local fontColor = data.name and colors.white or colors.equipScreenBgText + player:AddMoney(self.market:getSellPrice(equip)) + self.station:AddEquipmentStock(equip:GetPrototype(), 1) - local name = data.name or ("[" .. le.EMPTY_SLOT .. "]") - ui.withStyleColors({ Text = fontColor }, function() ui.text(name) end) + assert(self.ship:GetComponent("EquipSet"):Remove(equip)) - -- Draw the size of the slot - if data.size then - ui.setCursorPos(pos + Vector2(textWidth - ui.calcTextSize(data.size).x --[[ - self.lineSpacing.x ]], 0)) - ui.withStyleColors({ Text = colors.equipScreenBgText }, function() - ui.text(data.size) - end) - end + self.selectedEquip = nil end -- -- ============================================================================= --- Equipment Market Widget +-- Ship Equipment Widget Display -- ============================================================================= -- --- Copied from 05-equipmentMarket.lua -local hasTech = function (station, e) - local equip_tech_level = e.tech_level or 1 -- default to 1 +function EquipmentWidget:Constructor(id) + Module.Constructor(self) - if type(equip_tech_level) == "string" then - if equip_tech_level == "MILITARY" then - equip_tech_level = 11 - else - error("Unknown tech level:\t"..equip_tech_level) - end - end + self.market = Outfitter.New() - assert(type(equip_tech_level) == "number") - return station.techLevel >= equip_tech_level -end + self.market:hookMessage("onBuyItem", function(_, item) + self:onBuyItem(item) -local function makeEquipmentMarket() -return EquipMarket.New("EquipmentMarket", l.AVAILABLE_FOR_PURCHASE, { - itemTypes = { Equipment.misc, Equipment.laser, Equipment.hyperspace }, - columnCount = 5, - initTable = function(self) - ui.setColumnWidth(0, self.style.size.x / 2.5) - ui.setColumnWidth(3, ui.calcTextSize(l.MASS).x + self.style.itemSpacing.x + self.style.itemPadding.x) - ui.setColumnWidth(4, ui.calcTextSize(l.IN_STOCK).x + self.style.itemSpacing.x + self.style.itemPadding.x) - end, - renderHeaderRow = function(self) - ui.text(l.NAME_OBJECT) - ui.nextColumn() - ui.text(l.BUY) - ui.nextColumn() - ui.text(l.SELL) - ui.nextColumn() - ui.text(l.MASS) - ui.nextColumn() - ui.text(l.IN_STOCK) - ui.nextColumn() - end, - renderItem = function(self, item) - ui.withTooltip(item:GetDescription(), function() - ui.text(item:GetName()) - ui.nextColumn() - ui.text(Format.Money(self.funcs.getBuyPrice(self, item))) - ui.nextColumn() - ui.text(Format.Money(self.funcs.getSellPrice(self, item))) - ui.nextColumn() - ui.text(item.capabilities.mass.."t") - ui.nextColumn() - ui.text(self.funcs.getStock(self, item)) - ui.nextColumn() - end) - end, - canDisplayItem = function (s, e) - local filterSlot = not s.owner.selectedEquip - if s.owner.selectedEquip then - for k, v in pairs(s.owner.selectedEquipSlots) do - filterSlot = filterSlot or utils.contains(e.slots, v) - end - end - return e.purchasable and hasTech(s.owner.station, e) and filterSlot - end, - onMouseOverItem = function(s, e) - local tooltip = e:GetDescription() - if string.len(tooltip) > 0 then - ui.withFont(pionillium.body, function() ui.setTooltip(tooltip) end) - end - end, - onClickItem = function(s,e) - s.funcs.buy(s, e) - s:refresh() - end, - -- If we have an equipment item selected, we're replacing it. - onClickBuy = function(s,e) - local selected = s.owner.selectedEquip - if selected[1] then - s.owner.ship:RemoveEquip(selected[1], 1, selected.slot) - s.owner.ship:AddMoney(s.funcs.getSellPrice(s, selected[1])) - s.owner.ship:GetDockedWith():AddEquipmentStock(selected[1], 1) - end + self.market.replaceEquip = self.selectedEquip + self.market:refresh() + end) - return true - end, - -- If the purchase failed, undo the sale of the item previously in the slot - onBuyFailed = function(s, e, reason) - local selected = s.owner.selectedEquip - if selected[1] then - s.owner.ship:AddEquip(selected[1], 1, selected.slot) - s.owner.ship:AddMoney(-s.funcs.getSellPrice(s, selected[1])) - s.owner.ship:GetDockedWith():AddEquipmentStock(e, -1) - end + self.market:hookMessage("onSellItem", function(_, item) + self:onSellItem(item) - s.defaultFuncs.onBuyFailed(s, e, reason) - end -}) -end + self.market.replaceEquip = self.selectedEquip + self.market:refresh() + end) --- --- ============================================================================= --- Ship Equipment Widget Display --- ============================================================================= --- + self.market:hookMessage("onClose", function(_, item) + print("onClose") + self:clearSelection() -function EquipmentWidget.New(id) - ---@class EquipmentWidget - local self = setmetatable({}, EquipmentWidget.meta) + self.market.replaceEquip = nil + self.market.filterSlot = nil + end) + + self.market:hookMessage("drawShipSpinner", function(_, size) + self.modelSpinner:setSize(size) + self.modelSpinner:draw() + end) ---@type Ship self.ship = nil @@ -224,60 +129,86 @@ function EquipmentWidget.New(id) self.showShipNameEdit = false self.showEmptySlots = true + ---@type EquipType? self.selectedEquip = nil - self.selectedEquipSlots = nil + ---@type ShipDef.Slot? + self.selectedSlot = nil + self.selectionActive = false + self.modelSpinner = ModelSpinner() self.showHoveredEquipLocation = false self.lastHoveredEquipLine = Vector2(0, 0) self.lastHoveredEquipTag = nil - self.equipmentMarket = makeEquipmentMarket() - self.equipmentMarket.owner = self self.tabs = { equipmentInfoTab } self.activeTab = 1 self.id = id or "EquipmentWidget" - return self end ---[[ -self.selectedEquip format: -{ - [1] = equipment table or nil - [2] = equipment index in slot or nil - slot = name of slot currently selected - mass = mass of current equipment - name = name of current equipment -} ---]] +function EquipmentWidget:clearSelection() + self.selectedEquip = nil + self.selectedSlot = nil + self.selectionActive = false +end + +function EquipmentWidget:onSelectEquip(equipDetail) + if self.selectionActive and not self.selectedSlot and self.selectedEquip == equipDetail then + self:clearSelection() + return + end -function EquipmentWidget:onEquipmentClicked(equipDetail, slots) if self.station then self.selectedEquip = equipDetail - self.selectedEquipSlots = slots - self.equipmentMarket:refresh() - self.equipmentMarket.scrollReset = true + self.selectedSlot = nil + self.selectionActive = true + + self.market.filterSlot = nil + self.market.replaceEquip = self.selectedEquip + self.market:refresh() end end -function EquipmentWidget:onEmptySlotClicked(slots) +function EquipmentWidget:onSelectSlot(slot) + if self.selectionActive and self.selectedSlot == slot then + self:clearSelection() + return + end + if self.station then - self.selectedEquip = { nil, nil } - self.selectedEquipSlots = slots - self.equipmentMarket:refresh() - self.equipmentMarket.scrollReset = true + self.selectedSlot = slot + self.selectedEquip = self.ship:GetComponent("EquipSet"):GetItemInSlot(slot) + self.selectionActive = true + + self.market.filterSlot = self.selectedSlot + self.market.replaceEquip = self.selectedEquip + self.market:refresh() end end +function EquipmentWidget:onSetShipName(newName) + self.ship:SetShipName(newName) +end + equipmentInfoTab = { name = l.EQUIPMENT, - ---@param self EquipmentWidget + ---@param self UI.EquipmentWidget draw = function(self) ui.withFont(pionillium.body, function() - for i, v in ipairs(sections) do - self:drawEquipSection(v) + local equipSet = self.ship:GetComponent("EquipSet") + + for i, section in ipairs(sections) do + local slots = {} + + for _, type in ipairs(section.types or { section.type }) do + table.append(slots, equipSet:GetAllSlotsOfType(type, section.hardpoint)) + end + + self:drawSlotSection(section.name, slots) end + + self:drawEquipSection(le.MISC, equipSet:GetInstalledNonSlot()) end) end } @@ -288,66 +219,10 @@ equipmentInfoTab = { -- ============================================================================= -- --- Generate all UI data appropriate to the passed equipment item -local function makeEquipmentData(equip) - local out = { - icon = equip.icon_name and icons[equip.icon_name] or icons.systems_management, - name = equip:GetName(), - equip = equip - } - - if equip:Class() == EquipType.LaserType then - table.insert(out, { - icons.comms, -- PLACEHOLDER - string.format("%d RPM", 60 / equip.laser_stats.rechargeTime), - le.SHOTS_PER_MINUTE - }) - table.insert(out, { - icons.ecm_advanced, - string.format("%.1f KW", equip.laser_stats.damage), - le.DAMAGE_PER_SHOT - }) - elseif equip:Class() == EquipType.BodyScannerType then - table.insert(out, { - icons.scanner, - string.format("%s px", ui.Format.Number(equip.stats.resolution, 0)), - le.SENSOR_RESOLUTION - }) - - table.insert(out, { - icons.altitude, - ui.Format.Distance(equip.stats.minAltitude), - le.SENSOR_MIN_ALTITUDE - }) - -- elseif equip:Class() == EquipType.HyperdriveType then - -- elseif equip:Class() == EquipType.SensorType then - -- elseif utils.contains(equip.slots, "missile") then - -- elseif utils.contains(equip.slots, "shield") then - -- elseif utils.contains(equip.slots, "cabin") then - -- elseif utils.contains(equip.slots, "radar") then - -- elseif utils.contains(equip.slots, "autopilot") then - end -- TODO: more data for different equip types - - local equipHealth = 1 - local equipMass = equip.capabilities.mass * 1000 - table.insert(out, { icons.hull, ui.Format.Mass(equipMass, 1), le.EQUIPMENT_WEIGHT }) - table.insert(out, { icons.repairs, string.format("%d%%", equipHealth * 100), le.EQUIPMENT_INTEGRITY }) - - return out -end - --- Draw all visuals related to an individual equipment item. --- This includes the background, highlight, icon, [slot type], --- name, [size], and up to 4 sub-stats directly on the icon card --- --- DrawEquipmentItem* functions take a "data" argument table with the form: --- name - translated name to display on the slot --- equip - equipment object being drawn (or none for an empty slot) --- type* - translated "slot type" name to display --- size* - (short) string to be displayed in the "equipment size" field --- [...] - up to 4 { icon, value, tooltip } data items for the stats line +-- Wrapper for EquipCard which handles updating the "last hovered" information function EquipmentWidget:drawEquipmentItem(data, isSelected) - ui.addCursorPos(Vector2(lineSpacing.x * 2, 0)) + -- Apply tree indent from left + ui.addCursorPos(Vector2(lineSpacing.x, 0)) local pos = ui.getCursorScreenPos() local isClicked, isHovered, size = EquipCard:draw(data, isSelected) @@ -360,29 +235,37 @@ function EquipmentWidget:drawEquipmentItem(data, isSelected) return isClicked, isHovered end --- Override this to draw any detailed tooltips -function EquipmentWidget:drawEquipmentItemTooltip(data, isSelected) - if data.equip then - local desc = data.equip:GetDescription() - if desc and #desc > 0 then ui.setTooltip(desc) end - end -end - -- -- ============================================================================= -- Equipment Section Drawing Functions -- ============================================================================= -- +-- Return the translated name of a slot, falling back to a generic name for the +-- slot type if none is specified. +---@param slot ShipDef.Slot +function EquipmentWidget:getSlotName(slot) + if slot.i18n_key then + return Lang.GetResource(slot.i18n_res)[slot.i18n_key] + end + + local base_type = slot.type:match("(%w+)%.?") + local i18n_key = (slot.hardpoint and "HARDPOINT_" or "SLOT_") .. base_type:upper() + return le[i18n_key] +end + -- Show an inline detail on a section header line with optional tooltip ---@param cellEnd Vector2 local function drawHeaderDetail(cellEnd, text, icon, tooltip, textOffsetY) local textStart = cellEnd - Vector2(ui.calcTextSize(text).x + lineSpacing.x, 0) local iconPos = textStart - Vector2(iconSize.x + lineSpacing.x / 2, 0) + ui.setCursorPos(iconPos) ui.icon(icon, iconSize, colors.white) + ui.setCursorPos(textStart + Vector2(0, textOffsetY or 0)) ui.text(text) + local wp = ui.getWindowPos() if tooltip and ui.isMouseHoveringRect(wp + iconPos, wp + cellEnd + Vector2(0, iconSize.y)) then ui.setTooltip(tooltip) @@ -390,21 +273,24 @@ local function drawHeaderDetail(cellEnd, text, icon, tooltip, textOffsetY) end -- Draw an equipment section header -function EquipmentWidget:drawSectionHeader(data, numItems, maxSlots, totalWeight) - - -- This function makes heavy use of draw cursor maniupulation to achieve +function EquipmentWidget:drawSectionHeader(name, numItems, totalWeight, maxSlots) + -- This function makes heavy use of draw cursor manipulation to achieve -- complicated layout goals ---@type boolean, Vector2, Vector2 local sectionOpen, contentsPos, cursorPos + local cellWidth = ui.getContentRegion().x / 5 local textOffsetY = (pionillium.heading.size - pionillium.body.size) / 2 ui.withFont(pionillium.heading, function() ui.withStyleVars({FramePadding = lineSpacing}, function() - sectionOpen = ui.treeNode(data.name, { "FramePadding", (self.showEmptySlots or numItems > 0) and "DefaultOpen" or nil }) + + local nodeFlags = { "FramePadding", (self.showEmptySlots or numItems > 0) and "DefaultOpen" or nil } + sectionOpen = ui.treeNode(name, nodeFlags) contentsPos = ui.getCursorPos() ui.sameLine(0, 0) cursorPos = ui.getCursorPos() + Vector2(0, lineSpacing.y) + end) end) @@ -414,7 +300,7 @@ function EquipmentWidget:drawSectionHeader(data, numItems, maxSlots, totalWeight drawHeaderDetail(cellEnd, weightStr, icons.hull, le.TOTAL_MODULE_WEIGHT, textOffsetY) -- For sections with definite slot counts, show the number of used and total slots - if data.showCapacity then + if maxSlots then local capacityStr = maxSlots > 0 and string.format("%d/%d", numItems, maxSlots) or tostring(numItems) cellEnd = cellEnd - Vector2(cellWidth, 0) drawHeaderDetail(cellEnd, capacityStr, icons.antinormal, le.TOTAL_MODULE_CAPACITY, textOffsetY) @@ -426,63 +312,64 @@ function EquipmentWidget:drawSectionHeader(data, numItems, maxSlots, totalWeight end -- Calculate information about an equipment category for displaying ship internal equipment +---@param slots ShipDef.Slot[] function EquipmentWidget:calcEquipSectionInfo(slots) - local equipment = {} - local maxSlots = 0 + local EquipSet = self.ship:GetComponent("EquipSet") + + local names = {} + local occupied = 0 local totalWeight = 0 - -- Gather all equipment items in the specified slot(s) for this section - -- TODO: this can be refactored once the equipment system has been overhauled - - for _, name in ipairs(slots) do - local slot = self.ship:GetEquip(name) - maxSlots = maxSlots + self.ship:GetEquipSlotCapacity(name) - - for i, equip in pairs(slot) do - table.insert(equipment, { - equip, i, - slot = name, - mass = equip.capabilities.mass, - name = equip:GetName() - }) - totalWeight = totalWeight + (equip.capabilities.mass or 0) + for _, slot in ipairs(slots) do + names[slot] = self:getSlotName(slot) + + local item = EquipSet:GetItemInSlot(slot) + if item then + occupied = occupied + 1 + totalWeight = totalWeight + item.mass end end - return equipment, maxSlots, totalWeight + return names, occupied, totalWeight end -- Draw an equipment section and all contained equipment items -function EquipmentWidget:drawEquipSection(data) - - local slots = data.slots or { data.slot } - local equipment, maxSlots, weight = self:calcEquipSectionInfo(slots) +---@param equipment EquipType[] +function EquipmentWidget:drawEquipSection(name, equipment) + + -- local slots = data.slots or { data.slot } + -- local equipment, maxSlots, weight = self:calcEquipSectionInfo(slots) + local weight = 0 + for _, equip in ipairs(equipment) do + weight = weight + equip.mass + end - local sectionOpen = self:drawSectionHeader(data, #equipment, maxSlots, weight) + local sectionOpen = self:drawSectionHeader(name, #equipment, weight) if sectionOpen then -- heaviest items to the top, then stably sort based on name table.sort(equipment, function(a, b) - local mass = (a.mass or 0) - (b.mass or 0) - return mass > 0 or (mass == 0 and a.name < b.name) + local mass = a.mass - b.mass + return mass > 0 or (mass == 0 and a:GetName() < b:GetName()) end) -- Draw each equipment item in this section - for i, v in ipairs(equipment) do - local equipData = makeEquipmentData(v[1]) - local isSelected = self.selectedEquip and (self.selectedEquip[1] == v[1] and self.selectedEquip[2] == v[2]) + for i, equip in ipairs(equipment) do + local equipData = EquipCard.getDataForEquip(equip) + local isSelected = self.selectedEquip == equip if self:drawEquipmentItem(equipData, isSelected) then - self:onEquipmentClicked(v, slots) + self:message("onSelectEquip", equip) end end -- If we have more slots available in this section, show an empty slot - if maxSlots > 0 and self.showEmptySlots and #equipment < maxSlots then - local isSelected = self.selectedEquip and (not self.selectedEquip[1] and self.selectedEquipSlots[1] == slots[1]) + if self.showEmptySlots then + local equipData = EquipCard.getDataForEquip(nil) + local isSelected = self.selectionActive and not self.selectedSlot and not self.selectedEquip - if self:drawEquipmentItem(emptySlot, isSelected) then - self:onEmptySlotClicked(slots) + if self:drawEquipmentItem(equipData, isSelected) then + self:message("onSelectEquip", nil) end end @@ -491,6 +378,45 @@ function EquipmentWidget:drawEquipSection(data) end +-- Draw a list of equipment slots and contained items +---@param slots ShipDef.Slot[] +function EquipmentWidget:drawSlotSection(name, slots) + local names, numFull, totalMass = self:calcEquipSectionInfo(slots) + + local sectionOpen = self:drawSectionHeader(name, numFull, totalMass, #slots) + if not sectionOpen then + return + end + + -- Sort by slot name first, then internal ID for tiebreaker + table.sort(slots, function(a, b) + return names[a] < names[b] or (names[a] == names[b] and a.id < b.id) + end) + + local equipSet = self.ship:GetComponent("EquipSet") + + for _, slot in ipairs(slots) do + local isSelected = self.selectedSlot == slot + local equip = equipSet:GetItemInSlot(slot) + + if equip or self.showEmptySlots then + + local slotData = EquipCard.getDataForEquip(equip) + + slotData.size = slotData.size or ("S" .. slot.size) + slotData.type = names[slot] + slotData.count = slot.count + + if self:drawEquipmentItem(slotData, isSelected) then + self:message("onSelectSlot", slot) + end + + end + end + + ui.treePop() +end + -- -- ============================================================================= -- Ship Spinner / Station Market Display @@ -498,23 +424,30 @@ end -- function EquipmentWidget:drawShipSpinner() - if not self.modelSpinner then self:refresh() end - local shipDef = ShipDef[self.ship.shipId] + ui.group(function () + ui.withFont(ui.fonts.orbiteer.large, function() + if self.showShipNameEdit then + ui.alignTextToFramePadding() ui.text(shipDef.name) ui.sameLine() + ui.pushItemWidth(-1.0) local entry, apply = ui.inputText("##ShipNameEntry", self.ship.shipName, ui.InputTextFlags {"EnterReturnsTrue"}) ui.popItemWidth() - if (apply) then self.ship:SetShipName(entry) end + if (apply) then + self:message("onSetShipName", entry) + end + else ui.text(shipDef.name) end + end) local startPos = ui.getCursorScreenPos() @@ -525,7 +458,9 @@ function EquipmentWidget:drawShipSpinner() -- WIP "physicalized component" display - draw a line between the equipment item -- and the location in the ship where it is mounted local lineStartPos = self.lastHoveredEquipLine - if self.showHoveredEquipLocation then + + -- TODO: disabled until all ships have tag markup for internal components + if false and self.showHoveredEquipLocation then local tagPos = startPos + self.modelSpinner:getTagPos(self.lastHoveredEquipTag) local lineTurnPos = lineStartPos + Vector2(40, 0) local dir = (tagPos - lineTurnPos):normalized() @@ -533,35 +468,11 @@ function EquipmentWidget:drawShipSpinner() ui.addLine(lineTurnPos, tagPos - dir * 4, colors.white, 2) ui.addCircle(tagPos, 4, colors.white, 16, 2) end - end) -end - -function EquipmentWidget:drawMarketButtons() - if ui.button(l.GO_BACK, Vector2(0, 0)) then - self.selectedEquip = nil - return - end - ui.sameLine() - if self.selectedEquip[1] then - local price = self.equipmentMarket.funcs.getSellPrice(self.equipmentMarket, self.selectedEquip[1]) - - if ui.button(l.SELL_EQUIPPED, Vector2(0, 0)) then - self.ship:RemoveEquip(self.selectedEquip[1], 1, self.selectedEquip.slot) - self.ship:AddMoney(price) - self.ship:GetDockedWith():AddEquipmentStock(self.selectedEquip[1], 1) - self.selectedEquip = { nil, nil } - return - end - ui.sameLine() - ui.text(l.PRICE .. ": " .. Format.Money(price)) - end + end) end -function EquipmentWidget:draw() - -- reset hovered equipment state - self.showHoveredEquipLocation = false - +function EquipmentWidget:render() ui.withFont(pionillium.body, function() ui.child("ShipInfo", Vector2(ui.getContentRegion().x * 1 / 3, 0), { "NoSavedSettings" }, function() if #self.tabs > 1 then @@ -574,23 +485,12 @@ function EquipmentWidget:draw() ui.sameLine(0, lineSpacing.x * 2) ui.child("##container", function() - if self.tabs[self.activeTab] == equipmentInfoTab and self.station and self.selectedEquip then + if self.tabs[self.activeTab] == equipmentInfoTab and self.station and self.selectionActive then - local _pos = ui.getCursorPos() - local marketSize = ui.getContentRegion() - Vector2(0, ui.getButtonHeight(pionillium.heading)) - - if self.selectedEquip then - self.equipmentMarket.title = self.selectedEquip[1] and l.REPLACE_EQUIPMENT_WITH or l.AVAILABLE_FOR_PURCHASE - self.equipmentMarket.style.size = marketSize - self.equipmentMarket:render() + if self.selectionActive then + self.market:render() end - ui.withFont(pionillium.heading, function() - ui.setCursorPos(_pos + Vector2(0, marketSize.y)) - self:drawMarketButtons() - ui.sameLine() - end) - else self:drawShipSpinner() end @@ -598,17 +498,39 @@ function EquipmentWidget:draw() end) end +function EquipmentWidget:draw() + -- reset hovered equipment state + self.showHoveredEquipLocation = false + + self:update() + self.market:update() + + self:render() +end + function EquipmentWidget:refresh() self.selectedEquip = nil - self.selectedEquipSlots = nil + self.selectedSlot = nil + self.selectionActive = false local shipDef = ShipDef[self.ship.shipId] self.modelSpinner:setModel(shipDef.modelName, self.ship:GetSkin(), self.ship.model.pattern) self.modelSpinner.spinning = false + + self.market.ship = self.ship + self.market.station = self.station + + self.market.filterSlot = nil + self.market.replaceEquip = nil + + self.market:refresh() end function EquipmentWidget:debugReload() package.reimport('pigui.libs.item-card') + package.reimport('pigui.libs.equip-card') + package.reimport('pigui.libs.equipment-outfitter') + package.reimport('modules.Equipment.Stats') package.reimport() end From 8330e35c13d773db4ab6f6224ffa07aab939dc91 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 3 Feb 2024 02:33:40 -0500 Subject: [PATCH 025/119] hack: new equipment API in new game menu new-game: handle installing into multi-count item slots new-game-window: remove old equipment API - Temporarily breaks other spawns, the new game window needs to be migrated temp: new-game-window comment out equipment API --- data/pigui/modules/new-game-window/class.lua | 75 +++++++++++++----- data/pigui/modules/new-game-window/ship.lua | 78 +++++++++++-------- .../pigui/modules/new-game-window/summary.lua | 2 +- 3 files changed, 99 insertions(+), 56 deletions(-) diff --git a/data/pigui/modules/new-game-window/class.lua b/data/pigui/modules/new-game-window/class.lua index 565fb60180..d9a15fb343 100644 --- a/data/pigui/modules/new-game-window/class.lua +++ b/data/pigui/modules/new-game-window/class.lua @@ -14,9 +14,6 @@ local ModelSkin = require 'SceneGraph.ModelSkin' local ShipDef = require "ShipDef" local SystemPath = require 'SystemPath' -local misc = Equipment.misc -local laser = Equipment.laser - local Defs = require 'pigui.modules.new-game-window.defs' local Layout = require 'pigui.modules.new-game-window.layout' local Recovery = require 'pigui.modules.new-game-window.recovery' @@ -27,6 +24,16 @@ local Game = require 'Game' local profileCombo = { items = {}, selected = 0 } +local equipment2 = { + computer_1 = "misc.autopilot", + laser_front = "laser.pulsecannon_1mw", + shield = "shield.basic_s1", + sensor = "sensor.radar", + hull_mod = "hull.atmospheric_shielding", + hyperdrive = "hyperspace.hyperdrive_2", + thruster = "misc.thrusters_default" +} + StartVariants.register({ name = lui.START_AT_MARS, desc = lui.START_AT_MARS_DESC, @@ -36,10 +43,10 @@ StartVariants.register({ money = 600, hyperdrive = true, equipment = { - { laser.pulsecannon_1mw, 1 }, - { misc.atmospheric_shielding, 1 }, - { misc.autopilot, 1 }, - { misc.radar, 1 } + -- { laser.pulsecannon_1mw, 1 }, + -- { misc.atmospheric_shielding, 1 }, + -- { misc.autopilot, 1 }, + -- { misc.radar, 1 } }, cargo = { { Commodities.hydrogen, 2 } @@ -57,10 +64,10 @@ StartVariants.register({ money = 400, hyperdrive = true, equipment = { - { laser.pulsecannon_1mw, 1 }, - { misc.atmospheric_shielding, 1 }, - { misc.autopilot, 1 }, - { misc.radar, 1 } + -- { laser.pulsecannon_1mw, 1 }, + -- { misc.atmospheric_shielding, 1 }, + -- { misc.autopilot, 1 }, + -- { misc.radar, 1 } }, cargo = { { Commodities.hydrogen, 2 } @@ -78,9 +85,9 @@ StartVariants.register({ money = 100, hyperdrive = false, equipment = { - {misc.atmospheric_shielding,1}, - {misc.autopilot,1}, - {misc.radar,1} + -- {misc.atmospheric_shielding,1}, + -- {misc.autopilot,1}, + -- {misc.radar,1} }, cargo = { { Commodities.hydrogen, 2 } @@ -153,16 +160,42 @@ local function startGame(gameParams) laser_rear = 'laser', laser_front = 'laser' } - for _, slot in pairs({ 'engine', 'laser_rear', 'laser_front' }) do - local eqSection = eqSections[slot] - local eqEntry = gameParams.ship.equipment[slot] - if eqEntry then - player:AddEquip(Equipment[eqSection][eqEntry], 1, slot) + + if not equipment2 then + + -- TODO: old equipment API no longer supported + + else + + local equipSet = player:GetComponent("EquipSet") + player:UpdateEquipStats() + + for _, item in ipairs(equipment2) do + local proto = Equipment.Get(item) + if not equipSet:Install(proto()) then + print("Couldn't install equipment item {} in misc. cargo space" % { proto:GetName() }) + end + end + + for slot, item in pairs(equipment2) do + local proto = Equipment.Get(item) + -- print("Installing equipment {} (proto: {}) into slot {}" % { item, proto, slot }) + if type(slot) == "string" then + local slotHandle = equipSet:GetSlotHandle(slot) + assert(slotHandle) + + local inst = proto:Instance() + + if slotHandle.count then + inst:SetCount(slotHandle.count) + end + + if not equipSet:Install(inst, slotHandle) then + print("Couldn't install equipment item {} into slot {}" % { inst:GetName(), slot }) + end end end - for _,equip in pairs(gameParams.ship.equipment.misc) do - player:AddEquip(Equipment.misc[equip.id], equip.amount) end ---@type CargoManager diff --git a/data/pigui/modules/new-game-window/ship.lua b/data/pigui/modules/new-game-window/ship.lua index a3a8e3f6e0..cffb8a3084 100644 --- a/data/pigui/modules/new-game-window/ship.lua +++ b/data/pigui/modules/new-game-window/ship.lua @@ -454,11 +454,12 @@ ShipEquip.lists = { } -- fill and sort equipment lists for slot, tbl in pairs(ShipEquip.lists) do - for k, _ in pairs(Equipment[slot]) do - if not hiddenIDs[k] then - table.insert(tbl, k) - end - end + -- FIXME: convert to use new equipment API + -- for k, _ in pairs(Equipment[slot]) do + -- if not hiddenIDs[k] then + -- table.insert(tbl, k) + -- end + -- end end for _, v in pairs(ShipEquip.lists) do @@ -493,30 +494,35 @@ ShipEquip.value = { -- utils local function findEquipmentType(eqTypeID) - for _, eq_list in pairs({ 'misc', 'laser', 'hyperspace' }) do - if Equipment[eq_list][eqTypeID] then - return Equipment[eq_list][eqTypeID] - end - end - assert(false, "Wrong Equipment ID: " .. tostring(eqTypeID)) + -- FIXME: convert to new equipment APIs + -- for _, eq_list in pairs({ 'misc', 'laser', 'hyperspace' }) do + -- if Equipment[eq_list][eqTypeID] then + -- return Equipment[eq_list][eqTypeID] + -- end + -- end + -- assert(false, "Wrong Equipment ID: " .. tostring(eqTypeID)) + return nil end local function findEquipmentPath(eqKey) - for _, eq_list in pairs({ 'misc', 'laser', 'hyperspace' }) do - for id, obj in pairs(Equipment[eq_list]) do - if obj.l10n_key == eqKey then - return eq_list, id - end - end - end - assert(false, "Wrong Equipment ID: " .. tostring(eqKey)) + -- FIXME: convert to new equipment APIs + -- for _, eq_list in pairs({ 'misc', 'laser', 'hyperspace' }) do + -- for id, obj in pairs(Equipment[eq_list]) do + -- if obj.l10n_key == eqKey then + -- return eq_list, id + -- end + -- end + -- end + -- assert(false, "Wrong Equipment ID: " .. tostring(eqKey)) + return nil end local function hasSlotClass(eqTypeID, slotClass) - local eqType = findEquipmentType(eqTypeID) - for _, slot in pairs(eqType.slots) do - if slotClass[slot] then return true end - end + -- FIXME: convert to new equipment APIs + -- local eqType = findEquipmentType(eqTypeID) + -- for _, slot in pairs(eqType.slots) do + -- if slotClass[slot] then return true end + -- end return false end @@ -534,17 +540,20 @@ end function ShipEquip:getHyperDriveClass() if not self.value.engine then return 0 end - local drive = Equipment.hyperspace[self.value.engine] - return drive.capabilities.hyperclass + -- FIXME: convert to new equipment APIs + -- local drive = Equipment.hyperspace[self.value.engine] + -- return drive.capabilities.hyperclass + return 0 end function ShipEquip:getThrusterUpgradeLevel() - for _, eq_entry in pairs(self.value.misc) do - local eq = Equipment.misc[eq_entry.id] - if eq.capabilities.thruster_power then - return eq.capabilities.thruster_power - end - end + -- FIXME: convert to new equipment APIs + -- for _, eq_entry in pairs(self.value.misc) do + -- local eq = Equipment.misc[eq_entry.id] + -- if eq.capabilities.thruster_power then + -- return eq.capabilities.thruster_power + -- end + -- end return 0 end @@ -555,7 +564,8 @@ function ShipEquip:setDefaultHyperdrive() else local driveID = "hyperdrive_" .. drive_class local index = utils.indexOf(self.lists.hyperspace, driveID) - assert(index, "unknown drive ID: " .. tostring(driveID)) + -- FIXME: convert to new equipment API + --assert(index, "unknown drive ID: " .. tostring(driveID)) self.value.engine = driveID end self:update() @@ -650,7 +660,7 @@ function ShipEquip:update() local slot = section_id addToTable(self.usedSlots, slot, 1) self:addToSummary(eqType, 1) - self.mass = self.mass + eqType.capabilities.mass + --self.mass = self.mass + eqType.capabilities.mass end end @@ -659,7 +669,7 @@ function ShipEquip:update() local slot = eqType.slots[1] -- misc always has one slot addToTable(self.usedSlots, slot, entry.amount) self:addToSummary(eqType, entry.amount) - self.mass = self.mass + eqType.capabilities.mass * entry.amount + --self.mass = self.mass + eqType.capabilities.mass * entry.amount end end diff --git a/data/pigui/modules/new-game-window/summary.lua b/data/pigui/modules/new-game-window/summary.lua index 440618041e..618baad076 100644 --- a/data/pigui/modules/new-game-window/summary.lua +++ b/data/pigui/modules/new-game-window/summary.lua @@ -97,7 +97,7 @@ function Summary:draw() for _, eq in ipairs(Ship.Equip.summaryList) do -- eq: { obj, count } - if not eq.obj.capabilities.hyperclass then + if eq.obj and not eq.obj.capabilities.hyperclass then local count = eq.count > 1 and " x " .. tostring(eq.count) or "" ui.text(" - " .. leq[eq.obj.l10n_key] .. count) end From 7e34e2d0c688d5a86185c54712b897eba0b4ac06 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 3 Feb 2024 02:33:00 -0500 Subject: [PATCH 026/119] test: add ship configurations for bowfin + coronatrix Update coronatrix test layout - Set capacity to computed volume - Two S1 shield mounts instead of 1x S2 --- data/pigui/modules/new-game-window/class.lua | 17 +++--- data/ships/bowfin.json | 63 +++++++++++++++++++- data/ships/coronatrix.json | 61 ++++++++++++++++++- 3 files changed, 131 insertions(+), 10 deletions(-) diff --git a/data/pigui/modules/new-game-window/class.lua b/data/pigui/modules/new-game-window/class.lua index d9a15fb343..4b201df152 100644 --- a/data/pigui/modules/new-game-window/class.lua +++ b/data/pigui/modules/new-game-window/class.lua @@ -25,13 +25,16 @@ local Game = require 'Game' local profileCombo = { items = {}, selected = 0 } local equipment2 = { - computer_1 = "misc.autopilot", - laser_front = "laser.pulsecannon_1mw", - shield = "shield.basic_s1", - sensor = "sensor.radar", - hull_mod = "hull.atmospheric_shielding", - hyperdrive = "hyperspace.hyperdrive_2", - thruster = "misc.thrusters_default" + computer_1 = "misc.autopilot", + laser_front = "laser.pulsecannon_1mw", + shield_1 = "shield.basic_s1", + shield_2 = "shield.basic_s1", + sensor = "sensor.radar", + hull_mod = "hull.atmospheric_shielding", + hyperdrive = "hyperspace.hyperdrive_2", + thruster = "misc.thrusters_default", + missile_bay_1 = "missile_rack.opli_internal_s2", + missile_bay_2 = "missile_rack.opli_internal_s2", } StartVariants.register({ diff --git a/data/ships/bowfin.json b/data/ships/bowfin.json index ffe2cd5b0a..d3b22e2810 100644 --- a/data/ships/bowfin.json +++ b/data/ships/bowfin.json @@ -10,7 +10,8 @@ "price": 34430, "hull_mass": 25, "atmospheric_pressure_limit": 6.5, - "capacity": 18, + "capacity": 12, + "cargo": 6, "slots": { "engine": 1, "cabin": 0, @@ -20,7 +21,65 @@ "cargo": 6 }, "roles": ["mercenary", "pirate", "courier"], - + "equipment_slots": { + "laser_front": { + "type": "weapon", + "size": 2, + "size_min": 1, + "hardpoint": true, + "tag": "tag_laser_front", + "gimbal": [15, 15], + "default": "laser.pulsecannon_1mw" + }, + "missile_tl": { + "type": "missile", + "size": 2, + "size_min": 1, + "count": 6, + "hardpoint": true, + "tag": "tag_missile_tl", + "default": "missile.guided_s2" + }, + "missile_tr": { + "type": "missile", + "size": 2, + "size_min": 1, + "count": 6, + "hardpoint": true, + "tag": "tag_missile_tr", + "default": "missile.guided_s2" + }, + "missile_bl": { + "type": "missile", + "size": 2, + "size_min": 1, + "count": 6, + "hardpoint": true, + "tag": "tag_missile_bl", + "default": "missile.guided_s2" + }, + "missile_br": { + "type": "missile", + "size": 2, + "size_min": 1, + "count": 6, + "hardpoint": true, + "tag": "tag_missile_br", + "default": "missile.guided_s2" + }, + "shield_gen_1": { + "type": "shield", + "size": 1, + "i18n_key": "SLOT_SHIELD_LEFT", + "default": "shield.basic_s1" + }, + "shield_gen_2": { + "type": "shield", + "size": 1, + "i18n_key": "SLOT_SHIELD_RIGHT", + "default": "shield.basic_s1" + } + }, "effective_exhaust_velocity": 8400000, "thruster_fuel_use": -1.0, "fuel_tank_mass": 26, diff --git a/data/ships/coronatrix.json b/data/ships/coronatrix.json index 8de1a2133f..e27a385af4 100644 --- a/data/ships/coronatrix.json +++ b/data/ships/coronatrix.json @@ -10,7 +10,8 @@ "price": 46614, "hull_mass": 18, "atmospheric_pressure_limit": 3.2, - "capacity": 30, + "capacity": 32, + "cargo": 16, "slots": { "engine": 1, "cabin": 1, @@ -19,6 +20,64 @@ "shield": 3, "cargo": 16 }, + "equipment_slots": { + "laser_front": { + "type": "weapon", + "size": 2, + "size_min": 1, + "i18n_key": "HARDPOINT_LASER_FRONT", + "tag": "tag_laser_front", + "gimbal": [ 5, 5 ], + "hardpoint": true + }, + "missile_bay": { + "type": "missile", + "size": 2, + "size_min": 1, + "count": 5, + "i18n_key": "HARDPOINT_MISSILE_BAY", + "tag": "tag_missile_launch", + "hardpoint": true, + "default": "missile.guided_s2" + }, + "utility_1": { + "type": "utility", + "size": 1, + "tag": "tag_utility_1", + "hardpoint": true + }, + "utility_2": { + "type": "utility", + "size": 1, + "tag": "tag_utility_2", + "hardpoint": true + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 2, + "size_min": 1 + }, + "shield_1": { + "type": "shield", + "size": 1, + "tag": "tag_shield_1" + }, + "shield_2": { + "type": "shield", + "size": 1, + "tag": "tag_shield_2" + }, + "engine": { + "type": "engine", + "size": 2, + "count": 2 + }, + "thruster": { + "type": "thruster", + "size": 1, + "count": 12 + } + }, "roles": ["mercenary", "merchant", "pirate"], "effective_exhaust_velocity": 12600000, "thruster_fuel_use": -1.0, From 3dad275b4438a48d1c62671a758df93920b2bb62 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 3 Feb 2024 02:34:12 -0500 Subject: [PATCH 027/119] Add new translation strings for equipment system --- data/lang/equipment-core/en.json | 80 ++++++++++++++++++++++++++++++++ data/lang/ui-core/en.json | 28 ++++++++++- 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/data/lang/equipment-core/en.json b/data/lang/equipment-core/en.json index 0f7d2bf77f..0bb0a521e0 100644 --- a/data/lang/equipment-core/en.json +++ b/data/lang/equipment-core/en.json @@ -446,5 +446,85 @@ "WEAPONS": { "description": "Category name of weapon-related equipment", "message": "Weapons" + }, + "THRUSTERS_DEFAULT": { + "description": "", + "message": "Default Thrusters" + }, + "PROJECTILE_SPEED": { + "description": "Stat name for weapon projectile speeds", + "message": "Projectile Speed" + }, + "STAT_VOLUME": { + "description": "", + "message": "Volume" + }, + "STAT_WEIGHT": { + "description": "", + "message": "Weight" + }, + "STAT_POWER_DRAW": { + "description": "", + "message": "Power Draw" + }, + "COMPUTER_MODULES": { + "description": "Category name of computer-related equipment", + "message": "Computer Modules" + }, + "HULL_MOUNTS": { + "description": "Category name for hull-mounted equipment", + "message": "Hull Mounts" + }, + "HARDPOINT_LASER_FRONT": { + "description": "Name for the 'Laser Front' equipment hardpoint", + "message": "Laser Front" + }, + "HARDPOINT_MISSILE_BAY": { + "description": "Name for the 'Missile Bay' equipment hardpoint", + "message": "Missile Bay" + }, + "HARDPOINT_UTILITY": { + "description": "", + "message": "Utility" + }, + "SLOT_SENSOR": { + "description": "", + "message": "Sensor" + }, + "SLOT_COMPUTER": { + "description": "", + "message": "Computer" + }, + "SLOT_SHIELD": { + "description": "", + "message": "Shield" + }, + "SLOT_SHIELD_LEFT": { + "description": "", + "message": "Shield L" + }, + "SLOT_SHIELD_RIGHT": { + "description": "", + "message": "Shield R" + }, + "SLOT_HULL": { + "description": "", + "message": "Hull" + }, + "SLOT_HYPERDRIVE": { + "description": "", + "message": "Hyperdrive" + }, + "SLOT_ENGINE": { + "description": "", + "message": "Engines" + }, + "SLOT_THRUSTER": { + "description": "", + "message": "Thrusters" + }, + "MISC_EQUIPMENT": { + "description": "Category header for miscellaneous equipment", + "message": "Miscellaneous" } } diff --git a/data/lang/ui-core/en.json b/data/lang/ui-core/en.json index d2b79fd837..b0748bdc22 100644 --- a/data/lang/ui-core/en.json +++ b/data/lang/ui-core/en.json @@ -65,7 +65,7 @@ }, "AVAILABLE_FOR_PURCHASE": { "description": "Market header when buying items", - "message": "Available For Purchase" + "message": "Available For Purchase:" }, "AVERAGE": { "description": "", @@ -2013,7 +2013,7 @@ }, "REPLACE_EQUIPMENT_WITH": { "description": "Market header when replacing equipped items", - "message": "Replace Equipment With" + "message": "Replace Equipment With:" }, "REPUTATION": { "description": "", @@ -2618,5 +2618,29 @@ "ZOOM": { "description": "Label for a zoom (magnification) control bar.", "message": "Zoom" + }, + "SELECTED": { + "description": "Label indicating the following item is selected", + "message": "Selected" + }, + "INSTALLED": { + "description": "Label indicating something is installed", + "message": "Installed" + }, + "SELL_EQUIP": { + "description": "Button text to sell installed equipment. May used in a singular or plural context.", + "message": "Sell {name}" + }, + "BUY_EQUIP": { + "description": "Button text to buy selected equipment. May used in a singular or plural context.", + "message": "Buy {name}" + }, + "EXPAND": { + "description": "Open / expand a collapsed folder or group", + "message": "Expand" + }, + "COLLAPSE": { + "description": "Close / collapse an open folder or group", + "message": "Collapse" } } From a74db2e48d38183b521d5761b348fa5ace1f952a Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 11 Sep 2024 16:19:54 -0400 Subject: [PATCH 028/119] First pass on ship threat as spawn criteria - Add debug module for ShipBuilder - Split default ship outfitting rules into separate file --- .../modules/MissionUtils/DebugShipBuilder.lua | 65 +++ data/modules/MissionUtils/OutfitRules.lua | 44 ++ data/modules/MissionUtils/ShipBuilder.lua | 403 +++++++++++++++--- 3 files changed, 448 insertions(+), 64 deletions(-) create mode 100644 data/modules/MissionUtils/DebugShipBuilder.lua create mode 100644 data/modules/MissionUtils/OutfitRules.lua diff --git a/data/modules/MissionUtils/DebugShipBuilder.lua b/data/modules/MissionUtils/DebugShipBuilder.lua new file mode 100644 index 0000000000..3a90c28975 --- /dev/null +++ b/data/modules/MissionUtils/DebugShipBuilder.lua @@ -0,0 +1,65 @@ + +local debugView = require 'pigui.views.debug' +local ShipDef = require 'ShipDef' +local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' +local ShipConfig = require 'ShipConfig' + +local ui = require 'pigui' + +debugView.registerTab("ShipBuilder", { + selectedHull = nil, + draw = function(self) + if not ui.beginTabItem("ShipBuilder") then + return + end + + if self.selectedHull then + if ui.button("<") then + self.selectedHull = nil + + ui.endTabItem() + return + end + + ui.sameLine() + ui.text(self.selectedHull) + + ui.spacing() + + if ui.collapsingHeader("Hull Threat") then + local threat = ShipBuilder.GetHullThreat(self.selectedHull) + + for k, v in pairs(threat) do + ui.text(tostring(k) .. ": " .. tostring(v)) + end + end + + if ui.collapsingHeader("Slots") then + local config = ShipConfig[self.selectedHull] + + for k, v in pairs(config.slots) do + ui.text(k) + + local data = " " + for k2, v2 in pairs(v) do + data = data .. tostring(k2) .. " = " .. tostring(v2) .. ", " + end + + ui.text(data) + end + end + else + for id, def in pairs(ShipDef) do + if ui.selectable(id) then + self.selectedHull = id + end + + local threat = ShipBuilder.GetHullThreat(id) + ui.sameLine() + ui.text("threat: " .. tostring(threat.total)) + end + end + + ui.endTabItem() + end +}) diff --git a/data/modules/MissionUtils/OutfitRules.lua b/data/modules/MissionUtils/OutfitRules.lua new file mode 100644 index 0000000000..cafbf1c6a3 --- /dev/null +++ b/data/modules/MissionUtils/OutfitRules.lua @@ -0,0 +1,44 @@ +-- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +local Engine = require 'Engine' +local Equipment = require 'Equipment' +local EquipSet = require 'EquipSet' +local ShipDef = require 'ShipDef' +local ShipConfig = require 'ShipConfig' +local Space = require 'Space' +local Ship = require 'Ship' + +local utils = require 'utils' + +---@class MissionUtils.OutfitRules +local OutfitRules = {} + +OutfitRules.DefaultHyperdrive = { + slot = "hyperdrive" +} + +OutfitRules.DefaultAtmoShield = { + slot = "hull", + filter = "hull.atmo_shield", + limit = 1 +} + +OutfitRules.DefaultShieldGen = { + slot = "shield", + limit = 1 +} + +OutfitRules.DefaultAutopilot = { + slot = "computer", + equip = "misc.autopilot", + limit = 1 +} + +OutfitRules.DefaultLaserCooling = { + slot = nil, + equip = "misc.laser_cooling_booster", + limit = 1 +} + +return OutfitRules diff --git a/data/modules/MissionUtils/ShipBuilder.lua b/data/modules/MissionUtils/ShipBuilder.lua index 564be6db43..500f80d565 100644 --- a/data/modules/MissionUtils/ShipBuilder.lua +++ b/data/modules/MissionUtils/ShipBuilder.lua @@ -9,8 +9,12 @@ local ShipConfig = require 'ShipConfig' local Space = require 'Space' local Ship = require 'Ship' +local Rules = require '.OutfitRules' + local utils = require 'utils' +local hullThreatCache = {} + local slotTypeMatches = function(slot, filter) return string.sub(slot, 1, #filter) == filter end @@ -22,53 +26,185 @@ end ---@class MissionUtils.ShipBuilder local ShipBuilder = {} -local hyperdriveRule = { - slot = "hyperdrive", -} +-- ============================================================================= -local randomPulsecannonEasyRule = { - slot = "weapon", - filter = "weapon.energy.pulsecannon", - pick = "random", - maxSize = 3, - limit = 1 -} +-- Scalars determining a "threat factor" rating for a ship's hull and equipment +-- to generate difficulty-appropriate NPCs -local atmoShieldRule = { - slot = "hull", - filter = "hull.atmo_shield", - limit = 1 -} +-- Default values to use if a mission hasn't set anything +ShipBuilder.kDefaultRandomThreatMin = 10.0 +ShipBuilder.kDefaultRandomThreatMax = 100.0 -local pirateProduction = { - role = "pirate", - rules = { - hyperdriveRule, - randomPulsecannonEasyRule, - atmoShieldRule, - { - slot = "shield", - limit = 1 - }, - { - slot = "computer", - equip = "misc.autopilot", - limit = 1 - } - } -} +-- || Hull Threat Factor || + +-- A ship hull starts with at least this much threat +ShipBuilder.kBaseHullThreatFactor = 1.0 + +-- How much hull health contributes to ship threat factor (tons of armor to threat) +ShipBuilder.kArmorToThreatFactor = 0.15 + +-- How much the ship's maximum forward acceleration contributes to threat factor +ShipBuilder.kAccelToThreat = 0.000015 +-- Tbreat from acceleration is added to this number to determine the final modifier for ship hull HP +ShipBuilder.kAccelThreatBase = 0.5 + +-- Controls how a ship's atmospheric performance contributes to its threat factor +ShipBuilder.kAeroStabilityToThreat = 0.3 +ShipBuilder.kAeroStabilityThreatBase = 0.80 + +ShipBuilder.kCrossSectionToThreat = 0.3 +ShipBuilder.kCrossSectionThreatBase = 0.8 + +-- Only accept ships where the hull is at least this fraction of the desired total threat factor +ShipBuilder.kMinHullThreatFactor = 0.4 +ShipBuilder.kMaxHullThreatFactor = 0.8 +ShipBuilder.kMinReservedEquipThreat = 4.0 + +-- || Weapon Threat Factor || + +-- Damage in unit tons of armor per second to threat factor +ShipBuilder.kWeaponDPSToThreat = 0.35 + +-- Weapon projectile speed modifies threat, calculated as 1.0 + (speed / 1km/s) * mod +ShipBuilder.kWeaponSpeedToThreat = 0.2 + +-- Amount of hull threat included in this weapon's threat per unit of weapon damage +ShipBuilder.kWeaponSharedHullThreat = 0.02 + +-- || Shield Threat Factor || + +-- Threat factor per unit armor ton equivalent of shield health +-- (compare with hull threat factor per unit ton) +ShipBuilder.kShieldThreatFactor = 0.2 + +-- Portion of hull threat factor to contributed per unit ton +ShipBuilder.kShieldSharedHullThreat = 0.005 + +-- ============================================================================= + +local Template = utils.proto("MissionUtils.ShipTemplate") + +Template.role = nil +Template.shipId = nil +Template.label = nil +Template.rules = {} + +function Template:__clone() + self.rules = table.copy(self.rules) +end + +ShipBuilder.Template = Template + +-- ============================================================================= + +local ShipPlan = utils.proto("MissionUtils.ShipPlan") + +ShipPlan.shipId = "" +ShipPlan.label = "" +ShipPlan.freeVolume = 0 +ShipPlan.equipMass = 0 +ShipPlan.threat = 0 +ShipPlan.freeThreat = 0 +ShipPlan.slots = {} +ShipPlan.equip = {} + +function ShipPlan:__clone() + self.slots = {} + self.equip = {} +end + +function ShipPlan:AddEquipToPlan(equip, slot, threat) + if slot then + self.slots[slot.id] = equip + else + table.insert(self.equip, equip) + end + + self.freeVolume = self.freeVolume - equip.volume + self.equipMass = self.equipMass + equip.mass + self.threat = self.threat + (threat or 0) + self.freeThreat = self.freeThreat - (threat or 0) +end + +-- ============================================================================= + +local function calcWeaponThreat(equip, hullThreat) + local damage = equip.laser_stats.damage / 1000 -- unit tons of armor + local speed = equip.laser_stats.speed / 1000 + + local dps = damage / equip.laser_stats.rechargeTime + local speedMod = 1.0 + + -- Beam lasers don't factor in projectile speed (instant) + -- TODO: they should have a separate threat factor instead + if not equip.laser_stats.beam or equip.laser_stats.beam == 0.0 then + speedMod = 1.0 + ShipBuilder.kWeaponSpeedToThreat * speed + end + + local threat = ShipBuilder.kWeaponDPSToThreat * dps * speedMod + + ShipBuilder.kWeaponSharedHullThreat * damage * hullThreat + + return threat +end + +local function calcShieldThreat(equip, hullThreat) + -- XXX: this is a hardcoded constant shared with Ship.cpp + local shield_tons = equip.capabilities.shield * 10.0 + + local threat = ShipBuilder.kShieldThreatFactor * shield_tons + + ShipBuilder.kShieldSharedHullThreat * shield_tons * hullThreat + + return threat +end + +function ShipBuilder.ComputeEquipThreatFactor(equip, hullThreat) + if equip.slot and equip.slot.type:match("weapon.") and equip.laser_stats then + return calcWeaponThreat(equip, hullThreat) + end + + if equip.slot and equip.slot.type:match("shield.") and equip.capabilities.shield then + return calcShieldThreat(equip, hullThreat) + end + + return 0.0 +end + +-- Function: ComputeHullThreatFactor +-- +-- Compute a "balance number" according to a ship's potential combat threat to +-- be used when spawning a random ship for specific encounters. +-- +-- This function does not take into account the "potential threat" of a ship if +-- its slots were to be filled (or even the possible configurations of slots), +-- but only looks at concrete stats about the ship hull. +---@param shipDef ShipDef +function ShipBuilder.ComputeHullThreatFactor(shipDef) + local threat = { id = shipDef.id } + + local armor = shipDef.hullMass + local totalMass = shipDef.hullMass + shipDef.fuelTankMass + local forwardAccel = shipDef.linearThrust["FORWARD"] / totalMass + local crossSectionAvg = (shipDef.topCrossSec + shipDef.sideCrossSec + shipDef.frontCrossSec) / 3.0 + + threat.armor = ShipBuilder.kBaseHullThreatFactor + ShipBuilder.kArmorToThreatFactor * armor + threat.thrust = ShipBuilder.kAccelThreatBase + ShipBuilder.kAccelToThreat * forwardAccel + threat.aero = ShipBuilder.kAeroStabilityThreatBase + ShipBuilder.kAeroStabilityToThreat * (shipDef.raw.aero_stability or 0.0) + threat.crosssection = ShipBuilder.kCrossSectionThreatBase + ShipBuilder.kCrossSectionToThreat * (armor / crossSectionAvg) + + threat.total = threat.armor * threat.thrust * threat.aero * threat.crosssection + threat.total = utils.round(threat.total, 0.01) + + return threat +end + +-- ============================================================================= ---@param shipPlan table ---@param shipConfig ShipDef.Config ---@param rule table ---@param rand Rand -function ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, rand) - - local addEquipToPlan = function(equip, slot) - shipPlan.slots[slot.id] = equip - shipPlan.freeVolume = shipPlan.freeVolume - equip.volume - shipPlan.equipMass = shipPlan.equipMass + equip.volume - end +---@param hullThreat number +function ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, rand, hullThreat) local matchRuleSlot = function(slot) return slotTypeMatches(slot.type, rule.slot) @@ -96,10 +232,16 @@ function ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, rand) if rule.equip then local equip = Equipment.Get(rule.equip) + local threat = ShipBuilder.ComputeEquipThreatFactor(equip, hullThreat) for _, slot in ipairs(slots) do - if EquipSet.CompatibleWithSlot(equip, slot) and shipPlan.freeVolume >= equip.volume then - addEquipToPlan(equip, slot) + + local canInstall = shipPlan.freeVolume >= equip.volume + and shipPlan.freeThreat >= threat + and EquipSet.CompatibleWithSlot(equip, slot) + + if canInstall then + shipPlan:AddEquipToPlan(equip, slot, threat) end numInstalled = numInstalled + 1 @@ -129,6 +271,15 @@ function ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, rand) return end + -- Build a cache of the threat values for these equipment items + -- We may have multiple rounds of the install loop, so it's better to do it now + -- + -- NOTE: if equipment items don't include hull threat in their calculation, this + -- can be precached at startup + local threatCache = utils.map_table(filteredEquip, function(_, equip) + return equip, ShipBuilder.ComputeEquipThreatFactor(equip, hullThreat) + end) + -- Iterate over each slot and install items for _, slot in ipairs(slots) do @@ -138,6 +289,7 @@ function ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, rand) local compatible = utils.filter_array(filteredEquip, function(equip) return EquipSet.CompatibleWithSlot(equip, slot) and shipPlan.freeVolume >= equip.volume + and shipPlan.freeThreat >= threatCache[equip] end) -- Nothing fits in this slot, ignore it then @@ -146,7 +298,7 @@ function ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, rand) if rule.pick == "random" then -- Select a random item from the list local equip = compatible[rand:Integer(1, #compatible)] - addEquipToPlan(equip, slot) + shipPlan:AddEquipToPlan(equip, slot, threatCache[equip]) else -- Sort equipment items by size; heavier items of the same size -- class first. Assume the largest and heaviest item is the best, @@ -155,7 +307,9 @@ function ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, rand) return a.slot.size > b.slot.size or (a.slot.size == b.slot.size and a.mass > b.mass) end) - addEquipToPlan(compatible[1], slot) + -- Just install the "best" item we have + local equip = compatible[1] + shipPlan:AddEquipToPlan(equip, slot, threatCache[equip]) end numInstalled = numInstalled + 1 @@ -170,41 +324,81 @@ function ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, rand) end -function ShipBuilder.SelectHull(production) +-- ============================================================================= + +function ShipBuilder.GetHullThreat(shipId) + return hullThreatCache[shipId] or { total = 0.0 } +end + +function ShipBuilder.SelectHull(template, threat) local hullList = {} - -- TODO: some sort of balance metric should be expressed here + if template.shipId then + + table.insert(hullList, template.shipType) + + else + + for id, shipDef in pairs(ShipDef) do + + if shipDef.roles[template.role] then + + local hullThreat = ShipBuilder.GetHullThreat(id).total + + -- Use threat metric as a way to balance the random selection of ship hulls + local withinRange = hullThreat >= ShipBuilder.kMinHullThreatFactor * threat + and hullThreat <= ShipBuilder.kMaxHullThreatFactor * threat + local hasReserve = threat - hullThreat >= ShipBuilder.kMinReservedEquipThreat + + if withinRange and hasReserve then + table.insert(hullList, id) + end + + end - for id, shipDef in pairs(ShipDef) do - if utils.contains(shipDef.roles, production.role) then - table.insert(hullList, id) end + end if #hullList == 0 then return nil end - local selectedHull = hullList[Engine.rand:Integer(1, #hullList)] + local shipId = hullList[Engine.rand:Integer(1, #hullList)] - return ShipConfig[selectedHull] + return ShipConfig[shipId] end ---@param production table ---@param shipConfig ShipDef.Config -function ShipBuilder.MakePlan(production, shipConfig) +function ShipBuilder.MakePlan(production, shipConfig, threat) + + local hullThreat = ShipBuilder.GetHullThreat(shipConfig.id).total - local shipPlan = { + local shipPlan = ShipPlan:clone { shipId = shipConfig.id, - equipMass = 0, freeVolume = shipConfig.capacity, - slots = {} + threat = hullThreat, + freeThreat = threat - hullThreat, } for _, rule in ipairs(production.rules) do - ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, Engine.rand) + + if rule.slot then + ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, Engine.rand, hullThreat) + else + local equip = Equipment.Get(rule.equip) + assert(equip) + + if shipPlan.freeVolume >= equip.volume then + shipPlan:AddEquipToPlan(equip) + end + end + end + shipPlan.label = production.label or Ship.MakeRandomLabel() + return shipPlan end @@ -215,6 +409,7 @@ function ShipBuilder.ApplyPlan(ship, shipPlan) local equipSet = ship:GetComponent('EquipSet') + -- Apply slot-based equipment first for name, proto in pairs(shipPlan.slots) do local slot = equipSet:GetSlotHandle(name) assert(slot) @@ -222,27 +417,92 @@ function ShipBuilder.ApplyPlan(ship, shipPlan) equipSet:Install(proto(), slot) end - -- TODO: loose equipment + for _, proto in ipairs(shipPlan.equip) do + equipSet:Install(proto()) + end + + -- TODO: ammunition / other items inside of instanced equipment + + ship:SetLabel(shipPlan.label) end ----@param player Ship +-- ============================================================================= + +---@param body Body +---@param production table +---@param risk number? ---@param nearDist number? ---@param farDist number? -function ShipBuilder.MakeGenericPirateNear(player, risk, nearDist, farDist) +function ShipBuilder.MakeShipNear(body, production, risk, nearDist, farDist) + if not risk then + risk = Engine.rand:Number(ShipBuilder.kDefaultRandomThreatMin, ShipBuilder.kDefaultRandomThreatMax) + end + + local hullConfig = ShipBuilder.SelectHull(production, risk) + assert(hullConfig) + + local plan = ShipBuilder.MakePlan(production, hullConfig, risk) + assert(plan) + + local ship = Space.SpawnShipNear(plan.shipId, body, nearDist or 50, farDist or 100) + assert(ship) + + ShipBuilder.ApplyPlan(ship, plan) + + return ship +end + +---@param body Body +---@param production table +---@param risk number? +function ShipBuilder.MakeShipDocked(body, production, risk) + if not risk then + risk = Engine.rand:Number(ShipBuilder.kDefaultRandomThreatMin, ShipBuilder.kDefaultRandomThreatMax) + end - local hullConfig = ShipBuilder.SelectHull(pirateProduction) + local hullConfig = ShipBuilder.SelectHull(production, risk) assert(hullConfig) - local plan = ShipBuilder.MakePlan(pirateProduction, hullConfig) + local plan = ShipBuilder.MakePlan(production, hullConfig, risk) assert(plan) - ---@type Ship - local ship = Space.SpawnShipNear(plan.shipId, player, nearDist or 50, farDist or 100) - ship:SetLabel(Ship.MakeRandomLabel()) + local ship = Space.SpawnShipDocked(plan.shipId, body) + assert(ship) ShipBuilder.ApplyPlan(ship, plan) + return ship +end + +-- ============================================================================= + +local randomPulsecannonEasyRule = { + slot = "weapon", + filter = "weapon.energy.pulsecannon", + pick = "random", + maxSize = 3, + limit = 1 +} + +local pirateProduction = Template:clone { + role = "pirate", + rules = { + Rules.DefaultHyperdrive, + randomPulsecannonEasyRule, + Rules.DefaultAtmoShield, + Rules.DefaultShieldGen, + Rules.DefaultAutopilot + } +} + +---@param player Ship +---@param nearDist number? +---@param farDist number? +function ShipBuilder.MakeGenericPirateNear(player, risk, nearDist, farDist) + + local ship = ShipBuilder.MakeShipNear(player, pirateProduction, risk, nearDist, farDist) + -- TODO: handle risk/difficulty-based installation of equipment as part of -- the loadout rules local equipSet = ship:GetComponent('EquipSet') @@ -259,8 +519,23 @@ function ShipBuilder.MakeGenericPirateNear(player, risk, nearDist, farDist) end +-- Generate a cache of combined hull threat factor for each ship in the game +function ShipBuilder.BuildHullThreatCache() + for id, shipDef in pairs(ShipDef) do + local threat = ShipBuilder.ComputeHullThreatFactor(shipDef) + + hullThreatCache[id] = threat + end +end + require 'Event'.Register("onEnterMainMenu", function() - local plan = ShipBuilder.MakePlan(pirateProduction, ShipConfig['coronatrix']) + ShipBuilder.BuildHullThreatCache() + + local threat = 20.0 + + local hull = ShipBuilder.SelectHull({ role = "pirate" }, threat) + + local plan = ShipBuilder.MakePlan(pirateProduction, ShipConfig['coronatrix'], threat) utils.print_r(plan) end) From 31b644a27e564988b19e221169ef6677224a1a5e Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 11 Sep 2024 16:20:27 -0400 Subject: [PATCH 029/119] Spawn police with new ShipBuilder API - Threat criteria and police rules need some adjustment for final version --- data/libs/SpaceStation.lua | 43 ++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/data/libs/SpaceStation.lua b/data/libs/SpaceStation.lua index 09024391e7..515d1ba1b9 100644 --- a/data/libs/SpaceStation.lua +++ b/data/libs/SpaceStation.lua @@ -23,6 +23,9 @@ local Commodities = require 'Commodities' local Faction = require 'Faction' local Lang = require 'Lang' +local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' +local Rules = ShipBuilder.OutfitRules + local l = Lang.GetResource("ui-core") -- @@ -631,6 +634,20 @@ SpaceStation.lawEnforcedRange = 50000 local police = {} +local policeTemplate = ShipBuilder.Template:clone { + role = "police", + label = l.POLICE, + rules = { + { + slot = "weapon", + equip = "laser.pulsecannon_dual_1mw", + limit = 1 + }, + Rules.DefaultAtmoShield, + Rules.DefaultLaserCooling + } +} + -- -- Method: LaunchPolice -- @@ -660,23 +677,23 @@ function SpaceStation:LaunchPolice(targetShip) local lawlessness = Game.system.lawlessness local maxPolice = math.min(9, self.numDocks) local numberPolice = math.ceil(Engine.rand:Integer(1,maxPolice)*(1-lawlessness)) - local shiptype = ShipDef[Game.system.faction.policeShip] + + -- The more lawless/dangerous the space is, the better equipped the few police ships are + -- In a high-law area, a spacestation has a bunch of traffic cops due to low crime rates + local shipThreat = 10.0 + Engine.rand:Number(10, 50) * lawlessness + + local shipTemplate = policeTemplate:clone { + shipId = Game.system.faction.policeShip + } -- create and equip them - while numberPolice > 0 do - local policeShip = Space.SpawnShipDocked(shiptype.id, self) + for i = 1, numberPolice do + local policeShip = ShipBuilder.MakeShipDocked(self, shipTemplate, shipThreat) if policeShip == nil then - return - else - numberPolice = numberPolice - 1 - --policeShip:SetLabel(Game.system.faction.policeName) -- this is cool, but not translatable right now - policeShip:SetLabel(l.POLICE) - policeShip:AddEquip(Equipment.laser.pulsecannon_dual_1mw) - policeShip:AddEquip(Equipment.misc.atmospheric_shielding) - policeShip:AddEquip(Equipment.misc.laser_cooling_booster) - - table.insert(police[self], policeShip) + break end + + table.insert(police[self], policeShip) end end From 9fac8bb4468ecdebbef4e5c8a9c54c23b1775d7c Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 12 Sep 2024 15:32:52 -0400 Subject: [PATCH 030/119] ui: unify rounding value used for item-card like elements --- data/pigui/libs/equipment-outfitter.lua | 20 ++++++++++++++------ data/pigui/libs/item-card.lua | 6 +++--- data/pigui/themes/default.lua | 3 ++- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/data/pigui/libs/equipment-outfitter.lua b/data/pigui/libs/equipment-outfitter.lua index 8e4c382c40..bd7bcc86bf 100644 --- a/data/pigui/libs/equipment-outfitter.lua +++ b/data/pigui/libs/equipment-outfitter.lua @@ -11,8 +11,10 @@ local utils = require 'utils' local ui = require 'pigui' local pionillium = ui.fonts.pionillium + local colors = ui.theme.colors local icons = ui.theme.icons +local styles = ui.theme.styles local Module = require 'pigui.libs.module' local EquipCard = require 'pigui.libs.equip-card' @@ -43,8 +45,10 @@ local customButton = function(label, icon, infoText, variant) local height = lineHeight + framePadding.y * 2.0 local icon_size = Vector2(lineHeight) local size = Vector2(ui.getContentRegion().x, height) + local rounding = styles.ItemCardRounding - local textOffset = framePadding + Vector2(icon_size.x + framePadding.x, pionillium.heading.size * 0.25) + local iconOffset = framePadding + Vector2(rounding, 0) + local textOffset = iconOffset + Vector2(icon_size.x + framePadding.x, pionillium.heading.size * 0.25) local fontCol = ui.theme.colors.font local startPos = ui.getCursorScreenPos() @@ -54,13 +58,18 @@ local customButton = function(label, icon, infoText, variant) ui.withButtonColors(variant, function() pigui.PushStyleVar("FramePadding", framePadding) - pigui.PushStyleVar("FrameRounding", 4) + pigui.PushStyleVar("FrameRounding", rounding) clicked = pigui.Button("##" .. label, size) pigui.PopStyleVar(2) end) + if ui.isItemHovered() then + local tl, br = ui.getItemRect() + ui.addRectFilled(tl, tl + Vector2(rounding, size.y), fontCol, 4, 0x5) + end + ui.withFont(pionillium.heading, function() - ui.addIconSimple(startPos + framePadding, icon, icon_size, fontCol) + ui.addIconSimple(startPos + iconOffset, icon, icon_size, fontCol) ui.addText(startPos + textOffset, fontCol, label) if infoText then @@ -333,7 +342,7 @@ end function Outfitter:drawBuyButton(data) local icon = icons.autopilot_dock - local price_text = l.PRICE .. ": " .. ui.Format.Money(self:getInstallPrice(data.equip)) + local price_text = ui.Format.Money(self:getInstallPrice(data.equip)) local variant = data.available and ui.theme.buttonColors.dark or ui.theme.buttonColors.disabled if customButton(l.BUY_EQUIP % data, icon, price_text, variant) and data.available then @@ -343,7 +352,7 @@ end function Outfitter:drawSellButton(data) local icon = icons.autopilot_undock_illegal - local price_text = l.PRICE .. ": " .. ui.Format.Money(self:getSellPrice(data.equip)) + local price_text = ui.Format.Money(self:getSellPrice(data.equip)) if customButton(l.SELL_EQUIP % data, icon, price_text) then self:message("onSellItem", data.equip) @@ -450,7 +459,6 @@ function Outfitter:render() ui.sameLine() ui.withFont(pionillium.heading, function() - ui.text(self.replaceEquip and l.REPLACE_EQUIPMENT_WITH or l.AVAILABLE_FOR_PURCHASE) end) diff --git a/data/pigui/libs/item-card.lua b/data/pigui/libs/item-card.lua index a846e745b7..d92f43bbd9 100644 --- a/data/pigui/libs/item-card.lua +++ b/data/pigui/libs/item-card.lua @@ -23,8 +23,8 @@ ItemCard.highlightBar = false ItemCard.detailFields = 2 ItemCard.iconSize = nil ---@type Vector2? -ItemCard.lineSpacing = ui.theme.styles.ItemSpacing -ItemCard.rounding = 4 +ItemCard.lineSpacing = ui.theme.styles.ItemSpacing +ItemCard.rounding = ui.theme.styles.ItemCardRounding ItemCard.colors = ui.theme.buttonColors.card @@ -98,7 +98,7 @@ function ItemCard:draw(data, isSelected) local lineSpacing = self.lineSpacing if self.highlightBar then - ui.addCursorPos(Vector2(self.lineSpacing.x, 0)) + ui.addCursorPos(Vector2(self.rounding, 0)) end -- initial sizing setup diff --git a/data/pigui/themes/default.lua b/data/pigui/themes/default.lua index 2d7a72fc55..1842f83735 100644 --- a/data/pigui/themes/default.lua +++ b/data/pigui/themes/default.lua @@ -323,7 +323,8 @@ theme.styles = rescaleUI { SmallButtonSize = Vector2(30, 30), IconButtonPadding = Vector2(3, 3), InlineIconPadding = Vector2(2, 2), - MainButtonPadding = 3 + MainButtonPadding = 3, + ItemCardRounding = 4 } theme.icons = { From 3168013d9c1e5aebaad1a51e8e090bf181d0bb3b Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 12 Sep 2024 16:39:30 -0400 Subject: [PATCH 031/119] item-card: configurable main icon color --- data/pigui/libs/equip-card.lua | 3 ++- data/pigui/libs/item-card.lua | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/data/pigui/libs/equip-card.lua b/data/pigui/libs/equip-card.lua index bba2083671..aeaf8ff68d 100644 --- a/data/pigui/libs/equip-card.lua +++ b/data/pigui/libs/equip-card.lua @@ -175,7 +175,8 @@ end function EquipCard.getDataForEquip(equip, compare) ---@type UI.EquipCard.Data local out = { - icon = equip and icons[equip.icon_name] or EquipCard.emptyIcon + icon = equip and icons[equip.icon_name] or EquipCard.emptyIcon, + iconColor = equip and colors.white or colors.fontDim } if equip then diff --git a/data/pigui/libs/item-card.lua b/data/pigui/libs/item-card.lua index d92f43bbd9..2eb776fa58 100644 --- a/data/pigui/libs/item-card.lua +++ b/data/pigui/libs/item-card.lua @@ -27,6 +27,7 @@ ItemCard.lineSpacing = ui.theme.styles.ItemSpacing ItemCard.rounding = ui.theme.styles.ItemCardRounding ItemCard.colors = ui.theme.buttonColors.card +ItemCard.iconColor = colors.white function ItemCard:drawTitle(data, regionWidth, isSelected) -- override to draw your specific item card type! @@ -125,7 +126,7 @@ function ItemCard:draw(data, isSelected) -- Draw the main icon -- The icon is offset vertically to center it in the available space if -- smaller than the height of the text - ui.addIconSimple(tl + lineSpacing + Vector2(0, iconOffset), data.icon, iconSize, colors.white) + ui.addIconSimple(tl + lineSpacing + Vector2(0, iconOffset), data.icon, iconSize, data.iconColor or self.iconColor) -- Position the cursor for the title and details local textLinePos = tl + lineSpacing + Vector2(iconSize.x + lineSpacing.x, 0) From ffbe8d64aa7ff3f8f01d3db55bdbedb625224c6d Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 12 Sep 2024 17:03:25 -0400 Subject: [PATCH 032/119] EquipSet: don't copy installed array, write cache on install - Fix a bug where equipment items weren't written to the cache when installed - Don't make unnecessary copies of the installed equipment list - return it directly when asking for the whole list. --- data/libs/EquipSet.lua | 14 +++++--------- data/modules/Debug/DebugShipSpawn.lua | 2 +- data/modules/Debug/DebugShipSpawn.moon | 2 +- data/modules/TradeShips/Debug.lua | 2 +- data/pigui/modules/station-view/04-shipMarket.lua | 2 +- 5 files changed, 9 insertions(+), 13 deletions(-) diff --git a/data/libs/EquipSet.lua b/data/libs/EquipSet.lua index abc2fba538..f221759382 100644 --- a/data/libs/EquipSet.lua +++ b/data/libs/EquipSet.lua @@ -184,17 +184,11 @@ end -- Function: GetInstalledEquipment -- --- Returns an array containing all equipment items installed on this ship, +-- Returns a table containing all equipment items installed on this ship, -- including both slot-based equipment and freely-installed equipment. ----@return EquipType[] +---@return table function EquipSet:GetInstalledEquipment() - local out = {} - - for _, equip in pairs(self.installed) do - table.insert(out, equip) - end - - return out + return self.installed end -- Function: GetInstalledNonSlot @@ -286,12 +280,14 @@ function EquipSet:Install(equipment, slotHandle) end self.installed[slotHandle.id] = equipment + self.cache[equipment] = slotHandle.id else if not self:CanInstallLoose(equipment) then return false end table.insert(self.installed, equipment) + self.cache[equipment] = #self.installed end self:_InstallInternal(equipment) diff --git a/data/modules/Debug/DebugShipSpawn.lua b/data/modules/Debug/DebugShipSpawn.lua index f7d26ef70d..1b259f089b 100644 --- a/data/modules/Debug/DebugShipSpawn.lua +++ b/data/modules/Debug/DebugShipSpawn.lua @@ -131,7 +131,7 @@ set_player_ship_type = function(shipType) local equipSet = Game.player:GetComponent("EquipSet") local items = equipSet:GetInstalledEquipment() local returnedMoney = 0 - for i, item in ipairs(items) do + for i, item in pairs(items) do returnedMoney = returnedMoney + item.price end do diff --git a/data/modules/Debug/DebugShipSpawn.moon b/data/modules/Debug/DebugShipSpawn.moon index 88d8345b62..8c42ac9f25 100644 --- a/data/modules/Debug/DebugShipSpawn.moon +++ b/data/modules/Debug/DebugShipSpawn.moon @@ -119,7 +119,7 @@ set_player_ship_type = (shipType) -> items = equipSet\GetInstalledEquipment! returnedMoney = 0 - for i, item in ipairs items + for i, item in pairs items returnedMoney += item.price with Game.player diff --git a/data/modules/TradeShips/Debug.lua b/data/modules/TradeShips/Debug.lua index 00e7423060..815dc81168 100644 --- a/data/modules/TradeShips/Debug.lua +++ b/data/modules/TradeShips/Debug.lua @@ -309,7 +309,7 @@ debugView.registerTab('debug-trade-ships', { local equipSet = ship:GetComponent('EquipSet') local cargoMgr = ship:GetComponent('CargoManager') - for _, equip in ipairs(equipSet:GetInstalledEquipment()) do + for _, equip in pairs(equipSet:GetInstalledEquipment()) do local count = equip.count or 1 total_mass = total_mass + equip.mass table.insert(equipItems, { diff --git a/data/pigui/modules/station-view/04-shipMarket.lua b/data/pigui/modules/station-view/04-shipMarket.lua index ec1cb398de..9b8d32dd1f 100644 --- a/data/pigui/modules/station-view/04-shipMarket.lua +++ b/data/pigui/modules/station-view/04-shipMarket.lua @@ -100,7 +100,7 @@ local tradeInValue = function(ship) end local equipment = ship:GetComponent("EquipSet"):GetInstalledEquipment() - for _, e in ipairs(equipment) do + for _, e in pairs(equipment) do local n = e.count or 1 value = value + n * e.price * equipSellPriceReduction end From 57730c0f995bac0ab88123c83fda0051e3bd9a40 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 13 Sep 2024 17:00:44 -0400 Subject: [PATCH 033/119] Equip: handle multi-count item slots, SpecializeForShip - A slot may specify a "count", which is the number of instances of the in-universe item the specific EquipType instance logically represents - Mass, volume, price all scale with the count of items in the slot - Multi-count items are represented as an all-or-nothing item. No attempt should be made to modify the count of an instance at runtime. For items that may or may not be present, multiple independent slots should be used. - Finalize the API for SpecializeForShip() - this will be used to set costs for e.g. hull shielding or reinforcement items. --- data/libs/EquipType.lua | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index 61893146e8..035b5cf6ac 100644 --- a/data/libs/EquipType.lua +++ b/data/libs/EquipType.lua @@ -43,6 +43,7 @@ local misc = {} ---@field tech_level integer | "MILITARY" ---@field transient table ---@field slots table -- deprecated +---@field count integer? ---@field __proto EquipType? local EquipType = utils.inherits(nil, "EquipType") @@ -93,9 +94,9 @@ function EquipType.New (specs) return obj end --- Override this with a function returning an equipment instance appropriate for the passed ship +-- Override this with a function customizing the equipment instance for the passed ship -- (E.g. for equipment with mass/volume/cost dependent on the specific ship hull) -EquipType.SpecializeForShip = nil ---@type nil | fun(self: self, ship: Ship): EquipType +EquipType.SpecializeForShip = nil ---@type nil | fun(self: self, ship: ShipDef.Config) function EquipType._createTransient(obj) local l = Lang.GetResource(obj.l10n_resource) @@ -122,15 +123,30 @@ function EquipType.isProto(inst) return not rawget(inst, "__proto") end +---@return EquipType function EquipType:GetPrototype() return rawget(self, "__proto") or self end +-- Create an instance of this equipment prototype ---@return EquipType function EquipType:Instance() return setmetatable({ __proto = self }, self.meta) end +-- Some equipment slots represent multiple in-world items as a single logical +-- "item" for the player to interact with. This function handles scaling +-- equipment stats according to the number of "copies" of the item this +-- instance represents. +function EquipType:SetCount(count) + local proto = self:GetPrototype() + + self.mass = proto.mass * count + self.volume = proto.volume * count + self.price = proto.price * count + self.count = count +end + -- Patch an EquipType class to support a prototype-based equipment system -- `equipProto = EquipType.New({ ... })` to create an equipment prototype -- `equipInst = equipProto()` to create a new instance based on the created prototype @@ -427,12 +443,16 @@ function CabinType:OnRemove(ship, slot) end end +---@class Equipment.ThrusterType : EquipType +local ThrusterType = utils.inherits(EquipType, "Equipment.ThrusterType") + Serializer:RegisterClass("LaserType", LaserType) Serializer:RegisterClass("EquipType", EquipType) Serializer:RegisterClass("HyperdriveType", HyperdriveType) Serializer:RegisterClass("SensorType", SensorType) Serializer:RegisterClass("BodyScannerType", BodyScannerType) Serializer:RegisterClass("Equipment.CabinType", CabinType) +Serializer:RegisterClass("Equipment.ThrusterType", ThrusterType) EquipType:SetupPrototype() LaserType:SetupPrototype() @@ -440,6 +460,7 @@ HyperdriveType:SetupPrototype() SensorType:SetupPrototype() BodyScannerType:SetupPrototype() CabinType:SetupPrototype() +ThrusterType:SetupPrototype() return { laser = laser, @@ -451,4 +472,5 @@ return { SensorType = SensorType, BodyScannerType = BodyScannerType, CabinType = CabinType, + ThrusterType = ThrusterType, } From f50626279918f70ce644296a6887cb37d5eb3cd7 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 13 Sep 2024 17:04:56 -0400 Subject: [PATCH 034/119] Outfitter: handle multi-count and specialized items - Create equipment item instances for specialized / multi-count equipment items when building the list of equipment rather than when installing --- data/pigui/libs/equip-card.lua | 3 +- data/pigui/libs/equipment-outfitter.lua | 50 +++++++++++++++++-------- data/pigui/libs/ship-equipment.lua | 7 ++-- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/data/pigui/libs/equip-card.lua b/data/pigui/libs/equip-card.lua index aeaf8ff68d..c864774ca2 100644 --- a/data/pigui/libs/equip-card.lua +++ b/data/pigui/libs/equip-card.lua @@ -154,7 +154,7 @@ function EquipCard:drawTitle(data, textWidth, isSelected) if data.count then ui.sameLine() ui.withStyleColors(slotColors, function() - ui.text("x{count}" % data) + ui.text("x" .. data.count) end) end @@ -183,6 +183,7 @@ function EquipCard.getDataForEquip(equip, compare) out.name = equip:GetName() out.equip = equip out.size = equip.slot and ("S" .. equip.slot.size) or nil + out.count = equip.count out.stats = equip:GetDetailedStats() diff --git a/data/pigui/libs/equipment-outfitter.lua b/data/pigui/libs/equipment-outfitter.lua index bd7bcc86bf..8c953d3cd6 100644 --- a/data/pigui/libs/equipment-outfitter.lua +++ b/data/pigui/libs/equipment-outfitter.lua @@ -157,23 +157,41 @@ end -- Override to support e.g. custom equipment shops ---@return EquipType[] function Outfitter:getAvailableEquipment() - return utils.filter_table(Equipment.new, function(id, equip) + local shipConfig = self.ship:GetComponent('EquipSet').config + local slotCount = self.filterSlot and self.filterSlot.count + + return utils.map_table(Equipment.new, function(id, equip) if self:getStock(equip) <= 0 then -- FIXME: restore when equipment stocking converted to new system - --return false + -- return id, nil end - return self:canDisplayItem(equip) - end) -end + if not equip.purchasable or not self:stationHasTech(equip.tech_level) then + return id, nil + end ----@param e EquipType -function Outfitter:canDisplayItem(e) - if e.SpecializeForShip then - e = e:SpecializeForShip(self.ship) - end + if not EquipSet.CompatibleWithSlot(equip, self.filterSlot) then + return id, nil + end + + -- Instance the equipment item if we need to modify it for the ship it's installed in + if slotCount or equip.SpecializeForShip then + equip = equip:Instance() + end + + -- Some equipment items might change their details based on the ship they're installed in + if equip.SpecializeForShip then + equip:SpecializeForShip(shipConfig) + end + + -- Some slots collapse multiple equipment items into a single logical item + -- Those slots are treated as all-or-nothing for less confusion + if slotCount then + equip:SetCount(slotCount) + end - return e.purchasable and self:stationHasTech(e.tech_level) and EquipSet.CompatibleWithSlot(e, self.filterSlot) + return id, equip + end) end ---@param e EquipType @@ -184,14 +202,16 @@ end -- Cost of the equipment item if buying function Outfitter:getBuyPrice(e) - e = e.__proto or e - return self.station:GetEquipmentPrice(e) + -- If the item instance has a specific price, use that instead of the station price + -- TODO: the station should instead have a price modifier that adjusts the price of an equipment item + return rawget(e, "price") or self.station:GetEquipmentPrice(e:GetPrototype()) end -- Money gained from equipment item if selling function Outfitter:getSellPrice(e) - e = e.__proto or e - return self.station:GetEquipmentPrice(e) * Economy.BaseResellPriceModifier + -- If the item instance has a specific price, use that instead of the station price + -- TODO: the station should instead have a price modifier that adjusts the price of an equipment item + return (rawget(e, "price") or self.station:GetEquipmentPrice(e:GetPrototype())) * Economy.BaseResellPriceModifier end -- Purchase price of an item less the sale cost of the old item diff --git a/data/pigui/libs/ship-equipment.lua b/data/pigui/libs/ship-equipment.lua index 513b172131..a856e08675 100644 --- a/data/pigui/libs/ship-equipment.lua +++ b/data/pigui/libs/ship-equipment.lua @@ -55,9 +55,7 @@ function EquipmentWidget:onBuyItem(equip) self:onSellItem(self.selectedEquip) end - if equip.SpecializeForShip then - equip = equip:SpecializeForShip(self.ship) - else + if equip:isProto() then equip = equip:Instance() end @@ -405,7 +403,8 @@ function EquipmentWidget:drawSlotSection(name, slots) slotData.size = slotData.size or ("S" .. slot.size) slotData.type = names[slot] - slotData.count = slot.count + -- Use the equipment count if present + slotData.count = slotData.count or slot.count if self:drawEquipmentItem(slotData, isSelected) then self:message("onSelectSlot", slot) From 37b30414e9270f70c2906ad082dcee2d7b7af0c6 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 13 Sep 2024 17:01:35 -0400 Subject: [PATCH 035/119] Update thruster equipment to use multi-count slots --- data/modules/Equipment/Internal.lua | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/data/modules/Equipment/Internal.lua b/data/modules/Equipment/Internal.lua index b9cbb41661..38255f8b01 100644 --- a/data/modules/Equipment/Internal.lua +++ b/data/modules/Equipment/Internal.lua @@ -7,6 +7,7 @@ local Equipment = require 'Equipment' local EquipType = EquipTypes.EquipType local SensorType = EquipTypes.SensorType local CabinType = EquipTypes.CabinType +local ThrusterType = EquipTypes.ThrusterType --=============================================== -- Computer Modules @@ -92,31 +93,31 @@ Equipment.Register("misc.hull_autorepair", EquipType.New { -- Thruster Mods --=============================================== -Equipment.Register("misc.thrusters_default", EquipType.New { +Equipment.Register("misc.thrusters_default", ThrusterType.New { l10n_key="THRUSTERS_DEFAULT", slots="thruster", - price=1500, purchasable=true, tech_level=2, + price=120, purchasable=true, tech_level=2, slot = { type="thruster", size=1 }, mass=0, volume=0, capabilities={ thruster_power=0 }, icon_name="equip_thrusters_basic" }) -Equipment.Register("misc.thrusters_basic", EquipType.New { +Equipment.Register("misc.thrusters_basic", ThrusterType.New { l10n_key="THRUSTERS_BASIC", slots="thruster", - price=3000, purchasable=true, tech_level=5, + price=250, purchasable=true, tech_level=5, slot = { type="thruster", size=1 }, - mass=0, volume=0, capabilities={ thruster_power=1 }, + mass=0.1, volume=0.05, capabilities={ thruster_power=1 }, icon_name="equip_thrusters_basic" }) -Equipment.Register("misc.thrusters_medium", EquipType.New { +Equipment.Register("misc.thrusters_medium", ThrusterType.New { l10n_key="THRUSTERS_MEDIUM", slots="thruster", - price=6500, purchasable=true, tech_level=8, + price=560, purchasable=true, tech_level=8, slot = { type="thruster", size=1 }, - mass=0, volume=0, capabilities={ thruster_power=2 }, + mass=0.05, volume=0.05, capabilities={ thruster_power=2 }, icon_name="equip_thrusters_medium" }) -Equipment.Register("misc.thrusters_best", EquipType.New { +Equipment.Register("misc.thrusters_best", ThrusterType.New { l10n_key="THRUSTERS_BEST", slots="thruster", price=14000, purchasable=true, tech_level="MILITARY", slot = { type="thruster", size=1 }, From b185fcc518b41c1775e6f7e5246b798252c2e053 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 13 Sep 2024 17:43:02 -0400 Subject: [PATCH 036/119] ShipBuilder: handle multi-count slots and SpecializeForShip --- data/modules/MissionUtils/ShipBuilder.lua | 51 +++++++++++++++++------ 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/data/modules/MissionUtils/ShipBuilder.lua b/data/modules/MissionUtils/ShipBuilder.lua index 500f80d565..e0a8d50034 100644 --- a/data/modules/MissionUtils/ShipBuilder.lua +++ b/data/modules/MissionUtils/ShipBuilder.lua @@ -236,19 +236,30 @@ function ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, rand, hullTh for _, slot in ipairs(slots) do - local canInstall = shipPlan.freeVolume >= equip.volume - and shipPlan.freeThreat >= threat - and EquipSet.CompatibleWithSlot(equip, slot) + if EquipSet.CompatibleWithSlot(equip, slot) and shipPlan.freeThreat >= threat then - if canInstall then - shipPlan:AddEquipToPlan(equip, slot, threat) - end + local inst = equip:Instance() - numInstalled = numInstalled + 1 + if inst.SpecializeForShip then + inst:SpecializeForShip(shipConfig) + end + + if slot.count then + inst:SetCount(slot.count) + end + + if shipPlan.freeVolume >= inst.volume then + shipPlan:AddEquipToPlan(equip, slot, threat) + end + + numInstalled = numInstalled + 1 + + if rule.limit and numInstalled >= rule.limit then + break + end - if rule.limit and numInstalled >= rule.limit then - break end + end return @@ -286,10 +297,26 @@ function ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, rand, hullTh -- Not all equipment items which passed the size/slot type check earlier -- may be compatible with this specific slot (e.g. if it has a more -- specific slot type than the rule itself). - local compatible = utils.filter_array(filteredEquip, function(equip) - return EquipSet.CompatibleWithSlot(equip, slot) - and shipPlan.freeVolume >= equip.volume + ---@type EquipType[] + local compatible = utils.map_array(filteredEquip, function(equip) + local compat = EquipSet.CompatibleWithSlot(equip) and shipPlan.freeThreat >= threatCache[equip] + + if not compat then + return nil + end + + local inst = equip:Instance() + + if inst.SpecializeForShip then + inst:SpecializeForShip(shipConfig) + end + + if slot.count then + inst:SetCount(slot.count) + end + + return shipPlan.freeVolume >= inst.volume and inst or nil end) -- Nothing fits in this slot, ignore it then From c7b04b8d6e3274308b06e2c92264e4a46e246d92 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 16 Sep 2024 22:05:30 -0400 Subject: [PATCH 037/119] EquipSet: add recursive slot capability - Equipment items can define a provides_slots field which contains new equipment slots - Instances of slot-providing equipment must have unique slot instances for item installation to function - Both slotted and non-slot equipment can provide additional equipment slots - It is technically possible but not advised to nest slot-providing equipment recursively - Slot type filter changed to strictly match against identifier chains delimited by periods --- data/libs/EquipSet.lua | 103 ++++++++++++++++++++++++++++++++++------ data/libs/EquipType.lua | 8 +++- 2 files changed, 95 insertions(+), 16 deletions(-) diff --git a/data/libs/EquipSet.lua b/data/libs/EquipSet.lua index f221759382..22825834d1 100644 --- a/data/libs/EquipSet.lua +++ b/data/libs/EquipSet.lua @@ -17,9 +17,11 @@ local utils = require 'utils' local EquipSet = utils.class("EquipSet") local function slotTypeMatches(equipType, slotType) - return string.sub(equipType, 1, #slotType) == slotType + return equipType == slotType or string.sub(equipType, 1, #slotType + 1) == slotType .. "." end +EquipSet.SlotTypeMatches = slotTypeMatches + -- Function: CompatibleWithSlot -- -- Static helper function to check if the given equipment item is compatible @@ -43,12 +45,21 @@ function EquipSet:Constructor(ship) -- Stores a mapping of slot id -> equipment item -- Non-slot equipment is stored in the array portion - self.installed = {} ---@type table|EquipType[] + self.installed = {} ---@type table -- Note: the integer value stored in the cache is NOT the current array -- index of the given item. It's simply a non-nil integer to indicate the -- item is not installed in a slot. self.cache = {} ---@type table + -- Stores a mapping of slot id -> slot handle + -- Simplifies slot lookup for slots defined on equipment items + self.slotCache = {} ---@type table + -- Stores the inverse mapping for looking up the compound id of a slot by + -- the slot object itself. + self.idCache = {} ---@type table + + self:BuildSlotCache() + self.listeners = {} -- Initialize ship properties we're responsible for modifying @@ -72,7 +83,7 @@ end ---@param id string ---@return ShipDef.Slot? function EquipSet:GetSlotHandle(id) - return self.config.slots[id] + return self.slotCache[id] end -- Function: GetItemInSlot @@ -83,8 +94,10 @@ end function EquipSet:GetItemInSlot(slot) -- The equipment item is not stored in the slot itself to reduce savefile -- size. While the API would be marginally simpler if so, there would be a - -- significant amount of (de)serialization overhead. - return self.installed[slot.id] + -- significant amount of (de)serialization overhead as every ship and + -- equipment instance would need to own an instance of the slot. + local id = self.idCache[slot] + return id and self.installed[id] end -- Function: GetFreeSlotForEquip @@ -103,7 +116,7 @@ function EquipSet:GetFreeSlotForEquip(equip) and self:CanInstallInSlot(slot, equip) end - for _, slot in pairs(self.config.slots) do + for _, slot in pairs(self.slotCache) do if filter(_, slot) then return slot end @@ -122,7 +135,7 @@ end function EquipSet:GetAllSlotsOfType(type, hardpoint) local t = {} - for _, slot in pairs(self.config.slots) do + for _, slot in pairs(self.slotCache) do local match = (hardpoint == nil or hardpoint == slot.hardpoint) and slotTypeMatches(slot.type, type) if match then table.insert(t, slot) end @@ -269,18 +282,23 @@ end ---@return boolean function EquipSet:Install(equipment, slotHandle) print("Installing equip {} in slot {}" % { equipment:GetName(), slotHandle }) + local slotId = self.idCache[slotHandle] if slotHandle then - if not self:CanInstallInSlot(slotHandle, equipment) then - return false + if not slotId then + return false -- No such slot! end - if self.installed[slotHandle.id] then - return false + if self.installed[slotId] then + return false -- Slot already full! end - self.installed[slotHandle.id] = equipment - self.cache[equipment] = slotHandle.id + if not self:CanInstallInSlot(slotHandle, equipment) then + return false -- Doesn't fit! + end + + self.installed[slotId] = equipment + self.cache[equipment] = slotId else if not self:CanInstallLoose(equipment) then return false @@ -290,8 +308,8 @@ function EquipSet:Install(equipment, slotHandle) self.cache[equipment] = #self.installed end - self:_InstallInternal(equipment) equipment:OnInstall(self.ship, slotHandle) + self:_InstallInternal(equipment) for _, fun in ipairs(self.listeners) do fun("install", equipment, slotHandle) @@ -307,6 +325,11 @@ end -- The equipment item must be the same item instance that was installed prior; -- passing an equipment prototype instance will not result in any equipment -- item being removed. +-- +-- Note that when removing an equipment item that provides slots, all items +-- installed into those slots must be manually removed *before* calling this +-- function! EquipSet:Remove() will not recurse into an item's slots to remove +-- installed sub-items! ---@param equipment EquipType ---@return boolean function EquipSet:Remove(equipment) @@ -354,6 +377,17 @@ function EquipSet:_InstallInternal(equipment) end end + if equipment.provides_slots then + local baseId = tostring(self.cache[equipment]) .. "##" + for _, slot in pairs(equipment.provides_slots) do + local slotId = baseId .. slot.id + assert(not self.slotCache[slotId]) + + self.slotCache[slotId] = slot + self.idCache[slot] = slotId + end + end + self.ship:UpdateEquipStats() end @@ -364,6 +398,16 @@ function EquipSet:_RemoveInternal(equipment) self.ship:setprop("mass_cap", self.ship["mass_cap"] - equipment.mass) self.ship:setprop("equipVolume", self.ship.equipVolume - equipment.volume) + if equipment.provides_slots then + for _, slot in pairs(equipment.provides_slots) do + local slotId = self.idCache[slot] + assert(slotId) + + self.slotCache[slotId] = nil + self.idCache[slot] = nil + end + end + if equipment.capabilities then for k, v in pairs(equipment.capabilities) do local cap = k .. "_cap" @@ -374,11 +418,33 @@ function EquipSet:_RemoveInternal(equipment) self.ship:UpdateEquipStats() end +-- Populate the slot cache +---@private +function EquipSet:BuildSlotCache() + for _, slot in pairs(self.config.slots) do + self.slotCache[slot.id] = slot + self.idCache[slot] = slot.id + end + + -- id is the (potentially compound) slot id the equipment is already installed in + for id, equip in pairs(self.installed) do + if equip.provides_slots then + for _, slot in pairs(equip.provides_slots) do + local slotId = tostring(id) .. "##" .. slot.id + self.slotCache[slotId] = slot + self.idCache[slot] = slotId + end + end + end +end + -- Remove transient fields from the serialized copy of the EquipSet function EquipSet:Serialize() local obj = table.copy(self) obj.cache = nil + obj.slotCache = nil + obj.idCache = nil obj.listeners = nil return obj @@ -393,7 +459,14 @@ function EquipSet:Unserialize() self.cache[v] = k end - return setmetatable(self, EquipSet.meta) + self.slotCache = {} + self.idCache = {} + + setmetatable(self, EquipSet.meta) + + self:BuildSlotCache() + + return self end Serializer:RegisterClass("EquipSet", EquipSet) diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index 035b5cf6ac..08ea404845 100644 --- a/data/libs/EquipType.lua +++ b/data/libs/EquipType.lua @@ -44,6 +44,7 @@ local misc = {} ---@field transient table ---@field slots table -- deprecated ---@field count integer? +---@field provides_slots table? ---@field __proto EquipType? local EquipType = utils.inherits(nil, "EquipType") @@ -109,8 +110,13 @@ end ---@param ship Ship ---@param slot ShipDef.Slot? function EquipType:OnInstall(ship, slot) - -- Override this for any custom installation logic needed + -- Extend this for any custom installation logic needed -- (e.g. mounting weapons) + + -- Create unique instances of the slots provided by this equipment item + if self.provides_slots and not rawget(self, "provides_slots") then + self.provides_slots = utils.map_table(self.provides_slots, function(id, slot) return id, slot:clone() end) + end end ---@param ship Ship From 74e7ecff906919664bfb183822ab9d21fe3b34cf Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 16 Sep 2024 22:40:51 -0400 Subject: [PATCH 038/119] Support recursive slots in ship equipment UI - Build slot list on refresh or equipment change - Simplify slot traversal handling to a series of recursive groups - Add list expand/collapse widget for recursive slot display - Show number of filled slots on equipment card - Stub in adjacency information for better UX when buying equipment --- data/pigui/libs/equip-card.lua | 29 +- data/pigui/libs/equipment-outfitter.lua | 4 +- data/pigui/libs/ship-equipment.lua | 354 ++++++++++++++---------- data/pigui/themes/default.lua | 3 + 4 files changed, 238 insertions(+), 152 deletions(-) diff --git a/data/pigui/libs/equip-card.lua b/data/pigui/libs/equip-card.lua index c864774ca2..81d9ff7926 100644 --- a/data/pigui/libs/equip-card.lua +++ b/data/pigui/libs/equip-card.lua @@ -47,6 +47,9 @@ local EquipCard = utils.inherits(ItemCard, "UI.EquipCard") ---@field type string? ---@field size string? ---@field equip EquipType? +---@field slot ShipDef.Slot? +---@field present integer? +---@field total integer? ---@field stats EquipType.UI.Stats[] | nil EquipCard.tooltipStats = true @@ -63,28 +66,29 @@ local tooltipStyle = { ---@param data UI.EquipCard.Data function EquipCard:tooltipContents(data, isSelected) - if data.equip then - ui.withFont(pionillium.heading, function() - ui.text(data.equip:GetName()) - ui.spacing() - end) + if not data.equip then + return + end + + ui.withFont(pionillium.body, function() + ui.text(data.equip:GetName()) local desc = data.equip:GetDescription() if desc and desc ~= "" then - ui.withFont(pionillium.body, function() - - ui.textWrapped(desc) + ui.withFont(pionillium.details, function() ui.spacing() + ui.textWrapped(desc) end) end - end + end) if self.tooltipStats and data.stats then + ui.spacing() ui.separator() ui.spacing() - ui.withFont(pionillium.body, function() + ui.withFont(pionillium.details, function() self.drawEquipStats(data) end) end @@ -156,6 +160,11 @@ function EquipCard:drawTitle(data, textWidth, isSelected) ui.withStyleColors(slotColors, function() ui.text("x" .. data.count) end) + elseif data.present then + ui.sameLine() + ui.withStyleColors(slotColors, function() + ui.text(data.present .. "/" .. data.total) + end) end -- Draw the size and/or type of the slot diff --git a/data/pigui/libs/equipment-outfitter.lua b/data/pigui/libs/equipment-outfitter.lua index 8c953d3cd6..def40eb6e5 100644 --- a/data/pigui/libs/equipment-outfitter.lua +++ b/data/pigui/libs/equipment-outfitter.lua @@ -137,6 +137,7 @@ function Outfitter:Constructor() self.station = nil ---@type SpaceStation self.filterSlot = nil ---@type ShipDef.Slot? self.replaceEquip = nil ---@type EquipType? + self.canSellEquip = false self.equipmentList = {} ---@type UI.EquipmentOutfitter.EquipData[] self.selectedEquip = nil ---@type UI.EquipmentOutfitter.EquipData? @@ -374,7 +375,8 @@ function Outfitter:drawSellButton(data) local icon = icons.autopilot_undock_illegal local price_text = ui.Format.Money(self:getSellPrice(data.equip)) - if customButton(l.SELL_EQUIP % data, icon, price_text) then + local variant = self.canSellEquip and ui.theme.buttonColors.dark or ui.theme.buttonColors.disabled + if customButton(l.SELL_EQUIP % data, icon, price_text, variant) and self.canSellEquip then self:message("onSellItem", data.equip) end end diff --git a/data/pigui/libs/ship-equipment.lua b/data/pigui/libs/ship-equipment.lua index a856e08675..5c02e6475a 100644 --- a/data/pigui/libs/ship-equipment.lua +++ b/data/pigui/libs/ship-equipment.lua @@ -5,7 +5,10 @@ local Game = require 'Game' local ShipDef = require 'ShipDef' local ModelSpinner = require 'PiGui.Modules.ModelSpinner' local EquipCard = require 'pigui.libs.equip-card' +local EquipSet = require 'EquipSet' +local pigui = require 'Engine'.pigui local Vector2 = Vector2 + local utils = require 'utils' local Module = require 'pigui.libs.module' @@ -29,16 +32,15 @@ local equipmentInfoTab ---@field New fun(id): self local EquipmentWidget = utils.class("UI.EquipmentWidget", Module) --- Equipment item grouping by underlying slot type --- TODO: significant refactor to slot system to reduce highly-specialized slots -local sections = { - { name = le.PROPULSION, types = { "engine", "thruster", "hyperdrive" }, showCapacity = true }, - { name = le.WEAPONS, type = "weapon", showCapacity = true }, - { name = le.MISSILES, types = { "pylon", "missile" }, showCapacity = true }, - { name = le.SHIELDS, type = "shield", showCapacity = true }, - { name = le.SENSORS, type = "sensor", showCapacity = true, }, - { name = le.COMPUTER_MODULES, type = "computer", showCapacity = true, }, - { name = le.HULL_MOUNTS, types = { "hull", "utility" }, showCapacity = true }, +-- Equipment item grouping by underlying slot kinds +EquipmentWidget.Sections = { + { name = le.PROPULSION, types = { "engine", "thruster", "hyperdrive" } }, + { name = le.WEAPONS, type = "weapon" }, + { name = le.MISSILES, types = { "pylon", "missile_rack" } }, + { name = le.SHIELDS, type = "shield" }, + { name = le.SENSORS, type = "sensor", }, + { name = le.COMPUTER_MODULES, type = "computer", }, + { name = le.HULL_MOUNTS, types = { "hull", "utility" } }, } -- @@ -65,6 +67,7 @@ function EquipmentWidget:onBuyItem(equip) assert(self.ship:GetComponent("EquipSet"):Install(equip, self.selectedSlot)) self.selectedEquip = equip + self:buildSections() end ---@param equip EquipType @@ -80,6 +83,7 @@ function EquipmentWidget:onSellItem(equip) assert(self.ship:GetComponent("EquipSet"):Remove(equip)) self.selectedEquip = nil + self:buildSections() end -- @@ -108,7 +112,6 @@ function EquipmentWidget:Constructor(id) end) self.market:hookMessage("onClose", function(_, item) - print("onClose") self:clearSelection() self.market.replaceEquip = nil @@ -143,6 +146,8 @@ function EquipmentWidget:Constructor(id) self.activeTab = 1 self.id = id or "EquipmentWidget" + + self.sectionList = {} end function EquipmentWidget:clearSelection() @@ -151,36 +156,28 @@ function EquipmentWidget:clearSelection() self.selectionActive = false end -function EquipmentWidget:onSelectEquip(equipDetail) - if self.selectionActive and not self.selectedSlot and self.selectedEquip == equipDetail then +function EquipmentWidget:onSelectSlot(slotData, children) + if not slotData then self:clearSelection() return end - if self.station then - self.selectedEquip = equipDetail - self.selectedSlot = nil - self.selectionActive = true - - self.market.filterSlot = nil - self.market.replaceEquip = self.selectedEquip - self.market:refresh() - end -end + local isSelected = self.selectedEquip == slotData.equip + and self.selectedSlot == slotData.slot -function EquipmentWidget:onSelectSlot(slot) - if self.selectionActive and self.selectedSlot == slot then + if self.selectionActive and isSelected then self:clearSelection() return end if self.station then - self.selectedSlot = slot - self.selectedEquip = self.ship:GetComponent("EquipSet"):GetItemInSlot(slot) + self.selectedSlot = slotData.slot + self.selectedEquip = slotData.equip self.selectionActive = true self.market.filterSlot = self.selectedSlot self.market.replaceEquip = self.selectedEquip + self.market.canSellEquip = not children or children.count == 0 self.market:refresh() end end @@ -189,24 +186,142 @@ function EquipmentWidget:onSetShipName(newName) self.ship:SetShipName(newName) end +-- Return the translated name of a slot, falling back to a generic name for the +-- slot type if none is specified. +---@param slot ShipDef.Slot +function EquipmentWidget:getSlotName(slot) + if slot.i18n_key then + return Lang.GetResource(slot.i18n_res)[slot.i18n_key] + end + + local base_type = slot.type:match("(%w+)%.?") + local i18n_key = (slot.hardpoint and "HARDPOINT_" or "SLOT_") .. base_type:upper() + return le[i18n_key] +end + +function EquipmentWidget:buildSlotSubgroup(equipSet, equip, card) + local slots = utils.build_array(pairs(equip.provides_slots)) + local subGroup = self:buildSlotGroup(equipSet, equip:GetName(), slots) + + -- Subgroups use the table identity of the parent equipment item as a + -- stable identifier + subGroup.id = tostring(equip) + + card.present = subGroup.count + card.total = subGroup.countMax + + return subGroup +end + +function EquipmentWidget:buildSlotGroup(equipSet, name, slots) + local items = {} + local children = {} + local occupied = 0 + local totalWeight = 0 + + -- Sort the table of slots lexicographically + local names = utils.map_table(slots, function(_, slot) return slot, self:getSlotName(slot) end) + table.sort(slots, function(a, b) return names[a] < names[b] or (names[a] == names[b] and a.id < b.id) end) + + for i, slot in ipairs(slots) do + local equip = equipSet:GetItemInSlot(slot) + + -- Build item cards for all slots + local slotData = EquipCard.getDataForEquip(equip) + + slotData.type = names[slot] + slotData.size = slotData.size or ("S" .. slot.size) + slotData.count = slotData.count or slot.count + slotData.slot = slot + + if equip then + occupied = occupied + 1 + totalWeight = totalWeight + equip.mass + + if equip.provides_slots then + children[i] = self:buildSlotSubgroup(equipSet, equip, slotData) + totalWeight = totalWeight + children[i].weight + end + end + + table.insert(items, slotData) + end + + return { + name = name, + items = items, + children = children, + count = occupied, + countMax = #slots, + weight = totalWeight + } +end + +function EquipmentWidget:buildSections() + self.sectionList = {} + + local equipSet = self.ship:GetComponent("EquipSet") + local config = equipSet.config + + for i, section in ipairs(EquipmentWidget.Sections) do + local slots = {} + + for _, type in ipairs(section.types or { section.type }) do + for id, slot in pairs(config.slots) do + local matches = EquipSet.SlotTypeMatches(slot.type, type) and (section.hardpoint == nil or section.hardpoint == slot.hardpoint) + + if matches then + table.insert(slots, slot) + end + end + end + + table.insert(self.sectionList, self:buildSlotGroup(equipSet, section.name, slots)) + end + + local nonSlot = equipSet:GetInstalledNonSlot() + + -- Sort non-slot equipment by total mass + table.sort(nonSlot, function(a, b) + return a.mass > b.mass or (a.mass == b.mass and a:GetName() < b:GetName()) + end) + + local equipCards = {} + local children = {} + local equipWeight = 0.0 + + for i, equip in ipairs(nonSlot) do + local card = EquipCard.getDataForEquip(equip) + + equipWeight = equipWeight + equip.mass + + if equip.provides_slots then + children[i] = self:buildSlotSubgroup(equipSet, equip, card) + equipWeight = equipWeight + children[i].weight + end + + table.insert(equipCards, card) + end + + table.insert(self.sectionList, { + name = le.MISC_EQUIPMENT, + items = equipCards, + children = children, + count = #nonSlot, + weight = equipWeight, + isMiscEquip = true + }) +end + equipmentInfoTab = { name = l.EQUIPMENT, ---@param self UI.EquipmentWidget draw = function(self) ui.withFont(pionillium.body, function() - local equipSet = self.ship:GetComponent("EquipSet") - - for i, section in ipairs(sections) do - local slots = {} - - for _, type in ipairs(section.types or { section.type }) do - table.append(slots, equipSet:GetAllSlotsOfType(type, section.hardpoint)) - end - - self:drawSlotSection(section.name, slots) + for _, section in ipairs(self.sectionList) do + self:drawSlotGroup(section) end - self:drawEquipSection(le.MISC, equipSet:GetInstalledNonSlot()) end) end } @@ -219,9 +334,6 @@ equipmentInfoTab = { -- Wrapper for EquipCard which handles updating the "last hovered" information function EquipmentWidget:drawEquipmentItem(data, isSelected) - -- Apply tree indent from left - ui.addCursorPos(Vector2(lineSpacing.x, 0)) - local pos = ui.getCursorScreenPos() local isClicked, isHovered, size = EquipCard:draw(data, isSelected) @@ -239,19 +351,6 @@ end -- ============================================================================= -- --- Return the translated name of a slot, falling back to a generic name for the --- slot type if none is specified. ----@param slot ShipDef.Slot -function EquipmentWidget:getSlotName(slot) - if slot.i18n_key then - return Lang.GetResource(slot.i18n_res)[slot.i18n_key] - end - - local base_type = slot.type:match("(%w+)%.?") - local i18n_key = (slot.hardpoint and "HARDPOINT_" or "SLOT_") .. base_type:upper() - return le[i18n_key] -end - -- Show an inline detail on a section header line with optional tooltip ---@param cellEnd Vector2 local function drawHeaderDetail(cellEnd, text, icon, tooltip, textOffsetY) @@ -260,20 +359,27 @@ local function drawHeaderDetail(cellEnd, text, icon, tooltip, textOffsetY) ui.setCursorPos(iconPos) ui.icon(icon, iconSize, colors.white) + local tl = ui.getItemRect() ui.setCursorPos(textStart + Vector2(0, textOffsetY or 0)) ui.text(text) + local br = select(2, ui.getItemRect()) - local wp = ui.getWindowPos() - if tooltip and ui.isMouseHoveringRect(wp + iconPos, wp + cellEnd + Vector2(0, iconSize.y)) then + if tooltip and ui.isMouseHoveringRect(tl, br) then ui.setTooltip(tooltip) end end -- Draw an equipment section header -function EquipmentWidget:drawSectionHeader(name, numItems, totalWeight, maxSlots) +function EquipmentWidget:drawSectionHeader(section, fun) -- This function makes heavy use of draw cursor manipulation to achieve -- complicated layout goals + + local name = section.name + local totalWeight = section.weight + local numItems = section.count + local maxSlots = section.countMax + ---@type boolean, Vector2, Vector2 local sectionOpen, contentsPos, cursorPos @@ -306,114 +412,80 @@ function EquipmentWidget:drawSectionHeader(name, numItems, totalWeight, maxSlots ui.setCursorPos(contentsPos) - return sectionOpen -end - --- Calculate information about an equipment category for displaying ship internal equipment ----@param slots ShipDef.Slot[] -function EquipmentWidget:calcEquipSectionInfo(slots) - local EquipSet = self.ship:GetComponent("EquipSet") - - local names = {} - local occupied = 0 - local totalWeight = 0 - - for _, slot in ipairs(slots) do - names[slot] = self:getSlotName(slot) - - local item = EquipSet:GetItemInSlot(slot) - if item then - occupied = occupied + 1 - totalWeight = totalWeight + item.mass - end + if sectionOpen then + fun() + ui.treePop() end - return names, occupied, totalWeight + return sectionOpen end --- Draw an equipment section and all contained equipment items ----@param equipment EquipType[] -function EquipmentWidget:drawEquipSection(name, equipment) +function EquipmentWidget:drawOpenHeader(id, defaultOpen, fun) + local isOpen = pigui.GetBoolState(id, defaultOpen) - -- local slots = data.slots or { data.slot } - -- local equipment, maxSlots, weight = self:calcEquipSectionInfo(slots) - local weight = 0 - for _, equip in ipairs(equipment) do - weight = weight + equip.mass + if isOpen then + ui.treePush(id) + fun() + ui.treePop() end - local sectionOpen = self:drawSectionHeader(name, #equipment, weight) - - if sectionOpen then - -- heaviest items to the top, then stably sort based on name - table.sort(equipment, function(a, b) - local mass = a.mass - b.mass - return mass > 0 or (mass == 0 and a:GetName() < b:GetName()) - end) + local iconSize = Vector2(ui.getTextLineHeight()) - -- Draw each equipment item in this section - for i, equip in ipairs(equipment) do - local equipData = EquipCard.getDataForEquip(equip) - local isSelected = self.selectedEquip == equip + local clicked = ui.invisibleButton(id, Vector2(ui.getContentRegion().x, ui.getTextLineHeight())) + local tl, br = ui.getItemRect() - if self:drawEquipmentItem(equipData, isSelected) then - self:message("onSelectEquip", equip) - end - end + local color = ui.getButtonColor(ui.theme.buttonColors.transparent, ui.isItemHovered(), ui.isItemActive()) + ui.addRectFilled(tl, br, color, ui.theme.styles.ItemCardRounding, 0xF) - -- If we have more slots available in this section, show an empty slot - if self.showEmptySlots then - local equipData = EquipCard.getDataForEquip(nil) - local isSelected = self.selectionActive and not self.selectedSlot and not self.selectedEquip + ui.addIconSimple((tl + br - iconSize) * 0.5, + isOpen and icons.chevron_up or icons.chevron_down, + iconSize, colors.fontDim) - if self:drawEquipmentItem(equipData, isSelected) then - self:message("onSelectEquip", nil) - end - end + ui.setItemTooltip(isOpen and l.COLLAPSE or l.EXPAND) - ui.treePop() + if clicked then + pigui.SetBoolState(id, not isOpen) end - end --- Draw a list of equipment slots and contained items ----@param slots ShipDef.Slot[] -function EquipmentWidget:drawSlotSection(name, slots) - local names, numFull, totalMass = self:calcEquipSectionInfo(slots) - - local sectionOpen = self:drawSectionHeader(name, numFull, totalMass, #slots) - if not sectionOpen then - return - end - - -- Sort by slot name first, then internal ID for tiebreaker - table.sort(slots, function(a, b) - return names[a] < names[b] or (names[a] == names[b] and a.id < b.id) - end) +function EquipmentWidget:drawSlotGroup(list) + local drawList = function() + for i, card in ipairs(list.items) do - local equipSet = self.ship:GetComponent("EquipSet") + local equip = card.equip + local isSelected = self.selectedSlot == card.slot or + (not card.slot) and self.selectedEquip == equip - for _, slot in ipairs(slots) do - local isSelected = self.selectedSlot == slot - local equip = equipSet:GetItemInSlot(slot) + if equip or self.showEmptySlots then + if self:drawEquipmentItem(card, isSelected) then + self:message("onSelectSlot", card, list.children[i]) + end - if equip or self.showEmptySlots then + local childSlots = list.children[i] + if childSlots then + ui.withID(childSlots.id, function() + self:drawSlotGroup(childSlots) + end) + end + end - local slotData = EquipCard.getDataForEquip(equip) + end - slotData.size = slotData.size or ("S" .. slot.size) - slotData.type = names[slot] - -- Use the equipment count if present - slotData.count = slotData.count or slot.count + if self.showEmptySlots and list.isMiscEquip then + local card = EquipCard.getDataForEquip(nil) + local isSelected = self.selectionActive and not self.selectedSlot and not self.selectedEquip - if self:drawEquipmentItem(slotData, isSelected) then - self:message("onSelectSlot", slot) + if self:drawEquipmentItem(card, isSelected) then + self:message("onSelectSlot", card) end - end end - ui.treePop() + if list.id then + self:drawOpenHeader(list.id, self.showEmptySlots or list.count > 0, drawList) + else + self:drawSectionHeader(list, drawList) + end end -- @@ -522,7 +594,7 @@ function EquipmentWidget:refresh() self.market.filterSlot = nil self.market.replaceEquip = nil - self.market:refresh() + self:buildSections() end function EquipmentWidget:debugReload() diff --git a/data/pigui/themes/default.lua b/data/pigui/themes/default.lua index 1842f83735..223e61e168 100644 --- a/data/pigui/themes/default.lua +++ b/data/pigui/themes/default.lua @@ -677,6 +677,9 @@ theme.icons = { circle_md = 51, circle_sm = 110, + chevron_up = 38, + chevron_down = 40, + -- TODO: manual / autopilot -- dummy, until actually defined correctly mouse_move_direction = 14, From cd00604ea5eca6da4a50d025b8abf5cd83129884 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Tue, 17 Sep 2024 14:42:07 -0400 Subject: [PATCH 039/119] Outfitter: select adjacent slots on purchase, double-click to buy --- data/pigui/libs/equipment-outfitter.lua | 6 ++++++ data/pigui/libs/ship-equipment.lua | 27 +++++++++++++++++++------ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/data/pigui/libs/equipment-outfitter.lua b/data/pigui/libs/equipment-outfitter.lua index def40eb6e5..4692b1683c 100644 --- a/data/pigui/libs/equipment-outfitter.lua +++ b/data/pigui/libs/equipment-outfitter.lua @@ -508,6 +508,12 @@ function Outfitter:render() if clicked then self:message("onSelectItem", data) end + + local doubleClicked = clicked and ui.isMouseDoubleClicked(0) + + if doubleClicked then + self:message("onBuyItem", data.equip) + end end end) diff --git a/data/pigui/libs/ship-equipment.lua b/data/pigui/libs/ship-equipment.lua index 5c02e6475a..051c33a4c0 100644 --- a/data/pigui/libs/ship-equipment.lua +++ b/data/pigui/libs/ship-equipment.lua @@ -66,7 +66,7 @@ function EquipmentWidget:onBuyItem(equip) assert(self.ship:GetComponent("EquipSet"):Install(equip, self.selectedSlot)) - self.selectedEquip = equip + self.selectedEquip = self.selectedSlot and equip self:buildSections() end @@ -100,6 +100,13 @@ function EquipmentWidget:Constructor(id) self.market:hookMessage("onBuyItem", function(_, item) self:onBuyItem(item) + if self.selectedSlot then + local nextSlot = self.adjacentSlots[self.selectedSlot] + if nextSlot and not nextSlot.equip then + self:onSelectSlot(nextSlot) + end + end + self.market.replaceEquip = self.selectedEquip self.market:refresh() end) @@ -148,6 +155,8 @@ function EquipmentWidget:Constructor(id) self.id = id or "EquipmentWidget" self.sectionList = {} + ---@type table + self.adjacentSlots = {} end function EquipmentWidget:clearSelection() @@ -156,6 +165,8 @@ function EquipmentWidget:clearSelection() self.selectionActive = false end +---@param slotData UI.EquipCard.Data +---@param children table? function EquipmentWidget:onSelectSlot(slotData, children) if not slotData then self:clearSelection() @@ -244,6 +255,11 @@ function EquipmentWidget:buildSlotGroup(equipSet, name, slots) end end + local prevCard = items[#items] + if prevCard and prevCard.slot then + self.adjacentSlots[prevCard.slot] = slotData + end + table.insert(items, slotData) end @@ -259,6 +275,7 @@ end function EquipmentWidget:buildSections() self.sectionList = {} + self.adjacentSlots = {} local equipSet = self.ship:GetComponent("EquipSet") local config = equipSet.config @@ -453,8 +470,8 @@ function EquipmentWidget:drawSlotGroup(list) for i, card in ipairs(list.items) do local equip = card.equip - local isSelected = self.selectedSlot == card.slot or - (not card.slot) and self.selectedEquip == equip + local isSelected = self.selectionActive + and (self.selectedSlot == card.slot and self.selectedEquip == equip) if equip or self.showEmptySlots then if self:drawEquipmentItem(card, isSelected) then @@ -463,9 +480,7 @@ function EquipmentWidget:drawSlotGroup(list) local childSlots = list.children[i] if childSlots then - ui.withID(childSlots.id, function() - self:drawSlotGroup(childSlots) - end) + self:drawSlotGroup(childSlots) end end From 324b4abda35dd41a33c54bd58d709ee4a2964269 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 18 Sep 2024 01:20:19 -0400 Subject: [PATCH 040/119] Outfitter: allow user to sort market list - Add four sort states in addition to the default sort algorithm - Sort state is persistent across slot selection - Item card tooltips now use the default hover delay - Cleanup market purchase-mode header --- data/lang/equipment-core/en.json | 16 ++++ data/lang/ui-core/en.json | 6 +- data/pigui/libs/equipment-outfitter.lua | 117 +++++++++++++++++++----- data/pigui/libs/item-card.lua | 4 +- 4 files changed, 117 insertions(+), 26 deletions(-) diff --git a/data/lang/equipment-core/en.json b/data/lang/equipment-core/en.json index 0bb0a521e0..9f9a9f2fe3 100644 --- a/data/lang/equipment-core/en.json +++ b/data/lang/equipment-core/en.json @@ -526,5 +526,21 @@ "MISC_EQUIPMENT": { "description": "Category header for miscellaneous equipment", "message": "Miscellaneous" + }, + "SORT_NAME": { + "description": "", + "message": "Name" + }, + "SORT_WEIGHT": { + "description": "", + "message": "Weight" + }, + "SORT_VOLUME": { + "description": "", + "message": "Volume" + }, + "SORT_PRICE": { + "description": "", + "message": "Price" } } diff --git a/data/lang/ui-core/en.json b/data/lang/ui-core/en.json index b0748bdc22..d7d9dd49d8 100644 --- a/data/lang/ui-core/en.json +++ b/data/lang/ui-core/en.json @@ -65,7 +65,7 @@ }, "AVAILABLE_FOR_PURCHASE": { "description": "Market header when buying items", - "message": "Available For Purchase:" + "message": "Available For Purchase" }, "AVERAGE": { "description": "", @@ -2011,9 +2011,9 @@ "description": "", "message": "Repair {damage}% hull damage for {price}" }, - "REPLACE_EQUIPMENT_WITH": { + "REPLACE_EQUIPMENT": { "description": "Market header when replacing equipped items", - "message": "Replace Equipment With:" + "message": "Replace Equipment" }, "REPUTATION": { "description": "", diff --git a/data/pigui/libs/equipment-outfitter.lua b/data/pigui/libs/equipment-outfitter.lua index 4692b1683c..f451d458ba 100644 --- a/data/pigui/libs/equipment-outfitter.lua +++ b/data/pigui/libs/equipment-outfitter.lua @@ -11,6 +11,7 @@ local utils = require 'utils' local ui = require 'pigui' local pionillium = ui.fonts.pionillium +local orbiteer = ui.fonts.orbiteer local colors = ui.theme.colors local icons = ui.theme.icons @@ -20,6 +21,7 @@ local Module = require 'pigui.libs.module' local EquipCard = require 'pigui.libs.equip-card' local l = Lang.GetResource("ui-core") +local le = Lang.GetResource("equipment-core") local compare = function(a, b, invert) local n = invert and b - a or a - b @@ -103,9 +105,7 @@ EquipCardAvailable.tooltipStats = false local EquipCardUnavailable = EquipCard.New() EquipCardUnavailable.tooltipStats = false -EquipCardUnavailable.backgroundColor = ui.theme.styleColors.gray_800 -EquipCardUnavailable.hoveredColor = ui.theme.styleColors.gray_700 -EquipCardUnavailable.selectedColor = ui.theme.styleColors.gray_600 +EquipCardUnavailable.colors = ui.theme.buttonColors.deselected EquipCardUnavailable.textColor = ui.theme.styleColors.danger_300 ---@param data UI.EquipmentOutfitter.EquipData @@ -130,6 +130,37 @@ end ---@field New fun(): self local Outfitter = utils.class("UI.EquipmentOutfitter", Module) +Outfitter.SortKeys = { + { id = "name", label = le.SORT_NAME }, + { id = "mass", label = le.SORT_WEIGHT }, + { id = "volume", label = le.SORT_VOLUME }, + { id = "price", label = le.SORT_PRICE }, +} + +---@type table +Outfitter.SortFuncs = { + name = function(a, b) + return a.name < b.name + end, + mass = function(a, b) + return a.equip.mass < b.equip.mass or (a.equip.mass == b.equip.mass and a.name < b.name) + end, + volume = function(a, b) + return a.equip.volume < b.equip.volume or (a.equip.volume == b.equip.volume and a.name < b.name) + end, + price = function(a, b) + return a.price < b.price or (a.price == b.price and a.name < b.name) + end, + default = function(a, b) + if a.equip.slot then + return a.equip.slot.size < b.equip.slot.size + or (a.equip.slot.size == b.equip.slot.size and a.name < b.name) + else + return a.name < b.name + end + end +} + function Outfitter:Constructor() Module.Constructor(self) @@ -139,6 +170,9 @@ function Outfitter:Constructor() self.replaceEquip = nil ---@type EquipType? self.canSellEquip = false + self.sortId = nil ---@type string? + self.sortState = nil ---@type integer? + self.equipmentList = {} ---@type UI.EquipmentOutfitter.EquipData[] self.selectedEquip = nil ---@type UI.EquipmentOutfitter.EquipData? self.currentEquip = nil ---@type UI.EquipCard.Data? @@ -220,15 +254,6 @@ function Outfitter:getInstallPrice(e) return self:getBuyPrice(e) - (self.replaceEquip and self:getSellPrice(self.replaceEquip) or 0) end -function Outfitter.sortEquip(e1, e2) - if e1.slot then - return e1.slot.size < e2.slot.size - or (e1.slot.size == e2.slot.size and e1:GetName() < e2:GetName()) - else - return e1:GetName() < e2:GetName() - end -end - function Outfitter:buildEquipmentList() local equipment = self:getAvailableEquipment() @@ -239,8 +264,6 @@ function Outfitter:buildEquipmentList() table.insert(equipList, v) end - table.sort(equipList, self.sortEquip) - local currentProto = self.replaceEquip and self.replaceEquip:GetPrototype() local equipSet = self.ship:GetComponent("EquipSet") local money = Game.player:GetMoney() @@ -271,6 +294,16 @@ function Outfitter:buildEquipmentList() return data end) + + self:sortEquipmentList() +end + +function Outfitter:sortEquipmentList() + local sortFunc = self.sortId and Outfitter.SortFuncs[self.sortId] or Outfitter.SortFuncs.default + + table.sort(self.equipmentList, function(a, b) + return not sortFunc(a, b) ~= (not self.sortState or self.sortState > 0) + end) end local emptyStats = {} @@ -333,6 +366,13 @@ function Outfitter:onSellItem(item) self.selectedEquip = nil end +function Outfitter:onSetSort(id, state) + self.sortId = id + self.sortState = state + + self:sortEquipmentList() +end + function Outfitter:onClose() return end @@ -381,6 +421,35 @@ function Outfitter:drawSellButton(data) end end +function Outfitter:drawSortButton(id, label, state) + local variant = state and ui.theme.buttonColors.dark or ui.theme.buttonColors.deselected + local sortIcon = "" + + if state then + sortIcon = (state > 0 and ui.get_icon_glyph(icons.chevron_up) or ui.get_icon_glyph(icons.chevron_down)) .. " " + end + + local clicked = ui.button(sortIcon .. label, Vector2(ui.getContentRegion().x, 0), variant, nil, styles.IconButtonPadding) + + if clicked then + state = (not state and 1) or (state > 0 and -1) or nil + + self:message("onSetSort", state and id, state) + end +end + +function Outfitter:drawSortRow() + ui.beginTable("sort", #Outfitter.SortKeys) + ui.tableNextRow() + + for i, sort in ipairs(Outfitter.SortKeys) do + ui.tableSetColumnIndex(i - 1) + self:drawSortButton(sort.id, sort.label, self.sortId == sort.id and self.sortState or nil) + end + + ui.endTable() +end + ---@param label string ---@param stat_a EquipType.UI.Stats? ---@param stat_b EquipType.UI.Stats? @@ -473,17 +542,23 @@ function Outfitter:render() ui.child("##ListPane", Vector2(panelWidth, 0), function() ui.withStyleVars({ FrameRounding = 4 }, function() - if ui.button("<") then - self:message("onClose") - end - end) + ui.withFont(orbiteer.heading, function() + if ui.iconButton("back", icons.decrease_thick, l.CLOSE, nil, nil, styles.IconButtonPadding * 1.5) then + self:message("onClose") + end - ui.sameLine() + ui.sameLine() - ui.withFont(pionillium.heading, function() - ui.text(self.replaceEquip and l.REPLACE_EQUIPMENT_WITH or l.AVAILABLE_FOR_PURCHASE) + ui.alignTextToFramePadding() + ui.text(self.replaceEquip and l.REPLACE_EQUIPMENT or l.AVAILABLE_FOR_PURCHASE) + end) + + ui.withFont(pionillium.details, function() + self:drawSortRow() + end) end) + ui.spacing() local buttonLineHeight = 0.0 diff --git a/data/pigui/libs/item-card.lua b/data/pigui/libs/item-card.lua index 2eb776fa58..ac8fba0bab 100644 --- a/data/pigui/libs/item-card.lua +++ b/data/pigui/libs/item-card.lua @@ -149,7 +149,7 @@ function ItemCard:draw(data, isSelected) local offset = fieldSize * (i - 1) + smIconSize.x + 2 ui.setCursorScreenPos(pos + Vector2(offset, 1)) -- HACK: force 1-pixel offset here to align baselines ui.text(v[2]) - if v[3] and ui.isItemHovered() then + if v[3] and ui.isItemHovered("ForTooltip") then detailTooltip = v end end @@ -174,7 +174,7 @@ function ItemCard:draw(data, isSelected) ui.endGroup() - if ui.isItemHovered() and not detailTooltip then + if ui.isItemHovered("ForTooltip") and not detailTooltip then self:drawTooltip(data, isSelected) end From 159d77745125322ce4ff56a4847ee4a40cb597dc Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 18 Sep 2024 16:01:34 -0400 Subject: [PATCH 041/119] ShipBuilder: handle recursive slots - Reduce ship hull threat restrictions - Pre-sort list of available slots in ship plan, update when adding new slots from equipment - Install equipment by selection order (prevents installing equipment in slot that doesn't exist yet) - Add test/debug spawner menu, remove test onEnterMainMenu event hook - Default ship rules are accessible from ShipBuilder.OutfitRules --- .../modules/MissionUtils/DebugShipBuilder.lua | 131 +++++++++++----- data/modules/MissionUtils/ShipBuilder.lua | 142 ++++++++++++------ 2 files changed, 189 insertions(+), 84 deletions(-) diff --git a/data/modules/MissionUtils/DebugShipBuilder.lua b/data/modules/MissionUtils/DebugShipBuilder.lua index 3a90c28975..7409b7b83d 100644 --- a/data/modules/MissionUtils/DebugShipBuilder.lua +++ b/data/modules/MissionUtils/DebugShipBuilder.lua @@ -4,62 +4,123 @@ local ShipDef = require 'ShipDef' local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' local ShipConfig = require 'ShipConfig' +local Rules = ShipBuilder.OutfitRules + local ui = require 'pigui' +local debugRule = ShipBuilder.Template:clone { + label = "TEST SHIP", + shipId = 'coronatrix', + rules = { + { + slot = "weapon", + filter = "weapon.energy", + limit = 1 + }, + { + slot = "missile_rack", + maxSize = 2 + }, + { + slot = "missile", + maxSize = 2, + minSize = 2, + limit = 4 + }, + { + slot = "missile", + maxSize = 1, + minSize = 1, + }, + { + slot = "shield", + maxSize = 1, + limit = 2 + }, + Rules.DefaultHyperdrive, + Rules.DefaultLaserCooling, + Rules.DefaultAtmoShield, + } +} + debugView.registerTab("ShipBuilder", { + label = "Ship Builder", + icon = ui.theme.icons.ship, + selectedHull = nil, + spawnThreat = 20.0, + + show = function() return not require 'Game':InHyperspace() end, + draw = function(self) - if not ui.beginTabItem("ShipBuilder") then - return + self.spawnThreat = ui.sliderFloat("Threat", self.spawnThreat, 1.0, 200.0) + + if ui.button("Spawn Test Ship") then + ShipBuilder.MakeShipNear(require 'Game'.player, debugRule, self.spawnThreat, 15.00, 20.00) end - if self.selectedHull then - if ui.button("<") then - self.selectedHull = nil + local target = require 'Game'.player:GetCombatTarget() - ui.endTabItem() - return - end + if target then + ui.text("Equipment:") + ui.spacing() + + local equipSet = target:GetComponent('EquipSet') - ui.sameLine() - ui.text(self.selectedHull) + for id, equip in pairs(equipSet:GetInstalledEquipment()) do + ui.text(id .. ": " .. equip:GetName()) + end ui.spacing() + end - if ui.collapsingHeader("Hull Threat") then - local threat = ShipBuilder.GetHullThreat(self.selectedHull) + ui.child("hullList", function() + if self.selectedHull then + if ui.button("<") then + self.selectedHull = nil - for k, v in pairs(threat) do - ui.text(tostring(k) .. ": " .. tostring(v)) + ui.endTabItem() + return end - end - if ui.collapsingHeader("Slots") then - local config = ShipConfig[self.selectedHull] + ui.sameLine() + ui.text(self.selectedHull) + + ui.spacing() - for k, v in pairs(config.slots) do - ui.text(k) + if ui.collapsingHeader("Hull Threat") then + local threat = ShipBuilder.GetHullThreat(self.selectedHull) - local data = " " - for k2, v2 in pairs(v) do - data = data .. tostring(k2) .. " = " .. tostring(v2) .. ", " + for k, v in pairs(threat) do + ui.text(tostring(k) .. ": " .. tostring(v)) end - - ui.text(data) end - end - else - for id, def in pairs(ShipDef) do - if ui.selectable(id) then - self.selectedHull = id + + if ui.collapsingHeader("Slots") then + local config = ShipConfig[self.selectedHull] + + for k, v in pairs(config.slots) do + ui.text(k) + + local data = " " + for k2, v2 in pairs(v) do + data = data .. tostring(k2) .. " = " .. tostring(v2) .. ", " + end + + ui.text(data) + end end + else + for id, def in pairs(ShipDef) do + if ui.selectable(id) then + self.selectedHull = id + end - local threat = ShipBuilder.GetHullThreat(id) - ui.sameLine() - ui.text("threat: " .. tostring(threat.total)) + local threat = ShipBuilder.GetHullThreat(id) + ui.sameLine() + ui.text("threat: " .. tostring(threat.total)) + end end - end - - ui.endTabItem() + end) end }) diff --git a/data/modules/MissionUtils/ShipBuilder.lua b/data/modules/MissionUtils/ShipBuilder.lua index e0a8d50034..1d76c09698 100644 --- a/data/modules/MissionUtils/ShipBuilder.lua +++ b/data/modules/MissionUtils/ShipBuilder.lua @@ -4,6 +4,7 @@ local Engine = require 'Engine' local Equipment = require 'Equipment' local EquipSet = require 'EquipSet' +local Event = require 'Event' local ShipDef = require 'ShipDef' local ShipConfig = require 'ShipConfig' local Space = require 'Space' @@ -15,9 +16,7 @@ local utils = require 'utils' local hullThreatCache = {} -local slotTypeMatches = function(slot, filter) - return string.sub(slot, 1, #filter) == filter -end +local slotTypeMatches = EquipSet.SlotTypeMatches -- Class: MissionUtils.ShipBuilder -- @@ -26,6 +25,8 @@ end ---@class MissionUtils.ShipBuilder local ShipBuilder = {} +ShipBuilder.OutfitRules = Rules + -- ============================================================================= -- Scalars determining a "threat factor" rating for a ship's hull and equipment @@ -55,10 +56,8 @@ ShipBuilder.kAeroStabilityThreatBase = 0.80 ShipBuilder.kCrossSectionToThreat = 0.3 ShipBuilder.kCrossSectionThreatBase = 0.8 --- Only accept ships where the hull is at least this fraction of the desired total threat factor -ShipBuilder.kMinHullThreatFactor = 0.4 -ShipBuilder.kMaxHullThreatFactor = 0.8 -ShipBuilder.kMinReservedEquipThreat = 4.0 +-- Only accept ships where the hull is at most this fraction of the desired total threat factor +ShipBuilder.kMaxHullThreatFactor = 0.7 -- || Weapon Threat Factor || @@ -105,17 +104,55 @@ ShipPlan.freeVolume = 0 ShipPlan.equipMass = 0 ShipPlan.threat = 0 ShipPlan.freeThreat = 0 -ShipPlan.slots = {} +ShipPlan.filled = {} ShipPlan.equip = {} +ShipPlan.install = {} +ShipPlan.slots = {} function ShipPlan:__clone() - self.slots = {} + self.filled = {} self.equip = {} + self.install = {} + self.slots = {} +end + +function ShipPlan:SortSlots() + -- Stably sort with largest hardpoints first + table.sort(self.slots, function(a, b) return a.size > b.size or (a.size == b.size and a.id < b.id) end) +end + +function ShipPlan:SetConfig(shipConfig) + self.config = shipConfig + self.shipId = shipConfig.id + self.freeVolume = shipConfig.capacity + + for _, slot in pairs(shipConfig.slots) do + table.insert(self.slots, slot) + end + + self:SortSlots() +end + +-- Add extra slots from an equipment item to the list of available slots +function ShipPlan:AddSlots(baseId, slots) + for _, slot in pairs(slots) do + local id = baseId .. "##" .. slot.id + table.insert(self.slots, slot:clone({ id = id })) + end + + self:SortSlots() end function ShipPlan:AddEquipToPlan(equip, slot, threat) + print("Installing " .. equip:GetName()) + + if equip:isProto() then + equip = equip:Instance() + end + if slot then - self.slots[slot.id] = equip + self.filled[slot.id] = equip + table.insert(self.install, slot.id) else table.insert(self.equip, equip) end @@ -124,6 +161,10 @@ function ShipPlan:AddEquipToPlan(equip, slot, threat) self.equipMass = self.equipMass + equip.mass self.threat = self.threat + (threat or 0) self.freeThreat = self.freeThreat - (threat or 0) + + if equip.provides_slots then + self:AddSlots(slot.id, equip.provides_slots) + end end -- ============================================================================= @@ -200,31 +241,36 @@ end -- ============================================================================= ---@param shipPlan table ----@param shipConfig ShipDef.Config ---@param rule table ---@param rand Rand ---@param hullThreat number -function ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, rand, hullThreat) +function ShipBuilder.ApplyEquipmentRule(shipPlan, rule, rand, hullThreat) - local matchRuleSlot = function(slot) - return slotTypeMatches(slot.type, rule.slot) - and (not rule.maxSize or slot.size <= rule.maxSize) + -- print("Applying rule:") + -- utils.print_r(rule) + + local shipConfig = shipPlan.config + + local matchRuleSlot = function(slot, filter) + local minSize = slot.size_min or slot.size + return slotTypeMatches(slot.type, filter) + and (not rule.maxSize or minSize <= rule.maxSize) and (not rule.minSize or slot.size >= rule.minSize) end -- Get a list of all equipment slots on the ship that match this rule - local slots = utils.to_array(shipConfig.slots, function(slot) + local slots = utils.filter_array(shipPlan.slots, function(slot) -- Don't install in already-filled slots - return not shipPlan.slots[slot.id] - and matchRuleSlot(slot) + return not shipPlan.filled[slot.id] + and matchRuleSlot(slot, rule.slot) end) + -- print("Ship slots: " .. #shipPlan.slots) + -- print("Filtered slots: " .. #slots) + -- Early-out if we have nowhere to install equipment if #slots == 0 then return end - -- Sort the table of slots so we install in the best/biggest slot first - table.sort(slots, function(a, b) return a.size > b.size or (a.size == b.size and a.id < b.id) end) - -- Track how many items have been installed total local numInstalled = 0 @@ -272,11 +318,12 @@ function ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, rand, hullTh -- Build a list of all equipment items that could potentially be installed local filteredEquip = utils.to_array(Equipment.new, function(equip) return (equip.slot or false) - and matchRuleSlot(equip.slot) + and matchRuleSlot(equip.slot, rule.filter or rule.slot) and equip.volume <= maxVolume - and (not rule.filter or slotTypeMatches(equip.slot.type, rule.filter)) end) + -- print("Available equipment: " .. #filteredEquip) + -- No equipment items can be installed, rule is finished if #filteredEquip == 0 then return @@ -288,6 +335,7 @@ function ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, rand, hullTh -- NOTE: if equipment items don't include hull threat in their calculation, this -- can be precached at startup local threatCache = utils.map_table(filteredEquip, function(_, equip) + print(equip:GetName(), ShipBuilder.ComputeEquipThreatFactor(equip, hullThreat)) return equip, ShipBuilder.ComputeEquipThreatFactor(equip, hullThreat) end) @@ -299,7 +347,7 @@ function ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, rand, hullTh -- specific slot type than the rule itself). ---@type EquipType[] local compatible = utils.map_array(filteredEquip, function(equip) - local compat = EquipSet.CompatibleWithSlot(equip) + local compat = EquipSet.CompatibleWithSlot(equip, slot) and shipPlan.freeThreat >= threatCache[equip] if not compat then @@ -319,6 +367,8 @@ function ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, rand, hullTh return shipPlan.freeVolume >= inst.volume and inst or nil end) + -- print("Slot " .. slot.id .. " - compatible: " .. #compatible) + -- Nothing fits in this slot, ignore it then if #compatible > 0 then @@ -341,7 +391,7 @@ function ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, rand, hullTh numInstalled = numInstalled + 1 - if rule.limit and numInstalled > rule.limit then + if rule.limit and numInstalled >= rule.limit then break end @@ -362,22 +412,22 @@ function ShipBuilder.SelectHull(template, threat) if template.shipId then - table.insert(hullList, template.shipType) + table.insert(hullList, template.shipId) else for id, shipDef in pairs(ShipDef) do - if shipDef.roles[template.role] then + if not template.role or shipDef.roles[template.role] then local hullThreat = ShipBuilder.GetHullThreat(id).total + print(id, hullThreat, threat) + -- Use threat metric as a way to balance the random selection of ship hulls - local withinRange = hullThreat >= ShipBuilder.kMinHullThreatFactor * threat - and hullThreat <= ShipBuilder.kMaxHullThreatFactor * threat - local hasReserve = threat - hullThreat >= ShipBuilder.kMinReservedEquipThreat + local withinRange = hullThreat <= ShipBuilder.kMaxHullThreatFactor * threat - if withinRange and hasReserve then + if withinRange then table.insert(hullList, id) end @@ -403,16 +453,16 @@ function ShipBuilder.MakePlan(production, shipConfig, threat) local hullThreat = ShipBuilder.GetHullThreat(shipConfig.id).total local shipPlan = ShipPlan:clone { - shipId = shipConfig.id, - freeVolume = shipConfig.capacity, threat = hullThreat, freeThreat = threat - hullThreat, } + shipPlan:SetConfig(shipConfig) + for _, rule in ipairs(production.rules) do if rule.slot then - ShipBuilder.ApplyEquipmentRule(shipPlan, shipConfig, rule, Engine.rand, hullThreat) + ShipBuilder.ApplyEquipmentRule(shipPlan, rule, Engine.rand, hullThreat) else local equip = Equipment.Get(rule.equip) assert(equip) @@ -437,15 +487,17 @@ function ShipBuilder.ApplyPlan(ship, shipPlan) local equipSet = ship:GetComponent('EquipSet') -- Apply slot-based equipment first - for name, proto in pairs(shipPlan.slots) do - local slot = equipSet:GetSlotHandle(name) - assert(slot) + for _, slotId in ipairs(shipPlan.install) do + local slot = assert(equipSet:GetSlotHandle(slotId)) + local equip = assert(shipPlan.filled[slotId]) + assert(not equip:isProto()) - equipSet:Install(proto(), slot) + equipSet:Install(equip, slot) end - for _, proto in ipairs(shipPlan.equip) do - equipSet:Install(proto()) + for _, equip in ipairs(shipPlan.equip) do + assert(not equip:isProto()) + equipSet:Install(equip) end -- TODO: ammunition / other items inside of instanced equipment @@ -555,16 +607,8 @@ function ShipBuilder.BuildHullThreatCache() end end -require 'Event'.Register("onEnterMainMenu", function() +Event.Register("onGameStart", function() ShipBuilder.BuildHullThreatCache() - - local threat = 20.0 - - local hull = ShipBuilder.SelectHull({ role = "pirate" }, threat) - - local plan = ShipBuilder.MakePlan(pirateProduction, ShipConfig['coronatrix'], threat) - - utils.print_r(plan) end) return ShipBuilder From ea6a724dd69e9a85d38b4c4ad3b3770b4bcf59c5 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 19 Sep 2024 15:08:27 -0400 Subject: [PATCH 042/119] Rename ShipConfig -> HullConfig - Expose list of hull configs via HullConfig.GetHullConfigs() - Make HullConfig.Slot actually available to the rest of the Lua environment (oops) - Enables future groundwork for multiple hull configs sharing a ShipDef HullConfig: only create configs for actual ships HullConfig: remove default scoop + computer_2 slots - These slots are not common to all ships and are better to set up with each individual hull --- data/libs/EquipSet.lua | 22 ++++---- data/libs/EquipType.lua | 12 ++-- data/libs/{ShipConfig.lua => HullConfig.lua} | 56 +++++++++++-------- .../modules/MissionUtils/DebugShipBuilder.lua | 9 ++- data/modules/MissionUtils/OutfitRules.lua | 10 ---- data/modules/MissionUtils/ShipBuilder.lua | 6 +- data/pigui/libs/equip-card.lua | 2 +- data/pigui/libs/equipment-outfitter.lua | 2 +- data/pigui/libs/ship-equipment.lua | 6 +- 9 files changed, 64 insertions(+), 61 deletions(-) rename data/libs/{ShipConfig.lua => HullConfig.lua} (69%) diff --git a/data/libs/EquipSet.lua b/data/libs/EquipSet.lua index 22825834d1..49dab49458 100644 --- a/data/libs/EquipSet.lua +++ b/data/libs/EquipSet.lua @@ -1,7 +1,7 @@ -- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt -local ShipConfig = require 'ShipConfig' +local HullConfig = require 'HullConfig' local Serializer = require 'Serializer' local utils = require 'utils' @@ -27,7 +27,7 @@ EquipSet.SlotTypeMatches = slotTypeMatches -- Static helper function to check if the given equipment item is compatible -- with the given slot object. Validates type and size parameters of the slot. ---@param equip EquipType ----@param slot ShipDef.Slot? +---@param slot HullConfig.Slot? function EquipSet.CompatibleWithSlot(equip, slot) local equipSlot = equip.slot or false if not slot then return not equipSlot end @@ -41,7 +41,7 @@ end ---@param ship Ship function EquipSet:Constructor(ship) self.ship = ship - self.config = ShipConfig[ship.shipId] + self.config = HullConfig.GetHullConfigs()[ship.shipId] -- Stores a mapping of slot id -> equipment item -- Non-slot equipment is stored in the array portion @@ -53,10 +53,10 @@ function EquipSet:Constructor(ship) -- Stores a mapping of slot id -> slot handle -- Simplifies slot lookup for slots defined on equipment items - self.slotCache = {} ---@type table + self.slotCache = {} ---@type table -- Stores the inverse mapping for looking up the compound id of a slot by -- the slot object itself. - self.idCache = {} ---@type table + self.idCache = {} ---@type table self:BuildSlotCache() @@ -81,7 +81,7 @@ end -- Return a reference to the slot with the given ID managed by this EquipSet. -- The returned slot should be considered immutable. ---@param id string ----@return ShipDef.Slot? +---@return HullConfig.Slot? function EquipSet:GetSlotHandle(id) return self.slotCache[id] end @@ -89,7 +89,7 @@ end -- Function: GetItemInSlot -- -- Return the equipment item installed in the given slot, if present. ----@param slot ShipDef.Slot +---@param slot HullConfig.Slot ---@return EquipType? function EquipSet:GetItemInSlot(slot) -- The equipment item is not stored in the slot itself to reduce savefile @@ -106,7 +106,7 @@ end -- installed. Does not attempt to find the most optimal slot - the first slot -- which meets the type and size constraints for the equipment item is returned. ---@param equip EquipType ----@return ShipDef.Slot? +---@return HullConfig.Slot? function EquipSet:GetFreeSlotForEquip(equip) if not equip.slot then return nil end @@ -131,7 +131,7 @@ end -- If hardpoint is not specified, returns both hardpoint and internal slots. ---@param type string ---@param hardpoint boolean? ----@return ShipDef.Slot[] +---@return HullConfig.Slot[] function EquipSet:GetAllSlotsOfType(type, hardpoint) local t = {} @@ -232,7 +232,7 @@ end -- If there is an item in the current slot, validates the fit as though that -- item were not currently installed. -- Returns false if the equipment item is not compatible with slot mounting. ----@param slotHandle ShipDef.Slot +---@param slotHandle HullConfig.Slot ---@param equipment EquipType function EquipSet:CanInstallInSlot(slotHandle, equipment) local equipped = self:GetItemInSlot(slotHandle) @@ -278,7 +278,7 @@ end -- The passed slot must be a slot returned from caling GetSlotHandle or -- GetAllSlotsOfType on this EquipSet. ---@param equipment EquipType ----@param slotHandle ShipDef.Slot? +---@param slotHandle HullConfig.Slot? ---@return boolean function EquipSet:Install(equipment, slotHandle) print("Installing equip {} in slot {}" % { equipment:GetName(), slotHandle }) diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index 08ea404845..c3d6b1d648 100644 --- a/data/libs/EquipType.lua +++ b/data/libs/EquipType.lua @@ -44,7 +44,7 @@ local misc = {} ---@field transient table ---@field slots table -- deprecated ---@field count integer? ----@field provides_slots table? +---@field provides_slots table? ---@field __proto EquipType? local EquipType = utils.inherits(nil, "EquipType") @@ -97,7 +97,7 @@ end -- Override this with a function customizing the equipment instance for the passed ship -- (E.g. for equipment with mass/volume/cost dependent on the specific ship hull) -EquipType.SpecializeForShip = nil ---@type nil | fun(self: self, ship: ShipDef.Config) +EquipType.SpecializeForShip = nil ---@type nil | fun(self: self, ship: HullConfig) function EquipType._createTransient(obj) local l = Lang.GetResource(obj.l10n_resource) @@ -108,7 +108,7 @@ function EquipType._createTransient(obj) end ---@param ship Ship ----@param slot ShipDef.Slot? +---@param slot HullConfig.Slot? function EquipType:OnInstall(ship, slot) -- Extend this for any custom installation logic needed -- (e.g. mounting weapons) @@ -120,7 +120,7 @@ function EquipType:OnInstall(ship, slot) end ---@param ship Ship ----@param slot ShipDef.Slot? +---@param slot HullConfig.Slot? function EquipType:OnRemove(ship, slot) -- Override this for any custom uninstallation logic needed end @@ -268,7 +268,7 @@ end local LaserType = utils.inherits(EquipType, "LaserType") ---@param ship Ship ----@param slot ShipDef.Slot +---@param slot HullConfig.Slot function LaserType:OnInstall(ship, slot) for k, v in ipairs(self.laser_stats) do -- TODO: allow installing more than one laser @@ -277,7 +277,7 @@ function LaserType:OnInstall(ship, slot) end ---@param ship Ship ----@param slot ShipDef.Slot +---@param slot HullConfig.Slot function LaserType:OnUninstall(ship, slot) for k, v in ipairs(self.laser_stats) do -- TODO: allow installing more than one laser diff --git a/data/libs/ShipConfig.lua b/data/libs/HullConfig.lua similarity index 69% rename from data/libs/ShipConfig.lua rename to data/libs/HullConfig.lua index 84c57579e0..adfa89cf74 100644 --- a/data/libs/ShipConfig.lua +++ b/data/libs/HullConfig.lua @@ -3,18 +3,19 @@ local ShipDef = require 'ShipDef' local Serializer = require 'Serializer' + local utils = require 'utils' --- Class: ShipDef.Slot +-- Class: HullConfig.Slot -- -- Generic interface for an equipment-containing slot in a shipdef -- -- Represents a constrained potential mounting point for ship equipment -- either internal or external to a ship. -- Can contain additional fields for specific slot types. ----@class ShipDef.Slot +---@class HullConfig.Slot ---@field clone fun(self, mixin):self -local Slot = utils.proto("ShipDef.Slot") +local Slot = utils.proto("HullConfig.Slot") Slot.id = "" Slot.type = "" @@ -27,42 +28,44 @@ Slot.i18n_key = nil ---@type string? Slot.i18n_res = "equipment-core" Slot.count = nil ---@type integer? --- Class: ShipDef.Config +-- Class: HullConfig -- -- Represents a specific "ship configuration", being a list of equipment slots -- and associated data. -- -- The default configuration for a ship hull is defined in its JSON shipdef and -- consumed by Lua as a ship config. ----@class ShipDef.Config +---@class HullConfig ---@field clone fun():self -local ShipConfig = utils.proto("ShipDef.Config") +local HullConfig = utils.proto("HullConfig") -ShipConfig.id = "" -ShipConfig.capacity = 0 +HullConfig.id = "" +HullConfig.capacity = 0 -- Default slot config for a new shipdef -- Individual shipdefs can redefine slots or remove them by setting the slot to 'false' ----@type table -ShipConfig.slots = { +---@type table +HullConfig.slots = { sensor = Slot:clone { type = "sensor", size = 1 }, computer_1 = Slot:clone { type = "computer", size = 1 }, - computer_2 = Slot:clone { type = "computer", size = 1 }, hull_mod = Slot:clone { type = "hull", size = 1 }, hyperdrive = Slot:clone { type = "hyperdrive", size = 1 }, thruster = Slot:clone { type = "thruster", size = 1 }, - scoop = Slot:clone { type = "scoop", size = 1, hardpoint = true }, } -function ShipConfig:__clone() +function HullConfig:__clone() self.slots = utils.map_table(self.slots, function(key, slot) return key, slot:clone() end) end +Serializer:RegisterClass("HullConfig", HullConfig) +Serializer:RegisterClass("HullConfig.Slot", Slot) + +--============================================================================== + local function CreateShipConfig(def) - ---@class ShipDef.Config - local newShip = ShipConfig:clone() + local newShip = HullConfig:clone() Serializer:RegisterPersistent("ShipDef." .. def.id, newShip) newShip.id = def.id @@ -80,19 +83,26 @@ local function CreateShipConfig(def) slot.id = name end - -- if def.id == 'coronatrix' then utils.print_r(newShip) end - return newShip end ----@type table -local Config = {} +---@type table +local Configs = {} for id, def in pairs(ShipDef) do - Config[id] = CreateShipConfig(def) + if def.tag == "SHIP" or def.tag == "STATIC_SHIP" then + Configs[id] = CreateShipConfig(def) + end +end + +local function GetHullConfigs() + return Configs end -Serializer:RegisterClass("ShipDef.Config", ShipConfig) -Serializer:RegisterClass("ShipDef.Slot", Slot) +--============================================================================== -return Config +return { + Config = HullConfig, + Slot = Slot, + GetHullConfigs = GetHullConfigs +} diff --git a/data/modules/MissionUtils/DebugShipBuilder.lua b/data/modules/MissionUtils/DebugShipBuilder.lua index 7409b7b83d..f88fe83726 100644 --- a/data/modules/MissionUtils/DebugShipBuilder.lua +++ b/data/modules/MissionUtils/DebugShipBuilder.lua @@ -1,8 +1,11 @@ +-- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt -local debugView = require 'pigui.views.debug' +local HullConfig = require 'HullConfig' local ShipDef = require 'ShipDef' local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' -local ShipConfig = require 'ShipConfig' + +local debugView = require 'pigui.views.debug' local Rules = ShipBuilder.OutfitRules @@ -97,7 +100,7 @@ debugView.registerTab("ShipBuilder", { end if ui.collapsingHeader("Slots") then - local config = ShipConfig[self.selectedHull] + local config = HullConfig.GetHullConfigs()[self.selectedHull] for k, v in pairs(config.slots) do ui.text(k) diff --git a/data/modules/MissionUtils/OutfitRules.lua b/data/modules/MissionUtils/OutfitRules.lua index cafbf1c6a3..2174f676dc 100644 --- a/data/modules/MissionUtils/OutfitRules.lua +++ b/data/modules/MissionUtils/OutfitRules.lua @@ -1,16 +1,6 @@ -- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt -local Engine = require 'Engine' -local Equipment = require 'Equipment' -local EquipSet = require 'EquipSet' -local ShipDef = require 'ShipDef' -local ShipConfig = require 'ShipConfig' -local Space = require 'Space' -local Ship = require 'Ship' - -local utils = require 'utils' - ---@class MissionUtils.OutfitRules local OutfitRules = {} diff --git a/data/modules/MissionUtils/ShipBuilder.lua b/data/modules/MissionUtils/ShipBuilder.lua index 1d76c09698..3b6814f5fb 100644 --- a/data/modules/MissionUtils/ShipBuilder.lua +++ b/data/modules/MissionUtils/ShipBuilder.lua @@ -5,8 +5,8 @@ local Engine = require 'Engine' local Equipment = require 'Equipment' local EquipSet = require 'EquipSet' local Event = require 'Event' +local HullConfig = require 'HullConfig' local ShipDef = require 'ShipDef' -local ShipConfig = require 'ShipConfig' local Space = require 'Space' local Ship = require 'Ship' @@ -443,11 +443,11 @@ function ShipBuilder.SelectHull(template, threat) local shipId = hullList[Engine.rand:Integer(1, #hullList)] - return ShipConfig[shipId] + return HullConfig.GetHullConfigs()[shipId] end ---@param production table ----@param shipConfig ShipDef.Config +---@param shipConfig HullConfig function ShipBuilder.MakePlan(production, shipConfig, threat) local hullThreat = ShipBuilder.GetHullThreat(shipConfig.id).total diff --git a/data/pigui/libs/equip-card.lua b/data/pigui/libs/equip-card.lua index 81d9ff7926..046f6baf30 100644 --- a/data/pigui/libs/equip-card.lua +++ b/data/pigui/libs/equip-card.lua @@ -47,7 +47,7 @@ local EquipCard = utils.inherits(ItemCard, "UI.EquipCard") ---@field type string? ---@field size string? ---@field equip EquipType? ----@field slot ShipDef.Slot? +---@field slot HullConfig.Slot? ---@field present integer? ---@field total integer? ---@field stats EquipType.UI.Stats[] | nil diff --git a/data/pigui/libs/equipment-outfitter.lua b/data/pigui/libs/equipment-outfitter.lua index f451d458ba..c1649e3ad0 100644 --- a/data/pigui/libs/equipment-outfitter.lua +++ b/data/pigui/libs/equipment-outfitter.lua @@ -166,7 +166,7 @@ function Outfitter:Constructor() self.ship = nil ---@type Ship self.station = nil ---@type SpaceStation - self.filterSlot = nil ---@type ShipDef.Slot? + self.filterSlot = nil ---@type HullConfig.Slot? self.replaceEquip = nil ---@type EquipType? self.canSellEquip = false diff --git a/data/pigui/libs/ship-equipment.lua b/data/pigui/libs/ship-equipment.lua index 051c33a4c0..516f7dc8ea 100644 --- a/data/pigui/libs/ship-equipment.lua +++ b/data/pigui/libs/ship-equipment.lua @@ -139,7 +139,7 @@ function EquipmentWidget:Constructor(id) ---@type EquipType? self.selectedEquip = nil - ---@type ShipDef.Slot? + ---@type HullConfig.Slot? self.selectedSlot = nil self.selectionActive = false @@ -155,7 +155,7 @@ function EquipmentWidget:Constructor(id) self.id = id or "EquipmentWidget" self.sectionList = {} - ---@type table + ---@type table self.adjacentSlots = {} end @@ -199,7 +199,7 @@ end -- Return the translated name of a slot, falling back to a generic name for the -- slot type if none is specified. ----@param slot ShipDef.Slot +---@param slot HullConfig.Slot function EquipmentWidget:getSlotName(slot) if slot.i18n_key then return Lang.GetResource(slot.i18n_res)[slot.i18n_key] From 872e1195e47a0c014490ca6cda2a2abb6f357512 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 19 Sep 2024 15:26:23 -0400 Subject: [PATCH 043/119] fixup: type annotation for utils.proto --- data/libs/utils.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/libs/utils.lua b/data/libs/utils.lua index 9008818969..fd156b4dcc 100644 --- a/data/libs/utils.lua +++ b/data/libs/utils.lua @@ -510,6 +510,9 @@ local _proto = {} _proto.__clone = function(self) end +---@generic T +---@param self T +---@return T function _proto:clone(mixin) local new = { __index = self } setmetatable(new, new) From 2b50be8e11519cf41fb85930ae0a404ac9bb695a Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 19 Sep 2024 15:35:25 -0400 Subject: [PATCH 044/119] Add coronatrix internal missile rack for testing --- data/lang/equipment-core/en.json | 8 ++++++++ data/modules/Equipment/Weapons.lua | 16 ++++++++++++++++ data/ships/coronatrix.json | 16 +++++++++++----- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/data/lang/equipment-core/en.json b/data/lang/equipment-core/en.json index 9f9a9f2fe3..140b376c18 100644 --- a/data/lang/equipment-core/en.json +++ b/data/lang/equipment-core/en.json @@ -479,6 +479,10 @@ "description": "Name for the 'Laser Front' equipment hardpoint", "message": "Laser Front" }, + "HARDPOINT_MISSILE": { + "description": "", + "message": "Missile" + }, "HARDPOINT_MISSILE_BAY": { "description": "Name for the 'Missile Bay' equipment hardpoint", "message": "Missile Bay" @@ -542,5 +546,9 @@ "SORT_PRICE": { "description": "", "message": "Price" + }, + "OPLI_INTERNAL_MISSILE_RACK_S2": { + "description": "", + "message": "OPLI Internal Missile Rack" } } diff --git a/data/modules/Equipment/Weapons.lua b/data/modules/Equipment/Weapons.lua index ebf58cec50..dfaf95e238 100644 --- a/data/modules/Equipment/Weapons.lua +++ b/data/modules/Equipment/Weapons.lua @@ -3,6 +3,7 @@ local EquipTypes = require 'EquipType' local Equipment = require 'Equipment' +local Slot = require 'HullConfig'.Slot local EquipType = EquipTypes.EquipType local LaserType = EquipTypes.LaserType @@ -230,3 +231,18 @@ Equipment.Register("missile.naval_s4", EquipType.New { slot = { type="missile", size=4, hardpoint=true }, icon_name="equip_missile_naval" }) + +Equipment.Register("missile_rack.opli_internal_s2", EquipType.New { + l10n_key="OPLI_INTERNAL_MISSILE_RACK_S2", + price=150, purchasable=true, tech_level=1, + volume=5.0, mass=0.5, + slot = { type = "missile_rack.opli_internal", size=2, hardpoint=true }, + provides_slots = { + Slot:clone { id = "1", type = "missile", size = 2, size_min = 1, i18n_key = "HARDPOINT_MISSILE" }, + Slot:clone { id = "2", type = "missile", size = 2, size_min = 1, i18n_key = "HARDPOINT_MISSILE" }, + Slot:clone { id = "3", type = "missile", size = 2, size_min = 1, i18n_key = "HARDPOINT_MISSILE" }, + Slot:clone { id = "4", type = "missile", size = 2, size_min = 1, i18n_key = "HARDPOINT_MISSILE" }, + Slot:clone { id = "5", type = "missile", size = 2, size_min = 1, i18n_key = "HARDPOINT_MISSILE" }, + }, + icon_name="equip_missile_unguided" +}) diff --git a/data/ships/coronatrix.json b/data/ships/coronatrix.json index e27a385af4..e319639c18 100644 --- a/data/ships/coronatrix.json +++ b/data/ships/coronatrix.json @@ -30,15 +30,21 @@ "gimbal": [ 5, 5 ], "hardpoint": true }, - "missile_bay": { - "type": "missile", + "missile_bay_1": { + "type": "missile_rack.opli_internal", + "size": 2, + "i18n_key": "HARDPOINT_MISSILE_BAY", + "tag": "tag_missile_launch", + "hardpoint": true, + "default": "missile_rack.opli_internal_s2" + }, + "missile_bay_2": { + "type": "missile_rack.opli_internal", "size": 2, - "size_min": 1, - "count": 5, "i18n_key": "HARDPOINT_MISSILE_BAY", "tag": "tag_missile_launch", "hardpoint": true, - "default": "missile.guided_s2" + "default": "missile_rack.opli_internal_s2" }, "utility_1": { "type": "utility", From 5a871a217625cc161d66ef5a5fedbeec901eb4b0 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 19 Sep 2024 17:20:36 -0400 Subject: [PATCH 045/119] ShipBuilder: support controlling rules by threat factor - Allow specifying a maximum proportion of the remaining threat factor a rule is able to consume - Better threat calculation for beam lasers and dual-fire weapons - Improve ship cross-section to threat-factor calculations by computing average surface area - Equipment rules can be disabled below a minimum threat value - Add type checking when specifying custom outfitting rules in a template --- .../modules/MissionUtils/DebugShipBuilder.lua | 42 +++-- data/modules/MissionUtils/OutfitRules.lua | 12 ++ data/modules/MissionUtils/ShipBuilder.lua | 146 +++++++++++------- 3 files changed, 132 insertions(+), 68 deletions(-) diff --git a/data/modules/MissionUtils/DebugShipBuilder.lua b/data/modules/MissionUtils/DebugShipBuilder.lua index f88fe83726..ecf71d3856 100644 --- a/data/modules/MissionUtils/DebugShipBuilder.lua +++ b/data/modules/MissionUtils/DebugShipBuilder.lua @@ -4,6 +4,7 @@ local HullConfig = require 'HullConfig' local ShipDef = require 'ShipDef' local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' +local utils = require 'utils' local debugView = require 'pigui.views.debug' @@ -18,7 +19,9 @@ local debugRule = ShipBuilder.Template:clone { { slot = "weapon", filter = "weapon.energy", - limit = 1 + pick = "random", + limit = 1, + maxThreatFactor = 0.6 }, { slot = "missile_rack", @@ -38,10 +41,17 @@ local debugRule = ShipBuilder.Template:clone { { slot = "shield", maxSize = 1, - limit = 2 + limit = 2, + balance = true }, Rules.DefaultHyperdrive, - Rules.DefaultLaserCooling, + -- Rules.DefaultLaserCooling, + { + slot = nil, + equip = "misc.laser_cooling_booster", + limit = 1, + minThreat = 15.0 + }, Rules.DefaultAtmoShield, } } @@ -53,7 +63,7 @@ debugView.registerTab("ShipBuilder", { selectedHull = nil, spawnThreat = 20.0, - show = function() return not require 'Game':InHyperspace() end, + show = function() return require 'Game'.player end, draw = function(self) self.spawnThreat = ui.sliderFloat("Threat", self.spawnThreat, 1.0, 200.0) @@ -81,8 +91,6 @@ debugView.registerTab("ShipBuilder", { if self.selectedHull then if ui.button("<") then self.selectedHull = nil - - ui.endTabItem() return end @@ -114,14 +122,22 @@ debugView.registerTab("ShipBuilder", { end end else - for id, def in pairs(ShipDef) do - if ui.selectable(id) then - self.selectedHull = id - end + local hulls = utils.build_array(pairs(ShipDef)) - local threat = ShipBuilder.GetHullThreat(id) - ui.sameLine() - ui.text("threat: " .. tostring(threat.total)) + table.sort(hulls, function(a, b) + return ShipBuilder.GetHullThreat(a.id).total < ShipBuilder.GetHullThreat(b.id).total + end) + + for _, def in ipairs(hulls) do + if def.tag == "SHIP" then + if ui.selectable(def.id) then + self.selectedHull = def.id + end + + local threat = ShipBuilder.GetHullThreat(def.id) + ui.sameLine() + ui.text("threat: " .. tostring(threat.total)) + end end end end) diff --git a/data/modules/MissionUtils/OutfitRules.lua b/data/modules/MissionUtils/OutfitRules.lua index 2174f676dc..1f75bcb518 100644 --- a/data/modules/MissionUtils/OutfitRules.lua +++ b/data/modules/MissionUtils/OutfitRules.lua @@ -4,6 +4,18 @@ ---@class MissionUtils.OutfitRules local OutfitRules = {} +---@class MissionUtils.OutfitRule +---@field slot string? Slot type filter string +---@field equip string? Explicit equipment item to install in filtered slots +---@field filter string? Filter for random equipment types +---@field limit integer? Maximum number of equipment items to install +---@field pick nil | "random" Pick the biggest/best item for the slot, or a random compatible item +---@field maxSize integer? Limit the maximum size of items equipped by this rule +---@field minSize integer? Limit the minimum size of items equipped by this rule +---@field balance boolean? Attempt to balance volume / threat across all slots this rule matches (works best with .pick = nil) +---@field maxThreatFactor number? Maximum proportion of remaining threat that can be consumed by this rule +---@field minThreat number? Minimum threat value for the entire ship that has to be met to consider this rule + OutfitRules.DefaultHyperdrive = { slot = "hyperdrive" } diff --git a/data/modules/MissionUtils/ShipBuilder.lua b/data/modules/MissionUtils/ShipBuilder.lua index 3b6814f5fb..649537c0bf 100644 --- a/data/modules/MissionUtils/ShipBuilder.lua +++ b/data/modules/MissionUtils/ShipBuilder.lua @@ -45,19 +45,20 @@ ShipBuilder.kBaseHullThreatFactor = 1.0 ShipBuilder.kArmorToThreatFactor = 0.15 -- How much the ship's maximum forward acceleration contributes to threat factor -ShipBuilder.kAccelToThreat = 0.000015 +ShipBuilder.kAccelToThreat = 0.02 -- Tbreat from acceleration is added to this number to determine the final modifier for ship hull HP ShipBuilder.kAccelThreatBase = 0.5 -- Controls how a ship's atmospheric performance contributes to its threat factor -ShipBuilder.kAeroStabilityToThreat = 0.3 +ShipBuilder.kAeroStabilityToThreat = 0.20 ShipBuilder.kAeroStabilityThreatBase = 0.80 -ShipBuilder.kCrossSectionToThreat = 0.3 -ShipBuilder.kCrossSectionThreatBase = 0.8 +ShipBuilder.kCrossSectionToThreat = 1.0 +ShipBuilder.kCrossSectionThreatBase = 0.75 -- Only accept ships where the hull is at most this fraction of the desired total threat factor -ShipBuilder.kMaxHullThreatFactor = 0.7 +ShipBuilder.kMaxHullThreatFactor = 0.8 +ShipBuilder.kMinHullThreatFactor = 0.12 -- || Weapon Threat Factor || @@ -67,6 +68,12 @@ ShipBuilder.kWeaponDPSToThreat = 0.35 -- Weapon projectile speed modifies threat, calculated as 1.0 + (speed / 1km/s) * mod ShipBuilder.kWeaponSpeedToThreat = 0.2 +-- Threat factor increase for dual-fire weapons +ShipBuilder.kDualFireThreatFactor = 1.2 + +-- Threat factor increase for beam lasers +ShipBuilder.kBeamLaserThreatFactor = 1.8 + -- Amount of hull threat included in this weapon's threat per unit of weapon damage ShipBuilder.kWeaponSharedHullThreat = 0.02 @@ -81,16 +88,14 @@ ShipBuilder.kShieldSharedHullThreat = 0.005 -- ============================================================================= +---@class MissionUtils.ShipTemplate +---@field clone fun(self, mixin: { rules: MissionUtils.OutfitRule[] }): self local Template = utils.proto("MissionUtils.ShipTemplate") -Template.role = nil -Template.shipId = nil -Template.label = nil -Template.rules = {} - -function Template:__clone() - self.rules = table.copy(self.rules) -end +Template.role = nil ---@type string? +Template.shipId = nil ---@type string? +Template.label = nil ---@type string? +Template.rules = {} ---@type MissionUtils.OutfitRule[] ShipBuilder.Template = Template @@ -98,6 +103,7 @@ ShipBuilder.Template = Template local ShipPlan = utils.proto("MissionUtils.ShipPlan") +ShipPlan.config = nil ShipPlan.shipId = "" ShipPlan.label = "" ShipPlan.freeVolume = 0 @@ -175,21 +181,24 @@ local function calcWeaponThreat(equip, hullThreat) local dps = damage / equip.laser_stats.rechargeTime local speedMod = 1.0 + local dualMod = 1.0 + equip.laser_stats.dual * ShipBuilder.kDualFireThreatFactor -- Beam lasers don't factor in projectile speed (instant) - -- TODO: they should have a separate threat factor instead - if not equip.laser_stats.beam or equip.laser_stats.beam == 0.0 then + -- Instead they have a separate threat factor + if equip.laser_stats.beam or equip.laser_stats.beam == 0.0 then + speedMod = ShipBuilder.kBeamLaserThreatFactor + else speedMod = 1.0 + ShipBuilder.kWeaponSpeedToThreat * speed end - local threat = ShipBuilder.kWeaponDPSToThreat * dps * speedMod + local threat = ShipBuilder.kWeaponDPSToThreat * dps * speedMod * dualMod + ShipBuilder.kWeaponSharedHullThreat * damage * hullThreat return threat end local function calcShieldThreat(equip, hullThreat) - -- XXX: this is a hardcoded constant shared with Ship.cpp + -- FIXME: this is a hardcoded constant shared with Ship.cpp local shield_tons = equip.capabilities.shield * 10.0 local threat = ShipBuilder.kShieldThreatFactor * shield_tons @@ -199,11 +208,11 @@ local function calcShieldThreat(equip, hullThreat) end function ShipBuilder.ComputeEquipThreatFactor(equip, hullThreat) - if equip.slot and equip.slot.type:match("weapon.") and equip.laser_stats then + if equip.slot and equip.slot.type:match("^weapon") and equip.laser_stats then return calcWeaponThreat(equip, hullThreat) end - if equip.slot and equip.slot.type:match("shield.") and equip.capabilities.shield then + if equip.slot and equip.slot.type:match("^shield") and equip.capabilities.shield then return calcShieldThreat(equip, hullThreat) end @@ -223,14 +232,14 @@ function ShipBuilder.ComputeHullThreatFactor(shipDef) local threat = { id = shipDef.id } local armor = shipDef.hullMass - local totalMass = shipDef.hullMass + shipDef.fuelTankMass - local forwardAccel = shipDef.linearThrust["FORWARD"] / totalMass + local totalMass = shipDef.hullMass + shipDef.fuelTankMass + shipDef.capacity * 0.5 + local forwardAccel = shipDef.linearThrust["FORWARD"] / (1000.0 * totalMass) local crossSectionAvg = (shipDef.topCrossSec + shipDef.sideCrossSec + shipDef.frontCrossSec) / 3.0 threat.armor = ShipBuilder.kBaseHullThreatFactor + ShipBuilder.kArmorToThreatFactor * armor threat.thrust = ShipBuilder.kAccelThreatBase + ShipBuilder.kAccelToThreat * forwardAccel threat.aero = ShipBuilder.kAeroStabilityThreatBase + ShipBuilder.kAeroStabilityToThreat * (shipDef.raw.aero_stability or 0.0) - threat.crosssection = ShipBuilder.kCrossSectionThreatBase + ShipBuilder.kCrossSectionToThreat * (armor / crossSectionAvg) + threat.crosssection = ShipBuilder.kCrossSectionThreatBase + ShipBuilder.kCrossSectionToThreat * (armor^(1/3) / crossSectionAvg^(1/2)) threat.total = threat.armor * threat.thrust * threat.aero * threat.crosssection threat.total = utils.round(threat.total, 0.01) @@ -241,7 +250,7 @@ end -- ============================================================================= ---@param shipPlan table ----@param rule table +---@param rule MissionUtils.OutfitRule ---@param rand Rand ---@param hullThreat number function ShipBuilder.ApplyEquipmentRule(shipPlan, rule, rand, hullThreat) @@ -249,6 +258,7 @@ function ShipBuilder.ApplyEquipmentRule(shipPlan, rule, rand, hullThreat) -- print("Applying rule:") -- utils.print_r(rule) + ---@type HullConfig local shipConfig = shipPlan.config local matchRuleSlot = function(slot, filter) @@ -280,9 +290,12 @@ function ShipBuilder.ApplyEquipmentRule(shipPlan, rule, rand, hullThreat) local equip = Equipment.Get(rule.equip) local threat = ShipBuilder.ComputeEquipThreatFactor(equip, hullThreat) + -- Limit maximum threat consumption of this equipment rule + local reserveThreat = rule.maxThreatFactor and ((1.0 - shipPlan.freeThreat) * rule.maxThreatFactor) or 0.0 + for _, slot in ipairs(slots) do - if EquipSet.CompatibleWithSlot(equip, slot) and shipPlan.freeThreat >= threat then + if EquipSet.CompatibleWithSlot(equip, slot) and (shipPlan.freeThreat - reserveThreat) >= threat then local inst = equip:Instance() @@ -315,6 +328,11 @@ function ShipBuilder.ApplyEquipmentRule(shipPlan, rule, rand, hullThreat) -- NOTE: this does not guarantee all slots will be able to be filled in a balanced manner local maxVolume = rule.balance and shipPlan.freeVolume / #slots or shipPlan.freeVolume + -- Limit maximum threat consumption of this equipment rule + local allowedThreat = (rule.maxThreatFactor or 1.0) * shipPlan.freeThreat + local reserveThreat = shipPlan.freeThreat - allowedThreat + local maxThreat = rule.balance and allowedThreat / #slots or allowedThreat + -- Build a list of all equipment items that could potentially be installed local filteredEquip = utils.to_array(Equipment.new, function(equip) return (equip.slot or false) @@ -335,7 +353,6 @@ function ShipBuilder.ApplyEquipmentRule(shipPlan, rule, rand, hullThreat) -- NOTE: if equipment items don't include hull threat in their calculation, this -- can be precached at startup local threatCache = utils.map_table(filteredEquip, function(_, equip) - print(equip:GetName(), ShipBuilder.ComputeEquipThreatFactor(equip, hullThreat)) return equip, ShipBuilder.ComputeEquipThreatFactor(equip, hullThreat) end) @@ -347,8 +364,11 @@ function ShipBuilder.ApplyEquipmentRule(shipPlan, rule, rand, hullThreat) -- specific slot type than the rule itself). ---@type EquipType[] local compatible = utils.map_array(filteredEquip, function(equip) + local threat = threatCache[equip] + local compat = EquipSet.CompatibleWithSlot(equip, slot) - and shipPlan.freeThreat >= threatCache[equip] + and threat <= (shipPlan.freeThreat - reserveThreat) + and threat <= maxThreat if not compat then return nil @@ -407,6 +427,8 @@ function ShipBuilder.GetHullThreat(shipId) return hullThreatCache[shipId] or { total = 0.0 } end +---@param template MissionUtils.ShipTemplate +---@param threat number function ShipBuilder.SelectHull(template, threat) local hullList = {} @@ -418,14 +440,15 @@ function ShipBuilder.SelectHull(template, threat) for id, shipDef in pairs(ShipDef) do - if not template.role or shipDef.roles[template.role] then + if shipDef.tag == "SHIP" and (not template.role or shipDef.roles[template.role]) then local hullThreat = ShipBuilder.GetHullThreat(id).total - print(id, hullThreat, threat) - -- Use threat metric as a way to balance the random selection of ship hulls local withinRange = hullThreat <= ShipBuilder.kMaxHullThreatFactor * threat + and hullThreat >= ShipBuilder.kMinHullThreatFactor * threat + + -- print(id, hullThreat, threat, withinRange) if withinRange then table.insert(hullList, id) @@ -441,40 +464,51 @@ function ShipBuilder.SelectHull(template, threat) return nil end - local shipId = hullList[Engine.rand:Integer(1, #hullList)] + local hullIdx = Engine.rand:Integer(1, #hullList) + local shipId = hullList[hullIdx] + + print(" threat {} => {} ({} / {})" % { threat, shipId, hullIdx, #hullList }) return HullConfig.GetHullConfigs()[shipId] end ----@param production table +---@param template MissionUtils.ShipTemplate ---@param shipConfig HullConfig -function ShipBuilder.MakePlan(production, shipConfig, threat) +---@param threat number +function ShipBuilder.MakePlan(template, shipConfig, threat) local hullThreat = ShipBuilder.GetHullThreat(shipConfig.id).total local shipPlan = ShipPlan:clone { threat = hullThreat, freeThreat = threat - hullThreat, + maxThreat = threat } shipPlan:SetConfig(shipConfig) - for _, rule in ipairs(production.rules) do + for _, rule in ipairs(template.rules) do + + if not rule.minThreat or threat >= rule.minThreat then - if rule.slot then - ShipBuilder.ApplyEquipmentRule(shipPlan, rule, Engine.rand, hullThreat) - else - local equip = Equipment.Get(rule.equip) - assert(equip) + if rule.slot then + ShipBuilder.ApplyEquipmentRule(shipPlan, rule, Engine.rand, hullThreat) + else + local equip = Equipment.Get(rule.equip) + assert(equip) + + local equipThreat = ShipBuilder.ComputeEquipThreatFactor(equip, hullThreat) - if shipPlan.freeVolume >= equip.volume then - shipPlan:AddEquipToPlan(equip) + if shipPlan.freeVolume >= equip.volume and shipPlan.freeThreat >= equipThreat then + shipPlan:AddEquipToPlan(equip) + end end + end end - shipPlan.label = production.label or Ship.MakeRandomLabel() + shipPlan.label = template.label or Ship.MakeRandomLabel() return shipPlan @@ -509,19 +543,20 @@ end -- ============================================================================= ---@param body Body ----@param production table ----@param risk number? +---@param template MissionUtils.ShipTemplate +---@param threat number? ---@param nearDist number? ---@param farDist number? -function ShipBuilder.MakeShipNear(body, production, risk, nearDist, farDist) - if not risk then - risk = Engine.rand:Number(ShipBuilder.kDefaultRandomThreatMin, ShipBuilder.kDefaultRandomThreatMax) +---@return Ship? +function ShipBuilder.MakeShipNear(body, template, threat, nearDist, farDist) + if not threat then + threat = Engine.rand:Number(ShipBuilder.kDefaultRandomThreatMin, ShipBuilder.kDefaultRandomThreatMax) end - local hullConfig = ShipBuilder.SelectHull(production, risk) + local hullConfig = ShipBuilder.SelectHull(template, threat) assert(hullConfig) - local plan = ShipBuilder.MakePlan(production, hullConfig, risk) + local plan = ShipBuilder.MakePlan(template, hullConfig, threat) assert(plan) local ship = Space.SpawnShipNear(plan.shipId, body, nearDist or 50, farDist or 100) @@ -533,17 +568,18 @@ function ShipBuilder.MakeShipNear(body, production, risk, nearDist, farDist) end ---@param body Body ----@param production table ----@param risk number? -function ShipBuilder.MakeShipDocked(body, production, risk) - if not risk then - risk = Engine.rand:Number(ShipBuilder.kDefaultRandomThreatMin, ShipBuilder.kDefaultRandomThreatMax) +---@param template MissionUtils.ShipTemplate +---@param threat number? +---@return Ship? +function ShipBuilder.MakeShipDocked(body, template, threat) + if not threat then + threat = Engine.rand:Number(ShipBuilder.kDefaultRandomThreatMin, ShipBuilder.kDefaultRandomThreatMax) end - local hullConfig = ShipBuilder.SelectHull(production, risk) + local hullConfig = ShipBuilder.SelectHull(template, threat) assert(hullConfig) - local plan = ShipBuilder.MakePlan(production, hullConfig, risk) + local plan = ShipBuilder.MakePlan(template, hullConfig, threat) assert(plan) local ship = Space.SpawnShipDocked(plan.shipId, body) From cf3fa436616dbc939ddff5eed878f5cc9dd21316 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 20 Sep 2024 02:47:56 -0400 Subject: [PATCH 046/119] ShipBuilder: add shared rules for weapon and shield slots --- data/modules/MissionUtils/OutfitRules.lua | 59 ++++++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/data/modules/MissionUtils/OutfitRules.lua b/data/modules/MissionUtils/OutfitRules.lua index 1f75bcb518..62150fa8ef 100644 --- a/data/modules/MissionUtils/OutfitRules.lua +++ b/data/modules/MissionUtils/OutfitRules.lua @@ -12,9 +12,59 @@ local OutfitRules = {} ---@field pick nil | "random" Pick the biggest/best item for the slot, or a random compatible item ---@field maxSize integer? Limit the maximum size of items equipped by this rule ---@field minSize integer? Limit the minimum size of items equipped by this rule ----@field balance boolean? Attempt to balance volume / threat across all slots this rule matches (works best with .pick = nil) ----@field maxThreatFactor number? Maximum proportion of remaining threat that can be consumed by this rule ---@field minThreat number? Minimum threat value for the entire ship that has to be met to consider this rule +---@field maxThreatFactor number? Maximum proportion of remaining threat that can be consumed by this rule +---@field balance boolean? Attempt to balance volume / threat across all slots this rule matches (works best with .pick = nil) + +OutfitRules.DifficultWeapon = { + slot = "weapon", + minSize = 2, + minThreat = 50.0, + maxThreatFactor = 0.7, + balance = true, +} + +OutfitRules.ModerateWeapon = { + slot = "weapon", + limit = 2, + maxSize = 3, + minThreat = 40.0, + maxThreatFactor = 0.6, + balance = true, +} + +OutfitRules.EasyWeapon = { + slot = "weapon", + limit = 1, + maxSize = 2, + maxThreatFactor = 0.5, + balance = true, +} + +OutfitRules.DifficultShieldGen = { + slot = "shield", + minSize = 2, + minThreat = 50.0, + balance = true +} + +OutfitRules.ModerateShieldGen = { + slot = "shield", + maxSize = 3, + limit = 2, + minThreat = 20.0, + maxThreatFactor = 0.8, + balance = true +} + +OutfitRules.EasyShieldGen = { + slot = "shield", + maxSize = 2, + limit = 1, + maxThreatFactor = 0.6 +} + +-- Default rules always equip the item if there's enough space OutfitRules.DefaultHyperdrive = { slot = "hyperdrive" @@ -37,6 +87,11 @@ OutfitRules.DefaultAutopilot = { limit = 1 } +OutfitRules.DefaultShieldBooster = { + equip = "misc.shield_energy_booster", + limit = 1 +} + OutfitRules.DefaultLaserCooling = { slot = nil, equip = "misc.laser_cooling_booster", From d25a52292d98ff998972dacc6385e467e83ec43d Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 20 Sep 2024 02:48:23 -0400 Subject: [PATCH 047/119] Update Assassination mission to use ShipBuilder API --- data/modules/Assassination/Assassination.lua | 73 ++++++++++---------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/data/modules/Assassination/Assassination.lua b/data/modules/Assassination/Assassination.lua index 6ae4a126b5..65a7127318 100644 --- a/data/modules/Assassination/Assassination.lua +++ b/data/modules/Assassination/Assassination.lua @@ -12,12 +12,13 @@ local Mission = require 'Mission' local MissionUtils = require 'modules.MissionUtils' local NameGen = require 'NameGen' local Character = require 'Character' -local Commodities = require 'Commodities' local Format = require 'Format' local Serializer = require 'Serializer' -local Equipment = require 'Equipment' local ShipDef = require 'ShipDef' local Ship = require 'Ship' +local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' +local HullConfig = require 'HullConfig' +local OutfitRules = ShipBuilder.OutfitRules local utils = require 'utils' local lc = Lang.GetResource 'core' @@ -26,6 +27,25 @@ local l = Lang.GetResource("module-assassination") -- don't produce missions for further than this many light years away local max_ass_dist = 30 +local AssassinationTargetShip = ShipBuilder.Template:clone { + role = "mercenary", + rules = { + OutfitRules.DifficultWeapon, + OutfitRules.ModerateWeapon, + OutfitRules.EasyWeapon, + -- Set shield gens according to mission difficulty + OutfitRules.DifficultShieldGen, + OutfitRules.ModerateShieldGen, + OutfitRules.EasyShieldGen, + -- Enable add-on equipment based on mission difficulty + utils.mixin(OutfitRules.DefaultShieldBooster, { minThreat = 30.0 }), + utils.mixin(OutfitRules.DefaultLaserCooling, { minThreat = 40.0 }), + -- Default equipment in remaining space + OutfitRules.DefaultHyperdrive, + OutfitRules.DefaultAtmoShield, + } +} + local flavours = {} for i = 0,5 do table.insert(flavours, { @@ -134,6 +154,7 @@ local onChat = function (form, ref, option) flavour = ad.flavour, location = ad.location, reward = ad.reward, + threat = ad.threat, shipid = ad.shipid, shipname = ad.shipname, shipregid = ad.shipregid, @@ -195,12 +216,9 @@ local makeAdvert = function (station) reward = utils.round(reward, 500) due = utils.round(due + Game.time, 3600) - -- XXX hull mass is a bad way to determine suitability for role - --local shipdefs = utils.build_array(utils.filter(function (k,def) return def.tag == 'SHIP' and def.hullMass >= (danger * 17) and def.equipSlotCapacity.ATMOSHIELD > 0 end, pairs(ShipDef))) - local shipdefs = utils.build_array(utils.filter(function (k,def) return def.tag == 'SHIP' and def.hyperdriveClass > 0 and def.equipSlotCapacity.atmo_shield > 0 end, pairs(ShipDef))) - local shipdef = shipdefs[Engine.rand:Integer(1,#shipdefs)] - local shipid = shipdef.id - local shipname = shipdef.name + local threat = (1.0 + danger) * 10.0 + local hullConfig = ShipBuilder.SelectHull(AssassinationTargetShip, threat) + assert(hullConfig) local ad = { client = client, @@ -211,8 +229,9 @@ local makeAdvert = function (station) location = location, dist = dist, reward = reward, - shipid = shipid, - shipname = shipname, + threat = threat, + shipid = hullConfig.id, + shipname = ShipDef[hullConfig.id].name, shipregid = Ship.MakeRandomLabel(), station = station, target = target, @@ -284,35 +303,19 @@ local onEnterSystem = function (ship) if mission.due > Game.time then if mission.location:IsSameSystem(syspath) then -- spawn our target ship local station = Space.GetBody(mission.location.bodyIndex) - local shiptype = ShipDef[mission.shipid] - local default_drive = shiptype.hyperdriveClass - local laserdefs = utils.build_array(pairs(Equipment.laser)) - table.sort(laserdefs, function (l1, l2) return l1.price < l2.price end) - local laserdef = laserdefs[mission.danger] - local count = default_drive ^ 2 - - mission.ship = Space.SpawnShipDocked(mission.shipid, station) - if mission.ship == nil then - return -- TODO - end - mission.ship:SetLabel(mission.shipregid) + local plan = ShipBuilder.MakePlan(AssassinationTargetShip, HullConfig.GetHullConfigs()[mission.shipid], mission.threat) + assert(plan) - mission.ship:AddEquip(Equipment.misc.atmospheric_shielding) - local engine = Equipment.hyperspace['hyperdrive_'..tostring(default_drive)] - mission.ship:AddEquip(engine) - mission.ship:AddEquip(laserdef) - mission.ship:AddEquip(Equipment.misc.shield_generator, mission.danger) + local target = Space.SpawnShipDocked(mission.shipid, station) - mission.ship:GetComponent('CargoManager'):AddCommodity(Commodities.hydrogen, count) - - if mission.danger > 2 then - mission.ship:AddEquip(Equipment.misc.shield_energy_booster) + if not target then + return -- TODO end - if mission.danger > 3 then - mission.ship:AddEquip(Equipment.misc.laser_cooling_booster) - end + ShipBuilder.ApplyPlan(target, plan) + target:SetLabel(mission.shipregid) + mission.ship = target _setupHooksForMission(mission) mission.shipstate = 'docked' @@ -546,6 +549,6 @@ Event.Register("onUpdateBB", onUpdateBB) Event.Register("onGameEnd", onGameEnd) Event.Register("onReputationChanged", onReputationChanged) -Mission.RegisterType('Assassination',l.ASSASSINATION, buildMissionDescription) +Mission.RegisterType('Assassination', l.ASSASSINATION, buildMissionDescription) Serializer:Register("Assassination", serialize, unserialize) From ea4fa8865f9666f15d4f4b9fbd77795688de98df Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 20 Sep 2024 15:47:32 -0400 Subject: [PATCH 048/119] OutfitRules: add pulsecannon rules --- data/modules/MissionUtils/OutfitRules.lua | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/data/modules/MissionUtils/OutfitRules.lua b/data/modules/MissionUtils/OutfitRules.lua index 62150fa8ef..289e862a97 100644 --- a/data/modules/MissionUtils/OutfitRules.lua +++ b/data/modules/MissionUtils/OutfitRules.lua @@ -28,19 +28,37 @@ OutfitRules.ModerateWeapon = { slot = "weapon", limit = 2, maxSize = 3, - minThreat = 40.0, + minThreat = 30.0, maxThreatFactor = 0.6, balance = true, } OutfitRules.EasyWeapon = { slot = "weapon", - limit = 1, maxSize = 2, maxThreatFactor = 0.5, balance = true, } +OutfitRules.PulsecannonModerateWeapon = { + slot = "weapon", + filter = "weapon.energy.pulsecannon", + limit = 2, + maxSize = 3, + minThreat = 30.0, + maxThreatFactor = 0.6, + balance = true +} + +OutfitRules.PulsecannonEasyWeapon = { + slot = "weapon", + filter = "weapon.energy.pulsecannon", + limit = 2, + maxSize = 3, + maxThreatFactor = 0.5, + balance = true +} + OutfitRules.DifficultShieldGen = { slot = "shield", minSize = 2, @@ -61,6 +79,7 @@ OutfitRules.EasyShieldGen = { slot = "shield", maxSize = 2, limit = 1, + minThreat = 20.0, maxThreatFactor = 0.6 } From 48691f6d3e7b8ba3b89034b7fe89515df7581e88 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 20 Sep 2024 15:47:42 -0400 Subject: [PATCH 049/119] CargoRun: convert to ShipBuilder API --- data/modules/CargoRun/CargoRun.lua | 75 +++++++++++++++++++----------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/data/modules/CargoRun/CargoRun.lua b/data/modules/CargoRun/CargoRun.lua index 99af9ec3e9..9f271700e0 100644 --- a/data/modules/CargoRun/CargoRun.lua +++ b/data/modules/CargoRun/CargoRun.lua @@ -8,19 +8,47 @@ local Space = require 'Space' local Comms = require 'Comms' local Event = require 'Event' local Mission = require 'Mission' -local MissionUtils = require 'modules.MissionUtils' local Format = require 'Format' local Serializer = require 'Serializer' local Character = require 'Character' -local Equipment = require 'Equipment' -local ShipDef = require 'ShipDef' -local Ship = require 'Ship' local utils = require 'utils' +local MissionUtils = require 'modules.MissionUtils' +local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' + +local OutfitRules = ShipBuilder.OutfitRules + local l = Lang.GetResource("module-cargorun") local l_ui_core = Lang.GetResource("ui-core") local lc = Lang.GetResource 'core' +local PirateTemplate = ShipBuilder.Template:clone { + role = "pirate", + rules = { + OutfitRules.PulsecannonModerateWeapon, + OutfitRules.PulsecannonEasyWeapon, + OutfitRules.EasyShieldGen, + OutfitRules.DefaultHyperdrive, + OutfitRules.DefaultAtmoShield, + utils.mixin(OutfitRules.DefaultLaserCooling, { minThreat = 40.0 }), + utils.mixin(OutfitRules.DefaultShieldBooster, { minThreat = 30.0 }), + } +} + +local EscortTemplate = ShipBuilder.Template:clone { + role = "police", + label = l_ui_core.POLICE, + rules = { + OutfitRules.ModerateWeapon, + OutfitRules.EasyWeapon, + OutfitRules.ModerateShieldGen, + OutfitRules.EasyShieldGen, + utils.mixin(OutfitRules.DefaultLaserCooling, { minThreat = 30.0 }), + OutfitRules.DefaultHyperdrive, + OutfitRules.DefaultAtmoShield + } +} + -- don't produce missions for further than this many light years away local max_delivery_dist = 15 -- typical reward for delivery to a system max_delivery_dist away @@ -51,7 +79,7 @@ local setDefaultCustomCargo = function() for branch,branch_array in pairs(custom_cargo) do custom_cargo[branch].weight = #branch_array.goods custom_cargo_weight_sum = custom_cargo_weight_sum + #branch_array.goods - end + end end local isQualifiedFor = function(reputation, ad) @@ -504,31 +532,19 @@ local onEnterSystem = function (player) -- if there is some risk and still no pirates, flip a tricoin if pirates < 1 and risk >= 0.2 and Engine.rand:Integer(2) == 1 then pirates = 1 end - local shipdefs = utils.build_array(utils.filter(function (k,def) return def.tag == 'SHIP' - and def.hyperdriveClass > 0 and (def.roles.pirate or def.roles.mercenary) end, pairs(ShipDef))) - if #shipdefs == 0 then return end - local pirate while pirates > 0 do pirates = pirates - 1 if Engine.rand:Number(1) <= risk then - local shipdef = shipdefs[Engine.rand:Integer(1,#shipdefs)] - local default_drive = Equipment.hyperspace['hyperdrive_'..tostring(shipdef.hyperdriveClass)] - - local max_laser_size = shipdef.capacity - default_drive.capabilities.mass - local laserdefs = utils.build_array(utils.filter( - function (k,l) return l:IsValidSlot('laser_front') and l.capabilities.mass <= max_laser_size and l.l10n_key:find("PULSECANNON") end, - pairs(Equipment.laser) - )) - local laserdef = laserdefs[Engine.rand:Integer(1,#laserdefs)] - - pirate = Space.SpawnShipNear(shipdef.id, Game.player, 50, 100) - pirate:SetLabel(Ship.MakeRandomLabel()) - pirate:AddEquip(default_drive) - pirate:AddEquip(laserdef) + local threat = utils.round(10.0 + 25.0 * risk, 0.1) + + pirate = ShipBuilder.MakeShipNear(Game.player, PirateTemplate, threat, 50, 100) + assert(pirate) + pirate:AIKill(Game.player) + table.insert(pirate_ships, pirate) end end @@ -540,11 +556,14 @@ local onEnterSystem = function (player) pirate_gripes_time = Game.time if mission.wholesaler or Engine.rand:Number(0, 1) >= 0.75 then - local shipdef = ShipDef[Game.system.faction.policeShip] - local escort = Space.SpawnShipNear(shipdef.id, Game.player, 50, 100) - escort:SetLabel(l_ui_core.POLICE) - escort:AddEquip(Equipment.laser.pulsecannon_1mw) - escort:AddEquip(Equipment.misc.shield_generator) + local policeTemplate = EscortTemplate:clone { + shipId = Game.system.faction.policeShip + } + + local threat = utils.round(10.0 + 30.0 * risk, 0.1) + local escort = ShipBuilder.MakeShipNear(Game.player, policeTemplate, threat, 50, 100) + assert(escort) + escort:AIKill(pirate) table.insert(escort_ships, escort) From 6e6454b1fc5d6ba1e1def29997c91ad0de494804 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 20 Sep 2024 15:48:02 -0400 Subject: [PATCH 050/119] Taxi: convert to ShipBuilder template API --- data/modules/Taxi/Taxi.lua | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/data/modules/Taxi/Taxi.lua b/data/modules/Taxi/Taxi.lua index f7e3200130..7d1f9d14e0 100644 --- a/data/modules/Taxi/Taxi.lua +++ b/data/modules/Taxi/Taxi.lua @@ -16,6 +16,21 @@ local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' local ShipDef = require 'ShipDef' local utils = require 'utils' +local OutfitRules = ShipBuilder.OutfitRules + +local PirateTemplate = ShipBuilder.Template:clone { + role = "pirate", + rules = { + OutfitRules.PulsecannonModerateWeapon, + OutfitRules.PulsecannonEasyWeapon, + OutfitRules.EasyShieldGen, + OutfitRules.DefaultHyperdrive, + OutfitRules.DefaultAtmoShield, + utils.mixin(OutfitRules.DefaultLaserCooling, { minThreat = 40.0 }), + utils.mixin(OutfitRules.DefaultShieldBooster, { minThreat = 30.0 }), + } +} + -- Get the language resource local l = Lang.GetResource("module-taxi") local lc = Lang.GetResource 'core' @@ -106,17 +121,17 @@ local missions = {} local passengers = 0 local add_passengers = function (group) - for i = 1, group do + for i = 1, #group do Passengers.EmbarkPassenger(Game.player, group[i]) end - passengers = passengers + group + passengers = passengers + #group end local remove_passengers = function (group) - for i = 1, group do + for i = 1, #group do Passengers.DisembarkPassenger(Game.player, group[i]) end - passengers = passengers - group + passengers = passengers - #group end local isQualifiedFor = function(reputation, ad) @@ -361,7 +376,10 @@ local onEnterSystem = function (player) ships = ships-1 if Engine.rand:Number(1) <= risk then - ship = ShipBuilder.MakeGenericPirateNear(Game.player, risk, 50, 100) + local threat = utils.round(10 + 25.0 * risk, 0.1) + ship = ShipBuilder.MakeShipNear(Game.player, PirateTemplate, threat, 50, 100) + assert(ship) + ship:AIKill(Game.player) end end From 9fbfaddc3655b57696fa6c3cbcefe824f2cc31dd Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 20 Sep 2024 15:50:04 -0400 Subject: [PATCH 051/119] MissionUtils: add shared ShipTemplates file - Allows mods to alter ship templates in one spot without having to override files - Several ship templates can be easily shared between many mission modules --- data/modules/MissionUtils.lua | 2 + data/modules/MissionUtils/ShipTemplates.lua | 78 +++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 data/modules/MissionUtils/ShipTemplates.lua diff --git a/data/modules/MissionUtils.lua b/data/modules/MissionUtils.lua index 71df4fc8b0..294b457696 100644 --- a/data/modules/MissionUtils.lua +++ b/data/modules/MissionUtils.lua @@ -19,6 +19,8 @@ local MissionUtils = { Weeks = Weeks, } +MissionUtils.ShipTemplates = require 'modules.MissionUtils.ShipTemplates' + ---@class MissionUtils.Calculator ---@field New fun(): MissionUtils.Calculator local Calculator = utils.class("MissionUtils.Calculator") diff --git a/data/modules/MissionUtils/ShipTemplates.lua b/data/modules/MissionUtils/ShipTemplates.lua new file mode 100644 index 0000000000..2dbd9f1d0b --- /dev/null +++ b/data/modules/MissionUtils/ShipTemplates.lua @@ -0,0 +1,78 @@ +-- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' + +local utils = require 'utils' + +local OutfitRules = ShipBuilder.OutfitRules + +---@class MissionUtils.ShipTemplates +local ShipTemplates = {} + +ShipTemplates.StrongPirate = ShipBuilder.Template:clone { + role = "pirate", + rules = { + -- If the pirate is threatening enough, it can have any weapon, + -- otherwise it will get a simple spread of pulsecannons + OutfitRules.DifficultWeapon, + OutfitRules.PulsecannonModerateWeapon, + OutfitRules.PulsecannonEasyWeapon, + -- Equip shield generators in descending order of difficulty + OutfitRules.DifficultShieldGen, + OutfitRules.ModerateShieldGen, + OutfitRules.EasyShieldGen, + -- Potentially throw laser cooling and a shield booster in the mix + utils.mixin(OutfitRules.DefaultLaserCooling, { minThreat = 40.0 }), + utils.mixin(OutfitRules.DefaultShieldBooster, { minThreat = 30.0 }), + OutfitRules.DefaultHyperdrive, + OutfitRules.DefaultAtmoShield, + OutfitRules.DefaultAutopilot + } +} + +ShipTemplates.GenericPirate = ShipBuilder.Template:clone { + role = "pirate", + rules = { + OutfitRules.PulsecannonModerateWeapon, + OutfitRules.EasyWeapon, + OutfitRules.ModerateShieldGen, + OutfitRules.EasyShieldGen, + OutfitRules.DefaultHyperdrive, + OutfitRules.DefaultAtmoShield, + utils.mixin(OutfitRules.DefaultLaserCooling, { minThreat = 40.0 }), + utils.mixin(OutfitRules.DefaultShieldBooster, { minThreat = 30.0 }), + OutfitRules.DefaultAutopilot + } +} + +ShipTemplates.WeakPirate = ShipBuilder.Template:clone { + role = "pirate", + rules = { + OutfitRules.PulsecannonModerateWeapon, + OutfitRules.PulsecannonEasyWeapon, + -- Just a basic shield generator on the tougher ones + OutfitRules.EasyShieldGen, + -- No laser cooling or shield booster to be found here + OutfitRules.DefaultHyperdrive, + OutfitRules.DefaultAtmoShield, + OutfitRules.DefaultAutopilot + } +} + +ShipTemplates.GenericPolice = ShipBuilder.Template:clone { + role = "police", + rules = { + OutfitRules.ModerateWeapon, + OutfitRules.EasyWeapon, + OutfitRules.ModerateShieldGen, + OutfitRules.EasyShieldGen, + utils.mixin(OutfitRules.DefaultShieldBooster, { minThreat = 30.0 }), + utils.mixin(OutfitRules.DefaultLaserCooling, { minThreat = 20.0 }), + OutfitRules.DefaultHyperdrive, + OutfitRules.DefaultAtmoShield, + OutfitRules.DefaultAutopilot + } +} + +return ShipTemplates From 588e01b6bebbbffe96b73ae8e59927e33e64e2a0 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 20 Sep 2024 15:50:30 -0400 Subject: [PATCH 052/119] Update copyright --- data/libs/EquipSet.lua | 2 +- data/libs/HullConfig.lua | 2 +- data/libs/Passengers.lua | 2 +- data/meta/ShipDef.lua | 2 +- data/modules/Equipment/Hyperdrive.lua | 2 +- data/modules/Equipment/Internal.lua | 2 +- data/modules/Equipment/Stats.lua | 2 +- data/modules/Equipment/Utility.lua | 2 +- data/modules/Equipment/Weapons.lua | 2 +- data/modules/MissionUtils/OutfitRules.lua | 2 +- data/modules/MissionUtils/ShipBuilder.lua | 2 +- data/pigui/libs/equip-card.lua | 2 +- data/pigui/libs/equipment-outfitter.lua | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/data/libs/EquipSet.lua b/data/libs/EquipSet.lua index 49dab49458..45eb295cd3 100644 --- a/data/libs/EquipSet.lua +++ b/data/libs/EquipSet.lua @@ -1,4 +1,4 @@ --- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt local HullConfig = require 'HullConfig' diff --git a/data/libs/HullConfig.lua b/data/libs/HullConfig.lua index adfa89cf74..23486d4d7a 100644 --- a/data/libs/HullConfig.lua +++ b/data/libs/HullConfig.lua @@ -1,4 +1,4 @@ --- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt local ShipDef = require 'ShipDef' diff --git a/data/libs/Passengers.lua b/data/libs/Passengers.lua index e5834e847a..3bca1f22cd 100644 --- a/data/libs/Passengers.lua +++ b/data/libs/Passengers.lua @@ -1,4 +1,4 @@ --- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt local Equipment = require 'Equipment' diff --git a/data/meta/ShipDef.lua b/data/meta/ShipDef.lua index a51d7c7652..0a739c45b3 100644 --- a/data/meta/ShipDef.lua +++ b/data/meta/ShipDef.lua @@ -1,4 +1,4 @@ --- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt -- This file implements type information about C++ modules for Lua static analysis diff --git a/data/modules/Equipment/Hyperdrive.lua b/data/modules/Equipment/Hyperdrive.lua index c09474e768..e3369a2151 100644 --- a/data/modules/Equipment/Hyperdrive.lua +++ b/data/modules/Equipment/Hyperdrive.lua @@ -1,4 +1,4 @@ --- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt local Equipment = require 'Equipment' diff --git a/data/modules/Equipment/Internal.lua b/data/modules/Equipment/Internal.lua index 38255f8b01..4e776c4026 100644 --- a/data/modules/Equipment/Internal.lua +++ b/data/modules/Equipment/Internal.lua @@ -1,4 +1,4 @@ --- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt local EquipTypes = require 'EquipType' diff --git a/data/modules/Equipment/Stats.lua b/data/modules/Equipment/Stats.lua index 4c52d85dd3..43a8e201d1 100644 --- a/data/modules/Equipment/Stats.lua +++ b/data/modules/Equipment/Stats.lua @@ -1,4 +1,4 @@ --- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt local EquipTypes = require 'EquipType' diff --git a/data/modules/Equipment/Utility.lua b/data/modules/Equipment/Utility.lua index 83cda367ed..2b1f49b990 100644 --- a/data/modules/Equipment/Utility.lua +++ b/data/modules/Equipment/Utility.lua @@ -1,4 +1,4 @@ --- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt local EquipTypes = require 'EquipType' diff --git a/data/modules/Equipment/Weapons.lua b/data/modules/Equipment/Weapons.lua index dfaf95e238..79aedcd393 100644 --- a/data/modules/Equipment/Weapons.lua +++ b/data/modules/Equipment/Weapons.lua @@ -1,4 +1,4 @@ --- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt local EquipTypes = require 'EquipType' diff --git a/data/modules/MissionUtils/OutfitRules.lua b/data/modules/MissionUtils/OutfitRules.lua index 289e862a97..48d1817ce7 100644 --- a/data/modules/MissionUtils/OutfitRules.lua +++ b/data/modules/MissionUtils/OutfitRules.lua @@ -1,4 +1,4 @@ --- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt ---@class MissionUtils.OutfitRules diff --git a/data/modules/MissionUtils/ShipBuilder.lua b/data/modules/MissionUtils/ShipBuilder.lua index 649537c0bf..9b4d8d90d7 100644 --- a/data/modules/MissionUtils/ShipBuilder.lua +++ b/data/modules/MissionUtils/ShipBuilder.lua @@ -1,4 +1,4 @@ --- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt local Engine = require 'Engine' diff --git a/data/pigui/libs/equip-card.lua b/data/pigui/libs/equip-card.lua index 046f6baf30..396c3428c2 100644 --- a/data/pigui/libs/equip-card.lua +++ b/data/pigui/libs/equip-card.lua @@ -1,4 +1,4 @@ --- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt local ItemCard = require 'pigui.libs.item-card' diff --git a/data/pigui/libs/equipment-outfitter.lua b/data/pigui/libs/equipment-outfitter.lua index c1649e3ad0..895da26ab5 100644 --- a/data/pigui/libs/equipment-outfitter.lua +++ b/data/pigui/libs/equipment-outfitter.lua @@ -1,4 +1,4 @@ --- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt local Game = require 'Game' From 6c52d24ba4006fa11997681fdd33e522879e4e31 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 20 Sep 2024 16:03:39 -0400 Subject: [PATCH 053/119] Use shared ShipTemplates definition file --- data/libs/SpaceStation.lua | 24 +++----------- data/modules/Assassination/Assassination.lua | 20 +---------- data/modules/CargoRun/CargoRun.lua | 29 ++-------------- data/modules/MissionUtils/ShipTemplates.lua | 35 ++++++++++++++++++++ data/modules/Taxi/Taxi.lua | 15 +-------- 5 files changed, 44 insertions(+), 79 deletions(-) diff --git a/data/libs/SpaceStation.lua b/data/libs/SpaceStation.lua index 515d1ba1b9..87ffcebc48 100644 --- a/data/libs/SpaceStation.lua +++ b/data/libs/SpaceStation.lua @@ -15,16 +15,13 @@ local Engine = require 'Engine' local Timer = require 'Timer' local Game = require 'Game' local Ship = require 'Ship' -local Model = require 'SceneGraph.Model' local ModelSkin = require 'SceneGraph.ModelSkin' local Serializer = require 'Serializer' local Equipment = require 'Equipment' -local Commodities = require 'Commodities' -local Faction = require 'Faction' local Lang = require 'Lang' local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' -local Rules = ShipBuilder.OutfitRules +local ShipTemplates = require 'modules.MissionUtils.ShipTemplates' local l = Lang.GetResource("ui-core") @@ -634,20 +631,6 @@ SpaceStation.lawEnforcedRange = 50000 local police = {} -local policeTemplate = ShipBuilder.Template:clone { - role = "police", - label = l.POLICE, - rules = { - { - slot = "weapon", - equip = "laser.pulsecannon_dual_1mw", - limit = 1 - }, - Rules.DefaultAtmoShield, - Rules.DefaultLaserCooling - } -} - -- -- Method: LaunchPolice -- @@ -682,8 +665,9 @@ function SpaceStation:LaunchPolice(targetShip) -- In a high-law area, a spacestation has a bunch of traffic cops due to low crime rates local shipThreat = 10.0 + Engine.rand:Number(10, 50) * lawlessness - local shipTemplate = policeTemplate:clone { - shipId = Game.system.faction.policeShip + local shipTemplate = ShipTemplates.StationPolice:clone { + shipId = Game.system.faction.policeShip, + label = Game.system.faction.policeName or l.POLICE, } -- create and equip them diff --git a/data/modules/Assassination/Assassination.lua b/data/modules/Assassination/Assassination.lua index 65a7127318..2d0fe29cbd 100644 --- a/data/modules/Assassination/Assassination.lua +++ b/data/modules/Assassination/Assassination.lua @@ -18,7 +18,6 @@ local ShipDef = require 'ShipDef' local Ship = require 'Ship' local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' local HullConfig = require 'HullConfig' -local OutfitRules = ShipBuilder.OutfitRules local utils = require 'utils' local lc = Lang.GetResource 'core' @@ -27,24 +26,7 @@ local l = Lang.GetResource("module-assassination") -- don't produce missions for further than this many light years away local max_ass_dist = 30 -local AssassinationTargetShip = ShipBuilder.Template:clone { - role = "mercenary", - rules = { - OutfitRules.DifficultWeapon, - OutfitRules.ModerateWeapon, - OutfitRules.EasyWeapon, - -- Set shield gens according to mission difficulty - OutfitRules.DifficultShieldGen, - OutfitRules.ModerateShieldGen, - OutfitRules.EasyShieldGen, - -- Enable add-on equipment based on mission difficulty - utils.mixin(OutfitRules.DefaultShieldBooster, { minThreat = 30.0 }), - utils.mixin(OutfitRules.DefaultLaserCooling, { minThreat = 40.0 }), - -- Default equipment in remaining space - OutfitRules.DefaultHyperdrive, - OutfitRules.DefaultAtmoShield, - } -} +local AssassinationTargetShip = MissionUtils.ShipTemplates.GenericMercenary local flavours = {} for i = 0,5 do diff --git a/data/modules/CargoRun/CargoRun.lua b/data/modules/CargoRun/CargoRun.lua index 9f271700e0..67b7aed4bf 100644 --- a/data/modules/CargoRun/CargoRun.lua +++ b/data/modules/CargoRun/CargoRun.lua @@ -22,32 +22,8 @@ local l = Lang.GetResource("module-cargorun") local l_ui_core = Lang.GetResource("ui-core") local lc = Lang.GetResource 'core' -local PirateTemplate = ShipBuilder.Template:clone { - role = "pirate", - rules = { - OutfitRules.PulsecannonModerateWeapon, - OutfitRules.PulsecannonEasyWeapon, - OutfitRules.EasyShieldGen, - OutfitRules.DefaultHyperdrive, - OutfitRules.DefaultAtmoShield, - utils.mixin(OutfitRules.DefaultLaserCooling, { minThreat = 40.0 }), - utils.mixin(OutfitRules.DefaultShieldBooster, { minThreat = 30.0 }), - } -} - -local EscortTemplate = ShipBuilder.Template:clone { - role = "police", - label = l_ui_core.POLICE, - rules = { - OutfitRules.ModerateWeapon, - OutfitRules.EasyWeapon, - OutfitRules.ModerateShieldGen, - OutfitRules.EasyShieldGen, - utils.mixin(OutfitRules.DefaultLaserCooling, { minThreat = 30.0 }), - OutfitRules.DefaultHyperdrive, - OutfitRules.DefaultAtmoShield - } -} +local PirateTemplate = MissionUtils.ShipTemplates.WeakPirate +local EscortTemplate = MissionUtils.ShipTemplates.GenericPolice -- don't produce missions for further than this many light years away local max_delivery_dist = 15 @@ -557,6 +533,7 @@ local onEnterSystem = function (player) if mission.wholesaler or Engine.rand:Number(0, 1) >= 0.75 then local policeTemplate = EscortTemplate:clone { + label = Game.system.faction.policeName, shipId = Game.system.faction.policeShip } diff --git a/data/modules/MissionUtils/ShipTemplates.lua b/data/modules/MissionUtils/ShipTemplates.lua index 2dbd9f1d0b..666ff5097b 100644 --- a/data/modules/MissionUtils/ShipTemplates.lua +++ b/data/modules/MissionUtils/ShipTemplates.lua @@ -75,4 +75,39 @@ ShipTemplates.GenericPolice = ShipBuilder.Template:clone { } } +ShipTemplates.StationPolice = ShipBuilder.Template:clone { + role = "police", + rules = { + { + slot = "weapon", + equip = "laser.pulsecannon_dual_1mw", + limit = 1 + }, + OutfitRules.ModerateShieldGen, + -- Always has laser cooling but no need for hyperdrive + OutfitRules.DefaultLaserCooling, + OutfitRules.DefaultAtmoShield, + OutfitRules.DefaultAutopilot + } +} + +ShipTemplates.GenericMercenary = ShipBuilder.Template:clone { + role = "mercenary", + rules = { + OutfitRules.DifficultWeapon, + OutfitRules.ModerateWeapon, + OutfitRules.EasyWeapon, + -- Set shield gens according to mission difficulty + OutfitRules.DifficultShieldGen, + OutfitRules.ModerateShieldGen, + OutfitRules.EasyShieldGen, + -- Enable add-on equipment based on mission difficulty + utils.mixin(OutfitRules.DefaultLaserCooling, { minThreat = 40.0 }), + utils.mixin(OutfitRules.DefaultShieldBooster, { minThreat = 30.0 }), + -- Default equipment in remaining space + OutfitRules.DefaultHyperdrive, + OutfitRules.DefaultAtmoShield, + } +} + return ShipTemplates diff --git a/data/modules/Taxi/Taxi.lua b/data/modules/Taxi/Taxi.lua index 7d1f9d14e0..80049c2518 100644 --- a/data/modules/Taxi/Taxi.lua +++ b/data/modules/Taxi/Taxi.lua @@ -16,20 +16,7 @@ local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' local ShipDef = require 'ShipDef' local utils = require 'utils' -local OutfitRules = ShipBuilder.OutfitRules - -local PirateTemplate = ShipBuilder.Template:clone { - role = "pirate", - rules = { - OutfitRules.PulsecannonModerateWeapon, - OutfitRules.PulsecannonEasyWeapon, - OutfitRules.EasyShieldGen, - OutfitRules.DefaultHyperdrive, - OutfitRules.DefaultAtmoShield, - utils.mixin(OutfitRules.DefaultLaserCooling, { minThreat = 40.0 }), - utils.mixin(OutfitRules.DefaultShieldBooster, { minThreat = 30.0 }), - } -} +local PirateTemplate = MissionUtils.ShipTemplates.GenericPirate -- Get the language resource local l = Lang.GetResource("module-taxi") From 4f13325664e9d530412c65b01aea147291f8f709 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 20 Sep 2024 15:48:27 -0400 Subject: [PATCH 054/119] ShipBuilder: remove MakeGenericPirateNear() ShipBuilder: add MakeShipOrbit, MakeShipLanded methods --- data/modules/MissionUtils/ShipBuilder.lua | 84 +++++++++++++---------- 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/data/modules/MissionUtils/ShipBuilder.lua b/data/modules/MissionUtils/ShipBuilder.lua index 9b4d8d90d7..cc331a998b 100644 --- a/data/modules/MissionUtils/ShipBuilder.lua +++ b/data/modules/MissionUtils/ShipBuilder.lua @@ -547,7 +547,7 @@ end ---@param threat number? ---@param nearDist number? ---@param farDist number? ----@return Ship? +---@return Ship function ShipBuilder.MakeShipNear(body, template, threat, nearDist, farDist) if not threat then threat = Engine.rand:Number(ShipBuilder.kDefaultRandomThreatMin, ShipBuilder.kDefaultRandomThreatMax) @@ -569,9 +569,11 @@ end ---@param body Body ---@param template MissionUtils.ShipTemplate ----@param threat number? ----@return Ship? -function ShipBuilder.MakeShipDocked(body, template, threat) +---@param threat number +---@param nearDist number +---@param farDist number +---@return Ship +function ShipBuilder.MakeShipOrbit(body, template, threat, nearDist, farDist) if not threat then threat = Engine.rand:Number(ShipBuilder.kDefaultRandomThreatMin, ShipBuilder.kDefaultRandomThreatMax) end @@ -582,7 +584,7 @@ function ShipBuilder.MakeShipDocked(body, template, threat) local plan = ShipBuilder.MakePlan(template, hullConfig, threat) assert(plan) - local ship = Space.SpawnShipDocked(plan.shipId, body) + local ship = Space.SpawnShipOrbit(plan.shipId, body, nearDist, farDist) assert(ship) ShipBuilder.ApplyPlan(ship, plan) @@ -590,50 +592,56 @@ function ShipBuilder.MakeShipDocked(body, template, threat) return ship end --- ============================================================================= +---@param body Body +---@param template MissionUtils.ShipTemplate +---@param threat number +---@param lat number +---@param lon number +---@return Ship +function ShipBuilder.MakeShipLanded(body, template, threat, lat, lon) + if not threat then + threat = Engine.rand:Number(ShipBuilder.kDefaultRandomThreatMin, ShipBuilder.kDefaultRandomThreatMax) + end -local randomPulsecannonEasyRule = { - slot = "weapon", - filter = "weapon.energy.pulsecannon", - pick = "random", - maxSize = 3, - limit = 1 -} - -local pirateProduction = Template:clone { - role = "pirate", - rules = { - Rules.DefaultHyperdrive, - randomPulsecannonEasyRule, - Rules.DefaultAtmoShield, - Rules.DefaultShieldGen, - Rules.DefaultAutopilot - } -} + local hullConfig = ShipBuilder.SelectHull(template, threat) + assert(hullConfig) ----@param player Ship ----@param nearDist number? ----@param farDist number? -function ShipBuilder.MakeGenericPirateNear(player, risk, nearDist, farDist) + local plan = ShipBuilder.MakePlan(template, hullConfig, threat) + assert(plan) - local ship = ShipBuilder.MakeShipNear(player, pirateProduction, risk, nearDist, farDist) + local ship = Space.SpawnShipLanded(plan.shipId, body, lat, lon) + assert(ship) - -- TODO: handle risk/difficulty-based installation of equipment as part of - -- the loadout rules - local equipSet = ship:GetComponent('EquipSet') + ShipBuilder.ApplyPlan(ship, plan) - if Engine.rand:Number(2) <= risk then - equipSet:Install(Equipment.Get("misc.laser_cooling_booster")) - end + return ship +end - if Engine.rand:Number(3) <= risk then - equipSet:Install(Equipment.Get("misc.shield_energy_booster")) +---@param body Body +---@param template MissionUtils.ShipTemplate +---@param threat number? +---@return Ship +function ShipBuilder.MakeShipDocked(body, template, threat) + if not threat then + threat = Engine.rand:Number(ShipBuilder.kDefaultRandomThreatMin, ShipBuilder.kDefaultRandomThreatMax) end - return ship + local hullConfig = ShipBuilder.SelectHull(template, threat) + assert(hullConfig) + local plan = ShipBuilder.MakePlan(template, hullConfig, threat) + assert(plan) + + local ship = Space.SpawnShipDocked(plan.shipId, body) + assert(ship) + + ShipBuilder.ApplyPlan(ship, plan) + + return ship end +-- ============================================================================= + -- Generate a cache of combined hull threat factor for each ship in the game function ShipBuilder.BuildHullThreatCache() for id, shipDef in pairs(ShipDef) do From d862b384b43e61211c3ae2ee070897393d415d63 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 21 Sep 2024 13:22:49 -0400 Subject: [PATCH 055/119] ShipBuilder: add minimum hyperclass filter to hull selection --- data/modules/MissionUtils/DebugShipBuilder.lua | 4 +++- data/modules/MissionUtils/ShipBuilder.lua | 7 ++++++- data/modules/MissionUtils/ShipTemplates.lua | 5 +++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/data/modules/MissionUtils/DebugShipBuilder.lua b/data/modules/MissionUtils/DebugShipBuilder.lua index ecf71d3856..6310c2f155 100644 --- a/data/modules/MissionUtils/DebugShipBuilder.lua +++ b/data/modules/MissionUtils/DebugShipBuilder.lua @@ -14,7 +14,9 @@ local ui = require 'pigui' local debugRule = ShipBuilder.Template:clone { label = "TEST SHIP", - shipId = 'coronatrix', + -- shipId = 'coronatrix', + hyperclass = 1, + role = "mercenary", rules = { { slot = "weapon", diff --git a/data/modules/MissionUtils/ShipBuilder.lua b/data/modules/MissionUtils/ShipBuilder.lua index cc331a998b..ebbf6d2897 100644 --- a/data/modules/MissionUtils/ShipBuilder.lua +++ b/data/modules/MissionUtils/ShipBuilder.lua @@ -93,6 +93,7 @@ ShipBuilder.kShieldSharedHullThreat = 0.005 local Template = utils.proto("MissionUtils.ShipTemplate") Template.role = nil ---@type string? +Template.hyperclass = nil ---@type number? Template.shipId = nil ---@type string? Template.label = nil ---@type string? Template.rules = {} ---@type MissionUtils.OutfitRule[] @@ -440,7 +441,11 @@ function ShipBuilder.SelectHull(template, threat) for id, shipDef in pairs(ShipDef) do - if shipDef.tag == "SHIP" and (not template.role or shipDef.roles[template.role]) then + local acceptable = shipDef.tag == "SHIP" + and (not template.role or shipDef.roles[template.role]) + and (not template.hyperclass or shipDef.hyperdriveClass >= template.hyperclass) + + if acceptable then local hullThreat = ShipBuilder.GetHullThreat(id).total diff --git a/data/modules/MissionUtils/ShipTemplates.lua b/data/modules/MissionUtils/ShipTemplates.lua index 666ff5097b..ee43be2585 100644 --- a/data/modules/MissionUtils/ShipTemplates.lua +++ b/data/modules/MissionUtils/ShipTemplates.lua @@ -12,6 +12,7 @@ local ShipTemplates = {} ShipTemplates.StrongPirate = ShipBuilder.Template:clone { role = "pirate", + hyperclass = 1, rules = { -- If the pirate is threatening enough, it can have any weapon, -- otherwise it will get a simple spread of pulsecannons @@ -33,6 +34,7 @@ ShipTemplates.StrongPirate = ShipBuilder.Template:clone { ShipTemplates.GenericPirate = ShipBuilder.Template:clone { role = "pirate", + hyperclass = 1, rules = { OutfitRules.PulsecannonModerateWeapon, OutfitRules.EasyWeapon, @@ -48,6 +50,7 @@ ShipTemplates.GenericPirate = ShipBuilder.Template:clone { ShipTemplates.WeakPirate = ShipBuilder.Template:clone { role = "pirate", + hyperclass = 1, rules = { OutfitRules.PulsecannonModerateWeapon, OutfitRules.PulsecannonEasyWeapon, @@ -62,6 +65,7 @@ ShipTemplates.WeakPirate = ShipBuilder.Template:clone { ShipTemplates.GenericPolice = ShipBuilder.Template:clone { role = "police", + hyperclass = 1, rules = { OutfitRules.ModerateWeapon, OutfitRules.EasyWeapon, @@ -93,6 +97,7 @@ ShipTemplates.StationPolice = ShipBuilder.Template:clone { ShipTemplates.GenericMercenary = ShipBuilder.Template:clone { role = "mercenary", + hyperclass = 1, rules = { OutfitRules.DifficultWeapon, OutfitRules.ModerateWeapon, From 7a0224036096e39361f9f34e8a8223d9c9927e09 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 21 Sep 2024 13:23:05 -0400 Subject: [PATCH 056/119] Combat: convert to ShipBuilder APIs --- data/modules/Combat/Combat.lua | 68 ++++++++++++---------------------- 1 file changed, 24 insertions(+), 44 deletions(-) diff --git a/data/modules/Combat/Combat.lua b/data/modules/Combat/Combat.lua index 4183d85647..4dc335783b 100644 --- a/data/modules/Combat/Combat.lua +++ b/data/modules/Combat/Combat.lua @@ -9,16 +9,15 @@ local Comms = require 'Comms' local Event = require 'Event' local Timer = require 'Timer' local Mission = require 'Mission' -local MissionUtils = require 'modules.MissionUtils' local Format = require 'Format' local Serializer = require 'Serializer' local Character = require 'Character' local NameGen = require 'NameGen' -local Equipment = require 'Equipment' -local ShipDef = require 'ShipDef' -local Ship = require 'Ship' local utils = require 'utils' +local MissionUtils = require 'modules.MissionUtils' +local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' + local l = Lang.GetResource("module-combat") local lc = Lang.GetResource 'core' @@ -347,45 +346,26 @@ local onFrameChanged = function (player) if ships < 1 and Engine.rand:Integer(math.ceil(1/mission.risk)) == 1 then ships = 1 end - local shipdefs = utils.build_array(utils.filter( - function (k,def) - return def.tag == "SHIP" and def.hyperdriveClass > 0 and def.equipSlotCapacity.laser_front > 0 and def.roles[mission.flavour.enemy] - end, - pairs(ShipDef))) - if #shipdefs == 0 then ships = 0 end - while ships > 0 do ships = ships - 1 if Engine.rand:Number(1) <= mission.risk then - local shipdef = shipdefs[Engine.rand:Integer(1, #shipdefs)] - local default_drive = Equipment.hyperspace["hyperdrive_" .. tostring(shipdef.hyperdriveClass)] - - local max_laser_size = shipdef.capacity - default_drive.capabilities.mass - local laserdefs = utils.build_array(utils.filter( - function (k,l) - return l:IsValidSlot("laser_front") and l.capabilities.mass <= max_laser_size and l.l10n_key:find("PULSECANNON") - end, - pairs(Equipment.laser) - )) - local laserdef = laserdefs[Engine.rand:Integer(1, #laserdefs)] + + local threat = 10 + mission.risk * 40.0 + + local template = MissionUtils.ShipTemplates.GenericMercenary:clone { + role = mission.flavour.enemy + } local ship if mission.location:GetSystemBody().type == "PLANET_GAS_GIANT" then - ship = Space.SpawnShipOrbit(shipdef.id, player.frameBody, 1.2 * planet_radius, 3.5 * planet_radius) + ship = ShipBuilder.MakeShipOrbit(player.frameBody, template, threat, 1.2 * planet_radius, 3.5 * planet_radius) else - ship = Space.SpawnShipLanded(shipdef.id, player.frameBody, math.rad(Engine.rand:Number(360)), math.rad(Engine.rand:Number(360))) - end - ship:SetLabel(Ship.MakeRandomLabel()) - ship:AddEquip(default_drive) - ship:AddEquip(laserdef) - ship:AddEquip(Equipment.misc.shield_generator, math.ceil(mission.risk * 3)) - if Engine.rand:Number(2) <= mission.risk then - ship:AddEquip(Equipment.misc.laser_cooling_booster) - end - if Engine.rand:Number(3) <= mission.risk then - ship:AddEquip(Equipment.misc.shield_energy_booster) + ship = ShipBuilder.MakeShipLanded(player.frameBody, template, threat, math.rad(Engine.rand:Number(360)), math.rad(Engine.rand:Number(360))) end + + assert(ship) + table.insert(mission.mercenaries, ship) ship:AIEnterLowOrbit(Space.GetBody(mission.location.bodyIndex)) end @@ -446,16 +426,16 @@ local onEnterSystem = function (player) for ref, mission in pairs(missions) do if mission.rendezvous and mission.rendezvous:IsSameSystem(Game.system.path) then if mission.complete or Game.time > mission.due then - local shipdefs = utils.build_array(utils.filter( - function (k,def) - return def.tag == "SHIP" and def.hyperdriveClass > 0 - end, - pairs(ShipDef))) - local shipdef = shipdefs[Engine.rand:Integer(1, #shipdefs)] - - local ship = Space.SpawnShipNear(shipdef.id, Game.player, 50, 100) - ship:SetLabel(Ship.MakeRandomLabel()) - ship:AddEquip(Equipment.hyperspace["hyperdrive_" .. tostring(shipdef.hyperdriveClass)]) + local template = ShipBuilder.Template:clone { + hyperclass = 1, + rules = { + ShipBuilder.OutfitRules.DefaultHyperdrive, + ShipBuilder.OutfitRules.DefaultAutopilot, + } + } + + local ship = ShipBuilder.MakeShipNear(Game.player, template) + assert(ship) local path = mission.location:GetStarSystem().path finishMission(ref, mission) From cd667821349bec2142123fe3b4cf7e892a9fc0d5 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 21 Sep 2024 13:26:20 -0400 Subject: [PATCH 057/119] DeliverPackage: convert to ShipBuilder APIs --- .../modules/DeliverPackage/DeliverPackage.lua | 29 +++++-------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/data/modules/DeliverPackage/DeliverPackage.lua b/data/modules/DeliverPackage/DeliverPackage.lua index f5e10d99a1..2a8585396e 100644 --- a/data/modules/DeliverPackage/DeliverPackage.lua +++ b/data/modules/DeliverPackage/DeliverPackage.lua @@ -8,15 +8,14 @@ local Space = require 'Space' local Comms = require 'Comms' local Event = require 'Event' local Mission = require 'Mission' -local MissionUtils = require 'modules.MissionUtils' local Format = require 'Format' local Serializer = require 'Serializer' local Character = require 'Character' -local Equipment = require 'Equipment' -local ShipDef = require 'ShipDef' -local Ship = require 'Ship' local utils = require 'utils' +local MissionUtils = require 'modules.MissionUtils' +local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' + local l = Lang.GetResource("module-deliverpackage") local lc = Lang.GetResource 'core' @@ -366,30 +365,16 @@ local onEnterSystem = function (player) -- if there is some risk and still no ships, flip a tricoin if ships < 1 and risk >= 0.2 and Engine.rand:Integer(2) == 1 then ships = 1 end - local shipdefs = utils.build_array(utils.filter(function (k,def) return def.tag == 'SHIP' - and def.hyperdriveClass > 0 and (def.roles.pirate or def.roles.mercenary) end, pairs(ShipDef))) - if #shipdefs == 0 then return end - local ship while ships > 0 do ships = ships-1 if Engine.rand:Number(1) <= risk then - local shipdef = shipdefs[Engine.rand:Integer(1,#shipdefs)] - local default_drive = Equipment.hyperspace['hyperdrive_'..tostring(shipdef.hyperdriveClass)] - - local max_laser_size = shipdef.capacity - default_drive.capabilities.mass - local laserdefs = utils.build_array(utils.filter( - function (k,l) return l:IsValidSlot('laser_front') and l.capabilities.mass <= max_laser_size and l.l10n_key:find("PULSECANNON") end, - pairs(Equipment.laser) - )) - local laserdef = laserdefs[Engine.rand:Integer(1,#laserdefs)] - - ship = Space.SpawnShipNear(shipdef.id, Game.player, 50, 100) - ship:SetLabel(Ship.MakeRandomLabel()) - ship:AddEquip(default_drive) - ship:AddEquip(laserdef) + local threat = 10.0 + mission.risk * 25.0 + ship = ShipBuilder.MakeShipNear(Game.player, MissionUtils.ShipTemplates.WeakPirate, threat, 50, 100) + assert(ship) + ship:AIKill(Game.player) end end From a95d794575fabb49a372d7c0ff0d3b9a20284f18 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 21 Sep 2024 13:35:48 -0400 Subject: [PATCH 058/119] PolicePatrol: convert to ShipBuilder APIs --- data/modules/MissionUtils/ShipTemplates.lua | 19 +++++++++++ data/modules/PolicePatrol/PolicePatrol.lua | 35 +++++++++++++-------- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/data/modules/MissionUtils/ShipTemplates.lua b/data/modules/MissionUtils/ShipTemplates.lua index ee43be2585..10f1129b44 100644 --- a/data/modules/MissionUtils/ShipTemplates.lua +++ b/data/modules/MissionUtils/ShipTemplates.lua @@ -87,6 +87,8 @@ ShipTemplates.StationPolice = ShipBuilder.Template:clone { equip = "laser.pulsecannon_dual_1mw", limit = 1 }, + OutfitRules.ModerateWeapon, + OutfitRules.EasyWeapon, OutfitRules.ModerateShieldGen, -- Always has laser cooling but no need for hyperdrive OutfitRules.DefaultLaserCooling, @@ -95,6 +97,23 @@ ShipTemplates.StationPolice = ShipBuilder.Template:clone { } } +ShipTemplates.PolicePatrol = ShipBuilder.Template:clone { + role = "police", + rules = { + { + slot = "weapon", + equip = "laser.pulsecannon_1mw", + limit = 1 + }, + OutfitRules.ModerateWeapon, + OutfitRules.EasyWeapon, + OutfitRules.ModerateShieldGen, + OutfitRules.EasyShieldGen, + OutfitRules.DefaultAtmoShield, + OutfitRules.DefaultAutopilot + } +} + ShipTemplates.GenericMercenary = ShipBuilder.Template:clone { role = "mercenary", hyperclass = 1, diff --git a/data/modules/PolicePatrol/PolicePatrol.lua b/data/modules/PolicePatrol/PolicePatrol.lua index 9845c57563..8b67f85af3 100644 --- a/data/modules/PolicePatrol/PolicePatrol.lua +++ b/data/modules/PolicePatrol/PolicePatrol.lua @@ -9,11 +9,12 @@ local Comms = require 'Comms' local Event = require 'Event' local Legal = require 'Legal' local Serializer = require 'Serializer' -local Equipment = require 'Equipment' -local ShipDef = require 'ShipDef' local Timer = require 'Timer' local Commodities = require 'Commodities' +local MissionUtils = require 'modules.MissionUtils' +local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' + local l = Lang.GetResource("module-policepatrol") local l_ui_core = Lang.GetResource("ui-core") @@ -83,8 +84,10 @@ local onShipHit = function (ship, attacker) if not attacker:isa('Ship') then return end -- Support all police, not only the patrol - local police = ShipDef[Game.system.faction.policeShip] - if ship.shipId == police.id and attacker.shipId ~= police.id then + -- TODO: this should use a property set on the ship rather than checking for the shipid + -- (what happens if the player is committing piracy in a police hull?) + local policeId = Game.system.faction.policeShip + if ship.shipId == policeId and attacker.shipId ~= policeId then piracy = true attackShip(attacker) end @@ -93,8 +96,8 @@ end local onShipFiring = function (ship) if piracy or target then return end - local police = ShipDef[Game.system.faction.policeShip] - if ship.shipId ~= police.id then + local policeId = Game.system.faction.policeShip + if ship.shipId ~= policeId then for i = 1, #patrol do if ship:DistanceTo(patrol[i]) <= lawEnforcedRange then Comms.ImportantMessage(string.interp(l_ui_core.X_CANNOT_BE_TOLERATED_HERE, { crime = l_ui_core.UNLAWFUL_WEAPONS_DISCHARGE }), patrol[i].label) @@ -125,17 +128,23 @@ local onEnterSystem = function (player) if not hasIllegalGoods(Commodities) then return end - local system = Game.system + local system = assert(Game.system) if (1 - system.lawlessness) < Engine.rand:Number(4) then return end local crimes, fine = player:GetCrimeOutstanding() local ship - local shipdef = ShipDef[system.faction.policeShip] local n = 1 + math.floor((1 - system.lawlessness) * (system.population / 3)) + + local threat = 10.0 + Engine.rand:Number(10, 50) * system.lawlessness + local template = MissionUtils.ShipTemplates.PolicePatrol:clone { + shipId = system.faction.policeShip, + label = system.faction.policeName + } + for i = 1, n do - ship = Space.SpawnShipNear(shipdef.id, player, 50, 100) - ship:SetLabel(l_ui_core.POLICE) - ship:AddEquip(Equipment.laser.pulsecannon_1mw) + ship = ShipBuilder.MakeShipNear(player, template, threat, 50, 100) + assert(ship) + table.insert(patrol, ship) end @@ -164,14 +173,14 @@ local onEnterSystem = function (player) end end - local police = ShipDef[system.faction.policeShip] + local policeId = system.faction.policeShip Timer:CallEvery(15, function () if not Game.system or #patrol == 0 then return true end if target then return false end local ships = Space.GetBodiesNear(patrol[1], lawEnforcedRange, "Ship") for _, enemy in ipairs(ships) do - if enemy.shipId ~= police.id and enemy:GetCurrentAICommand() == "CMD_KILL" then + if enemy.shipId ~= policeId and enemy:GetCurrentAICommand() == "CMD_KILL" then if not piracy then Comms.ImportantMessage(string.interp(l_ui_core.X_CANNOT_BE_TOLERATED_HERE, { crime = l_ui_core.PIRACY }), patrol[1].label) Comms.ImportantMessage(string.interp(l["RESTRICTIONS_WITHDRAWN_" .. Engine.rand:Integer(1, getNumberOfFlavours("RESTRICTIONS_WITHDRAWN"))], { ship_label = player.label }), patrol[1].label) From bb09c289d88b4bd22cbd6e01f597f8692416a94b Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 21 Sep 2024 13:43:06 -0400 Subject: [PATCH 059/119] Rondel: convert to ShipBuilder APIs --- data/modules/Rondel/Rondel.lua | 38 +++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/data/modules/Rondel/Rondel.lua b/data/modules/Rondel/Rondel.lua index 079073c492..487549d86b 100644 --- a/data/modules/Rondel/Rondel.lua +++ b/data/modules/Rondel/Rondel.lua @@ -4,22 +4,40 @@ local Engine = require 'Engine' local Lang = require 'Lang' local Game = require 'Game' -local Space = require 'Space' local Comms = require 'Comms' local Event = require 'Event' -local Legal = require 'Legal' local Serializer = require 'Serializer' -local Equipment = require 'Equipment' local ShipDef = require 'ShipDef' local SystemPath = require 'SystemPath' local Timer = require 'Timer' local Commodities = require 'Commodities' +local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' +local OutfitRules = ShipBuilder.OutfitRules + --local Character = require 'Character' local l_rondel = Lang.GetResource("module-rondel") local l_ui_core = Lang.GetResource("ui-core") +local HaberPatrolCraft = ShipBuilder.Template:clone { + label = l_rondel.HABER_DEFENSE_CRAFT, + role = "police", -- overridden by setting shipId + rules = { + { + slot = "weapon", + equip = "laser.pulsecannon_2mw", + limit = 1 + }, + OutfitRules.ModerateWeapon, + OutfitRules.EasyWeapon, + OutfitRules.ModerateShieldGen, + OutfitRules.EasyShieldGen, + OutfitRules.DefaultAtmoShield, + OutfitRules.DefaultAutopilot + } +} + local patrol = {} local shipFiring = false local jetissionedCargo = false @@ -138,7 +156,7 @@ end local onEnterSystem = function (player) if not player:IsPlayer() then return end - local system = Game.system + local system = assert(Game.system) if not system.path:IsSameSystem(rondel_syspath) then return end local tolerance = 1 @@ -148,11 +166,15 @@ local onEnterSystem = function (player) end local ship - local shipdef = ShipDef[system.faction.policeShip] + + local threat = 20.0 + Engine.rand:Number(10.0, 30.0) + local template = HaberPatrolCraft:clone { + shipId = system.faction.policeShip + } + for i = 1, 7 do - ship = Space.SpawnShipNear(shipdef.id, player, 50, 100) - ship:SetLabel(l_rondel.HABER_DEFENSE_CRAFT) - ship:AddEquip(Equipment.laser.pulsecannon_2mw) + ship = ShipBuilder.MakeShipNear(player, template, threat, 50, 100) + assert(ship) table.insert(patrol, ship) end From 1daa1ea3741d33784406e37b218053678afeb530 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 21 Sep 2024 13:55:34 -0400 Subject: [PATCH 060/119] Scoop: convert to ShipBuilder APIs --- data/modules/Scoop/Scoop.lua | 52 +++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/data/modules/Scoop/Scoop.lua b/data/modules/Scoop/Scoop.lua index 278d6cb5ff..bff187b565 100644 --- a/data/modules/Scoop/Scoop.lua +++ b/data/modules/Scoop/Scoop.lua @@ -11,12 +11,13 @@ local Timer = require 'Timer' local Engine = require 'Engine' local Format = require 'Format' local Mission = require 'Mission' -local MissionUtils = require 'modules.MissionUtils' -local ShipDef = require 'ShipDef' local Character = require 'Character' -local Equipment = require 'Equipment' local Serializer = require 'Serializer' +local MissionUtils = require 'modules.MissionUtils' +local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' +local OutfitRules = ShipBuilder.OutfitRules + local CommodityType = require 'CommodityType' local Commodities = require 'Commodities' @@ -209,13 +210,17 @@ end local spawnPolice = function (station) local ship local police = {} - local shipdef = ShipDef[Game.system.faction.policeShip] + + local threat = 10.0 + Engine.rand:Number(10.0, 20.0) + local template = MissionUtils.ShipTemplates.PolicePatrol:clone { + shipId = Game.system.faction.policeShip, + label = Game.system.faction.policeName or lc.POLICE + } for i = 1, 2 do - ship = Space.SpawnShipDocked(shipdef.id, station) - ship:SetLabel(lc.POLICE) - ship:AddEquip(Equipment.laser.pulsecannon_1mw) + ship = ShipBuilder.MakeShipDocked(station, template, threat) table.insert(police, ship) + if station.type == "STARPORT_SURFACE" then ship:AIEnterLowOrbit(Space.GetBody(station:GetSystemBody().parent.index)) end @@ -244,13 +249,25 @@ local nearbySystem = function () end -- Create a ship in orbit +---@param star SystemPath local spawnClientShip = function (star, ship_label) - local shipdefs = utils.build_array(utils.filter( - function (k, def) - return def.tag == "SHIP" and def.hyperdriveClass > 0 and def.equipSlotCapacity["scoop"] > 0 - end, - pairs(ShipDef))) - local shipdef = shipdefs[Engine.rand:Integer(1, #shipdefs)] + + local template = ShipBuilder.Template:clone { + role = "merchant", + label = ship_label, + hyperclass = 1, + -- TODO: mission module wants this ship to have a scoop slot... but doesn't put a scoop in it + rules = { + OutfitRules.ModerateWeapon, + OutfitRules.EasyWeapon, + OutfitRules.ModerateShieldGen, + OutfitRules.EasyShieldGen, + OutfitRules.DefaultHyperdrive, + OutfitRules.DefaultAutopilot, + } + } + + local threat = 10.0 + Engine.rand:Number(10, 80) local radius = star:GetSystemBody().radius local min, max @@ -262,14 +279,7 @@ local spawnClientShip = function (star, ship_label) max = radius * 4.5 end - local ship = Space.SpawnShipOrbit(shipdef.id, Space.GetBody(star:GetSystemBody().index), min, max) - - ship:SetLabel(ship_label) - ship:AddEquip(Equipment.hyperspace["hyperdrive_" .. shipdef.hyperdriveClass]) - ship:AddEquip(Equipment.laser.pulsecannon_2mw) - ship:AddEquip(Equipment.misc.shield_generator) - - return ship + return ShipBuilder.MakeShipOrbit(star:GetSystemBody().body, template, threat, min, max) end local removeMission = function (mission, ref) From 00dc552cbc415cb478836c82449b99385f96d0e3 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 21 Sep 2024 16:42:17 -0400 Subject: [PATCH 061/119] Pirates: migrate to ShipBuilder APIs - Pirates will now have a chance to not spawn, rather than spinning endlessly until it draws a random number below the system's lawlessness factor --- data/modules/Pirates.lua | 55 +++++++++++++++------------------------- 1 file changed, 21 insertions(+), 34 deletions(-) diff --git a/data/modules/Pirates.lua b/data/modules/Pirates.lua index 9ed61554c4..ef71e859c9 100644 --- a/data/modules/Pirates.lua +++ b/data/modules/Pirates.lua @@ -3,13 +3,13 @@ local Engine = require 'Engine' local Game = require 'Game' -local Space = require 'Space' local Event = require 'Event' -local Equipment = require 'Equipment' local ShipDef = require 'ShipDef' -local Ship = require 'Ship' local utils = require 'utils' +local MissionUtils = require 'modules.MissionUtils' +local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' + local onEnterSystem = function (player) if not player:IsPlayer() then return end @@ -22,39 +22,26 @@ local onEnterSystem = function (player) -- XXX number should be some combination of population, lawlessness, -- proximity to shipping lanes, etc local max_pirates = 6 - while max_pirates > 0 and Engine.rand:Number(1) < lawlessness do + while max_pirates > 0 do max_pirates = max_pirates-1 - local shipdef = shipdefs[Engine.rand:Integer(1,#shipdefs)] - local default_drive = Equipment.hyperspace['hyperdrive_'..tostring(shipdef.hyperdriveClass)] - assert(default_drive) -- never be nil. - - -- select a laser. this is naive - it simply chooses at random from - -- the set of lasers that will fit, but never more than one above the - -- player's current weapon. - -- XXX this should use external factors (eg lawlessness) and not be - -- dependent on the player in any way - local max_laser_size = shipdef.capacity - default_drive.capabilities.mass - local laserdefs = utils.build_array(utils.filter( - function (k,l) return l:IsValidSlot('laser_front') and l.capabilities.mass <= max_laser_size and l.l10n_key:find("PULSECANNON") end, - pairs(Equipment.laser) - )) - local laserdef = laserdefs[Engine.rand:Integer(1,#laserdefs)] - - local ship = Space.SpawnShip(shipdef.id, 8, 12) - ship:SetLabel(Ship.MakeRandomLabel()) - ship:AddEquip(default_drive) - ship:AddEquip(laserdef) - - -- pirates know how big cargo hold the ship model has - local playerCargoCapacity = ShipDef[player.shipId].capacity - - -- Pirate attack probability proportional to how fully loaded cargo hold is. - local discount = 2 -- discount on 2t for small ships. - local probabilityPirateIsInterested = math.floor(player.usedCargo - discount) / math.max(1, playerCargoCapacity - discount) - - if Engine.rand:Number(1) <= probabilityPirateIsInterested then - ship:AIKill(Game.player) + if Engine.rand:Number(1) < lawlessness then + -- Simple threat calculation based on lawlessness factor and independent of the player's current state. + -- This will likely not produce an assailant larger than a Deneb. + local threat = 10.0 + Engine.rand:Number(100.0 * lawlessness) + + local ship = ShipBuilder.MakeShipAroundStar(MissionUtils.ShipTemplates.GenericPirate, threat, 8, 12) + + -- pirates know how big cargo hold the ship model has + local playerCargoCapacity = ShipDef[player.shipId].capacity + + -- Pirate attack probability proportional to how fully loaded cargo hold is. + local discount = 2 -- discount on 2t for small ships. + local probabilityPirateIsInterested = math.floor(player.usedCargo - discount) / math.max(1, playerCargoCapacity - discount) + + if Engine.rand:Number(1) <= probabilityPirateIsInterested then + ship:AIKill(Game.player) + end end end end From c31e21ba72a353d39f676c257e1d2938a3c8c678 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 21 Sep 2024 16:42:37 -0400 Subject: [PATCH 062/119] ShipBuilder: add MakeShipAroundStar --- data/modules/MissionUtils/ShipBuilder.lua | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/data/modules/MissionUtils/ShipBuilder.lua b/data/modules/MissionUtils/ShipBuilder.lua index ebbf6d2897..549b2a4b16 100644 --- a/data/modules/MissionUtils/ShipBuilder.lua +++ b/data/modules/MissionUtils/ShipBuilder.lua @@ -645,6 +645,30 @@ function ShipBuilder.MakeShipDocked(body, template, threat) return ship end +---@param template MissionUtils.ShipTemplate +---@param threat number +---@param minDistAu number +---@param maxDistAu number +---@return Ship +function ShipBuilder.MakeShipAroundStar(template, threat, minDistAu, maxDistAu) + if not threat then + threat = Engine.rand:Number(ShipBuilder.kDefaultRandomThreatMin, ShipBuilder.kDefaultRandomThreatMax) + end + + local hullConfig = ShipBuilder.SelectHull(template, threat) + assert(hullConfig) + + local plan = ShipBuilder.MakePlan(template, hullConfig, threat) + assert(plan) + + local ship = Space.SpawnShip(plan.shipId, minDistAu or 1, maxDistAu or 10) + assert(ship) + + ShipBuilder.ApplyPlan(ship, plan) + + return ship +end + -- ============================================================================= -- Generate a cache of combined hull threat factor for each ship in the game From c185d8991e994d829c46b41bfe7a0cfafefb12f6 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Tue, 24 Sep 2024 22:58:39 -0400 Subject: [PATCH 063/119] EquipSet: add equipment class filter to GetInstalledWithFilter() --- data/libs/EquipSet.lua | 12 +++++++----- data/libs/EquipType.lua | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/data/libs/EquipSet.lua b/data/libs/EquipSet.lua index 45eb295cd3..97a2a6d100 100644 --- a/data/libs/EquipSet.lua +++ b/data/libs/EquipSet.lua @@ -163,14 +163,16 @@ end -- Function: GetInstalledWithFilter -- --- Return a list of all installed equipment matching the given filter function ----@param filter fun(equip: EquipType): boolean ----@return EquipType[] -function EquipSet:GetInstalledWithFilter(filter) +-- Return a list of all installed equipment of the given EquipType class matching the filter function +---@generic T : EquipType +---@param typename `T` +---@param filter fun(equip: T): boolean +---@return T[] +function EquipSet:GetInstalledWithFilter(typename, filter) local out = {} for _, equip in pairs(self.installed) do - if filter(equip) then + if equip:IsA(typename) and filter(equip) then table.insert(out, equip) end end diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index c3d6b1d648..be7224990a 100644 --- a/data/libs/EquipType.lua +++ b/data/libs/EquipType.lua @@ -164,7 +164,7 @@ function EquipType.SetupPrototype(type) -- delegates serialization to the base class of the proto function type.New(...) local inst = old(...) - inst.meta = { __index = inst, class = type.meta.class } + inst.meta = utils.mixin(type.meta, { __index = inst }) return inst end From 241df22fe5ff49a740f6be362a9049c46fa236c5 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Tue, 24 Sep 2024 22:58:57 -0400 Subject: [PATCH 064/119] Improve Lua type information --- data/meta/ShipDef.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/data/meta/ShipDef.lua b/data/meta/ShipDef.lua index 0a739c45b3..285e594ae0 100644 --- a/data/meta/ShipDef.lua +++ b/data/meta/ShipDef.lua @@ -5,6 +5,8 @@ ---@meta +---@alias ShipDef.Thrust { UP:number, DOWN:number, LEFT:number, RIGHT:number, FORWARD:number, REVERSE:number } + ---@class ShipDef ---@field id string ---@field name string @@ -16,8 +18,8 @@ ---@field roles string[] -- ---@field angularThrust number ----@field linearThrust table ----@field linAccelerationCap table +---@field linearThrust ShipDef.Thrust +---@field linAccelerationCap ShipDef.Thrust ---@field effectiveExhaustVelocity number ---@field thrusterFuelUse number -- deprecated ---@field frontCrossSec number From 7f02d0b9cbf96f726791cd4f6b125dd0614c31d3 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Tue, 24 Sep 2024 23:01:16 -0400 Subject: [PATCH 065/119] Support multiple passengers per installed cabin - No UI support for inspecting "cabin state" yet - Passenger cabin equipment items support multiple passengers - Cabins are subdivided into logical "berths" - No consideration of passengers unwilling to share cabins with strangers --- data/libs/EquipType.lua | 34 ++++++++++--- data/libs/Passengers.lua | 87 +++++++++++++++++++++++--------- data/modules/Equipment/Stats.lua | 37 ++++++++++++++ 3 files changed, 129 insertions(+), 29 deletions(-) diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index be7224990a..31bf9bd035 100644 --- a/data/libs/EquipType.lua +++ b/data/libs/EquipType.lua @@ -425,27 +425,49 @@ local SensorType = utils.inherits(EquipType, "SensorType") local BodyScannerType = utils.inherits(SensorType, "BodyScannerType") ---@class Equipment.CabinType : EquipType ----@field passenger Character? +---@field passengers Character[]? local CabinType = utils.inherits(EquipType, "Equipment.CabinType") ---@param passenger Character function CabinType:AddPassenger(passenger) - self.passenger = passenger + table.insert(self.passengers, passenger) self.icon_name = "equip_cabin_occupied" end ---@param passenger Character function CabinType:RemovePassenger(passenger) - self.passenger = nil + utils.remove_elem(self.passengers, passenger) self.icon_name = "equip_cabin_empty" end +function CabinType:HasPassenger(passenger) + return utils.contains(self.passengers, passenger) +end + +function CabinType:GetNumPassengers() + return self.passengers and #self.passengers or 0 +end + +function CabinType:GetMaxPassengers() + return self.capabilities.cabin +end + +function CabinType:GetFreeBerths() + return self.capabilities.cabin - (self.passengers and #self.passengers or 0) +end + +function CabinType:OnInstall(ship, slot) + EquipType.OnInstall(self, ship, slot) + + self.passengers = {} +end + function CabinType:OnRemove(ship, slot) EquipType.OnRemove(self, ship, slot) - if self.passenger then - logWarning("Removing passenger cabin with passenger onboard!") - ship:setprop("cabin_occupied_cap", ship["cabin_occupied_cap"] - 1) + if self.passengers then + logWarning("Removing passenger cabin with passengers onboard!") + ship:setprop("cabin_occupied_cap", ship["cabin_occupied_cap"] - #self.passengers) end end diff --git a/data/libs/Passengers.lua b/data/libs/Passengers.lua index 3bca1f22cd..9c1df1c808 100644 --- a/data/libs/Passengers.lua +++ b/data/libs/Passengers.lua @@ -2,6 +2,8 @@ -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt local Equipment = require 'Equipment' +local EquipSet = require 'EquipSet' + local utils = require 'utils' -- Module: Passengers @@ -38,10 +40,9 @@ end ---@return Equipment.CabinType[] function Passengers.GetOccupiedCabins(ship) local equipSet = ship:GetComponent("EquipSet") - local cabin = Equipment.Get("misc.cabin") - return equipSet:GetInstalledWithFilter(function(equip) - return equip:GetPrototype() == cabin and equip.passenger + return equipSet:GetInstalledWithFilter('Equipment.CabinType', function(equip) + return equip:GetNumPassengers() > 0 end) end @@ -50,13 +51,13 @@ end -- Return a list of currently free passenger cabins -- ---@param ship Ship +---@param numPassengers integer? ---@return Equipment.CabinType[] -function Passengers.GetFreeCabins(ship) +function Passengers.GetFreeCabins(ship, numPassengers) local equipSet = ship:GetComponent("EquipSet") - local cabin = Equipment.Get("misc.cabin") - return equipSet:GetInstalledWithFilter(function(equip) - return equip:GetPrototype() == cabin and equip.passenger == nil + return equipSet:GetInstalledWithFilter('Equipment.CabinType', function(equip) + return equip:GetFreeBerths() >= (numPassengers or 1) end) end @@ -69,17 +70,17 @@ end ---@return integer function Passengers.CheckEmbarked(ship, passengers) local occupiedCabins = Passengers.GetOccupiedCabins(ship) - local passengerLookup = {} - - for i, cabin in ipairs(occupiedCabins) do - passengerLookup[cabin.passenger] = true - end + local passengerLookup = utils.map_table(passengers, function(_, passenger) + return passenger, true + end) local count = 0 - for i, passenger in ipairs(passengers) do - if passengerLookup[passenger] then - count = count + 1 + for _, cabin in ipairs(occupiedCabins) do + for _, passenger in ipairs(cabin.passengers) do + if passengerLookup[passenger] then + count = count + 1 + end end end @@ -94,15 +95,15 @@ end ---@param passenger Character ---@param cabin EquipType? function Passengers.EmbarkPassenger(ship, passenger, cabin) + ---@cast cabin Equipment.CabinType? if not cabin then cabin = Passengers.GetFreeCabins(ship)[1] end - if not cabin or cabin.passenger then + if not cabin then return false end - ---@cast cabin Equipment.CabinType cabin:AddPassenger(passenger) ship:setprop("cabin_occupied_cap", (ship["cabin_occupied_cap"] or 0) + 1) @@ -119,24 +120,64 @@ end ---@param passenger Character ---@param cabin EquipType? function Passengers.DisembarkPassenger(ship, passenger, cabin) + ---@cast cabin Equipment.CabinType? if not cabin then - local cabinProto = Equipment.Get("misc.cabin") local equipSet = ship:GetComponent("EquipSet") - cabin = equipSet:GetInstalledWithFilter(function(equip) - return equip:GetPrototype() == cabinProto and equip.passenger == passenger - end)[1] + ---@type Equipment.CabinType[] + local cabins = equipSet:GetInstalledWithFilter('Equipment.CabinType', function(equip) + return equip:HasPassenger(passenger) + end) + + cabin = cabins[1] + else + cabin = cabin:HasPassenger(passenger) and cabin or nil end - if not cabin or cabin.passenger ~= passenger then + if not cabin then return false end - ---@cast cabin Equipment.CabinType cabin:RemovePassenger(passenger) ship:setprop("cabin_occupied_cap", ship["cabin_occupied_cap"] - 1) return true end +-- Function: GetMaxPassengersForHull +-- +-- Determine the maximum number of passengers the passed HullConfig can +-- accommodate with its cabin slots. +-- +-- Optionally takes a mass limit to constrain the total mass cost of cabins +-- the ship can be equipped with. +---@param hull HullConfig +---@param maxMass number? +---@return integer +function Passengers.GetMaxPassengersForHull(hull, maxMass) + local numPassengers = 0 + local availMass = maxMass or math.huge + + ---@type Equipment.CabinType + local cabins = utils.to_array(Equipment.new, function(equip) + return equip:IsA('Equipment.CabinType') + end) + + -- Compute the theoretical maximum number of passengers + for _, slot in pairs(hull.slots) do + if EquipSet.SlotTypeMatches(slot.type, "cabin") then + local cabin, passengers = utils.best_score(cabins, function(_, equip) + return EquipSet.CompatibleWithSlot(equip, slot) + and (availMass - equip.mass > 0) + and equip.capabilities.cabin or nil + end) + + numPassengers = numPassengers + passengers + availMass = availMass - cabin.mass + end + end + + return numPassengers +end + return Passengers diff --git a/data/modules/Equipment/Stats.lua b/data/modules/Equipment/Stats.lua index 43a8e201d1..cdeca8a58a 100644 --- a/data/modules/Equipment/Stats.lua +++ b/data/modules/Equipment/Stats.lua @@ -49,6 +49,7 @@ end --============================================================================== +---@class Equipment.LaserType local LaserType = EquipTypes.LaserType local format_rpm = function(v) return string.format("%d RPM", 60 / v) end @@ -84,6 +85,7 @@ end --============================================================================== +---@class Equipment.BodyScannerType local BodyScannerType = EquipTypes.BodyScannerType local format_px = function(v) return string.format("%s px", ui.Format.Number(v, 0)) end @@ -110,3 +112,38 @@ function BodyScannerType:GetDetailedStats() end --============================================================================== + +---@class Equipment.CabinType +local CabinType = EquipTypes.CabinType + +function CabinType:GetDetailedStats() + local out = self:Super().GetDetailedStats(self) + + table.insert(out, { + le.PASSENGER_BERTHS, + icons.personal, + self.capabilities.cabin, + tostring + }) + + table.insert(out, { + le.OCCUPIED_BERTHS, + icons.personal, + self:GetNumPassengers(), + tostring + }) + + return out +end + +---@return table[] +function CabinType:GetItemCardStats() + return { + { icons.personal, "{}/{}" % { self:GetNumPassengers(), self:GetMaxPassengers() } }, + { icons.hull, format_mass(self.mass) }, + { icons.ecm, format_power(0) }, + { icons.repairs, format_integrity(1) } + } +end + +--============================================================================== From 9c87300b5167b58bcec05bf072443c923d4604f0 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Tue, 24 Sep 2024 23:33:01 -0400 Subject: [PATCH 066/119] Add basic cabin equipment for S1-S5 - Cabins show their occupied status via icon and count on equip card - Mass and price is generally placeholder for cabin equipment --- data/lang/equipment-core/en.json | 16 +++++++++++ data/modules/Equipment/Internal.lua | 43 +++++++++++++++++++++++++++-- data/modules/Equipment/Stats.lua | 8 +++--- data/pigui/libs/ship-equipment.lua | 1 + 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/data/lang/equipment-core/en.json b/data/lang/equipment-core/en.json index 140b376c18..b9b9a83f48 100644 --- a/data/lang/equipment-core/en.json +++ b/data/lang/equipment-core/en.json @@ -495,6 +495,10 @@ "description": "", "message": "Sensor" }, + "SLOT_CABIN": { + "description": "", + "message": "Cabin" + }, "SLOT_COMPUTER": { "description": "", "message": "Computer" @@ -550,5 +554,17 @@ "OPLI_INTERNAL_MISSILE_RACK_S2": { "description": "", "message": "OPLI Internal Missile Rack" + }, + "CABINS": { + "description": "Category header for pressurized cabin slots", + "message": "Cabins" + }, + "PASSENGER_BERTHS": { + "description": "", + "message": "Passenger Berths" + }, + "OCCUPIED_BERTHS": { + "description": "", + "message": "Occupied Berths" } } diff --git a/data/modules/Equipment/Internal.lua b/data/modules/Equipment/Internal.lua index 4e776c4026..28a344058f 100644 --- a/data/modules/Equipment/Internal.lua +++ b/data/modules/Equipment/Internal.lua @@ -152,16 +152,53 @@ Equipment.Register("misc.multi_scoop", EquipType.New { }) --=============================================== --- Slot-less equipment +-- Passenger Cabins --=============================================== -Equipment.Register("misc.cabin", CabinType.New { +Equipment.Register("misc.cabin_s1", CabinType.New { l10n_key="UNOCCUPIED_CABIN", price=1350, purchasable=true, tech_level=1, - mass=1, volume=5, capabilities={ cabin=1 }, + slot = { type="cabin.passenger.basic", size=1 }, + mass=1, volume=0, capabilities={ cabin=1 }, + icon_name="equip_cabin_empty" +}) + +Equipment.Register("misc.cabin_s2", CabinType.New { + l10n_key="UNOCCUPIED_CABIN", + price=3550, purchasable=true, tech_level=1, + slot = { type="cabin.passenger.basic", size=2 }, + mass=4, volume=0, capabilities={ cabin=3 }, icon_name="equip_cabin_empty" }) +Equipment.Register("misc.cabin_s3", CabinType.New { + l10n_key="UNOCCUPIED_CABIN", + price=6550, purchasable=true, tech_level=1, + slot = { type="cabin.passenger.basic", size=3 }, + mass=8, volume=0, capabilities={ cabin=8 }, + icon_name="equip_cabin_empty" +}) + +Equipment.Register("misc.cabin_s4", CabinType.New { + l10n_key="UNOCCUPIED_CABIN", + price=13550, purchasable=true, tech_level=1, + slot = { type="cabin.passenger.basic", size=4 }, + mass=16, volume=0, capabilities={ cabin=22 }, + icon_name="equip_cabin_empty" +}) + +Equipment.Register("misc.cabin_s5", CabinType.New { + l10n_key="UNOCCUPIED_CABIN", + price=35150, purchasable=true, tech_level=1, + slot = { type="cabin.passenger.basic", size=5 }, + mass=36, volume=0, capabilities={ cabin=60 }, + icon_name="equip_cabin_empty" +}) + +--=============================================== +-- Slot-less equipment +--=============================================== + Equipment.Register("misc.laser_cooling_booster", EquipType.New { l10n_key="LASER_COOLING_BOOSTER", price=380, purchasable=true, tech_level=8, diff --git a/data/modules/Equipment/Stats.lua b/data/modules/Equipment/Stats.lua index cdeca8a58a..b8bed1ce56 100644 --- a/data/modules/Equipment/Stats.lua +++ b/data/modules/Equipment/Stats.lua @@ -120,16 +120,16 @@ function CabinType:GetDetailedStats() local out = self:Super().GetDetailedStats(self) table.insert(out, { - le.PASSENGER_BERTHS, + le.OCCUPIED_BERTHS, icons.personal, - self.capabilities.cabin, + self:GetNumPassengers(), tostring }) table.insert(out, { - le.OCCUPIED_BERTHS, + le.PASSENGER_BERTHS, icons.personal, - self:GetNumPassengers(), + self.capabilities.cabin, tostring }) diff --git a/data/pigui/libs/ship-equipment.lua b/data/pigui/libs/ship-equipment.lua index 516f7dc8ea..0949703548 100644 --- a/data/pigui/libs/ship-equipment.lua +++ b/data/pigui/libs/ship-equipment.lua @@ -40,6 +40,7 @@ EquipmentWidget.Sections = { { name = le.SHIELDS, type = "shield" }, { name = le.SENSORS, type = "sensor", }, { name = le.COMPUTER_MODULES, type = "computer", }, + { name = le.CABINS, types = { "cabin" } }, { name = le.HULL_MOUNTS, types = { "hull", "utility" } }, } From a2bb6968d2940b5f03ea34178d3ba88482701264 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Tue, 24 Sep 2024 23:36:54 -0400 Subject: [PATCH 067/119] EquipType: remove __call metamethod - Functions cannot be serialized - It's much easier to find an explicit Instance() call --- data/libs/EquipType.lua | 4 ---- 1 file changed, 4 deletions(-) diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index 31bf9bd035..630017a631 100644 --- a/data/libs/EquipType.lua +++ b/data/libs/EquipType.lua @@ -178,10 +178,6 @@ function EquipType.SetupPrototype(type) return inst end - - type.meta.__call = function(equip) - return setmetatable({ __proto = equip }, equip.meta) - end end function EquipType:Serialize() From b0b6012b6df3fecec7669917af59bd1b11558b99 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Tue, 24 Sep 2024 23:41:32 -0400 Subject: [PATCH 068/119] SearchRescue: convert to use ShipBuilder APIs - Major refactor and cleanup of outdated / superseded code - Drop many utility functions which didn't have any benefit over directly doing the operation - Use ShipBuilder to outfit SAR ships - Perform all ship hull filtering in a single pass rather than striking indicies in many iterations - Pickup and delivery of cargo can both happen in the same tick; at worst, this makes SAR operations spend less time landed --- data/modules/MissionUtils/OutfitRules.lua | 5 + data/modules/SearchRescue/SearchRescue.lua | 378 +++++++++------------ 2 files changed, 158 insertions(+), 225 deletions(-) diff --git a/data/modules/MissionUtils/OutfitRules.lua b/data/modules/MissionUtils/OutfitRules.lua index 48d1817ce7..5a827d68c8 100644 --- a/data/modules/MissionUtils/OutfitRules.lua +++ b/data/modules/MissionUtils/OutfitRules.lua @@ -85,6 +85,11 @@ OutfitRules.EasyShieldGen = { -- Default rules always equip the item if there's enough space +OutfitRules.DefaultPassengerCabins = { + slot = "cabin", + filter = "cabin.passenger" +} + OutfitRules.DefaultHyperdrive = { slot = "hyperdrive" } diff --git a/data/modules/SearchRescue/SearchRescue.lua b/data/modules/SearchRescue/SearchRescue.lua index fdfe16b514..700418845b 100644 --- a/data/modules/SearchRescue/SearchRescue.lua +++ b/data/modules/SearchRescue/SearchRescue.lua @@ -33,6 +33,7 @@ local Game = require 'Game' local Space = require 'Space' local Comms = require 'Comms' local Event = require 'Event' +local HullConfig = require 'HullConfig' local Mission = require 'Mission' local Format = require 'Format' local Serializer = require 'Serializer' @@ -46,6 +47,12 @@ local utils = require 'utils' local Timer = require 'Timer' local Rand = require 'Rand' local ModelSkin = require 'SceneGraph.ModelSkin' + +local MissionUtils = require 'modules.MissionUtils' +local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' + +local OutfitRules = ShipBuilder.OutfitRules + local l = Lang.GetResource("module-searchrescue") local lc = Lang.GetResource 'core' @@ -223,62 +230,12 @@ for i = 1,#flavours do end - --- basic lua helper functions --- ========================== - -local arraySize = function (array) - -- Return the size (length) of an array that contains arbitrary entries. - local n = 0 - for _,_ in pairs(array) do n = n + 1 end - return n -end - -local containerContainsKey = function (container, key) - -- Return true if key is in container and false if not. - return container[key] ~= nil -end - -local copyTable = function (orig) - -- Return a copy of a table. Copies only the direct children (no deep copy!). - -- Taken from http://lua-users.org/wiki/CopyTable. - local orig_type = type(orig) - local copy - if orig_type == 'table' then - copy = {} - for orig_key, orig_value in pairs(orig) do - copy[orig_key] = orig_value - end - else -- number, string, boolean, etc - copy = orig - end - return copy -end - -local compressTableKeys = function (t) - -- Return the table with all keys in numerical order without gaps. - -- Taken from http://www.computercraft.info/forums2/index.php?/topic/18380-how-do-i-remove-gaps-in-an-ordered-list/. - local keySet = {} - for i in pairs(t) do - table.insert(keySet, i) - end - - table.sort(keySet) - - local retVal = {} - for i = 1, #keySet do - retVal[i] = t[keySet[i]] - end - - return retVal -end - -- housekeeping mission functions -- ============================== local addMission = function (mission) -- Add the supplied mission to the player, generate a unique ID and store this mission within the script. - table.insert(missions,Mission.New(mission)) + table.insert(missions, Mission.New(mission)) end local removeMission = function (mission) @@ -306,7 +263,7 @@ local triggerAdCreation = function () local freq = Game.system.lawlessness * ad_freq_max if freq < ad_freq_min then freq = ad_freq_min end local ad_num_max = freq * #stations - if arraySize(ads) < ad_num_max then + if utils.count(ads) < ad_num_max then if Engine.rand:Integer(0,1) == 1 then return true end @@ -324,11 +281,6 @@ local getNumberOfFlavours = function (str) return num - 1 end -local mToAU = function (meters) - -- Transform meters into AU. - return meters/149598000000 -end - local splitName = function (name) -- Splits the supplied name into first and last name and returns a table of both separately. -- Idea from http://stackoverflow.com/questions/2779700/lua-split-into-words. @@ -352,7 +304,7 @@ end local getAircontrolChar = function (station) -- Get the correct aircontrol character for the supplied station. If it does not exist -- create one and store it. - if containerContainsKey(aircontrol_chars, station.path) then + if aircontrol_chars[station.path] then return aircontrol_chars[station.path] else local char = Character.New() @@ -389,14 +341,6 @@ local randomLatLong = function (station) return lat, long, dist end -local shipdefFromName = function (shipdef_name) - -- Return the corresponding shipdef for the supplied shipdef name. Necessary because serialization - -- crashes if actual shipdef is stored in ad. There may be a smarter way to do this! - local shipdefs = utils.build_array(utils.filter(function (_,def) return def.tag == 'SHIP' - and def.name == shipdef_name end, pairs(ShipDef))) - return shipdefs[1] -end - local crewPresent = function (ship) -- Check if any crew is present on the ship. if ship:CrewNumber() > 0 then @@ -523,6 +467,7 @@ local calcReward = function (flavour, pickup_crew, pickup_pass, pickup_comm, del return reward end +---@param planet SystemPath local createTargetShipParameters = function (flavour, planet) -- Create the basic parameters for the target ship. It is important to set these before ad creation -- so certain info can be included in the ad text. The actual ship is created once the mission has @@ -532,81 +477,85 @@ local createTargetShipParameters = function (flavour, planet) local seed = Engine.rand:Integer(1,1000000) local rand = Rand.New(seed) - -- pick appropriate hull type - local shipdefs = utils.build_array(utils.filter(function (_,def) return def.tag == 'SHIP' - end, pairs(ShipDef))) - - ----> no police ships or other non-buyable ships - for i,shipdef in pairs(shipdefs) do - if shipdef.basePrice == 0 then shipdefs[i] = nil end - end - ----> hyperdrive mandatory (for clean exiting of ships) - for i,shipdef in pairs(shipdefs) do - if shipdef.equipSlotCapacity.engine == 0 then shipdefs[i] = nil end - end - ----> atmo-shield if ship stranded on planet - if flavour.loctype == "CLOSE_PLANET" or flavour.loctype == "MEDIUM_PLANET" then - for i,shipdef in pairs(shipdefs) do - -- make sure that this ship model has atmosferic shield capacity and can take off from the planet - -- e.g. Natrix with some fuel cant take off from Earth .. - local fullMass = shipdef.hullMass + shipdef.capacity + shipdef.fuelTankMass - local UpAccelFull = math.abs(shipdef.linearThrust.UP / (1000*fullMass)) - if shipdef.equipSlotCapacity.atmo_shield == 0 or planet:GetSystemBody().gravity > UpAccelFull then - shipdefs[i] = nil - end + ---@type table + local passengers = {} + + local shipdefs = utils.to_array(ShipDef, function(def) + if def.tag ~= 'SHIP' then + return false end - end - ----> crew quarters for crew delivery missions - if flavour.id == 7 then - for i,shipdef in pairs(shipdefs) do - if shipdef.maxCrew < 2 or shipdef.minCrew < 2 then shipdefs[i] = nil end + + ----> no police ships or other non-buyable ships + if def.basePrice == 0 then + return false end - elseif flavour.deliver_crew > 0 then - for i,shipdef in pairs(shipdefs) do - if shipdef.maxCrew < flavour.deliver_crew+1 then shipdefs[i] = nil end + + ----> hyperdrive mandatory (for clean exiting of ships) + if def.hyperdriveClass == 0 then + return false end - end - ----> crew quarters for crew pickup missions - if flavour.pickup_crew > 0 then - for i,shipdef in pairs(shipdefs) do - if shipdef.maxCrew < flavour.pickup_crew then shipdefs[i] = nil end + + ----> has to be able to take off from the planet with full fuel mass + local fullMass = def.hullMass + def.capacity + def.fuelTankMass + local upAccelFull = math.abs(def.linearThrust.UP / (1000 * fullMass)) + + if upAccelFull <= planet:GetSystemBody().gravity then + return false end - end - ----> cargo space for passenger pickup missions - ---- (this is just an estimate to make sure enough space remains after - ---- loading drive, weapons etc. - if flavour.id == 1 or flavour.id == 6 then - for i,shipdef in pairs(shipdefs) do - - -- get mass of hyperdrive if this ship has a default drive - -- if no default drive assume lowest mass drive - -- higher mass drives will only be fitted later at ship creation if capacity is huge - local drive = Equipment.hyperspace['hyperdrive_'..tostring(shipdef.hyperdriveClass)] - if not drive then - local drives = {} - for i = 9, 1, -1 do - table.insert(drives, Equipment.hyperspace['hyperdrive_'..tostring(i)]) - end - table.sort(drives, function (a,b) return a.capabilities.mass < b.capabilities.mass end) - drive = drives[1] + + -- TODO: do we need to filter for atmo shield capability? + local maxPressure = planet:GetSystemBody().surfacePressure + if def.atmosphericPressureLimit < maxPressure then + return false + end + + ----> crew quarters for crew delivery missions + if flavour.id == 7 then + if def.maxCrew < 2 or def.minCrew < 2 then + return false end - if (shipdef.capacity - drive.capabilities.mass - drive.capabilities.hyperclass^2 ) < 2 - or shipdef.equipSlotCapacity.cargo < drive.capabilities.hyperclass^2 - or shipdef.equipSlotCapacity.cabin == 0 then - shipdefs[i] = nil + elseif flavour.deliver_crew > 0 then + if def.maxCrew < flavour.deliver_crew+1 then + return false end end - end + ----> crew quarters for crew pickup missions + if flavour.pickup_crew > 0 then + if def.maxCrew < flavour.pickup_crew then + return false + end + end + + ----> needs to have enough passenger space for pickup + if flavour.id == 1 or flavour.id == 6 then + local config = HullConfig.GetHullConfigs()[def.id] ---@type HullConfig + + -- should have a default hull config + if not config then + return false + end + + -- limit the amount of cabins installed by the lift capability of the ship at full load + local lifted_mass = (def.linearThrust.UP / 1000) / planet:GetSystemBody().gravity + 1.0 + local max_cabin_mass = lifted_mass - fullMass + + local numPassengers = Passengers.GetMaxPassengersForHull(config, max_cabin_mass) + if numPassengers == 0 then + return false + end - if arraySize(shipdefs) == 0 then + passengers[def.id] = numPassengers + end + + return true + end) + + if #shipdefs == 0 then print("Could not find appropriate ship type for this mission!") return end - -- 1. compress table keys to eliminate 'nil' entries - -- 2. sort shipdefs by name so the list has the same order every time - -- utils.build_array returns the list with random order - shipdefs = compressTableKeys(shipdefs) + -- sort shipdefs by name so the list has the same order every time table.sort(shipdefs, function (a,b) return a.name < b.name end) local shipdef = shipdefs[rand:Integer(1,#shipdefs)] @@ -644,19 +593,7 @@ local createTargetShipParameters = function (flavour, planet) if flavour.id == 1 or flavour.id == 6 then local any_pass = rand:Integer(0,1) if any_pass > 0 then - local drive = Equipment.hyperspace['hyperdrive_'..tostring(shipdef.hyperdriveClass)] - if not drive then - local drives = {} - for i = 9, 1, -1 do - table.insert(drives, Equipment.hyperspace['hyperdrive_'..tostring(i)]) - end - table.sort(drives, function (a,b) return a.capabilities.mass < b.capabilities.mass end) - drive = drives[1] - end - --after drive, hyper fuel, atmo shield, laser - local max_cabins = shipdef.capacity - drive.capabilities.mass - drive.capabilities.hyperclass^2 - 2 - max_cabins = math.min(max_cabins, shipdef.equipSlotCapacity.cabin) - pickup_pass = rand:Integer(1, math.min(max_cabins, max_pass)) + pickup_pass = rand:Integer(1, passengers[shipdef.id]) else pickup_pass = 0 end @@ -673,22 +610,35 @@ end local createTargetShip = function (mission) -- Create the target ship to be search for. local ship ---@type Ship - local shipdef = shipdefFromName(mission.shipdef_name) + local shipdef = ShipDef[mission.shipid] + + local stranded = ShipBuilder.Template:clone { + shipId = mission.shipid, + rules = { + OutfitRules.EasyWeapon, + OutfitRules.DefaultHyperdrive, + OutfitRules.DefaultAutopilot, + OutfitRules.DefaultAtmoShield, + OutfitRules.DefaultPassengerCabins, + } + } -- set rand with unique ship seed local rand = Rand.New(mission.shipseed) + local planet = mission.planet_target and mission.planet_target:GetSystemBody().body + local station = mission.station_target and mission.station_target:GetSystemBody().body + -- create ship if mission.flavour.loctype == "CLOSE_PLANET" then - ship = Space.SpawnShipLanded(shipdef.id, Space.GetBody(mission.planet_target.bodyIndex), mission.lat, mission.long) + ship = ShipBuilder.MakeShipLanded(planet, stranded, math.huge, mission.lat, mission.long) elseif mission.flavour.loctype == "MEDIUM_PLANET" then - ship = Space.SpawnShipLanded(shipdef.id, Space.GetBody(mission.planet_target.bodyIndex), mission.lat, mission.long) + ship = ShipBuilder.MakeShipLanded(planet, stranded, math.huge, mission.lat, mission.long) elseif mission.flavour.loctype == "CLOSE_SPACE" then - ship = Space.SpawnShipNear(shipdef.id, Space.GetBody(mission.station_target.bodyIndex), mission.dist/1000, mission.dist/1000) + ship = ShipBuilder.MakeShipNear(station, stranded, math.huge, mission.dist/1000, mission.dist/1000) elseif mission.flavour.loctype == "FAR_SPACE" then - local planet_body = Space.GetBody(mission.planet_target.bodyIndex) - local orbit_radius = planet_body:GetPhysicalRadius() * far_space_orbit_dist - ship = Space.SpawnShipOrbit(shipdef.id, planet_body, orbit_radius, orbit_radius) + local orbit_radius = planet:GetPhysicalRadius() * far_space_orbit_dist + ship = ShipBuilder.MakeShipOrbit(planet, stranded, math.huge, orbit_radius, orbit_radius) end -- set ship looks (label, skin, pattern) @@ -696,78 +646,49 @@ local createTargetShip = function (mission) ship:SetSkin(skin) ship:SetLabel(mission.shiplabel) local model = Engine.GetModel(shipdef.modelName) - local pattern - if model.numPatterns <= 1 then - pattern = 0 - else - local pattern = rand:Integer(0,model.numPatterns-1) - end - ship:SetPattern(pattern) - - -- load a hyperdrive - -- 1st try: default drive for this ship class - -- 2nd try: largest drive possible that doesn't take more than a 10th of available room - -- fallback: smallest drive - local drives = {} - local drive = Equipment.hyperspace['hyperdrive_'..tostring(shipdef.hyperdriveClass)] - if not drive then - for i = 9, 1, -1 do - table.insert(drives, Equipment.hyperspace['hyperdrive_'..tostring(i)]) - end - table.sort(drives, function (a,b) return a.mass < b.mass end) - for i = #drives, 1, -1 do - local test_drive = drives[i] - if shipdef.capacity / 10 > test_drive.mass then - drive = test_drive - end - end + if model.numPatterns > 0 then + ship:SetPattern(rand:Integer(1, model.numPatterns)) end - if not drive then drive = drives[1] end - ship:AddEquip(drive) - local equipSet = ship:GetComponent("EquipSet") - local cabinType = Equipment.Get("misc.cabin") - - for i = 1, math.max(mission.deliver_pass, mission.pickup_pass) do - local cabin = cabinType:Instance() - local installed = equipSet:Install(cabin) - assert(installed, "Not enough space in mission ship for all passengers!") - - if mission.return_pass[i] then - Passengers.EmbarkPassenger(ship, mission.return_pass[i]) - end - end - - -- add hydrogen for hyperjumping even for refueling missoins - to reserve the space - -- for refueling missions it is removed later - local drive = ship:GetInstalledHyperdrive() - local hypfuel = drive.capabilities.hyperclass ^ 2 -- fuel for max range - ship:GetComponent('CargoManager'):AddCommodity(drive.fuel or Commodities.hydrogen, hypfuel) + local available_cabins = Passengers.CountFreeCabins(ship) + assert(available_cabins >= math.max(mission.deliver_pass, mission.pickup_pass), + "Not enough space in mission ship for all passengers!") -- load crew for _ = 1, mission.crew_num do ship:Enroll(Character.New()) end - -- load atmo_shield - if shipdef.equipSlotCapacity.atmo_shield ~= 0 then - ship:AddEquip(Equipment.misc.atmospheric_shielding) + -- load passengers + local passenger_idx = 1 + + for _, cabin in ipairs(Passengers.GetFreeCabins(ship)) do + while passenger_idx <= #mission.return_pass and cabin:GetFreeBerths() > 0 do + if Passengers.EmbarkPassenger(ship, mission.return_pass[passenger_idx], cabin) then + passenger_idx = passenger_idx + 1 + else + -- Generally not expected to happen, here as a canary regardless + assert("Cannot put passenger into mission ship!") + end + end + end + + if passenger_idx <= #mission.return_pass then + -- Generally not expected to happen, here as a canary regardless + error("Could not fit all passengers to return into mission ship!") end - -- load a laser - local max_laser_size = ship.freeCapacity - local laserdefs = utils.build_array(utils.filter(function (_,laser) - return laser:IsValidSlot('laser_front') - and laser.capabilities.mass <= max_laser_size - and laser.l10n_key:find("PULSECANNON") - end, pairs(Equipment.laser) )) - local laserdef = laserdefs[rand:Integer(1,#laserdefs)] - ship:AddEquip(laserdef) + local is_refueling = mission.flavour.id == 2 or mission.flavour.id == 4 or mission.flavour.id == 5 - -- remove all fuel for refueling mission - if mission.flavour.id == 2 or mission.flavour.id == 4 or mission.flavour.id == 5 then + if is_refueling then + -- remove all fuel for refueling mission ship:SetFuelPercent(0) - ship:GetComponent('CargoManager'):RemoveCommodity(drive.fuel or Commodities.hydrogen, hypfuel) + else + -- add hydrogen for hyperjumping + -- FIXME(fuel): hyperdrives will have their own independent fuel tanks and we should not add fuel to the cargo bay + local drive = assert(ship:GetInstalledHyperdrive(), "No hyperdrive in stranded ship!") + local hypfuel = drive.capabilities.hyperclass ^ 2 -- fuel for max range + ship:GetComponent('CargoManager'):AddCommodity(drive.fuel or Commodities.hydrogen, hypfuel) end return ship @@ -797,12 +718,14 @@ local onChat = function (form, ref, option) -- end if option == 0 then -- repeat original request + local shipdef = ShipDef[ad.shipid] + local introtext = string.interp(ad.flavour.introtext, { name = ad.client.name, entity = ad.entity, problem = ad.problem, cash = Format.Money(ad.reward), - ship = ad.shipdef_name, + ship = shipdef.name, starport = ad.station_local:GetSystemBody().name, shiplabel = ad.shiplabel, planet = ad.planet_target:GetSystemBody().name, @@ -898,7 +821,7 @@ local onChat = function (form, ref, option) target = nil, lat = ad.lat, long = ad.long, - shipdef_name = ad.shipdef_name, + shipid = ad.shipid, shiplabel = ad.shiplabel, crew_num = ad.crew_num, shipseed = ad.shipseed, @@ -906,18 +829,18 @@ local onChat = function (form, ref, option) -- "..._orig" => original variables from ad pickup_crew_orig = ad.pickup_crew, pickup_pass_orig = ad.pickup_pass, - pickup_comm_orig = copyTable(ad.pickup_comm), + pickup_comm_orig = table.copy(ad.pickup_comm), deliver_crew_orig = ad.deliver_crew, deliver_pass_orig = ad.deliver_pass, - deliver_comm_orig = copyTable(ad.deliver_comm), + deliver_comm_orig = table.copy(ad.deliver_comm), -- variables are changed based on completion status pickup_crew = ad.pickup_crew, pickup_pass = ad.pickup_pass, - pickup_comm = copyTable(ad.pickup_comm), + pickup_comm = table.copy(ad.pickup_comm), deliver_crew = ad.deliver_crew, deliver_pass = ad.deliver_pass, - deliver_comm = copyTable(ad.deliver_comm), + deliver_comm = table.copy(ad.deliver_comm), pickup_crew_check = "NOT", pickup_pass_check = "NOT", @@ -1245,7 +1168,7 @@ local makeAdvert = function (station, manualFlavour, closestplanets) lat, long, dist = randomLatLong() dist = station:DistanceTo(Space.GetBody(planet_target.bodyIndex)) --overwrite empty dist from randomLatLong() --1 added for short distances when most of the time is spent at low average speed (accelerating and deccelerating) - due = (mToAU(dist) * 2 + 1) * Engine.rand:Integer(20,24) * 60 * 60 -- TODO: adjust due date based on urgency + due = (dist / MissionUtils.AU * 2 + 1) * Engine.rand:Integer(20,24) * 60 * 60 -- TODO: adjust due date based on urgency elseif flavour.loctype == "CLOSE_SPACE" then station_target = station_local @@ -1285,12 +1208,16 @@ local makeAdvert = function (station, manualFlavour, closestplanets) local pickup_comm, deliver_comm, deliver_pass deliver_pass = flavour.deliver_pass - pickup_comm = copyTable(flavour.pickup_comm) - deliver_comm = copyTable(flavour.deliver_comm) + pickup_comm = table.copy(flavour.pickup_comm) + deliver_comm = table.copy(flavour.deliver_comm) -- set target ship parameters and determine pickup and delivery of personnel based on mission flavour local shipdef, crew_num, shiplabel, pickup_crew, pickup_pass, deliver_crew, shipseed = createTargetShipParameters(flavour, planet_target) + if not shipdef then + return + end + -- adjust fuel to deliver based on selected ship and mission flavour local needed_fuel if flavour.id == 2 or flavour.id == 5 then @@ -1371,10 +1298,10 @@ local makeAdvert = function (station, manualFlavour, closestplanets) entity = entity, problem = problem, dist = dist, - due = due, + due = due, urgency = urgency, reward = reward, - shipdef_name = shipdef.name, -- saving the actual shipdef causes crash at serialization (ship undock) + shipid = shipdef.id, crew_num = crew_num, pickup_crew = pickup_crew, pickup_pass = pickup_pass, @@ -1838,8 +1765,8 @@ local interactWithTarget = function (mission) end -- pickup commodity-cargo from target ship - elseif arraySize(mission.pickup_comm) > 0 then - for commodity,_ in pairs(mission.pickup_comm) do + else + for commodity, _ in pairs(mission.pickup_comm) do if mission.pickup_comm[commodity] > 0 then pickupCommodity(mission, commodity) if mission.pickup_comm_check[commodity] == "PARTIAL" then @@ -1849,8 +1776,7 @@ local interactWithTarget = function (mission) end -- transfer commodity-cargo to target ship - elseif arraySize(mission.deliver_comm) > 0 then - for commodity,_ in pairs(mission.deliver_comm) do + for commodity, _ in pairs(mission.deliver_comm) do if mission.deliver_comm[commodity] > 0 then deliverCommodity(mission, commodity) if mission.deliver_comm_check[commodity] ~= "PARTIAL" then @@ -2183,8 +2109,10 @@ local buildMissionDescription = function(mission) l.LON.." "..decToDegMinSec(math.rad2deg(mission.long)) end + local shipname = ShipDef[mission.shipid].name + desc.details = { - { l.TARGET_SHIP_ID, mission.shipdef_name.." <"..mission.shiplabel..">" }, + { l.TARGET_SHIP_ID, shipname.." <"..mission.shiplabel..">" }, { l.LAST_KNOWN_LOCATION, targetLocation }, { l.SYSTEM, ui.Format.SystemPath(mission.system_target) }, { l.DISTANCE, dist }, From eca429f77a23c739fd3adff956d6c7954dcae03e Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 25 Sep 2024 17:33:57 -0400 Subject: [PATCH 069/119] EquipSet: prevent ID collisions for non-slot entries - Prevents an install->remove->install loop potentially giving two non-slot equipment items the same cached index - The cached index is used as a unique identifier to build the fully-qualified slot ID for recursive slots and thus must not have collisions --- data/libs/EquipSet.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/data/libs/EquipSet.lua b/data/libs/EquipSet.lua index 97a2a6d100..bc8cf96856 100644 --- a/data/libs/EquipSet.lua +++ b/data/libs/EquipSet.lua @@ -50,6 +50,10 @@ function EquipSet:Constructor(ship) -- index of the given item. It's simply a non-nil integer to indicate the -- item is not installed in a slot. self.cache = {} ---@type table + -- This provides a unique index value for non-slot equipment cache entries. + -- It is monotonically increasing and prevents ID collisions for recursive + -- slots provided by non-slot equipment items. + self.cacheIndex = 1 -- Stores a mapping of slot id -> slot handle -- Simplifies slot lookup for slots defined on equipment items @@ -283,7 +287,6 @@ end ---@param slotHandle HullConfig.Slot? ---@return boolean function EquipSet:Install(equipment, slotHandle) - print("Installing equip {} in slot {}" % { equipment:GetName(), slotHandle }) local slotId = self.idCache[slotHandle] if slotHandle then @@ -307,7 +310,8 @@ function EquipSet:Install(equipment, slotHandle) end table.insert(self.installed, equipment) - self.cache[equipment] = #self.installed + self.cache[equipment] = self.cacheIndex + self.cacheIndex = self.cacheIndex + 1 end equipment:OnInstall(self.ship, slotHandle) From 1e397f6ad3d187b8392af594328b0a4b7bdcead8 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 25 Sep 2024 17:39:48 -0400 Subject: [PATCH 070/119] ShipBuilder: add randomChance rule selector - Instead of tying rules to the incoming total threat factor, rules can instead use a random roll to determine if they're applied - The threshold for this roll can be modified by an external value set in the ship template - Primarily intended to be used in the case where equipment is being created for an existing or pre-defined ship configuration --- data/modules/MissionUtils/OutfitRules.lua | 1 + data/modules/MissionUtils/ShipBuilder.lua | 14 +++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/data/modules/MissionUtils/OutfitRules.lua b/data/modules/MissionUtils/OutfitRules.lua index 5a827d68c8..338681ec88 100644 --- a/data/modules/MissionUtils/OutfitRules.lua +++ b/data/modules/MissionUtils/OutfitRules.lua @@ -14,6 +14,7 @@ local OutfitRules = {} ---@field minSize integer? Limit the minimum size of items equipped by this rule ---@field minThreat number? Minimum threat value for the entire ship that has to be met to consider this rule ---@field maxThreatFactor number? Maximum proportion of remaining threat that can be consumed by this rule +---@field randomChance number? Random chance to apply this rule, in [0..1] ---@field balance boolean? Attempt to balance volume / threat across all slots this rule matches (works best with .pick = nil) OutfitRules.DifficultWeapon = { diff --git a/data/modules/MissionUtils/ShipBuilder.lua b/data/modules/MissionUtils/ShipBuilder.lua index 549b2a4b16..ad65b1a2c4 100644 --- a/data/modules/MissionUtils/ShipBuilder.lua +++ b/data/modules/MissionUtils/ShipBuilder.lua @@ -96,6 +96,7 @@ Template.role = nil ---@type string? Template.hyperclass = nil ---@type number? Template.shipId = nil ---@type string? Template.label = nil ---@type string? +Template.randomModifier = nil ---@type number? Scalar modifying the randomChance value of contained equipment rules Template.rules = {} ---@type MissionUtils.OutfitRule[] ShipBuilder.Template = Template @@ -151,7 +152,7 @@ function ShipPlan:AddSlots(baseId, slots) end function ShipPlan:AddEquipToPlan(equip, slot, threat) - print("Installing " .. equip:GetName()) + -- print("Installing " .. equip:GetName()) if equip:isProto() then equip = equip:Instance() @@ -472,7 +473,7 @@ function ShipBuilder.SelectHull(template, threat) local hullIdx = Engine.rand:Integer(1, #hullList) local shipId = hullList[hullIdx] - print(" threat {} => {} ({} / {})" % { threat, shipId, hullIdx, #hullList }) + -- print(" threat {} => {} ({} / {})" % { threat, shipId, hullIdx, #hullList }) return HullConfig.GetHullConfigs()[shipId] end @@ -484,6 +485,8 @@ function ShipBuilder.MakePlan(template, shipConfig, threat) local hullThreat = ShipBuilder.GetHullThreat(shipConfig.id).total + local randomMod = template.randomModifier or 1.0 + local shipPlan = ShipPlan:clone { threat = hullThreat, freeThreat = threat - hullThreat, @@ -494,7 +497,12 @@ function ShipBuilder.MakePlan(template, shipConfig, threat) for _, rule in ipairs(template.rules) do - if not rule.minThreat or threat >= rule.minThreat then + local canApplyRule = true + + if rule.minThreat then canApplyRule = canApplyRule and threat >= rule.minThreat end + if rule.randomChance then canApplyRule = canApplyRule and Engine.rand:Number() < rule.randomChance * randomMod end + + if canApplyRule then if rule.slot then ShipBuilder.ApplyEquipmentRule(shipPlan, rule, Engine.rand, hullThreat) From 1547a65c66bd9fe434d9356d101231c81ecd7ac3 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 25 Sep 2024 17:40:56 -0400 Subject: [PATCH 071/119] ShipTemplates: add radar equipment - Most modules didn't bother to manually equip radar on combat ships - DefaultShieldGen rule fills all shield generator slots --- data/modules/MissionUtils/OutfitRules.lua | 9 +++++++-- data/modules/MissionUtils/ShipTemplates.lua | 19 +++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/data/modules/MissionUtils/OutfitRules.lua b/data/modules/MissionUtils/OutfitRules.lua index 338681ec88..f07c612c95 100644 --- a/data/modules/MissionUtils/OutfitRules.lua +++ b/data/modules/MissionUtils/OutfitRules.lua @@ -102,8 +102,7 @@ OutfitRules.DefaultAtmoShield = { } OutfitRules.DefaultShieldGen = { - slot = "shield", - limit = 1 + slot = "shield" } OutfitRules.DefaultAutopilot = { @@ -123,4 +122,10 @@ OutfitRules.DefaultLaserCooling = { limit = 1 } +OutfitRules.DefaultRadar = { + slot = "sensor", + filter = "sensor.radar", + limit = 1 +} + return OutfitRules diff --git a/data/modules/MissionUtils/ShipTemplates.lua b/data/modules/MissionUtils/ShipTemplates.lua index 10f1129b44..bce63b7308 100644 --- a/data/modules/MissionUtils/ShipTemplates.lua +++ b/data/modules/MissionUtils/ShipTemplates.lua @@ -28,7 +28,8 @@ ShipTemplates.StrongPirate = ShipBuilder.Template:clone { utils.mixin(OutfitRules.DefaultShieldBooster, { minThreat = 30.0 }), OutfitRules.DefaultHyperdrive, OutfitRules.DefaultAtmoShield, - OutfitRules.DefaultAutopilot + OutfitRules.DefaultAutopilot, + OutfitRules.DefaultRadar, } } @@ -44,7 +45,8 @@ ShipTemplates.GenericPirate = ShipBuilder.Template:clone { OutfitRules.DefaultAtmoShield, utils.mixin(OutfitRules.DefaultLaserCooling, { minThreat = 40.0 }), utils.mixin(OutfitRules.DefaultShieldBooster, { minThreat = 30.0 }), - OutfitRules.DefaultAutopilot + OutfitRules.DefaultAutopilot, + OutfitRules.DefaultRadar, } } @@ -59,7 +61,8 @@ ShipTemplates.WeakPirate = ShipBuilder.Template:clone { -- No laser cooling or shield booster to be found here OutfitRules.DefaultHyperdrive, OutfitRules.DefaultAtmoShield, - OutfitRules.DefaultAutopilot + OutfitRules.DefaultAutopilot, + OutfitRules.DefaultRadar, } } @@ -75,7 +78,8 @@ ShipTemplates.GenericPolice = ShipBuilder.Template:clone { utils.mixin(OutfitRules.DefaultLaserCooling, { minThreat = 20.0 }), OutfitRules.DefaultHyperdrive, OutfitRules.DefaultAtmoShield, - OutfitRules.DefaultAutopilot + OutfitRules.DefaultAutopilot, + OutfitRules.DefaultRadar, } } @@ -93,7 +97,8 @@ ShipTemplates.StationPolice = ShipBuilder.Template:clone { -- Always has laser cooling but no need for hyperdrive OutfitRules.DefaultLaserCooling, OutfitRules.DefaultAtmoShield, - OutfitRules.DefaultAutopilot + OutfitRules.DefaultAutopilot, + OutfitRules.DefaultRadar, } } @@ -110,7 +115,8 @@ ShipTemplates.PolicePatrol = ShipBuilder.Template:clone { OutfitRules.ModerateShieldGen, OutfitRules.EasyShieldGen, OutfitRules.DefaultAtmoShield, - OutfitRules.DefaultAutopilot + OutfitRules.DefaultAutopilot, + OutfitRules.DefaultRadar, } } @@ -131,6 +137,7 @@ ShipTemplates.GenericMercenary = ShipBuilder.Template:clone { -- Default equipment in remaining space OutfitRules.DefaultHyperdrive, OutfitRules.DefaultAtmoShield, + OutfitRules.DefaultRadar, } } From 349372695e211440a41971057e5e0a67991d5f4a Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 25 Sep 2024 17:53:34 -0400 Subject: [PATCH 072/119] TradeShips: convert to ShipBuilder APIs - Random selection of tradeship equipment according to system lawlessness - Filter tradeships by presence of a cargo bay - Tradeships can be equipped with (small) weapons for self-defense; will tie into turrets once those are added --- data/modules/TradeShips/Flow.lua | 3 +- data/modules/TradeShips/Trader.lua | 90 +++++++++++++++++------------- 2 files changed, 53 insertions(+), 40 deletions(-) diff --git a/data/modules/TradeShips/Flow.lua b/data/modules/TradeShips/Flow.lua index 5b24553a8c..d6409635df 100644 --- a/data/modules/TradeShips/Flow.lua +++ b/data/modules/TradeShips/Flow.lua @@ -34,10 +34,11 @@ end -- return an array of names of ships that (at first sight) can be traders local getAcceptableShips = function () -- accept all ships with the hyperdrive, in fact + ---@param def ShipDef local filter_function = function(_,def) -- XXX should limit to ships large enough to carry significant -- cargo, but we don't have enough ships yet - return def.tag == 'SHIP' and def.hyperdriveClass > 0 -- and def.roles.merchant + return def.tag == 'SHIP' and def.hyperdriveClass > 0 and def.cargo > 0 -- and def.roles.merchant end return utils.build_array( utils.map(function (k,def) diff --git a/data/modules/TradeShips/Trader.lua b/data/modules/TradeShips/Trader.lua index 2239fce7eb..7551e42b2d 100644 --- a/data/modules/TradeShips/Trader.lua +++ b/data/modules/TradeShips/Trader.lua @@ -1,64 +1,76 @@ -- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt -local e = require 'Equipment' +local Commodities = require 'Commodities' local Engine = require 'Engine' local Game = require 'Game' +local HullConfig = require 'HullConfig' local Ship = require 'Ship' -local ShipDef = require 'ShipDef' local Space = require 'Space' local Timer = require 'Timer' -local Commodities = require 'Commodities' + +local utils = require 'utils' + +local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' +local OutfitRules = ShipBuilder.OutfitRules local Core = require 'modules.TradeShips.Core' +local ECMRule = { + slot = "utility", + filter = "utility.ecm", + limit = 1, +} + +local TraderTemplate = ShipBuilder.Template:clone { + rules = { + OutfitRules.DefaultHyperdrive, + OutfitRules.DefaultAtmoShield, + OutfitRules.DefaultAutopilot, + OutfitRules.DefaultRadar, + { equip = "misc.cargo_life_support" }, + -- Defensive equipment is applied based on system lawlessness and a little luck + utils.mixin(OutfitRules.EasyWeapon, { randomChance = 0.8 }), + utils.mixin(OutfitRules.DefaultShieldGen, { randomChance = 1.0 }), + utils.mixin(OutfitRules.DefaultShieldBooster, { randomChance = 0.4 }), + -- ECM can't be used by NPC ships... yet + utils.mixin(ECMRule, { maxSize = 2, randomChance = 0.3 }), + -- Basic ECM is more prevalent than advanced ECM + utils.mixin(ECMRule, { maxSize = 1, randomChance = 0.8 }), + -- Extremely rare to have one of these onboard + { + equip = "misc.hull_autorepair", + limit = 1, + randomChance = 0.2 + } + } +} + local Trader = {} -- this module contains functions that work for single traders Trader.addEquip = function (ship) assert(ship.usedCargo == 0, "equipment is only installed on an empty ship") - local ship_type = ShipDef[ship.shipId] - - -- add standard equipment - ship:AddEquip(e.hyperspace['hyperdrive_'..tostring(ship_type.hyperdriveClass)]) - if ShipDef[ship.shipId].equipSlotCapacity.atmo_shield > 0 then - ship:AddEquip(e.misc.atmospheric_shielding) - end - ship:AddEquip(e.misc.radar) - ship:AddEquip(e.misc.autopilot) - ship:AddEquip(e.misc.cargo_life_support) + local shipId = ship.shipId - -- add defensive equipment based on lawlessness, luck and size + -- Compute a random modifier for more advanced equipment from lawlessness local lawlessness = Game.system.lawlessness - local size_factor = ship.freeCapacity ^ 2 / 2000000 + local randomMod = 0.1 + lawlessness * 0.9 - if Engine.rand:Number(1) - 0.1 < lawlessness then - local num = math.floor(math.sqrt(ship.freeCapacity / 50)) --[[ - - ship:CountEquip(e.misc.shield_generator) ]] - for i = 1, num do - ship:AddEquip(e.misc.shield_generator) - end + local hullConfig = HullConfig.GetHullConfigs()[shipId] + assert(hullConfig, "Can't find hull config for shipId " .. shipId) - if ship_type.equipSlotCapacity.energy_booster > 0 and - Engine.rand:Number(1) + 0.5 - size_factor < lawlessness then - ship:AddEquip(e.misc.shield_energy_booster) - end - end + local template = TraderTemplate:clone { + randomModifier = randomMod + } - -- we can't use these yet - if ship_type.equipSlotCapacity.ecm > 0 then - if Engine.rand:Number(1) + 0.2 < lawlessness then - ship:AddEquip(e.misc.ecm_advanced) - elseif Engine.rand:Number(1) < lawlessness then - ship:AddEquip(e.misc.ecm_basic) - end - end + local hullThreat = ShipBuilder.GetHullThreat(shipId).total - -- this should be rare - if ship_type.equipSlotCapacity.hull_autorepair > 0 and - Engine.rand:Number(1) + 0.75 - size_factor < lawlessness then - ship:AddEquip(e.misc.hull_autorepair) - end + -- TODO: Dummy threat value since we're not using it to select hulls + local plan = ShipBuilder.MakePlan(template, hullConfig, hullThreat + 50 * randomMod) + assert(plan, "Couldn't make an equipment plan for trader " .. shipId) + + ShipBuilder.ApplyPlan(ship, plan) end Trader.addCargo = function (ship, direction) From a5d93929ff775957da8d8408b800f9dea6584972 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 25 Sep 2024 22:16:13 -0400 Subject: [PATCH 073/119] Ship: remove freeCapacity, store masses as float - Fractional mass equipment items are now a thing - Remove the now-unused freeCapacity value - Fuel scooping now checks for free cargo instead (destined to be replaced anyways) --- data/meta/CoreObject/Ship.meta.lua | 2 -- src/Ship.cpp | 8 +++----- src/Ship.h | 6 +++--- src/lua/LuaShip.cpp | 13 ------------- 4 files changed, 6 insertions(+), 23 deletions(-) diff --git a/data/meta/CoreObject/Ship.meta.lua b/data/meta/CoreObject/Ship.meta.lua index ce166e83af..1f2bd70735 100644 --- a/data/meta/CoreObject/Ship.meta.lua +++ b/data/meta/CoreObject/Ship.meta.lua @@ -33,8 +33,6 @@ --- Total equipment volume ---@field totalVolume number --- ----@field freeCapacity number ---- ---@field usedCargo number ---@field totalCargo number --- diff --git a/src/Ship.cpp b/src/Ship.cpp index adc6734e13..4f02f1bed4 100644 --- a/src/Ship.cpp +++ b/src/Ship.cpp @@ -625,14 +625,12 @@ void Ship::UpdateEquipStats() m_stats.loaded_mass = p.Get("mass_cap"); m_stats.static_mass = m_stats.loaded_mass + m_type->hullMass; - m_stats.free_capacity = m_type->capacity - p.Get("equipVolume").get_integer(); - // m_stats.free_capacity = m_type->capacity - m_stats.loaded_mass; + m_stats.used_cargo = p.Get("usedCargo").get_integer(); + m_stats.free_cargo = p.Get("totalCargo").get_integer() - m_stats.used_cargo; p.Set("loadedMass", m_stats.loaded_mass); p.Set("staticMass", m_stats.static_mass); - p.Set("freeCapacity", m_stats.free_capacity); - float shield_cap = p.Get("shield_cap"); m_stats.shield_mass = TONS_HULL_PER_SHIELD * shield_cap; p.Set("shieldMass", m_stats.shield_mass); @@ -1266,7 +1264,7 @@ void Ship::StaticUpdate(const float timeStep) * fuel_scoop_cap = area, m^2. rate = kg^2/(m*s^3) = (Pa*kg)/s^2 */ const double hydrogen_density = 0.0002; - if ((m_stats.free_capacity) && (dot > 0.90) && speed_times_density > (100.0 * 0.3)) { + if ((m_stats.free_cargo > 0) && (dot > 0.90) && speed_times_density > (100.0 * 0.3)) { const double rate = speed_times_density * hydrogen_density * double(m_stats.fuel_scoop_cap); m_hydrogenScoopedAccumulator += rate * timeStep; if (m_hydrogenScoopedAccumulator > 1) { diff --git a/src/Ship.h b/src/Ship.h index 8bc8333030..416057fe06 100644 --- a/src/Ship.h +++ b/src/Ship.h @@ -36,10 +36,10 @@ namespace Graphics { } struct shipstats_t { - int loaded_mass; + float loaded_mass; + float static_mass; // cargo, equipment + hull + int free_cargo; int used_cargo; - int free_capacity; - int static_mass; // cargo, equipment + hull float hull_mass_left; // effectively hitpoints float hyperspace_range; float hyperspace_range_max; diff --git a/src/lua/LuaShip.cpp b/src/lua/LuaShip.cpp index 0b9718dc55..7f73d7b1b8 100644 --- a/src/lua/LuaShip.cpp +++ b/src/lua/LuaShip.cpp @@ -1895,17 +1895,4 @@ void LuaObject::RegisterClass() * * stable * - * - * Attribute: freeCapacity - * - * Total space remaining. Measured in tonnes. - * - * Availability: - * - * November 2013 - * - * Status: - * - * experimental - * */ From a32a601ea44d6ff0a5667de84790184e692f6f36 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 25 Sep 2024 22:17:42 -0400 Subject: [PATCH 074/119] EquipSet: remove all installed equipment when ship type changes - Ensures the ship "stats" are reset back to where they should be for a fresh ship with no equipment - Ultimately a stopgap measure until buying a ship means switching to a new Ship object entirely --- data/libs/EquipSet.lua | 25 +++++++++++++++++++++++++ data/libs/Ship.lua | 2 ++ 2 files changed, 27 insertions(+) diff --git a/data/libs/EquipSet.lua b/data/libs/EquipSet.lua index bc8cf96856..4411dc48f3 100644 --- a/data/libs/EquipSet.lua +++ b/data/libs/EquipSet.lua @@ -72,6 +72,31 @@ function EquipSet:Constructor(ship) self.ship:setprop("totalVolume", self.ship.totalVolume or self.config.capacity) end +function EquipSet:OnShipTypeChanged() + ---@type (string|integer)[] + local to_remove = {} + + for k in pairs(self.installed) do + table.insert(to_remove, k) + end + + -- Sort the longest strings first, as equipment installed in subslots will need to be removed first + table.sort(to_remove, function(a, b) + if type(a) == type(b) then + return type(a) == "string" and #a > #b or type(a) == "number" and a > b + else + return type(a) == "string" + end + end) + + for _, id in ipairs(to_remove) do + local equip = self.installed[id] + self:Remove(equip) + end + + assert(#self.installed == 0, "Missed some equipment while cleaning the ship") +end + -- Function: GetFreeVolume -- -- Returns the available volume for mounting equipment diff --git a/data/libs/Ship.lua b/data/libs/Ship.lua index 689bc69926..6aa8b29994 100644 --- a/data/libs/Ship.lua +++ b/data/libs/Ship.lua @@ -39,6 +39,7 @@ end function Ship:OnShipTypeChanged() -- immediately update any needed components or properties + self:GetComponent('EquipSet'):OnShipTypeChanged() end -- class method @@ -787,6 +788,7 @@ local onShipDestroyed = function (ship, attacker) end -- Reinitialize cargo-related ship properties when changing ship type +---@param ship Ship local onShipTypeChanged = function (ship) ship:GetComponent('CargoManager'):OnShipTypeChanged() end From 0ff716035b1475b1e91fe58298632aa0c43cf796 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 25 Sep 2024 22:18:22 -0400 Subject: [PATCH 075/119] Equipment: military/civilian hyperdrive slots types --- data/modules/Equipment/Hyperdrive.lua | 36 +++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/data/modules/Equipment/Hyperdrive.lua b/data/modules/Equipment/Hyperdrive.lua index e3369a2151..128b4f5276 100644 --- a/data/modules/Equipment/Hyperdrive.lua +++ b/data/modules/Equipment/Hyperdrive.lua @@ -8,126 +8,126 @@ local HyperdriveType = require 'EquipType'.HyperdriveType Equipment.Register("hyperspace.hyperdrive_1", HyperdriveType.New { l10n_key="DRIVE_CLASS1", fuel=Commodities.hydrogen, - slot = { type="hyperdrive", size=1 }, + slot = { type="hyperdrive.civilian", size=1 }, mass=2, volume=2, capabilities={ hyperclass=1 }, price=700, purchasable=true, tech_level=3, icon_name="equip_hyperdrive" }) Equipment.Register("hyperspace.hyperdrive_2", HyperdriveType.New { l10n_key="DRIVE_CLASS2", fuel=Commodities.hydrogen, - slot = { type="hyperdrive", size=2 }, + slot = { type="hyperdrive.civilian", size=2 }, mass=6, volume=6, capabilities={ hyperclass=2 }, price=1300, purchasable=true, tech_level=4, icon_name="equip_hyperdrive" }) Equipment.Register("hyperspace.hyperdrive_3", HyperdriveType.New { l10n_key="DRIVE_CLASS3", fuel=Commodities.hydrogen, - slot = { type="hyperdrive", size=3 }, + slot = { type="hyperdrive.civilian", size=3 }, mass=11, volume=11, capabilities={ hyperclass=3 }, price=2500, purchasable=true, tech_level=4, icon_name="equip_hyperdrive" }) Equipment.Register("hyperspace.hyperdrive_4", HyperdriveType.New { l10n_key="DRIVE_CLASS4", fuel=Commodities.hydrogen, - slot = { type="hyperdrive", size=4 }, + slot = { type="hyperdrive.civilian", size=4 }, mass=25, volume=25, capabilities={ hyperclass=4 }, price=5000, purchasable=true, tech_level=5, icon_name="equip_hyperdrive" }) Equipment.Register("hyperspace.hyperdrive_5", HyperdriveType.New { l10n_key="DRIVE_CLASS5", fuel=Commodities.hydrogen, - slot = { type="hyperdrive", size=5 }, + slot = { type="hyperdrive.civilian", size=5 }, mass=60, volume=60, capabilities={ hyperclass=5 }, price=10000, purchasable=true, tech_level=5, icon_name="equip_hyperdrive" }) Equipment.Register("hyperspace.hyperdrive_6", HyperdriveType.New { l10n_key="DRIVE_CLASS6", fuel=Commodities.hydrogen, - slot = { type="hyperdrive", size=6 }, + slot = { type="hyperdrive.civilian", size=6 }, mass=130, volume=130, capabilities={ hyperclass=6 }, price=20000, purchasable=true, tech_level=6, icon_name="equip_hyperdrive" }) Equipment.Register("hyperspace.hyperdrive_7", HyperdriveType.New { l10n_key="DRIVE_CLASS7", fuel=Commodities.hydrogen, - slot = { type="hyperdrive", size=7 }, + slot = { type="hyperdrive.civilian", size=7 }, mass=245, volume=245, capabilities={ hyperclass=7 }, price=30000, purchasable=true, tech_level=8, icon_name="equip_hyperdrive" }) Equipment.Register("hyperspace.hyperdrive_8", HyperdriveType.New { l10n_key="DRIVE_CLASS8", fuel=Commodities.hydrogen, - slot = { type="hyperdrive", size=8 }, + slot = { type="hyperdrive.civilian", size=8 }, mass=360, volume=360, capabilities={ hyperclass=8 }, price=60000, purchasable=true, tech_level=9, icon_name="equip_hyperdrive" }) Equipment.Register("hyperspace.hyperdrive_9", HyperdriveType.New { l10n_key="DRIVE_CLASS9", fuel=Commodities.hydrogen, - slot = { type="hyperdrive", size=9 }, + slot = { type="hyperdrive.civilian", size=9 }, mass=540, volume=540, capabilities={ hyperclass=9 }, price=120000, purchasable=true, tech_level=10, icon_name="equip_hyperdrive" }) Equipment.Register("hyperspace.hyperdrive_mil1", HyperdriveType.New { l10n_key="DRIVE_MIL1", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, - slot = { type="hyperdrive", size=1 }, + slot = { type="hyperdrive.military", size=1 }, mass=1, volume=1, capabilities={ hyperclass=1 }, price=23000, purchasable=true, tech_level=10, icon_name="equip_hyperdrive_mil" }) Equipment.Register("hyperspace.hyperdrive_mil2", HyperdriveType.New { l10n_key="DRIVE_MIL2", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, - slot = { type="hyperdrive", size=2 }, + slot = { type="hyperdrive.military", size=2 }, mass=3, volume=3, capabilities={ hyperclass=2 }, price=47000, purchasable=true, tech_level="MILITARY", icon_name="equip_hyperdrive_mil" }) Equipment.Register("hyperspace.hyperdrive_mil3", HyperdriveType.New { l10n_key="DRIVE_MIL3", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, - slot = { type="hyperdrive", size=3 }, + slot = { type="hyperdrive.military", size=3 }, mass=5, volume=5, capabilities={ hyperclass=3 }, price=85000, purchasable=true, tech_level=11, icon_name="equip_hyperdrive_mil" }) Equipment.Register("hyperspace.hyperdrive_mil4", HyperdriveType.New { l10n_key="DRIVE_MIL4", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, - slot = { type="hyperdrive", size=4 }, + slot = { type="hyperdrive.military", size=4 }, mass=13, volume=13, capabilities={ hyperclass=4 }, price=214000, purchasable=true, tech_level=12, icon_name="equip_hyperdrive_mil" }) Equipment.Register("hyperspace.hyperdrive_mil5", HyperdriveType.New { l10n_key="DRIVE_MIL5", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, - slot = { type="hyperdrive", size=5 }, + slot = { type="hyperdrive.military", size=5 }, mass=29, volume=29, capabilities={ hyperclass=5 }, price=540000, purchasable=false, tech_level="MILITARY", icon_name="equip_hyperdrive_mil" }) Equipment.Register("hyperspace.hyperdrive_mil6", HyperdriveType.New { l10n_key="DRIVE_MIL6", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, - slot = { type="hyperdrive", size=6 }, + slot = { type="hyperdrive.military", size=6 }, mass=60, volume=60, capabilities={ hyperclass=6 }, price=1350000, purchasable=false, tech_level="MILITARY", icon_name="equip_hyperdrive_mil" }) Equipment.Register("hyperspace.hyperdrive_mil7", HyperdriveType.New { l10n_key="DRIVE_MIL7", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, - slot = { type="hyperdrive", size=7 }, + slot = { type="hyperdrive.military", size=7 }, mass=135, volume=135, capabilities={ hyperclass=7 }, price=3500000, purchasable=false, tech_level="MILITARY", icon_name="equip_hyperdrive_mil" }) Equipment.Register("hyperspace.hyperdrive_mil8", HyperdriveType.New { l10n_key="DRIVE_MIL8", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, - slot = { type="hyperdrive", size=8 }, + slot = { type="hyperdrive.military", size=8 }, mass=190, volume=190, capabilities={ hyperclass=8 }, price=8500000, purchasable=false, tech_level="MILITARY", icon_name="equip_hyperdrive_mil" }) Equipment.Register("hyperspace.hyperdrive_mil9", HyperdriveType.New { l10n_key="DRIVE_MIL9", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, - slot = { type="hyperdrive", size=9 }, + slot = { type="hyperdrive.military", size=9 }, mass=260, volume=260, capabilities={ hyperclass=9 }, price=22000000, purchasable=false, tech_level="MILITARY", icon_name="equip_hyperdrive_mil" From 8b50b6d6b3a226b3bda2c30f615a67c5ba66684a Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 25 Sep 2024 22:20:50 -0400 Subject: [PATCH 076/119] ShipMarket: convert to new EquipSet API --- .../modules/station-view/04-shipMarket.lua | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/data/pigui/modules/station-view/04-shipMarket.lua b/data/pigui/modules/station-view/04-shipMarket.lua index 9b8d32dd1f..415e66e15f 100644 --- a/data/pigui/modules/station-view/04-shipMarket.lua +++ b/data/pigui/modules/station-view/04-shipMarket.lua @@ -10,7 +10,7 @@ local StationView = require 'pigui.views.station-view' local Table = require 'pigui.libs.table' local PiImage = require 'pigui.libs.image' local ModelSpinner = require 'PiGui.Modules.ModelSpinner' -local CommodityType= require 'CommodityType' +local EquipSet = require 'EquipSet' local ui = require 'pigui' @@ -110,7 +110,7 @@ end local function buyShip (mkt, sos) local player = Game.player - local station = player:GetDockedWith() + local station = assert(player:GetDockedWith()) local def = sos.def local cost = def.basePrice - tradeInValue(Game.player) @@ -129,8 +129,8 @@ local function buyShip (mkt, sos) return end - local hdrive = def.hyperdriveClass > 0 and Equipment.hyperspace["hyperdrive_" .. def.hyperdriveClass].capabilities.mass or 0 - if def.equipSlotCapacity.cargo < player.usedCargo or def.capacity < (player.usedCargo + hdrive) then + -- Not enough room to put all of the player's current cargo + if def.cargo < player.usedCargo then mkt.popup.msg = l.TOO_SMALL_TO_TRANSSHIP mkt.popup:open() return @@ -150,8 +150,19 @@ local function buyShip (mkt, sos) if sos.pattern then player.model:SetPattern(sos.pattern) end player:SetLabel(sos.label) + -- TODO: ships on sale should have their own pre-installed set of equipment + -- items instead of being completely empty + if def.hyperdriveClass > 0 then - player:AddEquip(Equipment.hyperspace["hyperdrive_" .. def.hyperdriveClass]) + local slot = player:GetComponent('EquipSet'):GetAllSlotsOfType('hyperdrive')[1] + + -- Install the best-fitting non-military hyperdrive we can + local hyperdrive = utils.best_score(Equipment.new, function(_, equip) + return EquipSet.CompatibleWithSlot(equip, slot) and equip.slot.type:match("%.civilian") + and equip.capabilities.hyperclass or nil + end) + + player:GetComponent('EquipSet'):Install(hyperdrive:Instance(), slot) end player:SetFuelPercent(100) From dea166ddad602d52818da1475e2f107ad0cd03ae Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 25 Sep 2024 22:55:42 -0400 Subject: [PATCH 077/119] EquipSet: type information for listeners --- data/libs/EquipSet.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/data/libs/EquipSet.lua b/data/libs/EquipSet.lua index 4411dc48f3..8452c8dc44 100644 --- a/data/libs/EquipSet.lua +++ b/data/libs/EquipSet.lua @@ -16,6 +16,8 @@ local utils = require 'utils' ---@field New fun(ship: Ship): EquipSet local EquipSet = utils.class("EquipSet") +---@alias EquipSet.Listener fun(op: 'install'|'remove', equip: EquipType, slot: HullConfig.Slot?) + local function slotTypeMatches(equipType, slotType) return equipType == slotType or string.sub(equipType, 1, #slotType + 1) == slotType .. "." end @@ -64,7 +66,7 @@ function EquipSet:Constructor(ship) self:BuildSlotCache() - self.listeners = {} + self.listeners = {} ---@type EquipSet.Listener[] -- Initialize ship properties we're responsible for modifying self.ship:setprop("mass_cap", self.ship["mass_cap"] or 0) @@ -288,6 +290,7 @@ end -- -- Register an event listener function to be notified of changes to this ship's -- equipment loadout. +---@param fun EquipSet.Listener function EquipSet:AddListener(fun) table.insert(self.listeners, fun) end @@ -343,7 +346,7 @@ function EquipSet:Install(equipment, slotHandle) self:_InstallInternal(equipment) for _, fun in ipairs(self.listeners) do - fun("install", equipment, slotHandle) + fun('install', equipment, slotHandle) end return true @@ -388,7 +391,7 @@ function EquipSet:Remove(equipment) end for _, fun in ipairs(self.listeners) do - fun("remove", equipment, slotHandle) + fun('remove', equipment, slotHandle) end return true From 7ad3369659d4ddccb7286370da53ef8aace1ae3f Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 25 Sep 2024 23:08:07 -0400 Subject: [PATCH 078/119] Migrate onShipEquipmentChanged to EquipSet API - Directly register to the player's events to be notified of changes to the hyperdrive - onShipEquipmentChanged is now unused as a global signal --- data/modules/BreakdownServicing/BreakdownServicing.lua | 9 ++++++--- data/pigui/modules/hyperjump-planner.lua | 6 ++++-- data/pigui/views/map-sector-view.lua | 10 +++++----- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/data/modules/BreakdownServicing/BreakdownServicing.lua b/data/modules/BreakdownServicing/BreakdownServicing.lua index 3965b0e3ff..8496845e72 100644 --- a/data/modules/BreakdownServicing/BreakdownServicing.lua +++ b/data/modules/BreakdownServicing/BreakdownServicing.lua @@ -174,8 +174,9 @@ local onShipTypeChanged = function (ship) end end -local onShipEquipmentChanged = function (ship, equipment) - if ship:IsPlayer() and equipment and equipment:IsValidSlot("engine", ship) then +---@type EquipSet.Listener +local onShipEquipmentChanged = function (op, equipment, slot) + if slot and slot.type:match("^hyperdrive") then service_history.company = nil service_history.lastdate = Game.time service_history.service_period = oneyear @@ -244,6 +245,9 @@ local onGameStart = function () loaded_data = nil end + + -- Listen to changes in the player's equipment + Game.player:GetComponent('EquipSet'):AddListener(onShipEquipmentChanged) end local savedByCrew = function(ship) @@ -316,7 +320,6 @@ end Event.Register("onCreateBB", onCreateBB) Event.Register("onGameStart", onGameStart) Event.Register("onShipTypeChanged", onShipTypeChanged) -Event.Register("onShipEquipmentChanged", onShipEquipmentChanged) Event.Register("onEnterSystem", onEnterSystem) Serializer:Register("BreakdownServicing", serialize, unserialize) diff --git a/data/pigui/modules/hyperjump-planner.lua b/data/pigui/modules/hyperjump-planner.lua index 4e50753e0c..2a323f5353 100644 --- a/data/pigui/modules/hyperjump-planner.lua +++ b/data/pigui/modules/hyperjump-planner.lua @@ -329,8 +329,9 @@ function hyperJumpPlanner.onPlayerCargoChanged(comm, count) end end -function hyperJumpPlanner.onShipEquipmentChanged(ship, equipment) - if ship:IsPlayer() and equipment and equipment:IsValidSlot("engine", ship) then +---@type EquipSet.Listener +function hyperJumpPlanner.onShipEquipmentChanged(op, equip, slot) + if op == 'install' and slot and slot.type:match("^hyperdrive") then buildJumpRouteList() end end @@ -350,6 +351,7 @@ end function hyperJumpPlanner.onGameStart() -- get notified when the player buys hydrogen Game.player:GetComponent('CargoManager'):AddListener('hyperjump-planner', hyperJumpPlanner.onPlayerCargoChanged) + Game.player:GetComponent('EquipSet'):AddListener(hyperJumpPlanner.onShipEquipmentChanged) -- we may have just loaded a jump route list, so lets build it fresh now buildJumpRouteList() diff --git a/data/pigui/views/map-sector-view.lua b/data/pigui/views/map-sector-view.lua index 50a5cb60f6..46396d5e6e 100644 --- a/data/pigui/views/map-sector-view.lua +++ b/data/pigui/views/map-sector-view.lua @@ -97,6 +97,11 @@ local onGameStart = function () -- allow hyperjump planner to register its events: hyperJumpPlanner.onGameStart() + + -- Reset hyperspace cache when ship equipment changes + Game.player:GetComponent('EquipSet'):AddListener(function (op, equip, slot) + hyperspaceDetailsCache = {} + end) end local function getHyperspaceDetails(path) @@ -486,11 +491,6 @@ Event.Register("onGameEnd", function() hyperJumpPlanner.onGameEnd() end) -Event.Register("onShipEquipmentChanged", function(ship, ...) - if ship:IsPlayer() then hyperspaceDetailsCache = {} end - hyperJumpPlanner.onShipEquipmentChanged(ship, ...) -end) - Event.Register("onShipTypeChanged", function(ship, ...) if ship:IsPlayer() then hyperspaceDetailsCache = {} end end) From a551e20ab1afb3f207d421290a2f96526784e463 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 25 Sep 2024 23:11:10 -0400 Subject: [PATCH 079/119] Remove Ship:AddEquip() and onShipEquipmentChanged --- data/libs/Event.lua | 24 ------------------- data/libs/Ship.lua | 56 --------------------------------------------- 2 files changed, 80 deletions(-) diff --git a/data/libs/Event.lua b/data/libs/Event.lua index ff744b8ee3..5f47f24d5f 100644 --- a/data/libs/Event.lua +++ b/data/libs/Event.lua @@ -777,30 +777,6 @@ end -- experimental -- --- --- Event: onShipEquipmentChanged --- --- Triggered when a ship's equipment set changes. --- --- > local onShipEquipmentChanged = function (ship, equipType) ... end --- > Event.Register("onShipEquipmentChanged", onShipEquipmentChanged) --- --- Parameters: --- --- ship - the whose equipment just changed --- --- equipType - The item that was added or removed, --- or nil if the change involved multiple types of equipment --- --- Availability: --- --- alpha 15 --- --- Status: --- --- experimental --- - -- -- Event: onShipFuelChanged -- diff --git a/data/libs/Ship.lua b/data/libs/Ship.lua index 6aa8b29994..73984e1d58 100644 --- a/data/libs/Ship.lua +++ b/data/libs/Ship.lua @@ -75,62 +75,6 @@ function Ship:GetInstalledHyperdrive() return drives[1] end --- --- Method: AddEquip --- --- Add an equipment item to its appropriate equipment slot --- --- > num_added = ship:AddEquip(item, count, slot) --- --- Parameters: --- --- item - an Equipment type object (e.g., require 'Equipment'.misc.radar) --- --- count - optional. The number of this item to add. Defaults to 1. --- --- slot - optional. The slot to mount the Equipment in, if other than default. --- --- Return: --- --- num_added - the number of items added. Can be less than count if there --- was not enough room. --- --- Example: --- --- > ship:AddEquip(Equipment.misc.cabin, 10) --- > ship:AddEquip(Equipment.laser.pulsecannon_dual_1mw, 1, "laser_rear") --- --- Availability: --- --- alpha 10 --- --- Status: --- --- deprecated --- ----@deprecated -function Ship:AddEquip(item, count, slot) - -- NOTE: this function only remains to ensure legacy "fire and forget" - -- ship equipment codepaths remain functional. New code should interact - -- directly without EquipSet instead - assert(not count or count == 1) - assert(not slot) - - local equipSet = self:GetComponent("EquipSet") - if not item:isProto() then - item = item:Instance() - end - - local slotHandle = item.slot and equipSet:GetFreeSlotForEquip(item) - local ok = equipSet:Install(item, slotHandle) - - if ok then - Event.Queue("onShipEquipmentChanged", self, item) - end - - return ok and 1 or 0 -end - -- -- Method: IsHyperjumpAllowed From 5eacb3354db74fef780d7383a8ef87cc8b73437e Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 26 Sep 2024 13:01:27 -0400 Subject: [PATCH 080/119] Cleanup leftovers in Ship --- src/Ship.cpp | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/Ship.cpp b/src/Ship.cpp index 4f02f1bed4..316847f3e1 100644 --- a/src/Ship.cpp +++ b/src/Ship.cpp @@ -7,12 +7,9 @@ #include "EnumStrings.h" #include "Frame.h" #include "Game.h" -#include "GameLog.h" #include "GameSaveError.h" -#include "HeatGradientPar.h" #include "HyperspaceCloud.h" #include "JsonUtils.h" -#include "Lang.h" #include "Missile.h" #include "NavLights.h" #include "Pi.h" @@ -24,15 +21,12 @@ #include "ShipAICmd.h" #include "Space.h" #include "SpaceStation.h" -#include "StringF.h" #include "WorldView.h" #include "collider/CollisionContact.h" #include "graphics/TextureBuilder.h" #include "graphics/Types.h" #include "lua/LuaEvent.h" #include "lua/LuaObject.h" -#include "lua/LuaTable.h" -#include "lua/LuaUtils.h" #include "scenegraph/Animation.h" #include "scenegraph/Tag.h" #include "scenegraph/CollisionGeometry.h" @@ -188,9 +182,6 @@ Ship::Ship(const Json &jsonObj, Space *space) : p.Set("shieldMassLeft", m_stats.shield_mass_left); p.Set("fuelMassLeft", m_stats.fuel_tank_mass_left); - // TODO: object components - // m_equipSet.LoadFromJson(shipObj["equipSet"]); - m_controller = 0; const ShipController::Type ctype = shipObj["controller_type"]; if (ctype == ShipController::PLAYER) @@ -296,7 +287,6 @@ void Ship::SaveToJson(Json &jsonObj, Space *space) shipObj["hyperspace_jump_sound"] = m_hyperspace.sounds.jump_sound; m_fixedGuns->SaveToJson(shipObj, space); - // m_equipSet.SaveToJson(shipObj["equipSet"]); shipObj["ecm_recharge"] = m_ecmRecharge; shipObj["ship_type_id"] = m_type->id; @@ -1558,7 +1548,6 @@ void Ship::OnEnterSystem() void Ship::SetupShields() { - // TODO: remove the fallback path once all shields are extracted to their own models SceneGraph::Model *sm = Pi::FindModel(m_type->shieldName, false); if (sm) { From f9eaf8358ad679d1a94e611d4cee85fdfab9b443 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 26 Sep 2024 13:30:01 -0400 Subject: [PATCH 081/119] Remove uses of equipSlotCapacity - Query hull config slots where possible/feasible - Remove missile/scoop display in 01-ship-info; the equipment display tab already handles this information - Comment-out the appropriate lines in the new-game-window --- data/libs/SpaceStation.lua | 7 ++- data/pigui/modules/info-view/01-ship-info.lua | 12 ++--- data/pigui/modules/new-game-window/ship.lua | 10 ++-- .../modules/station-view/04-shipMarket.lua | 54 ++++++++++++++----- 4 files changed, 55 insertions(+), 28 deletions(-) diff --git a/data/libs/SpaceStation.lua b/data/libs/SpaceStation.lua index 87ffcebc48..5586b88b29 100644 --- a/data/libs/SpaceStation.lua +++ b/data/libs/SpaceStation.lua @@ -19,6 +19,7 @@ local ModelSkin = require 'SceneGraph.ModelSkin' local Serializer = require 'Serializer' local Equipment = require 'Equipment' local Lang = require 'Lang' +local HullConfig = require 'HullConfig' local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' local ShipTemplates = require 'modules.MissionUtils.ShipTemplates' @@ -540,7 +541,11 @@ end local isPlayerShip = function (def) return def.tag == "SHIP" and def.basePrice > 0 end -local groundShips = utils.build_array(utils.filter(function (k,def) return isPlayerShip(def) and def.equipSlotCapacity.atmo_shield > 0 end, pairs(ShipDef))) +local groundShips = utils.build_array(utils.filter(function (k,def) + return isPlayerShip(def) + and utils.contains_if(HullConfig.GetHullConfigs()[def.id].slots, function(s) return s.type:match("^hull") end) +end, pairs(ShipDef))) + local spaceShips = utils.build_array(utils.filter(function (k,def) return isPlayerShip(def) end, pairs(ShipDef))) diff --git a/data/pigui/modules/info-view/01-ship-info.lua b/data/pigui/modules/info-view/01-ship-info.lua index db4ea5d30c..8457c37269 100644 --- a/data/pigui/modules/info-view/01-ship-info.lua +++ b/data/pigui/modules/info-view/01-ship-info.lua @@ -28,7 +28,7 @@ local function shipStats() local rearWeapon = equipSet:GetInstalledOfType("weapon")[2] local cabinEmpty = Passengers.CountFreeCabins(player) local cabinOccupied = Passengers.CountOccupiedCabins(player) - local cabinMaximum = shipDef.equipSlotCapacity.cabin + local cabinMaximum = cabinEmpty + cabinOccupied hyperdrive = hyperdrive or nil frontWeapon = frontWeapon or nil @@ -41,10 +41,7 @@ local function shipStats() local up_acc = player:GetAcceleration("up") local atmo_shield = equipSet:GetInstalledOfType("hull.atmo_shield")[1] - local atmo_shield_cap = 1 - if atmo_shield then - atmo_shield_cap = atmo_shield.capabilities.atmo_shield - end + local atmo_shield_cap = player["atmo_shield_cap"] textTable.draw({ { l.REGISTRATION_NUMBER..":", shipLabel}, @@ -78,10 +75,7 @@ local function shipStats() { l.OCCUPIED_PASSENGER_CABINS..":", cabinOccupied }, { l.PASSENGER_CABIN_CAPACITY..":", cabinMaximum }, false, - { l.MISSILE_MOUNTS..":", shipDef.equipSlotCapacity.missile}, - { l.SCOOP_MOUNTS..":", shipDef.equipSlotCapacity.scoop}, - false, - { l.ATMOSPHERIC_SHIELDING..":", shipDef.equipSlotCapacity.atmo_shield > 0 and l.YES or l.NO }, + { l.ATMOSPHERIC_SHIELDING..":", atmo_shield and l.YES or l.NO }, { l.ATMO_PRESS_LIMIT..":", string.format("%d atm", shipDef.atmosphericPressureLimit * atmo_shield_cap) }, }) end diff --git a/data/pigui/modules/new-game-window/ship.lua b/data/pigui/modules/new-game-window/ship.lua index cffb8a3084..2f49ee5be6 100644 --- a/data/pigui/modules/new-game-window/ship.lua +++ b/data/pigui/modules/new-game-window/ship.lua @@ -940,12 +940,12 @@ function ShipSummary:prepareAndValidateParamList() local paramList = { rowWithAlert(eq_n_cargo, lui.CAPACITY, ShipCargo.mass + ShipEquip.mass, def.capacity, greater, 't'), rowWithAlert(self.cargo, lui.CARGO_SPACE, ShipCargo.mass, freeCargo, greater, 't'), - rowWithAlert(self.equip, lui.FRONT_WEAPON, usedSlots.laser_front, def.equipSlotCapacity.laser_front, greater), - rowWithAlert(self.equip, lui.REAR_WEAPON, usedSlots.laser_rear, def.equipSlotCapacity.laser_rear, greater), - rowWithAlert(self.equip, lui.CABINS, usedSlots.cabin, def.equipSlotCapacity.cabin, greater), + --rowWithAlert(self.equip, lui.FRONT_WEAPON, usedSlots.laser_front, def.equipSlotCapacity.laser_front, greater), + --rowWithAlert(self.equip, lui.REAR_WEAPON, usedSlots.laser_rear, def.equipSlotCapacity.laser_rear, greater), + --rowWithAlert(self.equip, lui.CABINS, usedSlots.cabin, def.equipSlotCapacity.cabin, greater), rowWithAlert(self.equip, lui.CREW_CABINS, #Crew.value + 1, def.maxCrew, greater), - rowWithAlert(self.equip, lui.MISSILE_MOUNTS, usedSlots.missile, def.equipSlotCapacity.missile, greater), - rowWithAlert(self.equip, lui.SCOOP_MOUNTS, usedSlots.scoop, def.equipSlotCapacity.scoop, greater), + --rowWithAlert(self.equip, lui.MISSILE_MOUNTS, usedSlots.missile, def.equipSlotCapacity.missile, greater), + --rowWithAlert(self.equip, lui.SCOOP_MOUNTS, usedSlots.scoop, def.equipSlotCapacity.scoop, greater), } self.cargo.valid = self.cargo.valid and eq_n_cargo.valid self.equip.valid = self.equip.valid and eq_n_cargo.valid diff --git a/data/pigui/modules/station-view/04-shipMarket.lua b/data/pigui/modules/station-view/04-shipMarket.lua index 415e66e15f..2f3a88fbf9 100644 --- a/data/pigui/modules/station-view/04-shipMarket.lua +++ b/data/pigui/modules/station-view/04-shipMarket.lua @@ -11,6 +11,7 @@ local Table = require 'pigui.libs.table' local PiImage = require 'pigui.libs.image' local ModelSpinner = require 'PiGui.Modules.ModelSpinner' local EquipSet = require 'EquipSet' +local HullConfig = require 'HullConfig' local ui = require 'pigui' @@ -96,7 +97,7 @@ local tradeInValue = function(ship) local value = shipDef.basePrice * shipSellPriceReduction * ship.hullPercent/100 if shipDef.hyperdriveClass > 0 then - value = value - Equipment.hyperspace["hyperdrive_" .. shipDef.hyperdriveClass].price * equipSellPriceReduction + value = value - Equipment.new["hyperspace.hyperdrive_" .. shipDef.hyperdriveClass].price * equipSellPriceReduction end local equipment = ship:GetComponent("EquipSet"):GetInstalledEquipment() @@ -242,7 +243,7 @@ function FormatAndCompareShips:draw_hyperdrive_cell(desc) local function fmt( v ) return v > 0 and - Equipment.hyperspace["hyperdrive_" .. v]:GetName() or l.NONE + Equipment.new["hyperspace.hyperdrive_" .. v]:GetName() or l.NONE end self:compare_and_draw_column( desc, self.def.hyperdriveClass, self.b.def.hyperdriveClass, fmt ) @@ -281,26 +282,53 @@ function FormatAndCompareShips:draw_unformated_cell(desc, key) self:compare_and_draw_column( desc, self:get_value(key), self.b:get_value(key) ) end +local function getNumSlotsCompatibleWithType(def, type) + local config = HullConfig.GetHullConfigs()[def.id] + local count = 0 + + for _, slot in pairs(config.slots) do + if EquipSet.SlotTypeMatches(type, slot.type) then + count = count + (slot.count or 1) + end + end + + return count +end + +local function getBestSlotSizeOfType(def, type) + local config = HullConfig.GetHullConfigs()[def.id] + local slot, size = utils.best_score(config.slots, function(_, slot) + return EquipSet.SlotTypeMatches(type, slot.type) and slot.size or nil + end) + + return slot and size or 0 +end + function FormatAndCompareShips:draw_equip_slot_cell(desc, key) - self:compare_and_draw_column( desc, self.def.equipSlotCapacity[key], self.b.def.equipSlotCapacity[key] ) + self:compare_and_draw_column( desc, getNumSlotsCompatibleWithType(self.def, key), getNumSlotsCompatibleWithType(self.b.def, key) ) end function FormatAndCompareShips:draw_yes_no_equip_slot_cell(desc, key) local function fmt( v ) return v==1 and l.YES or l.NO end - self:compare_and_draw_column( desc, self.def.equipSlotCapacity[key], self.b.def.equipSlotCapacity[key], fmt ) + self:compare_and_draw_column( desc, getNumSlotsCompatibleWithType(self.def, key), getNumSlotsCompatibleWithType(self.b.def, key), fmt ) end function FormatAndCompareShips:draw_atmos_pressure_limit_cell(desc) + local a_shield = getBestSlotSizeOfType(self.def, "hull.atmo_shield") + local b_shield = getBestSlotSizeOfType(self.b.def, "hull.atmo_shield") - local function fmt( def ) + local function fmt( def, has_shield ) local atmoSlot - if def.equipSlotCapacity.atmo_shield > 0 then + if has_shield > 1 then atmoSlot = string.format("%d(+%d/+%d) atm", def.atmosphericPressureLimit, - def.atmosphericPressureLimit * (Equipment.misc.atmospheric_shielding.capabilities.atmo_shield - 1), - def.atmosphericPressureLimit * (Equipment.misc.heavy_atmospheric_shielding.capabilities.atmo_shield - 1) ) + def.atmosphericPressureLimit * (Equipment.new["hull.atmospheric_shielding"].capabilities.atmo_shield - 1), + def.atmosphericPressureLimit * (Equipment.new["hull.heavy_atmospheric_shielding"].capabilities.atmo_shield - 1) ) + elseif has_shield > 0 then + atmoSlot = string.format("%d(+%d) atm", def.atmosphericPressureLimit, + def.atmosphericPressureLimit * (Equipment.new["hull.atmospheric_shielding"].capabilities.atmo_shield - 1) ) else atmoSlot = string.format("%d atm", def.atmosphericPressureLimit) end @@ -308,16 +336,16 @@ function FormatAndCompareShips:draw_atmos_pressure_limit_cell(desc) end local function fmt_a( v ) - return fmt( self.def ) + return fmt( self.def, a_shield ) end local function fmt_b( v ) - return fmt( self.b.def ) + return fmt( self.b.def, b_shield ) end -- multiply the values by 1000 and then add on if there is capacity for atmo_shielding so that the compare takes that into account -- however, note the formatting ignores the passed in value and therefore displays correctly. - self:compare_and_draw_column( desc, self.def.atmosphericPressureLimit*1000+self.def.equipSlotCapacity.atmo_shield, self.b.def.atmosphericPressureLimit*1000+self.b.def.equipSlotCapacity.atmo_shield, fmt_a, fmt_b ) + self:compare_and_draw_column( desc, self.def.atmosphericPressureLimit*1000+a_shield, self.b.def.atmosphericPressureLimit*1000+b_shield, fmt_a, fmt_b ) end function FormatAndCompareShips:Constructor(def, b) @@ -325,7 +353,7 @@ function FormatAndCompareShips:Constructor(def, b) self.emptyMass = def.hullMass + def.fuelTankMass self.fullMass = def.hullMass + def.capacity + def.fuelTankMass self.massAtCapacity = def.hullMass + def.capacity - self.cargoCapacity = def.equipSlotCapacity["cargo"] + self.cargoCapacity = def.cargo self.def = def self.b = b end @@ -397,7 +425,7 @@ local tradeMenu = function() shipFormatAndCompare:draw_unformated_cell( l.MAXIMUM_CREW, "maxCrew" ) shipFormatAndCompare:draw_deltav_cell( l.DELTA_V_MAX, "fullMass", "hullMass") shipFormatAndCompare:draw_equip_slot_cell( l.MISSILE_MOUNTS, "missile" ) - shipFormatAndCompare:draw_yes_no_equip_slot_cell( l.ATMOSPHERIC_SHIELDING, "atmo_shield" ) + shipFormatAndCompare:draw_yes_no_equip_slot_cell( l.ATMOSPHERIC_SHIELDING, "hull.atmo_shield" ) shipFormatAndCompare:draw_atmos_pressure_limit_cell( l.ATMO_PRESS_LIMIT ) shipFormatAndCompare:draw_equip_slot_cell( l.SCOOP_MOUNTS, "scoop" ) shipFormatAndCompare:draw_equip_slot_cell( l.PASSENGER_CABIN_CAPACITY, "cabin" ) From 437c2ae515a1c24ed625fbcaf0a788755d37fc9d Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 26 Sep 2024 13:39:09 -0400 Subject: [PATCH 082/119] Remove ShipDef.equipSlotCapacity - Ship.json slots{} object is fully obsolete and unused - Remove the field from both Lua and C++ contexts --- data/libs/EquipSetCompat.lua | 27 --------------------------- data/meta/ShipDef.lua | 1 - src/ShipType.cpp | 12 ------------ src/ShipType.h | 1 - src/lua/LuaShipDef.cpp | 34 +--------------------------------- 5 files changed, 1 insertion(+), 74 deletions(-) delete mode 100644 data/libs/EquipSetCompat.lua diff --git a/data/libs/EquipSetCompat.lua b/data/libs/EquipSetCompat.lua deleted file mode 100644 index 4536bd246e..0000000000 --- a/data/libs/EquipSetCompat.lua +++ /dev/null @@ -1,27 +0,0 @@ -local Compat = {} - -Compat.default = { - cargo=0, - engine=1, - laser_front=1, - laser_rear=0, - missile=0, - ecm=1, - radar=1, - target_scanner=1, - hypercloud=1, - hull_autorepair=1, - energy_booster=1, - atmo_shield=1, - cabin=50, - shield=9999, - scoop=2, - laser_cooler=1, - cargo_life_support=1, - autopilot=1, - trade_computer=1, - sensor = 2, - thruster = 1 -} - -return Compat diff --git a/data/meta/ShipDef.lua b/data/meta/ShipDef.lua index 285e594ae0..d697d40826 100644 --- a/data/meta/ShipDef.lua +++ b/data/meta/ShipDef.lua @@ -35,7 +35,6 @@ ---@field minCrew integer ---@field maxCrew integer ---@field hyperdriveClass integer ----@field equipSlotCapacity table -- deprecated --- ---@field raw table The entire ShipDef JSON object as a Lua table diff --git a/src/ShipType.cpp b/src/ShipType.cpp index 6183b07b14..3b675f6885 100644 --- a/src/ShipType.cpp +++ b/src/ShipType.cpp @@ -183,11 +183,6 @@ ShipType::ShipType(const Id &_id, const std::string &path) cargo = data.value("cargo", 0); fuelTankMass = data.value("fuel_tank_mass", 5); - for (Json::iterator slot = data["slots"].begin(); slot != data["slots"].end(); ++slot) { - const std::string slotname = slot.key(); - slots[slotname] = data["slots"].value(slotname, 0); - } - for (Json::iterator role = data["roles"].begin(); role != data["roles"].end(); ++role) { roles[*role] = true; } @@ -202,13 +197,6 @@ ShipType::ShipType(const Id &_id, const std::string &path) atmosphericPressureLimit = data.value("atmospheric_pressure_limit", 10.0); // 10 atmosphere is about 90 metres underwater (on Earth) - { - const auto it = slots.find("engine"); - if (it != slots.end()) { - it->second = Clamp(it->second, 0, 1); - } - } - effectiveExhaustVelocity = data.value("effective_exhaust_velocity", -1.0f); const float thruster_fuel_use = data.value("thruster_fuel_use", -1.0f); diff --git a/src/ShipType.h b/src/ShipType.h index d7ac198a09..ae45793c94 100644 --- a/src/ShipType.h +++ b/src/ShipType.h @@ -42,7 +42,6 @@ struct ShipType { float linThrust[THRUSTER_MAX]; float angThrust; float linAccelerationCap[THRUSTER_MAX]; - std::map slots; std::map roles; Color globalThrusterColor; // Override default color for thrusters bool isGlobalColorDefined; // If globalThrusterColor is filled with... a color :) diff --git a/src/lua/LuaShipDef.cpp b/src/lua/LuaShipDef.cpp index bd641a7b9d..77eb6ba936 100644 --- a/src/lua/LuaShipDef.cpp +++ b/src/lua/LuaShipDef.cpp @@ -185,21 +185,6 @@ * experimental */ -/* - * Attribute: equipSlotCapacity - * - * Table keyed on , containing maximum number of items - * that can be held in that slot (ignoring mass) - * - * Availability: - * - * alpha 32 - * - * Status: - * - * experimental - */ - /* * Attribute: shipClass * @@ -290,24 +275,7 @@ void LuaShipDef::Register() lua_setfield(l, -3, "linAccelerationCap"); lua_pop(l, 1); - lua_newtable(l); - for (auto it = st.slots.cbegin(); it != st.slots.cend(); ++it) { - pi_lua_settable(l, it->first.c_str(), it->second); - } - pi_lua_readonly_table_proxy(l, -1); - luaL_getmetafield(l, -1, "__index"); - if (!lua_getmetatable(l, -1)) { - lua_newtable(l); - } - pi_lua_import(l, "EquipSetCompat"); - luaL_getsubtable(l, -1, "default"); - lua_setfield(l, -3, "__index"); - lua_pop(l, 1); - lua_setmetatable(l, -2); - lua_pop(l, 1); - lua_setfield(l, -3, "equipSlotCapacity"); - lua_pop(l, 1); - + // Set up roles table lua_newtable(l); for (auto it = st.roles.cbegin(); it != st.roles.cend(); ++it) { pi_lua_settable(l, it->first.c_str(), it->second); From 264bc6e08e577b42504bc6e215f8d62247fe1bfe Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 26 Sep 2024 14:08:34 -0400 Subject: [PATCH 083/119] SpaceStation: restore stocking, store equipment by id - Improved stocking algorithm with a little less randomness - Use new equipment list when computing equipment stock - Equipment stock / price are stored using equipment ID rather than equipment item identity --- data/libs/SpaceStation.lua | 30 +++++++++++++++++-------- data/pigui/libs/equipment-outfitter.lua | 6 ++--- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/data/libs/SpaceStation.lua b/data/libs/SpaceStation.lua index 5586b88b29..6e43c6e270 100644 --- a/data/libs/SpaceStation.lua +++ b/data/libs/SpaceStation.lua @@ -61,17 +61,29 @@ local transientMarket = utils.automagic() local ensureStationData +local function techLevelDiff(equip, station) + if equip == 'MILITARY' then equip = 11 end + if station == 'MILITARY' then station = 11 end + + return station - equip +end + -- create a transient entry for this station's equipment stock +---@param station SpaceStation local function createEquipmentStock (station) assert(station and station:exists()) if equipmentStock[station] then error("Attempt to create station equipment stock twice!") end - equipmentStock[station] = {} - for _,slot in pairs{"laser", "hyperspace", "misc"} do - for key, e in pairs(Equipment[slot]) do - equipmentStock[station][e] = Engine.rand:Integer(0,100) - end + local stock = {} + + for id, e in pairs(Equipment.new) do + -- Stations stock everything at least three tech levels below them, + -- with an increasing chance of being out-of-stock as the item's tech + -- approaches that of the station + stock[id] = math.max(0, Engine.rand:Integer(-30, 100) + techLevelDiff(e.tech_level, station.techLevel) * 10) end + + equipmentStock[station] = stock end -- Create a transient entry for this station's commodity stocks and seed it with @@ -118,7 +130,7 @@ function SpaceStation:GetEquipmentPrice (e) assert(self:exists()) if equipmentPrice[self] then - return equipmentPrice[self][e] or e.price + return equipmentPrice[self][e.id] or e.price end return e.price @@ -148,7 +160,7 @@ end function SpaceStation:SetEquipmentPrice (e, price) assert(self:exists()) if not equipmentPrice[self] then equipmentPrice[self] = {} end - equipmentPrice[self][e] = price + equipmentPrice[self][e.id] = price end -- @@ -176,7 +188,7 @@ end -- function SpaceStation:GetEquipmentStock (e) assert(self:exists()) - return equipmentStock[self] and equipmentStock[self][e] or 0 + return equipmentStock[self] and equipmentStock[self][e.id] or 0 end -- @@ -205,7 +217,7 @@ function SpaceStation:AddEquipmentStock (e, stock) ensureStationData(self) assert(equipmentStock[self]) - equipmentStock[self][e] = (equipmentStock[self][e] or 0) + stock + equipmentStock[self][e.id] = (equipmentStock[self][e.id] or 0) + stock end -- ============================================================================ diff --git a/data/pigui/libs/equipment-outfitter.lua b/data/pigui/libs/equipment-outfitter.lua index 895da26ab5..78c482b643 100644 --- a/data/pigui/libs/equipment-outfitter.lua +++ b/data/pigui/libs/equipment-outfitter.lua @@ -197,8 +197,7 @@ function Outfitter:getAvailableEquipment() return utils.map_table(Equipment.new, function(id, equip) if self:getStock(equip) <= 0 then - -- FIXME: restore when equipment stocking converted to new system - -- return id, nil + return id, nil end if not equip.purchasable or not self:stationHasTech(equip.tech_level) then @@ -231,8 +230,7 @@ end ---@param e EquipType function Outfitter:getStock(e) - e = e.__proto or e - return self.station:GetEquipmentStock(e) + return self.station:GetEquipmentStock(e:GetPrototype()) end -- Cost of the equipment item if buying From 28fc54693448215779e90428c1d60b83e7539d23 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 26 Sep 2024 14:28:24 -0400 Subject: [PATCH 084/119] EquipType: restore weapon functionality - Silly typo prevented lasers from being mounted --- data/libs/EquipType.lua | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index 630017a631..6b106a205e 100644 --- a/data/libs/EquipType.lua +++ b/data/libs/EquipType.lua @@ -266,7 +266,9 @@ local LaserType = utils.inherits(EquipType, "LaserType") ---@param ship Ship ---@param slot HullConfig.Slot function LaserType:OnInstall(ship, slot) - for k, v in ipairs(self.laser_stats) do + EquipType.OnInstall(self, ship, slot) + + for k, v in pairs(self.laser_stats) do -- TODO: allow installing more than one laser ship:setprop('laser_front_' .. k, v) end @@ -274,8 +276,10 @@ end ---@param ship Ship ---@param slot HullConfig.Slot -function LaserType:OnUninstall(ship, slot) - for k, v in ipairs(self.laser_stats) do +function LaserType:OnRemove(ship, slot) + EquipType.OnRemove(self, ship, slot) + + for k, v in pairs(self.laser_stats) do -- TODO: allow installing more than one laser ship:setprop('laser_front_' .. k, nil) end From 65f9698194c770e2a512c280c30754654ca817d2 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 26 Sep 2024 14:47:59 -0400 Subject: [PATCH 085/119] SecondHand: use new Equipment list --- data/modules/SecondHand/SecondHand.lua | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/data/modules/SecondHand/SecondHand.lua b/data/modules/SecondHand/SecondHand.lua index 07ea1d5626..e4c4376e02 100644 --- a/data/modules/SecondHand/SecondHand.lua +++ b/data/modules/SecondHand/SecondHand.lua @@ -122,12 +122,10 @@ local makeAdvert = function (station) -- Get all non-cargo or engines local avail_equipment = {} - for k,v in pairs(Equipment) do - if k == "laser" or k == "misc" then - for _,e in pairs(v) do - if e.purchasable then - table.insert(avail_equipment,e) - end + for id, equip in pairs(Equipment.new) do + if equip.slot and not equip.slot.type:match("^hyperdrive") then + if equip.purchasable then + table.insert(avail_equipment, equip) end end end From 0f09843342caea23922efcc119251f74bbf6fa2f Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 26 Sep 2024 14:48:12 -0400 Subject: [PATCH 086/119] SearchRescue: use new Equipment list --- data/modules/SearchRescue/SearchRescue.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/modules/SearchRescue/SearchRescue.lua b/data/modules/SearchRescue/SearchRescue.lua index 700418845b..97a8f133b0 100644 --- a/data/modules/SearchRescue/SearchRescue.lua +++ b/data/modules/SearchRescue/SearchRescue.lua @@ -444,7 +444,7 @@ local calcReward = function (flavour, pickup_crew, pickup_pass, pickup_comm, del -- factor in personnel to be delivered or picked up local personnel = pickup_crew + pickup_pass + deliver_crew + deliver_pass if personnel > 0 then - reward = reward + (personnel * (Equipment.misc.cabin.price * 0.5)) + reward = reward + (personnel * (Equipment.new['misc.cabin_s1'].price * 0.5)) end -- factor in commodities to be delivered or picked up From 854a9fa9816e7b9e11564daab1629716c5246db6 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 26 Sep 2024 14:48:26 -0400 Subject: [PATCH 087/119] TradeShips: remove unused Equipment import --- data/modules/TradeShips/Debug.lua | 1 - data/modules/TradeShips/Flow.lua | 1 - 2 files changed, 2 deletions(-) diff --git a/data/modules/TradeShips/Debug.lua b/data/modules/TradeShips/Debug.lua index 815dc81168..5281b88770 100644 --- a/data/modules/TradeShips/Debug.lua +++ b/data/modules/TradeShips/Debug.lua @@ -4,7 +4,6 @@ local ShipDef = require 'ShipDef' local debugView = require 'pigui.views.debug' local Engine = require 'Engine' -local e = require 'Equipment' local Game = require 'Game' local ui = require 'pigui.baseui' local utils = require 'utils' diff --git a/data/modules/TradeShips/Flow.lua b/data/modules/TradeShips/Flow.lua index d6409635df..f540647ccb 100644 --- a/data/modules/TradeShips/Flow.lua +++ b/data/modules/TradeShips/Flow.lua @@ -1,7 +1,6 @@ -- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt -local e = require 'Equipment' local Engine = require 'Engine' local Game = require 'Game' local Ship = require 'Ship' From a1702a891134042e577cd55192b1feb6476a1fb4 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 26 Sep 2024 14:52:15 -0400 Subject: [PATCH 088/119] Cleanup TODOs and unused variables --- data/pigui/modules/system-view-ui.lua | 1 - data/pigui/views/mainmenu.lua | 2 -- 2 files changed, 3 deletions(-) diff --git a/data/pigui/modules/system-view-ui.lua b/data/pigui/modules/system-view-ui.lua index bceb547d73..b4d4212769 100644 --- a/data/pigui/modules/system-view-ui.lua +++ b/data/pigui/modules/system-view-ui.lua @@ -855,7 +855,6 @@ function Windows.objectInfo:Show() local hd = body:GetInstalledHyperdrive() table.insert(data, { name = luc.HYPERDRIVE, value = hd and hd:GetName() or lc.NO_HYPERDRIVE }) table.insert(data, { name = luc.MASS, value = Format.MassTonnes(body.staticMass) }) - -- FIXME: this should use a separate cargoMass property table.insert(data, { name = luc.CARGO, value = Format.MassTonnes(body.usedCargo) }) end else diff --git a/data/pigui/views/mainmenu.lua b/data/pigui/views/mainmenu.lua index 088222b0a0..758b76f6f7 100644 --- a/data/pigui/views/mainmenu.lua +++ b/data/pigui/views/mainmenu.lua @@ -18,8 +18,6 @@ local qlc = Lang.GetResource("quitconfirmation-core") local ui = require 'pigui' -local hyperspace = Equipment.hyperspace - local colors = ui.theme.colors local pionillium = ui.fonts.pionillium local orbiteer = ui.fonts.orbiteer From 87da4f5fa425d9a11dbb568b46fddeef6e8d4a71 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 26 Sep 2024 14:52:49 -0400 Subject: [PATCH 089/119] Equipment: remove old equipment list --- data/libs/EquipType.lua | 9 - data/libs/Equipment.lua | 433 +--------------------------------------- 2 files changed, 8 insertions(+), 434 deletions(-) diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index 6b106a205e..6cee501093 100644 --- a/data/libs/EquipType.lua +++ b/data/libs/EquipType.lua @@ -83,15 +83,6 @@ function EquipType.New (specs) obj.price = obj.price or 0 end - -- TODO: remove all usage of obj.capabilities, transition to explicit volume for equipment - -- fixup old capabilities system to explicitly specified mass/volume - if obj.capabilities and obj.capabilities.mass then - obj.mass = obj.capabilities.mass - obj.volume = obj.mass - - -- obj.capabilities.mass = nil - end - return obj end diff --git a/data/libs/Equipment.lua b/data/libs/Equipment.lua index 9682c1e176..4a427bebb2 100644 --- a/data/libs/Equipment.lua +++ b/data/libs/Equipment.lua @@ -1,441 +1,24 @@ -- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt -local Commodities = require 'Commodities' local EquipTypes = require 'EquipType' local Serializer = require 'Serializer' -local LaserType = EquipTypes.LaserType -local EquipType = EquipTypes.EquipType -local HyperdriveType = EquipTypes.HyperdriveType -local SensorType = EquipTypes.SensorType -local BodyScannerType = EquipTypes.BodyScannerType - -local laser = EquipTypes.laser -local hyperspace = EquipTypes.hyperspace -local misc = EquipTypes.misc +---@class Equipment +local Equipment = {} ---@type table -EquipTypes.new = {} +Equipment.new = {} -function EquipTypes.Get(id) - return EquipTypes.new[id] +function Equipment.Get(id) + return Equipment.new[id] end -function EquipTypes.Register(id, type) - EquipTypes.new[id] = type +function Equipment.Register(id, type) + Equipment.new[id] = type type.id = id Serializer:RegisterPersistent("Equipment." .. id, type) end --- Constants: EquipSlot --- --- Equipment slots. Every equipment item has a corresponding --- "slot" that it fits in to. Each slot has an independent capacity. --- --- engine - hyperdrives and military drives --- laser_front - front attachment point for lasers and plasma accelerators --- laser_rear - rear attachment point for lasers and plasma accelerators --- missile - missile --- ecm - ecm system --- radar - radar --- target_scanner - target scanner --- hypercloud - hyperspace cloud analyser --- hull_autorepair - hull auto-repair system --- energy_booster - shield energy booster unit --- atmo_shield - atmospheric shielding --- cabin - cabin --- shield - shield --- scoop - scoop used for scooping things (cargo, fuel/hydrogen) --- laser_cooler - laser cooling booster --- cargo_life_support - cargo bay life support --- autopilot - autopilot --- trade_computer - commodity trade analyzer computer module - -misc.missile_unguided = EquipType.New({ - l10n_key="MISSILE_UNGUIDED", slots="missile", price=30, - missile_type="missile_unguided", tech_level=1, - capabilities={mass=0.2}, purchasable=true, - icon_name="equip_missile_unguided" -}) -misc.missile_guided = EquipType.New({ - l10n_key="MISSILE_GUIDED", slots="missile", price=50, - missile_type="missile_guided", tech_level=5, - capabilities={mass=0.5}, purchasable=true, - icon_name="equip_missile_guided" -}) -misc.missile_smart = EquipType.New({ - l10n_key="MISSILE_SMART", slots="missile", price=95, - missile_type="missile_smart", tech_level=10, - capabilities={mass=1}, purchasable=true, - icon_name="equip_missile_smart" -}) -misc.missile_naval = EquipType.New({ - l10n_key="MISSILE_NAVAL", slots="missile", price=160, - missile_type="missile_naval", tech_level="MILITARY", - capabilities={mass=1.5}, purchasable=true, - icon_name="equip_missile_naval" -}) -misc.atmospheric_shielding = EquipType.New({ - l10n_key="ATMOSPHERIC_SHIELDING", slots="atmo_shield", price=200, - capabilities={mass=1, atmo_shield=4}, - purchasable=true, tech_level=3, - icon_name="equip_atmo_shield_generator" -}) -misc.heavy_atmospheric_shielding = EquipType.New({ - l10n_key="ATMOSPHERIC_SHIELDING_HEAVY", slots="atmo_shield", price=900, - capabilities={mass=2, atmo_shield=9}, - purchasable=true, tech_level=5, - icon_name="equip_atmo_shield_generator" -}) -misc.ecm_basic = EquipType.New({ - l10n_key="ECM_BASIC", slots="ecm", price=6000, - capabilities={mass=2, ecm_power=2, ecm_recharge=5}, - purchasable=true, tech_level=9, ecm_type = 'ecm', - hover_message="ECM_HOVER_MESSAGE" -}) -misc.ecm_advanced = EquipType.New({ - l10n_key="ECM_ADVANCED", slots="ecm", price=15200, - capabilities={mass=2, ecm_power=3, ecm_recharge=5}, - purchasable=true, tech_level="MILITARY", ecm_type = 'ecm_advanced', - hover_message="ECM_HOVER_MESSAGE" -}) -misc.radar = EquipType.New({ - l10n_key="RADAR", slots="radar", price=680, - capabilities={mass=1, radar=1}, - purchasable=true, tech_level=3, - icon_name="equip_radar" -}) -misc.cabin = EquipType.New({ - l10n_key="UNOCCUPIED_CABIN", slots="cabin", price=1350, - capabilities={mass=1, cabin=1}, - purchasable=true, tech_level=1, - icon_name="equip_cabin_empty" -}) -misc.cabin_occupied = EquipType.New({ - l10n_key="PASSENGER_CABIN", slots="cabin", price=0, - capabilities={mass=1}, purchasable=false, tech_level=1, - icon_name="equip_cabin_occupied" -}) -misc.shield_generator = EquipType.New({ - l10n_key="SHIELD_GENERATOR", slots="shield", price=2500, - capabilities={mass=4, shield=1}, purchasable=true, tech_level=8, - icon_name="equip_shield_generator" -}) -misc.laser_cooling_booster = EquipType.New({ - l10n_key="LASER_COOLING_BOOSTER", slots="laser_cooler", price=380, - capabilities={mass=1, laser_cooler=2}, purchasable=true, tech_level=8 -}) -misc.cargo_life_support = EquipType.New({ - l10n_key="CARGO_LIFE_SUPPORT", slots="cargo_life_support", price=700, - capabilities={mass=1, cargo_life_support=1}, purchasable=true, tech_level=2 -}) -misc.autopilot = EquipType.New({ - l10n_key="AUTOPILOT", slots="autopilot", price=1400, - capabilities={mass=1, set_speed=1, autopilot=1}, purchasable=true, tech_level=1, - icon_name="equip_autopilot" -}) -misc.target_scanner = EquipType.New({ - l10n_key="TARGET_SCANNER", slots="target_scanner", price=900, - capabilities={mass=1, target_scanner_level=1}, purchasable=true, tech_level=9, - icon_name="equip_scanner" -}) -misc.advanced_target_scanner = EquipType.New({ - l10n_key="ADVANCED_TARGET_SCANNER", slots="target_scanner", price=1200, - capabilities={mass=1, target_scanner_level=2}, purchasable=true, tech_level="MILITARY", - icon_name="equip_scanner" -}) -misc.fuel_scoop = EquipType.New({ - l10n_key="FUEL_SCOOP", slots="scoop", price=3500, - capabilities={mass=6, fuel_scoop=3}, purchasable=true, tech_level=4, - icon_name="equip_fuel_scoop" -}) -misc.cargo_scoop = EquipType.New({ - l10n_key="CARGO_SCOOP", slots="scoop", price=3900, - capabilities={mass=7, cargo_scoop=1}, purchasable=true, tech_level=5, - icon_name="equip_cargo_scoop" -}) -misc.multi_scoop = EquipType.New({ - l10n_key="MULTI_SCOOP", slots="scoop", price=12000, - capabilities={mass=9, cargo_scoop=1, fuel_scoop=2}, purchasable=true, tech_level=9, - icon_name="equip_multi_scoop" -}) -misc.hypercloud_analyzer = EquipType.New({ - l10n_key="HYPERCLOUD_ANALYZER", slots="hypercloud", price=1500, - capabilities={mass=1, hypercloud_analyzer=1}, purchasable=true, tech_level=10, - icon_name="equip_scanner" -}) -misc.shield_energy_booster = EquipType.New({ - l10n_key="SHIELD_ENERGY_BOOSTER", slots="energy_booster", price=10000, - capabilities={mass=8, shield_energy_booster=1}, purchasable=true, tech_level=11 -}) -misc.hull_autorepair = EquipType.New({ - l10n_key="HULL_AUTOREPAIR", slots="hull_autorepair", price=16000, - capabilities={mass=40, hull_autorepair=1}, purchasable=true, tech_level="MILITARY", - icon_name="repairs" -}) -misc.thrusters_basic = EquipType.New({ - l10n_key="THRUSTERS_BASIC", slots="thruster", price=3000, - tech_level=5, - capabilities={mass=0, thruster_power=1}, purchasable=true, - icon_name="equip_thrusters_basic" -}) -misc.thrusters_medium = EquipType.New({ - l10n_key="THRUSTERS_MEDIUM", slots="thruster", price=6500, - tech_level=8, - capabilities={mass=0, thruster_power=2}, purchasable=true, - icon_name="equip_thrusters_medium" -}) -misc.thrusters_best = EquipType.New({ - l10n_key="THRUSTERS_BEST", slots="thruster", price=14000, - tech_level="MILITARY", - capabilities={mass=0, thruster_power=3}, purchasable=true, - icon_name="equip_thrusters_best" -}) -misc.trade_computer = EquipType.New({ - l10n_key="TRADE_COMPUTER", slots="trade_computer", price=400, - capabilities={mass=0, trade_computer=1}, purchasable=true, tech_level=9, - icon_name="equip_trade_computer" -}) -misc.planetscanner = BodyScannerType.New({ - l10n_key = 'SURFACE_SCANNER', slots="sensor", price=2950, - capabilities={mass=1,sensor=1}, purchasable=true, tech_level=5, - max_range=100000000, target_altitude=0, state="HALTED", progress=0, - bodyscanner_stats={scan_speed=3, scan_tolerance=0.05}, - stats={ aperture = 50.0, minAltitude = 150, resolution = 768, orbital = false }, - icon_name="equip_planet_scanner" -}) -misc.planetscanner_good = BodyScannerType.New({ - l10n_key = 'SURFACE_SCANNER_GOOD', slots="sensor", price=5000, - capabilities={mass=2,sensor=1}, purchasable=true, tech_level=8, - max_range=100000000, target_altitude=0, state="HALTED", progress=0, - bodyscanner_stats={scan_speed=3, scan_tolerance=0.05}, - stats={ aperture = 65.0, minAltitude = 250, resolution = 1092, orbital = false }, - icon_name="equip_planet_scanner" -}) -misc.orbitscanner = BodyScannerType.New({ - l10n_key = 'ORBIT_SCANNER', slots="sensor", price=7500, - capabilities={mass=3,sensor=1}, purchasable=true, tech_level=3, - max_range=100000000, target_altitude=0, state="HALTED", progress=0, - bodyscanner_stats={scan_speed=3, scan_tolerance=0.05}, - stats={ aperture = 4.0, minAltitude = 650000, resolution = 6802, orbital = true }, - icon_name="equip_orbit_scanner" -}) -misc.orbitscanner_good = BodyScannerType.New({ - l10n_key = 'ORBIT_SCANNER_GOOD', slots="sensor", price=11000, - capabilities={mass=7,sensor=1}, purchasable=true, tech_level=8, - max_range=100000000, target_altitude=0, state="HALTED", progress=0, - bodyscanner_stats={scan_speed=3, scan_tolerance=0.05}, - stats={ aperture = 2.8, minAltitude = 1750000, resolution = 12375, orbital = true }, - icon_name="equip_orbit_scanner" -}) - -hyperspace.hyperdrive_1 = HyperdriveType.New({ - l10n_key="DRIVE_CLASS1", fuel=Commodities.hydrogen, slots="engine", - price=700, capabilities={mass=2, hyperclass=1}, purchasable=true, tech_level=3, - icon_name="equip_hyperdrive" -}) -hyperspace.hyperdrive_2 = HyperdriveType.New({ - l10n_key="DRIVE_CLASS2", fuel=Commodities.hydrogen, slots="engine", - price=1300, capabilities={mass=6, hyperclass=2}, purchasable=true, tech_level=4, - icon_name="equip_hyperdrive" -}) -hyperspace.hyperdrive_3 = HyperdriveType.New({ - l10n_key="DRIVE_CLASS3", fuel=Commodities.hydrogen, slots="engine", - price=2500, capabilities={mass=11, hyperclass=3}, purchasable=true, tech_level=4, - icon_name="equip_hyperdrive" -}) -hyperspace.hyperdrive_4 = HyperdriveType.New({ - l10n_key="DRIVE_CLASS4", fuel=Commodities.hydrogen, slots="engine", - price=5000, capabilities={mass=25, hyperclass=4}, purchasable=true, tech_level=5, - icon_name="equip_hyperdrive" -}) -hyperspace.hyperdrive_5 = HyperdriveType.New({ - l10n_key="DRIVE_CLASS5", fuel=Commodities.hydrogen, slots="engine", - price=10000, capabilities={mass=60, hyperclass=5}, purchasable=true, tech_level=5, - icon_name="equip_hyperdrive" -}) -hyperspace.hyperdrive_6 = HyperdriveType.New({ - l10n_key="DRIVE_CLASS6", fuel=Commodities.hydrogen, slots="engine", - price=20000, capabilities={mass=130, hyperclass=6}, purchasable=true, tech_level=6, - icon_name="equip_hyperdrive" -}) -hyperspace.hyperdrive_7 = HyperdriveType.New({ - l10n_key="DRIVE_CLASS7", fuel=Commodities.hydrogen, slots="engine", - price=30000, capabilities={mass=245, hyperclass=7}, purchasable=true, tech_level=8, - icon_name="equip_hyperdrive" -}) -hyperspace.hyperdrive_8 = HyperdriveType.New({ - l10n_key="DRIVE_CLASS8", fuel=Commodities.hydrogen, slots="engine", - price=60000, capabilities={mass=360, hyperclass=8}, purchasable=true, tech_level=9, - icon_name="equip_hyperdrive" -}) -hyperspace.hyperdrive_9 = HyperdriveType.New({ - l10n_key="DRIVE_CLASS9", fuel=Commodities.hydrogen, slots="engine", - price=120000, capabilities={mass=540, hyperclass=9}, purchasable=true, tech_level=10, - icon_name="equip_hyperdrive" -}) -hyperspace.hyperdrive_mil1 = HyperdriveType.New({ - l10n_key="DRIVE_MIL1", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, slots="engine", - price=23000, capabilities={mass=1, hyperclass=1}, purchasable=true, tech_level=10, - icon_name="equip_hyperdrive_mil" -}) -hyperspace.hyperdrive_mil2 = HyperdriveType.New({ - l10n_key="DRIVE_MIL2", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, slots="engine", - price=47000, capabilities={mass=3, hyperclass=2}, purchasable=true, tech_level="MILITARY", - icon_name="equip_hyperdrive_mil" -}) -hyperspace.hyperdrive_mil3 = HyperdriveType.New({ - l10n_key="DRIVE_MIL3", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, slots="engine", - price=85000, capabilities={mass=5, hyperclass=3}, purchasable=true, tech_level=11, - icon_name="equip_hyperdrive_mil" -}) -hyperspace.hyperdrive_mil4 = HyperdriveType.New({ - l10n_key="DRIVE_MIL4", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, slots="engine", - price=214000, capabilities={mass=13, hyperclass=4}, purchasable=true, tech_level=12, - icon_name="equip_hyperdrive_mil" -}) -hyperspace.hyperdrive_mil5 = HyperdriveType.New({ - l10n_key="DRIVE_MIL5", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, slots="engine", - price=540000, capabilities={mass=29, hyperclass=5}, purchasable=false, tech_level="MILITARY", - icon_name="equip_hyperdrive_mil" -}) -hyperspace.hyperdrive_mil6 = HyperdriveType.New({ - l10n_key="DRIVE_MIL6", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, slots="engine", - price=1350000, capabilities={mass=60, hyperclass=6}, purchasable=false, tech_level="MILITARY", - icon_name="equip_hyperdrive_mil" -}) -hyperspace.hyperdrive_mil7 = HyperdriveType.New({ - l10n_key="DRIVE_MIL7", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, slots="engine", - price=3500000, capabilities={mass=135, hyperclass=7}, purchasable=false, tech_level="MILITARY", - icon_name="equip_hyperdrive_mil" -}) -hyperspace.hyperdrive_mil8 = HyperdriveType.New({ - l10n_key="DRIVE_MIL8", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, slots="engine", - price=8500000, capabilities={mass=190, hyperclass=8}, purchasable=false, tech_level="MILITARY", - icon_name="equip_hyperdrive_mil" -}) -hyperspace.hyperdrive_mil9 = HyperdriveType.New({ - l10n_key="DRIVE_MIL9", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, slots="engine", - price=22000000, capabilities={mass=260, hyperclass=9}, purchasable=false, tech_level="MILITARY", - icon_name="equip_hyperdrive_mil" -}) - -laser.pulsecannon_1mw = LaserType.New({ - l10n_key="PULSECANNON_1MW", price=600, capabilities={mass=1}, - slots = {"laser_front", "laser_rear"}, laser_stats = { - lifespan=8, speed=1000, damage=1000, rechargeTime=0.25, length=30, - width=5, beam=0, dual=0, mining=0, rgba_r = 255, rgba_g = 51, rgba_b = 51, rgba_a = 255 - }, purchasable=true, tech_level=3, - icon_name="equip_pulsecannon" -}) -laser.pulsecannon_dual_1mw = LaserType.New({ - l10n_key="PULSECANNON_DUAL_1MW", price=1100, capabilities={mass=4}, - slots = {"laser_front", "laser_rear"}, laser_stats = { - lifespan=8, speed=1000, damage=1000, rechargeTime=0.25, length=30, - width=5, beam=0, dual=1, mining=0, rgba_r = 255, rgba_g = 51, rgba_b = 51, rgba_a = 255 - }, purchasable=true, tech_level=4, - icon_name="equip_dual_pulsecannon" -}) -laser.pulsecannon_2mw = LaserType.New({ - l10n_key="PULSECANNON_2MW", price=1000, capabilities={mass=3}, - slots = {"laser_front", "laser_rear"}, laser_stats = { - lifespan=8, speed=1000, damage=2000, rechargeTime=0.25, length=30, - width=5, beam=0, dual=0, mining=0, rgba_r = 255, rgba_g = 127, rgba_b = 51, rgba_a = 255 - }, purchasable=true, tech_level=5, - icon_name="equip_pulsecannon" -}) -laser.pulsecannon_rapid_2mw = LaserType.New({ - l10n_key="PULSECANNON_RAPID_2MW", price=1800, capabilities={mass=7}, - slots = {"laser_front", "laser_rear"}, laser_stats = { - lifespan=8, speed=1000, damage=2000, rechargeTime=0.13, length=30, - width=5, beam=0, dual=0, mining=0, rgba_r = 255, rgba_g = 127, rgba_b = 51, rgba_a = 255 - }, purchasable=true, tech_level=5, - icon_name="equip_pulsecannon_rapid" -}) -laser.beamlaser_1mw = LaserType.New({ - l10n_key="BEAMLASER_1MW", price=2400, capabilities={mass=3}, - slots = {"laser_front", "laser_rear"}, laser_stats = { - lifespan=8, speed=1000, damage=1500, rechargeTime=0.25, length=10000, - width=1, beam=1, dual=0, mining=0, rgba_r = 255, rgba_g = 51, rgba_b = 127, rgba_a = 255, - heatrate=0.02, coolrate=0.01 - }, purchasable=true, tech_level=4, - icon_name="equip_beamlaser" -}) -laser.beamlaser_dual_1mw = LaserType.New({ - l10n_key="BEAMLASER_DUAL_1MW", price=4800, capabilities={mass=6}, - slots = {"laser_front", "laser_rear"}, laser_stats = { - lifespan=8, speed=1000, damage=1500, rechargeTime=0.5, length=10000, - width=1, beam=1, dual=1, mining=0, rgba_r = 255, rgba_g = 51, rgba_b = 127, rgba_a = 255, - heatrate=0.02, coolrate=0.01 - }, purchasable=true, tech_level=5, - icon_name="equip_dual_beamlaser" -}) -laser.beamlaser_2mw = LaserType.New({ - l10n_key="BEAMLASER_RAPID_2MW", price=5600, capabilities={mass=7}, - slots = {"laser_front", "laser_rear"}, laser_stats = { - lifespan=8, speed=1000, damage=3000, rechargeTime=0.13, length=20000, - width=1, beam=1, dual=0, mining=0, rgba_r = 255, rgba_g = 192, rgba_b = 192, rgba_a = 255, - heatrate=0.02, coolrate=0.01 - }, purchasable=true, tech_level=6, - icon_name="equip_beamlaser" -}) -laser.pulsecannon_4mw = LaserType.New({ - l10n_key="PULSECANNON_4MW", price=2200, capabilities={mass=10}, - slots = {"laser_front", "laser_rear"}, laser_stats = { - lifespan=8, speed=1000, damage=4000, rechargeTime=0.25, length=30, - width=5, beam=0, dual=0, mining=0, rgba_r = 255, rgba_g = 255, rgba_b = 51, rgba_a = 255 - }, purchasable=true, tech_level=6, - icon_name="equip_pulsecannon" -}) -laser.pulsecannon_10mw = LaserType.New({ - l10n_key="PULSECANNON_10MW", price=4900, capabilities={mass=30}, - slots = {"laser_front", "laser_rear"}, laser_stats = { - lifespan=8, speed=1000, damage=10000, rechargeTime=0.25, length=30, - width=5, beam=0, dual=0, mining=0, rgba_r = 51, rgba_g = 255, rgba_b = 51, rgba_a = 255 - }, purchasable=true, tech_level=7, - icon_name="equip_pulsecannon" -}) -laser.pulsecannon_20mw = LaserType.New({ - l10n_key="PULSECANNON_20MW", price=12000, capabilities={mass=65}, - slots = {"laser_front", "laser_rear"}, laser_stats = { - lifespan=8, speed=1000, damage=20000, rechargeTime=0.25, length=30, - width=5, beam=0, dual=0, mining=0, rgba_r = 0.1, rgba_g = 51, rgba_b = 255, rgba_a = 255 - }, purchasable=true, tech_level="MILITARY", - icon_name="equip_pulsecannon" -}) -laser.miningcannon_5mw = LaserType.New({ - l10n_key="MININGCANNON_5MW", price=3700, capabilities={mass=6}, - slots = {"laser_front", "laser_rear"}, laser_stats = { - lifespan=8, speed=1000, damage=5000, rechargeTime=1.5, length=30, - width=5, beam=0, dual=0, mining=1, rgba_r = 51, rgba_g = 127, rgba_b = 0, rgba_a = 255 - }, purchasable=true, tech_level=5, - icon_name="equip_mining_laser" -}) -laser.miningcannon_17mw = LaserType.New({ - l10n_key="MININGCANNON_17MW", price=10600, capabilities={mass=10}, - slots = {"laser_front", "laser_rear"}, laser_stats = { - lifespan=8, speed=1000, damage=17000, rechargeTime=2, length=30, - width=5, beam=0, dual=0, mining=1, rgba_r = 51, rgba_g = 127, rgba_b = 0, rgba_a = 255 - }, purchasable=true, tech_level=8, - icon_name="equip_mining_laser" -}) -laser.small_plasma_accelerator = LaserType.New({ - l10n_key="SMALL_PLASMA_ACCEL", price=120000, capabilities={mass=22}, - slots = {"laser_front", "laser_rear"}, laser_stats = { - lifespan=8, speed=1000, damage=50000, rechargeTime=0.3, length=42, - width=7, beam=0, dual=0, mining=0, rgba_r = 51, rgba_g = 255, rgba_b = 255, rgba_a = 255 - }, purchasable=true, tech_level=10, - icon_name="equip_plasma_accelerator" -}) -laser.large_plasma_accelerator = LaserType.New({ - l10n_key="LARGE_PLASMA_ACCEL", price=390000, capabilities={mass=50}, - slots = {"laser_front", "laser_rear"}, laser_stats = { - lifespan=8, speed=1000, damage=100000, rechargeTime=0.3, length=42, - width=7, beam=0, dual=0, mining=0, rgba_r = 127, rgba_g = 255, rgba_b = 255, rgba_a = 255 - }, purchasable=true, tech_level=12, - icon_name="equip_plasma_accelerator" -}) - -return EquipTypes +return Equipment From 9fc56bc6b81abf7e5fe459421069bf91622a3df3 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 26 Sep 2024 19:08:00 -0400 Subject: [PATCH 090/119] Document public methods of EquipSet - Remove unused CountInstalledWithFilter method - Add NaturalDocs comments for all public API --- data/libs/EquipSet.lua | 238 +++++++++++++++++++++++++++++++---------- 1 file changed, 182 insertions(+), 56 deletions(-) diff --git a/data/libs/EquipSet.lua b/data/libs/EquipSet.lua index 8452c8dc44..d8846709e5 100644 --- a/data/libs/EquipSet.lua +++ b/data/libs/EquipSet.lua @@ -18,8 +18,29 @@ local EquipSet = utils.class("EquipSet") ---@alias EquipSet.Listener fun(op: 'install'|'remove', equip: EquipType, slot: HullConfig.Slot?) -local function slotTypeMatches(equipType, slotType) - return equipType == slotType or string.sub(equipType, 1, #slotType + 1) == slotType .. "." +-- Function: SlotTypeMatches +-- +-- Static helper function that performs filtering of slot type identifiers. +-- +-- Call this function when you want to know if the actual type of some object +-- is a valid match with the given filter string. +-- +-- Example: +-- +-- > local validEquipForSlot = EquipSet.SlotTypeMatches(equip.slot.type, slot.type) +-- +-- Parameters: +-- +-- actualType - string, concrete fully-qualified type string of the object or +-- slot in question +-- +-- filter - string, the partially-qualified type to check the concrete type +-- against to determine a match +-- +---@param actualType string +---@param filter string +local function slotTypeMatches(actualType, filter) + return actualType == filter or string.sub(actualType, 1, #filter + 1) == filter .. "." end EquipSet.SlotTypeMatches = slotTypeMatches @@ -28,6 +49,15 @@ EquipSet.SlotTypeMatches = slotTypeMatches -- -- Static helper function to check if the given equipment item is compatible -- with the given slot object. Validates type and size parameters of the slot. +-- +-- Parameters: +-- +-- equip - EquipType, equipment item instance or prototype to check +-- +-- slot - optional HullConfig.Slot, the slot the equipment item is being +-- validated against. If not present, the function validates that the +-- passed equipment item is a non-slot equipment item. +-- ---@param equip EquipType ---@param slot HullConfig.Slot? function EquipSet.CompatibleWithSlot(equip, slot) @@ -40,15 +70,18 @@ function EquipSet.CompatibleWithSlot(equip, slot) and (equip.slot.size >= (slot.size_min or slot.size)) end +-- Constructor: New +-- +-- Construct a new EquipSet object for the given ship. ---@param ship Ship function EquipSet:Constructor(ship) self.ship = ship self.config = HullConfig.GetHullConfigs()[ship.shipId] -- Stores a mapping of slot id -> equipment item - -- Non-slot equipment is stored in the array portion + -- Non-slot equipment is stored in the array portion. self.installed = {} ---@type table - -- Note: the integer value stored in the cache is NOT the current array + -- NOTE: the integer value stored in the cache is NOT the current array -- index of the given item. It's simply a non-nil integer to indicate the -- item is not installed in a slot. self.cache = {} ---@type table @@ -66,6 +99,8 @@ function EquipSet:Constructor(ship) self:BuildSlotCache() + -- List of listener functions to be notified of changes to the set of + -- installed equipment in this EquipSet self.listeners = {} ---@type EquipSet.Listener[] -- Initialize ship properties we're responsible for modifying @@ -74,6 +109,10 @@ function EquipSet:Constructor(ship) self.ship:setprop("totalVolume", self.ship.totalVolume or self.config.capacity) end +-- Callback: OnShipTypeChanged +-- +-- Called when the type of the owning ship is changed. Removes all installed +-- equipment and tries to leave the ship in an "empty" state. function EquipSet:OnShipTypeChanged() ---@type (string|integer)[] local to_remove = {} @@ -99,7 +138,7 @@ function EquipSet:OnShipTypeChanged() assert(#self.installed == 0, "Missed some equipment while cleaning the ship") end --- Function: GetFreeVolume +-- Method: GetFreeVolume -- -- Returns the available volume for mounting equipment ---@return number @@ -107,19 +146,40 @@ function EquipSet:GetFreeVolume() return self.ship.totalVolume - self.ship.equipVolume end --- Function: GetSlotHandle +-- Method: GetSlotHandle -- -- Return a reference to the slot with the given ID managed by this EquipSet. -- The returned slot should be considered immutable. +-- +-- Parameters: +-- +-- id - string, fully-qualified ID of the slot to look up. +-- +-- Returns: +-- +-- slot - HullConfig.Slot?, the slot associated with that id if present or nil. +-- ---@param id string ---@return HullConfig.Slot? function EquipSet:GetSlotHandle(id) return self.slotCache[id] end --- Function: GetItemInSlot +-- Method: GetItemInSlot -- -- Return the equipment item installed in the given slot, if present. +-- +-- Parameters: +-- +-- slot - HullConfig.Slot, a slot instance present on this ship. +-- Should come from config.slots or an installed equipment instance's +-- provides_slots field. +-- +-- Returns: +-- +-- equip - EquipType?, the equipment instance installed in the given +-- slot if any. +-- ---@param slot HullConfig.Slot ---@return EquipType? function EquipSet:GetItemInSlot(slot) @@ -131,11 +191,24 @@ function EquipSet:GetItemInSlot(slot) return id and self.installed[id] end --- Function: GetFreeSlotForEquip +-- Method: GetFreeSlotForEquip +-- +-- Attempts to find an available slot where the passed equipment item instance +-- could be installed. +-- +-- Does not attempt to find the most optimal slot - the first slot which meets +-- the type and size constraints for the equipment item is returned. +-- +-- The list of slots is iterated in undefined order. +-- +-- Parameters: +-- +-- equip - EquipType, the equipment item instance to attempt to slot. +-- +-- Returns: +-- +-- slot - HullConfig.Slot?, a valid slot for the item or nil. -- --- Attempts to find an available slot where the passed equipment item could be --- installed. Does not attempt to find the most optimal slot - the first slot --- which meets the type and size constraints for the equipment item is returned. ---@param equip EquipType ---@return HullConfig.Slot? function EquipSet:GetFreeSlotForEquip(equip) @@ -156,10 +229,23 @@ function EquipSet:GetFreeSlotForEquip(equip) return nil end --- Function: GetAllSlotsOfType +-- Method: GetAllSlotsOfType -- -- Return a list of all slots matching the given filter parameters. -- If hardpoint is not specified, returns both hardpoint and internal slots. +-- +-- Parameters: +-- +-- type - string, slot type filter internally passed to SlotTypeMatches. +-- +-- hardpoint - optional boolean, constrains the lost of slots to only those +-- which have a `hardpoint` key of the given type. +-- +-- Returns: +-- +-- slots - HullConfig.Slot[], unsorted list of slots which match the given +-- search criteria. +-- ---@param type string ---@param hardpoint boolean? ---@return HullConfig.Slot[] @@ -175,26 +261,23 @@ function EquipSet:GetAllSlotsOfType(type, hardpoint) return t end --- Function: CountInstalledWithFilter --- --- Return a count of all installed equipment matching the given filter function ----@param filter fun(equip: EquipType): boolean ----@return integer -function EquipSet:CountInstalledWithFilter(filter) - local count = 0 - - for _, equip in pairs(self.installed) do - if filter(equip) then - count = count + (equip.count or 1) - end - end - - return count -end - --- Function: GetInstalledWithFilter +-- Method: GetInstalledWithFilter -- -- Return a list of all installed equipment of the given EquipType class matching the filter function +-- +-- Parameters: +-- +-- typename - string, constrains the results to only equipment items +-- inheriting from the given class. +-- +-- filter - function, returns a boolean indicating whether the equipment +-- item should be included in the returned list. +-- +-- Returns: +-- +-- items - EquipType[], list of items of the passed class which were accepted +-- by the filter function. +-- ---@generic T : EquipType ---@param typename `T` ---@param filter fun(equip: T): boolean @@ -211,9 +294,19 @@ function EquipSet:GetInstalledWithFilter(typename, filter) return out end --- Function: GetInstalledOfType +-- Method: GetInstalledOfType -- -- Return a list of all installed equipment matching the given slot type +-- +-- Parameters: +-- +-- type - string, slot type filter string according to SlotTypeMatches +-- +-- Returns: +-- +-- installed - EquipType[], list of installed equipment which passed the +-- given slot type filter +-- ---@param type string type filter ---@return EquipType[] function EquipSet:GetInstalledOfType(type) @@ -228,24 +321,23 @@ function EquipSet:GetInstalledOfType(type) return out end --- Function: GetInstalledEquipment +-- Method: GetInstalledEquipment -- -- Returns a table containing all equipment items installed on this ship, -- including both slot-based equipment and freely-installed equipment. +-- +-- The returned table has both string and integer keys but should only be +-- iterated via `pairs()` as the integer keys are not guaranteed to be +-- contiguous. ---@return table function EquipSet:GetInstalledEquipment() return self.installed end --- Function: GetInstalledNonSlot +-- Method: GetInstalledNonSlot -- -- Returns an array containing all non-slot equipment items installed on this -- ship. Items installed in a specific slot are not returned. --- --- Status: --- --- experimental --- ---@return EquipType[] function EquipSet:GetInstalledNonSlot() local out = {} @@ -257,14 +349,30 @@ function EquipSet:GetInstalledNonSlot() return out end --- Function: CanInstallInSlot +-- Method: CanInstallInSlot -- -- Checks if the given equipment item could potentially fit in the passed slot, -- given the current state of the ship. -- -- If there is an item in the current slot, validates the fit as though that -- item were not currently installed. --- Returns false if the equipment item is not compatible with slot mounting. +-- This function does not recurse into sub-slots provided by the item and thus +-- may not take into account the state of the ship if the equipped item and all +-- of its contained children were removed. +-- +-- Parameters: +-- +-- slotHandle - HullConfig.Slot, the slot to test the equipment item against. +-- +-- equipment - EquipType, the equipment item instance being checked for +-- installation. +-- +-- Returns: +-- +-- valid - boolean, indicates whether the given item could be installed in the +-- passed slot. Will always return false if the object is not a +-- slot-mounted item. +-- ---@param slotHandle HullConfig.Slot ---@param equipment EquipType function EquipSet:CanInstallInSlot(slotHandle, equipment) @@ -276,7 +384,7 @@ function EquipSet:CanInstallInSlot(slotHandle, equipment) and (freeVolume >= equipment.volume) end --- Function: CanInstallLoose +-- Method: CanInstallLoose -- -- Checks if the given equipment item can be installed in the free equipment -- volume of the ship. Returns false if the equipment item requires a slot. @@ -286,7 +394,7 @@ function EquipSet:CanInstallLoose(equipment) and self:GetFreeVolume() >= equipment.volume end --- Function: AddListener +-- Method: AddListener -- -- Register an event listener function to be notified of changes to this ship's -- equipment loadout. @@ -295,22 +403,32 @@ function EquipSet:AddListener(fun) table.insert(self.listeners, fun) end --- Function: RemoveListener +-- Method: RemoveListener -- -- Remove a previously-registered event listener function. function EquipSet:RemoveListener(fun) utils.remove_elem(self.listeners, fun) end --- Function: Install +-- Method: Install -- -- Install an equipment item in the given slot or in free equipment volume. -- --- The passed equipment item must be an equipment item instance, not an --- equipment item prototype. +-- Parameters: +-- +-- equipment - EquipType, an equipment item instance to install on this ship. +-- Must be an instance, not a equipment item prototype. Cannot be +-- currently installed on this or any other ship. +-- +-- slotHandle - optional HullConfig.Slot, the slot to install the item into or +-- nil if the item does not support slot mounting. If present, +-- must be a slot returned from calling GetSlotHandle or a similar +-- function on this EquipSet instance. +-- +-- Returns: +-- +-- installed - boolean, indicates whether the item was successfully installed -- --- The passed slot must be a slot returned from caling GetSlotHandle or --- GetAllSlotsOfType on this EquipSet. ---@param equipment EquipType ---@param slotHandle HullConfig.Slot? ---@return boolean @@ -352,18 +470,26 @@ function EquipSet:Install(equipment, slotHandle) return true end --- Function: Remove +-- Method: Remove -- -- Remove a previously-installed equipment item from this ship. -- --- The equipment item must be the same item instance that was installed prior; --- passing an equipment prototype instance will not result in any equipment --- item being removed. +-- Note that when removing an equipment item that provides slots, this function +-- will not recurse into an item's slots to remove installed sub-items. +-- +-- All items installed into those slots must be manually removed before calling +-- this function. +-- +-- Parameters: +-- +-- equipment - EquipType, the equipment item to remove. Must be an equipment +-- item instance that was installed prior to this ship. Passing +-- an equipment prototype instance will not remove any equipment. +-- +-- Returns: +-- +-- removed - boolean, indicates successful removal of the item -- --- Note that when removing an equipment item that provides slots, all items --- installed into those slots must be manually removed *before* calling this --- function! EquipSet:Remove() will not recurse into an item's slots to remove --- installed sub-items! ---@param equipment EquipType ---@return boolean function EquipSet:Remove(equipment) From 2f9ceb01ff875a78df18f581da014361236a1643 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 26 Sep 2024 19:24:00 -0400 Subject: [PATCH 091/119] Document EquipType.lua - Classname normalization pass - Remove unused methods related to old equipment slots - Provide lua type information for all EquipType subclasses --- data/libs/EquipType.lua | 143 +++++++++++++++++++++++++--------------- 1 file changed, 89 insertions(+), 54 deletions(-) diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index 6cee501093..72f6bc9781 100644 --- a/data/libs/EquipType.lua +++ b/data/libs/EquipType.lua @@ -86,8 +86,16 @@ function EquipType.New (specs) return obj end +-- Method: SpecializeForShip +-- -- Override this with a function customizing the equipment instance for the passed ship --- (E.g. for equipment with mass/volume/cost dependent on the specific ship hull) +-- (E.g. for equipment with mass/volume/cost dependent on the specific ship hull). +-- +-- Parameters: +-- +-- ship - HullConfig, hull configuration this item is tailored for. Note that +-- the config may not be associated with a concrete Ship object yet. +-- EquipType.SpecializeForShip = nil ---@type nil | fun(self: self, ship: HullConfig) function EquipType._createTransient(obj) @@ -98,6 +106,20 @@ function EquipType._createTransient(obj) } end +-- Method: OnInstall +-- +-- Perform any setup associated with installing this item on a Ship. +-- +-- If overriding this function in a subclass you should be careful to ensure +-- the parent class's implementation is always called. +-- +-- Parameters: +-- +-- ship - Ship, the ship this equipment item is being installed in. +-- +-- slot - optional HullConfig.Slot, the slot this item is being installed in +-- if it is a slot-mounted equipment item. +-- ---@param ship Ship ---@param slot HullConfig.Slot? function EquipType:OnInstall(ship, slot) @@ -110,31 +132,62 @@ function EquipType:OnInstall(ship, slot) end end +-- Method: OnRemove +-- +-- Perform any setup associated with uninstalling this item from a Ship. +-- +-- If overriding this function in a subclass you should be careful to ensure +-- the parent class's implementation is always called. +-- +-- Parameters: +-- +-- ship - Ship, the ship this equipment item is being removed from. +-- +-- slot - optional HullConfig.Slot, the slot this item is being removed from +-- if it is a slot-mounted equipment item. +-- ---@param ship Ship ---@param slot HullConfig.Slot? function EquipType:OnRemove(ship, slot) -- Override this for any custom uninstallation logic needed end -function EquipType.isProto(inst) - return not rawget(inst, "__proto") +-- Method: isProto +-- +-- Returns true if this object is an equipment item prototype, false if it is +-- an instance. +function EquipType:isProto() + return not rawget(self, "__proto") end +-- Method: GetPrototype +-- +-- Return the prototype this equipment item instance is derived from, or the +-- self argument if called on a prototype directly. ---@return EquipType function EquipType:GetPrototype() return rawget(self, "__proto") or self end --- Create an instance of this equipment prototype +-- Method: Instance +-- +-- Create and return an instance of this equipment prototype. ---@return EquipType function EquipType:Instance() return setmetatable({ __proto = self }, self.meta) end +-- Method: SetCount +-- +-- Update this equipment instance's stats to represent a logical "stack" of the +-- same item. This should never be called on an instance that is already +-- installed in an EquipSet. +-- -- Some equipment slots represent multiple in-world items as a single logical -- "item" for the player to interact with. This function handles scaling -- equipment stats according to the number of "copies" of the item this -- instance represents. +---@param count integer function EquipType:SetCount(count) local proto = self:GetPrototype() @@ -146,7 +199,7 @@ end -- Patch an EquipType class to support a prototype-based equipment system -- `equipProto = EquipType.New({ ... })` to create an equipment prototype --- `equipInst = equipProto()` to create a new instance based on the created prototype +-- `equipInst = equipProto:Instance()` to create a new instance based on the created prototype function EquipType.SetupPrototype(type) local old = type.New local un = type.Unserialize @@ -196,63 +249,30 @@ function EquipType.Unserialize(data) return obj end +-- Method: GetName -- --- Group: Methods --- - --- --- Method: GetDefaultSlot --- --- returns the default slot for this equipment --- --- Parameters: --- --- ship (optional) - if provided, tailors the answer for this specific ship --- --- Return: --- --- slot_name - A string identifying the slot. --- -function EquipType:GetDefaultSlot(ship) - return self.slots[1] -end - --- --- Method: IsValidSlot --- --- tells whether the given slot is valid for this equipment --- --- Parameters: --- --- slot - a string identifying the slot in question --- --- ship (optional) - if provided, tailors the answer for this specific ship --- --- Return: --- --- valid - a boolean qualifying the validity of the slot. --- -function EquipType:IsValidSlot(slot, ship) - for _, s in ipairs(self.slots) do - if s == slot then - return true - end - end - return false -end - +-- Returns the translated name of this equipment item suitable for display to +-- the user. +---@return string function EquipType:GetName() return self.transient.name end +-- Method: GetDescription +-- +-- Returns the translated description of this equipment item suitable for +-- display to the user +---@return string function EquipType:GetDescription() return self.transient.description end +--============================================================================== + -- Base type for weapons ----@class EquipType.LaserType : EquipType +---@class Equipment.LaserType : EquipType ---@field laser_stats table -local LaserType = utils.inherits(EquipType, "LaserType") +local LaserType = utils.inherits(EquipType, "Equipment.LaserType") ---@param ship Ship ---@param slot HullConfig.Slot @@ -276,11 +296,13 @@ function LaserType:OnRemove(ship, slot) end end +--============================================================================== + -- Single drive type, no support for slave drives. ---@class Equipment.HyperdriveType : EquipType ---@field fuel CommodityType ---@field byproduct CommodityType? -local HyperdriveType = utils.inherits(EquipType, "HyperdriveType") +local HyperdriveType = utils.inherits(EquipType, "Equipment.HyperdriveType") function HyperdriveType:GetMaximumRange(ship) return 625.0*(self.capabilities.hyperclass ^ 2) / (ship.staticMass + ship.fuelMassLeft) @@ -409,11 +431,20 @@ function HyperdriveType:OnLeaveHyperspace(ship) end end +--============================================================================== + -- NOTE: "sensors" have no general-purpose code associated with the equipment type -local SensorType = utils.inherits(EquipType, "SensorType") +---@class Equipment.SensorType : EquipType +local SensorType = utils.inherits(EquipType, "Equipment.SensorType") + +--============================================================================== -- NOTE: all code related to managing a body scanner is implemented in the ScanManager component -local BodyScannerType = utils.inherits(SensorType, "BodyScannerType") +---@class Equipment.BodyScannerType : EquipType +---@field stats table +local BodyScannerType = utils.inherits(SensorType, "Equipment.BodyScannerType") + +--============================================================================== ---@class Equipment.CabinType : EquipType ---@field passengers Character[]? @@ -462,9 +493,13 @@ function CabinType:OnRemove(ship, slot) end end +--============================================================================== + ---@class Equipment.ThrusterType : EquipType local ThrusterType = utils.inherits(EquipType, "Equipment.ThrusterType") +--============================================================================== + Serializer:RegisterClass("LaserType", LaserType) Serializer:RegisterClass("EquipType", EquipType) Serializer:RegisterClass("HyperdriveType", HyperdriveType) From 611a59a0b0f5b904f2c0b8f3758d8952c58d8bac Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 26 Sep 2024 20:01:31 -0400 Subject: [PATCH 092/119] Document ShipBuilder.lua --- data/modules/MissionUtils/ShipBuilder.lua | 172 +++++++++++++++++++++- 1 file changed, 166 insertions(+), 6 deletions(-) diff --git a/data/modules/MissionUtils/ShipBuilder.lua b/data/modules/MissionUtils/ShipBuilder.lua index ad65b1a2c4..36ceb3760e 100644 --- a/data/modules/MissionUtils/ShipBuilder.lua +++ b/data/modules/MissionUtils/ShipBuilder.lua @@ -20,8 +20,21 @@ local slotTypeMatches = EquipSet.SlotTypeMatches -- Class: MissionUtils.ShipBuilder -- --- Utilities for spawning and equipping NPC ships to be used in mission modules - +-- Utilities for spawning and equipping NPC ships to be used in mission modules. +-- +-- This class provides a complete API for describing in generic terms how a ship +-- should be equipped, allows tailoring that equipment for a wanted difficulty +-- of a combat encounter, and hides most of the complexity of dealing with +-- recursive, complicated slot layouts across all ship hulls in the game. +-- +-- The function of the ShipBuilder is to enable mission modules to be able to +-- write a single description specifying how a generic ship should be equipped, +-- and apply that description across a wildly varying selection of ships with +-- no commonality in slot IDs or internal layout. +-- +-- It also provides limited support for producing "natural" looking loadouts +-- with semi-balanced equipment across multiple slots of the same type. +-- ---@class MissionUtils.ShipBuilder local ShipBuilder = {} @@ -88,6 +101,19 @@ ShipBuilder.kShieldSharedHullThreat = 0.005 -- ============================================================================= +-- Class: MissionUtils.ShipTemplate +-- +-- This class is the primary data container for mission modules to express how +-- they would like spawned ships to be outfitted. +-- +-- Pre-existing templates can be found in MissionUtils.ShipTemplates, and +-- individual mission modules can construct their own specialized templates or +-- clone and modify existing templates. +-- +-- Note that the list of equipment rules is shared between clones of a template +-- for performance reasons and should be overwritten in cloned templates with a +-- modified copy as desired. +-- ---@class MissionUtils.ShipTemplate ---@field clone fun(self, mixin: { rules: MissionUtils.OutfitRule[] }): self local Template = utils.proto("MissionUtils.ShipTemplate") @@ -103,7 +129,11 @@ ShipBuilder.Template = Template -- ============================================================================= -local ShipPlan = utils.proto("MissionUtils.ShipPlan") +-- Internal ship plan class, intended as an opaque object from external users +-- of the API. +---@class MissionUtils.ShipBuilder.ShipPlan +---@field clone fun(self, mixin: table): self +local ShipPlan = utils.proto("MissionUtils.ShipBuilder.ShipPlan") ShipPlan.config = nil ShipPlan.shipId = "" @@ -129,6 +159,8 @@ function ShipPlan:SortSlots() table.sort(self.slots, function(a, b) return a.size > b.size or (a.size == b.size and a.id < b.id) end) end +-- Set the hull config this plan is going to be applied to. +-- Creates the list of slots to populate with equipment items. function ShipPlan:SetConfig(shipConfig) self.config = shipConfig self.shipId = shipConfig.id @@ -151,6 +183,8 @@ function ShipPlan:AddSlots(baseId, slots) self:SortSlots() end +-- Add the given equipment object to the plan, making an instance as needed +-- and applying its provided slots to the list of slots present in the plan. function ShipPlan:AddEquipToPlan(equip, slot, threat) -- print("Installing " .. equip:GetName()) @@ -177,6 +211,9 @@ end -- ============================================================================= +-- Class: MissionUtils.ShipBuilder + +-- Compute threat factor for weapon equipment local function calcWeaponThreat(equip, hullThreat) local damage = equip.laser_stats.damage / 1000 -- unit tons of armor local speed = equip.laser_stats.speed / 1000 @@ -199,6 +236,7 @@ local function calcWeaponThreat(equip, hullThreat) return threat end +-- Compute threat factor for shield equipment local function calcShieldThreat(equip, hullThreat) -- FIXME: this is a hardcoded constant shared with Ship.cpp local shield_tons = equip.capabilities.shield * 10.0 @@ -209,6 +247,10 @@ local function calcShieldThreat(equip, hullThreat) return threat end +-- Function: ComputeEquipThreatFactor +-- +-- Compute a threat factor for the given equipment item. The equipment threat +-- factor may be modified by the threat of the hull it is installed on. function ShipBuilder.ComputeEquipThreatFactor(equip, hullThreat) if equip.slot and equip.slot.type:match("^weapon") and equip.laser_stats then return calcWeaponThreat(equip, hullThreat) @@ -251,7 +293,16 @@ end -- ============================================================================= ----@param shipPlan table +-- Function: ApplyEquipmentRule +-- +-- Apply the passed equipment rule to an in-progress ship plan, observing the +-- limits expressed in the rule regarding threat, size, and number of items +-- installed. +-- +-- Intended as private internal API, and should not be called from outside the +-- ShipBuilder module. +-- +---@param shipPlan MissionUtils.ShipBuilder.ShipPlan ---@param rule MissionUtils.OutfitRule ---@param rand Rand ---@param hullThreat number @@ -425,13 +476,44 @@ end -- ============================================================================= +-- Function: GetHullThreat +-- +-- Return the threat factor table computed for a given hull configuration, +-- looked up by the passed identifier. function ShipBuilder.GetHullThreat(shipId) return hullThreatCache[shipId] or { total = 0.0 } end +-- Function: SelectHull +-- +-- Return a ship hull configuration appropriate for the passed template and +-- threat factor. +-- +-- If the passed template specifies a hull configuration identifier in the +-- shipId field, that configuration is returned directly. Otherwise, the list +-- of available hull configurations is filtered and a random valid hull is +-- returned. +-- +-- Parameters: +-- +-- template - MissionUtils.ShipTemplate, used to filter the selection of hulls +-- by role, hyperdrive class, etc. +-- +-- threat - number, intended encounter threat factor used to filter valid +-- hulls by their threat factor. This value is used to compute both +-- an upper and lower bound on allowable threat value for the hull. +-- +-- Returns: +-- +-- hull - HullConfig?, the hull configuration selected for this template, or +-- nil if no hulls passed the validity checks specified by the threat +-- factor and ship template. +-- ---@param template MissionUtils.ShipTemplate ---@param threat number +---@return HullConfig? function ShipBuilder.SelectHull(template, threat) + local hullList = {} if template.shipId then @@ -476,11 +558,42 @@ function ShipBuilder.SelectHull(template, threat) -- print(" threat {} => {} ({} / {})" % { threat, shipId, hullIdx, #hullList }) return HullConfig.GetHullConfigs()[shipId] + end +-- Function: MakePlan +-- +-- Evaluates all equipment rules specified in the ship template to produce a +-- concrete plan for equipping the given hull configuration. +-- +-- Rules are evaluated in order of appearance, and can be disabled by the +-- minThreat and randomChance rule parameters in each individual equipment rule. +-- +-- See data/modules/MissionUtils/OutfitRules.lua for information on the fields +-- applicable to an equipment rule. +-- +-- Parameters: +-- +-- template - MissionUtils.ShipTemplate, the template containing equipment +-- rules to evaluate. +-- +-- shipConfig - HullConfig, the hull configuration to make a plan for. +-- +-- threat - number, controls the intended difficulty of the encounter. The +-- threat value is used to limit which equipment can be installed +-- and whether an equipment rule can be evaluated at all via the +-- minThreat property. +-- +-- Returns: +-- +-- plan - table, an opaque structure containing information about the ship +-- to spawn and the equipment to install as a result of evaluating the +-- input template. +-- ---@param template MissionUtils.ShipTemplate ---@param shipConfig HullConfig ---@param threat number +---@return MissionUtils.ShipBuilder.ShipPlan function ShipBuilder.MakePlan(template, shipConfig, threat) local hullThreat = ShipBuilder.GetHullThreat(shipConfig.id).total @@ -527,8 +640,25 @@ function ShipBuilder.MakePlan(template, shipConfig, threat) end +-- Function: ApplyPlan +-- +-- Apply the previously created plan to a concrete ship object of the +-- previously-specified hull configuration, installing all equipment instances +-- created by applying the equipment rules contained in the ship template. +-- +-- This function ensures that equipment is properly installed in the order it +-- was selected and items are properly distributed to sub-slots provided by +-- installed equipment items. +-- +-- Parameters: +-- +-- ship - Ship, the ship to install equipment into. +-- +-- shipPlan - table, an opaque ship plan returned from a previous call to +-- ShipBuilder.MakePlan. +-- ---@param ship Ship ----@param shipPlan table +---@param shipPlan MissionUtils.ShipBuilder.ShipPlan function ShipBuilder.ApplyPlan(ship, shipPlan) local equipSet = ship:GetComponent('EquipSet') @@ -555,6 +685,12 @@ end -- ============================================================================= +-- Function: MakeShipNear +-- +-- Spawns a ship near the specified body according to the given ship template +-- and threat value. +-- +-- See: Space.SpawnShipNear ---@param body Body ---@param template MissionUtils.ShipTemplate ---@param threat number? @@ -580,6 +716,12 @@ function ShipBuilder.MakeShipNear(body, template, threat, nearDist, farDist) return ship end +-- Function: MakeShipOrbit +-- +-- Spawns a ship in orbit around a specified body according to the given ship +-- template and threat value. +-- +-- See: Spawn.SpawnShipOrbit ---@param body Body ---@param template MissionUtils.ShipTemplate ---@param threat number @@ -605,6 +747,12 @@ function ShipBuilder.MakeShipOrbit(body, template, threat, nearDist, farDist) return ship end +-- Function: MakeShipLanded +-- +-- Spawns a ship landed on a specified body according to the given ship +-- template and threat value. +-- +-- See: Spawn.SpawnShipLanded ---@param body Body ---@param template MissionUtils.ShipTemplate ---@param threat number @@ -630,7 +778,13 @@ function ShipBuilder.MakeShipLanded(body, template, threat, lat, lon) return ship end ----@param body Body +-- Function: MakeShipDocked +-- +-- Spawns a ship docked atan a specified station according to the given ship +-- template and threat value. +-- +-- See: Spawn.SpawnShipDocked +---@param body SpaceStation ---@param template MissionUtils.ShipTemplate ---@param threat number? ---@return Ship @@ -653,6 +807,12 @@ function ShipBuilder.MakeShipDocked(body, template, threat) return ship end +-- Function: MakeShipAroundStar +-- +-- Spawns a ship in orbit around the system center according to the given ship +-- template and threat value. +-- +-- See: Spawn.SpawnShip ---@param template MissionUtils.ShipTemplate ---@param threat number ---@param minDistAu number From a42000e3d19d5a06668aba6dcdefb72502082b81 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 27 Sep 2024 17:30:11 -0400 Subject: [PATCH 093/119] EquipSet: change HullConfig, rebuild slot cache on ShipType change --- data/libs/EquipSet.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/data/libs/EquipSet.lua b/data/libs/EquipSet.lua index d8846709e5..08e66d6d26 100644 --- a/data/libs/EquipSet.lua +++ b/data/libs/EquipSet.lua @@ -136,6 +136,14 @@ function EquipSet:OnShipTypeChanged() end assert(#self.installed == 0, "Missed some equipment while cleaning the ship") + + -- HullConfig changed, need to rebuild list of slots + self.config = HullConfig.GetHullConfigs()[self.ship.shipId] + + self.slotCache = {} + self.idCache = {} + + self:BuildSlotCache() end -- Method: GetFreeVolume From 406fa18618796568768d46ec5b066a79d952ee24 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 27 Sep 2024 18:45:31 -0400 Subject: [PATCH 094/119] Debug: remove old ship tools, change icons - Moonscript - although quite expressive - does not have enough tooling support to be a viable option for writing Pioneer code - Remove old ship spawner UIs - Give RPG debug tab the money icon, change commodity tab to cargo icon --- data/modules/Debug/DebugCommodityPrices.lua | 2 +- data/modules/Debug/DebugRPG.lua | 2 +- data/modules/Debug/DebugShipSpawn.lua | 222 ------------------ data/modules/Debug/DebugShipSpawn.moon | 195 --------------- .../modules/MissionUtils/DebugShipBuilder.lua | 147 ------------ 5 files changed, 2 insertions(+), 566 deletions(-) delete mode 100644 data/modules/Debug/DebugShipSpawn.lua delete mode 100644 data/modules/Debug/DebugShipSpawn.moon delete mode 100644 data/modules/MissionUtils/DebugShipBuilder.lua diff --git a/data/modules/Debug/DebugCommodityPrices.lua b/data/modules/Debug/DebugCommodityPrices.lua index 3f1e2dfbe6..77dfd66b05 100644 --- a/data/modules/Debug/DebugCommodityPrices.lua +++ b/data/modules/Debug/DebugCommodityPrices.lua @@ -379,7 +379,7 @@ end debugView.registerTab("debug-commodity-price", { - icon = ui.theme.icons.money, + icon = ui.theme.icons.cargo_crate, label = "Commodities", show = function() return Game.player ~= nil end, draw = main diff --git a/data/modules/Debug/DebugRPG.lua b/data/modules/Debug/DebugRPG.lua index 3cfb7007b6..5819ae558a 100644 --- a/data/modules/Debug/DebugRPG.lua +++ b/data/modules/Debug/DebugRPG.lua @@ -39,7 +39,7 @@ local get_commodities = function() end debugView.registerTab("RPG-debug-view", { - icon = ui.theme.icons.personal_info, + icon = ui.theme.icons.money, label = "RPG", show = function() return Game.player ~= nil end, draw = function() diff --git a/data/modules/Debug/DebugShipSpawn.lua b/data/modules/Debug/DebugShipSpawn.lua deleted file mode 100644 index 1b259f089b..0000000000 --- a/data/modules/Debug/DebugShipSpawn.lua +++ /dev/null @@ -1,222 +0,0 @@ -local Game = require('Game') -local Space = require('Space') -local Ship = require('Ship') -local ShipDef = require('ShipDef') -local Timer = require('Timer') -local Equipment = require('Equipment') -local Vector2 -Vector2 = _G.Vector2 -local ui = require('pigui') -local debug_ui = require('pigui.views.debug') -local ship_defs = { } -local update_ship_def_table -update_ship_def_table = function() - for name in pairs(ShipDef) do - table.insert(ship_defs, name) - print("Ship Def found: " .. tostring(name)) - end - return table.sort(ship_defs) -end -update_ship_def_table() -print(#ship_defs) -local selected_ship_type = 1 -local draw_ship_types -draw_ship_types = function() - for i, ship in ipairs(ship_defs) do - if ui.selectable(ship, selected_ship_type == i) then - selected_ship_type = i - end - end -end -local draw_ship_info -draw_ship_info = function(self) - ui.text("Ship Info") - ui.separator() - ui.columns(2, 'ship_info', true) - ui.text("Name:") - ui.nextColumn() - ui.text(self.name) - ui.nextColumn() - ui.text("Manufacturer:") - ui.nextColumn() - ui.text(self.manufacturer) - ui.nextColumn() - ui.text("Ship Class:") - ui.nextColumn() - ui.text(self.shipClass) - ui.nextColumn() - return ui.columns(1, '') -end -local ai_opt_selected = 1 -local ai_options = { - "FlyTo", - "Kamikaze", - "Kill" -} -local missile_selected = 0 -local missile_names = { - "Guided Missile", - "Unguided Missile", - "Smart Missile" -} -local missile_types = { - "missile_guided", - "missile_unguided", - "missile_smart" -} -local draw_ai_info -draw_ai_info = function() - for i, opt in ipairs(ai_options) do - if ui.selectable(opt, ai_opt_selected == i) then - ai_opt_selected = i - end - end -end -local spawn_distance = 5.0 -local spawn_ship_free -spawn_ship_free = function(ship_name, ai_option, equipment) - local new_ship - do - local _with_0 = Space.SpawnShipNear(ship_name, Game.player, spawn_distance, spawn_distance) - _with_0:SetLabel(Ship.MakeRandomLabel()) - for _index_0 = 1, #equipment do - local equip = equipment[_index_0] - _with_0:AddEquip(equip) - end - _with_0:UpdateEquipStats() - new_ship = _with_0 - end - return new_ship["AI" .. tostring(ai_option)](new_ship, Game.player) -end -local spawn_ship_docked -spawn_ship_docked = function(ship_name, ai_option, equipment) - local new_ship - do - local _with_0 = Space.SpawnShipDocked(ship_name, Game.player:GetNavTarget()) - _with_0:SetLabel(Ship.MakeRandomLabel()) - for _index_0 = 1, #equipment do - local equip = equipment[_index_0] - _with_0:AddEquip(equip) - end - _with_0:UpdateEquipStats() - new_ship = _with_0 - end - return new_ship["AI" .. tostring(ai_option)](new_ship, Game.player) -end -local do_spawn_missile -do_spawn_missile = function(type) - if Game.player:IsDocked() then - return nil - end - local new_missile - do - local _with_0 = Game.player:SpawnMissile(type) - _with_0:AIKamikaze(Game.player:GetCombatTarget()) - new_missile = _with_0 - end - return Timer:CallEvery(2, function() - if new_missile:exists() then - new_missile:Arm() - end - return true - end) -end -local ship_equip = { - Equipment.laser.pulsecannon_dual_1mw, - Equipment.misc.laser_cooling_booster, - Equipment.misc.atmospheric_shielding -} -local set_player_ship_type -set_player_ship_type = function(shipType) - local equipSet = Game.player:GetComponent("EquipSet") - local items = equipSet:GetInstalledEquipment() - local returnedMoney = 0 - for i, item in pairs(items) do - returnedMoney = returnedMoney + item.price - end - do - local _with_0 = Game.player - _with_0:SetShipType(shipType) - _with_0:UpdateEquipStats() - _with_0:AddMoney(returnedMoney) - return _with_0 - end -end -local ship_spawn_debug_window -ship_spawn_debug_window = function() - ui.child('ship_list', Vector2(150, 0), draw_ship_types) - local ship_name = ship_defs[selected_ship_type] - local ship - if ship_name then - ship = ShipDef[ship_name] - end - if not (ship) then - return - end - ui.sameLine() - return ui.group(function() - local spawner_group_height = ui.getFrameHeightWithSpacing() * 4 - ui.child('ship_info', Vector2(0, -spawner_group_height), function() - draw_ship_info(ship) - if ui.button("Set Player Ship Type", Vector2(0, 0)) then - set_player_ship_type(ship_name) - end - ui.spacing() - ui.separator() - ui.spacing() - return draw_ai_info() - end) - if ui.button("Spawn Ship", Vector2(0, 0)) then - spawn_ship_free(ship_name, ai_options[ai_opt_selected], ship_equip) - end - local nav_target = Game.player:GetNavTarget() - if nav_target and nav_target:isa("SpaceStation") then - ui.sameLine() - if ui.button("Spawn Docked", Vector2(0, 0)) then - spawn_ship_docked(ship_name, ai_options[ai_opt_selected], ship_equip) - end - end - local SectorView = Game.sectorView - if not Game.player:GetDockedWith() then - ui.sameLine() - if nav_target and nav_target:isa("SpaceStation") then - if ui.button("Teleport To", Vector2(0, 0)) then - Game.player:SetDockedWith(nav_target) - end - end - if SectorView:GetSelectedSystemPath() and Game.system and not SectorView:GetSelectedSystemPath():IsSameSystem(Game.system.path) then - if ui.button("Hyperjump To", Vector2(0, 0)) then - Game.player:InitiateHyperjumpTo(SectorView:GetSelectedSystemPath(), 1.0, 0.0, { }) - end - end - end - if Game.player:GetCombatTarget() then - if ui.button("Spawn##Missile", Vector2(0, 0)) then - do_spawn_missile(missile_types[missile_selected + 1]) - end - ui.sameLine(0, 2) - end - ui.nextItemWidth(-1.0) - local _ - _, missile_selected = ui.combo("##missile_type", missile_selected, missile_names) - ui.text("Spawn Distance:") - ui.nextItemWidth(-1.0) - spawn_distance = ui.sliderFloat("##spawn_distance", spawn_distance, 0.5, 20, "%.1fkm") - end) -end -debug_ui.registerTab("Ship Spawner", { - icon = ui.theme.icons.medium_courier, - label = "Ship Spawner", - show = function() - return Game.player and Game.CurrentView() == "world" - end, - draw = ship_spawn_debug_window -}) -return ui.registerModule("game", function() - if not (Game.CurrentView() == "world") then - return nil - end - if ui.isKeyReleased(ui.keys.f12) and ui.ctrlHeld() then - return spawn_ship_free("kanara", "Kill", ship_equip) - end -end) diff --git a/data/modules/Debug/DebugShipSpawn.moon b/data/modules/Debug/DebugShipSpawn.moon deleted file mode 100644 index 8c42ac9f25..0000000000 --- a/data/modules/Debug/DebugShipSpawn.moon +++ /dev/null @@ -1,195 +0,0 @@ - -Game = require 'Game' -Space = require 'Space' -Ship = require 'Ship' -ShipDef = require 'ShipDef' -Timer = require 'Timer' -Equipment = require 'Equipment' - -import Vector2 from _G - -ui = require 'pigui' -debug_ui = require 'pigui.views.debug' - -ship_defs = {} - -update_ship_def_table = -> - for name in pairs ShipDef - table.insert ship_defs, name - print "Ship Def found: #{name}" - table.sort ship_defs - -update_ship_def_table! -print #ship_defs - -selected_ship_type = 1 -draw_ship_types = -> - for i, ship in ipairs ship_defs - if ui.selectable ship, selected_ship_type == i - selected_ship_type = i - -draw_ship_info = => - ui.text "Ship Info" - ui.separator! - ui.columns 2, 'ship_info', true - ui.text "Name:" - ui.nextColumn! - ui.text @name - ui.nextColumn! - - ui.text "Manufacturer:" - ui.nextColumn! - ui.text @manufacturer - ui.nextColumn! - - ui.text "Ship Class:" - ui.nextColumn! - ui.text @shipClass - ui.nextColumn! - - ui.columns 1, '' - -ai_opt_selected = 1 -ai_options = { - "FlyTo", "Kamikaze", "Kill" -} - --- ui.combo is zero-based -missile_selected = 0 -missile_names = { - "Guided Missile", "Unguided Missile", "Smart Missile" -} -missile_types = { - "missile_guided", - "missile_unguided", - "missile_smart", -} - -draw_ai_info = -> - for i, opt in ipairs ai_options - if ui.selectable opt, ai_opt_selected == i - ai_opt_selected = i - -spawn_distance = 5.0 -- km - -spawn_ship_free = (ship_name, ai_option, equipment) -> - new_ship = with Space.SpawnShipNear ship_name, Game.player, spawn_distance, spawn_distance - \SetLabel Ship.MakeRandomLabel! - \AddEquip equip for equip in *equipment - \UpdateEquipStats! - - -- Invoke the specified AI method on the new ship. - new_ship["AI#{ai_option}"] new_ship, Game.player - -spawn_ship_docked = (ship_name, ai_option, equipment) -> - new_ship = with Space.SpawnShipDocked ship_name, Game.player\GetNavTarget! - \SetLabel Ship.MakeRandomLabel! - \AddEquip equip for equip in *equipment - \UpdateEquipStats! - - -- Invoke the specified AI method on the new ship. - new_ship["AI#{ai_option}"] new_ship, Game.player - --- Spawn a missile attacking the player's current combat target -do_spawn_missile = (type) -> - if Game.player\IsDocked! - return nil - - new_missile = with Game.player\SpawnMissile type - \AIKamikaze Game.player\GetCombatTarget! - - Timer\CallEvery 2, -> - if new_missile\exists! - new_missile\Arm! - - return true - -ship_equip = { - Equipment.laser.pulsecannon_dual_1mw - Equipment.misc.laser_cooling_booster - Equipment.misc.atmospheric_shielding -} - -set_player_ship_type = (shipType) -> - -- NOTE: this does not preserve installed items as there's no clear A->B - -- mapping for how to handle different slots between ships - -- Instead, refund the purchase price of all installed equipment - - equipSet = Game.player\GetComponent "EquipSet" - items = equipSet\GetInstalledEquipment! - returnedMoney = 0 - - for i, item in pairs items - returnedMoney += item.price - - with Game.player - \SetShipType shipType - \UpdateEquipStats! - \AddMoney returnedMoney - -ship_spawn_debug_window = -> - ui.child 'ship_list', Vector2(150, 0), draw_ship_types - - ship_name = ship_defs[selected_ship_type] - ship = ShipDef[ship_name] if ship_name - return unless ship - - ui.sameLine! - ui.group -> - spawner_group_height = ui.getFrameHeightWithSpacing! * 4 - ui.child 'ship_info', Vector2(0, -spawner_group_height), -> - draw_ship_info ship - if ui.button "Set Player Ship Type", Vector2(0, 0) - set_player_ship_type ship_name - - ui.spacing! - ui.separator! - ui.spacing! - - draw_ai_info! - - if ui.button "Spawn Ship", Vector2(0, 0) - spawn_ship_free ship_name, ai_options[ai_opt_selected], ship_equip - - nav_target = Game.player\GetNavTarget! - if nav_target and nav_target\isa "SpaceStation" - ui.sameLine! - if ui.button "Spawn Docked", Vector2(0, 0) - spawn_ship_docked ship_name, ai_options[ai_opt_selected], ship_equip - - SectorView = Game.sectorView - if not Game.player\GetDockedWith! - ui.sameLine! - if nav_target and nav_target\isa "SpaceStation" - if ui.button "Teleport To", Vector2(0, 0) - Game.player\SetDockedWith nav_target - - if SectorView\GetSelectedSystemPath! and Game.system and not SectorView\GetSelectedSystemPath!\IsSameSystem(Game.system.path) - if ui.button "Hyperjump To", Vector2(0, 0) - Game.player\InitiateHyperjumpTo(SectorView\GetSelectedSystemPath(), 1.0, 0.0, {}) - - if Game.player\GetCombatTarget! - if ui.button "Spawn##Missile", Vector2(0, 0) - do_spawn_missile missile_types[missile_selected + 1] - ui.sameLine 0, 2 - - ui.nextItemWidth -1.0 - _, missile_selected = ui.combo "##missile_type", missile_selected, missile_names - - ui.text "Spawn Distance:" - ui.nextItemWidth -1.0 - spawn_distance = ui.sliderFloat("##spawn_distance", spawn_distance, 0.5, 20, "%.1fkm") - -debug_ui.registerTab "Ship Spawner", { - icon: ui.theme.icons.medium_courier - label: "Ship Spawner" - show: -> Game.player and Game.CurrentView() == "world" - draw: ship_spawn_debug_window -} - -ui.registerModule "game", -> - unless Game.CurrentView() == "world" - return nil - - if ui.isKeyReleased(ui.keys.f12) and ui.ctrlHeld! - spawn_ship_free "kanara", "Kill", ship_equip diff --git a/data/modules/MissionUtils/DebugShipBuilder.lua b/data/modules/MissionUtils/DebugShipBuilder.lua deleted file mode 100644 index 6310c2f155..0000000000 --- a/data/modules/MissionUtils/DebugShipBuilder.lua +++ /dev/null @@ -1,147 +0,0 @@ --- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details --- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt - -local HullConfig = require 'HullConfig' -local ShipDef = require 'ShipDef' -local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' -local utils = require 'utils' - -local debugView = require 'pigui.views.debug' - -local Rules = ShipBuilder.OutfitRules - -local ui = require 'pigui' - -local debugRule = ShipBuilder.Template:clone { - label = "TEST SHIP", - -- shipId = 'coronatrix', - hyperclass = 1, - role = "mercenary", - rules = { - { - slot = "weapon", - filter = "weapon.energy", - pick = "random", - limit = 1, - maxThreatFactor = 0.6 - }, - { - slot = "missile_rack", - maxSize = 2 - }, - { - slot = "missile", - maxSize = 2, - minSize = 2, - limit = 4 - }, - { - slot = "missile", - maxSize = 1, - minSize = 1, - }, - { - slot = "shield", - maxSize = 1, - limit = 2, - balance = true - }, - Rules.DefaultHyperdrive, - -- Rules.DefaultLaserCooling, - { - slot = nil, - equip = "misc.laser_cooling_booster", - limit = 1, - minThreat = 15.0 - }, - Rules.DefaultAtmoShield, - } -} - -debugView.registerTab("ShipBuilder", { - label = "Ship Builder", - icon = ui.theme.icons.ship, - - selectedHull = nil, - spawnThreat = 20.0, - - show = function() return require 'Game'.player end, - - draw = function(self) - self.spawnThreat = ui.sliderFloat("Threat", self.spawnThreat, 1.0, 200.0) - - if ui.button("Spawn Test Ship") then - ShipBuilder.MakeShipNear(require 'Game'.player, debugRule, self.spawnThreat, 15.00, 20.00) - end - - local target = require 'Game'.player:GetCombatTarget() - - if target then - ui.text("Equipment:") - ui.spacing() - - local equipSet = target:GetComponent('EquipSet') - - for id, equip in pairs(equipSet:GetInstalledEquipment()) do - ui.text(id .. ": " .. equip:GetName()) - end - - ui.spacing() - end - - ui.child("hullList", function() - if self.selectedHull then - if ui.button("<") then - self.selectedHull = nil - return - end - - ui.sameLine() - ui.text(self.selectedHull) - - ui.spacing() - - if ui.collapsingHeader("Hull Threat") then - local threat = ShipBuilder.GetHullThreat(self.selectedHull) - - for k, v in pairs(threat) do - ui.text(tostring(k) .. ": " .. tostring(v)) - end - end - - if ui.collapsingHeader("Slots") then - local config = HullConfig.GetHullConfigs()[self.selectedHull] - - for k, v in pairs(config.slots) do - ui.text(k) - - local data = " " - for k2, v2 in pairs(v) do - data = data .. tostring(k2) .. " = " .. tostring(v2) .. ", " - end - - ui.text(data) - end - end - else - local hulls = utils.build_array(pairs(ShipDef)) - - table.sort(hulls, function(a, b) - return ShipBuilder.GetHullThreat(a.id).total < ShipBuilder.GetHullThreat(b.id).total - end) - - for _, def in ipairs(hulls) do - if def.tag == "SHIP" then - if ui.selectable(def.id) then - self.selectedHull = def.id - end - - local threat = ShipBuilder.GetHullThreat(def.id) - ui.sameLine() - ui.text("threat: " .. tostring(threat.total)) - end - end - end - end) - end -}) From a9bed3812b2adda28f1f8602cb377346b62b5efa Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 27 Sep 2024 18:50:04 -0400 Subject: [PATCH 095/119] Add DebugShipTool tab - Contains utilities to spawn ships according to predefined templates, inspect existing ships, spawn missiles, and perform debug travel - Allows in-depth investigation of all ship hull configs in the game - Enhanced ship spawning controls allowing ships to be outfitted according to a number of different templates - Inspect equipment of spawned ships with greater detail than target scanner --- data/modules/Debug/DebugShip.lua | 609 +++++++++++++++++++++++++++++++ 1 file changed, 609 insertions(+) create mode 100644 data/modules/Debug/DebugShip.lua diff --git a/data/modules/Debug/DebugShip.lua b/data/modules/Debug/DebugShip.lua new file mode 100644 index 0000000000..0f33210f76 --- /dev/null +++ b/data/modules/Debug/DebugShip.lua @@ -0,0 +1,609 @@ +-- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +local Game = require 'Game' +local HullConfig = require 'HullConfig' +local ShipDef = require 'ShipDef' +local Lang = require 'Lang' +local Timer = require 'Timer' + +local utils = require 'utils' + +local ui = require 'pigui' + +local colors = ui.theme.colors +local icons = ui.theme.icons + +local ShipTemplates = require 'modules.MissionUtils.ShipTemplates' +local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' + +local Notification = require 'pigui.libs.notification' +local debugView = require 'pigui.views.debug' + +--============================================================================= + +local aiOptions = { + "FlyTo", "Kamikaze", "Kill", "Hold Position" +} + +local spawnOptions = { + "Nearby", + "Docked", + "Orbit" +} + +local templateOptions = { + "GenericPirate", + "StrongPirate", + "GenericMercenary", + "GenericPolice", + "StationPolice", +} + +local missileOptions = { + "Guided Missile", + "Unguided Missile", + "Smart Missile", + "Naval Missile" +} + +local missileTypes = { + "missile_guided", + "missile_unguided", + "missile_smart", + "missile_naval", +} + +---@type HullConfig[] +local shipList = nil + +---@type table +local hullSlots = nil + +local function buildShipList() + shipList = {} + hullSlots = {} + + for _, hull in pairs(HullConfig.GetHullConfigs()) do + table.insert(shipList, hull) + + ---@type HullConfig.Slot[] + local slots = {} + + for _, slot in pairs(hull.slots) do + table.insert(slots, slot) + end + + table.sort(slots, function(a, b) return a.id < b.id end) + + hullSlots[hull.id] = slots + end + + table.sort(shipList, function(a, b) + return ShipBuilder.GetHullThreat(a.id).total < ShipBuilder.GetHullThreat(b.id).total + end) +end + +--============================================================================= + +---@class Debug.DebugShipTool : UI.Module +local DebugShipTool = utils.class("DebugShipSpawner", require 'pigui.libs.module') + +function DebugShipTool:Constructor() + DebugShipTool:Super().Constructor(self) + + self.selectedHullIdx = nil + + -- Hull Config options + self.aiCmdIdx = 1 + self.spawnTemplateIdx = 1 + self.spawnThreat = 20.0 + self.spawnDist = 20.0 + self.spawnLocIdx = 1 + + self.missileIdx = 1 +end + +function DebugShipTool:onClearSelection() + self.selectedHullIdx = nil +end + +function DebugShipTool:onSelectHull(idx) + self.selectedHullIdx = idx +end + +function DebugShipTool:onSetSpawnThreat(threat) + self.spawnThreat = threat +end + +function DebugShipTool:onSetSpawnDistance(dist) + self.spawnDist = dist +end + +function DebugShipTool:onSetSpawnAICmd(option) + self.aiCmdIdx = option +end + +function DebugShipTool:onSetSpawnTemplate(option) + self.spawnTemplateIdx = option +end + +function DebugShipTool:onSetSpawnLocation(loc) + self.spawnLocIdx = loc +end + +function DebugShipTool:onSetMissileIdx(missile) + self.missileIdx = missile +end + +--============================================================================= + +function DebugShipTool:onSpawnSelectedHull() + + local hull = shipList[self.selectedHullIdx] + + local templateName = templateOptions[self.spawnTemplateIdx] + + ---@type MissionUtils.ShipTemplate + local template = ShipTemplates[templateName] + + template = template:clone { + shipId = hull.id + } + + local location = spawnOptions[self.spawnLocIdx] + local ship ---@type Ship + + if location == "Nearby" then + + ship = ShipBuilder.MakeShipNear(Game.player, template, self.spawnThreat, self.spawnDist, self.spawnDist) + + elseif location == "Docked" then + + local body = Game.player:GetNavTarget() + + if not body or not body:isa("SpaceStation") then + Notification.add(Notification.Type.Error, "Debug: no station selected") + return + end + + ---@cast body SpaceStation + ship = ShipBuilder.MakeShipDocked(body, template, self.spawnThreat) + + elseif location == "Orbit" then + + local body = Game.player:GetNavTarget() or Game.player.frameBody + + assert(body) + + ship = ShipBuilder.MakeShipOrbit(body, template, self.spawnThreat, self.spawnDist, self.spawnDist) + + end + + if self.aiCmdIdx ~= #aiOptions then + local aiCmd = "AI" .. aiOptions[self.aiCmdIdx] + ship[aiCmd](ship, Game.player) + end + + Notification.add(Notification.Type.Info, "Debug: spawned {} nearby" % { ShipDef[hull.id].name }) + + Game.player:SetCombatTarget(ship) + +end + +function DebugShipTool:onSetPlayerShipType() + + local hull = shipList[self.selectedHullIdx] + + local equipSet = Game.player:GetComponent('EquipSet') + + local refundTotal = 0.0 + + -- Refund the player the value of their currently equipped items + -- (Not worth it to try to migrate it) + for _, equip in pairs(equipSet:GetInstalledEquipment()) do + refundTotal = refundTotal + equip.price + end + + Game.player:AddMoney(refundTotal) + Game.player:SetShipType(hull.id) + + Notification.add(Notification.Type.Info, + "Debug: set player ship to {}" % { ShipDef[hull.id].name }, + "Refunded {} in equipment value" % { ui.Format.Money(refundTotal) }) + +end + +function DebugShipTool:onSpawnMissile() + + local missile_type = missileTypes[self.missileIdx] + + if missile_type ~= "missile_unguided" 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()) + + Timer:CallAt(Game.time + 1, function() + if missile:exists() then + missile:Arm() + end + end) + +end + +---@param station SpaceStation +function DebugShipTool:onSetDockedWith(station) + Game.player:SetDockedWith(station) +end + +---@param systemPath SystemPath +function DebugShipTool:onHyperjumpTo(systemPath) + Game.player:InitiateHyperjumpTo(systemPath, 1.0, 0.0, {}) +end + +--============================================================================= + +function DebugShipTool:drawShipSelector(currentIdx) + if currentIdx then + if ui.iconButton("back", icons.decrease_1, "Go Back") then + self:message('onClearSelection') + end + + ui.sameLine(0, 2) + end + + local preview = currentIdx and ShipDef[shipList[currentIdx].id].name or "" + + ui.nextItemWidth(ui.getContentRegion().x - (currentIdx and ui.getFrameHeight() + 2 or 0) * 2) + + ui.comboBox("##HullConfig", preview, function() + for idx, hull in ipairs(shipList) do + local clicked = ui.selectable(ShipDef[hull.id].name, idx == currentIdx) + + local threatScore = ShipBuilder.GetHullThreat(hull.id).total + local threatStr = ui.Format.Number(threatScore, 2) .. " Thr." + + ui.sameLine(ui.getContentRegion().x - ui.calcTextSize(threatStr).x) + ui.text(threatStr) + + if clicked then + self:message('onSelectHull', idx) + end + end + end) + + if currentIdx then + ui.sameLine(0, 2) + if ui.iconButton("prev", icons.chevron_up, "Select Previous") then + self:message('onSelectHull', math.max(currentIdx - 1, 1)) + end + + ui.sameLine(0, 2) + if ui.iconButton("next", icons.chevron_down, "Select Next") then + self:message('onSelectHull', math.min(currentIdx + 1, #shipList)) + end + end +end + +local function drawKeyValue(name, val) + ui.tableNextRow() + + ui.tableNextColumn() + ui.text(name .. ":") + + ui.tableNextColumn() + ui.text(type(val) == "number" and ui.Format.Number(val) or tostring(val)) +end + +---@param shipDef ShipDef +function DebugShipTool:drawShipDefInfo(shipDef) + if ui.beginTable("Ship Def", 2) then + + drawKeyValue("Name", shipDef.name) + drawKeyValue("Manufacturer", shipDef.manufacturer) + drawKeyValue("Ship Class", shipDef.shipClass) + drawKeyValue("Model", shipDef.modelName) + drawKeyValue("Tag", shipDef.tag) + + drawKeyValue("Cargo Capacity", shipDef.cargo) + drawKeyValue("Equip Capacity", shipDef.capacity) + drawKeyValue("Hull Mass", shipDef.hullMass) + drawKeyValue("Fuel Tank Mass", shipDef.fuelTankMass) + drawKeyValue("Base Price", shipDef.basePrice) + drawKeyValue("Min. Crew", shipDef.minCrew) + drawKeyValue("Max. Crew", shipDef.maxCrew) + + drawKeyValue("Angular Thrust", shipDef.angularThrust) + drawKeyValue("Foward Thrust", shipDef.linearThrust.FORWARD) + drawKeyValue("Reverse Thrust", shipDef.linearThrust.REVERSE) + drawKeyValue("Up Thrust", shipDef.linearThrust.UP) + drawKeyValue("Down Thrust", shipDef.linearThrust.DOWN) + drawKeyValue("Left Thrust", shipDef.linearThrust.LEFT) + drawKeyValue("Right Thrust", shipDef.linearThrust.RIGHT) + drawKeyValue("Exhaust Velocity", shipDef.effectiveExhaustVelocity) + drawKeyValue("Pressure Limit", shipDef.atmosphericPressureLimit) + + drawKeyValue("Front Cross-Section", shipDef.frontCrossSec) + drawKeyValue("Side Cross-Section", shipDef.sideCrossSec) + drawKeyValue("Top Cross-Section", shipDef.topCrossSec) + + ui.endTable() + end +end + +local function drawSlotValue(slot, key) + if slot[key] then + ui.tableNextRow() + + ui.tableNextColumn() + ui.text(key .. ":") + + local val = slot[key] + if type(val) == "number" then + val = ui.Format.Number(val) + end + + ui.tableNextColumn() + ui.text(tostring(val)) + end +end + +---@param slot HullConfig.Slot +function DebugShipTool:drawSlotDetail(slot) + if ui.beginTable("SlotDetails", 2) then + + drawSlotValue(slot, "id") + drawSlotValue(slot, "type") + drawSlotValue(slot, "size") + drawSlotValue(slot, "size_min") + drawSlotValue(slot, "tag") + drawSlotValue(slot, "default") + drawSlotValue(slot, "hardpoint") + drawSlotValue(slot, "count") + + ui.endTable() + end +end + +---@param hull HullConfig +function DebugShipTool:drawHullSlots(hull) + for _, slot in ipairs(hullSlots[hull.id]) do + local open = ui.treeNode(slot.id) + + if slot.i18n_key then + local tl, br = ui.getItemRect() + local name = Lang.GetResource(slot.i18n_res)[slot.i18n_key] + + local pos = Vector2(ui.getCursorScreenPos().x + ui.getContentRegion().x - ui.calcTextSize(name).x - ui.getItemSpacing().x, tl.y) + ui.addText(pos, colors.font, name) + end + + if open then + self:drawSlotDetail(slot) + + ui.treePop() + end + end +end + +function DebugShipTool:drawHullThreat(hull) + local threat = ShipBuilder.GetHullThreat(hull.id) + + if ui.beginTable("Hull Threat", 2) then + for k, v in pairs(threat) do + ui.tableNextRow() + + ui.tableNextColumn() + ui.text(tostring(k)..":") + + ui.tableNextColumn() + ui.text(type(v) == "number" and ui.Format.Number(v) or tostring(v)) + end + + ui.endTable() + end +end + +function DebugShipTool:drawSpawnButtons() + + local threat, dist, changed + + local halfWidth = (ui.getContentRegion().x - ui.getItemSpacing().x) * 0.5 + + -- Template line + + ui.nextItemWidth(halfWidth) + ui.comboBox("##Template", templateOptions[self.spawnTemplateIdx], function() + for idx, option in ipairs(templateOptions) do + if ui.selectable(option, idx == self.spawnTemplateIdx) then + self:message('onSetSpawnTemplate', idx) + end + end + end) + + ui.sameLine() + + ui.nextItemWidth(halfWidth) + threat, changed = ui.sliderFloat("##Threat", self.spawnThreat, 5.0, 300.0, "Threat: %.1f") + + if changed then + self:message('onSetSpawnThreat', threat) + end + + -- Spawn location line + + ui.nextItemWidth(halfWidth) + ui.comboBox("##AICmd", aiOptions[self.aiCmdIdx], function() + for idx, option in ipairs(aiOptions) do + if ui.selectable(option, idx == self.aiCmdIdx) then + self:message('onSetSpawnAICmd', idx) + end + end + end) + + ui.sameLine() + + ui.nextItemWidth(halfWidth) + dist, changed = ui.sliderFloat("##Distance", self.spawnDist, 1.0, 300.0, "Distance: %.1fkm") + + if changed then + self:message('onSetSpawnDistance', dist) + end + + -- Button Line + + local buttonSize = Vector2((ui.getContentRegion().x - ui.getItemSpacing().x * 2) / 3, 0) + + if ui.button("Spawn Ship", buttonSize) then + self:message('onSpawnSelectedHull') + end + + ui.sameLine() + + ui.nextItemWidth(buttonSize.x) + ui.comboBox("##SpawnOption", spawnOptions[self.spawnLocIdx], function() + for idx, option in ipairs(spawnOptions) do + if ui.selectable(option, idx == self.spawnLocIdx) then + self:message('onSetSpawnLocation', idx) + end + end + end) + + ui.sameLine() + + if ui.button("Set Ship Type", buttonSize) then + self:message('onSetPlayerShipType') + end + +end + +function DebugShipTool:drawMissileOptions() + if ui.button("Launch Missile") then + self:message('onSpawnMissile') + end + + ui.sameLine() + + ui.addCursorPos(Vector2(0, (ui.getButtonHeight() - ui.getFrameHeight()) * 0.5)) + + ui.nextItemWidth(ui.getContentRegion().x) + ui.comboBox("##missile", missileOptions[self.missileIdx], function() + for idx, option in ipairs(missileOptions) do + if ui.selectable(option, idx == self.missileIdx) then + self:message('onSetMissileIdx', idx) + end + end + end) +end + +function DebugShipTool:drawTeleportOptions() + ---@type SystemPath + local hyperspaceTarget = Game.sectorView:GetSelectedSystemPath() + local navTarget = Game.player:GetNavTarget() + + ui.horizontalGroup(function() + + if navTarget and navTarget:isa("SpaceStation") then + if ui.button("Dock with {}" % { navTarget.label }) then + self:message('onSetDockedWith', navTarget) + end + end + + if Game.system and hyperspaceTarget and not hyperspaceTarget:IsSameSystem(Game.system.path) then + if ui.button("Hyperjump to {}" % { hyperspaceTarget:GetStarSystem().name }) then + self:message('onHyperjumpTo', hyperspaceTarget) + end + end + + end) +end + +---@param ship Ship +function DebugShipTool:drawShipEquipment(ship) + local equipSet = ship:GetComponent('EquipSet') + + if ui.beginTable("shipEquip", 2) then + + for id, equip in pairs(equipSet:GetInstalledEquipment()) do + drawKeyValue(id, equip:GetName()) + end + + ui.endTable() + end + + ui.spacing() +end + +function DebugShipTool:render() + self:drawShipSelector(self.selectedHullIdx) + + if self.selectedHullIdx then + + local buttonRowHeight = ui.getFrameHeightWithSpacing() * 2 + ui.getButtonHeightWithSpacing() + + ui.child("innerScroll", Vector2(0, -buttonRowHeight), function() + local hull = shipList[self.selectedHullIdx] + + if ui.collapsingHeader("ShipDef") then + self:drawShipDefInfo(ShipDef[hull.id]) + end + + if ui.collapsingHeader("Hull Slots") then + self:drawHullSlots(hull) + end + + if ui.collapsingHeader("Hull Threat") then + self:drawHullThreat(hull) + end + end) + + self:drawSpawnButtons() + + else + + ui.separator() + + if not Game.player:IsDocked() then + self:drawMissileOptions() + + self:drawTeleportOptions() + end + + local target = Game.player:GetCombatTarget() + + if target and target:isa('Ship') then + + ---@cast target Ship + + ui.text("{} ({}) | Equipment:" % { target.label, ShipDef[target.shipId].name }) + + ui.child("equipScroll", Vector2(0, 0), function() + + self:drawShipEquipment(target) + + end) + + end + + end +end + +--============================================================================= + +debugView.registerTab("DebugShip", { + label = "Ship Debug", + icon = icons.ship, + debugUI = DebugShipTool.New(), + show = function() return Game.player and not Game:InHyperspace() end, + draw = function(self) + if not shipList then + buildShipList() + end + + self.debugUI:update() + self.debugUI:render() + end +}) From 89d97ae594be38820a13daf09cc1048a45e1a078 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 28 Sep 2024 14:17:54 -0400 Subject: [PATCH 096/119] EquipSet: reset ship capacity on ShipType change --- data/libs/EquipSet.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/libs/EquipSet.lua b/data/libs/EquipSet.lua index 08e66d6d26..00a5f785d0 100644 --- a/data/libs/EquipSet.lua +++ b/data/libs/EquipSet.lua @@ -137,8 +137,9 @@ function EquipSet:OnShipTypeChanged() assert(#self.installed == 0, "Missed some equipment while cleaning the ship") - -- HullConfig changed, need to rebuild list of slots + -- HullConfig changed, need to reset volume and rebuild list of slots self.config = HullConfig.GetHullConfigs()[self.ship.shipId] + self.ship:setprop("totalVolume", self.config.capacity) self.slotCache = {} self.idCache = {} From 68baf80bf0787676aa8a5a6d8cd267796b42f026 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 28 Sep 2024 19:45:02 -0400 Subject: [PATCH 097/119] Equipment: fix incorrect serializer class names --- data/libs/EquipType.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index 72f6bc9781..ea71b2a8a5 100644 --- a/data/libs/EquipType.lua +++ b/data/libs/EquipType.lua @@ -500,11 +500,11 @@ local ThrusterType = utils.inherits(EquipType, "Equipment.ThrusterType") --============================================================================== -Serializer:RegisterClass("LaserType", LaserType) Serializer:RegisterClass("EquipType", EquipType) -Serializer:RegisterClass("HyperdriveType", HyperdriveType) -Serializer:RegisterClass("SensorType", SensorType) -Serializer:RegisterClass("BodyScannerType", BodyScannerType) +Serializer:RegisterClass("Equipment.LaserType", LaserType) +Serializer:RegisterClass("Equipment.HyperdriveType", HyperdriveType) +Serializer:RegisterClass("Equipment.SensorType", SensorType) +Serializer:RegisterClass("Equipment.BodyScannerType", BodyScannerType) Serializer:RegisterClass("Equipment.CabinType", CabinType) Serializer:RegisterClass("Equipment.ThrusterType", ThrusterType) From 1f2f19da83690de600be08ceceb44063cfcfca36 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 28 Sep 2024 19:48:40 -0400 Subject: [PATCH 098/119] Restat equipment for new size classes - Set up S1-S5 shield generators - Stat hyperdrives for S1-S5 (and S6/S7 though they won't be used on flyable ships) - Separate scoops by cargo/fuel/generic slots - Interim missile definitions until missiles are reworked - Add missile rails/racks for external missile carriage - Add internal missile bay equipment for Bowfin --- data/lang/equipment-core/en.json | 188 ++++++++++++++++++-------- data/modules/Equipment/Hyperdrive.lua | 55 ++++---- data/modules/Equipment/Internal.lua | 36 ++++- data/modules/Equipment/Weapons.lua | 128 ++++++++++++++++-- 4 files changed, 302 insertions(+), 105 deletions(-) diff --git a/data/lang/equipment-core/en.json b/data/lang/equipment-core/en.json index b9b9a83f48..a9680487a6 100644 --- a/data/lang/equipment-core/en.json +++ b/data/lang/equipment-core/en.json @@ -448,123 +448,191 @@ "message": "Weapons" }, "THRUSTERS_DEFAULT": { - "description": "", - "message": "Default Thrusters" + "description": "", + "message": "Default Thrusters" }, "PROJECTILE_SPEED": { - "description": "Stat name for weapon projectile speeds", - "message": "Projectile Speed" + "description": "Stat name for weapon projectile speeds", + "message": "Projectile Speed" }, "STAT_VOLUME": { - "description": "", - "message": "Volume" + "description": "", + "message": "Volume" }, "STAT_WEIGHT": { - "description": "", - "message": "Weight" + "description": "", + "message": "Weight" }, "STAT_POWER_DRAW": { - "description": "", - "message": "Power Draw" + "description": "", + "message": "Power Draw" }, "COMPUTER_MODULES": { - "description": "Category name of computer-related equipment", - "message": "Computer Modules" + "description": "Category name of computer-related equipment", + "message": "Computer Modules" }, "HULL_MOUNTS": { - "description": "Category name for hull-mounted equipment", - "message": "Hull Mounts" + "description": "Category name for hull-mounted equipment", + "message": "Hull Mounts" }, - "HARDPOINT_LASER_FRONT": { - "description": "Name for the 'Laser Front' equipment hardpoint", - "message": "Laser Front" + "HARDPOINT_CARGO_SCOOP": { + "description": "Name for the 'Cargo Scoop' equipment hardpoint", + "message": "Cargo Scoop" + }, + "HARDPOINT_FUEL_SCOOP": { + "description": "Name for the 'Fuel Scoop' equipment hardpoint", + "message": "Fuel Scoop" }, "HARDPOINT_MISSILE": { - "description": "", - "message": "Missile" + "description": "Name for a generic missile mount", + "message": "Missile" + }, + "HARDPOINT_PYLON": { + "description": "Name for a generic missile pylon", + "message": "Pylon" }, "HARDPOINT_MISSILE_BAY": { - "description": "Name for the 'Missile Bay' equipment hardpoint", - "message": "Missile Bay" + "description": "Name for the 'Missile Bay' equipment hardpoint", + "message": "Missile Bay" + }, + "HARDPOINT_MISSILE_ARRAY": { + "description": "Name for the 'Missile Array' equipment hardpoint", + "message": "Missile Array" + }, + "HARDPOINT_SCOOP": { + "description": "Name for a generic scoop hardpoint", + "message": "Scoop" }, "HARDPOINT_UTILITY": { - "description": "", - "message": "Utility" + "description": "", + "message": "Utility" + }, + "HARDPOINT_WEAPON_FRONT": { + "description": "Name for the 'Laser Front' equipment hardpoint", + "message": "Front Weapon" + }, + "HARDPOINT_WEAPON_CHIN": { + "description": "Name for the 'Laser Chin' equipment hardpoint", + "message": "Chin Weapon" + }, + "HARDPOINT_WEAPON_LEFT_NOSE": { + "description": "", + "message": "Left Nose" + }, + "HARDPOINT_WEAPON_RIGHT_NOSE": { + "description": "", + "message": "Right Nose" }, "SLOT_SENSOR": { - "description": "", - "message": "Sensor" + "description": "", + "message": "Sensor" }, "SLOT_CABIN": { - "description": "", - "message": "Cabin" + "description": "", + "message": "Cabin" }, "SLOT_COMPUTER": { - "description": "", - "message": "Computer" + "description": "", + "message": "Computer" }, "SLOT_SHIELD": { - "description": "", - "message": "Shield" + "description": "", + "message": "Shield" }, "SLOT_SHIELD_LEFT": { - "description": "", - "message": "Shield L" + "description": "", + "message": "Shield L" }, "SLOT_SHIELD_RIGHT": { - "description": "", - "message": "Shield R" + "description": "", + "message": "Shield R" }, "SLOT_HULL": { - "description": "", - "message": "Hull" + "description": "", + "message": "Hull" }, "SLOT_HYPERDRIVE": { - "description": "", - "message": "Hyperdrive" + "description": "", + "message": "Hyperdrive" }, "SLOT_ENGINE": { - "description": "", - "message": "Engines" + "description": "", + "message": "Engines" }, "SLOT_THRUSTER": { - "description": "", - "message": "Thrusters" + "description": "", + "message": "Thrusters" + }, + "SLOT_CABIN_FORE": { + "description": "", + "message": "Fore Cabin" + }, + "SLOT_CABIN_REAR": { + "description": "", + "message": "Rear Cabin" }, "MISC_EQUIPMENT": { - "description": "Category header for miscellaneous equipment", - "message": "Miscellaneous" + "description": "Category header for miscellaneous equipment", + "message": "Miscellaneous" }, "SORT_NAME": { - "description": "", - "message": "Name" + "description": "", + "message": "Name" }, "SORT_WEIGHT": { - "description": "", - "message": "Weight" + "description": "", + "message": "Weight" }, "SORT_VOLUME": { - "description": "", - "message": "Volume" + "description": "", + "message": "Volume" }, "SORT_PRICE": { - "description": "", - "message": "Price" + "description": "", + "message": "Price" }, "OPLI_INTERNAL_MISSILE_RACK_S2": { - "description": "", - "message": "OPLI Internal Missile Rack" + "description": "", + "message": "OPLI Internal Missile Rack" + }, + "OKB_KALURI_BOWFIN_MISSILE_RACK": { + "description": "", + "message": "Bowfin Missile Launcher" }, "CABINS": { - "description": "Category header for pressurized cabin slots", - "message": "Cabins" + "description": "Category header for pressurized cabin slots", + "message": "Cabins" }, "PASSENGER_BERTHS": { - "description": "", - "message": "Passenger Berths" + "description": "", + "message": "Passenger Berths" }, "OCCUPIED_BERTHS": { + "description": "", + "message": "Occupied Berths" + }, + "MISSILE_RAIL_S1": { + "description": "", + "message": "Cnida-101 Missile Rail" + }, + "MISSILE_RAIL_S2": { + "description": "", + "message": "Cnida-102 Missile Rail" + }, + "MISSILE_RAIL_S3": { + "description": "", + "message": "Cnida-103 Missile Rail" + }, + "MISSILE_RACK_341": { + "description": "", + "message": "LH-140 Hydri Missile Rack" + }, + "MISSILE_RACK_322": { + "description": "", + "message": "LH-230 Hydri Missile Rack" + }, + "MISSILE_RACK_221": { "description": "", - "message": "Occupied Berths" + "message": "LH-120 Hydri Missile Rack" } } diff --git a/data/modules/Equipment/Hyperdrive.lua b/data/modules/Equipment/Hyperdrive.lua index 128b4f5276..b3673326af 100644 --- a/data/modules/Equipment/Hyperdrive.lua +++ b/data/modules/Equipment/Hyperdrive.lua @@ -6,69 +6,68 @@ local Commodities = require 'Commodities' local HyperdriveType = require 'EquipType'.HyperdriveType +-- +-- Civilian Drives +-- + +-- Player-flyable ships Equipment.Register("hyperspace.hyperdrive_1", HyperdriveType.New { l10n_key="DRIVE_CLASS1", fuel=Commodities.hydrogen, slot = { type="hyperdrive.civilian", size=1 }, - mass=2, volume=2, capabilities={ hyperclass=1 }, - price=700, purchasable=true, tech_level=3, + mass=1.4, volume=2.5, capabilities={ hyperclass=1 }, + price=1700, purchasable=true, tech_level=3, icon_name="equip_hyperdrive" }) Equipment.Register("hyperspace.hyperdrive_2", HyperdriveType.New { l10n_key="DRIVE_CLASS2", fuel=Commodities.hydrogen, slot = { type="hyperdrive.civilian", size=2 }, - mass=6, volume=6, capabilities={ hyperclass=2 }, - price=1300, purchasable=true, tech_level=4, + mass=4, volume=6, capabilities={ hyperclass=2 }, + price=2300, purchasable=true, tech_level=4, icon_name="equip_hyperdrive" }) Equipment.Register("hyperspace.hyperdrive_3", HyperdriveType.New { l10n_key="DRIVE_CLASS3", fuel=Commodities.hydrogen, slot = { type="hyperdrive.civilian", size=3 }, - mass=11, volume=11, capabilities={ hyperclass=3 }, - price=2500, purchasable=true, tech_level=4, + mass=9.5, volume=15, capabilities={ hyperclass=3 }, + price=7500, purchasable=true, tech_level=4, icon_name="equip_hyperdrive" }) Equipment.Register("hyperspace.hyperdrive_4", HyperdriveType.New { l10n_key="DRIVE_CLASS4", fuel=Commodities.hydrogen, slot = { type="hyperdrive.civilian", size=4 }, - mass=25, volume=25, capabilities={ hyperclass=4 }, - price=5000, purchasable=true, tech_level=5, + mass=25, volume=40, capabilities={ hyperclass=4 }, + price=21000, purchasable=true, tech_level=6, icon_name="equip_hyperdrive" }) Equipment.Register("hyperspace.hyperdrive_5", HyperdriveType.New { l10n_key="DRIVE_CLASS5", fuel=Commodities.hydrogen, slot = { type="hyperdrive.civilian", size=5 }, - mass=60, volume=60, capabilities={ hyperclass=5 }, - price=10000, purchasable=true, tech_level=5, + mass=76, volume=120, capabilities={ hyperclass=5 }, + price=68000, purchasable=true, tech_level=7, icon_name="equip_hyperdrive" }) + +-- Small bulk-ship jumpdrive Equipment.Register("hyperspace.hyperdrive_6", HyperdriveType.New { l10n_key="DRIVE_CLASS6", fuel=Commodities.hydrogen, slot = { type="hyperdrive.civilian", size=6 }, - mass=130, volume=130, capabilities={ hyperclass=6 }, - price=20000, purchasable=true, tech_level=6, + mass=152, volume=340, capabilities={ hyperclass=6 }, + price=129000, purchasable=true, tech_level=7, icon_name="equip_hyperdrive" }) +-- Large bulk-ship jumpdrive Equipment.Register("hyperspace.hyperdrive_7", HyperdriveType.New { l10n_key="DRIVE_CLASS7", fuel=Commodities.hydrogen, slot = { type="hyperdrive.civilian", size=7 }, - mass=245, volume=245, capabilities={ hyperclass=7 }, - price=30000, purchasable=true, tech_level=8, - icon_name="equip_hyperdrive" -}) -Equipment.Register("hyperspace.hyperdrive_8", HyperdriveType.New { - l10n_key="DRIVE_CLASS8", fuel=Commodities.hydrogen, - slot = { type="hyperdrive.civilian", size=8 }, - mass=360, volume=360, capabilities={ hyperclass=8 }, - price=60000, purchasable=true, tech_level=9, - icon_name="equip_hyperdrive" -}) -Equipment.Register("hyperspace.hyperdrive_9", HyperdriveType.New { - l10n_key="DRIVE_CLASS9", fuel=Commodities.hydrogen, - slot = { type="hyperdrive.civilian", size=9 }, - mass=540, volume=540, capabilities={ hyperclass=9 }, - price=120000, purchasable=true, tech_level=10, + mass=540, volume=960, capabilities={ hyperclass=7 }, + price=341000, purchasable=true, tech_level=9, icon_name="equip_hyperdrive" }) + +-- +-- Military Drives (not yet ported) +-- + Equipment.Register("hyperspace.hyperdrive_mil1", HyperdriveType.New { l10n_key="DRIVE_MIL1", fuel=Commodities.military_fuel, byproduct=Commodities.radioactives, slot = { type="hyperdrive.military", size=1 }, diff --git a/data/modules/Equipment/Internal.lua b/data/modules/Equipment/Internal.lua index 28a344058f..33ec62ac2a 100644 --- a/data/modules/Equipment/Internal.lua +++ b/data/modules/Equipment/Internal.lua @@ -47,17 +47,41 @@ Equipment.Register("sensor.radar", SensorType.New { Equipment.Register("shield.basic_s1", EquipType.New { l10n_key="SHIELD_GENERATOR", - price=2500, purchasable=true, tech_level=8, + price=2500, purchasable=true, tech_level=5, slot = { type="shield", size=1 }, - mass=2, volume=1, capabilities = { shield=1 }, + mass=1, volume=2, capabilities = { shield=1 }, icon_name="equip_shield_generator" }) Equipment.Register("shield.basic_s2", EquipType.New { l10n_key="SHIELD_GENERATOR", - price=5500, purchasable=true, tech_level=9, + price=5500, purchasable=true, tech_level=7, slot = { type="shield", size=2 }, - mass=4, volume=2.5, capabilities = { shield=2 }, + mass=2.8, volume=5, capabilities = { shield=2 }, + icon_name="equip_shield_generator" +}) + +Equipment.Register("shield.basic_s3", EquipType.New { + l10n_key="SHIELD_GENERATOR", + price=11500, purchasable=true, tech_level=8, + slot = { type="shield", size=3 }, + mass=7, volume=12.5, capabilities = { shield=3 }, + icon_name="equip_shield_generator" +}) + +Equipment.Register("shield.basic_s4", EquipType.New { + l10n_key="SHIELD_GENERATOR", + price=23500, purchasable=true, tech_level=9, + slot = { type="shield", size=4 }, + mass=17.9, volume=32, capabilities = { shield=4 }, + icon_name="equip_shield_generator" +}) + +Equipment.Register("shield.basic_s5", EquipType.New { + l10n_key="SHIELD_GENERATOR", + price=58500, purchasable=true, tech_level=10, + slot = { type="shield", size=5 }, + mass=43.7, volume=78, capabilities = { shield=5 }, icon_name="equip_shield_generator" }) @@ -132,14 +156,14 @@ Equipment.Register("misc.thrusters_best", ThrusterType.New { Equipment.Register("misc.fuel_scoop", EquipType.New { l10n_key="FUEL_SCOOP", price=3500, purchasable=true, tech_level=4, - slot = { type="scoop", size=1, hardpoint=true }, + slot = { type="scoop.fuel", size=1, hardpoint=true }, mass=6, volume=4, capabilities={ fuel_scoop=3 }, icon_name="equip_fuel_scoop" }) Equipment.Register("misc.cargo_scoop", EquipType.New { l10n_key="CARGO_SCOOP", price=3900, purchasable=true, tech_level=5, - slot = { type="scoop", size=1, hardpoint=true }, + slot = { type="scoop.cargo", size=1, hardpoint=true }, mass=4, volume=7, capabilities={ cargo_scoop=1 }, icon_name="equip_cargo_scoop" }) diff --git a/data/modules/Equipment/Weapons.lua b/data/modules/Equipment/Weapons.lua index 79aedcd393..3eade59d9d 100644 --- a/data/modules/Equipment/Weapons.lua +++ b/data/modules/Equipment/Weapons.lua @@ -203,26 +203,38 @@ Equipment.Register("missile.unguided_s1", EquipType.New { l10n_key="MISSILE_UNGUIDED", price=30, purchasable=true, tech_level=1, missile_type="missile_unguided", - volume=0, mass=0.1, + 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 { + l10n_key="MISSILE_GUIDED", + price=45, purchasable=true, tech_level=5, + missile_type="missile_guided", + 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 { l10n_key="MISSILE_GUIDED", - price=50, purchasable=true, tech_level=5, + price=60, purchasable=true, tech_level=5, missile_type="missile_guided", - volume=0, mass=0.3, + 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 { l10n_key="MISSILE_SMART", - price=95, purchasable=true, tech_level=10, + price=95, purchasable=true, tech_level=9, missile_type="missile_smart", 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 { l10n_key="MISSILE_NAVAL", price=160, purchasable=true, tech_level="MILITARY", @@ -232,17 +244,111 @@ Equipment.Register("missile.naval_s4", EquipType.New { icon_name="equip_missile_naval" }) -Equipment.Register("missile_rack.opli_internal_s2", EquipType.New { +--=============================================== +-- Missile Pylons +--=============================================== + +Equipment.Register("missile_rack.313", EquipType.New { + l10n_key="MISSILE_RAIL_S3", + price=150, purchasable=true, tech_level=1, + volume=0.0, mass=0.2, + slot = { type = "pylon.rack", size=3, hardpoint=true }, + provides_slots = { + Slot:clone { id = "1", type = "missile", size = 3, hardpoint = true }, + }, + icon_name="equip_missile_unguided" +}) + +Equipment.Register("missile_rack.322", EquipType.New { + l10n_key="MISSILE_RACK_322", + price=150, purchasable=true, tech_level=1, + volume=0.0, mass=0.4, + slot = { type = "pylon.rack", size=3, hardpoint=true }, + provides_slots = { + Slot:clone { id = "1", type = "missile", size = 2, hardpoint = true }, + Slot:clone { id = "2", type = "missile", size = 2, hardpoint = true }, + }, + icon_name="equip_missile_unguided" +}) + +Equipment.Register("missile_rack.341", EquipType.New { + l10n_key="MISSILE_RACK_341", + price=150, purchasable=true, tech_level=1, + volume=0.0, mass=0.5, + slot = { type = "pylon.rack", size=3, hardpoint=true }, + provides_slots = { + Slot:clone { id = "1", type = "missile", size = 1, hardpoint = true }, + Slot:clone { id = "2", type = "missile", size = 1, hardpoint = true }, + Slot:clone { id = "3", type = "missile", size = 1, hardpoint = true }, + Slot:clone { id = "4", type = "missile", size = 1, hardpoint = true }, + }, + icon_name="equip_missile_unguided" +}) + +Equipment.Register("missile_rack.212", EquipType.New { + l10n_key="MISSILE_RAIL_S2", + price=150, purchasable=true, tech_level=1, + volume=0.0, mass=0.1, + slot = { type = "pylon.rack", size=2, hardpoint=true }, + provides_slots = { + Slot:clone { id = "1", type = "missile", size = 2, hardpoint = true }, + }, + icon_name="equip_missile_unguided" +}) + +Equipment.Register("missile_rack.221", EquipType.New { + l10n_key="MISSILE_RACK_221", + price=150, purchasable=true, tech_level=1, + volume=0.0, mass=0.2, + slot = { type = "pylon.rack", size=2, hardpoint=true }, + provides_slots = { + Slot:clone { id = "1", type = "missile", size = 1, hardpoint = true }, + Slot:clone { id = "2", type = "missile", size = 1, hardpoint = true }, + }, + icon_name="equip_missile_unguided" +}) + +Equipment.Register("missile_rack.111", EquipType.New { + l10n_key="MISSILE_RAIL_S1", + price=150, purchasable=true, tech_level=1, + volume=0.0, mass=0.1, + slot = { type = "pylon.rack", size=1, hardpoint=true }, + provides_slots = { + Slot:clone { id = "1", type = "missile", size = 1, hardpoint = true }, + }, + icon_name="equip_missile_unguided" +}) + +--=============================================== +-- Internal Missile Bays +--=============================================== + +Equipment.Register("missile_bay.opli_internal_s2", EquipType.New { l10n_key="OPLI_INTERNAL_MISSILE_RACK_S2", price=150, purchasable=true, tech_level=1, volume=5.0, mass=0.5, - slot = { type = "missile_rack.opli_internal", size=2, hardpoint=true }, + slot = { type = "missile_bay.opli_internal", size=2, hardpoint=true }, + provides_slots = { + Slot:clone { id = "1", type = "missile", size = 2, hardpoint = true }, + Slot:clone { id = "2", type = "missile", size = 2, hardpoint = true }, + Slot:clone { id = "3", type = "missile", size = 2, hardpoint = true }, + Slot:clone { id = "4", type = "missile", size = 2, hardpoint = true }, + Slot:clone { id = "5", type = "missile", size = 2, hardpoint = true }, + }, + icon_name="equip_missile_unguided" +}) + +Equipment.Register("missile_bay.bowfin", EquipType.New { + l10n_key="OKB_KALURI_BOWFIN_MISSILE_RACK", + price=150, purchasable=true, tech_level=1, + volume=0.0, mass=0.2, + slot = { type = "missile_bay.bowfin", size=2, hardpoint=true }, provides_slots = { - Slot:clone { id = "1", type = "missile", size = 2, size_min = 1, i18n_key = "HARDPOINT_MISSILE" }, - Slot:clone { id = "2", type = "missile", size = 2, size_min = 1, i18n_key = "HARDPOINT_MISSILE" }, - Slot:clone { id = "3", type = "missile", size = 2, size_min = 1, i18n_key = "HARDPOINT_MISSILE" }, - Slot:clone { id = "4", type = "missile", size = 2, size_min = 1, i18n_key = "HARDPOINT_MISSILE" }, - Slot:clone { id = "5", type = "missile", size = 2, size_min = 1, i18n_key = "HARDPOINT_MISSILE" }, + Slot:clone { id = "1", type = "missile", size = 1, hardpoint = true }, + Slot:clone { id = "2", type = "missile", size = 1, hardpoint = true }, + Slot:clone { id = "3", type = "missile", size = 1, hardpoint = true }, + Slot:clone { id = "4", type = "missile", size = 1, hardpoint = true }, + Slot:clone { id = "5", type = "missile", size = 1, hardpoint = true }, }, icon_name="equip_missile_unguided" }) From 058d4060308a5ea607a9fc6715afab34faa5acec Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 28 Sep 2024 19:48:55 -0400 Subject: [PATCH 099/119] ui: update for equipment changes --- data/pigui/libs/ship-equipment.lua | 2 +- data/pigui/modules/new-game-window/class.lua | 20 +++++++++---------- .../modules/station-view/04-shipMarket.lua | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/data/pigui/libs/ship-equipment.lua b/data/pigui/libs/ship-equipment.lua index 0949703548..bf92ec573d 100644 --- a/data/pigui/libs/ship-equipment.lua +++ b/data/pigui/libs/ship-equipment.lua @@ -36,7 +36,7 @@ local EquipmentWidget = utils.class("UI.EquipmentWidget", Module) EquipmentWidget.Sections = { { name = le.PROPULSION, types = { "engine", "thruster", "hyperdrive" } }, { name = le.WEAPONS, type = "weapon" }, - { name = le.MISSILES, types = { "pylon", "missile_rack" } }, + { name = le.MISSILES, types = { "pylon", "missile_bay", "missile" } }, { name = le.SHIELDS, type = "shield" }, { name = le.SENSORS, type = "sensor", }, { name = le.COMPUTER_MODULES, type = "computer", }, diff --git a/data/pigui/modules/new-game-window/class.lua b/data/pigui/modules/new-game-window/class.lua index 4b201df152..476f12265b 100644 --- a/data/pigui/modules/new-game-window/class.lua +++ b/data/pigui/modules/new-game-window/class.lua @@ -25,16 +25,16 @@ local Game = require 'Game' local profileCombo = { items = {}, selected = 0 } local equipment2 = { - computer_1 = "misc.autopilot", - laser_front = "laser.pulsecannon_1mw", - shield_1 = "shield.basic_s1", - shield_2 = "shield.basic_s1", - sensor = "sensor.radar", - hull_mod = "hull.atmospheric_shielding", - hyperdrive = "hyperspace.hyperdrive_2", - thruster = "misc.thrusters_default", - missile_bay_1 = "missile_rack.opli_internal_s2", - missile_bay_2 = "missile_rack.opli_internal_s2", + computer_1 = "misc.autopilot", + laser_front_s2 = "laser.pulsecannon_1mw", + shield_s1_1 = "shield.basic_s1", + shield_s1_2 = "shield.basic_s1", + sensor = "sensor.radar", + hull_mod = "hull.atmospheric_shielding", + hyperdrive = "hyperspace.hyperdrive_2", + thruster = "misc.thrusters_default", + missile_bay_1 = "missile_bay.opli_internal_s2", + missile_bay_2 = "missile_bay.opli_internal_s2", } StartVariants.register({ diff --git a/data/pigui/modules/station-view/04-shipMarket.lua b/data/pigui/modules/station-view/04-shipMarket.lua index 2f3a88fbf9..568509da82 100644 --- a/data/pigui/modules/station-view/04-shipMarket.lua +++ b/data/pigui/modules/station-view/04-shipMarket.lua @@ -242,7 +242,7 @@ end function FormatAndCompareShips:draw_hyperdrive_cell(desc) local function fmt( v ) - return v > 0 and + return v > 0 and v < 8 and Equipment.new["hyperspace.hyperdrive_" .. v]:GetName() or l.NONE end From e049417b8ade459e19ad99b563c21903bae64099 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 28 Sep 2024 21:50:14 -0400 Subject: [PATCH 100/119] Add equipment slots to all S1 ships - Bowfin, Coronatrix, Lunar Shuttle, Mola Mola, Pumpkinseed - Most ships increased in hull mass overall - Structure and Armor mass are enumerated separately for future work - Many ships expect an internal jump fuel tank integral to the hyperdrive and do not have extra cargo space for jump fuel --- data/ships/bowfin.json | 123 ++++++++++++++++------------- data/ships/coronatrix.json | 89 +++++++++++---------- data/ships/coronatrix_police.json | 87 +++++++++++++++++--- data/ships/lunarshuttle.json | 74 ++++++++++++----- data/ships/molamola.json | 77 +++++++++++++++--- data/ships/pumpkinseed.json | 97 +++++++++++++++++++---- data/ships/pumpkinseed_police.json | 101 +++++++++++++++++++---- 7 files changed, 475 insertions(+), 173 deletions(-) diff --git a/data/ships/bowfin.json b/data/ships/bowfin.json index d3b22e2810..db1c0557d9 100644 --- a/data/ships/bowfin.json +++ b/data/ships/bowfin.json @@ -7,82 +7,95 @@ "ship_class": "light_fighter", "min_crew": 1, "max_crew": 1, - "price": 34430, - "hull_mass": 25, + "price": 48674.18056474379, + "hull_mass": 19, + "structure_mass": 7.8, + "armor_mass": 11.6, + "volume": 135, "atmospheric_pressure_limit": 6.5, - "capacity": 12, - "cargo": 6, - "slots": { - "engine": 1, - "cabin": 0, - "scoop": 0, - "laser_front": 1, - "missile": 24, - "cargo": 6 - }, - "roles": ["mercenary", "pirate", "courier"], + "capacity": 15, + "cargo": 4, + "equipment_slots": { - "laser_front": { - "type": "weapon", + "missile_bay_1": { + "type": "missile_bay.bowfin_internal", "size": 2, - "size_min": 1, + "i18n_key": "HARDPOINT_MISSILE_ARRAY", + "tag": "tag_missile_bay_1", "hardpoint": true, - "tag": "tag_laser_front", - "gimbal": [15, 15], - "default": "laser.pulsecannon_1mw" + "default": "missile_rack.bowfin_internal_s2" }, - "missile_tl": { - "type": "missile", + "missile_bay_2": { + "type": "missile_bay.bowfin_internal", "size": 2, - "size_min": 1, - "count": 6, + "i18n_key": "HARDPOINT_MISSILE_ARRAY", + "tag": "tag_missile_bay_2", "hardpoint": true, - "tag": "tag_missile_tl", - "default": "missile.guided_s2" + "default": "missile_rack.bowfin_internal_s2" }, - "missile_tr": { - "type": "missile", + "missile_bay_3": { + "type": "missile_bay.bowfin_internal", "size": 2, - "size_min": 1, - "count": 6, + "i18n_key": "HARDPOINT_MISSILE_ARRAY", + "tag": "tag_missile_bay_3", "hardpoint": true, - "tag": "tag_missile_tr", - "default": "missile.guided_s2" + "default": "missile_rack.bowfin_internal_s2" }, - "missile_bl": { - "type": "missile", + "missile_bay_4": { + "type": "missile_bay.bowfin_internal", "size": 2, - "size_min": 1, - "count": 6, + "i18n_key": "HARDPOINT_MISSILE_ARRAY", + "tag": "tag_missile_bay_4", "hardpoint": true, - "tag": "tag_missile_bl", - "default": "missile.guided_s2" + "default": "missile_rack.bowfin_internal_s2" }, - "missile_br": { - "type": "missile", + "shield_s1_1": { + "type": "shield", + "size": 1 + }, + "shield_s1_2": { + "type": "shield", + "size": 1 + }, + "utility_s1_1": { + "type": "utility", + "size": 1, + "tag": "tag_utility_s1_1", + "hardpoint": true + }, + "computer_2": { + "type": "computer", + "size": 1 + }, + "laser_chin": { + "type": "weapon", "size": 2, "size_min": 1, - "count": 6, + "i18n_key": "HARDPOINT_WEAPON_CHIN", + "tag": "tag_laser_chin", "hardpoint": true, - "tag": "tag_missile_br", - "default": "missile.guided_s2" + "gimbal": [8,8] }, - "shield_gen_1": { - "type": "shield", + "hyperdrive": { + "type": "hyperdrive", "size": 1, - "i18n_key": "SLOT_SHIELD_LEFT", - "default": "shield.basic_s1" + "size_min": 1 }, - "shield_gen_2": { - "type": "shield", + "engine": { + "type": "engine", + "size": 2, + "count": 2 + }, + "thruster": { + "type": "thruster", "size": 1, - "i18n_key": "SLOT_SHIELD_RIGHT", - "default": "shield.basic_s1" + "count": 12 } }, + "roles": ["pirate","mercenary","police"], "effective_exhaust_velocity": 8400000, "thruster_fuel_use": -1.0, - "fuel_tank_mass": 26, + "fuel_tank_mass": 20, "hyperdrive_class": 1, "forward_thrust": 2000000, @@ -90,7 +103,7 @@ "reverse_thrust": 1250000, "reverse_acceleration_cap": 24.525, "up_thrust": 1300000, - "up_acceleration_cap": 29.43, + "up_acceleration_cap": 24.525, "down_thrust": 1000000, "down_acceleration_cap": 24.525, "left_thrust": 1000000, @@ -98,7 +111,7 @@ "right_thrust": 1000000, "right_acceleration_cap": 24.525, - "angular_thrust": 4648782.6507229, + "angular_thrust": 4648782.65, "front_cross_section": 33, "side_cross_section": 27, @@ -108,6 +121,6 @@ "side_drag_coeff": 0.78, "top_drag_coeff": 0.92, - "lift_coeff": 0.42, - "aero_stability": 0.5 + "lift_coeff": 0.21, + "aero_stability": 0.8 } diff --git a/data/ships/coronatrix.json b/data/ships/coronatrix.json index e319639c18..4694a102eb 100644 --- a/data/ships/coronatrix.json +++ b/data/ships/coronatrix.json @@ -7,72 +7,77 @@ "ship_class": "light_courier", "min_crew": 1, "max_crew": 1, - "price": 46614, - "hull_mass": 18, + "price": 46580.34, + "hull_mass": 19, + "structure_mass": 7.1, + "armor_mass": 11.9, + "volume": 190, "atmospheric_pressure_limit": 3.2, - "capacity": 32, + "capacity": 36, "cargo": 16, - "slots": { - "engine": 1, - "cabin": 1, - "laser_front": 1, - "missile": 10, - "shield": 3, - "cargo": 16 - }, + "equipment_slots": { - "laser_front": { - "type": "weapon", - "size": 2, - "size_min": 1, - "i18n_key": "HARDPOINT_LASER_FRONT", - "tag": "tag_laser_front", - "gimbal": [ 5, 5 ], + "utility_s1_2": { + "type": "utility", + "size": 1, + "tag": "tag_utility_s1_2", "hardpoint": true }, "missile_bay_1": { - "type": "missile_rack.opli_internal", + "type": "missile_bay.opli_internal", "size": 2, "i18n_key": "HARDPOINT_MISSILE_BAY", - "tag": "tag_missile_launch", + "tag": "tag_missile_bay_1", "hardpoint": true, "default": "missile_rack.opli_internal_s2" }, "missile_bay_2": { - "type": "missile_rack.opli_internal", + "type": "missile_bay.opli_internal", "size": 2, "i18n_key": "HARDPOINT_MISSILE_BAY", - "tag": "tag_missile_launch", + "tag": "tag_missile_bay_2", "hardpoint": true, "default": "missile_rack.opli_internal_s2" }, - "utility_1": { - "type": "utility", + "fuel_scoop_s1": { + "type": "scoop.fuel", "size": 1, - "tag": "tag_utility_1", + "i18n_key": "HARDPOINT_FUEL_SCOOP", + "tag": "tag_fuel_scoop_s1", "hardpoint": true }, - "utility_2": { + "utility_s1_1": { "type": "utility", "size": 1, - "tag": "tag_utility_2", + "tag": "tag_utility_s1_1", "hardpoint": true }, + "shield_s1_1": { + "type": "shield", + "size": 1 + }, + "shield_s1_2": { + "type": "shield", + "size": 1 + }, + "computer_2": { + "type": "computer", + "size": 1 + }, + "laser_front_s2": { + "type": "weapon", + "size": 2, + "size_min": 1, + "i18n_key": "HARDPOINT_WEAPON_FRONT", + "tag": "tag_laser_front_s2", + "hardpoint": true, + "gimbal": [4,4] + }, "hyperdrive": { "type": "hyperdrive", "size": 2, "size_min": 1 }, - "shield_1": { - "type": "shield", - "size": 1, - "tag": "tag_shield_1" - }, - "shield_2": { - "type": "shield", - "size": 1, - "tag": "tag_shield_2" - }, "engine": { "type": "engine", "size": 2, @@ -81,17 +86,17 @@ "thruster": { "type": "thruster", "size": 1, - "count": 12 + "count": 16 } }, - "roles": ["mercenary", "merchant", "pirate"], + "roles": ["pirate","mercenary"], "effective_exhaust_velocity": 12600000, "thruster_fuel_use": -1.0, "fuel_tank_mass": 23, - "hyperdrive_class": 2, + "hyperdrive_class": 1, "forward_thrust": 1800000, - "forward_acceleration_cap": 26.487, + "forward_acceleration_cap": 40.221, "reverse_thrust": 920000, "reverse_acceleration_cap": 20.601, "up_thrust": 1400000, @@ -103,7 +108,7 @@ "right_thrust": 920000, "right_acceleration_cap": 11.4777, - "angular_thrust": 3799843.41896781, + "angular_thrust": 3799843.42, "front_cross_section": 16.2, "side_cross_section": 39, diff --git a/data/ships/coronatrix_police.json b/data/ships/coronatrix_police.json index 58b9a3f937..344a08a0dc 100644 --- a/data/ships/coronatrix_police.json +++ b/data/ships/coronatrix_police.json @@ -8,25 +8,88 @@ "min_crew": 1, "max_crew": 1, "price": 0, - "hull_mass": 18, + "hull_mass": 19, + "structure_mass": 7.1, + "armor_mass": 11.9, + "volume": 190, "atmospheric_pressure_limit": 3.2, - "capacity": 30, - "slots": { - "engine": 1, - "cabin": 1, - "laser_front": 1, - "missile": 10, - "shield": 3, - "cargo": 16 + "capacity": 36, + "cargo": 16, + + "equipment_slots": { + "utility_s1_1": { + "type": "utility", + "size": 1, + "tag": "tag_utility_s1_1", + "hardpoint": true + }, + "utility_s1_2": { + "type": "utility", + "size": 1, + "tag": "tag_utility_s1_2", + "hardpoint": true + }, + "missile_bay_1": { + "type": "missile_bay.opli_internal", + "size": 2, + "i18n_key": "HARDPOINT_MISSILE_BAY", + "tag": "tag_missile_bay_1", + "hardpoint": true, + "default": "missile_rack.opli_internal_s2" + }, + "missile_bay_2": { + "type": "missile_bay.opli_internal", + "size": 2, + "i18n_key": "HARDPOINT_MISSILE_BAY", + "tag": "tag_missile_bay_2", + "hardpoint": true, + "default": "missile_rack.opli_internal_s2" + }, + "computer_2": { + "type": "computer", + "size": 1 + }, + "laser_front_s2": { + "type": "weapon", + "size": 2, + "size_min": 1, + "i18n_key": "HARDPOINT_WEAPON_FRONT", + "tag": "tag_laser_front_s2", + "hardpoint": true, + "gimbal": [4,4] + }, + "shield_s1_1": { + "type": "shield", + "size": 1 + }, + "shield_s1_2": { + "type": "shield", + "size": 1 + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 2, + "size_min": 1 + }, + "engine": { + "type": "engine", + "size": 2, + "count": 2 + }, + "thruster": { + "type": "thruster", + "size": 1, + "count": 12 + } }, - "roles": ["mercenary"], + "roles": ["mercenary","police"], "effective_exhaust_velocity": 12600000, "thruster_fuel_use": -1.0, "fuel_tank_mass": 23, "hyperdrive_class": 2, "forward_thrust": 1800000, - "forward_acceleration_cap": 26.487, + "forward_acceleration_cap": 42.183, "reverse_thrust": 920000, "reverse_acceleration_cap": 20.601, "up_thrust": 1400000, @@ -38,7 +101,7 @@ "right_thrust": 920000, "right_acceleration_cap": 11.4777, - "angular_thrust": 3799843.41896781, + "angular_thrust": 3799843.42, "front_cross_section": 16.2, "side_cross_section": 39, diff --git a/data/ships/lunarshuttle.json b/data/ships/lunarshuttle.json index cedab2c241..d572337d31 100644 --- a/data/ships/lunarshuttle.json +++ b/data/ships/lunarshuttle.json @@ -4,33 +4,67 @@ "cockpit": "", "shield_model": "lunarshuttle_shield", "manufacturer": "haber", - "ship_class": "medium_passenger_shuttle", + "ship_class": "light_passenger_shuttle", "min_crew": 1, - "max_crew": 4, - "price": 35002, - "hull_mass": 30, - "atmospheric_pressure_limit": 3.5, - "capacity": 30, - "slots": { - "engine": 1, - "cabin": 20, - "laser_front": 1, - "missile": 1, - "cargo": 30 + "max_crew": 1, + "price": 16224, + "hull_mass": 14, + "structure_mass": 8.7, + "armor_mass": 5.6, + "volume": 310, + "atmospheric_pressure_limit": 2.5, + "capacity": 16, + "cargo": 6, + + "equipment_slots": { + "cabin_s2_fore": { + "type": "cabin", + "size": 2, + "i18n_key": "SLOT_CABIN_FORE" + }, + "cabin_s1_rear2": { + "type": "cabin", + "size": 1, + "i18n_key": "SLOT_CABIN_REAR" + }, + "utility_s1_1": { + "type": "utility", + "size": 1, + "tag": "tag_utility_s1_1", + "hardpoint": true + }, + "shield_s1_1": { + "type": "shield", + "size": 1 + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 1, + "size_min": 1 + }, + "engine": { + "type": "engine", + "size": 2, + "count": 2 + }, + "thruster": { + "type": "thruster", + "size": 1, + "count": 6 + } }, - "roles": ["mercenary", "merchant", "pirate", "courier"], - + "roles": ["passenger"], "effective_exhaust_velocity": 7900000, "thruster_fuel_use": -1.0, - "fuel_tank_mass": 25, - "hyperdrive_class": 0, + "fuel_tank_mass": 26, + "hyperdrive_class": 1, - "forward_thrust": 1400000, + "forward_thrust": 1200000, "forward_acceleration_cap": 18.5409, - "reverse_thrust": 1000000, + "reverse_thrust": 800000, "reverse_acceleration_cap": 17.658, "up_thrust": 1000000, - "up_acceleration_cap": 29.43, + "up_acceleration_cap": 17.658, "down_thrust": 680000, "down_acceleration_cap": 13.734, "left_thrust": 680000, @@ -38,7 +72,7 @@ "right_thrust": 680000, "right_acceleration_cap": 13.734, - "angular_thrust": 2553254.47124319, + "angular_thrust": 2353254.47, "front_cross_section": 31, "side_cross_section": 70, diff --git a/data/ships/molamola.json b/data/ships/molamola.json index 9f9f3007dc..89801689a4 100644 --- a/data/ships/molamola.json +++ b/data/ships/molamola.json @@ -6,20 +6,71 @@ "manufacturer": "kaluri", "ship_class": "light_freighter", "min_crew": 1, - "max_crew": 3, - "price": 81062, - "hull_mass": 27, + "max_crew": 1, + "price": 59029.35, + "hull_mass": 18, + "structure_mass": 11.3, + "armor_mass": 7.1, + "volume": 378, "atmospheric_pressure_limit": 5.1, - "capacity": 84, - "slots": { - "engine": 1, - "cabin": 8, - "laser_front": 1, - "missile": 2, - "cargo": 80 + "capacity": 31, + "cargo": 72, + + "equipment_slots": { + "weapon_left_nose": { + "type": "weapon", + "size": 1, + "i18n_key": "HARDPOINT_WEAPON_LEFT_NOSE", + "tag": "tag_weapon_left_nose", + "hardpoint": true, + "gimbal": [0,0] + }, + "weapon_right_nose": { + "type": "weapon", + "size": 1, + "i18n_key": "HARDPOINT_WEAPON_RIGHT_NOSE", + "tag": "tag_weapon_right_nose", + "hardpoint": true, + "gimbal": [0,0] + }, + "computer_2": { + "type": "computer", + "size": 1 + }, + "cargo_scoop_s1": { + "type": "scoop.cargo", + "size": 1, + "i18n_key": "HARDPOINT_CARGO_SCOOP", + "tag": "tag_cargo_scoop_s1", + "hardpoint": true + }, + "utility_s1_1": { + "type": "utility", + "size": 1, + "tag": "tag_utility_s1_1", + "hardpoint": true + }, + "shield_s1_1": { + "type": "shield", + "size": 1 + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 2, + "size_min": 2 + }, + "engine": { + "type": "engine", + "size": 1, + "count": 4 + }, + "thruster": { + "type": "thruster", + "size": 2, + "count": 12 + } }, - "roles": ["mercenary", "merchant", "pirate", "courier"], - + "roles": ["merchant"], "effective_exhaust_velocity": 13000000, "thruster_fuel_use": -1.0, "fuel_tank_mass": 40, @@ -38,7 +89,7 @@ "right_thrust": 1000000, "right_acceleration_cap": 16.677, - "angular_thrust": 2714173.87069129, + "angular_thrust": 2714173.87, "front_cross_section": 46, "side_cross_section": 75, diff --git a/data/ships/pumpkinseed.json b/data/ships/pumpkinseed.json index 16d57d9e65..343fefd0fe 100644 --- a/data/ships/pumpkinseed.json +++ b/data/ships/pumpkinseed.json @@ -1,28 +1,95 @@ { "model": "pumpkinseed", "name": "Pumpkinseed", - "cockpit": " ", + "cockpit": "", "shield_model": "pumpkinseed_shield", "manufacturer": "kaluri", "ship_class": "light_courier", "min_crew": 1, - "max_crew": 2, - "price": 36151, - "hull_mass": 10, + "max_crew": 1, + "price": 19104, + "hull_mass": 11, + "structure_mass": 4.2, + "armor_mass": 6.6, + "volume": 87, "atmospheric_pressure_limit": 4, - "capacity": 17, - "slots": { - "engine": 1, - "cabin": 3, - "laser_front": 1, - "missile": 4, - "cargo": 10 - }, - "roles": ["mercenary", "merchant", "pirate", "courier"], + "capacity": 14.5, + "cargo": 4, + "equipment_slots": { + "missile_s1_1": { + "type": "missile", + "size": 1, + "tag": "tag_missile_s1_1", + "hardpoint": true + }, + "missile_s1_2": { + "type": "missile", + "size": 1, + "tag": "tag_missile_s1_2", + "hardpoint": true + }, + "missile_s1_3": { + "type": "missile", + "size": 1, + "tag": "tag_missile_s1_3", + "hardpoint": true + }, + "missile_s1_4": { + "type": "missile", + "size": 1, + "tag": "tag_missile_s1_4", + "hardpoint": true + }, + "fuel_scoop_s1": { + "type": "scoop.fuel", + "size": 1, + "i18n_key": "HARDPOINT_FUEL_SCOOP", + "tag": "tag_fuel_scoop_s1", + "hardpoint": true + }, + "cabin_s1_1": { + "type": "cabin", + "size": 1 + }, + "laser_front_s1": { + "type": "weapon", + "size": 1, + "i18n_key": "HARDPOINT_WEAPON_FRONT", + "tag": "tag_laser_front_s1", + "hardpoint": true, + "gimbal": [2,2] + }, + "shield_s1_1": { + "type": "shield", + "size": 1 + }, + "utility_s1_1": { + "type": "utility", + "size": 1, + "tag": "tag_utility_s1_1", + "hardpoint": true + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 1, + "size_min": 1 + }, + "engine": { + "type": "engine", + "size": 2, + "count": 4 + }, + "thruster": { + "type": "thruster", + "size": 1, + "count": 12 + } + }, + "roles": ["pirate","mercenary"], "effective_exhaust_velocity": 15200000, "thruster_fuel_use": -1.0, - "fuel_tank_mass": 9, + "fuel_tank_mass": 10, "hyperdrive_class": 1, "forward_thrust": 1500000, @@ -38,7 +105,7 @@ "right_thrust": 160000, "right_acceleration_cap": 15.9903, - "angular_thrust": 1001276.26323262, + "angular_thrust": 1001276.26, "front_cross_section": 38, "side_cross_section": 45, diff --git a/data/ships/pumpkinseed_police.json b/data/ships/pumpkinseed_police.json index eaa46e2cef..64f81619bd 100644 --- a/data/ships/pumpkinseed_police.json +++ b/data/ships/pumpkinseed_police.json @@ -6,25 +6,94 @@ "manufacturer": "kaluri", "ship_class": "light_fighter", "min_crew": 1, - "max_crew": 2, + "max_crew": 1, "price": 0, - "hull_mass": 8, - "atmospheric_pressure_limit": 5, - "capacity": 17, - "slots": { - "engine": 1, - "cabin": 3, - "scoop": 0, - "laser_front": 1, - "laser_rear": 1, - "missile": 8, - "cargo": 10 + "hull_mass": 17, + "structure_mass": 4.2, + "armor_mass": 12.4, + "volume": 87, + "atmospheric_pressure_limit": 4, + "capacity": 21, + "cargo": 0, + + "equipment_slots": { + "laser_front_s1": { + "type": "weapon", + "size": 1, + "i18n_key": "HARDPOINT_WEAPON_FRONT", + "tag": "tag_laser_front_s1", + "hardpoint": true, + "gimbal": [2,2] + }, + "shield_s1_1": { + "type": "shield", + "size": 1 + }, + "utility_s1_1": { + "type": "utility", + "size": 1, + "tag": "tag_utility_s1_1", + "hardpoint": true + }, + "missile_s1_1": { + "type": "missile", + "size": 1, + "tag": "tag_missile_s1_1", + "hardpoint": true + }, + "missile_s1_2": { + "type": "missile", + "size": 1, + "tag": "tag_missile_s1_2", + "hardpoint": true + }, + "missile_s1_3": { + "type": "missile", + "size": 1, + "tag": "tag_missile_s1_3", + "hardpoint": true + }, + "missile_s1_4": { + "type": "missile", + "size": 1, + "tag": "tag_missile_s1_4", + "hardpoint": true + }, + "fuel_scoop_s1": { + "type": "scoop.fuel", + "size": 1, + "i18n_key": "HARDPOINT_FUEL_SCOOP", + "tag": "tag_fuel_scoop_s1", + "hardpoint": true + }, + "computer_2": { + "type": "computer", + "size": 1 + }, + "shield_s1_2": { + "type": "shield", + "size": 1 + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 1, + "size_min": 1 + }, + "engine": { + "type": "engine", + "size": 2, + "count": 4 + }, + "thruster": { + "type": "thruster", + "size": 1, + "count": 12 + } }, - "roles": ["mercenary"], - + "roles": ["mercenary","police"], "effective_exhaust_velocity": 9900000, "thruster_fuel_use": -1.0, - "fuel_tank_mass": 9, + "fuel_tank_mass": 12, "hyperdrive_class": 1, "forward_thrust": 1500000, @@ -40,7 +109,7 @@ "right_thrust": 160000, "right_acceleration_cap": 20.0124, - "angular_thrust": 1001276.26323262, + "angular_thrust": 1001276.26, "front_cross_section": 38, "side_cross_section": 45, From edc5e907f313ba8238371c50c9b6b3760134d628 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 1 Nov 2024 13:35:02 -0400 Subject: [PATCH 101/119] Rename misleading predicate argument to transformer --- data/libs/autoload.lua | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/data/libs/autoload.lua b/data/libs/autoload.lua index c0c1aea18f..044b25f7c5 100644 --- a/data/libs/autoload.lua +++ b/data/libs/autoload.lua @@ -78,16 +78,16 @@ end -- Copy values from table b into a -- -- Does not copy metatable nor recurse into the table. --- Pass an optional predicate to transform the keys and values before assignment. +-- Pass an optional transformer to mutate the keys and values before assignment. ---@generic K, V ---@param a table ---@param b table ----@param predicate nil|fun(k: K, v: V): any, any +---@param transformer nil|fun(k: K, v: V): any, any ---@return table -table.merge = function(a, b, predicate) - if predicate then +table.merge = function(a, b, transformer) + if transformer then for k, v in pairs(b) do - k, v = predicate(k, v) + k, v = transformer(k, v) a[k] = v end else @@ -101,16 +101,16 @@ end -- Append array b to array a -- -- Does not copy metatable nor recurse into the table. --- Pass an optional predicate to transform the keys and values before assignment. +-- Pass an optional transformer to mutate the keys and values before assignment. ---@generic T ---@param a table ---@param b T[] ----@param predicate nil|fun(v: T): any +---@param transformer nil|fun(v: T): any ---@return table -table.append = function(a, b, predicate) - if predicate then +table.append = function(a, b, transformer) + if transformer then for _, v in ipairs(b) do - v = predicate(v) + v = transformer(v) table.insert(a, v) end else From c946eb387c28676c4e30ff01f61efae4afa33eb2 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 1 Nov 2024 13:57:15 -0400 Subject: [PATCH 102/119] Outfitter: prevent replacing non-empty equipment - If an item cannot be sold directly, don't allow implicitly selling it and replacing it with a new item. - Fix that double-clicking an item could purchase it even if unavailable --- data/lang/ui-core/en.json | 4 ++++ data/pigui/libs/equipment-outfitter.lua | 22 +++++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/data/lang/ui-core/en.json b/data/lang/ui-core/en.json index d7d9dd49d8..59458f320a 100644 --- a/data/lang/ui-core/en.json +++ b/data/lang/ui-core/en.json @@ -2642,5 +2642,9 @@ "COLLAPSE": { "description": "Close / collapse an open folder or group", "message": "Collapse" + }, + "CANNOT_SELL_NONEMPTY_EQUIP": { + "description": "", + "message": "Cannot sell an equipment item unless it is empty." } } diff --git a/data/pigui/libs/equipment-outfitter.lua b/data/pigui/libs/equipment-outfitter.lua index 78c482b643..8f4c6c9c95 100644 --- a/data/pigui/libs/equipment-outfitter.lua +++ b/data/pigui/libs/equipment-outfitter.lua @@ -98,6 +98,7 @@ EquipCardAvailable.tooltipStats = false ---@class UI.EquipmentOutfitter.EquipData : UI.EquipCard.Data ---@field canInstall boolean +---@field canReplace boolean ---@field available boolean ---@field price number @@ -115,12 +116,17 @@ function EquipCardUnavailable:tooltipContents(data, isSelected) ui.spacing() ui.withStyleColors({ Text = self.textColor }, function() + ui.withFont(pionillium.details, function() + + if not data.canInstall then + ui.textWrapped(l.NOT_SUPPORTED_ON_THIS_SHIP % { equipment = data.name } .. ".") + elseif not data.canReplace then + ui.textWrapped(l.CANNOT_SELL_NONEMPTY_EQUIP .. ".") + else + ui.textWrapped(l.YOU_NOT_ENOUGH_MONEY) + end - if not data.canInstall then - ui.textWrapped(l.NOT_SUPPORTED_ON_THIS_SHIP % { equipment = data.name }) - else - ui.textWrapped(l.YOU_NOT_ENOUGH_MONEY) - end + end) end) end @@ -280,7 +286,9 @@ function Outfitter:buildEquipmentList() data.canInstall = equipSet:CanInstallLoose(equip) end - data.available = data.canInstall and money >= self:getInstallPrice(equip) + data.canReplace = not self.replaceEquip or self.canSellEquip + + data.available = data.canInstall and data.canReplace and money >= self:getInstallPrice(equip) -- Replace condition widget with price instead -- trim leading '$' character since we're drawing it with an icon instead @@ -584,7 +592,7 @@ function Outfitter:render() local doubleClicked = clicked and ui.isMouseDoubleClicked(0) - if doubleClicked then + if doubleClicked and data.available then self:message("onBuyItem", data.equip) end end From 67e30ff2f634b200c16c7e10be645bee46475f5e Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 2 Nov 2024 20:32:05 -0400 Subject: [PATCH 103/119] Fix EquipSet:GetFreeSlotForEquip allowing occupied slots --- data/libs/EquipSet.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/libs/EquipSet.lua b/data/libs/EquipSet.lua index 00a5f785d0..c6e8933f34 100644 --- a/data/libs/EquipSet.lua +++ b/data/libs/EquipSet.lua @@ -223,14 +223,14 @@ end function EquipSet:GetFreeSlotForEquip(equip) if not equip.slot then return nil end - local filter = function(_, slot) - return not slot.item + local filter = function(id, slot) + return not self.installed[id] and slot.hardpoint == equip.slot.hardpoint and self:CanInstallInSlot(slot, equip) end - for _, slot in pairs(self.slotCache) do - if filter(_, slot) then + for id, slot in pairs(self.slotCache) do + if filter(id, slot) then return slot end end From 79ae5578f23ce5999a1c69a6248706a1fbd94d8d Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 2 Nov 2024 23:37:35 -0400 Subject: [PATCH 104/119] Import ship JSONs - Initial stats for most of these hulls, should be considered a rough draft to polish in future PRs --- data/ships/ac33.json | 226 ++++++++++++++++++++--- data/ships/bluenose.json | 167 ++++++++++++++--- data/ships/bowfin.json | 14 +- data/ships/coronatrix.json | 21 +-- data/ships/coronatrix_police.json | 10 +- data/ships/deneb.json | 136 +++++++++++--- data/ships/dsminer.json | 151 +++++++++++++-- data/ships/lodos.json | 210 ++++++++++++++++++--- data/ships/lunarshuttle.json | 10 +- data/ships/malabar.json | 284 ++++++++++++++++++++++++++--- data/ships/molamola.json | 27 +-- data/ships/molaramsayi.json | 220 +++++++++++++++++++--- data/ships/nerodia.json | 173 +++++++++++++++--- data/ships/pumpkinseed.json | 19 +- data/ships/pumpkinseed_police.json | 15 +- data/ships/sinonatrix.json | 141 ++++++++++++-- data/ships/sinonatrix_police.json | 158 +++++++++++++--- data/ships/skipjack.json | 170 +++++++++++++---- data/ships/storeria.json | 151 ++++++++++++--- data/ships/vatakara.json | 236 +++++++++++++++++++++--- data/ships/venturestar.json | 160 ++++++++++++++-- data/ships/wave.json | 163 +++++++++++++---- data/ships/xylophis.json | 50 +++-- 23 files changed, 2457 insertions(+), 455 deletions(-) diff --git a/data/ships/ac33.json b/data/ships/ac33.json index 339aab9288..d33ae8ea5d 100644 --- a/data/ships/ac33.json +++ b/data/ships/ac33.json @@ -4,44 +4,214 @@ "cockpit": "", "shield_model": "ac33_shield", "manufacturer": "albr", - "ship_class": "medium_freighter", + "ship_class": "heavy_fighter", "min_crew": 3, "max_crew": 5, - "price": 1015644, - "hull_mass": 780, + "price": 1316807, + "hull_mass": 311, + "structure_mass": 68.1, + "armor_mass": 180.6, + "volume": 4002, "atmospheric_pressure_limit": 7.9, - "capacity": 920, - "slots": { - "engine": 1, - "cabin": 50, - "laser_front": 1, - "laser_rear": 1, - "missile": 26, - "cargo": 480 + "capacity": 280, + "cargo": 360, + + "equipment_slots": { + "utility_s4_1": { + "type": "utility", + "size": 4, + "size_min": 1, + "tag": "tag_utility_s4_1", + "hardpoint": true + }, + "utility_s3_2": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "tag_utility_s3_2", + "hardpoint": true + }, + "utility_s3_3": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "tag_utility_s3_3", + "hardpoint": true + }, + "utility_s2_4": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "tag_utility_s2_4", + "hardpoint": true + }, + "cabin_s3_1": { + "type": "cabin", + "size": 3 + }, + "cabin_s3_2": { + "type": "cabin", + "size": 3 + }, + "cabin_s3_3": { + "type": "cabin", + "size": 3 + }, + "weapon_front_s4_1": { + "type": "weapon", + "size": 4, + "i18n_key": "HARDPOINT_WEAPON_FRONT", + "tag": "tag_weapon_front_s4_1", + "hardpoint": true, + "gimbal": [6,6] + }, + "weapon_front_s4_2": { + "type": "weapon", + "size": 4, + "i18n_key": "HARDPOINT_WEAPON_FRONT", + "tag": "tag_weapon_front_s4_2", + "hardpoint": true, + "gimbal": [6,6] + }, + "missile_bay_s5_l": { + "type": "pylon.rack", + "size": 5, + "i18n_key": "HARDPOINT_MISSILE_BAY_LEFT", + "tag": "tag_missile_bay_s5_l", + "hardpoint": true + }, + "missile_bay_s5_r": { + "type": "pylon.rack", + "size": 5, + "i18n_key": "HARDPOINT_MISSILE_BAY_RIGHT", + "tag": "tag_missile_bay_s5_r", + "hardpoint": true + }, + "missile_bay_s3_l1": { + "type": "pylon.rack", + "size": 3, + "i18n_key": "HARDPOINT_MISSILE_BAY_LEFT", + "tag": "tag_missile_bay_s3_l1", + "hardpoint": true + }, + "missile_bay_s3_l2": { + "type": "pylon.rack", + "size": 3, + "i18n_key": "HARDPOINT_MISSILE_BAY_LEFT", + "tag": "tag_missile_bay_s3_l2", + "hardpoint": true + }, + "missile_bay_s3_r1": { + "type": "pylon.rack", + "size": 3, + "i18n_key": "HARDPOINT_MISSILE_BAY_RIGHT", + "tag": "tag_missile_bay_s3_r1", + "hardpoint": true + }, + "missile_bay_s3_r2": { + "type": "pylon.rack", + "size": 3, + "i18n_key": "HARDPOINT_MISSILE_BAY_RIGHT", + "tag": "tag_missile_bay_s3_r2", + "hardpoint": true + }, + "pylon_s4_l": { + "type": "pylon", + "size": 4, + "i18n_key": "HARDPOINT_PYLON_LEFT", + "tag": "tag_pylon_s4_l", + "hardpoint": true + }, + "pylon_s4_r": { + "type": "pylon", + "size": 4, + "i18n_key": "HARDPOINT_PYLON_RIGHT", + "tag": "tag_pylon_s4_r", + "hardpoint": true + }, + "shield_s4_1": { + "type": "shield", + "size": 4, + "size_min": 3 + }, + "shield_s4_2": { + "type": "shield", + "size": 4, + "size_min": 3 + }, + "fuel_scoop_l": { + "type": "fuel_scoop", + "size": 2, + "size_min": 1, + "tag": "tag_fuel_scoop_l", + "hardpoint": true + }, + "fuel_scoop_r": { + "type": "fuel_scoop", + "size": 2, + "size_min": 1, + "tag": "tag_fuel_scoop_r", + "hardpoint": true + }, + "computer_2": { + "type": "computer", + "size": 2, + "size_min": 1 + }, + "computer_3": { + "type": "computer", + "size": 2, + "size_min": 1 + }, + "computer_1": { + "type": "computer", + "size": 3, + "size_min": 1 + }, + "sensor_2": { + "type": "sensor", + "size": 3, + "size_min": 1 + }, + "sensor": { + "type": "sensor", + "size": 4, + "size_min": 1 + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 4, + "size_min": 4 + }, + "thruster": { + "type": "thruster", + "size": 4, + "count": 8 + } }, - "roles": ["mercenary", "merchant", "pirate"], - - "effective_exhaust_velocity": 20300000, + + "roles": ["pirate","merchant","mercenary"], + "effective_exhaust_velocity": 13000000, "thruster_fuel_use": -1.0, - "fuel_tank_mass": 870, - "hyperdrive_class": 6, + "fuel_tank_mass": 740, + "hyperdrive_class": 4, "forward_thrust": 60000000, "forward_acceleration_cap": 23.8383, - "reverse_thrust": 64000000, - "reverse_acceleration_cap": 24.525, - "up_thrust": 60000000, - "up_acceleration_cap": 24.525, - "down_thrust": 30000000, - "down_acceleration_cap": 17.2656, - "left_thrust": 30000000, - "left_acceleration_cap": 17.2656, - "right_thrust": 30000000, - "right_acceleration_cap": 17.2656, + "reverse_thrust": 35000000, + "reverse_acceleration_cap": 17.658, + "up_thrust": 40000000, + "up_acceleration_cap": 20.601, + "down_thrust": 20000000, + "down_acceleration_cap": 17.658, + "left_thrust": 20000000, + "left_acceleration_cap": 17.658, + "right_thrust": 20000000, + "right_acceleration_cap": 17.658, - "angular_thrust": 201149249.310126, + "angular_thrust": 201149249.01, - "front_cross_section": 120, + "front_cross_section": 120.002, "side_cross_section": 280, "top_cross_section": 1190, diff --git a/data/ships/bluenose.json b/data/ships/bluenose.json index c97fb9ba4c..3a89c0d970 100644 --- a/data/ships/bluenose.json +++ b/data/ships/bluenose.json @@ -1,45 +1,152 @@ { "model": "bluenose", "name": "Bluenose", - "cockpit": " ", + "cockpit": "", "shield_model": "bluenose_shield", "manufacturer": "kaluri", "ship_class": "medium_freighter", "min_crew": 2, "max_crew": 3, - "price": 518244, - "hull_mass": 210, - "atmospheric_pressure_limit": 4.2, - "capacity": 600, - "slots": { - "engine": 1, - "cabin": 20, - "laser_front": 1, - "laser_rear": 1, - "missile": 2, - "cargo": 550 + "price": 269560, + "hull_mass": 105, + "structure_mass": 50.5, + "armor_mass": 42.7, + "volume": 2219, + "atmospheric_pressure_limit": 3.8, + "capacity": 110, + "cargo": 288, + + "equipment_slots": { + "weapon_s2_1": { + "type": "weapon", + "size": 2, + "size_min": 1, + "i18n_key": "HARDPOINT_WEAPON_FRONT", + "tag": "tag_weapon_s2_1", + "hardpoint": true, + "gimbal": [5,5] + }, + "weapon_s2_2": { + "type": "weapon", + "size": 2, + "size_min": 1, + "i18n_key": "HARDPOINT_WEAPON_FRONT", + "tag": "tag_weapon_s2_2", + "hardpoint": true, + "gimbal": [5,5] + }, + "missile_s3_left": { + "type": "missile", + "size": 3, + "tag": "tag_missile_s3_left", + "hardpoint": true + }, + "missile_s3_right": { + "type": "missile", + "size": 3, + "tag": "tag_missile_s3_right", + "hardpoint": true + }, + "shield_s2_1": { + "type": "shield", + "size": 2 + }, + "shield_s2_2": { + "type": "shield", + "size": 2 + }, + "utility_s2_1": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "tag_utility_s2_1", + "hardpoint": true + }, + "utility_s2_2": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "tag_utility_s2_2", + "hardpoint": true + }, + "utility_s1_3": { + "type": "utility", + "size": 1, + "size_min": 1, + "tag": "tag_utility_s1_3", + "hardpoint": true + }, + "utility_s1_4": { + "type": "utility", + "size": 1, + "size_min": 1, + "tag": "tag_utility_s1_4", + "hardpoint": true + }, + "computer": { + "type": "computer", + "size": 2, + "size_min": 1 + }, + "computer_2": { + "type": "computer", + "size": 1, + "size_min": 1 + }, + "cabin_s3_1": { + "type": "cabin", + "size": 3 + }, + "cabin_s3_2": { + "type": "cabin", + "size": 3 + }, + "cabin_s1_1": { + "type": "cabin", + "size": 1 + }, + "cabin_s1_2": { + "type": "cabin", + "size": 1 + }, + "fuel_scoop_s2": { + "type": "fuel_scoop", + "size": 2, + "tag": "tag_fuel_scoop_s2", + "hardpoint": true + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 3, + "size_min": 3 + }, + "thruster": { + "type": "thruster", + "size": 3, + "count": 12 + } }, - "roles": ["merchant", "pirate"], - - "effective_exhaust_velocity": 17600000, + + "roles": ["merchant"], + "effective_exhaust_velocity": 12700000, "thruster_fuel_use": -1.0, - "fuel_tank_mass": 375, - "hyperdrive_class": 4, + "fuel_tank_mass": 278, + "hyperdrive_class": 3, - "forward_thrust": 21000000, - "forward_acceleration_cap": 23.544, - "reverse_thrust": 9000000, - "reverse_acceleration_cap": 9.81, - "up_thrust": 12000000, - "up_acceleration_cap": 19.62, - "down_thrust": 7000000, - "down_acceleration_cap": 15.3036, - "left_thrust": 7000000, - "left_acceleration_cap": 15.3036, - "right_thrust": 7000000, - "right_acceleration_cap": 15.3036, + "forward_thrust": 10000000, + "forward_acceleration_cap": 20.601, + "reverse_thrust": 7200000, + "reverse_acceleration_cap": 15.696, + "up_thrust": 8500000, + "up_acceleration_cap": 20.601, + "down_thrust": 4000000, + "down_acceleration_cap": 9.81, + "left_thrust": 4000000, + "left_acceleration_cap": 9.81, + "right_thrust": 4000000, + "right_acceleration_cap": 9.81, - "angular_thrust": 93869649.6780586, + "angular_thrust": 93869649.68, "front_cross_section": 80, "side_cross_section": 337, diff --git a/data/ships/bowfin.json b/data/ships/bowfin.json index db1c0557d9..f89cb95c1c 100644 --- a/data/ships/bowfin.json +++ b/data/ships/bowfin.json @@ -7,9 +7,9 @@ "ship_class": "light_fighter", "min_crew": 1, "max_crew": 1, - "price": 48674.18056474379, - "hull_mass": 19, - "structure_mass": 7.8, + "price": 43104, + "hull_mass": 20, + "structure_mass": 7.1, "armor_mass": 11.6, "volume": 135, "atmospheric_pressure_limit": 6.5, @@ -81,24 +81,20 @@ "size": 1, "size_min": 1 }, - "engine": { - "type": "engine", - "size": 2, - "count": 2 - }, "thruster": { "type": "thruster", "size": 1, "count": 12 } }, + "roles": ["pirate","mercenary","police"], "effective_exhaust_velocity": 8400000, "thruster_fuel_use": -1.0, "fuel_tank_mass": 20, "hyperdrive_class": 1, - "forward_thrust": 2000000, + "forward_thrust": 2100000, "forward_acceleration_cap": 33.354, "reverse_thrust": 1250000, "reverse_acceleration_cap": 24.525, diff --git a/data/ships/coronatrix.json b/data/ships/coronatrix.json index 4694a102eb..9ad7bebdb9 100644 --- a/data/ships/coronatrix.json +++ b/data/ships/coronatrix.json @@ -7,13 +7,13 @@ "ship_class": "light_courier", "min_crew": 1, "max_crew": 1, - "price": 46580.34, - "hull_mass": 19, + "price": 50349, + "hull_mass": 21, "structure_mass": 7.1, "armor_mass": 11.9, "volume": 190, "atmospheric_pressure_limit": 3.2, - "capacity": 36, + "capacity": 35, "cargo": 16, "equipment_slots": { @@ -40,9 +40,8 @@ "default": "missile_rack.opli_internal_s2" }, "fuel_scoop_s1": { - "type": "scoop.fuel", + "type": "fuel_scoop", "size": 1, - "i18n_key": "HARDPOINT_FUEL_SCOOP", "tag": "tag_fuel_scoop_s1", "hardpoint": true }, @@ -73,23 +72,23 @@ "hardpoint": true, "gimbal": [4,4] }, + "computer_1": { + "type": "computer", + "size": 1 + }, "hyperdrive": { "type": "hyperdrive", "size": 2, "size_min": 1 }, - "engine": { - "type": "engine", - "size": 2, - "count": 2 - }, "thruster": { "type": "thruster", "size": 1, "count": 16 } }, - "roles": ["pirate","mercenary"], + + "roles": ["pirate","mercenary","courier"], "effective_exhaust_velocity": 12600000, "thruster_fuel_use": -1.0, "fuel_tank_mass": 23, diff --git a/data/ships/coronatrix_police.json b/data/ships/coronatrix_police.json index 344a08a0dc..82038ddfc4 100644 --- a/data/ships/coronatrix_police.json +++ b/data/ships/coronatrix_police.json @@ -8,12 +8,12 @@ "min_crew": 1, "max_crew": 1, "price": 0, - "hull_mass": 19, + "hull_mass": 21, "structure_mass": 7.1, "armor_mass": 11.9, "volume": 190, "atmospheric_pressure_limit": 3.2, - "capacity": 36, + "capacity": 35, "cargo": 16, "equipment_slots": { @@ -71,17 +71,13 @@ "size": 2, "size_min": 1 }, - "engine": { - "type": "engine", - "size": 2, - "count": 2 - }, "thruster": { "type": "thruster", "size": 1, "count": 12 } }, + "roles": ["mercenary","police"], "effective_exhaust_velocity": 12600000, "thruster_fuel_use": -1.0, diff --git a/data/ships/deneb.json b/data/ships/deneb.json index 30fa960b62..fa7c4dc5b2 100644 --- a/data/ships/deneb.json +++ b/data/ships/deneb.json @@ -6,49 +6,129 @@ "manufacturer": "albr", "ship_class": "medium_freighter", "min_crew": 2, - "max_crew": 3, - "price": 424104, - "hull_mass": 175, - "atmospheric_pressure_limit": 5.7, - "capacity": 430, - "slots": { - "engine": 1, - "cabin": 50, - "laser_front": 1, - "laser_rear": 1, - "missile": 2, - "cargo": 360 + "max_crew": 2, + "price": 380117, + "hull_mass": 97, + "structure_mass": 26.3, + "armor_mass": 56.2, + "volume": 1342, + "atmospheric_pressure_limit": 7.2, + "capacity": 125, + "cargo": 198, + + "equipment_slots": { + "shield_s3": { + "type": "shield", + "size": 3, + "size_min": 2 + }, + "cabin_s2_1": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_2": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_3": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_4": { + "type": "cabin", + "size": 2 + }, + "weapon_s2_1": { + "type": "weapon", + "size": 2, + "size_min": 1, + "i18n_key": "HARDPOINT_WEAPON_FRONT", + "tag": "tag_weapon_s2_1", + "hardpoint": true, + "gimbal": [5,5] + }, + "weapon_s2_2": { + "type": "weapon", + "size": 2, + "size_min": 1, + "i18n_key": "HARDPOINT_WEAPON_FRONT", + "tag": "tag_weapon_s2_2", + "hardpoint": true, + "gimbal": [5,5] + }, + "utility_s2_1": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "tag_utility_s2_1", + "hardpoint": true + }, + "utility_s2_2": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "tag_utility_s2_2", + "hardpoint": true + }, + "utility_s2_3": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "tag_utility_s2_3", + "hardpoint": true + }, + "computer_2": { + "type": "computer", + "size": 1, + "size_min": 1 + }, + "fuel_scoop_s2": { + "type": "fuel_scoop", + "size": 2, + "tag": "tag_fuel_scoop_s2", + "hardpoint": true + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 3, + "size_min": 3 + }, + "thruster": { + "type": "thruster", + "size": 3, + "count": 12 + } }, - "roles": ["mercenary", "merchant", "pirate"], - "effective_exhaust_velocity": 23900000, + "roles": ["merchant","pirate","mercenary"], + "effective_exhaust_velocity": 17100000, "thruster_fuel_use": -1.0, - "fuel_tank_mass": 225, - "hyperdrive_class": 4, + "fuel_tank_mass": 182, + "hyperdrive_class": 3, - "forward_thrust": 16800000, - "forward_acceleration_cap": 20.601, - "reverse_thrust": 11500000, + "forward_thrust": 11200000, + "forward_acceleration_cap": 22.563, + "reverse_thrust": 9500000, "reverse_acceleration_cap": 17.7561, "up_thrust": 14400000, "up_acceleration_cap": 19.62, - "down_thrust": 11000000, + "down_thrust": 6800000, "down_acceleration_cap": 12.753, - "left_thrust": 11000000, + "left_thrust": 6200000, "left_acceleration_cap": 12.753, - "right_thrust": 11000000, + "right_thrust": 6200000, "right_acceleration_cap": 12.753, - "angular_thrust": 73754724.747046, + "angular_thrust": 73754724.75, - "front_cross_section": 60, - "side_cross_section": 225, - "top_cross_section": 712, + "front_cross_section": 60.002, + "side_cross_section": 225.002, + "top_cross_section": 712.002, "front_drag_coeff": 0.08, "side_drag_coeff": 0.25, "top_drag_coeff": 0.89, - "lift_coeff": 0.81, - "aero_stability": 2.1 + "lift_coeff": 0.95, + "aero_stability": 1.6 } diff --git a/data/ships/dsminer.json b/data/ships/dsminer.json index 61b1af94c4..b47e1dac89 100644 --- a/data/ships/dsminer.json +++ b/data/ships/dsminer.json @@ -1,31 +1,144 @@ { "model": "dsminer", "name": "Deep Space Miner", - "cockpit": " ", + "cockpit": "", "shield_model": "dsminer_shield", "manufacturer": "haber", "ship_class": "heavy_freighter", "min_crew": 5, - "max_crew": 12, - "price": 2676331, - "hull_mass": 1380, - "atmospheric_pressure_limit": 2, - "capacity": 3900, - "slots": { - "engine": 1, - "cabin": 50, - "laser_front": 1, - "laser_rear": 1, - "missile": 4, - "atmo_shield": 0, - "cargo": 3100 + "max_crew": 5, + "price": 1493716, + "hull_mass": 308, + "structure_mass": 155.2, + "armor_mass": 89.8, + "volume": 13777, + "atmospheric_pressure_limit": 0.8, + "capacity": 300, + "cargo": 3200, + + "equipment_slots": { + "mining_laser_s4_1": { + "type": "weapon.mining", + "size": 4, + "size_min": 2, + "i18n_key": "HARDPOINT_MINING_LASER", + "tag": "tag_mining_laser_s4_1", + "hardpoint": true + }, + "mining_laser_s4_2": { + "type": "weapon.mining", + "size": 4, + "size_min": 2, + "i18n_key": "HARDPOINT_MINING_LASER", + "tag": "tag_mining_laser_s4_2", + "hardpoint": true + }, + "utility_s3_1": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "tag_utility_s3_1", + "hardpoint": true + }, + "utility_s3_2": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "tag_utility_s3_2", + "hardpoint": true + }, + "utility_s3_3": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "tag_utility_s3_3", + "hardpoint": true + }, + "utility_s3_4": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "tag_utility_s3_4", + "hardpoint": true + }, + "utility_s2_5": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "tag_utility_s2_5", + "hardpoint": true + }, + "utility_s2_6": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "tag_utility_s2_6", + "hardpoint": true + }, + "cabin_s2_1": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_2": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_3": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_4": { + "type": "cabin", + "size": 2 + }, + "shield_s3_1": { + "type": "shield", + "size": 3 + }, + "shield_s3_2": { + "type": "shield", + "size": 3 + }, + "shield_s3_3": { + "type": "shield", + "size": 3 + }, + "shuttle_bay": { + "type": "vehicle_bay", + "size": 1, + "tag": "tag_shuttle_bay", + "hardpoint": true + }, + "computer_1": { + "type": "computer", + "size": 3, + "size_min": 1 + }, + "computer_2": { + "type": "computer", + "size": 1 + }, + "computer_3": { + "type": "computer", + "size": 1 + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 5, + "size_min": 4 + }, + "thruster": { + "type": "thruster", + "size": 3, + "count": 24 + } }, + "roles": ["merchant"], - - "effective_exhaust_velocity": 12900000, + "effective_exhaust_velocity": 13900000, "thruster_fuel_use": -1.0, - "fuel_tank_mass": 2700, - "hyperdrive_class": 9, + "fuel_tank_mass": 1850, + "hyperdrive_class": 5, "forward_thrust": 46000000, "forward_acceleration_cap": 9.81, @@ -40,7 +153,7 @@ "right_thrust": 9000000, "right_acceleration_cap": 5.0031, - "angular_thrust": 92528654.6826577, + "angular_thrust": 92528654.68, "front_cross_section": 515, "side_cross_section": 911, diff --git a/data/ships/lodos.json b/data/ships/lodos.json index 6312ef4be6..fbfc1ba77f 100644 --- a/data/ships/lodos.json +++ b/data/ships/lodos.json @@ -3,34 +3,202 @@ "name": "Lodos", "cockpit": "", "shield_model": "lodos_shield", - "manufacturer": "auronox", + "manufacturer": "albr", "ship_class": "heavy_freighter", "min_crew": 2, "max_crew": 5, - "price": 2541351, - "hull_mass": 850, - "atmospheric_pressure_limit": 3.2, - "capacity": 3100, - "slots": { - "engine": 1, - "cabin": 50, - "laser_front": 1, - "laser_rear": 1, - "missile": 2, - "cargo": 2800 + "price": 3603283, + "hull_mass": 568, + "structure_mass": 227.6, + "armor_mass": 159.5, + "volume": 13321, + "atmospheric_pressure_limit": 5.1, + "capacity": 584, + "cargo": 2560, + + "equipment_slots": { + "weapon_front_s3_1": { + "type": "weapon", + "size": 3, + "size_min": 2, + "i18n_key": "HARDPOINT_WEAPON_FRONT_LEFT", + "tag": "tag_weapon_front_s3_1", + "hardpoint": true, + "gimbal": [3,3] + }, + "weapon_front_s3_2": { + "type": "weapon", + "size": 3, + "size_min": 2, + "i18n_key": "HARDPOINT_WEAPON_FRONT_LEFT", + "tag": "tag_weapon_front_s3_2", + "hardpoint": true, + "gimbal": [3,3] + }, + "weapon_front_s3_3": { + "type": "weapon", + "size": 3, + "size_min": 2, + "i18n_key": "HARDPOINT_WEAPON_FRONT_RIGHT", + "tag": "tag_weapon_front_s3_3", + "hardpoint": true, + "gimbal": [3,3] + }, + "weapon_front_s3_4": { + "type": "weapon", + "size": 3, + "size_min": 2, + "i18n_key": "HARDPOINT_WEAPON_FRONT_RIGHT", + "tag": "tag_weapon_front_s3_4", + "hardpoint": true, + "gimbal": [3,3] + }, + "missile_rack_s4_l1": { + "type": "pylon.rack", + "size": 4, + "tag": "tag_missile_rack_s4_l1", + "hardpoint": true + }, + "missile_rack_s4_l2": { + "type": "pylon.rack", + "size": 4, + "tag": "tag_missile_rack_s4_l2", + "hardpoint": true + }, + "missile_rack_s4_r1": { + "type": "pylon.rack", + "size": 4, + "tag": "tag_missile_rack_s4_r1", + "hardpoint": true + }, + "missile_rack_s4_r2": { + "type": "pylon.rack", + "size": 4, + "tag": "tag_missile_rack_s4_r2", + "hardpoint": true + }, + "utility_s5_1": { + "type": "utility", + "size": 5, + "size_min": 1, + "tag": "tag_utility_s5_1", + "hardpoint": true + }, + "utility_s4_2": { + "type": "utility", + "size": 4, + "size_min": 1, + "tag": "tag_utility_s4_2", + "hardpoint": true + }, + "utility_s4_3": { + "type": "utility", + "size": 4, + "size_min": 1, + "tag": "tag_utility_s4_3", + "hardpoint": true + }, + "utility_s2_4": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "tag_utility_s2_4", + "hardpoint": true + }, + "utility_s2_5": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "tag_utility_s2_5", + "hardpoint": true + }, + "computer_1": { + "type": "computer", + "size": 3, + "size_min": 1 + }, + "computer_2": { + "type": "computer", + "size": 3, + "size_min": 1 + }, + "computer_3": { + "type": "computer", + "size": 2, + "size_min": 1 + }, + "computer_4": { + "type": "computer", + "size": 2, + "size_min": 1 + }, + "fuel_scoop_1": { + "type": "fuel_scoop", + "size": 4, + "size_min": 1, + "tag": "tag_fuel_scoop_1", + "hardpoint": true + }, + "fuel_scoop_2": { + "type": "fuel_scoop", + "size": 4, + "size_min": 1, + "tag": "tag_fuel_scoop_2", + "hardpoint": true + }, + "cabin_s3_1": { + "type": "cabin", + "size": 3 + }, + "cabin_s3_2": { + "type": "cabin", + "size": 3 + }, + "cabin_s3_3": { + "type": "cabin", + "size": 3 + }, + "cabin_s2_4": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_5": { + "type": "cabin", + "size": 2 + }, + "shield_s4_1": { + "type": "shield", + "size": 4, + "size_min": 3 + }, + "shield_s4_2": { + "type": "shield", + "size": 4, + "size_min": 3 + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 5, + "size_min": 4 + }, + "thruster": { + "type": "thruster", + "size": 5, + "count": 12 + } }, + "roles": ["merchant"], - "effective_exhaust_velocity": 18900000, "thruster_fuel_use": -1.0, - "fuel_tank_mass": 2100, - "hyperdrive_class": 7, + "fuel_tank_mass": 2190, + "hyperdrive_class": 5, "forward_thrust": 98000000, "forward_acceleration_cap": 22.563, - "reverse_thrust": 33000000, + "reverse_thrust": 35600000, "reverse_acceleration_cap": 17.658, - "up_thrust": 33000000, + "up_thrust": 35600000, "up_acceleration_cap": 19.62, "down_thrust": 13000000, "down_acceleration_cap": 17.658, @@ -39,11 +207,11 @@ "right_thrust": 13000000, "right_acceleration_cap": 17.658, - "angular_thrust": 267305002.416567, + "angular_thrust": 267305002.42, - "front_cross_section": 345, - "side_cross_section": 690, - "top_cross_section": 1430, + "front_cross_section": 345.002, + "side_cross_section": 690.002, + "top_cross_section": 1430.002, "front_drag_coeff": 0.32, "side_drag_coeff": 0.69, diff --git a/data/ships/lunarshuttle.json b/data/ships/lunarshuttle.json index d572337d31..87317977f8 100644 --- a/data/ships/lunarshuttle.json +++ b/data/ships/lunarshuttle.json @@ -7,8 +7,8 @@ "ship_class": "light_passenger_shuttle", "min_crew": 1, "max_crew": 1, - "price": 16224, - "hull_mass": 14, + "price": 27062, + "hull_mass": 15, "structure_mass": 8.7, "armor_mass": 5.6, "volume": 310, @@ -42,17 +42,13 @@ "size": 1, "size_min": 1 }, - "engine": { - "type": "engine", - "size": 2, - "count": 2 - }, "thruster": { "type": "thruster", "size": 1, "count": 6 } }, + "roles": ["passenger"], "effective_exhaust_velocity": 7900000, "thruster_fuel_use": -1.0, diff --git a/data/ships/malabar.json b/data/ships/malabar.json index 64af76ea04..d64dbe01a4 100644 --- a/data/ships/malabar.json +++ b/data/ships/malabar.json @@ -3,28 +3,270 @@ "name": "Malabar", "cockpit": "", "shield_model": "malabar_shield", - "manufacturer": "mandarava_csepel", + "manufacturer": "mandarava-csepel", "ship_class": "heavy_passenger_transport", - "min_crew": 1, + "min_crew": 4, "max_crew": 8, - "price": 2219852, - "hull_mass": 940, - "atmospheric_pressure_limit": 4.7, - "capacity": 2600, - "slots": { - "engine": 1, - "cabin": 80, - "laser_front": 1, - "laser_rear": 1, - "missile": 6, - "cargo": 1600 + "price": 2562074, + "hull_mass": 607, + "structure_mass": 173.8, + "armor_mass": 256.7, + "volume": 14162, + "atmospheric_pressure_limit": 3, + "capacity": 1093, + "cargo": 800, + + "equipment_slots": { + "cabin_s4_1": { + "type": "cabin", + "size": 4 + }, + "cabin_s4_2": { + "type": "cabin", + "size": 4 + }, + "cabin_s3_3": { + "type": "cabin", + "size": 3 + }, + "cabin_s3_4": { + "type": "cabin", + "size": 3 + }, + "cabin_s3_5": { + "type": "cabin", + "size": 3 + }, + "cabin_s3_6": { + "type": "cabin", + "size": 3 + }, + "cabin_s2_7": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_8": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_9": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_10": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_11": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_12": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_13": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_14": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_15": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_16": { + "type": "cabin", + "size": 2 + }, + "fuel_scoop": { + "type": "fuel_scoop", + "size": 5, + "size_min": 3, + "tag": "", + "hardpoint": true + }, + "computer_1": { + "type": "computer", + "size": 3, + "size_min": 1 + }, + "computer_2": { + "type": "computer", + "size": 2, + "size_min": 1 + }, + "computer_3": { + "type": "computer", + "size": 1 + }, + "computer_4": { + "type": "computer", + "size": 1 + }, + "utility_s1_1": { + "type": "utility", + "size": 1, + "tag": "", + "hardpoint": true + }, + "utility_s1_2": { + "type": "utility", + "size": 1, + "tag": "", + "hardpoint": true + }, + "utility_s2_3": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "", + "hardpoint": true + }, + "utility_s2_4": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "", + "hardpoint": true + }, + "utility_s3_5": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "", + "hardpoint": true + }, + "utility_s3_6": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "", + "hardpoint": true + }, + "utility_s3_7": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "", + "hardpoint": true + }, + "utility_s3_8": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "", + "hardpoint": true + }, + "utility_s4_9": { + "type": "utility", + "size": 4, + "size_min": 2, + "tag": "", + "hardpoint": true + }, + "utility_s4_10": { + "type": "utility", + "size": 4, + "size_min": 2, + "tag": "", + "hardpoint": true + }, + "utility_s5_11": { + "type": "utility", + "size": 5, + "size_min": 3, + "tag": "", + "hardpoint": true + }, + "weapon_s3_1": { + "type": "weapon", + "size": 3, + "size_min": 2, + "i18n_key": "HARDPOINT_WEAPON_FRONT", + "tag": "", + "hardpoint": true, + "gimbal": [30,30] + }, + "weapon_s3_2": { + "type": "weapon", + "size": 3, + "size_min": 2, + "i18n_key": "HARDPOINT_WEAPON_REAR", + "tag": "", + "hardpoint": true, + "gimbal": [30,30] + }, + "shield_s3_1": { + "type": "shield", + "size": 3 + }, + "shield_s3_2": { + "type": "shield", + "size": 3 + }, + "shield_s3_3": { + "type": "shield", + "size": 3 + }, + "missile_bay_1 (cap:5xS2)": { + "type": "missile_bay.opli_internal", + "size": 2, + "i18n_key": "HARDPOINT_MISSILE_BAY" + }, + "missile_bay_2 (cap:5xS2)": { + "type": "missile_bay.opli_internal", + "size": 2, + "i18n_key": "HARDPOINT_MISSILE_BAY" + }, + "utility_s5_12": { + "type": "utility", + "size": 5, + "size_min": 3, + "tag": "", + "hardpoint": true + }, + "sensor_s3_1": { + "type": "", + "size": 3, + "size_min": 2 + }, + "sensor_s3_2": { + "type": "", + "size": 3, + "size_min": 2 + }, + "sensor_s2_3": { + "type": "", + "size": 2 + }, + "sensor_s2_4": { + "type": "", + "size": 2 + }, + "shield_s3_4": { + "type": "shield", + "size": 3 + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 5, + "size_min": 3 + }, + "thruster": { + "type": "thruster", + "size": 5, + "count": 12 + } }, + "roles": ["merchant"], - - "effective_exhaust_velocity": 14900000, + "effective_exhaust_velocity": 16900000, "thruster_fuel_use": -1.0, - "fuel_tank_mass": 2000, - "hyperdrive_class": 9, + "fuel_tank_mass": 1720, + "hyperdrive_class": 4, "forward_thrust": 120000000, "forward_acceleration_cap": 17.658, @@ -39,11 +281,11 @@ "right_thrust": 27000000, "right_acceleration_cap": 12.1644, - "angular_thrust": 555171928.095946, + "angular_thrust": 555171928.1, - "front_cross_section": 635, - "side_cross_section": 1215, - "top_cross_section": 980, + "front_cross_section": 635.002, + "side_cross_section": 1215.002, + "top_cross_section": 980.002, "front_drag_coeff": 1.3, "side_drag_coeff": 1.21, diff --git a/data/ships/molamola.json b/data/ships/molamola.json index 89801689a4..a311433ed6 100644 --- a/data/ships/molamola.json +++ b/data/ships/molamola.json @@ -7,9 +7,9 @@ "ship_class": "light_freighter", "min_crew": 1, "max_crew": 1, - "price": 59029.35, - "hull_mass": 18, - "structure_mass": 11.3, + "price": 60279, + "hull_mass": 23, + "structure_mass": 12.7, "armor_mass": 7.1, "volume": 378, "atmospheric_pressure_limit": 5.1, @@ -17,14 +17,6 @@ "cargo": 72, "equipment_slots": { - "weapon_left_nose": { - "type": "weapon", - "size": 1, - "i18n_key": "HARDPOINT_WEAPON_LEFT_NOSE", - "tag": "tag_weapon_left_nose", - "hardpoint": true, - "gimbal": [0,0] - }, "weapon_right_nose": { "type": "weapon", "size": 1, @@ -37,11 +29,10 @@ "type": "computer", "size": 1 }, - "cargo_scoop_s1": { - "type": "scoop.cargo", + "fuel_scoop_s1": { + "type": "fuel_scoop", "size": 1, - "i18n_key": "HARDPOINT_CARGO_SCOOP", - "tag": "tag_cargo_scoop_s1", + "tag": "tag_fuel_scoop_s1", "hardpoint": true }, "utility_s1_1": { @@ -59,17 +50,13 @@ "size": 2, "size_min": 2 }, - "engine": { - "type": "engine", - "size": 1, - "count": 4 - }, "thruster": { "type": "thruster", "size": 2, "count": 12 } }, + "roles": ["merchant"], "effective_exhaust_velocity": 13000000, "thruster_fuel_use": -1.0, diff --git a/data/ships/molaramsayi.json b/data/ships/molaramsayi.json index 0321ff1ffc..af46d46044 100644 --- a/data/ships/molaramsayi.json +++ b/data/ships/molaramsayi.json @@ -1,36 +1,204 @@ { "model": "molaramsayi", "name": "Mola Ramsayi", - "cockpit": " ", + "cockpit": "", "shield_model": "molaramsayi_shield", "manufacturer": "kaluri", "ship_class": "medium_freighter", - "min_crew": 1, - "max_crew": 5, - "price": 814200, - "hull_mass": 380, - "atmospheric_pressure_limit": 3.8, - "capacity": 950, - "slots": { - "engine": 1, - "cabin": 60, - "laser_front": 1, - "laser_rear": 1, - "missile": 3, - "cargo": 900 + "min_crew": 2, + "max_crew": 3, + "price": 607846, + "hull_mass": 143, + "structure_mass": 54.7, + "armor_mass": 40.7, + "volume": 2881, + "atmospheric_pressure_limit": 4.6, + "capacity": 120, + "cargo": 440, + + "equipment_slots": { + "cabin_s1_1": { + "type": "cabin", + "size": 1 + }, + "cabin_s1_2": { + "type": "cabin", + "size": 1 + }, + "cabin_s2_3": { + "type": "cabin", + "size": 3 + }, + "cabin_s2_4": { + "type": "cabin", + "size": 3 + }, + "fuel_scoop": { + "type": "fuel_scoop", + "size": 3, + "size_min": 1 + }, + "computer_1": { + "type": "computer", + "size": 2, + "size_min": 1 + }, + "computer_2": { + "type": "computer", + "size": 2, + "size_min": 1 + }, + "utility_s1_2": { + "type": "utility", + "size": 1, + "size_min": 1, + "tag": "", + "hardpoint": true + }, + "utility_s1_3": { + "type": "utility", + "size": 1, + "size_min": 1, + "tag": "", + "hardpoint": true + }, + "utility_s1_1": { + "type": "utility", + "size": 1, + "size_min": 1, + "tag": "", + "hardpoint": true + }, + "utility_s2_4": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "", + "hardpoint": true + }, + "utility_s2_5": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "", + "hardpoint": true + }, + "utility_s2_6": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "", + "hardpoint": true + }, + "utility_s3_7": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "", + "hardpoint": true + }, + "utility_s3_8": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "", + "hardpoint": true + }, + "utility_s3_9": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "", + "hardpoint": true + }, + "computer_3": { + "type": "computer", + "size": 2, + "size_min": 1 + }, + "weapon_s2_1": { + "type": "weapon", + "size": 2, + "tag": "", + "hardpoint": true, + "gimbal": [5,5] + }, + "weapon_s2_2": { + "type": "weapon", + "size": 2, + "tag": "", + "hardpoint": true, + "gimbal": [5,5] + }, + "missile_bay_1 (cap:12xS2)": { + "type": "missile_bay.opli_internal", + "size": 4, + "tag": "", + "hardpoint": true + }, + "shield_s2_1": { + "type": "shield", + "size": 2 + }, + "shield_s2_2": { + "type": "shield", + "size": 2 + }, + "shield_s2_3": { + "type": "shield", + "size": 2 + }, + "sensor_s3_1": { + "type": "sensor", + "size": 3, + "size_min": 1 + }, + "sensor_s2_2": { + "type": "sensor", + "size": 2, + "size_min": 1 + }, + "sensor_s2_3": { + "type": "sensor", + "size": 2, + "size_min": 1 + }, + "sensor_s2_3": { + "type": "sensor", + "size": 2, + "size_min": 1 + }, + "cabin_s2_5": { + "type": "cabin", + "size": 3 + }, + "cabin_s2_6": { + "type": "cabin", + "size": 3 + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 4, + "size_min": 3 + }, + "thruster": { + "type": "thruster", + "size": 4, + "count": 12 + } }, - "roles": ["merchant"], - "effective_exhaust_velocity": 17600000, + "roles": ["merchant"], + "effective_exhaust_velocity": 16300000, "thruster_fuel_use": -1.0, - "fuel_tank_mass": 473, - "hyperdrive_class": 5, + "fuel_tank_mass": 480, + "hyperdrive_class": 3, - "forward_thrust": 36000000, + "forward_thrust": 31200000, "forward_acceleration_cap": 21.582, - "reverse_thrust": 20000000, - "reverse_acceleration_cap": 21.582, - "up_thrust": 32000000, + "reverse_thrust": 16000000, + "reverse_acceleration_cap": 16.1865, + "up_thrust": 17000000, "up_acceleration_cap": 21.582, "down_thrust": 13000000, "down_acceleration_cap": 16.677, @@ -39,11 +207,11 @@ "right_thrust": 13000000, "right_acceleration_cap": 16.677, - "angular_thrust": 76704913.7369279, + "angular_thrust": 76704913.74, - "front_cross_section": 165, - "side_cross_section": 300, - "top_cross_section": 307, + "front_cross_section": 165.002, + "side_cross_section": 300.002, + "top_cross_section": 307.002, "front_drag_coeff": 0.34, "side_drag_coeff": 0.56, diff --git a/data/ships/nerodia.json b/data/ships/nerodia.json index f0c6f94005..b9a11244ee 100644 --- a/data/ships/nerodia.json +++ b/data/ships/nerodia.json @@ -4,34 +4,161 @@ "cockpit": "", "shield_model": "nerodia_shield", "manufacturer": "opli", - "ship_class": "medium_freighter", - "min_crew": 1, + "ship_class": "heavy_freighter", + "min_crew": 4, "max_crew": 6, - "price": 2059371, - "hull_mass": 450, + "price": 1233699, + "hull_mass": 308, + "structure_mass": 115.5, + "armor_mass": 121.5, + "volume": 5970, "atmospheric_pressure_limit": 3.7, - "capacity": 2700, - "slots": { - "engine": 1, - "cabin": 35, - "laser_front": 1, - "laser_rear": 1, - "missile": 2, - "atmo_shield": 0, - "cargo": 2500 + "capacity": 251, + "cargo": 1320, + + "equipment_slots": { + "turret_s2_1": { + "type": "turret", + "size": 2, + "tag": "tag_turret_s2_1", + "hardpoint": true + }, + "turret_s2_2": { + "type": "turret", + "size": 2, + "tag": "tag_turret_s2_2", + "hardpoint": true + }, + "turret_s2_3": { + "type": "turret", + "size": 2, + "tag": "tag_turret_s2_3", + "hardpoint": true + }, + "turret_s2_4": { + "type": "turret", + "size": 2, + "tag": "tag_turret_s2_4", + "hardpoint": true + }, + "utility_s4_1": { + "type": "utility", + "size": 4, + "size_min": 1, + "tag": "tag_utility_s4_1", + "hardpoint": true + }, + "utility_s3_2": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "tag_utility_s3_2", + "hardpoint": true + }, + "utility_s3_3": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "tag_utility_s3_3", + "hardpoint": true + }, + "utility_s2_4": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "tag_utility_s2_4", + "hardpoint": true + }, + "utility_s2_5": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "tag_utility_s2_5", + "hardpoint": true + }, + "missile_bay_1": { + "type": "missile_bay.opli_internal", + "size": 3, + "tag": "tag_missile_bay_1", + "hardpoint": true + }, + "missile_bay_2": { + "type": "missile_bay.opli_internal", + "size": 3, + "tag": "tag_missile_bay_2", + "hardpoint": true + }, + "computer_1": { + "type": "computer", + "size": 3, + "size_min": 1 + }, + "computer_2": { + "type": "computer", + "size": 3, + "size_min": 1 + }, + "computer_3": { + "type": "computer", + "size": 1, + "size_min": 1 + }, + "computer_4": { + "type": "computer", + "size": 1, + "size_min": 1 + }, + "cabin_s3_1": { + "type": "cabin", + "size": 3 + }, + "cabin_s3_2": { + "type": "cabin", + "size": 3 + }, + "cabin_s2_1": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_2": { + "type": "cabin", + "size": 2 + }, + "fuel_scoop_s4": { + "type": "fuel_scoop", + "size": 4, + "size_min": 1, + "tag": "tag_fuel_scoop_s4", + "hardpoint": true + }, + "shield_s4_1": { + "type": "shield", + "size": 4, + "size_min": 3 + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 4, + "size_min": 4 + }, + "thruster": { + "type": "thruster", + "size": 4, + "count": 18 + } }, + "roles": ["merchant"], - - "effective_exhaust_velocity": 21900000, + "effective_exhaust_velocity": 15700000, "thruster_fuel_use": -1.0, - "fuel_tank_mass": 1000, - "hyperdrive_class": 8, + "fuel_tank_mass": 870, + "hyperdrive_class": 4, - "forward_thrust": 56000000, + "forward_thrust": 55350000, "forward_acceleration_cap": 14.715, "reverse_thrust": 9000000, "reverse_acceleration_cap": 9.81, - "up_thrust": 19000000, + "up_thrust": 29000000, "up_acceleration_cap": 9.81, "down_thrust": 9000000, "down_acceleration_cap": 9.81, @@ -40,11 +167,11 @@ "right_thrust": 9000000, "right_acceleration_cap": 9.81, - "angular_thrust": 128735519.55848, + "angular_thrust": 128735519.56, - "front_cross_section": 210, - "side_cross_section": 358, - "top_cross_section": 810, + "front_cross_section": 210.002, + "side_cross_section": 358.002, + "top_cross_section": 810.002, "front_drag_coeff": 0.6, "side_drag_coeff": 0.8, diff --git a/data/ships/pumpkinseed.json b/data/ships/pumpkinseed.json index 343fefd0fe..0d4266e145 100644 --- a/data/ships/pumpkinseed.json +++ b/data/ships/pumpkinseed.json @@ -7,8 +7,8 @@ "ship_class": "light_courier", "min_crew": 1, "max_crew": 1, - "price": 19104, - "hull_mass": 11, + "price": 46180, + "hull_mass": 13, "structure_mass": 4.2, "armor_mass": 6.6, "volume": 87, @@ -42,9 +42,8 @@ "hardpoint": true }, "fuel_scoop_s1": { - "type": "scoop.fuel", + "type": "fuel_scoop", "size": 1, - "i18n_key": "HARDPOINT_FUEL_SCOOP", "tag": "tag_fuel_scoop_s1", "hardpoint": true }, @@ -70,29 +69,29 @@ "tag": "tag_utility_s1_1", "hardpoint": true }, + "cabin_s1_2": { + "type": "cabin", + "size": 1 + }, "hyperdrive": { "type": "hyperdrive", "size": 1, "size_min": 1 }, - "engine": { - "type": "engine", - "size": 2, - "count": 4 - }, "thruster": { "type": "thruster", "size": 1, "count": 12 } }, + "roles": ["pirate","mercenary"], "effective_exhaust_velocity": 15200000, "thruster_fuel_use": -1.0, "fuel_tank_mass": 10, "hyperdrive_class": 1, - "forward_thrust": 1500000, + "forward_thrust": 1320000, "forward_acceleration_cap": 41.202, "reverse_thrust": 700000, "reverse_acceleration_cap": 21.582, diff --git a/data/ships/pumpkinseed_police.json b/data/ships/pumpkinseed_police.json index 64f81619bd..d6dd956d71 100644 --- a/data/ships/pumpkinseed_police.json +++ b/data/ships/pumpkinseed_police.json @@ -8,7 +8,7 @@ "min_crew": 1, "max_crew": 1, "price": 0, - "hull_mass": 17, + "hull_mass": 18, "structure_mass": 4.2, "armor_mass": 12.4, "volume": 87, @@ -60,9 +60,8 @@ "hardpoint": true }, "fuel_scoop_s1": { - "type": "scoop.fuel", + "type": "fuel_scoop", "size": 1, - "i18n_key": "HARDPOINT_FUEL_SCOOP", "tag": "tag_fuel_scoop_s1", "hardpoint": true }, @@ -79,24 +78,20 @@ "size": 1, "size_min": 1 }, - "engine": { - "type": "engine", - "size": 2, - "count": 4 - }, "thruster": { "type": "thruster", "size": 1, "count": 12 } }, + "roles": ["mercenary","police"], - "effective_exhaust_velocity": 9900000, + "effective_exhaust_velocity": 15200000, "thruster_fuel_use": -1.0, "fuel_tank_mass": 12, "hyperdrive_class": 1, - "forward_thrust": 1500000, + "forward_thrust": 1320000, "forward_acceleration_cap": 41.202, "reverse_thrust": 700000, "reverse_acceleration_cap": 27.468, diff --git a/data/ships/sinonatrix.json b/data/ships/sinonatrix.json index 0a9024e956..a28ed36712 100644 --- a/data/ships/sinonatrix.json +++ b/data/ships/sinonatrix.json @@ -4,29 +4,134 @@ "cockpit": "sinonatrix_cockpit", "shield_model": "sinonatrix_shield", "manufacturer": "opli", - "ship_class": "light_courier", + "ship_class": "medium_courier", "min_crew": 1, "max_crew": 2, - "price": 79073, - "hull_mass": 28, + "price": 95647, + "hull_mass": 39, + "structure_mass": 15, + "armor_mass": 19, + "volume": 576, "atmospheric_pressure_limit": 3.2, - "capacity": 53, - "slots": { - "engine": 1, - "cabin": 3, - "laser_front": 1, - "missile": 5, - "shield": 2, - "cargo": 30 + "capacity": 59, + "cargo": 24, + + "equipment_slots": { + "weapon_s2_1": { + "type": "weapon", + "size": 2, + "size_min": 1, + "i18n_key": "HARDPOINT_WEAPON_FRONT", + "tag": "tag_weapon_s2_1", + "hardpoint": true, + "gimbal": [5,5] + }, + "weapon_s2_2": { + "type": "weapon", + "size": 2, + "size_min": 1, + "i18n_key": "HARDPOINT_WEAPON_FRONT", + "tag": "tag_weapon_s2_2", + "hardpoint": true, + "gimbal": [5,5] + }, + "utility_s2_1": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "tag_utility_s2_1", + "hardpoint": true + }, + "utility_s2_2": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "tag_utility_s2_2", + "hardpoint": true + }, + "utility_s1_3": { + "type": "utility", + "size": 1, + "size_min": 1, + "tag": "tag_utility_s1_3", + "hardpoint": true + }, + "utility_s1_4": { + "type": "utility", + "size": 1, + "size_min": 1, + "tag": "tag_utility_s1_4", + "hardpoint": true + }, + "computer": { + "type": "computer", + "size": 1, + "size_min": 1 + }, + "computer_2": { + "type": "computer", + "size": 1, + "size_min": 1 + }, + "shield_s2_1": { + "type": "shield", + "size": 2, + "size_min": 1 + }, + "shield_s2_2": { + "type": "shield", + "size": 2, + "size_min": 1 + }, + "missile_bay_1": { + "type": "missile_bay.opli_internal", + "size": 2, + "size_min": 1, + "i18n_key": "HARDPOINT_MISSILE_BAY", + "tag": "tag_missile_bay_1", + "hardpoint": true, + "default": "missile_rack.opli_internal_s2" + }, + "missile_bay_2": { + "type": "missile_bay.opli_internal", + "size": 2, + "size_min": 1, + "i18n_key": "HARDPOINT_MISSILE_BAY", + "tag": "tag_missile_bay_2", + "hardpoint": true, + "default": "missile_rack.opli_internal_s2" + }, + "fuel_scoop_s1": { + "type": "fuel_scoop", + "size": 1 + }, + "cabin_s1_1": { + "type": "cabin", + "size": 1 + }, + "cabin_s1_2": { + "type": "cabin", + "size": 1 + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 2, + "size_min": 1 + }, + "thruster": { + "type": "thruster", + "size": 2, + "count": 12 + } }, - "roles": ["mercenary", "merchant", "pirate", "courier"], - "effective_exhaust_velocity": 16000000, + "roles": ["pirate","merchant","mercenary","courier"], + "effective_exhaust_velocity": 16700000, "thruster_fuel_use": -1.0, "fuel_tank_mass": 54, "hyperdrive_class": 2, - "forward_thrust": 2300000, + "forward_thrust": 3160000, "forward_acceleration_cap": 38.259, "reverse_thrust": 900000, "reverse_acceleration_cap": 17.658, @@ -39,11 +144,11 @@ "right_thrust": 600000, "right_acceleration_cap": 14.715, - "angular_thrust": 4022984.98620251, + "angular_thrust": 4500000, - "front_cross_section": 42.2, - "side_cross_section": 60.8, - "top_cross_section": 170, + "front_cross_section": 42.202, + "side_cross_section": 60.802, + "top_cross_section": 170.002, "front_drag_coeff": 0.3, "side_drag_coeff": 0.5, diff --git a/data/ships/sinonatrix_police.json b/data/ships/sinonatrix_police.json index b9ebf574e3..dd26fe6f87 100644 --- a/data/ships/sinonatrix_police.json +++ b/data/ships/sinonatrix_police.json @@ -4,33 +4,145 @@ "cockpit": "sinonatrix_cockpit", "shield_model": "sinonatrix_shield", "manufacturer": "opli", - "ship_class": "light_fighter", + "ship_class": "medium_fighter", "min_crew": 1, "max_crew": 2, "price": 0, - "hull_mass": 32, + "hull_mass": 46, + "structure_mass": 15, + "armor_mass": 25.4, + "volume": 576, "atmospheric_pressure_limit": 3.5, - "capacity": 35, - "slots": { - "engine": 1, - "cabin": 3, - "scoop": 0, - "laser_front": 1, - "laser_rear": 1, - "missile": 8, - "cargo": 25 + "capacity": 59, + "cargo": 24, + + "equipment_slots": { + "weapon_s2_2": { + "type": "weapon", + "size": 2, + "size_min": 1, + "i18n_key": "HARDPOINT_WEAPON_FRONT", + "tag": "tag_weapon_s2_2", + "hardpoint": true, + "gimbal": [6,6] + }, + "weapon_s2_1": { + "type": "weapon", + "size": 2, + "size_min": 1, + "i18n_key": "HARDPOINT_WEAPON_FRONT", + "tag": "tag_weapon_s2_1", + "hardpoint": true, + "gimbal": [6,6] + }, + "utility_s2_1": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "tag_utility_s2_1", + "hardpoint": true + }, + "utility_s2_2": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "tag_utility_s2_2", + "hardpoint": true + }, + "utility_s1_5": { + "type": "utility", + "size": 1, + "size_min": 1, + "tag": "tag_utility_s1_5", + "hardpoint": true + }, + "utility_s1_4": { + "type": "utility", + "size": 1, + "size_min": 1, + "tag": "tag_utility_s1_4", + "hardpoint": true + }, + "computer_1": { + "type": "computer", + "size": 1, + "size_min": 1 + }, + "computer_2": { + "type": "computer", + "size": 1, + "size_min": 1 + }, + "shield_s2_1": { + "type": "shield", + "size": 2, + "size_min": 1 + }, + "shield_s2_2": { + "type": "shield", + "size": 2, + "size_min": 1 + }, + "missile_bay_1": { + "type": "missile_bay.opli_internal", + "size": 2, + "size_min": 1, + "i18n_key": "HARDPOINT_MISSILE_BAY", + "tag": "tag_missile_bay_1", + "hardpoint": true, + "default": "missile_rack.opli_internal_s2" + }, + "missile_bay_2": { + "type": "missile_bay.opli_internal", + "size": 2, + "size_min": 1, + "i18n_key": "HARDPOINT_MISSILE_BAY", + "tag": "tag_missile_bay_2", + "hardpoint": true, + "default": "missile_rack.opli_internal_s2" + }, + "fuel_scoop_s1": { + "type": "fuel_scoop", + "size": 1, + "size_min": 1 + }, + "utility_s2_3": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "tag_utility_s2_3", + "hardpoint": true + }, + "cabin_s1_1": { + "type": "cabin", + "size": 1 + }, + "cabin_s1_2": { + "type": "cabin", + "size": 1 + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 2, + "size_min": 1 + }, + "thruster": { + "type": "thruster", + "size": 2, + "count": 12 + } }, - "roles": ["mercenary"], - - "effective_exhaust_velocity": 16000000, + + "roles": ["police"], + "effective_exhaust_velocity": 13010000, "thruster_fuel_use": -1.0, - "fuel_tank_mass": 30, - "hyperdrive_class": 1, + "fuel_tank_mass": 54, + "hyperdrive_class": 2, - "forward_thrust": 2000000, - "forward_acceleration_cap": 38.259, + "forward_thrust": 4680000, + "forward_acceleration_cap": 40.221, "reverse_thrust": 1000000, - "reverse_acceleration_cap": 29.43, + "reverse_acceleration_cap": 19.62, "up_thrust": 1800000, "up_acceleration_cap": 20.6991, "down_thrust": 600000, @@ -40,11 +152,11 @@ "right_thrust": 600000, "right_acceleration_cap": 16.677, - "angular_thrust": 4022984.98620251, + "angular_thrust": 4582984, - "front_cross_section": 26, - "side_cross_section": 40, - "top_cross_section": 97, + "front_cross_section": 42.202, + "side_cross_section": 60.802, + "top_cross_section": 170.002, "front_drag_coeff": 0.3, "side_drag_coeff": 0.5, diff --git a/data/ships/skipjack.json b/data/ships/skipjack.json index c52233ee71..34fc21cf0e 100644 --- a/data/ships/skipjack.json +++ b/data/ships/skipjack.json @@ -1,55 +1,163 @@ { "model": "skipjack", "name": "Skipjack", - "cockpit": " ", + "cockpit": "", "shield_model": "skipjack_shield", "manufacturer": "kaluri", "ship_class": "medium_courier", - "min_crew": 2, - "max_crew": 3, - "price": 204837, - "hull_mass": 80, - "atmospheric_pressure_limit": 4, - "capacity": 192, - "slots": { - "engine": 1, - "cabin": 5, - "scoop": 2, - "laser_front": 1, - "missile": 8, - "sensor": 3, - "cargo": 140 + "min_crew": 1, + "max_crew": 2, + "price": 175779, + "hull_mass": 87, + "structure_mass": 28.6, + "armor_mass": 44.3, + "volume": 831, + "atmospheric_pressure_limit": 4.8, + "capacity": 75, + "cargo": 78, + + "equipment_slots": { + "cabin_s1_1": { + "type": "cabin", + "size": 1 + }, + "cabin_s1_2": { + "type": "cabin", + "size": 1 + }, + "cabin_s2_1": { + "type": "cabin", + "size": 2 + }, + "weapon_s2_1": { + "type": "weapon", + "size": 2, + "size_min": 1, + "i18n_key": "HARDPOINT_WEAPON_FRONT", + "tag": "tag_weapon_s2_1", + "hardpoint": true, + "gimbal": [5,5] + }, + "weapon_s1_left": { + "type": "weapon", + "size": 1, + "i18n_key": "HARDPOINT_WEAPON_LEFT", + "tag": "tag_weapon_s1_left", + "hardpoint": true, + "gimbal": [1,1] + }, + "weapon_s1_right": { + "type": "weapon", + "size": 1, + "i18n_key": "HARDPOINT_WEAPON_RIGHT", + "tag": "tag_weapon_s1_right", + "hardpoint": true, + "gimbal": [1,1] + }, + "missile_rack_s2_left": { + "type": "pylon.rack", + "size": 2, + "i18n_key": "HARDPOINT_PYLON_LEFT", + "tag": "tag_missile_rack_s2_left", + "hardpoint": true + }, + "missile_rack_s2_right": { + "type": "pylon.rack", + "size": 2, + "i18n_key": "HARDPOINT_PYLON_LEFT", + "tag": "tag_missile_rack_s2_right", + "hardpoint": true + }, + "utility_s2_1": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "tag_utility_s2_1", + "hardpoint": true + }, + "utility_s1_1": { + "type": "utility", + "size": 1, + "tag": "tag_utility_s1_1", + "hardpoint": true + }, + "utility_s1_2": { + "type": "utility", + "size": 1, + "tag": "tag_utility_s1_2", + "hardpoint": true + }, + "shield_s2": { + "type": "shield", + "size": 2, + "size_min": 1 + }, + "computer_2": { + "type": "computer", + "size": 1 + }, + "computer_3": { + "type": "computer", + "size": 1 + }, + "computer": { + "type": "computer", + "size": 2, + "size_min": 1 + }, + "fuel_scoop_s1_left": { + "type": "fuel_scoop", + "size": 1, + "tag": "tag_fuel_scoop_s1_left", + "hardpoint": true + }, + "fuel_scoop_s1_right": { + "type": "fuel_scoop", + "size": 1, + "tag": "tag_fuel_scoop_s1_right", + "hardpoint": true + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 3, + "size_min": 2 + }, + "thruster": { + "type": "thruster", + "size": 3, + "count": 12 + } }, - "roles": ["mercenary", "merchant", "pirate", "courier"], - "effective_exhaust_velocity": 18900000, + "roles": ["pirate","merchant","mercenary","courier"], + "effective_exhaust_velocity": 15700000, "thruster_fuel_use": -1.0, - "fuel_tank_mass": 135, - "hyperdrive_class": 4, + "fuel_tank_mass": 116, + "hyperdrive_class": 3, - "forward_thrust": 9000000, + "forward_thrust": 9440000, "forward_acceleration_cap": 31.392, - "reverse_thrust": 9000000, - "reverse_acceleration_cap": 19.62, - "up_thrust": 14000000, + "reverse_thrust": 3640000, + "reverse_acceleration_cap": 24.525, + "up_thrust": 7380000, "up_acceleration_cap": 25.506, - "down_thrust": 6000000, + "down_thrust": 3120000, "down_acceleration_cap": 19.62, - "left_thrust": 6000000, + "left_thrust": 2080000, "left_acceleration_cap": 19.62, - "right_thrust": 6000000, + "right_thrust": 2080000, "right_acceleration_cap": 19.62, - "angular_thrust": 56321789.8068352, + "angular_thrust": 56321789, - "front_cross_section": 78, - "side_cross_section": 142, - "top_cross_section": 223, + "front_cross_section": 78.002, + "side_cross_section": 142.002, + "top_cross_section": 223.002, "front_drag_coeff": 0.4, "side_drag_coeff": 0.8, "top_drag_coeff": 0.9, "lift_coeff": 0.3, - "aero_stability": 1.6 + "aero_stability": 1.4 } diff --git a/data/ships/storeria.json b/data/ships/storeria.json index 6046add6c9..1e47b6cdcf 100644 --- a/data/ships/storeria.json +++ b/data/ships/storeria.json @@ -5,33 +5,134 @@ "shield_model": "storeria_shield", "manufacturer": "opli", "ship_class": "medium_freighter", - "min_crew": 1, + "min_crew": 3, "max_crew": 5, - "price": 1219170, - "hull_mass": 500, + "price": 897564, + "hull_mass": 225, + "structure_mass": 81, + "armor_mass": 84.2, + "volume": 4502, "atmospheric_pressure_limit": 3.2, - "capacity": 1650, - "slots": { - "engine": 1, - "cabin": 30, - "laser_front": 1, - "laser_rear": 1, - "missile": 1, - "cargo": 1500 + "capacity": 220, + "cargo": 960, + + "equipment_slots": { + "turret_s2_1": { + "type": "turret", + "size": 2, + "tag": "tag_turret_s2_1", + "hardpoint": true + }, + "turret_s2_2": { + "type": "turret", + "size": 2, + "tag": "tag_turret_s2_2", + "hardpoint": true + }, + "turret_s1_1": { + "type": "turret", + "size": 1, + "tag": "tag_turret_s1_1", + "hardpoint": true + }, + "turret_s1_2": { + "type": "turret", + "size": 1, + "tag": "tag_turret_s1_2", + "hardpoint": true + }, + "utility_s3_1": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "tag_utility_s3_1", + "hardpoint": true + }, + "utility_s3_2": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "tag_utility_s3_2", + "hardpoint": true + }, + "utility_s2_3": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "tag_utility_s2_3", + "hardpoint": true + }, + "computer_1": { + "type": "computer", + "size": 3, + "size_min": 1 + }, + "computer_2": { + "type": "computer", + "size": 2, + "size_min": 1 + }, + "computer_3": { + "type": "computer", + "size": 1, + "size_min": 1 + }, + "computer_4": { + "type": "computer", + "size": 1, + "size_min": 1 + }, + "fuel_scoop_s3": { + "type": "fuel_scoop", + "size": 3, + "size_min": 1, + "tag": "tag_fuel_scoop_s3", + "hardpoint": true + }, + "cabin_s1_1": { + "type": "cabin", + "size": 1 + }, + "cabin_s1_2": { + "type": "cabin", + "size": 1 + }, + "cabin_s1_3": { + "type": "cabin", + "size": 1 + }, + "shield_s3_1": { + "type": "shield", + "size": 3 + }, + "shield_s3_2": { + "type": "shield", + "size": 3 + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 4, + "size_min": 3 + }, + "thruster": { + "type": "thruster", + "size": 3, + "count": 36 + } }, - "roles": ["merchant"], - - "effective_exhaust_velocity": 11900000, + + "roles": ["merchant"], + "effective_exhaust_velocity": 17800000, "thruster_fuel_use": -1.0, - "fuel_tank_mass": 1000, - "hyperdrive_class": 7, + "fuel_tank_mass": 660, + "hyperdrive_class": 4, - "forward_thrust": 45000000, + "forward_thrust": 38700000, "forward_acceleration_cap": 16.677, - "reverse_thrust": 11000000, - "reverse_acceleration_cap": 11.772, - "up_thrust": 14000000, - "up_acceleration_cap": 14.715, + "reverse_thrust": 23000000, + "reverse_acceleration_cap": 14.715, + "up_thrust": 15500000, + "up_acceleration_cap": 11.772, "down_thrust": 11000000, "down_acceleration_cap": 11.772, "left_thrust": 11000000, @@ -39,11 +140,11 @@ "right_thrust": 11000000, "right_acceleration_cap": 11.772, - "angular_thrust": 196679265.992123, + "angular_thrust": 196679265.99, - "front_cross_section": 125, - "side_cross_section": 240, - "top_cross_section": 865, + "front_cross_section": 125.002, + "side_cross_section": 240.002, + "top_cross_section": 865.002, "front_drag_coeff": 0.67, "side_drag_coeff": 1.05, diff --git a/data/ships/vatakara.json b/data/ships/vatakara.json index a6f6ee6a5d..b969782d7c 100644 --- a/data/ships/vatakara.json +++ b/data/ships/vatakara.json @@ -2,29 +2,223 @@ "model": "vatakara", "name": "Vatakara", "cockpit": "", - "shield_model": "malabar_shield", - "manufacturer": "mandarava_csepel", + "shield_model": "vatakara_shield", + "manufacturer": "mandarava-csepel", "ship_class": "heavy_freighter", - "min_crew": 1, + "min_crew": 4, "max_crew": 8, - "price": 2655296, - "hull_mass": 890, + "price": 2833883, + "hull_mass": 576, + "structure_mass": 158, + "armor_mass": 256.7, + "volume": 14162, "atmospheric_pressure_limit": 4.9, - "capacity": 3200, - "slots": { - "engine": 1, - "cabin": 10, - "laser_front": 1, - "laser_rear": 1, - "missile": 6, - "cargo": 2900 + "capacity": 484, + "cargo": 2080, + + "equipment_slots": { + "cabin_s4_1": { + "type": "cabin", + "size": 4 + }, + "cabin_s2_2": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_3": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_4": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_5": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_6": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_7": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_8": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_9": { + "type": "cabin", + "size": 2 + }, + "cabin_s2_10": { + "type": "cabin", + "size": 2 + }, + "fuel_scoop": { + "type": "", + "size": 5, + "size_min": 3, + "tag": "", + "hardpoint": true + }, + "computer_1": { + "type": "computer", + "size": 3, + "size_min": 1 + }, + "computer_2": { + "type": "computer", + "size": 2, + "size_min": 1 + }, + "computer_3": { + "type": "computer", + "size": 1 + }, + "computer_4": { + "type": "computer", + "size": 1 + }, + "utility_s1_1": { + "type": "utility", + "size": 1, + "tag": "", + "hardpoint": true + }, + "utility_s1_2": { + "type": "utility", + "size": 1, + "tag": "", + "hardpoint": true + }, + "utility_s2_3": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "", + "hardpoint": true + }, + "utility_s2_4": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "", + "hardpoint": true + }, + "utility_s3_5": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "", + "hardpoint": true + }, + "utility_s3_6": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "", + "hardpoint": true + }, + "utility_s4_9": { + "type": "utility", + "size": 4, + "size_min": 2, + "tag": "", + "hardpoint": true + }, + "utility_s5_11": { + "type": "utility", + "size": 5, + "size_min": 3, + "tag": "", + "hardpoint": true + }, + "weapon_s3_1": { + "type": "weapon", + "size": 3, + "size_min": 2, + "i18n_key": "HARDPOINT_WEAPON_FRONT", + "tag": "", + "hardpoint": true, + "gimbal": [30,30] + }, + "weapon_s3_2": { + "type": "weapon", + "size": 3, + "size_min": 2, + "i18n_key": "HARDPOINT_WEAPON_REAR", + "tag": "", + "hardpoint": true, + "gimbal": [30,30] + }, + "shield_s3_1": { + "type": "shield", + "size": 3, + "size_min": 2 + }, + "shield_s3_2": { + "type": "shield", + "size": 3, + "size_min": 2 + }, + "shield_s3_3": { + "type": "shield", + "size": 3, + "size_min": 2 + }, + "missile_bay_1 (cap:5xS2)": { + "type": "missile_bay.opli_internal", + "size": 2, + "i18n_key": "HARDPOINT_MISSILE_BAY" + }, + "missile_bay_2 (cap:5xS2)": { + "type": "missile_bay.opli_internal", + "size": 2, + "i18n_key": "HARDPOINT_MISSILE_BAY" + }, + "sensor_s3_1": { + "type": "", + "size": 3, + "size_min": 2 + }, + "sensor_s3_2": { + "type": "", + "size": 3, + "size_min": 2 + }, + "sensor_s2_3": { + "type": "", + "size": 2 + }, + "sensor_s2_4": { + "type": "", + "size": 2 + }, + "shield_s3_4": { + "type": "shield", + "size": 3, + "size_min": 2 + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 5, + "size_min": 3 + }, + "thruster": { + "type": "thruster", + "size": 5, + "count": 12 + } }, + "roles": ["merchant"], - - "effective_exhaust_velocity": 16900000, + "effective_exhaust_velocity": 14900000, "thruster_fuel_use": -1.0, - "fuel_tank_mass": 2200, - "hyperdrive_class": 9, + "fuel_tank_mass": 2135, + "hyperdrive_class": 4, "forward_thrust": 120000000, "forward_acceleration_cap": 19.62, @@ -39,11 +233,11 @@ "right_thrust": 27000000, "right_acceleration_cap": 12.1644, - "angular_thrust": 555171928.095946, + "angular_thrust": 555171928.1, - "front_cross_section": 635, - "side_cross_section": 1215, - "top_cross_section": 980, + "front_cross_section": 635.002, + "side_cross_section": 1215.002, + "top_cross_section": 980.002, "front_drag_coeff": 1.3, "side_drag_coeff": 1.21, diff --git a/data/ships/venturestar.json b/data/ships/venturestar.json index 7b80f70f1f..1bdd7586f9 100644 --- a/data/ships/venturestar.json +++ b/data/ships/venturestar.json @@ -1,30 +1,150 @@ { "model": "venturestar", "name": "Venturestar", - "shield_model": "venturestar_shield", "cockpit": "", + "shield_model": "venturestar_shield", "manufacturer": "albr", "ship_class": "medium_freighter", "min_crew": 2, "max_crew": 5, - "price": 1127263, - "hull_mass": 460, + "price": 1182421, + "hull_mass": 239, + "structure_mass": 68.1, + "armor_mass": 108.4, + "volume": 4002, "atmospheric_pressure_limit": 7.2, - "capacity": 1160, - "slots": { - "engine": 1, - "cabin": 35, - "laser_front": 1, - "laser_rear": 1, - "missile": 6, - "cargo": 880 + "capacity": 340, + "cargo": 900, + + "equipment_slots": { + "utility_s4_1": { + "type": "utility", + "size": 4, + "size_min": 1, + "tag": "tag_utility_s4_1", + "hardpoint": true + }, + "utility_s3_2": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "tag_utility_s3_2", + "hardpoint": true + }, + "utility_s3_3": { + "type": "utility", + "size": 3, + "size_min": 1, + "tag": "tag_utility_s3_3", + "hardpoint": true + }, + "utility_s2_4": { + "type": "utility", + "size": 2, + "size_min": 1, + "tag": "tag_utility_s2_4", + "hardpoint": true + }, + "weapon_front_s4_1": { + "type": "weapon", + "size": 4, + "i18n_key": "HARDPOINT_WEAPON_FRONT", + "tag": "tag_weapon_front_s4_1", + "hardpoint": true, + "gimbal": [6,6] + }, + "weapon_front_s4_2": { + "type": "weapon", + "size": 4, + "i18n_key": "HARDPOINT_WEAPON_FRONT", + "tag": "tag_weapon_front_s4_2", + "hardpoint": true, + "gimbal": [6,6] + }, + "pylon_s4_l": { + "type": "pylon", + "size": 4, + "i18n_key": "HARDPOINT_PYLON_LEFT", + "tag": "tag_pylon_s4_l", + "hardpoint": true + }, + "pylon_s4_r": { + "type": "pylon", + "size": 4, + "i18n_key": "HARDPOINT_PYLON_RIGHT", + "tag": "tag_pylon_s4_r", + "hardpoint": true + }, + "shield_s4_1": { + "type": "shield", + "size": 4, + "size_min": 3 + }, + "shield_s4_2": { + "type": "shield", + "size": 4, + "size_min": 3 + }, + "cabin_s2_1": { + "type": "cabin", + "size": 2 + }, + "fuel_scoop_l": { + "type": "fuel_scoop", + "size": 2, + "size_min": 1, + "tag": "tag_fuel_scoop_l", + "hardpoint": true + }, + "fuel_scoop_r": { + "type": "fuel_scoop", + "size": 2, + "size_min": 1, + "tag": "tag_fuel_scoop_r", + "hardpoint": true + }, + "computer_2": { + "type": "computer", + "size": 2, + "size_min": 1 + }, + "computer_3": { + "type": "computer", + "size": 2, + "size_min": 1 + }, + "computer_1": { + "type": "computer", + "size": 3, + "size_min": 1 + }, + "sensor": { + "type": "sensor", + "size": 4, + "size_min": 1 + }, + "sensor_2": { + "type": "sensor", + "size": 3, + "size_min": 1 + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 4, + "size_min": 4 + }, + "thruster": { + "type": "thruster", + "size": 4, + "count": 8 + } }, - "roles": ["merchant"], - - "effective_exhaust_velocity": 21700000, + + "roles": ["merchant","pirate"], + "effective_exhaust_velocity": 14000000, "thruster_fuel_use": -1.0, - "fuel_tank_mass": 700, - "hyperdrive_class": 6, + "fuel_tank_mass": 740, + "hyperdrive_class": 4, "forward_thrust": 55000000, "forward_acceleration_cap": 21.582, @@ -39,11 +159,11 @@ "right_thrust": 30000000, "right_acceleration_cap": 17.2656, - "angular_thrust": 201149249.310126, + "angular_thrust": 201149249.31, - "front_cross_section": 120, - "side_cross_section": 280, - "top_cross_section": 1190, + "front_cross_section": 120.002, + "side_cross_section": 280.002, + "top_cross_section": 1190.002, "front_drag_coeff": 0.06, "side_drag_coeff": 0.21, diff --git a/data/ships/wave.json b/data/ships/wave.json index 49bae16351..e4c2079c6d 100644 --- a/data/ships/wave.json +++ b/data/ships/wave.json @@ -7,49 +7,148 @@ "ship_class": "medium_fighter", "min_crew": 1, "max_crew": 1, - "price": 55305, - "hull_mass": 13, + "price": 84380, + "hull_mass": 41, + "structure_mass": 12.4, + "armor_mass": 24.2, + "volume": 434, "atmospheric_pressure_limit": 14, - "capacity": 30, - "slots": { - "engine": 1, - "cabin": 0, - "scoop": 2, - "laser_front": 1, - "laser_rear": 1, - "missile": 12, - "cargo": 30 + "capacity": 22, + "cargo": 6, + + "equipment_slots": { + "weapon_left_s1": { + "type": "weapon", + "size": 1, + "i18n_key": "HARDPOINT_WEAPON_LEFT", + "tag": "tag_weapon_left_s1", + "hardpoint": true, + "gimbal": [0,0] + }, + "weapon_right_s1": { + "type": "weapon", + "size": 1, + "i18n_key": "HARDPOINT_WEAPON_RIGHT", + "tag": "tag_weapon_right_s1", + "hardpoint": true, + "gimbal": [0,0] + }, + "laser_front_s2": { + "type": "weapon", + "size": 2, + "size_min": 1, + "i18n_key": "HARDPOINT_WEAPON_FRONT", + "tag": "tag_laser_front_s2", + "hardpoint": true, + "gimbal": [4,4] + }, + "missile_rack_s3_left": { + "type": "pylon.rack", + "size": 3, + "i18n_key": "HARDPOINT_PYLON_LEFT", + "tag": "tag_missile_rack_s3_left", + "hardpoint": true + }, + "missile_rack_s3_right": { + "type": "pylon.rack", + "size": 3, + "i18n_key": "HARDPOINT_PYLON_RIGHT", + "tag": "tag_missile_rack_s3_right", + "hardpoint": true + }, + "pylon_s2_left_wing": { + "type": "pylon", + "size": 2, + "i18n_key": "HARDPOINT_PYLON_LEFT_WING", + "tag": "tag_pylon_s2_left_wing", + "hardpoint": true + }, + "pylon_s2_right_wing": { + "type": "pylon", + "size": 2, + "i18n_key": "HARDPOINT_PYLON_RIGHT_WING", + "tag": "tag_pylon_s2_right_wing", + "hardpoint": true + }, + "missile_s3_left": { + "type": "missile", + "size": 3, + "tag": "tag_missile_s3_left", + "hardpoint": true + }, + "missile_s3_right": { + "type": "missile", + "size": 3, + "tag": "tag_missile_s3_right", + "hardpoint": true + }, + "computer_2": { + "type": "computer", + "size": 1 + }, + "shield_s2_1": { + "type": "shield", + "size": 2, + "size_min": 1 + }, + "utility_s1_1": { + "type": "utility", + "size": 1, + "tag": "tag_utility_s1_1", + "hardpoint": true + }, + "utility_s1_2": { + "type": "utility", + "size": 1, + "tag": "tag_utility_s1_2", + "hardpoint": true + }, + "shield_s2_2": { + "type": "shield", + "size": 2, + "size_min": 1 + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 2, + "size_min": 2 + }, + "thruster": { + "type": "thruster", + "size": 2, + "count": 12 + } }, - "roles": ["mercenary", "pirate", "courier"], - + + "roles": ["pirate","mercenary"], "effective_exhaust_velocity": 16900000, "thruster_fuel_use": -1.0, "fuel_tank_mass": 22, - "hyperdrive_class": 1, + "hyperdrive_class": 2, - "forward_thrust": 2800000, + "forward_thrust": 3060000, "forward_acceleration_cap": 44.145, - "reverse_thrust": 700000, - "reverse_acceleration_cap": 11.772, - "up_thrust": 2000000, - "up_acceleration_cap": 39.24, - "down_thrust": 800000, - "down_acceleration_cap": 19.62, - "left_thrust": 800000, - "left_acceleration_cap": 19.62, - "right_thrust": 800000, - "right_acceleration_cap": 19.62, + "reverse_thrust": 1040000, + "reverse_acceleration_cap": 23.544, + "up_thrust": 1160000, + "up_acceleration_cap": 17.658, + "down_thrust": 1160000, + "down_acceleration_cap": 11.772, + "left_thrust": 840000, + "left_acceleration_cap": 13.734, + "right_thrust": 840000, + "right_acceleration_cap": 13.734, - "angular_thrust": 4505743.18454681, + "angular_thrust": 4505743, - "front_cross_section": 35.2, - "side_cross_section": 46, - "top_cross_section": 345, + "front_cross_section": 35.202, + "side_cross_section": 46.002, + "top_cross_section": 345.002, "front_drag_coeff": 0.04, - "side_drag_coeff": 0.1, - "top_drag_coeff": 0.85, + "side_drag_coeff": 0.21, + "top_drag_coeff": 1.05, "lift_coeff": 1, - "aero_stability": 2.6 + "aero_stability": 2.2 } diff --git a/data/ships/xylophis.json b/data/ships/xylophis.json index cacf50144f..c8c087c49b 100644 --- a/data/ships/xylophis.json +++ b/data/ships/xylophis.json @@ -7,22 +7,42 @@ "ship_class": "light_passenger_shuttle", "min_crew": 1, "max_crew": 2, - "price": 16872, + "price": 11685, "hull_mass": 5, + "structure_mass": 2.1, + "armor_mass": 2.1, + "volume": 48, "atmospheric_pressure_limit": 3, - "capacity": 10, - "slots": { - "engine": 1, - "cabin": 1, - "scoop": 0, - "laser_front": 1, - "cargo": 10 + "capacity": 5.5, + "cargo": 8, + + "equipment_slots": { + "utility_s1": { + "type": "utility", + "size": 1, + "tag": "tag_utility_s1", + "hardpoint": true + }, + "computer_1": { + "type": "computer", + "size": 1 + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 1, + "size_min": 1 + }, + "thruster": { + "type": "thruster", + "size": 1, + "count": 12 + } }, - "roles": ["mercenary", "merchant", "pirate", "courier"], - "effective_exhaust_velocity": 9900000, + "roles": [], + "effective_exhaust_velocity": 10900000, "thruster_fuel_use": -1.0, - "fuel_tank_mass": 4, + "fuel_tank_mass": 5, "hyperdrive_class": 0, "forward_thrust": 280000, @@ -38,11 +58,11 @@ "right_thrust": 150000, "right_acceleration_cap": 9.81, - "angular_thrust": 354022.678785821, + "angular_thrust": 354022.68, - "front_cross_section": 7, - "side_cross_section": 17.5, - "top_cross_section": 32, + "front_cross_section": 7.002, + "side_cross_section": 17.502, + "top_cross_section": 32.002, "front_drag_coeff": 0.56, "side_drag_coeff": 0.84, From 0178192ccf3499b0f8a201bb18075aeb8c8719a7 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 2 Nov 2024 23:55:29 -0400 Subject: [PATCH 105/119] Update some lang strings for shipdefs --- data/lang/equipment-core/en.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/data/lang/equipment-core/en.json b/data/lang/equipment-core/en.json index a9680487a6..a075d077b9 100644 --- a/data/lang/equipment-core/en.json +++ b/data/lang/equipment-core/en.json @@ -475,10 +475,6 @@ "description": "Category name for hull-mounted equipment", "message": "Hull Mounts" }, - "HARDPOINT_CARGO_SCOOP": { - "description": "Name for the 'Cargo Scoop' equipment hardpoint", - "message": "Cargo Scoop" - }, "HARDPOINT_FUEL_SCOOP": { "description": "Name for the 'Fuel Scoop' equipment hardpoint", "message": "Fuel Scoop" @@ -499,14 +495,14 @@ "description": "Name for the 'Missile Array' equipment hardpoint", "message": "Missile Array" }, - "HARDPOINT_SCOOP": { - "description": "Name for a generic scoop hardpoint", - "message": "Scoop" - }, "HARDPOINT_UTILITY": { "description": "", "message": "Utility" }, + "HARDPOINT_WEAPON": { + "description": "", + "message": "Weapon" + }, "HARDPOINT_WEAPON_FRONT": { "description": "Name for the 'Laser Front' equipment hardpoint", "message": "Front Weapon" @@ -523,6 +519,10 @@ "description": "", "message": "Right Nose" }, + "HARDPOINT_WEAPON_REAR": { + "description": "", + "message": "Rear Weapon" + }, "SLOT_SENSOR": { "description": "", "message": "Sensor" From 57fd8d8b45cc534b698d520f9878628c9cf1126c Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sat, 2 Nov 2024 23:55:47 -0400 Subject: [PATCH 106/119] Properly wrap ui.beginTable() in an if block --- data/pigui/libs/equip-card.lua | 33 ++++++++++++----------- data/pigui/libs/equipment-outfitter.lua | 35 ++++++++++++++----------- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/data/pigui/libs/equip-card.lua b/data/pigui/libs/equip-card.lua index 396c3428c2..5c7b0d2544 100644 --- a/data/pigui/libs/equip-card.lua +++ b/data/pigui/libs/equip-card.lua @@ -115,28 +115,29 @@ end ---@param data UI.EquipCard.Data function EquipCard.drawEquipStats(data) - ui.beginTable("EquipStats", 2, { "NoSavedSettings" }) + if ui.beginTable("EquipStats", 2, { "NoSavedSettings" }) then - ui.tableSetupColumn("##name", { "WidthStretch" }) - ui.tableSetupColumn("##value", { "WidthFixed" }) + ui.tableSetupColumn("##name", { "WidthStretch" }) + ui.tableSetupColumn("##value", { "WidthFixed" }) - ui.pushTextWrapPos(-1) - for i, tooltip in ipairs(data.stats) do - ui.tableNextRow() + ui.pushTextWrapPos(-1) + for i, tooltip in ipairs(data.stats) do + ui.tableNextRow() - ui.tableSetColumnIndex(0) - ui.text(tooltip[1] .. ":") + ui.tableSetColumnIndex(0) + ui.text(tooltip[1] .. ":") - ui.tableSetColumnIndex(1) - ui.icon(tooltip[2], Vector2(ui.getTextLineHeight(), ui.getTextLineHeight()), ui.theme.colors.font) - ui.sameLine(0, ui.getTextLineHeight() / 4.0) + ui.tableSetColumnIndex(1) + ui.icon(tooltip[2], Vector2(ui.getTextLineHeight(), ui.getTextLineHeight()), ui.theme.colors.font) + ui.sameLine(0, ui.getTextLineHeight() / 4.0) - local value, format = tooltip[3], tooltip[4] - ui.text(format(value)) - end - ui.popTextWrapPos() + local value, format = tooltip[3], tooltip[4] + ui.text(format(value)) + end + ui.popTextWrapPos() - ui.endTable() + ui.endTable() + end end local slotColors = { Text = colors.equipScreenBgText } diff --git a/data/pigui/libs/equipment-outfitter.lua b/data/pigui/libs/equipment-outfitter.lua index 8f4c6c9c95..91bcbde06e 100644 --- a/data/pigui/libs/equipment-outfitter.lua +++ b/data/pigui/libs/equipment-outfitter.lua @@ -445,15 +445,16 @@ function Outfitter:drawSortButton(id, label, state) end function Outfitter:drawSortRow() - ui.beginTable("sort", #Outfitter.SortKeys) - ui.tableNextRow() + if ui.beginTable("sort", #Outfitter.SortKeys) then + ui.tableNextRow() - for i, sort in ipairs(Outfitter.SortKeys) do - ui.tableSetColumnIndex(i - 1) - self:drawSortButton(sort.id, sort.label, self.sortId == sort.id and self.sortState or nil) - end + for i, sort in ipairs(Outfitter.SortKeys) do + ui.tableSetColumnIndex(i - 1) + self:drawSortButton(sort.id, sort.label, self.sortId == sort.id and self.sortState or nil) + end - ui.endTable() + ui.endTable() + end end ---@param label string @@ -523,18 +524,20 @@ function Outfitter:renderCompareStats() if self.compare_stats then - ui.beginTable("##CompareEquipStats", 3) + if ui.beginTable("##CompareEquipStats", 3) then - ui.tableSetupColumn("##name", { "WidthStretch" }) - ui.tableSetupColumn("##selected", { "WidthFixed" }) - ui.tableSetupColumn("##current", { "WidthFixed" }) + ui.tableSetupColumn("##name", { "WidthStretch" }) + ui.tableSetupColumn("##selected", { "WidthFixed" }) + ui.tableSetupColumn("##current", { "WidthFixed" }) - for _, row in ipairs(self.compare_stats) do - ui.tableNextRow() - self:renderCompareRow(row.label, row.a, row.b) - end + for _, row in ipairs(self.compare_stats) do + ui.tableNextRow() + self:renderCompareRow(row.label, row.a, row.b) + end - ui.endTable() + ui.endTable() + + end end From 125630d474b3924ce85d5c3fac2fc9ce1820e253 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Sun, 3 Nov 2024 09:03:27 -0500 Subject: [PATCH 107/119] Fix comment descriptions, bowfin missile bay slot Co-authored-by: Michael Werle --- data/lang/ui-core/en.json | 4 ++-- data/modules/Equipment/Weapons.lua | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/data/lang/ui-core/en.json b/data/lang/ui-core/en.json index 59458f320a..5914228943 100644 --- a/data/lang/ui-core/en.json +++ b/data/lang/ui-core/en.json @@ -2628,11 +2628,11 @@ "message": "Installed" }, "SELL_EQUIP": { - "description": "Button text to sell installed equipment. May used in a singular or plural context.", + "description": "Button text to sell installed equipment. May be used in a singular or plural context.", "message": "Sell {name}" }, "BUY_EQUIP": { - "description": "Button text to buy selected equipment. May used in a singular or plural context.", + "description": "Button text to buy selected equipment. May be used in a singular or plural context.", "message": "Buy {name}" }, "EXPAND": { diff --git a/data/modules/Equipment/Weapons.lua b/data/modules/Equipment/Weapons.lua index 3eade59d9d..64f7db00a9 100644 --- a/data/modules/Equipment/Weapons.lua +++ b/data/modules/Equipment/Weapons.lua @@ -338,11 +338,11 @@ Equipment.Register("missile_bay.opli_internal_s2", EquipType.New { icon_name="equip_missile_unguided" }) -Equipment.Register("missile_bay.bowfin", EquipType.New { +Equipment.Register("missile_bay.bowfin_internal", EquipType.New { l10n_key="OKB_KALURI_BOWFIN_MISSILE_RACK", price=150, purchasable=true, tech_level=1, volume=0.0, mass=0.2, - slot = { type = "missile_bay.bowfin", size=2, hardpoint=true }, + slot = { type = "missile_bay.bowfin_internal", size=2, hardpoint=true }, provides_slots = { Slot:clone { id = "1", type = "missile", size = 1, hardpoint = true }, Slot:clone { id = "2", type = "missile", size = 1, hardpoint = true }, From 092d4d5c2fc1136f5f5f78ac164a7d57182f90c5 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 4 Nov 2024 14:20:32 -0500 Subject: [PATCH 108/119] Outfitter: use sort translation strings from ui-core --- data/lang/equipment-core/en.json | 16 ---------------- data/lang/ui-core/en.json | 4 ++++ data/pigui/libs/equipment-outfitter.lua | 8 ++++---- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/data/lang/equipment-core/en.json b/data/lang/equipment-core/en.json index a075d077b9..2cb8a75c20 100644 --- a/data/lang/equipment-core/en.json +++ b/data/lang/equipment-core/en.json @@ -575,22 +575,6 @@ "description": "Category header for miscellaneous equipment", "message": "Miscellaneous" }, - "SORT_NAME": { - "description": "", - "message": "Name" - }, - "SORT_WEIGHT": { - "description": "", - "message": "Weight" - }, - "SORT_VOLUME": { - "description": "", - "message": "Volume" - }, - "SORT_PRICE": { - "description": "", - "message": "Price" - }, "OPLI_INTERNAL_MISSILE_RACK_S2": { "description": "", "message": "OPLI Internal Missile Rack" diff --git a/data/lang/ui-core/en.json b/data/lang/ui-core/en.json index 5914228943..dffadafac5 100644 --- a/data/lang/ui-core/en.json +++ b/data/lang/ui-core/en.json @@ -2646,5 +2646,9 @@ "CANNOT_SELL_NONEMPTY_EQUIP": { "description": "", "message": "Cannot sell an equipment item unless it is empty." + }, + "VOLUME": { + "description": "The volume property of some object", + "message": "Volume" } } diff --git a/data/pigui/libs/equipment-outfitter.lua b/data/pigui/libs/equipment-outfitter.lua index 91bcbde06e..4178f92d34 100644 --- a/data/pigui/libs/equipment-outfitter.lua +++ b/data/pigui/libs/equipment-outfitter.lua @@ -137,10 +137,10 @@ end local Outfitter = utils.class("UI.EquipmentOutfitter", Module) Outfitter.SortKeys = { - { id = "name", label = le.SORT_NAME }, - { id = "mass", label = le.SORT_WEIGHT }, - { id = "volume", label = le.SORT_VOLUME }, - { id = "price", label = le.SORT_PRICE }, + { id = "name", label = l.NAME_OBJECT }, + { id = "mass", label = l.MASS }, + { id = "volume", label = l.VOLUME }, + { id = "price", label = l.PRICE }, } ---@type table From 5aae2f3b6fc431f37fea3bf984a6baa3d5ad8cd1 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 4 Nov 2024 14:31:49 -0500 Subject: [PATCH 109/119] Rename Passengers.CountCabins -> CountBerths - Berths is a more appropriate term and avoids semantic overload between "cabin equipment" and an individual "passenger cabin" inside that equipment --- data/libs/Passengers.lua | 8 ++++---- data/modules/SearchRescue/SearchRescue.lua | 8 ++++---- data/modules/Taxi/Taxi.lua | 2 +- data/pigui/modules/info-view/01-ship-info.lua | 4 ++-- data/pigui/modules/info-view/03-econ-trade.lua | 10 +++++----- data/pigui/views/station-view.lua | 8 ++++---- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/data/libs/Passengers.lua b/data/libs/Passengers.lua index 9c1df1c808..4a809792da 100644 --- a/data/libs/Passengers.lua +++ b/data/libs/Passengers.lua @@ -14,21 +14,21 @@ local Passengers = {} -- Function: CountOccupiedCabins -- --- Return the number of occupied cabins present on the ship +-- Return the number of occupied passenger berths present on the ship -- ---@param ship Ship ---@return integer -function Passengers.CountOccupiedCabins(ship) +function Passengers.CountOccupiedBerths(ship) return ship["cabin_occupied_cap"] or 0 end -- Function: CountFreeCabins -- --- Return the number of unoccupied cabins present on the ship +-- Return the number of unoccupied passenger berths present on the ship -- ---@param ship Ship ---@return integer -function Passengers.CountFreeCabins(ship) +function Passengers.CountFreeBerths(ship) return (ship["cabin_cap"] or 0) - (ship["cabin_occupied_cap"] or 0) end diff --git a/data/modules/SearchRescue/SearchRescue.lua b/data/modules/SearchRescue/SearchRescue.lua index 97a8f133b0..86540d20c4 100644 --- a/data/modules/SearchRescue/SearchRescue.lua +++ b/data/modules/SearchRescue/SearchRescue.lua @@ -352,12 +352,12 @@ end local passengersPresent = function (ship) -- Check if any passengers are present on the ship. - return Passengers.CountOccupiedCabins(ship) > 0 + return Passengers.CountOccupiedBerths(ship) > 0 end local passengerSpace = function (ship) -- Check if the ship has space for passengers. - return Passengers.CountFreeCabins(ship) > 0 + return Passengers.CountFreeBerths(ship) > 0 end local cargoPresent = function (ship, item) @@ -420,7 +420,7 @@ local isQualifiedFor = function(ad) -- collect equipment requirements per mission flavor local empty_cabins = ad.pickup_crew + ad.deliver_crew + ad.pickup_pass + ad.deliver_pass - local avail_cabins = Passengers.CountFreeCabins(Game.player) + local avail_cabins = Passengers.CountFreeBerths(Game.player) return avail_cabins >= empty_cabins end @@ -650,7 +650,7 @@ local createTargetShip = function (mission) ship:SetPattern(rand:Integer(1, model.numPatterns)) end - local available_cabins = Passengers.CountFreeCabins(ship) + local available_cabins = Passengers.CountFreeBerths(ship) assert(available_cabins >= math.max(mission.deliver_pass, mission.pickup_pass), "Not enough space in mission ship for all passengers!") diff --git a/data/modules/Taxi/Taxi.lua b/data/modules/Taxi/Taxi.lua index 80049c2518..8a2ac4d07f 100644 --- a/data/modules/Taxi/Taxi.lua +++ b/data/modules/Taxi/Taxi.lua @@ -182,7 +182,7 @@ local onChat = function (form, ref, option) form:SetMessage(howmany) elseif option == 3 then - if Passengers.CountFreeCabins(Game.player) < ad.group then + if Passengers.CountFreeBerths(Game.player) < ad.group then form:SetMessage(l.YOU_DO_NOT_HAVE_ENOUGH_CABIN_SPACE_ON_YOUR_SHIP) form:RemoveNavButton() return diff --git a/data/pigui/modules/info-view/01-ship-info.lua b/data/pigui/modules/info-view/01-ship-info.lua index 8457c37269..e70b6d395a 100644 --- a/data/pigui/modules/info-view/01-ship-info.lua +++ b/data/pigui/modules/info-view/01-ship-info.lua @@ -26,8 +26,8 @@ local function shipStats() local hyperdrive = equipSet:GetInstalledOfType("hyperdrive")[1] local frontWeapon = equipSet:GetInstalledOfType("weapon")[1] local rearWeapon = equipSet:GetInstalledOfType("weapon")[2] - local cabinEmpty = Passengers.CountFreeCabins(player) - local cabinOccupied = Passengers.CountOccupiedCabins(player) + local cabinEmpty = Passengers.CountFreeBerths(player) + local cabinOccupied = Passengers.CountOccupiedBerths(player) local cabinMaximum = cabinEmpty + cabinOccupied hyperdrive = hyperdrive or nil diff --git a/data/pigui/modules/info-view/03-econ-trade.lua b/data/pigui/modules/info-view/03-econ-trade.lua index 4cc05dd8f9..e9416d9d48 100644 --- a/data/pigui/modules/info-view/03-econ-trade.lua +++ b/data/pigui/modules/info-view/03-econ-trade.lua @@ -175,12 +175,12 @@ end -- Gauge bar for used/free cabins local function gauge_cabins() local player = Game.player - local cabins_free = Passengers.CountFreeCabins(player) - local cabins_used = Passengers.CountOccupiedCabins(player) - local cabins_total = cabins_used + cabins_free + local berths_free = Passengers.CountFreeBerths(player) + local berths_used = Passengers.CountOccupiedBerths(player) + local berths_total = berths_used + berths_free - gauge_bar(cabins_used, string.format('%%i %s / %i %s', l.USED, cabins_free, l.FREE), - 0, cabins_total, icons.personal) + gauge_bar(berths_used, string.format('%%i %s / %i %s', l.USED, berths_free, l.FREE), + 0, berths_total, icons.personal) end local function drawPumpDialog() diff --git a/data/pigui/views/station-view.lua b/data/pigui/views/station-view.lua index dc21bdb9e3..74ad0b1d51 100644 --- a/data/pigui/views/station-view.lua +++ b/data/pigui/views/station-view.lua @@ -66,13 +66,13 @@ if not stationView then ui.text(l.CABINS .. ': ') ui.sameLine() - local cabins_free = Passengers.CountFreeCabins(player) - local cabins_used = Passengers.CountOccupiedCabins(player) - local cabins_total = cabins_used + cabins_free + local berths_free = Passengers.CountFreeBerths(player) + local berths_used = Passengers.CountOccupiedBerths(player) + local berths_total = berths_used + berths_free gaugePos = ui.getWindowPos() + ui.getCursorPos() + Vector2(0, ui.getTextLineHeight() / 2) gaugeWidth = ui.getContentRegion().x - self.style.inventoryPadding.x - self.style.itemSpacing.x - ui.gauge(gaugePos, cabins_used, '', string.format('%%i %s / %i %s', l.USED, cabins_free, l.FREE), 0, cabins_total, icons.personal, colors.gaugeEquipmentMarket, '', gaugeWidth, ui.getTextLineHeight()) + ui.gauge(gaugePos, berths_used, '', string.format('%%i %s / %i %s', l.USED, berths_free, l.FREE), 0, berths_total, icons.personal, colors.gaugeEquipmentMarket, '', gaugeWidth, ui.getTextLineHeight()) ui.nextColumn() ui.text(legalText) ui.columns(1, '', false) From 3fa73a07c95d59546703b81a016c0cd9a0a9f35d Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 4 Nov 2024 14:42:47 -0500 Subject: [PATCH 110/119] Rename ShipDef.capacity -> ShipDef.equipCapacity - More clearly separate from cargo space / cargo capacity - Introduce new EQUIPMENT_CAPACITY translation string --- data/lang/ui-core/en.json | 4 ++++ data/libs/EquipSet.lua | 4 ++-- data/libs/HullConfig.lua | 4 ++-- data/meta/ShipDef.lua | 4 ++-- data/modules/Debug/DebugShip.lua | 2 +- data/modules/MissionUtils/ShipBuilder.lua | 4 ++-- data/modules/Pirates.lua | 2 +- data/modules/SearchRescue/SearchRescue.lua | 2 +- data/modules/TradeShips/Debug.lua | 2 +- data/modules/TradeShips/Events.lua | 2 +- data/pigui/modules/new-game-window/ship.lua | 2 +- data/pigui/modules/station-view/04-shipMarket.lua | 8 ++++---- src/lua/LuaShipDef.cpp | 2 +- 13 files changed, 23 insertions(+), 19 deletions(-) diff --git a/data/lang/ui-core/en.json b/data/lang/ui-core/en.json index dffadafac5..24443c5c00 100644 --- a/data/lang/ui-core/en.json +++ b/data/lang/ui-core/en.json @@ -2650,5 +2650,9 @@ "VOLUME": { "description": "The volume property of some object", "message": "Volume" + }, + "EQUIPMENT_CAPACITY": { + "description": "The equipment capacity of a ship", + "message": "Equipment Capacity" } } diff --git a/data/libs/EquipSet.lua b/data/libs/EquipSet.lua index c6e8933f34..1aa6c44922 100644 --- a/data/libs/EquipSet.lua +++ b/data/libs/EquipSet.lua @@ -106,7 +106,7 @@ function EquipSet:Constructor(ship) -- Initialize ship properties we're responsible for modifying self.ship:setprop("mass_cap", self.ship["mass_cap"] or 0) self.ship:setprop("equipVolume", self.ship.equipVolume or 0) - self.ship:setprop("totalVolume", self.ship.totalVolume or self.config.capacity) + self.ship:setprop("totalVolume", self.ship.totalVolume or self.config.equipCapacity) end -- Callback: OnShipTypeChanged @@ -139,7 +139,7 @@ function EquipSet:OnShipTypeChanged() -- HullConfig changed, need to reset volume and rebuild list of slots self.config = HullConfig.GetHullConfigs()[self.ship.shipId] - self.ship:setprop("totalVolume", self.config.capacity) + self.ship:setprop("totalVolume", self.config.equipCapacity) self.slotCache = {} self.idCache = {} diff --git a/data/libs/HullConfig.lua b/data/libs/HullConfig.lua index 23486d4d7a..bf4327203a 100644 --- a/data/libs/HullConfig.lua +++ b/data/libs/HullConfig.lua @@ -40,7 +40,7 @@ Slot.count = nil ---@type integer? local HullConfig = utils.proto("HullConfig") HullConfig.id = "" -HullConfig.capacity = 0 +HullConfig.equipCapacity = 0 -- Default slot config for a new shipdef -- Individual shipdefs can redefine slots or remove them by setting the slot to 'false' @@ -69,7 +69,7 @@ local function CreateShipConfig(def) Serializer:RegisterPersistent("ShipDef." .. def.id, newShip) newShip.id = def.id - newShip.capacity = def.capacity + newShip.equipCapacity = def.equipCapacity table.merge(newShip.slots, def.raw.equipment_slots or {}, function(name, slotDef) if slotDef == false then return name, nil end diff --git a/data/meta/ShipDef.lua b/data/meta/ShipDef.lua index d697d40826..99d5903ed1 100644 --- a/data/meta/ShipDef.lua +++ b/data/meta/ShipDef.lua @@ -27,8 +27,8 @@ ---@field topCrossSec number ---@field atmosphericPressureLimit number -- ----@field capacity number ----@field cargo integer +---@field equipCapacity number Equipment volume available on the ship +---@field cargo integer Number of units of cargo the ship can carry ---@field hullMass number ---@field fuelTankMass number ---@field basePrice number diff --git a/data/modules/Debug/DebugShip.lua b/data/modules/Debug/DebugShip.lua index 0f33210f76..f3b460b6b1 100644 --- a/data/modules/Debug/DebugShip.lua +++ b/data/modules/Debug/DebugShip.lua @@ -309,7 +309,7 @@ function DebugShipTool:drawShipDefInfo(shipDef) drawKeyValue("Tag", shipDef.tag) drawKeyValue("Cargo Capacity", shipDef.cargo) - drawKeyValue("Equip Capacity", shipDef.capacity) + drawKeyValue("Equip Capacity", shipDef.equipCapacity) drawKeyValue("Hull Mass", shipDef.hullMass) drawKeyValue("Fuel Tank Mass", shipDef.fuelTankMass) drawKeyValue("Base Price", shipDef.basePrice) diff --git a/data/modules/MissionUtils/ShipBuilder.lua b/data/modules/MissionUtils/ShipBuilder.lua index 36ceb3760e..553806c4fb 100644 --- a/data/modules/MissionUtils/ShipBuilder.lua +++ b/data/modules/MissionUtils/ShipBuilder.lua @@ -164,7 +164,7 @@ end function ShipPlan:SetConfig(shipConfig) self.config = shipConfig self.shipId = shipConfig.id - self.freeVolume = shipConfig.capacity + self.freeVolume = shipConfig.equipCapacity for _, slot in pairs(shipConfig.slots) do table.insert(self.slots, slot) @@ -276,7 +276,7 @@ function ShipBuilder.ComputeHullThreatFactor(shipDef) local threat = { id = shipDef.id } local armor = shipDef.hullMass - local totalMass = shipDef.hullMass + shipDef.fuelTankMass + shipDef.capacity * 0.5 + local totalMass = shipDef.hullMass + shipDef.fuelTankMass + shipDef.equipCapacity * 0.5 local forwardAccel = shipDef.linearThrust["FORWARD"] / (1000.0 * totalMass) local crossSectionAvg = (shipDef.topCrossSec + shipDef.sideCrossSec + shipDef.frontCrossSec) / 3.0 diff --git a/data/modules/Pirates.lua b/data/modules/Pirates.lua index ef71e859c9..b73fee7bad 100644 --- a/data/modules/Pirates.lua +++ b/data/modules/Pirates.lua @@ -33,7 +33,7 @@ local onEnterSystem = function (player) local ship = ShipBuilder.MakeShipAroundStar(MissionUtils.ShipTemplates.GenericPirate, threat, 8, 12) -- pirates know how big cargo hold the ship model has - local playerCargoCapacity = ShipDef[player.shipId].capacity + local playerCargoCapacity = ShipDef[player.shipId].equipCapacity -- Pirate attack probability proportional to how fully loaded cargo hold is. local discount = 2 -- discount on 2t for small ships. diff --git a/data/modules/SearchRescue/SearchRescue.lua b/data/modules/SearchRescue/SearchRescue.lua index 86540d20c4..27838f3fca 100644 --- a/data/modules/SearchRescue/SearchRescue.lua +++ b/data/modules/SearchRescue/SearchRescue.lua @@ -496,7 +496,7 @@ local createTargetShipParameters = function (flavour, planet) end ----> has to be able to take off from the planet with full fuel mass - local fullMass = def.hullMass + def.capacity + def.fuelTankMass + local fullMass = def.hullMass + def.equipCapacity + def.fuelTankMass local upAccelFull = math.abs(def.linearThrust.UP / (1000 * fullMass)) if upAccelFull <= planet:GetSystemBody().gravity then diff --git a/data/modules/TradeShips/Debug.lua b/data/modules/TradeShips/Debug.lua index 5281b88770..95df107e36 100644 --- a/data/modules/TradeShips/Debug.lua +++ b/data/modules/TradeShips/Debug.lua @@ -332,7 +332,7 @@ debugView.registerTab('debug-trade-ships', { }) end - local capacity = ShipDef[ship.shipId].capacity + local capacity = ShipDef[ship.shipId].equipCapacity arrayTable.draw("tradeships_traderequipment", equipItems, ipairs, { { name = "Name", key = "name", string = true }, diff --git a/data/modules/TradeShips/Events.lua b/data/modules/TradeShips/Events.lua index 6e22a8546a..2a960cc848 100644 --- a/data/modules/TradeShips/Events.lua +++ b/data/modules/TradeShips/Events.lua @@ -246,7 +246,7 @@ local onShipHit = function (ship, attacker) -- maybe jettison a bit of cargo if Engine.rand:Number(1) < trader.chance then local cargo_type = nil - local max_cap = ShipDef[ship.shipId].capacity + local max_cap = ShipDef[ship.shipId].equipCapacity ---@type CargoManager local cargoMgr = ship:GetComponent('CargoManager') diff --git a/data/pigui/modules/new-game-window/ship.lua b/data/pigui/modules/new-game-window/ship.lua index 2f49ee5be6..11ea54c7db 100644 --- a/data/pigui/modules/new-game-window/ship.lua +++ b/data/pigui/modules/new-game-window/ship.lua @@ -938,7 +938,7 @@ function ShipSummary:prepareAndValidateParamList() local eq_n_cargo = { valid = true } freeCargo = math.max(freeCargo, 0) local paramList = { - rowWithAlert(eq_n_cargo, lui.CAPACITY, ShipCargo.mass + ShipEquip.mass, def.capacity, greater, 't'), + rowWithAlert(eq_n_cargo, lui.CAPACITY, ShipCargo.mass + ShipEquip.mass, def.equipCapacity, greater, 't'), rowWithAlert(self.cargo, lui.CARGO_SPACE, ShipCargo.mass, freeCargo, greater, 't'), --rowWithAlert(self.equip, lui.FRONT_WEAPON, usedSlots.laser_front, def.equipSlotCapacity.laser_front, greater), --rowWithAlert(self.equip, lui.REAR_WEAPON, usedSlots.laser_rear, def.equipSlotCapacity.laser_rear, greater), diff --git a/data/pigui/modules/station-view/04-shipMarket.lua b/data/pigui/modules/station-view/04-shipMarket.lua index 568509da82..ff16518a9e 100644 --- a/data/pigui/modules/station-view/04-shipMarket.lua +++ b/data/pigui/modules/station-view/04-shipMarket.lua @@ -351,8 +351,8 @@ end function FormatAndCompareShips:Constructor(def, b) self.column = 0 self.emptyMass = def.hullMass + def.fuelTankMass - self.fullMass = def.hullMass + def.capacity + def.fuelTankMass - self.massAtCapacity = def.hullMass + def.capacity + self.fullMass = def.hullMass + def.equipCapacity + def.fuelTankMass + self.massAtCapacity = def.hullMass + def.equipCapacity self.cargoCapacity = def.cargo self.def = def self.b = b @@ -416,7 +416,7 @@ local tradeMenu = function() shipFormatAndCompare:draw_accel_cell( l.FORWARD_ACCEL_EMPTY, "FORWARD", "emptyMass" ) shipFormatAndCompare:draw_tonnage_cell( l.WEIGHT_EMPTY, "hullMass" ) shipFormatAndCompare:draw_accel_cell( l.REVERSE_ACCEL_EMPTY, "REVERSE", "emptyMass" ) - shipFormatAndCompare:draw_tonnage_cell( l.CAPACITY, "capacity" ) + shipFormatAndCompare:draw_tonnage_cell( l.EQUIPMENT_CAPACITY, "equipCapacity" ) shipFormatAndCompare:draw_accel_cell( l.REVERSE_ACCEL_FULL, "REVERSE", "fullMass" ) shipFormatAndCompare:draw_tonnage_cell( l.FUEL_WEIGHT, "fuelTankMass" ) shipFormatAndCompare:draw_deltav_cell( l.DELTA_V_EMPTY, "emptyMass", "hullMass") @@ -481,7 +481,7 @@ shipMarket = Table.New("shipMarketWidget", false, { ui.text(Format.Money(item.def.basePrice,false)) ui.nextColumn() ui.dummy(widgetSizes.rowVerticalSpacing) - ui.text(item.def.capacity.."t") + ui.text(item.def.equipCapacity.."t") ui.nextColumn() end) end, diff --git a/src/lua/LuaShipDef.cpp b/src/lua/LuaShipDef.cpp index 77eb6ba936..db3389ac9c 100644 --- a/src/lua/LuaShipDef.cpp +++ b/src/lua/LuaShipDef.cpp @@ -246,7 +246,7 @@ void LuaShipDef::Register() pi_lua_settable(l, "cockpitName", st.cockpitName.c_str()); pi_lua_settable(l, "tag", EnumStrings::GetString("ShipTypeTag", st.tag)); pi_lua_settable(l, "angularThrust", st.angThrust); - pi_lua_settable(l, "capacity", st.capacity); + pi_lua_settable(l, "equipCapacity", st.capacity); pi_lua_settable(l, "cargo", st.cargo); pi_lua_settable(l, "hullMass", st.hullMass); pi_lua_settable(l, "fuelTankMass", st.fuelTankMass); From 5100afe4a9c77cf43741a94ef9b0b9ae871aa1e3 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 4 Nov 2024 15:06:54 -0500 Subject: [PATCH 111/119] Address review feedback, amend documentation --- data/libs/EquipSet.lua | 6 +++--- data/libs/EquipType.lua | 4 +++- data/libs/Passengers.lua | 14 ++++++++++---- data/modules/Assassination/Assassination.lua | 4 ++-- data/modules/MissionUtils/ShipBuilder.lua | 2 ++ 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/data/libs/EquipSet.lua b/data/libs/EquipSet.lua index 1aa6c44922..9406bc0a13 100644 --- a/data/libs/EquipSet.lua +++ b/data/libs/EquipSet.lua @@ -65,9 +65,9 @@ function EquipSet.CompatibleWithSlot(equip, slot) if not slot then return not equipSlot end return equipSlot and - slotTypeMatches(equip.slot.type, slot.type) - and (equip.slot.size <= slot.size) - and (equip.slot.size >= (slot.size_min or slot.size)) + slotTypeMatches(equipSlot.type, slot.type) + and (equipSlot.size <= slot.size) + and (equipSlot.size >= (slot.size_min or slot.size)) end -- Constructor: New diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index ea71b2a8a5..f3aa647d4a 100644 --- a/data/libs/EquipType.lua +++ b/data/libs/EquipType.lua @@ -459,7 +459,9 @@ end ---@param passenger Character function CabinType:RemovePassenger(passenger) utils.remove_elem(self.passengers, passenger) - self.icon_name = "equip_cabin_empty" + if #self.passengers == 0 then + self.icon_name = "equip_cabin_empty" + end end function CabinType:HasPassenger(passenger) diff --git a/data/libs/Passengers.lua b/data/libs/Passengers.lua index 4a809792da..becbc3bce5 100644 --- a/data/libs/Passengers.lua +++ b/data/libs/Passengers.lua @@ -48,16 +48,17 @@ end -- Function: GetFreeCabins -- --- Return a list of currently free passenger cabins +-- Return a list of passenger cabins containing at least numBerths free +-- passenger berths. If not specified, numBerths defaults to 1. -- ---@param ship Ship ----@param numPassengers integer? +---@param numBerths integer? ---@return Equipment.CabinType[] -function Passengers.GetFreeCabins(ship, numPassengers) +function Passengers.GetFreeCabins(ship, numBerths) local equipSet = ship:GetComponent("EquipSet") return equipSet:GetInstalledWithFilter('Equipment.CabinType', function(equip) - return equip:GetFreeBerths() >= (numPassengers or 1) + return equip:GetFreeBerths() >= (numBerths or 1) end) end @@ -94,6 +95,7 @@ end ---@param ship Ship ---@param passenger Character ---@param cabin EquipType? +---@return boolean success function Passengers.EmbarkPassenger(ship, passenger, cabin) ---@cast cabin Equipment.CabinType? if not cabin then @@ -104,6 +106,10 @@ function Passengers.EmbarkPassenger(ship, passenger, cabin) return false end + if not cabin:GetFreeBerths() > 0 then + return false + end + cabin:AddPassenger(passenger) ship:setprop("cabin_occupied_cap", (ship["cabin_occupied_cap"] or 0) + 1) diff --git a/data/modules/Assassination/Assassination.lua b/data/modules/Assassination/Assassination.lua index 2d0fe29cbd..0a48ffdb5a 100644 --- a/data/modules/Assassination/Assassination.lua +++ b/data/modules/Assassination/Assassination.lua @@ -132,11 +132,11 @@ local onChat = function (form, ref, option) backstation = backstation, client = ad.client, danger = ad.danger, - due = ad.due, + due = ad.due, flavour = ad.flavour, location = ad.location, reward = ad.reward, - threat = ad.threat, + threat = ad.threat, shipid = ad.shipid, shipname = ad.shipname, shipregid = ad.shipregid, diff --git a/data/modules/MissionUtils/ShipBuilder.lua b/data/modules/MissionUtils/ShipBuilder.lua index 553806c4fb..6c5a194711 100644 --- a/data/modules/MissionUtils/ShipBuilder.lua +++ b/data/modules/MissionUtils/ShipBuilder.lua @@ -661,6 +661,8 @@ end ---@param shipPlan MissionUtils.ShipBuilder.ShipPlan function ShipBuilder.ApplyPlan(ship, shipPlan) + assert(ship.shipId == shipPlan.shipId, "Applying a ship plan to an incompatible ship!") + local equipSet = ship:GetComponent('EquipSet') -- Apply slot-based equipment first From 9f3a4fb364ae96c64f081c375476100a0534a7dd Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 4 Nov 2024 15:10:49 -0500 Subject: [PATCH 112/119] Add HullConfig.GetHullConfig(id) helper --- data/libs/EquipSet.lua | 4 ++-- data/libs/HullConfig.lua | 17 ++++++++++++++++- data/libs/SpaceStation.lua | 2 +- data/modules/Assassination/Assassination.lua | 2 +- data/modules/MissionUtils/ShipBuilder.lua | 2 +- data/modules/SearchRescue/SearchRescue.lua | 2 +- data/modules/TradeShips/Trader.lua | 2 +- .../modules/station-view/04-shipMarket.lua | 4 ++-- 8 files changed, 25 insertions(+), 10 deletions(-) diff --git a/data/libs/EquipSet.lua b/data/libs/EquipSet.lua index 9406bc0a13..fedee1cf5b 100644 --- a/data/libs/EquipSet.lua +++ b/data/libs/EquipSet.lua @@ -76,7 +76,7 @@ end ---@param ship Ship function EquipSet:Constructor(ship) self.ship = ship - self.config = HullConfig.GetHullConfigs()[ship.shipId] + self.config = HullConfig.GetHullConfig(ship.shipId) -- Stores a mapping of slot id -> equipment item -- Non-slot equipment is stored in the array portion. @@ -138,7 +138,7 @@ function EquipSet:OnShipTypeChanged() assert(#self.installed == 0, "Missed some equipment while cleaning the ship") -- HullConfig changed, need to reset volume and rebuild list of slots - self.config = HullConfig.GetHullConfigs()[self.ship.shipId] + self.config = HullConfig.GetHullConfig(self.ship.shipId) self.ship:setprop("totalVolume", self.config.equipCapacity) self.slotCache = {} diff --git a/data/libs/HullConfig.lua b/data/libs/HullConfig.lua index bf4327203a..77d409a237 100644 --- a/data/libs/HullConfig.lua +++ b/data/libs/HullConfig.lua @@ -95,14 +95,29 @@ for id, def in pairs(ShipDef) do end end +-- Function: GetHullConfigs +-- +-- Return a table containing all registered hull configurations. +---@return table local function GetHullConfigs() return Configs end +-- Function: GetHullConfig +-- +-- Return the hull configuration corresponding to the given ID +-- +---@param id string +---@return HullConfig +local function GetHullConfig(id) + return Configs[id] +end + --============================================================================== return { Config = HullConfig, Slot = Slot, - GetHullConfigs = GetHullConfigs + GetHullConfigs = GetHullConfigs, + GetHullConfig = GetHullConfig } diff --git a/data/libs/SpaceStation.lua b/data/libs/SpaceStation.lua index 6e43c6e270..30221a2e2a 100644 --- a/data/libs/SpaceStation.lua +++ b/data/libs/SpaceStation.lua @@ -555,7 +555,7 @@ local isPlayerShip = function (def) return def.tag == "SHIP" and def.basePrice > local groundShips = utils.build_array(utils.filter(function (k,def) return isPlayerShip(def) - and utils.contains_if(HullConfig.GetHullConfigs()[def.id].slots, function(s) return s.type:match("^hull") end) + and utils.contains_if(HullConfig.GetHullConfig(def.id).slots, function(s) return s.type:match("^hull") end) end, pairs(ShipDef))) local spaceShips = utils.build_array(utils.filter(function (k,def) return isPlayerShip(def) end, pairs(ShipDef))) diff --git a/data/modules/Assassination/Assassination.lua b/data/modules/Assassination/Assassination.lua index 0a48ffdb5a..1c88c98bac 100644 --- a/data/modules/Assassination/Assassination.lua +++ b/data/modules/Assassination/Assassination.lua @@ -286,7 +286,7 @@ local onEnterSystem = function (ship) if mission.location:IsSameSystem(syspath) then -- spawn our target ship local station = Space.GetBody(mission.location.bodyIndex) - local plan = ShipBuilder.MakePlan(AssassinationTargetShip, HullConfig.GetHullConfigs()[mission.shipid], mission.threat) + local plan = ShipBuilder.MakePlan(AssassinationTargetShip, HullConfig.GetHullConfig(mission.shipid), mission.threat) assert(plan) local target = Space.SpawnShipDocked(mission.shipid, station) diff --git a/data/modules/MissionUtils/ShipBuilder.lua b/data/modules/MissionUtils/ShipBuilder.lua index 6c5a194711..949ea20167 100644 --- a/data/modules/MissionUtils/ShipBuilder.lua +++ b/data/modules/MissionUtils/ShipBuilder.lua @@ -557,7 +557,7 @@ function ShipBuilder.SelectHull(template, threat) -- print(" threat {} => {} ({} / {})" % { threat, shipId, hullIdx, #hullList }) - return HullConfig.GetHullConfigs()[shipId] + return HullConfig.GetHullConfig(shipId) end diff --git a/data/modules/SearchRescue/SearchRescue.lua b/data/modules/SearchRescue/SearchRescue.lua index 27838f3fca..079320f3d7 100644 --- a/data/modules/SearchRescue/SearchRescue.lua +++ b/data/modules/SearchRescue/SearchRescue.lua @@ -528,7 +528,7 @@ local createTargetShipParameters = function (flavour, planet) ----> needs to have enough passenger space for pickup if flavour.id == 1 or flavour.id == 6 then - local config = HullConfig.GetHullConfigs()[def.id] ---@type HullConfig + local config = HullConfig.GetHullConfig(def.id) ---@type HullConfig -- should have a default hull config if not config then diff --git a/data/modules/TradeShips/Trader.lua b/data/modules/TradeShips/Trader.lua index 7551e42b2d..5a35a02107 100644 --- a/data/modules/TradeShips/Trader.lua +++ b/data/modules/TradeShips/Trader.lua @@ -57,7 +57,7 @@ Trader.addEquip = function (ship) local lawlessness = Game.system.lawlessness local randomMod = 0.1 + lawlessness * 0.9 - local hullConfig = HullConfig.GetHullConfigs()[shipId] + local hullConfig = HullConfig.GetHullConfig(shipId) assert(hullConfig, "Can't find hull config for shipId " .. shipId) local template = TraderTemplate:clone { diff --git a/data/pigui/modules/station-view/04-shipMarket.lua b/data/pigui/modules/station-view/04-shipMarket.lua index ff16518a9e..2f40ec1d5e 100644 --- a/data/pigui/modules/station-view/04-shipMarket.lua +++ b/data/pigui/modules/station-view/04-shipMarket.lua @@ -283,7 +283,7 @@ function FormatAndCompareShips:draw_unformated_cell(desc, key) end local function getNumSlotsCompatibleWithType(def, type) - local config = HullConfig.GetHullConfigs()[def.id] + local config = HullConfig.GetHullConfig(def.id) local count = 0 for _, slot in pairs(config.slots) do @@ -296,7 +296,7 @@ local function getNumSlotsCompatibleWithType(def, type) end local function getBestSlotSizeOfType(def, type) - local config = HullConfig.GetHullConfigs()[def.id] + local config = HullConfig.GetHullConfig(def.id) local slot, size = utils.best_score(config.slots, function(_, slot) return EquipSet.SlotTypeMatches(type, slot.type) and slot.size or nil end) From 251e69d66eea7461551d5ca0c519a6bab6d2b87b Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 4 Nov 2024 15:18:40 -0500 Subject: [PATCH 113/119] Fix crash when uninstalling passenger cabin --- data/libs/EquipType.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index f3aa647d4a..b16238ce06 100644 --- a/data/libs/EquipType.lua +++ b/data/libs/EquipType.lua @@ -489,7 +489,7 @@ end function CabinType:OnRemove(ship, slot) EquipType.OnRemove(self, ship, slot) - if self.passengers then + if #self.passengers > 0 then logWarning("Removing passenger cabin with passengers onboard!") ship:setprop("cabin_occupied_cap", ship["cabin_occupied_cap"] - #self.passengers) end From 94241f4e4e5c482ca5c2b139a6d83362e95aa9bb Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 4 Nov 2024 15:21:07 -0500 Subject: [PATCH 114/119] Add fuel scoops to "Hull Mounts" equipment category --- data/pigui/libs/ship-equipment.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/pigui/libs/ship-equipment.lua b/data/pigui/libs/ship-equipment.lua index bf92ec573d..b28d131b3c 100644 --- a/data/pigui/libs/ship-equipment.lua +++ b/data/pigui/libs/ship-equipment.lua @@ -41,7 +41,7 @@ EquipmentWidget.Sections = { { name = le.SENSORS, type = "sensor", }, { name = le.COMPUTER_MODULES, type = "computer", }, { name = le.CABINS, types = { "cabin" } }, - { name = le.HULL_MOUNTS, types = { "hull", "utility" } }, + { name = le.HULL_MOUNTS, types = { "hull", "utility", "fuel_scoop" } }, } -- From 32dd758ad97ea8d9b5d282e65d01191d0f23638b Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 4 Nov 2024 15:31:53 -0500 Subject: [PATCH 115/119] Add descriptions to new translation strings --- data/lang/equipment-core/en.json | 70 ++++++++++++++++---------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/data/lang/equipment-core/en.json b/data/lang/equipment-core/en.json index 2cb8a75c20..47e6031889 100644 --- a/data/lang/equipment-core/en.json +++ b/data/lang/equipment-core/en.json @@ -448,23 +448,23 @@ "message": "Weapons" }, "THRUSTERS_DEFAULT": { - "description": "", + "description": "Equipment name for default RCS thrusters", "message": "Default Thrusters" }, "PROJECTILE_SPEED": { - "description": "Stat name for weapon projectile speeds", + "description": "Stat label for weapon projectile speeds", "message": "Projectile Speed" }, "STAT_VOLUME": { - "description": "", + "description": "Stat label for equipment volume", "message": "Volume" }, "STAT_WEIGHT": { - "description": "", + "description": "Stat label for equipment weight", "message": "Weight" }, "STAT_POWER_DRAW": { - "description": "", + "description": "Stat label for equipment power draw", "message": "Power Draw" }, "COMPUTER_MODULES": { @@ -496,79 +496,79 @@ "message": "Missile Array" }, "HARDPOINT_UTILITY": { - "description": "", + "description": "Name for a generic utility hardpoint", "message": "Utility" }, "HARDPOINT_WEAPON": { - "description": "", + "description": "Name for a generic weapon hardpoint", "message": "Weapon" }, "HARDPOINT_WEAPON_FRONT": { - "description": "Name for the 'Laser Front' equipment hardpoint", + "description": "Name for a generic front-facing weapon hardpoint", "message": "Front Weapon" }, "HARDPOINT_WEAPON_CHIN": { - "description": "Name for the 'Laser Chin' equipment hardpoint", - "message": "Chin Weapon" + "description": "Name for the 'Chin Mount' equipment hardpoint", + "message": "Chin Mount" }, "HARDPOINT_WEAPON_LEFT_NOSE": { - "description": "", + "description": "Name for the 'Left Nose' weapon hardpoint", "message": "Left Nose" }, "HARDPOINT_WEAPON_RIGHT_NOSE": { - "description": "", + "description": "Name for the 'Right Nose' weapon hardpoint", "message": "Right Nose" }, "HARDPOINT_WEAPON_REAR": { - "description": "", + "description": "Name for a generic rear-facing weapon hardpoint", "message": "Rear Weapon" }, "SLOT_SENSOR": { - "description": "", + "description": "Name for a generic (flight/navigation) sensor slot", "message": "Sensor" }, "SLOT_CABIN": { - "description": "", + "description": "Name for a generic room slot inside the pressurized living space", "message": "Cabin" }, "SLOT_COMPUTER": { - "description": "", + "description": "Name for a generic computer equipment slot", "message": "Computer" }, "SLOT_SHIELD": { - "description": "", + "description": "Name for a generic shield slot", "message": "Shield" }, "SLOT_SHIELD_LEFT": { - "description": "", + "description": "Name for a left-side shield slot", "message": "Shield L" }, "SLOT_SHIELD_RIGHT": { - "description": "", + "description": "Name for a right-side shield equipment slot", "message": "Shield R" }, "SLOT_HULL": { - "description": "", + "description": "Name for an equipment slot that augments or modifies the ship hull", "message": "Hull" }, "SLOT_HYPERDRIVE": { - "description": "", + "description": "Name for a ship's singular hyperdrive equipment slot", "message": "Hyperdrive" }, "SLOT_ENGINE": { - "description": "", + "description": "Name for the ship's primary travel engines", "message": "Engines" }, "SLOT_THRUSTER": { - "description": "", + "description": "Generic name for secondary attitude or maneuvering thrusters", "message": "Thrusters" }, "SLOT_CABIN_FORE": { - "description": "", + "description": "Name for a cabin slot towards the front of the ship", "message": "Fore Cabin" }, "SLOT_CABIN_REAR": { - "description": "", + "description": "Name for a cabin slot towards the rear of the ship", "message": "Rear Cabin" }, "MISC_EQUIPMENT": { @@ -576,11 +576,11 @@ "message": "Miscellaneous" }, "OPLI_INTERNAL_MISSILE_RACK_S2": { - "description": "", + "description": "Equipment name for an OPLI internal missile rack", "message": "OPLI Internal Missile Rack" }, "OKB_KALURI_BOWFIN_MISSILE_RACK": { - "description": "", + "description": "Equipment name for an OKB-Kaluri Bowfin internal missile launcher", "message": "Bowfin Missile Launcher" }, "CABINS": { @@ -588,35 +588,35 @@ "message": "Cabins" }, "PASSENGER_BERTHS": { - "description": "", + "description": "Label for the total number of passenger berths in a cabin equipment item", "message": "Passenger Berths" }, "OCCUPIED_BERTHS": { - "description": "", + "description": "Label for the number of passenger berths currently occupied in a cabin equipment item", "message": "Occupied Berths" }, "MISSILE_RAIL_S1": { - "description": "", + "description": "Name for a single-missile external launcher. 'Cnida' is a brand name.", "message": "Cnida-101 Missile Rail" }, "MISSILE_RAIL_S2": { - "description": "", + "description": "Name for a single-missile external launcher. 'Cnida' is a brand name.", "message": "Cnida-102 Missile Rail" }, "MISSILE_RAIL_S3": { - "description": "", + "description": "Name for a single-missile external launcher. 'Cnida' is a brand name.", "message": "Cnida-103 Missile Rail" }, "MISSILE_RACK_341": { - "description": "", + "description": "Name for a multiple-missile external missile rack. 'Hydri' is a brand name.", "message": "LH-140 Hydri Missile Rack" }, "MISSILE_RACK_322": { - "description": "", + "description": "Name for a multiple-missile external missile rack. 'Hydri' is a brand name.", "message": "LH-230 Hydri Missile Rack" }, "MISSILE_RACK_221": { - "description": "", + "description": "Name for a multiple-missile external missile rack. 'Hydri' is a brand name.", "message": "LH-120 Hydri Missile Rack" } } From 862890daeb365b310ceb91af0bce07dad8be63cf Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 4 Nov 2024 15:32:28 -0500 Subject: [PATCH 116/119] Fix outfitter not handling slot types with underscores --- data/pigui/libs/ship-equipment.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/pigui/libs/ship-equipment.lua b/data/pigui/libs/ship-equipment.lua index b28d131b3c..617ea11104 100644 --- a/data/pigui/libs/ship-equipment.lua +++ b/data/pigui/libs/ship-equipment.lua @@ -206,7 +206,7 @@ function EquipmentWidget:getSlotName(slot) return Lang.GetResource(slot.i18n_res)[slot.i18n_key] end - local base_type = slot.type:match("(%w+)%.?") + local base_type = slot.type:match("([%w_-]+)%.?") local i18n_key = (slot.hardpoint and "HARDPOINT_" or "SLOT_") .. base_type:upper() return le[i18n_key] end From 01d6ec40267390faa651c02d7e3a3d7b4bde974c Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 4 Nov 2024 15:40:30 -0500 Subject: [PATCH 117/119] Add placeholder s1/2/3 fuel scoops, remove multi_scoop - Convert cargo scoop to a hull-slot equipment item - Add placeholder variants of the fuel scoop for different slot sizes - Remove multi-scoop entirely as cargo and fuel scoops now occupy entirely different slots --- data/lang/equipment-core/en.json | 8 ------- data/modules/Equipment/Internal.lua | 34 +++++++++++++++++++---------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/data/lang/equipment-core/en.json b/data/lang/equipment-core/en.json index 47e6031889..618d33aa13 100644 --- a/data/lang/equipment-core/en.json +++ b/data/lang/equipment-core/en.json @@ -239,14 +239,6 @@ "description": "", "message": "R40 Unguided Rocket" }, - "MULTI_SCOOP": { - "description": "A ship equipment: a cargo scoop and fuel scoop combined into one", - "message": "Multi Scoop" - }, - "MULTI_SCOOP_DESCRIPTION": { - "description": "", - "message": "Although less effective than a fuel scoop it permits scooping of both cargo and fuel" - }, "ORBIT_SCANNER": { "description": "A ship equipment that records data of terrain, for cartography mapping/geological survey. Note: Scout Mission module reference and distinguishes between 'orbital' and 'surface' scanner types, the former scans planet from orbit, the latter from low altitude.", "message": "Orbital scanner XKM-650" diff --git a/data/modules/Equipment/Internal.lua b/data/modules/Equipment/Internal.lua index 33ec62ac2a..04ba15a3c4 100644 --- a/data/modules/Equipment/Internal.lua +++ b/data/modules/Equipment/Internal.lua @@ -153,27 +153,37 @@ Equipment.Register("misc.thrusters_best", ThrusterType.New { -- Scoops --=============================================== -Equipment.Register("misc.fuel_scoop", EquipType.New { +Equipment.Register("misc.fuel_scoop_s1", EquipType.New { l10n_key="FUEL_SCOOP", price=3500, purchasable=true, tech_level=4, - slot = { type="scoop.fuel", size=1, hardpoint=true }, - mass=6, volume=4, capabilities={ fuel_scoop=3 }, + slot = { type="fuel_scoop", size=1, hardpoint=true }, + mass=6, volume=4, capabilities={ fuel_scoop=2 }, icon_name="equip_fuel_scoop" }) + +Equipment.Register("misc.fuel_scoop_s2", EquipType.New { + l10n_key="FUEL_SCOOP", + price=6500, purchasable=true, tech_level=5, + slot = { type="fuel_scoop", size=2, hardpoint=true }, + mass=8, volume=7, capabilities={ fuel_scoop=3 }, + icon_name="equip_fuel_scoop" +}) + +Equipment.Register("misc.fuel_scoop_s3", EquipType.New { + l10n_key="FUEL_SCOOP", + price=9500, purchasable=true, tech_level=7, + slot = { type="fuel_scoop", size=3, hardpoint=true }, + mass=14, volume=10, capabilities={ fuel_scoop=5 }, + icon_name="equip_fuel_scoop" +}) + Equipment.Register("misc.cargo_scoop", EquipType.New { l10n_key="CARGO_SCOOP", price=3900, purchasable=true, tech_level=5, - slot = { type="scoop.cargo", size=1, hardpoint=true }, - mass=4, volume=7, capabilities={ cargo_scoop=1 }, + slot = { type="hull.cargo_scoop", size=1, hardpoint=true }, + mass=2, volume=4, capabilities={ cargo_scoop=1 }, icon_name="equip_cargo_scoop" }) -Equipment.Register("misc.multi_scoop", EquipType.New { - l10n_key="MULTI_SCOOP", - price=12000, purchasable=true, tech_level=9, - slot = { type="scoop", size=1, hardpoint=true }, - mass=11, volume=9, capabilities={ cargo_scoop=1, fuel_scoop=2 }, - icon_name="equip_multi_scoop" -}) --=============================================== -- Passenger Cabins From d13cbcafbb460d52ad37cf7a42eaac6ad616d4e7 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 4 Nov 2024 16:04:05 -0500 Subject: [PATCH 118/119] Add structure slot, placeholder hull reinforcement equipment - Per design discussion, we intend to split atmospheric shielding etc. into superstructure and hull-surface components - This provides a placeholder equipment item and slot for future work --- data/lang/equipment-core/en.json | 8 ++++++++ data/libs/HullConfig.lua | 1 + data/modules/Equipment/Internal.lua | 8 ++++++++ data/pigui/libs/ship-equipment.lua | 2 +- 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/data/lang/equipment-core/en.json b/data/lang/equipment-core/en.json index 618d33aa13..8d64177ac3 100644 --- a/data/lang/equipment-core/en.json +++ b/data/lang/equipment-core/en.json @@ -563,6 +563,10 @@ "description": "Name for a cabin slot towards the rear of the ship", "message": "Rear Cabin" }, + "SLOT_STRUCTURE": { + "description": "Name for the ship's internal structure modification slot", + "message": "Structure" + }, "MISC_EQUIPMENT": { "description": "Category header for miscellaneous equipment", "message": "Miscellaneous" @@ -610,5 +614,9 @@ "MISSILE_RACK_221": { "description": "Name for a multiple-missile external missile rack. 'Hydri' is a brand name.", "message": "LH-120 Hydri Missile Rack" + }, + "REINFORCED_STRUCTURE": { + "description": "Name for an equipment item that reinforces the hull's superstructure", + "message": "Reinforced Structure" } } diff --git a/data/libs/HullConfig.lua b/data/libs/HullConfig.lua index 77d409a237..42941aa12a 100644 --- a/data/libs/HullConfig.lua +++ b/data/libs/HullConfig.lua @@ -49,6 +49,7 @@ HullConfig.slots = { sensor = Slot:clone { type = "sensor", size = 1 }, computer_1 = Slot:clone { type = "computer", size = 1 }, hull_mod = Slot:clone { type = "hull", size = 1 }, + structure = Slot:clone { type = "structure", size = 1 }, hyperdrive = Slot:clone { type = "hyperdrive", size = 1 }, thruster = Slot:clone { type = "thruster", size = 1 }, } diff --git a/data/modules/Equipment/Internal.lua b/data/modules/Equipment/Internal.lua index 04ba15a3c4..387ba71483 100644 --- a/data/modules/Equipment/Internal.lua +++ b/data/modules/Equipment/Internal.lua @@ -89,6 +89,14 @@ Equipment.Register("shield.basic_s5", EquipType.New { -- Hull Modifications --=============================================== +Equipment.Register("hull.reinforced_structure", EquipType.New { + l10n_key="REINFORCED_STRUCTURE", + price=1200, purchasable=true, tech_level=5, + slot = { type="structure", size=1 }, + mass=5, volume=2, capabilities = { atmo_shield=3 }, + icon_name="equip_generic" +}) + Equipment.Register("hull.atmospheric_shielding", EquipType.New { l10n_key="ATMOSPHERIC_SHIELDING", price=200, purchasable=true, tech_level=3, diff --git a/data/pigui/libs/ship-equipment.lua b/data/pigui/libs/ship-equipment.lua index 617ea11104..5c4fb46772 100644 --- a/data/pigui/libs/ship-equipment.lua +++ b/data/pigui/libs/ship-equipment.lua @@ -41,7 +41,7 @@ EquipmentWidget.Sections = { { name = le.SENSORS, type = "sensor", }, { name = le.COMPUTER_MODULES, type = "computer", }, { name = le.CABINS, types = { "cabin" } }, - { name = le.HULL_MOUNTS, types = { "hull", "utility", "fuel_scoop" } }, + { name = le.HULL_MOUNTS, types = { "hull", "utility", "fuel_scoop", "structure" } }, } -- From 1f6f38b60635c17b3926252bb36bf85e12ccb239 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Mon, 4 Nov 2024 16:07:22 -0500 Subject: [PATCH 119/119] Use correct spacing in equipment stats tooltip --- data/pigui/libs/equip-card.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/pigui/libs/equip-card.lua b/data/pigui/libs/equip-card.lua index 5c7b0d2544..90674b5b3b 100644 --- a/data/pigui/libs/equip-card.lua +++ b/data/pigui/libs/equip-card.lua @@ -12,6 +12,7 @@ local ui = require 'pigui' local icons = ui.theme.icons local colors = ui.theme.colors +local styles = ui.theme.styles local pionillium = ui.fonts.pionillium -- @@ -129,7 +130,7 @@ function EquipCard.drawEquipStats(data) ui.tableSetColumnIndex(1) ui.icon(tooltip[2], Vector2(ui.getTextLineHeight(), ui.getTextLineHeight()), ui.theme.colors.font) - ui.sameLine(0, ui.getTextLineHeight() / 4.0) + ui.sameLine(0, styles.ItemInnerSpacing.x) local value, format = tooltip[3], tooltip[4] ui.text(format(value))