diff --git a/data/lang/equipment-core/en.json b/data/lang/equipment-core/en.json index 0f7d2bf77f2..8d64177ac35 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" @@ -446,5 +438,185 @@ "WEAPONS": { "description": "Category name of weapon-related equipment", "message": "Weapons" + }, + "THRUSTERS_DEFAULT": { + "description": "Equipment name for default RCS thrusters", + "message": "Default Thrusters" + }, + "PROJECTILE_SPEED": { + "description": "Stat label for weapon projectile speeds", + "message": "Projectile Speed" + }, + "STAT_VOLUME": { + "description": "Stat label for equipment volume", + "message": "Volume" + }, + "STAT_WEIGHT": { + "description": "Stat label for equipment weight", + "message": "Weight" + }, + "STAT_POWER_DRAW": { + "description": "Stat label for equipment power draw", + "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_FUEL_SCOOP": { + "description": "Name for the 'Fuel Scoop' equipment hardpoint", + "message": "Fuel Scoop" + }, + "HARDPOINT_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" + }, + "HARDPOINT_MISSILE_ARRAY": { + "description": "Name for the 'Missile Array' equipment hardpoint", + "message": "Missile Array" + }, + "HARDPOINT_UTILITY": { + "description": "Name for a generic utility hardpoint", + "message": "Utility" + }, + "HARDPOINT_WEAPON": { + "description": "Name for a generic weapon hardpoint", + "message": "Weapon" + }, + "HARDPOINT_WEAPON_FRONT": { + "description": "Name for a generic front-facing weapon hardpoint", + "message": "Front Weapon" + }, + "HARDPOINT_WEAPON_CHIN": { + "description": "Name for the 'Chin Mount' equipment hardpoint", + "message": "Chin Mount" + }, + "HARDPOINT_WEAPON_LEFT_NOSE": { + "description": "Name for the 'Left Nose' weapon hardpoint", + "message": "Left Nose" + }, + "HARDPOINT_WEAPON_RIGHT_NOSE": { + "description": "Name for the 'Right Nose' weapon hardpoint", + "message": "Right Nose" + }, + "HARDPOINT_WEAPON_REAR": { + "description": "Name for a generic rear-facing weapon hardpoint", + "message": "Rear Weapon" + }, + "SLOT_SENSOR": { + "description": "Name for a generic (flight/navigation) sensor slot", + "message": "Sensor" + }, + "SLOT_CABIN": { + "description": "Name for a generic room slot inside the pressurized living space", + "message": "Cabin" + }, + "SLOT_COMPUTER": { + "description": "Name for a generic computer equipment slot", + "message": "Computer" + }, + "SLOT_SHIELD": { + "description": "Name for a generic shield slot", + "message": "Shield" + }, + "SLOT_SHIELD_LEFT": { + "description": "Name for a left-side shield slot", + "message": "Shield L" + }, + "SLOT_SHIELD_RIGHT": { + "description": "Name for a right-side shield equipment slot", + "message": "Shield R" + }, + "SLOT_HULL": { + "description": "Name for an equipment slot that augments or modifies the ship hull", + "message": "Hull" + }, + "SLOT_HYPERDRIVE": { + "description": "Name for a ship's singular hyperdrive equipment slot", + "message": "Hyperdrive" + }, + "SLOT_ENGINE": { + "description": "Name for the ship's primary travel engines", + "message": "Engines" + }, + "SLOT_THRUSTER": { + "description": "Generic name for secondary attitude or maneuvering thrusters", + "message": "Thrusters" + }, + "SLOT_CABIN_FORE": { + "description": "Name for a cabin slot towards the front of the ship", + "message": "Fore Cabin" + }, + "SLOT_CABIN_REAR": { + "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" + }, + "OPLI_INTERNAL_MISSILE_RACK_S2": { + "description": "Equipment name for an OPLI internal missile rack", + "message": "OPLI Internal Missile Rack" + }, + "OKB_KALURI_BOWFIN_MISSILE_RACK": { + "description": "Equipment name for an OKB-Kaluri Bowfin internal missile launcher", + "message": "Bowfin Missile Launcher" + }, + "CABINS": { + "description": "Category header for pressurized cabin slots", + "message": "Cabins" + }, + "PASSENGER_BERTHS": { + "description": "Label for the total number of passenger berths in a cabin equipment item", + "message": "Passenger Berths" + }, + "OCCUPIED_BERTHS": { + "description": "Label for the number of passenger berths currently occupied in a cabin equipment item", + "message": "Occupied Berths" + }, + "MISSILE_RAIL_S1": { + "description": "Name for a single-missile external launcher. 'Cnida' is a brand name.", + "message": "Cnida-101 Missile Rail" + }, + "MISSILE_RAIL_S2": { + "description": "Name for a single-missile external launcher. 'Cnida' is a brand name.", + "message": "Cnida-102 Missile Rail" + }, + "MISSILE_RAIL_S3": { + "description": "Name for a single-missile external launcher. 'Cnida' is a brand name.", + "message": "Cnida-103 Missile Rail" + }, + "MISSILE_RACK_341": { + "description": "Name for a multiple-missile external missile rack. 'Hydri' is a brand name.", + "message": "LH-140 Hydri Missile Rack" + }, + "MISSILE_RACK_322": { + "description": "Name for a multiple-missile external missile rack. 'Hydri' is a brand name.", + "message": "LH-230 Hydri Missile Rack" + }, + "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/lang/ui-core/en.json b/data/lang/ui-core/en.json index d2b79fd8374..24443c5c009 100644 --- a/data/lang/ui-core/en.json +++ b/data/lang/ui-core/en.json @@ -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": "", @@ -2618,5 +2618,41 @@ "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 be used in a singular or plural context.", + "message": "Sell {name}" + }, + "BUY_EQUIP": { + "description": "Button text to buy selected equipment. May be 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" + }, + "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" + }, + "EQUIPMENT_CAPACITY": { + "description": "The equipment capacity of a ship", + "message": "Equipment Capacity" } } diff --git a/data/libs/CargoManager.lua b/data/libs/CargoManager.lua index 43aaeef3fe6..4f2e90b9170 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/libs/Character.lua b/data/libs/Character.lua index ebf0e600421..1a44d354894 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/Economy.lua b/data/libs/Economy.lua index 0df7bca9ce0..013ebb12064 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 = {} diff --git a/data/libs/EquipSet.lua b/data/libs/EquipSet.lua index dfa258cf623..fedee1cf5bc 100644 --- a/data/libs/EquipSet.lua +++ b/data/libs/EquipSet.lua @@ -1,404 +1,643 @@ -- 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 HullConfig = require 'HullConfig' local Serializer = require 'Serializer' --- + +local utils = require 'utils' + -- Class: EquipSet -- --- A container for a ship's equipment. +-- 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 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 +---@field New fun(ship: Ship): EquipSet +local EquipSet = utils.class("EquipSet") + +---@alias EquipSet.Listener fun(op: 'install'|'remove', equip: EquipType, slot: HullConfig.Slot?) + +-- 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 -local listeners = {} -function EquipSet:AddListener(listener) - listeners[self] = listeners[self] or {} - table.insert(listeners[self], listener) +EquipSet.SlotTypeMatches = slotTypeMatches + +-- 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. +-- +-- 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) + local equipSlot = equip.slot or false + if not slot then return not equipSlot end + + return equipSlot and + slotTypeMatches(equipSlot.type, slot.type) + and (equipSlot.size <= slot.size) + and (equipSlot.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.GetHullConfig(ship.shipId) + + -- Stores a mapping of slot id -> equipment item + -- 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 + -- 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 + 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() + + -- 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 + 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.equipCapacity) end -function EquipSet:CallListener(slot) - if not listeners[self] then return 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 = {} - for _, listener in ipairs(listeners[self]) do - listener(slot) + for k in pairs(self.installed) do + table.insert(to_remove, k) end -end -function EquipSet:Serialize() - local serialize = { - slots = {} - } + -- 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 k, v in pairs(self.slots) do - serialize.slots[k] = v + for _, id in ipairs(to_remove) do + local equip = self.installed[id] + self:Remove(equip) end - return serialize + 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.GetHullConfig(self.ship.shipId) + self.ship:setprop("totalVolume", self.config.equipCapacity) + + self.slotCache = {} + self.idCache = {} + + self:BuildSlotCache() end -function EquipSet.Unserialize(data) - setmetatable(data, EquipSet.meta) - return data +-- Method: GetFreeVolume +-- +-- Returns the available volume for mounting equipment +---@return number +function EquipSet:GetFreeVolume() + return self.ship.totalVolume - self.ship.equipVolume end +-- Method: GetSlotHandle -- --- Group: Methods +-- 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 + +-- 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) + -- 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 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 +-- Method: GetFreeSlotForEquip +-- +-- Attempts to find an available slot where the passed equipment item instance +-- could be installed. -- --- Method: FreeSpace +-- 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. -- --- returns the available space in the given slot. +-- The list of slots is iterated in undefined order. -- -- Parameters: -- --- slot - The slot name. +-- equip - EquipType, the equipment item instance to attempt to slot. -- --- Return: +-- Returns: -- --- free_space - The available space (integer) +-- slot - HullConfig.Slot?, a valid slot for the item or nil. -- -function EquipSet:FreeSpace (slot) - local s = self.slots[slot] - if not s then - return 0 +---@param equip EquipType +---@return HullConfig.Slot? +function EquipSet:GetFreeSlotForEquip(equip) + if not equip.slot then return nil end + + local filter = function(id, slot) + return not self.installed[id] + and slot.hardpoint == equip.slot.hardpoint + and self:CanInstallInSlot(slot, equip) end - return s.__limit - s.__occupied -end -function EquipSet:SlotSize(slot) - local s = self.slots[slot] - if not s then - return 0 + for id, slot in pairs(self.slotCache) do + if filter(id, slot) then + return slot + end end - return s.__limit + + return nil end +-- Method: GetAllSlotsOfType -- --- Method: OccupiedSpace --- --- returns the space occupied in the given slot. +-- Return a list of all slots matching the given filter parameters. +-- If hardpoint is not specified, returns both hardpoint and internal slots. -- -- Parameters: -- --- slot - The slot name. +-- type - string, slot type filter internally passed to SlotTypeMatches. -- --- Return: +-- hardpoint - optional boolean, constrains the lost of slots to only those +-- which have a `hardpoint` key of the given type. -- --- occupied_space - The occupied space (integer) +-- Returns: -- -function EquipSet:OccupiedSpace (slot) - local s = self.slots[slot] - if not s then - return 0 +-- slots - HullConfig.Slot[], unsorted list of slots which match the given +-- search criteria. +-- +---@param type string +---@param hardpoint boolean? +---@return HullConfig.Slot[] +function EquipSet:GetAllSlotsOfType(type, hardpoint) + local t = {} + + 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 end - return s.__occupied + + return t end +-- Method: GetInstalledWithFilter -- --- Method: Count --- --- returns the number of occurrences of the given equipment in the specified slot. +-- Return a list of all installed equipment of the given EquipType class matching the filter function -- -- Parameters: -- --- item - The equipment to count. +-- typename - string, constrains the results to only equipment items +-- inheriting from the given class. -- --- 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. +-- filter - function, returns a boolean indicating whether the equipment +-- item should be included in the returned list. -- --- Return: +-- Returns: -- --- free_space - The available space (integer) +-- items - EquipType[], list of items of the passed class which were accepted +-- by the filter function. -- -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 +---@generic T : EquipType +---@param typename `T` +---@param filter fun(equip: T): boolean +---@return T[] +function EquipSet:GetInstalledWithFilter(typename, filter) + local out = {} - local count = 0 - for _, slot in pairs(to_check) do - for _, e in pairs(slot) do - if e == item then - count = count + 1 - end + for _, equip in pairs(self.installed) do + if equip:IsA(typename) and filter(equip) then + table.insert(out, equip) 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) + return out end --- Method: __Remove_NoCheck (PRIVATE) +-- Method: GetInstalledOfType -- --- 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. +-- Return a list of all installed equipment matching the given slot type -- -- Parameters: -- --- Please refer to the Remove method. +-- type - string, slot type filter string according to SlotTypeMatches -- --- Return: +-- Returns: -- --- Please refer to the Remove method. +-- installed - EquipType[], list of installed equipment which passed the +-- given slot type filter -- -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 +---@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 removed + + return out end --- Method: __Add_NoCheck (PRIVATE) +-- Method: GetInstalledEquipment -- --- 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. +-- 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 + +-- Method: GetInstalledNonSlot +-- +-- Returns an array containing all non-slot equipment items installed on this +-- ship. Items installed in a specific slot are not returned. +---@return EquipType[] +function EquipSet:GetInstalledNonSlot() + local out = {} + + for i, equip in ipairs(self.installed) do + out[i] = equip + end + + return out +end + +-- 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. +-- 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: -- --- Please refer to the Add method. +-- slotHandle - HullConfig.Slot, the slot to test the equipment item against. -- --- Return: +-- equipment - EquipType, the equipment item instance being checked for +-- installation. -- --- Please refer to the Add method. +-- Returns: -- -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 +-- 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) + 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 + +-- 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. +---@param equipment EquipType +function EquipSet:CanInstallLoose(equipment) + return not equipment.slot + and self:GetFreeVolume() >= equipment.volume end --- Method: Add +-- Method: AddListener -- --- Add some equipment to the set, filling the specified slot as much as --- possible. +-- 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 + +-- Method: RemoveListener +-- +-- Remove a previously-registered event listener function. +function EquipSet:RemoveListener(fun) + utils.remove_elem(self.listeners, fun) +end + +-- Method: Install +-- +-- Install an equipment item in the given slot or in free equipment volume. -- -- 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. +-- 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. -- --- Return: +-- 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. -- --- installed - the number of pieces actually installed, or -1 if the specified --- slot is not valid. +-- Returns: -- -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") +-- installed - boolean, indicates whether the item was successfully installed +-- +---@param equipment EquipType +---@param slotHandle HullConfig.Slot? +---@return boolean +function EquipSet:Install(equipment, slotHandle) + local slotId = self.idCache[slotHandle] - 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 + if slotHandle then + if not slotId then + return false -- No such slot! + end + + if self.installed[slotId] then + return false -- Slot already full! + end + + 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 + end + + table.insert(self.installed, equipment) + self.cache[equipment] = self.cacheIndex + self.cacheIndex = self.cacheIndex + 1 end - if added > 0 then - self:__TriggerCallbacks(ship, slot) + + equipment:OnInstall(self.ship, slotHandle) + self:_InstallInternal(equipment) + + for _, fun in ipairs(self.listeners) do + fun('install', equipment, slotHandle) end - return added + + return true end -- Method: Remove -- --- Remove some equipment from the set. +-- Remove a previously-installed equipment item from this ship. +-- +-- 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: -- --- 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. +-- 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. -- --- Return: +-- Returns: -- --- removed - the number of pieces actually removed. +-- removed - boolean, indicates successful removal of the item -- -function EquipSet:Remove(ship, item, num, slot) - num = num or 1 - if not slot then - slot = item:GetDefaultSlot(ship) +---@param equipment EquipType +---@return boolean +function EquipSet:Remove(equipment) + local cacheKey = self.cache[equipment] + + if not cacheKey then + return false 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 + local slotHandle = nil + + if type(cacheKey) == "string" then + slotHandle = self:GetSlotHandle(cacheKey) 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 + + 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 - if removed > 0 then - self:__TriggerCallbacks(ship, slot) + + for _, fun in ipairs(self.listeners) do + fun('remove', equipment, slotHandle) end - return removed + + return true 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 +-- 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 - 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) + + 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.slots[slot] = {__occupied = 0, __limit = s.__limit} - self:__TriggerCallbacks(ship, slot) + self.ship:UpdateEquipStats() 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 +-- 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.provides_slots then + for _, slot in pairs(equipment.provides_slots) do + local slotId = self.idCache[slot] + assert(slotId) - elseif type(slot_names) == 'string' then - EquipSet__ClearSlot(self, ship, slot_names) + self.slotCache[slotId] = nil + self.idCache[slot] = nil + end + end - elseif type(slot_names) == 'table' then - for _, s in ipairs(slot_names) do - EquipSet__ClearSlot(self, ship, s) + 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 -function EquipSet:Get(slot, index) - if type(index) == "number" then - return self.slots[slot][index] +-- 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 - local ret = {} - for i,v in pairs(self.slots[slot]) do - if type(i) == 'number' then - ret[i] = v + + -- 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 - return ret end -function EquipSet:Set(ship, slot_name, index, item) - local slot = self.slots[slot_name] +-- Remove transient fields from the serialized copy of the EquipSet +function EquipSet:Serialize() + local obj = table.copy(self) - if index < 1 or index > slot.__limit then - error("EquipSet:Set(): argument 'index' out of range") - end + obj.cache = nil + obj.slotCache = nil + obj.idCache = nil + obj.listeners = nil + + return obj +end - local to_remove = slot[index] - if item == to_remove then return end +-- Restore transient fields to the unserialized version of the EquipSet +function EquipSet:Unserialize() + self.cache = {} + self.listeners = {} - 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 + for k, v in pairs(self.installed) do + self.cache[v] = k end + + self.slotCache = {} + self.idCache = {} + + setmetatable(self, EquipSet.meta) + + self:BuildSlotCache() + + return self end + Serializer:RegisterClass("EquipSet", EquipSet) + return EquipSet diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index 29a02c33ab6..b16238ce06f 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 @@ -32,166 +31,278 @@ local misc = {} -- author, but who knows ? Some people might find it useful.) -- -- +---@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 price number +---@field icon_name string? +---@field tech_level integer | "MILITARY" +---@field transient table +---@field slots table -- deprecated +---@field count integer? +---@field provides_slots table? +---@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 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:Serialize() - local tmp = EquipType.Super().Serialize(self) - local ret = {} - for k,v in pairs(tmp) do - if type(v) ~= "function" then - ret[k] = v - end + if obj.slot and not obj.slot.hardpoint then + obj.slot.hardpoint = false end - ret.volatile = nil - return ret -end + if not obj.tech_level then + obj.tech_level = 1 + 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" + if not obj.icon_name then + obj.icon_name = "equip_generic" end - local l = Lang.GetResource(obj.l10n_resource) - obj.volatile = { - description = l:get(obj.l10n_key.."_DESCRIPTION") or "", - name = l[obj.l10n_key] or "" - } + + if not obj.purchasable then + obj.price = obj.price or 0 + end + 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). +-- +-- Parameters: -- --- Group: Methods +-- 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) + local l = Lang.GetResource(obj.l10n_resource) + obj.transient = { + description = l:get(obj.l10n_key .. "_DESCRIPTION") or "", + name = l[obj.l10n_key] or "" + } +end +-- Method: OnInstall -- --- Method: GetDefaultSlot +-- Perform any setup associated with installing this item on a Ship. -- --- returns the default slot for this equipment +-- If overriding this function in a subclass you should be careful to ensure +-- the parent class's implementation is always called. -- -- Parameters: -- --- ship (optional) - if provided, tailors the answer for this specific ship +-- ship - Ship, the ship this equipment item is being installed in. -- --- Return: +-- slot - optional HullConfig.Slot, the slot this item is being installed in +-- if it is a slot-mounted equipment item. -- --- slot_name - A string identifying the slot. --- -function EquipType:GetDefaultSlot(ship) - return self.slots[1] +---@param ship Ship +---@param slot HullConfig.Slot? +function EquipType:OnInstall(ship, slot) + -- 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 +-- Method: OnRemove -- --- Method: IsValidSlot +-- Perform any setup associated with uninstalling this item from a Ship. -- --- tells whether the given slot is valid for this equipment +-- If overriding this function in a subclass you should be careful to ensure +-- the parent class's implementation is always called. -- -- Parameters: -- --- slot - a string identifying the slot in question +-- ship - Ship, the ship this equipment item is being removed from. -- --- ship (optional) - if provided, tailors the answer for this specific ship +-- slot - optional HullConfig.Slot, the slot this item is being removed from +-- if it is a slot-mounted equipment item. -- --- Return: +---@param ship Ship +---@param slot HullConfig.Slot? +function EquipType:OnRemove(ship, slot) + -- Override this for any custom uninstallation logic needed +end + +-- Method: isProto -- --- valid - a boolean qualifying the validity of the slot. +-- 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 -- -function EquipType:IsValidSlot(slot, ship) - for _, s in ipairs(self.slots) do - if s == slot then - return true - end - end - return false +-- 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 -function EquipType:GetName() - return self.volatile.name +-- Method: Instance +-- +-- Create and return an instance of this equipment prototype. +---@return EquipType +function EquipType:Instance() + return setmetatable({ __proto = self }, self.meta) end -function EquipType:GetDescription() - return self.volatile.description +-- 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() + + 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:Instance()` 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 = utils.mixin(type.meta, { __index = inst }) + 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 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)) +function EquipType:Serialize() + local tmp = EquipType.Super().Serialize(self) + local ret = {} + for k,v in pairs(tmp) do + if type(v) ~= "function" then + ret[k] = v + end end - return num + + ret.transient = nil + return ret 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) +function EquipType.Unserialize(data) + local obj = EquipType.Super().Unserialize(data) + setmetatable(obj, EquipType.meta) + + -- Only patch the common prototype with runtime transient data + if EquipType.isProto(obj) then + EquipType._createTransient(obj) end - return num + + return obj end -function EquipType:Install(ship, num, slot) - local caps = self.capabilities - num = __ApplyMassLimit(ship, caps, num) - return __ApplyCapabilities(ship, caps, num, 1) +-- Method: GetName +-- +-- Returns the translated name of this equipment item suitable for display to +-- the user. +---@return string +function EquipType:GetName() + return self.transient.name end -function EquipType:Uninstall(ship, num, slot) - return __ApplyCapabilities(ship, self.capabilities, num, -1) +-- 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 -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) +---@class Equipment.LaserType : EquipType +---@field laser_stats table +local LaserType = utils.inherits(EquipType, "Equipment.LaserType") + +---@param ship Ship +---@param slot HullConfig.Slot +function LaserType:OnInstall(ship, slot) + 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 - 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 HullConfig.Slot +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 - return 1 end +--============================================================================== + -- Single drive type, no support for slave drives. -local HyperdriveType = utils.inherits(EquipType, "HyperdriveType") +---@class Equipment.HyperdriveType : EquipType +---@field fuel CommodityType +---@field byproduct CommodityType? +local HyperdriveType = utils.inherits(EquipType, "Equipment.HyperdriveType") function HyperdriveType:GetMaximumRange(ship) return 625.0*(self.capabilities.hyperclass ^ 2) / (ship.staticMass + ship.fuelMassLeft) @@ -216,7 +327,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) @@ -279,18 +390,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" @@ -325,17 +431,92 @@ 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[]? +local CabinType = utils.inherits(EquipType, "Equipment.CabinType") + +---@param passenger Character +function CabinType:AddPassenger(passenger) + table.insert(self.passengers, passenger) + self.icon_name = "equip_cabin_occupied" +end + +---@param passenger Character +function CabinType:RemovePassenger(passenger) + utils.remove_elem(self.passengers, passenger) + if #self.passengers == 0 then + self.icon_name = "equip_cabin_empty" + end +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.passengers > 0 then + logWarning("Removing passenger cabin with passengers onboard!") + ship:setprop("cabin_occupied_cap", ship["cabin_occupied_cap"] - #self.passengers) + 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.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) + +EquipType:SetupPrototype() +LaserType:SetupPrototype() +HyperdriveType:SetupPrototype() +SensorType:SetupPrototype() +BodyScannerType:SetupPrototype() +CabinType:SetupPrototype() +ThrusterType:SetupPrototype() return { laser = laser, @@ -345,5 +526,7 @@ return { LaserType = LaserType, HyperdriveType = HyperdriveType, SensorType = SensorType, - BodyScannerType = BodyScannerType + BodyScannerType = BodyScannerType, + CabinType = CabinType, + ThrusterType = ThrusterType, } diff --git a/data/libs/Equipment.lua b/data/libs/Equipment.lua index 22247280774..4a427bebb27 100644 --- a/data/libs/Equipment.lua +++ b/data/libs/Equipment.lua @@ -1,450 +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 +---@class Equipment +local Equipment = {} -local laser = EquipTypes.laser -local hyperspace = EquipTypes.hyperspace -local misc = EquipTypes.misc +---@type table +Equipment.new = {} --- 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" -}) - -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 +function Equipment.Get(id) + return Equipment.new[id] 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 +function Equipment.Register(id, type) + Equipment.new[id] = type + type.id = id -Serializer:Register("Equipment", serialize, unserialize) + Serializer:RegisterPersistent("Equipment." .. id, type) +end -return EquipTypes +return Equipment diff --git a/data/libs/Event.lua b/data/libs/Event.lua index ff744b8ee3c..5f47f24d5fb 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/HullConfig.lua b/data/libs/HullConfig.lua new file mode 100644 index 00000000000..42941aa12a7 --- /dev/null +++ b/data/libs/HullConfig.lua @@ -0,0 +1,124 @@ +-- 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' +local Serializer = require 'Serializer' + +local utils = require 'utils' + +-- 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 HullConfig.Slot +---@field clone fun(self, mixin):self +local Slot = utils.proto("HullConfig.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: 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 HullConfig +---@field clone fun():self +local HullConfig = utils.proto("HullConfig") + +HullConfig.id = "" +HullConfig.equipCapacity = 0 + +-- Default slot config for a new shipdef +-- Individual shipdefs can redefine slots or remove them by setting the slot to 'false' +---@type table +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 }, +} + +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) + local newShip = HullConfig:clone() + Serializer:RegisterPersistent("ShipDef." .. def.id, newShip) + + newShip.id = def.id + newShip.equipCapacity = def.equipCapacity + + 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 + + return newShip +end + +---@type table +local Configs = {} + +for id, def in pairs(ShipDef) do + if def.tag == "SHIP" or def.tag == "STATIC_SHIP" then + Configs[id] = CreateShipConfig(def) + 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, + GetHullConfig = GetHullConfig +} diff --git a/data/libs/Passengers.lua b/data/libs/Passengers.lua new file mode 100644 index 00000000000..becbc3bce5a --- /dev/null +++ b/data/libs/Passengers.lua @@ -0,0 +1,189 @@ +-- 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 EquipSet = require 'EquipSet' + +local utils = require 'utils' + +-- Module: Passengers +-- +-- Helper utilities for working with passengers onboard a ship +local Passengers = {} + + +-- Function: CountOccupiedCabins +-- +-- Return the number of occupied passenger berths present on the ship +-- +---@param ship Ship +---@return integer +function Passengers.CountOccupiedBerths(ship) + return ship["cabin_occupied_cap"] or 0 +end + +-- Function: CountFreeCabins +-- +-- Return the number of unoccupied passenger berths present on the ship +-- +---@param ship Ship +---@return integer +function Passengers.CountFreeBerths(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") + + return equipSet:GetInstalledWithFilter('Equipment.CabinType', function(equip) + return equip:GetNumPassengers() > 0 + end) +end + +-- Function: GetFreeCabins +-- +-- Return a list of passenger cabins containing at least numBerths free +-- passenger berths. If not specified, numBerths defaults to 1. +-- +---@param ship Ship +---@param numBerths integer? +---@return Equipment.CabinType[] +function Passengers.GetFreeCabins(ship, numBerths) + local equipSet = ship:GetComponent("EquipSet") + + return equipSet:GetInstalledWithFilter('Equipment.CabinType', function(equip) + return equip:GetFreeBerths() >= (numBerths or 1) + 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 = utils.map_table(passengers, function(_, passenger) + return passenger, true + end) + + local count = 0 + + for _, cabin in ipairs(occupiedCabins) do + for _, passenger in ipairs(cabin.passengers) do + if passengerLookup[passenger] then + count = count + 1 + end + 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? +---@return boolean success +function Passengers.EmbarkPassenger(ship, passenger, cabin) + ---@cast cabin Equipment.CabinType? + if not cabin then + cabin = Passengers.GetFreeCabins(ship)[1] + end + + if not cabin then + 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) + + 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) + ---@cast cabin Equipment.CabinType? + if not cabin then + local equipSet = ship:GetComponent("EquipSet") + + ---@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 then + return false + end + + 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/libs/Ship.lua b/data/libs/Ship.lua index beecb46763a..73984e1d58e 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. @@ -35,6 +37,11 @@ function Ship:Constructor() Event.Queue('onShipCreated', self) end +function Ship:OnShipTypeChanged() + -- immediately update any needed components or properties + self:GetComponent('EquipSet'):OnShipTypeChanged() +end + -- class method function Ship.MakeRandomLabel () local letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -51,260 +58,23 @@ local CrewRoster = {} -- Group: Methods -- --- Method: GetEquipSlotCapacity --- --- 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. --- --- > 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 --- --- Status: --- --- experimental --- -function Ship:GetEquipCountOccupied(slot) - return self.equipSet:OccupiedSpace(slot) -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 --- --- 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: --- --- experimental --- -function Ship:AddEquip(item, count, slot) - local ret = self.equipSet:Add(self, item, count, slot) - if ret > 0 then - Event.Queue("onShipEquipmentChanged", self, item) - end - return ret -end - --- --- 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: +-- Method: GetInstalledHyperdrive -- --- > -- add a laser to the rear laser mount --- > ship:SetEquip("laser_rear", 1, Equipment.laser.pulsecannon_4mw) +-- Return the ship's installed hyperdrive equipment item if present. -- --- Availability: --- --- alpha 10 +-- > if ship:GetInstalledHyperdrive() then HyperdriveOverloadAndExplode(ship) end -- -- Status: -- --- experimental +-- stable -- -Ship.SetEquip = function (self, slot, index, item) - self.equipSet:Set(self, slot, index, item) - Event.Queue("onShipEquipmentChanged", self) +---@return Equipment.HyperdriveType? hyperdrive +function Ship:GetInstalledHyperdrive() + ---@type Equipment.HyperdriveType[] + local drives = self:GetComponent("EquipSet"):GetInstalledOfType("hyperdrive") + return drives[1] 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 --- - -Ship.RemoveEquip = function (self, item, count, slot) - local ret = self.equipSet:Remove(self, item, count, slot) - if ret > 0 then - Event.Queue("onShipEquipmentChanged", self, item) - end - return ret -end -- -- Method: IsHyperjumpAllowed @@ -397,7 +167,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" @@ -438,7 +208,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 @@ -462,7 +232,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 @@ -498,28 +268,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) @@ -953,7 +714,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 @@ -971,6 +732,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 diff --git a/data/libs/SpaceStation.lua b/data/libs/SpaceStation.lua index 09024391e71..30221a2e2a7 100644 --- a/data/libs/SpaceStation.lua +++ b/data/libs/SpaceStation.lua @@ -15,13 +15,14 @@ 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 HullConfig = require 'HullConfig' + +local ShipBuilder = require 'modules.MissionUtils.ShipBuilder' +local ShipTemplates = require 'modules.MissionUtils.ShipTemplates' local l = Lang.GetResource("ui-core") @@ -60,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 @@ -117,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 @@ -147,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 -- @@ -175,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 -- @@ -204,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 -- ============================================================================ @@ -540,7 +553,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.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))) @@ -660,23 +677,24 @@ 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 = ShipTemplates.StationPolice:clone { + shipId = Game.system.faction.policeShip, + label = Game.system.faction.policeName or l.POLICE, + } -- 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 diff --git a/data/libs/autoload.lua b/data/libs/autoload.lua index c0c1aea18f9..044b25f7c5d 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 diff --git a/data/libs/utils.lua b/data/libs/utils.lua index 90088189692..fd156b4dcca 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) diff --git a/data/meta/CoreObject/Ship.meta.lua b/data/meta/CoreObject/Ship.meta.lua index dc37389aade..1f2bd70735f 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,16 @@ --- Remaining fuel mass in tons ---@field fuelMassLeft number --- ----@field usedCapacity number ----@field freeCapacity number +--- Currently used equipment volume +---@field equipVolume number +--- Total equipment volume +---@field totalVolume 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/data/meta/ShipDef.lua b/data/meta/ShipDef.lua new file mode 100644 index 00000000000..99d5903ed1d --- /dev/null +++ b/data/meta/ShipDef.lua @@ -0,0 +1,44 @@ +-- 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 + +---@meta + +---@alias ShipDef.Thrust { UP:number, DOWN:number, LEFT:number, RIGHT:number, FORWARD:number, REVERSE:number } + +---@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 ShipDef.Thrust +---@field linAccelerationCap ShipDef.Thrust +---@field effectiveExhaustVelocity number +---@field thrusterFuelUse number -- deprecated +---@field frontCrossSec number +---@field sideCrossSec number +---@field topCrossSec number +---@field atmosphericPressureLimit number +-- +---@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 +---@field minCrew integer +---@field maxCrew integer +---@field hyperdriveClass integer +--- +---@field raw table The entire ShipDef JSON object as a Lua table + +---@type table +local ShipDef = {} + +return ShipDef diff --git a/data/modules/Assassination/Assassination.lua b/data/modules/Assassination/Assassination.lua index 6ae4a126b50..1c88c98bace 100644 --- a/data/modules/Assassination/Assassination.lua +++ b/data/modules/Assassination/Assassination.lua @@ -12,12 +12,12 @@ 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 utils = require 'utils' local lc = Lang.GetResource 'core' @@ -26,6 +26,8 @@ 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 = MissionUtils.ShipTemplates.GenericMercenary + local flavours = {} for i = 0,5 do table.insert(flavours, { @@ -130,10 +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, shipid = ad.shipid, shipname = ad.shipname, shipregid = ad.shipregid, @@ -195,12 +198,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 +211,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 +285,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.GetHullConfig(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 +531,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) diff --git a/data/modules/BreakdownServicing/BreakdownServicing.lua b/data/modules/BreakdownServicing/BreakdownServicing.lua index 6322042260f..8496845e72c 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 @@ -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) @@ -253,6 +257,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 +265,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 +295,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 @@ -311,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/modules/CargoRun/CargoRun.lua b/data/modules/CargoRun/CargoRun.lua index 99af9ec3e9e..67b7aed4bff 100644 --- a/data/modules/CargoRun/CargoRun.lua +++ b/data/modules/CargoRun/CargoRun.lua @@ -8,19 +8,23 @@ 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 = 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 -- typical reward for delivery to a system max_delivery_dist away @@ -51,7 +55,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 +508,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 +532,15 @@ 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 { + label = Game.system.faction.policeName, + 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) diff --git a/data/modules/Combat/Combat.lua b/data/modules/Combat/Combat.lua index 687f351f8bf..4dc335783b9 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' @@ -133,7 +132,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 @@ -346,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 @@ -445,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) diff --git a/data/modules/Debug/DebugCommodityPrices.lua b/data/modules/Debug/DebugCommodityPrices.lua index 3f1e2dfbe67..77dfd66b05a 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 3cfb7007b62..5819ae558ad 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/DebugShip.lua b/data/modules/Debug/DebugShip.lua new file mode 100644 index 00000000000..f3b460b6b17 --- /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.equipCapacity) + 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 +}) diff --git a/data/modules/Debug/DebugShipSpawn.lua b/data/modules/Debug/DebugShipSpawn.lua deleted file mode 100644 index 62f1802cd63..00000000000 --- a/data/modules/Debug/DebugShipSpawn.lua +++ /dev/null @@ -1,239 +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 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 - 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() - 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 de7a650aefe..00000000000 --- a/data/modules/Debug/DebugShipSpawn.moon +++ /dev/null @@ -1,187 +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) -> - 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] - - with Game.player - \SetShipType shipType - \AddEquip item for item in *equip - \UpdateEquipStats! - -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/DeliverPackage/DeliverPackage.lua b/data/modules/DeliverPackage/DeliverPackage.lua index f5e10d99a19..2a8585396e9 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 diff --git a/data/modules/Equipment/Hyperdrive.lua b/data/modules/Equipment/Hyperdrive.lua new file mode 100644 index 00000000000..b3673326aff --- /dev/null +++ b/data/modules/Equipment/Hyperdrive.lua @@ -0,0 +1,133 @@ +-- 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 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=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=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=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=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=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=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=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 }, + 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.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.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.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.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.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.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.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.military", 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 00000000000..387ba714839 --- /dev/null +++ b/data/modules/Equipment/Internal.lua @@ -0,0 +1,260 @@ +-- 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' +local Equipment = require 'Equipment' + +local EquipType = EquipTypes.EquipType +local SensorType = EquipTypes.SensorType +local CabinType = EquipTypes.CabinType +local ThrusterType = EquipTypes.ThrusterType + +--=============================================== +-- Computer Modules +--=============================================== + +Equipment.Register("misc.autopilot", EquipType.New { + l10n_key="AUTOPILOT", + price=1400, purchasable=true, tech_level=1, + slot = { type="computer.autopilot", 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=5, + slot = { type="shield", size=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=7, + slot = { type="shield", size=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" +}) + +--=============================================== +-- 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, + 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", ThrusterType.New { + l10n_key="THRUSTERS_DEFAULT", slots="thruster", + 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", ThrusterType.New { + l10n_key="THRUSTERS_BASIC", slots="thruster", + price=250, purchasable=true, tech_level=5, + slot = { type="thruster", size=1 }, + mass=0.1, volume=0.05, capabilities={ thruster_power=1 }, + icon_name="equip_thrusters_basic" +}) + +Equipment.Register("misc.thrusters_medium", ThrusterType.New { + l10n_key="THRUSTERS_MEDIUM", slots="thruster", + price=560, purchasable=true, tech_level=8, + slot = { type="thruster", size=1 }, + mass=0.05, volume=0.05, capabilities={ thruster_power=2 }, + icon_name="equip_thrusters_medium" +}) + +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 }, + mass=0, volume=0, capabilities={ thruster_power=3 }, + icon_name="equip_thrusters_best" +}) + +--=============================================== +-- Scoops +--=============================================== + +Equipment.Register("misc.fuel_scoop_s1", EquipType.New { + l10n_key="FUEL_SCOOP", + price=3500, purchasable=true, tech_level=4, + 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="hull.cargo_scoop", size=1, hardpoint=true }, + mass=2, volume=4, capabilities={ cargo_scoop=1 }, + icon_name="equip_cargo_scoop" +}) + +--=============================================== +-- Passenger Cabins +--=============================================== + +Equipment.Register("misc.cabin_s1", CabinType.New { + l10n_key="UNOCCUPIED_CABIN", + price=1350, purchasable=true, tech_level=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, + 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 00000000000..b8bed1ce566 --- /dev/null +++ b/data/modules/Equipment/Stats.lua @@ -0,0 +1,149 @@ +-- 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' + +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 + +--============================================================================== + +---@class Equipment.LaserType +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 + +--============================================================================== + +---@class Equipment.BodyScannerType +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 + +--============================================================================== + +---@class Equipment.CabinType +local CabinType = EquipTypes.CabinType + +function CabinType:GetDetailedStats() + local out = self:Super().GetDetailedStats(self) + + table.insert(out, { + le.OCCUPIED_BERTHS, + icons.personal, + self:GetNumPassengers(), + tostring + }) + + table.insert(out, { + le.PASSENGER_BERTHS, + icons.personal, + self.capabilities.cabin, + 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 + +--============================================================================== diff --git a/data/modules/Equipment/Utility.lua b/data/modules/Equipment/Utility.lua new file mode 100644 index 00000000000..2b1f49b990c --- /dev/null +++ b/data/modules/Equipment/Utility.lua @@ -0,0 +1,94 @@ +-- 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' +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 00000000000..64f7db00a99 --- /dev/null +++ b/data/modules/Equipment/Weapons.lua @@ -0,0 +1,354 @@ +-- 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' +local Equipment = require 'Equipment' +local Slot = require 'HullConfig'.Slot + +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.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=60, purchasable=true, tech_level=5, + missile_type="missile_guided", + 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=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", + missile_type="missile_naval", + volume=0, mass=1, + slot = { type="missile", size=4, hardpoint=true }, + icon_name="equip_missile_naval" +}) + +--=============================================== +-- 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_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_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_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 }, + 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" +}) diff --git a/data/modules/MissionUtils.lua b/data/modules/MissionUtils.lua index 71df4fc8b07..294b4576963 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/OutfitRules.lua b/data/modules/MissionUtils/OutfitRules.lua new file mode 100644 index 00000000000..f07c612c958 --- /dev/null +++ b/data/modules/MissionUtils/OutfitRules.lua @@ -0,0 +1,131 @@ +-- 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 +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 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 = { + slot = "weapon", + minSize = 2, + minThreat = 50.0, + maxThreatFactor = 0.7, + balance = true, +} + +OutfitRules.ModerateWeapon = { + slot = "weapon", + limit = 2, + maxSize = 3, + minThreat = 30.0, + maxThreatFactor = 0.6, + balance = true, +} + +OutfitRules.EasyWeapon = { + slot = "weapon", + 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, + 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, + minThreat = 20.0, + maxThreatFactor = 0.6 +} + +-- Default rules always equip the item if there's enough space + +OutfitRules.DefaultPassengerCabins = { + slot = "cabin", + filter = "cabin.passenger" +} + +OutfitRules.DefaultHyperdrive = { + slot = "hyperdrive" +} + +OutfitRules.DefaultAtmoShield = { + slot = "hull", + filter = "hull.atmo_shield", + limit = 1 +} + +OutfitRules.DefaultShieldGen = { + slot = "shield" +} + +OutfitRules.DefaultAutopilot = { + slot = "computer", + equip = "misc.autopilot", + limit = 1 +} + +OutfitRules.DefaultShieldBooster = { + equip = "misc.shield_energy_booster", + limit = 1 +} + +OutfitRules.DefaultLaserCooling = { + slot = nil, + equip = "misc.laser_cooling_booster", + limit = 1 +} + +OutfitRules.DefaultRadar = { + slot = "sensor", + filter = "sensor.radar", + limit = 1 +} + +return OutfitRules diff --git a/data/modules/MissionUtils/ShipBuilder.lua b/data/modules/MissionUtils/ShipBuilder.lua new file mode 100644 index 00000000000..949ea201675 --- /dev/null +++ b/data/modules/MissionUtils/ShipBuilder.lua @@ -0,0 +1,857 @@ +-- 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' +local Equipment = require 'Equipment' +local EquipSet = require 'EquipSet' +local Event = require 'Event' +local HullConfig = require 'HullConfig' +local ShipDef = require 'ShipDef' +local Space = require 'Space' +local Ship = require 'Ship' + +local Rules = require '.OutfitRules' + +local utils = require 'utils' + +local hullThreatCache = {} + +local slotTypeMatches = EquipSet.SlotTypeMatches + +-- Class: MissionUtils.ShipBuilder +-- +-- 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 = {} + +ShipBuilder.OutfitRules = Rules + +-- ============================================================================= + +-- Scalars determining a "threat factor" rating for a ship's hull and equipment +-- to generate difficulty-appropriate NPCs + +-- Default values to use if a mission hasn't set anything +ShipBuilder.kDefaultRandomThreatMin = 10.0 +ShipBuilder.kDefaultRandomThreatMax = 100.0 + +-- || 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.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.20 +ShipBuilder.kAeroStabilityThreatBase = 0.80 + +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.8 +ShipBuilder.kMinHullThreatFactor = 0.12 + +-- || 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 + +-- 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 + +-- || 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 + +-- ============================================================================= + +-- 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") + +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 + +-- ============================================================================= + +-- 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 = "" +ShipPlan.label = "" +ShipPlan.freeVolume = 0 +ShipPlan.equipMass = 0 +ShipPlan.threat = 0 +ShipPlan.freeThreat = 0 +ShipPlan.filled = {} +ShipPlan.equip = {} +ShipPlan.install = {} +ShipPlan.slots = {} + +function ShipPlan:__clone() + 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 + +-- 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 + self.freeVolume = shipConfig.equipCapacity + + 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 + +-- 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()) + + if equip:isProto() then + equip = equip:Instance() + end + + if slot then + self.filled[slot.id] = equip + table.insert(self.install, slot.id) + 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) + + if equip.provides_slots then + self:AddSlots(slot.id, equip.provides_slots) + end +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 + + 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) + -- 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 * dualMod + + ShipBuilder.kWeaponSharedHullThreat * damage * 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 + + local threat = ShipBuilder.kShieldThreatFactor * shield_tons + + ShipBuilder.kShieldSharedHullThreat * shield_tons * 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) + 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 + shipDef.equipCapacity * 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^(1/3) / crossSectionAvg^(1/2)) + + threat.total = threat.armor * threat.thrust * threat.aero * threat.crosssection + threat.total = utils.round(threat.total, 0.01) + + return threat +end + +-- ============================================================================= + +-- 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 +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) + 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.filter_array(shipPlan.slots, function(slot) + -- Don't install in already-filled slots + 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 + + -- 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) + 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 - reserveThreat) >= threat then + + local inst = equip:Instance() + + 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 + + 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 + + -- 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) + and matchRuleSlot(equip.slot, rule.filter or rule.slot) + and equip.volume <= maxVolume + end) + + -- print("Available equipment: " .. #filteredEquip) + + -- No equipment items can be installed, rule is finished + if #filteredEquip == 0 then + 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 + + -- 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). + ---@type EquipType[] + local compatible = utils.map_array(filteredEquip, function(equip) + local threat = threatCache[equip] + + local compat = EquipSet.CompatibleWithSlot(equip, slot) + and threat <= (shipPlan.freeThreat - reserveThreat) + and threat <= maxThreat + + 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) + + -- print("Slot " .. slot.id .. " - compatible: " .. #compatible) + + -- 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)] + 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, + -- 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) + + -- Just install the "best" item we have + local equip = compatible[1] + shipPlan:AddEquipToPlan(equip, slot, threatCache[equip]) + end + + numInstalled = numInstalled + 1 + + if rule.limit and numInstalled >= rule.limit then + break + end + + end + + end + +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 + + table.insert(hullList, template.shipId) + + else + + for id, shipDef in pairs(ShipDef) do + + 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 + + -- 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) + end + + end + + end + + end + + if #hullList == 0 then + return nil + end + + local hullIdx = Engine.rand:Integer(1, #hullList) + local shipId = hullList[hullIdx] + + -- print(" threat {} => {} ({} / {})" % { threat, shipId, hullIdx, #hullList }) + + return HullConfig.GetHullConfig(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 + + local randomMod = template.randomModifier or 1.0 + + local shipPlan = ShipPlan:clone { + threat = hullThreat, + freeThreat = threat - hullThreat, + maxThreat = threat + } + + shipPlan:SetConfig(shipConfig) + + for _, rule in ipairs(template.rules) do + + 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) + else + local equip = Equipment.Get(rule.equip) + assert(equip) + + local equipThreat = ShipBuilder.ComputeEquipThreatFactor(equip, hullThreat) + + if shipPlan.freeVolume >= equip.volume and shipPlan.freeThreat >= equipThreat then + shipPlan:AddEquipToPlan(equip) + end + end + + end + + end + + shipPlan.label = template.label or Ship.MakeRandomLabel() + + return shipPlan + +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 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 + 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(equip, slot) + end + + for _, equip in ipairs(shipPlan.equip) do + assert(not equip:isProto()) + equipSet:Install(equip) + end + + -- TODO: ammunition / other items inside of instanced equipment + + ship:SetLabel(shipPlan.label) + +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? +---@param nearDist number? +---@param farDist number? +---@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(template, threat) + assert(hullConfig) + + local plan = ShipBuilder.MakePlan(template, hullConfig, threat) + assert(plan) + + local ship = Space.SpawnShipNear(plan.shipId, body, nearDist or 50, farDist or 100) + assert(ship) + + ShipBuilder.ApplyPlan(ship, plan) + + 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 +---@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 + + local hullConfig = ShipBuilder.SelectHull(template, threat) + assert(hullConfig) + + local plan = ShipBuilder.MakePlan(template, hullConfig, threat) + assert(plan) + + local ship = Space.SpawnShipOrbit(plan.shipId, body, nearDist, farDist) + assert(ship) + + ShipBuilder.ApplyPlan(ship, plan) + + 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 +---@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 hullConfig = ShipBuilder.SelectHull(template, threat) + assert(hullConfig) + + local plan = ShipBuilder.MakePlan(template, hullConfig, threat) + assert(plan) + + local ship = Space.SpawnShipLanded(plan.shipId, body, lat, lon) + assert(ship) + + ShipBuilder.ApplyPlan(ship, plan) + + return ship +end + +-- 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 +function ShipBuilder.MakeShipDocked(body, template, threat) + 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.SpawnShipDocked(plan.shipId, body) + assert(ship) + + ShipBuilder.ApplyPlan(ship, plan) + + 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 +---@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 +function ShipBuilder.BuildHullThreatCache() + for id, shipDef in pairs(ShipDef) do + local threat = ShipBuilder.ComputeHullThreatFactor(shipDef) + + hullThreatCache[id] = threat + end +end + +Event.Register("onGameStart", function() + ShipBuilder.BuildHullThreatCache() +end) + +return ShipBuilder diff --git a/data/modules/MissionUtils/ShipTemplates.lua b/data/modules/MissionUtils/ShipTemplates.lua new file mode 100644 index 00000000000..bce63b7308b --- /dev/null +++ b/data/modules/MissionUtils/ShipTemplates.lua @@ -0,0 +1,144 @@ +-- 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", + hyperclass = 1, + 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, + OutfitRules.DefaultRadar, + } +} + +ShipTemplates.GenericPirate = ShipBuilder.Template:clone { + role = "pirate", + hyperclass = 1, + 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, + OutfitRules.DefaultRadar, + } +} + +ShipTemplates.WeakPirate = ShipBuilder.Template:clone { + role = "pirate", + hyperclass = 1, + 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, + OutfitRules.DefaultRadar, + } +} + +ShipTemplates.GenericPolice = ShipBuilder.Template:clone { + role = "police", + hyperclass = 1, + 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, + OutfitRules.DefaultRadar, + } +} + +ShipTemplates.StationPolice = ShipBuilder.Template:clone { + role = "police", + rules = { + { + slot = "weapon", + equip = "laser.pulsecannon_dual_1mw", + limit = 1 + }, + OutfitRules.ModerateWeapon, + OutfitRules.EasyWeapon, + OutfitRules.ModerateShieldGen, + -- Always has laser cooling but no need for hyperdrive + OutfitRules.DefaultLaserCooling, + OutfitRules.DefaultAtmoShield, + OutfitRules.DefaultAutopilot, + OutfitRules.DefaultRadar, + } +} + +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, + OutfitRules.DefaultRadar, + } +} + +ShipTemplates.GenericMercenary = ShipBuilder.Template:clone { + role = "mercenary", + hyperclass = 1, + 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, + OutfitRules.DefaultRadar, + } +} + +return ShipTemplates diff --git a/data/modules/Pirates.lua b/data/modules/Pirates.lua index 9ed61554c43..b73fee7bad2 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].equipCapacity + + -- 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 diff --git a/data/modules/PolicePatrol/PolicePatrol.lua b/data/modules/PolicePatrol/PolicePatrol.lua index 9845c575639..8b67f85af3e 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) diff --git a/data/modules/Rondel/Rondel.lua b/data/modules/Rondel/Rondel.lua index fb2054e8483..487549d86b4 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,21 +156,25 @@ 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 - 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 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 diff --git a/data/modules/Scoop/Scoop.lua b/data/modules/Scoop/Scoop.lua index 7330be2e966..bff187b565b 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) @@ -392,7 +402,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 fbc26d9f456..9b997cd4fbb 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 039f257ae13..079320f3d78 100644 --- a/data/modules/SearchRescue/SearchRescue.lua +++ b/data/modules/SearchRescue/SearchRescue.lua @@ -33,18 +33,26 @@ 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' 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' 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' @@ -222,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) @@ -305,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 @@ -323,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. @@ -351,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() @@ -388,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 @@ -407,20 +352,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.CountOccupiedBerths(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.CountFreeBerths(ship) > 0 end local cargoPresent = function (ship, item) @@ -454,18 +391,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 +413,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.CountFreeBerths(Game.player) + + return avail_cabins >= empty_cabins end -- extended mission functions @@ -519,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 @@ -542,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 @@ -551,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.equipCapacity + 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.GetHullConfig(def.id) ---@type HullConfig + + -- should have a default hull config + if not config then + return false + end - if arraySize(shipdefs) == 0 then + -- 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 + + 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)] @@ -663,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 @@ -691,23 +609,36 @@ end local createTargetShip = function (mission) -- Create the target ship to be search for. - local ship - local shipdef = shipdefFromName(mission.shipdef_name) + local ship ---@type Ship + 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) @@ -715,70 +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.capabilities.mass < b.capabilities.mass end) - for i = #drives, 1, -1 do - local test_drive = drives[i] - if shipdef.capacity / 10 > test_drive.capabilities.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) - -- load passengers - if mission.pickup_pass > 0 then - ship:AddEquip(Equipment.misc.cabin_occupied, mission.pickup_pass) - 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 hypfuel = drive.capabilities.hyperclass ^ 2 -- fuel for max range - ship:GetComponent('CargoManager'):AddCommodity(drive.fuel or Commodities.hydrogen, hypfuel) + 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!") -- 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 - -- 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) + 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 + + 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 @@ -808,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, @@ -909,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, @@ -917,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", @@ -937,11 +849,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 +868,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 +1028,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 @@ -1251,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 @@ -1291,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 @@ -1377,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, @@ -1514,9 +1435,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 +1482,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 +1520,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 +1595,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 +1630,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) @@ -1843,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 @@ -1854,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 @@ -2097,7 +2018,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') @@ -2188,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 }, diff --git a/data/modules/SecondHand/SecondHand.lua b/data/modules/SecondHand/SecondHand.lua index 53fde8d899a..e4c4376e02b 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 @@ -126,18 +122,20 @@ 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 -- 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 0b03919f193..8a2ac4d07f9 100644 --- a/data/modules/Taxi/Taxi.lua +++ b/data/modules/Taxi/Taxi.lua @@ -4,19 +4,20 @@ 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' +local PirateTemplate = MissionUtils.ShipTemplates.GenericPirate + -- Get the language resource local l = Lang.GetResource("module-taxi") local lc = Lang.GetResource 'core' @@ -107,15 +108,17 @@ 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) - passengers = passengers + group + for i = 1, #group do + Passengers.EmbarkPassenger(Game.player, group[i]) + 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) - passengers = passengers - group + for i = 1, #group do + Passengers.DisembarkPassenger(Game.player, group[i]) + end + passengers = passengers - #group end local isQualifiedFor = function(reputation, ad) @@ -179,18 +182,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.CountFreeBerths(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, @@ -198,12 +207,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) @@ -354,27 +363,10 @@ 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 + 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 @@ -423,17 +415,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 @@ -491,7 +488,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 9bc4f3854f1..95df107e366 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' @@ -281,7 +280,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,44 +305,38 @@ 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 pairs(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 }) 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 }, - { 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/Events.lua b/data/modules/TradeShips/Events.lua index 6e22a8546a5..2a960cc8488 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/modules/TradeShips/Flow.lua b/data/modules/TradeShips/Flow.lua index 5b24553a8c7..f540647ccb4 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' @@ -34,10 +33,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 0d0ca629a06..5a35a02107e 100644 --- a/data/modules/TradeShips/Trader.lua +++ b/data/modules/TradeShips/Trader.lua @@ -1,61 +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 - - 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 - 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 randomMod = 0.1 + lawlessness * 0.9 - -- 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 hullConfig = HullConfig.GetHullConfig(shipId) + assert(hullConfig, "Can't find hull config for shipId " .. shipId) - -- 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 + local template = TraderTemplate:clone { + randomModifier = randomMod + } + + local hullThreat = ShipBuilder.GetHullThreat(shipId).total + + -- 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) @@ -120,7 +135,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) @@ -209,7 +224,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') @@ -242,7 +257,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 diff --git a/data/pigui/libs/equip-card.lua b/data/pigui/libs/equip-card.lua new file mode 100644 index 00000000000..90674b5b3b7 --- /dev/null +++ b/data/pigui/libs/equip-card.lua @@ -0,0 +1,209 @@ +-- 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' +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 styles = ui.theme.styles +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 slot HullConfig.Slot? +---@field present integer? +---@field total integer? +---@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 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.details, function() + ui.spacing() + ui.textWrapped(desc) + end) + end + end) + + if self.tooltipStats and data.stats then + ui.spacing() + ui.separator() + ui.spacing() + + ui.withFont(pionillium.details, 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) + if ui.beginTable("EquipStats", 2, { "NoSavedSettings" }) then + + 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, styles.ItemInnerSpacing.x) + + local value, format = tooltip[3], tooltip[4] + ui.text(format(value)) + end + ui.popTextWrapPos() + + ui.endTable() + end +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" .. 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 + 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, + iconColor = equip and colors.white or colors.fontDim + } + + if equip then + 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() + + for i, v in ipairs(equip:GetItemCardStats()) do + out[i] = v + end + end + + return out +end + +return EquipCard diff --git a/data/pigui/libs/equipment-market.lua b/data/pigui/libs/equipment-market.lua deleted file mode 100644 index 0ca85f83508..00000000000 --- 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 00000000000..4178f92d341 --- /dev/null +++ b/data/pigui/libs/equipment-outfitter.lua @@ -0,0 +1,648 @@ +-- 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 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 orbiteer = ui.fonts.orbiteer + +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' + +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 + 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 rounding = styles.ItemCardRounding + + 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() + + variant = variant or ui.theme.buttonColors.dark + local clicked = false + + ui.withButtonColors(variant, function() + pigui.PushStyleVar("FramePadding", framePadding) + 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 + iconOffset, 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 canReplace boolean +---@field available boolean +---@field price number + +---@class UI.EquipmentOutfitter.EquipCard : UI.EquipCard +local EquipCardUnavailable = EquipCard.New() + +EquipCardUnavailable.tooltipStats = false +EquipCardUnavailable.colors = ui.theme.buttonColors.deselected +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() + 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 + + end) + end) +end + +--============================================================================= + +---@class UI.EquipmentOutfitter : UI.Module +---@field New fun(): self +local Outfitter = utils.class("UI.EquipmentOutfitter", Module) + +Outfitter.SortKeys = { + { id = "name", label = l.NAME_OBJECT }, + { id = "mass", label = l.MASS }, + { id = "volume", label = l.VOLUME }, + { id = "price", label = l.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) + + self.ship = nil ---@type Ship + self.station = nil ---@type SpaceStation + self.filterSlot = nil ---@type HullConfig.Slot? + 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? + + 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() + 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 + return id, nil + end + + if not equip.purchasable or not self:stationHasTech(equip.tech_level) then + return id, nil + 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 id, equip + end) +end + +---@param e EquipType +function Outfitter:getStock(e) + return self.station:GetEquipmentStock(e:GetPrototype()) +end + +-- Cost of the equipment item if buying +function Outfitter:getBuyPrice(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) + -- 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 +function Outfitter:getInstallPrice(e) + return self:getBuyPrice(e) - (self.replaceEquip and self:getSellPrice(self.replaceEquip) or 0) +end + +function Outfitter:buildEquipmentList() + local equipment = self:getAvailableEquipment() + + ---@type EquipType[] + local equipList = {} + + for _, v in pairs(equipment) do + table.insert(equipList, v) + end + + 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.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 + 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) + + 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 = {} + +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:onSetSort(id, state) + self.sortId = id + self.sortState = state + + self:sortEquipmentList() +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 = 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 = ui.Format.Money(self:getSellPrice(data.equip)) + + 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 + +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() + 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 + + ui.endTable() + 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 + + if ui.beginTable("##CompareEquipStats", 3) then + + 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 + +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() + 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.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 + 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 + + local doubleClicked = clicked and ui.isMouseDoubleClicked(0) + + if doubleClicked and data.available then + self:message("onBuyItem", data.equip) + 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/item-card.lua b/data/pigui/libs/item-card.lua index b202f7b4c6d..ac8fba0babf 100644 --- a/data/pigui/libs/item-card.lua +++ b/data/pigui/libs/item-card.lua @@ -23,10 +23,11 @@ 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 +ItemCard.iconColor = colors.white function ItemCard:drawTitle(data, regionWidth, isSelected) -- override to draw your specific item card type! @@ -59,33 +60,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 +98,10 @@ end function ItemCard:draw(data, isSelected) local lineSpacing = self.lineSpacing + if self.highlightBar then + ui.addCursorPos(Vector2(self.rounding, 0)) + end + -- initial sizing setup local textHeight = pionillium.body.size + pionillium.details.size + lineSpacing.y @@ -107,55 +110,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, data.iconColor or self.iconColor) - -- 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,9 +147,9 @@ 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 + if v[3] and ui.isItemHovered("ForTooltip") then detailTooltip = v end end @@ -176,22 +157,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 +174,10 @@ function ItemCard:draw(data, isSelected) ui.endGroup() + if ui.isItemHovered("ForTooltip") and not detailTooltip then + self:drawTooltip(data, isSelected) + end + return isClicked, isHovered, highlightSize end diff --git a/data/pigui/libs/radial-menu.lua b/data/pigui/libs/radial-menu.lua index e58adc5276e..9271be32175 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/libs/ship-equipment.lua b/data/pigui/libs/ship-equipment.lua index 25cd9cb97e7..5c4fb46772e 100644 --- a/data/pigui/libs/ship-equipment.lua +++ b/data/pigui/libs/ship-equipment.lua @@ -1,17 +1,19 @@ -- 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 EquipSet = require 'EquipSet' +local pigui = require 'Engine'.pigui 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 +28,108 @@ 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" }, -} - --- 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" - } } +---@class UI.EquipmentWidget : UI.Module +---@field New fun(id): self +local EquipmentWidget = utils.class("UI.EquipmentWidget", Module) + +-- 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_bay", "missile" } }, + { 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", "fuel_scoop", "structure" } }, } -- -- ============================================================================= --- 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:isProto() then + 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 = self.selectedSlot and equip + self:buildSections() 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 + self:buildSections() 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) + if self.selectedSlot then + local nextSlot = self.adjacentSlots[self.selectedSlot] + if nextSlot and not nextSlot.equip then + self:onSelectSlot(nextSlot) 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 - 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.replaceEquip = self.selectedEquip + self.market:refresh() + end) - s.defaultFuncs.onBuyFailed(s, e, reason) - end -}) -end + self.market:hookMessage("onSellItem", function(_, item) + self:onSellItem(item) --- --- ============================================================================= --- Ship Equipment Widget Display --- ============================================================================= --- + self.market.replaceEquip = self.selectedEquip + self.market:refresh() + end) -function EquipmentWidget.New(id) - ---@class EquipmentWidget - local self = setmetatable({}, EquipmentWidget.meta) + self.market:hookMessage("onClose", function(_, item) + self:clearSelection() + + 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 +138,208 @@ function EquipmentWidget.New(id) self.showShipNameEdit = false self.showEmptySlots = true + ---@type EquipType? self.selectedEquip = nil - self.selectedEquipSlots = nil + ---@type HullConfig.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 + + self.sectionList = {} + ---@type table + self.adjacentSlots = {} 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 + +---@param slotData UI.EquipCard.Data +---@param children table? +function EquipmentWidget:onSelectSlot(slotData, children) + if not slotData then + self:clearSelection() + return + end + + local isSelected = self.selectedEquip == slotData.equip + and self.selectedSlot == slotData.slot + + if self.selectionActive and isSelected 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 = 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 -function EquipmentWidget:onEmptySlotClicked(slots) - if self.station then - self.selectedEquip = { nil, nil } - self.selectedEquipSlots = slots - self.equipmentMarket:refresh() - self.equipmentMarket.scrollReset = true +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 HullConfig.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 + + local prevCard = items[#items] + if prevCard and prevCard.slot then + self.adjacentSlots[prevCard.slot] = slotData + 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 = {} + self.adjacentSlots = {} + + 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 EquipmentWidget + ---@param self UI.EquipmentWidget draw = function(self) ui.withFont(pionillium.body, function() - for i, v in ipairs(sections) do - self:drawEquipSection(v) + for _, section in ipairs(self.sectionList) do + self:drawSlotGroup(section) end + end) end } @@ -288,67 +350,8 @@ 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)) - local pos = ui.getCursorScreenPos() local isClicked, isHovered, size = EquipCard:draw(data, isSelected) @@ -360,14 +363,6 @@ 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 @@ -379,32 +374,45 @@ end 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) + local tl = ui.getItemRect() + 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 + local br = select(2, ui.getItemRect()) + + if tooltip and ui.isMouseHoveringRect(tl, br) then ui.setTooltip(tooltip) end 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(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 + 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 +422,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) @@ -422,73 +430,78 @@ function EquipmentWidget:drawSectionHeader(data, numItems, maxSlots, totalWeight ui.setCursorPos(contentsPos) + if sectionOpen then + fun() + ui.treePop() + end + return sectionOpen end --- Calculate information about an equipment category for displaying ship internal equipment -function EquipmentWidget:calcEquipSectionInfo(slots) - local equipment = {} - local maxSlots = 0 - local totalWeight = 0 +function EquipmentWidget:drawOpenHeader(id, defaultOpen, fun) + local isOpen = pigui.GetBoolState(id, defaultOpen) - -- 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) - end + if isOpen then + ui.treePush(id) + fun() + ui.treePop() end - return equipment, maxSlots, totalWeight -end + local iconSize = Vector2(ui.getTextLineHeight()) --- Draw an equipment section and all contained equipment items -function EquipmentWidget:drawEquipSection(data) + local clicked = ui.invisibleButton(id, Vector2(ui.getContentRegion().x, ui.getTextLineHeight())) + local tl, br = ui.getItemRect() - local slots = data.slots or { data.slot } - local equipment, maxSlots, weight = self:calcEquipSectionInfo(slots) + local color = ui.getButtonColor(ui.theme.buttonColors.transparent, ui.isItemHovered(), ui.isItemActive()) + ui.addRectFilled(tl, br, color, ui.theme.styles.ItemCardRounding, 0xF) - local sectionOpen = self:drawSectionHeader(data, #equipment, maxSlots, weight) + ui.addIconSimple((tl + br - iconSize) * 0.5, + isOpen and icons.chevron_up or icons.chevron_down, + iconSize, colors.fontDim) - 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) - end) + ui.setItemTooltip(isOpen and l.COLLAPSE or l.EXPAND) + + if clicked then + pigui.SetBoolState(id, not isOpen) + end +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]) +function EquipmentWidget:drawSlotGroup(list) + local drawList = function() + for i, card in ipairs(list.items) do - if self:drawEquipmentItem(equipData, isSelected) then - self:onEquipmentClicked(v, slots) + local equip = card.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 + self:message("onSelectSlot", card, list.children[i]) + end + + local childSlots = list.children[i] + if childSlots then + self:drawSlotGroup(childSlots) + end 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 and list.isMiscEquip then + local card = 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(card, isSelected) then + self:message("onSelectSlot", card) end end - - ui.treePop() end + if list.id then + self:drawOpenHeader(list.id, self.showEmptySlots or list.count > 0, drawList) + else + self:drawSectionHeader(list, drawList) + end end -- @@ -498,23 +511,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 +545,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 +555,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 +572,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 +585,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:buildSections() 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 diff --git a/data/pigui/modules/equipment.lua b/data/pigui/modules/equipment.lua index dbf59a4bc7f..83691e34a43 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 637bca25c60..c5047b226ed 100644 --- a/data/pigui/modules/flight-ui/target-scanner.lua +++ b/data/pigui/modules/flight-ui/target-scanner.lua @@ -14,20 +14,20 @@ 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 stats = target:GetStats() - local mass = stats.staticMass - local cargo = stats.usedCargo + local engine = target:GetInstalledHyperdrive() + local mass = target.staticMass + local cargo = target.usedCargo if engine then engine = engine:GetName() else @@ -63,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 @@ -75,14 +79,15 @@ 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() 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/hyperjump-planner.lua b/data/pigui/modules/hyperjump-planner.lua index 5dcb46c01ad..2a323f53533 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 @@ -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/modules/info-view/01-ship-info.lua b/data/pigui/modules/info-view/01-ship-info.lua index 08a335edf10..e70b6d395a6 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.CountFreeBerths(player) + local cabinOccupied = Passengers.CountOccupiedBerths(player) + local cabinMaximum = cabinEmpty + cabinOccupied hyperdrive = hyperdrive or nil frontWeapon = frontWeapon or nil @@ -39,11 +40,8 @@ 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_cap = 1 - if atmo_shield then - atmo_shield_cap = atmo_shield.capabilities.atmo_shield - end + local atmo_shield = equipSet:GetInstalledOfType("hull.atmo_shield")[1] + local atmo_shield_cap = player["atmo_shield_cap"] textTable.draw({ { l.REGISTRATION_NUMBER..":", shipLabel}, @@ -56,12 +54,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 +73,10 @@ 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}, - false, - { l.MISSILE_MOUNTS..":", shipDef.equipSlotCapacity.missile}, - { l.SCOOP_MOUNTS..":", shipDef.equipSlotCapacity.scoop}, + { l.PASSENGER_CABIN_CAPACITY..":", cabinMaximum }, 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..":", 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/info-view/03-econ-trade.lua b/data/pigui/modules/info-view/03-econ-trade.lua index bfdccc72d6a..e9416d9d481 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,11 +175,12 @@ 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 - gauge_bar(cabins_used, string.format('%%i %s / %i %s', l.USED, cabins_free, l.FREE), - 0, cabins_total, icons.personal) + local berths_free = Passengers.CountFreeBerths(player) + local berths_used = Passengers.CountOccupiedBerths(player) + local berths_total = berths_used + berths_free + + 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() @@ -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/new-game-window/class.lua b/data/pigui/modules/new-game-window/class.lua index 565fb601805..476f12265b1 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,19 @@ local Game = require 'Game' local profileCombo = { items = {}, selected = 0 } +local equipment2 = { + 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({ name = lui.START_AT_MARS, desc = lui.START_AT_MARS_DESC, @@ -36,10 +46,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 +67,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 +88,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 +163,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 5392b33de38..11ea54c7dba 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 @@ -922,20 +932,20 @@ 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 } 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), - 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/new-game-window/summary.lua b/data/pigui/modules/new-game-window/summary.lua index 440618041e9..618baad0767 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 diff --git a/data/pigui/modules/radar.lua b/data/pigui/modules/radar.lua index 56c03abe9ab..97a8201e81d 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 7fde58ccebd..428f8212438 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 62009f98245..2f40ec1d5e1 100644 --- a/data/pigui/modules/station-view/04-shipMarket.lua +++ b/data/pigui/modules/station-view/04-shipMarket.lua @@ -10,7 +10,8 @@ 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 HullConfig = require 'HullConfig' local ui = require 'pigui' @@ -90,19 +91,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 + value = value - Equipment.new["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 pairs(equipment) do + local n = e.count or 1 + value = value + n * e.price * equipSellPriceReduction end return math.ceil(value) @@ -110,10 +111,10 @@ 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(ShipDef[Game.player.shipId]) + local cost = def.basePrice - tradeInValue(Game.player) if math.floor(cost) ~= cost then error("Ship price non-integer value.") end @@ -129,8 +130,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 +151,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) @@ -230,8 +242,8 @@ end function FormatAndCompareShips:draw_hyperdrive_cell(desc) local function fmt( v ) - return v > 0 and - Equipment.hyperspace["hyperdrive_" .. v]:GetName() or l.NONE + return v > 0 and v < 8 and + Equipment.new["hyperspace.hyperdrive_" .. v]:GetName() or l.NONE end self:compare_and_draw_column( desc, self.def.hyperdriveClass, self.b.def.hyperdriveClass, fmt ) @@ -270,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.GetHullConfig(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.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) + + 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 @@ -297,24 +336,24 @@ 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) self.column = 0 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.fullMass = def.hullMass + def.equipCapacity + def.fuelTankMass + self.massAtCapacity = def.hullMass + def.equipCapacity + self.cargoCapacity = def.cargo self.def = def self.b = b end @@ -340,7 +379,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() @@ -377,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") @@ -386,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" ) @@ -442,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/data/pigui/modules/system-view-ui.lua b/data/pigui/modules/system-view-ui.lua index 0cd8c94d59c..b4d42127693 100644 --- a/data/pigui/modules/system-view-ui.lua +++ b/data/pigui/modules/system-view-ui.lua @@ -844,17 +844,18 @@ 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) }) + table.insert(data, { name = luc.CARGO, value = Format.MassTonnes(body.usedCargo) }) end else data = {} diff --git a/data/pigui/themes/default.lua b/data/pigui/themes/default.lua index 2d7a72fc551..223e61e1685 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 = { @@ -676,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, diff --git a/data/pigui/views/mainmenu.lua b/data/pigui/views/mainmenu.lua index 088222b0a07..758b76f6f78 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 diff --git a/data/pigui/views/map-sector-view.lua b/data/pigui/views/map-sector-view.lua index 50a5cb60f65..46396d5e6e0 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) diff --git a/data/pigui/views/station-view.lua b/data/pigui/views/station-view.lua index 10dca141b13..74ad0b1d51d 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,16 +56,23 @@ 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 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) diff --git a/data/ships/ac33.json b/data/ships/ac33.json index 339aab9288a..d33ae8ea5d5 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 c97fb9ba4c3..3a89c0d9705 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 ffe2cd5b0a3..f89cb95c1c2 100644 --- a/data/ships/bowfin.json +++ b/data/ships/bowfin.json @@ -7,31 +7,99 @@ "ship_class": "light_fighter", "min_crew": 1, "max_crew": 1, - "price": 34430, - "hull_mass": 25, + "price": 43104, + "hull_mass": 20, + "structure_mass": 7.1, + "armor_mass": 11.6, + "volume": 135, "atmospheric_pressure_limit": 6.5, - "capacity": 18, - "slots": { - "engine": 1, - "cabin": 0, - "scoop": 0, - "laser_front": 1, - "missile": 24, - "cargo": 6 + "capacity": 15, + "cargo": 4, + + "equipment_slots": { + "missile_bay_1": { + "type": "missile_bay.bowfin_internal", + "size": 2, + "i18n_key": "HARDPOINT_MISSILE_ARRAY", + "tag": "tag_missile_bay_1", + "hardpoint": true, + "default": "missile_rack.bowfin_internal_s2" + }, + "missile_bay_2": { + "type": "missile_bay.bowfin_internal", + "size": 2, + "i18n_key": "HARDPOINT_MISSILE_ARRAY", + "tag": "tag_missile_bay_2", + "hardpoint": true, + "default": "missile_rack.bowfin_internal_s2" + }, + "missile_bay_3": { + "type": "missile_bay.bowfin_internal", + "size": 2, + "i18n_key": "HARDPOINT_MISSILE_ARRAY", + "tag": "tag_missile_bay_3", + "hardpoint": true, + "default": "missile_rack.bowfin_internal_s2" + }, + "missile_bay_4": { + "type": "missile_bay.bowfin_internal", + "size": 2, + "i18n_key": "HARDPOINT_MISSILE_ARRAY", + "tag": "tag_missile_bay_4", + "hardpoint": true, + "default": "missile_rack.bowfin_internal_s2" + }, + "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, + "i18n_key": "HARDPOINT_WEAPON_CHIN", + "tag": "tag_laser_chin", + "hardpoint": true, + "gimbal": [8,8] + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 1, + "size_min": 1 + }, + "thruster": { + "type": "thruster", + "size": 1, + "count": 12 + } }, - "roles": ["mercenary", "pirate", "courier"], + "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, + "forward_thrust": 2100000, "forward_acceleration_cap": 33.354, "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, @@ -39,7 +107,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, @@ -49,6 +117,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 8de1a2133f3..9ad7bebdb92 100644 --- a/data/ships/coronatrix.json +++ b/data/ships/coronatrix.json @@ -7,26 +7,95 @@ "ship_class": "light_courier", "min_crew": 1, "max_crew": 1, - "price": 46614, - "hull_mass": 18, + "price": 50349, + "hull_mass": 21, + "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": 35, + "cargo": 16, + + "equipment_slots": { + "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" + }, + "fuel_scoop_s1": { + "type": "fuel_scoop", + "size": 1, + "tag": "tag_fuel_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 + }, + "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] + }, + "computer_1": { + "type": "computer", + "size": 1 + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 2, + "size_min": 1 + }, + "thruster": { + "type": "thruster", + "size": 1, + "count": 16 + } }, - "roles": ["mercenary", "merchant", "pirate"], + + "roles": ["pirate","mercenary","courier"], "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, @@ -38,7 +107,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 58b9a3f9374..82038ddfc40 100644 --- a/data/ships/coronatrix_police.json +++ b/data/ships/coronatrix_police.json @@ -8,25 +8,84 @@ "min_crew": 1, "max_crew": 1, "price": 0, - "hull_mass": 18, + "hull_mass": 21, + "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": 35, + "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 + }, + "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 +97,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/deneb.json b/data/ships/deneb.json index 30fa960b62d..fa7c4dc5b23 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 61b1af94c41..b47e1dac896 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 6312ef4be69..fbfc1ba77f1 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 cedab2c2413..87317977f8c 100644 --- a/data/ships/lunarshuttle.json +++ b/data/ships/lunarshuttle.json @@ -4,33 +4,63 @@ "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": 27062, + "hull_mass": 15, + "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 + }, + "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 +68,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/malabar.json b/data/ships/malabar.json index 64af76ea049..d64dbe01a40 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 9f9f3007dca..a311433ed6d 100644 --- a/data/ships/molamola.json +++ b/data/ships/molamola.json @@ -6,20 +6,58 @@ "manufacturer": "kaluri", "ship_class": "light_freighter", "min_crew": 1, - "max_crew": 3, - "price": 81062, - "hull_mass": 27, + "max_crew": 1, + "price": 60279, + "hull_mass": 23, + "structure_mass": 12.7, + "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_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 + }, + "fuel_scoop_s1": { + "type": "fuel_scoop", + "size": 1, + "tag": "tag_fuel_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 + }, + "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 +76,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/molaramsayi.json b/data/ships/molaramsayi.json index 0321ff1ffc4..af46d460447 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 f0c6f940058..b9a11244ee1 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 16d57d9e65d..0d4266e145e 100644 --- a/data/ships/pumpkinseed.json +++ b/data/ships/pumpkinseed.json @@ -1,31 +1,97 @@ { "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": 46180, + "hull_mass": 13, + "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 + "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": "fuel_scoop", + "size": 1, + "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 + }, + "cabin_s1_2": { + "type": "cabin", + "size": 1 + }, + "hyperdrive": { + "type": "hyperdrive", + "size": 1, + "size_min": 1 + }, + "thruster": { + "type": "thruster", + "size": 1, + "count": 12 + } }, - "roles": ["mercenary", "merchant", "pirate", "courier"], + "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, + "forward_thrust": 1320000, "forward_acceleration_cap": 41.202, "reverse_thrust": 700000, "reverse_acceleration_cap": 21.582, @@ -38,7 +104,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 eaa46e2cef8..d6dd956d71b 100644 --- a/data/ships/pumpkinseed_police.json +++ b/data/ships/pumpkinseed_police.json @@ -6,28 +6,92 @@ "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": 18, + "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": "fuel_scoop", + "size": 1, + "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 + }, + "thruster": { + "type": "thruster", + "size": 1, + "count": 12 + } }, - "roles": ["mercenary"], - - "effective_exhaust_velocity": 9900000, + + "roles": ["mercenary","police"], + "effective_exhaust_velocity": 15200000, "thruster_fuel_use": -1.0, - "fuel_tank_mass": 9, + "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, @@ -40,7 +104,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, diff --git a/data/ships/sinonatrix.json b/data/ships/sinonatrix.json index 0a9024e9565..a28ed367123 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 b9ebf574e3d..dd26fe6f876 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 c52233ee71d..34fc21cf0ec 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 6046add6c9a..1e47b6cdcfd 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 a6f6ee6a5db..b969782d7cb 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 7b80f70f1f3..1bdd7586f97 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 49bae163512..e4c2079c6dc 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 cacf50144f2..c8c087c49bf 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, diff --git a/src/SectorView.cpp b/src/SectorView.cpp index 08b76914398..74c21860303 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 diff --git a/src/Ship.cpp b/src/Ship.cpp index 7264563088d..316847f3e18 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" @@ -91,8 +85,6 @@ Ship::Ship(const ShipType::Id &shipId) : m_aiMessage = AIERROR_NONE; m_decelerating = false; - InitEquipSet(); - SetModel(m_type->modelName.c_str()); SetupShields(); @@ -190,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) @@ -298,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; @@ -322,24 +310,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(); @@ -642,16 +612,13 @@ 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.used_cargo = p.Get("usedCargo").get_integer(); + m_stats.free_cargo = p.Get("totalCargo").get_integer() - m_stats.used_cargo; - 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); float shield_cap = p.Get("shield_cap"); @@ -1287,7 +1254,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) { @@ -1581,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) { @@ -1602,9 +1568,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 +1578,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 8f8f205b244..416057fe062 100644 --- a/src/Ship.h +++ b/src/Ship.h @@ -36,10 +36,10 @@ namespace Graphics { } struct shipstats_t { - int used_capacity; + 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; @@ -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; @@ -291,7 +287,6 @@ class Ship : public DynamicBody { void SetupShields(); void EnterHyperspace(); void InitMaterials(); - void InitEquipSet(); bool m_invulnerable; diff --git a/src/ShipType.cpp b/src/ShipType.cpp index 3ee626ded45..3b675f6885b 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", ""); @@ -177,14 +179,10 @@ 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) { - 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; } @@ -199,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 646b92f7a67..ae45793c944 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 :) @@ -50,7 +49,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 @@ -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/LuaShip.cpp b/src/lua/LuaShip.cpp index f56bb0eea4d..7f73d7b1b8c 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 * @@ -1659,13 +1622,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"; @@ -1738,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 }, @@ -1756,7 +1711,6 @@ void LuaObject::RegisterClass() }; const luaL_Reg l_attrs[] = { - { "equipSet", l_ship_attr_equipset }, { 0, 0 } }; @@ -1904,56 +1858,41 @@ void LuaObject::RegisterClass() * experimental * * - * Attribute: staticMass + * Attribute: loadedMass * - * Mass of the ship including hull, equipment and cargo, but excluding - * thruster fuel mass. Measured in tonnes. - * - * 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. - * - * Availability: - * - * November 2013 + * Hull capacity used by cargo only (not equipment). Measured in cargo units. * * Status: * - * experimental - * - * - * Attribute: freeCapacity + * stable * - * Total space remaining. Measured in tonnes. * - * Availability: + * Attribute: totalCargo * - * November 2013 + * Hull capacity available for cargo (not equipment). Measured in cargo units. * * Status: * - * experimental + * stable * */ diff --git a/src/lua/LuaShipDef.cpp b/src/lua/LuaShipDef.cpp index ef1dd48eb66..db3389ac9ca 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" @@ -183,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 * @@ -259,7 +246,8 @@ 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); pi_lua_settable(l, "basePrice", st.baseprice); @@ -287,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, "EquipSet"); - 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); @@ -313,6 +284,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);