diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 0000000..4beea6e --- /dev/null +++ b/.luarc.json @@ -0,0 +1,5 @@ +{ + "diagnostics.globals": [ + "data" + ] +} \ No newline at end of file diff --git a/Graphics/Base/base_core.png b/Graphics/Base/base_core.png new file mode 100644 index 0000000..f80e3ab Binary files /dev/null and b/Graphics/Base/base_core.png differ diff --git a/Graphics/Tech/nauvis-tech.png b/Graphics/Tech/nauvis-tech.png new file mode 100644 index 0000000..73eb566 Binary files /dev/null and b/Graphics/Tech/nauvis-tech.png differ diff --git a/base.lua b/base.lua new file mode 100644 index 0000000..7321fe0 --- /dev/null +++ b/base.lua @@ -0,0 +1,165 @@ +local drop_zone = require("drop_zone") +local base = {} + +local COLORS = { + {r = .90, g = .45, b = .10, a = 1}, {r = .95, g = .20, b = .20, a = 1}, + {r = .70, g = .20, b = .90, a = 1}, {r = .20, g = .85, b = .20, a = 1}, + {r = .20, g = .60, b = .95, a = 1}, +} + +local function notify(force_name, message) + local force = game.forces[force_name] + if force then for _, player in pairs(force.players) do if player.valid then player.print(message) end end end +end + +local function count(force_name) + local total = 0 + for _ in pairs(storage.force_bases[force_name] or {}) do total = total + 1 end + return total +end + +local function color_for(force_name) + return storage.force_colors[force_name] or {r = 1, g = 1, b = 1, a = 1} +end + +function base.assign_force_color(force_name) + if storage.force_colors[force_name] then return end + local used = 0 + for _ in pairs(storage.force_colors) do used = used + 1 end + storage.force_colors[force_name] = COLORS[(used % #COLORS) + 1] +end + +function base.register(entity) + if not (entity and entity.valid) then return end + local force_name = entity.force.name + storage.force_bases[force_name] = storage.force_bases[force_name] or {} + storage.force_bases[force_name][entity.unit_number] = true + local color = color_for(force_name) + entity.color = color + storage.base_label_ids[entity.unit_number] = rendering.draw_text{ + text = force_name, surface = entity.surface, target = entity, target_offset = {0, -3.5}, + color = color, scale = 2, font = "default-bold", alignment = "center", visible = true, + } + notify(force_name, "[color=green][BASE] A base has been established at (" .. math.floor(entity.position.x) + .. ", " .. math.floor(entity.position.y) .. ") on " .. entity.surface.name .. ".[/color]") +end + +function base.spawn_initial(force_name, planet_name) + local planet = game.planets[planet_name] + if not planet then return end + local surface = planet.create_surface() + local position = surface.find_non_colliding_position("market", {8, 0}, 200, 1) or {x = 8, y = 0} + local entity = surface.create_entity{name = "pvp-base", position = position, force = game.forces[force_name], raise_built = true} + if entity and entity.valid then base.register(entity) end +end + +local function destroyed(entity) + local force_name = entity.force.name + if storage.force_bases[force_name] then storage.force_bases[force_name][entity.unit_number] = nil end + local label = storage.base_label_ids[entity.unit_number] + if label and label.valid then label.destroy() end + storage.base_label_ids[entity.unit_number] = nil + local remaining = count(force_name) + if remaining > 0 then + notify(force_name, "[color=orange][BASE] A base was destroyed! You have " .. remaining .. " base(s) remaining. Respawn is still active.[/color]") + else + notify(force_name, "[color=red][BASE] ALL BASES DESTROYED! Any player who dies now is PERMANENTLY ELIMINATED. Rebuild a base core to restore respawning.[/color]") + end +end + +local function spectator(player) + player.set_controller{type = defines.controllers.spectator} + player.print("[color=red]You have been permanently eliminated.[/color]") +end + +function base.on_built_entity(event) + if event.entity and event.entity.valid and event.entity.name == "pvp-base" then base.register(event.entity) end +end + +function base.on_entity_died(event) + if event.entity and event.entity.valid and event.entity.name == "pvp-base" then destroyed(event.entity) end +end + +function base.on_player_died(event) + local player = game.players[event.player_index] + if not (player and player.valid) then return end + if not (storage.player_team_joined and storage.player_team_joined[player.index]) then return end + local force_name = player.force.name + if storage.permadead[force_name] and storage.permadead[force_name][player.index] then return end + if count(force_name) > 0 then + player.print("[color=yellow]You died. Respawning... (Base is alive)[/color]") + else + storage.permadead[force_name] = storage.permadead[force_name] or {} + storage.permadead[force_name][player.index] = true + storage.pending_spectator[player.index] = force_name + player.print("[color=red]You died while your team had no base. You are PERMANENTLY ELIMINATED.[/color]") + end +end + +function base.on_player_respawned(event) + local player = game.players[event.player_index] + if not (player and player.valid) then return end + if not (storage.player_team_joined and storage.player_team_joined[player.index]) then return end + local force_name = player.force.name + if storage.permadead[force_name] and storage.permadead[force_name][player.index] then spectator(player); return end + if count(force_name) == 0 then + storage.permadead[force_name] = storage.permadead[force_name] or {} + storage.permadead[force_name][player.index] = true + spectator(player) + return + end + for planet_name, assigned_force in pairs(storage.planet_teams or {}) do + if assigned_force == force_name then + local planet = game.planets[planet_name] + if planet and planet.surface then drop_zone.teleport_to_planet(player, planet.surface) end + return + end + end +end + +function base.on_pre_player_mined_item(event) + local entity, player = event.entity, game.players[event.player_index] + if entity and entity.valid and entity.name == "pvp-base" and player and player.valid then + player.mining_state = {mining = false} + player.print("[color=red]The base cannot be moved once placed. Destroy it to retrieve the core.[/color]") + end +end + +function base.on_robot_pre_mined(event) + local entity = event.entity + if entity and entity.valid and entity.name == "pvp-base" then entity.cancel_deconstruction("all") end +end + +function base.on_tick(event) + if not storage.crash_site_cleanup_done then + local nauvis = game.planets["nauvis"] and game.planets["nauvis"].surface + if nauvis then + local crash_names = { + "crash-site-spaceship", "crash-site-spaceship-wreck-big-1", "crash-site-spaceship-wreck-big-2", + "crash-site-spaceship-wreck-medium-1", "crash-site-spaceship-wreck-medium-2", "crash-site-spaceship-wreck-medium-3", + "crash-site-spaceship-wreck-small-1", "crash-site-spaceship-wreck-small-2", "crash-site-spaceship-wreck-small-3", + "crash-site-spaceship-wreck-small-4", "crash-site-spaceship-wreck-small-5", "crash-site-spaceship-wreck-small-6", + "crash-site-chest-1", "crash-site-chest-2", + } + for _, name in pairs(crash_names) do + for _, entity in pairs(nauvis.find_entities_filtered{name = name}) do if entity.valid then entity.destroy() end end + end + for _, type in pairs({"fire", "smoke", "explosion", "particle"}) do + for _, entity in pairs(nauvis.find_entities_filtered{type = type, area = {{-200, -200}, {200, 200}}}) do if entity.valid then entity.destroy() end end + end + if event.tick >= 300 then storage.crash_site_cleanup_done = true end + end + end + if event.tick % 10 ~= 0 or not next(storage.pending_spectator) then return end + for index in pairs(storage.pending_spectator) do + local player = game.players[index] + if player and player.valid and not player.character then spectator(player); storage.pending_spectator[index] = nil end + end +end + +function base.status_command(event) + local player = game.players[event.player_index] + if player then for force_name in pairs(storage.force_bases) do player.print(force_name .. ": " .. count(force_name) .. " base(s)") end end +end + +return base diff --git a/control.lua b/control.lua new file mode 100644 index 0000000..01c6bb4 --- /dev/null +++ b/control.lua @@ -0,0 +1,69 @@ +local observer = require("observer") +local radar = require("radar") +local drop_zone = require("drop_zone") +local base = require("base") +local setup = require("setup") +local player_system = require("player") + +-- One registration per Factorio event lets the separate systems share events safely. +script.on_init(function() setup.on_init(observer); player_system.on_init() end) +script.on_configuration_changed(setup.on_configuration_changed) + +script.on_event(defines.events.on_player_created, function(event) + player_system.on_player_created(event) + setup.on_player_created(event) +end) +script.on_event(defines.events.on_player_joined_game, function(event) + player_system.on_player_joined_game(event) + setup.on_player_joined_game(event) +end) +script.on_event(defines.events.on_player_respawned, function(event) + player_system.on_player_respawned(event) + base.on_player_respawned(event) + setup.on_player_respawned(event) +end) +script.on_event(defines.events.on_player_changed_surface, function(event) + player_system.on_player_changed_surface(event) + drop_zone.on_player_changed_surface(event) +end) +script.on_event(defines.events.on_chunk_generated, setup.on_chunk_generated) +script.on_event(defines.events.on_built_entity, function(event) + radar.on_built_entity(event) + base.on_built_entity(event) +end) +script.on_event(defines.events.on_entity_died, base.on_entity_died) +script.on_event(defines.events.on_player_died, base.on_player_died) +script.on_event(defines.events.on_pre_player_mined_item, base.on_pre_player_mined_item) +script.on_event(defines.events.on_robot_pre_mined, base.on_robot_pre_mined) +script.on_nth_tick(1800, radar.on_orbital_scan_tick) +script.on_event(defines.events.on_cargo_pod_finished_ascending, drop_zone.on_cargo_pod_finished_ascending) +script.on_event(defines.events.on_entity_damaged, drop_zone.on_entity_damaged) +script.on_event(defines.events.on_lua_shortcut, drop_zone.on_lua_shortcut) +script.on_event(defines.events.on_player_selected_area, drop_zone.on_player_selected_area) +script.on_event(defines.events.on_gui_click, function(event) + if not observer.on_gui_click(event) then setup.on_gui_click(event) end +end) +script.on_event("planet-pvp-toggle-observer", function(event) + local player = game.get_player(event.player_index) + if player then observer.toggle_window(player) end +end) +script.on_nth_tick(5, observer.on_tick) +script.on_nth_tick(120, player_system.on_light_tick) +script.on_event(defines.events.on_tick, base.on_tick) + +commands.add_command("basestatus", "Show base count for every force", base.status_command) +commands.add_command("ping", nil, function(command) + local player = game.players[command.player_index] + if not (player and player.valid and player.character) then return end + for _, mode in pairs({"game", "chart"}) do + rendering.draw_circle{color = {r = 1, g = 1, b = 0, a = 1}, radius = 3, width = 10, filled = false, target = player.position, surface = player.surface, players = player.force.players, time_to_live = 300, render_mode = mode} + rendering.draw_text{text = "PING", surface = player.surface, target = {position = player.position, offset = {0, -2.5}}, color = {r = 1, g = 1, b = 0, a = 1}, players = player.force.players, alignment = "center", scale = 3, time_to_live = 300, render_mode = mode} + end + for _, target in pairs(player.force.players) do if target.valid then target.print(player.name .. " pinged their location") end end +end) +commands.add_command("observer", "Become an observer", function(command) + local player = game.get_player(command.player_index) + if not player then return end + if not player.admin then player.print("Only admins can become observers."); return end + observer.enable(player) +end) diff --git a/data.lua b/data.lua new file mode 100644 index 0000000..b066481 --- /dev/null +++ b/data.lua @@ -0,0 +1,164 @@ +data:extend({ + { + type = "selection-tool", + name = "drop-zone-selector", + icon = "__base__/graphics/icons/artillery-targeting-remote.png", + icon_size = 64, + flags = {"not-stackable", "only-in-cursor"}, + stack_size = 1, + select = { + border_color = {r = 0, g = 1, b = 0}, + cursor_box_type = "copy", + mode = {"nothing"} + }, + alt_select = { + border_color = {r = 1, g = 0.5, b = 0}, + cursor_box_type = "not-allowed", + mode = {"nothing"} + } + }, + { + type = "shortcut", + name = "lock_drop_zone_shortcut", + action = "lua", + order = "z[spidertron-remote-b]", + localised_name = "Drop Zone Selector", + icon = "__base__/graphics/icons/artillery-targeting-remote.png", + small_icon = "__base__/graphics/icons/artillery-targeting-remote.png" + } +}) + +data:extend({ + { + type = "simple-entity-with-owner", + name = "pvp-base", + icon = "__base__/graphics/icons/market.png", + icon_size = 64, + flags = { "placeable-neutral", "player-creation", "not-rotatable" }, + weight = 1000001, + max_health = 2000, + resistances = { + { type = "fire", decrease = 15, percent = 30 }, + { type = "physical", decrease = 10, percent = 20 }, + { type = "explosion", decrease = 20, percent = 40 }, + { type = "laser", decrease = 5, percent = 20 }, + }, + loot = { + { type = "item", name = "pvp-base-core", amount_min = 1, amount_max = 1, independent_probability = 1 } + }, + collision_box = { {-1.2, -1.2}, {1.2, 1.2} }, + selection_box = { {-1.5, -1.5}, {1.5, 1.5} }, + collision_mask = { layers = { object = true, player = true } }, + picture = { + filename = "__base__/graphics/entity/market/market.png", + priority = "extra-high", + width = 156, + height = 127, + shift = { 1, 0.0001 }, + scale = 1.0, + }, + minable = nil, + map_color = { r = 0.9, g = 0.2, b = 0.2 }, + friendly_map_color = { r = 0.2, g = 0.8, b = 0.2 }, + alert_when_damaged = true, + is_military_target = true, + } +}) + +data:extend({ + { + type = "item", + name = "pvp-base-building", + icon = "__base__/graphics/icons/market.png", + icon_size = 64, + subgroup = "other", + order = "z[pvp-base-building]", + stack_size = 1, + weight = 1000001, + place_result = "pvp-base", + } +}) + +data:extend({ + { + type = "item", + name = "pvp-base-core", + icon = "__Planet-PvP__/Graphics/Base/base_core.png", + icon_size = 128, + subgroup = "other", + order = "z[pvp-base-core]", + stack_size = 1, + weight = 1000001, + } +}) + +data:extend({ + { + type = "recipe", + name = "pvp-base-repair", + enabled = true, + categories = {"crafting"}, + energy_required = 60, + ingredients = { + { type = "item", name = "pvp-base-core", amount = 1 }, + { type = "item", name = "repair-pack", amount = 1 }, + }, + results = { + { type = "item", name = "pvp-base-building", amount = 1 }, + }, + } +}) + +if data.raw["character"] and data.raw["character"]["character"] then + data.raw["character"]["character"].light = { + { + type = "basic", + intensity = 0, + size = 0, + color = {r=0, g=0, b=0} + } + } + data.raw["character"]["character"].flashlight = nil +end + +data:extend({ + { + type = "custom-input", + name = "planet-pvp-toggle-observer", + key_sequence = "O", + consuming = "game-only", + enabled_while_spectating = true + } +}) + +data:extend({ + { + type = "technology", + name = "planet-discovery-nauvis", + icon = "__Planet-PvP__/Graphics/Tech/nauvis-tech.png", + icon_size = 210, + essential = true, + effects = { + { + type = "unlock-space-location", + space_location = "nauvis", + use_icon_overlay_constant = true + }, + { + type = "unlock-travel-to-space-platforms", + modifier = true + } + }, + prerequisites = {"space-platform-thruster"}, + unit = { + count = 1000, + ingredients = { + {"automation-science-pack", 1}, + {"logistic-science-pack", 1}, + {"chemical-science-pack", 1}, + {"space-science-pack", 1} + }, + time = 60 + } + } +}) \ No newline at end of file diff --git a/drop_zone.lua b/drop_zone.lua new file mode 100644 index 0000000..08d8ea0 --- /dev/null +++ b/drop_zone.lua @@ -0,0 +1,81 @@ +local drop_zone = {} + +function drop_zone.teleport_to_planet(player, surface) + local planet_name = surface.name + storage.player_targets = storage.player_targets or {} + local target = storage.player_targets[player.force.name .. "_" .. planet_name] + local position = target and target.position + or surface.find_non_colliding_position("character", {0, 0}, 500, 2) or {x = 0, y = 0} + local attempts = 0 + script.on_nth_tick(1, function() + if player and player.valid and attempts < 30 then + player.teleport(position, surface) + if player.character then player.set_controller{type = defines.controllers.character, character = player.character} end + attempts = attempts + 1 + else + script.on_nth_tick(1, nil) + end + end) +end + +function drop_zone.on_player_changed_surface(event) + local player = game.players[event.player_index] + if not (player and player.valid) then return end + local surface = player.surface + if event.surface_index == surface.index or surface.platform or player.render_mode == defines.render_mode.chart then return end + storage.player_surfaces = storage.player_surfaces or {} + if storage.player_surfaces[player.index] == surface.index then return end + storage.player_surfaces[player.index] = surface.index + if player.controller_type == defines.controllers.character or player.controller_type == defines.controllers.cutscene then + drop_zone.teleport_to_planet(player, surface) + end +end + +function drop_zone.on_cargo_pod_finished_ascending(event) + local pod = event.cargo_pod + if not (pod and pod.valid) or not pod.cargo_pod_destination then return end + local surface = pod.cargo_pod_destination.surface + if not surface or surface.platform then return end + local target = storage.player_targets and storage.player_targets[pod.force.name .. "_" .. surface.name] + if target then pod.cargo_pod_destination = {type = defines.cargo_destination.surface, surface = surface, position = target.position} end +end + +function drop_zone.on_entity_damaged(event) + local entity, cause = event.entity, event.cause + if entity and entity.valid and entity.type == "character" and cause and cause.valid and cause.name == "cargo-pod" and cause.force == entity.force then + entity.health = entity.health + event.final_damage_amount + end +end + +function drop_zone.on_lua_shortcut(event) + if event.prototype_name == "lock_drop_zone_shortcut" then + local player = game.players[event.player_index] + if player and player.cursor_stack then player.cursor_stack.set_stack{name = "drop-zone-selector", count = 1} end + end +end + +function drop_zone.on_player_selected_area(event) + if event.item ~= "drop-zone-selector" then return end + local player, surface = game.players[event.player_index], event.surface + storage.player_targets = storage.player_targets or {} + local pos = {x = (event.area.left_top.x + event.area.right_bottom.x) / 2, y = (event.area.left_top.y + event.area.right_bottom.y) / 2} + if not player.force.is_chunk_charted(surface, {math.floor(pos.x / 32), math.floor(pos.y / 32)}) then + player.print("Cannot lock drop zone: area is not scanned!") + else + for _, jammer in pairs(surface.find_entities_filtered{ + area = {{pos.x - 120, pos.y - 120}, {pos.x + 120, pos.y + 120}}, + type = "radar" + }) do + if jammer.force ~= player.force and jammer.force.name ~= "neutral" then + player.print("Cannot lock drop zone: area is jammed by an enemy!") + if player.cursor_stack then player.cursor_stack.clear() end + return + end + end + storage.player_targets[player.force.name .. "_" .. surface.name] = {position = pos} + player.print("Drop Zone LOCKED for " .. surface.name .. " at [" .. math.floor(pos.x) .. ", " .. math.floor(pos.y) .. "]") + end + if player.cursor_stack then player.cursor_stack.clear() end +end + +return drop_zone diff --git a/info.json b/info.json new file mode 100644 index 0000000..db61ab1 --- /dev/null +++ b/info.json @@ -0,0 +1,11 @@ +{ + "name": "Planet-PvP", + "version": "1.2.1", + "title": "Planet PvP", + "author": "SpRiTz708", + "factorio_version": "2.1", + "dependencies": [ + "base >= 2.0.77", + "space-age"], + "description": "A PvP mod for factorio where you can choose a planet of the first 4. Aquilo will be available in a future version." +} diff --git a/locale/en/locale.cfg b/locale/en/locale.cfg new file mode 100644 index 0000000..4db835e --- /dev/null +++ b/locale/en/locale.cfg @@ -0,0 +1,28 @@ +[entity-name] +pvp-base=Base + +[item-name] +pvp-base-building=Base +pvp-base-core=Base Core + +[recipe-name] +pvp-base-repair=Repair Base + +[technology-name] +planet-discovery-nauvis=Planet discovery Nauvis + +[technology-description] +planet-discovery-nauvis=Chart a course to Nauvis, allowing space platforms to travel there. + +[entity-description] +pvp-base=The team base. As long as it stands your team can respawn. Destroy all enemy bases to eliminate them permanently. + +[item-description] +pvp-base-building=Place this to establish a team base. +pvp-base-core=A destroyed base core. Craft it with repair packs to rebuild the base. + +[recipe-description] +pvp-base-repair=Repair a destroyed base core into a placeable base. + +[controls] +planet-pvp-toggle-observer=Toggle observer window \ No newline at end of file diff --git a/observer.lua b/observer.lua new file mode 100644 index 0000000..a2be213 --- /dev/null +++ b/observer.lua @@ -0,0 +1,570 @@ +local mod_gui = require("__core__.lualib.mod-gui") + +local observer = {} + +local PLANETS = { + "nauvis", + "vulcanus", + "fulgora", + "gleba", + "aquilo" +} + +local function get_data(player) + + storage.observers = storage.observers or {} + + if not storage.observers[player.index] then + storage.observers[player.index] = { + following = nil, + perspective = "observer", + gui_open = false + } + end + + return storage.observers[player.index] + +end + +local function is_observer(player) + return player.controller_type == defines.controllers.spectator +end + +local function create_observer_force() + + local force = game.forces["observer"] + + if not force then + force = game.create_force("observer") + end + + return force + +end + +observer.create_force = create_observer_force + +function observer.create_button(player) + + if not is_observer(player) then + return + end + + local flow = mod_gui.get_button_flow(player) + + if flow.observer_button then + return + end + + flow.add{ + type = "sprite-button", + name = "observer_button", + sprite = "utility/search_icon", + tooltip = "Observer" + } + +end + +function observer.destroy_button(player) + + local flow = mod_gui.get_button_flow(player) + + if flow.observer_button then + flow.observer_button.destroy() + end + + if player.gui.screen.observer_window then + player.gui.screen.observer_window.destroy() + end + +end + +local function build_window(player) + + local data = get_data(player) + local selected_force_name = nil + + if data.perspective + and data.perspective ~= "observer" + and storage.planet_teams then + selected_force_name = storage.planet_teams[data.perspective] + end + + if player.gui.screen.observer_window then + player.gui.screen.observer_window.destroy() + end + + local frame = player.gui.screen.add{ + type = "frame", + name = "observer_window", + direction = "vertical", + caption = "Observer" + } + + frame.auto_center = true + player.opened = frame + + local perspective_header = frame.add{ + type = "flow", + direction = "horizontal" + } + + perspective_header.add{ + type = "sprite", + sprite = "utility/search_icon" + } + + perspective_header.add{ + type = "label", + caption = "Perspective" + } + + local perspective_flow = frame.add{ + type = "flow", + direction = "horizontal" + } + + for _, planet in ipairs(PLANETS) do + perspective_flow.add{ + type = "sprite-button", + name = "observer_perspective_" .. planet, + sprite = "space-location/" .. planet, + tooltip = planet:gsub("^%l", string.upper), + style = "slot_button", + toggled = data.perspective == planet + } + end + + frame.add{ + type = "line" + } + + local perspective_caption = "None selected" + + if selected_force_name then + perspective_caption = selected_force_name + end + + frame.add{ + type = "label", + caption = "Viewing: " .. perspective_caption + } + + frame.add{ + type = "line" + } + + local platform_header = frame.add{ + type = "flow", + direction = "horizontal" + } + + platform_header.add{ + type = "sprite", + sprite = "item/space-platform-foundation" + } + + platform_header.add{ + type = "label", + caption = "Team Platforms" + } + + local platform_scroll = frame.add{ + type = "scroll-pane", + name = "observer_platforms" + } + + platform_scroll.style.maximal_height = 220 + + local platforms_found = 0 + + if selected_force_name then + for _, surface in pairs(game.surfaces) do + local platform = surface.platform + + if platform + and platform.valid + and platform.force + and platform.force.name == selected_force_name then + + local caption = + "[img=item/space-platform-foundation] " + .. platform.name + + if platform.space_location then + caption = caption + .. " (" .. platform.space_location.name .. ")" + elseif platform.space_connection then + caption = caption .. " (traveling)" + end + + platform_scroll.add{ + type = "button", + name = "observer_platform_" .. platform.index, + caption = caption + } + + platforms_found = platforms_found + 1 + end + end + end + + if not selected_force_name then + platform_scroll.add{ + type = "label", + caption = "Select a team perspective." + } + elseif platforms_found == 0 then + platform_scroll.add{ + type = "label", + caption = "No platforms owned by this team." + } + end + + frame.add{ + type = "line" + } + + local player_header = frame.add{ + type = "flow", + direction = "horizontal" + } + + player_header.add{ + type = "sprite", + sprite = "entity/character" + } + + player_header.add{ + type = "label", + caption = "Team Players" + } + + local scroll = frame.add{ + type = "scroll-pane", + name = "observer_players" + } + + scroll.style.maximal_height = 320 + + local players_found = 0 + + for _, target in pairs(game.connected_players) do + + local target_is_observer = + storage.observers + and storage.observers[target.index] ~= nil + + local belongs_to_selected_team = + selected_force_name + and target.force + and target.force.name == selected_force_name + + if belongs_to_selected_team and not target_is_observer then + + local caption = "[img=entity/character] " .. target.name + + if not target.character then + caption = caption .. " (dead)" + elseif target.surface and target.surface.platform then + caption = caption .. " (platform)" + elseif target.surface then + caption = caption .. " (" .. target.surface.name .. ")" + end + + scroll.add{ + type = "button", + name = "observer_follow_" .. target.index, + caption = caption + } + + players_found = players_found + 1 + end + end + + if not selected_force_name then + scroll.add{ + type = "label", + caption = "Select a team perspective." + } + elseif players_found == 0 then + scroll.add{ + type = "label", + caption = "No connected non-observer players on this team." + } + end + + frame.add{ + type = "line" + } + + frame.add{ + type = "button", + name = "observer_stop_follow", + caption = "[img=utility/close] Stop Following" + } + + data.gui_open = true +end + +function observer.toggle_window(player) + + if not player or not player.valid then + return + end + + if not is_observer(player) then + return + end + + local data = get_data(player) + local window = player.gui.screen.observer_window + + if window and window.valid then + + if player.opened == window then + player.opened = nil + end + + window.destroy() + data.gui_open = false + else + build_window(player) + end +end + +function observer.on_gui_click(event) + + local player = game.get_player(event.player_index) + if not player then + return false + end + + if not is_observer(player) then + return false + end + + local element = event.element + if not (element and element.valid) then + return false + end + + local data = get_data(player) + + if element.name == "observer_button" then + + if player.gui.screen.observer_window then + player.gui.screen.observer_window.destroy() + data.gui_open = false + else + build_window(player) + end + + return true + end + +if element.name:find("^observer_perspective_") then + + local perspective = + element.name:gsub("^observer_perspective_", "") + + local force_name = + storage.planet_teams + and storage.planet_teams[perspective] + + local force = + force_name + and game.forces[force_name] + + if force then + player.force = force + data.perspective = perspective + data.following = nil + + local surface = game.surfaces[perspective] + + if surface then + player.teleport({0, 0}, surface) + end + + player.print("Perspective: " .. force.name) + + build_window(player) + else + player.print( + "Force not found for perspective: " + .. tostring(perspective) + ) + end + + return true +end + + if element.name:find("^observer_follow_") then + + local id = tonumber(element.name:match("%d+")) + + if id then + local target = game.get_player(id) + + if target and target.valid and target.character and target.character.valid then + data.following = id + player.centered_on = target.character + player.print("Now following " .. target.name) + else + player.print("That player is not currently available to follow.") + end + end + + return true + end + + if element.name:find("^observer_platform_") then + + local platform_index = + tonumber(element.name:match("%d+")) + + local selected_platform = nil + + if platform_index then + for _, surface in pairs(game.surfaces) do + local platform = surface.platform + + if platform + and platform.valid + and platform.index == platform_index then + selected_platform = platform + break + end + end + end + + if selected_platform + and selected_platform.surface + and selected_platform.surface.valid then + + data.following = nil + + player.teleport( + {0, 0}, + selected_platform.surface + ) + + player.print( + "Viewing platform: " + .. selected_platform.name + ) + else + player.print("Platform is no longer available.") + build_window(player) + end + + return true + end + + if element.name == "observer_stop_follow" then + + data.following = nil + player.print("Stopped following.") + + return true + end +end + +function observer.on_tick(event) + + if not storage.observers then + return + end + + for player_index, data in pairs(storage.observers) do + + local observer_player = game.get_player(player_index) + + if observer_player + and observer_player.valid + and observer_player.controller_type == defines.controllers.spectator + and data.following then + + local target = game.get_player(data.following) + + if target + and target.valid + and target.connected + and target.character then + + if observer_player.surface ~= target.surface then + + observer_player.teleport( + target.position, + target.surface + ) + + else + + local dx = observer_player.position.x - target.position.x + local dy = observer_player.position.y - target.position.y + + if math.abs(dx) > 2 + or math.abs(dy) > 2 then + + observer_player.teleport( + target.position, + target.surface + ) + + end + + end + + else + + data.following = nil + + observer_player.print("Target unavailable.") + + end + + end + + end + +end + +function observer.enable(player) + + local force = create_observer_force() + + player.force = force + + player.set_controller{ + type = defines.controllers.spectator +} + + player.print("Perspective: " .. force.name) + + player.show_on_map = false + + player.set_controller{ + type = defines.controllers.spectator + } + + get_data(player) + + observer.create_button(player) + +end + +function observer.disable(player) + + observer.destroy_button(player) + + if storage.observers then + storage.observers[player.index] = nil + end + +end + +-------------------------------------------------------------------------------- + +return observer diff --git a/player.lua b/player.lua new file mode 100644 index 0000000..3529c13 --- /dev/null +++ b/player.lua @@ -0,0 +1,50 @@ +local player_system = {} + +local function hide_dot(player) + if player and player.valid then player.show_on_map = false end +end + +local function maintain_light(player) + if not (player and player.valid) then return end + storage.player_lights = storage.player_lights or {} + local light = storage.player_lights[player.index] + if not player.character or not player.character.valid then + if light and light.valid then light.destroy() end + storage.player_lights[player.index] = nil + return + end + if not light or not light.valid then + storage.player_lights[player.index] = rendering.draw_light{sprite = "utility/light_medium", target = player.character, surface = player.character.surface, players = {player}, scale = 1.2, intensity = .8, minimum_darkness = .2} + end +end + +function player_system.on_init() + storage.player_lights = storage.player_lights or {} +end + +function player_system.on_player_created(event) + local player = game.get_player(event.player_index) + hide_dot(player); maintain_light(player) +end + +function player_system.on_player_joined_game(event) hide_dot(game.get_player(event.player_index)) end +function player_system.on_player_respawned(event) hide_dot(game.get_player(event.player_index)); maintain_light(game.get_player(event.player_index)) end + +function player_system.on_player_changed_surface(event) + local player = game.get_player(event.player_index) + if player and storage.player_lights[player.index] then + local light = storage.player_lights[player.index] + if light and light.valid then light.destroy() end + storage.player_lights[player.index] = nil + end + maintain_light(player) +end + +function player_system.on_light_tick() + for _, player in pairs(game.connected_players) do + maintain_light(player) + if player.character and player.character.valid then player.character.disable_flashlight() end + end +end + +return player_system diff --git a/radar.lua b/radar.lua new file mode 100644 index 0000000..e71980a --- /dev/null +++ b/radar.lua @@ -0,0 +1,102 @@ +local radar = {} + +local GRID_SIZE = 16 +local MAX_RADIUS = 3200 + +local function scan_orbit(entity, planet_name) + local surface = game.planets[planet_name] and game.planets[planet_name].surface + if not surface then return end + + storage.grid_pos = storage.grid_pos or {} + local scan_key = entity.force.name .. "_" .. planet_name + if not storage.grid_pos[scan_key] then + storage.grid_pos[scan_key] = {x = 0, y = 0, d = 0, step_limit = 1, steps_taken = 0, iteration = 0} + end + + local state = storage.grid_pos[scan_key] + local position = {x = state.x, y = state.y} + local blocked = false + for _, jammer in pairs(surface.find_entities_filtered{ + area = {{position.x - 120, position.y - 120}, {position.x + 120, position.y + 120}}, + type = "radar" + }) do + if jammer.force ~= entity.force and jammer.force.name ~= "neutral" then + blocked = true + break + end + end + + local scan_range = 1 + if not blocked then + surface.request_to_generate_chunks(position, scan_range) + entity.force.chart(surface, { + {position.x - (GRID_SIZE * scan_range), position.y - (GRID_SIZE * scan_range)}, + {position.x + (GRID_SIZE * scan_range), position.y + (GRID_SIZE * scan_range)} + }) + end + + if state.d == 0 then state.x = state.x + GRID_SIZE * scan_range * 2 + elseif state.d == 1 then state.y = state.y + GRID_SIZE * scan_range * 2 + elseif state.d == 2 then state.x = state.x - GRID_SIZE * scan_range * 2 + else state.y = state.y - GRID_SIZE * scan_range * 2 end + + state.steps_taken = state.steps_taken + 1 + if state.steps_taken >= state.step_limit then + state.steps_taken = 0 + state.d = (state.d + 1) % 4 + state.iteration = state.iteration + 1 + if state.iteration % 2 == 0 then state.step_limit = state.step_limit + 1 end + end + if math.abs(state.x) > MAX_RADIUS or math.abs(state.y) > MAX_RADIUS then + storage.grid_pos[scan_key] = {x = 0, y = 0, d = 0, step_limit = 1, steps_taken = 0, iteration = 0} + end +end + +function radar.on_orbital_scan_tick() + for _, surface in pairs(game.surfaces) do + local platform = surface.platform + local location = platform and platform.space_location + if location and game.planets[location.name] then + for _, entity in pairs(surface.find_entities_filtered{type = "radar"}) do + if entity.valid and entity.active then + scan_orbit(entity, location.name) + end + end + end + end +end + +function radar.on_built_entity(event) + local entity = event.entity + if not (entity and entity.valid) or entity.type ~= "radar" or entity.surface.platform then return end + + local jammer_force, jammer_pos, jammer_radius = entity.force, entity.position, 120 + for _, enemy_force in pairs(game.forces) do + if enemy_force.name ~= jammer_force.name and enemy_force.name ~= "neutral" then + local min_x = math.floor((jammer_pos.x - jammer_radius) / 32) + local min_y = math.floor((jammer_pos.y - jammer_radius) / 32) + local max_x = math.ceil((jammer_pos.x + jammer_radius) / 32) + local max_y = math.ceil((jammer_pos.y + jammer_radius) / 32) + for x = min_x, max_x do + for y = min_y, max_y do enemy_force.unchart_chunk({x = x, y = y}, entity.surface) end + end + + storage.player_targets = storage.player_targets or {} + local key = enemy_force.name .. "_" .. entity.surface.name + local target = storage.player_targets[key] + if target then + local dx, dy = math.abs(target.position.x - jammer_pos.x), math.abs(target.position.y - jammer_pos.y) + -- Jam the same reach on each axis, rather than a circular radius. + if dx <= jammer_radius and dy <= jammer_radius then + storage.player_targets[key] = nil + local message = "[color=red][JAMMER] Your drop zone at [" .. math.floor(target.position.x) + .. ", " .. math.floor(target.position.y) .. "] on " .. entity.surface.name + .. " has been JAMMED by " .. jammer_force.name .. "![/color]" + for _, player in pairs(enemy_force.players) do if player.valid then player.print(message) end end + end + end + end + end +end + +return radar diff --git a/setup.lua b/setup.lua new file mode 100644 index 0000000..3faf050 --- /dev/null +++ b/setup.lua @@ -0,0 +1,298 @@ +local base = require("base") +local drop_zone = require("drop_zone") +local setup = {} + +local PLANETS = {"nauvis", "vulcanus", "fulgora", "gleba"} +local HUB_SURFACE_NAME = "planet-pvp-hub" +local HUB_MIN = -50 +local HUB_MAX = 49 +local HUB_VOID_MIN = -80 +local HUB_VOID_MAX = 79 +local HUB_LAYOUT_VERSION = 2 +local LOADOUTS = { + nauvis = {{name = "burner-mining-drill", count = 1}, {name = "stone-furnace", count = 1}, {name = "iron-plate", count = 8}}, + vulcanus = {{name = "burner-mining-drill", count = 1}, {name = "stone-furnace", count = 1}, {name = "iron-plate", count = 8}}, + fulgora = {{name = "burner-mining-drill", count = 1}, {name = "stone-furnace", count = 1}, {name = "iron-plate", count = 8}}, + gleba = {{name = "burner-mining-drill", count = 1}, {name = "stone-furnace", count = 1}, {name = "iron-plate", count = 8}}, +} +local UNIVERSAL = { ["automation-science-pack"] = true, ["logistic-science-pack"] = true, ["military-science-pack"] = true, ["chemical-science-pack"] = true, ["production-science-pack"] = true, ["utility-science-pack"] = true, ["space-science-pack"] = true } +local TRIGGERS = { + ["planet-discovery-nauvis"] = "nauvis", ["planet-discovery-vulcanus"] = "vulcanus", ["planet-discovery-fulgora"] = "fulgora", ["planet-discovery-gleba"] = "gleba", ["planet-discovery-aquilo"] = "aquilo", + ["uranium-mining"] = "nauvis", ["bitter-egg-handling"] = "nauvis", ["recycling"] = "fulgora", ["holmium-processing"] = "fulgora", ["electromagnetic-plant"] = "fulgora", ["electromagnetic-science-pack"] = "fulgora", + ["jellynut"] = "gleba", ["yumako"] = "gleba", ["heating-tower"] = "gleba", ["agriculture"] = "gleba", ["biochamber"] = "gleba", ["artificial-soil"] = "gleba", ["bioflux"] = "gleba", ["bacteria-cultivation"] = "gleba", ["bioflux-processing"] = "gleba", ["agricultural-science-pack"] = "gleba", + ["tungsten-carbide"] = "vulcanus", ["calcite-processing"] = "vulcanus", ["foundry"] = "vulcanus", ["big-mining-drill"] = "vulcanus", ["tungsten-steel"] = "vulcanus", ["metallurgic-science-pack"] = "vulcanus", + ["lithium-processing"] = "aquilo", ["cryogenic-plant"] = "aquilo", ["cryogenic-science-pack"] = "aquilo", +} +local BASIC = {["steam-power"] = true, ["electronics"] = true, ["automation-science-pack"] = true, ["steel-axe"] = true, ["oil-processing"] = true} + +local function protect_hub_walls(surface) + for _, wall in pairs(surface.find_entities_filtered{name = "stone-wall", area = {{HUB_MIN - 1, HUB_MIN - 1}, {HUB_MAX + 1, HUB_MAX + 1}}}) do + wall.destructible = false + wall.minable_flag = false + end +end + +local function build_hub_layout(surface) + surface.request_to_generate_chunks({0, 0}, 2) + surface.force_generate_chunk_requests() + + local tiles = {} + for x = HUB_VOID_MIN, HUB_VOID_MAX do + for y = HUB_VOID_MIN, HUB_VOID_MAX do + tiles[#tiles + 1] = {name = "out-of-map", position = {x, y}} + end + end + for x = HUB_MIN, HUB_MAX do + for y = HUB_MIN, HUB_MAX do + tiles[#tiles + 1] = {name = "refined-concrete", position = {x, y}} + end + end + surface.set_tiles(tiles, true) + + -- Replace any older lobby border before placing the updated perimeter. + for _, wall in pairs(surface.find_entities_filtered{name = "stone-wall", area = {{HUB_VOID_MIN, HUB_VOID_MIN}, {HUB_VOID_MAX + 1, HUB_VOID_MAX + 1}}}) do + wall.destroy() + end + local function create_wall(position) + local wall = surface.create_entity{name = "stone-wall", position = position, force = game.forces.player} + if wall then wall.destructible = false; wall.minable_flag = false end + end + for x = HUB_MIN, HUB_MAX do + create_wall({x, HUB_MIN}) + create_wall({x, HUB_MAX}) + end + for y = HUB_MIN + 1, HUB_MAX - 1 do + create_wall({HUB_MIN, y}) + create_wall({HUB_MAX, y}) + end + storage.hub_layout_version = HUB_LAYOUT_VERSION +end + +local function get_hub_surface() + local surface = game.surfaces[HUB_SURFACE_NAME] + if not surface then + surface = game.create_surface(HUB_SURFACE_NAME, { + seed = 7082026, + peaceful_mode = true, + autoplace_controls = {}, + autoplace_settings = { + entity = {treat_missing_as_default = false, settings = {}}, + tile = {treat_missing_as_default = false, settings = {}}, + }, + }) + end + if storage.hub_layout_version ~= HUB_LAYOUT_VERSION then + build_hub_layout(surface) + else + protect_hub_walls(surface) + end + return surface +end + +local function teleport_to_hub(player) + if not (player and player.valid) or player.controller_type == defines.controllers.spectator then return end + local surface = get_hub_surface() + if player.controller_type == defines.controllers.cutscene then player.exit_cutscene() end + local position = surface.find_non_colliding_position("character", {0, 0}, 48, 1) or {0, 0} + player.teleport(position, surface) + storage.player_surfaces[player.index] = surface.index +end + +local function has_selected_team(player) + if storage.player_team_joined and storage.player_team_joined[player.index] then return true end + for _, force_name in pairs(storage.planet_teams or {}) do + if player.force and player.force.name == force_name then + storage.player_team_joined = storage.player_team_joined or {} + storage.player_team_joined[player.index] = true + return true + end + end + return false +end + +local function assign_fulgora_attractors(surface, area) + local force_name = storage.planet_teams and storage.planet_teams.fulgora + local force = force_name and game.forces[force_name] + if not (surface and surface.valid and force) then return end + for _, attractor in pairs(surface.find_entities_filtered{ + name = "fulgoran-ruin-attractor", + area = area, + }) do + attractor.force = force + end +end + +local function set_pvp_relations() + for i, a_name in ipairs(PLANETS) do + local a = game.forces[a_name] + if a then for j, b_name in ipairs(PLANETS) do + local b = game.forces[b_name] + if i ~= j and b then a.set_friend(b, false); b.set_friend(a, false); a.set_cease_fire(b, false); b.set_cease_fire(a, false) end + end end + end +end + +local function requirement(tech) + local found + for _, ingredient in pairs(tech.research_unit_ingredients) do + if not UNIVERSAL[ingredient.name] then if found and found ~= ingredient.name then return "mixed" end; found = ingredient.name end + end + return found or "base" +end + +local function unlock(force, planet_name) + force.unlock_space_location(planet_name) + for _, tech in pairs(force.technologies) do + if tech.enabled then + local assigned = TRIGGERS[tech.name] + local unlock_tech = BASIC[tech.name] or (requirement(tech) == "base" and not tech.prototype.upgrade) + if assigned then unlock_tech = assigned == planet_name and not tech.prototype.upgrade end + if tech.name == "metallurgic-science-pack" or tech.name == "electromagnetic-science-pack" or tech.name == "agricultural-science-pack" or tech.name == "cryogenic-science-pack" then unlock_tech = false end + if (tech.name:find("productivity%-module") or tech.name:find("speed%-module") or tech.name:find("efficiency%-module") or tech.name:find("quality%-module")) and tech.prototype.max_level < 3 then unlock_tech = true end + tech.researched = unlock_tech + end + end +end + +local function show_admin(player) + if player.gui.screen.pvp_admin_frame then player.gui.screen.pvp_admin_frame.destroy() end + local frame = player.gui.screen.add{type = "frame", name = "pvp_admin_frame", caption = "ADMIN: CONFIGURE PLANET TEAMS", direction = "vertical"} + frame.auto_center = true + local table_ui = frame.add{type = "table", column_count = 2, name = "setup_table"} + for _, planet in pairs(PLANETS) do + table_ui.add{type = "label", caption = planet:gsub("^%l", string.upper) .. " Force Name:"} + table_ui.add{type = "textfield", name = "tf_" .. planet, text = "Team " .. planet:gsub("^%l", string.upper)} + end + frame.add{type = "button", name = "confirm_admin_setup", caption = "INITIALIZE WORLD", style = "confirm_button"} +end + +local function show_join(player) + if player.gui.screen.pvp_join_frame then player.gui.screen.pvp_join_frame.destroy() end + if not storage.game_started or not storage.planet_teams then return end + local frame = player.gui.screen.add{type = "frame", name = "pvp_join_frame", caption = "CHOOSE YOUR HOME PLANET", direction = "vertical"} + frame.auto_center = true + for planet, force_name in pairs(storage.planet_teams) do frame.add{type = "button", name = "join_btn_" .. planet, caption = "Join " .. force_name .. " (" .. planet .. ")", style = "confirm_button"} end +end + +local function show_waiting(player) + if player.gui.screen.pvp_waiting_frame then player.gui.screen.pvp_waiting_frame.destroy() end + local frame = player.gui.screen.add{type = "frame", name = "pvp_waiting_frame", caption = "PLANET PvP LOBBY", direction = "vertical"} + frame.auto_center = true + frame.add{type = "label", caption = "The admin is setting up the game. You can choose a team when setup is complete."} +end + +function setup.on_init(observer) + storage.planet_teams, storage.player_targets, storage.grid_pos = {}, {}, {} + storage.game_started, storage.team_kit_spawned, storage.player_surfaces = false, {}, {} + storage.player_team_joined = {} + storage.force_bases, storage.permadead, storage.pending_spectator, storage.force_colors, storage.base_label_ids = {}, {}, {}, {}, {} + storage.player_lights, storage.crash_site_cleanup_done = {}, false + observer.create_force() + set_pvp_relations() + get_hub_surface() + for _, player in pairs(game.players) do player.set_shortcut_available("lock_drop_zone_shortcut", true) end +end + +function setup.on_configuration_changed() + for _, key in pairs({"team_kit_spawned", "player_surfaces", "force_bases", "permadead", "pending_spectator", "force_colors", "base_label_ids", "player_targets", "grid_pos", "player_lights", "player_team_joined"}) do storage[key] = storage[key] or {} end + get_hub_surface() + for planet_name, force_name in pairs(storage.planet_teams or {}) do + local force = game.forces[force_name] + if force then + force.unlock_space_location(planet_name) + local tech = force.technologies["planet-discovery-nauvis"] + if tech and planet_name == "nauvis" then tech.researched = true end + end + end + local fulgora = game.planets.fulgora + if fulgora and fulgora.surface then assign_fulgora_attractors(fulgora.surface) end + for _, player in pairs(game.players) do player.set_shortcut_available("lock_drop_zone_shortcut", true) end + for _, player in pairs(game.connected_players) do + if not has_selected_team(player) then + teleport_to_hub(player) + if storage.game_started then show_join(player) elseif player.admin then show_admin(player) else show_waiting(player) end + end + end +end + +function setup.on_chunk_generated(event) + if event.surface and event.surface.name == "fulgora" then + assign_fulgora_attractors(event.surface, event.area) + end +end + +function setup.on_player_created(event) + local player = game.players[event.player_index] + storage.player_team_joined = storage.player_team_joined or {} + player.set_shortcut_available("lock_drop_zone_shortcut", true) + if player.controller_type == defines.controllers.cutscene then player.exit_cutscene() end + teleport_to_hub(player) + if storage.game_started then show_join(player) elseif player.admin then show_admin(player) else show_waiting(player) end +end + +function setup.on_player_joined_game(event) + local player = game.players[event.player_index] + if not (player and player.valid) then return end + storage.player_team_joined = storage.player_team_joined or {} + if has_selected_team(player) or (storage.observers and storage.observers[player.index]) then return end + teleport_to_hub(player) + if storage.game_started then show_join(player) elseif player.admin then show_admin(player) else show_waiting(player) end +end + +function setup.on_player_respawned(event) + local player = game.players[event.player_index] + if player and player.valid and not has_selected_team(player) then + teleport_to_hub(player) + end +end + +function setup.on_gui_click(event) + local element, player = event.element, game.players[event.player_index] + if not (element and element.valid and player and player.valid) then return end + if element.name == "confirm_admin_setup" then + storage.planet_teams = {} + for _, planet in pairs(PLANETS) do + local textfield = element.parent.setup_table["tf_" .. planet] + local name = textfield and textfield.text ~= "" and textfield.text or "Team " .. planet + if not game.forces[name] then game.create_force(name) end + unlock(game.forces[name], planet); base.assign_force_color(name); storage.planet_teams[planet] = name + end + local fulgora = game.planets.fulgora + if fulgora and fulgora.surface then assign_fulgora_attractors(fulgora.surface) end + storage.game_started = true; element.parent.destroy() + for _, target in pairs(game.players) do + if target.valid and not (storage.player_team_joined and storage.player_team_joined[target.index]) then + teleport_to_hub(target) + if target.gui.screen.pvp_waiting_frame then target.gui.screen.pvp_waiting_frame.destroy() end + show_join(target) + end + end + elseif element.name:find("^join_btn_") then + local planet_name = element.name:gsub("^join_btn_", "") + local force_name = storage.planet_teams[planet_name] + if force_name then + player.force = game.forces[force_name]; player.clear_items_inside() + storage.player_team_joined = storage.player_team_joined or {} + storage.player_team_joined[player.index] = true + local planet = game.planets[planet_name] + if planet then + local surface = planet.create_surface() + if planet_name == "fulgora" then assign_fulgora_attractors(surface) end + if not storage.team_kit_spawned[force_name] then + storage.team_kit_spawned[force_name] = true + local position = surface.find_non_colliding_position("steel-chest", {x = 5, y = 0}, 100, .5) or {x = 5, y = 0} + local chest = surface.create_entity{name = "steel-chest", position = position, force = player.force, raise_built = true} + if chest and chest.valid then + local inventory = chest.get_inventory(defines.inventory.chest) + if inventory then for _, item in pairs(LOADOUTS[planet_name]) do if prototypes.item[item.name] then inventory.insert(item) end end end + end + base.spawn_initial(force_name, planet_name) + end + drop_zone.teleport_to_planet(player, surface) + end + end + if element.parent and element.parent.valid then element.parent.destroy() end + end +end + +return setup diff --git a/thumbnail.png b/thumbnail.png new file mode 100644 index 0000000..e9f7464 Binary files /dev/null and b/thumbnail.png differ