1v1 me first to 31 no powerups [Deathmatch Gamemode] (#19467)

Co-authored-by: Kara <lunarautomaton6@gmail.com>
This commit is contained in:
Nemanja
2023-08-30 21:06:15 -04:00
committed by GitHub
parent 3f678104e3
commit 4d677f0685
44 changed files with 2821 additions and 155 deletions

View File

@@ -80,7 +80,7 @@ namespace Content.Server.Chat
/// <returns>Returns true if there was a blocked attempt</returns>
private bool SuicideAttemptBlocked(EntityUid victim, SuicideEvent suicideEvent)
{
RaiseLocalEvent(victim, suicideEvent, false);
RaiseLocalEvent(victim, suicideEvent, true);
if (suicideEvent.AttemptBlocked)
return true;

View File

@@ -144,9 +144,10 @@ namespace Content.Server.GameTicking
return (HumanoidCharacterProfile) _prefsManager.GetPreferences(p.UserId).SelectedCharacter;
}
public void PlayerJoinGame(IPlayerSession session)
public void PlayerJoinGame(IPlayerSession session, bool silent = false)
{
_chatManager.DispatchServerMessage(session, Loc.GetString("game-ticker-player-join-game-message"));
if (!silent)
_chatManager.DispatchServerMessage(session, Loc.GetString("game-ticker-player-join-game-message"));
_playerGameStatuses[session.UserId] = PlayerGameStatus.JoinedGame;
_db.AddRoundPlayers(RoundId, session.UserId);

View File

@@ -117,7 +117,7 @@ namespace Content.Server.GameTicking
if (CurrentPreset?.MapPool != null &&
_prototypeManager.TryIndex<GameMapPoolPrototype>(CurrentPreset.MapPool, out var pool) &&
pool.Maps.Contains(mainStationMap.ID))
!pool.Maps.Contains(mainStationMap.ID))
{
var msg = Loc.GetString("game-ticker-start-round-invalid-map",
("map", mainStationMap.MapName),

View File

@@ -104,7 +104,7 @@ namespace Content.Server.GameTicking
RaiseLocalEvent(new RulePlayerJobsAssignedEvent(assignedJobs.Keys.Select(x => _playerManager.GetSessionByUserId(x)).ToArray(), profiles, force));
}
private void SpawnPlayer(IPlayerSession player, EntityUid station, string? jobId = null, bool lateJoin = true)
private void SpawnPlayer(IPlayerSession player, EntityUid station, string? jobId = null, bool lateJoin = true, bool silent = false)
{
var character = GetPlayerProfile(player);
@@ -114,10 +114,10 @@ namespace Content.Server.GameTicking
if (jobId != null && !_playTimeTrackings.IsAllowed(player, jobId))
return;
SpawnPlayer(player, character, station, jobId, lateJoin);
SpawnPlayer(player, character, station, jobId, lateJoin, silent);
}
private void SpawnPlayer(IPlayerSession player, HumanoidCharacterProfile character, EntityUid station, string? jobId = null, bool lateJoin = true)
private void SpawnPlayer(IPlayerSession player, HumanoidCharacterProfile character, EntityUid station, string? jobId = null, bool lateJoin = true, bool silent = false)
{
// Can't spawn players with a dummy ticker!
if (DummyTicker)
@@ -150,7 +150,7 @@ namespace Content.Server.GameTicking
// Do nothing, something else has handled spawning this player for us!
if (bev.Handled)
{
PlayerJoinGame(player);
PlayerJoinGame(player, silent);
return;
}
@@ -177,7 +177,7 @@ namespace Content.Server.GameTicking
return;
}
PlayerJoinGame(player);
PlayerJoinGame(player, silent);
var data = player.ContentData();
@@ -188,7 +188,7 @@ namespace Content.Server.GameTicking
var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
var job = new JobComponent { PrototypeId = jobId };
_roles.MindAddRole(newMind, job);
_roles.MindAddRole(newMind, job, silent: silent);
var jobName = _jobs.MindTryGetJobName(newMind);
_playTimeTrackings.PlayerRolesChanged(player);
@@ -199,7 +199,7 @@ namespace Content.Server.GameTicking
_mind.TransferTo(newMind, mob);
if (lateJoin)
if (lateJoin && !silent)
{
_chatSystem.DispatchStationAnnouncement(station,
Loc.GetString(
@@ -230,7 +230,7 @@ namespace Content.Server.GameTicking
_chatManager.DispatchServerMessage(player, Loc.GetString("job-greet-crew-shortages"));
}
if (TryComp(station, out MetaDataComponent? metaData))
if (!silent && TryComp(station, out MetaDataComponent? metaData))
{
_chatManager.DispatchServerMessage(player,
Loc.GetString("job-greet-station-name", ("stationName", metaData.EntityName)));
@@ -238,7 +238,7 @@ namespace Content.Server.GameTicking
// Arrivals is unable to do this during spawning as no actor is attached yet.
// We also want this message last.
if (lateJoin && _arrivals.Enabled)
if (!silent && lateJoin && _arrivals.Enabled)
{
var arrival = _arrivals.NextShuttleArrival();
if (arrival == null)
@@ -269,7 +269,14 @@ namespace Content.Server.GameTicking
SpawnPlayer(player, EntityUid.Invalid);
}
public void MakeJoinGame(IPlayerSession player, EntityUid station, string? jobId = null)
/// <summary>
/// Makes a player join into the game and spawn on a staiton.
/// </summary>
/// <param name="player">The player joining</param>
/// <param name="station">The station they're spawning on</param>
/// <param name="jobId">An optional job for them to spawn as</param>
/// <param name="silent">Whether or not the player should be greeted upon joining</param>
public void MakeJoinGame(IPlayerSession player, EntityUid station, string? jobId = null, bool silent = false)
{
if (!_playerGameStatuses.ContainsKey(player.UserId))
return;
@@ -277,7 +284,7 @@ namespace Content.Server.GameTicking
if (!_userDb.IsLoadComplete(player))
return;
SpawnPlayer(player, station, jobId);
SpawnPlayer(player, station, jobId, silent: silent);
}
/// <summary>

View File

@@ -1,33 +1,46 @@
namespace Content.Server.GameTicking.Rules.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Roles;
using Content.Shared.Storage;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// Simple GameRule that will do a free-for-all death match.
/// Kill everybody else to win.
/// Gamerule that ends when a player gets a certain number of kills.
/// </summary>
[RegisterComponent, Access(typeof(DeathMatchRuleSystem))]
public sealed partial class DeathMatchRuleComponent : Component
{
/// <summary>
/// The number of points a player has to get to win.
/// </summary>
[DataField("killCap"), ViewVariables(VVAccess.ReadWrite)]
public FixedPoint2 KillCap = 31;
/// <summary>
/// How long until the round restarts
/// </summary>
[DataField("restartDelay"), ViewVariables(VVAccess.ReadWrite)]
public float RestartDelay = 10f;
public TimeSpan RestartDelay = TimeSpan.FromSeconds(10f);
/// <summary>
/// How long after a person dies will the restart be checked
/// The person who won.
/// We store this here in case of some assist shenanigans.
/// </summary>
[DataField("deadCheckDelay"), ViewVariables(VVAccess.ReadWrite)]
public float DeadCheckDelay = 5f;
[DataField("victor")]
public NetUserId? Victor;
/// <summary>
/// A timer for checking after a death
/// An entity spawned after a player is killed.
/// </summary>
[DataField("deadCheckTimer"), ViewVariables(VVAccess.ReadWrite)]
public float? DeadCheckTimer;
[DataField("rewardSpawns")]
public List<EntitySpawnEntry> RewardSpawns = new();
/// <summary>
/// A timer for the restart.
/// The gear all players spawn with.
/// </summary>
[DataField("restartTimer"), ViewVariables(VVAccess.ReadWrite)]
public float? RestartTimer;
[DataField("gear", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>)), ViewVariables(VVAccess.ReadWrite)]
public string Gear = "DeathMatchGear";
}

View File

@@ -0,0 +1,32 @@
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// This is used for a rule that announces kills globally.
/// </summary>
[RegisterComponent, Access(typeof(KillCalloutRuleSystem))]
public sealed partial class KillCalloutRuleComponent : Component
{
/// <summary>
/// Root used to generate kill callouts
/// </summary>
[DataField("killCalloutPrefix")]
public string KillCalloutPrefix = "death-match-kill-callout-";
/// <summary>
/// A value used to randomly select a kill callout
/// </summary>
[DataField("killCalloutAmount")]
public int KillCalloutAmount = 60;
/// <summary>
/// Root used to generate kill callouts when a player is killed by the environment
/// </summary>
[DataField("environmentKillCallouts")]
public string SelfKillCalloutPrefix = "death-match-kill-callout-env-";
/// <summary>
/// A value used to randomly select a kill callout when a player is killed by the environment
/// </summary>
[DataField("selfKillCalloutAmount")]
public int SelfKillCalloutAmount = 10;
}

View File

@@ -0,0 +1,9 @@
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// This is used for gamemodes that automatically respawn players when they're no longer alive.
/// </summary>
[RegisterComponent, Access(typeof(RespawnRuleSystem))]
public sealed partial class RespawnDeadRuleComponent : Component
{
}

View File

@@ -0,0 +1,30 @@
using Robust.Shared.Network;
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// This is used for globally tracking players that need to be respawned.
/// Used on gamerule entities.
/// </summary>
[RegisterComponent, Access(typeof(RespawnRuleSystem))]
public sealed partial class RespawnTrackerComponent : Component
{
/// <summary>
/// A list of the people that should be respawned.
/// Used to make sure that we don't respawn aghosts or observers.
/// </summary>
[DataField("players")]
public HashSet<NetUserId> Players = new();
/// <summary>
/// The delay between dying and respawning.
/// </summary>
[DataField("respawnDelay")]
public TimeSpan RespawnDelay = TimeSpan.Zero;
/// <summary>
/// A dictionary of player netuserids and when they will respawn.
/// </summary>
[DataField("respawnQueue")]
public Dictionary<NetUserId, TimeSpan> RespawnQueue = new();
}

View File

@@ -1,12 +1,14 @@
using Content.Server.Chat.Managers;
using Content.Server.Administration.Commands;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared.CCVar;
using Content.Shared.Damage;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Server.KillTracking;
using Content.Server.Mind;
using Content.Server.Points;
using Content.Server.RoundEnd;
using Content.Server.Station.Systems;
using Content.Shared.Points;
using Content.Shared.Storage;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Utility;
namespace Content.Server.GameTicking.Rules;
@@ -15,116 +17,116 @@ namespace Content.Server.GameTicking.Rules;
/// </summary>
public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponent>
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly PointSystem _point = default!;
[Dependency] private readonly RespawnRuleSystem _respawn = default!;
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
[Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DamageChangedEvent>(OnHealthChanged);
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
SubscribeLocalEvent<PlayerBeforeSpawnEvent>(OnBeforeSpawn);
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
SubscribeLocalEvent<KillReportedEvent>(OnKillReported);
SubscribeLocalEvent<DeathMatchRuleComponent, PlayerPointChangedEvent>(OnPointChanged);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndTextAppend);
}
public override void Shutdown()
private void OnBeforeSpawn(PlayerBeforeSpawnEvent ev)
{
base.Shutdown();
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
}
protected override void Started(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-death-match-added-announcement"));
}
protected override void Ended(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
{
base.Ended(uid, component, gameRule, args);
component.DeadCheckTimer = null;
component.RestartTimer = null;
}
private void OnHealthChanged(DamageChangedEvent _)
{
RunDelayedCheck();
}
private void OnPlayerStatusChanged(object? ojb, SessionStatusEventArgs e)
{
if (e.NewStatus == SessionStatus.Disconnected)
var query = EntityQueryEnumerator<DeathMatchRuleComponent, RespawnTrackerComponent, PointManagerComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var dm, out var tracker, out var point, out var rule))
{
RunDelayedCheck();
}
}
private void RunDelayedCheck()
{
var query = EntityQueryEnumerator<DeathMatchRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var deathMatch, out var gameRule))
{
if (!GameTicker.IsGameRuleActive(uid, gameRule) || deathMatch.DeadCheckTimer != null)
if (!GameTicker.IsGameRuleActive(uid, rule))
continue;
deathMatch.DeadCheckTimer = deathMatch.DeadCheckDelay;
var newMind = _mind.CreateMind(ev.Player.UserId, ev.Profile.Name);
_mind.SetUserId(newMind, ev.Player.UserId);
var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(ev.Station, null, ev.Profile);
DebugTools.AssertNotNull(mobMaybe);
var mob = mobMaybe!.Value;
_mind.TransferTo(newMind, mob);
SetOutfitCommand.SetOutfit(mob, dm.Gear, EntityManager);
EnsureComp<KillTrackerComponent>(mob);
_respawn.AddToTracker(ev.Player.UserId, uid, tracker);
_point.EnsurePlayer(ev.Player.UserId, uid, point);
ev.Handled = true;
break;
}
}
protected override void ActiveTick(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, float frameTime)
private void OnSpawnComplete(PlayerSpawnCompleteEvent ev)
{
base.ActiveTick(uid, component, gameRule, frameTime);
// If the restart timer is active, that means the round is ending soon, no need to check for winners.
// TODO: We probably want a sane, centralized round end thingie in GameTicker, RoundEndSystem is no good...
if (component.RestartTimer != null)
EnsureComp<KillTrackerComponent>(ev.Mob);
var query = EntityQueryEnumerator<DeathMatchRuleComponent, RespawnTrackerComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out _, out var tracker, out var rule))
{
component.RestartTimer -= frameTime;
if (component.RestartTimer > 0f)
return;
GameTicker.EndRound();
GameTicker.RestartRound();
return;
if (!GameTicker.IsGameRuleActive(uid, rule))
continue;
_respawn.AddToTracker(ev.Mob, uid, tracker);
}
}
if (!_cfg.GetCVar(CCVars.GameLobbyEnableWin) || component.DeadCheckTimer == null)
return;
component.DeadCheckTimer -= frameTime;
if (component.DeadCheckTimer > 0)
return;
component.DeadCheckTimer = null;
IPlayerSession? winner = null;
foreach (var playerSession in _playerManager.ServerSessions)
private void OnKillReported(ref KillReportedEvent ev)
{
var query = EntityQueryEnumerator<DeathMatchRuleComponent, PointManagerComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var dm, out var point, out var rule))
{
if (playerSession.AttachedEntity is not { Valid: true } playerEntity
|| !TryComp(playerEntity, out MobStateComponent? state))
if (!GameTicker.IsGameRuleActive(uid, rule))
continue;
if (!_mobStateSystem.IsAlive(playerEntity, state))
// YOU SUICIDED OR GOT THROWN INTO LAVA!
// WHAT A GIANT FUCKING NERD! LAUGH NOW!
if (ev.Primary is not KillPlayerSource player)
{
_point.AdjustPointValue(ev.Entity, -1, uid, point);
continue;
}
_point.AdjustPointValue(player.PlayerId, 1, uid, point);
if (ev.Assist is KillPlayerSource assist && dm.Victor == null)
_point.AdjustPointValue(assist.PlayerId, 1, uid, point);
var spawns = EntitySpawnCollection.GetSpawns(dm.RewardSpawns);
EntityManager.SpawnEntities(Transform(ev.Entity).MapPosition, spawns);
}
}
private void OnPointChanged(EntityUid uid, DeathMatchRuleComponent component, ref PlayerPointChangedEvent args)
{
if (component.Victor != null)
return;
if (args.Points < component.KillCap)
return;
component.Victor = args.Player;
_roundEnd.EndRound(component.RestartDelay);
}
private void OnRoundEndTextAppend(RoundEndTextAppendEvent ev)
{
var query = EntityQueryEnumerator<DeathMatchRuleComponent, PointManagerComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var dm, out var point, out var rule))
{
if (!GameTicker.IsGameRuleAdded(uid, rule))
continue;
// Found a second person alive, nothing decided yet!
if (winner != null)
return;
winner = playerSession;
if (dm.Victor != null && _player.TryGetPlayerData(dm.Victor.Value, out var data))
{
ev.AddLine(Loc.GetString("point-scoreboard-winner", ("player", data.UserName)));
ev.AddLine("");
}
ev.AddLine(Loc.GetString("point-scoreboard-header"));
ev.AddLine(new FormattedMessage(point.Scoreboard).ToMarkup());
}
_chatManager.DispatchServerAnnouncement(winner == null
? Loc.GetString("rule-death-match-check-winner-stalemate")
: Loc.GetString("rule-death-match-check-winner", ("winner", winner)));
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds",
("seconds", component.RestartDelay)));
component.RestartTimer = component.RestartDelay;
}
}

View File

@@ -0,0 +1,99 @@
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.KillTracking;
using Content.Shared.Chat;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Random;
namespace Content.Server.GameTicking.Rules;
/// <summary>
/// This handles calling out kills from <see cref="KillTrackingSystem"/>
/// </summary>
public sealed class KillCalloutRuleSystem : GameRuleSystem<KillCalloutRuleComponent>
{
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<KillReportedEvent>(OnKillReported);
}
private void OnKillReported(ref KillReportedEvent ev)
{
var query = EntityQueryEnumerator<KillCalloutRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var kill, out var rule))
{
if (!GameTicker.IsGameRuleActive(uid, rule))
continue;
var callout = GetCallout(kill, ev);
_chatManager.ChatMessageToAll(ChatChannel.Server, callout, callout, uid, false, true, Color.OrangeRed);
}
}
private string GetCallout(KillCalloutRuleComponent component, KillReportedEvent ev)
{
// Do the humiliation callouts if you kill yourself or die from bleeding out or something lame.
if (ev.Primary is KillEnvironmentSource || ev.Suicide)
{
var selfCallout = $"{component.SelfKillCalloutPrefix}{_random.Next(component.SelfKillCalloutAmount)}";
return Loc.GetString(selfCallout,
("victim", GetCalloutName(ev.Entity)));
}
var primary = GetCalloutName(ev.Primary);
var killerString = primary;
if (ev.Assist != null)
{
var secondary = GetCalloutName(ev.Assist);
killerString = Loc.GetString("death-match-assist",
("primary", primary), ("secondary", secondary));
}
var callout = $"{component.KillCalloutPrefix}{_random.Next(component.KillCalloutAmount)}";
return Loc.GetString(callout, ("killer", killerString),
("victim", GetCalloutName(ev.Entity)));
}
private string GetCalloutName(KillSource source)
{
switch (source)
{
case KillPlayerSource player:
if (!_playerManager.TryGetSessionById(player.PlayerId, out var session))
break;
if (session.AttachedEntity == null)
break;
return Loc.GetString("death-match-name-player",
("name", MetaData(session.AttachedEntity.Value).EntityName),
("username", session.Name));
case KillNpcSource npc:
if (Deleted(npc.NpcEnt))
return string.Empty;
return Loc.GetString("death-match-name-npc", ("name", MetaData(npc.NpcEnt).EntityName));
}
return string.Empty;
}
private string GetCalloutName(EntityUid source)
{
if (TryComp<ActorComponent>(source, out var actorComp))
{
return Loc.GetString("death-match-name-player",
("name", MetaData(source).EntityName),
("username", actorComp.PlayerSession.Name));
}
return Loc.GetString("death-match-name-npc", ("name", MetaData(source).EntityName));
}
}

View File

@@ -0,0 +1,145 @@
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Server.Mind.Components;
using Content.Server.Mind.Toolshed;
using Content.Server.Players;
using Content.Server.Station.Systems;
using Content.Shared.Chat;
using Content.Shared.Interaction.Events;
using Content.Shared.Mobs;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.GameTicking.Rules;
/// <summary>
/// This handles logic and interactions related to <see cref="RespawnDeadRuleComponent"/>
/// </summary>
public sealed class RespawnRuleSystem : GameRuleSystem<RespawnDeadRuleComponent>
{
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly StationSystem _station = default!;
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SuicideEvent>(OnSuicide);
SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged);
}
private void OnSuicide(SuicideEvent ev)
{
if (!TryComp<ActorComponent>(ev.Victim, out var actor))
return;
var query = EntityQueryEnumerator<RespawnTrackerComponent>();
while (query.MoveNext(out _, out var respawn))
{
respawn.Players.Remove(actor.PlayerSession.UserId);
}
QueueDel(ev.Victim);
}
private void OnMobStateChanged(MobStateChangedEvent args)
{
if (args.NewMobState == MobState.Alive)
return;
if (!TryComp<ActorComponent>(args.Target, out var actor))
return;
var query = EntityQueryEnumerator<RespawnDeadRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out _, out var rule))
{
if (!GameTicker.IsGameRuleActive(uid, rule))
continue;
if (RespawnPlayer(args.Target, uid, actor: actor))
break;
}
}
public override void Update(float frameTime)
{
base.Update(frameTime);
if (_station.GetStations().FirstOrNull() is not { } station)
return;
foreach (var tracker in EntityQuery<RespawnTrackerComponent>())
{
var queue = new Dictionary<NetUserId, TimeSpan>(tracker.RespawnQueue);
foreach (var (player, time) in queue)
{
if (_timing.CurTime < time)
continue;
if (!_playerManager.TryGetSessionById(player, out var session))
continue;
if (session.GetMind() is { } mind && TryComp<MindComponent>(mind, out var mindComp) && mindComp.OwnedEntity.HasValue)
QueueDel(mindComp.OwnedEntity.Value);
GameTicker.MakeJoinGame(session, station, silent: true);
tracker.RespawnQueue.Remove(player);
}
}
}
/// <summary>
/// Adds a given player to the respawn tracker, ensuring that they are respawned if they die.
/// </summary>
public void AddToTracker(EntityUid player, EntityUid tracker, RespawnTrackerComponent? component = null, ActorComponent? actor = null)
{
if (!Resolve(tracker, ref component) || !Resolve(player, ref actor, false))
return;
AddToTracker(actor.PlayerSession.UserId, tracker, component);
}
/// <summary>
/// Adds a given player to the respawn tracker, ensuring that they are respawned if they die.
/// </summary>
public void AddToTracker(NetUserId id, EntityUid tracker, RespawnTrackerComponent? component = null)
{
if (!Resolve(tracker, ref component))
return;
component.Players.Add(id);
}
/// <summary>
/// Attempts to directly respawn a player, skipping the lobby screen.
/// </summary>
public bool RespawnPlayer(EntityUid player, EntityUid respawnTracker, RespawnTrackerComponent? component = null, ActorComponent? actor = null)
{
if (!Resolve(respawnTracker, ref component) || !Resolve(player, ref actor, false))
return false;
if (!component.Players.Contains(actor.PlayerSession.UserId) || component.RespawnQueue.ContainsKey(actor.PlayerSession.UserId))
return false;
if (component.RespawnDelay == TimeSpan.Zero)
{
if (_station.GetStations().FirstOrNull() is not { } station)
return false;
QueueDel(player);
GameTicker.MakeJoinGame(actor.PlayerSession, station, silent: true);
return false;
}
var msg = Loc.GetString("rule-respawn-in-seconds", ("second", component.RespawnDelay.TotalSeconds));
var wrappedMsg = Loc.GetString("chat-manager-server-wrap-message", ("message", msg));
_chatManager.ChatMessageToOne(ChatChannel.Server, msg, wrappedMsg, respawnTracker, false, actor.PlayerSession.ConnectedClient, Color.LimeGreen);
component.RespawnQueue[actor.PlayerSession.UserId] = _timing.CurTime + component.RespawnDelay;
return true;
}
}

View File

@@ -0,0 +1,62 @@
using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
using Robust.Shared.Network;
namespace Content.Server.KillTracking;
/// <summary>
/// This is used for entities that track player damage sources and killers.
/// </summary>
[RegisterComponent, Access(typeof(KillTrackingSystem))]
public sealed partial class KillTrackerComponent : Component
{
/// <summary>
/// The mobstate that registers as a "kill"
/// </summary>
[DataField("killState")]
public MobState KillState = MobState.Critical;
/// <summary>
/// A dictionary of sources and how much damage they've done to this entity over time.
/// </summary>
[DataField("lifetimeDamage")]
public Dictionary<KillSource, FixedPoint2> LifetimeDamage = new();
}
public abstract record KillSource;
/// <summary>
/// A kill source for players
/// </summary>
[DataDefinition, Serializable]
public sealed partial record KillPlayerSource : KillSource
{
[DataField("playerId")]
public NetUserId PlayerId;
public KillPlayerSource(NetUserId playerId)
{
PlayerId = playerId;
}
}
/// <summary>
/// A kill source for non-player controlled entities
/// </summary>
[DataDefinition, Serializable]
public sealed partial record KillNpcSource : KillSource
{
[DataField("npcEnt")]
public EntityUid NpcEnt;
public KillNpcSource(EntityUid npcEnt)
{
NpcEnt = npcEnt;
}
}
/// <summary>
/// A kill source for kills with no damage origin
/// </summary>
[DataDefinition, Serializable]
public sealed partial record KillEnvironmentSource : KillSource;

View File

@@ -0,0 +1,129 @@
using Content.Server.NPC.HTN;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
using Robust.Server.GameObjects;
namespace Content.Server.KillTracking;
/// <summary>
/// This handles <see cref="KillTrackerComponent"/> and recording who is damaging and killing entities.
/// </summary>
public sealed class KillTrackingSystem : EntitySystem
{
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<KillTrackerComponent, DamageChangedEvent>(OnDamageChanged);
SubscribeLocalEvent<KillTrackerComponent, MobStateChangedEvent>(OnMobStateChanged);
}
private void OnDamageChanged(EntityUid uid, KillTrackerComponent component, DamageChangedEvent args)
{
if (args.DamageDelta == null)
return;
if (!args.DamageIncreased)
{
foreach (var key in component.LifetimeDamage.Keys)
{
component.LifetimeDamage[key] -= args.DamageDelta.Total;
}
return;
}
var source = GetKillSource(args.Origin);
var damage = component.LifetimeDamage.GetValueOrDefault(source);
component.LifetimeDamage[source] = damage + args.DamageDelta.Total;
}
private void OnMobStateChanged(EntityUid uid, KillTrackerComponent component, MobStateChangedEvent args)
{
if (args.NewMobState != component.KillState || args.OldMobState >= args.NewMobState)
return;
// impulse is the entity that did the finishing blow.
var killImpulse = GetKillSource(args.Origin);
// source is the kill tracker source with the most damage dealt.
var largestSource = GetLargestSource(component.LifetimeDamage);
largestSource ??= killImpulse;
KillSource? killSource;
KillSource? assistSource = null;
if (killImpulse is KillEnvironmentSource)
{
// if the kill was environmental, whatever did the most damage gets the kill.
killSource = largestSource;
}
else if (killImpulse == largestSource)
{
// if the impulse and the source are the same, there's no assist
killSource = killImpulse;
}
else
{
// the impulse gets the kill and the most damage gets the assist
killSource = killImpulse;
// no assist is given to environmental kills
if (largestSource is not KillEnvironmentSource)
{
// you have to do at least 50% of largest source's damage to get the assist.
if (component.LifetimeDamage[largestSource] >= component.LifetimeDamage[killSource] / 2)
{
assistSource = largestSource;
}
}
}
// it's a suicide if:
// - you caused your own death
// - the kill source was the entity that died
// - the entity that died had an assist on themselves
var suicide = args.Origin == uid ||
killSource is KillNpcSource npc && npc.NpcEnt == uid ||
killSource is KillPlayerSource player && player.PlayerId == CompOrNull<ActorComponent>(uid)?.PlayerSession.UserId ||
assistSource is KillNpcSource assistNpc && assistNpc.NpcEnt == uid ||
assistSource is KillPlayerSource assistPlayer && assistPlayer.PlayerId == CompOrNull<ActorComponent>(uid)?.PlayerSession.UserId;
var ev = new KillReportedEvent(uid, killSource, assistSource, suicide);
RaiseLocalEvent(uid, ref ev, true);
}
private KillSource GetKillSource(EntityUid? sourceEntity)
{
if (TryComp<ActorComponent>(sourceEntity, out var actor))
return new KillPlayerSource(actor.PlayerSession.UserId);
if (HasComp<HTNComponent>(sourceEntity))
return new KillNpcSource(sourceEntity.Value);
return new KillEnvironmentSource();
}
private KillSource? GetLargestSource(Dictionary<KillSource, FixedPoint2> lifetimeDamages)
{
KillSource? maxSource = null;
var maxDamage = FixedPoint2.Zero;
foreach (var (source, damage) in lifetimeDamages)
{
if (damage < maxDamage)
continue;
maxSource = source;
maxDamage = damage;
}
return maxSource;
}
}
/// <summary>
/// Event broadcasted and raised by-ref on an entity with <see cref="KillTrackerComponent"/> when they are killed.
/// </summary>
/// <param name="Entity">The entity that was killed</param>
/// <param name="Primary">The primary source of the kill</param>
/// <param name="Assist">A secondary source of the kill. Can be null.</param>
/// <param name="Suicide">True if the entity that was killed caused their own death.</param>
[ByRefEvent]
public readonly record struct KillReportedEvent(EntityUid Entity, KillSource Primary, KillSource? Assist, bool Suicide);

View File

@@ -85,7 +85,7 @@ public sealed class GameMapManager : IGameMapManager
var poolPrototype = _entityManager.System<GameTicker>().Preset?.MapPool ??
_configurationManager.GetCVar(CCVars.GameMapPool);
if (_prototypeManager.TryIndex<GameMapPoolPrototype>(_configurationManager.GetCVar(CCVars.GameMapPool), out var pool))
if (_prototypeManager.TryIndex<GameMapPoolPrototype>(poolPrototype, out var pool))
{
foreach (var map in pool.Maps)
{

View File

@@ -84,10 +84,18 @@ public sealed class HealingSystem : EntitySystem
var total = healed?.Total ?? FixedPoint2.Zero;
// Re-verify that we can heal the damage.
_stacks.Use(args.Used.Value, 1);
if (_stacks.GetCount(args.Used.Value) <= 0)
dontRepeat = true;
if (TryComp<StackComponent>(args.Used.Value, out var stackComp))
{
_stacks.Use(args.Used.Value, 1, stackComp);
if (_stacks.GetCount(args.Used.Value, stackComp) <= 0)
dontRepeat = true;
}
else
{
QueueDel(args.Used.Value);
}
if (uid != args.User)
{
@@ -157,7 +165,7 @@ public sealed class HealingSystem : EntitySystem
if (user != target && !_interactionSystem.InRangeUnobstructed(user, target, popup: true))
return false;
if (!TryComp<StackComponent>(uid, out var stack) || stack.Count < 1)
if (TryComp<StackComponent>(uid, out var stack) && stack.Count < 1)
return false;
if (!TryComp<BloodstreamComponent>(target, out var bloodstream))

View File

@@ -0,0 +1,96 @@
using System.Linq;
using Content.Shared.FixedPoint;
using Content.Shared.Points;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.GameStates;
using Robust.Server.Player;
using Robust.Shared.GameStates;
using Robust.Shared.Utility;
namespace Content.Server.Points;
/// <inheritdoc/>
public sealed class PointSystem : SharedPointSystem
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly PvsOverrideSystem _pvsOverride = default!;
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PointManagerComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<PointManagerComponent, ComponentGetState>(OnGetState);
}
private void OnStartup(EntityUid uid, PointManagerComponent component, ComponentStartup args)
{
_pvsOverride.AddGlobalOverride(uid);
}
private void OnGetState(EntityUid uid, PointManagerComponent component, ref ComponentGetState args)
{
args.State = new PointManagerComponentState(component.Points, component.Scoreboard);
}
/// <summary>
/// Adds the specified point value to a player.
/// </summary>
[PublicAPI]
public void AdjustPointValue(EntityUid user, FixedPoint2 value, EntityUid uid, PointManagerComponent? component, ActorComponent? actor = null)
{
if (!Resolve(uid, ref component) || !Resolve(user, ref actor, false))
return;
AdjustPointValue(actor.PlayerSession.UserId, value, uid, component);
}
/// <summary>
/// Sets the amount of points for a player
/// </summary>
[PublicAPI]
public void SetPointValue(EntityUid user, FixedPoint2 value, EntityUid uid, PointManagerComponent? component, ActorComponent? actor = null)
{
if (!Resolve(uid, ref component) || !Resolve(user, ref actor, false))
return;
SetPointValue(actor.PlayerSession.UserId, value, uid, component);
}
/// <summary>
/// Gets the amount of points for a given player
/// </summary>
[PublicAPI]
public FixedPoint2 GetPointValue(EntityUid user, EntityUid uid, PointManagerComponent? component, ActorComponent? actor = null)
{
if (!Resolve(uid, ref component) || !Resolve(user, ref actor, false))
return FixedPoint2.Zero;
return GetPointValue(actor.PlayerSession.UserId, uid, component);
}
/// <inheritdoc/>
public override FormattedMessage GetScoreboard(EntityUid uid, PointManagerComponent? component = null)
{
var msg = new FormattedMessage();
if (!Resolve(uid, ref component))
return msg;
var orderedPlayers = component.Points.OrderByDescending(p => p.Value).ToList();
var place = 1;
foreach (var (id, points) in orderedPlayers)
{
if (!_player.TryGetPlayerData(id, out var data))
continue;
msg.AddMarkup(Loc.GetString("point-scoreboard-list",
("place", place),
("name", data.UserName),
("points", points.Int())));
msg.PushNewline();
place++;
}
return msg;
}
}

View File

@@ -26,6 +26,9 @@ public sealed class JobSystem : EntitySystem
private void MindOnDoGreeting(EntityUid mindId, MindComponent component, ref MindRoleAddedEvent args)
{
if (args.Silent)
return;
if (!_mind.TryGetSession(mindId, out var session))
return;

View File

@@ -5,4 +5,4 @@
/// <see cref="RoleAddedEvent"/> for the one raised on player entities.
/// </summary>
[ByRefEvent]
public readonly record struct MindRoleAddedEvent;
public readonly record struct MindRoleAddedEvent(bool Silent);

View File

@@ -9,4 +9,4 @@ namespace Content.Server.Roles;
/// <param name="MindId">The mind id associated with the player.</param>
/// <param name="Mind">The mind component associated with the mind id.</param>
/// <param name="Antagonist">Whether or not the role makes the player an antagonist.</param>
public sealed record RoleAddedEvent(EntityUid MindId, MindComponent Mind, bool Antagonist) : RoleEvent(MindId, Mind, Antagonist);
public sealed record RoleAddedEvent(EntityUid MindId, MindComponent Mind, bool Antagonist, bool Silent = false) : RoleEvent(MindId, Mind, Antagonist);

View File

@@ -66,11 +66,12 @@ public sealed class RoleSystem : EntitySystem
/// <param name="mindId">The mind to add the role to.</param>
/// <param name="component">The role instance to add.</param>
/// <typeparam name="T">The role type to add.</typeparam>
/// <param name="silent">Whether or not the role should be added silently</param>
/// <returns>The instance of the role.</returns>
/// <exception cref="ArgumentException">
/// Thrown if we already have a role with this type.
/// </exception>
public void MindAddRole<T>(EntityUid mindId, T component, MindComponent? mind = null) where T : Component, new()
public void MindAddRole<T>(EntityUid mindId, T component, MindComponent? mind = null, bool silent = false) where T : Component, new()
{
if (!Resolve(mindId, ref mind))
return;
@@ -86,7 +87,7 @@ public sealed class RoleSystem : EntitySystem
var mindEv = new MindRoleAddedEvent();
RaiseLocalEvent(mindId, ref mindEv);
var message = new RoleAddedEvent(mindId, mind, antagonist);
var message = new RoleAddedEvent(mindId, mind, antagonist, silent);
if (mind.OwnedEntity != null)
{
RaiseLocalEvent(mind.OwnedEntity.Value, message, true);

View File

@@ -201,7 +201,7 @@ namespace Content.Server.RoundEnd
RaiseLocalEvent(RoundEndSystemChangedEvent.Default);
}
public void EndRound()
public void EndRound(TimeSpan? countdownTime = null)
{
if (_gameTicker.RunLevel != GameRunLevel.InRound) return;
LastCountdownStart = null;
@@ -211,17 +211,17 @@ namespace Content.Server.RoundEnd
_countdownTokenSource?.Cancel();
_countdownTokenSource = new();
var countdownTime = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.RoundRestartTime));
countdownTime ??= TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.RoundRestartTime));
int time;
string unitsLocString;
if (countdownTime.TotalSeconds < 60)
if (countdownTime.Value.TotalSeconds < 60)
{
time = countdownTime.Seconds;
time = countdownTime.Value.Seconds;
unitsLocString = "eta-units-seconds";
}
else
{
time = countdownTime.Minutes;
time = countdownTime.Value.Minutes;
unitsLocString = "eta-units-minutes";
}
_chatManager.DispatchServerAnnouncement(
@@ -229,7 +229,7 @@ namespace Content.Server.RoundEnd
"round-end-system-round-restart-eta-announcement",
("time", time),
("units", Loc.GetString(unitsLocString))));
Timer.Spawn(countdownTime, AfterEndRoundRestart, _countdownTokenSource.Token);
Timer.Spawn(countdownTime.Value, AfterEndRoundRestart, _countdownTokenSource.Token);
}
private void AfterEndRoundRestart()