* some work * equip: done unequip: todo * unequipping done & refactored events * workin * movin * reee namespaces * stun * mobstate * fixes * some work on events * removes serverside itemcomp & misc fixes * work * smol merge fix * ports template to prototype & finishes ui * moves relay & adds containerenumerator * actions & cuffs * my god what is actioncode * more fixes * im loosing my grasp on reality * more fixes * more work * explosions * yes * more work * more fixes * merge master & misc fixed because i forgot to commit before merging master * more fixes * fixes * moar * more work * moar fixes * suffixmap * more work on client * motivation low * no. no containers * mirroring client to server * fixes * move serverinvcomp * serverinventorycomponent is dead * gaming * only strippable & ai left... * only ai and richtext left * fixes ai * fixes * fixes sprite layers * more fixes * resolves optional * yes * stable™️ * fixes * moar fixes * moar * fix some tests * lmao * no comment * good to merge™️ * fixes build but for real * adresses some reviews * adresses some more reviews * nullables, yo * fixes lobbyscreen * timid refactor to differentiate actor & target * adresses more reviews * more * my god what a mess * removed the rest of duplicates * removed duplicate slotflags and renamed shoes to feet * removes another unused one * yes * fixes lobby & makes tryunequip return unequipped item * fixes * some funny renames * fixes * misc improvements to attemptevents * fixes * merge fixes Co-authored-by: Paul Ritter <ritter.paul1@gmail.com>
274 lines
10 KiB
C#
274 lines
10 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Content.Server.Atmos.EntitySystems;
|
|
using Content.Server.Chat.Managers;
|
|
using Content.Server.Hands.Components;
|
|
using Content.Server.PDA;
|
|
using Content.Server.Players;
|
|
using Content.Server.Spawners.Components;
|
|
using Content.Server.Traitor;
|
|
using Content.Server.Traitor.Uplink;
|
|
using Content.Server.Traitor.Uplink.Account;
|
|
using Content.Server.Traitor.Uplink.Components;
|
|
using Content.Server.TraitorDeathMatch.Components;
|
|
using Content.Shared.CCVar;
|
|
using Content.Shared.Damage;
|
|
using Content.Shared.Damage.Prototypes;
|
|
using Content.Shared.Inventory;
|
|
using Content.Shared.MobState.Components;
|
|
using Content.Shared.PDA;
|
|
using Content.Shared.Roles;
|
|
using Content.Shared.Traitor.Uplink;
|
|
using Robust.Server.Player;
|
|
using Robust.Shared.Configuration;
|
|
using Robust.Shared.GameObjects;
|
|
using Robust.Shared.IoC;
|
|
using Robust.Shared.Localization;
|
|
using Robust.Shared.Log;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Random;
|
|
|
|
namespace Content.Server.GameTicking.Rules;
|
|
|
|
public class TraitorDeathMatchRuleSystem : GameRuleSystem
|
|
{
|
|
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
|
[Dependency] private readonly IChatManager _chatManager = default!;
|
|
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
|
[Dependency] private readonly MaxTimeRestartRuleSystem _restarter = default!;
|
|
[Dependency] private readonly InventorySystem _inventory = default!;
|
|
|
|
public override string Prototype => "TraitorDeathMatch";
|
|
|
|
public string PDAPrototypeName => "CaptainPDA";
|
|
public string BeltPrototypeName => "ClothingBeltJanitorFilled";
|
|
public string BackpackPrototypeName => "ClothingBackpackFilled";
|
|
|
|
private bool _safeToEndRound = false;
|
|
|
|
private readonly Dictionary<UplinkAccount, string> _allOriginalNames = new();
|
|
|
|
private const string TraitorPrototypeID = "Traitor";
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
|
|
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawned);
|
|
SubscribeLocalEvent<GhostAttemptHandleEvent>(OnGhostAttempt);
|
|
}
|
|
|
|
private void OnPlayerSpawned(PlayerSpawnCompleteEvent ev)
|
|
{
|
|
if (!Enabled)
|
|
return;
|
|
|
|
var session = ev.Player;
|
|
var startingBalance = _cfg.GetCVar(CCVars.TraitorDeathMatchStartingBalance);
|
|
|
|
// Yup, they're a traitor
|
|
var mind = session.Data.ContentData()?.Mind;
|
|
if (mind == null)
|
|
{
|
|
Logger.ErrorS("preset", "Failed getting mind for TDM player.");
|
|
return;
|
|
}
|
|
|
|
var antagPrototype = _prototypeManager.Index<AntagPrototype>(TraitorPrototypeID);
|
|
var traitorRole = new TraitorRole(mind, antagPrototype);
|
|
mind.AddRole(traitorRole);
|
|
|
|
// Delete anything that may contain "dangerous" role-specific items.
|
|
// (This includes the PDA, as everybody gets the captain PDA in this mode for true-all-access reasons.)
|
|
if (mind.OwnedEntity is {Valid: true} owned)
|
|
{
|
|
var victimSlots = new[] {"id", "belt", "back"};
|
|
foreach (var slot in victimSlots)
|
|
{
|
|
if(_inventory.TryUnequip(owned, slot, out var entityUid, true, true))
|
|
Del(entityUid.Value);
|
|
}
|
|
|
|
// Replace their items:
|
|
|
|
var ownedCoords = Transform(owned).Coordinates;
|
|
|
|
// pda
|
|
var newPDA = Spawn(PDAPrototypeName, ownedCoords);
|
|
_inventory.TryEquip(owned, newPDA, "id", true);
|
|
|
|
// belt
|
|
var newTmp = Spawn(BeltPrototypeName, ownedCoords);
|
|
_inventory.TryEquip(owned, newTmp, "belt", true);
|
|
|
|
// backpack
|
|
newTmp = Spawn(BackpackPrototypeName, ownedCoords);
|
|
_inventory.TryEquip(owned, newTmp, "back", true);
|
|
|
|
// Like normal traitors, they need access to a traitor account.
|
|
var uplinkAccount = new UplinkAccount(startingBalance, owned);
|
|
var accounts = EntityManager.EntitySysManager.GetEntitySystem<UplinkAccountsSystem>();
|
|
accounts.AddNewAccount(uplinkAccount);
|
|
|
|
EntityManager.EntitySysManager.GetEntitySystem<UplinkSystem>()
|
|
.AddUplink(owned, uplinkAccount, newPDA);
|
|
|
|
_allOriginalNames[uplinkAccount] = Name(owned);
|
|
|
|
// The PDA needs to be marked with the correct owner.
|
|
var pda = Comp<PDAComponent>(newPDA);
|
|
EntityManager.EntitySysManager.GetEntitySystem<PDASystem>().SetOwner(pda, Name(owned));
|
|
EntityManager.AddComponent<TraitorDeathMatchReliableOwnerTagComponent>(newPDA).UserId = mind.UserId;
|
|
}
|
|
|
|
// Finally, it would be preferable if they spawned as far away from other players as reasonably possible.
|
|
if (mind.OwnedEntity != null && FindAnyIsolatedSpawnLocation(mind, out var bestTarget))
|
|
{
|
|
Transform(mind.OwnedEntity.Value).Coordinates = bestTarget;
|
|
}
|
|
else
|
|
{
|
|
// The station is too drained of air to safely continue.
|
|
if (_safeToEndRound)
|
|
{
|
|
_chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-death-match-station-is-too-unsafe-announcement"));
|
|
_restarter.RoundMaxTime = TimeSpan.FromMinutes(1);
|
|
_restarter.RestartTimer();
|
|
_safeToEndRound = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnGhostAttempt(GhostAttemptHandleEvent ev)
|
|
{
|
|
if (!Enabled || ev.Handled)
|
|
return;
|
|
|
|
ev.Handled = true;
|
|
|
|
var mind = ev.Mind;
|
|
|
|
if (mind.OwnedEntity is {Valid: true} entity && TryComp(entity, out MobStateComponent? mobState))
|
|
{
|
|
if (mobState.IsCritical())
|
|
{
|
|
// TODO BODY SYSTEM KILL
|
|
var damage = new DamageSpecifier(_prototypeManager.Index<DamageTypePrototype>("Asphyxiation"), 100);
|
|
Get<DamageableSystem>().TryChangeDamage(entity, damage, true);
|
|
}
|
|
else if (!mobState.IsDead())
|
|
{
|
|
if (HasComp<HandsComponent>(entity))
|
|
{
|
|
ev.Result = false;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
var session = mind.Session;
|
|
if (session == null)
|
|
{
|
|
ev.Result = false;
|
|
return;
|
|
}
|
|
|
|
GameTicker.Respawn(session);
|
|
ev.Result = true;
|
|
}
|
|
|
|
private void OnRoundEndText(RoundEndTextAppendEvent ev)
|
|
{
|
|
if (!Enabled)
|
|
return;
|
|
|
|
var lines = new List<string>();
|
|
lines.Add(Loc.GetString("traitor-death-match-end-round-description-first-line"));
|
|
foreach (var uplink in EntityManager.EntityQuery<UplinkComponent>(true))
|
|
{
|
|
var uplinkAcc = uplink.UplinkAccount;
|
|
if (uplinkAcc != null && _allOriginalNames.ContainsKey(uplinkAcc))
|
|
{
|
|
lines.Add(Loc.GetString("traitor-death-match-end-round-description-entry",
|
|
("originalName", _allOriginalNames[uplinkAcc]),
|
|
("tcBalance", uplinkAcc.Balance)));
|
|
}
|
|
}
|
|
|
|
ev.AddLine(string.Join('\n', lines));
|
|
}
|
|
|
|
public override void Added()
|
|
{
|
|
_restarter.RoundMaxTime = TimeSpan.FromMinutes(30);
|
|
_restarter.RestartTimer();
|
|
_safeToEndRound = true;
|
|
}
|
|
|
|
public override void Removed()
|
|
{
|
|
}
|
|
|
|
// It would be nice if this function were moved to some generic helpers class.
|
|
private bool FindAnyIsolatedSpawnLocation(Mind.Mind ignoreMe, out EntityCoordinates bestTarget)
|
|
{
|
|
// Collate people to avoid...
|
|
var existingPlayerPoints = new List<EntityCoordinates>();
|
|
foreach (var player in _playerManager.ServerSessions)
|
|
{
|
|
var avoidMeMind = player.Data.ContentData()?.Mind;
|
|
if ((avoidMeMind == null) || (avoidMeMind == ignoreMe))
|
|
continue;
|
|
var avoidMeEntity = avoidMeMind.OwnedEntity;
|
|
if (avoidMeEntity == null)
|
|
continue;
|
|
if (TryComp(avoidMeEntity.Value, out MobStateComponent? mobState))
|
|
{
|
|
// Does have mob state component; if critical or dead, they don't really matter for spawn checks
|
|
if (mobState.IsCritical() || mobState.IsDead())
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
// Doesn't have mob state component. Assume something interesting is going on and don't count this as someone to avoid.
|
|
continue;
|
|
}
|
|
existingPlayerPoints.Add(Transform(avoidMeEntity.Value).Coordinates);
|
|
}
|
|
|
|
// Iterate over each possible spawn point, comparing to the existing player points.
|
|
// On failure, the returned target is the location that we're already at.
|
|
var bestTargetDistanceFromNearest = -1.0f;
|
|
// Need the random shuffle or it stuffs the first person into Atmospherics pretty reliably
|
|
var ents = EntityManager.EntityQuery<SpawnPointComponent>().Select(x => x.Owner).ToList();
|
|
_robustRandom.Shuffle(ents);
|
|
var foundATarget = false;
|
|
bestTarget = EntityCoordinates.Invalid;
|
|
var atmosphereSystem = EntitySystem.Get<AtmosphereSystem>();
|
|
foreach (var entity in ents)
|
|
{
|
|
if (!atmosphereSystem.IsTileMixtureProbablySafe(Transform(entity).Coordinates))
|
|
continue;
|
|
|
|
var distanceFromNearest = float.PositiveInfinity;
|
|
foreach (var existing in existingPlayerPoints)
|
|
{
|
|
if (Transform(entity).Coordinates.TryDistance(EntityManager, existing, out var dist))
|
|
distanceFromNearest = Math.Min(distanceFromNearest, dist);
|
|
}
|
|
if (bestTargetDistanceFromNearest < distanceFromNearest)
|
|
{
|
|
bestTarget = Transform(entity).Coordinates;
|
|
bestTargetDistanceFromNearest = distanceFromNearest;
|
|
foundATarget = true;
|
|
}
|
|
}
|
|
return foundATarget;
|
|
}
|
|
|
|
}
|