From 2df523061b8cd4cf2a8ef0507f7a4578718b2784 Mon Sep 17 00:00:00 2001 From: Red <96445749+TheShuEd@users.noreply.github.com> Date: Sun, 24 Aug 2025 18:36:24 +0300 Subject: [PATCH] Respawn button (#1702) * Add respawn action for ghosts with confirmation popup Introduces a new respawn action for ghosts, allowing them to return to the lobby and respawn as a new character. Adds supporting systems, localization, prototype definitions, and an icon for the action. The action includes a confirmation popup to prevent accidental use. * Prevent respawning with the same character in a round Introduces a system to block players from rejoining a round with a character that has already died, notifying them with a localized message. Refactors and moves respawn-related localization strings to new files for both English and Russian. * Prevent admins from character reuse restriction Added a check in CP14RespawnSystem to allow admins to bypass the restriction that prevents the same character from re-entering the round. This ensures that admin users are not limited by the character reuse logic. --- Content.Client/Ghost/GhostSystem.cs | 1 + .../GameTicking/GameTicker.Spawning.cs | 8 ++ Content.Server/Ghost/GhostSystem.cs | 5 +- .../_CP14/Respawn/CP14RespawnSystem.cs | 97 ++++++++++++++++++ Content.Shared/Ghost/GhostComponent.cs | 6 ++ .../_CP14/Respawn/CP14SharedRespawnSystem.cs | 7 ++ Resources/Locale/en-US/_CP14/respawn.ftl | 3 + Resources/Locale/ru-RU/_CP14/respawn.ftl | 3 + .../_CP14/Entities/Actions/ghost.yml | 17 +++ .../_CP14/Actions/zLevel.rsi/meta.json | 3 + .../_CP14/Actions/zLevel.rsi/respawn.png | Bin 0 -> 550 bytes 11 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 Content.Server/_CP14/Respawn/CP14RespawnSystem.cs create mode 100644 Content.Shared/_CP14/Respawn/CP14SharedRespawnSystem.cs create mode 100644 Resources/Locale/en-US/_CP14/respawn.ftl create mode 100644 Resources/Locale/ru-RU/_CP14/respawn.ftl create mode 100644 Resources/Textures/_CP14/Actions/zLevel.rsi/respawn.png diff --git a/Content.Client/Ghost/GhostSystem.cs b/Content.Client/Ghost/GhostSystem.cs index ee80470807..ebe9629a4a 100644 --- a/Content.Client/Ghost/GhostSystem.cs +++ b/Content.Client/Ghost/GhostSystem.cs @@ -136,6 +136,7 @@ namespace Content.Client.Ghost //_actions.RemoveAction(uid, component.ToggleGhostHearingActionEntity); //Dont need in CP14 //CP14 _actions.RemoveAction(uid, component.CP14ToggleRoofActionEntity); + _actions.RemoveAction(uid, component.CP14RespawnActionEntity); //CP14 end if (uid != _playerManager.LocalEntity) diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs index d758b36197..b05572849e 100644 --- a/Content.Server/GameTicking/GameTicker.Spawning.cs +++ b/Content.Server/GameTicking/GameTicker.Spawning.cs @@ -1,6 +1,7 @@ using System.Globalization; using System.Linq; using System.Numerics; +using Content.Server._CP14.Respawn; using Content.Server.Administration.Managers; using Content.Server.Administration.Systems; using Content.Server.GameTicking.Events; @@ -212,6 +213,13 @@ namespace Content.Server.GameTicking character = HumanoidCharacterProfile.RandomWithSpecies(speciesId); } + //CP14 blocking rejoining to game with same character + var CP14Ev = new CP14PlayerSpawnAttemptEvent(player, character, jobId, lateJoin, station); + RaiseLocalEvent(CP14Ev); + if (CP14Ev.Cancelled) + return; + //CP14 end + // We raise this event to allow other systems to handle spawning this player themselves. (e.g. late-join wizard, etc) var bev = new PlayerBeforeSpawnEvent(player, character, jobId, lateJoin, station); RaiseLocalEvent(bev); diff --git a/Content.Server/Ghost/GhostSystem.cs b/Content.Server/Ghost/GhostSystem.cs index 1c10d08dcd..cda6996053 100644 --- a/Content.Server/Ghost/GhostSystem.cs +++ b/Content.Server/Ghost/GhostSystem.cs @@ -224,18 +224,19 @@ namespace Content.Server.Ghost // Entity can't see ghosts anymore. _eye.RefreshVisibilityMask(uid); - _actions.RemoveAction(uid, component.BooActionEntity); + //_actions.RemoveAction(uid, component.BooActionEntity); //Dont need in CP14 } private void OnMapInit(EntityUid uid, GhostComponent component, MapInitEvent args) { - _actions.AddAction(uid, ref component.BooActionEntity, component.BooAction); + //_actions.AddAction(uid, ref component.BooActionEntity, component.BooAction); //Dont need in CP14 //_actions.AddAction(uid, ref component.ToggleGhostHearingActionEntity, component.ToggleGhostHearingAction); //Dont need in CP14 _actions.AddAction(uid, ref component.ToggleLightingActionEntity, component.ToggleLightingAction); _actions.AddAction(uid, ref component.ToggleFoVActionEntity, component.ToggleFoVAction); _actions.AddAction(uid, ref component.ToggleGhostsActionEntity, component.ToggleGhostsAction); //CP14 _actions.AddAction(uid, ref component.CP14ToggleRoofActionEntity, component.CP14ToggleRoofAction); + _actions.AddAction(uid, ref component.CP14RespawnActionEntity, component.CP14RespawnAction); //CP14 end } diff --git a/Content.Server/_CP14/Respawn/CP14RespawnSystem.cs b/Content.Server/_CP14/Respawn/CP14RespawnSystem.cs new file mode 100644 index 0000000000..8b50e5a3e9 --- /dev/null +++ b/Content.Server/_CP14/Respawn/CP14RespawnSystem.cs @@ -0,0 +1,97 @@ +using Content.Server.Chat.Systems; +using Content.Server.GameTicking; +using Content.Server.GameTicking.Events; +using Content.Shared._CP14.Respawn; +using Content.Shared.Administration.Managers; +using Content.Shared.GameTicking; +using Content.Shared.Ghost; +using Content.Shared.Preferences; +using JetBrains.Annotations; +using Robust.Shared.Audio; +using Robust.Shared.Network; +using Robust.Shared.Player; + +namespace Content.Server._CP14.Respawn; + +public sealed partial class CP14RespawnSystem : EntitySystem +{ + [Dependency] private readonly GameTicker _gameTicker = default!; + [Dependency] private readonly ChatSystem _chat = default!; + [Dependency] private readonly ISharedAdminManager _admin = default!; + + /// + /// We keep a list of all characters connected in a round to prevent the same character from re-entering the round. + /// Of course, we only check by character name, and this is easy to circumvent by renaming the character, + /// but the main goal here is to make the player understand that this is not the right thing to do. + /// If a player abuses the ability to re-enter a round, the administration will take care of it. + /// + private Dictionary> _usedCharacters = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnRespawnAction); + SubscribeLocalEvent(OnBeforePlayerSpawn); + SubscribeLocalEvent(OnRoundEnd); + } + + private void OnRoundEnd(RoundStartingEvent ev) + { + _usedCharacters.Clear(); + } + + private void OnBeforePlayerSpawn(CP14PlayerSpawnAttemptEvent ev) + { + if (_admin.IsAdmin(ev.Player, true)) + return; + + if (!_usedCharacters.TryGetValue(ev.Player.UserId, out var usedCharacters)) + { + usedCharacters = []; + _usedCharacters[ev.Player.UserId] = usedCharacters; + } + + if (usedCharacters.Contains(ev.Profile.Name)) + { + var filter = Filter.Empty().AddPlayer(ev.Player); + _chat.DispatchFilteredAnnouncement( + filter, + Loc.GetString("cp14-respawn-same-char-bloked"), + colorOverride: Color.Red, + announcementSound: new SoundPathSpecifier("/Audio/Effects/beep1.ogg"), + sender: "Server"); + ev.Cancel(); + return; + } + + usedCharacters.Add(ev.Profile.Name); + } + + private void OnRespawnAction(Entity ent, ref CP14RespawnAction args) + { + if (!TryComp(ent, out var actor)) + return; + + _gameTicker.Respawn(actor.PlayerSession); + } +} + +/// +/// Called before a player spawns for a specific character. Can be canceled by prohibiting connection for that character. +/// +[PublicAPI] +public sealed class CP14PlayerSpawnAttemptEvent( + ICommonSession player, + HumanoidCharacterProfile profile, + string? jobId, + bool lateJoin, + EntityUid station) + : CancellableEntityEventArgs +{ + public ICommonSession Player { get; } = player; + public HumanoidCharacterProfile Profile { get; } = profile; + public string? JobId { get; } = jobId; + public bool LateJoin { get; } = lateJoin; + public EntityUid Station { get; } = station; +} diff --git a/Content.Shared/Ghost/GhostComponent.cs b/Content.Shared/Ghost/GhostComponent.cs index c0a287af0d..ca87871c5c 100644 --- a/Content.Shared/Ghost/GhostComponent.cs +++ b/Content.Shared/Ghost/GhostComponent.cs @@ -47,8 +47,14 @@ public sealed partial class GhostComponent : Component [DataField] public EntProtoId CP14ToggleRoofAction = "CP14ActionToggleRoofs"; + [DataField] + public EntProtoId CP14RespawnAction = "CP14ActionRespawn"; + [DataField, AutoNetworkedField] public EntityUid? CP14ToggleRoofActionEntity; + + [DataField, AutoNetworkedField] + public EntityUid? CP14RespawnActionEntity; //CP14 Ghost entities end // End actions diff --git a/Content.Shared/_CP14/Respawn/CP14SharedRespawnSystem.cs b/Content.Shared/_CP14/Respawn/CP14SharedRespawnSystem.cs new file mode 100644 index 0000000000..09c49d3ed7 --- /dev/null +++ b/Content.Shared/_CP14/Respawn/CP14SharedRespawnSystem.cs @@ -0,0 +1,7 @@ +using Content.Shared.Actions; + +namespace Content.Shared._CP14.Respawn; + +public sealed partial class CP14RespawnAction : InstantActionEvent +{ +} diff --git a/Resources/Locale/en-US/_CP14/respawn.ftl b/Resources/Locale/en-US/_CP14/respawn.ftl new file mode 100644 index 0000000000..a7b27721f8 --- /dev/null +++ b/Resources/Locale/en-US/_CP14/respawn.ftl @@ -0,0 +1,3 @@ +cp14-respawn-action-popup = Are you sure you want to return to the lobby? You will no longer be able to play as this character! + +cp14-respawn-same-char-bloked = This character has died. You can no longer use them in this round. \ No newline at end of file diff --git a/Resources/Locale/ru-RU/_CP14/respawn.ftl b/Resources/Locale/ru-RU/_CP14/respawn.ftl new file mode 100644 index 0000000000..cd5e437cc4 --- /dev/null +++ b/Resources/Locale/ru-RU/_CP14/respawn.ftl @@ -0,0 +1,3 @@ +cp14-respawn-action-popup = Вы уверены что хотите вернуться в лобби? Игра за этого персонажа станет недоступной! + +cp14-respawn-same-char-bloked = Этот персонаж погиб. Вы больше не можете использовать его в этом раунде. \ No newline at end of file diff --git a/Resources/Prototypes/_CP14/Entities/Actions/ghost.yml b/Resources/Prototypes/_CP14/Entities/Actions/ghost.yml index a4aed57606..8acb83eca7 100644 --- a/Resources/Prototypes/_CP14/Entities/Actions/ghost.yml +++ b/Resources/Prototypes/_CP14/Entities/Actions/ghost.yml @@ -38,3 +38,20 @@ - type: InstantAction event: !type:CP14ToggleRoofVisibilityAction +- type: entity + id: CP14ActionRespawn + name: Respawn + description: Return to the lobby to connect with a new character. + components: + - type: Action + priority: 20 + startDelay: true + useDelay: 300 + checkCanInteract: false + icon: + sprite: _CP14/Actions/zLevel.rsi + state: respawn + - type: ConfirmableAction + popup: cp14-respawn-action-popup + - type: InstantAction + event: !type:CP14RespawnAction \ No newline at end of file diff --git a/Resources/Textures/_CP14/Actions/zLevel.rsi/meta.json b/Resources/Textures/_CP14/Actions/zLevel.rsi/meta.json index 59cc25eb7b..6abdad2bc3 100644 --- a/Resources/Textures/_CP14/Actions/zLevel.rsi/meta.json +++ b/Resources/Textures/_CP14/Actions/zLevel.rsi/meta.json @@ -15,6 +15,9 @@ }, { "name": "roof" + }, + { + "name": "respawn" } ] } diff --git a/Resources/Textures/_CP14/Actions/zLevel.rsi/respawn.png b/Resources/Textures/_CP14/Actions/zLevel.rsi/respawn.png new file mode 100644 index 0000000000000000000000000000000000000000..d0fe4e1abc1c8619a88481cb8d8761296203f22d GIT binary patch literal 550 zcmV+>0@?kEP)Px$;7LS5R9J<@R=bhHKnxY0n;|11BZY*(bx;8n(7=^K1FnDyTn7OOq|Tf;yNC-w zzGwMqS3bD8H{%WTSiVPRCfKhCMl>GNwck&uuo_Y`|X zA|$c`p7Q#<9Q1c(`zsR3Nv2#CTZL>qLZXN&ug^=V&r9j5W~5vdcU3d+jIK=uu1f$n z5E1FBM!Kp=iT83I2}Xnxu!6c_G_n~{FMwSSSnH!ulFSQ}2q>8KHUrq07o-*+ImOKS zu4<${tVUyYJ(BRe=z4%fJI2PYYzLo}!C*|q5Dlu$dV9xn!xEdThA1l{vN)`I>luNO z2Ux18(nu*rnf-Czd&kQldIY($`%btqfJ#~A%OQGkQ36_WZ7yp$_Mr3Z?(i@=7aIeM z!z!H9OfD6GlmH{cNZ4p+ag$S?C6FWGA*wt?z>*O0gVI*dx4eif2_#~H<@T*_LXQZ; z)K~GpW0te%uTh9RQb<46$FbDBZF_%g_m$8V7p!`$3bHCW^|BIy2g$B%pPM#)Uqs{t z^S00Ab9fN_ksWvdH$jWp9e~9~`w`eXroagVzI3#W3$45*gU4dYp=^Qdt;M#l3+)rj ou2(DZkMr1mDGjf;%gbr}4V%t!#QVy%#sB~S07*qoM6N<$g0hbMfdBvi literal 0 HcmV?d00001