diff --git a/base.lua b/base.lua index 7321fe0..af431f7 100644 --- a/base.lua +++ b/base.lua @@ -29,7 +29,20 @@ function base.assign_force_color(force_name) storage.force_colors[force_name] = COLORS[(used % #COLORS) + 1] end -function base.register(entity) +function base.player_color(player) + if not (player and player.valid and player.force) then return end + local force_name = player.force.name + local is_team = false + for _, name in pairs(storage.planet_teams or {}) do + if name == force_name then is_team = true; break end + end + if not is_team then return end + storage.force_colors = storage.force_colors or {} + base.assign_force_color(force_name) + player.color = storage.force_colors[force_name] +end + +function base.register(entity, silent) 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 {} @@ -40,8 +53,10 @@ function base.register(entity) 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]") + if not silent then + 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 end function base.spawn_initial(force_name, planet_name) @@ -99,6 +114,7 @@ end function base.on_player_respawned(event) local player = game.players[event.player_index] if not (player and player.valid) then return end + base.player_color(player) 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 diff --git a/control.lua b/control.lua index 307c7f2..d6d7fc4 100644 --- a/control.lua +++ b/control.lua @@ -4,17 +4,27 @@ local drop_zone = require("drop_zone") local base = require("base") local setup = require("setup") local player_system = require("player") +local roster = require("roster") -- 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_configuration_changed(function(event) + setup.on_configuration_changed(event) + for _, player in pairs(game.players) do + roster.create_button(player) + base.player_color(player) + end +end) script.on_event(defines.events.on_player_created, function(event) player_system.on_player_created(event) + roster.create_button(game.get_player(event.player_index)) setup.on_player_created(event) end) script.on_event(defines.events.on_player_joined_game, function(event) player_system.on_player_joined_game(event) + roster.create_button(game.get_player(event.player_index)) + base.player_color(game.get_player(event.player_index)) setup.on_player_joined_game(event) end) script.on_event(defines.events.on_player_respawned, function(event) @@ -38,25 +48,34 @@ 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) --- Fires once on the first tick after any save load, then unregisters itself. -script.on_nth_tick(1, function() - script.on_nth_tick(1, nil) - setup.on_save_loaded() -end) +-- No writable "save loaded" event exists; a local flag on the permanent on_tick +-- handler fires once per load. on_nth_tick(1) cannot be used: registering a +-- transient handler makes join-time event checks fail for multiplayer clients. 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 + if not (observer.on_gui_click(event) or roster.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) +script.on_nth_tick(120, function() + player_system.on_light_tick() + for _, player in pairs(game.connected_players) do base.player_color(player) end +end) +local save_loaded_fired = false +script.on_event(defines.events.on_tick, function(event) + if not save_loaded_fired then + save_loaded_fired = true + setup.on_save_loaded() + end + base.on_tick(event) + drop_zone.on_tick(event) +end) commands.add_command("basestatus", "Show base count for every force", base.status_command) commands.add_command("ping", nil, function(command) diff --git a/drop_zone.lua b/drop_zone.lua index e1dc546..37b4054 100644 --- a/drop_zone.lua +++ b/drop_zone.lua @@ -8,16 +8,26 @@ function drop_zone.teleport_to_planet(player, surface) 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) + -- Retries live in storage and are pumped from the permanent on_tick; + -- transient on_nth_tick registrations break multiplayer join checks. + storage.pending_teleports = storage.pending_teleports or {} + storage.pending_teleports[player.index] = {position = position, surface_index = surface.index, attempts = 0} +end + +function drop_zone.on_tick() + local pending = storage.pending_teleports + if not pending or not next(pending) then return end + for index, data in pairs(pending) do + local player = game.players[index] + local surface = game.surfaces[data.surface_index] + if player and player.valid and surface and data.attempts < 30 then + player.teleport(data.position, surface) if player.character then player.set_controller{type = defines.controllers.character, character = player.character} end - attempts = attempts + 1 + data.attempts = data.attempts + 1 else - script.on_nth_tick(1, nil) + pending[index] = nil end - end) + end end function drop_zone.on_player_changed_surface(event) diff --git a/info.json b/info.json index 4044676..8ce4ac4 100644 --- a/info.json +++ b/info.json @@ -1,11 +1,11 @@ { "name": "Planet-PvP-Green", - "version": "1.0.0", + "version": "1.0.1", "title": "Planet PvP Green", "author": "Blizliam", "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." + "description": "A PvP mod for factorio where you can choose a planet of the first 4." } diff --git a/player.lua b/player.lua index 3529c13..ddb9b25 100644 --- a/player.lua +++ b/player.lua @@ -18,6 +18,88 @@ local function maintain_light(player) end end +-- show_on_map is all-or-nothing, so allies are revealed with rendering objects +-- whose players list is restricted to the owner's force instead. +-- Name over the character in-world and on the map, plus a dot on the map. +-- Each is separate because render_mode is per-object. +local ALLY_MARKER_SPECS = { + {key = "name_game", mode = "game", text = true}, + {key = "name_chart", mode = "chart", text = true}, + {key = "dot_chart", mode = "chart", radius = 2}, +} + +local function is_team_force(force_name) + for _, name in pairs(storage.planet_teams or {}) do + if name == force_name then return true end + end + return false +end + +local function maintain_marker(player) + if not (player and player.valid) then return end + storage.ally_markers = storage.ally_markers or {} + local markers = storage.ally_markers[player.index] + local viewers = {} + if player.character and player.character.valid and player.force and is_team_force(player.force.name) then + for _, ally in pairs(player.force.players) do + viewers[#viewers + 1] = ally.index + end + end + if #viewers == 0 then + if markers then for _, marker in pairs(markers) do if marker.valid then marker.destroy() end end end + storage.ally_markers[player.index] = nil + return + end + local color = (storage.force_colors and storage.force_colors[player.force.name]) or {r = 1, g = 1, b = 1, a = 1} + markers = markers or {} + for _, spec in pairs(ALLY_MARKER_SPECS) do + local marker = markers[spec.key] + if marker and marker.valid and marker.surface ~= player.character.surface then + marker.destroy() + marker = nil + markers[spec.key] = nil + end + if not (marker and marker.valid) then + if spec.text then + marker = rendering.draw_text{ + text = player.name, surface = player.character.surface, target = player.character, + target_offset = {0, -2.5}, color = color, scale = 1.5, render_mode = spec.mode, players = viewers, + } + else + marker = rendering.draw_circle{ + surface = player.character.surface, target = player.character, color = color, + radius = spec.radius, filled = true, render_mode = spec.mode, players = viewers, + } + end + markers[spec.key] = marker + else + marker.players = viewers + marker.target = player.character + marker.color = color + end + end + storage.ally_markers[player.index] = markers +end + +-- Vanilla pins can bind to a player or their character, leaking a live +-- tracker arrow that defeats show_on_map = false. Drop any pin that targets +-- a player/character of another force. +local function clear_enemy_pins(player) + for _, pin in pairs(player.get_pins()) do + local pinned = pin.player + local leak = pinned and pinned.valid and pinned.force ~= player.force + if not leak then + for _, target in pairs(pin.targets or {}) do + if target.valid and target.type == "character" and target.force ~= player.force then + leak = true + break + end + end + end + if leak and pin.valid then pin.destroy() end + end +end + function player_system.on_init() storage.player_lights = storage.player_lights or {} end @@ -43,6 +125,8 @@ end function player_system.on_light_tick() for _, player in pairs(game.connected_players) do maintain_light(player) + maintain_marker(player) + clear_enemy_pins(player) if player.character and player.character.valid then player.character.disable_flashlight() end end end diff --git a/roster.lua b/roster.lua new file mode 100644 index 0000000..8992888 --- /dev/null +++ b/roster.lua @@ -0,0 +1,80 @@ +local mod_gui = require("__core__.lualib.mod-gui") + +local roster = {} + +local PLANETS = {"nauvis", "vulcanus", "fulgora", "gleba"} + +local function format_playtime(ticks) + local total_seconds = math.floor((ticks or 0) / 60) + local hours = math.floor(total_seconds / 3600) + local minutes = math.floor((total_seconds % 3600) / 60) + return string.format("%d:%02d", hours, minutes) +end + +function roster.create_button(player) + if not (player and player.valid) then return end + local flow = mod_gui.get_button_flow(player) + if flow.pvp_roster_button then return end + flow.add{ + type = "sprite-button", + name = "pvp_roster_button", + sprite = "entity/character", + tooltip = "Team roster", + } +end + +local function build_window(player) + if player.gui.screen.pvp_roster_frame then player.gui.screen.pvp_roster_frame.destroy() end + local frame = player.gui.screen.add{type = "frame", name = "pvp_roster_frame", caption = "TEAM ROSTER", direction = "vertical"} + frame.auto_center = true + local any_team = false + for _, planet in ipairs(PLANETS) do + local force_name = storage.planet_teams and storage.planet_teams[planet] + if force_name then + any_team = true + local color = storage.force_colors and storage.force_colors[force_name] + local header = frame.add{type = "label", caption = force_name .. " (" .. planet .. ")", style = "caption_label"} + if color then header.style.font_color = color end + local found = false + for _, target in pairs(game.players) do + if target.force and target.force.name == force_name then + found = true + local status = target.connected and "" or " (offline)" + local line = frame.add{type = "label", caption = " " .. target.name .. status .. " - " .. format_playtime(target.online_time)} + if color then line.style.font_color = color end + end + end + if not found then frame.add{type = "label", caption = " (no players)"} end + end + end + if not any_team then frame.add{type = "label", caption = "No teams initialized yet."} end + local buttons = frame.add{type = "flow", direction = "horizontal"} + buttons.add{type = "button", name = "pvp_roster_refresh", caption = "Refresh"} + buttons.add{type = "button", name = "pvp_roster_close", caption = "Close"} +end + +function roster.on_gui_click(event) + local element = event.element + if not (element and element.valid) then return false end + local player = game.get_player(event.player_index) + if not (player and player.valid) then return false end + if element.name == "pvp_roster_button" then + if player.gui.screen.pvp_roster_frame then + player.gui.screen.pvp_roster_frame.destroy() + else + build_window(player) + end + return true + end + if element.name == "pvp_roster_refresh" then + build_window(player) + return true + end + if element.name == "pvp_roster_close" then + if player.gui.screen.pvp_roster_frame then player.gui.screen.pvp_roster_frame.destroy() end + return true + end + return false +end + +return roster diff --git a/setup.lua b/setup.lua index 6b3be2e..eeb30fa 100644 --- a/setup.lua +++ b/setup.lua @@ -164,6 +164,100 @@ local function init_planet_surfaces() end end +local RESERVED_FORCES = {player = true, enemy = true, neutral = true, observer = true} + +-- storage is per-mod, so a Planet-PvP save loses everything on the swap. Team +-- forces persist in the save itself; find which force owns a planet via its +-- surviving base, then its researched discovery tech, then unlocked locations. +local function find_team_force(planet_name) + local planet = game.planets[planet_name] + local surface = planet and planet.surface + if surface then + for _, entity in pairs(surface.find_entities_filtered{name = "pvp-base"}) do + if entity.valid and not RESERVED_FORCES[entity.force.name] then return entity.force.name end + end + end + local tech_name = "planet-discovery-" .. planet_name + for _, force in pairs(game.forces) do + if not RESERVED_FORCES[force.name] then + local tech = force.technologies[tech_name] + if tech and tech.researched then return force.name end + end + end + -- Nauvis can be unlocked by default, so only trust a force whose single + -- unlocked planet of the four is the one being matched. + for _, force in pairs(game.forces) do + if not RESERVED_FORCES[force.name] then + local unlocked = {} + for _, p in ipairs(PLANETS) do + if force.is_space_location_unlocked(p) then unlocked[#unlocked + 1] = p end + end + if #unlocked == 1 and unlocked[1] == planet_name then return force.name end + end + end +end + +local function migrate_from_planet_pvp() + for _, planet_name in ipairs(PLANETS) do + if not storage.planet_teams[planet_name] then + local force_name = find_team_force(planet_name) + if force_name then + storage.planet_teams[planet_name] = force_name + base.assign_force_color(force_name) + end + end + end + local team_forces = {} + for _, force_name in pairs(storage.planet_teams) do team_forces[force_name] = true end + for planet_name, force_name in pairs(storage.planet_teams) do + local planet = game.planets[planet_name] + if planet and planet.surface then + for _, entity in pairs(planet.surface.find_entities_filtered{name = "pvp-base", force = force_name}) do + if entity.valid then + base.register(entity, true) + storage.team_kit_spawned[force_name] = true + end + end + end + end + for _, player in pairs(game.players) do + if player.force and team_forces[player.force.name] then + storage.team_kit_spawned[player.force.name] = true + storage.player_team_joined[player.index] = true + base.player_color(player) + end + if player.controller_type == defines.controllers.spectator and player.force.name == "observer" then + storage.observers[player.index] = {following = nil, perspective = "observer", gui_open = false} + end + end + if next(storage.planet_teams) then + storage.game_started = true + storage.crash_site_cleanup_done = true + local names = {} + for force_name in pairs(team_forces) do names[#names + 1] = force_name end + for i, a_name in ipairs(names) do + for j, b_name in ipairs(names) do + if i ~= j then + local a, b = game.forces[a_name], game.forces[b_name] + if a and b then a.set_friend(b, false); a.set_cease_fire(b, false) end + end + end + end + for planet_name, force_name in pairs(storage.planet_teams) do + local members = {} + for _, player in pairs(game.players) do + if player.force and player.force.name == force_name then members[#members + 1] = player.name end + end + game.print("[Planet PvP] " .. planet_name .. " = " .. force_name .. " (" .. #members .. "): " .. table.concat(members, ", ")) + end + end + for _, player in pairs(game.players) do + if player.force and not team_forces[player.force.name] and not RESERVED_FORCES[player.force.name] then + game.print("[Planet PvP] WARNING: " .. player.name .. " is on unknown force '" .. player.force.name .. "'. They were sent to the lobby to pick a team.") + 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"} @@ -207,7 +301,8 @@ function setup.on_configuration_changed(event) if event and event.mod_changes and event.mod_changes["Planet-PvP"] then game.print("Save migrated from Planet-PvP to Planet PvP Green. All teams, bases and drop zones carry over.") end - for _, key in pairs({"planet_teams", "team_kit_spawned", "player_surfaces", "force_bases", "permadead", "pending_spectator", "force_colors", "base_label_ids", "player_targets", "grid_pos", "player_lights", "player_team_joined", "observers", "radar_scan_progress"}) do storage[key] = storage[key] or {} end + for _, key in pairs({"planet_teams", "team_kit_spawned", "player_surfaces", "force_bases", "permadead", "pending_spectator", "force_colors", "base_label_ids", "player_targets", "grid_pos", "player_lights", "player_team_joined", "observers", "radar_scan_progress", "ally_markers", "pending_teleports"}) do storage[key] = storage[key] or {} end + if event and event.mod_changes and event.mod_changes["Planet-PvP"] then migrate_from_planet_pvp() end get_hub_surface() if storage.game_started then init_planet_surfaces() end for planet_name, force_name in pairs(storage.planet_teams or {}) do @@ -298,6 +393,7 @@ function setup.on_gui_click(event) local force_name = storage.planet_teams[planet_name] if force_name then player.force = game.forces[force_name]; player.clear_items_inside() + base.player_color(player) storage.player_team_joined = storage.player_team_joined or {} storage.player_team_joined[player.index] = true local planet = game.planets[planet_name]