diff --git a/Content.Client/Cargo/BUI/CargoBountyConsoleBoundUserInterface.cs b/Content.Client/Cargo/BUI/CargoBountyConsoleBoundUserInterface.cs
new file mode 100644
index 0000000000..63587d9875
--- /dev/null
+++ b/Content.Client/Cargo/BUI/CargoBountyConsoleBoundUserInterface.cs
@@ -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();
+ }
+}
diff --git a/Content.Client/Cargo/UI/BountyEntry.xaml b/Content.Client/Cargo/UI/BountyEntry.xaml
new file mode 100644
index 0000000000..e570b03746
--- /dev/null
+++ b/Content.Client/Cargo/UI/BountyEntry.xaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Cargo/UI/BountyEntry.xaml.cs b/Content.Client/Cargo/UI/BountyEntry.xaml.cs
new file mode 100644
index 0000000000..54d1110840
--- /dev/null
+++ b/Content.Client/Cargo/UI/BountyEntry.xaml.cs
@@ -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(bounty.Bounty, out var bountyPrototype))
+ return;
+
+ EndTime = bounty.EndTime;
+
+ var items = new List();
+ 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"))));
+ }
+}
diff --git a/Content.Client/Cargo/UI/CargoBountyMenu.xaml b/Content.Client/Cargo/UI/CargoBountyMenu.xaml
new file mode 100644
index 0000000000..bb263ff6c4
--- /dev/null
+++ b/Content.Client/Cargo/UI/CargoBountyMenu.xaml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Cargo/UI/CargoBountyMenu.xaml.cs b/Content.Client/Cargo/UI/CargoBountyMenu.xaml.cs
new file mode 100644
index 0000000000..62ff31df89
--- /dev/null
+++ b/Content.Client/Cargo/UI/CargoBountyMenu.xaml.cs
@@ -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? OnLabelButtonPressed;
+
+ public CargoBountyMenu()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ public void UpdateEntries(List 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
+ });
+ }
+}
diff --git a/Content.IntegrationTests/Tests/CargoTest.cs b/Content.IntegrationTests/Tests/CargoTest.cs
index ed011aa530..61a451af8a 100644
--- a/Content.IntegrationTests/Tests/CargoTest.cs
+++ b/Content.IntegrationTests/Tests/CargoTest.cs
@@ -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();
+ var mapManager = server.ResolveDependency();
+ var protoManager = server.ResolveDependency();
+ var cargo = entManager.System();
+
+ var bounties = protoManager.EnumeratePrototypes().ToList();
+
+ await server.WaitAssertion(() =>
+ {
+ var mapId = testMap.MapId;
+
+ Assert.Multiple(() =>
+ {
+ foreach (var proto in protoManager.EnumeratePrototypes())
+ {
+ 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()
diff --git a/Content.Server/Cargo/Components/CargoBountyLabelComponent.cs b/Content.Server/Cargo/Components/CargoBountyLabelComponent.cs
new file mode 100644
index 0000000000..8c907bf84c
--- /dev/null
+++ b/Content.Server/Cargo/Components/CargoBountyLabelComponent.cs
@@ -0,0 +1,20 @@
+namespace Content.Server.Cargo.Components;
+
+///
+/// This is used for marking containers as
+/// containing goods for fulfilling bounties.
+///
+[RegisterComponent]
+public sealed class CargoBountyLabelComponent : Component
+{
+ ///
+ /// The ID for the bounty this label corresponds to.
+ ///
+ [DataField("id"), ViewVariables(VVAccess.ReadWrite)]
+ public int Id;
+
+ ///
+ /// Used to prevent recursion in calculating the price.
+ ///
+ public bool Calculating;
+}
diff --git a/Content.Server/Cargo/Components/StationCargoBountyDatabaseComponent.cs b/Content.Server/Cargo/Components/StationCargoBountyDatabaseComponent.cs
new file mode 100644
index 0000000000..656f64b542
--- /dev/null
+++ b/Content.Server/Cargo/Components/StationCargoBountyDatabaseComponent.cs
@@ -0,0 +1,47 @@
+using Content.Shared.Cargo;
+
+namespace Content.Server.Cargo.Components;
+
+///
+/// Stores all active cargo bounties for a particular station.
+///
+[RegisterComponent]
+public sealed class StationCargoBountyDatabaseComponent : Component
+{
+ ///
+ /// Maximum amount of bounties a station can have.
+ ///
+ [DataField("maxBounties"), ViewVariables(VVAccess.ReadWrite)]
+ public int MaxBounties = 3;
+
+ ///
+ /// A list of all the bounties currently active for a station.
+ ///
+ [DataField("bounties"), ViewVariables(VVAccess.ReadWrite)]
+ public List Bounties = new();
+
+ ///
+ /// Used to determine unique order IDs
+ ///
+ [DataField("totalBounties")]
+ public int TotalBounties;
+
+ ///
+ /// A poor-man's weighted list of the durations for how long
+ /// each bounty will last.
+ ///
+ [DataField("bountyDurations")]
+ public List 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)
+ };
+}
diff --git a/Content.Server/Cargo/Systems/CargoSystem.Bounty.cs b/Content.Server/Cargo/Systems/CargoSystem.Bounty.cs
new file mode 100644
index 0000000000..bd8fed644a
--- /dev/null
+++ b/Content.Server/Cargo/Systems/CargoSystem.Bounty.cs
@@ -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(OnBountyConsoleOpened);
+ SubscribeLocalEvent(OnPrintLabelMessage);
+ SubscribeLocalEvent(OnGetBountyPrice);
+ SubscribeLocalEvent(OnSold);
+ SubscribeLocalEvent(OnMapInit);
+ }
+
+ private void OnBountyConsoleOpened(EntityUid uid, CargoBountyConsoleComponent component, BoundUIOpenedEvent args)
+ {
+ if (_station.GetOwningStation(uid) is not { } station ||
+ !TryComp(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(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);
+ }
+
+ ///
+ /// Bounties do not sell for any currency. The reward for a bounty is
+ /// calculated after it is sold separately from the selling system.
+ ///
+ 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(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();
+ var labelQuery = GetEntityQuery();
+ 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);
+ }
+
+ ///
+ /// Fills up the bounty database with random bounties.
+ ///
+ 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(data.Bounty, out var proto))
+ return false;
+
+ return IsBountyComplete(container, proto.Entries);
+ }
+
+ public bool IsBountyComplete(EntityUid container, string id)
+ {
+ if (!_protoMan.TryIndex(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 entries)
+ {
+ var contained = new HashSet();
+ if (TryComp(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 entities, IEnumerable 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();
+ 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().ToList());
+ return TryAddBounty(uid, bounty, component);
+ }
+
+ [PublicAPI]
+ public bool TryAddBounty(EntityUid uid, string bountyId, StationCargoBountyDatabaseComponent? component = null)
+ {
+ if (!_protoMan.TryIndex(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();
+ while (query.MoveNext(out var uid, out _, out var ui))
+ {
+ if (_station.GetOwningStation(uid) is not { } station ||
+ !TryComp(station, out var db))
+ continue;
+
+ _uiSystem.TrySetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties), ui: ui);
+ }
+ }
+
+ private void UpdateBounty()
+ {
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var bountyDatabase))
+ {
+ var bounties = new ValueList(bountyDatabase.Bounties);
+ foreach (var bounty in bounties)
+ {
+ if (_timing.CurTime < bounty.EndTime)
+ continue;
+ TryRemoveBounty(uid, bounty, bountyDatabase);
+ FillBountyDatabase(uid, bountyDatabase);
+ }
+ }
+ }
+}
diff --git a/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs b/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs
index 99dfcbb166..a54ed58761 100644
--- a/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs
+++ b/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs
@@ -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(component.CashType);
_stack.Spawn((int)price, stackPrototype, uid.ToCoordinates());
UpdatePalletConsoleInterface(uid);
@@ -359,7 +367,7 @@ public sealed partial class CargoSystem
if (TryComp(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();
}
}
+
+///
+/// Event broadcast raised by-ref before it is sold and
+/// deleted but after the price has been calculated.
+///
+[ByRefEvent]
+public readonly record struct EntitySoldEvent(EntityUid Station, HashSet Sold);
diff --git a/Content.Server/Cargo/Systems/CargoSystem.cs b/Content.Server/Cargo/Systems/CargoSystem.cs
index b1fe4bc2ae..d56a97b4af 100644
--- a/Content.Server/Cargo/Systems/CargoSystem.cs
+++ b/Content.Server/Cargo/Systems/CargoSystem.cs
@@ -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]
diff --git a/Content.Shared/Cargo/CargoBountyData.cs b/Content.Shared/Cargo/CargoBountyData.cs
new file mode 100644
index 0000000000..8b5abd1412
--- /dev/null
+++ b/Content.Shared/Cargo/CargoBountyData.cs
@@ -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;
+
+///
+/// A data structure for storing currently available bounties.
+///
+[DataDefinition, NetSerializable, Serializable]
+public readonly record struct CargoBountyData(int Id, string Bounty, TimeSpan EndTime)
+{
+ ///
+ /// A numeric id used to identify the bounty
+ ///
+ [DataField("id"), ViewVariables(VVAccess.ReadWrite)]
+ public readonly int Id = Id;
+
+ ///
+ /// The prototype containing information about the bounty.
+ ///
+ [DataField("bounty", customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)]
+ public readonly string Bounty = Bounty;
+
+ ///
+ /// The time at which the bounty is closed and no longer is available.
+ ///
+ [DataField("endTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
+ public readonly TimeSpan EndTime = EndTime;
+}
diff --git a/Content.Shared/Cargo/Components/CargoBountyConsoleComponent.cs b/Content.Shared/Cargo/Components/CargoBountyConsoleComponent.cs
new file mode 100644
index 0000000000..e547fa59de
--- /dev/null
+++ b/Content.Shared/Cargo/Components/CargoBountyConsoleComponent.cs
@@ -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
+{
+ ///
+ /// The id of the label entity spawned by the print label button.
+ ///
+ [DataField("bountyLabelId", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string BountyLabelId = "PaperCargoBountyManifest";
+
+ ///
+ /// The time at which the console will be able to print a label again.
+ ///
+ [DataField("nextPrintTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
+ public TimeSpan NextPrintTime = TimeSpan.Zero;
+
+ ///
+ /// The time between prints.
+ ///
+ [DataField("printDelay")]
+ public TimeSpan PrintDelay = TimeSpan.FromSeconds(5);
+
+ ///
+ /// The sound made when printing occurs
+ ///
+ [DataField("printSound")]
+ public SoundSpecifier PrintSound = new SoundPathSpecifier("/Audio/Machines/printer.ogg");
+}
+
+[NetSerializable, Serializable]
+public sealed class CargoBountyConsoleState : BoundUserInterfaceState
+{
+ public List Bounties;
+
+ public CargoBountyConsoleState(List bounties)
+ {
+ Bounties = bounties;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed class BountyPrintLabelMessage : BoundUserInterfaceMessage
+{
+ public int BountyId;
+
+ public BountyPrintLabelMessage(int bountyId)
+ {
+ BountyId = bountyId;
+ }
+}
diff --git a/Content.Shared/Cargo/Prototypes/CargoBountyPrototype.cs b/Content.Shared/Cargo/Prototypes/CargoBountyPrototype.cs
new file mode 100644
index 0000000000..4f048004dd
--- /dev/null
+++ b/Content.Shared/Cargo/Prototypes/CargoBountyPrototype.cs
@@ -0,0 +1,60 @@
+using Content.Shared.Whitelist;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Cargo.Prototypes;
+
+///
+/// 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.
+///
+[Prototype("cargoBounty"), Serializable, NetSerializable]
+public sealed class CargoBountyPrototype : IPrototype
+{
+ ///
+ [IdDataField]
+ public string ID { get; } = default!;
+
+ ///
+ /// The monetary reward for completing the bounty
+ ///
+ [DataField("reward", required: true)]
+ public readonly int Reward;
+
+ ///
+ /// A description for flava purposes.
+ ///
+ [DataField("description")]
+ public readonly string Description = string.Empty;
+
+ ///
+ /// The entries that must be satisfied for the cargo bounty to be complete.
+ ///
+ [DataField("entries", required: true)]
+ public readonly List Entries = new();
+}
+
+[DataDefinition, Serializable, NetSerializable]
+public readonly record struct CargoBountyItemEntry()
+{
+ ///
+ /// A whitelist for determining what items satisfy the entry.
+ ///
+ [DataField("whitelist", required: true)]
+ public readonly EntityWhitelist Whitelist = default!;
+
+ // todo: implement some kind of simple generic condition system
+
+ ///
+ /// How much of the item must be present to satisfy the entry
+ ///
+ [DataField("amount")]
+ public readonly int Amount = 1;
+
+ ///
+ /// A player-facing name for the item.
+ ///
+ [DataField("name")]
+ public readonly string Name = string.Empty;
+}
diff --git a/Content.Shared/Cargo/SharedCargoSystem.cs b/Content.Shared/Cargo/SharedCargoSystem.cs
index 98ea3bc00c..5351932cdf 100644
--- a/Content.Shared/Cargo/SharedCargoSystem.cs
+++ b/Content.Shared/Cargo/SharedCargoSystem.cs
@@ -6,6 +6,7 @@ namespace Content.Shared.Cargo;
public enum CargoConsoleUiKey : byte
{
Orders,
+ Bounty,
Shuttle,
Telepad
}
diff --git a/Resources/Locale/en-US/cargo/bounties.ftl b/Resources/Locale/en-US/cargo/bounties.ftl
new file mode 100644
index 0000000000..158e08c446
--- /dev/null
+++ b/Resources/Locale/en-US/cargo/bounties.ftl
@@ -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.
diff --git a/Resources/Locale/en-US/cargo/cargo-bounty-console.ftl b/Resources/Locale/en-US/cargo/cargo-bounty-console.ftl
new file mode 100644
index 0000000000..54ff46f2b6
--- /dev/null
+++ b/Resources/Locale/en-US/cargo/cargo-bounty-console.ftl
@@ -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:
diff --git a/Resources/Locale/en-US/guidebook/guides.ftl b/Resources/Locale/en-US/guidebook/guides.ftl
index c81a08693d..6b713c0ce1 100644
--- a/Resources/Locale/en-US/guidebook/guides.ftl
+++ b/Resources/Locale/en-US/guidebook/guides.ftl
@@ -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
diff --git a/Resources/Prototypes/Catalog/Bounties/bounties.yml b/Resources/Prototypes/Catalog/Bounties/bounties.yml
new file mode 100644
index 0000000000..3c1f52e886
--- /dev/null
+++ b/Resources/Prototypes/Catalog/Bounties/bounties.yml
@@ -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
diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
index d80fad39e8..401459bb30 100644
--- a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
+++ b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
@@ -20,6 +20,7 @@
- id: CargoShuttleComputerCircuitboard
- id: CargoShuttleConsoleCircuitboard
- id: SalvageShuttleConsoleCircuitboard
+ - id: CargoBountyComputerCircuitboard
- id: CigPackGreen
prob: 0.50
- id: DoorRemoteCargo
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
index 2cebd34b24..b4c01f2bf4 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
@@ -908,6 +908,7 @@
tags:
- Trash
- CannotSuicide
+ - Mouse
- type: Respirator
damage:
types:
diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
index 0a5f99f134..b3adc66ead 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
@@ -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
diff --git a/Resources/Prototypes/Entities/Objects/Fun/toys.yml b/Resources/Prototypes/Entities/Objects/Fun/toys.yml
index 96fd720fe6..2f1321bae8 100644
--- a/Resources/Prototypes/Entities/Objects/Fun/toys.yml
+++ b/Resources/Prototypes/Entities/Objects/Fun/toys.yml
@@ -475,6 +475,9 @@
Plastic: 100
- type: StaticPrice
price: 10
+ - type: Tag
+ tags:
+ - Figurine
- type: entity
parent: BaseFigurine
diff --git a/Resources/Prototypes/Entities/Objects/Misc/briefcases.yml b/Resources/Prototypes/Entities/Objects/Misc/briefcases.yml
index 9aa03af147..66b049f89b 100644
--- a/Resources/Prototypes/Entities/Objects/Misc/briefcases.yml
+++ b/Resources/Prototypes/Entities/Objects/Misc/briefcases.yml
@@ -8,6 +8,9 @@
size: 60
- type: Storage
capacity: 60
+ - type: Tag
+ tags:
+ - Briefcase
- type: entity
name: brown briefcase
diff --git a/Resources/Prototypes/Entities/Objects/Misc/paper.yml b/Resources/Prototypes/Entities/Objects/Misc/paper.yml
index bac3ccd2f0..561645cffd 100644
--- a/Resources/Prototypes/Entities/Objects/Misc/paper.yml
+++ b/Resources/Prototypes/Entities/Objects/Misc/paper.yml
@@ -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
diff --git a/Resources/Prototypes/Entities/Objects/Tools/toolbox.yml b/Resources/Prototypes/Entities/Objects/Tools/toolbox.yml
index 884afd19d3..037c86e843 100644
--- a/Resources/Prototypes/Entities/Objects/Tools/toolbox.yml
+++ b/Resources/Prototypes/Entities/Objects/Tools/toolbox.yml
@@ -20,7 +20,8 @@
path: "/Audio/Weapons/smash.ogg"
- type: Tag
tags:
- - DroneUsable
+ - DroneUsable
+ - Toolbox
- type: entity
name: emergency toolbox
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/baseball_bat.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/baseball_bat.yml
index 78df0f9989..c8134180bc 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/baseball_bat.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/baseball_bat.yml
@@ -25,3 +25,6 @@
- type: Construction
graph: WoodenBat
node: bat
+ - type: Tag
+ tags:
+ - BaseballBat
diff --git a/Resources/Prototypes/Entities/Stations/base.yml b/Resources/Prototypes/Entities/Stations/base.yml
index add77dc0db..b758b4e395 100644
--- a/Resources/Prototypes/Entities/Stations/base.yml
+++ b/Resources/Prototypes/Entities/Stations/base.yml
@@ -10,6 +10,7 @@
components:
- type: StationBankAccount
- type: StationCargoOrderDatabase
+ - type: StationCargoBountyDatabase
- type: entity
id: BaseStationJobsSpawning
diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
index 3720cf8706..6ddc7b5a3e 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
@@ -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
diff --git a/Resources/Prototypes/Guidebook/cargo.yml b/Resources/Prototypes/Guidebook/cargo.yml
index 02893e2148..b2a8012e05 100644
--- a/Resources/Prototypes/Guidebook/cargo.yml
+++ b/Resources/Prototypes/Guidebook/cargo.yml
@@ -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"
diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml
index bf31967aec..311a21852f 100644
--- a/Resources/Prototypes/tags.yml
+++ b/Resources/Prototypes/tags.yml
@@ -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
diff --git a/Resources/ServerInfo/Guidebook/Cargo.xml b/Resources/ServerInfo/Guidebook/Cargo/Cargo.xml
similarity index 92%
rename from Resources/ServerInfo/Guidebook/Cargo.xml
rename to Resources/ServerInfo/Guidebook/Cargo/Cargo.xml
index 182463a550..587fcee9c9 100644
--- a/Resources/ServerInfo/Guidebook/Cargo.xml
+++ b/Resources/ServerInfo/Guidebook/Cargo/Cargo.xml
@@ -26,6 +26,6 @@
- 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"].
diff --git a/Resources/ServerInfo/Guidebook/Cargo/CargoBounties.xml b/Resources/ServerInfo/Guidebook/Cargo/CargoBounties.xml
new file mode 100644
index 0000000000..d6a45abcfa
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/Cargo/CargoBounties.xml
@@ -0,0 +1,29 @@
+
+ # 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].
+
+
+
+
+ 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
+
+
+
+
+
+ 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.
+
diff --git a/Resources/Textures/Structures/Machines/computers.rsi/bounty.png b/Resources/Textures/Structures/Machines/computers.rsi/bounty.png
new file mode 100644
index 0000000000..c9fccc6f6a
Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/bounty.png differ
diff --git a/Resources/Textures/Structures/Machines/computers.rsi/meta.json b/Resources/Textures/Structures/Machines/computers.rsi/meta.json
index 2fb8dcee28..b663858ccd 100644
--- a/Resources/Textures/Structures/Machines/computers.rsi/meta.json
+++ b/Resources/Textures/Structures/Machines/computers.rsi/meta.json
@@ -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