Cargo Bounties (#17344)

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
This commit is contained in:
Nemanja
2023-06-22 07:49:33 -04:00
committed by GitHub
parent ad89184d70
commit 7ab5127286
35 changed files with 1269 additions and 6 deletions

View File

@@ -0,0 +1,54 @@
using Content.Client.Cargo.UI;
using Content.Shared.Cargo.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
namespace Content.Client.Cargo.BUI;
[UsedImplicitly]
public sealed class CargoBountyConsoleBoundUserInterface : BoundUserInterface
{
[ViewVariables]
private CargoBountyMenu? _menu;
public CargoBountyConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_menu = new();
_menu.OnClose += Close;
_menu.OnLabelButtonPressed += id =>
{
SendMessage(new BountyPrintLabelMessage(id));
};
_menu.OpenCentered();
}
protected override void UpdateState(BoundUserInterfaceState message)
{
base.UpdateState(message);
if (message is not CargoBountyConsoleState state)
return;
_menu?.UpdateEntries(state.Bounties);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_menu?.Dispose();
}
}

View File

@@ -0,0 +1,27 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
Margin="10 10 10 0"
HorizontalExpand="True"
Visible="True">
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True">
<BoxContainer Orientation="Vertical"
HorizontalExpand="True">
<BoxContainer Orientation="Horizontal">
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<RichTextLabel Name="TimeLabel"/>
<RichTextLabel Name="RewardLabel"/>
<RichTextLabel Name="ManifestLabel"/>
</BoxContainer>
<Control MinWidth="10"/>
<BoxContainer Orientation="Vertical" MinWidth="120">
<Button Name="PrintButton" Text="{Loc 'bounty-console-label-button-text'}" HorizontalExpand="False" HorizontalAlignment="Right"/>
<Label Name="IdLabel" HorizontalAlignment="Right" Margin="0 0 5 0"/>
</BoxContainer>
</BoxContainer>
<customControls:HSeparator Margin="5 10 5 10"/>
<BoxContainer>
<RichTextLabel Name="DescriptionLabel"/>
</BoxContainer>
</BoxContainer>
</PanelContainer>
</BoxContainer>

View File

@@ -0,0 +1,54 @@
using Content.Client.Message;
using Content.Shared.Cargo;
using Content.Shared.Cargo.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Cargo.UI;
[GenerateTypedNameReferences]
public sealed partial class BountyEntry : BoxContainer
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
public Action? OnButtonPressed;
public TimeSpan EndTime;
public BountyEntry(CargoBountyData bounty)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
if (!_prototype.TryIndex<CargoBountyPrototype>(bounty.Bounty, out var bountyPrototype))
return;
EndTime = bounty.EndTime;
var items = new List<string>();
foreach (var entry in bountyPrototype.Entries)
{
items.Add(Loc.GetString("bounty-console-manifest-entry",
("amount", entry.Amount),
("item", Loc.GetString(entry.Name))));
}
ManifestLabel.SetMarkup(Loc.GetString("bounty-console-manifest-label", ("item", string.Join(", ", items))));
RewardLabel.SetMarkup(Loc.GetString("bounty-console-reward-label", ("reward", bountyPrototype.Reward)));
DescriptionLabel.SetMarkup(Loc.GetString("bounty-console-description-label", ("description", Loc.GetString(bountyPrototype.Description))));
IdLabel.Text = Loc.GetString("bounty-console-id-label", ("id", bounty.Id));
PrintButton.OnPressed += _ => OnButtonPressed?.Invoke();
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
var remaining = TimeSpan.FromSeconds(Math.Max((EndTime - _timing.CurTime).TotalSeconds, 0));
TimeLabel.SetMarkup(Loc.GetString("bounty-console-time-label", ("time", remaining.ToString("mm':'ss"))));
}
}

View File

@@ -0,0 +1,36 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'bounty-console-menu-title'}"
SetSize="550 420"
MinSize="400 350">
<BoxContainer Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True">
<PanelContainer VerticalExpand="True" HorizontalExpand="True" Margin="10">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<ScrollContainer HScrollEnabled="False"
HorizontalExpand="True"
VerticalExpand="True">
<BoxContainer Name="BountyEntriesContainer"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True">
</BoxContainer>
</ScrollContainer>
</PanelContainer>
<!-- Footer -->
<BoxContainer Orientation="Vertical">
<PanelContainer StyleClasses="LowDivider" />
<BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
<Label Text="{Loc 'bounty-console-flavor-left'}" StyleClasses="WindowFooterText" />
<Label Text="{Loc 'bounty-console-flavor-right'}" StyleClasses="WindowFooterText"
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
<TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,34 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Cargo;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Cargo.UI;
[GenerateTypedNameReferences]
public sealed partial class CargoBountyMenu : FancyWindow
{
public Action<int>? OnLabelButtonPressed;
public CargoBountyMenu()
{
RobustXamlLoader.Load(this);
}
public void UpdateEntries(List<CargoBountyData> bounties)
{
BountyEntriesContainer.Children.Clear();
foreach (var b in bounties)
{
var entry = new BountyEntry(b);
entry.OnButtonPressed += () => OnLabelButtonPressed?.Invoke(b.Id);
BountyEntriesContainer.AddChild(entry);
}
BountyEntriesContainer.AddChild(new Control
{
MinHeight = 10
});
}
}

View File

@@ -51,6 +51,46 @@ public sealed class CargoTest
await pairTracker.CleanReturnAsync();
}
[Test]
public async Task NoCargoBountyArbitageTest()
{
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings() {NoClient = true});
var server = pairTracker.Pair.Server;
var testMap = await PoolManager.CreateTestMap(pairTracker);
var entManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var protoManager = server.ResolveDependency<IPrototypeManager>();
var cargo = entManager.System<CargoSystem>();
var bounties = protoManager.EnumeratePrototypes<CargoBountyPrototype>().ToList();
await server.WaitAssertion(() =>
{
var mapId = testMap.MapId;
Assert.Multiple(() =>
{
foreach (var proto in protoManager.EnumeratePrototypes<CargoProductPrototype>())
{
var ent = entManager.SpawnEntity(proto.Product, new MapCoordinates(Vector2.Zero, mapId));
foreach (var bounty in bounties)
{
if (cargo.IsBountyComplete(ent, bounty))
Assert.That(proto.PointCost, Is.GreaterThan(bounty.Reward), $"Found arbitrage on {bounty.ID} cargo bounty! Product {proto.ID} costs {proto.PointCost} but fulfills bounty {bounty.ID} with reward {bounty.Reward}!");
}
entManager.DeleteEntity(ent);
}
});
mapManager.DeleteMap(mapId);
});
await pairTracker.CleanReturnAsync();
}
[Test]
public async Task NoStaticPriceAndStackPrice()

View File

@@ -0,0 +1,20 @@
namespace Content.Server.Cargo.Components;
/// <summary>
/// This is used for marking containers as
/// containing goods for fulfilling bounties.
/// </summary>
[RegisterComponent]
public sealed class CargoBountyLabelComponent : Component
{
/// <summary>
/// The ID for the bounty this label corresponds to.
/// </summary>
[DataField("id"), ViewVariables(VVAccess.ReadWrite)]
public int Id;
/// <summary>
/// Used to prevent recursion in calculating the price.
/// </summary>
public bool Calculating;
}

View File

@@ -0,0 +1,47 @@
using Content.Shared.Cargo;
namespace Content.Server.Cargo.Components;
/// <summary>
/// Stores all active cargo bounties for a particular station.
/// </summary>
[RegisterComponent]
public sealed class StationCargoBountyDatabaseComponent : Component
{
/// <summary>
/// Maximum amount of bounties a station can have.
/// </summary>
[DataField("maxBounties"), ViewVariables(VVAccess.ReadWrite)]
public int MaxBounties = 3;
/// <summary>
/// A list of all the bounties currently active for a station.
/// </summary>
[DataField("bounties"), ViewVariables(VVAccess.ReadWrite)]
public List<CargoBountyData> Bounties = new();
/// <summary>
/// Used to determine unique order IDs
/// </summary>
[DataField("totalBounties")]
public int TotalBounties;
/// <summary>
/// A poor-man's weighted list of the durations for how long
/// each bounty will last.
/// </summary>
[DataField("bountyDurations")]
public List<TimeSpan> BountyDurations = new()
{
TimeSpan.FromMinutes(5),
TimeSpan.FromMinutes(7.5f),
TimeSpan.FromMinutes(7.5f),
TimeSpan.FromMinutes(7.5f),
TimeSpan.FromMinutes(10),
TimeSpan.FromMinutes(10),
TimeSpan.FromMinutes(10),
TimeSpan.FromMinutes(10),
TimeSpan.FromMinutes(10),
TimeSpan.FromMinutes(15)
};
}

View File

@@ -0,0 +1,341 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Cargo.Components;
using Content.Server.Labels;
using Content.Server.Paper;
using Content.Shared.Cargo;
using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Database;
using JetBrains.Annotations;
using Robust.Server.Containers;
using Robust.Server.GameObjects;
using Robust.Shared.Collections;
using Robust.Shared.Containers;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Cargo.Systems;
public sealed partial class CargoSystem
{
[Dependency] private readonly ContainerSystem _container = default!;
private void InitializeBounty()
{
SubscribeLocalEvent<CargoBountyConsoleComponent, BoundUIOpenedEvent>(OnBountyConsoleOpened);
SubscribeLocalEvent<CargoBountyConsoleComponent, BountyPrintLabelMessage>(OnPrintLabelMessage);
SubscribeLocalEvent<CargoBountyLabelComponent, PriceCalculationEvent>(OnGetBountyPrice);
SubscribeLocalEvent<EntitySoldEvent>(OnSold);
SubscribeLocalEvent<StationCargoBountyDatabaseComponent, MapInitEvent>(OnMapInit);
}
private void OnBountyConsoleOpened(EntityUid uid, CargoBountyConsoleComponent component, BoundUIOpenedEvent args)
{
if (_station.GetOwningStation(uid) is not { } station ||
!TryComp<StationCargoBountyDatabaseComponent>(station, out var bountyDb))
return;
_uiSystem.TrySetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(bountyDb.Bounties));
}
private void OnPrintLabelMessage(EntityUid uid, CargoBountyConsoleComponent component, BountyPrintLabelMessage args)
{
if (_timing.CurTime < component.NextPrintTime)
return;
if (_station.GetOwningStation(uid) is not { } station)
return;
if (!TryGetBountyFromId(station, args.BountyId, out var bounty))
return;
var label = Spawn(component.BountyLabelId, Transform(uid).Coordinates);
component.NextPrintTime = _timing.CurTime + component.PrintDelay;
SetupBountyLabel(label, bounty.Value);
_audio.PlayPvs(component.PrintSound, uid);
}
public void SetupBountyLabel(EntityUid uid, CargoBountyData bounty, PaperComponent? paper = null, CargoBountyLabelComponent? label = null)
{
if (!Resolve(uid, ref paper, ref label) || !_protoMan.TryIndex<CargoBountyPrototype>(bounty.Bounty, out var prototype))
return;
label.Id = bounty.Id;
var msg = new FormattedMessage();
msg.AddText(Loc.GetString("bounty-manifest-header", ("id", bounty.Id)));
msg.PushNewline();
msg.AddText(Loc.GetString("bounty-manifest-list-start"));
msg.PushNewline();
foreach (var entry in prototype.Entries)
{
msg.AddMarkup($"- {Loc.GetString("bounty-console-manifest-entry",
("amount", entry.Amount),
("item", Loc.GetString(entry.Name)))}");
msg.PushNewline();
}
_paperSystem.SetContent(uid, msg.ToMarkup(), paper);
}
/// <summary>
/// Bounties do not sell for any currency. The reward for a bounty is
/// calculated after it is sold separately from the selling system.
/// </summary>
private void OnGetBountyPrice(EntityUid uid, CargoBountyLabelComponent component, ref PriceCalculationEvent args)
{
if (args.Handled || component.Calculating)
return;
// make sure this label was actually applied to a crate.
if (!_container.TryGetContainingContainer(uid, out var container) || container.ID != LabelSystem.ContainerName)
return;
if (_station.GetOwningStation(uid) is not { } station)
return;
if (!TryGetBountyFromId(station, component.Id, out var bounty))
return;
if (!_protoMan.TryIndex<CargoBountyPrototype>(bounty.Value.Bounty, out var bountyProtoype) ||!IsBountyComplete(container.Owner, bountyProtoype))
return;
args.Handled = true;
component.Calculating = true;
args.Price = bountyProtoype.Reward - _pricing.GetPrice(container.Owner);
component.Calculating = false;
}
private void OnSold(ref EntitySoldEvent args)
{
var containerQuery = GetEntityQuery<ContainerManagerComponent>();
var labelQuery = GetEntityQuery<CargoBountyLabelComponent>();
foreach (var sold in args.Sold)
{
if (!containerQuery.TryGetComponent(sold, out var containerMan))
continue;
// make sure this label was actually applied to a crate.
if (!_container.TryGetContainer(sold, LabelSystem.ContainerName, out var container, containerMan))
continue;
if (container.ContainedEntities.FirstOrNull() is not { } label ||
!labelQuery.TryGetComponent(label, out var component))
continue;
if (!TryGetBountyFromId(args.Station, component.Id, out var bounty))
continue;
if (!IsBountyComplete(container.Owner, bounty.Value))
continue;
TryRemoveBounty(args.Station, bounty.Value);
FillBountyDatabase(args.Station);
_adminLogger.Add(LogType.Action, LogImpact.Low, $"Bounty \"{bounty.Value.Bounty}\" (id:{bounty.Value.Id}) was fulfilled");
}
}
private void OnMapInit(EntityUid uid, StationCargoBountyDatabaseComponent component, MapInitEvent args)
{
FillBountyDatabase(uid, component);
}
/// <summary>
/// Fills up the bounty database with random bounties.
/// </summary>
public void FillBountyDatabase(EntityUid uid, StationCargoBountyDatabaseComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
while (component.Bounties.Count < component.MaxBounties)
{
if (!TryAddBounty(uid, component))
break;
}
UpdateBountyConsoles();
}
public bool IsBountyComplete(EntityUid container, CargoBountyData data)
{
if (!_protoMan.TryIndex<CargoBountyPrototype>(data.Bounty, out var proto))
return false;
return IsBountyComplete(container, proto.Entries);
}
public bool IsBountyComplete(EntityUid container, string id)
{
if (!_protoMan.TryIndex<CargoBountyPrototype>(id, out var proto))
return false;
return IsBountyComplete(container, proto.Entries);
}
public bool IsBountyComplete(EntityUid container, CargoBountyPrototype prototype)
{
return IsBountyComplete(container, prototype.Entries);
}
public bool IsBountyComplete(EntityUid container, IEnumerable<CargoBountyItemEntry> entries)
{
var contained = new HashSet<EntityUid>();
if (TryComp<ContainerManagerComponent>(container, out var containers))
{
foreach (var con in containers.Containers.Values)
{
if (con.ID == LabelSystem.ContainerName)
continue;
foreach (var ent in con.ContainedEntities)
{
contained.Add(ent);
}
}
}
return IsBountyComplete(contained, entries);
}
public bool IsBountyComplete(HashSet<EntityUid> entities, IEnumerable<CargoBountyItemEntry> entries)
{
foreach (var entry in entries)
{
var count = 0;
// store entities that already satisfied an
// entry so we don't double-count them.
var temp = new HashSet<EntityUid>();
foreach (var entity in entities)
{
if (!entry.Whitelist.IsValid(entity, EntityManager))
continue;
count++;
temp.Add(entity);
if (count >= entry.Amount)
break;
}
if (count < entry.Amount)
return false;
foreach (var ent in temp)
{
entities.Remove(ent);
}
}
return true;
}
[PublicAPI]
public bool TryAddBounty(EntityUid uid, StationCargoBountyDatabaseComponent? component = null)
{
// todo: consider making the cargo bounties weighted.
var bounty = _random.Pick(_protoMan.EnumeratePrototypes<CargoBountyPrototype>().ToList());
return TryAddBounty(uid, bounty, component);
}
[PublicAPI]
public bool TryAddBounty(EntityUid uid, string bountyId, StationCargoBountyDatabaseComponent? component = null)
{
if (!_protoMan.TryIndex<CargoBountyPrototype>(bountyId, out var bounty))
{
return false;
}
return TryAddBounty(uid, bounty, component);
}
public bool TryAddBounty(EntityUid uid, CargoBountyPrototype bounty, StationCargoBountyDatabaseComponent? component = null)
{
if (!Resolve(uid, ref component))
return false;
if (component.Bounties.Count >= component.MaxBounties)
return false;
var endTime = _timing.CurTime + _random.Pick(component.BountyDurations) + TimeSpan.FromSeconds(_random.Next(-10, 10));
component.Bounties.Add(new CargoBountyData(component.TotalBounties, bounty.ID, endTime));
_adminLogger.Add(LogType.Action, LogImpact.Low, $"Added bounty \"{bounty.ID}\" (id:{component.TotalBounties}) to station {ToPrettyString(uid)}");
component.TotalBounties++;
return true;
}
[PublicAPI]
public bool TryRemoveBounty(EntityUid uid, int dataId, StationCargoBountyDatabaseComponent? component = null)
{
if (!TryGetBountyFromId(uid, dataId, out var data, component))
return false;
return TryRemoveBounty(uid, data.Value, component);
}
public bool TryRemoveBounty(EntityUid uid, CargoBountyData data, StationCargoBountyDatabaseComponent? component = null)
{
if (!Resolve(uid, ref component))
return false;
for (var i = 0; i < component.Bounties.Count; i++)
{
if (component.Bounties[i].Id == data.Id)
{
component.Bounties.RemoveAt(i);
return true;
}
}
return false;
}
public bool TryGetBountyFromId(
EntityUid uid,
int id,
[NotNullWhen(true)] out CargoBountyData? bounty,
StationCargoBountyDatabaseComponent? component = null)
{
bounty = null;
if (!Resolve(uid, ref component))
return false;
foreach (var bountyData in component.Bounties)
{
if (bountyData.Id != id)
continue;
bounty = bountyData;
break;
}
return bounty != null;
}
public void UpdateBountyConsoles()
{
var query = EntityQueryEnumerator<CargoBountyConsoleComponent, ServerUserInterfaceComponent>();
while (query.MoveNext(out var uid, out _, out var ui))
{
if (_station.GetOwningStation(uid) is not { } station ||
!TryComp<StationCargoBountyDatabaseComponent>(station, out var db))
continue;
_uiSystem.TrySetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties), ui: ui);
}
}
private void UpdateBounty()
{
var query = EntityQueryEnumerator<StationCargoBountyDatabaseComponent>();
while (query.MoveNext(out var uid, out var bountyDatabase))
{
var bounties = new ValueList<CargoBountyData>(bountyDatabase.Bounties);
foreach (var bounty in bounties)
{
if (_timing.CurTime < bounty.EndTime)
continue;
TryRemoveBounty(uid, bounty, bountyDatabase);
FillBountyDatabase(uid, bountyDatabase);
}
}
}
}

View File

@@ -20,6 +20,7 @@ using Robust.Shared.Prototypes;
using Content.Shared.Coordinates;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Robust.Shared.Containers;
namespace Content.Server.Cargo.Systems;
@@ -228,14 +229,21 @@ public sealed partial class CargoSystem
#region Station
private void SellPallets(EntityUid gridUid, out double amount)
private void SellPallets(EntityUid gridUid, EntityUid? station, out double amount)
{
station ??= _station.GetOwningStation(gridUid);
GetPalletGoods(gridUid, out var toSell, out amount);
_sawmill.Debug($"Cargo sold {toSell.Count} entities for {amount}");
foreach (var ent in toSell)
{
if (station != null)
{
var ev = new EntitySoldEvent(station.Value, toSell);
RaiseLocalEvent(ref ev);
}
Del(ent);
}
}
@@ -325,7 +333,7 @@ public sealed partial class CargoSystem
return;
}
SellPallets(gridUid, out var price);
SellPallets(gridUid, null, out var price);
var stackPrototype = _protoMan.Index<StackPrototype>(component.CashType);
_stack.Spawn((int)price, stackPrototype, uid.ToCoordinates());
UpdatePalletConsoleInterface(uid);
@@ -359,7 +367,7 @@ public sealed partial class CargoSystem
if (TryComp<StationBankAccountComponent>(stationUid, out var bank))
{
SellPallets(uid, out var amount);
SellPallets(uid, stationUid, out var amount);
bank.Balance += (int) amount;
}
}
@@ -424,3 +432,10 @@ public sealed partial class CargoSystem
_console.RefreshShuttleConsoles();
}
}
/// <summary>
/// Event broadcast raised by-ref before it is sold and
/// deleted but after the price has been calculated.
/// </summary>
[ByRefEvent]
public readonly record struct EntitySoldEvent(EntityUid Station, HashSet<EntityUid> Sold);

View File

@@ -15,12 +15,14 @@ using Robust.Server.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Random;
namespace Content.Server.Cargo.Systems;
public sealed partial class CargoSystem : SharedCargoSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
@@ -51,6 +53,7 @@ public sealed partial class CargoSystem : SharedCargoSystem
InitializeConsole();
InitializeShuttle();
InitializeTelepad();
InitializeBounty();
}
public override void Shutdown()
@@ -64,6 +67,7 @@ public sealed partial class CargoSystem : SharedCargoSystem
base.Update(frameTime);
UpdateConsole(frameTime);
UpdateTelepad(frameTime);
UpdateBounty();
}
[PublicAPI]

View File

@@ -0,0 +1,31 @@
using Robust.Shared.Serialization;
using Content.Shared.Cargo.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Cargo;
/// <summary>
/// A data structure for storing currently available bounties.
/// </summary>
[DataDefinition, NetSerializable, Serializable]
public readonly record struct CargoBountyData(int Id, string Bounty, TimeSpan EndTime)
{
/// <summary>
/// A numeric id used to identify the bounty
/// </summary>
[DataField("id"), ViewVariables(VVAccess.ReadWrite)]
public readonly int Id = Id;
/// <summary>
/// The prototype containing information about the bounty.
/// </summary>
[DataField("bounty", customTypeSerializer: typeof(PrototypeIdSerializer<CargoBountyPrototype>)), ViewVariables(VVAccess.ReadWrite)]
public readonly string Bounty = Bounty;
/// <summary>
/// The time at which the bounty is closed and no longer is available.
/// </summary>
[DataField("endTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
public readonly TimeSpan EndTime = EndTime;
}

View File

@@ -0,0 +1,57 @@
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Cargo.Components;
[RegisterComponent]
public sealed class CargoBountyConsoleComponent : Component
{
/// <summary>
/// The id of the label entity spawned by the print label button.
/// </summary>
[DataField("bountyLabelId", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string BountyLabelId = "PaperCargoBountyManifest";
/// <summary>
/// The time at which the console will be able to print a label again.
/// </summary>
[DataField("nextPrintTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextPrintTime = TimeSpan.Zero;
/// <summary>
/// The time between prints.
/// </summary>
[DataField("printDelay")]
public TimeSpan PrintDelay = TimeSpan.FromSeconds(5);
/// <summary>
/// The sound made when printing occurs
/// </summary>
[DataField("printSound")]
public SoundSpecifier PrintSound = new SoundPathSpecifier("/Audio/Machines/printer.ogg");
}
[NetSerializable, Serializable]
public sealed class CargoBountyConsoleState : BoundUserInterfaceState
{
public List<CargoBountyData> Bounties;
public CargoBountyConsoleState(List<CargoBountyData> bounties)
{
Bounties = bounties;
}
}
[Serializable, NetSerializable]
public sealed class BountyPrintLabelMessage : BoundUserInterfaceMessage
{
public int BountyId;
public BountyPrintLabelMessage(int bountyId)
{
BountyId = bountyId;
}
}

View File

@@ -0,0 +1,60 @@
using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Cargo.Prototypes;
/// <summary>
/// This is a prototype for a cargo bounty, a set of items
/// that must be sold together in a labeled container in order
/// to receive a monetary reward.
/// </summary>
[Prototype("cargoBounty"), Serializable, NetSerializable]
public sealed class CargoBountyPrototype : IPrototype
{
/// <inheritdoc/>
[IdDataField]
public string ID { get; } = default!;
/// <summary>
/// The monetary reward for completing the bounty
/// </summary>
[DataField("reward", required: true)]
public readonly int Reward;
/// <summary>
/// A description for flava purposes.
/// </summary>
[DataField("description")]
public readonly string Description = string.Empty;
/// <summary>
/// The entries that must be satisfied for the cargo bounty to be complete.
/// </summary>
[DataField("entries", required: true)]
public readonly List<CargoBountyItemEntry> Entries = new();
}
[DataDefinition, Serializable, NetSerializable]
public readonly record struct CargoBountyItemEntry()
{
/// <summary>
/// A whitelist for determining what items satisfy the entry.
/// </summary>
[DataField("whitelist", required: true)]
public readonly EntityWhitelist Whitelist = default!;
// todo: implement some kind of simple generic condition system
/// <summary>
/// How much of the item must be present to satisfy the entry
/// </summary>
[DataField("amount")]
public readonly int Amount = 1;
/// <summary>
/// A player-facing name for the item.
/// </summary>
[DataField("name")]
public readonly string Name = string.Empty;
}

View File

@@ -6,6 +6,7 @@ namespace Content.Shared.Cargo;
public enum CargoConsoleUiKey : byte
{
Orders,
Bounty,
Shuttle,
Telepad
}

View File

@@ -0,0 +1,41 @@
bounty-item-artifact = Alien artifact
bounty-item-baseball-bat = Baseball bat
bounty-item-box-hugs = Box of hugs
bounty-item-brain = Brain
bounty-item-briefcase = Briefcase
bounty-item-carp = Space carp
bounty-item-crayon = Crayon
bounty-item-donk-pocket = Donk-pocket
bounty-item-donut = Donut
bounty-item-figurine = Action figure
bounty-item-flower = Flower
bounty-item-lung = Lung
bounty-item-mouse = Dead mouse
bounty-item-research-disk = Research disk
bounty-item-soap = Soap
bounty-item-spear = Spear
bounty-item-toolbox = Toolbox
bounty-item-tech-disk = Technology disk
bounty-item-trash = Trash
bounty-item-pen = Pen
bounty-description-artifact = NanoTrasen is in some hot water for stealing artifacts from non-spacefaring planets. Return one and we'll compensate you for it.
bounty-description-baseball-bat = Baseball fever is going on at CentCom! Be a dear and ship them some baseball bats, so that management can live out their childhood dream.
bounty-description-box-hugs = Several chief officials have sustained serious boo-boos. A box of hugs is urgently needed to aid in their recovery.
bounty-description-brain = Commander Caldwell was rendered brain-dead by a recent space lube accident. Unfortunately, we can't hire a replacement, so just send us a new brain to put in her instead.
bounty-description-briefcase = Central Command will be holding a business convention this year. Ship a few briefcases in support.
bounty-description-carp = Admiral Pavlov has gone on strike ever since Central Command confiscated her "pet." She is demanding a space carp as a replacement, dead or alive.
bounty-description-crayon = Dr Jones' kid ate all our crayons again. Please send us yours.
bounty-description-donk-pocket = Consumer safety recall: Warning. Donk-Pockets manufactured in the past year contain hazardous lizard biomatter. Return units to CentCom immediately.
bounty-description-donut = CentCom's security forces are facing heavy losses against the Syndicate. Ship donuts to raise morale.
bounty-description-figurine = The vice president's son saw an ad for action figures on the telescreen and now he won't shut up about them. Ship some to ease his complaints.
bounty-description-flower = Commander Zot really wants to sweep Security Officer Olivia off her feet. Send a shipment of flowers and he'll happily reward you.
bounty-description-lung = The pro-smoking league has been fighting to keep cigarettes on our stations for millennia. Unfortunately, they're lungs aren't fighting so hard anymore. Send them some new ones.
bounty-description-mouse = Station 13 ran out of freeze-dried mice. Ship some fresh ones so their janitor doesn't go on strike.
bounty-description-research-disk = Turns out those bozos in the Research department have been spending all their time getting janitorial equipment. Send some research up to Central Command so we can actually get what we need.
bounty-description-soap = Soap has gone missing from CentCom's bathrooms and nobody knows who took it. Replace it and be the hero CentCom needs.
bounty-description-spear = CentCom's security forces are going through budget cuts. You will be paid if you ship a set of spears.
bounty-description-toolbox = There's an absence of robustness at Central Command. Hurry up and ship some toolboxes as a solution.
bounty-description-tech-disk = The new research assistant on Station 13 spilled a soda on the RND server. Send them some technology disks so they can build up their recipes.
bounty-description-trash = Recently a group of janitors have run out of trash to clean up, without any trash Centcom wants to fire them to cut costs. Send a shipment of trash to keep them employed, and they'll give you a small compensation.
bounty-description-pen = We are hosting the intergalactic pen balancing competition. We need you to send us some standardized ball point pens.

View File

@@ -0,0 +1,18 @@
bounty-console-menu-title = Cargo bounty console
bounty-console-label-button-text = Print label
bounty-console-time-label = Time: [color=orange]{$time}[/color]
bounty-console-reward-label = Reward: [color=limegreen]${$reward}[/color]
bounty-console-manifest-label = Manifest: [color=gray]{$item}[/color]
bounty-console-manifest-entry =
{ $amount ->
[1] {$item}
*[other] {$item} x{$amount}
}
bounty-console-description-label = [color=gray]{$description}[/color]
bounty-console-id-label = ID#{$id}
bounty-console-flavor-left = Bounties sourced from local unscrupulous dealers.
bounty-console-flavor-right = v1.4
bounty-manifest-header = Official cargo bounty manifest (ID#{$id})
bounty-manifest-list-start = Item manifest:

View File

@@ -13,6 +13,7 @@ guide-entry-controls = Controls
guide-entry-radio = Radio
guide-entry-jobs = Jobs
guide-entry-cargo = Cargo
guide-entry-cargo-bounties = Cargo Bounties
guide-entry-salvage = Salvage
guide-entry-survival = Survival
guide-entry-chemicals = Chemicals

View File

@@ -0,0 +1,219 @@
- type: cargoBounty
id: BountyArtifact
reward: 2500
description: bounty-description-artifact
entries:
- name: bounty-item-artifact
amount: 1
whitelist:
components:
- Artifact
- type: cargoBounty
id: BountyBaseballBat
reward: 800
description: bounty-description-baseball-bat
entries:
- name: bounty-item-baseball-bat
amount: 5
whitelist:
tags:
- BaseballBat
- type: cargoBounty
id: BountyBoxHug
reward: 600
description: bounty-description-box-hugs
entries:
- name: bounty-item-box-hugs
amount: 1
whitelist:
tags:
- BoxHug
- type: cargoBounty
id: BountyBrain
reward: 2000
description: bounty-description-brain
entries:
- name: bounty-item-brain
amount: 1
whitelist:
components:
- Brain
- type: cargoBounty
id: BountyBriefcase
reward: 1000
description: bounty-description-briefcase
entries:
- name: bounty-item-briefcase
amount: 5
whitelist:
tags:
- Briefcase
- type: cargoBounty
id: BountyCarp
reward: 2000
description: bounty-description-carp
entries:
- name: bounty-item-carp
amount: 1
whitelist:
tags:
- Carp
- type: cargoBounty
id: BountyCrayon
reward: 800
description: bounty-description-crayon
entries:
- name: bounty-item-crayon
amount: 24
whitelist:
tags:
- Crayon
- type: cargoBounty
id: BountyDonkPocket
reward: 1200
description: bounty-description-donk-pocket
entries:
- name: bounty-item-donk-pocket
amount: 12
whitelist:
tags:
- DonkPocket
- type: cargoBounty
id: BountyDonut
reward: 1200
description: bounty-description-donut
entries:
- name: bounty-item-donut
amount: 10
whitelist:
tags:
- Donut
- type: cargoBounty
id: BountyFigurine
reward: 1600
description: bounty-description-figurine
entries:
- name: bounty-item-figurine
amount: 5
whitelist:
tags:
- Figurine
- type: cargoBounty
id: BountyFlower
reward: 400
description: bounty-description-flower
entries:
- name: bounty-item-flower
amount: 3
whitelist:
tags:
- Flower
- type: cargoBounty
id: BountyLung
reward: 3000
description: bounty-description-lung
entries:
- name: bounty-item-lung
amount: 3
whitelist:
components:
- Lung
- type: cargoBounty
id: BountyMouse
reward: 600
description: bounty-description-mouse
entries:
- name: bounty-item-mouse
amount: 5
whitelist:
tags:
- Mouse
- type: cargoBounty
id: BountyResearchDisk
reward: 1200
description: bounty-description-research-disk
entries:
- name: bounty-item-research-disk
amount: 1
whitelist:
components:
- ResearchDisk
- type: cargoBounty
id: BountySoap
reward: 800
description: bounty-description-soap
entries:
- name: bounty-item-soap
amount: 3
whitelist:
tags:
- Soap
- type: cargoBounty
id: BountySpear
reward: 800
description: bounty-description-spear
entries:
- name: bounty-item-spear
amount: 5
whitelist:
tags:
- Spear
- type: cargoBounty
id: BountyTechDisk
reward: 2000
description: bounty-description-tech-disk
entries:
- name: bounty-item-tech-disk
amount: 10
whitelist:
components:
- TechnologyDisk
- type: cargoBounty
id: BountyToolbox
reward: 800
description: bounty-description-toolbox
entries:
- name: bounty-item-toolbox
amount: 6
whitelist:
tags:
- Toolbox
- type: cargoBounty
id: BountyTrash
reward: 400
description: bounty-description-trash
entries:
- name: bounty-item-trash
amount: 10
whitelist:
tags:
- Trash
- type: cargoBounty
id: BountyPen
reward: 800
description: bounty-description-pen
entries:
- name: bounty-item-pen
amount: 10
whitelist:
tags:
- Write

View File

@@ -20,6 +20,7 @@
- id: CargoShuttleComputerCircuitboard
- id: CargoShuttleConsoleCircuitboard
- id: SalvageShuttleConsoleCircuitboard
- id: CargoBountyComputerCircuitboard
- id: CigPackGreen
prob: 0.50
- id: DoorRemoteCargo

View File

@@ -908,6 +908,7 @@
tags:
- Trash
- CannotSuicide
- Mouse
- type: Respirator
damage:
types:

View File

@@ -78,6 +78,21 @@
- DroneUsable
- HighRiskItem
- type: entity
id: CargoBountyComputerCircuitboard
parent: BaseComputerCircuitboard
name: cargo bounty computer board
description: A computer printed circuit board for a cargo bounty computer.
components:
- type: Sprite
state: cpu_supply
- type: ComputerBoard
prototype: ComputerCargoBounty
- type: StaticPrice
- type: Tag
tags:
- DroneUsable
- type: entity
parent: BaseComputerCircuitboard
id: CargoShuttleComputerCircuitboard

View File

@@ -475,6 +475,9 @@
Plastic: 100
- type: StaticPrice
price: 10
- type: Tag
tags:
- Figurine
- type: entity
parent: BaseFigurine

View File

@@ -8,6 +8,9 @@
size: 60
- type: Storage
capacity: 60
- type: Tag
tags:
- Briefcase
- type: entity
name: brown briefcase

View File

@@ -127,6 +127,19 @@
headerImagePath: "/Textures/Interface/Paper/paper_heading_cargo_invoice.svg.96dpi.png"
headerMargin: 0.0, 12.0, 0.0, 0.0
- type: entity
id: PaperCargoBountyManifest
parent: PaperCargoInvoice
name: bounty manifest
description: A paper label designating a crate as containing a bounty. Selling a crate with this label will fulfill the bounty.
components:
- type: CargoBountyLabel
- type: StaticPrice
price: 0
- type: GuideHelp
guides:
- CargoBounties
- type: entity
parent: Paper
id: PaperWritten

View File

@@ -21,6 +21,7 @@
- type: Tag
tags:
- DroneUsable
- Toolbox
- type: entity
name: emergency toolbox

View File

@@ -25,3 +25,6 @@
- type: Construction
graph: WoodenBat
node: bat
- type: Tag
tags:
- BaseballBat

View File

@@ -10,6 +10,7 @@
components:
- type: StationBankAccount
- type: StationCargoOrderDatabase
- type: StationCargoBountyDatabase
- type: entity
id: BaseStationJobsSpawning

View File

@@ -713,6 +713,39 @@
ports:
- OrderSender
- type: entity
id: ComputerCargoBounty
parent: BaseComputer
name: cargo bounty computer
description: Used to manage currently active bounties.
components:
- type: Sprite
layers:
- map: ["computerLayerBody"]
state: computer
- map: ["computerLayerKeyboard"]
state: generic_keyboard
- map: ["computerLayerScreen"]
state: bounty
- map: ["computerLayerKeys"]
state: tech_key
- type: CargoBountyConsole
- type: ActivatableUI
key: enum.CargoConsoleUiKey.Bounty
- type: UserInterface
interfaces:
- key: enum.CargoConsoleUiKey.Bounty
type: CargoBountyConsoleBoundUserInterface
- type: Computer
board: CargoBountyComputerCircuitboard
- type: PointLight
radius: 1.5
energy: 1.6
color: "#b89f25"
- type: GuideHelp
guides:
- CargoBounties
- type: entity
parent: BaseComputer
id: ComputerCloningConsole

View File

@@ -1,4 +1,11 @@
- type: guideEntry
id: Cargo
name: guide-entry-cargo
text: "/ServerInfo/Guidebook/Cargo.xml"
text: "/ServerInfo/Guidebook/Cargo/Cargo.xml"
children:
- CargoBounties
- type: guideEntry
id: CargoBounties
name: guide-entry-cargo-bounties
text: "/ServerInfo/Guidebook/Cargo/CargoBounties.xml"

View File

@@ -19,6 +19,9 @@
- type: Tag
id: Baguette
- type: Tag
id: BaseballBat
- type: Tag
id: Bee
@@ -67,6 +70,9 @@
- type: Tag
id: BrassInstrument
- type: Tag
id: Briefcase
- type: Tag
id: Brutepack
@@ -260,6 +266,9 @@
- type: Tag
id: ExplosivePassable
- type: Tag
id: Figurine
- type: Tag
id: FireAlarm
@@ -529,6 +538,9 @@
- type: Tag
id: Mop
- type: Tag
id: Mouse
- type: Tag
id: Multitool
@@ -740,6 +752,9 @@
- type: Tag
id: TimerSignalElectronics
- type: Tag
id: Toolbox
- type: Tag
id: Trash

View File

@@ -26,6 +26,6 @@
<GuideEntityEmbed Entity="AppraisalTool"/>
<GuideEntityEmbed Entity="CargoPallet"/>
</Box>
After finding something worth selling, place it on one of the shuttle's cargo pallets. The next time the shuttle is sent to a trading post, the item will be sold and the money will be directly transferred back to the station's bank account.
After finding something worth selling, place it on one of the shuttle's cargo pallets. The next time the shuttle is sent to a trading post, the item will be sold and the money will be directly transferred back to the station's bank account. You can also make even more money by completing [textlink="bounties" link="CargoBounties"] or selling valuable items from [textlink="salvage" link="Salvage"].
</Document>

View File

@@ -0,0 +1,29 @@
<Document>
# Cargo Bounties
[textlink="Cargo" link="Cargo"] can always make money selling items whenever they feel like it. However, there are ways to make even more money by being more selective about what and when you're selling. One of these ways is [color=#a4885c]bounties[/color].
<Box>
<GuideEntityEmbed Entity="ComputerCargoBounty"/>
</Box>
Every station has a fixed amount of bounties that are globally available. These can be checked at the [color=#a4885c]cargo bounty computer[/color], which shows the following information about them:
- What items are needed to fulfill the bounty
- How much money do you get for completing the bounty
- How much time is left before the bounty expires
Assuming that you are able to aquire all of the items for it in a time efficient and low-cost manner, you will be able to complete the bounty and get a significantly higher payout than you would be selling the items conventionally.
## Completing Bounites
<Box>
<GuideEntityEmbed Entity="PaperCargoBountyManifest"/>
<GuideEntityEmbed Entity="CrateGenericSteel"/>
<GuideEntityEmbed Entity="CargoPallet"/>
</Box>
Once you have collected the items you need for a bounty, completing it is a simple process:
- Place all of the items inside of the crate and [bold]close it![/bold]
- Get the bounties manifest label by clicking the [color=#a4885c]print label[/color] button on corresponding entry on the cargo bounty computer.
- Place the label on the crate containing the items.
- Move the crate onto the cargo shuttle.
- Sell it.
And there you go! After you follow these steps, the bounty should disappear from the computer and a new one will take its place.
</Document>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -203,6 +203,44 @@
"name": "atmos_key_off",
"directions": 4
},
{
"name": "bounty",
"directions": 4,
"delays": [
[
0.3,
0.3,
0.3,
0.3,
0.3,
0.3
],
[
0.3,
0.3,
0.3,
0.3,
0.3,
0.3
],
[
0.3,
0.3,
0.3,
0.3,
0.3,
0.3
],
[
0.3,
0.3,
0.3,
0.3,
0.3,
0.3
]
]
},
{
"name": "broken",
"directions": 4