diff --git a/data/lang/module-scoop/en.json b/data/lang/module-scoop/en.json new file mode 100644 index 000000000..cb61e4ac0 --- /dev/null +++ b/data/lang/module-scoop/en.json @@ -0,0 +1,198 @@ +{ + "ACCEPTED_ARMS_DEALER": { + "description": "", + "message": "Great! Thank you." + }, + "ACCEPTED_ILLEGAL_GOODS": { + "description": "", + "message": "Thanks. You won't regret it!" + }, + "ACCEPTED_LEGAL_GOODS": { + "description": "", + "message": "Thank you. You know a good deal when you see one!" + }, + "ACCEPTED_RESCUE": { + "description": "", + "message": "Thank you very much. The crew will appreciate that." + }, + "ADTEXT_ARMS_DEALER": { + "description": "", + "message": "PICK UP: Prompt recovery of drifting container required." + }, + "ADTEXT_ILLEGAL_GOODS": { + "description": "", + "message": "EASY MONEY: Valuable information for sale." + }, + "ADTEXT_LEGAL_GOODS": { + "description": "", + "message": "INFORMATION FOR SALE: Location of a debris field with valuable cargo." + }, + "ADTEXT_RESCUE": { + "description": "", + "message": "HELP NEEDED: Urgent escape capsule rescue operation." + }, + "CLIENT": { + "description": "", + "message": "Client:" + }, + "DANGER": { + "description": "The risk level", + "message": "Danger:" + }, + "DEADLINE": { + "description": "Must be delivered by", + "message": "Deadline:" + }, + "DENY_1": { + "description": "", + "message": "Excuse me, sir? I don't think your current qualification is sufficient." + }, + "DENY_2": { + "description": "", + "message": "Sorry, I think this task is beyond your ability as a pilot." + }, + "DETONATORS": { + "description": "", + "message": "Detonators" + }, + "DOCKING_INSTRUCTION": { + "description": "", + "message": "Nice to meet you. Please approach to within 100m to complete the cargo transfer!" + }, + "FAILURE_MSG_ARMS_DEALER": { + "description": "", + "message": "You fool. The client's not gonna like this!" + }, + "FAILURE_MSG_RESCUE": { + "description": "", + "message": "Your unreliability has caused great harm. Get out of my sight!" + }, + "HOW_MUCH_TIME": { + "description": "", + "message": "How much time do I have?" + }, + "HOW_MUCH_TIME_ARMS_DEALER": { + "description": "", + "message": "My business partner is in orbit around {star} to avoid detection. It would be great if you could reach him before {date}." + }, + "HOW_MUCH_TIME_ILLEGAL_GOODS": { + "description": "", + "message": "Don't waste time. Only the early bird catches the worm!" + }, + "HOW_MUCH_TIME_LEGAL_GOODS": { + "description": "", + "message": "I don't know. But if I were you, I would be quick!" + }, + "HOW_MUCH_TIME_RESCUE": { + "description": "", + "message": "It would be good if you could reach the location before {date}." + }, + "INTROTEXT_ARMS_DEALER": { + "description": "", + "message": "Hello Commander. My name is {client}. It would be great if you could help me out. An unreliable freighter captain dropped my cargo near {planet} and has disappeared since. Your task would be to pick up one container and deliver it to ship {shipid} in orbit around {star}. I would pay you {cash}." + }, + "INTROTEXT_ILLEGAL_GOODS": { + "description": "", + "message": "Hey buddy, I know about a debris field with illegal goods. The police caught a smuggler close to {planet}. They haven't had time to clean up the area yet. If you are quick, you can make easy money! I can give you the coordinates. It only costs you {cash}. Don't let the chance slip by!" + }, + "INTROTEXT_LEGAL_GOODS": { + "description": "", + "message": "Hello my friend. I'm {client}. I heard about a cargo hauler that got into trouble near {planet} because of bad ship maintenance. Amazing who gets to fly a spaceship these days, isn't it? Anyway, they had to dump a lot of the cargo. If you're quick, you can grab the cargo very easily! This info is as good as new! I'll sell you the coordinates for lousy {cash}. What do you say?" + }, + "INTROTEXT_RESCUE": { + "description": "", + "message": "Hi, my name is {client}. The crew of a freighter had to abandon their ship after an incident. The ship owner is willing to pay {cash} for a discreet rescue mission. The accident site is near {planet}. Please pick up the crew and bring them to a station!" + }, + "NUCLEAR_MISSILE": { + "description": "A cargo item", + "message": "Nuclear missile" + }, + "OK_AGREED": { + "description": "", + "message": "OK, agreed." + }, + "REPEAT_THE_REQUEST": { + "description": "", + "message": "Could you please repeat the request?" + }, + "RESCUE_CAPSULE": { + "description": "", + "message": "Rescue capsule" + }, + "ROCKET_LAUNCHERS": { + "description": "", + "message": "Rocket launchers" + }, + "SCOOP": { + "description": "Name of mission type", + "message": "Scoop" + }, + "SET_AS_TARGET": { + "description": "", + "message": "Set as navigation target" + }, + "SHIP": { + "description": "", + "message": "Ship:" + }, + "SHIP_DESTROYED": { + "description": "", + "message": "Oh no. {shipid} has been destroyed. Deliver the cargo to {station} now!" + }, + "SPACEPORT": { + "description": "", + "message": "Spaceport:" + }, + "SPOILED_FOOD": { + "description": "cargo item", + "message": "Spoiled food" + }, + "SUCCESS_MSG_ARMS_DEALER": { + "description": "", + "message": "Hey Ace, nice flying! Money has been transferred." + }, + "SUCCESS_MSG_RESCUE": { + "description": "", + "message": "Thank you very much! The crew would also like to express their thanks." + }, + "TOXIC_WASTE": { + "description": "cargo item", + "message": "Toxic waste" + }, + "UNKNOWN": { + "description": "", + "message": "Unknown" + }, + "WARNING": { + "description": "", + "message": "Hey! What are you trying to do? Our contract is at risk! Please pick that up again!" + }, + "WHY_NOT_YOURSELF": { + "description": "", + "message": "Why don't you do it yourself?" + }, + "WHY_NOT_YOURSELF_ARMS_DEALER": { + "description": "", + "message": "The local authorities are watching me! I have to keep a low profile for a few weeks." + }, + "WHY_NOT_YOURSELF_ILLEGAL_GOODS": { + "description": "", + "message": "I'm just an office clerk. I don't own a ship." + }, + "WHY_NOT_YOURSELF_LEGAL_GOODS": { + "description": "", + "message": "I don't have a ship." + }, + "WHY_NOT_YOURSELF_RESCUE": { + "description": "", + "message": "I have no ship available at this time." + }, + "YOU_DO_NOT_HAVE_A_SCOOP": { + "description": "", + "message": "Sorry, you do not have a cargo scoop." + }, + "YOU_DO_NOT_HAVE_ENOUGH_MONEY": { + "description": "", + "message": "You don't have enough money." + } +} diff --git a/data/modules/Scoop/Scoop.lua b/data/modules/Scoop/Scoop.lua new file mode 100644 index 000000000..8a1801599 --- /dev/null +++ b/data/modules/Scoop/Scoop.lua @@ -0,0 +1,857 @@ +-- Copyright © 2008-2021 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 Ship = require 'Ship' +local Comms = require 'Comms' +local Event = require 'Event' +local Space = require 'Space' +local Timer = require 'Timer' +local Engine = require 'Engine' +local Format = require 'Format' +local Mission = require 'Mission' +local ShipDef = require 'ShipDef' +local Character = require 'Character' +local Equipment = require 'Equipment' +local Serializer = require 'Serializer' + +local utils = require 'utils' + +local l = Lang.GetResource("module-scoop") +local lc = Lang.GetResource("ui-core") + +local AU = 149597870700.0 +local LEGAL = 1 +local ILLEGAL = 2 + +local mission_reputation = 1 +local mission_time = 14*24*60*60 +local max_dist = 20 * AU + +local ads = {} +local missions = {} + +local rescue_capsule = Equipment.EquipType.New({ + name = "rescue_capsule", + l10n_key = "RESCUE_CAPSULE", + l10n_resource = "module-scoop", + slots = "cargo", + price = 500, + icon_name = "Default", + model_name = "escape_pod", + capabilities = { mass = 1, crew = 1 }, + purchasable = false +}) + +local rocket_launchers = Equipment.EquipType.New({ + name = "rocket_launchers", + l10n_key = "ROCKET_LAUNCHERS", + l10n_resource = "module-scoop", + slots = "cargo", + price = 500, + icon_name = "Default", + capabilities = { mass = 1 }, + purchasable = false +}) + +local detonators = Equipment.EquipType.New({ + name = "detonators", + l10n_key = "DETONATORS", + l10n_resource = "module-scoop", + slots = "cargo", + price = 250, + icon_name = "Default", + capabilities = { mass = 1 }, + purchasable = false +}) + +local nuclear_missile = Equipment.EquipType.New({ + name = "nuclear_missile", + l10n_key = "NUCLEAR_MISSILE", + l10n_resource = "module-scoop", + slots = "cargo", + price = 1250, + icon_name = "Default", + model_name = "missile", + capabilities = { mass = 1 }, + purchasable = false +}) + +-- Useless waste that the player has to sort out +local toxic_waste = Equipment.EquipType.New({ + name = "toxic_waste", + l10n_key = "TOXIC_WASTE", + l10n_resource = "module-scoop", + slots = "cargo", + price = -50, + icon_name = "Default", + capabilities = { mass = 1 }, + purchasable = false +}) + +local spoiled_food = Equipment.EquipType.New({ + name = "spoiled_food", + l10n_key = "SPOILED_FOOD", + l10n_resource = "module-scoop", + slots = "cargo", + price = -10, + icon_name = "Default", + capabilities = { mass = 1 }, + purchasable = false +}) + +local unknown = Equipment.EquipType.New({ + name = "unknown", + l10n_key = "UNKNOWN", + l10n_resource = "module-scoop", + slots = "cargo", + price = -5, + icon_name = "Default", + capabilities = { mass = 1 }, + purchasable = false +}) + +local rescue_capsules = { + rescue_capsule +} + +local weapons = { + rocket_launchers, + detonators, + nuclear_missile +} + +local waste = { + toxic_waste, + spoiled_food, + unknown, + Equipment.cargo.radioactives, + Equipment.cargo.rubbish +} + +local flavours = { + { + id = "LEGAL_GOODS", + cargo_type = nil, + reward = -500, + amount = 20, + }, + { + id = "ILLEGAL_GOODS", + cargo_type = nil, + reward = -1000, + amount = 10, + }, + { + id = "RESCUE", + cargo_type = rescue_capsules, + reward = 750, + amount = 4, + return_to_station = true, + }, + { + id = "ARMS_DEALER", + cargo_type = weapons, + reward = 1000, + amount = 1, + deliver_to_ship = true, + }, +} + +-- Sort goods, legal and illegal +local sortGoods = function (goods) + local legal_goods = {} + local illegal_goods = {} + local system = Game.system + + for _, e in pairs(goods) do + if e.purchasable and system:IsCommodityLegal(e.name) then + table.insert(legal_goods, e) + else + table.insert(illegal_goods, e) + end + end + + return legal_goods, illegal_goods +end + +-- Returns the number of flavours of the given string (assuming first flavour has suffix '_1'). +local getNumberOfFlavours = function (str) + local num = 1 + + while l:get(str .. "_" .. num) do + num = num + 1 + end + + return num - 1 +end + +-- Create a debris field in a random distance to a system body +local spawnDebris = function (debris, amount, sbody, min, max, lifetime) + local list = {} + local cargo + local body = Space.GetBody(sbody:GetSystemBody().index) + + for i = 1, Engine.rand:Integer(math.ceil(amount / 4), amount) do + cargo = debris[Engine.rand:Integer(1, #debris)] + body = Space.SpawnCargoNear(cargo, body, min, max, lifetime) + if i > 1 then body:SetVelocity(list[1].body:GetVelocity()) end + table.insert(list, { cargo = cargo, body = body }) + min = 10 + max = 1000 + end + + -- add some useless waste + for i = 1, Engine.rand:Integer(1, 9) do + cargo = waste[Engine.rand:Integer(1, #waste)] + body = Space.SpawnCargoNear(cargo, body, min, max, lifetime) + body:SetVelocity(list[1].body:GetVelocity()) + end + + return list +end + +-- Create a couple of police ships +local spawnPolice = function (station) + local ship + local police = {} + local shipdef = ShipDef[Game.system.faction.policeShip] + + for i = 1, 2 do + ship = Space.SpawnShipDocked(shipdef.id, station) + ship:SetLabel(lc.POLICE) + ship:AddEquip(Equipment.laser.pulsecannon_1mw) + table.insert(police, ship) + if station.type == "STARPORT_SURFACE" then + ship:AIEnterLowOrbit(Space.GetBody(station:GetSystemBody().parent.index)) + end + end + + Timer:CallAt(Game.time + 5, function () + for _, s in pairs(police) do + s:AIKill(Game.player) + end + end) + + return police +end + +-- Returns a random system close to the players location +local nearbySystem = function () + local dist = 5 + local systems = {} + + while #systems < 1 do + systems = Game.system:GetNearbySystems(dist) + dist = dist + 5 + end + + return systems[Engine.rand:Integer(1, #systems)].path +end + +-- Create a ship in orbit +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 radius = star:GetSystemBody().radius + local min, max + if star:GetSystemBody().type == "WHITE_DWARF" then + min = radius * 30 + max = radius * 40 + else + min = radius * 3.5 + 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 +end + +local removeMission = function (mission, ref) + local oldReputation = Character.persistent.player.reputation + local sender = mission.client_ship and mission.ship_label or mission.client.name + + if mission.status == "COMPLETED" then + Character.persistent.player.reputation = oldReputation + mission_reputation + Game.player:AddMoney(mission.reward) + Comms.ImportantMessage(l["SUCCESS_MSG_" .. mission.id], sender) + elseif mission.status == "FAILED" then + Character.persistent.player.reputation = oldReputation - mission_reputation + Comms.ImportantMessage(l["FAILURE_MSG_" .. mission.id], sender) + end + Event.Queue("onReputationChanged", oldReputation, Character.persistent.player.killcount, + Character.persistent.player.reputation, Character.persistent.player.killcount) + + if ref == nil then + for r, m in pairs(missions) do + if m == mission then ref = r break end + end + end + mission:Remove() + missions[ref] = nil +end + +-- Cargo transfer to a ship +local transferCargo = function (mission, ref) + Timer:CallEvery(9, function () + if not mission.client_ship then return true end + + if not mission.docking_in_progress and Game.player:DistanceTo(mission.client_ship) <= 5000 then + mission.docking_in_progress = true + Comms.ImportantMessage(l.DOCKING_INSTRUCTION, mission.ship_label) + end + + if Game.player:DistanceTo(mission.client_ship) <= 100 then + + -- unload mission cargo + for i, e in pairs(mission.debris) do + if e.body == nil then + if Game.player:RemoveEquip(e.cargo, 1, "cargo") == 1 then + mission.client_ship:AddEquip(e.cargo, 1, "cargo") + mission.debris[i] = nil + mission.amount = mission.amount - 1 + end + end + end + + if mission.amount == 0 then + mission.status = "COMPLETED" + elseif mission.destination == nil then + mission.status = "FAILED" + end + end + + if mission.status == "COMPLETED" or mission.status == "FAILED" then + local ship = mission.client_ship + mission.client_ship = nil + removeMission(mission, ref) + ship:HyperjumpTo(nearbySystem()) + end + end) +end + +local isQualifiedFor = function(reputation, ad) + return reputation > (ad.reward/100) or false +end + +local onDelete = function (ref) + ads[ref] = nil +end + +local isEnabled = function (ref) + return ads[ref] ~= nil and isQualifiedFor(Character.persistent.player.reputation, ads[ref]) +end + +local onChat = function (form, ref, option) + local ad = ads[ref] + local player = Game.player + local debris, ship, radius, mindist, maxdist + + form:Clear() + + if option == -1 then + form:Close() + return + end + + local qualified = isQualifiedFor(Character.persistent.player.reputation, ad) + + form:SetFace(ad.client) + + if not qualified then + form:SetMessage(l["DENY_" .. Engine.rand:Integer(1, getNumberOfFlavours("DENY"))]) + return + end + + form:AddNavButton(ad.planet) + + if option == 0 then + local introtext = string.interp(ad.introtext, { + client = ad.client.name, + shipid = ad.ship_label, + star = ad.star:GetSystemBody().name, + planet = ad.planet:GetSystemBody().name, + cash = Format.Money(math.abs(ad.reward), false), + }) + form:SetMessage(introtext) + + elseif option == 1 then + form:SetMessage(l["WHY_NOT_YOURSELF_" .. ad.id]) + + elseif option == 2 then + 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 + form:SetMessage(l.YOU_DO_NOT_HAVE_A_SCOOP) + form:RemoveNavButton() + return + end + + if ad.reward < 0 and player:GetMoney() < math.abs(ad.reward) then + form:SetMessage(l.YOU_DO_NOT_HAVE_ENOUGH_MONEY) + form:RemoveNavButton() + return + end + + form:RemoveAdvertOnClose() + ads[ref] = nil + + radius = ad.planet:GetSystemBody().radius + if ad.planet:GetSystemBody().superType == "ROCKY_PLANET" then + mindist = radius * 2.5 + maxdist = radius * 3.5 + else + mindist = radius * 25 + maxdist = radius * 35 + end + debris = spawnDebris(ad.debris_type, ad.amount, ad.planet, mindist, maxdist, ad.due - Game.time) + + if ad.reward < 0 then player:AddMoney(ad.reward) end + if ad.deliver_to_ship then + ship = spawnClientShip(ad.star, ad.ship_label) + end + + local mission = { + type = "Scoop", + location = ad.location, + introtext = ad.introtext, + client = ad.client, + station = ad.station.path, + star = ad.star, + planet = ad.planet, + id = ad.id, + debris = debris, + amount = #debris, + reward = ad.reward, + due = ad.due, + return_to_station = ad.return_to_station, + deliver_to_ship = ad.deliver_to_ship, + client_ship = ship, + ship_label = ad.ship_label, + destination = debris[1].body + } + + table.insert(missions, Mission.New(mission)) + form:SetMessage(l["ACCEPTED_" .. ad.id]) + form:RemoveNavButton() + form:AddNavButton(debris[1].body) + return + end + + form:AddOption(l.WHY_NOT_YOURSELF, 1) + form:AddOption(l.HOW_MUCH_TIME, 2) + form:AddOption(l.REPEAT_THE_REQUEST, 0) + form:AddOption(l.OK_AGREED, 3) +end + +local getPlanets = function (system) + local planets = {} + + for _, p in ipairs(system:GetBodyPaths()) do + if p:GetSystemBody().superType == "ROCKY_PLANET" or p:GetSystemBody().superType == "GAS_GIANT" then + table.insert(planets, p) + end + end + return planets +end + +local planets = nil + +local makeAdvert = function (station) + if planets == nil then planets = getPlanets(Game.system) end + if #planets == 0 then return end + + if flavours[LEGAL].cargo_type == nil then + flavours[LEGAL].cargo_type, flavours[ILLEGAL].cargo_type = sortGoods(Equipment.cargo) + end + + local stars = Game.system:GetStars() + local star = stars[Engine.rand:Integer(1, #stars)] + local planet = planets[Engine.rand:Integer(1, #planets)] + local dist = station:DistanceTo(Space.GetBody(planet:GetSystemBody().index)) + local flavour = flavours[Engine.rand:Integer(1, #flavours)] + local due = Game.time + mission_time * (1 + dist / max_dist) * Engine.rand:Number(0.8, 1.2) + local reward + + if flavour.reward < 0 then + reward = flavour.reward * (1.15 - dist / max_dist) * Engine.rand:Number(0.9, 1.1) + else + reward = flavour.reward * (1 + dist / max_dist) * Engine.rand:Number(0.75, 1.25) + end + reward = utils.round(reward, 50) + + if #flavour.cargo_type > 0 and dist < max_dist and station:DistanceTo(Space.GetBody(star.index)) < max_dist then + local ad = { + station = station, + location = planet, + introtext = l["INTROTEXT_" .. flavour.id], + client = Character.New(), + star = star.path, + planet = planet, + id = flavour.id, + debris_type = flavour.cargo_type, + reward = math.ceil(reward), + amount = flavour.amount, + due = due, + return_to_station = flavour.return_to_station, + deliver_to_ship = flavour.deliver_to_ship, + ship_label = flavour.deliver_to_ship and Ship.MakeRandomLabel() or nil + } + + ad.desc = string.interp(l["ADTEXT_" .. flavour.id], { cash = Format.Money(ad.reward, false) }) + + local ref = station:AddAdvert({ + description = ad.desc, + icon = flavour.id == "RESCUE" and "searchrescue" or "haul", + onChat = onChat, + onDelete = onDelete, + isEnabled = isEnabled + }) + ads[ref] = ad + end +end + +local onCreateBB = function (station) + local num = Engine.rand:Integer(0, math.ceil(Game.system.population * Game.system.lawlessness)) + for i = 1, num do + makeAdvert(station) + end +end + +local onUpdateBB = function (station) + for ref, ad in pairs(ads) do + if ad.due < Game.time + 5*24*60*60 then -- five day timeout + ad.station:RemoveAdvert(ref) + end + end + if Engine.rand:Integer(4*24*60*60) < 60*60 then -- roughly once every four days + makeAdvert(station) + end +end + +local onShipEquipmentChanged = function (ship, equipment) + if not ship:IsPlayer() or equipment == nil or equipment:GetDefaultSlot() ~= "cargo" then return end + + for ref, mission in pairs(missions) do + if not mission.police and not Game.system:IsCommodityLegal(equipment.name) and not ship:IsDocked() and mission.location:IsSameSystem(Game.system.path) then + if (1 - Game.system.lawlessness) > Engine.rand:Number(4) then + local station = ship:FindNearestTo("SPACESTATION") + if station then mission.police = spawnPolice(station) end + end + end + end +end + +-- The attacker could be a ship or the planet +-- If scooped or destroyed by self-destruction, attacker is nil +local onCargoDestroyed = function (body, attacker) + for ref, mission in pairs(missions) do + for i, e in pairs(mission.debris) do + if body == e.body then + e.body = nil + if body == mission.destination then + -- remove NavButton + mission.destination = nil + end + if attacker and (mission.return_to_station or mission.deliver_to_ship) then + mission.status = "FAILED" + end + if mission.destination == nil then + for i, e in pairs(mission.debris) do + if e.body ~= nil then + -- set next target + mission.destination = e.body + break + end + end + end + break + end + end + end +end + +local onJettison = function (ship, cargo) + if not ship:IsPlayer() then return end + + for ref, mission in pairs(missions) do + if mission.reward > 0 and not mission.warning then + for i, e in pairs(mission.debris) do + if cargo == e.cargo then + mission.warning = true + Comms.ImportantMessage(l.WARNING, mission.client.name) + break + end + end + end + end +end + +local onShipHit = function (ship, attacker) + if ship:IsPlayer() then return end + if attacker == nil or not attacker:isa('Ship') then return end + + for ref, mission in pairs(missions) do + if mission.police then + for _, s in pairs(mission.police) do + if s == ship then + ship:AIKill(attacker) + break + end + end + elseif mission.client_ship == ship then + ship:AIKill(attacker) + break + end + end +end + +local onShipDestroyed = function (ship, attacker) + if ship:IsPlayer() then return end + + for ref, mission in pairs(missions) do + if mission.police then + for i, s in pairs(mission.police) do + if s == ship then + table.remove(mission.police, i) + break + end + end + elseif mission.client_ship == ship then + mission.client_ship = nil + local msg = string.interp(l.SHIP_DESTROYED, { + shipid = mission.ship_label, + station = mission.station:GetSystemBody().name + }) + Comms.ImportantMessage(msg, mission.client.name) + break + end + end +end + +local onShipDocked = function (player, station) + if not player:IsPlayer() then return end + + for ref, mission in pairs(missions) do + if mission.police then + for _, s in pairs(mission.police) do + if station.type == "STARPORT_SURFACE" then + s:AIEnterLowOrbit(Space.GetBody(station:GetSystemBody().parent.index)) + else + s:AIFlyTo(station) + end + end + end + + if mission.return_to_station or mission.deliver_to_ship and not mission.client_ship and mission.station == station.path then + + -- unload mission cargo + for i, e in pairs(mission.debris) do + if e.body == nil then + if player:RemoveEquip(e.cargo, 1, "cargo") == 1 then + mission.debris[i] = nil + mission.amount = mission.amount - 1 + end + end + end + if mission.amount == 0 then + mission.status = "COMPLETED" + elseif mission.destination == nil then + mission.status = "FAILED" + end + + if mission.status == "COMPLETED" or mission.status == "FAILED" then + removeMission(mission, ref) + end + + -- remove stale missions, if any + -- all cargo related to flavour 1 and 2 scooped or destroyed + elseif mission.reward < 0 and mission.destination == nil then + mission:Remove() + missions[ref] = nil + end + end +end + +local onShipUndocked = function (player, station) + if not player:IsPlayer() then return end + + for ref, mission in pairs(missions) do + if mission.police then + for _, s in pairs(mission.police) do + s:AIKill(player) + end + end + + if mission.deliver_to_ship and not mission.in_progess then + mission.in_progress = true + transferCargo(mission, ref) + end + end +end + +local getPopulatedPlanets = function (system) + local planets = {} + + for _, p in ipairs(system:GetBodyPaths()) do + if p:GetSystemBody().population > 0 then + table.insert(planets, p) + end + end + return planets +end + +local onEnterSystem = function (ship) + if not ship:IsPlayer() or Game.system.population == 0 then return end + + local planets = getPopulatedPlanets(Game.system) + local num = Engine.rand:Integer(0, math.ceil(Game.system.population * Game.system.lawlessness)) + + flavours[LEGAL].cargo_type, flavours[ILLEGAL].cargo_type = sortGoods(Equipment.cargo) + + -- spawn random cargo (legal or illegal goods) + for i = 1, num do + local planet = planets[Engine.rand:Integer(1, #planets)] + local radius = planet:GetSystemBody().radius + local flavour = flavours[Engine.rand:Integer(LEGAL, ILLEGAL)] + local debris = flavour.cargo_type + if #debris > 0 then + spawnDebris(debris, flavour.amount, planet, radius * 1.2, radius * 3.5, mission_time) + end + end +end + +local onLeaveSystem = function (ship) + if ship:IsPlayer() then + for ref, mission in pairs(missions) do + mission.destination = nil + mission.police = nil + if mission.client_ship then + mission.client_ship = nil + mission.status = "FAILED" + end + for i, e in pairs(mission.debris) do + e.body = nil + end + end + planets = nil + flavours[LEGAL].cargo_type = nil + flavours[ILLEGAL].cargo_type = nil + end +end + +local onReputationChanged = function (oldRep, oldKills, newRep, newKills) + for ref, ad in pairs(ads) do + local oldQualified = isQualifiedFor(oldRep, ad) + if isQualifiedFor(newRep, ad) ~= oldQualified then + Event.Queue("onAdvertChanged", ad.station, ref); + end + end +end + +local buildMissionDescription = function(mission) + local ui = require 'pigui' + local desc = {} + + desc.description = mission.introtext:interp({ + client = mission.client.name, + shipid = mission.ship_label, + star = mission.star:GetSystemBody().name, + planet = mission.planet:GetSystemBody().name, + cash = Format.Money(math.abs(mission.reward), false) + }) + + desc.details = { + { l.CLIENT, mission.client.name }, + { l.SPACEPORT, mission.station:GetSystemBody().name }, + mission.client_ship and { l.SHIP, mission.client_ship.label } or false, + false, + { l.DEADLINE, ui.Format.Date(mission.due) } + } + + desc.client = mission.client + desc.location = mission.destination or nil + if mission.deliver_to_ship then + desc.returnLocation = mission.client_ship or mission.station + end + + return desc +end + +local loaded_data + +local onGameStart = function () + ads = {} + missions = {} + + if loaded_data and loaded_data.ads then + + for k, ad in pairs(loaded_data.ads) do + local ref = ad.station:AddAdvert({ + description = ad.desc, + icon = ad.id == "RESCUE" and "searchrescue" or "haul", + onChat = onChat, + onDelete = onDelete, + isEnabled = isEnabled + }) + ads[ref] = ad + end + + missions = loaded_data.missions + + loaded_data = nil + + for ref, mission in pairs(missions) do + if mission.deliver_to_ship then + mission.in_progress = true + transferCargo(mission, ref) + end + end + end +end + +local onGameEnd = function () + planets = nil + flavours[LEGAL].cargo_type = nil + flavours[ILLEGAL].cargo_type = nil +end + +local serialize = function () + return { ads = ads, missions = missions } +end + +local unserialize = function (data) + loaded_data = data +end + +Event.Register("onCreateBB", onCreateBB) +Event.Register("onUpdateBB", onUpdateBB) +Event.Register("onShipEquipmentChanged", onShipEquipmentChanged) +Event.Register("onShipDocked", onShipDocked) +Event.Register("onShipUndocked", onShipUndocked) +Event.Register("onShipHit", onShipHit) +Event.Register("onShipDestroyed", onShipDestroyed) +Event.Register("onJettison", onJettison) +Event.Register("onCargoDestroyed", onCargoDestroyed) +Event.Register("onEnterSystem", onEnterSystem) +Event.Register("onLeaveSystem", onLeaveSystem) +Event.Register("onGameStart", onGameStart) +Event.Register("onGameEnd", onGameEnd) +Event.Register("onReputationChanged", onReputationChanged) + +Mission.RegisterType("Scoop", l.SCOOP, buildMissionDescription) + +Serializer:Register("Scoop", serialize, unserialize) diff --git a/src/CargoBody.cpp b/src/CargoBody.cpp index 52cd38f11..22a2033f6 100644 --- a/src/CargoBody.cpp +++ b/src/CargoBody.cpp @@ -102,7 +102,7 @@ void CargoBody::TimeStepUpdate(const float timeStep) if (m_hasSelfdestruct) { m_selfdestructTimer -= timeStep; if (m_selfdestructTimer <= 0) { - LuaEvent::Queue("onCargoDestroyed", this, NULL); + LuaEvent::Queue("onCargoDestroyed", this); Pi::game->GetSpace()->KillBody(this); SfxManager::Add(this, TYPE_EXPLOSION); } @@ -114,10 +114,10 @@ bool CargoBody::OnDamage(Body *attacker, float kgDamage, const CollisionContact { m_hitpoints -= kgDamage * 0.001f; if (m_hitpoints < 0) { - if (attacker && attacker->IsType(Object::BODY)) + if (attacker && attacker->IsType(ObjectType::BODY)) LuaEvent::Queue("onCargoDestroyed", this, dynamic_cast(attacker)); else - LuaEvent::Queue("onCargoDestroyed", this, NULL); + LuaEvent::Queue("onCargoDestroyed", this); Pi::game->GetSpace()->KillBody(this); SfxManager::Add(this, TYPE_EXPLOSION); } @@ -131,7 +131,7 @@ bool CargoBody::OnCollision(Body *b, Uint32 flags, double relVel) int cargoscoop_cap = 0; static_cast(b)->Properties().Get("cargo_scoop_cap", cargoscoop_cap); if (cargoscoop_cap > 0) { - LuaEvent::Queue("onCargoDestroyed", this, NULL); + LuaEvent::Queue("onCargoDestroyed", this); return true; } } diff --git a/src/CargoBody.h b/src/CargoBody.h index a08e1a88b..3b049ef15 100644 --- a/src/CargoBody.h +++ b/src/CargoBody.h @@ -16,7 +16,7 @@ class CargoBody : public DynamicBody { public: OBJDEF(CargoBody, DynamicBody, CARGOBODY); CargoBody() = delete; - CargoBody(const LuaRef &cargo, float selfdestructTimer = 86400.0f); // default to 24 h lifetime + CargoBody(const LuaRef &cargo, float selfdestructTimer = 86400.0f); // default to 24 h lifetime CargoBody(const char *modelName, const LuaRef &cargo, float selfdestructTimer = 86400.0f); // default to 24 h lifetime CargoBody(const Json &jsonObj, Space *space); LuaRef GetCargoType() const { return m_cargo; } diff --git a/src/lua/LuaBody.cpp b/src/lua/LuaBody.cpp index 8b6ddb0e3..34095d706 100644 --- a/src/lua/LuaBody.cpp +++ b/src/lua/LuaBody.cpp @@ -728,6 +728,20 @@ static bool _body_from_json(const Json &obj) return push_body_to_lua(body); } +static int l_body_get_velocity(lua_State *l) +{ + Body *b = LuaObject::CheckFromLua(1); + LuaPush(l, b->GetVelocity()); + return 1; +} + +static int l_body_set_velocity(lua_State *l) +{ + Body *b = LuaObject::CheckFromLua(1); + b->SetVelocity(LuaPull(l, 2)); + return 0; +} + template <> const char *LuaObject::s_type = "Body"; @@ -758,6 +772,8 @@ void LuaObject::RegisterClass() { "IsGroundStation", l_body_is_ground_station }, { "IsCargoContainer", l_body_is_cargo_container }, { "GetSystemBody", l_body_get_system_body }, + { "GetVelocity", l_body_get_velocity }, + { "SetVelocity", l_body_set_velocity }, { 0, 0 } }; diff --git a/src/lua/LuaPiGui.cpp b/src/lua/LuaPiGui.cpp index 68c7ef186..dbbf8148f 100644 --- a/src/lua/LuaPiGui.cpp +++ b/src/lua/LuaPiGui.cpp @@ -1891,7 +1891,7 @@ static int l_pigui_get_projected_bodies_grouped(lua_State *l) for (Body *body : Pi::game->GetSpace()->GetBodies()) { if (body == Pi::game->GetPlayer()) continue; if (body->GetType() == ObjectType::PROJECTILE) continue; - if (body->GetType() == ObjectType::SHIP && + if ((body->GetType() == ObjectType::SHIP || body->GetType() == ObjectType::CARGOBODY) && body->GetPositionRelTo(Pi::player).Length() > ship_max_distance) continue; const PiGui::TScreenSpace res = lua_world_space_to_screen_space(body); // defined in LuaPiGui.cpp if (!res._onScreen) continue; diff --git a/src/lua/LuaSpace.cpp b/src/lua/LuaSpace.cpp index d18a2aa49..622a136c3 100644 --- a/src/lua/LuaSpace.cpp +++ b/src/lua/LuaSpace.cpp @@ -2,6 +2,7 @@ // Licensed under the terms of the GPL v3. See licenses/GPL-3.txt #include "LuaSpace.h" +#include "CargoBody.h" #include "Frame.h" #include "Game.h" #include "HyperspaceCloud.h" @@ -16,7 +17,6 @@ #include "Ship.h" #include "Space.h" #include "SpaceStation.h" -#include "CargoBody.h" /* * Interface: Space @@ -361,7 +361,7 @@ static int l_space_spawn_ship_parked(lua_State *l) Ship *ship = new Ship(type); assert(ship); - const double parkDist = station->GetStationType()->ParkingDistance() - ship->GetPhysRadius(); // park inside parking radius + const double parkDist = station->GetStationType()->ParkingDistance() - ship->GetPhysRadius(); // park inside parking radius const double parkOffset = (0.5 * station->GetStationType()->ParkingGapSize()) + ship->GetPhysRadius(); // but outside the docking gap double xpos = (slot == 0 || slot == 3) ? -parkOffset : parkOffset; @@ -551,6 +551,27 @@ static int l_space_spawn_ship_landed_near(lua_State *l) return 1; } +// sb - central systembody, pos - absolute coordinates of given object +static vector3d _orbital_velocity_random_direction(const SystemBody *sb, const vector3d &pos) +{ + // If we got a zero mass of central body - there is no orbit + if (sb->GetMass() < 0.01) + return vector3d(0.0); + // calculating basis from radius - vector + vector3d k = pos.Normalized(); + vector3d i; + if (std::fabs(k.z) > 0.999999) // very vertical = z + i = vector3d(1.0, 0.0, 0.0); // second ort = x + else + i = k.Cross(vector3d(0.0, 0.0, 1.0)).Normalized(); + vector3d j = k.Cross(i); + // generating random 2d direction and putting it into basis + vector3d randomOrthoDirection = MathUtil::RandomPointOnCircle(1.0) * matrix3x3d::FromVectors(i, j, k).Transpose(); + // calculate the value of the orbital velocity + double orbitalVelocity = sqrt(G * sb->GetMass() / pos.Length()); + return randomOrthoDirection * orbitalVelocity; +} + /* * Function: SpawnCargoNear * @@ -585,8 +606,8 @@ static int l_space_spawn_cargo_near(lua_State *l) LUA_DEBUG_START(l); - CargoBody * c_body; - const char * model; + CargoBody *c_body; + const char *model; lua_getfield(l, 1, "model_name"); if (lua_isstring(l, -1)) @@ -594,21 +615,29 @@ static int l_space_spawn_cargo_near(lua_State *l) else model = "cargo"; - if (lua_gettop(l) >= 5){ + if (lua_gettop(l) >= 5) { float lifetime = lua_tonumber(l, 5); c_body = new CargoBody(model, LuaRef(l, 1), lifetime); } else { c_body = new CargoBody(model, LuaRef(l, 1)); } - Body * nearbody = LuaObject::CheckFromLua(2); + Body *nearbody = LuaObject::CheckFromLua(2); float min_dist = luaL_checknumber(l, 3); float max_dist = luaL_checknumber(l, 4); if (min_dist > max_dist) luaL_error(l, "min_dist must not be larger than max_dist"); - c_body->SetFrame(nearbody->GetFrame()); - c_body->SetPosition((MathUtil::RandomPointOnSphere(min_dist, max_dist)) + nearbody->GetPosition()); - c_body->SetVelocity(vector3d(0,0,0)); + FrameId frameId = nearbody->GetFrame(); + Frame *frame = Frame::GetFrame(frameId); + // if the frame is rotating, use non-rotating parent + if (frame->IsRotFrame()) { + assert(frame->GetParent()); + frame = Frame::GetFrame(frame->GetParent()); + frameId = frame->GetId(); + } + c_body->SetFrame(frameId); + c_body->SetPosition(MathUtil::RandomPointOnSphere(min_dist, max_dist) + nearbody->GetPosition()); + c_body->SetVelocity(_orbital_velocity_random_direction(frame->GetSystemBody(), c_body->GetPosition())); Pi::game->GetSpace()->AddBody(c_body); LuaObject::PushToLua(c_body); @@ -618,6 +647,72 @@ static int l_space_spawn_cargo_near(lua_State *l) return 1; } +/* + * Function: SpawnShipOrbit + * + * Create a ship and place it in orbit near the given . + * + * > ship = Space.SpawnShip(type, body, min, max) + * + * Parameters: + * + * type - the name of the ship + * + * body - the near which the ship should be spawned + * + * min - minimum distance from the body to place the ship, in m + * + * max - maximum distance to place the ship + * + * + * Return: + * + * ship - a object for the new ship + * + * Status: + * + * experimental + */ +static int l_space_spawn_ship_orbit(lua_State *l) +{ + if (!Pi::game) + luaL_error(l, "Game is not started"); + + LUA_DEBUG_START(l); + + const char *type = luaL_checkstring(l, 1); + if (!ShipType::Get(type)) + luaL_error(l, "Unknown ship type '%s'", type); + + Body *nearbody = LuaObject::CheckFromLua(2); + float min_dist = luaL_checknumber(l, 3); + float max_dist = luaL_checknumber(l, 4); + if (min_dist > max_dist) + luaL_error(l, "min_dist must not be larger than max_dist"); + + Ship *ship = new Ship(type); + assert(ship); + + FrameId frameId = nearbody->GetFrame(); + Frame *frame = Frame::GetFrame(frameId); + // if the frame is rotating, use non-rotating parent + if (frame->IsRotFrame()) { + assert(frame->GetParent()); + frame = Frame::GetFrame(frame->GetParent()); + frameId = frame->GetId(); + } + ship->SetFrame(frameId); + ship->SetPosition(MathUtil::RandomPointOnSphere(min_dist, max_dist) + nearbody->GetPosition()); + ship->SetVelocity(_orbital_velocity_random_direction(frame->GetSystemBody(), ship->GetPosition())); + Pi::game->GetSpace()->AddBody(ship); + + LuaObject::PushToLua(ship); + + LUA_DEBUG_END(l, 1); + + return 1; +} + /* * Function: GetBody * @@ -789,11 +884,11 @@ void LuaSpace::Register() { "SpawnShipLanded", l_space_spawn_ship_landed }, { "SpawnShipLandedNear", l_space_spawn_ship_landed_near }, { "SpawnCargoNear", l_space_spawn_cargo_near }, + { "SpawnShipOrbit", l_space_spawn_ship_orbit }, { "GetBody", l_space_get_body }, { "GetBodies", l_space_get_bodies }, - { "DbgDumpFrames", l_space_dump_frames }, { 0, 0 } };