1-- Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details 2-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt 3 4local utils = require 'utils' 5local Serializer = require 'Serializer' 6local Lang = require 'Lang' 7local ShipDef = require 'ShipDef' 8local Timer = require 'Timer' 9local Comms = require 'Comms' 10 11local Game = package.core['Game'] 12local Space = package.core['Space'] 13 14-- XXX this is kind of hacky, but we'll put up with it for now 15-- Ideally we should separate out the hyperdrives into their own module 16-- that can function independently of the cargo 17local cargo = {} 18local laser = {} 19local hyperspace = {} 20local misc = {} 21 22-- 23-- Class: EquipType 24-- 25-- A container for a ship's equipment. 26-- 27-- Its constructor takes a table, the "specs". Mandatory fields are the following: 28-- * l10n_key: the key to look up the name and description of 29-- the object in a language-agnostic way 30-- * l10n_resource: where to look up the aforementioned key. If not specified, 31-- the system assumes "equipment-core" 32-- * capabilities: a table of string->int, having at least "mass" as a valid key 33-- 34-- All specs are copied directly within the object (even those I know nothing about), 35-- but it is a shallow copy. This is particularly important for the capabilities, as 36-- modifying the capabilities of one EquipType instance might modify them for other 37-- instances if the same table was used for all (which is strongly discouraged by the 38-- author, but who knows ? Some people might find it useful.) 39-- 40-- 41local EquipType = utils.inherits(nil, "EquipType") 42 43function EquipType.New (specs) 44 local obj = {} 45 for i,v in pairs(specs) do 46 obj[i] = v 47 end 48 if not obj.l10n_resource then 49 obj.l10n_resource = "equipment-core" 50 end 51 local l = Lang.GetResource(obj.l10n_resource) 52 obj.volatile = { 53 description = l:get(obj.l10n_key.."_DESCRIPTION") or "", 54 name = l[obj.l10n_key] or "" 55 } 56 setmetatable(obj, EquipType.meta) 57 if type(obj.slots) ~= "table" then 58 obj.slots = {obj.slots} 59 end 60 return obj 61end 62 63function EquipType:Serialize() 64 local tmp = EquipType.Super().Serialize(self) 65 local ret = {} 66 for k,v in pairs(tmp) do 67 if type(v) ~= "function" then 68 ret[k] = v 69 end 70 end 71 72 ret.volatile = nil 73 return ret 74end 75 76function EquipType.Unserialize(data) 77 local obj = EquipType.Super().Unserialize(data) 78 setmetatable(obj, EquipType.meta) 79 if not obj.l10n_resource then 80 obj.l10n_resource = "equipment-core" 81 end 82 local l = Lang.GetResource(obj.l10n_resource) 83 obj.volatile = { 84 description = l:get(obj.l10n_key.."_DESCRIPTION") or "", 85 name = l[obj.l10n_key] or "" 86 } 87 return obj 88end 89 90-- 91-- Group: Methods 92-- 93 94-- 95-- Method: GetDefaultSlot 96-- 97-- returns the default slot for this equipment 98-- 99-- Parameters: 100-- 101-- ship (optional) - if provided, tailors the answer for this specific ship 102-- 103-- Return: 104-- 105-- slot_name - A string identifying the slot. 106-- 107function EquipType:GetDefaultSlot(ship) 108 return self.slots[1] 109end 110 111-- 112-- Method: IsValidSlot 113-- 114-- tells whether the given slot is valid for this equipment 115-- 116-- Parameters: 117-- 118-- slot - a string identifying the slot in question 119-- 120-- ship (optional) - if provided, tailors the answer for this specific ship 121-- 122-- Return: 123-- 124-- valid - a boolean qualifying the validity of the slot. 125-- 126function EquipType:IsValidSlot(slot, ship) 127 for _, s in ipairs(self.slots) do 128 if s == slot then 129 return true 130 end 131 end 132 return false 133end 134 135function EquipType:GetName() 136 return self.volatile.name 137end 138 139function EquipType:GetDescription() 140 return self.volatile.description 141end 142 143local function __ApplyMassLimit(ship, capabilities, num) 144 if num <= 0 then return 0 end 145 -- we need to use mass_cap directly (not, eg, ship.freeCapacity), 146 -- because ship.freeCapacity may not have been updated when Install is called 147 -- (see implementation of EquipSet:Set) 148 local avail_mass = ShipDef[ship.shipId].capacity - (ship.mass_cap or 0) 149 local item_mass = capabilities.mass or 0 150 if item_mass > 0 then 151 num = math.min(num, math.floor(avail_mass / item_mass)) 152 end 153 return num 154end 155 156local function __ApplyCapabilities(ship, capabilities, num, factor) 157 if num <= 0 then return 0 end 158 factor = factor or 1 159 for k,v in pairs(capabilities) do 160 local full_name = k.."_cap" 161 local prev = (ship:hasprop(full_name) and ship[full_name]) or 0 162 ship:setprop(full_name, (factor*v*num)+prev) 163 end 164 return num 165end 166 167function EquipType:Install(ship, num, slot) 168 local caps = self.capabilities 169 num = __ApplyMassLimit(ship, caps, num) 170 return __ApplyCapabilities(ship, caps, num, 1) 171end 172 173function EquipType:Uninstall(ship, num, slot) 174 return __ApplyCapabilities(ship, self.capabilities, num, -1) 175end 176 177-- Base type for weapons 178local LaserType = utils.inherits(EquipType, "LaserType") 179function LaserType:Install(ship, num, slot) 180 if num > 1 then num = 1 end -- FIXME: support installing multiple lasers (e.g., in the "cargo" slot?) 181 if LaserType.Super().Install(self, ship, 1, slot) < 1 then return 0 end 182 local prefix = slot..'_' 183 for k,v in pairs(self.laser_stats) do 184 ship:setprop(prefix..k, v) 185 end 186 return 1 187end 188 189function LaserType:Uninstall(ship, num, slot) 190 if num > 1 then num = 1 end -- FIXME: support uninstalling multiple lasers (e.g., in the "cargo" slot?) 191 if LaserType.Super().Uninstall(self, ship, 1) < 1 then return 0 end 192 local prefix = (slot or "laser_front").."_" 193 for k,v in pairs(self.laser_stats) do 194 ship:unsetprop(prefix..k) 195 end 196 return 1 197end 198 199-- Single drive type, no support for slave drives. 200local HyperdriveType = utils.inherits(EquipType, "HyperdriveType") 201 202function HyperdriveType:GetMaximumRange(ship) 203 return 625.0*(self.capabilities.hyperclass ^ 2) / (ship.staticMass + ship.fuelMassLeft) 204end 205 206-- range_max is as usual optional 207function HyperdriveType:GetDuration(ship, distance, range_max) 208 range_max = range_max or self:GetMaximumRange(ship) 209 local hyperclass = self.capabilities.hyperclass 210 return 0.36*distance^2/(range_max*hyperclass) * (86400*math.sqrt(ship.staticMass + ship.fuelMassLeft)) 211end 212 213-- range_max is optional, distance defaults to the maximal range. 214function HyperdriveType:GetFuelUse(ship, distance, range_max) 215 range_max = range_max or self:GetMaximumRange(ship) 216 local distance = distance or range_max 217 local hyperclass_squared = self.capabilities.hyperclass^2 218 return math.clamp(math.ceil(hyperclass_squared*distance / range_max), 1, hyperclass_squared); 219end 220 221-- if the destination is reachable, returns: distance, fuel, duration 222-- if the destination is out of range, returns: distance 223-- if the specified jump is invalid, returns nil 224function HyperdriveType:CheckJump(ship, source, destination) 225 if ship:GetEquip('engine', 1) ~= self or source:IsSameSystem(destination) then 226 return nil 227 end 228 local distance = source:DistanceTo(destination) 229 local max_range = self:GetMaximumRange(ship) -- takes fuel into account 230 if distance > max_range then 231 return distance 232 end 233 local fuel = self:GetFuelUse(ship, distance, max_range) -- specify range_max to avoid unnecessary recomputing. 234 235 local duration = self:GetDuration(ship, distance, max_range) -- same as above 236 return distance, fuel, duration 237end 238 239-- like HyperdriveType.CheckJump, but uses Game.system as the source system 240-- if the destination is reachable, returns: distance, fuel, duration 241-- if the destination is out of range, returns: distance 242-- if the specified jump is invalid, returns nil 243function HyperdriveType:CheckDestination(ship, destination) 244 if not Game.system then 245 return nil 246 end 247 return self:CheckJump(ship, Game.system.path, destination) 248end 249 250-- Give the range for the given remaining fuel 251-- If the fuel isn't specified, it takes the current value. 252function HyperdriveType:GetRange(ship, remaining_fuel) 253 local range_max = self:GetMaximumRange(ship) 254 local fuel_max = self:GetFuelUse(ship, range_max, range_max) 255 remaining_fuel = remaining_fuel or ship:CountEquip(self.fuel) 256 257 if fuel_max <= remaining_fuel then 258 return range_max, range_max 259 end 260 local range = range_max*remaining_fuel/fuel_max 261 262 while range > 0 and self:GetFuelUse(ship, range, range_max) > remaining_fuel do 263 range = range - 0.05 264 end 265 266 -- range is never negative 267 range = math.max(range, 0) 268 return range, range_max 269end 270 271local HYPERDRIVE_SOUNDS_NORMAL = { 272 warmup = "Hyperdrive_Charge", 273 abort = "Hyperdrive_Abort", 274 jump = "Hyperdrive_Jump", 275} 276 277local HYPERDRIVE_SOUNDS_MILITARY = { 278 warmup = "Hyperdrive_Charge_Military", 279 abort = "Hyperdrive_Abort_Military", 280 jump = "Hyperdrive_Jump_Military", 281} 282 283function HyperdriveType:HyperjumpTo(ship, destination) 284 -- First off, check that this is the primary engine. 285 local engines = ship:GetEquip('engine') 286 local primary_index = 0 287 for i,e in ipairs(engines) do 288 if e == self then 289 primary_index = i 290 break 291 end 292 end 293 if primary_index == 0 then 294 -- wrong ship 295 return "WRONG_SHIP" 296 end 297 local distance, fuel_use, duration = self:CheckDestination(ship, destination) 298 if not distance then 299 return "OUT_OF_RANGE" 300 end 301 if not fuel_use then 302 return "INSUFFICIENT_FUEL" 303 end 304 ship:setprop('nextJumpFuelUse', fuel_use) 305 local warmup_time = 5 + self.capabilities.hyperclass*1.5 306 307 local sounds 308 if self.fuel == cargo.military_fuel then 309 sounds = HYPERDRIVE_SOUNDS_MILITARY 310 else 311 sounds = HYPERDRIVE_SOUNDS_NORMAL 312 end 313 314 return ship:InitiateHyperjumpTo(destination, warmup_time, duration, sounds), fuel_use, duration 315end 316 317function HyperdriveType:OnLeaveHyperspace(ship) 318 if ship:hasprop('nextJumpFuelUse') then 319 local amount = ship.nextJumpFuelUse 320 ship:RemoveEquip(self.fuel, amount) 321 if self.byproduct then 322 ship:AddEquip(self.byproduct, amount) 323 end 324 ship:unsetprop('nextJumpFuelUse') 325 end 326end 327 328local SensorType = utils.inherits(EquipType, "SensorType") 329 330function SensorType:BeginAcquisition(callback) 331 self:ClearAcquisition() 332 self.callback = callback 333 if self:OnBeginAcquisition() then 334 self.state = "RUNNING" 335 self.stop_timer = false 336 Timer:CallEvery(1, function() 337 return self:ScanProgress() 338 end) 339 end 340 self:DoCallBack() 341end 342 343function SensorType:ScanProgress() 344 if self.stop_timer == true then 345 return true 346 end 347 if self:IsScanning() then 348 self:OnProgress() 349 if self:IsScanning() then 350 self.stop_timer = false 351 end 352 elseif self.state == "PAUSED" then 353 self.stop_timer = false 354 elseif self.state == "DONE" then 355 self.stop_timer = true 356 end 357 self:DoCallBack() 358 return self.stop_timer 359end 360 361function SensorType:PauseAcquisition() 362 if self:IsScanning() then 363 self.state = "PAUSED" 364 end 365 self:DoCallBack() 366end 367 368function SensorType:UnPauseAcquisition() 369 if self.state == "PAUSED" then 370 self.state = "RUNNING" 371 end 372 self:DoCallBack() 373end 374 375function SensorType:ClearAcquisition() 376 self:OnClear() 377 self.state = "DONE" 378 self.stop_timer = true 379 self:DoCallBack() 380 self.callback = nil 381end 382 383function SensorType:GetLastResults() 384 return self.progress 385end 386 387-- gets called from C++ to set the MeterBar value 388-- must return a number 389function SensorType:GetProgress() 390 if type(self.progress) == "number" then 391 return self.progress 392 else 393 return 0 394 end 395end 396 397function SensorType:IsScanning() 398 return self.state == "RUNNING" or self.state == "HALTED" 399end 400 401function SensorType:DoCallBack() 402 if self.callback then self.callback(self.progress, self.state) end 403end 404 405local BodyScannerType = utils.inherits(SensorType, "BodyScannerType") 406 407function BodyScannerType:OnBeginAcquisition() 408 local closest_planet = Game.player:FindNearestTo("PLANET") 409 if closest_planet then 410 local altitude = self:DistanceToSurface(closest_planet) 411 if altitude and altitude < self.max_range then 412 self.target_altitude = altitude 413 self.target_body_path = closest_planet.path 414 local l = Lang.GetResource(self.l10n_resource) 415 Comms.Message(l.STARTING_SCAN.." "..string.format('%6.3f km',self.target_altitude/1000)) 416 return true 417 end 418 end 419 return false 420end 421 422function BodyScannerType:OnProgress() 423 local l = Lang.GetResource(self.l10n_resource) 424 local target_body = Space.GetBody(self.target_body_path.bodyIndex) 425 if target_body and target_body:exists() then 426 local altitude = self:DistanceToSurface(target_body) 427 local distance_diff = math.abs(altitude - self.target_altitude) 428 local percentual_diff = distance_diff/self.target_altitude 429 if percentual_diff <= self.bodyscanner_stats.scan_tolerance then 430 if self.state == "HALTED" then 431 Comms.Message(l.SCAN_RESUMED) 432 self.state = "RUNNING" 433 end 434 self.progress = self.progress + self.bodyscanner_stats.scan_speed 435 if self.progress > 100 then 436 self.state = "DONE" 437 self.progress = {body=target_body.path, altitude=self.target_altitude} 438 Comms.Message(l.SCAN_COMPLETED) 439 end 440 else -- strayed out off range 441 if self.state == "RUNNING" then 442 local lower_limit = self.target_altitude-(percentual_diff*self.target_altitude) 443 local upper_limit = self.target_altitude+(percentual_diff*self.target_altitude) 444 Comms.Message(l.OUT_OF_SCANRANGE.." "..string.format('%6.3f km',lower_limit/1000).." - "..string.format('%6.3f km',upper_limit/1000)) 445 end 446 self.state = "HALTED" 447 end 448 else -- we lost the target body 449 self:ClearAcquisition() 450 end 451end 452 453function BodyScannerType:OnClear() 454 self.target_altitude = 0 455 self.progress = 0 456end 457 458function BodyScannerType:DistanceToSurface(body) 459 return select(3,Game.player:GetGroundPosition(body)) -- altitude 460end 461 462Serializer:RegisterClass("LaserType", LaserType) 463Serializer:RegisterClass("EquipType", EquipType) 464Serializer:RegisterClass("HyperdriveType", HyperdriveType) 465Serializer:RegisterClass("SensorType", SensorType) 466Serializer:RegisterClass("BodyScannerType", BodyScannerType) 467 468return { 469 cargo = cargo, 470 laser = laser, 471 hyperspace = hyperspace, 472 misc = misc, 473 EquipType = EquipType, 474 LaserType = LaserType, 475 HyperdriveType = HyperdriveType, 476 SensorType = SensorType, 477 BodyScannerType = BodyScannerType 478} 479