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.
This commit is contained in:
Red
2025-08-24 18:36:24 +03:00
committed by GitHub
parent ac2eb6f51c
commit 2df523061b
11 changed files with 148 additions and 2 deletions

View File

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

View File

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

View File

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

View File

@@ -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!;
/// <summary>
/// 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.
/// </summary>
private Dictionary<NetUserId, List<string>> _usedCharacters = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GhostComponent, CP14RespawnAction>(OnRespawnAction);
SubscribeLocalEvent<CP14PlayerSpawnAttemptEvent>(OnBeforePlayerSpawn);
SubscribeLocalEvent<RoundStartingEvent>(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<GhostComponent> ent, ref CP14RespawnAction args)
{
if (!TryComp<ActorComponent>(ent, out var actor))
return;
_gameTicker.Respawn(actor.PlayerSession);
}
}
/// <summary>
/// Called before a player spawns for a specific character. Can be canceled by prohibiting connection for that character.
/// </summary>
[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;
}

View File

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

View File

@@ -0,0 +1,7 @@
using Content.Shared.Actions;
namespace Content.Shared._CP14.Respawn;
public sealed partial class CP14RespawnAction : InstantActionEvent
{
}

View File

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

View File

@@ -0,0 +1,3 @@
cp14-respawn-action-popup = Вы уверены что хотите вернуться в лобби? Игра за этого персонажа станет недоступной!
cp14-respawn-same-char-bloked = Этот персонаж погиб. Вы больше не можете использовать его в этом раунде.

View File

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

View File

@@ -15,6 +15,9 @@
},
{
"name": "roof"
},
{
"name": "respawn"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B