First commit

This commit is contained in:
William Lewis 2026-09-30 21:13:44 -05:00
parent e6d1ac54c9
commit 59aa6514dc
14 changed files with 1543 additions and 0 deletions

5
.luarc.json Normal file
View File

@ -0,0 +1,5 @@
{
"diagnostics.globals": [
"data"
]
}

BIN
Graphics/Base/base_core.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

165
base.lua Normal file
View File

@ -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

69
control.lua Normal file
View File

@ -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)

164
data.lua Normal file
View File

@ -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
}
}
})

81
drop_zone.lua Normal file
View File

@ -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

11
info.json Normal file
View File

@ -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."
}

28
locale/en/locale.cfg Normal file
View File

@ -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

570
observer.lua Normal file
View File

@ -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

50
player.lua Normal file
View File

@ -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

102
radar.lua Normal file
View File

@ -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

298
setup.lua Normal file
View File

@ -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

BIN
thumbnail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB