diff --git a/exp_legacy/module/modules/control/spectate.lua b/exp_legacy/module/modules/control/spectate.lua deleted file mode 100644 index dc2784f8..00000000 --- a/exp_legacy/module/modules/control/spectate.lua +++ /dev/null @@ -1,173 +0,0 @@ -local Storage = require("modules/exp_util/storage") -local Gui = require("modules/exp_gui") - -local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event - ------ Locals ----- -local follow_label --- @type ExpElement Gui constructor -local following = {} -local spectating = {} -local Public = {} - ------ Storage data ----- -Storage.register({ - following = following, - spectating = spectating, -}, function(tbl) - following = tbl.following - spectating = tbl.spectating -end) - ------ Public Functions ----- - ---- Test if a player is in spectator mode --- @tparam LuaPlayer player The player to test the controller type of --- @treturn boolean Returns true if the player is in spectator mode -function Public.is_spectating(player) - assert(player and player.valid, "Invalid player given to follower") - return player.controller_type == defines.controllers.spectator -end - ---- Puts a player into spectator mode while maintaining an association to their character --- @tparam LuaPlayer player The player that will be placed into spectator mode --- @treturn boolean Returns false if the player was already in spectator mode -function Public.start_spectate(player) - assert(player and player.valid, "Invalid player given to follower") - if spectating[player.index] or not player.character then return false end - local character = player.character - local opened = player.opened - player.set_controller{ type = defines.controllers.spectator } - player.associate_character(character) - spectating[player.index] = character - if opened then player.opened = opened end -- Maintain opened after controller change - return true -end - ---- Return a player from spectator mode back to their character, if their character was killed then respawn them --- @tparam LuaPlayer player The player that will leave spectator mode -function Public.stop_spectate(player) - assert(player and player.valid, "Invalid player given to follower") - local character = spectating[player.index] - spectating[player.index] = nil - if character and character.valid then - local opened = player.opened - player.teleport(character.position, character.surface) - player.set_controller{ type = defines.controllers.character, character = character } - if opened then player.opened = opened end -- Maintain opened after controller change - else - player.ticks_to_respawn = 300 - end -end - ---- Test if a player is in follow mode --- @tparam LuaPlayer player The player to test the follow mode of --- @treturn boolean Returns true if the player is in follow mode -function Public.is_following(player) - assert(player and player.valid, "Invalid player given to follower") - return following[player.index] ~= nil -end - ---- Puts a player into spectator mode and follows an entity as it moves --- @tparam LuaPlayer player The player that will follow the entity --- @tparam ?LuaPlayer|LuaEntity entity The player or entity that will be followed -function Public.start_follow(player, entity) - assert(player and player.valid, "Invalid player given to follower") - assert(entity and entity.valid, "Invalid entity given to follower") - local spectate = Public.start_spectate(player) - - player.close_map() - follow_label(player.gui.screen, entity) - player.teleport(entity.position, entity.surface) - following[player.index] = { player, entity, entity.position, spectate } -end - ---- Returns camera control to the player, will return a player to their character if start_follow placed them into spectator mode --- @tparam LuaPlayer player The player that will regain control of their camera -function Public.stop_follow(player) - assert(player and player.valid, "Invalid player given to follower") - if following[player.index] and following[player.index][4] and Public.is_spectating(player) then - Public.stop_spectate(player) - end - - Gui.destroy_if_valid(player.gui.screen.follow_label) - following[player.index] = nil -end - ---- Returns camera control to all players, will return a player to their character if start_follow placed them into spectator mode -function Public.stop_all() - for key, data in pairs(following) do - Public.stop_follow(data[1]) - end -end - ------ Gui ----- - ---- Label used to show that the player is following, also used to allow esc to stop following -follow_label = Gui.define("follow-label") - :draw(function(def, parent, target) - Gui.destroy_if_valid(parent.follow_label) - - local label = parent.add{ - type = "label", - name = "follow_label", - style = "frame_title", - caption = "Following " .. target.name .. ".\nClick here or press esc to stop following.", - } - - local player = Gui.get_player(parent) - local res = player.display_resolution - label.location = { 0, res.height - 150 } - label.style.width = res.width - label.style.horizontal_align = "center" - player.opened = label - - return label - end) - :on_click(function(def, player, element) - Public.stop_follow(player) - end) - :on_closed(function(def, player, element) - -- Don't call set_controller during on_close as it invalidates the controller - -- Setting an invalid position (as to not equal their current) will call stop_follow on the next tick - following[player.index][3] = {} - end) - ------ Events ----- - ---- Updates the location of the player as well as doing some sanity checks --- @tparam LuaPlayer player The player to update the position of --- @tparam ?LuaPlayer|LuaEntity entity The player or entity being followed -local function update_player_location(player, entity, old_position) - if player.character or not entity.valid then - Public.stop_follow(player) - elseif player.position.x ~= old_position.x or player.position.y ~= old_position.y then - Public.stop_follow(player) - else - player.teleport(entity.position, entity.surface) - end -end - ---- Updates the locations of all players currently following something -local function update_all() - for _, data in pairs(following) do - update_player_location(data[1], data[2], data[3]) - data[3] = data[1].position - end -end - --- Update the location of all players each tick -Event.add(defines.events.on_tick, update_all) - --- Check for player leaving -Event.add(defines.events.on_pre_player_left_game, function(event) - local player = game.players[event.player_index] - Public.stop_follow(player) - for _, data in pairs(following) do - if data[2] == player then - Public.stop_follow(data[1]) - end - end -end) - ------ Module Return ----- -return Public diff --git a/exp_scenario/module/commands/spectate.lua b/exp_scenario/module/commands/spectate.lua index 13907289..816d3152 100644 --- a/exp_scenario/module/commands/spectate.lua +++ b/exp_scenario/module/commands/spectate.lua @@ -3,7 +3,7 @@ Adds commands relating to spectate and follow ]] local Commands = require("modules/exp_commands") -local Spectate = require("modules.exp_legacy.modules.control.spectate") --- @dep modules.control.spectate +local Spectate = require("modules/exp_scenario/control/spectate") --- Toggles spectator mode for the caller Commands.new("spectate", { "exp-commands_spectate.description-spectate" }) diff --git a/exp_scenario/module/control.lua b/exp_scenario/module/control.lua index 2e04e552..08f8a3f2 100644 --- a/exp_scenario/module/control.lua +++ b/exp_scenario/module/control.lua @@ -65,6 +65,7 @@ add(require("modules/exp_scenario/control/report_jail")) add(require("modules/exp_scenario/control/research")) add(require("modules/exp_scenario/control/roles")) add(require("modules/exp_scenario/control/spawn_area")) +add(require("modules/exp_scenario/control/spectate")) add(require("modules/exp_scenario/control/station_auto_name")) --- Guis diff --git a/exp_scenario/module/control/spectate.lua b/exp_scenario/module/control/spectate.lua new file mode 100644 index 00000000..b8cac7be --- /dev/null +++ b/exp_scenario/module/control/spectate.lua @@ -0,0 +1,173 @@ +--[[-- Control - Spectate +Lets players spectate without losing their character, and follow other players or entities +]] + +local Storage = require("modules/exp_util/storage") +local Gui = require("modules/exp_gui") + +--- @class ExpScenario_Spectate +local Spectate = { + --- @package + events = {}, +} + +--- @class ExpScenario_Spectate.Following +--- @field player LuaPlayer +--- @field target LuaPlayer | LuaEntity +--- @field position MapPosition Where the player was after the last update, moving away stops following +--- @field started_spectate boolean True when start_follow put the player into spectator mode +--- @field stop boolean? Set when following must end on the next tick + +local following = {} --- @type table Keyed by player index +local spectating = {} --- @type table The character a player returns to, keyed by player index + +Storage.register({ + following = following, + spectating = spectating, +}, function(tbl) + following = tbl.following + spectating = tbl.spectating +end) + +--- Label shown while following, clicking it or pressing escape stops following +--- @class ExpScenario_Spectate.follow_label: ExpElement +--- @overload fun(parent: LuaGuiElement, target: LuaPlayer | LuaEntity): LuaGuiElement +local follow_label = Gui.define("spectate/follow_label") + :draw(function(_, parent, target) + Gui.destroy_if_valid(parent.follow_label) + + local label = parent.add{ + type = "label", + name = "follow_label", + style = "frame_title", + caption = { "exp_spectate.follow-label", target.name }, + } + + local player = Gui.get_player(parent) + local res = player.display_resolution + label.location = { 0, res.height - 150 } + label.style.width = res.width + label.style.horizontal_align = "center" + player.opened = label + + return label + end) + :on_click(function(_, player) + Spectate.stop_follow(player) + end) + :on_closed(function(_, player) + -- set_controller can not be called during on_gui_closed, so the next update stops following + local data = following[player.index] + if data then data.stop = true end + end) --[[@as any]] + +--- Check if a player is in spectator mode +--- @param player LuaPlayer +--- @return boolean +function Spectate.is_spectating(player) + return player.controller_type == defines.controllers.spectator +end + +--- Put a player into spectator mode while keeping their character +--- @param player LuaPlayer +--- @return boolean # False when the player was already spectating or has no character +function Spectate.start_spectate(player) + if spectating[player.index] or not player.character then return false end + local character = player.character + local opened = player.opened + player.set_controller{ type = defines.controllers.spectator } + player.associate_character(character) + spectating[player.index] = character + if opened then player.opened = opened end -- Changing controller closes the opened gui + return true +end + +--- Return a player to their character, or respawn them if it was killed +--- @param player LuaPlayer +function Spectate.stop_spectate(player) + local character = spectating[player.index] + spectating[player.index] = nil + if character and character.valid then + local opened = player.opened + player.teleport(character.position, character.surface) + player.set_controller{ type = defines.controllers.character, character = character } + if opened then player.opened = opened end -- Changing controller closes the opened gui + else + player.ticks_to_respawn = 300 + end +end + +--- Check if a player is following something +--- @param player LuaPlayer +--- @return boolean +function Spectate.is_following(player) + return following[player.index] ~= nil +end + +--- Put a player into spectator mode and keep their camera on a target as it moves +--- @param player LuaPlayer +--- @param target LuaPlayer | LuaEntity +function Spectate.start_follow(player, target) + local started_spectate = Spectate.start_spectate(player) + + follow_label(player.gui.screen, target) + player.teleport(target.position, target.surface) + following[player.index] = { + player = player, + target = target, + position = player.position, + started_spectate = started_spectate, + } +end + +--- Give a player their camera back, returning them to their character if start_follow took it +--- @param player LuaPlayer +function Spectate.stop_follow(player) + local data = following[player.index] + if data and data.started_spectate and Spectate.is_spectating(player) then + Spectate.stop_spectate(player) + end + + Gui.destroy_if_valid(player.gui.screen.follow_label) + following[player.index] = nil +end + +--- Move a following player onto their target, or stop following when they have moved away or regained a character +--- @param data ExpScenario_Spectate.Following +local function update_following(data) + local player, target = data.player, data.target + local position = player.position + if data.stop or player.character or not target.valid + or position.x ~= data.position.x or position.y ~= data.position.y + then + Spectate.stop_follow(player) + else + player.teleport(target.position, target.surface) + data.position = player.position + end +end + +local function on_tick() + for _, data in pairs(following) do + update_following(data) + end +end + +--- Stop following when a player leaves, and stop anyone who was following them +--- @param event EventData.on_pre_player_left_game +local function on_pre_player_left_game(event) + local player = game.players[event.player_index] + Spectate.stop_follow(player) + for _, data in pairs(following) do + if data.target == player then + Spectate.stop_follow(data.player) + end + end +end + +local e = defines.events + +Spectate.events[e.on_tick] = on_tick +Spectate.events[e.on_pre_player_left_game] = on_pre_player_left_game + +return Spectate diff --git a/exp_scenario/module/locale/en.cfg b/exp_scenario/module/locale/en.cfg index 80e8a1ec..ff47c27c 100644 --- a/exp_scenario/module/locale/en.cfg +++ b/exp_scenario/module/locale/en.cfg @@ -577,3 +577,6 @@ chat-jailed=__1__ was jailed because they removed too many protected entities. P [exp_report-jail] chat-jailed=__1__ was jailed because they were reported too many times. Please wait for a moderator. + +[exp_spectate] +follow-label=Following __1__.\nClick here or press escape to stop following.