traitor reputation real (#2913)

* add reputation system

* add contracts button to PDA

* give traitors contracts

* add GetRandomObjective to SharedObjectivesSystem

* add TryRemoveObjective overload

* add everything needed for reputation store and objectives

* "ui"

* giant uplink reputation tagging

* prevent buying reputation-locked gear with uplink implant

* :trollface:

* more ui stuff

* :trollface:

* :trollface:

* remove default objectives

* :trollface:

* UI fixes

* stuff

* objective component changes

* add offerings and stuff to yml

* make some objectives work

* make rcd objective real

* more ui fix

* :trollface:

* ui usable

* bunch of low risk objectives

* syndie jail wip

* more wip

* massive amount of work

* add bad guidebooks

* prevent fultoning anchored things

* fixes

* disable claim buttons when no slots are open

* :trollface:

* :trollface:

* :trollface:

* update ui when a slot unlocks

* move rejecting to offerings, remove rescan (automatic now)

* add verb to attach fulton, fix

* dont get objective to ransom/kill someone you already ransomed

* make offering slots random + maximum count in rep level

* :trollface:

* rela

* update syndie jail

* more syndie jail update

* make marshal real

* fix

* fix power room power

* remove access from timer

* :trollface:

* engine

* allow syndicate items on syndie jail

* add ransom ui

* update map loading

* add ransom purchasing to cargo request console

* warn not error for loading

* ui fixes

* final fixy

* mg book fix

* final fixy II

* remove redundant Announcement word

* more []

* end ransom when leaving the jail

* :trollface:

* 10 tc for roundstart traitors

* make nuke core objective work for traitor

* guidebook gaming

* early merge of guidebook comments

* update bunch of objectives

* add min reputation to kill fellow traitor

* guidebook gaming

* evil

* giant stuff

* dont offer dupe objectives

* evil

* pronoun

* fix

* grr

* blacklist assist and DAGD from assist objective

* grr

* fix linter

* remove objectives from traitor rule test

* nuke disk anti-troll

* evil test

* add objectives test to find bad objective groups

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fixy

* fix

* fix stuff

* reword ransom announcement

* pro

* goida

---------

Signed-off-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: deltanedas <@deltanedas:kde.org>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
deltanedas 2025-05-12 22:16:22 +01:00 committed by GitHub
parent 3f0b91df49
commit ac1b82d406
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
137 changed files with 9199 additions and 244 deletions

View File

@ -1,5 +1,6 @@
using Content.Shared.Cargo;
using Content.Client.Cargo.UI;
using Content.Shared._DV.Traitor; // DeltaV
using Content.Shared.Cargo.BUI;
using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Events;
@ -92,6 +93,7 @@ namespace Content.Client.Cargo.BUI
};
_menu.OnOrderApproved += ApproveOrder;
_menu.OnOrderCanceled += RemoveOrder;
_menu.OnRansomPurchase += ent => SendMessage(new RansomPurchaseMessage(ent)); // DeltaV
_orderMenu.SubmitButton.OnPressed += (_) =>
{
if (AddOrder())
@ -140,6 +142,7 @@ namespace Content.Client.Cargo.BUI
_menu?.UpdateStation(station);
Populate(cState.Orders);
_menu?.UpdateRansoms(cState.Ransoms, BankBalance); // DeltaV
}
protected override void Dispose(bool disposing)

View File

@ -1,6 +1,7 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:traitor="clr-namespace:Content.Client._DV.Traitor.UI"
SetSize="600 600"
MinSize="600 600">
<BoxContainer Orientation="Vertical" Margin="15 5 15 10">
@ -16,6 +17,7 @@
<RichTextLabel Name="PointsLabel"
Text="$0" />
</BoxContainer>
<traitor:RansomContainer Name="RansomContainer"/> <!-- DeltaV - show ransomed mobs -->
<Control MinHeight="10"/>
<TabContainer Name="TabContainer" VerticalExpand="True">
<BoxContainer Orientation="Vertical" VerticalExpand="True">

View File

@ -1,6 +1,7 @@
using System.Linq;
using Content.Client.Cargo.Systems;
using Content.Client.UserInterface.Controls;
using Content.Shared._DV.Traitor; // DeltaV
using Content.Shared.Cargo;
using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Prototypes;
@ -32,6 +33,7 @@ namespace Content.Client.Cargo.UI
public event Action<ButtonEventArgs>? OnItemSelected;
public event Action<ButtonEventArgs>? OnOrderApproved;
public event Action<ButtonEventArgs>? OnOrderCanceled;
public event Action<NetEntity>? OnRansomPurchase; // DeltaV
public event Action<ProtoId<CargoAccountPrototype>?, int>? OnAccountAction;
@ -57,6 +59,7 @@ namespace Content.Client.Cargo.UI
SearchBar.OnTextChanged += OnSearchBarTextChanged;
Categories.OnItemSelected += OnCategoryItemSelected;
RansomContainer.OnPurchase += ent => OnRansomPurchase?.Invoke(ent); // DeltaV
if (entMan.TryGetComponent<CargoOrderConsoleComponent>(owner, out var orderConsole))
{
@ -238,6 +241,14 @@ namespace Content.Client.Cargo.UI
}
}
/// <summary>
/// DeltaV: Forwards new ransom data to the ransom container.
/// </summary>
public void UpdateRansoms(List<RansomData> ransoms, int balance)
{
RansomContainer.UpdateRansoms(ransoms, balance);
}
public void PopulateAccountActions()
{
if (!_entityManager.TryGetComponent<StationBankAccountComponent>(_station, out var bank) ||
@ -264,6 +275,7 @@ namespace Content.Client.Cargo.UI
public void UpdateStation(EntityUid station)
{
_station = station;
RansomContainer.UpdateStation(_station); // DeltaV
}
protected override void FrameUpdate(FrameEventArgs args)

View File

@ -1,4 +1,5 @@
using Content.Client.CartridgeLoader;
using Content.Shared._DV.Reputation; // DeltaV
using Content.Shared.CartridgeLoader;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.PDA;
@ -72,6 +73,8 @@ namespace Content.Client.PDA
SendMessage(new PdaLockUplinkMessage());
};
_menu.ContractsButton.OnPressed += _ => SendMessage(new PdaShowContractsMessage()); // DeltaV
_menu.OnProgramItemPressed += ActivateCartridge;
_menu.OnInstallButtonPressed += InstallCartridge;
_menu.OnUninstallButtonPressed += UninstallCartridge;

View File

@ -80,6 +80,13 @@
Visible="False"
Text="{Loc 'pda-bound-user-interface-lock-uplink-title'}"
Description="{Loc 'pda-bound-user-interface-lock-uplink-description'}"/>
<!-- Begin DeltaV Additions -->
<pda:PdaSettingsButton Name="ContractsButton"
Access="Public"
Visible="False"
Text="{Loc 'pda-bound-user-interface-contracts-title'}"
Description="{Loc 'pda-bound-user-interface-contracts-description'}"/>
<!-- End DeltaV Additions -->
</BoxContainer>
</ScrollContainer>
<BoxContainer Orientation="Vertical"

View File

@ -196,6 +196,7 @@ namespace Content.Client.PDA
ActivateMusicButton.Visible = state.CanPlayMusic;
ShowUplinkButton.Visible = state.HasUplink;
LockUplinkButton.Visible = state.HasUplink;
ContractsButton.Visible = state.HasUplink; // DeltaV
}
public void UpdateAvailablePrograms(List<(EntityUid, CartridgeComponent)> programs)

View File

@ -96,7 +96,7 @@ public sealed class FultonSystem : SharedFultonSystem
UpdateAppearance(uid, component);
}
protected override void UpdateAppearance(EntityUid uid, FultonedComponent component)
public override void UpdateAppearance(EntityUid uid, FultonedComponent component) // DeltaV - made public
{
if (!component.Effect.IsValid())
return;

View File

@ -0,0 +1,5 @@
using Content.Shared._DV.Objectives.Systems;
namespace Content.Client._DV.Objectives.Systems;
public sealed class ContractObjectiveSystem : SharedContractObjectiveSystem;

View File

@ -0,0 +1,9 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Orientation="Horizontal"
HorizontalExpand="true"
MinHeight="64"
Margin="5">
<Label Name="Title" HorizontalExpand="True"/> <!-- Set in constructor -->
<Button Name="CompleteButton" Text="Complete" Margin="5"/>
</BoxContainer>

View File

@ -0,0 +1,23 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._DV.Reputation.UI;
/// <summary>
/// Contract control placed in a slot that has a contract active.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class Contract : BoxContainer
{
public event Action? OnComplete;
public Contract(string title)
{
RobustXamlLoader.Load(this);
Title.Text = title;
CompleteButton.OnPressed += _ => OnComplete?.Invoke();
}
}

View File

@ -0,0 +1,17 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:rep="clr-namespace:Content.Client._DV.Reputation.UI"
Orientation="Horizontal"
HorizontalExpand="true"
Align="Center"
MinHeight="48"
Margin="5">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="5">
<Label Name="Title"/> <!-- Set in constructor -->
<rep:UnlockLabel Name="UnlockLabel"/>
</BoxContainer>
<BoxContainer Name="ButtonsContainer" Orientation="Vertical">
<Button Name="AcceptButton" Text="{Loc 'contracts-accept'}"/>
<Button Name="RejectButton" Text="{Loc 'contracts-reject'}"/>
</BoxContainer>
</BoxContainer>

View File

@ -0,0 +1,48 @@
using Content.Shared._DV.Reputation;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._DV.Reputation.UI;
/// <summary>
/// Contract control placed in a slot that with no contract.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class ContractOffering : BoxContainer
{
public event Action? OnAccept;
public event Action? OnReject;
public event Action? OnUnlock;
public bool AcceptDisabled
{
get
{
return AcceptButton.Disabled;
}
set
{
AcceptButton.Disabled = value;
}
}
public ContractOffering(OfferingSlot slot)
{
RobustXamlLoader.Load(this);
if (slot.Title is {} title)
Title.Text = title;
else
Title.Visible = false;
UnlockLabel.NextUnlock = slot.NextUnlock;
UnlockLabel.OnUnlock += () => OnUnlock?.Invoke();
OnUnlock += () => ButtonsContainer.Visible = true;
ButtonsContainer.Visible = !UnlockLabel.IsLocked;
AcceptButton.OnPressed += _ => OnAccept?.Invoke();
RejectButton.OnPressed += _ => OnReject?.Invoke();
}
}

View File

@ -0,0 +1,33 @@
using Content.Shared._DV.Reputation;
using Robust.Client.UserInterface;
namespace Content.Client._DV.Reputation.UI;
public sealed class ContractsBUI : BoundUserInterface
{
[ViewVariables]
private ContractsWindow? _window;
public ContractsBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
_window = this.CreateWindow<ContractsWindow>();
_window.Owner = Owner;
_window.OnAccept += i => SendMessage(new ContractsAcceptMessage(i));
_window.OnComplete += i => SendMessage(new ContractsCompleteMessage(i));
_window.OnReject += i => SendMessage(new ContractsRejectMessage(i));
_window.OnClose += Close;
_window.OpenCentered();
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is ContractsState)
_window?.UpdateState();
}
}

View File

@ -0,0 +1,20 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
MinSize="500 400"
Title="{Loc 'contracts-menu-title'}">
<BoxContainer Orientation="Vertical">
<controls:StripeBack>
<BoxContainer Orientation="Vertical">
<Label Name="Level" Align="Center" StyleClasses="LabelBig"/> <!-- Set at runtime -->
<Label Name="Reputation" Align="Center" StyleClasses="LabelBig"/> <!-- Set at runtime -->
</BoxContainer>
</controls:StripeBack>
<Label Text="{Loc 'contracts-contracts'}" Margin="5"/>
<controls:HLine Color="#404040" Thickness="2" Margin="0 5 0 5"/>
<BoxContainer Name="Contracts" Orientation="Vertical" HorizontalExpand="True" Margin="5"/> <!-- Populated at runtime -->
<controls:HLine Color="#404040" Thickness="2" Margin="0 5 0 5"/>
<Label Text="{Loc 'contracts-offerings'}" Align="Right" Margin="5"/>
<controls:HLine Color="#404040" Thickness="2" Margin="0 5 0 5"/>
<BoxContainer Name="Offerings" Orientation="Vertical" HorizontalExpand="True" Margin="5"/> <!-- Populated at runtime -->
</BoxContainer>
</controls:FancyWindow>

View File

@ -0,0 +1,77 @@
using Content.Client.UserInterface.Controls;
using Content.Shared._DV.Reputation;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._DV.Reputation.UI;
[GenerateTypedNameReferences]
public sealed partial class ContractsWindow : FancyWindow
{
[Dependency] private EntityManager _entMan = default!;
public event Action<int>? OnAccept;
public event Action<int>? OnComplete;
public event Action<int>? OnReject;
public EntityUid Owner;
public ContractsWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
}
public void UpdateState()
{
if (!_entMan.TryGetComponent<ContractsComponent>(Owner, out var comp))
return;
if (comp.CurrentLevel is {} level)
Level.Text = Loc.GetString(level.Name);
Reputation.Text = $"{comp.Reputation} Reputation";
Contracts.RemoveAllChildren();
var slotsFull = true;
for (int i = 0; i < comp.Slots.Count; i++)
{
var index = i;
if (comp.Slots[i].ObjectiveTitle is {} title)
{
var contract = new Contract(title);
contract.OnComplete += () => OnComplete?.Invoke(index);
Contracts.AddChild(contract);
// TODO: green when objective is complete
}
else
{
var empty = new EmptyContract(comp.Slots[i].NextUnlock);
empty.OnUnlock += EnableAccepts;
if (!empty.IsLocked)
slotsFull = false;
Contracts.AddChild(empty);
}
}
Offerings.RemoveAllChildren();
for (int i = 0; i < comp.OfferingSlots.Count; i++)
{
var index = i;
var offering = new ContractOffering(comp.OfferingSlots[i]);
offering.AcceptDisabled = slotsFull;
offering.OnAccept += () => OnAccept?.Invoke(index);
offering.OnReject += () => OnReject?.Invoke(index);
Offerings.AddChild(offering);
}
}
private void EnableAccepts()
{
foreach (var child in Offerings.Children)
{
((ContractOffering) child).AcceptDisabled = false;
}
}
}

View File

@ -0,0 +1,11 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:rep="clr-namespace:Content.Client._DV.Reputation.UI"
Orientation="Vertical"
HorizontalExpand="true"
Align="Center"
MinHeight="64"
Margin="5">
<Label Text="{Loc 'contract-slot-empty'}" HorizontalExpand="True"/>
<rep:UnlockLabel Name="UnlockLabel"/>
</BoxContainer>

View File

@ -0,0 +1,24 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._DV.Reputation.UI;
/// <summary>
/// Contract control placed in a slot that with no contract.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class EmptyContract : BoxContainer
{
public event Action? OnUnlock;
public bool IsLocked => UnlockLabel.IsLocked;
public EmptyContract(TimeSpan? nextUnlock)
{
RobustXamlLoader.Load(this);
UnlockLabel.NextUnlock = nextUnlock;
UnlockLabel.OnUnlock += () => OnUnlock?.Invoke();
}
}

View File

@ -0,0 +1,73 @@
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
namespace Content.Client._DV.Reputation.UI;
/// <summary>
/// Label that updates for an unlock timer automatically.
/// </summary>
public sealed class UnlockLabel : Label
{
[Dependency] private readonly IGameTiming _timing = default!;
public event Action? OnUnlock;
private TimeSpan? _nextUnlock;
private TimeSpan _nextUpdate;
public TimeSpan? NextUnlock
{
get
{
return _nextUnlock;
}
set
{
_nextUnlock = value;
_nextUpdate = _timing.CurTime;
UpdateUnlock();
}
}
public bool IsLocked => Visible;
public UnlockLabel()
{
IoCManager.InjectDependencies(this);
Visible = false;
}
private void UpdateUnlock()
{
if (_nextUnlock is not {} next)
return;
var now = _timing.CurTime;
if (now >= next)
{
// unlocked now
_nextUnlock = null;
Visible = false;
OnUnlock?.Invoke();
return;
}
Visible = true;
var remaining = next - now;
var time = $"{remaining.Minutes:00}:{remaining.Seconds:00}";
Text = Loc.GetString("contract-next-unlock", ("time", time));
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
var now = _timing.CurTime;
if (now < _nextUpdate)
return;
_nextUpdate = now + TimeSpan.FromSeconds(1);
UpdateUnlock();
}
}

View File

@ -0,0 +1,5 @@
using Content.Shared._DV.Traitor;
namespace Content.Client._DV.Traitor;
public sealed class ExtractionFultonSystem : SharedExtractionFultonSystem;

View File

@ -0,0 +1,19 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Visible="False"
HorizontalExpand="True">
<controls:StripeBack HorizontalExpand="True">
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<BoxContainer Orientation="Horizontal" Align="Center">
<RichTextLabel Text="{Loc 'ransom-ui-warning'}"/>
</BoxContainer>
<PanelContainer HorizontalExpand="True" Margin="8">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#222" />
</PanelContainer.PanelOverride>
<BoxContainer Name="ListingsContainer" Orientation="Vertical"/> <!-- Populated at runtime with RansomListing -->
</PanelContainer>
</BoxContainer>
</controls:StripeBack>
</BoxContainer>

View File

@ -0,0 +1,47 @@
using Content.Shared._DV.Traitor;
using Content.Shared.Cargo.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._DV.Traitor.UI;
[GenerateTypedNameReferences]
public sealed partial class RansomContainer : BoxContainer
{
[Dependency] private readonly IEntityManager _entMan = default!;
public event Action<NetEntity>? OnPurchase;
public RansomContainer()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
}
public void UpdateRansoms(List<RansomData> ransoms, int balance)
{
Visible = ransoms.Count > 0;
ListingsContainer.RemoveAllChildren();
foreach (var ransom in ransoms)
{
var listing = new RansomListing(ransom);
listing.UpdateBalance(balance);
listing.OnPurchase += ent => OnPurchase?.Invoke(ent);
ListingsContainer.AddChild(listing);
}
}
public void UpdateStation(EntityUid? station)
{
if (!_entMan.TryGetComponent<StationBankAccountComponent>(station, out var bank))
return;
var balance = bank.Accounts[bank.PrimaryAccount];
foreach (var control in ListingsContainer.Children)
{
if (control is RansomListing listing)
listing.UpdateBalance(balance);
}
}
}

View File

@ -0,0 +1,5 @@
<BoxContainer xmlns="https://spacestation14.io"
MinHeight="64" HorizontalExpand="True">
<Button Name="PurchaseButton" HorizontalExpand="True" Disabled="True"/>
<Label Name="PriceLabel" Margin="8" MinWidth="64" Align="Center"/>
</BoxContainer>

View File

@ -0,0 +1,33 @@
using Content.Shared._DV.Traitor;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._DV.Traitor.UI;
[GenerateTypedNameReferences]
public sealed partial class RansomListing : BoxContainer
{
public event Action<NetEntity>? OnPurchase;
private NetEntity _ent;
private int _price;
public RansomListing(RansomData data)
{
RobustXamlLoader.Load(this);
_ent = data.Entity;
_price = data.Price;
PurchaseButton.Text = Loc.GetString("ransom-ui-purchase", ("name", data.Name));
PriceLabel.Text = "$" + _price;
PurchaseButton.OnPressed += _ => OnPurchase?.Invoke(_ent);
}
public void UpdateBalance(int balance)
{
PurchaseButton.Disabled = balance < _price;
}
}

View File

@ -21,7 +21,7 @@ public sealed class TraitorRuleTest
private const string TraitorGameRuleProtoId = "Traitor";
private const string TraitorAntagRoleName = "Traitor";
[Test]
//[Test] // DeltaV - test is useless with reputation
public async Task TestTraitorObjectives()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings()

View File

@ -0,0 +1,56 @@
using Content.Shared._DV.Reputation;
using Content.Shared.Objectives.Components;
using Content.Shared.Prototypes;
using Content.Shared.Random;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
using System.Collections.Generic;
namespace Content.IntegrationTests.Tests._DV;
/// <summary>
/// Checks that all ids in objective groups are valid
/// Objectives system just does nothing if you have a nonexistent prototype in an objective group.
/// </summary>
public sealed class ObjectivesTest
{
[Test]
public async Task AllReputationObjectivesValid()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var protoMan = server.ResolveDependency<IPrototypeManager>();
var factory = server.ResolveDependency<IComponentFactory>();
var pools = new HashSet<WeightedRandomPrototype>();
foreach (var level in protoMan.EnumeratePrototypes<ReputationLevelPrototype>())
{
pools.Add(protoMan.Index(level.OfferingGroups));
}
await server.WaitPost(() =>
{
// 3 nested loops we gaming
Assert.Multiple(() =>
{
foreach (var pool in pools)
{
foreach (var id in pool.Weights.Keys)
{
Assert.That(protoMan.TryIndex<WeightedRandomPrototype>(id, out var proto),
$"Unknown objective group {id} found in offering group {pool.ID}");
foreach (var obj in proto.Weights.Keys)
{
Assert.That(protoMan.TryIndex<EntityPrototype>(obj, out var objProto),
$"Unknown objective {obj} found in objective group {id}");
Assert.That(objProto.TryGetComponent<ObjectiveComponent>(out _, factory),
$"Entity {obj} in objective group {id}");
}
}
}
});
});
await pair.CleanReturnAsync();
}
}

View File

@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Cargo.Components;
using Content.Server.Station.Components;
using Content.Shared._DV.Traitor; // DeltaV
using Content.Shared.Cargo;
using Content.Shared.Cargo.BUI;
using Content.Shared.Cargo.Components;
@ -24,6 +25,7 @@ namespace Content.Server.Cargo.Systems
{
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly RansomSystem _ransom = default!; // DeltaV
private void InitializeConsole()
{
@ -419,7 +421,8 @@ namespace Content.Server.Cargo.Systems
GetOutstandingOrderCount(orderDatabase, console.Account),
orderDatabase.Capacity,
GetNetEntity(station.Value),
orderDatabase.Orders[console.Account]
orderDatabase.Orders[console.Account],
_ransom.GetRansoms() // DeltaV
));
}
}

View File

@ -69,6 +69,7 @@ public sealed partial class CargoSystem : SharedCargoSystem
InitializeBounty();
InitializeFunds();
InitializeATS(); // DeltaV
InitializeRansom(); // DeltaV
}
public override void Update(float frameTime)

View File

@ -6,6 +6,7 @@ using Content.Server.Objectives;
using Content.Server.PDA.Ringer;
using Content.Server.Roles;
using Content.Server.Traitor.Uplink;
using Content.Shared._DV.Reputation; // DeltaV
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.GameTicking.Components;
@ -34,6 +35,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ReputationSystem _reputation = default!; // DeltaV
[Dependency] private readonly SharedRoleCodewordSystem _roleCodewordSystem = default!;
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
[Dependency] private readonly UplinkSystem _uplink = default!;
@ -186,6 +188,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
// Codes are only generated if the uplink is a PDA
var ev = new GenerateUplinkCodeEvent();
RaiseLocalEvent(pda.Value, ref ev);
_reputation.AddContracts(traitor, pda.Value); // DeltaV
if (ev.Code is { } generatedCode)
{

View File

@ -237,7 +237,8 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
}
}
public EntityUid? GetRandomObjective(EntityUid mindId, MindComponent mind, ProtoId<WeightedRandomPrototype> objectiveGroupProto, float maxDifficulty)
// DeltaV - override shared method
public override EntityUid? GetRandomObjective(EntityUid mindId, MindComponent mind, ProtoId<WeightedRandomPrototype> objectiveGroupProto, float maxDifficulty)
{
if (!_prototypeManager.TryIndex(objectiveGroupProto, out var groupsProto))
{

View File

@ -92,7 +92,7 @@ public sealed class PickObjectiveTargetSystem : EntitySystem
/// DeltaV - Common code deduplicated from above functions.
/// Filters all alive humans and picks a target from them.
/// </summary>
private void AssignRandomTarget(EntityUid uid, ref ObjectiveAssignedEvent args, Predicate<EntityUid> filter, bool fallbackToAny = true)
public void AssignRandomTarget(EntityUid uid, ref ObjectiveAssignedEvent args, Predicate<EntityUid> filter, bool fallbackToAny = true)
{
// invalid prototype
if (!TryComp<TargetObjectiveComponent>(uid, out var target))
@ -124,6 +124,20 @@ public sealed class PickObjectiveTargetSystem : EntitySystem
}
}
// Begin DeltaV Additions - no being asked to kill someone at centcom or whatever
if (args.Mind.OwnedEntity is {} mob)
{
var map = Transform(mob).MapID;
allHumans.RemoveAll(mindId =>
{
if (Comp<MindComponent>(mindId).OwnedEntity is {} otherMob)
return Transform(otherMob).MapID != map;
return false;
});
}
// End DeltaV Additions
// Filter out targets based on the filter
var filteredHumans = allHumans.Where(mind => filter(mind)).ToList();

View File

@ -7,6 +7,7 @@ using Content.Server.PDA.Ringer;
using Content.Server.Station.Systems;
using Content.Server.Store.Systems;
using Content.Server.Traitor.Uplink;
using Content.Shared._DV.Reputation; // DeltaV
using Content.Shared.Access.Components;
using Content.Shared.CartridgeLoader;
using Content.Shared.Chat;
@ -35,6 +36,7 @@ namespace Content.Server.PDA
[Dependency] private readonly UnpoweredFlashlightSystem _unpoweredFlashlight = default!;
[Dependency] private readonly ContainerSystem _containerSystem = default!;
[Dependency] private readonly IdCardSystem _idCard = default!;
[Dependency] private readonly ReputationSystem _reputation = default!; // DeltaV
public override void Initialize()
{
@ -50,6 +52,7 @@ namespace Content.Server.PDA
SubscribeLocalEvent<PdaComponent, PdaShowMusicMessage>(OnUiMessage);
SubscribeLocalEvent<PdaComponent, PdaShowUplinkMessage>(OnUiMessage);
SubscribeLocalEvent<PdaComponent, PdaLockUplinkMessage>(OnUiMessage);
SubscribeLocalEvent<PdaComponent, PdaShowContractsMessage>(OnShowContracts); // DeltaV
SubscribeLocalEvent<PdaComponent, CartridgeLoaderNotificationSentEvent>(OnNotification);
@ -278,6 +281,18 @@ namespace Content.Server.PDA
}
}
// End DeltaV Additions
private void OnShowContracts(EntityUid uid, PdaComponent pda, PdaShowContractsMessage msg)
{
if (!PdaUiKey.Key.Equals(msg.UiKey))
return;
// check if its locked again to prevent malicious clients opening locked uplinks
if (HasComp<ContractsComponent>(uid) && IsUnlocked(uid))
_reputation.ToggleUI(msg.Actor, uid);
}
// End DeltaV Additions
private bool IsUnlocked(EntityUid uid)
{
return !TryComp<RingerUplinkComponent>(uid, out var uplink) || uplink.Unlocked;

View File

@ -1,4 +1,5 @@
using System.Numerics;
using Content.Shared._DV.Traitor; // DeltaV
using Content.Shared.Salvage.Fulton;
using Robust.Shared.Containers;
using Robust.Shared.Map;
@ -74,6 +75,10 @@ public sealed class FultonSystem : SharedFultonSystem
Coordinates = GetNetCoordinates(oldCoords),
});
}
// Begin DeltaV Additions: Event for syndicate fultons to use
var ev = new FultonedEvent();
RaiseLocalEvent(uid, ref ev);
// End DeltaV Additions
Audio.PlayPvs(component.Sound, uid);
RemCompDeferred<FultonedComponent>(uid);

View File

@ -0,0 +1,7 @@
namespace Content.Server._DV.Cabinet;
/// <summary>
/// Makes a cabinet count as a steal target if it has a steal target inside it.
/// </summary>
[RegisterComponent]
public sealed partial class StealTargetCabinetComponent : Component;

View File

@ -0,0 +1,29 @@
using Content.Shared.Objectives.Components;
using Robust.Shared.Containers;
namespace Content.Server._DV.Cabinet;
/// <summary>
/// Handles container events for <see cref="StealTargetCabinetComponent"/>.
/// </summary>
public sealed class StealTargetCabinetSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StealTargetCabinetComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
SubscribeLocalEvent<StealTargetCabinetComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
}
private void OnEntInserted(Entity<StealTargetCabinetComponent> ent, ref EntInsertedIntoContainerMessage args)
{
if (TryComp<StealTargetComponent>(args.Entity, out var target))
EnsureComp<StealTargetComponent>(ent).StealGroup = target.StealGroup;
}
private void OnEntRemoved(Entity<StealTargetCabinetComponent> ent, ref EntRemovedFromContainerMessage args)
{
RemComp<StealTargetComponent>(ent);
}
}

View File

@ -0,0 +1,139 @@
using Content.Server.Cargo.Components;
using Content.Server.Chat.Systems;
using Content.Server.Station.Components;
using Content.Shared._DV.Traitor;
using Content.Shared.Bed.Sleep;
using Content.Shared.Cargo;
using Content.Shared.Cargo.Components;
using Content.Shared.Database;
using Content.Shared.StatusEffect;
using Content.Shared.Storage.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Cargo.Systems;
/// <summary>
/// Handles purchasing ransomed entities from a cargo request console.
/// </summary>
public sealed partial class CargoSystem
{
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly SharedEntityStorageSystem _entityStorage = default!;
[Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
/// <summary>
/// The crate to put ransomed entities in when purchasing them.
/// </summary>
public static readonly EntProtoId RansomCrate = "CrateSyndicate";
/// <summary>
/// Status effect for <see cref="ForcedSleepingComponent"/>.
/// </summary>
public static readonly ProtoId<StatusEffectPrototype> StatusEffectKey = "ForcedSleep";
/// <summary>
/// Sound to play for the ransom victim when being "trafficked" to the ATS.
/// </summary>
public static readonly SoundSpecifier HypoSound = new SoundPathSpecifier("/Audio/Items/hypospray.ogg");
/// <summary>
/// How long to be slept for.
/// </summary>
public static readonly TimeSpan SleepyTime = TimeSpan.FromSeconds(10);
private void InitializeRansom()
{
Subs.BuiEvents<CargoOrderConsoleComponent>(CargoConsoleUiKey.Orders, subs =>
{
subs.Event<RansomPurchaseMessage>(OnPurchaseMessage);
});
}
private void OnPurchaseMessage(Entity<CargoOrderConsoleComponent> ent, ref RansomPurchaseMessage args)
{
var user = args.Actor;
if (!_accessReaderSystem.IsAllowed(user, ent))
{
ConsolePopup(user, Loc.GetString("cargo-console-order-not-allowed"));
PlayDenySound(ent, ent);
return;
}
// malf client or they somehow got gibbed in jail
if (GetEntity(args.Entity) is not { Valid: true } uid ||
// got released already
!TryComp<RansomComponent>(uid, out var ransom) ||
// not on a station
_station.GetOwningStation(uid) is not {} station ||
!TryComp<StationBankAccountComponent>(station, out var bank) ||
!TryComp<StationDataComponent>(station, out var stationData))
{
ConsolePopup(user, Loc.GetString("cargo-console-station-not-found"));
PlayDenySound(ent, ent);
return;
}
var cost = ransom.Ransom;
var balance = bank.Accounts[bank.PrimaryAccount];
if (cost > balance)
{
ConsolePopup(user, Loc.GetString("cargo-console-insufficient-funds", ("cost", cost)));
PlayDenySound(ent, ent);
return;
}
// paid the ransom, time to bring em home
if (TryReturnEntity(uid, stationData) is not {} trade)
{
ConsolePopup(user, Loc.GetString("cargo-console-unfulfilled"));
PlayDenySound(ent, ent);
return;
}
_audio.PlayPvs(ApproveSound, ent);
_adminLogger.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(user):user} paid the ransom of ${cost} for {ToPrettyString(uid)} with balance at {balance}");
UpdateBankAccount((station, bank), -cost, CreateAccountDistribution((station, bank)));
// announce it so everyone knows
var msg = Loc.GetString("syndicate-ransom-return-announcement", ("station", trade));
var sender = Loc.GetString("syndicate-ransom-return-announcement-sender");
var sound = new SoundPathSpecifier("/Audio/Misc/notice1.ogg");
var color = Color.Red;
_chat.DispatchGlobalAnnouncement(msg, sender, playSound: true, sound, color);
}
// like TryFulfillOrder but for ransoms
private EntityUid? TryReturnEntity(EntityUid uid, StationDataComponent station)
{
_listEnts.Clear();
GetTradeStations(station, ref _listEnts);
// Try to fulfill from any station where possible, if the pad is not occupied.
foreach (var trade in _listEnts)
{
var tradePads = GetCargoPallets(trade, BuySellType.Buy);
_random.Shuffle(tradePads);
var freePads = GetFreeCargoPallets(trade, tradePads);
if (freePads.Count == 0)
continue;
// sleepy time
_audio.PlayPvs(HypoSound, uid);
_statusEffects.TryAddStatusEffect<ForcedSleepingComponent>(uid, StatusEffectKey, SleepyTime, refresh: false);
var pad = _random.Pick(freePads);
var coordinates = new EntityCoordinates(trade, pad.Transform.LocalPosition);
var crate = Spawn(RansomCrate, coordinates);
if (!_entityStorage.Insert(uid, crate))
_transformSystem.DropNextTo(uid, crate); // just teleport directly if it somehow fails
return trade;
}
return null;
}
}

View File

@ -0,0 +1,111 @@
using Content.Server.Antag;
using Content.Server.Antag.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared._DV.Traitor;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nuke;
using Content.Shared.Popups;
namespace Content.Server._DV.Nuke;
/// <summary>
/// When a syndie extracts the nuke disk, gives it to nukies as soon as possible.
/// If nukies are taking years and a sleeper steals it, an arbitrary nukie gets it.
/// If there are no nukies it waits until a loneop spawns.
/// </summary>
public sealed class NukeDiskSystem : EntitySystem
{
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly MobStateSystem _mob = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NukeDiskComponent, FultonedEvent>(OnFultoned);
SubscribeLocalEvent<TeleportDiskRuleComponent, AfterAntagEntitySelectedEvent>(OnAntagEntSelected);
}
private void OnFultoned(Entity<NukeDiskComponent> ent, ref FultonedEvent args)
{
// no free win for using salv fultons
if (!HasComp<ExtractingComponent>(ent))
return;
// just incase another system somehow doesn't do it
RemCompDeferred<ExtractingComponent>(ent);
ent.Comp.Extracted = true;
// give it to an arbitrary nukie if a sleeper/whatever steals it
// everyone wins
if (FindLivingNukie() is {} target)
TeleportDisk(ent, target);
}
private void OnAntagEntSelected(Entity<TeleportDiskRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
{
if (FindExtractedDisk() is not {} disk)
return;
// this nukie is arbitrary but its probably definitely a loneop anyway
TeleportDisk(disk, args.EntityUid);
}
/// <summary>
/// Tries to give the disk to a living nukie.
/// </summary>
public void TeleportDisk(Entity<NukeDiskComponent> ent, EntityUid target)
{
if (!ent.Comp.Extracted)
return;
ent.Comp.Extracted = false; // no repeated teleports
_adminLogger.Add(LogType.Teleport, LogImpact.High, $"Teleported {ToPrettyString(ent):disk} to {ToPrettyString(target)} because it was extracted by a syndie");
_hands.PickupOrDrop(target, ent);
_popup.PopupEntity(Loc.GetString("nuke-disk-teleported", ("disk", ent)), target, target);
}
/// <summary>
/// Find a nuke disk that has been stolen by a syndie via extraction fulton.
/// </summary>
public Entity<NukeDiskComponent>? FindExtractedDisk()
{
var query = EntityQueryEnumerator<NukeDiskComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (comp.Extracted)
return (uid, comp);
}
return null;
}
/// <summary>
/// Find a living nukie mob to give the disk to.
/// </summary>
public EntityUid? FindLivingNukie()
{
var query = EntityQueryEnumerator<NukeopsRuleComponent, AntagSelectionComponent>();
while (query.MoveNext(out _, out _, out var comp))
{
foreach (var (mindId, _) in comp.AssignedMinds)
{
if (TryComp<MindComponent>(mindId, out var mind) &&
GetEntity(mind.OriginalOwnedEntity) is {} mob &&
_mob.IsAlive(mob))
{
return mob;
}
}
}
return null;
}
}

View File

@ -0,0 +1,8 @@
namespace Content.Server._DV.Nuke;
/// <summary>
/// Component added to a nukie gamerule to teleport a stolen nuke disk to the first nukie it spawns.
/// <see cref="NukeDiskSystem"/>.
/// </summary>
[RegisterComponent]
public sealed partial class TeleportDiskRuleComponent : Component;

View File

@ -0,0 +1,30 @@
using Content.Server._DV.Objectives.Systems;
using Content.Shared.Whitelist;
namespace Content.Server._DV.Objectives.Components;
/// <summary>
/// Picks a random contract of the target mind and requires that it be completed.
/// Requires that the objective has picked a target with at least 1 objective.
/// </summary>
[RegisterComponent, Access(typeof(AssistRandomContractSystem))]
public sealed partial class AssistRandomContractComponent : Component
{
/// <summary>
/// Description that gets "contract" passed.
/// </summary>
[DataField]
public LocId Description = "objective-condition-assist-traitor-description";
/// <summary>
/// Blacklist for objective entities that cannot be assisted with.
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
/// <summary>
/// The picked contract.
/// </summary>
[DataField]
public EntityUid? Contract;
}

View File

@ -0,0 +1,14 @@
using Content.Server._DV.Objectives.Systems;
namespace Content.Server._DV.Objectives.Components;
/// <summary>
/// Tracks which assist objectives are active for a given contract.
/// If this contract is failed all of the assists are failed too.
/// </summary>
[RegisterComponent, Access(typeof(AssistRandomContractSystem))]
public sealed partial class AssistedContractComponent : Component
{
[DataField]
public HashSet<EntityUid> Assisting = new();
}

View File

@ -0,0 +1,51 @@
using Content.Server._DV.Objectives.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.Store;
using Robust.Shared.Prototypes;
namespace Content.Server._DV.Objectives.Components;
/// <summary>
/// Makes this objective part of a syndicate contract, granting TC and reputation upon completion.
/// </summary>
[RegisterComponent, Access(typeof(ContractObjectiveSystem))]
public sealed partial class ContractObjectiveComponent : Component
{
/// <summary>
/// How much reputation to add when completed.
/// </summary>
[DataField]
public int Reputation;
/// <summary>
/// How much currency to give when completed.
/// </summary>
[DataField]
public FixedPoint2 Payment;
/// <summary>
/// Pay when the contract is taken but disable rejecting it.
/// </summary>
[DataField]
public bool Prepaid;
/// <summary>
/// Whether this contract can be rejected.
/// Funded contracts cannot be rejected to prevent infinite TC exploiting.
/// </summary>
[ViewVariables]
public bool Rejectable => !Prepaid;
/// <summary>
/// What currency to add.
/// </summary>
[DataField]
public ProtoId<CurrencyPrototype> Currency = "Telecrystal";
/// <summary>
/// The PDA used to take this contract.
/// Might not always exist.
/// </summary>
[DataField]
public EntityUid? Pda;
}

View File

@ -0,0 +1,39 @@
using Content.Server._DV.Objectives.Systems;
using Content.Shared.Objectives;
using Robust.Shared.Prototypes;
namespace Content.Server._DV.Objectives.Components;
/// <summary>
/// Requires that you extract an item using syndicate fultons.
/// This effectively removes it from the round.
/// </summary>
[RegisterComponent, Access(typeof(ExtractConditionSystem))]
public sealed partial class ExtractConditionComponent : Component
{
/// <summary>
/// A group of items to be stolen
/// </summary>
[DataField(required: true)]
public ProtoId<StealTargetGroupPrototype> StealGroup;
/// <summary>
/// When enabled, disables generation of this target if there is no entity on the map (disable for objects that can be created mid-round).
/// </summary>
[DataField]
public bool VerifyMapExistence = true;
/// <summary>
/// Help newer players by saying e.g. "steal the chief engineer's advanced magboots"
/// instead of "steal advanced magboots". Should be a loc string.
/// </summary>
[DataField("owner")]
public LocId? OwnerText;
[DataField(required: true)]
public LocId ObjectiveText;
[DataField(required: true)]
public LocId ObjectiveNoOwnerText;
[DataField(required: true)]
public LocId DescriptionText;
}

View File

@ -0,0 +1,18 @@
using Content.Server._DV.Objectives.Systems;
using Content.Shared.Roles;
using Robust.Shared.Prototypes;
namespace Content.Server._DV.Objectives.Components;
/// <summary>
/// Requires that the player is not in a certain department to have this objective.
/// </summary>
[RegisterComponent, Access(typeof(NotDepartmentRequirementSystem))]
public sealed partial class NotDepartmentRequirementComponent : Component
{
/// <summary>
/// ID of the department to ban from having this objective.
/// </summary>
[DataField(required: true)]
public ProtoId<DepartmentPrototype> Department;
}

View File

@ -1,9 +1,23 @@
using Content.Server.Objectives.Systems;
using Content.Server._DV.Objectives.Systems;
namespace Content.Server._DV.Objectives.Components;
/// <summary>
/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random traitor.
/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random traitor who has enough reputation.
/// </summary>
[RegisterComponent]
public sealed partial class PickRandomTraitorComponent : Component;
[RegisterComponent, Access(typeof(PickRandomTraitorSystem))]
public sealed partial class PickRandomTraitorComponent : Component
{
/// <summary>
/// Minimum reputation to require, or 0 for no requirement.
/// </summary>
[DataField]
public int MinReputation;
/// <summary>
/// Minimum number of active contracts a traitor needs to have.
/// By necessity requires a traitor to have a PDA that isn't deleted.
/// </summary>
[DataField]
public int MinContracts;
}

View File

@ -0,0 +1,10 @@
using Content.Server._DV.Objectives.Systems;
namespace Content.Server._DV.Objectives.Components;
/// <summary>
/// Requires that you kidnap a person using syndicate fultons.
/// They can be bought back at a cargo ordering console.
/// </summary>
[RegisterComponent, Access(typeof(RansomConditionSystem))]
public sealed partial class RansomConditionComponent : Component;

View File

@ -0,0 +1,16 @@
using Content.Server._DV.Objectives.Systems;
namespace Content.Server._DV.Objectives.Components;
/// <summary>
/// Requires a certain number of reputation to roll an objective.
/// </summary>
[RegisterComponent, Access(typeof(ReputationConditionSystem))]
public sealed partial class ReputationConditionComponent : Component
{
/// <summary>
/// The required reputation.
/// </summary>
[DataField(required: true)]
public int Reputation;
}

View File

@ -0,0 +1,96 @@
using Content.Server._DV.Objectives.Components;
using Content.Server.Objectives.Systems;
using Content.Shared._DV.Reputation;
using Content.Shared.Objectives.Components;
using Content.Shared.Whitelist;
using Robust.Shared.Random;
namespace Content.Server._DV.Objectives.Systems;
public sealed class AssistRandomContractSystem : EntitySystem
{
[Dependency] private readonly CodeConditionSystem _codeCondition = default!;
[Dependency] private readonly ContractObjectiveSystem _contractObjective = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly MetaDataSystem _meta = default!;
[Dependency] private readonly ReputationSystem _reputation = default!;
[Dependency] private readonly TargetObjectiveSystem _target = default!;
private List<EntityUid> _available = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AssistRandomContractComponent, ObjectiveAfterAssignEvent>(OnAfterAssign);
SubscribeLocalEvent<AssistRandomContractComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<AssistedContractComponent, ContractCompletedEvent>(OnCompleted);
SubscribeLocalEvent<AssistedContractComponent, ContractFailedEvent>(OnFailed);
}
private void OnAfterAssign(Entity<AssistRandomContractComponent> ent, ref ObjectiveAfterAssignEvent args)
{
if (!_target.GetTarget(ent, out var target))
return;
if (_reputation.GetMindContracts(target.Value) is not {} contracts)
return;
_available.Clear();
foreach (var obj in contracts.Comp.Objectives)
{
if (obj is {} uid && _whitelist.IsBlacklistFailOrNull(ent.Comp.Blacklist, uid))
_available.Add(uid);
}
var contract = _random.Pick(_available);
ent.Comp.Contract = contract;
StartAssisting(contract, ent);
// set description so you know what to do
var desc = Loc.GetString(ent.Comp.Description, ("contract", Name(contract)));
_meta.SetEntityDescription(ent, desc, args.Meta);
}
private void OnShutdown(Entity<AssistRandomContractComponent> ent, ref ComponentShutdown args)
{
if (ent.Comp.Contract is {} contract)
StopAssisting(contract, ent);
}
private void OnCompleted(Entity<AssistedContractComponent> ent, ref ContractCompletedEvent args)
{
foreach (var uid in ent.Comp.Assisting)
{
_codeCondition.SetCompleted(uid);
}
}
private void OnFailed(Entity<AssistedContractComponent> ent, ref ContractFailedEvent args)
{
foreach (var uid in ent.Comp.Assisting)
{
_contractObjective.TryFailContract(uid);
}
}
public void StartAssisting(EntityUid contract, EntityUid assisting)
{
EnsureComp<AssistedContractComponent>(contract).Assisting.Add(assisting);
}
public void StopAssisting(EntityUid contract, EntityUid assisting)
{
if (!TryComp<AssistedContractComponent>(contract, out var comp))
return;
comp.Assisting.Remove(assisting);
if (comp.Assisting.Count > 0)
return;
// nobody is assisting anymore :(
RemComp<AssistedContractComponent>(contract);
}
}

View File

@ -0,0 +1,86 @@
using Content.Server._DV.Objectives.Components;
using Content.Server.Objectives.Systems;
using Content.Server.Store.Systems;
using Content.Shared._DV.Objectives.Systems;
using Content.Shared._DV.Reputation;
using Content.Shared.FixedPoint;
namespace Content.Server._DV.Objectives.Systems;
/// <summary>
/// Handles reputation + TC gains for <see cref="ContractObjectiveComponent"/>.
/// </summary>
public sealed class ContractObjectiveSystem : SharedContractObjectiveSystem
{
[Dependency] private readonly CodeConditionSystem _codeCondition = default!;
[Dependency] private readonly ReputationSystem _reputation = default!;
[Dependency] private readonly StoreSystem _store = default!;
private Dictionary<string, FixedPoint2> _currency = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ContractObjectiveComponent, ContractTakenEvent>(OnTaken);
SubscribeLocalEvent<ContractObjectiveComponent, ContractCompletedEvent>(OnCompleted);
}
private void OnTaken(Entity<ContractObjectiveComponent> ent, ref ContractTakenEvent args)
{
ent.Comp.Pda = args.Pda;
if (ent.Comp.Prepaid)
Pay(ent, args.Pda);
}
private void OnCompleted(Entity<ContractObjectiveComponent> ent, ref ContractCompletedEvent args)
{
_reputation.GiveReputation(args.Pda, ent.Comp.Reputation);
if (!ent.Comp.Prepaid)
Pay(ent, args.Pda);
}
private void Pay(Entity<ContractObjectiveComponent> ent, EntityUid pda)
{
_currency.Clear();
_currency[ent.Comp.Currency] = ent.Comp.Payment;
_store.TryAddCurrency(_currency, pda);
}
/// <summary>
/// Fail all active incomplete contracts with a given component, based on a predicate.
/// </summary>
public void FailContracts<T>(Predicate<Entity<T>> pred) where T: Component
{
var query = EntityQueryEnumerator<T, ContractObjectiveComponent>();
while (query.MoveNext(out var uid, out var comp, out var contract))
{
if (_codeCondition.IsCompleted(uid) || !pred((uid, comp)))
continue;
if (contract.Pda is {} pda && TryComp<ContractsComponent>(pda, out var contracts))
_reputation.TryFailContract((pda, contracts), uid);
}
}
/// <summary>
/// Look up an objective's stored pda and try to fail it.
/// </summary>
public bool TryFailContract(Entity<ContractObjectiveComponent?> objective)
{
return Resolve(objective, ref objective.Comp) &&
objective.Comp.Pda is {} pda &&
TryComp<ContractsComponent>(pda, out var comp) &&
_reputation.TryFailContract((pda, comp), objective);
}
public override string ContractName(EntityUid objective)
{
var title = base.ContractName(objective);
if (!TryComp<ContractObjectiveComponent>(objective, out var contract))
return title;
return $"{title} - {contract.Reputation} REP + {contract.Payment} TC";
}
}

View File

@ -0,0 +1,114 @@
using Content.Server._DV.Objectives.Components;
using Content.Server.Objectives.Systems;
using Content.Shared._DV.Traitor;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Objectives.Components;
using Content.Shared.Objectives.Systems;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server._DV.Objectives.Systems;
public sealed class ExtractConditionSystem : EntitySystem
{
[Dependency] private readonly CodeConditionSystem _codeCondition = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly MetaDataSystem _meta = default!;
[Dependency] private readonly ContractObjectiveSystem _contract = default!;
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ExtractConditionComponent, ObjectiveAssignedEvent>(OnAssigned);
SubscribeLocalEvent<ExtractConditionComponent, ObjectiveAfterAssignEvent>(OnAfterAssign);
SubscribeLocalEvent<StealTargetComponent, FultonedEvent>(OnFultoned);
}
/// start checks of target acceptability, and generation of start values.
private void OnAssigned(Entity<ExtractConditionComponent> ent, ref ObjectiveAssignedEvent args)
{
if (args.Cancelled || !ent.Comp.VerifyMapExistence || args.Mind.OwnedEntity is not {} mob)
return;
// very important: only check the current map, so syndie vault doesn't count as existing
var map = Transform(mob).MapID;
var found = false;
var query = EntityQueryEnumerator<StealTargetComponent, TransformComponent>();
var group = ent.Comp.StealGroup;
while (query.MoveNext(out var target, out var xform))
{
if (xform.MapID != map || target.StealGroup != group)
continue;
found = true;
break;
}
args.Cancelled = !found;
}
//Set the visual, name, icon for the objective.
private void OnAfterAssign(Entity<ExtractConditionComponent> ent, ref ObjectiveAfterAssignEvent args)
{
var group = _proto.Index(ent.Comp.StealGroup);
string localizedName = Loc.GetString(group.Name);
var title = ent.Comp.OwnerText == null
? Loc.GetString(ent.Comp.ObjectiveNoOwnerText, ("itemName", localizedName))
: Loc.GetString(ent.Comp.ObjectiveText, ("owner", Loc.GetString(ent.Comp.OwnerText)), ("itemName", localizedName));
var description = Loc.GetString(ent.Comp.DescriptionText, ("itemName", localizedName));
_meta.SetEntityName(ent, title, args.Meta);
_meta.SetEntityDescription(ent, description, args.Meta);
_objectives.SetIcon(ent, group.Sprite, args.Objective);
}
private void OnFultoned(Entity<StealTargetComponent> ent, ref FultonedEvent args)
{
// don't touch objectives for salv fultons, return early
if (!TryComp<ExtractingComponent>(ent, out var extracting))
return;
RemCompDeferred<ExtractingComponent>(ent);
// complete the objective of the person that extracted it
if (extracting.Mind is {} mindId && FindObjective(mindId, (ent, ent.Comp)) is {} objective)
_codeCondition.SetCompleted(objective);
// fail every other contract for the same thing
var group = ent.Comp.StealGroup;
_contract.FailContracts<ExtractConditionComponent>(obj => obj.Comp.StealGroup == group);
}
/// <summary>
/// Find an objective that wants an item, or null if it isn't wanted.
/// </summary>
public EntityUid? FindObjective(Entity<MindComponent?> mind, Entity<StealTargetComponent?> item)
{
if (!Resolve(mind, ref mind.Comp) || !Resolve(item, ref item.Comp, false))
return null;
var group = item.Comp.StealGroup;
foreach (var objective in mind.Comp.Objectives)
{
if (!TryComp<ExtractConditionComponent>(objective, out var comp))
continue;
// skip already completed objectives
if (_codeCondition.IsCompleted(objective))
continue;
if (comp.StealGroup == group)
return objective;
}
return null;
}
}

View File

@ -1,73 +0,0 @@
using Content.Server._DV.Objectives.Components;
using Content.Server.Objectives.Components;
using Content.Server.GameTicking.Rules;
using Content.Server.Objectives.Systems;
using Content.Shared.Objectives.Components;
using Robust.Shared.Random;
namespace Content.Server._DV.Objectives.Systems;
/// <summary>
/// Handles the kill fellow traitor objective.
/// </summary>
public sealed class KillFellowTraitorObjectiveSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly TargetObjectiveSystem _target = default!;
[Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PickRandomTraitorComponent, ObjectiveAssignedEvent>(OnTraitorKillAssigned);
}
private void OnTraitorKillAssigned(EntityUid uid, PickRandomTraitorComponent comp, ref ObjectiveAssignedEvent args)
{
if (!TryComp<TargetObjectiveComponent>(uid, out var target))
{
Log.Error($"Missing components for {uid}.");
args.Cancelled = true;
return;
}
// Target already assigned
if (target.Target != null)
{
Log.Error($"Target already assigned for {uid}.");
args.Cancelled = true;
return;
}
var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind);
List<EntityUid> validTraitorMinds = [];
// Going through each OTHER traitor
foreach (var traitor in traitors)
{
var valid = true;
// Going through each of OUR objectives.
foreach (var objective in args.Mind.Objectives)
{
// If one of OUR objectives already targets a traitor, don't add it to the list.
if (TryComp<TargetObjectiveComponent>(objective, out var targetComp) && targetComp.Target == traitor.Id)
{
valid = false;
break;
}
}
if (valid)
validTraitorMinds.Add(traitor.Id);
}
// No other traitors
if (validTraitorMinds.Count == 0)
{
args.Cancelled = true;
return;
}
_target.SetTarget(uid, _random.Pick(validTraitorMinds), target);
}
}

View File

@ -0,0 +1,33 @@
using Content.Server._DV.Objectives.Components;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles.Jobs;
namespace Content.Server._DV.Objectives.Systems;
/// <summary>
/// Handles checking the department blacklist for this objective.
/// </summary>
public sealed class NotDepartmentRequirementSystem : EntitySystem
{
[Dependency] private readonly SharedJobSystem _job = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NotDepartmentRequirementComponent, RequirementCheckEvent>(OnCheck);
}
private void OnCheck(Entity<NotDepartmentRequirementComponent> ent, ref RequirementCheckEvent args)
{
if (args.Cancelled ||
!_job.MindTryGetJob(args.MindId, out var job) ||
!_job.TryGetPrimaryDepartment(job.ID, out var primary))
{
return;
}
if (primary.ID == ent.Comp.Department)
args.Cancelled = true;
}
}

View File

@ -0,0 +1,30 @@
using Content.Server._DV.Objectives.Components;
using Content.Server.Objectives.Components;
using Content.Server.Objectives.Systems;
using Content.Server.Roles;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles;
namespace Content.Server._DV.Objectives.Systems;
/// <summary>
/// Handles picking a random traitor for the kill fellow traitor objective.
/// </summary>
public sealed class PickRandomTraitorSystem : EntitySystem
{
[Dependency] private readonly PickObjectiveTargetSystem _pickTarget = default!;
[Dependency] private readonly SharedRoleSystem _role = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PickRandomTraitorComponent, ObjectiveAssignedEvent>(OnRandomTraitorAssigned);
}
private void OnRandomTraitorAssigned(Entity<PickRandomTraitorComponent> ent, ref ObjectiveAssignedEvent args)
{
_pickTarget.AssignRandomTarget(ent, ref args, mindId =>
_role.MindHasRole<TraitorRoleComponent>(mindId));
}
}

View File

@ -0,0 +1,82 @@
using Content.Server._DV.Objectives.Components;
using Content.Server.Chat.Systems;
using Content.Server.Objectives.Systems;
using Content.Shared._DV.Traitor;
using Content.Shared.Mind;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Robust.Shared.Audio;
namespace Content.Server._DV.Objectives.Systems;
/// <summary>
/// Makes ransom announcements for ransom objectives and mob extraction objectives.
/// </summary>
public sealed class RansomConditionSystem : EntitySystem
{
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly CodeConditionSystem _codeCondition = default!;
[Dependency] private readonly ContractObjectiveSystem _contract = default!;
[Dependency] private readonly MobStateSystem _mob = default!;
[Dependency] private readonly RansomSystem _ransom = default!;
[Dependency] private readonly TargetObjectiveSystem _targetObjective = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MobStateComponent, FultonedEvent>(OnFultoned);
}
private void OnFultoned(Entity<MobStateComponent> ent, ref FultonedEvent args)
{
if (!TryComp<ExtractingComponent>(ent, out var extracting))
return;
RemCompDeferred<ExtractingComponent>(ent);
var ransom = _ransom.RansomEntity(ent);
var msg = Loc.GetString("syndicate-ransom-announcement", ("hostage", ent), ("ransom", ransom));
var sender = Loc.GetString("syndicate-ransom-announcement-sender");
var sound = new SoundPathSpecifier("/Audio/Misc/notice1.ogg");
var color = Color.Red;
_chat.DispatchGlobalAnnouncement(msg, sender, playSound: true, sound, color);
// TODO: put their inventory into the vault
// complete the objective of the person that kidnapped them
if (_mob.IsAlive(ent) && extracting.Mind is {} mindId && FindObjective(mindId, ent) is {} objective)
_codeCondition.SetCompleted(objective);
_contract.FailContracts<RansomConditionComponent>(obj => TargetEquals(obj, ent));
}
public EntityUid? FindObjective(Entity<MindComponent?> mind, EntityUid mob)
{
if (!Resolve(mind, ref mind.Comp))
return null;
foreach (var objective in mind.Comp.Objectives)
{
if (!HasComp<RansomConditionComponent>(objective) || _codeCondition.IsCompleted(objective))
continue;
if (TargetEquals(objective, mob))
return objective;
}
return null;
}
private bool TargetEquals(EntityUid objective, EntityUid mob)
{
if (!_targetObjective.GetTarget(objective, out var target))
return false;
// get the actual mob targeted for the objective
if (TryComp<MindComponent>(target, out var targetMind) && GetEntity(targetMind.OriginalOwnedEntity) is {} targetMob)
target = targetMob;
return mob == target;
}
}

View File

@ -0,0 +1,27 @@
using Content.Server._DV.Objectives.Components;
using Content.Shared._DV.Reputation;
using Content.Shared.Objectives.Components;
namespace Content.Server._DV.Objectives.Systems;
/// <summary>
/// Prevents <see cref="ReputationConditionComponent"/> being added if you lack the required reputation.
/// </summary>
public sealed class ReputationConditionSystem : EntitySystem
{
[Dependency] private readonly ReputationSystem _reputation = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ReputationConditionComponent, ObjectiveAssignedEvent>(OnAssigned);
}
private void OnAssigned(Entity<ReputationConditionComponent> ent, ref ObjectiveAssignedEvent args)
{
var reputation = _reputation.GetMindReputation(args.MindId) ?? 0;
if (reputation < ent.Comp.Reputation)
args.Cancelled = true;
}
}

View File

@ -0,0 +1,30 @@
using Content.Server._DV.Shuttles.Systems;
using Robust.Shared.Utility;
namespace Content.Server._DV.Shuttles.Components;
/// <summary>
/// Added to station entity to load the syndie jail in its centcomm map.
/// Without this syndie fultons won't work.
/// </summary>
[RegisterComponent, Access(typeof(SyndieJailSystem))]
public sealed partial class SyndieJailComponent : Component
{
/// <summary>
/// The grid to load.
/// </summary>
[DataField]
public ResPath Path = new ResPath("/Maps/_DV/Nonstations/syndie_jail.yml");
/// <summary>
/// Minimum distance to load the grid at.
/// </summary>
[DataField]
public float MinRange = 800f;
/// <summary>
/// Maximum distance to load the grid at.
/// </summary>
[DataField]
public float MaxRange = 1000f;
}

View File

@ -0,0 +1,37 @@
using Content.Server._DV.Shuttles.Components;
using Content.Server.Shuttles.Components;
using Content.Server.Station.Events;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Map.Components;
using Robust.Shared.Random;
namespace Content.Server._DV.Shuttles.Systems;
public sealed class SyndieJailSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly MapLoaderSystem _mapLoader = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SyndieJailComponent, StationPostInitEvent>(OnStationStartup);
}
private void OnStationStartup(Entity<SyndieJailComponent> ent, ref StationPostInitEvent args)
{
var cc = Comp<StationCentcommComponent>(ent);
if (cc.Entity is not {} gridUid || cc.MapEntity is not {} map)
{
Log.Warning($"No centcomm grid to load syndie jail from {ToPrettyString(ent)}!");
return;
}
var mapId = Comp<MapComponent>(map).MapId;
var offset = _random.NextVector2(ent.Comp.MinRange, ent.Comp.MaxRange);
_mapLoader.TryLoadGrid(mapId, ent.Comp.Path, out _,
offset: _transform.GetWorldPosition(gridUid) + offset);
}
}

View File

@ -0,0 +1,27 @@
using Content.Shared._DV.Reputation;
using Content.Shared.Store;
namespace Content.Server._DV.Store.Conditions;
/// <summary>
/// Requires that an uplink using <see cref="ContractsComponent"/> has enough reputation.
/// This is ignored for nukie uplinks and surplus crates.
/// </summary>
public sealed partial class ReputationCondition : ListingCondition
{
/// <summary>
/// The required reputation for traitors.
/// This is unused for nukie uplinks.
/// </summary>
[DataField(required: true)]
public int Reputation;
public override bool Condition(ListingConditionArgs args)
{
var reputation = args.EntityManager.System<ReputationSystem>();
if (args.StoreEntity is not {} pda || reputation.GetReputation(pda) is not {} rep)
return true; // nukie uplink or a surplus
return rep >= Reputation;
}
}

View File

@ -0,0 +1,135 @@
using Content.Server._DV.Objectives.Systems;
using Content.Server.Objectives.Systems;
using Content.Shared._DV.Traitor;
using Content.Shared.Charges.Systems;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Content.Shared.Mind;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Objectives.Components;
using Content.Shared.Popups;
using Content.Shared.Salvage.Fulton;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Timing;
namespace Content.Server._DV.Traitor;
public sealed class ExtractionFultonSystem : SharedExtractionFultonSystem
{
[Dependency] private readonly ExtractConditionSystem _extractCondition = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly MobStateSystem _mob = default!;
[Dependency] private readonly RansomConditionSystem _ransomCondition = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedChargesSystem _charges = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedFultonSystem _fulton = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ExtractionFultonComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<ExtractionFultonComponent, ExtractionFultonDoAfterEvent>(OnDoAfter);
}
private void OnAfterInteract(Entity<ExtractionFultonComponent> ent, ref AfterInteractEvent args)
{
if (args.Handled || args.Target is not {} target)
return;
args.Handled = true;
AttachFulton(ent, target, args.User);
}
protected override void AttachFulton(Entity<ExtractionFultonComponent> ent, EntityUid target, EntityUid user)
{
if (_mind.GetMind(user) is not {} mindId || !TryComp<MindComponent>(mindId, out var mind))
return;
if (HasComp<FultonedComponent>(target))
{
Popup.PopupEntity(Loc.GetString("fulton-fultoned"), target, user);
return;
}
if (_charges.IsEmpty(ent.Owner))
{
Popup.PopupEntity(Loc.GetString("emag-no-charges"), ent, user);
return;
}
if (!CanExtractPopup((mindId, mind), user, target))
return;
if (FindBeacon(ent, target) is not {} beacon)
{
Log.Error($"No beacon found accepting {ToPrettyString(target)} from {ToPrettyString(ent)}");
Popup.PopupEntity(Loc.GetString("extraction-fulton-no-destination"), ent, user);
return;
}
var ev = new ExtractionFultonDoAfterEvent(GetNetEntity(beacon));
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, ent.Comp.ApplyDelay, ev, eventTarget: ent, target: target, used: ent)
{
BreakOnMove = true,
NeedHand = true
});
}
private void OnDoAfter(Entity<ExtractionFultonComponent> ent, ref ExtractionFultonDoAfterEvent args)
{
if (args.Cancelled || args.Target is not {} target || GetEntity(args.Beacon) is not {} beacon)
return;
if (!_charges.TryUseCharge(ent.Owner))
return;
var duration = HasComp<MobStateComponent>(target)
? ent.Comp.MobDelay
: ent.Comp.ItemDelay;
// this is checked when extracted to only complete this persons objective
EnsureComp<ExtractingComponent>(target).Mind = _mind.GetMind(args.User);
var comp = AddComp<FultonedComponent>(target);
comp.Beacon = beacon;
comp.NextFulton = _timing.CurTime + duration;
comp.FultonDuration = duration;
comp.Removeable = true;
_fulton.UpdateAppearance(target, comp);
Dirty(target, comp);
_audio.PlayPvs(ent.Comp.FultonSound, target);
// TODO: make mobs beep while fultoned
}
private bool CanExtractPopup(Entity<MindComponent?> mind, EntityUid user, EntityUid target)
{
if (Transform(target).Anchored)
{
Popup.PopupEntity(Loc.GetString("extraction-fulton-anchored"), target, user);
return false;
}
if (_extractCondition.FindObjective(mind, target) != null)
return true;
if (_ransomCondition.FindObjective(mind, target) != null)
{
if (!_mob.IsAlive(target))
{
Popup.PopupEntity(Loc.GetString("extraction-fulton-dead"), target, user);
return false;
}
return true;
}
Popup.PopupEntity(Loc.GetString("extraction-fulton-not-target"), target, user);
return false;
}
}

View File

@ -1,3 +1,4 @@
using Content.Shared._DV.Traitor; // DeltaV
using Robust.Shared.Serialization;
namespace Content.Shared.Cargo.BUI;
@ -10,13 +11,16 @@ public sealed class CargoConsoleInterfaceState : BoundUserInterfaceState
public int Capacity;
public NetEntity Station;
public List<CargoOrderData> Orders;
public List<RansomData> Ransoms; // DeltaV
public CargoConsoleInterfaceState(string name, int count, int capacity, NetEntity station, List<CargoOrderData> orders)
// DeltaV - added ransoms
public CargoConsoleInterfaceState(string name, int count, int capacity, NetEntity station, List<CargoOrderData> orders, List<RansomData> ransoms)
{
Name = name;
Count = count;
Capacity = capacity;
Station = station;
Orders = orders;
Ransoms = ransoms;
}
}

View File

@ -374,10 +374,20 @@ public abstract partial class SharedMindSystem : EntitySystem
return false;
var objective = mind.Objectives[index];
return TryRemoveObjective((mindId, mind), objective); // DeltaV
}
/// <summary>
/// DeltaV: Remove an objective from this mind, if you already know its uid.
/// </summary>
public bool TryRemoveObjective(Entity<MindComponent> mind, EntityUid objective)
{
if (!mind.Comp.Objectives.Remove(objective))
return false;
var title = Name(objective);
_adminLogger.Add(LogType.Mind, LogImpact.Low, $"Objective {objective} ({title}) removed from the mind of {MindOwnerLoggingString(mind)}");
mind.Objectives.Remove(objective);
mind.Comp.Objectives.Remove(objective);
// garbage collection - only delete the objective entity if no mind uses it anymore
// This comes up for stuff like paradox clones where the objectives share the same entity

View File

@ -8,5 +8,9 @@ namespace Content.Shared.Nuke;
[RegisterComponent, NetworkedComponent]
public sealed partial class NukeDiskComponent : Component
{
/// <summary>
/// DeltaV: When extracted by a syndie, this makes the disk teleport to any nukies.
/// </summary>
[DataField]
public bool Extracted;
}

View File

@ -1,6 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared._DV.Reputation; // DeltaV
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Random; // DeltaV
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@ -13,6 +15,7 @@ public abstract class SharedObjectivesSystem : EntitySystem
{
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly ReputationSystem _reputation = default!; // DeltaV
private EntityQuery<MetaDataComponent> _metaQuery;
@ -45,6 +48,16 @@ public abstract class SharedObjectivesSystem : EntitySystem
if (_metaQuery.GetComponent(objective).EntityPrototype?.ID == proto)
return false;
}
// Begin DeltaV Additions - check available contracts too
if (_reputation.GetMindContracts(mindId) is {} contracts)
{
foreach (var objective in contracts.Comp.Offerings)
{
if (objective is {} obj && _metaQuery.Comp(obj).EntityPrototype?.ID == proto)
return false;
}
}
// End DeltaV Additions
}
return true;
@ -164,4 +177,12 @@ public abstract class SharedObjectivesSystem : EntitySystem
comp.Icon = icon;
}
/// <summary>
/// DeltaV - Lets code in shared call this
/// </summary>
public virtual EntityUid? GetRandomObjective(EntityUid mindId, MindComponent mind, ProtoId<WeightedRandomPrototype> objectiveGroupProto, float maxDifficulty)
{
return null;
}
}

View File

@ -90,6 +90,12 @@ namespace Content.Shared.Roles
[DataField("alwaysUseSpawner")]
public bool AlwaysUseSpawner { get; } = false;
/// <summary>
/// DeltaV: Multiplies syndicate ransom price by this number.
/// </summary>
[DataField]
public float RansomModifier = 1f;
/// <summary>
/// The "weight" or importance of this job. If this number is large, the job system will assign this job
/// before assigning other jobs.

View File

@ -53,6 +53,7 @@ public abstract partial class SharedFultonSystem : EntitySystem
private void OnFultonContainerInserted(EntityUid uid, FultonedComponent component, EntGotInsertedIntoContainerMessage args)
{
if (!component.Removeable) return; // DeltaV
RemCompDeferred<FultonedComponent>(uid);
}
@ -167,7 +168,7 @@ public abstract partial class SharedFultonSystem : EntitySystem
Dirty(args.NewId, newFulton);
}
protected virtual void UpdateAppearance(EntityUid uid, FultonedComponent fultoned)
public virtual void UpdateAppearance(EntityUid uid, FultonedComponent fultoned) // DeltaV - made public
{
return;
}

View File

@ -155,6 +155,8 @@ public sealed class DCCVars
public static readonly CVarDef<bool> EnableBacktoBack =
CVarDef.Create("game.disable_preset_test", false, CVar.SERVERONLY);
/* Chat highlighting */
/// <summary>
/// A string containing a list of newline-separated strings to be highlighted in the chat.
/// </summary>
@ -182,6 +184,33 @@ public sealed class DCCVars
CVar.CLIENTONLY | CVar.ARCHIVE,
"The color in which the highlights will be displayed.");
/* Traitors */
/// <summary>
/// Base ransom for a non-humanoid mob, like shiva.
/// </summary>
public static readonly CVarDef<float> MobRansom =
CVarDef.Create("game.ransom.mob_base", 5000f, CVar.REPLICATED);
/// <summary>
/// Base ransom for a humanoid.
/// </summary>
public static readonly CVarDef<float> HumanoidRansom =
CVarDef.Create("game.ransom.humanoid_base", 10000f, CVar.REPLICATED);
/// <summary>
/// Ransom modifier for critical mobs.
/// </summary>
public static readonly CVarDef<float> RansomCritModifier =
CVarDef.Create("game.ransom.critical_modifier", 0.5f, CVar.REPLICATED);
/// <summary>
/// Ransom modifier for dead mobs.
/// The ransomer will also fail their objective.
/// </summary>
public static readonly CVarDef<float> RansomDeadModifier =
CVarDef.Create("game.ransom.dead_modifier", 0.2f, CVar.REPLICATED);
/* Laying down combat */
/// <summary>

View File

@ -0,0 +1,9 @@
namespace Content.Shared._DV.Objectives.Systems;
public abstract class SharedContractObjectiveSystem : EntitySystem
{
public virtual string ContractName(EntityUid objective)
{
return Name(objective);
}
}

View File

@ -0,0 +1,115 @@
using Content.Shared.Random;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared._DV.Reputation;
/// <summary>
/// Component added to traitor PDAs to store contract data.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(ReputationSystem))]
[AutoGenerateComponentState(true)]
public sealed partial class ContractsComponent : Component
{
/// <summary>
/// How much reputation there is.
/// </summary>
[DataField, AutoNetworkedField]
public int Reputation;
/// <summary>
/// The mind of the traitor.
/// </summary>
[DataField]
public EntityUid? Mind;
/// <summary>
/// The current reputation level, updated when it changes.
/// </summary>
[ViewVariables]
public ReputationLevelPrototype? CurrentLevel;
/// <summary>
/// Offering objectives that can be taken in the UI.
/// </summary>
[DataField]
public List<EntityUid?> Offerings = new();
/// <summary>
/// All slots for offerings.
/// </summary>
[DataField, AutoNetworkedField]
public List<OfferingSlot> OfferingSlots = new();
/// <summary>
/// The objectives for each slot.
/// Not sent to the client as objective entities are not networked.
/// </summary>
[DataField]
public List<EntityUid?> Objectives = new();
/// <summary>
/// All slots for contracts.
/// Dynamically increased when levelling up.
/// </summary>
[DataField, AutoNetworkedField]
public List<ContractSlot> Slots = new();
/// <summary>
/// How long you have to wait before you can get a new contract after the objective is completed or failed.
/// </summary>
[DataField]
public TimeSpan CompleteDelay = TimeSpan.FromMinutes(5);
/// <summary>
/// How long you have to wait before you can get a new offering after you reject one.
/// </summary>
[DataField]
public TimeSpan RejectDelay = TimeSpan.FromMinutes(15);
/// <summary>
/// How long you have to wait before you can get a new offering after you accept one.
/// </summary>
[DataField]
public TimeSpan AcceptDelay = TimeSpan.FromMinutes(3);
}
/// <summary>
/// A contract slot which can either have a contract objective, be available for new contracts or be on cooldown.
/// </summary>
[DataDefinition, Serializable, NetSerializable]
public partial record struct ContractSlot
{
/// <summary>
/// The title of the current objective, or null if there is none.
/// </summary>
[DataField]
public string? ObjectiveTitle;
/// <summary>
/// When the slot gets unlocked and a new contract can be taken.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan? NextUnlock;
}
/// <summary>
/// An offering slot which can have an available objective.
/// </summary>
[DataDefinition, Serializable, NetSerializable]
public partial record struct OfferingSlot
{
/// <summary>
/// The title of the available objective, or null if locked.
/// </summary>
[DataField]
public string? Title;
/// <summary>
/// When the slot gets unlocked and a new offering is rolled.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan? NextUnlock;
}

View File

@ -0,0 +1,43 @@
using Robust.Shared.Serialization;
namespace Content.Shared._DV.Reputation;
[Serializable, NetSerializable]
public enum ContractsUiKey : byte
{
Key
}
[Serializable, NetSerializable]
public sealed class ContractsState : BoundUserInterfaceState;
// TODO
/// <summary>
/// Accept a contract with offerings index.
/// </summary>
[Serializable, NetSerializable]
public sealed class ContractsAcceptMessage(int index) : BoundUserInterfaceMessage
{
public readonly int Index = index;
}
/// <summary>
/// Complete a contract whose objective has been completed, with slot index.
/// </summary>
[Serializable, NetSerializable]
public sealed class ContractsCompleteMessage(int index) : BoundUserInterfaceMessage
{
public readonly int Index = index;
}
/// <summary>
/// Rejects a contract offering with offerings index.
/// </summary>
[Serializable, NetSerializable]
public sealed class ContractsRejectMessage(int index) : BoundUserInterfaceMessage
{
public readonly int Index = index;
}
[Serializable, NetSerializable]
public sealed class PdaShowContractsMessage : BoundUserInterfaceMessage;

View File

@ -0,0 +1,22 @@
using Content.Shared.Mind;
namespace Content.Shared._DV.Reputation;
/// <summary>
/// Event that gets raised on an objective after it has been added and taken.
/// </summary>
[ByRefEvent]
public record struct ContractTakenEvent(Entity<ContractsComponent> Pda, Entity<MindComponent> Mind);
/// <summary>
/// Event that gets raised on an objective after it becomes impossible to completed.
/// It gets deleted afterwards.
/// </summary>
[ByRefEvent]
public record struct ContractFailedEvent(Entity<ContractsComponent> Pda);
/// <summary>
/// Event that gets raised on an objective after it has been completed.
/// </summary>
[ByRefEvent]
public record struct ContractCompletedEvent(Entity<ContractsComponent> Pda);

View File

@ -0,0 +1,20 @@
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Reputation;
/// <summary>
/// Stores reputation-related data for mind entities.
/// Has a backup reputation value incase their PDA is deleted.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(ReputationSystem))]
public sealed partial class MindReputationComponent : Component
{
/// <summary>
/// The traitor's PDA with <see cref="ContractsComponent"/>, might not always exist.
/// </summary>
[DataField]
public EntityUid? Pda;
[DataField]
public int Reputation;
}

View File

@ -0,0 +1,51 @@
using Content.Shared.Random;
using Robust.Shared.Prototypes;
namespace Content.Shared._DV.Reputation;
/// <summary>
/// Data associated with a reputation level.
/// </summary>
[Prototype]
public sealed partial class ReputationLevelPrototype : IPrototype
{
[IdDataField]
public string ID { get; set; } = string.Empty;
/// <summary>
/// Name of the reputation level to display in UIs.
/// </summary>
[DataField(required: true)]
public LocId Name;
/// <summary>
/// Minimum reputation someone needs to get this.
/// </summary>
[DataField(required: true)]
public int Reputation;
/// <summary>
/// Maximum number of contracts that can be active at once.
/// </summary>
[DataField(required: true)]
public int MaxContracts;
/// <summary>
/// Maximum number of offering slots that there can be.
/// </summary>
[DataField(required: true)]
public int MaxOfferings;
/// <summary>
/// Maximum difficulty for objectives that can be rolled.
/// <c>ReputationCondition</c> should be used for fine-grained control.
/// </summary>
[DataField]
public float MaxDifficulty = 6f;
/// <summary>
/// Offering groups that can be used.
/// </summary>
[DataField]
public ProtoId<WeightedRandomPrototype> OfferingGroups = "ReputationOfferings";
}

View File

@ -0,0 +1,516 @@
using Content.Shared._DV.Objectives.Systems;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Objectives.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Shared._DV.Reputation;
public sealed class ReputationSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedContractObjectiveSystem _contract = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
private List<ReputationLevelPrototype> _levels = new();
public IReadOnlyList<ReputationLevelPrototype> AllLevels => _levels;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ContractsComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<ContractsComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ContractsComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<ContractsComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<ContractsComponent, AfterAutoHandleStateEvent>(OnHandleState);
Subs.BuiEvents<ContractsComponent>(ContractsUiKey.Key, subs =>
{
subs.Event<ContractsAcceptMessage>(OnAcceptMessage);
subs.Event<ContractsCompleteMessage>(OnCompleteMessage);
subs.Event<ContractsRejectMessage>(OnRejectMessage);
});
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
CacheLevels();
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<ContractsComponent>();
while (query.MoveNext(out var uid, out var comp))
{
PickOfferings((uid, comp));
}
}
#region Event Handlers
private void OnInit(Entity<ContractsComponent> ent, ref ComponentInit args)
{
_ui.SetUi(ent.Owner, ContractsUiKey.Key, new InterfaceData("ContractsBUI"));
}
private void OnMapInit(Entity<ContractsComponent> ent, ref MapInitEvent args)
{
// creates the slots for fresh pdas
UpdateLevel(ent);
PickOfferings(ent);
}
private void OnShutdown(Entity<ContractsComponent> ent, ref ComponentShutdown args)
{
// if the PDA is cremated or thrown in a singulo or something,
// delete all the offerings and fail the active contracts
foreach (var uid in ent.Comp.Offerings)
{
Del(uid);
}
foreach (var obj in ent.Comp.Objectives)
{
ContractFailed(ent, obj);
}
// unlink it from the mind
if (TryComp<MindReputationComponent>(ent.Comp.Mind, out var mind))
mind.Pda = null;
}
private void OnUnpaused(Entity<ContractsComponent> ent, ref EntityUnpausedEvent args)
{
for (var i = 0; i < ent.Comp.Slots.Count; i++)
{
var slot = ent.Comp.Slots[i];
slot.NextUnlock += args.PausedTime;
ent.Comp.Slots[i] = slot;
}
for (var i = 0; i < ent.Comp.OfferingSlots.Count; i++)
{
var slot = ent.Comp.OfferingSlots[i];
slot.NextUnlock += args.PausedTime;
ent.Comp.OfferingSlots[i] = slot;
}
Dirty(ent);
}
private void OnHandleState(Entity<ContractsComponent> ent, ref AfterAutoHandleStateEvent args)
{
// update CurrentLevel for client after server changes it, so UI can use it
UpdateLevel(ent);
UpdateUI(ent);
}
private void OnAcceptMessage(Entity<ContractsComponent> ent, ref ContractsAcceptMessage args)
{
var i = args.Index;
if (i < 0 || i >= ent.Comp.Offerings.Count)
return;
if (ent.Comp.Offerings[i] is not {} objective || !TryTakeContract(ent, objective))
return;
ent.Comp.Offerings[i] = null;
ent.Comp.OfferingSlots[i] = new OfferingSlot
{
NextUnlock = _timing.CurTime + ent.Comp.AcceptDelay
};
}
private void OnCompleteMessage(Entity<ContractsComponent> ent, ref ContractsCompleteMessage args)
{
TryCompleteContract(ent, args.Index);
}
private void OnRejectMessage(Entity<ContractsComponent> ent, ref ContractsRejectMessage args)
{
TryRejectOffering(ent, args.Index);
}
#endregion
#region Public API
/// <summary>
/// Add contracts to a traitor's PDA.
/// Throws if you call this multiple times on the same mind or pda.
/// </summary>
public void AddContracts(EntityUid mob, EntityUid pda)
{
if (_mind.GetMind(mob) is not {} mindId)
return;
// AddComp so it will throw if you are trying to bulldoze a used mind or pda
var contracts = AddComp<ContractsComponent>(pda);
var mind = AddComp<MindReputationComponent>(mindId);
contracts.Mind = mindId;
mind.Pda = pda;
PickOfferings((pda, contracts));
}
public void ToggleUI(EntityUid user, EntityUid uid)
{
UpdateUI(uid);
_ui.TryToggleUi(uid, ContractsUiKey.Key, user);
}
private void UpdateUI(EntityUid uid)
{
_ui.SetUiState(uid, ContractsUiKey.Key, new ContractsState());
}
/// <summary>
/// Pick new offerings for open offering slots.
/// </summary>
public void PickOfferings(Entity<ContractsComponent> ent)
{
if (GetMind(ent) is not {} mind || ent.Comp.CurrentLevel is not {} level)
return;
var difficulty = level.MaxDifficulty;
var groups = level.OfferingGroups;
for (var i = 0; i < ent.Comp.OfferingSlots.Count; i++)
{
// can't add a new offering yet
if (ent.Comp.Offerings[i] != null || IsLocked(ent.Comp.OfferingSlots[i].NextUnlock))
continue;
if (_objectives.GetRandomObjective(mind, mind, groups, difficulty) is not {} objective)
{
// prevent spinlock
ent.Comp.OfferingSlots[i] = new OfferingSlot
{
NextUnlock = _timing.CurTime + ent.Comp.AcceptDelay
};
Dirty(ent);
continue;
}
ent.Comp.Offerings[i] = objective;
ent.Comp.OfferingSlots[i] = new OfferingSlot
{
Title = _contract.ContractName(objective)
};
Dirty(ent);
}
}
/// <summary>
/// Try to take a new contract by adding an existing objective entity.
/// </summary>
public bool TryTakeContract(Entity<ContractsComponent> ent, EntityUid objective)
{
if (GetMind(ent) is not {} mind ||
FindOpenSlot(ent) is not {} index)
{
return false;
}
_mind.AddObjective(mind, mind, objective);
ent.Comp.Objectives[index] = objective;
var slot = ent.Comp.Slots[index];
slot.ObjectiveTitle = _contract.ContractName(objective);
ent.Comp.Slots[index] = slot;
Dirty(ent);
var ev = new ContractTakenEvent(ent, mind);
RaiseLocalEvent(objective, ref ev);
return true;
}
/// <summary>
/// If a contract's objective is complete, pays out etc and removes it.
/// </summary>
public bool TryCompleteContract(Entity<ContractsComponent> ent, int index)
{
if (index < 0 ||
index >= ent.Comp.Slots.Count ||
ent.Comp.Objectives[index] is not {} objective ||
GetMind(ent) is not {} mind ||
!_objectives.IsCompleted(objective, mind))
{
return false;
}
var ev = new ContractCompletedEvent(ent);
RaiseLocalEvent(objective, ref ev);
ClearSlot(ent, index, ent.Comp.CompleteDelay);
return true;
}
public bool TryRejectOffering(Entity<ContractsComponent> ent, int index)
{
if (index < 0 ||
index >= ent.Comp.OfferingSlots.Count ||
ent.Comp.Offerings[index] is not {} objective)
{
return false;
}
ent.Comp.Offerings[index] = null;
ent.Comp.OfferingSlots[index] = new OfferingSlot
{
Title = null,
NextUnlock = _timing.CurTime + ent.Comp.RejectDelay
};
Dirty(ent);
Del(objective);
return true;
}
/// <summary>
/// Call this to fail a contract if it becomes impossible to complete.
/// E.g. trying to steal an item that gets deleted
/// </summary>
public bool TryFailContract(Entity<ContractsComponent> ent, EntityUid objective)
{
if (FindContract(ent, objective) is not {} index)
return false;
ContractFailed(ent, objective);
ClearSlot(ent, index, ent.Comp.CompleteDelay);
return true;
}
/// <summary>
/// Get the mind that belongs to a contracts PDA.
/// </summary>
public Entity<MindComponent>? GetMind(Entity<ContractsComponent> ent)
{
if (ent.Comp.Mind is not {} mindId)
return null;
if (!TryComp<MindComponent>(mindId, out var mind))
return null;
return (mindId, mind);
}
/// <summary>
/// Get the contracts pda for a mind, if it exists.
/// </summary>
public Entity<ContractsComponent>? GetMindContracts(EntityUid mindId)
{
if (CompOrNull<MindReputationComponent>(mindId)?.Pda is not {} pda)
return null;
if (!TryComp<ContractsComponent>(pda, out var comp))
return null;
return (pda, comp);
}
/// <summary>
/// Gets the reputation for a mind, null if it had no <see cref="ContractsComponent"/>.
/// </summary>
public int? GetMindReputation(EntityUid mindId)
{
if (CompOrNull<MindReputationComponent>(mindId)?.Pda is not {} pda)
return null;
return GetReputation(pda);
}
/// <summary>
/// Gets the reputation for a PDA, null if it had no <see cref="ContractsComponent"/>.
/// </summary>
public int? GetReputation(Entity<ContractsComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return null;
return ent.Comp.Reputation;
}
public bool GiveMindReputation(EntityUid mindId, int amount)
{
return amount != 0 &&
GetMindContracts(mindId) is {} contracts &&
GiveReputation(contracts, amount);
}
public bool GiveReputation(Entity<ContractsComponent> ent, int amount)
{
if (amount == 0)
return false;
ent.Comp.Reputation = Math.Clamp(ent.Comp.Reputation + amount, 0, 100);
Dirty(ent);
if (TryComp<MindReputationComponent>(ent.Comp.Mind, out var mind))
mind.Reputation = ent.Comp.Reputation;
UpdateLevel(ent);
return true;
}
/// <summary>
/// Gets the level prototype for a given reputation.
/// </summary>
public ReputationLevelPrototype? GetLevel(int rep)
{
foreach (var proto in _levels)
{
if (rep >= proto.Reputation)
return proto;
}
return null;
}
#endregion
private bool IsLocked(TimeSpan? nextUnlock)
{
return nextUnlock is {} unlock && _timing.CurTime < unlock;
}
private int? FindOpenSlot(Entity<ContractsComponent> ent)
{
for (var i = 0; i < ent.Comp.Slots.Count; i++)
{
if (ent.Comp.Objectives[i] != null)
continue;
if (IsLocked(ent.Comp.Slots[i].NextUnlock))
continue;
return i;
}
return null;
}
private int? FindContract(Entity<ContractsComponent> ent, EntityUid objective)
{
for (var i = 0; i < ent.Comp.Slots.Count; i++)
{
if (ent.Comp.Objectives[i] == objective)
return i;
}
return null;
}
private void ClearSlot(Entity<ContractsComponent> ent, int index, TimeSpan delay)
{
// old objective is intentionally not deleted, objective stays in the character menu for your greentextful glory / redtextful shame
ent.Comp.Objectives[index] = null;
ent.Comp.Slots[index] = new ContractSlot()
{
NextUnlock = _timing.CurTime + delay
};
Dirty(ent);
}
private void UpdateLevel(Entity<ContractsComponent> ent)
{
var old = ent.Comp.CurrentLevel;
ent.Comp.CurrentLevel = GetLevel(ent.Comp.Reputation);
UpdateContractSlots(ent);
UpdateOfferingSlots(ent);
}
private void UpdateContractSlots(Entity<ContractsComponent> ent)
{
var oldSlots = ent.Comp.Slots.Count;
var newSlots = ent.Comp.CurrentLevel?.MaxContracts ?? 0;
if (oldSlots == newSlots)
return;
if (newSlots > oldSlots)
{
// levelling up, add new slot(s)
for (var i = oldSlots; i < newSlots; i++)
{
ent.Comp.Objectives.Add(null);
ent.Comp.Slots.Add(new ContractSlot());
}
}
else
{
// this should never happen but removing objectives just incase
for (var i = newSlots; i > oldSlots; i--)
{
var j = i - 1;
var objective = ent.Comp.Objectives[j];
ContractFailed(ent, objective);
ent.Comp.Objectives.RemoveAt(j);
ent.Comp.Slots.RemoveAt(j);
}
}
Dirty(ent);
}
private void UpdateOfferingSlots(Entity<ContractsComponent> ent)
{
var oldSlots = ent.Comp.OfferingSlots.Count;
var newSlots = ent.Comp.CurrentLevel?.MaxOfferings ?? 0;
if (oldSlots == newSlots)
return;
if (newSlots > oldSlots)
{
// levelling up, add new slot(s)
for (var i = oldSlots; i < newSlots; i++)
{
ent.Comp.Offerings.Add(null);
ent.Comp.OfferingSlots.Add(new OfferingSlot());
}
}
else
{
// this should never happen but removing objectives just incase
for (var i = newSlots; i > oldSlots; i--)
{
var j = i - 1;
var objective = ent.Comp.Offerings[j];
Del(objective);
ent.Comp.Offerings.RemoveAt(j);
ent.Comp.OfferingSlots.RemoveAt(j);
}
}
Dirty(ent);
}
private void ContractFailed(Entity<ContractsComponent> ent, EntityUid? uid)
{
if (GetMind(ent) is not {} mind)
return;
if (uid is not {} objective)
return;
var ev = new ContractFailedEvent(ent);
RaiseLocalEvent(objective, ref ev);
_mind.TryRemoveObjective(mind, objective);
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
{
if (!args.WasModified<ReputationLevelPrototype>())
return;
CacheLevels();
}
private void CacheLevels()
{
_levels.Clear();
foreach (var proto in _proto.EnumeratePrototypes<ReputationLevelPrototype>())
{
_levels.Add(proto);
}
// sort levels by their reputation requirement, descending
// this allows GetLevel to work
_levels.Sort((a, b) => (b.Reputation.CompareTo(a.Reputation)));
}
}

View File

@ -0,0 +1,15 @@
namespace Content.Shared._DV.Traitor;
/// <summary>
/// Added to an entity being extracted with a syndie fulton.
/// Used to control whos objectives get completed.
/// </summary>
[RegisterComponent, Access(typeof(SharedExtractionFultonSystem))]
public sealed partial class ExtractingComponent : Component
{
/// <summary>
/// Mind of the player that extracted it.
/// </summary>
[DataField]
public EntityUid? Mind;
}

View File

@ -0,0 +1,23 @@
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Traitor;
/// <summary>
/// A marker that extraction beacons can teleport entities to.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedExtractionFultonSystem))]
public sealed partial class ExtractionBeaconComponent : Component
{
/// <summary>
/// If defined, entities must match this whitelist to get teleported here.
/// </summary>
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// If defined, entities cannot match this blacklist to get teleported here.
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
}

View File

@ -0,0 +1,32 @@
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Traitor;
/// <summary>
/// Fulton that can be used for traitor extraction and ransom objectives.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedExtractionFultonSystem))]
public sealed partial class ExtractionFultonComponent : Component
{
[DataField]
public TimeSpan ApplyDelay = TimeSpan.FromSeconds(3);
/// <summary>
/// How long it takes for a stolen item to get sent to the vault.
/// </summary>
[DataField]
public TimeSpan ItemDelay = TimeSpan.FromSeconds(30);
/// <summary>
/// How long it takes for a mob to get sent to jail.
/// </summary>
[DataField]
public TimeSpan MobDelay = TimeSpan.FromSeconds(60);
/// <summary>
/// Sound that gets played when the fulton is applied.
/// </summary>
[DataField]
public SoundSpecifier? FultonSound = new SoundPathSpecifier("/Audio/Items/Mining/fultext_deploy.ogg");
}

View File

@ -0,0 +1,37 @@
using Robust.Shared.Map;
using Robust.Shared.Serialization;
namespace Content.Shared._DV.Traitor;
/// <summary>
/// This entity is being held ransom and can be purchased to teleport to the ATS.
/// </summary>
[RegisterComponent, Access(typeof(RansomSystem))]
public sealed partial class RansomComponent : Component
{
[DataField]
public int Ransom;
/// <summary>
/// The map the entity is being held on.
/// Ransom is ended if the entity leaves this map for any reason.
/// </summary>
[DataField]
public MapId Map = MapId.Nullspace;
}
/// <summary>
/// Ransom data for an entity visible on a cargo request console.
/// </summary>
[Serializable, NetSerializable]
public readonly record struct RansomData(NetEntity Entity, string Name, int Price);
/// <summary>
/// BUI message for a cargo request console to purchase a ransomed entity.
/// It gets teleported to the ATS if successful.
/// </summary>
[Serializable, NetSerializable]
public sealed class RansomPurchaseMessage(NetEntity entity) : BoundUserInterfaceMessage
{
public readonly NetEntity Entity = entity;
}

View File

@ -0,0 +1,100 @@
using Content.Shared._DV.CCVars;
using Content.Shared.Humanoid;
using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Configuration;
namespace Content.Shared._DV.Traitor;
/// <summary>
/// Provides API for ransoming entities.
/// </summary>
public sealed class RansomSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly MobStateSystem _mob = default!;
[Dependency] private readonly SharedJobSystem _job = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
private float _mobBase;
private float _humanoidBase;
private float _deadMod;
private float _critMod;
private List<RansomData> _ransoms = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RansomComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<RansomComponent, MoveEvent>(OnMove);
Subs.CVar(_cfg, DCCVars.MobRansom, n => _mobBase = n, true);
Subs.CVar(_cfg, DCCVars.HumanoidRansom, n => _humanoidBase = n, true);
Subs.CVar(_cfg, DCCVars.RansomDeadModifier, n => _deadMod = n, true);
Subs.CVar(_cfg, DCCVars.RansomCritModifier, n => _critMod = n, true);
}
private void OnMapInit(Entity<RansomComponent> ent, ref MapInitEvent args)
{
ent.Comp.Map = Transform(ent).MapID;
}
private void OnMove(Entity<RansomComponent> ent, ref MoveEvent args)
{
// remove ransom when its paid, or if they sneak a fulton/whatever into the jail, or get admin help
if (Transform(ent).MapID != ent.Comp.Map)
RemCompDeferred<RansomComponent>(ent);
}
/// <summary>
/// Ransoms an entity and returns the price.
/// </summary>
public int RansomEntity(EntityUid uid)
{
var ransom = GetRansom(uid);
EnsureComp<RansomComponent>(uid).Ransom = ransom;
return ransom;
}
/// <summary>
/// Get the price for an entity's ransom.
/// </summary>
public int GetRansom(EntityUid uid)
{
var ransom = HasComp<HumanoidAppearanceComponent>(uid)
? _humanoidBase
: _mobBase;
// hostages are more valuable alive than dead, shocker
if (_mob.IsDead(uid))
ransom *= _deadMod;
else if (_mob.IsCritical(uid))
ransom *= _critMod;
// multiply by the job's ransom value
if (_mind.GetMind(uid) is {} mind && _job.MindTryGetJob(mind, out var job))
ransom *= job.RansomModifier;
return (int) ransom;
}
/// <summary>
/// Get a list of all ransomed mobs for sending to cargo request console UI.
/// It is reused, do not modify it.
/// </summary>
public List<RansomData> GetRansoms()
{
_ransoms.Clear();
var query = EntityQueryEnumerator<RansomComponent>();
while (query.MoveNext(out var uid, out var comp))
{
var ent = GetNetEntity(uid);
var name = Name(uid);
var price = comp.Ransom;
_ransoms.Add(new RansomData(ent, name, price));
}
return _ransoms;
}
}

View File

@ -0,0 +1,88 @@
using Content.Shared.DoAfter;
using Content.Shared.Popups;
using Content.Shared.Salvage.Fulton;
using Content.Shared.Verbs;
using Content.Shared.Whitelist;
using Robust.Shared.Containers;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
namespace Content.Shared._DV.Traitor;
public abstract class SharedExtractionFultonSystem : EntitySystem
{
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] protected readonly SharedPopupSystem Popup = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ExtractionFultonComponent, GetVerbsEvent<UtilityVerb>>(OnGetVerbs);
SubscribeLocalEvent<FultonedComponent, ContainerGettingInsertedAttemptEvent>(OnInsertAttempt);
}
private void OnGetVerbs(Entity<ExtractionFultonComponent> ent, ref GetVerbsEvent<UtilityVerb> args)
{
var target = args.Target;
var user = args.User;
args.Verbs.Add(new UtilityVerb()
{
Act = () => AttachFulton(ent, target, user),
Text = Loc.GetString("extraction-fulton-verb-text"),
Disabled = FindBeacon(ent, target) != null
});
}
private void OnInsertAttempt(Entity<FultonedComponent> ent, ref ContainerGettingInsertedAttemptEvent args)
{
args.Cancel();
if (_net.IsServer)
Popup.PopupEntity(Loc.GetString("extraction-fulton-remove-first"), ent);
}
protected virtual void AttachFulton(Entity<ExtractionFultonComponent> ent, EntityUid target, EntityUid user)
{
}
public EntityUid? FindBeacon(Entity<ExtractionFultonComponent> ent, EntityUid target)
{
// TODO: whitelist for the fulton to support non-traitor uses
var query = EntityQueryEnumerator<ExtractionBeaconComponent>();
while (query.MoveNext(out var uid, out var beacon))
{
if (ValidTarget(beacon, target))
return uid;
}
return null;
}
/// <summary>
/// Returns whether an extraction beacon can accept a given target entity.
/// </summary>
public bool ValidTarget(ExtractionBeaconComponent comp, EntityUid uid)
{
return _whitelist.IsWhitelistPassOrNull(comp.Whitelist, uid)
&& !_whitelist.IsBlacklistPass(comp.Blacklist, uid);
}
}
/// <summary>
/// Raised on an entity after it has been fultoned to somewhere.
/// </summary>
[ByRefEvent]
public record struct FultonedEvent;
[Serializable, NetSerializable]
public sealed partial class ExtractionFultonDoAfterEvent : SimpleDoAfterEvent
{
[DataField]
public NetEntity? Beacon;
public ExtractionFultonDoAfterEvent(NetEntity? beacon = null)
{
Beacon = beacon;
}
}

View File

@ -0,0 +1,7 @@
ghost-role-information-syndicate-marshal-name = Syndicate Marshal
ghost-role-information-syndicate-marshal-description = Run the syndicate prison and keep the hostages in line.
ghost-role-information-syndicate-marshal-rules =
You are a [color=green][bold]Non-antagonist[/bold][/color] working for the Syndicate.
Don't abuse prisoners, they are hostages being kept for ransom!
You can /ghost if you get bored, the role will stay available for others to take.
All normal rules apply unless an administrator tells you otherwise.

View File

@ -32,3 +32,10 @@ guide-entry-glimmer-creatures = Glimmer Creatures
guide-entry-trade-station = Trade Station
guide-entry-frequently-used-chemicals = Frequently Used Chemicals
guide-entry-contracts = Syndicate Contracts
guide-entry-assisting = Assisting Traitors
guide-entry-extraction = Extraction
guide-entry-ransom = Ransom
guide-entry-murder = Murder
guide-entry-special-objectives = Special Objectives

View File

@ -0,0 +1 @@
nuke-disk-teleported = {CAPITALIZE(THE($disk))} materializes into your hands!

View File

@ -0,0 +1,2 @@
objective-condition-assist-traitor-title = Assist fellow traitor {$targetName}, {CAPITALIZE($job)}
objective-condition-assist-traitor-description = Help your fellow traitor to complete this contract: {$contract}

View File

@ -1,3 +1,4 @@
# Traitor
steal-target-groups-plutonium-core = plutonium core
steal-target-groups-lucky-bill = logistics officer's lucky bill
steal-target-groups-ian-dossier = head of personnel's photobook
@ -7,7 +8,18 @@ steal-target-groups-notary-stamp = notary stamp
steal-target-groups-silvia = silvia
steal-target-groups-box-folder-rd-clipboard = research digi-board
steal-target-groups-bible-mystagogue = book of mysteries
steal-target-groups-rcd = RCD
steal-target-groups-research-computer-circuitboard = R&D computer board
steal-target-groups-cargo-request-computer-circuitboard = cargo request computer board
steal-target-groups-cargo-bounty-computer-circuitboard = cargo bounty computer board
steal-target-groups-criminal-records-computer-circuitboard = criminal records computer board
steal-target-groups-id-computer-circuitboard = ID computer board
steal-target-groups-comms-computer-circuitboard = comms computer board
# Recruiter
steal-target-groups-recruiter-pen = recruiter's pen
# Ninja
steal-target-groups-captains-cloak = captain's cloak
steal-target-groups-engineering-techfab-circuitboard = engineering techfab's circuitboard
steal-target-groups-logistics-techfab-circuitboard = logistics techfab's circuitboard

View File

@ -1 +1,7 @@
objective-condition-extract-title-no-owner = Extract the {$itemName}
objective-condition-extract-title-alive-no-owner = Extract {$itemName}
objective-condition-extract-title = Extract the {$owner}'s {$itemName}
objective-condition-extract-description = We need you to extract {$itemName} using syndicate fultons from your uplink.
objective-condition-steal-nuclear-bomb = nuclear bomb
objective-condition-steal-engineering = engineering department

View File

@ -1 +1,2 @@
objective-condition-teach-person-title = Teach {$targetName}, {CAPITALIZE($job)} a lesson
objective-condition-make-example-title = Make an example of {$targetName}, {CAPITALIZE($job)}

View File

@ -0,0 +1,15 @@
extraction-fulton-not-target = You don't need to extract this.
extraction-fulton-dead = They need to be alive to be extracted!
extraction-fulton-no-destination = No destination found!
extraction-fulton-anchored = It needs to be unanchored first.
extraction-fulton-verb-text = Attach Fulton
extraction-fulton-remove-first = Remove the fulton first!
syndicate-ransom-announcement-sender = Automated Systems
syndicate-ransom-announcement =
Transmissions received from the Syndicate. They have taken {$hostage} as a ransomed hostage.
They are requesting {$ransom} spesos. Entry automatically added to the Cargo Request Computer.
Threat of taking too much time: HIGH.
syndicate-ransom-return-announcement-sender = Automated Trade Station
syndicate-ransom-return-announcement = Unauthorized delivery received at [{$station}] from [UNKNOWN SENDER]. Crew are to inspect it and may claim it if desired.

View File

@ -0,0 +1,4 @@
objective-condition-ransom-title = Ransom {$targetName}, {$job}
ransom-ui-warning = [color=red][bold]WARNING: Ransomed crewmembers detected![/bold][/color]
ransom-ui-purchase = Buy back {$name}

View File

@ -0,0 +1,5 @@
reputation-level-unknown-agent = Unknown Agent
reputation-level-preferred-pawn = Preferred Pawn
reputation-level-reputable-insider = Reputable Insider
reputation-level-reliable-contact = Reliable Contact
reputation-level-syndicate-operative = Syndicate Operative

View File

@ -0,0 +1,2 @@
pda-bound-user-interface-contracts-title = Contract Hub
pda-bound-user-interface-contracts-description = Complete syndicate contracts to earn the big bucks.

View File

@ -0,0 +1,12 @@
# UI
contracts-menu-title = Contract Hub
contracts-contracts = Active Contracts
contracts-offerings = Available
contracts-rescan = Rescan
contracts-complete = Complete
contracts-accept = Accept
contracts-reject = Reject
contract-slot-empty = <no contract active>
contract-unavailable = <no contract available>
contract-next-unlock = Locked for {$time}

View File

@ -15,3 +15,6 @@ uplink-syndicate-hostage-implanter-bundle-desc = These implants pacify when inje
uplink-objective-syndicate-board-name = Syndicate law board
uplink-objective-syndicate-board-desc = Its expensive, don't lose it!
uplink-syndie-fulton-name = Syndicate Fultons
uplink-syndie-fulton-desc = Proprietary Waffle Corp fultons that send high value items to the Syndicate Vault for processing, or send sophonts to the Syndicate Jail for ransom.

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,9 @@
Telecrystal: 4 # DeltaV - was 3
categories:
- UplinkWeaponry
conditions: # DeltaV - Sidearm reputation
- !type:ReputationCondition
reputation: 40
- type: listing
id: UplinkRevolverPython
@ -26,6 +29,9 @@
Telecrystal: 6 # DeltaV - was 4
categories:
- UplinkWeaponry
conditions: # DeltaV - Sidearm reputation
- !type:ReputationCondition
reputation: 40
# Inbuilt suppressor so it's sneaky + more expensive.
- type: listing
@ -40,6 +46,9 @@
Telecrystal: 6 # DeltaV - was 4
categories:
- UplinkWeaponry
conditions: # DeltaV - Sidearm reputation
- !type:ReputationCondition
reputation: 45
# Poor accuracy, slow to fire, cheap option
- type: listing
@ -51,6 +60,9 @@
Telecrystal: 1
categories:
- UplinkWeaponry
conditions: # DeltaV - Long arm reputation
- !type:ReputationCondition
reputation: 60
- type: listing
id: UplinkEsword
@ -65,6 +77,9 @@
Telecrystal: 8
categories:
- UplinkWeaponry
conditions: # DeltaV - Melee weapon reputation
- !type:ReputationCondition
reputation: 30
- type: listing
id: UplinkEnergyDagger
@ -79,6 +94,9 @@
Telecrystal: 2
categories:
- UplinkWeaponry
conditions: # DeltaV - Melee weapon reputation
- !type:ReputationCondition
reputation: 20
- type: listing
id: UplinkThrowingKnivesKit
@ -93,6 +111,9 @@
Telecrystal: 4 # DeltaV - was 6
categories:
- UplinkWeaponry
conditions: # DeltaV - Melee weapons reputation
- !type:ReputationCondition
reputation: 20
- type: listing
id: UplinkGlovesNorthStar
@ -106,6 +127,9 @@
Telecrystal: 8
categories:
- UplinkWeaponry
conditions: # DeltaV - Melee weapons reputation
- !type:ReputationCondition
reputation: 30
- type: listing
id: UplinkDisposableTurret
@ -124,6 +148,8 @@
blacklist:
tags:
- NukeOpsUplink
- !type:ReputationCondition # DeltaV - Long arms(???) reputation
reputation: 60
- type: listing
id: UplinkEshield
@ -143,6 +169,8 @@
whitelist:
tags:
- NukeOpsUplink
- !type:ReputationCondition # DeltaV - Sidearms(?????) reputation
reputation: 40
- type: listing
id: UplinkSniperBundle
@ -152,11 +180,14 @@
productEntity: BriefcaseSyndieSniperBundleFilled
discountCategory: usualDiscounts
discountDownTo:
Telecrystal: 5 # DeltaV - was 6
Telecrystal: 6
cost:
Telecrystal: 10 # DeltaV - was 12 changed because the Hristov is REALLY bad
Telecrystal: 12
categories:
- UplinkWeaponry
conditions: # DeltaV - Long Arms reputation
- !type:ReputationCondition
reputation: 60
- type: listing
id: UplinkC20RBundle
@ -171,6 +202,9 @@
Telecrystal: 20 # DeltaV - was 17
categories:
- UplinkWeaponry
conditions: # DeltaV - Long Arms reputation
- !type:ReputationCondition
reputation: 60
- type: listing
id: UplinkBulldogBundle
@ -185,6 +219,9 @@
Telecrystal: 22 # DeltaV - was 20
categories:
- UplinkWeaponry
conditions: # DeltaV - Long Arms reputation
- !type:ReputationCondition
reputation: 60
- type: listing
id: UplinkGrenadeLauncherBundle
@ -199,6 +236,9 @@
Telecrystal: 40 # DeltaV - Was 25
categories:
- UplinkWeaponry
conditions: # DeltaV - High Power Weapons reputation
- !type:ReputationCondition
reputation: 95
- type: listing
id: UplinkL6SawBundle
@ -213,6 +253,9 @@
Telecrystal: 28 # DeltaV - Was 12
categories:
- UplinkWeaponry
conditions: # DeltaV - High Power Weapons reputation
- !type:ReputationCondition
reputation: 80
# Explosives
@ -228,6 +271,9 @@
Telecrystal: 2 # DeltaV - was 4
categories:
- UplinkExplosives
conditions: # DeltaV - Explosives reputation
- !type:ReputationCondition
reputation: 40
- type: listing
id: UplinkExplosiveGrenadeFlash
@ -261,6 +307,9 @@
Telecrystal: 6
categories:
- UplinkExplosives
conditions: # DeltaV - Explosives reputation
- !type:ReputationCondition
reputation: 50
- type: listing
id: UplinkSingularityGrenade
@ -274,6 +323,9 @@
Telecrystal: 6 # DeltaV - was 2, this is explosive now
categories:
- UplinkExplosives # DeltaV - this is explosive here
conditions: # DeltaV - Explosives reputation
- !type:ReputationCondition
reputation: 55
- type: listing
id: UplinkWhiteholeGrenade
@ -287,6 +339,9 @@
Telecrystal: 2
categories:
- UplinkDisruption
conditions: # DeltaV - Explosives reputation
- !type:ReputationCondition
reputation: 40
- type: listing
id: UplinkGrenadePenguin
@ -305,6 +360,8 @@
blacklist:
components:
- SurplusBundle
- !type:ReputationCondition # DeltaV - Explosives reputation
reputation: 40
- type: listing
id: UplinkC4
@ -318,6 +375,9 @@
Telecrystal: 2
categories:
- UplinkExplosives
conditions: # DeltaV - Explosives reputation
- !type:ReputationCondition
reputation: 40
- type: listing
id: UplinkGrenadierRig
@ -349,6 +409,9 @@
Telecrystal: 12 #you're buying bulk so its a 25% discount, so no additional random discount over it
categories:
- UplinkExplosives
conditions: # DeltaV - More Powerful Explosions reputation
- !type:ReputationCondition
reputation: 65
- type: listing
id: UplinkEmpGrenade
@ -362,6 +425,9 @@
Telecrystal: 2
categories:
- UplinkExplosives
conditions: # DeltaV - Explosives reputation
- !type:ReputationCondition
reputation: 40
- type: listing
id: UplinkExplodingPen
@ -376,6 +442,9 @@
Telecrystal: 3 # DeltaV - was 4
categories:
- UplinkExplosives
conditions: # DeltaV - Explosives reputation
- !type:ReputationCondition
reputation: 40
- type: listing
id: UplinkSyndicateBomb
@ -392,6 +461,8 @@
blacklist:
tags:
- NukeOpsUplink
- !type:ReputationCondition # DeltaV - More Powerful Explosions reputation
reputation: 75
- type: listing
id: UplinkSyndicateBombNukie
@ -420,6 +491,9 @@
Telecrystal: 6 # DeltaV - was 8
categories:
- UplinkExplosives
conditions: # DeltaV - More Powerful Explosions reputation
- !type:ReputationCondition
reputation: 60
- type: listing
id: UplinkGrenadeShrapnel
@ -433,6 +507,9 @@
Telecrystal: 3 # DeltaV - was 4
categories:
- UplinkExplosives
conditions: # DeltaV - Explosives reputation
- !type:ReputationCondition
reputation: 45
- type: listing
id: UplinkGrenadeIncendiary
@ -446,6 +523,9 @@
Telecrystal: 4
categories:
- UplinkExplosives
conditions: # DeltaV - Explosives reputation
- !type:ReputationCondition
reputation: 45
- type: listing
id: UplinkEmpKit
@ -459,6 +539,9 @@
Telecrystal: 5 # DeltaV - was 6
categories:
- UplinkExplosives
conditions: # DeltaV - Explosives reputation
- !type:ReputationCondition
reputation: 50
# Ammo
@ -565,6 +648,9 @@
Telecrystal: 8 # DeltaV - was 6
categories:
- UplinkChemicals
conditions: # DeltaV - Chemicals reputation
- !type:ReputationCondition
reputation: 25
- type: listing
id: UplinkHypoDart
@ -579,6 +665,9 @@
Telecrystal: 1 # DeltaV - was 2
categories:
- UplinkChemicals
conditions: # DeltaV - Chemicals reputation
- !type:ReputationCondition
reputation: 20
- type: listing
id: UplinkChemistryKitBundle
@ -593,6 +682,9 @@
Telecrystal: 4
categories:
- UplinkChemicals
conditions: # DeltaV - Chemicals reputation
- !type:ReputationCondition
reputation: 20
- type: listing
id: UplinkZombieBundle
@ -626,6 +718,9 @@
Telecrystal: 6
categories:
- UplinkChemicals
conditions: # DeltaV - Chemicals reputation
- !type:ReputationCondition
reputation: 25
- type: listing
id: UplinkCombatMedkit
@ -639,6 +734,9 @@
Telecrystal: 5
categories:
- UplinkChemicals
conditions: # DeltaV - Chemicals reputation
- !type:ReputationCondition
reputation: 30
- type: listing
id: UplinkCombatMedipen
@ -652,6 +750,9 @@
Telecrystal: 3 # DeltaV - was 4
categories:
- UplinkChemicals
conditions: # DeltaV - Chemicals reputation
- !type:ReputationCondition
reputation: 25
- type: listing
id: UplinkStimpack
@ -665,6 +766,9 @@
Telecrystal: 3 # DeltaV - was 4
categories:
- UplinkChemicals
conditions: # DeltaV - Chemicals reputation
- !type:ReputationCondition
reputation: 20
- type: listing
id: UplinkStimkit
@ -678,6 +782,9 @@
Telecrystal: 10 # DeltaV - was 12
categories:
- UplinkChemicals
conditions: # DeltaV - Chemicals reputation
- !type:ReputationCondition
reputation: 35
- type: listing
id: UplinkCigarettes
@ -752,7 +859,7 @@
categories:
- UplinkDeception
# Delta-V: replaced by syndicate radio implant
# DeltaV - replaced by syndicate radio implant
#- type: listing
# id: UplinkHeadsetEncryptionKey
# name: uplink-encryption-key-name
@ -784,6 +891,9 @@
Telecrystal: 1
categories:
- UplinkDeception
conditions: # DeltaV - Melee Weapons reputation
- !type:ReputationCondition
reputation: 20
- type: listing
id: UplinkDecoyDisk
@ -820,6 +930,9 @@
Telecrystal: 4
categories:
- UplinkDeception
conditions: # DeltaV - Money is expensive? no bribing fresh out of cryo
- !type:ReputationCondition
reputation: 15
# - type: listing
# id: UplinkGigacancerScanner
@ -857,6 +970,9 @@
Telecrystal: 3 # DeltaV - was 4
categories:
- UplinkDeception
conditions: # DeltaV - Explosives, to not have fake syndie bombs 5 minutes into the round
- !type:ReputationCondition
reputation: 40
# Disruption
@ -886,6 +1002,9 @@
Telecrystal: 4 # DeltaV - was 5
categories:
- UplinkDisruption
conditions: # DeltaV - it's iconic...
- !type:ReputationCondition
reputation: 20
- type: listing
id: UplinkRadioJammer
@ -912,6 +1031,9 @@
Telecrystal: 5
categories:
- UplinkDisruption
conditions: # DeltaV - Side Arms reputation
- !type:ReputationCondition
reputation: 40
- type: listing
id: UplinkSyndicateMartyrModule
@ -926,6 +1048,9 @@
Telecrystal: 4
categories:
- UplinkDisruption
conditions: # DeltaV - More Powerful Explosions reputation
- !type:ReputationCondition
reputation: 60
- type: listing
id: UplinkSoapSyndie
@ -949,6 +1074,9 @@
Telecrystal: 2
categories:
- UplinkDisruption
conditions: # DeltaV - do 1 task before you get super soap
- !type:ReputationCondition
reputation: 5
- type: listing
id: UplinkToolbox
@ -1007,6 +1135,8 @@
blacklist:
components:
- SurplusBundle
- !type:ReputationCondition # DeltaV - Explosives reputation
reputation: 50
- type: listing
id: UplinkAntimovCircuitBoard
@ -1025,6 +1155,8 @@
blacklist:
tags:
- NukeOpsUplink
- !type:ReputationCondition # DeltaV - Station Destructive Items reputation
reputation: 80
- type: listing
id: UplinkNukieAntimovCircuitBoard
@ -1089,20 +1221,14 @@
productEntity: SingularityBeacon
discountCategory: usualDiscounts
discountDownTo:
Telecrystal: 4
Telecrystal: 10 # DeltaV - was 4
cost:
Telecrystal: 12
Telecrystal: 16 # DeltaV - was 12
categories:
- UplinkDisruption
conditions: # DeltaV - Blacklists from traitor uplink and removes from surplus bundle
- !type:StoreWhitelistCondition
whitelist:
tags:
- NukeOpsUplink
- !type:BuyerWhitelistCondition
blacklist:
components:
- SurplusBundle
conditions: # DeltaV - Station Destructive Items reputation
- !type:ReputationCondition
reputation: 85
# DeltaV: disabled in favour of observation kit
#- type: listing
@ -1135,6 +1261,8 @@
blacklist:
tags:
- NukeOpsUplink
- !type:ReputationCondition # DeltaV - Allies reputation
reputation: 50
- type: listing
id: UplinkReinforcementRadioSyndicate
@ -1154,6 +1282,8 @@
blacklist:
tags:
- NukeOpsUplink
- !type:ReputationCondition # DeltaV - Allies reputation
reputation: 45
- type: listing
id: UplinkReinforcementRadioSyndicateNukeops # Version for Nukeops that spawns another nuclear operative without the uplink.
@ -1205,6 +1335,8 @@
blacklist:
tags:
- NukeOpsUplink
- !type:ReputationCondition # DeltaV - Allies reputation but its a monkey so less
reputation: 30
- type: listing
id: UplinkReinforcementRadioSyndicateAncestorNukeops # Version for Nukeops that spawns a syndicate monkey with the NukeOperative component.
@ -1256,6 +1388,9 @@
# Telecrystal: 6
# categories:
# - UplinkAllies
# conditions: # DeltaV - Allies reputation
# - !type:ReputationCondition
# reputation: 40
- type: listing
id: UplinkSyndicatePersonalAI
@ -1336,6 +1471,9 @@
Telecrystal: 5
categories:
- UplinkImplants
conditions: # DeltaV - You don't need this for stealing shoes...
- !type:ReputationCondition
reputation: 25
- type: listing
id: UplinkEmpImplanter
@ -1350,6 +1488,9 @@
Telecrystal: 2
categories:
- UplinkImplants
conditions: # DeltaV - Melee Weapons reputation, its melee range
- !type:ReputationCondition
reputation: 25
- type: listing
id: UplinkMicroBombImplanter
@ -1401,8 +1542,10 @@
Telecrystal: 3 # DeltaV- Was 4
categories:
- UplinkImplants
# conditions: # DeltaV- Allows Death Acidifer for our Syndibros
# - !type:StoreWhitelistCondition
conditions:
- !type:ReputationCondition # DeltaV - Chemicals reputation
reputation: 20
# - !type:StoreWhitelistCondition # DeltaV - Allows Death Acidifer for our Syndibros
# whitelist:
# tags:
# - NukeOpsUplink
@ -1548,6 +1691,9 @@
Telecrystal: 7 # DeltaV - Was 4, 4TC was too cheap for the items power
categories:
- UplinkWearables
conditions: # DeltaV - They're very strong, shouldn't have it roundstart
- !type:ReputationCondition
reputation: 20
- type: listing
id: UplinkClothingOuterVestWeb
@ -1561,6 +1707,9 @@
Telecrystal: 3
categories:
- UplinkWearables
conditions: # DeltaV - Armour
- !type:ReputationCondition
reputation: 15
- type: listing
id: UplinkClothingOuterVestWebElite
@ -1602,6 +1751,9 @@
Telecrystal: 1 # DeltaV - was 2
categories:
- UplinkWearables
conditions: # DeltaV - Evil EVA suit
- !type:ReputationCondition
reputation: 10
- type: listing
id: UplinkHardsuitCarp
@ -1616,6 +1768,9 @@
Telecrystal: 3 # DeltaV - was 4
categories:
- UplinkWearables
conditions: # DeltaV - Funny EVA suit
- !type:ReputationCondition
reputation: 15
- type: listing
id: UplinkHardsuitSyndie
@ -1630,6 +1785,9 @@
Telecrystal: 8
categories:
- UplinkWearables
conditions: # DeltaV - Hardsuits reputation
- !type:ReputationCondition
reputation: 65
- type: listing
id: UplinkClothingOuterArmorRaid
@ -1767,15 +1925,18 @@
categories:
- UplinkPointless
#- type: listing # DeltaV - Remove cat ears
# id: UplinkCatEars
# name: uplink-cat-ears-name
# description: uplink-cat-ears-desc
# productEntity: ClothingHeadHatCatEars
# cost:
# Telecrystal: 26
# categories:
# - UplinkPointless
- type: listing
id: UplinkCatEars
name: uplink-cat-ears-name
description: uplink-cat-ears-desc
productEntity: ClothingHeadHatCatEars
cost:
Telecrystal: 26
categories:
- UplinkPointless
conditions: # DeltaV - Station Destructive Items...
- !type:ReputationCondition
reputation: 100
- type: listing
id: UplinkOutlawHat
@ -1852,6 +2013,9 @@
Telecrystal: 1 # DeltaV - was 20
categories:
- UplinkPointless
conditions: # DeltaV
- !type:ReputationCondition
reputation: 50
- type: listing
id: UplinkScarfSyndieRed
@ -1903,6 +2067,8 @@
whitelist:
- Botanist
- ServiceWorker # DeltaV
- !type:ReputationCondition # DeltaV - Less than Sidearms as you have to actually grow it
reputation: 30
- type: listing
id: uplinkRiggedBoxingGlovesPassenger
@ -1920,6 +2086,8 @@
- !type:BuyerJobCondition
whitelist:
- Passenger
- !type:ReputationCondition # DeltaV - Melee Weapons reputation
reputation: 20
- type: listing
id: uplinkRiggedBoxingGlovesBoxer
@ -1937,6 +2105,8 @@
- !type:BuyerJobCondition
whitelist:
- Boxer
- !type:ReputationCondition # DeltaV - Melee Weapons reputation
reputation: 20
- type: listing
id: uplinkNecronomicon
@ -1958,6 +2128,8 @@
blacklist:
components:
- SurplusBundle
- !type:ReputationCondition # DeltaV - Allies reputation
reputation: 40
- type: listing
id: uplinkHolyHandGrenade
@ -1975,6 +2147,8 @@
- !type:BuyerJobCondition
whitelist:
- Chaplain
- !type:ReputationCondition # DeltaV - More Powerful Explosions
reputation: 70
- type: listing
id: uplinkRevolverCapGunFake
@ -1993,6 +2167,8 @@
whitelist:
- Mime
- Clown
- !type:ReputationCondition # DeltaV - Sidearms reputation
reputation: 40
- type: listing
id: uplinkBananaPeelExplosive
@ -2011,6 +2187,8 @@
- !type:BuyerJobCondition
whitelist:
- Clown
- !type:ReputationCondition # DeltaV - Explosives reputation
reputation: 40
- type: listing
id: UplinkClusterBananaPeel
@ -2028,6 +2206,8 @@
- !type:BuyerJobCondition
whitelist:
- Clown
- !type:ReputationCondition # DeltaV - More Powerful Explosions reputation
reputation: 65
- type: listing
id: UplinkHoloclownKit
@ -2046,6 +2226,8 @@
- !type:BuyerJobCondition
whitelist:
- Clown
- !type:ReputationCondition # DeltaV - Allies reputation
reputation: 45
- type: listing
id: uplinkHotPotato
@ -2067,6 +2249,8 @@
- Clown
- Mime
- ServiceWorker # DeltaV
- !type:ReputationCondition # DeltaV - Explosives reputation
reputation: 40
- type: listing
id: UplinkChimpUpgradeKit
@ -2084,6 +2268,8 @@
- !type:BuyerDepartmentCondition
whitelist:
- Epistemics # DeltaV - Epistemics Department replacing Science
- !type:ReputationCondition # DeltaV - Sidearms reputation
reputation: 40
- type: listing
id: uplinkProximityMine
@ -2105,6 +2291,8 @@
blacklist:
components:
- SurplusBundle
- !type:ReputationCondition # DeltaV - Explosives reputation
reputation: 45
- type: listing
id: UplinkSyndicateSpongeBox
@ -2148,6 +2336,8 @@
blacklist:
components:
- SurplusBundle
- !type:ReputationCondition # DeltaV - Melee Weapons reputation
reputation: 20
- type: listing
id: UplinkCombatBakery
@ -2167,6 +2357,8 @@
whitelist:
- Chef
- Mime
- !type:ReputationCondition # DeltaV - Melee Weapons reputation
reputation: 20
- type: listing
id: UplinkSmugglerSatchel

View File

@ -72,6 +72,8 @@
state: cpu_security
- type: ComputerBoard
prototype: ComputerCriminalRecords
- type: StealTarget # DeltaV
stealGroup: CriminalRecordsComputerCircuitboard
- type: entity
parent: BaseComputerCircuitboard
@ -96,6 +98,8 @@
prototype: ComputerCargoOrders
- type: StaticPrice
price: 750
- type: StealTarget # DeltaV
stealGroup: CargoRequestComputerCircuitboard
- type: entity
parent: BaseComputerCircuitboard
@ -197,6 +201,8 @@
- type: ComputerBoard
prototype: ComputerCargoBounty
- type: StaticPrice
- type: StealTarget # DeltaV
stealGroup: CargoBountyComputerCircuitboard
- type: entity
parent: BaseComputerCircuitboard
@ -271,6 +277,8 @@
state: cpu_science
- type: ComputerBoard
prototype: ComputerResearchAndDevelopment
- type: StealTarget # DeltaV
stealGroup: ResearchComputerCircuitboard
- type: entity
parent: BaseComputerCircuitboard
@ -324,6 +332,8 @@
- type: Tag
tags:
- HighRiskItem
- type: StealTarget # DeltaV
stealGroup: IDComputerCircuitboard
- type: entity
parent: BaseComputerCircuitboard
@ -346,6 +356,8 @@
state: cpu_command
- type: ComputerBoard
prototype: ComputerComms
- type: StealTarget # DeltaV
stealGroup: CommsComputerCircuitboard
- type: entity
parent: [ BaseComputerCircuitboard, BaseSyndicateContraband ]

View File

@ -164,10 +164,13 @@
- type: Store
balance:
Telecrystal: 0
- type: Contracts # DeltaV - Forced 0 reputation to prevent buying bloodreds etc on it
- type: UserInterface
interfaces:
enum.StoreUiKey.Key:
type: StoreBoundUserInterface
enum.ContractsUiKey.Key: # DeltaV
type: ContractsBUI
- type: entity
parent: BaseSubdermalImplant

View File

@ -369,6 +369,8 @@
- type: ActivatableUI
inHandsOnly: true
key: enum.RcdUiKey.Key
- type: StealTarget # DeltaV
stealGroup: RCD
- type: entity
id: RCDEmpty

View File

@ -99,6 +99,7 @@
abstract: true
components:
- type: StationCentcomm
- type: SyndieJail # DeltaV
- type: entity
id: BaseStationEvacuation

View File

@ -99,6 +99,7 @@
- NamesOperationSuffix
nameFormat: name-format-nuclear-operation
- type: NukeopsRule
- type: TeleportDiskRule # DeltaV - syndie fulton disk interaction
- type: RuleGrids
- type: AntagSelection
- type: AntagLoadProfileRule
@ -187,10 +188,10 @@
- type: TraitorRule
# TODO: codewords in yml
# TODO: uplink in yml
- type: AntagRandomObjectives
sets:
- groups: TraitorObjectiveGroups
maxDifficulty: 5
#- type: AntagRandomObjectives # DeltaV - replaced by contracts system
# sets:
# - groups: TraitorObjectiveGroups
# maxDifficulty: 5
- type: AntagSelection
agentName: traitor-round-end-agent-name
@ -203,6 +204,8 @@
delay:
min: 240
max: 420
- type: TraitorRule # DeltaV - roundstart traitors have less TC as they have time to complete more contracts
startingBalance: 10
- type: AntagSelection
selectionTime: IntraPlayerSpawn
definitions:

View File

@ -17,6 +17,8 @@
id: Traitors
name: guide-entry-traitors
text: "/ServerInfo/Guidebook/Antagonist/Traitors.xml"
children: # DeltaV
- Contracts
- type: guideEntry
id: NuclearOperatives

View File

@ -14,6 +14,8 @@
- !type:BuyerSpeciesCondition
whitelist:
- Oni
- !type:ReputationCondition
reputation: 30 # Melee Weapons
- type: listing
id: UplinkRickenbacker
@ -31,6 +33,8 @@
- !type:BuyerJobCondition
whitelist:
- Musician
- !type:ReputationCondition
reputation: 20 # Melee Weapons
- type: listing
id: UplinkSamurai
@ -52,3 +56,5 @@
blacklist:
components:
- SurplusBundle
- !type:ReputationCondition
reputation: 15 # Armour

View File

@ -4,49 +4,6 @@
components:
- type: NotJobRequirement
job: ForensicMantis
- type: StealCondition
- type: ExtractCondition
stealGroup: AntiPsychicKnife
owner: job-name-mantis
# /-- This objective does not encourage antagonism, thus 1984
#- type: entity
# id: BecomePsionicObjective
# parent: BaseTraitorObjective
# name: Become psionic
# description: We need you to acquire psionics and keep them until your mission is complete.
# components:
# - type: NotJobsRequirement
# jobs:
# - Mime
# - ForensicMantis
# - type: Objective
# difficulty: 2.5
# #unique: false
# icon:
# sprite: Nyanotrasen/Icons/psi.rsi
# state: psi
# - type: ObjectiveBlacklistRequirement
# blacklist:
# components:
# - BecomeGolemCondition
# - type: BecomePsionicCondition
#- type: entity
# id: BecomeGolemObjective
# parent: BaseTraitorObjective
# name: objective-condition-become-golem-title
# description: objective-condition-become-golem-description.
# components:
# - type: NotJobRequirement
# job: Chaplain
# - type: Objective
# difficulty: 3.5
# #unique: false
# icon:
# sprite: Nyanotrasen/Mobs/Species/Golem/cult.rsi
# state: full
# - type: ObjectiveBlacklistRequirement
# blacklist:
# components:
# - BecomePsionicCondition
# - type: BecomeGolemCondition

Some files were not shown because too many files have changed in this diff Show More