local Launchpad = {} -- launchpad, cargo rockeet, and rocket landing / crashing sequences --custon event Launchpad.on_cargo_rocket_launched_event = script.generate_event_name() -- constants Launchpad.name_rocket_launch_pad = mod_prefix.."rocket-launch-pad" Launchpad.name_rocket_launch_pad_section_input = mod_prefix.."rocket-launch-pad-_-section-input" Launchpad.name_rocket_launch_pad_tank = mod_prefix.."rocket-launch-pad-tank" Launchpad.name_rocket_launch_pad_combinator = mod_prefix.."rocket-launch-pad-combinator" Launchpad.name_rocket_launch_pad_silo = mod_prefix.."rocket-launch-pad-silo" Launchpad.name_rocket_launch_pad_silo_dummy = mod_prefix.."rocket-launch-pad-silo-dummy-ingredient-item" Launchpad.name_rocket_launch_pad_seat = mod_prefix.."rocket-launch-pad-_-seat" Launchpad.name_rocket_section = mod_prefix.."cargo-rocket-section" Launchpad.name_cargo_rocket = mod_prefix.."cargo-rocket" Launchpad.name_cargo_pod = mod_prefix.."cargo-rocket-cargo-pod" Launchpad.name_tech_rocket_cargo_safety = mod_prefix.."rocket-cargo-safety" Launchpad.name_tech_rocket_reusability = mod_prefix.."rocket-reusability" Launchpad.name_tech_rocket_survivability = mod_prefix.."rocket-survivability" Launchpad.rocket_capacity = 500 Launchpad.fuel_per_delta_v = 8 -- maybe this should be an energy value? Launchpad.rocket_sections_per_rocket = 100 Launchpad.crew_capsules_per_rocket = 1 Launchpad.signal_for_launch = {type = "virtual", name = "signal-green"} Launchpad.signal_for_rocket_complete = {type = "virtual", name = mod_prefix.."cargo-rocket"} Launchpad.trigger_options = { { name = "none", display = {"space-exploration.trigger-none"}}, { name = "fuel-full", display = {"space-exploration.trigger-fuel-full"}}, { name = "cargo-full", display = {"space-exploration.trigger-cargo-full"}}, { name = "fuel-full-signal", display = {"space-exploration.trigger-fuel-full-signal"}}, { name = "cargo-full-signal", display = {"space-exploration.trigger-cargo-full-signal"}}, { name = "cargo-full-or-signal", display = {"space-exploration.trigger-cargo-full-or-signal"}}, } Launchpad.time_takeoff = 440 -- the delay before the rocket gets going Launchpad.time_takeoff_finish_ascent = 1100 --1164 -- just before the rocket launch event would fire Launchpad.time_landing_capsule_touchdown = 6 * 60 -- when the capsule lands Launchpad.time_landing_cargopod_first = 3 * 60 -- when the first cargopod lands Launchpad.time_landing_cargopod_last = 5 * 60 -- when the last cargopod lands Launchpad.time_landing_debris_first = 2 * 60 -- when the first debris lands Launchpad.time_landing_debris_last = 7 * 60 -- when the last debris lands Launchpad.landing_start_altitude = 128 Launchpad.safety_delta_v_base = 1000 Launchpad.safety_delta_v_factor = 100000 Launchpad.names_cargo_fragment = { -- these are the base names, actual names are "se-falling-" "cargo-fragment-a", "cargo-fragment-b", "cargo-fragment-c", "cargo-fragment-d", } Launchpad.rocket_fragments_large = 4 Launchpad.names_rocket_fragments_large = { "rocket-fragment-big-d", "rocket-fragment-big-a", "rocket-fragment-big-b", "rocket-fragment-big-c" } Launchpad.rocket_fragments_medium = 6 Launchpad.names_rocket_fragments_medium = { "rocket-fragment-medium-a", "rocket-fragment-medium-b", "rocket-fragment-medium-c" } Launchpad.rocket_fragments_small = 20 Launchpad.names_rocket_fragments_small = { "rocket-fragment-small-a", "rocket-fragment-small-b", "rocket-fragment-small-c", "rocket-fragment-small-d", "rocket-fragment-small-e", "rocket-fragment-small-f", "rocket-fragment-small-g", "rocket-fragment-small-h", "rocket-fragment-small-i", "rocket-fragment-small-j" } Launchpad.rocket_fragments_tiny = 20 Launchpad.names_rocket_fragments_tiny = { "rocket-fragment-tiny-a", "rocket-fragment-tiny-b", "rocket-fragment-tiny-c", "rocket-fragment-tiny-d", "rocket-fragment-tiny-e", "rocket-fragment-tiny-f" } function Launchpad.get_force_cargo_loss_modifier(force) local researched_levels = 0 local i = 1 local current_level = force.technologies[Launchpad.name_tech_rocket_cargo_safety.."-"..i] while current_level and (current_level.researched or current_level.level > i) do local more_levels = (current_level.level or i) - i i = i + 1 + more_levels researched_levels = researched_levels + (1 + more_levels) current_level = force.technologies[Launchpad.name_tech_rocket_cargo_safety.."-"..i] end return 0.9 ^ researched_levels end function Launchpad.get_cargo_loss(force, delta_v) -- percentage of cargo lost delta_v = (delta_v or Launchpad.safety_delta_v_factor) + Launchpad.safety_delta_v_base -- 50% local loss = delta_v/(delta_v+Launchpad.safety_delta_v_factor) --local loss = 0.5 * global.forces[force.name].cargo_rockets_launched / (global.forces[force.name].cargo_rockets_launched + 500) return loss *Launchpad.get_force_cargo_loss_modifier(force) end function Launchpad.get_reusability(force) -- percentage of parts not recoverable (all at start) local recovery = 0.2 local i = 1 local current_level = force.technologies[Launchpad.name_tech_rocket_reusability.."-"..i] while current_level and (current_level.researched or current_level.level > i) do local more_levels = (current_level.level or i) - i i = i + 1 + more_levels recovery = recovery + 0.04 * (1 + more_levels) current_level = force.technologies[Launchpad.name_tech_rocket_reusability.."-"..i] end return recovery end function Launchpad.get_force_survivability_loss_modifier(force) -- chance to fail to land on rocket launch pad local multiplier = 1 local i = 1 while force.technologies[Launchpad.name_tech_rocket_survivability.."-"..i] and force.technologies[Launchpad.name_tech_rocket_survivability.."-"..i].researched do i = i + 1 multiplier = multiplier * 0.9 end if force.technologies[Launchpad.name_tech_rocket_survivability.."-"..i] then local tech = force.technologies[Launchpad.name_tech_rocket_survivability.."-"..i] if tech.prototype.max_level > 10 then -- infinite for j = i, tech.level do multiplier = multiplier * 0.9 end end end return multiplier end function Launchpad.get_survivability_loss(force, delta_v) -- chance to fail to land on rocket launch pad delta_v = (delta_v or Launchpad.safety_delta_v_factor) + Launchpad.safety_delta_v_base -- 50% local loss = delta_v/(delta_v+Launchpad.safety_delta_v_factor) --local loss = 0.5 * global.forces[force.name].cargo_rockets_launched / (global.forces[force.name].cargo_rockets_launched + 500) return loss * Launchpad.get_force_survivability_loss_modifier(force) end ---@class Launchpad All data necessary to maintain the state of a launchpad --- Gets the Launchpad for this unit_number ---@param unit_number number function Launchpad.from_unit_number (unit_number) if not unit_number then Log.trace("Launchpad.from_unit_number: invalid unit_number: nil") return end unit_number = tonumber(unit_number) -- NOTE: only supports container as the entity if global.rocket_launch_pads[unit_number] then return global.rocket_launch_pads[unit_number] else Log.trace("Launchpad.from_unit_number: invalid unit_number: " .. unit_number) end end --- Gets the Launchpad for this entity ---@param entity any function Launchpad.from_entity (entity) if not(entity and entity.valid) then Log.trace("Launchpad.from_entity: invalid entity") return end -- NOTE: only supports container as the entity return Launchpad.from_unit_number(entity.unit_number) end --- Computes the cost to launch a rocket ---@param origin Zone launch zone ---@param destination Zone destination zone function Launchpad.get_fuel_cost(origin, destination) if origin and destination then return Launchpad.fuel_per_delta_v * (Zone.get_launch_delta_v(origin) + Zone.get_travel_delta_v(origin, destination)) end end --[[ Input the destination as either: { type = "zone", zone_index = name } { type = "landing-pad", zone_index = name, landing_pad_name = name } Output the destination as: { type = "zone", zone_index = name, zone = Zone, position = {x=#,y=#} } { type = "landing-pad", landing_pad_name = name, landing_pad = rocket_landing_pad} ]]-- function Launchpad.lock_destination (struct, allow_non_empty) -- force launch can go to non-empty landings if struct.destination then if struct.destination.zone then -- can't have people changing destination mid-launch local force_name = struct.force_name -- destination to locked destination local locked_destination = { -- don't use deepcopy be zone = struct.destination.zone, landing_pad_name = struct.destination.landing_pad_name, } local zone = locked_destination.zone if not zone then return false end if locked_destination.landing_pad_name then local landing_pads = Landingpad.get_zone_landing_pads_availability(force_name, zone, locked_destination.landing_pad_name) if #landing_pads.empty_landing_pads > 0 then local landing_pad = landing_pads.empty_landing_pads[math.random(#landing_pads.empty_landing_pads)] locked_destination.landing_pad = landing_pad Zone.get_make_surface(zone) return locked_destination elseif allow_non_empty and #landing_pads.filled_landing_pads > 0 then local landing_pad = landing_pads.filled_landing_pads[math.random(#landing_pads.filled_landing_pads)] locked_destination.landing_pad = landing_pad Zone.get_make_surface(zone) return locked_destination end else -- not landing pad name Zone.get_make_surface(zone) locked_destination.position = Zone.find_zone_landing_position(zone) --this is the first cargo rocket to space, go to the space platform satellite if (zone.type == "orbit" and zone.parent.name == "Nauvis") then if (global.forces[force_name].cargo_rockets_launched or 0) == 0 and global.forces[force_name].nauvis_satellite then local satellite_blueprint = Ruin.ruins["satellite"] locked_destination.position = { x = global.forces[force_name].nauvis_satellite.x + (satellite_blueprint.landing_offset.x or satellite_blueprint.landing_offset[1]) - (satellite_blueprint.center.x or satellite_blueprint.center[1]), y = global.forces[force_name].nauvis_satellite.y + (satellite_blueprint.landing_offset.y or satellite_blueprint.landing_offset[2]) - (satellite_blueprint.center.y or satellite_blueprint.center[2]) } end end return locked_destination end elseif struct.destination.landing_pad_name then -- can't have people changing destination mid-launch local force_name = struct.force_name local locked_destination = { -- don't use deepcopy be landing_pad_name = struct.destination.landing_pad_name, } local landing_pads = Landingpad.get_force_landing_pads_availability(force_name, locked_destination.landing_pad_name) if #landing_pads.empty_landing_pads > 0 then local landing_pad = landing_pads.empty_landing_pads[math.random(#landing_pads.empty_landing_pads)] locked_destination.zone = landing_pad.zone locked_destination.landing_pad = landing_pad return locked_destination elseif allow_non_empty and #landing_pads.filled_landing_pads > 0 then local landing_pad = landing_pads.filled_landing_pads[math.random(#landing_pads.filled_landing_pads)] locked_destination.zone = landing_pad.zone locked_destination.landing_pad = landing_pad return locked_destination end end end return false end function Launchpad.add_section_input(struct) -- spawn section input struct.section_input = struct.container.surface.create_entity{ name = Launchpad.name_rocket_launch_pad_section_input, force = struct.container.force, position = {struct.container.position.x, struct.container.position.y}} local inv = struct.section_input.get_inventory(defines.inventory.car_trunk) inv.set_filter(1, Launchpad.name_rocket_section) inv.set_filter(2, Capsule.name_space_capsule) end function Launchpad.update_combinator(struct, main_inv) if struct.combinator then local comb = struct.combinator.get_or_create_control_behavior() -- comb.set_signal(1, {signal={type="fluid", name=name_fluid_rocket_fuel}, count=struct.total_fuel}) comb.set_signal(1, {signal={type="fluid", name=name_fluid_rocket_fuel}, count=struct.lua_fuel}) comb.set_signal(2, {signal={type="item", name=Launchpad.name_rocket_section}, count=struct.rocket_sections}) comb.set_signal(3, {signal={type="item", name=Capsule.name_space_capsule}, count=struct.crew_capsules}) if struct.rocket_sections >= Launchpad.rocket_sections_per_rocket and struct.crew_capsules >= Launchpad.crew_capsules_per_rocket then comb.set_signal(4, {signal=Launchpad.signal_for_rocket_complete, count=1}) else comb.set_signal(4, {signal=Launchpad.signal_for_rocket_complete, count=0}) end local slots_free = main_inv.count_empty_stacks() comb.set_signal(5, {signal={type = "virtual", name = "signal-E"}, count= slots_free}) comb.set_signal(6, {signal={type = "virtual", name = "signal-F"}, count= #main_inv - slots_free}) comb.set_signal(7, {signal={type = "virtual", name = "signal-L"}, count= struct.required_fuel or 0}) end end function Launchpad.prep(struct) --local profiler = game.create_profiler() if not (struct.container and struct.container.valid) then return Launchpad.destroy(struct) end struct.required_fuel = nil -- invalid if struct.destination then local origin_zone = struct.zone if struct.destination.landing_pad_name and not struct.destination.zone then if global.forces[struct.force_name] and global.forces[struct.force_name].rocket_landing_pad_names and global.forces[struct.force_name].rocket_landing_pad_names[struct.destination.landing_pad_name] then local required_fuel_max = 0 local landing_pads_list = global.forces[struct.force_name].rocket_landing_pad_names[struct.destination.landing_pad_name] for _, landing_pad in pairs(landing_pads_list) do required_fuel_max = math.max(required_fuel_max, Launchpad.get_fuel_cost(origin_zone, landing_pad.zone)) end if required_fuel_max > 0 then struct.required_fuel = required_fuel_max end end else local destination_zone = struct.destination.zone struct.required_fuel = Launchpad.get_fuel_cost(origin_zone, destination_zone); -- self-validates end else struct.required_fuel = nil -- invalid end -- balance tank fuel and lua fuel -- fuel tank % should match total % local fluidbox local effective_required_fuel = (struct.required_fuel or 0) + 1000 if not (struct.tank and struct.tank.valid) then struct.tank = struct.container.surface.create_entity{ name = Launchpad.name_rocket_launch_pad_tank, force = struct.container.force, position = {struct.container.position.x, struct.container.position.y + 1}} -- 1 tile down to be in front of silo struct.tank.fluidbox.set_filter(1, {name = name_fluid_rocket_fuel, force = true}) struct.container.connect_neighbour({wire = defines.wire_type.red, target_entity = struct.tank}) struct.container.connect_neighbour({wire = defines.wire_type.green, target_entity = struct.tank}) struct.tank.destructible = false end if struct.tank and #struct.tank.fluidbox > 0 then fluidbox = struct.tank.fluidbox[1] or {name = name_fluid_rocket_fuel, amount = 0} struct.total_fuel = (struct.lua_fuel or 0) + fluidbox.amount if struct.total_fuel > 0 then local percent_of_required = 1 -- overflow percent_of_required = struct.total_fuel / effective_required_fuel -- leave 10% available to fill faster if percent_of_required < 1 then percent_of_required = percent_of_required * 0.5 end fluidbox.amount = math.min(struct.total_fuel, math.min(1, percent_of_required) * struct.tank.fluidbox.get_capacity(1)) if fluidbox.amount < 1 then fluidbox.amount = 1 end struct.lua_fuel = struct.total_fuel - fluidbox.amount struct.tank.fluidbox[1] = fluidbox end end local main_inv = struct.container.get_inventory(defines.inventory.chest) struct.crew_capsules = struct.crew_capsules or 0 struct.rocket_sections = struct.rocket_sections or 0 -- take from section input first if (struct.crew_capsules or 0) < Launchpad.crew_capsules_per_rocket and struct.section_input and struct.section_input.valid then local inv = struct.section_input.get_inventory(defines.inventory.car_trunk) local items = inv.get_item_count(Capsule.name_space_capsule) if items > 0 then local take = 1 inv.remove({name=Capsule.name_space_capsule, count=take}) struct.crew_capsules = struct.crew_capsules + take end end -- otherwise take from main container if (struct.crew_capsules or 0) < Launchpad.crew_capsules_per_rocket then local items = main_inv.get_item_count(Capsule.name_space_capsule) if items > 0 then local take = math.min(items, Launchpad.crew_capsules_per_rocket - struct.crew_capsules) main_inv.remove({name=Capsule.name_space_capsule, count=take}) struct.crew_capsules = struct.crew_capsules + take end end -- take from section input first if struct.rocket_sections < Launchpad.rocket_sections_per_rocket and struct.section_input and struct.section_input.valid then local inv = struct.section_input.get_inventory(defines.inventory.car_trunk) local items = inv.get_item_count(Launchpad.name_rocket_section) if items > 0 then local take = math.min(items, Launchpad.rocket_sections_per_rocket - struct.rocket_sections) inv.remove({name=Launchpad.name_rocket_section, count=take}) struct.rocket_sections = struct.rocket_sections + take end end -- otherwise take from main container if struct.rocket_sections < Launchpad.rocket_sections_per_rocket then local items = main_inv.get_item_count(Launchpad.name_rocket_section) if items > 0 then local take = math.min(items, Launchpad.rocket_sections_per_rocket - struct.rocket_sections) main_inv.remove({name=Launchpad.name_rocket_section, count=take}) struct.rocket_sections = struct.rocket_sections + take end end -- not enough parts, add a section input to collect parts -- this will probably prevent input of other parts. if struct.rocket_sections < Launchpad.rocket_sections_per_rocket and not main_inv.can_insert{name = Launchpad.name_rocket_section, count = 1} then if not(struct.section_input and struct.section_input.valid) then Launchpad.add_section_input(struct) end else Launchpad.destroy_sub(struct, "section_input") end Launchpad.update_combinator(struct, main_inv) if struct.rocket_sections >= Launchpad.rocket_sections_per_rocket and struct.crew_capsules >= Launchpad.crew_capsules_per_rocket and not struct.has_inserted_dummy then local inserted = struct.silo.insert{name = Launchpad.name_rocket_launch_pad_silo_dummy, count = 1} if inserted == 1 then struct.has_inserted_dummy = true end end --game.print(profiler) end function Launchpad.launch(struct, skip_prep, allow_non_empty) if struct.launch_status > -1 then return end -- already launching if skip_prep ~= true then Launchpad.prep(struct) end local destination = Launchpad.lock_destination(struct, allow_non_empty) if destination then local current_zone = struct.zone local destination_zone = destination.zone struct.launching_to_destination = destination local required_fuel = Launchpad.get_fuel_cost(current_zone, destination_zone) struct.required_fuel = required_fuel -- the destination is locked so let's go if destination.landing_pad then destination.position = Util.vectors_add(destination.landing_pad.container.position, {x = 0, y = 2}) destination.landing_pad.inbound_rocket = true end -- set the contents local main_inv = struct.container.get_inventory(defines.inventory.chest) local main_contents = main_inv.get_contents() struct.launched_contents = main_contents -- make a global temp inventory. -- swap items to that inventory. -- make sure to destroy later struct.launched_inventory = game.create_inventory(#main_inv) Util.swap_inventories(main_inv, struct.launched_inventory) main_inv.clear() -- chould already be empty anyway. Log.trace("make launched_inventory with size" .. #struct.launched_inventory) -- consume the fuel local lua_fuel_consumed = math.min(struct.lua_fuel, required_fuel) struct.lua_fuel = struct.lua_fuel - lua_fuel_consumed if lua_fuel_consumed < required_fuel then local fluidbox = struct.tank.fluidbox[1] local tank_fuel_consumed = math.min(required_fuel - lua_fuel_consumed, fluidbox.amount) fluidbox.amount = math.max(1, fluidbox.amount - tank_fuel_consumed) struct.tank.fluidbox[1] = fluidbox struct.total_fuel = struct.lua_fuel + fluidbox.amount end if struct.tank and struct.tank.fluidbox and struct.tank.fluidbox[1] and struct.tank.fluidbox[1].name then struct.container.force.fluid_production_statistics.on_flow(struct.tank.fluidbox[1].name, -required_fuel) end -- consume rocket parts struct.rocket_sections = struct.rocket_sections - Launchpad.rocket_sections_per_rocket -- struct.container.force.item_production_statistics.on_flow(Launchpad.name_rocket_section, -Launchpad.rocket_sections_per_rocket) -- consume crew capsule struct.crew_capsules = struct.crew_capsules - Launchpad.crew_capsules_per_rocket -- struct.container.force.item_production_statistics.on_flow(Capsule.name_space_capsule, -Launchpad.crew_capsules_per_rocket) -- start charting the landing if destination_zone.type ~= "spaceship" then local range = Zone.discovery_scan_radius struct.container.force.chart(Zone.get_make_surface(destination_zone), { {destination.position.x - range, destination.position.y - range - Launchpad.landing_start_altitude}, {destination.position.x + range, destination.position.y + range} }) end -- set launch status struct.launch_status = 1 script.raise_event(Launchpad.on_cargo_rocket_launched_event, {struct = struct}) Launchpad.update_combinator(struct, main_inv) end end function Launchpad.tick(struct) -- this should only be called once per 60 ticks during prep and every tick during launch -- actually, launch should be handles seperatly. -- make launches and capsule launches in to tick tasks -- Launchpad.prep can be called every tick for gui updates if not (struct.container and struct.container.valid) then return Launchpad.destroy(struct) end if struct.launch_status < 1 then Launchpad.prep(struct) -- if struct.required_fuel then the destination must be valid if struct.required_fuel and struct.total_fuel >= struct.required_fuel and struct.rocket_sections >= Launchpad.rocket_sections_per_rocket and struct.crew_capsules >= Launchpad.crew_capsules_per_rocket then -- can launch -- TRIGGERS local destination = nil if struct.launch_trigger == "fuel-full" then -- we already know fuel is full so launch Launchpad.launch(struct, true) elseif struct.launch_trigger == "cargo-full" then local main_inv = struct.container.get_inventory(defines.inventory.chest) if main_inv then if not main_inv.can_insert({name = Launchpad.name_rocket_section, count = 1}) then -- cargo is full so try launch Launchpad.launch(struct, true) end end elseif struct.launch_trigger == "fuel-full-signal" then if struct.container.get_circuit_network(defines.wire_type.red).get_signal(Launchpad.signal_for_launch) > 0 or struct.container.get_circuit_network(defines.wire_type.green).get_signal(Launchpad.signal_for_launch) > 0 then Launchpad.launch(struct, true) end elseif struct.launch_trigger == "cargo-full-signal" then local main_inv = struct.container.get_inventory(defines.inventory.chest) if main_inv then if not main_inv.can_insert({name = Launchpad.name_rocket_section, count = 1}) then if struct.container.get_circuit_network(defines.wire_type.red).get_signal(Launchpad.signal_for_launch) > 0 or struct.container.get_circuit_network(defines.wire_type.green).get_signal(Launchpad.signal_for_launch) > 0 then Launchpad.launch(struct, true) end end end elseif struct.launch_trigger == "cargo-full-or-signal" then local main_inv = struct.container.get_inventory(defines.inventory.chest) if main_inv then if not main_inv.can_insert({name = Launchpad.name_rocket_section, count = 1}) then -- cargo is full so try launch Launchpad.launch(struct, true) else if struct.container.get_circuit_network(defines.wire_type.red).get_signal(Launchpad.signal_for_launch) > 0 or struct.container.get_circuit_network(defines.wire_type.green).get_signal(Launchpad.signal_for_launch) > 0 then -- signal so try launch Launchpad.launch(struct, true) end end end end end elseif struct.launch_status == 1 then -- the rocket should be ready if struct.silo.launch_rocket() then --Log.trace("launching") struct.launch_status = 2 struct.rocket_entity = struct.container.surface.find_entities_filtered{name=Launchpad.name_cargo_rocket, area=util.position_to_area(struct.container.position, 0.5)}[1] -- the launch pad could get removed at this point and the rocket would need to keep going. local tick_task = new_tick_task("launchpad-journey") tick_task.struct = struct tick_task.force_name = struct.force_name tick_task.rocket_entity = struct.rocket_entity tick_task.launch_timer = 0 tick_task.launched_contents = struct.launched_contents tick_task.launched_inventory = struct.launched_inventory Log.trace("Move launched_inventory to tick task " .. #tick_task.launched_inventory) struct.launched_inventory = nil tick_task.launching_to_destination = struct.launching_to_destination tick_task.delta_v = Zone.get_travel_delta_v(tick_task.struct.zone, tick_task.launching_to_destination.zone) local target_surface = Zone.get_make_surface(tick_task.launching_to_destination.zone) target_surface.request_to_generate_chunks(Util.vectors_add(tick_task.launching_to_destination.position, {x = 0, y = Launchpad.landing_start_altitude}), Launchpad.landing_start_altitude / 32 / 2) -- start the launch animation -- move the player seats over the rocket -- follow rocket launch -- if not landing_pad clear destination: remove biters -- teleport over landing zone -- if not landing_pad start descent / crash sequence -- place landfill, place scaffold, place cargo Pods -- place recoverable sections -- destroy the seats to release the players end else -- stays at struct.launch_status 2 until tick_task sets it back to -1 end end function Launchpad.drop_rocket_fragment(zone, size, name, position, start_position_x_offset) local target_surface = Zone.get_make_surface(zone) local falling_time if math.random() < 0.5 then -- biggest first falling_time = Launchpad.time_landing_debris_first + (size - math.random()) / 4 * (Launchpad.time_landing_debris_last - Launchpad.time_landing_debris_first) else -- or just random falling_time = Launchpad.time_landing_debris_first + math.random() * (Launchpad.time_landing_debris_last - Launchpad.time_landing_debris_first) end local landing_start_altitude = Launchpad.landing_start_altitude if Zone.is_space(zone) then landing_start_altitude = landing_start_altitude * 0.5 end local random_start_offset = {x = 8 * (math.random() - 0.5), y = 8 * (math.random() - 0.5)} local start_position = Util.vectors_add(position, { x = random_start_offset.x + start_position_x_offset, y = random_start_offset.y - landing_start_altitude}) local start_position_shadow = Util.vectors_add(position, { x = random_start_offset.x + landing_start_altitude + start_position_x_offset, y = random_start_offset.y}) local land_position = Util.vectors_add(position, { x = random_start_offset.x + 32 * (math.random() - 0.5), y = random_start_offset.y + 32 * (math.random() - 0.5)}) local flat_distance = Util.vectors_delta_length(start_position, land_position) local flat_distance_shadow = Util.vectors_delta_length(start_position_shadow, land_position) local speed = flat_distance / falling_time local speed_shadow = flat_distance_shadow / falling_time if Zone.is_space(zone) then target_surface.create_entity{ name = mod_prefix .. "space-" ..name, position = start_position, target = land_position, speed = speed, --max_rage = flat_distance, } else target_surface.create_entity{ name = mod_prefix .. "falling-" ..name, position = start_position, target = land_position, speed = speed, max_rage = flat_distance, } target_surface.create_entity{ name = mod_prefix .. "shadow-" ..name, position = start_position_shadow, target = land_position, speed = speed_shadow, max_rage = flat_distance_shadow, } end end function Launchpad.tick_journey(tick_task) -- from tick_task.type = "launchpad-journey" if tick_task.launch_timer then tick_task.launch_timer = tick_task.launch_timer + 1 local struct = tick_task.struct if struct and struct.valid then if tick_task.launch_timer <= Launchpad.time_takeoff then -- collect passengers while rocket is low tick_task.passengers = tick_task.passengers or {} -- characters not players for _, seat in pairs(struct.seats) do if seat.get_driver() then table.insert(tick_task.passengers, seat.get_driver()) if remote.interfaces["jetpack"] and remote.interfaces["jetpack"]["block_jetpack"] then remote.call("jetpack", "block_jetpack", {character=seat.get_driver()}) end seat.set_driver(nil) end if seat.get_passenger() then table.insert(tick_task.passengers, seat.get_passenger()) if remote.interfaces["jetpack"] and remote.interfaces["jetpack"]["block_jetpack"] then remote.call("jetpack", "block_jetpack", {character=seat.get_passenger()}) end seat.set_passenger(nil) end end end end if tick_task.rocket_entity and tick_task.rocket_entity.valid then -- move passengers with the rocket if tick_task.passengers then for _, passenger in pairs(tick_task.passengers) do if passenger and passenger.valid then if remote.interfaces["jetpack"] and remote.interfaces["jetpack"]["block_jetpack"] then remote.call("jetpack", "block_jetpack", {character=passenger}) end local target_position = { x = tick_task.rocket_entity.position.x, y = tick_task.rocket_entity.position.y - 2 - math.max(0, tick_task.launch_timer - Launchpad.time_takeoff) / 64} passenger.teleport(target_position) passenger.destructible = false --passenger.teleport(Util.move_to(passenger.position, target_position, tick_task.launch_timer / 100) ) end end end if tick_task.launch_timer >= Launchpad.time_takeoff_finish_ascent then -- skip to next phase tick_task.launch_timer = nil end else -- skip to next phase tick_task.launch_timer = nil end else if not tick_task.land_timer then tick_task.land_timer = 0 -- setup next landing sequence if tick_task.rocket_entity and tick_task.rocket_entity.valid then tick_task.rocket_entity.destroy() tick_task.rocket_entity = nil end -- reset the landing pad local struct = tick_task.struct if struct and struct.valid then struct.launched_contents = nil if struct.launched_inventory and struct.launched_inventory.valid then struct.launched_inventory.destroy() struct.launched_inventory = nil end struct.launch_status = -1 struct.launching_to_destination = nil struct.has_inserted_dummy = nil global.forces[tick_task.force_name].cargo_rockets_launched = (global.forces[tick_task.force_name].cargo_rockets_launched or 0) + 1 end --game.print("Dev note: Show capsule landing sequence and discarded rocket part debris falling from the sky.") -- determine success rate -- for landing pad: -- show capsule descent over the pad, then put capsule inside the pad and player outside. -- show up to 10 cargo pods the fly past to the landing pad first. -- failed cargo pods are added to crash debris. -- crash debris based on recovery tech. -- put some recovered parts in the landing pad based on recovery tech. -- no fire from spilt fuel --for general location or guidance failure -- show capsule descent at random locaiton then put player outside. -- show up to 10 cargo pods the fly past to random locations -- failed cargo pods are added to crash debris. -- all rocket parts added to crash debris regardless of recovery tech. -- add fire from spilt fuel -- add some wider area explosive damage local is_landingpad = (tick_task.launching_to_destination.landing_pad and tick_task.launching_to_destination.landing_pad.container and tick_task.launching_to_destination.landing_pad.container.valid) if is_landingpad then local error_chance = Launchpad.get_survivability_loss(game.forces[tick_task.force_name], tick_task.delta_v) if math.random() < error_chance then -- off course is_landingpad = false local force = tick_task.launching_to_destination.landing_pad.container.force force.item_production_statistics.on_flow(Launchpad.name_rocket_section, -Launchpad.rocket_sections_per_rocket) tick_task.launching_to_destination.landing_pad.inbound_rocket = nil tick_task.launching_to_destination.attempted_position = tick_task.launching_to_destination.landing_pad.container.position tick_task.launching_to_destination.attempted_landing_pad = tick_task.launching_to_destination.landing_pad tick_task.launching_to_destination.landing_pad = nil local new_pos = tick_task.launching_to_destination.position new_pos.x = new_pos.x + (math.random() - 0.5) * 150 new_pos.y = new_pos.y + (math.random() - 0.5) * 128 tick_task.launching_to_destination.position = Zone.find_zone_landing_position(tick_task.launching_to_destination.zone, new_pos) force.print({"space-exploration.rocket_survivability_fail", "[gps="..math.floor(new_pos.x)..","..math.floor(new_pos.y)..","..Zone.get_surface_name(tick_task.launching_to_destination.zone).."]"}) end end local cargo_loss = Launchpad.get_cargo_loss(game.forces[tick_task.force_name], tick_task.delta_v) if is_landingpad then cargo_loss = cargo_loss / 2 end local keep_percent = 1 - cargo_loss if tick_task.launched_inventory and tick_task.launched_inventory.valid then Log.trace("Loss launched_inventory") -- remove items from the inventory directly for item_name, item_count in pairs(tick_task.launched_contents) do local item_type = game.item_prototypes[item_name] if item_type and item_count > 1 and item_type.stack_size > 1 then local loss = cargo_loss * 2 * math.random() -- +/- 100% local lost_items = math.floor(item_count * loss) if lost_items > 0 then tick_task.launched_inventory.remove({name = item_name, count = lost_items}) end end end else -- old method for legacy launches in progress. Log.trace("Loss safe_contents") tick_task.safe_contents = {} for item_type, item_count in pairs(tick_task.launched_contents) do local item_type = game.item_prototypes[item_name] if item_type and item_count > 1 and item_type.stack_size > 1 then local loss = 2 * cargo_loss * math.random() -- +/- 100% -- keep 10% at a minimum tick_task.safe_contents[item_type] = math.max(math.ceil(item_count / 10), item_count - item_count * loss) else tick_task.safe_contents[item_type] = item_count end end end -- cargo loss should never go above 90%, it can be below 10% round up for number of crashing pods. local total_pods = 10 local crashing_pods = math.floor(total_pods * cargo_loss + 0.49) local safe_pods = total_pods - crashing_pods local pods = {} local target_surface = Zone.get_make_surface(tick_task.launching_to_destination.zone) Zone.apply_markers(tick_task.launching_to_destination.zone) -- in case the surface exists for i = 1, safe_pods do table.insert(pods, {type = "safe"}) end for i = 1, crashing_pods do table.insert(pods, {type = "crash"}) end pods = Util.shuffle(pods) local landing_start_altitude = Launchpad.landing_start_altitude if Zone.is_space(tick_task.launching_to_destination.zone) then landing_start_altitude = landing_start_altitude * 0.5 end local start_position_x_offset = landing_start_altitude * (math.random() - 0.5) -- gives an angle to all pieces local start_position = Util.vectors_add(tick_task.launching_to_destination.position, { x = start_position_x_offset, y = - landing_start_altitude}) local start_position_shadow = Util.vectors_add(tick_task.launching_to_destination.position, { x = start_position_x_offset + landing_start_altitude, y = 0}) tick_task.safe_pods = {} for i, pod in pairs(pods) do local falling_time = Launchpad.time_landing_cargopod_first + (Launchpad.time_landing_cargopod_last - Launchpad.time_landing_cargopod_first) * (i - 1) / (total_pods - 1) local land_position if pod.type == "safe" and is_landingpad then land_position = tick_task.launching_to_destination.position else land_position = Util.vectors_add(tick_task.launching_to_destination.position, {x = 32 * (math.random() - 0.5), y = 32 * (math.random() - 0.5)}) end local flat_distance = Util.vectors_delta_length(start_position, land_position) local flat_distance_shadow = Util.vectors_delta_length(start_position_shadow, land_position) local speed = flat_distance / falling_time local speed_shadow = flat_distance_shadow / falling_time if pod.type == "safe" then -- track the safe ones for adding items local entity = target_surface.create_entity{ name = mod_prefix .. "falling-cargo-pod", position = start_position, target = land_position, speed = speed, max_rage = flat_distance, } entity.orientation = 0 table.insert(tick_task.safe_pods, {falling_entity=entity, land_position = land_position}) target_surface.create_entity{ name = mod_prefix .. "shadow-cargo-pod", position = start_position_shadow, target = land_position, speed = speed_shadow, max_rage = flat_distance_shadow, } else target_surface.create_entity{ name = mod_prefix .. "falling-" .. Launchpad.names_cargo_fragment[(i % #Launchpad.names_cargo_fragment) + 1], position = start_position, target = land_position, speed = speed, max_rage = flat_distance, } target_surface.create_entity{ name = mod_prefix .. "shadow-" .. Launchpad.names_cargo_fragment[(i % #Launchpad.names_cargo_fragment) + 1], position = start_position_shadow, target = land_position, speed = speed_shadow, max_rage = flat_distance_shadow, } end end if not is_landingpad then for i = 1, Launchpad.rocket_fragments_large do Launchpad.drop_rocket_fragment(tick_task.launching_to_destination.zone, 4, Launchpad.names_rocket_fragments_large[(i % #Launchpad.names_rocket_fragments_large) + 1], tick_task.launching_to_destination.position, start_position_x_offset) end for i = 1, Launchpad.rocket_fragments_medium do Launchpad.drop_rocket_fragment(tick_task.launching_to_destination.zone, 3, Launchpad.names_rocket_fragments_medium[(i % #Launchpad.names_rocket_fragments_medium) + 1], tick_task.launching_to_destination.position, start_position_x_offset) end for i = 1, Launchpad.rocket_fragments_small do Launchpad.drop_rocket_fragment(tick_task.launching_to_destination.zone, 2, Launchpad.names_rocket_fragments_small[(i % #Launchpad.names_rocket_fragments_small) + 1], tick_task.launching_to_destination.position, start_position_x_offset) end for i = 1, Launchpad.rocket_fragments_tiny do Launchpad.drop_rocket_fragment(tick_task.launching_to_destination.zone, 1, Launchpad.names_rocket_fragments_tiny[(i % #Launchpad.names_rocket_fragments_tiny) + 1], tick_task.launching_to_destination.position, start_position_x_offset) end end -- capsule tick_task.capsule = target_surface.create_entity{ name = Capsule.name_space_capsule_vehicle, position=start_position, force = tick_task.force_name } tick_task.capsule.destructible = false tick_task.capsule_shadow = target_surface.create_entity{name = Capsule.name_space_capsule_vehicle_shadow, position=start_position_shadow, force = "neutral"} tick_task.capsule_light = target_surface.create_entity{name = Capsule.name_space_capsule_vehicle_light, position=start_position, speed = 0, target = tick_task.capsule} if tick_task.passengers then for _, passenger in pairs(tick_task.passengers) do if passenger and passenger.valid then -- this disassociates the character if passenger.player then LaunchpadGUI.gui_close(passenger.player) end tick_task.passengers[_] = teleport_character_to_surface(passenger, target_surface, tick_task.capsule.position) if tick_task.passengers[_] then tick_task.passengers[_].destructible = false end end end end -- end landing sequence setup else -- continuation for landing sqequence tick_task.land_timer = tick_task.land_timer + 1 local time_remaining = math.max(1, Launchpad.time_landing_capsule_touchdown - tick_task.land_timer) local target_pos = tick_task.launching_to_destination.position local capsule_target_pos = tick_task.launching_to_destination.attempted_position or target_pos --local travel = Util.vector_multiply(Util.vectors_delta(tick_task.capsule.position, target_pos), 1 / time_remaining) --local travel_shadow = Util.vector_multiply(Util.vectors_delta(tick_task.capsule_shadow.position, target_pos), 1 / time_remaining) if not(tick_task.capsule and tick_task.capsule.valid) then local target_surface = Zone.get_make_surface(tick_task.launching_to_destination.zone) tick_task.capsule = target_surface.create_entity{ name = Capsule.name_space_capsule_vehicle, position={x = capsule_target_pos.x, y = capsule_target_pos.y - Launchpad.landing_start_altitude * time_remaining / Launchpad.time_landing_capsule_touchdown}, force = tick_task.force_name } end tick_task.capsule.destructible = true local travel = Util.vector_multiply(Util.vectors_delta(tick_task.capsule.position, capsule_target_pos), math.min(0.9, 2 / time_remaining)) local animation_speed = 1/3 local animation_frames = 24 local animation_frame = math.max(math.min(math.floor(time_remaining * animation_speed), animation_frames), 1) tick_task.capsule.teleport(Util.vectors_add(tick_task.capsule.position, travel)) tick_task.capsule.orientation = (animation_frame - 1) / animation_frames if tick_task.capsule_light and tick_task.capsule_light.valid then tick_task.capsule_light.teleport(tick_task.capsule.position) end if tick_task.capsule_shadow and tick_task.capsule_shadow.valid then local travel_shadow = Util.vector_multiply(Util.vectors_delta(tick_task.capsule_shadow.position, capsule_target_pos), math.min(0.9, 2 / time_remaining)) tick_task.capsule_shadow.teleport(Util.vectors_add(tick_task.capsule_shadow.position, travel_shadow)) tick_task.capsule_shadow.graphics_variation = animation_frame end if tick_task.passengers then for _, passenger in pairs(tick_task.passengers) do if passenger and passenger.valid then passenger.teleport({x = tick_task.capsule.position.x, y = tick_task.capsule.position.y - 0.2}) end end end for _, safe_pod in pairs(tick_task.safe_pods) do if safe_pod.falling_entity then if not safe_pod.falling_entity.valid then safe_pod.falling_entity = nil if tick_task.launching_to_destination.landing_pad and tick_task.launching_to_destination.landing_pad.valid then --tick_task.launching_to_destination.landing_pad.container.insert{name = Launchpad.name_cargo_pod, count = 1} -- we have rocket parts instead --tick_task.launching_to_destination.landing_pad.container.force.item_production_statistics.on_flow(Launchpad.name_cargo_pod, 1) else safe_pod.cargo_entity = tick_task.capsule.surface.create_entity{ name = Launchpad.name_cargo_pod, position = safe_pod.land_position, force = tick_task.force_name } end end end end if tick_task.land_timer > Launchpad.time_landing_capsule_touchdown then if tick_task.launching_to_destination.landing_pad and tick_task.launching_to_destination.landing_pad.valid then -- going to a landing pad tick_task.launching_to_destination.landing_pad.container.insert{name = Capsule.name_space_capsule, count = 1} --tick_task.launching_to_destination.landing_pad.container.force.item_production_statistics.on_flow(Capsule.name_space_capsule, 1) tick_task.capsule.destroy() if tick_task.capsule_light and tick_task.capsule_light.valid then tick_task.capsule_light.destroy() end if tick_task.capsule_shadow and tick_task.capsule_shadow.valid then tick_task.capsule_shadow.destroy() end if tick_task.launched_inventory and tick_task.launched_inventory.valid then Log.trace("Insert to pad launched_inventory") local inv = tick_task.launching_to_destination.landing_pad.container.get_inventory(defines.inventory.chest) for i = 1, #tick_task.launched_inventory do local stack = tick_task.launched_inventory[i] if stack and stack.valid and stack.valid_for_read and stack.count then inv.insert(tick_task.launched_inventory[i]) end end tick_task.launched_inventory.clear() tick_task.launched_inventory.destroy() tick_task.launched_inventory = nil elseif tick_task.safe_contents then -- legacy method Log.trace("Insert to pad safe_contents") for name, count in pairs(tick_task.safe_contents) do tick_task.launching_to_destination.landing_pad.container.insert{name=name, count=count} end end local reusability = Launchpad.get_reusability(game.forces[tick_task.force_name]) local reusable_parts = math.floor(math.min(Launchpad.rocket_sections_per_rocket, Launchpad.rocket_sections_per_rocket * reusability * ( 0.9 + 0.2 * math.random()))) if reusable_parts > 0 then tick_task.launching_to_destination.landing_pad.container.insert{name = Launchpad.name_rocket_section, count = reusable_parts} -- tick_task.launching_to_destination.landing_pad.container.force.item_production_statistics.on_flow(Launchpad.name_rocket_section, reusable_parts) end if reusable_parts < Launchpad.rocket_sections_per_rocket then tick_task.launching_to_destination.landing_pad.container.force.item_production_statistics.on_flow(Launchpad.name_rocket_section, -(Launchpad.rocket_sections_per_rocket - reusable_parts)) end tick_task.launching_to_destination.landing_pad.inbound_rocket = nil else if tick_task.launched_inventory and tick_task.launched_inventory.valid then Log.trace("Distribute to pods launched_inventory") local spidertrons = {"spidertron-spidertron-rocket-launcher-1", "spidertron"} for _, spidertron_name in pairs(spidertrons) do if tick_task.launched_contents[spidertron_name] and tick_task.launched_contents[spidertron_name] > 0 then local pod = tick_task.safe_pods[#tick_task.safe_pods] local spider = pod.cargo_entity.surface.create_entity{ name = spidertron_name, position = pod.cargo_entity.position, force = pod.cargo_entity.force, raise_built = true } if spider then tick_task.launched_contents[spidertron_name] = tick_task.launched_contents[spidertron_name] - 1 local spider_item = tick_task.launched_inventory.find_item_stack(spidertron_name) if spider_item.grid then Util.transfer_equipment_grid(spider_item, spider) end local inventory = spider_item.get_inventory(defines.inventory.item_main) or spider_item.get_inventory(defines.inventory.car_trunk) local reserve_item_stacks = { "construction-robot", "logistic-chest-storage", "roboport", } local take_item_stacks = { "construction-robot", "logistic-chest-storage", "roboport", "substation", mod_prefix .. "pylon-substation", "solar-panel", mod_prefix .. "space-solar-panel-1", mod_prefix .. "space-solar-panel-2", mod_prefix .. "space-solar-panel-3", "accumulator", "laser-turret", } if inventory then Util.transfer_inventory_filters_direct(spider_item.get_inventory(defines.inventory.item_main), spider.get_inventory(defines.inventory.car_trunk)) else -- item inv not supported yet? local spider_inv = spider.get_inventory(defines.inventory.car_trunk) for i, item in pairs(reserve_item_stacks) do spider_inv.set_filter(i, item) end end tick_task.launched_inventory.remove({name = spidertron_name, count = 1}) for _, item in pairs(take_item_stacks) do if tick_task.launched_contents[item] and tick_task.launched_contents[item] > 0 then local remove = math.min(tick_task.launched_contents[item], 50) tick_task.launched_contents[item] = tick_task.launched_contents[item] - remove tick_task.launched_inventory.remove({name = item, count = remove}) spider.insert({name = item, count = remove}) end end -- the auto-deconstruction is a problem for the spider, clear nearby deconstruction orders. tick_task.spider = spider break end end end for i = 1, #tick_task.launched_inventory do for j = 0, #tick_task.safe_pods do local pod = tick_task.safe_pods[((i + j) % #tick_task.safe_pods) + 1] local stack = tick_task.launched_inventory[i] if pod and pod.cargo_entity and pod.cargo_entity.valid and stack and stack.valid and stack.valid_for_read and stack.count then local inserted = pod.cargo_entity.insert(tick_task.launched_inventory[i]) if inserted == stack.count then tick_task.launched_inventory[i].clear() else tick_task.launched_inventory[i].count = stack.count - inserted end end end end tick_task.launched_inventory.clear() tick_task.launched_inventory.destroy() tick_task.launched_inventory = nil else -- legacy version Log.trace("Distribute to pods safe_contents") local i = 1 for name, count in pairs(tick_task.safe_contents) do local remaining = count for _, pod in pairs(tick_task.safe_pods) do if pod and pod.cargo_entity and pod.cargo_entity.valid and remaining >= 1 then remaining = remaining - pod.cargo_entity.insert{name=name, count=remaining} end end end end for _, pod in pairs(tick_task.safe_pods) do -- HERE if pod and pod.cargo_entity and pod.cargo_entity.valid and not tick_task.spider then pod.cargo_entity.order_deconstruction(tick_task.force_name) end end if tick_task.launching_to_destination.attempted_landing_pad and tick_task.launching_to_destination.attempted_landing_pad.valid then tick_task.launching_to_destination.attempted_landing_pad.container.insert{name = Capsule.name_space_capsule, count = 1} -- tick_task.launching_to_destination.attempted_landing_pad.container.force.item_production_statistics.on_flow(Capsule.name_space_capsule, 1) tick_task.capsule.destroy() if tick_task.capsule_light and tick_task.capsule_light.valid then tick_task.capsule_light.destroy() end if tick_task.capsule_shadow and tick_task.capsule_shadow.valid then tick_task.capsule_shadow.destroy() end end end if tick_task.passengers then for _, passenger in pairs(tick_task.passengers) do if passenger and passenger.valid then teleport_non_colliding(passenger, capsule_target_pos) passenger.destructible = true if passenger.player then local playerdata = get_make_playerdata(passenger.player) playerdata.zero_velocity = true passenger.player.print({"space-exploration.respawn-if-stranded"}) end if remote.interfaces["jetpack"] and remote.interfaces["jetpack"]["unblock_jetpack"] then remote.call("jetpack", "unblock_jetpack", {character=passenger}) -- last otherwise the teleport breacks the chatacter reference end end end end if tick_task.capsule and tick_task.capsule.valid then tick_task.capsule.destructible = false end tick_task.launched_contents = nil if tick_task.launched_inventory and tick_task.launched_inventory.valid then tick_task.launched_inventory.destroy() end tick_task.launched_inventory = nil tick_task.safe_contents = nil tick_task.valid = false -- close tick task end end end end function Launchpad.on_player_driving_changed_state(event) local player = game.players[event.player_index] if player then if player.vehicle and player.vehicle.name == Launchpad.name_rocket_launch_pad_seat then if player.character then remote.call("jetpack", "stop_jetpack_immediate", {character = player.character}) end local armor_inv = player.get_inventory(defines.inventory.character_armor) if not(armor_inv and armor_inv[1] and armor_inv[1].valid_for_read and Util.table_contains(name_thruster_suits, armor_inv[1].name)) then player.print({"space-exploration.launch-suit-warning"}) end local launch_pad_entity = player.surface.find_entities_filtered{ limit = 1, area = util.position_to_area(util.vectors_add(player.position, {x = 0, y = -5}), 5), name = Launchpad.name_rocket_launch_pad }[1] if launch_pad_entity then LaunchpadGUI.gui_open(player, Launchpad.from_entity(launch_pad_entity)) end end end end Event.addListener(defines.events.on_player_driving_changed_state, Launchpad.on_player_driving_changed_state) --- Creates the composite entity when the laund pad is made --- Does not handle clone_area in any acceptable manner --- (it doesn't listen to on_cloned and even if it did it --- would start duplicated entities) --- TODO: maybe make it support cloning by get_make the entities --- instead of making them every time ---@param event any function Launchpad.on_entity_created(event) local entity if event.entity and event.entity.valid then entity = event.entity end if event.created_entity and event.created_entity.valid then entity = event.created_entity end if not entity then return end if entity.name == Launchpad.name_rocket_launch_pad then local force_name = entity.force.name local zone = Zone.from_surface(entity.surface) if not zone then return cancel_entity_creation(entity, event.player_index, "Invalid launch pad location") end local default_name = zone.name local default_destination_zone = zone if zone.orbit then default_destination_zone = zone.orbit elseif zone.parent and zone.parent.type ~= "star" then default_destination_zone = zone.parent end local struct = { type = Launchpad.name_rocket_launch_pad, valid = true, force_name = force_name, unit_number = entity.unit_number, container = entity, name = default_name, rocket_sections = 0, crew_capsules = 0, launch_trigger = "none", lua_fuel = 0, total_fuel = 0, zone = zone, destination = { zone = default_destination_zone }, launch_status = -1 } global.rocket_launch_pads[entity.unit_number] = struct Log.trace("Launchpad: launch_pad added") Launchpad.name(struct) -- assigns to zone_assets Launchpad.add_section_input(struct) -- spawn combinator struct.combinator = entity.surface.create_entity{ name = Launchpad.name_rocket_launch_pad_combinator, force = entity.force, position = {entity.position.x, entity.position.y}} entity.connect_neighbour({wire = defines.wire_type.red, target_entity = struct.combinator}) entity.connect_neighbour({wire = defines.wire_type.green, target_entity = struct.combinator}) struct.combinator.destructible = false -- spawn silo struct.silo = entity.surface.create_entity{ name = Launchpad.name_rocket_launch_pad_silo, force = entity.force, position = {entity.position.x, entity.position.y + 1/32}} -- 1 px down to be in front of container struct.silo.destructible = false -- spawn storage tank struct.tank = entity.surface.create_entity{ name = Launchpad.name_rocket_launch_pad_tank, force = entity.force, position = {entity.position.x, entity.position.y + 1}} -- 1 tile down to be in front of silo struct.tank.fluidbox.set_filter(1, {name = name_fluid_rocket_fuel, force = true}) entity.connect_neighbour({wire = defines.wire_type.red, target_entity = struct.tank}) entity.connect_neighbour({wire = defines.wire_type.green, target_entity = struct.tank}) struct.tank.destructible = false struct.seats = {} -- spawn passenger seats for i = -2, 2, 1 do local seat = entity.surface.create_entity{ name = Launchpad.name_rocket_launch_pad_seat, force = entity.force, position = {entity.position.x + i, entity.position.y + 5.9}} seat.destructible = false table.insert(struct.seats, seat) end -- set settings if event.tags then if event.tags.name then Launchpad.rename(struct, event.tags.name) end struct.launch_trigger = event.tags.launch_trigger if event.tags.zone_name then struct.destination.zone = Zone.from_name(event.tags.zone_name) else struct.destination.zone = nil end if event.tags.landing_pad_name then struct.destination.landing_pad_name = event.tags.landing_pad_name end end if event.player_index and game.players[event.player_index] and game.players[event.player_index].connected then LaunchpadGUI.gui_open(game.players[event.player_index], struct) end end end Event.addListener(defines.events.on_built_entity, Launchpad.on_entity_created) Event.addListener(defines.events.on_robot_built_entity, Launchpad.on_entity_created) Event.addListener(defines.events.script_raised_built, Launchpad.on_entity_created) Event.addListener(defines.events.script_raised_revive, Launchpad.on_entity_created) function Launchpad.get_struct_type_table(struct) local zone_assets = Zone.get_force_assets(struct.force_name, struct.zone.index) if struct.type == Launchpad.name_rocket_launch_pad then zone_assets.rocket_launch_pad_names = zone_assets.rocket_launch_pad_names or {} return zone_assets.rocket_launch_pad_names end end function Launchpad.remove_struct_from_table(struct) local type_table = Launchpad.get_struct_type_table(struct) if not type_table[struct.name] then return end type_table[struct.name][struct.unit_number] = nil local count_remaining = 0 for _, remaining in pairs(type_table[struct.name]) do count_remaining = count_remaining + 1 end if count_remaining == 0 then type_table[struct.name] = nil end end function Launchpad.destroy_sub(struct, key) if struct[key] and struct[key].valid then struct[key].destroy() struct[key] = nil end end function Launchpad.destroy(struct, player_index) if not struct then Log.trace("struct_destroy: no struct") return end struct.valid = false local capsules = struct.crew_capsules or 0 local sections = struct.rocket_sections or 0 if player_index then local player = game.players[player_index] if player and player.connected then if capsules > 0 then local inserted = player.insert{name = Capsule.name_space_capsule, count = capsules} capsules = capsules - inserted end if sections > 0 then local inserted = player.insert{name = Launchpad.name_rocket_section, count = sections} sections = sections - inserted end end end if struct.container and struct.container.valid then local position = struct.container.position local surface = struct.container.surface if capsules > 0 then surface.spill_item_stack(position, {name = Capsule.name_space_capsule, count = capsules}, true, struct.container.force, false) end if sections > 0 then surface.spill_item_stack(position, {name = Launchpad.name_rocket_section, count = sections}, true, struct.container.force, false) end end Launchpad.destroy_sub(struct, 'container') Launchpad.destroy_sub(struct, 'section_input') Launchpad.destroy_sub(struct, 'tank') Launchpad.destroy_sub(struct, 'silo') Launchpad.destroy_sub(struct, 'combinator') for _, seat in pairs(struct.seats or {}) do if seat and seat.valid then seat.destroy() end end struct.seats = nil Launchpad.remove_struct_from_table(struct) global.rocket_launch_pads[struct.unit_number] = nil -- if a player has this gui open then close it local gui_name = LaunchpadGUI.name_rocket_launch_pad_gui_root for _, player in pairs(game.connected_players) do local root = player.gui.relative[gui_name] if root and root.tags and root.tags.unit_number == struct.unit_number then root.destroy() end end end function Launchpad.name(struct, new_name) struct.name = (new_name or struct.name) local type_table = Launchpad.get_struct_type_table(struct) type_table[struct.name] = type_table[struct.name] or {} type_table[struct.name][struct.unit_number] = struct end function Launchpad.rename(struct, new_name) local old_name = struct.name Launchpad.remove_struct_from_table(struct) Launchpad.name(struct, new_name) end function Launchpad.on_entity_removed(event) local entity = event.entity if entity and entity.valid and entity.name == Launchpad.name_rocket_launch_pad then Launchpad.destroy(Launchpad.from_entity(entity), event.player_index ) end end Event.addListener(defines.events.on_entity_died, Launchpad.on_entity_removed) Event.addListener(defines.events.on_robot_mined_entity, Launchpad.on_entity_removed) Event.addListener(defines.events.on_player_mined_entity, Launchpad.on_entity_removed) Event.addListener(defines.events.script_raised_destroy, Launchpad.on_entity_removed) function Launchpad.on_tick(struct) -- handle launchpads for _, struct in pairs(global.rocket_launch_pads) do if (struct.launch_status and struct.launch_status > 0) or (game.tick + struct.unit_number) % 60 == 0 then Launchpad.tick(struct) end end -- update guis if game.tick % 60 == 0 then for _, player in pairs(game.connected_players) do LaunchpadGUI.gui_update(player) end end end Event.addListener(defines.events.on_tick, Launchpad.on_tick) --- Handles the player creating a blueprint by setting tags to store the state of launch pads ---@param event any function Launchpad.on_player_setup_blueprint(event) local player_index = event.player_index if player_index and game.players[player_index] and game.players[player_index].connected then local player = game.players[player_index] -- this setup code and checks is a workaround for the fact that the event doesn't specify the blueprint on the event -- and the player.blueprint_to_setup isn't actually set in the case of copy/paste or blueprint library or select new contents local blueprint = nil if player and player.blueprint_to_setup and player.blueprint_to_setup.valid_for_read then blueprint = player.blueprint_to_setup elseif player and player.cursor_stack.valid_for_read and player.cursor_stack.is_blueprint then blueprint = player.cursor_stack end if blueprint and blueprint.is_blueprint_setup() then local mapping = event.mapping.get() local blueprint_entities = blueprint.get_blueprint_entities() if blueprint_entities then for _, blueprint_entity in pairs(blueprint_entities) do if blueprint_entity.name == Launchpad.name_rocket_launch_pad then local entity = mapping[blueprint_entity.entity_number] if entity then local launch_pad = Launchpad.from_entity(entity) if launch_pad then local tags = {} tags.name = launch_pad.name tags.launch_trigger = launch_pad.launch_trigger if launch_pad.destination.zone then tags.zone_name = launch_pad.destination.zone.name end if launch_pad.destination.landing_pad_name then tags.landing_pad_name = launch_pad.destination.landing_pad_name end blueprint.set_blueprint_entity_tags(blueprint_entity.entity_number, tags) end end end end end end end end Event.addListener(defines.events.on_player_setup_blueprint, Launchpad.on_player_setup_blueprint) --- Handles the player copy/pasting settings between launch pads ---@param event any function Launchpad.on_entity_settings_pasted(event) local player_index = event.player_index if player_index and game.players[player_index] and game.players[player_index].connected and event.source and event.source.valid and event.destination and event.destination.valid then if not (event.source.name == Launchpad.name_rocket_launch_pad) then return end if not (event.destination.name == Launchpad.name_rocket_launch_pad) then return end local player = game.players[player_index] local launch_pad_from = Launchpad.from_entity(event.source) local launch_pad_to = Launchpad.from_entity(event.destination) if launch_pad_from and launch_pad_to then -- actual settings copy Launchpad.rename(launch_pad_to, launch_pad_from.name) launch_pad_to.launch_trigger = launch_pad_from.launch_trigger launch_pad_to.destination.zone = launch_pad_from.destination.zone launch_pad_to.destination.landing_pad_name = launch_pad_from.destination.landing_pad_name end end end Event.addListener(defines.events.on_entity_settings_pasted, Launchpad.on_entity_settings_pasted) function Launchpad.on_init(event) global.rocket_launch_pads = {} end Event.addListener("on_init", Launchpad.on_init, true) return Launchpad