From 7ab5127286726971239a78a4d60346e0958d4327 Mon Sep 17 00:00:00 2001 From: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> Date: Thu, 22 Jun 2023 07:49:33 -0400 Subject: [PATCH] Cargo Bounties (#17344) Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> --- .../CargoBountyConsoleBoundUserInterface.cs | 54 +++ Content.Client/Cargo/UI/BountyEntry.xaml | 27 ++ Content.Client/Cargo/UI/BountyEntry.xaml.cs | 54 +++ Content.Client/Cargo/UI/CargoBountyMenu.xaml | 36 ++ .../Cargo/UI/CargoBountyMenu.xaml.cs | 34 ++ Content.IntegrationTests/Tests/CargoTest.cs | 40 ++ .../Components/CargoBountyLabelComponent.cs | 20 + .../StationCargoBountyDatabaseComponent.cs | 47 +++ .../Cargo/Systems/CargoSystem.Bounty.cs | 341 ++++++++++++++++++ .../Cargo/Systems/CargoSystem.Shuttle.cs | 21 +- Content.Server/Cargo/Systems/CargoSystem.cs | 4 + Content.Shared/Cargo/CargoBountyData.cs | 31 ++ .../Components/CargoBountyConsoleComponent.cs | 57 +++ .../Cargo/Prototypes/CargoBountyPrototype.cs | 60 +++ Content.Shared/Cargo/SharedCargoSystem.cs | 1 + Resources/Locale/en-US/cargo/bounties.ftl | 41 +++ .../en-US/cargo/cargo-bounty-console.ftl | 18 + Resources/Locale/en-US/guidebook/guides.ftl | 1 + .../Prototypes/Catalog/Bounties/bounties.yml | 219 +++++++++++ .../Catalog/Fills/Lockers/heads.yml | 1 + .../Prototypes/Entities/Mobs/NPCs/animals.yml | 1 + .../Devices/Circuitboards/computer.yml | 15 + .../Prototypes/Entities/Objects/Fun/toys.yml | 3 + .../Entities/Objects/Misc/briefcases.yml | 3 + .../Entities/Objects/Misc/paper.yml | 13 + .../Entities/Objects/Tools/toolbox.yml | 3 +- .../Objects/Weapons/Melee/baseball_bat.yml | 3 + .../Prototypes/Entities/Stations/base.yml | 1 + .../Machines/Computers/computers.yml | 33 ++ Resources/Prototypes/Guidebook/cargo.yml | 9 +- Resources/Prototypes/tags.yml | 15 + .../Guidebook/{ => Cargo}/Cargo.xml | 2 +- .../Guidebook/Cargo/CargoBounties.xml | 29 ++ .../Machines/computers.rsi/bounty.png | Bin 0 -> 2362 bytes .../Machines/computers.rsi/meta.json | 38 ++ 35 files changed, 1269 insertions(+), 6 deletions(-) create mode 100644 Content.Client/Cargo/BUI/CargoBountyConsoleBoundUserInterface.cs create mode 100644 Content.Client/Cargo/UI/BountyEntry.xaml create mode 100644 Content.Client/Cargo/UI/BountyEntry.xaml.cs create mode 100644 Content.Client/Cargo/UI/CargoBountyMenu.xaml create mode 100644 Content.Client/Cargo/UI/CargoBountyMenu.xaml.cs create mode 100644 Content.Server/Cargo/Components/CargoBountyLabelComponent.cs create mode 100644 Content.Server/Cargo/Components/StationCargoBountyDatabaseComponent.cs create mode 100644 Content.Server/Cargo/Systems/CargoSystem.Bounty.cs create mode 100644 Content.Shared/Cargo/CargoBountyData.cs create mode 100644 Content.Shared/Cargo/Components/CargoBountyConsoleComponent.cs create mode 100644 Content.Shared/Cargo/Prototypes/CargoBountyPrototype.cs create mode 100644 Resources/Locale/en-US/cargo/bounties.ftl create mode 100644 Resources/Locale/en-US/cargo/cargo-bounty-console.ftl create mode 100644 Resources/Prototypes/Catalog/Bounties/bounties.yml rename Resources/ServerInfo/Guidebook/{ => Cargo}/Cargo.xml (92%) create mode 100644 Resources/ServerInfo/Guidebook/Cargo/CargoBounties.xml create mode 100644 Resources/Textures/Structures/Machines/computers.rsi/bounty.png 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 0000000000000000000000000000000000000000..c9fccc6f6ac578287effadb538e8f54117227b9e GIT binary patch literal 2362 zcmai0c{J4R9=BvNA|*+-Q3#KWr+;ea5y|;7!c%JWhKA-P-&U3!s@A-V5$96VmLIM&3TwGj2 z7Ut(2II(bd@q;*f5*eY!3ELi}f zDPp4jkqbs00R?KI!QK4!#g~qyJP-hXd~~){TkCqVPiE&(aPbjOWsB;Fm?NNb?OozR zgfub9oBaM@p$p08vCh0EiLIa*+3Wm8%V8Mh(Z(?nNf)<5rEdQr`f?zeOf`s{Md%0N z_Z>4)5PRe3PgzzhgPZqSz+r-dhQZ9uPU7IJ+#q6PU}{7ALO>M}dc3m^_|AX1hYn@g z1z=Kqklh~N_j(kgpkZi` zqShGOz1rkxI(qo~#D+}zxxJfM{ALKs{(*TjQzMOg9ULOtVw(w9XYDk!1es6Jp3*`m z&JNcuNYl4mJXQ!8(vnYHV%hOofo!N>4luvI`J|?eK^t%|3@sb}zEn-_tt1C#1fjMS zaMTDDa1ROMXbi3DZ~aP}b_%BTN3;a@?boG~JN>v`9i&uw`i&pe5|OffX@a}z3RFuE z6DehF_}m0?*VPxD1%qCi60>AX6Kn-Gte2n?=R(X02|T`H3Y+K^%rUy-OlhQMN{VRr z*2fFc<`xG080~q0=T6403UD(Yth!P{3%pl%wXv~9*JrET7O(k-Hd?oC8gNSzdG6#=j-L4f`jx%w;98x|HGma%Bk zrY5i4;12a|iIo05*Nvd%jKwEH4w^HFrz1LE8-YkK8(Xa3yAx>^-mgiVZW`uLaF{v} zrqz9=T)*_rLjEV#aCUvuVkR{U?&hprmzoYFVF#ZqA(VKAQ-bpD|d=1D6%x}j2 z=BnNqOattt{^#@w@X6c#VlaP2J-!B)t&pnP=I)s;F()xw#?XT z2(q;_foSr^RS2GHbt?4XA8gH^f5ea2}MN)5Q0uGrnu91d8@xSM?`BREov_L(C;GP=aJ$r5?5fJ`s2A; zt75~Ax8K$-w(4DAZ7afn45rd4{Yu96s01=>>8;CW=ZFqx(Lsh6Y%(}Q)DGdjQ-Iwv zY~DaIdzqhR%p8mzbpIkqzv??jtA3`$544Ag-Cs0urzv|IzZl=AKeSP1EdR*Lh_695 za==E-I#y@e(bx(^f|9AJ9WhJ$`wRL5+HZ1KJGR{SM> zB@i-nm?ph+z2XSxCP*kq)d}u?iJnvhZOW^7({NAFkD{h*9!Tl*wz#j%`1&lzOI2@N zoez?9>7+uhyp2gNGZv37oqw1VjBF_!LqBz~B4ot!V4guv5rBAphvuUfkO7Ba6PSNt z%@HPt2)zy(fZq18R9=s2)Yrsuk+mLEjR`2@==@^99gzs&xkqgh(_YeH^kg%nMm$9B zLFn*v5!(G@BixLHvzB%wK(znhyysHzhw{Hx^goQ@=VJe$6l&4njVYmLC_aLm{ks}d zZ*J{ch%j~~qg_F^&CRBV)HK4^w2Rb8df^KXADzJlMGNY5Aq~tsmN!7838<3$j2k#S zj}@LCHMS)zkI132iQ<+ieNighi$iI9`v$G6RVrGSZk?Sx39m&jQSouJD%fhg_j3&B z2;hw9&gURAE90-MoNq<+Bwsy;%Veq!IdyzF6|S(oS_mi#S3eX}Z+Ra^v*53X)Hu+! z?#4{^Gd-l^f}f2pxAi?n&m-F`iJOjuqbEkd^E^l~Yo4!-!NWeGVp8!O=EG+H6m;4( zkj}eLJZtw%=37-xCn1Ngz*?g!J#JrOte(`GkmNl+_(YhE7Z~L{xp#0(hUQZ1KBKP- zw!lGZrh$bJgKv|0`F!QWX)g6looW?m&w+nB?SB>Y-zNMMrO2^ks(>}FQvCph2NMaG zrq{WG#UCQ|W_{~^pHDfr3+xe5w_$}4@gm`%WUC(;(!B4A3Q_@Ao_*rkKT@&WQ~kF1 zDAgFgWD`zCUqKg-KRj2t@&z#yn*Me{5ytt%!Ig9Q!}QO@n+R`?}Kk!UQ>= PAubD3oAWPBykq|aj=pl} literal 0 HcmV?d00001 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
_foSr^RS2GHbt?4XA8gH^f5ea2}MN)5Q0uGrnu91d8@xSM?`BREov_L(C;GP=aJ$r5?5fJ`s2A; zt75~Ax8K$-w(4DAZ7afn45rd4{Yu96s01=>>8;CW=ZFqx(Lsh6Y%(}Q)DGdjQ-Iwv zY~DaIdzqhR%p8mzbpIkqzv??jtA3`$544Ag-Cs0urzv|IzZl=AKeSP1EdR*Lh_695 za==E-I#y@e(bx(^f|9AJ9WhJ$`wRL5+HZ1KJGR{SM> zB@i-nm?ph+z2XSxCP*kq)d}u?iJnvhZOW^7({NAFkD{h*9!Tl*wz#j%`1&lzOI2@N zoez?9>7+uhyp2gNGZv37oqw1VjBF_!LqBz~B4ot!V4guv5rBAphvuUfkO7Ba6PSNt z%@HPt2)zy(fZq18R9=s2)Yrsuk+mLEjR`2@==@^99gzs&xkqgh(_YeH^kg%nMm$9B zLFn*v5!(G@BixLHvzB%wK(znhyysHzhw{Hx^goQ@=VJe$6l&4njVYmLC_aLm{ks}d zZ*J{ch%j~~qg_F^&CRBV)HK4^w2Rb8df^KXADzJlMGNY5Aq~tsmN!7838<3$j2k#S zj}@LCHMS)zkI132iQ<+ieNighi$iI9`v$G6RVrGSZk?Sx39m&jQSouJD%fhg_j3&B z2;hw9&gURAE90-MoNq<+Bwsy;%Veq!IdyzF6|S(oS_mi#S3eX}Z+Ra^v*53X)Hu+! z?#4{^Gd-l^f}f2pxAi?n&m-F`iJOjuqbEkd^E^l~Yo4!-!NWeGVp8!O=EG+H6m;4( zkj}eLJZtw%=37-xCn1Ngz*?g!J#JrOte(`GkmNl+_(YhE7Z~L{xp#0(hUQZ1KBKP- zw!lGZrh$bJgKv|0`F!QWX)g6looW?m&w+nB?SB>Y-zNMMrO2^ks(>}FQvCph2NMaG zrq{WG#UCQ|W_{~^pHDfr3+xe5w_$}4@gm`%WUC(;(!B4A3Q_@Ao_*rkKT@&WQ~kF1 zDAgFgWD`zCUqKg-KRj2t@&z#yn*Me{5ytt%!Ig9Q!}QO@n+R`?}Kk!UQ>= PAubD3oAWPBykq|aj=pl} literal 0 HcmV?d00001 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